Skip to main content

st/smart/
smart_ls.rs

1//! 📂 SmartLS - Task-Aware Directory Intelligence
2//!
3//! This module provides intelligent directory listings that understand
4//! the user's current task and prioritize files by relevance, achieving
5//! significant token savings while improving workflow efficiency.
6
7use 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
14/// 📂 Smart directory lister with task awareness
15pub struct SmartLS {
16    context_analyzer: ContextAnalyzer,
17}
18
19/// 📁 Smart directory entry with relevance scoring
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct SmartDirEntry {
22    /// File node information
23    pub node: FileNode,
24    /// Relevance score for current task
25    pub relevance: super::RelevanceScore,
26    /// Suggested actions for this file
27    pub suggested_actions: Vec<String>,
28}
29
30/// 📊 Smart directory listing response
31pub type SmartLSResponse = SmartResponse<SmartDirEntry>;
32
33impl SmartLS {
34    /// Create new smart directory lister
35    pub fn new() -> Self {
36        Self {
37            context_analyzer: ContextAnalyzer::new(),
38        }
39    }
40
41    /// 📂 List directory with task awareness
42    pub fn list_smart(
43        &self,
44        path: &Path,
45        context: &TaskContext,
46        max_depth: Option<usize>,
47    ) -> Result<SmartLSResponse> {
48        // Scan directory
49        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        // Score and categorize files
60        let scored_entries = self.score_and_categorize(&nodes, context)?;
61
62        // Split into primary and secondary based on relevance
63        let (primary, secondary) = self.split_by_relevance(&scored_entries, context);
64
65        // Calculate token savings
66        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        // Generate context summary and suggestions
72        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    /// Score and categorize directory entries
85    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        // Sort by relevance score
110        entries.sort_by(|a, b| b.relevance.score.partial_cmp(&a.relevance.score).unwrap());
111
112        Ok(entries)
113    }
114
115    /// Split entries by relevance threshold
116    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            // Entries below 60% of threshold are filtered out
131        }
132
133        // Limit results if specified
134        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    /// Generate suggested actions for a file
143    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            // Task-specific suggestions
163            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    /// Estimate tokens for all nodes
200    fn estimate_tokens_for_all(&self, nodes: &[FileNode]) -> usize {
201        // Rough estimation based on file count and average metadata size
202        nodes.len() * 50 // ~50 tokens per file entry
203    }
204
205    /// Estimate tokens for smart entries
206    fn estimate_tokens_for_entries(&self, entries: &[SmartDirEntry]) -> usize {
207        // Smart entries have more metadata but are filtered
208        entries.len() * 30 // ~30 tokens per smart entry
209    }
210
211    /// Generate context summary
212    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    /// Generate proactive suggestions
225    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        // Suggest related tools based on file types found
240        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    // use std::path::PathBuf;  // Commented out as unused
281
282    #[test]
283    fn test_smart_ls_creation() {
284        let _smart_ls = SmartLS::new();
285        // Basic creation test - verify it was created
286        // SmartLS structure verified by successful creation
287    }
288}