recursive/tui/app/
render.rs1use serde_json::Value;
7
8use super::{DiffHunk, DiffLine, DiffLineKind};
9
10pub fn preview_args(arguments: &str) -> String {
17 let parsed: Result<Value, _> = serde_json::from_str(arguments);
18 let Ok(Value::Object(map)) = parsed else {
19 return clamp(arguments, 60);
21 };
22
23 let mut parts = Vec::new();
24 for (k, v) in map.iter().take(2) {
25 let v_str = match v {
26 Value::String(s) => format!("\"{}\"", clamp(s, 30)),
27 other => clamp(&other.to_string(), 30),
28 };
29 parts.push(format!("{k}={v_str}"));
30 }
31 clamp(&parts.join(" "), 60)
32}
33
34fn clamp(s: &str, max: usize) -> String {
35 if s.chars().count() <= max {
36 s.to_string()
37 } else {
38 let head: String = s.chars().take(max.saturating_sub(1)).collect();
39 format!("{head}…")
40 }
41}
42
43pub fn verb_for_tool(name: &str) -> &'static str {
47 match name {
48 "read_file" | "list_dir" | "search_files" => "Reading",
49 "apply_patch" | "write_file" => "Editing",
50 "run_shell" => "Running",
51 _ => "Calling tool",
52 }
53}
54
55pub fn parse_apply_patch_input(arguments: &str) -> Option<(String, Vec<DiffHunk>)> {
63 let v: Value = serde_json::from_str(arguments).ok()?;
64 let input = v.get("input")?.as_str()?;
65 parse_v4a_patch(input)
66}
67
68pub fn parse_v4a_patch(input: &str) -> Option<(String, Vec<DiffHunk>)> {
70 let mut path: Option<String> = None;
71 let mut current = Vec::new();
72 let mut hunks: Vec<DiffHunk> = Vec::new();
73
74 for line in input.lines() {
75 if let Some(rest) = line
76 .strip_prefix("*** Update File: ")
77 .or_else(|| line.strip_prefix("*** Add File: "))
78 {
79 if path.is_some() {
80 break;
83 }
84 path = Some(rest.trim().to_string());
85 continue;
86 }
87 if line.starts_with("*** Begin Patch")
88 || line.starts_with("*** End Patch")
89 || line.starts_with("*** End of File")
90 {
91 continue;
92 }
93 if path.is_none() {
94 continue;
95 }
96 if let Some(stripped) = line.strip_prefix("@@") {
98 if !current.is_empty() {
99 hunks.push(DiffHunk {
100 lines: std::mem::take(&mut current),
101 });
102 }
103 let text = stripped.trim_start().to_string();
104 if !text.is_empty() {
105 current.push(DiffLine {
106 kind: DiffLineKind::Context,
107 text,
108 });
109 }
110 continue;
111 }
112 if let Some(rest) = line.strip_prefix('+') {
113 current.push(DiffLine {
114 kind: DiffLineKind::Add,
115 text: rest.to_string(),
116 });
117 } else if let Some(rest) = line.strip_prefix('-') {
118 current.push(DiffLine {
119 kind: DiffLineKind::Remove,
120 text: rest.to_string(),
121 });
122 } else if let Some(rest) = line.strip_prefix(' ') {
123 current.push(DiffLine {
124 kind: DiffLineKind::Context,
125 text: rest.to_string(),
126 });
127 }
128 }
129 if !current.is_empty() {
130 hunks.push(DiffHunk { lines: current });
131 }
132
133 let path = path?;
134 if hunks.is_empty() {
135 return None;
136 }
137 Some((path, hunks))
138}
139
140pub(crate) fn extract_write_file_path_from_result(output: &str) -> Option<String> {
148 let trimmed = output.trim();
149 if let Some(idx) = trimmed.rfind(" to ") {
150 let candidate = &trimmed[idx + 4..];
151 if !candidate.is_empty() {
152 return Some(candidate.to_string());
153 }
154 }
155 None
156}
157
158#[cfg(test)]
163mod tests {
164 use super::*;
165 use crate::tui::app::DiffLineKind;
166
167 #[test]
168 fn tool_call_args_preview_extracts_path() {
169 let preview = preview_args(r#"{"path":"src/agent.rs"}"#);
170 assert!(preview.contains("path"));
171 assert!(preview.contains("src/agent.rs"));
172 }
173
174 #[test]
175 fn verb_for_tool_categorises_tools() {
176 assert_eq!(verb_for_tool("read_file"), "Reading");
177 assert_eq!(verb_for_tool("apply_patch"), "Editing");
178 assert_eq!(verb_for_tool("run_shell"), "Running");
179 assert_eq!(verb_for_tool("custom_xyz"), "Calling tool");
180 }
181
182 #[test]
183 fn parse_v4a_patch_extracts_path_and_pm_lines() {
184 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";
185 let (path, hunks) = parse_v4a_patch(patch).unwrap();
186 assert_eq!(path, "src/foo.rs");
187 assert!(!hunks.is_empty());
188 let kinds: Vec<_> = hunks
189 .iter()
190 .flat_map(|h| h.lines.iter().map(|l| l.kind.clone()))
191 .collect();
192 assert!(kinds.contains(&DiffLineKind::Add));
193 assert!(kinds.contains(&DiffLineKind::Remove));
194 }
195}