Skip to main content

mur_chat/
autocomplete.rs

1//! Autocomplete — real-time workflow/command suggestions (Team tier).
2
3use mur_core::workflow::parser;
4use serde::{Deserialize, Serialize};
5
6/// Autocomplete suggestion.
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct Suggestion {
9    pub text: String,
10    pub description: String,
11    pub category: SuggestionCategory,
12}
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
15#[serde(rename_all = "snake_case")]
16pub enum SuggestionCategory {
17    Workflow,
18    Command,
19    Variable,
20}
21
22/// Autocomplete engine.
23pub struct AutocompleteEngine {
24    workflows: Vec<(String, String)>, // (id, description)
25    commands: Vec<(String, String)>,
26}
27
28impl AutocompleteEngine {
29    pub fn new() -> Self {
30        let workflows = parser::load_all_workflows()
31            .unwrap_or_default()
32            .into_iter()
33            .map(|w| (w.id, w.description))
34            .collect();
35
36        let commands = vec![
37            ("/run".into(), "Execute a workflow".into()),
38            ("/workflows".into(), "List workflows".into()),
39            ("/status".into(), "Check daemon status".into()),
40            ("/audit".into(), "View audit log".into()),
41            ("/stop".into(), "Stop execution".into()),
42            ("/help".into(), "Show help".into()),
43        ];
44
45        Self {
46            workflows,
47            commands,
48        }
49    }
50
51    /// Refresh workflow list.
52    pub fn refresh(&mut self) {
53        self.workflows = parser::load_all_workflows()
54            .unwrap_or_default()
55            .into_iter()
56            .map(|w| (w.id, w.description))
57            .collect();
58    }
59
60    /// Get suggestions for a prefix.
61    pub fn suggest(&self, prefix: &str, limit: usize) -> Vec<Suggestion> {
62        let prefix_lower = prefix.to_lowercase();
63        let mut suggestions = Vec::new();
64
65        // Match commands first
66        if prefix.starts_with('/') {
67            for (cmd, desc) in &self.commands {
68                if cmd.to_lowercase().starts_with(&prefix_lower) {
69                    suggestions.push(Suggestion {
70                        text: cmd.clone(),
71                        description: desc.clone(),
72                        category: SuggestionCategory::Command,
73                    });
74                }
75            }
76        }
77
78        // Match workflows
79        for (id, desc) in &self.workflows {
80            if id.to_lowercase().contains(&prefix_lower) {
81                suggestions.push(Suggestion {
82                    text: id.clone(),
83                    description: desc.clone(),
84                    category: SuggestionCategory::Workflow,
85                });
86            }
87        }
88
89        suggestions.truncate(limit);
90        suggestions
91    }
92}
93
94impl Default for AutocompleteEngine {
95    fn default() -> Self {
96        Self::new()
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn test_command_suggestions() {
106        let engine = AutocompleteEngine::new();
107        let results = engine.suggest("/ru", 5);
108        assert!(!results.is_empty());
109        assert!(results.iter().any(|s| s.text == "/run"));
110    }
111
112    #[test]
113    fn test_empty_prefix() {
114        let engine = AutocompleteEngine::new();
115        let results = engine.suggest("", 10);
116        // Should match all workflows (if any loaded)
117        // Commands won't match since prefix doesn't start with /
118        assert!(results.len() <= 10);
119    }
120}