pub mod registry; pub use registry::{AppCommand, CommandRegistry}; // Command catalog and lookup utilities for the command palette. /// Metadata describing a single command keyword. #[derive(Debug, Clone, Copy)] pub struct CommandSpec { pub keyword: &'static str, pub description: &'static str, } const COMMANDS: &[CommandSpec] = &[ CommandSpec { keyword: "quit", description: "Exit the application", }, CommandSpec { keyword: "q", description: "Close the active file", }, CommandSpec { keyword: "w", description: "Save the active file", }, CommandSpec { keyword: "write", description: "Alias for w", }, CommandSpec { keyword: "clear", description: "Clear the conversation", }, CommandSpec { keyword: "c", description: "Alias for clear", }, CommandSpec { keyword: "save", description: "Alias for w", }, CommandSpec { keyword: "wq", description: "Save and close the active file", }, CommandSpec { keyword: "x", description: "Alias for wq", }, CommandSpec { keyword: "load", description: "Load a saved conversation", }, CommandSpec { keyword: "o", description: "Alias for load", }, CommandSpec { keyword: "open", description: "Open a file in the code view", }, CommandSpec { keyword: "create", description: "Create a file (creates missing directories)", }, CommandSpec { keyword: "close", description: "Close the active code view", }, CommandSpec { keyword: "mode", description: "Switch operating mode (chat/code)", }, CommandSpec { keyword: "code", description: "Switch to code mode", }, CommandSpec { keyword: "chat", description: "Switch to chat mode", }, CommandSpec { keyword: "tools", description: "List available tools in current mode", }, CommandSpec { keyword: "sessions", description: "List saved sessions", }, CommandSpec { keyword: "session save", description: "Save the current conversation", }, CommandSpec { keyword: "help", description: "Open the help overlay", }, CommandSpec { keyword: "h", description: "Alias for help", }, CommandSpec { keyword: "model", description: "Select a model", }, CommandSpec { keyword: "provider", description: "Switch provider or set its mode", }, CommandSpec { keyword: "cloud setup", description: "Configure Ollama Cloud credentials", }, CommandSpec { keyword: "cloud status", description: "Check Ollama Cloud connectivity", }, CommandSpec { keyword: "cloud models", description: "List models available in Ollama Cloud", }, CommandSpec { keyword: "cloud logout", description: "Remove stored Ollama Cloud credentials", }, CommandSpec { keyword: "model info", description: "Show detailed information for a model", }, CommandSpec { keyword: "model refresh", description: "Refresh cached model information", }, CommandSpec { keyword: "model details", description: "Show details for the active model", }, CommandSpec { keyword: "m", description: "Alias for model", }, CommandSpec { keyword: "models info", description: "Prefetch detailed information for all models", }, CommandSpec { keyword: "models --local", description: "Open model picker focused on local models", }, CommandSpec { keyword: "models --cloud", description: "Open model picker focused on cloud models", }, CommandSpec { keyword: "models --available", description: "Open model picker showing available models", }, CommandSpec { keyword: "new", description: "Start a new conversation", }, CommandSpec { keyword: "n", description: "Alias for new", }, CommandSpec { keyword: "theme", description: "Switch theme", }, CommandSpec { keyword: "themes", description: "List available themes", }, CommandSpec { keyword: "tutorial", description: "Show keybinding tutorial", }, CommandSpec { keyword: "reload", description: "Reload configuration and themes", }, CommandSpec { keyword: "markdown", description: "Toggle markdown rendering", }, CommandSpec { keyword: "e", description: "Edit a file", }, CommandSpec { keyword: "edit", description: "Alias for edit", }, CommandSpec { keyword: "ls", description: "List directory contents", }, CommandSpec { keyword: "privacy-enable", description: "Enable a privacy-sensitive tool", }, CommandSpec { keyword: "privacy-disable", description: "Disable a privacy-sensitive tool", }, CommandSpec { keyword: "privacy-clear", description: "Clear stored secure data", }, CommandSpec { keyword: "agent", description: "Enable agent mode for autonomous task execution", }, CommandSpec { keyword: "stop-agent", description: "Stop the running agent", }, CommandSpec { keyword: "agent status", description: "Show current agent status", }, CommandSpec { keyword: "agent start", description: "Arm the agent for the next request", }, CommandSpec { keyword: "agent stop", description: "Stop the running agent", }, CommandSpec { keyword: "layout save", description: "Persist the current pane layout", }, CommandSpec { keyword: "layout load", description: "Restore the last saved pane layout", }, CommandSpec { keyword: "files", description: "Toggle the files panel", }, CommandSpec { keyword: "explorer", description: "Alias for files", }, CommandSpec { keyword: "debug log", description: "Toggle the debug log panel", }, ]; /// Return the static catalog of commands. pub fn all() -> &'static [CommandSpec] { COMMANDS } /// Return the default suggestion list (all command keywords). pub fn default_suggestions() -> Vec { COMMANDS.to_vec() } /// Generate keyword suggestions for the given input. pub fn suggestions(input: &str) -> Vec { let trimmed = input.trim(); if trimmed.is_empty() { return default_suggestions(); } let mut matches: Vec<(usize, usize, CommandSpec)> = COMMANDS .iter() .filter_map(|spec| { match_score(spec.keyword, trimmed).map(|score| (score.0, score.1, *spec)) }) .collect(); if matches.is_empty() { return default_suggestions(); } matches.sort_by(|a, b| { a.0.cmp(&b.0) .then(a.1.cmp(&b.1)) .then(a.2.keyword.cmp(b.2.keyword)) }); matches.into_iter().map(|(_, _, spec)| spec).collect() } pub fn match_score(candidate: &str, query: &str) -> Option<(usize, usize)> { let query = query.trim(); if query.is_empty() { return Some((usize::MAX, candidate.len())); } let candidate_normalized = candidate.trim().to_lowercase(); if candidate_normalized.is_empty() { return None; } let query_normalized = query.to_lowercase(); if candidate_normalized == query_normalized { Some((0, candidate.len())) } else if candidate_normalized.starts_with(&query_normalized) { Some((1, 0)) } else if let Some(pos) = candidate_normalized.find(&query_normalized) { Some((2, pos)) } else if is_subsequence(&candidate_normalized, &query_normalized) { Some((3, candidate.len())) } else { None } } #[cfg(test)] mod tests { use super::*; #[test] fn suggestions_prioritize_agent_start() { let results = suggestions("agent st"); assert!(!results.is_empty()); assert_eq!(results[0].keyword, "agent start"); assert!(results.iter().any(|spec| spec.keyword == "agent stop")); } } fn is_subsequence(text: &str, pattern: &str) -> bool { if pattern.is_empty() { return true; } let mut pattern_chars = pattern.chars(); let mut current = match pattern_chars.next() { Some(ch) => ch, None => return true, }; for ch in text.chars() { if ch == current { match pattern_chars.next() { Some(next_ch) => current = next_ch, None => return true, } } } false }