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