Skip to main content

st/
st_context_aware.rs

1// ST Context-Aware System - The helper who knows what you need!
2// "Like a good roadie who hands you the right guitar at the right time" - The Cheet 🎸
3
4use anyhow::{Context, Result};
5use serde::{Deserialize, Serialize};
6use std::collections::{HashMap, VecDeque};
7use std::path::{Path, PathBuf};
8use std::sync::{Arc, RwLock};
9use std::time::SystemTime;
10
11/// Context types that ST tracks
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
13pub enum WorkContext {
14    /// Writing new code
15    Coding {
16        language: String,
17        focus_file: PathBuf,
18    },
19    /// Debugging/fixing issues
20    Debugging {
21        error_pattern: String,
22        files: Vec<PathBuf>,
23    },
24    /// Refactoring code
25    Refactoring { pattern: String, scope: PathBuf },
26    /// Exploring/understanding codebase
27    Exploring {
28        depth: usize,
29        areas_visited: Vec<PathBuf>,
30    },
31    /// Testing/validation
32    Testing {
33        test_files: Vec<PathBuf>,
34        target_files: Vec<PathBuf>,
35    },
36    /// Documentation
37    Documenting { doc_type: String, target: PathBuf },
38    /// Performance optimization
39    Optimizing {
40        metrics: Vec<String>,
41        hotspots: Vec<PathBuf>,
42    },
43    /// Searching for something specific
44    Hunting {
45        query: String,
46        found_locations: Vec<PathBuf>,
47    },
48    /// Building/compilation
49    Building {
50        build_system: String,
51        targets: Vec<String>,
52    },
53    /// Git operations
54    VersionControl {
55        operation: String,
56        changed_files: Vec<PathBuf>,
57    },
58}
59
60/// A single operation in context
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct ContextualOperation {
63    pub timestamp: SystemTime,
64    pub operation: String,
65    pub path: PathBuf,
66    pub result_summary: String,
67    pub context_hints: Vec<String>,
68}
69
70/// Smart context tracker
71#[derive(Debug, Clone)]
72pub struct StContextTracker {
73    /// Current work context
74    current_context: Arc<RwLock<Option<WorkContext>>>,
75    /// Recent operations (last 50)
76    operation_history: Arc<RwLock<VecDeque<ContextualOperation>>>,
77    /// Pattern recognition cache
78    #[allow(dead_code)]
79    patterns: Arc<RwLock<HashMap<String, Vec<String>>>>,
80    /// Project-specific knowledge
81    project_knowledge: Arc<RwLock<ProjectKnowledge>>,
82}
83
84#[derive(Debug, Clone, Default, Serialize, Deserialize)]
85pub struct ProjectKnowledge {
86    /// Key files in the project
87    pub key_files: Vec<PathBuf>,
88    /// Common search patterns
89    pub common_searches: HashMap<String, usize>,
90    /// Frequently accessed directories
91    pub hot_directories: HashMap<PathBuf, usize>,
92    /// Known build commands
93    pub build_commands: Vec<String>,
94    /// Test patterns
95    pub test_patterns: Vec<String>,
96    /// Documentation locations
97    pub doc_locations: Vec<PathBuf>,
98}
99
100impl Default for StContextTracker {
101    fn default() -> Self {
102        Self::new()
103    }
104}
105
106impl StContextTracker {
107    pub fn new() -> Self {
108        Self {
109            current_context: Arc::new(RwLock::new(None)),
110            operation_history: Arc::new(RwLock::new(VecDeque::with_capacity(50))),
111            patterns: Arc::new(RwLock::new(HashMap::new())),
112            project_knowledge: Arc::new(RwLock::new(ProjectKnowledge::default())),
113        }
114    }
115
116    /// Analyze recent operations to determine context
117    pub fn analyze_context(&self) -> Result<WorkContext> {
118        let history = self.operation_history.read().unwrap();
119
120        if history.is_empty() {
121            return Ok(WorkContext::Exploring {
122                depth: 3,
123                areas_visited: vec![],
124            });
125        }
126
127        // Look at recent operations
128        let recent_ops: Vec<_> = history.iter().take(10).collect();
129
130        // Count operation types
131        let mut search_count = 0;
132        let mut edit_count = 0;
133        let mut read_count = 0;
134        let mut test_count = 0;
135        let mut _build_count = 0;
136
137        for op in &recent_ops {
138            if op.operation.contains("search") || op.operation.contains("grep") {
139                search_count += 1;
140            }
141            if op.operation.contains("edit") || op.operation.contains("write") {
142                edit_count += 1;
143            }
144            if op.operation.contains("read") || op.operation.contains("view") {
145                read_count += 1;
146            }
147            if op.path.to_string_lossy().contains("test") {
148                test_count += 1;
149            }
150            if op.operation.contains("build") || op.operation.contains("compile") {
151                _build_count += 1;
152            }
153        }
154
155        // Determine context based on patterns
156        if search_count >= 3 {
157            // Multiple searches = hunting for something
158            let query = recent_ops
159                .iter()
160                .find(|op| op.operation.contains("search"))
161                .map(|op| op.operation.clone())
162                .unwrap_or_default();
163
164            Ok(WorkContext::Hunting {
165                query,
166                found_locations: vec![],
167            })
168        } else if edit_count >= 2 && test_count >= 1 {
169            // Edits + tests = active development
170            let language = Self::detect_language(&recent_ops[0].path);
171            Ok(WorkContext::Coding {
172                language,
173                focus_file: recent_ops[0].path.clone(),
174            })
175        } else if test_count >= 2 {
176            // Lots of test activity
177            Ok(WorkContext::Testing {
178                test_files: recent_ops
179                    .iter()
180                    .filter(|op| op.path.to_string_lossy().contains("test"))
181                    .map(|op| op.path.clone())
182                    .collect(),
183                target_files: vec![],
184            })
185        } else if read_count >= 4 {
186            // Lots of reading = exploring
187            Ok(WorkContext::Exploring {
188                depth: 5,
189                areas_visited: recent_ops.iter().map(|op| op.path.clone()).collect(),
190            })
191        } else {
192            // Default to exploring
193            Ok(WorkContext::Exploring {
194                depth: 3,
195                areas_visited: vec![],
196            })
197        }
198    }
199
200    /// Get smart suggestions based on context
201    pub fn get_suggestions(&self, _current_path: &Path) -> Vec<String> {
202        let context = self.current_context.read().unwrap();
203        let knowledge = self.project_knowledge.read().unwrap();
204
205        match context.as_ref() {
206            Some(WorkContext::Coding {
207                language,
208                focus_file,
209            }) => vec![
210                format!(
211                    "πŸ’‘ Working on {}? Try: st --mode relations --focus {}",
212                    language,
213                    focus_file.display()
214                ),
215                format!(
216                    "πŸ§ͺ Run tests: st --search test --type {}",
217                    Self::lang_to_ext(language)
218                ),
219                format!(
220                    "πŸ“Š See impact: st --mode quantum-semantic {}",
221                    focus_file.parent().unwrap_or(Path::new(".")).display()
222                ),
223            ],
224
225            Some(WorkContext::Debugging { error_pattern, .. }) => vec![
226                format!(
227                    "πŸ” Search for error: st --search \"{}\" --mode ai",
228                    error_pattern
229                ),
230                format!("πŸ“ˆ Recent changes: st --newer-than 1 --sort newest"),
231                format!("🌳 Check dependencies: st --mode relations"),
232            ],
233
234            Some(WorkContext::Exploring {
235                depth,
236                areas_visited,
237            }) => {
238                let mut suggestions = vec![
239                    format!("πŸ—ΊοΈ Get overview: st --mode summary-ai --depth {}", depth),
240                    format!("🧭 Semantic map: st --mode semantic"),
241                ];
242
243                // Suggest unexplored areas
244                if let Some(hot_dir) = knowledge
245                    .hot_directories
246                    .iter()
247                    .filter(|(path, _)| !areas_visited.contains(path))
248                    .max_by_key(|(_, count)| *count)
249                    .map(|(path, _)| path)
250                {
251                    suggestions.push(format!("πŸ”₯ Check hot area: st {}", hot_dir.display()));
252                }
253
254                suggestions
255            }
256
257            Some(WorkContext::Testing { .. }) => vec![
258                format!("πŸ§ͺ Run all tests: st --search \"test_\" --type rs"),
259                format!("πŸ“Š Coverage gaps: st --mode waste tests/"),
260                format!("πŸ”— Test dependencies: st --mode relations --focus tests/"),
261            ],
262
263            Some(WorkContext::Hunting {
264                query,
265                found_locations,
266            }) => {
267                let mut suggestions = vec![format!(
268                    "🎯 Refine search: st --search \"{}\" --type rs",
269                    query
270                )];
271
272                if !found_locations.is_empty() {
273                    suggestions.push(format!(
274                        "πŸ“ Focus area: st --mode ai {}",
275                        found_locations[0].display()
276                    ));
277                }
278
279                // Suggest similar past searches
280                if let Some(similar) = knowledge
281                    .common_searches
282                    .keys()
283                    .find(|s| s.contains(query) || query.contains(s.as_str()))
284                {
285                    suggestions.push(format!("πŸ’­ Similar search: st --search \"{}\"", similar));
286                }
287
288                suggestions
289            }
290
291            _ => vec![
292                "🌟 Quick overview: st --mode summary-ai".to_string(),
293                "πŸ” Search code: st --search \"pattern\" --type rs".to_string(),
294                "πŸ“Š See structure: st --mode semantic".to_string(),
295            ],
296        }
297    }
298
299    /// Record an operation
300    pub fn record_operation(&self, op: ContextualOperation) -> Result<()> {
301        let mut history = self.operation_history.write().unwrap();
302
303        // Add to history
304        history.push_front(op.clone());
305        if history.len() > 50 {
306            history.pop_back();
307        }
308
309        // Update knowledge
310        let mut knowledge = self.project_knowledge.write().unwrap();
311
312        // Track hot directories
313        let dir = op.path.parent().unwrap_or(&op.path);
314        *knowledge.hot_directories.entry(dir.to_owned()).or_insert(0) += 1;
315
316        // Track searches
317        if op.operation.contains("search") {
318            if let Some(query) = op.operation.split("search").nth(1) {
319                let query = query.trim().to_string();
320                *knowledge.common_searches.entry(query).or_insert(0) += 1;
321            }
322        }
323
324        // Update context based on new operation
325        self.update_context()?;
326
327        Ok(())
328    }
329
330    /// Update context based on recent operations
331    fn update_context(&self) -> Result<()> {
332        let new_context = self.analyze_context()?;
333        let mut current = self.current_context.write().unwrap();
334        *current = Some(new_context);
335        Ok(())
336    }
337
338    /// Get optimal ST arguments for current context
339    pub fn get_optimal_args(&self, _base_command: &str) -> Vec<String> {
340        let context = self.current_context.read().unwrap();
341
342        match context.as_ref() {
343            Some(WorkContext::Coding { .. }) => {
344                vec![
345                    "--depth".to_string(),
346                    "5".to_string(),
347                    "--mode".to_string(),
348                    "ai".to_string(),
349                ]
350            }
351            Some(WorkContext::Debugging { .. }) => {
352                vec![
353                    "--depth".to_string(),
354                    "0".to_string(), // Auto depth
355                    "--mode".to_string(),
356                    "ai".to_string(),
357                    "--compress".to_string(),
358                ] // Easier to scan
359            }
360            Some(WorkContext::Exploring { depth, .. }) => {
361                vec![
362                    "--depth".to_string(),
363                    depth.to_string(),
364                    "--mode".to_string(),
365                    "semantic".to_string(),
366                ]
367            }
368            Some(WorkContext::Testing { .. }) => {
369                vec![
370                    "--search".to_string(),
371                    "test".to_string(),
372                    "--mode".to_string(),
373                    "relations".to_string(),
374                ]
375            }
376            Some(WorkContext::Hunting { .. }) => {
377                vec![
378                    "--mode".to_string(),
379                    "ai".to_string(),
380                    "--stream".to_string(),
381                ] // For large searches
382            }
383            _ => vec!["--depth".to_string(), "0".to_string()], // Auto
384        }
385    }
386
387    /// Detect programming language from path
388    fn detect_language(path: &Path) -> String {
389        match path.extension().and_then(|s| s.to_str()) {
390            Some("rs") => "rust".to_string(),
391            Some("py") => "python".to_string(),
392            Some("js") | Some("jsx") => "javascript".to_string(),
393            Some("ts") | Some("tsx") => "typescript".to_string(),
394            Some("go") => "go".to_string(),
395            Some("java") => "java".to_string(),
396            Some("cpp") | Some("cc") | Some("cxx") => "cpp".to_string(),
397            Some("c") | Some("h") => "c".to_string(),
398            _ => "unknown".to_string(),
399        }
400    }
401
402    fn lang_to_ext(lang: &str) -> &str {
403        match lang {
404            "rust" => "rs",
405            "python" => "py",
406            "javascript" => "js",
407            "typescript" => "ts",
408            "go" => "go",
409            "java" => "java",
410            "cpp" => "cpp",
411            "c" => "c",
412            _ => "*",
413        }
414    }
415
416    /// Save context to disk
417    pub fn save_context(&self, path: &Path) -> Result<()> {
418        let context_file = path.join(".st_context.json");
419
420        let data = serde_json::json!({
421            "current_context": self.current_context.read().unwrap().clone(),
422            "project_knowledge": self.project_knowledge.read().unwrap().clone(),
423            "history": self.operation_history.read().unwrap().clone(),
424        });
425
426        std::fs::write(context_file, serde_json::to_string_pretty(&data)?)
427            .context("Failed to save context")?;
428
429        Ok(())
430    }
431
432    /// Load context from disk
433    pub fn load_context(&self, path: &Path) -> Result<()> {
434        let context_file = path.join(".st_context.json");
435
436        if context_file.exists() {
437            let data = std::fs::read_to_string(context_file)?;
438            let json: serde_json::Value = serde_json::from_str(&data)?;
439
440            // Restore context
441            if let Some(ctx) = json.get("current_context") {
442                if let Ok(context) = serde_json::from_value::<WorkContext>(ctx.clone()) {
443                    *self.current_context.write().unwrap() = Some(context);
444                }
445            }
446
447            // Restore knowledge
448            if let Some(know) = json.get("project_knowledge") {
449                if let Ok(knowledge) = serde_json::from_value::<ProjectKnowledge>(know.clone()) {
450                    *self.project_knowledge.write().unwrap() = knowledge;
451                }
452            }
453        }
454
455        Ok(())
456    }
457}
458
459/// Context-aware ST command builder
460pub struct ContextualStCommand {
461    tracker: Arc<StContextTracker>,
462    base_args: Vec<String>,
463}
464
465impl ContextualStCommand {
466    pub fn new(tracker: Arc<StContextTracker>) -> Self {
467        Self {
468            tracker,
469            base_args: vec![],
470        }
471    }
472
473    /// Build command with context awareness
474    pub fn build(&self, intent: &str) -> Vec<String> {
475        let mut args = self.base_args.clone();
476
477        // Get optimal args based on context
478        let context_args = self.tracker.get_optimal_args(intent);
479        args.extend(context_args);
480
481        // Add specific args based on intent
482        match intent {
483            "explore" => {
484                if !args.contains(&"--mode".to_string()) {
485                    args.extend(vec!["--mode".to_string(), "summary-ai".to_string()]);
486                }
487            }
488            "debug" => {
489                args.extend(vec!["--compress".to_string()]);
490            }
491            "document" => {
492                args.extend(vec!["--mode".to_string(), "function-markdown".to_string()]);
493            }
494            _ => {}
495        }
496
497        args
498    }
499
500    /// Get suggestions for next command
501    pub fn suggest_next(&self) -> Vec<String> {
502        self.tracker.get_suggestions(Path::new("."))
503    }
504}
505
506#[cfg(test)]
507mod tests {
508    use super::*;
509
510    #[test]
511    #[ignore = "Hangs - needs investigation"]
512    fn test_context_detection() {
513        let tracker = StContextTracker::new();
514
515        // Simulate some operations
516        tracker
517            .record_operation(ContextualOperation {
518                timestamp: SystemTime::now(),
519                operation: "search TODO".to_string(),
520                path: PathBuf::from("src/main.rs"),
521                result_summary: "Found 5 matches".to_string(),
522                context_hints: vec!["searching".to_string()],
523            })
524            .unwrap();
525
526        tracker
527            .record_operation(ContextualOperation {
528                timestamp: SystemTime::now(),
529                operation: "search FIXME".to_string(),
530                path: PathBuf::from("src/lib.rs"),
531                result_summary: "Found 2 matches".to_string(),
532                context_hints: vec!["searching".to_string()],
533            })
534            .unwrap();
535
536        tracker
537            .record_operation(ContextualOperation {
538                timestamp: SystemTime::now(),
539                operation: "search bug".to_string(),
540                path: PathBuf::from("tests/test.rs"),
541                result_summary: "Found 1 match".to_string(),
542                context_hints: vec!["searching".to_string()],
543            })
544            .unwrap();
545
546        // Should detect hunting context
547        let context = tracker.analyze_context().unwrap();
548        match context {
549            WorkContext::Hunting { .. } => {} // Expected - test passes
550            _ => panic!("Expected Hunting context"),
551        }
552
553        // Get suggestions
554        let suggestions = tracker.get_suggestions(Path::new("."));
555        assert!(!suggestions.is_empty());
556    }
557}