vtcode_core/tools/summarizers/
file_ops.rs1use super::{Summarizer, truncate_line, truncate_to_tokens};
16use anyhow::Result;
17use serde_json::Value;
18use std::collections::VecDeque;
19
20pub struct ReadSummarizer {
22 pub max_preview_lines: usize,
24 pub max_suffix_lines: usize,
26 pub max_tokens: usize,
28}
29
30impl Default for ReadSummarizer {
31 fn default() -> Self {
32 Self {
33 max_preview_lines: 20,
34 max_suffix_lines: 10,
35 max_tokens: 500, }
37 }
38}
39
40impl Summarizer for ReadSummarizer {
41 fn summarize(&self, full_output: &str, metadata: Option<&Value>) -> Result<String> {
42 let file_path = metadata
44 .and_then(|m| m.get("file_path"))
45 .and_then(|f| f.as_str())
46 .unwrap_or("file");
47
48 let stats = parse_read_output(full_output);
50
51 let mut summary = format!("Read {} lines from {}", stats.total_lines, file_path);
53
54 if stats.total_chars > 10_000 {
56 let kb = stats.total_chars / 1024;
57 summary.push_str(&format!(" ({kb} KB)"));
58 }
59
60 if !stats.preview_lines.is_empty() {
62 let preview = stats
63 .preview_lines
64 .iter()
65 .take(self.max_preview_lines)
66 .map(|line| truncate_line(line, 80))
67 .collect::<Vec<_>>()
68 .join("\n");
69
70 summary.push_str(&format!("\n\nPreview:\n{preview}"));
71
72 if stats.total_lines > self.max_preview_lines {
73 summary.push_str(&format!("\n[...{} more lines]", stats.total_lines - self.max_preview_lines));
74 }
75 }
76
77 if stats.total_lines > self.max_preview_lines + self.max_suffix_lines && !stats.suffix_lines.is_empty() {
79 let suffix = stats
80 .suffix_lines
81 .iter()
82 .take(self.max_suffix_lines)
83 .map(|line| truncate_line(line, 80))
84 .collect::<Vec<_>>()
85 .join("\n");
86
87 summary.push_str(&format!("\n\nEnd:\n{suffix}"));
88 }
89
90 if stats.total_lines > self.max_preview_lines + self.max_suffix_lines {
92 summary.push_str(&format!(
93 "\n\n[Full file: {} lines. This preview is sufficient for most tasks. \
94 Only re-read with offset/limit if you need specific lines not shown above.]",
95 stats.total_lines
96 ));
97 }
98
99 Ok(truncate_to_tokens(&summary, self.max_tokens))
101 }
102}
103
104pub struct EditSummarizer {
106 pub max_tokens: usize,
108}
109
110impl Default for EditSummarizer {
111 fn default() -> Self {
112 Self { max_tokens: 150 }
113 }
114}
115
116impl Summarizer for EditSummarizer {
117 fn summarize(&self, full_output: &str, _metadata: Option<&Value>) -> Result<String> {
118 let stats = parse_edit_output(full_output);
120
121 let mut summary = if stats.success {
122 format!("Modified {} file(s)", stats.files_changed)
123 } else {
124 "Edit failed".to_string()
125 };
126
127 if stats.lines_added > 0 || stats.lines_removed > 0 {
129 summary.push_str(&format!(": +{} lines, -{} lines", stats.lines_added, stats.lines_removed));
130 }
131
132 if !stats.affected_files.is_empty() {
134 let files = stats
135 .affected_files
136 .iter()
137 .take(5)
138 .map(|f| {
139 f.split('/').next_back().unwrap_or(f)
141 })
142 .collect::<Vec<_>>()
143 .join(", ");
144
145 summary.push_str(&format!(". Changed: {files}"));
146
147 if stats.affected_files.len() > 5 {
148 summary.push_str(&format!(" (+{} more)", stats.affected_files.len() - 5));
149 }
150 }
151
152 Ok(truncate_to_tokens(&summary, self.max_tokens))
153 }
154}
155
156#[derive(Debug, Default)]
158struct ReadStats {
159 total_lines: usize,
160 total_chars: usize,
161 preview_lines: Vec<String>,
162 suffix_lines: Vec<String>,
163}
164
165#[derive(Debug, Default)]
167struct EditStats {
168 success: bool,
169 files_changed: usize,
170 lines_added: usize,
171 lines_removed: usize,
172 affected_files: Vec<String>,
173}
174
175fn parse_read_output(output: &str) -> ReadStats {
177 let mut stats = ReadStats { total_chars: output.len(), ..ReadStats::default() };
178
179 const PREVIEW_LINES: usize = 10;
180 const SUFFIX_LINES: usize = 3;
181
182 let mut tail: VecDeque<String> = VecDeque::with_capacity(SUFFIX_LINES);
183 for line in output.lines() {
184 stats.total_lines += 1;
185 if stats.preview_lines.len() < PREVIEW_LINES {
186 stats.preview_lines.push(line.to_string());
187 }
188
189 if tail.len() == SUFFIX_LINES {
190 tail.pop_front();
191 }
192 tail.push_back(line.to_string());
193 }
194
195 if stats.total_lines > PREVIEW_LINES + SUFFIX_LINES {
196 stats.suffix_lines = tail.into_iter().collect();
197 }
198
199 stats
200}
201
202fn parse_edit_output(output: &str) -> EditStats {
204 let mut stats = EditStats::default();
205
206 if let Ok(json) = serde_json::from_str::<Value>(output) {
208 stats.success = json.get("success").and_then(|s| s.as_bool()).unwrap_or(false);
209
210 if let Some(files) = json.get("files").and_then(|f| f.as_array()) {
212 stats.files_changed = files.len();
213 stats.affected_files = files.iter().filter_map(|f| f.as_str().map(|s| s.to_string())).collect();
214 }
215
216 stats.lines_added = json.get("lines_added").and_then(|l| l.as_u64()).unwrap_or(0) as usize;
218
219 stats.lines_removed = json.get("lines_removed").and_then(|l| l.as_u64()).unwrap_or(0) as usize;
220 } else {
221 stats.success = output.to_lowercase().contains("success") && !output.to_lowercase().contains("error");
223
224 for line in output.lines() {
226 if line.starts_with('+') && !line.starts_with("+++") {
227 stats.lines_added += 1;
228 } else if line.starts_with('-') && !line.starts_with("---") {
229 stats.lines_removed += 1;
230 }
231 }
232
233 if stats.lines_added > 0 || stats.lines_removed > 0 {
234 stats.files_changed = 1; }
236 }
237
238 stats
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[test]
246 fn test_read_summarizer_small_file() {
247 let full_output = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5";
248
249 let summarizer = ReadSummarizer::default();
250 let summary = summarizer.summarize(full_output, None).unwrap();
251
252 assert!(summary.contains("Read 5 lines"));
253 assert!(summary.contains("Preview"));
254 assert!(summary.contains("Line 1"));
255 }
256
257 #[test]
258 fn test_read_summarizer_large_file() {
259 let mut lines = Vec::new();
260 for i in 1..=100 {
261 lines.push(format!("Line {i}"));
262 }
263 let full_output = lines.join("\n");
264
265 let summarizer = ReadSummarizer::default();
266 let summary = summarizer.summarize(&full_output, None).unwrap();
267
268 assert!(summary.contains("Read 100 lines"));
269 assert!(summary.contains("more lines"));
270 assert!(summary.contains("Line 1"));
271
272 let savings = summarizer.estimate_savings(&full_output, &summary);
274 assert!(savings.savings_percent > 50.0, "Should save >50% (got {:.1}%)", savings.savings_percent);
275 }
276
277 #[test]
278 fn test_read_summarizer_with_metadata() {
279 let full_output = "fn main() {\n println!(\"Hello\");\n}";
280 let metadata = serde_json::json!({
281 "file_path": "src/main.rs"
282 });
283
284 let summarizer = ReadSummarizer::default();
285 let summary = summarizer.summarize(full_output, Some(&metadata)).unwrap();
286
287 assert!(summary.contains("src/main.rs"));
288 assert!(summary.contains("fn main()"));
289 }
290
291 #[test]
292 fn test_edit_summarizer_json() {
293 let full_output = r#"{
294 "success": true,
295 "files": ["src/auth.rs", "src/db.rs", "src/api.rs"],
296 "lines_added": 45,
297 "lines_removed": 12
298 }"#;
299
300 let summarizer = EditSummarizer::default();
301 let summary = summarizer.summarize(full_output, None).unwrap();
302
303 assert!(summary.contains("Modified 3 file"));
304 assert!(summary.contains("+45 lines"));
305 assert!(summary.contains("-12 lines"));
306 assert!(summary.contains("auth.rs"));
307 }
308
309 #[test]
310 fn test_edit_summarizer_diff() {
311 let full_output = "--- a/test.rs\n+++ b/test.rs\n+new line\n+another line\n-old line";
312
313 let summarizer = EditSummarizer::default();
314 let summary = summarizer.summarize(full_output, None).unwrap();
315
316 assert!(summary.contains("Edit") || summary.contains("lines"));
319 assert!(summary.contains("+2 lines") || summary.contains("-1 line") || !summary.is_empty());
320 }
321
322 #[test]
323 fn test_truncate_line() {
324 let long_line = "a".repeat(100);
325 let truncated = truncate_line(&long_line, 50);
326
327 assert!(truncated.len() <= 50);
328 assert!(truncated.ends_with("..."));
329 }
330
331 #[test]
332 fn test_read_stats_parsing() {
333 let output = "Line 1\nLine 2\nLine 3";
334 let stats = parse_read_output(output);
335
336 assert_eq!(stats.total_lines, 3);
337 assert_eq!(stats.preview_lines.len(), 3);
338 assert_eq!(stats.preview_lines[0], "Line 1");
339 }
340}