Remove ANSI formatting codes from g3-core

Move terminal formatting responsibility to g3-cli layer:

- format_str_replace_summary(): Remove ANSI codes, add colorize_str_replace_summary()
  helper in CLI to apply green/red colors for insertions/deletions
- format_timing_footer(): Remove dimming ANSI codes (now plain text)
- str_replace tool result: Remove ANSI codes from success message

Remaining acceptable ANSI usage in g3-core:
- iTerm2 inline image protocol (terminal-specific escape sequence)
- Image metadata dimming (direct print, would need larger refactor)
- Terminal beep for stale TODO warning (audio, not visual)
- ANSI stripping utility in research.rs (not output)

This continues the separation of concerns: g3-core handles logic,
g3-cli handles all terminal formatting.
This commit is contained in:
Dhanji R. Prasanna
2026-01-20 10:00:37 +05:30
parent 182f5f98fe
commit 10bce7f66f
3 changed files with 32 additions and 9 deletions

View File

@@ -15,6 +15,22 @@ mod ansi {
pub const RED: &str = "\x1b[31m"; pub const RED: &str = "\x1b[31m";
} }
/// Colorize a str_replace summary (e.g., "+5 | -3" -> green "+5" | red "-3")
fn colorize_str_replace_summary(summary: &str) -> String {
// Parse patterns like "+5 | -3", "+5", "-3"
if summary.contains(" | ") {
let parts: Vec<&str> = summary.split(" | ").collect();
if parts.len() == 2 {
return format!("\x1b[32m{}\x1b[0m \x1b[2m|\x1b[0m \x1b[31m{}\x1b[0m", parts[0], parts[1]);
}
} else if summary.starts_with('+') {
return format!("\x1b[32m{}\x1b[0m", summary);
} else if summary.starts_with('-') {
return format!("\x1b[31m{}\x1b[0m", summary);
}
summary.to_string()
}
/// ANSI color codes for tool names /// ANSI color codes for tool names
const TOOL_COLOR_NORMAL: &str = "\x1b[32m"; const TOOL_COLOR_NORMAL: &str = "\x1b[32m";
const TOOL_COLOR_NORMAL_BOLD: &str = "\x1b[1;32m"; const TOOL_COLOR_NORMAL_BOLD: &str = "\x1b[1;32m";
@@ -523,6 +539,13 @@ impl UiWriter for ConsoleUiWriter {
// Color for tool name // Color for tool name
let tool_color = if is_agent_mode { TOOL_COLOR_AGENT } else { TOOL_COLOR_NORMAL }; let tool_color = if is_agent_mode { TOOL_COLOR_AGENT } else { TOOL_COLOR_NORMAL };
// Colorize summary for str_replace (green insertions, red deletions)
let display_summary = if tool_name == "str_replace" {
colorize_str_replace_summary(summary)
} else {
summary.to_string()
};
// Print compact single line // Print compact single line
if is_continuation { if is_continuation {
// Continuation line for consecutive read_file on same file: // Continuation line for consecutive read_file on same file:
@@ -530,7 +553,7 @@ impl UiWriter for ConsoleUiWriter {
println!( println!(
" \x1b[2m└─ reading further\x1b[0m\x1b[35m{}\x1b[0m \x1b[2m| {}\x1b[0m \x1b[2m| {}{}\x1b[0m", " \x1b[2m└─ reading further\x1b[0m\x1b[35m{}\x1b[0m \x1b[2m| {}\x1b[0m \x1b[2m| {}{}\x1b[0m",
range_suffix, range_suffix,
summary, display_summary,
tokens_delta, tokens_delta,
duration_str duration_str
); );
@@ -539,14 +562,14 @@ impl UiWriter for ConsoleUiWriter {
// Pad to align with longest compact tool (str_replace = 11 chars) // Pad to align with longest compact tool (str_replace = 11 chars)
println!( println!(
" \x1b[2m●\x1b[0m {}{:<11}\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 tool_color, tool_name, display_summary, tokens_delta, duration_str
); );
} else { } else {
// Tools with file path: " ● tool_name | path [range] | summary | tokens ◉ time" // Tools with file path: " ● tool_name | path [range] | summary | tokens ◉ time"
// Pad to align with longest compact tool (str_replace = 11 chars) // Pad to align with longest compact tool (str_replace = 11 chars)
println!( println!(
" \x1b[2m●\x1b[0m {}{:<11}\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 tool_color, tool_name, display_arg, range_suffix, display_summary, tokens_delta, duration_str
); );
} }

View File

@@ -142,11 +142,11 @@ pub fn format_timing_footer(
// Add token usage info if available (dimmed) // Add token usage info if available (dimmed)
if let Some(tokens) = turn_tokens { if let Some(tokens) = turn_tokens {
format!( format!(
"{} \x1b[2m{} ◉ | {:.0}%\x1b[0m", "{} {} ◉ | {:.0}%",
timing, tokens, context_percentage timing, tokens, context_percentage
) )
} else { } else {
format!("{} \x1b[2m{:.0}%\x1b[0m", timing, context_percentage) format!("{} {:.0}%", timing, context_percentage)
} }
} }
@@ -326,11 +326,11 @@ pub fn format_write_file_result(tool_result: &str) -> String {
/// Format a str_replace result summary. /// Format a str_replace result summary.
pub fn format_str_replace_summary(insertions: i32, deletions: i32) -> String { pub fn format_str_replace_summary(insertions: i32, deletions: i32) -> String {
if insertions > 0 && deletions > 0 { if insertions > 0 && deletions > 0 {
format!("\x1b[32m+{}\x1b[0m \x1b[2m|\x1b[0m \x1b[31m-{}\x1b[0m", insertions, deletions) format!("+{} | -{}", insertions, deletions)
} else if insertions > 0 { } else if insertions > 0 {
format!("\x1b[32m+{}\x1b[0m", insertions) format!("+{}", insertions)
} else { } else {
format!("\x1b[31m-{}\x1b[0m", deletions) format!("-{}", deletions)
} }
} }

View File

@@ -582,7 +582,7 @@ pub async fn execute_str_replace<W: UiWriter>(
// Write the result back to the file // Write the result back to the file
match std::fs::write(&file_path, &result) { match std::fs::write(&file_path, &result) {
Ok(()) => Ok(format!("\x1b[32m+{} insertions\x1b[0m | \x1b[31m-{} deletions\x1b[0m", insertions, deletions)), Ok(()) => Ok(format!("✅ +{} insertions | -{} deletions", insertions, deletions)),
Err(e) => Ok(format!("❌ Failed to write to file '{}': {}", file_path, e)), Err(e) => Ok(format!("❌ Failed to write to file '{}': {}", file_path, e)),
} }
} }