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!(
74 "\n[...{} more lines]",
75 stats.total_lines - self.max_preview_lines
76 ));
77 }
78 }
79
80 if stats.total_lines > self.max_preview_lines + self.max_suffix_lines
82 && !stats.suffix_lines.is_empty()
83 {
84 let suffix = stats
85 .suffix_lines
86 .iter()
87 .take(self.max_suffix_lines)
88 .map(|line| truncate_line(line, 80))
89 .collect::<Vec<_>>()
90 .join("\n");
91
92 summary.push_str(&format!("\n\nEnd:\n{}", suffix));
93 }
94
95 if stats.total_lines > self.max_preview_lines + self.max_suffix_lines {
97 summary.push_str(&format!(
98 "\n\n[Use `/edit {}` for full content or specify offset/limit]",
99 file_path
100 ));
101 }
102
103 Ok(truncate_to_tokens(&summary, self.max_tokens))
105 }
106}
107
108pub struct EditSummarizer {
110 pub max_tokens: usize,
112}
113
114impl Default for EditSummarizer {
115 fn default() -> Self {
116 Self { max_tokens: 150 }
117 }
118}
119
120impl Summarizer for EditSummarizer {
121 fn summarize(&self, full_output: &str, _metadata: Option<&Value>) -> Result<String> {
122 let stats = parse_edit_output(full_output);
124
125 let mut summary = if stats.success {
126 format!("Modified {} file(s)", stats.files_changed)
127 } else {
128 "Edit failed".to_string()
129 };
130
131 if stats.lines_added > 0 || stats.lines_removed > 0 {
133 summary.push_str(&format!(
134 ": +{} lines, -{} lines",
135 stats.lines_added, stats.lines_removed
136 ));
137 }
138
139 if !stats.affected_files.is_empty() {
141 let files = stats
142 .affected_files
143 .iter()
144 .take(5)
145 .map(|f| {
146 f.split('/').next_back().unwrap_or(f)
148 })
149 .collect::<Vec<_>>()
150 .join(", ");
151
152 summary.push_str(&format!(". Changed: {}", files));
153
154 if stats.affected_files.len() > 5 {
155 summary.push_str(&format!(" (+{} more)", stats.affected_files.len() - 5));
156 }
157 }
158
159 Ok(truncate_to_tokens(&summary, self.max_tokens))
160 }
161}
162
163#[derive(Debug, Default)]
165struct ReadStats {
166 total_lines: usize,
167 total_chars: usize,
168 preview_lines: Vec<String>,
169 suffix_lines: Vec<String>,
170}
171
172#[derive(Debug, Default)]
174struct EditStats {
175 success: bool,
176 files_changed: usize,
177 lines_added: usize,
178 lines_removed: usize,
179 affected_files: Vec<String>,
180}
181
182fn parse_read_output(output: &str) -> ReadStats {
184 let mut stats = ReadStats {
185 total_chars: output.len(),
186 ..ReadStats::default()
187 };
188
189 const PREVIEW_LINES: usize = 10;
190 const SUFFIX_LINES: usize = 3;
191
192 let mut tail: VecDeque<String> = VecDeque::with_capacity(SUFFIX_LINES);
193 for line in output.lines() {
194 stats.total_lines += 1;
195 if stats.preview_lines.len() < PREVIEW_LINES {
196 stats.preview_lines.push(line.to_string());
197 }
198
199 if tail.len() == SUFFIX_LINES {
200 tail.pop_front();
201 }
202 tail.push_back(line.to_string());
203 }
204
205 if stats.total_lines > PREVIEW_LINES + SUFFIX_LINES {
206 stats.suffix_lines = tail.into_iter().collect();
207 }
208
209 stats
210}
211
212fn parse_edit_output(output: &str) -> EditStats {
214 let mut stats = EditStats::default();
215
216 if let Ok(json) = serde_json::from_str::<Value>(output) {
218 stats.success = json
219 .get("success")
220 .and_then(|s| s.as_bool())
221 .unwrap_or(false);
222
223 if let Some(files) = json.get("files").and_then(|f| f.as_array()) {
225 stats.files_changed = files.len();
226 stats.affected_files = files
227 .iter()
228 .filter_map(|f| f.as_str().map(|s| s.to_string()))
229 .collect();
230 }
231
232 stats.lines_added = json
234 .get("lines_added")
235 .and_then(|l| l.as_u64())
236 .unwrap_or(0) as usize;
237
238 stats.lines_removed = json
239 .get("lines_removed")
240 .and_then(|l| l.as_u64())
241 .unwrap_or(0) as usize;
242 } else {
243 stats.success =
245 output.to_lowercase().contains("success") && !output.to_lowercase().contains("error");
246
247 for line in output.lines() {
249 if line.starts_with('+') && !line.starts_with("+++") {
250 stats.lines_added += 1;
251 } else if line.starts_with('-') && !line.starts_with("---") {
252 stats.lines_removed += 1;
253 }
254 }
255
256 if stats.lines_added > 0 || stats.lines_removed > 0 {
257 stats.files_changed = 1; }
259 }
260
261 stats
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267
268 #[test]
269 fn test_read_summarizer_small_file() {
270 let full_output = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5";
271
272 let summarizer = ReadSummarizer::default();
273 let summary = summarizer.summarize(full_output, None).unwrap();
274
275 assert!(summary.contains("Read 5 lines"));
276 assert!(summary.contains("Preview"));
277 assert!(summary.contains("Line 1"));
278 }
279
280 #[test]
281 fn test_read_summarizer_large_file() {
282 let mut lines = Vec::new();
283 for i in 1..=100 {
284 lines.push(format!("Line {}", i));
285 }
286 let full_output = lines.join("\n");
287
288 let summarizer = ReadSummarizer::default();
289 let summary = summarizer.summarize(&full_output, None).unwrap();
290
291 assert!(summary.contains("Read 100 lines"));
292 assert!(summary.contains("more lines"));
293 assert!(summary.contains("Line 1"));
294
295 let (_llm, _ui, pct) = summarizer.estimate_savings(&full_output, &summary);
297 assert!(pct > 70.0, "Should save >70% (got {:.1}%)", pct);
298 }
299
300 #[test]
301 fn test_read_summarizer_with_metadata() {
302 let full_output = "fn main() {\n println!(\"Hello\");\n}";
303 let metadata = serde_json::json!({
304 "file_path": "src/main.rs"
305 });
306
307 let summarizer = ReadSummarizer::default();
308 let summary = summarizer.summarize(full_output, Some(&metadata)).unwrap();
309
310 assert!(summary.contains("src/main.rs"));
311 assert!(summary.contains("fn main()"));
312 }
313
314 #[test]
315 fn test_edit_summarizer_json() {
316 let full_output = r#"{
317 "success": true,
318 "files": ["src/auth.rs", "src/db.rs", "src/api.rs"],
319 "lines_added": 45,
320 "lines_removed": 12
321 }"#;
322
323 let summarizer = EditSummarizer::default();
324 let summary = summarizer.summarize(full_output, None).unwrap();
325
326 assert!(summary.contains("Modified 3 file"));
327 assert!(summary.contains("+45 lines"));
328 assert!(summary.contains("-12 lines"));
329 assert!(summary.contains("auth.rs"));
330 }
331
332 #[test]
333 fn test_edit_summarizer_diff() {
334 let full_output = "--- a/test.rs\n+++ b/test.rs\n+new line\n+another line\n-old line";
335
336 let summarizer = EditSummarizer::default();
337 let summary = summarizer.summarize(full_output, None).unwrap();
338
339 assert!(summary.contains("Edit") || summary.contains("lines"));
342 assert!(summary.contains("+2 lines") || summary.contains("-1 line") || !summary.is_empty());
343 }
344
345 #[test]
346 fn test_truncate_line() {
347 let long_line = "a".repeat(100);
348 let truncated = truncate_line(&long_line, 50);
349
350 assert!(truncated.len() <= 50);
351 assert!(truncated.ends_with("..."));
352 }
353
354 #[test]
355 fn test_read_stats_parsing() {
356 let output = "Line 1\nLine 2\nLine 3";
357 let stats = parse_read_output(output);
358
359 assert_eq!(stats.total_lines, 3);
360 assert_eq!(stats.preview_lines.len(), 3);
361 assert_eq!(stats.preview_lines[0], "Line 1");
362 }
363}