use similar::TextDiff;
use crate::cm_tools::tool_result::{
WorkspaceWriteDiffFields, WorkspaceWriteDiffFile, prepend_crabmate_tool_output,
};
pub const MAX_UNIFIED_PREVIEW_FILE_CHARS: usize = 6000;
pub const WORKSPACE_WRITE_DIFF_BUDGET_CHARS: usize = 24 * 1024;
#[derive(Debug, Clone)]
pub struct WriteDiffFileState {
pub rel_path: String,
pub before: Option<String>,
pub after: Option<String>,
}
fn one_unified_diff(rel_path: &str, before: Option<&str>, after: Option<&str>) -> (String, bool) {
let left = before.unwrap_or("");
let right = after.unwrap_or("");
let diff = TextDiff::from_lines(left, right);
let unified = diff
.unified_diff()
.context_radius(3)
.header(&format!("a/{rel_path}"), &format!("b/{rel_path}"))
.to_string();
let n = unified.chars().count();
if n <= MAX_UNIFIED_PREVIEW_FILE_CHARS {
return (unified, false);
}
let truncated: String = unified
.chars()
.take(MAX_UNIFIED_PREVIEW_FILE_CHARS)
.collect();
(format!("{truncated}…\n(该文件 diff 已截断)"), true)
}
pub fn format_tool_output_with_write_diff_preview(
tool_name: &str,
body: String,
files: Vec<WriteDiffFileState>,
budget_chars: usize,
) -> String {
if files.is_empty() {
return body;
}
let mut remaining = budget_chars;
let mut out_files: Vec<WorkspaceWriteDiffFile> = Vec::new();
let mut preview_truncated = false;
for f in files {
let rel = f.rel_path.trim();
if rel.is_empty() {
continue;
}
if remaining < 80 {
preview_truncated = true;
break;
}
let (udiff, trunc_file) = one_unified_diff(rel, f.before.as_deref(), f.after.as_deref());
let cost = udiff.chars().count();
if cost > remaining {
preview_truncated = true;
let take = remaining.saturating_sub(80);
let partial: String = udiff.chars().take(take).collect();
out_files.push(WorkspaceWriteDiffFile {
path: rel.to_string(),
unified_diff: format!("{partial}…\n(已达本轮预览体积上限)"),
truncated: true,
});
break;
}
remaining = remaining.saturating_sub(cost);
out_files.push(WorkspaceWriteDiffFile {
path: rel.to_string(),
unified_diff: udiff,
truncated: trunc_file,
});
}
if out_files.is_empty() {
return body;
}
prepend_crabmate_tool_output(
tool_name,
WorkspaceWriteDiffFields::new(out_files, preview_truncated),
&body,
)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::Value;
#[test]
fn prepends_header_and_diff() {
let s = format_tool_output_with_write_diff_preview(
"create_file",
"路径:a\nok".to_string(),
vec![WriteDiffFileState {
rel_path: "a.rs".to_string(),
before: None,
after: Some("x\n".to_string()),
}],
WORKSPACE_WRITE_DIFF_BUDGET_CHARS,
);
let first = s.lines().next().unwrap();
let v: Value = serde_json::from_str(first).unwrap();
assert_eq!(v["preview"], "workspace_write_diff");
assert_eq!(v["tool"], "create_file");
assert!(s.contains("路径:a"));
assert!(first.contains("crabmate_tool_output"));
}
}