From f72fdf7565d18b044f035fa6aca9ae8dbba34fc6 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 23 Apr 2026 20:31:53 -0700 Subject: perf: Reduce AI TUI rendering overhead for long conversations (#3447) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- crates/atuin-ai/src/commands/inline.rs | 48 +++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 3 deletions(-) (limited to 'crates/atuin-ai/src/commands/inline.rs') 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::() + { + 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(), -- cgit v1.3.1