Skip to main content

lean_ctx/tools/
ctx_smart_read.rs

1use crate::core::cache::SessionCache;
2use crate::core::mode_predictor::{FileSignature, ModePredictor};
3use crate::core::tokens::count_tokens;
4use crate::tools::CrpMode;
5
6pub fn select_mode(cache: &SessionCache, path: &str) -> String {
7    select_mode_with_task(cache, path, None)
8}
9
10pub fn select_mode_with_task(cache: &SessionCache, path: &str, task: Option<&str>) -> String {
11    if crate::tools::ctx_read::is_instruction_file(path) {
12        return "full".to_string();
13    }
14
15    if crate::core::binary_detect::is_binary_file(path) {
16        return "full".to_string();
17    }
18
19    if let Ok(meta) = std::fs::metadata(path) {
20        let cap = crate::core::limits::max_read_bytes() as u64;
21        if meta.len() > cap {
22            return "full".to_string();
23        }
24    }
25
26    let Ok(content) = std::fs::read_to_string(path) else {
27        return "full".to_string();
28    };
29
30    let token_count = count_tokens(&content);
31    let ext = std::path::Path::new(path)
32        .extension()
33        .and_then(|e| e.to_str())
34        .unwrap_or("");
35
36    if let Some(cached) = cache.get(path) {
37        if cached.hash == compute_hash(&content) {
38            return "full".to_string();
39        }
40        return "diff".to_string();
41    }
42
43    if token_count <= 200 {
44        return "full".to_string();
45    }
46
47    if is_config_or_data(ext, path) {
48        return "full".to_string();
49    }
50
51    // task mode (IB-filter) is never auto-selected — it reorders lines and breaks edits.
52    // Users can still explicitly request mode: "task".
53
54    if let Some(recommended) = intent_recommended_mode(task) {
55        return recommended;
56    }
57
58    let sig = FileSignature::from_path(path, token_count);
59    let predictor = ModePredictor::new();
60    if let Some(predicted) = predictor.predict_best_mode(&sig) {
61        return predicted;
62    }
63
64    heuristic_mode(ext, token_count)
65}
66
67/// Queries the intent engine + router for a task-aware read mode recommendation.
68/// Returns `None` when there is no task, confidence is too low, or the router
69/// recommends "auto" (which would recurse).
70fn intent_recommended_mode(task: Option<&str>) -> Option<String> {
71    let task_desc = task?;
72    let classification = crate::core::intent_engine::classify(task_desc);
73    if classification.confidence < 0.4 {
74        return None;
75    }
76    let route = crate::core::intent_engine::route_intent(task_desc, &classification);
77    let mode =
78        crate::core::intent_router::read_mode_for_tier(route.model_tier, classification.task_type);
79    if mode == "auto" {
80        return None;
81    }
82    Some(mode)
83}
84
85fn heuristic_mode(ext: &str, token_count: usize) -> String {
86    if token_count > 8000 {
87        if is_code(ext) {
88            return "map".to_string();
89        }
90        return "aggressive".to_string();
91    }
92    if token_count > 3000 && is_code(ext) {
93        return "map".to_string();
94    }
95    "full".to_string()
96}
97
98pub fn handle(cache: &mut SessionCache, path: &str, crp_mode: CrpMode) -> String {
99    let mode = select_mode(cache, path);
100    let result = crate::tools::ctx_read::handle(cache, path, &mode, crp_mode);
101    format!("[auto:{mode}] {result}")
102}
103
104fn compute_hash(content: &str) -> String {
105    use md5::{Digest, Md5};
106    let mut hasher = Md5::new();
107    hasher.update(content.as_bytes());
108    format!("{:x}", hasher.finalize())
109}
110
111pub fn is_code_ext(ext: &str) -> bool {
112    is_code(ext)
113}
114
115fn is_code(ext: &str) -> bool {
116    matches!(
117        ext,
118        "rs" | "ts"
119            | "tsx"
120            | "js"
121            | "jsx"
122            | "py"
123            | "go"
124            | "java"
125            | "c"
126            | "cpp"
127            | "cc"
128            | "h"
129            | "hpp"
130            | "rb"
131            | "cs"
132            | "kt"
133            | "swift"
134            | "php"
135            | "zig"
136            | "ex"
137            | "exs"
138            | "scala"
139            | "sc"
140            | "dart"
141            | "sh"
142            | "bash"
143            | "svelte"
144            | "vue"
145    )
146}
147
148fn is_config_or_data(ext: &str, path: &str) -> bool {
149    if matches!(
150        ext,
151        "json" | "yaml" | "yml" | "toml" | "xml" | "ini" | "cfg" | "env" | "lock"
152    ) {
153        return true;
154    }
155    let name = std::path::Path::new(path)
156        .file_name()
157        .and_then(|n| n.to_str())
158        .unwrap_or("");
159    matches!(
160        name,
161        "Cargo.toml"
162            | "package.json"
163            | "tsconfig.json"
164            | "Makefile"
165            | "Dockerfile"
166            | "docker-compose.yml"
167            | ".gitignore"
168            | ".env"
169            | "pyproject.toml"
170            | "go.mod"
171            | "build.gradle"
172            | "pom.xml"
173    )
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    #[test]
181    fn test_config_detection() {
182        assert!(is_config_or_data("json", "package.json"));
183        assert!(is_config_or_data("toml", "Cargo.toml"));
184        assert!(!is_config_or_data("rs", "main.rs"));
185    }
186
187    #[test]
188    fn test_code_detection() {
189        assert!(is_code("rs"));
190        assert!(is_code("py"));
191        assert!(is_code("tsx"));
192        assert!(!is_code("json"));
193    }
194
195    #[test]
196    fn intent_mode_for_explore_task() {
197        let mode = intent_recommended_mode(Some("how does the session cache work?"));
198        assert_eq!(mode, Some("map".to_string()));
199    }
200
201    #[test]
202    fn intent_mode_for_fix_task() {
203        let mode = intent_recommended_mode(Some("fix the bug in auth.rs"));
204        assert_eq!(mode, Some("full".to_string()));
205    }
206
207    #[test]
208    fn intent_mode_none_without_task() {
209        assert_eq!(intent_recommended_mode(None), None);
210    }
211
212    #[test]
213    fn intent_mode_none_for_low_confidence() {
214        let mode = intent_recommended_mode(Some("xyz qqq"));
215        assert_eq!(mode, None);
216    }
217}