lean_ctx/tools/
ctx_smart_read.rs1use 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 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
56fn 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}