diff --git a/crates/chat-cli/src/cli/chat/mod.rs b/crates/chat-cli/src/cli/chat/mod.rs index 81043fdad..01551b5a6 100644 --- a/crates/chat-cli/src/cli/chat/mod.rs +++ b/crates/chat-cli/src/cli/chat/mod.rs @@ -677,6 +677,8 @@ pub struct ChatSession { prompt_ack_rx: std::sync::mpsc::Receiver<()>, /// Additional context to be added to the next user message (e.g., delegate task summaries) pending_additional_context: Option, + /// Tools used in the current turn (for non-verbose completion messages) + tools_used_in_turn: Vec, } impl ChatSession { @@ -814,6 +816,7 @@ impl ChatSession { wrap, prompt_ack_rx, pending_additional_context: None, + tools_used_in_turn: Vec::new(), }) } @@ -2443,6 +2446,14 @@ impl ChatSession { } } + // Determine if we should show detailed output (same logic as print_tool_description) + let verbose = os.database + .settings + .get_bool(Setting::ChatToolOutputVerbose) + .unwrap_or(true); + // tool.accepted is true if the tool was trusted (allowed without permission) + let show_details = verbose || !tool.accepted; + let invoke_result = tool .tool .invoke( @@ -2450,6 +2461,7 @@ impl ChatSession { &mut self.stdout, &mut self.conversation.file_line_tracker, &self.conversation.agents, + show_details, ) .await; @@ -2578,13 +2590,28 @@ impl ChatSession { } debug!("tool result output: {:#?}", result); + + // Check verbose setting + let verbose = os.database + .settings + .get_bool(Setting::ChatToolOutputVerbose) + .unwrap_or(true); + + // Build completion message + let completion_msg = if verbose || self.tools_used_in_turn.is_empty() { + format!(" ● Completed in {}s", tool_time) + } else { + let tools = self.tools_used_in_turn.join(", "); + format!(" ● Completed in {}s ({})", tool_time, tools) + }; + execute!( self.stdout, style::Print(CONTINUATION_LINE), style::Print("\n"), StyledText::success_fg(), style::SetAttribute(Attribute::Bold), - style::Print(format!(" ● Completed in {}s", tool_time)), + style::Print(completion_msg), StyledText::reset(), )?; if let Some(tag) = checkpoint_tag { @@ -3137,6 +3164,7 @@ impl ChatSession { self.tool_uses.clear(); self.pending_tool_index = None; self.tool_turn_start_time = None; + self.tools_used_in_turn.clear(); // Create turn checkpoint if tools were used if ExperimentManager::is_enabled(os, ExperimentName::Checkpoint) && !self.conversation.is_in_tangent_mode() @@ -3450,6 +3478,19 @@ impl ChatSession { async fn print_tool_description(&mut self, os: &Os, tool_index: usize, trusted: bool) -> Result<(), ChatError> { let tool_use = &self.tool_uses[tool_index]; + let tool_name = tool_use.tool.display_name(); + + // Always track tool usage for completion message + self.tools_used_in_turn.push(tool_name.clone()); + + // Determine if we should show detailed output + let verbose = os.database + .settings + .get_bool(Setting::ChatToolOutputVerbose) + .unwrap_or(true); // Default to verbose + + // Show details if: verbose mode is on OR tool requires permission + let show_details = verbose || !trusted; if self.stderr.should_send_structured_event { let tool_call_start = ToolCallStart { @@ -3464,7 +3505,7 @@ impl ChatSession { parent_message_id: None, }; self.stdout.send(Event::ToolCallStart(tool_call_start))?; - } else { + } else if show_details { queue!( self.stdout, StyledText::emphasis_fg(), @@ -3495,11 +3536,13 @@ impl ChatSession { )?; } - tool_use - .tool - .queue_description(os, &mut self.stdout) - .await - .map_err(|e| ChatError::Custom(format!("failed to print tool, `{}`: {}", tool_use.name, e).into()))?; + if show_details { + tool_use + .tool + .queue_description(os, &mut self.stdout) + .await + .map_err(|e| ChatError::Custom(format!("failed to print tool, `{}`: {}", tool_use.name, e).into()))?; + } self.stdout.flush()?; diff --git a/crates/chat-cli/src/cli/chat/tools/fs_read.rs b/crates/chat-cli/src/cli/chat/tools/fs_read.rs index b08b27043..027af7c8d 100644 --- a/crates/chat-cli/src/cli/chat/tools/fs_read.rs +++ b/crates/chat-cli/src/cli/chat/tools/fs_read.rs @@ -266,10 +266,10 @@ impl FsRead { } } - pub async fn invoke(&self, os: &Os, updates: &mut impl Write) -> Result { + pub async fn invoke(&self, os: &Os, updates: &mut impl Write, show_details: bool) -> Result { if self.operations.len() == 1 { // Single operation - return result directly - self.operations[0].invoke(os, updates).await + self.operations[0].invoke(os, updates, show_details).await } else { // Multiple operations - combine results let mut combined_results = Vec::new(); @@ -279,7 +279,7 @@ impl FsRead { let mut failed_ops = 0usize; for (i, op) in self.operations.iter().enumerate() { - match op.invoke(os, updates).await { + match op.invoke(os, updates, show_details).await { Ok(result) => { success_ops += 1; @@ -333,6 +333,7 @@ impl FsRead { updates, false, true, + show_details, )?; let combined_text = combined_results.join("\n\n"); @@ -376,12 +377,12 @@ impl FsReadOperation { } } - pub async fn invoke(&self, os: &Os, updates: &mut impl Write) -> Result { + pub async fn invoke(&self, os: &Os, updates: &mut impl Write, show_details: bool) -> Result { match self { - FsReadOperation::Line(fs_line) => fs_line.invoke(os, updates).await, - FsReadOperation::Directory(fs_directory) => fs_directory.invoke(os, updates).await, - FsReadOperation::Search(fs_search) => fs_search.invoke(os, updates).await, - FsReadOperation::Image(fs_image) => fs_image.invoke(updates).await, + FsReadOperation::Line(fs_line) => fs_line.invoke(os, updates, show_details).await, + FsReadOperation::Directory(fs_directory) => fs_directory.invoke(os, updates, show_details).await, + FsReadOperation::Search(fs_search) => fs_search.invoke(os, updates, show_details).await, + FsReadOperation::Image(fs_image) => fs_image.invoke(updates, show_details).await, } } } @@ -412,10 +413,10 @@ impl FsImage { Ok(()) } - pub async fn invoke(&self, updates: &mut impl Write) -> Result { + pub async fn invoke(&self, updates: &mut impl Write, show_details: bool) -> Result { let pre_processed_paths: Vec = self.image_paths.iter().map(|path| pre_process(path)).collect(); let valid_images = handle_images_from_paths(updates, &pre_processed_paths); - super::queue_function_result("Successfully read image", updates, false, false)?; + super::queue_function_result("Successfully read image", updates, false, false, show_details)?; Ok(InvokeOutput { output: OutputKind::Images(valid_images), }) @@ -498,7 +499,7 @@ impl FsLine { } } - pub async fn invoke(&self, os: &Os, updates: &mut impl Write) -> Result { + pub async fn invoke(&self, os: &Os, updates: &mut impl Write, show_details: bool) -> Result { let path = sanitize_path_tool_arg(os, &self.path); debug!(?path, "Reading"); let file_bytes = os.fs.read(&path).await?; @@ -547,6 +548,7 @@ time. You tried to read {byte_count} bytes. Try executing with fewer lines speci updates, false, false, + show_details, )?; Ok(InvokeOutput { @@ -606,7 +608,7 @@ impl FsSearch { Ok(()) } - pub async fn invoke(&self, os: &Os, updates: &mut impl Write) -> Result { + pub async fn invoke(&self, os: &Os, updates: &mut impl Write, show_details: bool) -> Result { let file_path = sanitize_path_tool_arg(os, &self.path); let pattern = &self.pattern; @@ -653,6 +655,7 @@ impl FsSearch { updates, false, false, + show_details, )?; Ok(InvokeOutput { @@ -703,7 +706,7 @@ impl FsDirectory { )?) } - pub async fn invoke(&self, os: &Os, updates: &mut impl Write) -> Result { + pub async fn invoke(&self, os: &Os, updates: &mut impl Write, show_details: bool) -> Result { let path = sanitize_path_tool_arg(os, &self.path); let max_depth = self.depth(); debug!(?path, max_depth, "Reading directory at path with depth"); @@ -796,6 +799,7 @@ impl FsDirectory { updates, false, false, + show_details, )?; Ok(InvokeOutput { diff --git a/crates/chat-cli/src/cli/chat/tools/mod.rs b/crates/chat-cli/src/cli/chat/tools/mod.rs index 4859fbf21..a75f63fa1 100644 --- a/crates/chat-cli/src/cli/chat/tools/mod.rs +++ b/crates/chat-cli/src/cli/chat/tools/mod.rs @@ -149,10 +149,11 @@ impl Tool { stdout: &mut impl Write, line_tracker: &mut HashMap, agents: &crate::cli::agent::Agents, + show_details: bool, ) -> Result { let active_agent = agents.get_active(); match self { - Tool::FsRead(fs_read) => fs_read.invoke(os, stdout).await, + Tool::FsRead(fs_read) => fs_read.invoke(os, stdout, show_details).await, Tool::FsWrite(fs_write) => fs_write.invoke(os, stdout, line_tracker).await, Tool::ExecuteCommand(execute_command) => execute_command.invoke(os, stdout).await, Tool::UseAws(use_aws) => use_aws.invoke(os, stdout).await, @@ -475,7 +476,12 @@ pub fn display_purpose(purpose: Option<&String>, updates: &mut impl Write) -> Re /// * `updates` - The output to write to /// * `is_error` - Whether this is an error message (changes formatting) /// * `use_bullet` - Whether to use a bullet point instead of a tick/exclamation -pub fn queue_function_result(result: &str, updates: &mut impl Write, is_error: bool, use_bullet: bool) -> Result<()> { +pub fn queue_function_result(result: &str, updates: &mut impl Write, is_error: bool, use_bullet: bool, should_display: bool) -> Result<()> { + // Skip output if should_display is false + if !should_display { + return Ok(()); + } + let lines = result.lines().collect::>(); // Determine symbol and color diff --git a/crates/chat-cli/src/database/settings.rs b/crates/chat-cli/src/database/settings.rs index 02aec64a9..1e38a5f0b 100644 --- a/crates/chat-cli/src/database/settings.rs +++ b/crates/chat-cli/src/database/settings.rs @@ -91,6 +91,8 @@ pub enum Setting { EnabledDelegate, #[strum(message = "Specify UI variant to use (string)")] UiMode, + #[strum(message = "Show detailed tool execution information (boolean)")] + ChatToolOutputVerbose, } impl AsRef for Setting { @@ -133,6 +135,7 @@ impl AsRef for Setting { Self::EnabledContextUsageIndicator => "chat.enableContextUsageIndicator", Self::EnabledDelegate => "chat.enableDelegate", Self::UiMode => "chat.uiMode", + Self::ChatToolOutputVerbose => "chat.tool_output_verbose", } } } @@ -183,6 +186,7 @@ impl TryFrom<&str> for Setting { "chat.enableCheckpoint" => Ok(Self::EnabledCheckpoint), "chat.enableContextUsageIndicator" => Ok(Self::EnabledContextUsageIndicator), "chat.uiMode" => Ok(Self::UiMode), + "chat.tool_output_verbose" => Ok(Self::ChatToolOutputVerbose), _ => Err(DatabaseError::InvalidSetting(value.to_string())), } }