1use super::context::ContextAnalyzer;
8use super::{SmartResponse, TaskContext, TokenSavings};
9use crate::scanner::{FileNode, Scanner, ScannerConfig};
10use anyhow::Result;
11use serde::{Deserialize, Serialize};
12use std::path::Path;
13
14pub struct SmartLS {
16 context_analyzer: ContextAnalyzer,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct SmartDirEntry {
22 pub node: FileNode,
24 pub relevance: super::RelevanceScore,
26 pub suggested_actions: Vec<String>,
28}
29
30pub type SmartLSResponse = SmartResponse<SmartDirEntry>;
32
33impl SmartLS {
34 pub fn new() -> Self {
36 Self {
37 context_analyzer: ContextAnalyzer::new(),
38 }
39 }
40
41 pub fn list_smart(
43 &self,
44 path: &Path,
45 context: &TaskContext,
46 max_depth: Option<usize>,
47 ) -> Result<SmartLSResponse> {
48 let config = ScannerConfig {
50 max_depth: max_depth.unwrap_or(2),
51 show_hidden: false,
52 follow_symlinks: false,
53 ..Default::default()
54 };
55
56 let scanner = Scanner::new(path, config)?;
57 let (nodes, _stats) = scanner.scan()?;
58
59 let scored_entries = self.score_and_categorize(&nodes, context)?;
61
62 let (primary, secondary) = self.split_by_relevance(&scored_entries, context);
64
65 let original_tokens = self.estimate_tokens_for_all(&nodes);
67 let compressed_tokens = self.estimate_tokens_for_entries(&primary)
68 + self.estimate_tokens_for_entries(&secondary);
69 let token_savings = TokenSavings::new(original_tokens, compressed_tokens, "smart-ls");
70
71 let context_summary = self.generate_context_summary(&primary, &secondary, context);
73 let suggestions = self.generate_suggestions(&primary, &secondary, context);
74
75 Ok(SmartLSResponse {
76 primary,
77 secondary,
78 context_summary,
79 token_savings,
80 suggestions,
81 })
82 }
83
84 fn score_and_categorize(
86 &self,
87 nodes: &[FileNode],
88 context: &TaskContext,
89 ) -> Result<Vec<SmartDirEntry>> {
90 let mut entries = Vec::new();
91
92 for node in nodes {
93 let relevance = if node.is_dir {
94 self.context_analyzer
95 .score_directory_relevance(node, context)
96 } else {
97 self.context_analyzer.score_file_relevance(node, context)
98 };
99
100 let suggested_actions = self.generate_file_actions(node, context, &relevance);
101
102 entries.push(SmartDirEntry {
103 node: node.clone(),
104 relevance,
105 suggested_actions,
106 });
107 }
108
109 entries.sort_by(|a, b| b.relevance.score.partial_cmp(&a.relevance.score).unwrap());
111
112 Ok(entries)
113 }
114
115 fn split_by_relevance(
117 &self,
118 entries: &[SmartDirEntry],
119 context: &TaskContext,
120 ) -> (Vec<SmartDirEntry>, Vec<SmartDirEntry>) {
121 let mut primary = Vec::new();
122 let mut secondary = Vec::new();
123
124 for entry in entries {
125 if entry.relevance.score >= context.relevance_threshold {
126 primary.push(entry.clone());
127 } else if entry.relevance.score >= context.relevance_threshold * 0.6 {
128 secondary.push(entry.clone());
129 }
130 }
132
133 if let Some(max_results) = context.max_results {
135 primary.truncate(max_results / 2);
136 secondary.truncate(max_results / 2);
137 }
138
139 (primary, secondary)
140 }
141
142 fn generate_file_actions(
144 &self,
145 node: &FileNode,
146 context: &TaskContext,
147 relevance: &super::RelevanceScore,
148 ) -> Vec<String> {
149 let mut actions = Vec::new();
150
151 if node.is_dir {
152 actions.push("Explore directory".to_string());
153 if relevance.score > 0.7 {
154 actions.push("Analyze contents".to_string());
155 }
156 } else {
157 actions.push("Read file".to_string());
158 if relevance.score > 0.8 {
159 actions.push("Smart read with context".to_string());
160 }
161
162 for focus_area in &context.focus_areas {
164 match focus_area {
165 super::FocusArea::Testing
166 if node
167 .path
168 .file_name()
169 .and_then(|n| n.to_str())
170 .unwrap_or("")
171 .contains("test") =>
172 {
173 actions.push("Run tests".to_string());
174 }
175 super::FocusArea::Configuration
176 if {
177 let name = node.path.file_name().and_then(|n| n.to_str()).unwrap_or("");
178 name.ends_with(".json") || name.ends_with(".yaml")
179 } =>
180 {
181 actions.push("Edit configuration".to_string());
182 }
183 super::FocusArea::API
184 if {
185 let name = node.path.file_name().and_then(|n| n.to_str()).unwrap_or("");
186 name.contains("api") || name.contains("handler")
187 } =>
188 {
189 actions.push("Analyze API endpoints".to_string());
190 }
191 _ => {}
192 }
193 }
194 }
195
196 actions
197 }
198
199 fn estimate_tokens_for_all(&self, nodes: &[FileNode]) -> usize {
201 nodes.len() * 50 }
204
205 fn estimate_tokens_for_entries(&self, entries: &[SmartDirEntry]) -> usize {
207 entries.len() * 30 }
210
211 fn generate_context_summary(
213 &self,
214 primary: &[SmartDirEntry],
215 _secondary: &[SmartDirEntry],
216 _context: &TaskContext,
217 ) -> String {
218 format!(
219 "SmartLS analyzed directory. Found {} high-priority items.",
220 primary.len()
221 )
222 }
223
224 fn generate_suggestions(
226 &self,
227 primary: &[SmartDirEntry],
228 _secondary: &[SmartDirEntry],
229 _context: &TaskContext,
230 ) -> Vec<String> {
231 let mut suggestions = Vec::new();
232
233 if primary.is_empty() {
234 suggestions.push(
235 "No high-priority files found. Consider broadening the task context.".to_string(),
236 );
237 }
238
239 let has_config = primary.iter().any(|e| {
241 let name = e
242 .node
243 .path
244 .file_name()
245 .and_then(|n| n.to_str())
246 .unwrap_or("");
247 name.ends_with(".json") || name.ends_with(".yaml")
248 });
249 let has_code = primary.iter().any(|e| {
250 matches!(
251 e.node.category,
252 crate::scanner::FileCategory::Rust
253 | crate::scanner::FileCategory::Python
254 | crate::scanner::FileCategory::JavaScript
255 )
256 });
257
258 if has_config {
259 suggestions
260 .push("Use find_config_files for detailed configuration analysis.".to_string());
261 }
262
263 if has_code {
264 suggestions.push("Use find_code_files for comprehensive code discovery.".to_string());
265 }
266
267 suggestions
268 }
269}
270
271impl Default for SmartLS {
272 fn default() -> Self {
273 Self::new()
274 }
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280 #[test]
283 fn test_smart_ls_creation() {
284 let _smart_ls = SmartLS::new();
285 }
288}