Display diff

This commit is contained in:
Drew 2026-02-25 23:30:52 -08:00
parent af080710cc
commit 4336dc7b3c
9 changed files with 481 additions and 114 deletions

133
src/tui/tool_display.rs Normal file
View file

@ -0,0 +1,133 @@
//! 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"));
}
}