//! Formatting functions for tool-specific display in the TUI. //! //! Each tool type has a structured [`ToolDisplay`] variant carrying the data //! needed for rich rendering. These functions turn that data into the //! user-facing strings shown in the conversation pane. use similar::{ChangeTag, TextDiff}; use crate::core::types::ToolDisplay; /// Format a tool that is currently executing (or awaiting approval). pub fn format_executing(name: &str, display: &ToolDisplay) -> String { match display { ToolDisplay::WriteFile { path, old_content, new_content, } => { let mut out = format!("write {path}\n"); out.push_str(&unified_diff( old_content.as_deref().unwrap_or(""), new_content, )); out } ToolDisplay::ShellExec { command } => format!("$ {command}"), ToolDisplay::ListDirectory { path } => format!("ls {path}"), ToolDisplay::ReadFile { path } => format!("read {path}"), ToolDisplay::Generic { summary } => format!("[{name}] {summary}"), } } /// Format a tool result after execution completes. pub fn format_result(name: &str, display: &ToolDisplay, is_error: bool) -> String { let prefix = if is_error { "error" } else { "result" }; match display { ToolDisplay::WriteFile { path, old_content, new_content, } => { let mut out = format!("write {path}\n"); out.push_str(&unified_diff( old_content.as_deref().unwrap_or(""), new_content, )); out.push_str(&format!("\nWrote {} bytes", new_content.len())); out } ToolDisplay::ShellExec { command } => { // For results, the command field carries "command\nstdout\nstderr". format!("$ {command}") } ToolDisplay::ListDirectory { path } => format!("ls {path}"), ToolDisplay::ReadFile { path } => format!("read {path}"), ToolDisplay::Generic { summary } => format!("[{name} {prefix}] {summary}"), } } /// Produce a unified diff between `old` and `new` text. /// /// Uses the `similar` crate to compute line-level changes and formats them /// with the conventional `+`/`-`/` ` prefix markers. fn unified_diff(old: &str, new: &str) -> String { let diff = TextDiff::from_lines(old, new); let mut out = String::new(); for change in diff.iter_all_changes() { let marker = match change.tag() { ChangeTag::Delete => "-", ChangeTag::Insert => "+", ChangeTag::Equal => " ", }; out.push_str(marker); out.push_str(change.as_str().unwrap_or("")); if !change.as_str().unwrap_or("").ends_with('\n') { out.push('\n'); } } out } #[cfg(test)] mod tests { use super::*; #[test] fn format_executing_shell() { let display = ToolDisplay::ShellExec { command: "cargo test".to_string(), }; assert_eq!(format_executing("shell_exec", &display), "$ cargo test"); } #[test] fn format_executing_write_with_diff() { let display = ToolDisplay::WriteFile { path: "src/lib.rs".to_string(), old_content: Some("fn hello() {\n println!(\"hello\");\n}\n".to_string()), new_content: "fn hello() {\n println!(\"hello world\");\n}\n".to_string(), }; let output = format_executing("write_file", &display); assert!(output.starts_with("write src/lib.rs\n")); assert!(output.contains("- println!(\"hello\");")); assert!(output.contains("+ println!(\"hello world\");")); } #[test] fn format_result_write_shows_byte_count() { let display = ToolDisplay::WriteFile { path: "foo.txt".to_string(), old_content: None, new_content: "hello".to_string(), }; let output = format_result("write_file", &display, false); assert!(output.contains("Wrote 5 bytes")); } #[test] fn format_result_generic_error() { let display = ToolDisplay::Generic { summary: "something failed".to_string(), }; let output = format_result("unknown_tool", &display, true); assert_eq!(output, "[unknown_tool error] something failed"); } #[test] fn unified_diff_empty_old() { let diff = unified_diff("", "line1\nline2\n"); assert!(diff.contains("+line1")); assert!(diff.contains("+line2")); } }