From dea0e6b1ca21ad55e18e042b6edd78d95a00fd40 Mon Sep 17 00:00:00 2001 From: "Dhanji R. Prasanna" Date: Wed, 14 Jan 2026 08:12:50 +0530 Subject: [PATCH] Compact tool output improvements - Rename take_screenshot -> screenshot, code_coverage -> coverage (shorter names) - Align | character across all compact tools (pad to 11 chars for str_replace) - Make code_search a compact tool with summary display - Show language and search name in code_search output (e.g., rust:"find structs") - Add format_code_search_summary() to extract match/file counts from JSON response --- crates/g3-cli/src/ui_writer_impl.rs | 50 +++++++++++++++++---- crates/g3-core/src/lib.rs | 10 +++-- crates/g3-core/src/streaming.rs | 19 ++++++++ crates/g3-core/src/take_screenshot_test.rs | 4 +- crates/g3-core/src/tool_definitions.rs | 8 ++-- crates/g3-core/src/tool_dispatch.rs | 4 +- crates/g3-core/tests/tool_execution_test.rs | 4 +- 7 files changed, 77 insertions(+), 22 deletions(-) diff --git a/crates/g3-cli/src/ui_writer_impl.rs b/crates/g3-cli/src/ui_writer_impl.rs index 65e3f22..a904bf6 100644 --- a/crates/g3-cli/src/ui_writer_impl.rs +++ b/crates/g3-cli/src/ui_writer_impl.rs @@ -237,9 +237,10 @@ impl UiWriter for ConsoleUiWriter { // Check if this is a shell command - use compact format if tool_name == "shell" { *self.is_shell_compact.lock().unwrap() = true; - // Print compact shell header: "● shell | command" + // Print compact shell header: "● shell | command" + // Pad to align with longest compact tool (str_replace = 11 chars) println!( - " \x1b[2m●\x1b[0m {}{} \x1b[2m|\x1b[0m \x1b[35m{}\x1b[0m", + " \x1b[2m●\x1b[0m {}{:<11}\x1b[0m \x1b[2m|\x1b[0m \x1b[35m{}\x1b[0m", tool_color, tool_name, display_value ); return; @@ -323,7 +324,7 @@ impl UiWriter for ConsoleUiWriter { fn print_tool_compact(&self, tool_name: &str, summary: &str, duration_str: &str, tokens_delta: u32, _context_percentage: f32) -> bool { // Handle file operation tools and other compact tools - let is_compact_tool = matches!(tool_name, "read_file" | "write_file" | "str_replace" | "remember" | "take_screenshot" | "code_coverage" | "rehydrate"); + let is_compact_tool = matches!(tool_name, "read_file" | "write_file" | "str_replace" | "remember" | "screenshot" | "coverage" | "rehydrate" | "code_search"); if !is_compact_tool { // Reset continuation tracking for non-compact tools *self.last_read_file_path.lock().unwrap() = None; @@ -354,8 +355,35 @@ impl UiWriter for ConsoleUiWriter { // For tools without file_path, get other relevant args let display_arg = if file_path.is_empty() { - // For remember, take_screenshot, etc. - no path to show - String::new() + // For code_search, extract language and name from searches + if tool_name == "code_search" { + // searches arg is JSON array, try to extract first search's language and name + if let Some((_, searches_json)) = args.iter().find(|(k, _)| k == "searches") { + if let Ok(searches) = serde_json::from_str::(searches_json) { + if let Some(first_search) = searches.as_array().and_then(|arr| arr.first()) { + let lang = first_search.get("language").and_then(|v| v.as_str()).unwrap_or("?"); + let name = first_search.get("name").and_then(|v| v.as_str()).unwrap_or("?"); + // Truncate name if too long + let display_name = if name.len() > 30 { + let truncate_at = name.char_indices().nth(27).map(|(i, _)| i).unwrap_or(name.len()); + format!("{}...", &name[..truncate_at]) + } else { + name.to_string() + }; + format!("{}:\"{}\"", lang, display_name) + } else { + String::new() + } + } else { + String::new() + } + } else { + String::new() + } + } else { + // For remember, screenshot, etc. - no path to show + String::new() + } } else { // Truncate long paths if file_path.len() > 60 { @@ -409,14 +437,16 @@ impl UiWriter for ConsoleUiWriter { ); } else if display_arg.is_empty() { // Tools without file path: " ● tool_name | summary | tokens ◉ time" + // Pad to align with longest compact tool (str_replace = 11 chars) println!( - " \x1b[2m●\x1b[0m {}{} \x1b[2m| {}\x1b[0m \x1b[2m| {} ◉ {}\x1b[0m", + " \x1b[2m●\x1b[0m {}{:<11}\x1b[0m \x1b[2m| {}\x1b[0m \x1b[2m| {} ◉ {}\x1b[0m", tool_color, tool_name, summary, tokens_delta, duration_str ); } else { // Tools with file path: " ● tool_name | path [range] | summary | tokens ◉ time" + // Pad to align with longest compact tool (str_replace = 11 chars) println!( - " \x1b[2m●\x1b[0m {}{} \x1b[2m|\x1b[0m \x1b[35m{}{}\x1b[0m \x1b[2m| {}\x1b[0m \x1b[2m| {} ◉ {}\x1b[0m", + " \x1b[2m●\x1b[0m {}{:<11}\x1b[0m \x1b[2m|\x1b[0m \x1b[35m{}{}\x1b[0m \x1b[2m| {}\x1b[0m \x1b[2m| {} ◉ {}\x1b[0m", tool_color, tool_name, display_arg, range_suffix, summary, tokens_delta, duration_str ); } @@ -455,11 +485,13 @@ impl UiWriter for ConsoleUiWriter { match content { None => { // Empty TODO - println!(" \x1b[2m●\x1b[0m {}{}\x1b[0m \x1b[2m|\x1b[0m \x1b[35mempty\x1b[0m", tool_color, tool_name); + // Pad to align with longest compact tool (str_replace = 11 chars) + println!(" \x1b[2m●\x1b[0m {}{:<11}\x1b[0m \x1b[2m|\x1b[0m \x1b[35mempty\x1b[0m", tool_color, tool_name); } Some(text) => { // Header - println!(" \x1b[2m●\x1b[0m {}{}\x1b[0m", tool_color, tool_name); + // Pad to align with longest compact tool (str_replace = 11 chars) + println!(" \x1b[2m●\x1b[0m {}{:<11}\x1b[0m", tool_color, tool_name); let lines: Vec<&str> = text.lines().collect(); let last_idx = lines.len().saturating_sub(1); diff --git a/crates/g3-core/src/lib.rs b/crates/g3-core/src/lib.rs index 410b1c8..fc1c06b 100644 --- a/crates/g3-core/src/lib.rs +++ b/crates/g3-core/src/lib.rs @@ -2047,7 +2047,7 @@ Skip if nothing new. Be brief."#; } // Check if this is a compact tool (file operations) - let is_compact_tool = matches!(tool_call.tool.as_str(), "read_file" | "write_file" | "str_replace" | "remember" | "take_screenshot" | "code_coverage" | "rehydrate"); + let is_compact_tool = matches!(tool_call.tool.as_str(), "read_file" | "write_file" | "str_replace" | "remember" | "screenshot" | "coverage" | "rehydrate" | "code_search"); // Only print output header for non-compact tools if !is_compact_tool && !is_todo_tool { @@ -2120,11 +2120,11 @@ Skip if nothing new. Be brief."#; // Extract size from result like "Memory updated. Size: 1.2k" Some(streaming::format_remember_summary(&tool_result)) } - "take_screenshot" => { + "screenshot" => { // Extract path from result Some(streaming::format_screenshot_summary(&tool_result)) } - "code_coverage" => { + "coverage" => { // Show coverage summary Some(streaming::format_coverage_summary(&tool_result)) } @@ -2132,6 +2132,10 @@ Skip if nothing new. Be brief."#; // Show fragment info Some(streaming::format_rehydrate_summary(&tool_result)) } + "code_search" => { + // Show search summary (matches and files) + Some(streaming::format_code_search_summary(&tool_result)) + } _ => Some(format!("✅ completed")) } } diff --git a/crates/g3-core/src/streaming.rs b/crates/g3-core/src/streaming.rs index 293fd58..fd6946e 100644 --- a/crates/g3-core/src/streaming.rs +++ b/crates/g3-core/src/streaming.rs @@ -386,6 +386,25 @@ pub fn format_rehydrate_summary(result: &str) -> String { } } +/// Format a code_search result summary. +pub fn format_code_search_summary(result: &str) -> String { + // Result format: "✅ Code search completed\n{json}" + // JSON contains: {"searches": [...], "total_matches": N, "total_files_searched": M} + if result.contains("❌") { + "❌ failed".to_string() + } else if let Some(json_start) = result.find('{') { + // Try to parse the JSON to extract summary stats + if let Ok(parsed) = serde_json::from_str::(&result[json_start..]) { + let matches = parsed.get("total_matches").and_then(|v| v.as_u64()).unwrap_or(0); + let files = parsed.get("total_files_searched").and_then(|v| v.as_u64()).unwrap_or(0); + return format!("🔍 {} matches in {} files", matches, files); + } + "🔍 search complete".to_string() + } else { + "🔍 search complete".to_string() + } +} + // ============================================================================= // Tool Call Deduplication // ============================================================================= diff --git a/crates/g3-core/src/take_screenshot_test.rs b/crates/g3-core/src/take_screenshot_test.rs index a90d81e..1457033 100644 --- a/crates/g3-core/src/take_screenshot_test.rs +++ b/crates/g3-core/src/take_screenshot_test.rs @@ -9,7 +9,7 @@ mod take_screenshot_tests { fn test_take_screenshot_requires_window_id() { // Create a tool call without window_id let tool_call = ToolCall { - tool: "take_screenshot".to_string(), + tool: "screenshot".to_string(), args: json!({ "path": "test.png" }), @@ -23,7 +23,7 @@ mod take_screenshot_tests { fn test_take_screenshot_with_window_id() { // Create a tool call with window_id let tool_call = ToolCall { - tool: "take_screenshot".to_string(), + tool: "screenshot".to_string(), args: json!({ "path": "test.png", "window_id": "Safari" diff --git a/crates/g3-core/src/tool_definitions.rs b/crates/g3-core/src/tool_definitions.rs index db716ed..58843a0 100644 --- a/crates/g3-core/src/tool_definitions.rs +++ b/crates/g3-core/src/tool_definitions.rs @@ -167,7 +167,7 @@ fn create_core_tools(exclude_research: bool) -> Vec { }), }, Tool { - name: "take_screenshot".to_string(), + name: "screenshot".to_string(), description: "Capture a screenshot of a specific application window. You MUST specify the window_id parameter with the application name (e.g., 'Safari', 'Terminal', 'Google Chrome'). The tool will automatically use the native screencapture command with the application's window ID for a clean capture. Use list_windows first to identify available windows.".to_string(), input_schema: json!({ "type": "object", @@ -217,7 +217,7 @@ fn create_core_tools(exclude_research: bool) -> Vec { }), }, Tool { - name: "code_coverage".to_string(), + name: "coverage".to_string(), description: "Generate a code coverage report for the entire workspace using cargo llvm-cov. This runs all tests with coverage instrumentation and returns a summary of coverage statistics. Requires llvm-tools-preview and cargo-llvm-cov to be installed (they will be auto-installed if missing).".to_string(), input_schema: json!({ "type": "object", @@ -508,8 +508,8 @@ mod tests { fn test_core_tools_count() { let tools = create_core_tools(false); // Should have the core tools: shell, background_process, read_file, read_image, - // write_file, str_replace, take_screenshot, - // todo_read, todo_write, code_coverage, code_search, research, remember + // write_file, str_replace, screenshot, + // todo_read, todo_write, coverage, code_search, research, remember // (13 total - memory is auto-loaded, only remember tool needed) assert_eq!(tools.len(), 14); } diff --git a/crates/g3-core/src/tool_dispatch.rs b/crates/g3-core/src/tool_dispatch.rs index 0630ce0..8be9cfd 100644 --- a/crates/g3-core/src/tool_dispatch.rs +++ b/crates/g3-core/src/tool_dispatch.rs @@ -37,8 +37,8 @@ pub async fn dispatch_tool( "todo_write" => todo::execute_todo_write(tool_call, ctx).await, // Miscellaneous tools - "take_screenshot" => misc::execute_take_screenshot(tool_call, ctx).await, - "code_coverage" => misc::execute_code_coverage(tool_call, ctx).await, + "screenshot" => misc::execute_take_screenshot(tool_call, ctx).await, + "coverage" => misc::execute_code_coverage(tool_call, ctx).await, "code_search" => misc::execute_code_search(tool_call, ctx).await, // Research tool diff --git a/crates/g3-core/tests/tool_execution_test.rs b/crates/g3-core/tests/tool_execution_test.rs index 044d573..d32be3d 100644 --- a/crates/g3-core/tests/tool_execution_test.rs +++ b/crates/g3-core/tests/tool_execution_test.rs @@ -393,14 +393,14 @@ mod screenshot_tests { #[test] fn test_screenshot_tool_call_structure() { let tool_call = make_tool_call( - "take_screenshot", + "screenshot", json!({ "path": "screenshot.png", "window_id": "Safari" }), ); - assert_eq!(tool_call.tool, "take_screenshot"); + assert_eq!(tool_call.tool, "screenshot"); assert_eq!(tool_call.args.get("path").unwrap().as_str(), Some("screenshot.png")); assert_eq!(tool_call.args.get("window_id").unwrap().as_str(), Some("Safari")); }