346 lines
8.7 KiB
Rust
346 lines
8.7 KiB
Rust
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",
|
|
},
|
|
];
|
|
|
|
/// 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<CommandSpec> {
|
|
COMMANDS.to_vec()
|
|
}
|
|
|
|
/// Generate keyword suggestions for the given input.
|
|
pub fn suggestions(input: &str) -> Vec<CommandSpec> {
|
|
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
|
|
}
|