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