1use 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
13pub enum WorkContext {
14 Coding {
16 language: String,
17 focus_file: PathBuf,
18 },
19 Debugging {
21 error_pattern: String,
22 files: Vec<PathBuf>,
23 },
24 Refactoring { pattern: String, scope: PathBuf },
26 Exploring {
28 depth: usize,
29 areas_visited: Vec<PathBuf>,
30 },
31 Testing {
33 test_files: Vec<PathBuf>,
34 target_files: Vec<PathBuf>,
35 },
36 Documenting { doc_type: String, target: PathBuf },
38 Optimizing {
40 metrics: Vec<String>,
41 hotspots: Vec<PathBuf>,
42 },
43 Hunting {
45 query: String,
46 found_locations: Vec<PathBuf>,
47 },
48 Building {
50 build_system: String,
51 targets: Vec<String>,
52 },
53 VersionControl {
55 operation: String,
56 changed_files: Vec<PathBuf>,
57 },
58}
59
60#[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#[derive(Debug, Clone)]
72pub struct StContextTracker {
73 current_context: Arc<RwLock<Option<WorkContext>>>,
75 operation_history: Arc<RwLock<VecDeque<ContextualOperation>>>,
77 #[allow(dead_code)]
79 patterns: Arc<RwLock<HashMap<String, Vec<String>>>>,
80 project_knowledge: Arc<RwLock<ProjectKnowledge>>,
82}
83
84#[derive(Debug, Clone, Default, Serialize, Deserialize)]
85pub struct ProjectKnowledge {
86 pub key_files: Vec<PathBuf>,
88 pub common_searches: HashMap<String, usize>,
90 pub hot_directories: HashMap<PathBuf, usize>,
92 pub build_commands: Vec<String>,
94 pub test_patterns: Vec<String>,
96 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 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 let recent_ops: Vec<_> = history.iter().take(10).collect();
129
130 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 if search_count >= 3 {
157 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 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 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 Ok(WorkContext::Exploring {
188 depth: 5,
189 areas_visited: recent_ops.iter().map(|op| op.path.clone()).collect(),
190 })
191 } else {
192 Ok(WorkContext::Exploring {
194 depth: 3,
195 areas_visited: vec![],
196 })
197 }
198 }
199
200 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 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 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 pub fn record_operation(&self, op: ContextualOperation) -> Result<()> {
301 let mut history = self.operation_history.write().unwrap();
302
303 history.push_front(op.clone());
305 if history.len() > 50 {
306 history.pop_back();
307 }
308
309 let mut knowledge = self.project_knowledge.write().unwrap();
311
312 let dir = op.path.parent().unwrap_or(&op.path);
314 *knowledge.hot_directories.entry(dir.to_owned()).or_insert(0) += 1;
315
316 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 self.update_context()?;
326
327 Ok(())
328 }
329
330 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 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(), "--mode".to_string(),
356 "ai".to_string(),
357 "--compress".to_string(),
358 ] }
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 ] }
383 _ => vec!["--depth".to_string(), "0".to_string()], }
385 }
386
387 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 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 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 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 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
459pub 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 pub fn build(&self, intent: &str) -> Vec<String> {
475 let mut args = self.base_args.clone();
476
477 let context_args = self.tracker.get_optimal_args(intent);
479 args.extend(context_args);
480
481 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 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 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 let context = tracker.analyze_context().unwrap();
548 match context {
549 WorkContext::Hunting { .. } => {} _ => panic!("Expected Hunting context"),
551 }
552
553 let suggestions = tracker.get_suggestions(Path::new("."));
555 assert!(!suggestions.is_empty());
556 }
557}