Skip to main content

lean_ctx/tools/
ctx_shell.rs

1use 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 output = crate::core::compressor::strip_ansi(output);
40    let lines: Vec<&str> = output
41        .lines()
42        .filter(|l| {
43            let t = l.trim();
44            !t.is_empty()
45        })
46        .collect();
47
48    if lines.len() <= 10 {
49        return lines.join("\n");
50    }
51
52    let first_3 = &lines[..3];
53    let last_3 = &lines[lines.len() - 3..];
54    let omitted = lines.len() - 6;
55    format!(
56        "{}\n[truncated: showing 6/{} lines, {} omitted. Use raw=true for full output.]\n{}",
57        first_3.join("\n"),
58        lines.len(),
59        omitted,
60        last_3.join("\n")
61    )
62}
63
64fn looks_like_code(text: &str) -> bool {
65    let indicators = [
66        "fn ",
67        "pub ",
68        "let ",
69        "const ",
70        "impl ",
71        "struct ",
72        "enum ",
73        "function ",
74        "class ",
75        "import ",
76        "export ",
77        "def ",
78        "async ",
79        "=>",
80        "->",
81        "::",
82        "self.",
83        "this.",
84    ];
85    let total_lines = text.lines().count();
86    if total_lines < 3 {
87        return false;
88    }
89    let code_lines = text
90        .lines()
91        .filter(|l| indicators.iter().any(|i| l.contains(i)))
92        .count();
93    code_lines as f64 / total_lines as f64 > 0.15
94}
95
96fn detect_ext_from_command(command: &str) -> &str {
97    let cmd = command.to_lowercase();
98    if cmd.contains("cargo") || cmd.contains(".rs") {
99        "rs"
100    } else if cmd.contains("npm")
101        || cmd.contains("node")
102        || cmd.contains(".ts")
103        || cmd.contains(".js")
104    {
105        "ts"
106    } else if cmd.contains("python") || cmd.contains("pip") || cmd.contains(".py") {
107        "py"
108    } else if cmd.contains("go ") || cmd.contains(".go") {
109        "go"
110    } else {
111        "rs"
112    }
113}