use serde_json::Value;
use super::{DiffHunk, DiffLine, DiffLineKind};
pub fn preview_args(arguments: &str) -> String {
let parsed: Result<Value, _> = serde_json::from_str(arguments);
let Ok(Value::Object(map)) = parsed else {
return clamp(arguments, 60);
};
let mut parts = Vec::new();
for (k, v) in map.iter().take(2) {
let v_str = match v {
Value::String(s) => format!("\"{}\"", clamp(s, 30)),
other => clamp(&other.to_string(), 30),
};
parts.push(format!("{k}={v_str}"));
}
clamp(&parts.join(" "), 60)
}
fn clamp(s: &str, max: usize) -> String {
if s.chars().count() <= max {
s.to_string()
} else {
let head: String = s.chars().take(max.saturating_sub(1)).collect();
format!("{head}…")
}
}
pub fn verb_for_tool(name: &str) -> &'static str {
match name {
"read_file" | "list_dir" | "search_files" => "Reading",
"apply_patch" | "write_file" => "Editing",
"run_shell" => "Running",
_ => "Calling tool",
}
}
pub fn parse_apply_patch_input(arguments: &str) -> Option<(String, Vec<DiffHunk>)> {
let v: Value = serde_json::from_str(arguments).ok()?;
let input = v.get("input")?.as_str()?;
parse_v4a_patch(input)
}
pub fn parse_v4a_patch(input: &str) -> Option<(String, Vec<DiffHunk>)> {
let mut path: Option<String> = None;
let mut current = Vec::new();
let mut hunks: Vec<DiffHunk> = Vec::new();
for line in input.lines() {
if let Some(rest) = line
.strip_prefix("*** Update File: ")
.or_else(|| line.strip_prefix("*** Add File: "))
{
if path.is_some() {
break;
}
path = Some(rest.trim().to_string());
continue;
}
if line.starts_with("*** Begin Patch")
|| line.starts_with("*** End Patch")
|| line.starts_with("*** End of File")
{
continue;
}
if path.is_none() {
continue;
}
if let Some(stripped) = line.strip_prefix("@@") {
if !current.is_empty() {
hunks.push(DiffHunk {
lines: std::mem::take(&mut current),
});
}
let text = stripped.trim_start().to_string();
if !text.is_empty() {
current.push(DiffLine {
kind: DiffLineKind::Context,
text,
});
}
continue;
}
if let Some(rest) = line.strip_prefix('+') {
current.push(DiffLine {
kind: DiffLineKind::Add,
text: rest.to_string(),
});
} else if let Some(rest) = line.strip_prefix('-') {
current.push(DiffLine {
kind: DiffLineKind::Remove,
text: rest.to_string(),
});
} else if let Some(rest) = line.strip_prefix(' ') {
current.push(DiffLine {
kind: DiffLineKind::Context,
text: rest.to_string(),
});
}
}
if !current.is_empty() {
hunks.push(DiffHunk { lines: current });
}
let path = path?;
if hunks.is_empty() {
return None;
}
Some((path, hunks))
}
pub(crate) fn extract_write_file_path_from_result(output: &str) -> Option<String> {
let trimmed = output.trim();
if let Some(idx) = trimmed.rfind(" to ") {
let candidate = &trimmed[idx + 4..];
if !candidate.is_empty() {
return Some(candidate.to_string());
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tui::app::DiffLineKind;
#[test]
fn tool_call_args_preview_extracts_path() {
let preview = preview_args(r#"{"path":"src/agent.rs"}"#);
assert!(preview.contains("path"));
assert!(preview.contains("src/agent.rs"));
}
#[test]
fn verb_for_tool_categorises_tools() {
assert_eq!(verb_for_tool("read_file"), "Reading");
assert_eq!(verb_for_tool("apply_patch"), "Editing");
assert_eq!(verb_for_tool("run_shell"), "Running");
assert_eq!(verb_for_tool("custom_xyz"), "Calling tool");
}
#[test]
fn parse_v4a_patch_extracts_path_and_pm_lines() {
let patch = "*** Begin Patch\n*** Update File: src/foo.rs\n@@ pub fn bar()\n pub fn bar() {\n- let x = 1;\n+ let x = 2;\n }\n*** End Patch";
let (path, hunks) = parse_v4a_patch(patch).unwrap();
assert_eq!(path, "src/foo.rs");
assert!(!hunks.is_empty());
let kinds: Vec<_> = hunks
.iter()
.flat_map(|h| h.lines.iter().map(|l| l.kind.clone()))
.collect();
assert!(kinds.contains(&DiffLineKind::Add));
assert!(kinds.contains(&DiffLineKind::Remove));
}
}