add markdown format only to final_output and fix todo duplication

This commit is contained in:
Dhanji R. Prasanna
2025-12-02 14:26:22 +11:00
parent a6537e4dba
commit d9ad244197
5 changed files with 79 additions and 34 deletions

View File

@@ -105,4 +105,9 @@ impl UiWriter for MachineUiWriter {
// Default to first option (index 0) for automation // Default to first option (index 0) for automation
0 0
} }
fn print_final_output(&self, summary: &str) {
println!("FINAL_OUTPUT:");
println!("{}", summary);
}
} }

View File

@@ -1,5 +1,6 @@
use g3_core::ui_writer::UiWriter; use g3_core::ui_writer::UiWriter;
use std::io::{self, Write}; use std::io::{self, Write};
use termimad::MadSkin;
/// Console implementation of UiWriter that prints to stdout /// Console implementation of UiWriter that prints to stdout
pub struct ConsoleUiWriter { pub struct ConsoleUiWriter {
@@ -104,6 +105,9 @@ impl UiWriter for ConsoleUiWriter {
fn print_tool_output_header(&self) { fn print_tool_output_header(&self) {
println!(); println!();
// Reset output_line_printed at the start of a new tool output
// This ensures the header isn't cleared by update_tool_output_line
*self.output_line_printed.lock().unwrap() = false;
// Now print the tool header with the most important arg in bold green // Now print the tool header with the most important arg in bold green
if let Some(tool_name) = self.current_tool_name.lock().unwrap().as_ref() { if let Some(tool_name) = self.current_tool_name.lock().unwrap().as_ref() {
let args = self.current_tool_args.lock().unwrap(); let args = self.current_tool_args.lock().unwrap();
@@ -306,4 +310,44 @@ impl UiWriter for ConsoleUiWriter {
let _ = io::stdout().flush(); let _ = io::stdout().flush();
} }
} }
fn print_final_output(&self, summary: &str) {
// Show spinner while "formatting"
let spinner_frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
let message = "summarizing work done...";
// Brief spinner animation (about 0.5 seconds)
for i in 0..5 {
let frame = spinner_frames[i % spinner_frames.len()];
print!("\r\x1b[36m{} {}\x1b[0m", frame, message);
let _ = io::stdout().flush();
std::thread::sleep(std::time::Duration::from_millis(100));
}
// Clear the spinner line
print!("\r\x1b[2K");
let _ = io::stdout().flush();
// Create a styled markdown skin
let mut skin = MadSkin::default();
// Customize colors for better terminal appearance
skin.bold.set_fg(termimad::crossterm::style::Color::Green);
skin.italic.set_fg(termimad::crossterm::style::Color::Cyan);
skin.headers[0].set_fg(termimad::crossterm::style::Color::Magenta);
skin.headers[1].set_fg(termimad::crossterm::style::Color::Magenta);
skin.code_block.set_fg(termimad::crossterm::style::Color::Yellow);
skin.inline_code.set_fg(termimad::crossterm::style::Color::Yellow);
// Print a header separator
println!("\x1b[1;35m━━━ Summary ━━━\x1b[0m");
println!();
// Render the markdown
let rendered = skin.term_text(summary);
print!("{}", rendered);
// Print a footer separator
println!();
println!("\x1b[1;35m━━━━━━━━━━━━━━━\x1b[0m");
}
} }

View File

@@ -3529,11 +3529,9 @@ impl<W: UiWriter> Agent<W> {
// Display tool execution result with proper indentation // Display tool execution result with proper indentation
if tool_call.tool == "final_output" { if tool_call.tool == "final_output" {
// For final_output, display the summary without truncation // For final_output, use the dedicated method that renders markdown
for line in tool_result.lines() { // with a spinner animation
self.ui_writer.update_tool_output_line(line); self.ui_writer.print_final_output(&tool_result);
}
self.ui_writer.println("");
} else { } else {
let output_lines: Vec<&str> = tool_result.lines().collect(); let output_lines: Vec<&str> = tool_result.lines().collect();
@@ -3563,44 +3561,32 @@ impl<W: UiWriter> Agent<W> {
const MAX_LINE_WIDTH: usize = 80; const MAX_LINE_WIDTH: usize = 80;
let output_len = output_lines.len(); let output_len = output_lines.len();
// For todo tools, show all lines without truncation // Skip printing for todo tools - they already print their content
let is_todo_tool = let is_todo_tool =
tool_call.tool == "todo_read" || tool_call.tool == "todo_write"; tool_call.tool == "todo_read" || tool_call.tool == "todo_write";
let max_lines_to_show = if is_todo_tool || wants_full {
output_len
} else {
MAX_LINES
};
for (idx, line) in output_lines.iter().enumerate() { if !is_todo_tool {
if !is_todo_tool && !wants_full && idx >= max_lines_to_show { let max_lines_to_show = if wants_full { output_len } else { MAX_LINES };
break;
}
// Clip line to max width (but not for todo tools)
let clipped_line = truncate_line(
line,
MAX_LINE_WIDTH,
!wants_full && !is_todo_tool,
);
// Use print_tool_output_line for todo tools to get special formatting for (idx, line) in output_lines.iter().enumerate() {
if is_todo_tool { if !wants_full && idx >= max_lines_to_show {
self.ui_writer.print_tool_output_line(&clipped_line); break;
} else { }
let clipped_line = truncate_line(line, MAX_LINE_WIDTH, !wants_full);
self.ui_writer.update_tool_output_line(&clipped_line); self.ui_writer.update_tool_output_line(&clipped_line);
} }
}
if !is_todo_tool && !wants_full && output_len > MAX_LINES { if !wants_full && output_len > MAX_LINES {
self.ui_writer.print_tool_output_summary(output_len); self.ui_writer.print_tool_output_summary(output_len);
}
} }
} }
// Check if this was a final_output tool call // Check if this was a final_output tool call
if tool_call.tool == "final_output" { if tool_call.tool == "final_output" {
// The summary was displayed above when we printed the tool result // The summary was already displayed via print_final_output
// Add it to full_response so it's included in the TaskResult // Don't add it to full_response to avoid duplicate printing
full_response.push_str(&tool_result); // full_response is intentionally left empty/unchanged
self.ui_writer.println(""); self.ui_writer.println("");
let _ttft = let _ttft =
first_token_time.unwrap_or_else(|| stream_start.elapsed()); first_token_time.unwrap_or_else(|| stream_start.elapsed());
@@ -3608,13 +3594,13 @@ impl<W: UiWriter> Agent<W> {
// Add timing if needed // Add timing if needed
let final_response = if show_timing { let final_response = if show_timing {
format!( format!(
"{}\n\n🕝 {} | 💭 {}", "🕝 {} | 💭 {}",
full_response,
Self::format_duration(stream_start.elapsed()), Self::format_duration(stream_start.elapsed()),
Self::format_duration(_ttft) Self::format_duration(_ttft)
) )
} else { } else {
full_response // Return empty string since content was already displayed
String::new()
}; };
return Ok(TaskResult::new( return Ok(TaskResult::new(

View File

@@ -65,6 +65,10 @@ pub trait UiWriter: Send + Sync {
/// Prompt the user to choose from a list of options /// Prompt the user to choose from a list of options
/// Returns the index of the selected option /// Returns the index of the selected option
fn prompt_user_choice(&self, message: &str, options: &[&str]) -> usize; fn prompt_user_choice(&self, message: &str, options: &[&str]) -> usize;
/// Print the final output summary with markdown formatting
/// Shows a spinner while formatting, then renders the markdown
fn print_final_output(&self, summary: &str);
} }
/// A no-op implementation for when UI output is not needed /// A no-op implementation for when UI output is not needed
@@ -97,4 +101,7 @@ impl UiWriter for NullUiWriter {
fn prompt_user_choice(&self, _message: &str, _options: &[&str]) -> usize { fn prompt_user_choice(&self, _message: &str, _options: &[&str]) -> usize {
0 0
} }
fn print_final_output(&self, _summary: &str) {
// No-op for null writer
}
} }

View File

@@ -81,6 +81,9 @@ impl UiWriter for MockUiWriter {
.push(format!("CHOICE: {} Options: {:?}", message, options)); .push(format!("CHOICE: {} Options: {:?}", message, options));
self.choice_responses.lock().unwrap().pop().unwrap_or(0) self.choice_responses.lock().unwrap().pop().unwrap_or(0)
} }
fn print_final_output(&self, summary: &str) {
self.output.lock().unwrap().push(format!("FINAL: {}", summary));
}
} }
#[tokio::test] #[tokio::test]