Compact single-line tool output for file operations and shell
Implement compact display format for read_file, write_file, str_replace, and shell:
- read_file/write_file/str_replace: Single line with dimmed summary and timing
Format: ● tool_name | path [range] | summary | tokens ◉ time
- shell: Two-line format with command header and dimmed output
Format: ● shell | command
└─ output (N lines) | tokens ◉ time
Changes:
- Add print_tool_compact() method to UiWriter trait
- Add is_shell_compact state tracking in ConsoleUiWriter
- Add format_write_file_summary() and format_str_replace_summary() helpers
- Fix duplicate response output by checking if response is empty before printing
- Add finish_streaming_markdown() call before return to flush markdown buffer
This commit is contained in:
@@ -418,7 +418,10 @@ async fn execute_player_turn(
|
|||||||
{
|
{
|
||||||
Ok(result) => {
|
Ok(result) => {
|
||||||
output.print("📝 Player implementation completed:");
|
output.print("📝 Player implementation completed:");
|
||||||
output.print_smart(&result.response);
|
// Only print response if it's not empty (streaming already displayed it)
|
||||||
|
if !result.response.trim().is_empty() {
|
||||||
|
output.print_smart(&result.response);
|
||||||
|
}
|
||||||
return PlayerTurnResult::Success;
|
return PlayerTurnResult::Success;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|||||||
@@ -220,7 +220,10 @@ async fn run_console_mode(
|
|||||||
let result = agent
|
let result = agent
|
||||||
.execute_task_with_timing(&task, None, false, cli.show_prompt, cli.show_code, true, None)
|
.execute_task_with_timing(&task, None, false, cli.show_prompt, cli.show_code, true, None)
|
||||||
.await?;
|
.await?;
|
||||||
output.print_smart(&result.response);
|
// Only print response if it's not empty (streaming already displayed it)
|
||||||
|
if !result.response.trim().is_empty() {
|
||||||
|
output.print_smart(&result.response);
|
||||||
|
}
|
||||||
|
|
||||||
if let Err(e) = agent.send_auto_memory_reminder().await {
|
if let Err(e) = agent.send_auto_memory_reminder().await {
|
||||||
debug!("Auto-memory reminder failed: {}", e);
|
debug!("Auto-memory reminder failed: {}", e);
|
||||||
|
|||||||
@@ -49,7 +49,10 @@ pub async fn execute_task_with_retry<W: UiWriter>(
|
|||||||
if attempt > 1 {
|
if attempt > 1 {
|
||||||
output.print(&format!("✅ Request succeeded after {} attempts", attempt));
|
output.print(&format!("✅ Request succeeded after {} attempts", attempt));
|
||||||
}
|
}
|
||||||
output.print_smart(&result.response);
|
// Only print response if it's not empty (streaming already displayed it)
|
||||||
|
if !result.response.trim().is_empty() {
|
||||||
|
output.print_smart(&result.response);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ pub struct ConsoleUiWriter {
|
|||||||
current_output_line: std::sync::Mutex<Option<String>>,
|
current_output_line: std::sync::Mutex<Option<String>>,
|
||||||
output_line_printed: std::sync::Mutex<bool>,
|
output_line_printed: std::sync::Mutex<bool>,
|
||||||
is_agent_mode: std::sync::Mutex<bool>,
|
is_agent_mode: std::sync::Mutex<bool>,
|
||||||
|
/// Track if we're in shell compact mode (for appending timing to output line)
|
||||||
|
is_shell_compact: std::sync::Mutex<bool>,
|
||||||
/// Streaming markdown formatter for agent responses
|
/// Streaming markdown formatter for agent responses
|
||||||
markdown_formatter: Mutex<Option<StreamingMarkdownFormatter>>,
|
markdown_formatter: Mutex<Option<StreamingMarkdownFormatter>>,
|
||||||
}
|
}
|
||||||
@@ -24,6 +26,7 @@ impl ConsoleUiWriter {
|
|||||||
current_output_line: std::sync::Mutex::new(None),
|
current_output_line: std::sync::Mutex::new(None),
|
||||||
output_line_printed: std::sync::Mutex::new(false),
|
output_line_printed: std::sync::Mutex::new(false),
|
||||||
is_agent_mode: std::sync::Mutex::new(false),
|
is_agent_mode: std::sync::Mutex::new(false),
|
||||||
|
is_shell_compact: std::sync::Mutex::new(false),
|
||||||
markdown_formatter: Mutex::new(None),
|
markdown_formatter: Mutex::new(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,6 +119,8 @@ impl UiWriter for ConsoleUiWriter {
|
|||||||
// Reset output_line_printed at the start of a new tool output
|
// Reset output_line_printed at the start of a new tool output
|
||||||
// This ensures the header isn't cleared by update_tool_output_line
|
// This ensures the header isn't cleared by update_tool_output_line
|
||||||
*self.output_line_printed.lock().unwrap() = false;
|
*self.output_line_printed.lock().unwrap() = false;
|
||||||
|
// Reset shell compact mode
|
||||||
|
*self.is_shell_compact.lock().unwrap() = false;
|
||||||
// Now print the tool header with the most important arg
|
// Now print the tool header with the most important arg
|
||||||
// Use light gray/silver in agent mode, bold green otherwise
|
// Use light gray/silver in agent mode, bold green otherwise
|
||||||
let is_agent_mode = *self.is_agent_mode.lock().unwrap();
|
let is_agent_mode = *self.is_agent_mode.lock().unwrap();
|
||||||
@@ -173,6 +178,17 @@ impl UiWriter for ConsoleUiWriter {
|
|||||||
String::new()
|
String::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Check if this is a shell command - use compact format
|
||||||
|
if tool_name == "shell" {
|
||||||
|
*self.is_shell_compact.lock().unwrap() = true;
|
||||||
|
// Print compact shell header: "● shell | command"
|
||||||
|
println!(
|
||||||
|
" \x1b[2m●\x1b[0m {}{} \x1b[2m|\x1b[0m \x1b[35m{}\x1b[0m",
|
||||||
|
tool_color, tool_name, display_value
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Print with tool name in color (royal blue for agent mode, green otherwise)
|
// Print with tool name in color (royal blue for agent mode, green otherwise)
|
||||||
println!(
|
println!(
|
||||||
"┌─{} {}\x1b[0m\x1b[35m | {}{}\x1b[0m",
|
"┌─{} {}\x1b[0m\x1b[35m | {}{}\x1b[0m",
|
||||||
@@ -191,11 +207,17 @@ impl UiWriter for ConsoleUiWriter {
|
|||||||
const MAX_LINE_WIDTH: usize = 120;
|
const MAX_LINE_WIDTH: usize = 120;
|
||||||
let mut current_line = self.current_output_line.lock().unwrap();
|
let mut current_line = self.current_output_line.lock().unwrap();
|
||||||
let mut line_printed = self.output_line_printed.lock().unwrap();
|
let mut line_printed = self.output_line_printed.lock().unwrap();
|
||||||
|
let is_shell = *self.is_shell_compact.lock().unwrap();
|
||||||
|
|
||||||
// If we've already printed a line, clear it first
|
// If we've already printed a line, clear it first
|
||||||
if *line_printed {
|
if *line_printed {
|
||||||
// Move cursor up one line and clear it
|
if is_shell {
|
||||||
print!("\x1b[1A\x1b[2K");
|
// For shell, we printed without newline, so just clear the line
|
||||||
|
print!("\r\x1b[2K");
|
||||||
|
} else {
|
||||||
|
// Move cursor up one line and clear it
|
||||||
|
print!("\x1b[1A\x1b[2K");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Truncate line if needed to prevent wrapping
|
// Truncate line if needed to prevent wrapping
|
||||||
@@ -206,7 +228,13 @@ impl UiWriter for ConsoleUiWriter {
|
|||||||
line.to_string()
|
line.to_string()
|
||||||
};
|
};
|
||||||
|
|
||||||
println!("│ \x1b[2m{}\x1b[0m", display_line);
|
// Use different prefix for shell (└─) vs other tools (│)
|
||||||
|
if is_shell {
|
||||||
|
// For shell, print without newline so timing can be appended
|
||||||
|
print!(" \x1b[2m└─ {}\x1b[0m", display_line);
|
||||||
|
} else {
|
||||||
|
println!("│ \x1b[2m{}\x1b[0m", display_line);
|
||||||
|
}
|
||||||
let _ = io::stdout().flush();
|
let _ = io::stdout().flush();
|
||||||
|
|
||||||
// Update state
|
// Update state
|
||||||
@@ -223,11 +251,96 @@ impl UiWriter for ConsoleUiWriter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn print_tool_output_summary(&self, count: usize) {
|
fn print_tool_output_summary(&self, count: usize) {
|
||||||
|
let is_shell = *self.is_shell_compact.lock().unwrap();
|
||||||
|
if is_shell {
|
||||||
|
// For shell, append to the same line (no newline)
|
||||||
|
print!(" \x1b[2m({} line{})\x1b[0m", count, if count == 1 { "" } else { "s" });
|
||||||
|
let _ = io::stdout().flush();
|
||||||
|
} else {
|
||||||
|
println!(
|
||||||
|
"│ \x1b[2m({} line{})\x1b[0m",
|
||||||
|
count,
|
||||||
|
if count == 1 { "" } else { "s" }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_tool_compact(&self, tool_name: &str, summary: &str, duration_str: &str, tokens_delta: u32, _context_percentage: f32) -> bool {
|
||||||
|
// Only handle file operation tools in compact format
|
||||||
|
let is_compact_tool = matches!(tool_name, "read_file" | "write_file" | "str_replace");
|
||||||
|
if !is_compact_tool {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let args = self.current_tool_args.lock().unwrap();
|
||||||
|
let is_agent_mode = *self.is_agent_mode.lock().unwrap();
|
||||||
|
|
||||||
|
// Get file path
|
||||||
|
let file_path = args
|
||||||
|
.iter()
|
||||||
|
.find(|(k, _)| k == "file_path")
|
||||||
|
.map(|(_, v)| v.as_str())
|
||||||
|
.unwrap_or("?");
|
||||||
|
|
||||||
|
// Truncate long paths
|
||||||
|
let display_path = if file_path.len() > 60 {
|
||||||
|
let truncate_at = file_path
|
||||||
|
.char_indices()
|
||||||
|
.nth(57)
|
||||||
|
.map(|(i, _)| i)
|
||||||
|
.unwrap_or(file_path.len());
|
||||||
|
format!("{}...", &file_path[..truncate_at])
|
||||||
|
} else {
|
||||||
|
file_path.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build range suffix for read_file
|
||||||
|
let range_suffix = if tool_name == "read_file" {
|
||||||
|
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()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Color for tool name
|
||||||
|
let tool_color = if is_agent_mode { "\x1b[38;5;250m" } else { "\x1b[32m" };
|
||||||
|
|
||||||
|
// Print compact single line:
|
||||||
|
// " ● read_file | path [range] | summary | tokens ◉ time"
|
||||||
println!(
|
println!(
|
||||||
"│ \x1b[2m({} line{})\x1b[0m",
|
" \x1b[2m●\x1b[0m {}{} \x1b[2m|\x1b[0m \x1b[35m{}{}\x1b[0m \x1b[2m| {}\x1b[0m \x1b[2m| {} ◉ {}\x1b[0m",
|
||||||
count,
|
tool_color,
|
||||||
if count == 1 { "" } else { "s" }
|
tool_name,
|
||||||
|
display_path,
|
||||||
|
range_suffix,
|
||||||
|
summary,
|
||||||
|
tokens_delta,
|
||||||
|
duration_str
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Clear the stored tool info
|
||||||
|
drop(args); // Release the lock before clearing
|
||||||
|
*self.current_tool_name.lock().unwrap() = None;
|
||||||
|
self.current_tool_args.lock().unwrap().clear();
|
||||||
|
*self.current_output_line.lock().unwrap() = None;
|
||||||
|
*self.output_line_printed.lock().unwrap() = false;
|
||||||
|
|
||||||
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_tool_timing(&self, duration_str: &str, tokens_delta: u32, context_percentage: f32) {
|
fn print_tool_timing(&self, duration_str: &str, tokens_delta: u32, context_percentage: f32) {
|
||||||
@@ -278,13 +391,24 @@ impl UiWriter for ConsoleUiWriter {
|
|||||||
println!();
|
println!();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
println!("└─ ⚡️ {}{}\x1b[0m \x1b[2m{} ◉ | {:.0}%\x1b[0m", color_code, duration_str, tokens_delta, context_percentage);
|
|
||||||
println!();
|
// Check if we're in shell compact mode - append timing to the output line
|
||||||
|
let is_shell = *self.is_shell_compact.lock().unwrap();
|
||||||
|
if is_shell {
|
||||||
|
// Append timing to the same line as shell output
|
||||||
|
println!(" \x1b[2m| {} ◉ {}{}\x1b[0m", tokens_delta, color_code, duration_str);
|
||||||
|
println!();
|
||||||
|
} else {
|
||||||
|
println!("└─ ⚡️ {}{}\x1b[0m \x1b[2m{} ◉ | {:.0}%\x1b[0m", color_code, duration_str, tokens_delta, context_percentage);
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
// Clear the stored tool info
|
// Clear the stored tool info
|
||||||
*self.current_tool_name.lock().unwrap() = None;
|
*self.current_tool_name.lock().unwrap() = None;
|
||||||
self.current_tool_args.lock().unwrap().clear();
|
self.current_tool_args.lock().unwrap().clear();
|
||||||
*self.current_output_line.lock().unwrap() = None;
|
*self.current_output_line.lock().unwrap() = None;
|
||||||
*self.output_line_printed.lock().unwrap() = false;
|
*self.output_line_printed.lock().unwrap() = false;
|
||||||
|
*self.is_shell_compact.lock().unwrap() = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_agent_prompt(&self) {
|
fn print_agent_prompt(&self) {
|
||||||
|
|||||||
@@ -2142,7 +2142,14 @@ impl<W: UiWriter> Agent<W> {
|
|||||||
self.ui_writer.print_tool_arg(key, &value_str);
|
self.ui_writer.print_tool_arg(key, &value_str);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.ui_writer.print_tool_output_header();
|
|
||||||
|
// 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");
|
||||||
|
|
||||||
|
// Only print output header for non-compact tools
|
||||||
|
if !is_compact_tool {
|
||||||
|
self.ui_writer.print_tool_output_header();
|
||||||
|
}
|
||||||
|
|
||||||
// Clone working_dir to avoid borrow checker issues
|
// Clone working_dir to avoid borrow checker issues
|
||||||
let working_dir = self.working_dir.clone();
|
let working_dir = self.working_dir.clone();
|
||||||
@@ -2172,7 +2179,7 @@ impl<W: UiWriter> Agent<W> {
|
|||||||
));
|
));
|
||||||
|
|
||||||
// Display tool execution result with proper indentation
|
// Display tool execution result with proper indentation
|
||||||
{
|
let compact_summary = {
|
||||||
let output_lines: Vec<&str> = tool_result.lines().collect();
|
let output_lines: Vec<&str> = tool_result.lines().collect();
|
||||||
|
|
||||||
// Check if UI wants full output (machine mode) or truncated (human mode)
|
// Check if UI wants full output (machine mode) or truncated (human mode)
|
||||||
@@ -2186,14 +2193,26 @@ impl<W: UiWriter> Agent<W> {
|
|||||||
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";
|
||||||
|
|
||||||
// For read_file, show a summary instead of file contents
|
if is_compact_tool && tool_success {
|
||||||
let is_read_file = tool_call.tool == "read_file";
|
// Generate appropriate summary based on tool type
|
||||||
|
match tool_call.tool.as_str() {
|
||||||
if is_read_file && tool_success {
|
"read_file" => Some(streaming::format_read_file_summary(output_len, tool_result.len())),
|
||||||
let summary = streaming::format_read_file_summary(output_len, tool_result.len());
|
"write_file" => {
|
||||||
self.ui_writer.update_tool_output_line(&summary);
|
// Parse the result to get line/char counts
|
||||||
|
// Result format: "✅ +N insertions | -M deletions" or similar
|
||||||
|
Some(streaming::format_write_file_summary(output_len, tool_result.len()))
|
||||||
|
}
|
||||||
|
"str_replace" => {
|
||||||
|
// Parse insertions/deletions from result
|
||||||
|
// Result format: "✅ +N insertions | -M deletions"
|
||||||
|
let (ins, del) = parse_diff_stats(&tool_result);
|
||||||
|
Some(streaming::format_str_replace_summary(ins, del))
|
||||||
|
}
|
||||||
|
_ => Some(streaming::format_read_file_summary(output_len, tool_result.len()))
|
||||||
|
}
|
||||||
} else if is_todo_tool {
|
} else if is_todo_tool {
|
||||||
// Skip - todo tools print their own content
|
// Skip - todo tools print their own content
|
||||||
|
None
|
||||||
} else {
|
} else {
|
||||||
let max_lines_to_show = if wants_full { output_len } else { MAX_LINES };
|
let max_lines_to_show = if wants_full { output_len } else { MAX_LINES };
|
||||||
|
|
||||||
@@ -2208,8 +2227,9 @@ impl<W: UiWriter> Agent<W> {
|
|||||||
if !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);
|
||||||
}
|
}
|
||||||
|
None
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
// Add the tool call and result to the context window using RAW unfiltered content
|
// Add the tool call and result to the context window using RAW unfiltered content
|
||||||
// This ensures the log file contains the true raw content including JSON tool calls
|
// This ensures the log file contains the true raw content including JSON tool calls
|
||||||
@@ -2272,10 +2292,22 @@ impl<W: UiWriter> Agent<W> {
|
|||||||
|
|
||||||
// Closure marker with timing
|
// Closure marker with timing
|
||||||
let tokens_delta = self.context_window.used_tokens.saturating_sub(tokens_before);
|
let tokens_delta = self.context_window.used_tokens.saturating_sub(tokens_before);
|
||||||
self.ui_writer
|
|
||||||
.print_tool_timing(&streaming::format_duration(exec_duration),
|
// 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,
|
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();
|
self.ui_writer.print_agent_prompt();
|
||||||
|
|
||||||
// Update the request with the new context for next iteration
|
// Update the request with the new context for next iteration
|
||||||
@@ -2751,6 +2783,9 @@ impl<W: UiWriter> Agent<W> {
|
|||||||
full_response
|
full_response
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Finish streaming markdown before returning
|
||||||
|
self.ui_writer.finish_streaming_markdown();
|
||||||
|
|
||||||
// Dehydrate context - the function extracts the summary from context itself
|
// Dehydrate context - the function extracts the summary from context itself
|
||||||
self.dehydrate_context();
|
self.dehydrate_context();
|
||||||
|
|
||||||
@@ -2863,6 +2898,25 @@ impl<W: UiWriter> Agent<W> {
|
|||||||
pub use utils::apply_unified_diff_to_string;
|
pub use utils::apply_unified_diff_to_string;
|
||||||
use utils::truncate_to_word_boundary;
|
use utils::truncate_to_word_boundary;
|
||||||
|
|
||||||
|
/// Parse insertions and deletions from a str_replace result.
|
||||||
|
/// Result format: "✅ +N insertions | -M deletions"
|
||||||
|
fn parse_diff_stats(result: &str) -> (i32, i32) {
|
||||||
|
let mut insertions = 0i32;
|
||||||
|
let mut deletions = 0i32;
|
||||||
|
|
||||||
|
// Look for "+N insertions" pattern
|
||||||
|
if let Some(pos) = result.find("+") {
|
||||||
|
let after_plus = &result[pos + 1..];
|
||||||
|
insertions = after_plus.split_whitespace().next().and_then(|s| s.parse().ok()).unwrap_or(0);
|
||||||
|
}
|
||||||
|
// Look for "-M deletions" pattern
|
||||||
|
if let Some(pos) = result.find("-") {
|
||||||
|
let after_minus = &result[pos + 1..];
|
||||||
|
deletions = after_minus.split_whitespace().next().and_then(|s| s.parse().ok()).unwrap_or(0);
|
||||||
|
}
|
||||||
|
(insertions, deletions)
|
||||||
|
}
|
||||||
|
|
||||||
// Implement Drop to clean up safaridriver process
|
// Implement Drop to clean up safaridriver process
|
||||||
impl<W: UiWriter> Drop for Agent<W> {
|
impl<W: UiWriter> Drop for Agent<W> {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
|
|||||||
@@ -289,7 +289,28 @@ pub fn format_read_file_summary(line_count: usize, char_count: usize) -> String
|
|||||||
} else {
|
} else {
|
||||||
format!("{}", char_count)
|
format!("{}", char_count)
|
||||||
};
|
};
|
||||||
format!("🔍 {} lines read ({} chars)", line_count, char_display)
|
format!("{} lines ({} chars)", line_count, char_display)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format a write_file result summary.
|
||||||
|
pub fn format_write_file_summary(line_count: usize, char_count: usize) -> String {
|
||||||
|
let char_display = if char_count >= 1000 {
|
||||||
|
format!("{:.1}k", char_count as f64 / 1000.0)
|
||||||
|
} else {
|
||||||
|
format!("{}", char_count)
|
||||||
|
};
|
||||||
|
format!("✏️ {} lines ({} chars)", line_count, char_display)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format a str_replace result summary.
|
||||||
|
pub fn format_str_replace_summary(insertions: i32, deletions: i32) -> String {
|
||||||
|
if insertions > 0 && deletions > 0 {
|
||||||
|
format!("\x1b[32m+{}\x1b[0m \x1b[2m|\x1b[0m \x1b[31m-{}\x1b[0m", insertions, deletions)
|
||||||
|
} else if insertions > 0 {
|
||||||
|
format!("\x1b[32m+{}\x1b[0m", insertions)
|
||||||
|
} else {
|
||||||
|
format!("\x1b[31m-{}\x1b[0m", deletions)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Determine if a response is essentially empty (whitespace or timing only)
|
/// Determine if a response is essentially empty (whitespace or timing only)
|
||||||
@@ -367,8 +388,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_format_read_file_summary() {
|
fn test_format_read_file_summary() {
|
||||||
assert_eq!(format_read_file_summary(42, 500), "🔍 42 lines read (500 chars)");
|
assert_eq!(format_read_file_summary(42, 500), "42 lines (500 chars)");
|
||||||
assert_eq!(format_read_file_summary(100, 1500), "🔍 100 lines read (1.5k chars)");
|
assert_eq!(format_read_file_summary(100, 1500), "100 lines (1.5k chars)");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -38,6 +38,14 @@ pub trait UiWriter: Send + Sync {
|
|||||||
/// Print tool output summary (when output is truncated)
|
/// Print tool output summary (when output is truncated)
|
||||||
fn print_tool_output_summary(&self, hidden_count: usize);
|
fn print_tool_output_summary(&self, hidden_count: usize);
|
||||||
|
|
||||||
|
/// Print a compact single-line tool output (for file operations)
|
||||||
|
/// Format: " ● tool_name | path [range] | summary | tokens ◉ time"
|
||||||
|
/// Returns true if the tool was handled in compact format, false to use normal format
|
||||||
|
fn print_tool_compact(&self, _tool_name: &str, _summary: &str, _duration_str: &str, _tokens_delta: u32, _context_percentage: f32) -> bool {
|
||||||
|
// Default: don't use compact format
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
/// Print tool execution timing
|
/// Print tool execution timing
|
||||||
fn print_tool_timing(&self, duration_str: &str, tokens_delta: u32, context_percentage: f32);
|
fn print_tool_timing(&self, duration_str: &str, tokens_delta: u32, context_percentage: f32);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user