lean_ctx/tools/
ctx_shell.rs1use crate::core::patterns;
2use crate::core::protocol;
3use crate::core::symbol_map::{self, SymbolMap};
4use crate::core::tokens::count_tokens;
5use crate::tools::CrpMode;
6
7pub fn handle(command: &str, output: &str, crp_mode: CrpMode) -> String {
8 let original_tokens = count_tokens(output);
9
10 let compressed = match patterns::compress_output(command, output) {
11 Some(c) => c,
12 None => generic_compress(output),
13 };
14
15 if crp_mode.is_tdd() && looks_like_code(&compressed) {
16 let ext = detect_ext_from_command(command);
17 let mut sym = SymbolMap::new();
18 let idents = symbol_map::extract_identifiers(&compressed, ext);
19 for ident in &idents {
20 sym.register(ident);
21 }
22 if !sym.is_empty() {
23 let mapped = sym.apply(&compressed);
24 let sym_table = sym.format_table();
25 let result = format!("{mapped}{sym_table}");
26 let sent = count_tokens(&result);
27 let savings = protocol::format_savings(original_tokens, sent);
28 return format!("{result}\n{savings}");
29 }
30 }
31
32 let sent = count_tokens(&compressed);
33 let savings = protocol::format_savings(original_tokens, sent);
34
35 format!("{compressed}\n{savings}")
36}
37
38fn generic_compress(output: &str) -> String {
39 let lines: Vec<&str> = output
40 .lines()
41 .filter(|l| {
42 let t = l.trim();
43 !t.is_empty()
44 })
45 .collect();
46
47 if lines.len() <= 10 {
48 return lines.join("\n");
49 }
50
51 let first_3 = &lines[..3];
52 let last_3 = &lines[lines.len() - 3..];
53 format!(
54 "{}\n...({} lines omitted)\n{}",
55 first_3.join("\n"),
56 lines.len() - 6,
57 last_3.join("\n")
58 )
59}
60
61fn looks_like_code(text: &str) -> bool {
62 let indicators = [
63 "fn ",
64 "pub ",
65 "let ",
66 "const ",
67 "impl ",
68 "struct ",
69 "enum ",
70 "function ",
71 "class ",
72 "import ",
73 "export ",
74 "def ",
75 "async ",
76 "=>",
77 "->",
78 "::",
79 "self.",
80 "this.",
81 ];
82 let total_lines = text.lines().count();
83 if total_lines < 3 {
84 return false;
85 }
86 let code_lines = text
87 .lines()
88 .filter(|l| indicators.iter().any(|i| l.contains(i)))
89 .count();
90 code_lines as f64 / total_lines as f64 > 0.15
91}
92
93fn detect_ext_from_command(command: &str) -> &str {
94 let cmd = command.to_lowercase();
95 if cmd.contains("cargo") || cmd.contains(".rs") {
96 "rs"
97 } else if cmd.contains("npm")
98 || cmd.contains("node")
99 || cmd.contains(".ts")
100 || cmd.contains(".js")
101 {
102 "ts"
103 } else if cmd.contains("python") || cmd.contains("pip") || cmd.contains(".py") {
104 "py"
105 } else if cmd.contains("go ") || cmd.contains(".go") {
106 "go"
107 } else {
108 "rs"
109 }
110}