From 662748ed23686c7e0cb59a3ba167018115e688db Mon Sep 17 00:00:00 2001 From: Dhanji Prasanna Date: Wed, 15 Oct 2025 22:04:39 +1100 Subject: [PATCH] better formatting cli --- crates/g3-cli/src/lib.rs | 8 ++-- crates/g3-cli/src/tui.rs | 60 +++++++++++++++++++++++++++++ crates/g3-cli/src/ui_writer_impl.rs | 48 +++++++++++++++++++++-- 3 files changed, 109 insertions(+), 7 deletions(-) diff --git a/crates/g3-cli/src/lib.rs b/crates/g3-cli/src/lib.rs index 73872c4..1ca997d 100644 --- a/crates/g3-cli/src/lib.rs +++ b/crates/g3-cli/src/lib.rs @@ -236,7 +236,7 @@ pub async fn run() -> Result<()> { let result = agent .execute_task_with_timing(&task, None, false, cli.show_prompt, cli.show_code, true) .await?; - output.print_markdown(&result.response); + output.print_smart(&result.response); } else { // Interactive mode (default) if !cli.retro { @@ -809,7 +809,7 @@ async fn execute_task( if attempt > 1 { output.print(&format!("✅ Request succeeded after {} attempts", attempt)); } - output.print_markdown(&result.response); + output.print_smart(&result.response); return; } Err(e) => { @@ -1087,7 +1087,7 @@ async fn run_autonomous( Ok(result) => { // Display player's implementation result output.print("📝 Player implementation completed:"); - output.print_markdown(&result.response); + output.print_smart(&result.response); break; } Err(e) => { @@ -1332,7 +1332,7 @@ Remember: Be thorough in your review but concise in your feedback. APPROVE if th continue; } - output.print(&format!("Coach feedback:\n{}", coach_feedback_text)); + output.print_smart(&format!("Coach feedback:\n{}", coach_feedback_text)); // Check if coach approved the implementation if coach_result.is_approved() { diff --git a/crates/g3-cli/src/tui.rs b/crates/g3-cli/src/tui.rs index 7ba44c0..261408e 100644 --- a/crates/g3-cli/src/tui.rs +++ b/crates/g3-cli/src/tui.rs @@ -27,10 +27,40 @@ impl SimpleOutput { Self { mad_skin } } + /// Detect if text contains markdown formatting + fn has_markdown(&self, text: &str) -> bool { + // Check for common markdown patterns + text.contains("**") || + text.contains("```") || + text.contains("`") || + text.lines().any(|line| { + let trimmed = line.trim(); + trimmed.starts_with('#') || + trimmed.starts_with("- ") || + trimmed.starts_with("* ") || + trimmed.starts_with("+ ") || + (trimmed.len() > 2 && + trimmed.chars().next().map_or(false, |c| c.is_ascii_digit()) && + trimmed.chars().nth(1) == Some('.') && + trimmed.chars().nth(2) == Some(' ')) || + (trimmed.contains('[') && trimmed.contains("](")) + }) || + (text.matches('*').count() >= 2 && !text.contains("/*") && !text.contains("*/")) + } + pub fn print(&self, text: &str) { println!("{}", text); } + /// Smart print that automatically detects and renders markdown + pub fn print_smart(&self, text: &str) { + if self.has_markdown(text) { + self.print_markdown(text); + } else { + self.print(text); + } + } + pub fn print_markdown(&self, markdown: &str) { self.mad_skin.print_text(markdown); } @@ -64,3 +94,33 @@ impl SimpleOutput { println!(" {:.1}% | {}/{} tokens", percentage, used, total); } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_markdown_detection() { + let output = SimpleOutput::new(); + + // Should detect markdown + assert!(output.has_markdown("**bold text**")); + assert!(output.has_markdown("`code`")); + assert!(output.has_markdown("```\ncode block\n```")); + assert!(output.has_markdown("# Header")); + assert!(output.has_markdown("- list item")); + assert!(output.has_markdown("* list item")); + assert!(output.has_markdown("+ list item")); + assert!(output.has_markdown("1. numbered item")); + assert!(output.has_markdown("[link](url)")); + assert!(output.has_markdown("*italic* text")); + + // Should NOT detect markdown + assert!(!output.has_markdown("plain text")); + assert!(!output.has_markdown("file.txt")); + assert!(!output.has_markdown("/* comment */")); + assert!(!output.has_markdown("just one * asterisk")); + assert!(!output.has_markdown("📁 Workspace: /path/to/dir")); + assert!(!output.has_markdown("✅ Success message")); + } +} \ No newline at end of file diff --git a/crates/g3-cli/src/ui_writer_impl.rs b/crates/g3-cli/src/ui_writer_impl.rs index a041ce8..88755fc 100644 --- a/crates/g3-cli/src/ui_writer_impl.rs +++ b/crates/g3-cli/src/ui_writer_impl.rs @@ -98,8 +98,25 @@ impl UiWriter for ConsoleUiWriter { first_line.to_string() }; + // Add range information for read_file tool calls + let header_suffix = if tool_name == "read_file" { + // Check if start or end parameters are present + let has_start = args.iter().any(|(k, _)| k == "start"); + let has_end = args.iter().any(|(k, _)| k == "end"); + + if has_start || has_end { + let start_val = args.iter().find(|(k, _)| k == "start").map(|(_, v)| v.as_str()).unwrap_or("0"); + let end_val = args.iter().find(|(k, _)| k == "end").map(|(_, v)| v.as_str()).unwrap_or("end"); + format!(" [{}..{}]", start_val, end_val) + } else { + String::new() + } + } else { + String::new() + }; + // Print with bold green formatting using ANSI escape codes - println!("┌─\x1b[1;32m {} | {}\x1b[0m", tool_name, display_value); + println!("┌─\x1b[1;32m {} | {}{}\x1b[0m", tool_name, display_value, header_suffix); } else { // Print with bold green formatting using ANSI escape codes println!("┌─\x1b[1;32m {}\x1b[0m", tool_name); @@ -255,7 +272,18 @@ impl UiWriter for RetroTuiWriter { } else { value.to_string() }; - *caption = truncated; + + // Add range information for read_file tool calls + let tool_name = self.current_tool_name.lock().unwrap(); + let range_suffix = if tool_name.as_ref().map_or(false, |name| name == "read_file") { + // We need to check if start/end args will be provided - for now just check if this is a partial read + // This is a simplified approach since we're building the caption incrementally + String::new() // We'll handle this in print_tool_output_header instead + } else { + String::new() + }; + + *caption = format!("{}{}", truncated, range_suffix); } } @@ -263,7 +291,21 @@ impl UiWriter for RetroTuiWriter { // This is called right before tool execution starts // Send the initial tool header to the TUI now if let Some(tool_name) = self.current_tool_name.lock().unwrap().as_ref() { - let caption = self.current_tool_caption.lock().unwrap().clone(); + let mut caption = self.current_tool_caption.lock().unwrap().clone(); + + // Add range information for read_file tool calls + if tool_name == "read_file" { + // Check the tool output for start/end parameters + let output = self.current_tool_output.lock().unwrap(); + let has_start = output.iter().any(|line| line.starts_with("start:")); + let has_end = output.iter().any(|line| line.starts_with("end:")); + + if has_start || has_end { + let start_val = output.iter().find(|line| line.starts_with("start:")).map(|line| line.split(':').nth(1).unwrap_or("0").trim()).unwrap_or("0"); + let end_val = output.iter().find(|line| line.starts_with("end:")).map(|line| line.split(':').nth(1).unwrap_or("end").trim()).unwrap_or("end"); + caption = format!("{} [{}..{}]", caption, start_val, end_val); + } + } // Send the tool output with initial header self.tui.tool_output(tool_name, &caption, "");