feat(commands): unified slash-command namespace with template substitution (closes #435)
Three sources share the /foo namespace with clear precedence: 1. Native built-ins (match block in mod.rs) 2. User-config commands (~/.deepseek/commands/*.md) — checked first 3. Skills (~/.deepseek/skills/) — new fallback in the _ arm Template substitution: $1, $2, $ARGUMENTS are replaced in user-command and skill content before the message is sent. Existing exact-match and alias behavior is unchanged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -567,6 +567,11 @@ pub fn execute(cmd: &str, app: &mut App) -> CommandResult {
|
||||
),
|
||||
|
||||
_ => {
|
||||
// Third source: skills (lowest precedence after native and user-config).
|
||||
// Try to run a skill whose name matches the command.
|
||||
if skills::run_skill_by_name(app, command, arg).is_some() {
|
||||
return skills::run_skill_by_name(app, command, arg).unwrap();
|
||||
}
|
||||
let suggestions = suggest_command_names(command, 3);
|
||||
if suggestions.is_empty() {
|
||||
CommandResult::error(format!(
|
||||
|
||||
@@ -78,6 +78,18 @@ pub fn list_skills(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
|
||||
/// Run a specific skill — activates skill for next user message, or
|
||||
/// dispatches a sub-command (`install`, `update`, `uninstall`, `trust`).
|
||||
/// Try to run a skill by exact name (used for unified slash-command namespace, #435).
|
||||
/// Returns None when no skill with that name exists, so the caller can try other sources.
|
||||
pub fn run_skill_by_name(app: &mut App, name: &str, _arg: Option<&str>) -> Option<CommandResult> {
|
||||
let skills_dir = app.skills_dir.clone();
|
||||
let registry = crate::skills::SkillRegistry::discover(&skills_dir);
|
||||
if registry.get(name).is_some() {
|
||||
Some(activate_skill(app, name))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_skill(app: &mut App, name: Option<&str>) -> CommandResult {
|
||||
let raw = match name {
|
||||
Some(n) => n.trim(),
|
||||
|
||||
@@ -62,18 +62,28 @@ pub fn load_user_commands() -> Vec<(String, String)> {
|
||||
/// The `input` should be the full command string including the `/`
|
||||
/// prefix (e.g. `/mycmd` or `/mycmd with args`). Only exact matches
|
||||
/// on the command name are considered (no partial/alias matching).
|
||||
/// Substitute $1, $2, $ARGUMENTS placeholders in a command template.
|
||||
fn apply_template(template: &str, args: &str) -> String {
|
||||
let positional: Vec<&str> = args.split_whitespace().collect();
|
||||
let mut result = template.replace("$ARGUMENTS", args);
|
||||
for (i, arg) in positional.iter().enumerate() {
|
||||
result = result.replace(&format!("${}", i + 1), arg);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub fn try_dispatch_user_command(_app: &mut App, input: &str) -> Option<CommandResult> {
|
||||
let parts: Vec<&str> = input.trim().splitn(2, ' ').collect();
|
||||
let command = parts[0].to_lowercase();
|
||||
let command = command.strip_prefix('/').unwrap_or(&command);
|
||||
let args = parts.get(1).copied().unwrap_or("").trim();
|
||||
|
||||
let user_commands = load_user_commands();
|
||||
|
||||
for (name, content) in &user_commands {
|
||||
if name == command {
|
||||
return Some(CommandResult::action(AppAction::SendMessage(
|
||||
content.clone(),
|
||||
)));
|
||||
let message = apply_template(content, args);
|
||||
return Some(CommandResult::action(AppAction::SendMessage(message)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user