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 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 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
67fn 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}