Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion rust/crates/rusty-claude-cli/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -1784,6 +1784,7 @@ pub(crate) fn build_runtime_with_plugin_state(
emit_output,
tool_registry.clone(),
mcp_state.clone(),
None,
),
policy,
system_prompt,
Expand Down Expand Up @@ -2328,6 +2329,7 @@ pub(crate) struct CliToolExecutor {
allowed_tools: Option<AllowedToolSet>,
tool_registry: GlobalToolRegistry,
mcp_state: Option<Arc<Mutex<RuntimeMcpState>>>,
tool_timeline: Option<SharedToolCallTimeline>,
}

impl CliToolExecutor {
Expand All @@ -2336,16 +2338,23 @@ impl CliToolExecutor {
emit_output: bool,
tool_registry: GlobalToolRegistry,
mcp_state: Option<Arc<Mutex<RuntimeMcpState>>>,
tool_timeline: Option<SharedToolCallTimeline>,
) -> Self {
Self {
renderer: TerminalRenderer::new(),
emit_output,
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<String, ToolError> {
let input: ToolSearchRequest = serde_json::from_value(value)
.map_err(|error| ToolError::new(format!("invalid tool input JSON: {error}")))?;
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions rust/crates/rusty-claude-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6032,6 +6032,7 @@ UU conflicted.rs",
false,
state.tool_registry.clone(),
state.mcp_state.clone(),
None,
);

let tool_output = executor
Expand Down Expand Up @@ -6130,6 +6131,7 @@ UU conflicted.rs",
false,
state.tool_registry.clone(),
state.mcp_state.clone(),
None,
);

let search_output = executor
Expand Down
2 changes: 1 addition & 1 deletion rust/crates/rusty-claude-cli/src/tui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
23 changes: 23 additions & 0 deletions rust/crates/rusty-claude-cli/src/tui/timeline.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::fmt::Write as _;
use std::sync::{Arc, Mutex};
use std::time::Instant;

use crate::tui::theme::Theme;
Expand All @@ -15,13 +16,35 @@ pub struct ToolCallEvent {
pub output_lines: usize,
}

/// Accumulator for building a tool call timeline during a turn.
///
/// Wrapped in `Arc<Mutex<>>` 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<Mutex<ToolCallTimeline>>);

/// Accumulator for building a tool call timeline during a turn.
#[derive(Debug, Default)]
pub struct ToolCallTimeline {
events: Vec<ToolCallEvent>,
start: Option<Instant>,
}

impl SharedToolCallTimeline {
/// Lock the inner timeline and call a function on it.
pub fn with<F, R>(&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 {
Expand Down