diff --git a/rust/crates/rusty-claude-cli/src/app.rs b/rust/crates/rusty-claude-cli/src/app.rs index ff535a8f8d..4e23489e87 100644 --- a/rust/crates/rusty-claude-cli/src/app.rs +++ b/rust/crates/rusty-claude-cli/src/app.rs @@ -54,7 +54,7 @@ use crate::tui::permission::{ use crate::tui::status_bar::{StatusBar, StatusBarState}; use crate::tui::terminal::TerminalSize; use crate::tui::theme::Theme; -use crate::tui::timeline::ToolCallTimeline; +use crate::tui::timeline::{SharedToolCallTimeline, ToolCallTimeline}; use crate::{ AllowedToolSet, RuntimePluginStateBuildOutput, DEFAULT_DATE, INTERNAL_PROGRESS_HEARTBEAT_INTERVAL, POST_TOOL_STALL_TIMEOUT, @@ -1784,6 +1784,7 @@ pub(crate) fn build_runtime_with_plugin_state( emit_output, tool_registry.clone(), mcp_state.clone(), + None, ), policy, system_prompt, @@ -2328,6 +2329,7 @@ pub(crate) struct CliToolExecutor { allowed_tools: Option, tool_registry: GlobalToolRegistry, mcp_state: Option>>, + tool_timeline: Option, } impl CliToolExecutor { @@ -2336,6 +2338,7 @@ impl CliToolExecutor { emit_output: bool, tool_registry: GlobalToolRegistry, mcp_state: Option>>, + tool_timeline: Option, ) -> Self { Self { renderer: TerminalRenderer::new(), @@ -2343,9 +2346,15 @@ impl CliToolExecutor { allowed_tools, tool_registry, mcp_state, + tool_timeline, } } + /// Attach a shared timeline so tool execution duration is recorded. + pub(crate) fn set_timeline(&mut self, timeline: SharedToolCallTimeline) { + self.tool_timeline = Some(timeline); + } + fn execute_search_tool(&self, value: serde_json::Value) -> Result { let input: ToolSearchRequest = serde_json::from_value(value) .map_err(|error| ToolError::new(format!("invalid tool input JSON: {error}")))?; @@ -2431,6 +2440,10 @@ impl ToolExecutor for CliToolExecutor { }; match result { Ok(output) => { + if let Some(ref timeline) = self.tool_timeline { + let lines = output.lines().count(); + timeline.with(|t| t.complete_tool(false, lines > 100, lines)); + } if self.emit_output { let markdown = format_tool_result(tool_name, &output, false); self.renderer @@ -2440,6 +2453,9 @@ impl ToolExecutor for CliToolExecutor { Ok(output) } Err(error) => { + if let Some(ref timeline) = self.tool_timeline { + timeline.with(|t| t.complete_tool(true, false, 0)); + } if self.emit_output { let markdown = format_tool_result(tool_name, &error.to_string(), true); self.renderer diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index 3153a025a2..96545c081b 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -6032,6 +6032,7 @@ UU conflicted.rs", false, state.tool_registry.clone(), state.mcp_state.clone(), + None, ); let tool_output = executor @@ -6130,6 +6131,7 @@ UU conflicted.rs", false, state.tool_registry.clone(), state.mcp_state.clone(), + None, ); let search_output = executor diff --git a/rust/crates/rusty-claude-cli/src/tui/mod.rs b/rust/crates/rusty-claude-cli/src/tui/mod.rs index cf50fe1b9c..830e80b392 100644 --- a/rust/crates/rusty-claude-cli/src/tui/mod.rs +++ b/rust/crates/rusty-claude-cli/src/tui/mod.rs @@ -19,5 +19,5 @@ pub use status_bar::StatusBar; pub use terminal::TerminalSize; pub use theme::Theme; pub use thinking::{format_thinking_completed, render_thinking_inline, ThinkingFrames}; -pub use timeline::ToolCallTimeline; +pub use timeline::{SharedToolCallTimeline, ToolCallTimeline}; pub use tool_panel::{collapse_tool_output, CollapsedToolOutput, ToolDisplayConfig}; diff --git a/rust/crates/rusty-claude-cli/src/tui/timeline.rs b/rust/crates/rusty-claude-cli/src/tui/timeline.rs index 24cb802836..8d3979c8ce 100644 --- a/rust/crates/rusty-claude-cli/src/tui/timeline.rs +++ b/rust/crates/rusty-claude-cli/src/tui/timeline.rs @@ -1,4 +1,5 @@ use std::fmt::Write as _; +use std::sync::{Arc, Mutex}; use std::time::Instant; use crate::tui::theme::Theme; @@ -15,6 +16,14 @@ pub struct ToolCallEvent { pub output_lines: usize, } +/// Accumulator for building a tool call timeline during a turn. +/// +/// Wrapped in `Arc>` so it can be shared between the streaming +/// client (which records `start_tool`) and the tool executor (which +/// records `complete_tool`). +#[derive(Debug, Default, Clone)] +pub struct SharedToolCallTimeline(pub Arc>); + /// Accumulator for building a tool call timeline during a turn. #[derive(Debug, Default)] pub struct ToolCallTimeline { @@ -22,6 +31,20 @@ pub struct ToolCallTimeline { start: Option, } +impl SharedToolCallTimeline { + /// Lock the inner timeline and call a function on it. + pub fn with(&self, f: F) -> R + where + F: FnOnce(&mut ToolCallTimeline) -> R, + { + let mut guard = self + .0 + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + f(&mut guard) + } +} + impl ToolCallTimeline { /// Create a new empty timeline. pub fn new() -> Self {