diff options
| author | Michelle Tilley <michelle@michelletilley.net> | 2026-04-23 20:31:53 -0700 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-04-23 20:31:53 -0700 |
| commit | f72fdf7565d18b044f035fa6aca9ae8dbba34fc6 (patch) | |
| tree | d0c08204ed712f0f788cd262f56894b8a2af7d49 /crates/atuin-ai/src/commands/inline.rs | |
| parent | feat: Add skill discovery, loading, and invocation (#3444) (diff) | |
| download | atuin-f72fdf7565d18b044f035fa6aca9ae8dbba34fc6.zip | |
perf: Reduce AI TUI rendering overhead for long conversations (#3447)
Fixes keystroke lag in Atuin AI that scales with conversation length.
After extended use (many turns, lots of tool calls with output
viewports), pressing a key had noticeable delay before the letter
appeared.
Three layers of optimization:
- **Skip `sync_view_state` for `InputUpdated`** — every keystroke was
cloning all events, tools, and archived data even though no FSM state
changed. Uses `handle.update_tracked()` (eye_declare 0.5) to skip
rebuilds when values haven't actually changed.
- **Pre-compute turns and `has_command` on the driver thread** — the
view function was rebuilding the full turn structure from raw events and
scanning for `suggest_command` tool calls 3× per render frame. Now
computed once per FSM state change and cached in ViewState.
- **Commit-based element tree pruning** — turns that scroll into
terminal scrollback are tracked via `on_commit` and filtered from the
element tree, keeping rendering work proportional to visible content.
Turn views are now direct children of the root VStack (not nested inside
AtuinAi) so `detect_committed` can see them.
Diffstat (limited to 'crates/atuin-ai/src/commands/inline.rs')
| -rw-r--r-- | crates/atuin-ai/src/commands/inline.rs | 48 |
1 files changed, 45 insertions, 3 deletions
diff --git a/crates/atuin-ai/src/commands/inline.rs b/crates/atuin-ai/src/commands/inline.rs index 70f26c65..989b95c0 100644 --- a/crates/atuin-ai/src/commands/inline.rs +++ b/crates/atuin-ai/src/commands/inline.rs @@ -292,6 +292,17 @@ async fn run_inline_tui( .bracketed_paste(true) .with_context(tui_tx) .extra_newlines_at_exit(1) + .on_commit(|committed, state| { + if let Some(key) = &committed.key + && let Some(id_str) = key.strip_prefix("turn-") + && let Ok(id) = id_str.parse::<usize>() + { + let new_count = id + 1; + if new_count > state.committed_turn_count { + state.committed_turn_count = new_count; + } + } + }) .build()?; // ─── Driver loop ──────────────────────────────────────────── @@ -349,17 +360,48 @@ fn build_view_state( skill_names.insert(skill.name.clone()); } + let tools = fsm.ctx.tools.clone(); + let visible_events = fsm.ctx.events[safe_start..].to_vec(); + let archived_events = fsm.ctx.archived_events.clone(); + + let mut archived_builder = crate::tui::view::turn::TurnBuilder::new(&tools); + for event in &archived_events { + archived_builder.add_event(event); + } + let archived_turns = archived_builder.build(); + let archived_turn_count = archived_turns.len(); + + let mut visible_builder = + crate::tui::view::turn::TurnBuilder::new_starting_at(&tools, archived_turn_count); + for event in &visible_events { + visible_builder.add_event(event); + } + let visible_turns = visible_builder.build(); + + let mut turns = archived_turns; + turns.extend(visible_turns); + + let has_command = visible_events.iter().any(|e| { + matches!(e, ConversationEvent::ToolCall { name, input, .. } + if name == "suggest_command" + && input.get("command").and_then(|v| v.as_str()).is_some()) + }); + ViewState { agent_state: fsm.state.clone(), - visible_events: fsm.ctx.events[safe_start..].to_vec(), + visible_events, all_events: fsm.ctx.events.clone(), session_id: fsm.ctx.session_id.clone(), - tools: fsm.ctx.tools.clone(), + tools, current_response: fsm.ctx.current_response.clone(), is_resumed: fsm.ctx.is_resumed, last_event_time: fsm.ctx.last_event_time, in_git_project, - archived_events: fsm.ctx.archived_events.clone(), + archived_events, + turns, + has_command, + committed_turn_count: 0, + archived_turn_count, is_input_blank: true, slash_command_input: None, slash_command_search_results: Vec::new(), |
