diff --git a/crates/g3-cli/src/retro_tui.rs b/crates/g3-cli/src/retro_tui.rs index 82cb782..6bf5788 100644 --- a/crates/g3-cli/src/retro_tui.rs +++ b/crates/g3-cli/src/retro_tui.rs @@ -31,6 +31,7 @@ const TERMINAL_DARK_AMBER: Color = Color::Rgb(204, 119, 34); // Dark amber for P #[derive(Debug, Clone)] pub enum TuiMessage { AgentOutput(String), + ToolOutput { name: String, content: String }, SystemStatus(String), ContextUpdate { used: u32, @@ -91,6 +92,64 @@ impl TerminalState { } } + /// Format tool call output with a box + fn format_tool_output(&mut self, tool_name: &str, content: &str) { + // Calculate box width (use a reasonable width, accounting for terminal size) + let box_width = 80; + let border_char = "─"; + let corner_tl = "┌"; + let corner_tr = "┐"; + let corner_bl = "└"; + let corner_br = "┘"; + let vertical = "│"; + + // Add top border + self.output_history.push(format!( + "{}{}{}", + corner_tl, + border_char.repeat(box_width - 2), + corner_tr + )); + + // Add header with tool name (will be styled with green background in draw) + let header_text = format!(" {} ", tool_name.to_uppercase()); + let padding = box_width - 2 - header_text.len(); + self.output_history.push(format!( + "{}[TOOL_HEADER]{}{}{}", + vertical, + header_text, + " ".repeat(padding), + vertical + )); + + // Add separator between header and content + self.output_history.push(format!( + "{}{}{}", + "├", + border_char.repeat(box_width - 2), + "┤" + )); + + // Add content lines + for line in content.lines() { + // Wrap long lines if needed + let max_content_width = box_width - 4; // Account for borders and padding + if line.len() <= max_content_width { + self.output_history.push(format!("{} {:>().chunks(max_content_width) { + let chunk_str: String = chunk.iter().collect(); + self.output_history.push(format!("{} {: { state.add_output(&text); } + TuiMessage::ToolOutput { name, content } => { + state.format_tool_output(&name, &content); + } TuiMessage::SystemStatus(status) => { state.status_line = status; } @@ -293,6 +355,24 @@ impl RetroTui { .skip(scroll) .take(visible_height) .map(|line| { + // Check if this is a tool header line + if line.contains("[TOOL_HEADER]") { + // Extract the actual header text + let cleaned = line.replace("[TOOL_HEADER]", ""); + // Style with green background and black text + return Line::from(Span::styled( + format!(" {}", cleaned), + Style::default() + .bg(TERMINAL_GREEN) + .fg(Color::Black) + .add_modifier(Modifier::BOLD), + )); + } + + // Check if this is a box border line + if line.starts_with("┌") || line.starts_with("└") || line.starts_with("│") || line.starts_with("├") { + return Line::from(Span::styled(format!(" {}", line), Style::default().fg(TERMINAL_DIM_GREEN))); + } // Apply different colors based on content let style = if line.starts_with("ERROR:") { Style::default() @@ -445,6 +525,11 @@ impl RetroTui { let _ = self.tx.send(TuiMessage::AgentOutput(text.to_string())); } + /// Send tool output to the terminal + pub fn tool_output(&self, name: &str, content: &str) { + let _ = self.tx.send(TuiMessage::ToolOutput { name: name.to_string(), content: content.to_string() }); + } + /// Update system status pub fn status(&self, status: &str) { let _ = self.tx.send(TuiMessage::SystemStatus(status.to_string())); diff --git a/crates/g3-cli/src/ui_writer_impl.rs b/crates/g3-cli/src/ui_writer_impl.rs index 0cfafa6..f420d6c 100644 --- a/crates/g3-cli/src/ui_writer_impl.rs +++ b/crates/g3-cli/src/ui_writer_impl.rs @@ -1,6 +1,7 @@ use crate::retro_tui::RetroTui; use g3_core::ui_writer::UiWriter; use std::io::{self, Write}; +use std::sync::Mutex; /// Console implementation of UiWriter that prints to stdout pub struct ConsoleUiWriter; @@ -84,11 +85,13 @@ impl UiWriter for ConsoleUiWriter { /// RetroTui implementation of UiWriter that sends output to the TUI pub struct RetroTuiWriter { tui: RetroTui, + current_tool_name: Mutex>, + current_tool_output: Mutex>, } impl RetroTuiWriter { pub fn new(tui: RetroTui) -> Self { - Self { tui } + Self { tui, current_tool_name: Mutex::new(None), current_tool_output: Mutex::new(Vec::new()) } } } @@ -121,32 +124,45 @@ impl UiWriter for RetroTuiWriter { } fn print_tool_header(&self, tool_name: &str) { - self.tui.output(&format!("┌─ {}", tool_name)); + // Start collecting tool output + *self.current_tool_name.lock().unwrap() = Some(tool_name.to_string()); + self.current_tool_output.lock().unwrap().clear(); + self.current_tool_output.lock().unwrap().push(format!("Tool: {}", tool_name)); } fn print_tool_arg(&self, key: &str, value: &str) { - self.tui.output(&format!("│ {}: {}", key, value)); + self.current_tool_output.lock().unwrap().push(format!("{}: {}", key, value)); } fn print_tool_output_header(&self) { - self.tui.output("├─ output:"); + self.current_tool_output.lock().unwrap().push(String::new()); + self.current_tool_output.lock().unwrap().push("Output:".to_string()); } fn print_tool_output_line(&self, line: &str) { - self.tui.output(&format!("│ {}", line)); + self.current_tool_output.lock().unwrap().push(line.to_string()); } fn print_tool_output_summary(&self, hidden_count: usize) { - self.tui.output(&format!( - "│ ... ({} more line{} hidden)", + self.current_tool_output.lock().unwrap().push(format!( + "... ({} more line{} hidden)", hidden_count, if hidden_count == 1 { "" } else { "s" } )); } fn print_tool_timing(&self, duration_str: &str) { - self.tui.output(&format!("└─ ⚡️ {}", duration_str)); - self.tui.output(""); + self.current_tool_output.lock().unwrap().push(format!("⚡️ {}", duration_str)); + + // Now send the complete tool output as a box + if let Some(tool_name) = self.current_tool_name.lock().unwrap().as_ref() { + let content = self.current_tool_output.lock().unwrap().join("\n"); + self.tui.tool_output(tool_name, &content); + } + + // Clear the buffers + *self.current_tool_name.lock().unwrap() = None; + self.current_tool_output.lock().unwrap().clear(); } fn print_agent_prompt(&self) {