aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMichelle Tilley <michelle@michelletilley.net>2026-04-23 20:31:53 -0700
committerGitHub <noreply@github.com>2026-04-23 20:31:53 -0700
commitf72fdf7565d18b044f035fa6aca9ae8dbba34fc6 (patch)
treed0c08204ed712f0f788cd262f56894b8a2af7d49
parentfeat: Add skill discovery, loading, and invocation (#3444) (diff)
downloadatuin-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 '')
-rw-r--r--Cargo.lock8
-rw-r--r--crates/atuin-ai/Cargo.toml2
-rw-r--r--crates/atuin-ai/src/commands/inline.rs48
-rw-r--r--crates/atuin-ai/src/driver.rs74
-rw-r--r--crates/atuin-ai/src/tui/view/mod.rs81
-rw-r--r--crates/atuin-ai/src/tui/view/turn.rs111
6 files changed, 214 insertions, 110 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 81fa5f8f..465f0cbd 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1551,9 +1551,9 @@ dependencies = [
[[package]]
name = "eye_declare"
-version = "0.4.3"
+version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fa99f9efa03c7fae32abd0b2e77d22dac5c2a137d1267c3496b44a711d139bd6"
+checksum = "960fe0e307c26cfaf176a1d0b5c355ea0a641596e1b57676fb4a67718969fe5a"
dependencies = [
"crossterm",
"eye_declare_macros",
@@ -1567,9 +1567,9 @@ dependencies = [
[[package]]
name = "eye_declare_macros"
-version = "0.4.3"
+version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a285ad61e123dad4c505e2d96818b0378d9facecdee074a02c525719a1db2f9b"
+checksum = "c973cc0ad5038f4d248fd949384b9610edacffec3959ec81926a34bf277bbb9f"
dependencies = [
"proc-macro2",
"quote",
diff --git a/crates/atuin-ai/Cargo.toml b/crates/atuin-ai/Cargo.toml
index 90e64b2a..377017b7 100644
--- a/crates/atuin-ai/Cargo.toml
+++ b/crates/atuin-ai/Cargo.toml
@@ -45,7 +45,7 @@ async-stream = "0.3"
uuid = { workspace = true }
tui-textarea-2 = "0.10.2"
unicode-width = "0.2"
-eye_declare = "0.4.3"
+eye_declare = "0.5"
ratatui-core = "0.1"
ratatui-widgets = "0.3"
thiserror = { workspace = true }
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(),
diff --git a/crates/atuin-ai/src/driver.rs b/crates/atuin-ai/src/driver.rs
index 1285f2da..ddb839b7 100644
--- a/crates/atuin-ai/src/driver.rs
+++ b/crates/atuin-ai/src/driver.rs
@@ -27,6 +27,7 @@ use crate::stream::ChatRequest;
use crate::tools::ClientToolCall;
use crate::tui::events::{AiTuiEvent, PermissionResult};
use crate::tui::state::ConversationEvent;
+use crate::tui::view::turn;
// ============================================================================
// Driver event — the unified channel type
@@ -82,6 +83,12 @@ pub(crate) struct ViewState {
// ─── View-only ──────────────────────────────────────────────
pub archived_events: Vec<ConversationEvent>,
+ // ─── Pre-computed for rendering ────────────────────────────
+ pub turns: Vec<turn::UiTurn>,
+ pub has_command: bool,
+ pub committed_turn_count: usize,
+ pub archived_turn_count: usize,
+
// ─── Ephemeral interaction state ────────────────────────────
pub is_input_blank: bool,
pub slash_command_input: Option<String>,
@@ -113,21 +120,10 @@ impl ViewState {
matches!(self.agent_state, AgentState::Idle { .. }) && !self.has_confirmation()
}
- /// Whether any command has been suggested in the current invocation.
- pub fn has_command(&self) -> bool {
- self.visible_events.iter().any(|e| {
- if let ConversationEvent::ToolCall { name, input, .. } = e {
- name == "suggest_command" && input.get("command").and_then(|v| v.as_str()).is_some()
- } else {
- false
- }
- })
- }
-
pub fn footer_text(&self) -> &'static str {
match &self.agent_state {
AgentState::Idle { confirmation: None } => {
- if self.has_command() && self.is_input_blank {
+ if self.has_command && self.is_input_blank {
"[Enter] Execute suggested command [Tab] Insert Command"
} else {
"[Enter] Send [Shift+Enter] New line [Esc] Exit"
@@ -220,10 +216,10 @@ pub(crate) fn run_driver(
if !effects.is_empty() {
sync_view_state(&handle, &fsm, in_git_project);
}
- } else {
- // Event was handled directly (e.g. InputUpdated) — just sync
- sync_view_state(&handle, &fsm, in_git_project);
}
+ // InputUpdated (the only event that returns None) already pushed
+ // its view-only changes via handle.update() — no FSM state changed,
+ // so skip the expensive sync_view_state that clones all events.
if exiting.load(Ordering::Acquire) {
break;
@@ -273,8 +269,14 @@ fn translate_tui_event(event: AiTuiEvent, handle: &Handle<ViewState>) -> Option<
}
AiTuiEvent::InputUpdated(text) => {
let is_blank = text.is_empty();
- handle.update(move |vs| {
- vs.is_input_blank = is_blank;
+
+ // Hot path (every keystroke); uses handle.update_tracked
+ // to allow read()ing the state without marking it dirty.
+ handle.update_tracked(move |vs| {
+ if vs.read().is_input_blank != is_blank {
+ vs.is_input_blank = is_blank;
+ }
+
if text.starts_with('/') {
let query = text.trim_start_matches('/').to_string();
let mut results = vs.slash_registry.search_fuzzy(&query);
@@ -286,8 +288,13 @@ fn translate_tui_event(event: AiTuiEvent, handle: &Handle<ViewState>) -> Option<
vs.slash_command_input = Some(query);
vs.slash_command_search_results = results;
} else {
- vs.slash_command_input = None;
- vs.slash_command_search_results.clear();
+ if vs.read().slash_command_input.is_some() {
+ vs.slash_command_input = None;
+ }
+
+ if !vs.read().slash_command_search_results.is_empty() {
+ vs.slash_command_search_results.clear();
+ }
}
});
None
@@ -407,6 +414,32 @@ fn sync_view_state(handle: &Handle<ViewState>, fsm: &AgentFsm, in_git_project: b
});
}
+ // Pre-compute turns and has_command on the driver thread so the
+ // render-thread view function doesn't redo O(n) work every frame.
+ let mut archived_builder = 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 = 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| {
+ if let ConversationEvent::ToolCall { name, input, .. } = e {
+ name == "suggest_command" && input.get("command").and_then(|v| v.as_str()).is_some()
+ } else {
+ false
+ }
+ });
+
tracing::trace!(?state, "sync_view_state pushing to handle");
handle.update(move |vs| {
vs.agent_state = state;
@@ -419,6 +452,9 @@ fn sync_view_state(handle: &Handle<ViewState>, fsm: &AgentFsm, in_git_project: b
vs.last_event_time = last_event_time;
vs.in_git_project = in_git_project;
vs.archived_events = archived_events;
+ vs.turns = turns;
+ vs.has_command = has_command;
+ vs.archived_turn_count = archived_turn_count;
});
}
diff --git a/crates/atuin-ai/src/tui/view/mod.rs b/crates/atuin-ai/src/tui/view/mod.rs
index 96ad5d85..73dc2ad7 100644
--- a/crates/atuin-ai/src/tui/view/mod.rs
+++ b/crates/atuin-ai/src/tui/view/mod.rs
@@ -18,7 +18,7 @@ use super::components::markdown::Markdown;
use super::components::select::Select;
use super::state::AppMode;
-mod turn;
+pub(crate) mod turn;
impl From<&AgentState> for AppMode {
fn from(state: &AgentState) -> Self {
@@ -42,50 +42,48 @@ impl From<&AgentState> for AppMode {
/// - Spacer
/// - Input box (bordered, with contextual keybindings)
pub(crate) fn ai_view(state: &ViewState) -> Elements {
- let mut turn_builder = turn::TurnBuilder::new(&state.tools);
-
- for event in &state.archived_events {
- turn_builder.add_event(event);
- }
- for event in &state.visible_events {
- turn_builder.add_event(event);
- }
- let turns = turn_builder.build();
-
+ let committed = state.committed_turn_count;
+ let turns: Vec<&turn::UiTurn> = state.turns.iter().filter(|t| t.id >= committed).collect();
let busy = state.is_busy();
let last_index = turns.len().saturating_sub(1);
+ // Turns are direct children of the root VStack so that eye_declare's
+ // on_commit can detect them scrolling into terminal scrollback and
+ // prune them from the tree. AtuinAi wraps only the interactive footer
+ // (input box, error display, pending banner) so its event capture/bubble
+ // handlers still fire for keyboard events.
element! {
+ #(if state.is_resumed && (!state.is_exiting() || !turns.is_empty()) {
+ SessionContinue(key: "continuation-notice", continued_at: state.last_event_time)
+ })
+
+ #(for (index, turn) in turns.iter().enumerate() {
+ #(match &turn.kind {
+ turn::UiTurnKind::User { events } => {
+ user_turn_view(events, index == 0, turn.id)
+ }
+ turn::UiTurnKind::Agent { events } => {
+ agent_turn_view(events, busy && index == last_index, state.tools.awaiting_permission().is_some(), turn.id)
+ }
+ turn::UiTurnKind::OutOfBand { events } => {
+ out_of_band_turn_view(events, turn.id)
+ }
+ })
+ })
+
AtuinAi(
+ key: "footer",
mode: AppMode::from(&state.agent_state),
- has_command: state.has_command(),
+ has_command: state.has_command,
is_input_blank: state.is_input_blank,
pending_confirmation: state.has_confirmation(),
has_executing_preview: state.tools.has_executing_preview(),
) {
- #(if state.is_resumed && (!state.is_exiting() || !turns.is_empty()) {
- SessionContinue(key: "continuation-notice", continued_at: state.last_event_time)
- })
-
- #(for (index, turn) in turns.iter().enumerate() {
- #(match turn {
- turn::UiTurn::User { events } => {
- user_turn_view(events, index == 0)
- }
- turn::UiTurn::Agent { events } => {
- agent_turn_view(events, busy && index == last_index, state.tools.awaiting_permission().is_some())
- }
- turn::UiTurn::OutOfBand { events } => {
- out_of_band_turn_view(events)
- }
- })
- })
-
#({
- let needs_pending_banner = busy && !matches!(turns.last(), Some(turn::UiTurn::Agent { .. }));
+ let needs_pending_banner = busy && !matches!(turns.last(), Some(turn::UiTurn { kind: turn::UiTurnKind::Agent { .. }, .. }));
if needs_pending_banner {
let empty: &[turn::UiEvent] = &[];
- agent_turn_view(empty, true, false)
+ agent_turn_view(empty, true, false, usize::MAX)
} else {
element! {}
}
@@ -133,7 +131,7 @@ fn input_view(state: &ViewState) -> Elements {
slash_suggestion: first_slash_result.cloned()
)
- #(if state.is_input_blank && state.has_command() && state.is_input_active() {
+ #(if state.is_input_blank && state.has_command && state.is_input_active() {
#(if state.has_confirmation() {
Text { Span(text: "[Enter] Confirm dangerous command [Esc] Cancel", style: Style::default().fg(Color::Gray)) }
} else {
@@ -244,7 +242,7 @@ fn permission_options_for_tool(tool: &ClientToolCall, in_git_project: bool) -> V
}
}
-fn user_turn_view(events: &[turn::UiEvent], first_turn: bool) -> Elements {
+fn user_turn_view(events: &[turn::UiEvent], first_turn: bool, turn_id: usize) -> Elements {
let label_style = Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD);
@@ -252,7 +250,7 @@ fn user_turn_view(events: &[turn::UiEvent], first_turn: bool) -> Elements {
let padding = if first_turn { 0 } else { 1 };
element! {
- View(padding_top: Cells::from(padding)) {
+ View(key: format!("turn-{turn_id}"), padding_top: Cells::from(padding)) {
Text {
Span(text: " You ", style: label_style.reversed())
}
@@ -274,13 +272,18 @@ fn user_turn_view(events: &[turn::UiEvent], first_turn: bool) -> Elements {
}
}
-fn agent_turn_view(events: &[turn::UiEvent], busy: bool, showing_ui: bool) -> Elements {
+fn agent_turn_view(
+ events: &[turn::UiEvent],
+ busy: bool,
+ showing_ui: bool,
+ turn_id: usize,
+) -> Elements {
let label_style = Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD);
element! {
- View {
+ View(key: format!("turn-{turn_id}")) {
Text {
Span(text: " Atuin AI ", style: label_style.reversed())
}
@@ -360,9 +363,9 @@ fn agent_turn_view(events: &[turn::UiEvent], busy: bool, showing_ui: bool) -> El
}
}
-fn out_of_band_turn_view(events: &[turn::UiEvent]) -> Elements {
+fn out_of_band_turn_view(events: &[turn::UiEvent], turn_id: usize) -> Elements {
element! {
- View {
+ View(key: format!("turn-{turn_id}")) {
Text {
Span(text: " System ", style: Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD).add_modifier(Modifier::REVERSED))
}
diff --git a/crates/atuin-ai/src/tui/view/turn.rs b/crates/atuin-ai/src/tui/view/turn.rs
index 9f4460eb..c74395b8 100644
--- a/crates/atuin-ai/src/tui/view/turn.rs
+++ b/crates/atuin-ai/src/tui/view/turn.rs
@@ -204,16 +204,23 @@ pub(crate) enum ToolResultStatus {
}
#[derive(Debug)]
-pub(crate) enum UiTurn {
+pub(crate) struct UiTurn {
+ pub(crate) id: usize,
+ pub(crate) kind: UiTurnKind,
+}
+
+#[derive(Debug)]
+pub(crate) enum UiTurnKind {
User { events: Vec<UiEvent> },
Agent { events: Vec<UiEvent> },
OutOfBand { events: Vec<UiEvent> },
}
pub(crate) struct TurnBuilder<'a> {
- turns: Vec<UiTurn>,
- current_turn: Option<UiTurn>,
+ turns: Vec<UiTurnKind>,
+ current_turn: Option<UiTurnKind>,
tracker: &'a ToolManager,
+ next_id: usize,
}
/// A struct to iteratively build [UiTurn] events from [ConversationEvent]s.
@@ -223,6 +230,16 @@ impl<'a> TurnBuilder<'a> {
turns: Vec::new(),
current_turn: None,
tracker,
+ next_id: 0,
+ }
+ }
+
+ pub(crate) fn new_starting_at(tracker: &'a ToolManager, start_id: usize) -> Self {
+ Self {
+ turns: Vec::new(),
+ current_turn: None,
+ tracker,
+ next_id: start_id,
}
}
@@ -280,7 +297,7 @@ impl<'a> TurnBuilder<'a> {
// into a ToolGroup (e.g. N file reads → one group)
// - All other events pass through unchanged
for turn in &mut self.turns {
- if let UiTurn::Agent { events } = turn {
+ if let UiTurnKind::Agent { events } = turn {
let mut new_events: Vec<UiEvent> = Vec::new();
let mut pending_remote: Vec<ToolCallDetails> = Vec::new();
let mut pending_group: Option<(ToolGroupKind, Vec<ToolCallDetails>)> = None;
@@ -322,7 +339,15 @@ impl<'a> TurnBuilder<'a> {
}
}
- std::mem::take(&mut self.turns)
+ let kinds = std::mem::take(&mut self.turns);
+ kinds
+ .into_iter()
+ .enumerate()
+ .map(|(i, kind)| UiTurn {
+ id: self.next_id + i,
+ kind,
+ })
+ .collect()
}
fn commit_turn(&mut self) {
@@ -332,37 +357,39 @@ impl<'a> TurnBuilder<'a> {
}
fn start_user_turn(&mut self) {
- if !matches!(self.current_turn, Some(UiTurn::User { .. })) {
+ if !matches!(self.current_turn, Some(UiTurnKind::User { .. })) {
self.commit_turn();
- self.current_turn = Some(UiTurn::User { events: vec![] });
+ self.current_turn = Some(UiTurnKind::User { events: vec![] });
}
}
fn start_agent_turn(&mut self) {
- if !matches!(self.current_turn, Some(UiTurn::Agent { .. })) {
+ if !matches!(self.current_turn, Some(UiTurnKind::Agent { .. })) {
self.commit_turn();
- self.current_turn = Some(UiTurn::Agent { events: vec![] });
+ self.current_turn = Some(UiTurnKind::Agent { events: vec![] });
}
}
fn start_out_of_band_turn(&mut self) {
- if !matches!(self.current_turn, Some(UiTurn::OutOfBand { .. })) {
+ if !matches!(self.current_turn, Some(UiTurnKind::OutOfBand { .. })) {
self.commit_turn();
- self.current_turn = Some(UiTurn::OutOfBand { events: vec![] });
+ self.current_turn = Some(UiTurnKind::OutOfBand { events: vec![] });
}
}
- fn turn_mut_unsafe(&mut self) -> &mut UiTurn {
- self.current_turn.as_mut().unwrap()
+ fn current_events_mut(&mut self) -> &mut Vec<UiEvent> {
+ match self.current_turn.as_mut().unwrap() {
+ UiTurnKind::User { events }
+ | UiTurnKind::Agent { events }
+ | UiTurnKind::OutOfBand { events } => events,
+ }
}
fn add_user_message(&mut self, content: &str) {
self.start_user_turn();
- if let UiTurn::User { events } = self.turn_mut_unsafe() {
- events.push(UiEvent::Text {
- content: content.to_string(),
- });
- }
+ self.current_events_mut().push(UiEvent::Text {
+ content: content.to_string(),
+ });
}
fn add_agent_text(&mut self, content: &str) {
@@ -370,11 +397,9 @@ impl<'a> TurnBuilder<'a> {
return;
}
self.start_agent_turn();
- if let UiTurn::Agent { events } = self.turn_mut_unsafe() {
- events.push(UiEvent::Text {
- content: content.to_string(),
- });
- }
+ self.current_events_mut().push(UiEvent::Text {
+ content: content.to_string(),
+ });
}
fn add_suggested_command(&mut self, input: &serde_json::Value) {
@@ -389,7 +414,8 @@ impl<'a> TurnBuilder<'a> {
}
self.start_agent_turn();
- if let UiTurn::Agent { events } = self.turn_mut_unsafe() {
+ {
+ let events = self.current_events_mut();
let danger_level = input
.get("danger")
.and_then(|v| v.as_str())
@@ -433,14 +459,13 @@ impl<'a> TurnBuilder<'a> {
let render_data = self.build_render_data(id, name);
self.start_agent_turn();
- if let UiTurn::Agent { events } = self.turn_mut_unsafe() {
- events.push(UiEvent::ToolCall(ToolCallDetails {
+ self.current_events_mut()
+ .push(UiEvent::ToolCall(ToolCallDetails {
tool_use_id: id.to_string(),
name: name.to_string(),
status: ToolResultStatus::Pending,
render_data,
}));
- }
}
/// Build tool-type-specific render data from the ToolTracker.
@@ -482,31 +507,29 @@ impl<'a> TurnBuilder<'a> {
fn add_tool_result(&mut self, tool_use_id: &str, _content: &str, is_error: bool) {
self.start_agent_turn();
- if let UiTurn::Agent { events } = self.turn_mut_unsafe() {
- let event = events.iter_mut().find(|e| match e {
- UiEvent::ToolCall(ToolCallDetails {
- tool_use_id: id, ..
- }) => id == tool_use_id,
- _ => false,
- });
- if let Some(UiEvent::ToolCall(ToolCallDetails { status, .. })) = event {
- *status = if is_error {
- ToolResultStatus::Error
- } else {
- ToolResultStatus::Success
- };
- }
+ let events = self.current_events_mut();
+ let event = events.iter_mut().find(|e| match e {
+ UiEvent::ToolCall(ToolCallDetails {
+ tool_use_id: id, ..
+ }) => id == tool_use_id,
+ _ => false,
+ });
+ if let Some(UiEvent::ToolCall(ToolCallDetails { status, .. })) = event {
+ *status = if is_error {
+ ToolResultStatus::Error
+ } else {
+ ToolResultStatus::Success
+ };
}
}
fn add_out_of_band_output(&mut self, _name: &str, command: Option<&str>, content: &str) {
self.start_out_of_band_turn();
- if let UiTurn::OutOfBand { events } = self.turn_mut_unsafe() {
- events.push(UiEvent::OutOfBandOutput(OutOfBandOutputDetails {
+ self.current_events_mut()
+ .push(UiEvent::OutOfBandOutput(OutOfBandOutputDetails {
command: command.map(|c| c.to_string()),
content: content.to_string(),
}));
- }
}
}