From 8dcb7a3dbad250972bc24428a7ce5a632fc53100 Mon Sep 17 00:00:00 2001 From: "Dhanji R. Prasanna" Date: Tue, 13 Jan 2026 10:58:55 +0530 Subject: [PATCH] feat: add compact styled output for TODO tools MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TODO tools (todo_read, todo_write) now display with a cleaner, more compact format: - Styled header: " ● todo_read" or " ● todo_write" - Tree-style prefixes for content lines (│ and └) - Checkbox conversion: "- [ ]" → □, "- [x]" → ■ - Dimmed content for visual distinction - No timing footer (cleaner output) Changes: - Add print_todo_compact() method to UiWriter trait - Implement print_todo_compact() in ConsoleUiWriter - Update todo.rs to call print_todo_compact() instead of line-by-line output - Skip tool header, output header, and timing for TODO tools in agent streaming --- crates/g3-cli/src/ui_writer_impl.rs | 51 +++++++++++++++++++++++++++++ crates/g3-core/src/lib.rs | 40 +++++++++++++--------- crates/g3-core/src/tools/todo.rs | 23 +++++-------- crates/g3-core/src/ui_writer.rs | 9 +++++ 4 files changed, 92 insertions(+), 31 deletions(-) diff --git a/crates/g3-cli/src/ui_writer_impl.rs b/crates/g3-cli/src/ui_writer_impl.rs index 9db8ef3..65e3f22 100644 --- a/crates/g3-cli/src/ui_writer_impl.rs +++ b/crates/g3-cli/src/ui_writer_impl.rs @@ -437,6 +437,57 @@ impl UiWriter for ConsoleUiWriter { true } + fn print_todo_compact(&self, content: Option<&str>, is_write: bool) -> bool { + let tool_name = if is_write { "todo_write" } else { "todo_read" }; + let is_agent_mode = *self.is_agent_mode.lock().unwrap(); + let tool_color = if is_agent_mode { "\x1b[38;5;250m" } else { "\x1b[32m" }; + + // Add blank line if last output was text (for visual separation) + let mut last_was_text = self.last_output_was_text.lock().unwrap(); + if *last_was_text { + println!(); + } + *last_was_text = false; + *self.last_output_was_tool.lock().unwrap() = true; + // Reset read_file continuation tracking + *self.last_read_file_path.lock().unwrap() = None; + + match content { + None => { + // Empty TODO + println!(" \x1b[2m●\x1b[0m {}{}\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); + + let lines: Vec<&str> = text.lines().collect(); + let last_idx = lines.len().saturating_sub(1); + + for (i, line) in lines.iter().enumerate() { + let is_last = i == last_idx; + let prefix = if is_last { "└" } else { "│" }; + + // Convert checkboxes to styled symbols + let styled_line = line + .replace("- [x]", "■") + .replace("- [X]", "■") + .replace("- [ ]", "□"); + + // Dim the line content + println!(" \x1b[2m{} {}\x1b[0m", prefix, styled_line); + } + // Add blank line after content for readability + println!(); + } + } + + // Clear tool state + self.clear_tool_state(); + + true + } + fn print_tool_timing(&self, duration_str: &str, tokens_delta: u32, context_percentage: f32) { let color_code = duration_color(duration_str); diff --git a/crates/g3-core/src/lib.rs b/crates/g3-core/src/lib.rs index 37ce929..e6c7af2 100644 --- a/crates/g3-core/src/lib.rs +++ b/crates/g3-core/src/lib.rs @@ -88,7 +88,7 @@ pub enum StreamState { // Re-export StreamingToolParser from its own module -pub use streaming_parser::StreamingToolParser; +pub use streaming_parser::{StreamingToolParser, sanitize_inline_tool_patterns, LBRACE_HOMOGLYPH}; pub struct Agent { providers: ProviderRegistry, @@ -2131,7 +2131,11 @@ impl Agent { // Finish streaming markdown before showing tool output self.ui_writer.finish_streaming_markdown(); - // Tool call header + // Check if this is a TODO tool (they handle their own output) + let is_todo_tool = tool_call.tool == "todo_read" || tool_call.tool == "todo_write"; + + // Tool call header (skip for TODO tools - they print their own compact header) + if !is_todo_tool { self.ui_writer.print_tool_header(&tool_call.tool, Some(&tool_call.args)); if let Some(args_obj) = tool_call.args.as_object() { for (key, value) in args_obj { @@ -2143,12 +2147,13 @@ impl Agent { self.ui_writer.print_tool_arg(key, &value_str); } } + } // 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"); // Only print output header for non-compact tools - if !is_compact_tool { + if !is_compact_tool && !is_todo_tool { self.ui_writer.print_tool_output_header(); } @@ -2316,20 +2321,23 @@ impl Agent { // Closure marker with timing let tokens_delta = self.context_window.used_tokens.saturating_sub(tokens_before); - // Use compact format for file operations, normal format for others - if let Some(summary) = compact_summary { - self.ui_writer.print_tool_compact( - &tool_call.tool, - &summary, - &streaming::format_duration(exec_duration), - tokens_delta, - self.context_window.percentage_used(), - ); - } else { - self.ui_writer - .print_tool_timing(&streaming::format_duration(exec_duration), + // TODO tools handle their own output via print_todo_compact, skip timing + if !is_todo_tool { + // Use compact format for file operations, normal format for others + if let Some(summary) = compact_summary { + self.ui_writer.print_tool_compact( + &tool_call.tool, + &summary, + &streaming::format_duration(exec_duration), tokens_delta, - self.context_window.percentage_used()); + self.context_window.percentage_used(), + ); + } else { + self.ui_writer + .print_tool_timing(&streaming::format_duration(exec_duration), + tokens_delta, + self.context_window.percentage_used()); + } } self.ui_writer.print_agent_prompt(); diff --git a/crates/g3-core/src/tools/todo.rs b/crates/g3-core/src/tools/todo.rs index 2571e04..e65ff83 100644 --- a/crates/g3-core/src/tools/todo.rs +++ b/crates/g3-core/src/tools/todo.rs @@ -23,6 +23,7 @@ pub async fn execute_todo_read( // Also update in-memory content to stay in sync let mut todo = ctx.todo_content.write().await; *todo = String::new(); + ctx.ui_writer.print_todo_compact(None, false); return Ok("📝 TODO list is empty (no todo.g3.md file found)".to_string()); } @@ -42,11 +43,10 @@ pub async fn execute_todo_read( } if content.trim().is_empty() { + ctx.ui_writer.print_todo_compact(None, false); Ok("📝 TODO list is empty".to_string()) } else { - for line in content.lines() { - ctx.ui_writer.print_tool_output_line(line); - } + ctx.ui_writer.print_todo_compact(Some(&content), false); Ok(format!("📝 TODO list:\n{}", content)) } } @@ -98,14 +98,10 @@ pub async fn execute_todo_write( Ok(_) => { let mut todo = ctx.todo_content.write().await; *todo = String::new(); - // Show the final completed TODOs before deletion - let mut result = - String::from("✅ All TODOs completed! Removed todo.g3.md\n\nFinal status:\n"); - for line in content_str.lines() { - ctx.ui_writer.print_tool_output_line(line); - result.push_str(line); - result.push('\n'); - } + // Show the final completed TODOs + ctx.ui_writer.print_todo_compact(Some(content_str), true); + let mut result = String::from("✅ All TODOs completed! Removed todo.g3.md\n\nFinal status:\n"); + result.push_str(content_str); return Ok(result); } Err(e) => return Ok(format!("❌ Failed to remove todo.g3.md: {}", e)), @@ -117,10 +113,7 @@ pub async fn execute_todo_write( // Also update in-memory content to stay in sync let mut todo = ctx.todo_content.write().await; *todo = content_str.to_string(); - // Print the TODO content to the console (inside the tool frame) - for line in content_str.lines() { - ctx.ui_writer.print_tool_output_line(line); - } + ctx.ui_writer.print_todo_compact(Some(content_str), true); Ok(format!( "✅ TODO list updated ({} chars) and saved to todo.g3.md:\n{}", char_count, content_str diff --git a/crates/g3-core/src/ui_writer.rs b/crates/g3-core/src/ui_writer.rs index 015659c..ee12238 100644 --- a/crates/g3-core/src/ui_writer.rs +++ b/crates/g3-core/src/ui_writer.rs @@ -46,6 +46,15 @@ pub trait UiWriter: Send + Sync { false } + /// Print a compact TODO tool output with styled content + /// Format: " ● todo_read" header, then styled content lines, no timing + /// content: None for empty, Some(content) for TODO content + /// is_write: true for todo_write, false for todo_read + /// Returns true if handled, false to fall back to normal format + fn print_todo_compact(&self, _content: Option<&str>, _is_write: bool) -> bool { + false + } + /// Print tool execution timing fn print_tool_timing(&self, duration_str: &str, tokens_delta: u32, context_percentage: f32);