133 lines
4.5 KiB
Rust
133 lines
4.5 KiB
Rust
//! 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"));
|
|
}
|
|
}
|