1use crate::core::tokens::count_tokens;
2use crate::tools::CrpMode;
3
4pub fn handle(response: &str, crp_mode: CrpMode) -> String {
5 handle_with_context(response, crp_mode, None)
6}
7
8pub fn handle_with_context(
9 response: &str,
10 crp_mode: CrpMode,
11 input_context: Option<&str>,
12) -> String {
13 let original_tokens = count_tokens(response);
14
15 if original_tokens <= 100 {
16 return response.to_string();
17 }
18
19 let compressed = if crp_mode.is_tdd() {
20 compress_tdd(response, input_context)
21 } else {
22 compress_standard(response, input_context)
23 };
24
25 let compressed_tokens = count_tokens(&compressed);
26 let savings = original_tokens.saturating_sub(compressed_tokens);
27 let pct = if original_tokens > 0 {
28 (savings as f64 / original_tokens as f64 * 100.0) as u32
29 } else {
30 0
31 };
32
33 if savings < 20 {
34 return response.to_string();
35 }
36
37 if crate::core::protocol::savings_footer_visible() {
38 format!(
39 "{compressed}\n[response compressed: {original_tokens}→{compressed_tokens} tok, -{pct}%]"
40 )
41 } else {
42 compressed
43 }
44}
45
46fn compress_standard(text: &str, input_context: Option<&str>) -> String {
47 let echo_lines = input_context.map(build_echo_set);
48
49 let mut result = Vec::new();
50 let mut prev_empty = false;
51
52 for line in text.lines() {
53 let trimmed = line.trim();
54
55 if trimmed.is_empty() {
56 if !prev_empty {
57 result.push(String::new());
58 prev_empty = true;
59 }
60 continue;
61 }
62 prev_empty = false;
63
64 if is_filler_line(trimmed) {
65 continue;
66 }
67 if is_boilerplate_code(trimmed) {
68 continue;
69 }
70 if let Some(ref echoes) = echo_lines {
71 if is_context_echo(trimmed, echoes) {
72 continue;
73 }
74 }
75
76 result.push(line.to_string());
77 }
78
79 result.join("\n")
80}
81
82fn compress_tdd(text: &str, input_context: Option<&str>) -> String {
83 let echo_lines = input_context.map(build_echo_set);
84
85 let mut result = Vec::new();
86 let mut prev_empty = false;
87
88 for line in text.lines() {
89 let trimmed = line.trim();
90
91 if trimmed.is_empty() {
92 if !prev_empty {
93 prev_empty = true;
94 }
95 continue;
96 }
97 prev_empty = false;
98
99 if is_filler_line(trimmed) {
100 continue;
101 }
102 if is_boilerplate_code(trimmed) {
103 continue;
104 }
105 if let Some(ref echoes) = echo_lines {
106 if is_context_echo(trimmed, echoes) {
107 continue;
108 }
109 }
110
111 let compressed = apply_tdd_shortcuts(trimmed);
112 result.push(compressed);
113 }
114
115 result.join("\n")
116}
117
118fn build_echo_set(context: &str) -> std::collections::HashSet<String> {
119 context
120 .lines()
121 .map(normalize_for_echo)
122 .filter(|l| l.len() > 10)
123 .collect()
124}
125
126fn normalize_for_echo(line: &str) -> String {
127 line.trim().to_lowercase().replace(char::is_whitespace, " ")
128}
129
130fn is_context_echo(line: &str, echo_set: &std::collections::HashSet<String>) -> bool {
131 let normalized = normalize_for_echo(line);
132 if normalized.len() <= 10 {
133 return false;
134 }
135 echo_set.contains(&normalized)
136}
137
138fn is_boilerplate_code(line: &str) -> bool {
139 let trimmed = line.trim();
140
141 if trimmed.starts_with("//")
142 && !trimmed.starts_with("// TODO")
143 && !trimmed.starts_with("// FIXME")
144 && !trimmed.starts_with("// SAFETY")
145 && !trimmed.starts_with("// NOTE")
146 {
147 let comment_body = trimmed.trim_start_matches("//").trim();
148 if is_narration_comment(comment_body) {
149 return true;
150 }
151 }
152
153 if trimmed.starts_with('#') && !trimmed.starts_with("#[") && !trimmed.starts_with("#!") {
154 let comment_body = trimmed.trim_start_matches('#').trim();
155 if is_narration_comment(comment_body) {
156 return true;
157 }
158 }
159
160 false
161}
162
163fn is_narration_comment(body: &str) -> bool {
164 let b = body.to_lowercase();
165
166 let what_prefixes = [
167 "import ",
168 "define ",
169 "create ",
170 "set up ",
171 "initialize ",
172 "declare ",
173 "add ",
174 "get ",
175 "return ",
176 "check ",
177 "handle ",
178 "call ",
179 "update ",
180 "increment ",
181 "decrement ",
182 "loop ",
183 "iterate ",
184 "print ",
185 "log ",
186 "convert ",
187 "parse ",
188 "read ",
189 "write ",
190 "send ",
191 "receive ",
192 "validate ",
193 "set ",
194 "start ",
195 "stop ",
196 "open ",
197 "close ",
198 "fetch ",
199 "load ",
200 "save ",
201 "store ",
202 "delete ",
203 "remove ",
204 "calculate ",
205 "compute ",
206 "render ",
207 "display ",
208 "show ",
209 "this function ",
210 "this method ",
211 "this class ",
212 "the following ",
213 "here we ",
214 "now we ",
215 ];
216 if what_prefixes.iter().any(|p| b.starts_with(p)) {
217 return true;
218 }
219
220 let what_patterns = [" the ", " a ", " an "];
221 if b.len() < 60 && what_patterns.iter().all(|p| !b.contains(p)) {
222 return false;
223 }
224 if b.len() < 40
225 && b.split_whitespace().count() <= 5
226 && b.chars().filter(|c| c.is_uppercase()).count() == 0
227 {
228 return false;
229 }
230
231 false
232}
233
234fn is_filler_line(line: &str) -> bool {
235 let l = line.to_lowercase();
236
237 if l.starts_with("note:")
239 || l.starts_with("hint:")
240 || l.starts_with("warning:")
241 || l.starts_with("error:")
242 || l.starts_with("however,")
243 || l.starts_with("but ")
244 || l.starts_with("caution:")
245 || l.starts_with("important:")
246 {
247 return false;
248 }
249
250 let prefix_fillers = [
252 "here's what i",
254 "here is what i",
255 "let me explain",
256 "let me walk you",
257 "let me break",
258 "i'll now",
259 "i will now",
260 "i'm going to",
261 "first, let me",
262 "allow me to",
263 "i think",
265 "i believe",
266 "i would say",
267 "it seems like",
268 "it looks like",
269 "it appears that",
270 "that's a great question",
272 "that's an interesting",
273 "good question",
274 "great question",
275 "sure thing",
276 "sure,",
277 "of course,",
278 "absolutely,",
279 "now, let's",
281 "now let's",
282 "next, i'll",
283 "moving on",
284 "going forward",
285 "with that said",
286 "with that in mind",
287 "having said that",
288 "that being said",
289 "hope this helps",
291 "i hope this",
292 "let me know if",
293 "feel free to",
294 "don't hesitate",
295 "happy to help",
296 "as you can see",
298 "as we can see",
299 "this is because",
300 "the reason is",
301 "in this case",
302 "in other words",
303 "to summarize",
304 "to sum up",
305 "basically,",
306 "essentially,",
307 "it's worth noting",
308 "it should be noted",
309 "as mentioned",
310 "as i mentioned",
311 "understood.",
313 "got it.",
314 "i understand.",
315 "i see.",
316 "right,",
317 "okay,",
318 "ok,",
319 ];
320
321 prefix_fillers.iter().any(|f| l.starts_with(f))
322}
323
324fn apply_tdd_shortcuts(line: &str) -> String {
325 let mut result = line.to_string();
326
327 let replacements = [
328 ("function", "fn"),
330 ("configuration", "cfg"),
331 ("implementation", "impl"),
332 ("dependencies", "deps"),
333 ("dependency", "dep"),
334 ("request", "req"),
335 ("response", "res"),
336 ("context", "ctx"),
337 ("parameter", "param"),
338 ("argument", "arg"),
339 ("variable", "val"),
340 ("directory", "dir"),
341 ("repository", "repo"),
342 ("application", "app"),
343 ("environment", "env"),
344 ("description", "desc"),
345 ("information", "info"),
346 ("returns ", "→ "),
348 ("therefore", "∴"),
349 ("approximately", "≈"),
350 ("successfully", "✓"),
351 ("completed", "✓"),
352 ("failed", "✗"),
353 ("warning", "⚠"),
354 (" is not ", " ≠ "),
356 (" does not ", " ≠ "),
357 (" equals ", " = "),
358 (" and ", " & "),
359 ("error", "err"),
360 ("module", "mod"),
361 ("package", "pkg"),
362 ("initialize", "init"),
363 ];
364
365 for (from, to) in &replacements {
366 result = result.replace(from, to);
367 }
368
369 result
370}
371
372#[cfg(test)]
373mod tests {
374 use super::*;
375
376 #[test]
377 fn test_filler_detection_original() {
378 assert!(is_filler_line("Here's what I found"));
379 assert!(is_filler_line("Let me explain how this works"));
380 assert!(!is_filler_line("fn main() {}"));
381 assert!(!is_filler_line("Note: important detail"));
382 }
383
384 #[test]
385 fn test_filler_hedging_patterns() {
386 assert!(is_filler_line("I think the issue is here"));
387 assert!(is_filler_line("I believe this is correct"));
388 assert!(is_filler_line("It seems like the problem is"));
389 assert!(is_filler_line("It looks like we need to"));
390 assert!(is_filler_line("It appears that something broke"));
391 }
392
393 #[test]
394 fn test_filler_meta_commentary() {
395 assert!(is_filler_line("That's a great question!"));
396 assert!(is_filler_line("Good question, let me check"));
397 assert!(is_filler_line("Sure thing, I'll do that"));
398 assert!(is_filler_line("Of course, here's the code"));
399 assert!(is_filler_line("Absolutely, that makes sense"));
400 }
401
402 #[test]
403 fn test_filler_closings() {
404 assert!(is_filler_line("Hope this helps!"));
405 assert!(is_filler_line("Let me know if you need more"));
406 assert!(is_filler_line("Feel free to ask questions"));
407 assert!(is_filler_line("Don't hesitate to reach out"));
408 assert!(is_filler_line("Happy to help with anything"));
409 }
410
411 #[test]
412 fn test_filler_transitions() {
413 assert!(is_filler_line("Now, let's move on"));
414 assert!(is_filler_line("Moving on to the next part"));
415 assert!(is_filler_line("Going forward, we should"));
416 assert!(is_filler_line("With that said, here's what"));
417 assert!(is_filler_line("Having said that, let's"));
418 }
419
420 #[test]
421 fn test_filler_acknowledgments() {
422 assert!(is_filler_line("Understood."));
423 assert!(is_filler_line("Got it."));
424 assert!(is_filler_line("I understand."));
425 assert!(is_filler_line("I see."));
426 }
427
428 #[test]
429 fn test_filler_false_positive_protection() {
430 assert!(!is_filler_line("Note: this is critical"));
431 assert!(!is_filler_line("Warning: deprecated API"));
432 assert!(!is_filler_line("Error: connection refused"));
433 assert!(!is_filler_line("However, the edge case fails"));
434 assert!(!is_filler_line("But the second argument is wrong"));
435 assert!(!is_filler_line("Important: do not skip this step"));
436 assert!(!is_filler_line("Caution: this deletes all data"));
437 assert!(!is_filler_line("Hint: use --force flag"));
438 assert!(!is_filler_line("fn validate_token()"));
439 assert!(!is_filler_line(" let result = parse(input);"));
440 assert!(!is_filler_line("The token is expired after 24h"));
441 }
442
443 #[test]
444 fn test_tdd_shortcuts() {
445 let result = apply_tdd_shortcuts("the function returns successfully");
446 assert!(result.contains("fn"));
447 assert!(result.contains("→"));
448 assert!(result.contains("✓"));
449 }
450
451 #[test]
452 fn test_tdd_shortcuts_extended() {
453 let result = apply_tdd_shortcuts("the application environment failed");
454 assert!(result.contains("app"));
455 assert!(result.contains("env"));
456 assert!(result.contains("✗"));
457 }
458
459 #[test]
460 fn test_compress_integration() {
461 let response = "Let me explain how this works.\n\
462 I think this is correct.\n\
463 Hope this helps!\n\
464 \n\
465 The function returns an error when the token is expired.\n\
466 Note: always check the expiry first.";
467
468 let compressed = compress_standard(response, None);
469 assert!(!compressed.contains("Let me explain"));
470 assert!(!compressed.contains("I think"));
471 assert!(!compressed.contains("Hope this helps"));
472 assert!(compressed.contains("error when the token"));
473 assert!(compressed.contains("Note:"));
474 }
475
476 #[test]
477 fn test_echo_detection() {
478 let context = "fn shannon_entropy(text: &str) -> f64 {\n let freq = HashMap::new();\n}";
479 let response = "Here's the code:\nfn shannon_entropy(text: &str) -> f64 {\n let freq = HashMap::new();\n}\nI added the new function below.";
480
481 let compressed = compress_standard(response, Some(context));
482 assert!(!compressed.contains("fn shannon_entropy"));
483 assert!(compressed.contains("added the new function"));
484 }
485
486 #[test]
487 fn test_boilerplate_comment_removal() {
488 let response = "// Import the module\nuse std::io;\n// Define the function\nfn main() {}\n// NOTE: important edge case\nlet x = 1;";
489 let compressed = compress_standard(response, None);
490 assert!(!compressed.contains("Import the module"));
491 assert!(!compressed.contains("Define the function"));
492 assert!(compressed.contains("NOTE: important edge case"));
493 assert!(compressed.contains("use std::io"));
494 assert!(compressed.contains("fn main()"));
495 }
496}