Harvest PR #2851: command strategy registry
Harvested from PR #2851 by @aboimpinto Co-authored-by: Paulo Aboim Pinto <aboimpinto@gmail.com> Co-authored-by: Hunter Bown <hmbown@gmail.com>
This commit is contained in:
@@ -5,8 +5,135 @@ pub mod config;
|
||||
mod status;
|
||||
|
||||
use crate::commands::CommandResult;
|
||||
use crate::commands::traits::{Command, CommandGroup, CommandInfo, FunctionCommand};
|
||||
use crate::localization::MessageId;
|
||||
use crate::tui::app::App;
|
||||
|
||||
pub struct ConfigCommands;
|
||||
|
||||
impl CommandGroup for ConfigCommands {
|
||||
fn commands(&self) -> Vec<Box<dyn Command>> {
|
||||
vec![
|
||||
Box::new(FunctionCommand::new(&CONFIG_INFO, run_config)),
|
||||
Box::new(FunctionCommand::new(&SIDEBAR_INFO, run_sidebar)),
|
||||
Box::new(FunctionCommand::new(&SETTINGS_INFO, run_settings)),
|
||||
Box::new(FunctionCommand::new(&STATUS_INFO, run_status)),
|
||||
Box::new(FunctionCommand::new(&STATUSLINE_INFO, run_statusline)),
|
||||
Box::new(FunctionCommand::new(&MODE_INFO, run_mode)),
|
||||
Box::new(FunctionCommand::new(&THEME_INFO, run_theme)),
|
||||
Box::new(FunctionCommand::new(&VERBOSE_INFO, run_verbose)),
|
||||
Box::new(FunctionCommand::new(&TRUST_INFO, run_trust)),
|
||||
Box::new(FunctionCommand::new(&LOGOUT_INFO, run_logout)),
|
||||
Box::new(FunctionCommand::new(&SLOP_INFO, run_slop)),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
static CONFIG_INFO: CommandInfo = CommandInfo {
|
||||
name: "config",
|
||||
aliases: &[],
|
||||
usage: "/config",
|
||||
description_id: MessageId::CmdConfigDescription,
|
||||
};
|
||||
static SIDEBAR_INFO: CommandInfo = CommandInfo {
|
||||
name: "sidebar",
|
||||
aliases: &[],
|
||||
usage: "/sidebar [on|off|auto|work|tasks|agents|context] [--save]",
|
||||
description_id: MessageId::CmdSidebarDescription,
|
||||
};
|
||||
static SETTINGS_INFO: CommandInfo = CommandInfo {
|
||||
name: "settings",
|
||||
aliases: &[],
|
||||
usage: "/settings",
|
||||
description_id: MessageId::CmdSettingsDescription,
|
||||
};
|
||||
static STATUS_INFO: CommandInfo = CommandInfo {
|
||||
name: "status",
|
||||
aliases: &[],
|
||||
usage: "/status",
|
||||
description_id: MessageId::CmdStatusDescription,
|
||||
};
|
||||
static STATUSLINE_INFO: CommandInfo = CommandInfo {
|
||||
name: "statusline",
|
||||
aliases: &[],
|
||||
usage: "/statusline",
|
||||
description_id: MessageId::CmdStatuslineDescription,
|
||||
};
|
||||
static MODE_INFO: CommandInfo = CommandInfo {
|
||||
name: "mode",
|
||||
aliases: &["jihua", "zidong"],
|
||||
usage: "/mode [agent|plan|yolo|1|2|3]",
|
||||
description_id: MessageId::CmdModeDescription,
|
||||
};
|
||||
static THEME_INFO: CommandInfo = CommandInfo {
|
||||
name: "theme",
|
||||
aliases: &[],
|
||||
usage: "/theme [name]",
|
||||
description_id: MessageId::CmdThemeDescription,
|
||||
};
|
||||
static VERBOSE_INFO: CommandInfo = CommandInfo {
|
||||
name: "verbose",
|
||||
aliases: &[],
|
||||
usage: "/verbose [on|off]",
|
||||
description_id: MessageId::CmdVerboseDescription,
|
||||
};
|
||||
static TRUST_INFO: CommandInfo = CommandInfo {
|
||||
name: "trust",
|
||||
aliases: &["xinren"],
|
||||
usage: "/trust [on|off|add <path>|remove <path>|list]",
|
||||
description_id: MessageId::CmdTrustDescription,
|
||||
};
|
||||
static LOGOUT_INFO: CommandInfo = CommandInfo {
|
||||
name: "logout",
|
||||
aliases: &[],
|
||||
usage: "/logout",
|
||||
description_id: MessageId::CmdLogoutDescription,
|
||||
};
|
||||
static SLOP_INFO: CommandInfo = CommandInfo {
|
||||
name: "slop",
|
||||
aliases: &["canzha"],
|
||||
usage: "/slop [query|export]",
|
||||
description_id: MessageId::CmdSlopDescription,
|
||||
};
|
||||
|
||||
fn run_registered(app: &mut App, name: &str, arg: Option<&str>) -> CommandResult {
|
||||
dispatch(app, name, arg).expect("registered config command should dispatch")
|
||||
}
|
||||
|
||||
fn run_config(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "config", arg)
|
||||
}
|
||||
fn run_sidebar(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "sidebar", arg)
|
||||
}
|
||||
fn run_settings(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "settings", arg)
|
||||
}
|
||||
fn run_status(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "status", arg)
|
||||
}
|
||||
fn run_statusline(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "statusline", arg)
|
||||
}
|
||||
fn run_mode(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "mode", arg)
|
||||
}
|
||||
fn run_theme(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "theme", arg)
|
||||
}
|
||||
fn run_verbose(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "verbose", arg)
|
||||
}
|
||||
fn run_trust(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "trust", arg)
|
||||
}
|
||||
fn run_logout(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "logout", arg)
|
||||
}
|
||||
fn run_slop(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "slop", arg)
|
||||
}
|
||||
|
||||
pub(in crate::commands) fn dispatch(
|
||||
app: &mut App,
|
||||
command: &str,
|
||||
|
||||
@@ -14,8 +14,225 @@ mod stash;
|
||||
pub(in crate::commands) use self::core::reset_conversation_state;
|
||||
|
||||
use crate::commands::CommandResult;
|
||||
use crate::commands::traits::{Command, CommandGroup, CommandInfo, FunctionCommand};
|
||||
use crate::localization::MessageId;
|
||||
use crate::tui::app::{App, AppAction};
|
||||
|
||||
pub struct CoreCommands;
|
||||
|
||||
impl CommandGroup for CoreCommands {
|
||||
fn commands(&self) -> Vec<Box<dyn Command>> {
|
||||
vec![
|
||||
Box::new(FunctionCommand::new(&ANCHOR_INFO, run_anchor)),
|
||||
Box::new(FunctionCommand::new(&HELP_INFO, run_help)),
|
||||
Box::new(FunctionCommand::new(&CLEAR_INFO, run_clear)),
|
||||
Box::new(FunctionCommand::new(&EXIT_INFO, run_exit)),
|
||||
Box::new(FunctionCommand::new(&MODEL_INFO, run_model)),
|
||||
Box::new(FunctionCommand::new(&MODELS_INFO, run_models)),
|
||||
Box::new(FunctionCommand::new(&PROVIDER_INFO, run_provider)),
|
||||
Box::new(FunctionCommand::new(&QUEUE_INFO, run_queue)),
|
||||
Box::new(FunctionCommand::new(&STASH_INFO, run_stash)),
|
||||
Box::new(FunctionCommand::new(&HOOKS_INFO, run_hooks)),
|
||||
Box::new(FunctionCommand::new(&SUBAGENTS_INFO, run_subagents)),
|
||||
Box::new(FunctionCommand::new(&AGENT_INFO, run_agent)),
|
||||
Box::new(FunctionCommand::new(&LINKS_INFO, run_links)),
|
||||
Box::new(FunctionCommand::new(&FEEDBACK_INFO, run_feedback)),
|
||||
Box::new(FunctionCommand::new(&HF_INFO, run_hf)),
|
||||
Box::new(FunctionCommand::new(&HOME_INFO, run_home)),
|
||||
Box::new(FunctionCommand::new(&WORKSPACE_INFO, run_workspace)),
|
||||
Box::new(FunctionCommand::new(&PROFILE_INFO, run_profile)),
|
||||
Box::new(FunctionCommand::new(&RLM_INFO, run_rlm)),
|
||||
Box::new(FunctionCommand::new(&TRANSLATE_INFO, run_translate)),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
static ANCHOR_INFO: CommandInfo = CommandInfo {
|
||||
name: "anchor",
|
||||
aliases: &["maodian"],
|
||||
usage: "/anchor <text> | /anchor list | /anchor remove <n>",
|
||||
description_id: MessageId::CmdAnchorDescription,
|
||||
};
|
||||
static HELP_INFO: CommandInfo = CommandInfo {
|
||||
name: "help",
|
||||
aliases: &["?", "bangzhu", "帮助"],
|
||||
usage: "/help [command]",
|
||||
description_id: MessageId::CmdHelpDescription,
|
||||
};
|
||||
static CLEAR_INFO: CommandInfo = CommandInfo {
|
||||
name: "clear",
|
||||
aliases: &["qingping"],
|
||||
usage: "/clear",
|
||||
description_id: MessageId::CmdClearDescription,
|
||||
};
|
||||
static EXIT_INFO: CommandInfo = CommandInfo {
|
||||
name: "exit",
|
||||
aliases: &["quit", "q", "tuichu"],
|
||||
usage: "/exit",
|
||||
description_id: MessageId::CmdExitDescription,
|
||||
};
|
||||
static MODEL_INFO: CommandInfo = CommandInfo {
|
||||
name: "model",
|
||||
aliases: &["moxing"],
|
||||
usage: "/model [name]",
|
||||
description_id: MessageId::CmdModelDescription,
|
||||
};
|
||||
static MODELS_INFO: CommandInfo = CommandInfo {
|
||||
name: "models",
|
||||
aliases: &["moxingliebiao"],
|
||||
usage: "/models",
|
||||
description_id: MessageId::CmdModelsDescription,
|
||||
};
|
||||
static PROVIDER_INFO: CommandInfo = CommandInfo {
|
||||
name: "provider",
|
||||
aliases: &[],
|
||||
usage: "/provider [name] [model]",
|
||||
description_id: MessageId::CmdProviderDescription,
|
||||
};
|
||||
static QUEUE_INFO: CommandInfo = CommandInfo {
|
||||
name: "queue",
|
||||
aliases: &["queued"],
|
||||
usage: "/queue [list|edit <n>|drop <n>|clear]",
|
||||
description_id: MessageId::CmdQueueDescription,
|
||||
};
|
||||
static STASH_INFO: CommandInfo = CommandInfo {
|
||||
name: "stash",
|
||||
aliases: &["park"],
|
||||
usage: "/stash [list|pop|clear]",
|
||||
description_id: MessageId::CmdStashDescription,
|
||||
};
|
||||
static HOOKS_INFO: CommandInfo = CommandInfo {
|
||||
name: "hooks",
|
||||
aliases: &["hook", "gouzi"],
|
||||
usage: "/hooks [list|events]",
|
||||
description_id: MessageId::CmdHooksDescription,
|
||||
};
|
||||
static SUBAGENTS_INFO: CommandInfo = CommandInfo {
|
||||
name: "subagents",
|
||||
aliases: &["agents", "zhinengti"],
|
||||
usage: "/subagents",
|
||||
description_id: MessageId::CmdSubagentsDescription,
|
||||
};
|
||||
static AGENT_INFO: CommandInfo = CommandInfo {
|
||||
name: "agent",
|
||||
aliases: &["daili"],
|
||||
usage: "/agent [N] <task>",
|
||||
description_id: MessageId::CmdAgentDescription,
|
||||
};
|
||||
static LINKS_INFO: CommandInfo = CommandInfo {
|
||||
name: "links",
|
||||
aliases: &["dashboard", "api", "lianjie"],
|
||||
usage: "/links",
|
||||
description_id: MessageId::CmdLinksDescription,
|
||||
};
|
||||
static FEEDBACK_INFO: CommandInfo = CommandInfo {
|
||||
name: "feedback",
|
||||
aliases: &[],
|
||||
usage: "/feedback [bug|feature|security]",
|
||||
description_id: MessageId::CmdFeedbackDescription,
|
||||
};
|
||||
static HF_INFO: CommandInfo = CommandInfo {
|
||||
name: "hf",
|
||||
aliases: &["huggingface"],
|
||||
usage: "/hf [mcp <status|setup>|concepts]",
|
||||
description_id: MessageId::CmdHfDescription,
|
||||
};
|
||||
static HOME_INFO: CommandInfo = CommandInfo {
|
||||
name: "home",
|
||||
aliases: &["stats", "overview", "zhuye", "shouye"],
|
||||
usage: "/home",
|
||||
description_id: MessageId::CmdHomeDescription,
|
||||
};
|
||||
static WORKSPACE_INFO: CommandInfo = CommandInfo {
|
||||
name: "workspace",
|
||||
aliases: &["cwd"],
|
||||
usage: "/workspace [path]",
|
||||
description_id: MessageId::CmdWorkspaceDescription,
|
||||
};
|
||||
static PROFILE_INFO: CommandInfo = CommandInfo {
|
||||
name: "profile",
|
||||
aliases: &["dangan"],
|
||||
usage: "/profile <name>",
|
||||
description_id: MessageId::CmdHelpDescription,
|
||||
};
|
||||
static RLM_INFO: CommandInfo = CommandInfo {
|
||||
name: "rlm",
|
||||
aliases: &["recursive", "digui"],
|
||||
usage: "/rlm [N] <file_or_text>",
|
||||
description_id: MessageId::CmdRlmDescription,
|
||||
};
|
||||
static TRANSLATE_INFO: CommandInfo = CommandInfo {
|
||||
name: "translate",
|
||||
aliases: &["translation", "transale"],
|
||||
usage: "/translate",
|
||||
description_id: MessageId::CmdTranslateDescription,
|
||||
};
|
||||
|
||||
fn run_registered(app: &mut App, name: &str, arg: Option<&str>) -> CommandResult {
|
||||
dispatch(app, name, arg).expect("registered core command should dispatch")
|
||||
}
|
||||
|
||||
fn run_anchor(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "anchor", arg)
|
||||
}
|
||||
fn run_help(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "help", arg)
|
||||
}
|
||||
fn run_clear(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "clear", arg)
|
||||
}
|
||||
fn run_exit(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "exit", arg)
|
||||
}
|
||||
fn run_model(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "model", arg)
|
||||
}
|
||||
fn run_models(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "models", arg)
|
||||
}
|
||||
fn run_provider(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "provider", arg)
|
||||
}
|
||||
fn run_queue(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "queue", arg)
|
||||
}
|
||||
fn run_stash(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "stash", arg)
|
||||
}
|
||||
fn run_hooks(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "hooks", arg)
|
||||
}
|
||||
fn run_subagents(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "subagents", arg)
|
||||
}
|
||||
fn run_agent(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "agent", arg)
|
||||
}
|
||||
fn run_links(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "links", arg)
|
||||
}
|
||||
fn run_feedback(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "feedback", arg)
|
||||
}
|
||||
fn run_hf(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "hf", arg)
|
||||
}
|
||||
fn run_home(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "home", arg)
|
||||
}
|
||||
fn run_workspace(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "workspace", arg)
|
||||
}
|
||||
fn run_profile(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "profile", arg)
|
||||
}
|
||||
fn run_rlm(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "rlm", arg)
|
||||
}
|
||||
fn run_translate(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "translate", arg)
|
||||
}
|
||||
|
||||
pub(in crate::commands) fn dispatch(
|
||||
app: &mut App,
|
||||
command: &str,
|
||||
|
||||
@@ -7,8 +7,135 @@ mod change;
|
||||
mod debug;
|
||||
|
||||
use crate::commands::CommandResult;
|
||||
use crate::commands::traits::{Command, CommandGroup, CommandInfo, FunctionCommand};
|
||||
use crate::localization::MessageId;
|
||||
use crate::tui::app::App;
|
||||
|
||||
pub struct DebugCommands;
|
||||
|
||||
impl CommandGroup for DebugCommands {
|
||||
fn commands(&self) -> Vec<Box<dyn Command>> {
|
||||
vec![
|
||||
Box::new(FunctionCommand::new(&TOKENS_INFO, run_tokens)),
|
||||
Box::new(FunctionCommand::new(&COST_INFO, run_cost)),
|
||||
Box::new(FunctionCommand::new(&BALANCE_INFO, run_balance)),
|
||||
Box::new(FunctionCommand::new(&CACHE_INFO, run_cache)),
|
||||
Box::new(FunctionCommand::new(&CHANGE_INFO, run_change)),
|
||||
Box::new(FunctionCommand::new(&SYSTEM_INFO, run_system)),
|
||||
Box::new(FunctionCommand::new(&CONTEXT_INFO, run_context)),
|
||||
Box::new(FunctionCommand::new(&EDIT_INFO, run_edit)),
|
||||
Box::new(FunctionCommand::new(&DIFF_INFO, run_diff)),
|
||||
Box::new(FunctionCommand::new(&UNDO_INFO, run_undo)),
|
||||
Box::new(FunctionCommand::new(&RETRY_INFO, run_retry)),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
static TOKENS_INFO: CommandInfo = CommandInfo {
|
||||
name: "tokens",
|
||||
aliases: &[],
|
||||
usage: "/tokens",
|
||||
description_id: MessageId::CmdTokensDescription,
|
||||
};
|
||||
static COST_INFO: CommandInfo = CommandInfo {
|
||||
name: "cost",
|
||||
aliases: &[],
|
||||
usage: "/cost",
|
||||
description_id: MessageId::CmdCostDescription,
|
||||
};
|
||||
static BALANCE_INFO: CommandInfo = CommandInfo {
|
||||
name: "balance",
|
||||
aliases: &[],
|
||||
usage: "/balance",
|
||||
description_id: MessageId::CmdBalanceDescription,
|
||||
};
|
||||
static CACHE_INFO: CommandInfo = CommandInfo {
|
||||
name: "cache",
|
||||
aliases: &[],
|
||||
usage: "/cache [count|inspect|stats|zones|warmup]",
|
||||
description_id: MessageId::CmdCacheDescription,
|
||||
};
|
||||
static CHANGE_INFO: CommandInfo = CommandInfo {
|
||||
name: "change",
|
||||
aliases: &[],
|
||||
usage: "/change [version]",
|
||||
description_id: MessageId::CmdChangeDescription,
|
||||
};
|
||||
static SYSTEM_INFO: CommandInfo = CommandInfo {
|
||||
name: "system",
|
||||
aliases: &["xitong"],
|
||||
usage: "/system",
|
||||
description_id: MessageId::CmdSystemDescription,
|
||||
};
|
||||
static CONTEXT_INFO: CommandInfo = CommandInfo {
|
||||
name: "context",
|
||||
aliases: &["ctx"],
|
||||
usage: "/context [report|json|summary]",
|
||||
description_id: MessageId::CmdContextDescription,
|
||||
};
|
||||
static EDIT_INFO: CommandInfo = CommandInfo {
|
||||
name: "edit",
|
||||
aliases: &[],
|
||||
usage: "/edit",
|
||||
description_id: MessageId::CmdEditDescription,
|
||||
};
|
||||
static DIFF_INFO: CommandInfo = CommandInfo {
|
||||
name: "diff",
|
||||
aliases: &[],
|
||||
usage: "/diff",
|
||||
description_id: MessageId::CmdDiffDescription,
|
||||
};
|
||||
static UNDO_INFO: CommandInfo = CommandInfo {
|
||||
name: "undo",
|
||||
aliases: &[],
|
||||
usage: "/undo",
|
||||
description_id: MessageId::CmdUndoDescription,
|
||||
};
|
||||
static RETRY_INFO: CommandInfo = CommandInfo {
|
||||
name: "retry",
|
||||
aliases: &["chongshi"],
|
||||
usage: "/retry",
|
||||
description_id: MessageId::CmdRetryDescription,
|
||||
};
|
||||
|
||||
fn run_registered(app: &mut App, name: &str, arg: Option<&str>) -> CommandResult {
|
||||
dispatch(app, name, arg).expect("registered debug command should dispatch")
|
||||
}
|
||||
|
||||
fn run_tokens(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "tokens", arg)
|
||||
}
|
||||
fn run_cost(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "cost", arg)
|
||||
}
|
||||
fn run_balance(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "balance", arg)
|
||||
}
|
||||
fn run_cache(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "cache", arg)
|
||||
}
|
||||
fn run_change(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "change", arg)
|
||||
}
|
||||
fn run_system(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "system", arg)
|
||||
}
|
||||
fn run_context(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "context", arg)
|
||||
}
|
||||
fn run_edit(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "edit", arg)
|
||||
}
|
||||
fn run_diff(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "diff", arg)
|
||||
}
|
||||
fn run_undo(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "undo", arg)
|
||||
}
|
||||
fn run_retry(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "retry", arg)
|
||||
}
|
||||
|
||||
pub(in crate::commands) fn dispatch(
|
||||
app: &mut App,
|
||||
command: &str,
|
||||
|
||||
@@ -5,8 +5,45 @@ mod memory;
|
||||
mod note;
|
||||
|
||||
use crate::commands::CommandResult;
|
||||
use crate::commands::traits::{Command, CommandGroup, CommandInfo, FunctionCommand};
|
||||
use crate::localization::MessageId;
|
||||
use crate::tui::app::App;
|
||||
|
||||
pub struct MemoryCommands;
|
||||
|
||||
impl CommandGroup for MemoryCommands {
|
||||
fn commands(&self) -> Vec<Box<dyn Command>> {
|
||||
vec![
|
||||
Box::new(FunctionCommand::new(&NOTE_INFO, run_note)),
|
||||
Box::new(FunctionCommand::new(&MEMORY_INFO, run_memory)),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
static NOTE_INFO: CommandInfo = CommandInfo {
|
||||
name: "note",
|
||||
aliases: &[],
|
||||
usage: "/note [add|list|show|edit|remove|clear|path]",
|
||||
description_id: MessageId::CmdNoteDescription,
|
||||
};
|
||||
static MEMORY_INFO: CommandInfo = CommandInfo {
|
||||
name: "memory",
|
||||
aliases: &[],
|
||||
usage: "/memory [show|path|clear|edit|help]",
|
||||
description_id: MessageId::CmdMemoryDescription,
|
||||
};
|
||||
|
||||
fn run_registered(app: &mut App, name: &str, arg: Option<&str>) -> CommandResult {
|
||||
dispatch(app, name, arg).expect("registered memory command should dispatch")
|
||||
}
|
||||
|
||||
fn run_note(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "note", arg)
|
||||
}
|
||||
fn run_memory(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "memory", arg)
|
||||
}
|
||||
|
||||
pub(in crate::commands) fn dispatch(
|
||||
app: &mut App,
|
||||
command: &str,
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
//! Group-owned built-in command areas.
|
||||
//!
|
||||
//! Each group module owns the handler files for its command area and
|
||||
//! exposes a `dispatch` slice that claims the command names it owns and
|
||||
//! returns `None` for everything else. `commands::execute` chains the
|
||||
//! group dispatchers in order, so a command name must be claimed by
|
||||
//! exactly one group.
|
||||
//! Each group module registers command objects into the central command
|
||||
//! registry. Command implementation functions still live with their owning
|
||||
//! groups, while dispatch, palette metadata, and help lookup all read from the
|
||||
//! same registry surface.
|
||||
|
||||
pub mod config;
|
||||
pub mod core;
|
||||
@@ -14,3 +13,18 @@ pub mod project;
|
||||
pub mod session;
|
||||
pub mod skills;
|
||||
pub mod utility;
|
||||
|
||||
use crate::commands::traits::CommandGroup;
|
||||
|
||||
pub fn all_command_groups() -> Vec<&'static dyn CommandGroup> {
|
||||
vec![
|
||||
&core::CoreCommands,
|
||||
&session::SessionCommands,
|
||||
&config::ConfigCommands,
|
||||
&debug::DebugCommands,
|
||||
&project::ProjectCommands,
|
||||
&skills::SkillsCommands,
|
||||
&memory::MemoryCommands,
|
||||
&utility::UtilityCommands,
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,8 +5,65 @@ mod init;
|
||||
pub mod share;
|
||||
|
||||
use crate::commands::CommandResult;
|
||||
use crate::commands::traits::{Command, CommandGroup, CommandInfo, FunctionCommand};
|
||||
use crate::localization::MessageId;
|
||||
use crate::tui::app::App;
|
||||
|
||||
pub struct ProjectCommands;
|
||||
|
||||
impl CommandGroup for ProjectCommands {
|
||||
fn commands(&self) -> Vec<Box<dyn Command>> {
|
||||
vec![
|
||||
Box::new(FunctionCommand::new(&INIT_INFO, run_init)),
|
||||
Box::new(FunctionCommand::new(&LSP_INFO, run_lsp)),
|
||||
Box::new(FunctionCommand::new(&SHARE_INFO, run_share)),
|
||||
Box::new(FunctionCommand::new(&GOAL_INFO, run_goal)),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
static INIT_INFO: CommandInfo = CommandInfo {
|
||||
name: "init",
|
||||
aliases: &[],
|
||||
usage: "/init",
|
||||
description_id: MessageId::CmdInitDescription,
|
||||
};
|
||||
static LSP_INFO: CommandInfo = CommandInfo {
|
||||
name: "lsp",
|
||||
aliases: &[],
|
||||
usage: "/lsp [on|off|status]",
|
||||
description_id: MessageId::CmdLspDescription,
|
||||
};
|
||||
static SHARE_INFO: CommandInfo = CommandInfo {
|
||||
name: "share",
|
||||
aliases: &[],
|
||||
usage: "/share",
|
||||
description_id: MessageId::CmdShareDescription,
|
||||
};
|
||||
static GOAL_INFO: CommandInfo = CommandInfo {
|
||||
name: "goal",
|
||||
aliases: &["hunt", "mubiao", "狩猎"],
|
||||
usage: "/goal [objective|clear|pause|resume|complete|blocked] [budget: N]",
|
||||
description_id: MessageId::CmdGoalDescription,
|
||||
};
|
||||
|
||||
fn run_registered(app: &mut App, name: &str, arg: Option<&str>) -> CommandResult {
|
||||
dispatch(app, name, arg).expect("registered project command should dispatch")
|
||||
}
|
||||
|
||||
fn run_init(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "init", arg)
|
||||
}
|
||||
fn run_lsp(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "lsp", arg)
|
||||
}
|
||||
fn run_share(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "share", arg)
|
||||
}
|
||||
fn run_goal(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "goal", arg)
|
||||
}
|
||||
|
||||
pub(in crate::commands) fn dispatch(
|
||||
app: &mut App,
|
||||
command: &str,
|
||||
|
||||
@@ -8,8 +8,125 @@ mod session;
|
||||
use std::fmt::Write as _;
|
||||
|
||||
use crate::commands::CommandResult;
|
||||
use crate::commands::traits::{Command, CommandGroup, CommandInfo, FunctionCommand};
|
||||
use crate::localization::MessageId;
|
||||
use crate::tui::app::{App, AppAction};
|
||||
|
||||
pub struct SessionCommands;
|
||||
|
||||
impl CommandGroup for SessionCommands {
|
||||
fn commands(&self) -> Vec<Box<dyn Command>> {
|
||||
vec![
|
||||
Box::new(FunctionCommand::new(&RENAME_INFO, run_rename)),
|
||||
Box::new(FunctionCommand::new(&SAVE_INFO, run_save)),
|
||||
Box::new(FunctionCommand::new(&FORK_INFO, run_fork)),
|
||||
Box::new(FunctionCommand::new(&NEW_INFO, run_new)),
|
||||
Box::new(FunctionCommand::new(&SESSIONS_INFO, run_sessions)),
|
||||
Box::new(FunctionCommand::new(&LOAD_INFO, run_load)),
|
||||
Box::new(FunctionCommand::new(&COMPACT_INFO, run_compact)),
|
||||
Box::new(FunctionCommand::new(&PURGE_INFO, run_purge)),
|
||||
Box::new(FunctionCommand::new(&RELAY_INFO, run_relay)),
|
||||
Box::new(FunctionCommand::new(&EXPORT_INFO, run_export)),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
static RENAME_INFO: CommandInfo = CommandInfo {
|
||||
name: "rename",
|
||||
aliases: &["gaiming", "chongmingming"],
|
||||
usage: "/rename <new title>",
|
||||
description_id: MessageId::CmdRenameDescription,
|
||||
};
|
||||
static SAVE_INFO: CommandInfo = CommandInfo {
|
||||
name: "save",
|
||||
aliases: &[],
|
||||
usage: "/save [path]",
|
||||
description_id: MessageId::CmdSaveDescription,
|
||||
};
|
||||
static FORK_INFO: CommandInfo = CommandInfo {
|
||||
name: "fork",
|
||||
aliases: &["branch"],
|
||||
usage: "/fork",
|
||||
description_id: MessageId::CmdForkDescription,
|
||||
};
|
||||
static NEW_INFO: CommandInfo = CommandInfo {
|
||||
name: "new",
|
||||
aliases: &[],
|
||||
usage: "/new [--force]",
|
||||
description_id: MessageId::CmdNewDescription,
|
||||
};
|
||||
static SESSIONS_INFO: CommandInfo = CommandInfo {
|
||||
name: "sessions",
|
||||
aliases: &["resume"],
|
||||
usage: "/sessions [show|prune <days>]",
|
||||
description_id: MessageId::CmdSessionsDescription,
|
||||
};
|
||||
static LOAD_INFO: CommandInfo = CommandInfo {
|
||||
name: "load",
|
||||
aliases: &["jiazai"],
|
||||
usage: "/load [path]",
|
||||
description_id: MessageId::CmdLoadDescription,
|
||||
};
|
||||
static COMPACT_INFO: CommandInfo = CommandInfo {
|
||||
name: "compact",
|
||||
aliases: &["yasuo"],
|
||||
usage: "/compact",
|
||||
description_id: MessageId::CmdCompactDescription,
|
||||
};
|
||||
static PURGE_INFO: CommandInfo = CommandInfo {
|
||||
name: "purge",
|
||||
aliases: &["qingchu"],
|
||||
usage: "/purge",
|
||||
description_id: MessageId::CmdPurgeDescription,
|
||||
};
|
||||
static RELAY_INFO: CommandInfo = CommandInfo {
|
||||
name: "relay",
|
||||
aliases: &["batonpass", "接力"],
|
||||
usage: "/relay [focus]",
|
||||
description_id: MessageId::CmdRelayDescription,
|
||||
};
|
||||
static EXPORT_INFO: CommandInfo = CommandInfo {
|
||||
name: "export",
|
||||
aliases: &["daochu"],
|
||||
usage: "/export [path]",
|
||||
description_id: MessageId::CmdExportDescription,
|
||||
};
|
||||
|
||||
fn run_registered(app: &mut App, name: &str, arg: Option<&str>) -> CommandResult {
|
||||
dispatch(app, name, arg).expect("registered session command should dispatch")
|
||||
}
|
||||
|
||||
fn run_rename(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "rename", arg)
|
||||
}
|
||||
fn run_save(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "save", arg)
|
||||
}
|
||||
fn run_fork(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "fork", arg)
|
||||
}
|
||||
fn run_new(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "new", arg)
|
||||
}
|
||||
fn run_sessions(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "sessions", arg)
|
||||
}
|
||||
fn run_load(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "load", arg)
|
||||
}
|
||||
fn run_compact(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "compact", arg)
|
||||
}
|
||||
fn run_purge(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "purge", arg)
|
||||
}
|
||||
fn run_relay(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "relay", arg)
|
||||
}
|
||||
fn run_export(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "export", arg)
|
||||
}
|
||||
|
||||
pub(in crate::commands) fn dispatch(
|
||||
app: &mut App,
|
||||
command: &str,
|
||||
|
||||
@@ -8,8 +8,65 @@ mod skills;
|
||||
pub(in crate::commands) use self::skills::run_skill_by_name;
|
||||
|
||||
use crate::commands::CommandResult;
|
||||
use crate::commands::traits::{Command, CommandGroup, CommandInfo, FunctionCommand};
|
||||
use crate::localization::MessageId;
|
||||
use crate::tui::app::App;
|
||||
|
||||
pub struct SkillsCommands;
|
||||
|
||||
impl CommandGroup for SkillsCommands {
|
||||
fn commands(&self) -> Vec<Box<dyn Command>> {
|
||||
vec![
|
||||
Box::new(FunctionCommand::new(&SKILLS_INFO, run_skills)),
|
||||
Box::new(FunctionCommand::new(&SKILL_INFO, run_skill)),
|
||||
Box::new(FunctionCommand::new(&REVIEW_INFO, run_review)),
|
||||
Box::new(FunctionCommand::new(&RESTORE_INFO, run_restore)),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
static SKILLS_INFO: CommandInfo = CommandInfo {
|
||||
name: "skills",
|
||||
aliases: &["jinengliebiao"],
|
||||
usage: "/skills [--remote|sync|<prefix>]",
|
||||
description_id: MessageId::CmdSkillsDescription,
|
||||
};
|
||||
static SKILL_INFO: CommandInfo = CommandInfo {
|
||||
name: "skill",
|
||||
aliases: &["jineng"],
|
||||
usage: "/skill <name|install <spec>|update <name>|uninstall <name>|trust <name>>",
|
||||
description_id: MessageId::CmdSkillDescription,
|
||||
};
|
||||
static REVIEW_INFO: CommandInfo = CommandInfo {
|
||||
name: "review",
|
||||
aliases: &["shencha"],
|
||||
usage: "/review <target>",
|
||||
description_id: MessageId::CmdReviewDescription,
|
||||
};
|
||||
static RESTORE_INFO: CommandInfo = CommandInfo {
|
||||
name: "restore",
|
||||
aliases: &[],
|
||||
usage: "/restore [N|list [N]]",
|
||||
description_id: MessageId::CmdRestoreDescription,
|
||||
};
|
||||
|
||||
fn run_registered(app: &mut App, name: &str, arg: Option<&str>) -> CommandResult {
|
||||
dispatch(app, name, arg).expect("registered skills command should dispatch")
|
||||
}
|
||||
|
||||
fn run_skills(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "skills", arg)
|
||||
}
|
||||
fn run_skill(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "skill", arg)
|
||||
}
|
||||
fn run_review(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "review", arg)
|
||||
}
|
||||
fn run_restore(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "restore", arg)
|
||||
}
|
||||
|
||||
pub(in crate::commands) fn dispatch(
|
||||
app: &mut App,
|
||||
command: &str,
|
||||
|
||||
@@ -8,8 +8,75 @@ mod network;
|
||||
mod task;
|
||||
|
||||
use crate::commands::CommandResult;
|
||||
use crate::commands::traits::{Command, CommandGroup, CommandInfo, FunctionCommand};
|
||||
use crate::localization::MessageId;
|
||||
use crate::tui::app::App;
|
||||
|
||||
pub struct UtilityCommands;
|
||||
|
||||
impl CommandGroup for UtilityCommands {
|
||||
fn commands(&self) -> Vec<Box<dyn Command>> {
|
||||
vec![
|
||||
Box::new(FunctionCommand::new(&ATTACH_INFO, run_attach)),
|
||||
Box::new(FunctionCommand::new(&TASK_INFO, run_task)),
|
||||
Box::new(FunctionCommand::new(&JOBS_INFO, run_jobs)),
|
||||
Box::new(FunctionCommand::new(&MCP_INFO, run_mcp)),
|
||||
Box::new(FunctionCommand::new(&NETWORK_INFO, run_network)),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
static ATTACH_INFO: CommandInfo = CommandInfo {
|
||||
name: "attach",
|
||||
aliases: &["image", "media", "fujian"],
|
||||
usage: "/attach <path>",
|
||||
description_id: MessageId::CmdAttachDescription,
|
||||
};
|
||||
static TASK_INFO: CommandInfo = CommandInfo {
|
||||
name: "task",
|
||||
aliases: &["tasks"],
|
||||
usage: "/task [add <prompt>|list|show <id>|cancel <id>]",
|
||||
description_id: MessageId::CmdTaskDescription,
|
||||
};
|
||||
static JOBS_INFO: CommandInfo = CommandInfo {
|
||||
name: "jobs",
|
||||
aliases: &["job", "zuoye"],
|
||||
usage: "/jobs [list|show <id>|poll <id>|wait <id>|stdin <id> <input>|cancel <id>]",
|
||||
description_id: MessageId::CmdJobsDescription,
|
||||
};
|
||||
static MCP_INFO: CommandInfo = CommandInfo {
|
||||
name: "mcp",
|
||||
aliases: &[],
|
||||
usage: "/mcp [init|add stdio <name> <command> [args...]|add http <name> <url>|enable <name>|disable <name>|remove <name>|validate|reload]",
|
||||
description_id: MessageId::CmdMcpDescription,
|
||||
};
|
||||
static NETWORK_INFO: CommandInfo = CommandInfo {
|
||||
name: "network",
|
||||
aliases: &[],
|
||||
usage: "/network [list|allow <host>|deny <host>|remove <host>|default <allow|deny|prompt>]",
|
||||
description_id: MessageId::CmdNetworkDescription,
|
||||
};
|
||||
|
||||
fn run_registered(app: &mut App, name: &str, arg: Option<&str>) -> CommandResult {
|
||||
dispatch(app, name, arg).expect("registered utility command should dispatch")
|
||||
}
|
||||
|
||||
fn run_attach(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "attach", arg)
|
||||
}
|
||||
fn run_task(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "task", arg)
|
||||
}
|
||||
fn run_jobs(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "jobs", arg)
|
||||
}
|
||||
fn run_mcp(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "mcp", arg)
|
||||
}
|
||||
fn run_network(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
run_registered(app, "network", arg)
|
||||
}
|
||||
|
||||
pub(in crate::commands) fn dispatch(
|
||||
app: &mut App,
|
||||
command: &str,
|
||||
|
||||
+167
-57
@@ -1,19 +1,18 @@
|
||||
//! Slash command registry and dispatch system
|
||||
//!
|
||||
//! This module provides a modular command system inspired by Codex-rs.
|
||||
//! Commands are organized by category and dispatched through a central registry.
|
||||
//! Built-in handlers live in group-owned areas under [`groups`]; this module
|
||||
//! keeps parsing, registry metadata, user-command precedence, and the
|
||||
//! Commands are organized by category and dispatched through a central strategy
|
||||
//! registry. Built-in handlers live in group-owned areas under [`groups`]; this
|
||||
//! module keeps registry construction, user-command precedence, and the
|
||||
//! fall-through behaviour.
|
||||
|
||||
mod groups;
|
||||
mod parse;
|
||||
mod registry;
|
||||
pub mod traits;
|
||||
pub mod user_commands;
|
||||
|
||||
use parse::parse_slash_command;
|
||||
use registry::suggest_command_names;
|
||||
pub use registry::{COMMANDS, get_command_info};
|
||||
use std::sync::OnceLock;
|
||||
|
||||
pub use traits::CommandInfo;
|
||||
|
||||
// Long-standing public paths that predate the group layout.
|
||||
pub use groups::project::share;
|
||||
@@ -78,37 +77,68 @@ impl CommandResult {
|
||||
}
|
||||
}
|
||||
|
||||
static REGISTRY: OnceLock<traits::CommandRegistry> = OnceLock::new();
|
||||
|
||||
fn build_registry() -> traits::CommandRegistry {
|
||||
let mut registry = traits::CommandRegistry::empty();
|
||||
for group in groups::all_command_groups() {
|
||||
registry.register_group(group);
|
||||
}
|
||||
registry
|
||||
}
|
||||
|
||||
pub fn registry() -> &'static traits::CommandRegistry {
|
||||
REGISTRY.get_or_init(build_registry)
|
||||
}
|
||||
|
||||
pub fn command_infos() -> Vec<&'static CommandInfo> {
|
||||
registry().infos()
|
||||
}
|
||||
|
||||
pub fn get_command_info(name: &str) -> Option<&'static CommandInfo> {
|
||||
registry().get_info(name)
|
||||
}
|
||||
|
||||
/// Execute a slash command
|
||||
pub fn execute(cmd: &str, app: &mut App) -> CommandResult {
|
||||
let parsed = parse_slash_command(cmd);
|
||||
let command = parsed.name.as_str();
|
||||
let arg = parsed.arg;
|
||||
let trimmed = cmd.trim();
|
||||
let parts: Vec<&str> = trimmed.splitn(2, char::is_whitespace).collect();
|
||||
let command = parts
|
||||
.first()
|
||||
.copied()
|
||||
.unwrap_or_default()
|
||||
.trim_start_matches('/')
|
||||
.to_ascii_lowercase();
|
||||
let arg = parts
|
||||
.get(1)
|
||||
.map(|value| value.trim())
|
||||
.filter(|value| !value.is_empty());
|
||||
|
||||
// Check user-defined commands FIRST so they can override built-ins.
|
||||
if let Some(result) = user_commands::try_dispatch_user_command(app, cmd.trim()) {
|
||||
if let Some(result) = user_commands::try_dispatch_user_command(app, trimmed) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Built-in commands are owned by their group areas; each group claims
|
||||
// the names it owns and returns None for everything else.
|
||||
type GroupDispatcher = fn(&mut App, &str, Option<&str>) -> Option<CommandResult>;
|
||||
let group_dispatchers: [GroupDispatcher; 8] = [
|
||||
groups::core::dispatch,
|
||||
groups::session::dispatch,
|
||||
groups::config::dispatch,
|
||||
groups::debug::dispatch,
|
||||
groups::project::dispatch,
|
||||
groups::skills::dispatch,
|
||||
groups::memory::dispatch,
|
||||
groups::utility::dispatch,
|
||||
];
|
||||
for dispatch in group_dispatchers {
|
||||
if let Some(result) = dispatch(app, command, arg) {
|
||||
return result;
|
||||
// Compatibility aliases whose historical behavior also supplied an arg.
|
||||
match command.as_str() {
|
||||
"jihua" => {
|
||||
return groups::config::dispatch(app, "jihua", arg).unwrap_or_else(|| {
|
||||
CommandResult::error("The /jihua alias could not be dispatched.")
|
||||
});
|
||||
}
|
||||
"zidong" => {
|
||||
return groups::config::dispatch(app, "zidong", arg).unwrap_or_else(|| {
|
||||
CommandResult::error("The /zidong alias could not be dispatched.")
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
match command {
|
||||
if let Some(command_object) = registry().get(command.as_str()) {
|
||||
return command_object.execute(app, arg);
|
||||
}
|
||||
|
||||
match command.as_str() {
|
||||
// Legacy command migrations (kept out of registry/autocomplete intentionally).
|
||||
"set" => CommandResult::error(
|
||||
"The /set command was retired. Use /config to edit settings and /settings to inspect current values.",
|
||||
@@ -120,10 +150,10 @@ 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 let Some(result) = groups::skills::run_skill_by_name(app, command, arg) {
|
||||
if let Some(result) = groups::skills::run_skill_by_name(app, command.as_str(), arg) {
|
||||
return result;
|
||||
}
|
||||
let suggestions = suggest_command_names(command, 3);
|
||||
let suggestions = suggest_command_names(command.as_str(), 3);
|
||||
if suggestions.is_empty() {
|
||||
CommandResult::error(format!(
|
||||
"Unknown command: /{command}. Type /help for available commands."
|
||||
@@ -151,6 +181,86 @@ pub fn switch_mode(app: &mut App, mode: crate::tui::app::AppMode) -> String {
|
||||
groups::config::config::switch_mode(app, mode)
|
||||
}
|
||||
|
||||
fn edit_distance(a: &str, b: &str) -> usize {
|
||||
if a == b {
|
||||
return 0;
|
||||
}
|
||||
if a.is_empty() {
|
||||
return b.chars().count();
|
||||
}
|
||||
if b.is_empty() {
|
||||
return a.chars().count();
|
||||
}
|
||||
|
||||
let b_chars: Vec<char> = b.chars().collect();
|
||||
let mut previous: Vec<usize> = (0..=b_chars.len()).collect();
|
||||
let mut current = vec![0usize; b_chars.len() + 1];
|
||||
|
||||
for (i, a_ch) in a.chars().enumerate() {
|
||||
current[0] = i + 1;
|
||||
for (j, b_ch) in b_chars.iter().enumerate() {
|
||||
let cost = if a_ch == *b_ch { 0 } else { 1 };
|
||||
let delete = previous[j + 1] + 1;
|
||||
let insert = current[j] + 1;
|
||||
let substitute = previous[j] + cost;
|
||||
current[j + 1] = delete.min(insert).min(substitute);
|
||||
}
|
||||
std::mem::swap(&mut previous, &mut current);
|
||||
}
|
||||
|
||||
previous[b_chars.len()]
|
||||
}
|
||||
|
||||
fn suggest_command_names(input: &str, limit: usize) -> Vec<String> {
|
||||
let query = input.trim().to_ascii_lowercase();
|
||||
if query.is_empty() || limit == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let mut scored: Vec<(u8, usize, String)> = Vec::new();
|
||||
for command in registry().infos() {
|
||||
let mut best: Option<(u8, usize)> = None;
|
||||
for candidate in std::iter::once(command.name).chain(command.aliases.iter().copied()) {
|
||||
let prefix_match = candidate.starts_with(&query) || query.starts_with(candidate);
|
||||
let contains_match = candidate.contains(&query) || query.contains(candidate);
|
||||
let distance = edit_distance(candidate, &query);
|
||||
let close_typo = distance <= 2;
|
||||
if !(prefix_match || contains_match || close_typo) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let rank = if prefix_match {
|
||||
0
|
||||
} else if contains_match {
|
||||
1
|
||||
} else {
|
||||
2
|
||||
};
|
||||
|
||||
match best {
|
||||
Some((best_rank, best_distance))
|
||||
if rank > best_rank || (rank == best_rank && distance >= best_distance) => {}
|
||||
_ => best = Some((rank, distance)),
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((rank, distance)) = best {
|
||||
scored.push((rank, distance, command.name.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
scored.sort_by(|a, b| {
|
||||
a.0.cmp(&b.0)
|
||||
.then_with(|| a.1.cmp(&b.1))
|
||||
.then_with(|| a.2.cmp(&b.2))
|
||||
});
|
||||
scored
|
||||
.into_iter()
|
||||
.take(limit)
|
||||
.map(|(_, _, name)| name)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -191,9 +301,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn command_registry_contains_config_and_links_but_not_set_or_deepseek() {
|
||||
assert!(COMMANDS.iter().any(|cmd| cmd.name == "config"));
|
||||
let sidebar = COMMANDS
|
||||
.iter()
|
||||
assert!(command_infos().iter().any(|cmd| cmd.name == "config"));
|
||||
let sidebar = command_infos()
|
||||
.into_iter()
|
||||
.find(|cmd| cmd.name == "sidebar")
|
||||
.expect("sidebar command should exist");
|
||||
assert_eq!(sidebar.description_id, MessageId::CmdSidebarDescription);
|
||||
@@ -202,23 +312,23 @@ mod tests {
|
||||
.description_for(Locale::En)
|
||||
.contains("right sidebar")
|
||||
);
|
||||
assert!(COMMANDS.iter().any(|cmd| cmd.name == "links"));
|
||||
let hf = COMMANDS
|
||||
.iter()
|
||||
assert!(command_infos().iter().any(|cmd| cmd.name == "links"));
|
||||
let hf = command_infos()
|
||||
.into_iter()
|
||||
.find(|cmd| cmd.name == "hf")
|
||||
.expect("hf command should exist");
|
||||
assert_eq!(hf.aliases, &["huggingface"]);
|
||||
assert_eq!(hf.description_id, MessageId::CmdHfDescription);
|
||||
assert!(hf.description_for(Locale::En).contains("Hugging Face"));
|
||||
assert!(COMMANDS.iter().any(|cmd| cmd.name == "memory"));
|
||||
assert!(!COMMANDS.iter().any(|cmd| cmd.name == "set"));
|
||||
assert!(!COMMANDS.iter().any(|cmd| cmd.name == "deepseek"));
|
||||
assert!(command_infos().iter().any(|cmd| cmd.name == "memory"));
|
||||
assert!(!command_infos().iter().any(|cmd| cmd.name == "set"));
|
||||
assert!(!command_infos().iter().any(|cmd| cmd.name == "deepseek"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn links_command_has_dashboard_and_api_aliases() {
|
||||
let links = COMMANDS
|
||||
.iter()
|
||||
let links = command_infos()
|
||||
.into_iter()
|
||||
.find(|cmd| cmd.name == "links")
|
||||
.expect("links command should exist");
|
||||
assert_eq!(links.aliases, &["dashboard", "api", "lianjie"]);
|
||||
@@ -324,8 +434,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn relay_command_has_bilingual_aliases() {
|
||||
let relay = COMMANDS
|
||||
.iter()
|
||||
let relay = command_infos()
|
||||
.into_iter()
|
||||
.find(|cmd| cmd.name == "relay")
|
||||
.expect("relay command should exist");
|
||||
assert_eq!(relay.aliases, &["batonpass", "接力"]);
|
||||
@@ -344,7 +454,7 @@ mod tests {
|
||||
#[test]
|
||||
fn command_registry_has_unique_names_and_aliases() {
|
||||
let mut names = std::collections::BTreeSet::new();
|
||||
for command in COMMANDS {
|
||||
for command in command_infos() {
|
||||
assert!(
|
||||
names.insert(command.name),
|
||||
"duplicate command name /{}",
|
||||
@@ -353,7 +463,7 @@ mod tests {
|
||||
}
|
||||
|
||||
let mut aliases = std::collections::BTreeSet::new();
|
||||
for command in COMMANDS {
|
||||
for command in command_infos() {
|
||||
for alias in command.aliases {
|
||||
assert!(
|
||||
!names.contains(alias),
|
||||
@@ -366,7 +476,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn command_registry_metadata_is_complete_and_palette_safe() {
|
||||
for command in COMMANDS {
|
||||
for command in command_infos() {
|
||||
assert!(!command.name.is_empty(), "command name must not be empty");
|
||||
assert_eq!(
|
||||
command.name.trim(),
|
||||
@@ -445,7 +555,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn command_info_resolves_canonical_names_and_aliases() {
|
||||
for command in COMMANDS {
|
||||
for command in command_infos() {
|
||||
for lookup in [command.name.to_string(), format!("/{}", command.name)] {
|
||||
let resolved = get_command_info(&lookup)
|
||||
.unwrap_or_else(|| panic!("{lookup:?} should resolve to /{}", command.name));
|
||||
@@ -466,7 +576,7 @@ mod tests {
|
||||
#[test]
|
||||
fn every_registered_command_has_a_help_topic() {
|
||||
let mut app = create_test_app();
|
||||
for command in COMMANDS {
|
||||
for command in command_infos() {
|
||||
let result = execute(&format!("/help {}", command.name), &mut app);
|
||||
assert!(
|
||||
!result.is_error,
|
||||
@@ -492,8 +602,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn context_command_opens_inspector_and_keeps_ctx_alias() {
|
||||
let context = COMMANDS
|
||||
.iter()
|
||||
let context = command_infos()
|
||||
.into_iter()
|
||||
.find(|cmd| cmd.name == "context")
|
||||
.expect("context command should exist");
|
||||
assert_eq!(context.aliases, &["ctx"]);
|
||||
@@ -729,10 +839,10 @@ mod tests {
|
||||
(app, tmpdir, guard)
|
||||
}
|
||||
|
||||
/// Smoke test: every entry in `COMMANDS` must dispatch to a real handler.
|
||||
/// Smoke test: every entry in `command_infos()` must dispatch to a real handler.
|
||||
/// A dispatch miss surfaces as the fall-through `Unknown command:` error
|
||||
/// message in `execute`. This catches the case where a new command is
|
||||
/// added to `COMMANDS` (so it shows up in `/help` and the palette) but
|
||||
/// added to `command_infos()` (so it shows up in `/help` and the palette) but
|
||||
/// the matching arm in `execute` is forgotten — the user would type the
|
||||
/// command, see it autocomplete, and then get an unhelpful "did you
|
||||
/// mean" suggestion. Also catches panics in handlers because the test
|
||||
@@ -838,17 +948,17 @@ mod tests {
|
||||
assert!(tokens.contains("deepseek-v4-pro"));
|
||||
}
|
||||
|
||||
/// Smoke test: every entry in `COMMANDS` must dispatch to a real handler.
|
||||
/// Smoke test: every entry in `command_infos()` must dispatch to a real handler.
|
||||
/// A dispatch miss surfaces as the fall-through `Unknown command:` error
|
||||
/// message in `execute`. This catches the case where a new command is
|
||||
/// added to `COMMANDS` (so it shows up in `/help` and the palette) but
|
||||
/// added to `command_infos()` (so it shows up in `/help` and the palette) but
|
||||
/// the matching arm in `execute` is forgotten — the user would type the
|
||||
/// command, see it autocomplete, and then get an unhelpful "did you
|
||||
/// mean" suggestion. Also catches panics in handlers because the test
|
||||
/// runner unwinds the panic and reports the offending command.
|
||||
#[test]
|
||||
fn every_registered_command_dispatches_to_a_handler() {
|
||||
for command in COMMANDS {
|
||||
for command in command_infos() {
|
||||
if skip_in_dispatch_smoke(command.name) {
|
||||
continue;
|
||||
}
|
||||
@@ -869,7 +979,7 @@ mod tests {
|
||||
/// just because the registry lists it as an alias of `/exit`.
|
||||
#[test]
|
||||
fn every_command_alias_dispatches_to_a_handler() {
|
||||
for command in COMMANDS {
|
||||
for command in command_infos() {
|
||||
if skip_in_dispatch_smoke(command.name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
//! Slash command input parsing helpers.
|
||||
|
||||
pub(super) struct ParsedCommand<'a> {
|
||||
pub(super) name: String,
|
||||
pub(super) arg: Option<&'a str>,
|
||||
}
|
||||
|
||||
pub(super) fn parse_slash_command(cmd: &str) -> ParsedCommand<'_> {
|
||||
let trimmed = cmd.trim();
|
||||
let mut parts = trimmed.splitn(2, ' ');
|
||||
let raw_command = parts.next().unwrap_or_default();
|
||||
let name = raw_command
|
||||
.strip_prefix('/')
|
||||
.unwrap_or(raw_command)
|
||||
.to_lowercase();
|
||||
let arg = parts.next().map(str::trim);
|
||||
|
||||
ParsedCommand { name, arg }
|
||||
}
|
||||
@@ -1,549 +0,0 @@
|
||||
//! Command registry metadata and lookup helpers.
|
||||
|
||||
use crate::localization::{Locale, MessageId, tr};
|
||||
|
||||
/// Command metadata for help and autocomplete.
|
||||
///
|
||||
/// The English description lives in `localization::english` (private), keyed
|
||||
/// by `description_id`. Callers resolve a localized description through
|
||||
/// [`CommandInfo::description_for`] which delegates to
|
||||
/// [`crate::localization::tr`].
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct CommandInfo {
|
||||
pub name: &'static str,
|
||||
pub aliases: &'static [&'static str],
|
||||
pub usage: &'static str,
|
||||
pub description_id: MessageId,
|
||||
}
|
||||
|
||||
impl CommandInfo {
|
||||
pub fn requires_argument(&self) -> bool {
|
||||
self.usage.contains('<') || self.usage.contains('[')
|
||||
}
|
||||
|
||||
pub fn palette_command(&self) -> String {
|
||||
if self.requires_argument() {
|
||||
format!("/{} ", self.name)
|
||||
} else {
|
||||
format!("/{}", self.name)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn description_for(&self, locale: Locale) -> &'static str {
|
||||
tr(locale, self.description_id)
|
||||
}
|
||||
|
||||
pub fn palette_description_for(&self, locale: Locale) -> String {
|
||||
let desc = self.description_for(locale);
|
||||
if self.aliases.is_empty() {
|
||||
desc.to_string()
|
||||
} else {
|
||||
format!("{} aliases: {}", desc, self.aliases.join(", "))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// All registered commands
|
||||
pub const COMMANDS: &[CommandInfo] = &[
|
||||
// Core commands
|
||||
CommandInfo {
|
||||
name: "anchor",
|
||||
aliases: &["maodian"],
|
||||
usage: "/anchor <text> | /anchor list | /anchor remove <n>",
|
||||
description_id: MessageId::CmdAnchorDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "help",
|
||||
aliases: &["?", "bangzhu", "帮助"],
|
||||
usage: "/help [command]",
|
||||
description_id: MessageId::CmdHelpDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "clear",
|
||||
aliases: &["qingping"],
|
||||
usage: "/clear",
|
||||
description_id: MessageId::CmdClearDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "exit",
|
||||
aliases: &["quit", "q", "tuichu"],
|
||||
usage: "/exit",
|
||||
description_id: MessageId::CmdExitDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "model",
|
||||
aliases: &["moxing"],
|
||||
usage: "/model [name]",
|
||||
description_id: MessageId::CmdModelDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "models",
|
||||
aliases: &["moxingliebiao"],
|
||||
usage: "/models",
|
||||
description_id: MessageId::CmdModelsDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "provider",
|
||||
aliases: &[],
|
||||
usage: "/provider [name] [model]",
|
||||
description_id: MessageId::CmdProviderDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "queue",
|
||||
aliases: &["queued"],
|
||||
usage: "/queue [list|edit <n>|drop <n>|clear]",
|
||||
description_id: MessageId::CmdQueueDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "stash",
|
||||
aliases: &["park"],
|
||||
usage: "/stash [list|pop|clear]",
|
||||
description_id: MessageId::CmdStashDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "hooks",
|
||||
aliases: &["hook", "gouzi"],
|
||||
usage: "/hooks [list|events]",
|
||||
description_id: MessageId::CmdHooksDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "subagents",
|
||||
aliases: &["agents", "zhinengti"],
|
||||
usage: "/subagents",
|
||||
description_id: MessageId::CmdSubagentsDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "agent",
|
||||
aliases: &["daili"],
|
||||
usage: "/agent [N] <task>",
|
||||
description_id: MessageId::CmdAgentDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "links",
|
||||
aliases: &["dashboard", "api", "lianjie"],
|
||||
usage: "/links",
|
||||
description_id: MessageId::CmdLinksDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "feedback",
|
||||
aliases: &[],
|
||||
usage: "/feedback [bug|feature|security]",
|
||||
description_id: MessageId::CmdFeedbackDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "hf",
|
||||
aliases: &["huggingface"],
|
||||
usage: "/hf [mcp <status|setup>|concepts]",
|
||||
description_id: MessageId::CmdHfDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "home",
|
||||
aliases: &["stats", "overview", "zhuye", "shouye"],
|
||||
usage: "/home",
|
||||
description_id: MessageId::CmdHomeDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "workspace",
|
||||
aliases: &["cwd"],
|
||||
usage: "/workspace [path]",
|
||||
description_id: MessageId::CmdWorkspaceDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "note",
|
||||
aliases: &[],
|
||||
usage: "/note [add|list|show|edit|remove|clear|path]",
|
||||
description_id: MessageId::CmdNoteDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "memory",
|
||||
aliases: &[],
|
||||
usage: "/memory [show|path|clear|edit|help]",
|
||||
description_id: MessageId::CmdMemoryDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "attach",
|
||||
aliases: &["image", "media", "fujian"],
|
||||
usage: "/attach <path>",
|
||||
description_id: MessageId::CmdAttachDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "task",
|
||||
aliases: &["tasks"],
|
||||
usage: "/task [add <prompt>|list|show <id>|cancel <id>]",
|
||||
description_id: MessageId::CmdTaskDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "jobs",
|
||||
aliases: &["job", "zuoye"],
|
||||
usage: "/jobs [list|show <id>|poll <id>|wait <id>|stdin <id> <input>|cancel <id>]",
|
||||
description_id: MessageId::CmdJobsDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "mcp",
|
||||
aliases: &[],
|
||||
usage: "/mcp [init|add stdio <name> <command> [args...]|add http <name> <url>|enable <name>|disable <name>|remove <name>|validate|reload]",
|
||||
description_id: MessageId::CmdMcpDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "network",
|
||||
aliases: &[],
|
||||
usage: "/network [list|allow <host>|deny <host>|remove <host>|default <allow|deny|prompt>]",
|
||||
description_id: MessageId::CmdNetworkDescription,
|
||||
},
|
||||
// Session commands
|
||||
CommandInfo {
|
||||
name: "rename",
|
||||
aliases: &["gaiming", "chongmingming"],
|
||||
usage: "/rename <new title>",
|
||||
description_id: MessageId::CmdRenameDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "save",
|
||||
aliases: &[],
|
||||
usage: "/save [path]",
|
||||
description_id: MessageId::CmdSaveDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "fork",
|
||||
aliases: &["branch"],
|
||||
usage: "/fork",
|
||||
description_id: MessageId::CmdForkDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "new",
|
||||
aliases: &[],
|
||||
usage: "/new [--force]",
|
||||
description_id: MessageId::CmdNewDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "sessions",
|
||||
aliases: &["resume"],
|
||||
usage: "/sessions [show|prune <days>]",
|
||||
description_id: MessageId::CmdSessionsDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "load",
|
||||
aliases: &["jiazai"],
|
||||
usage: "/load [path]",
|
||||
description_id: MessageId::CmdLoadDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "compact",
|
||||
aliases: &["yasuo"],
|
||||
usage: "/compact",
|
||||
description_id: MessageId::CmdCompactDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "purge",
|
||||
aliases: &["qingchu"],
|
||||
usage: "/purge",
|
||||
description_id: MessageId::CmdPurgeDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "relay",
|
||||
aliases: &["batonpass", "接力"],
|
||||
usage: "/relay [focus]",
|
||||
description_id: MessageId::CmdRelayDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "context",
|
||||
aliases: &["ctx"],
|
||||
usage: "/context [report|json|summary]",
|
||||
description_id: MessageId::CmdContextDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "export",
|
||||
aliases: &["daochu"],
|
||||
usage: "/export [path]",
|
||||
description_id: MessageId::CmdExportDescription,
|
||||
},
|
||||
// Config commands
|
||||
CommandInfo {
|
||||
name: "config",
|
||||
aliases: &[],
|
||||
usage: "/config",
|
||||
description_id: MessageId::CmdConfigDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "sidebar",
|
||||
aliases: &[],
|
||||
usage: "/sidebar [on|off|auto|work|tasks|agents|context] [--save]",
|
||||
description_id: MessageId::CmdSidebarDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "mode",
|
||||
aliases: &["jihua", "zidong"],
|
||||
usage: "/mode [agent|plan|yolo|1|2|3]",
|
||||
description_id: MessageId::CmdModeDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "theme",
|
||||
aliases: &[],
|
||||
usage: "/theme [name]",
|
||||
description_id: MessageId::CmdThemeDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "verbose",
|
||||
aliases: &[],
|
||||
usage: "/verbose [on|off]",
|
||||
description_id: MessageId::CmdVerboseDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "trust",
|
||||
aliases: &["xinren"],
|
||||
usage: "/trust [on|off|add <path>|remove <path>|list]",
|
||||
description_id: MessageId::CmdTrustDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "logout",
|
||||
aliases: &[],
|
||||
usage: "/logout",
|
||||
description_id: MessageId::CmdLogoutDescription,
|
||||
},
|
||||
// Debug commands
|
||||
CommandInfo {
|
||||
name: "tokens",
|
||||
aliases: &[],
|
||||
usage: "/tokens",
|
||||
description_id: MessageId::CmdTokensDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "translate",
|
||||
aliases: &["translation", "transale"],
|
||||
usage: "/translate",
|
||||
description_id: MessageId::CmdTranslateDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "system",
|
||||
aliases: &["xitong"],
|
||||
usage: "/system",
|
||||
description_id: MessageId::CmdSystemDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "edit",
|
||||
aliases: &[],
|
||||
usage: "/edit",
|
||||
description_id: MessageId::CmdEditDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "diff",
|
||||
aliases: &[],
|
||||
usage: "/diff",
|
||||
description_id: MessageId::CmdDiffDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "change",
|
||||
aliases: &[],
|
||||
usage: "/change [version]",
|
||||
description_id: MessageId::CmdChangeDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "undo",
|
||||
aliases: &[],
|
||||
usage: "/undo",
|
||||
description_id: MessageId::CmdUndoDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "retry",
|
||||
aliases: &["chongshi"],
|
||||
usage: "/retry",
|
||||
description_id: MessageId::CmdRetryDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "init",
|
||||
aliases: &[],
|
||||
usage: "/init",
|
||||
description_id: MessageId::CmdInitDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "lsp",
|
||||
aliases: &[],
|
||||
usage: "/lsp [on|off|status]",
|
||||
description_id: MessageId::CmdLspDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "share",
|
||||
aliases: &[],
|
||||
usage: "/share",
|
||||
description_id: MessageId::CmdShareDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "goal",
|
||||
aliases: &["hunt", "mubiao", "狩猎"],
|
||||
usage: "/goal [objective|clear|pause|resume|complete|blocked] [budget: N]",
|
||||
description_id: MessageId::CmdGoalDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "settings",
|
||||
aliases: &[],
|
||||
usage: "/settings",
|
||||
description_id: MessageId::CmdSettingsDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "status",
|
||||
aliases: &[],
|
||||
usage: "/status",
|
||||
description_id: MessageId::CmdStatusDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "statusline",
|
||||
aliases: &[],
|
||||
usage: "/statusline",
|
||||
description_id: MessageId::CmdStatuslineDescription,
|
||||
},
|
||||
// Skills commands
|
||||
CommandInfo {
|
||||
name: "skills",
|
||||
aliases: &["jinengliebiao"],
|
||||
usage: "/skills [--remote|sync|<prefix>]",
|
||||
description_id: MessageId::CmdSkillsDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "skill",
|
||||
aliases: &["jineng"],
|
||||
usage: "/skill <name|install <spec>|update <name>|uninstall <name>|trust <name>>",
|
||||
description_id: MessageId::CmdSkillDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "review",
|
||||
aliases: &["shencha"],
|
||||
usage: "/review <target>",
|
||||
description_id: MessageId::CmdReviewDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "restore",
|
||||
aliases: &[],
|
||||
usage: "/restore [N|list [N]]",
|
||||
description_id: MessageId::CmdRestoreDescription,
|
||||
},
|
||||
// RLM command
|
||||
CommandInfo {
|
||||
name: "rlm",
|
||||
aliases: &["recursive", "digui"],
|
||||
usage: "/rlm [N] <file_or_text>",
|
||||
description_id: MessageId::CmdRlmDescription,
|
||||
},
|
||||
// Debug/cost command
|
||||
CommandInfo {
|
||||
name: "cost",
|
||||
aliases: &[],
|
||||
usage: "/cost",
|
||||
description_id: MessageId::CmdCostDescription,
|
||||
},
|
||||
// Balance query (#2019)
|
||||
CommandInfo {
|
||||
name: "balance",
|
||||
aliases: &[],
|
||||
usage: "/balance",
|
||||
description_id: MessageId::CmdBalanceDescription,
|
||||
},
|
||||
// Profile switching (#390)
|
||||
CommandInfo {
|
||||
name: "profile",
|
||||
aliases: &["dangan"],
|
||||
usage: "/profile <name>",
|
||||
description_id: MessageId::CmdHelpDescription, // reuse for now
|
||||
},
|
||||
// Cache telemetry (#263)
|
||||
CommandInfo {
|
||||
name: "cache",
|
||||
aliases: &[],
|
||||
usage: "/cache [count|inspect|stats|zones|warmup]",
|
||||
description_id: MessageId::CmdCacheDescription,
|
||||
},
|
||||
// Slop Ledger (#2127)
|
||||
CommandInfo {
|
||||
name: "slop",
|
||||
aliases: &["canzha"],
|
||||
usage: "/slop [query|export]",
|
||||
description_id: MessageId::CmdSlopDescription,
|
||||
},
|
||||
];
|
||||
|
||||
/// Get command info by name or alias
|
||||
pub fn get_command_info(name: &str) -> Option<&'static CommandInfo> {
|
||||
let name = name.strip_prefix('/').unwrap_or(name);
|
||||
COMMANDS
|
||||
.iter()
|
||||
.find(|cmd| cmd.name == name || cmd.aliases.contains(&name))
|
||||
}
|
||||
|
||||
fn edit_distance(a: &str, b: &str) -> usize {
|
||||
if a == b {
|
||||
return 0;
|
||||
}
|
||||
if a.is_empty() {
|
||||
return b.chars().count();
|
||||
}
|
||||
if b.is_empty() {
|
||||
return a.chars().count();
|
||||
}
|
||||
|
||||
let b_chars: Vec<char> = b.chars().collect();
|
||||
let mut prev: Vec<usize> = (0..=b_chars.len()).collect();
|
||||
let mut curr = vec![0usize; b_chars.len() + 1];
|
||||
|
||||
for (i, a_ch) in a.chars().enumerate() {
|
||||
curr[0] = i + 1;
|
||||
for (j, b_ch) in b_chars.iter().enumerate() {
|
||||
let cost = if a_ch == *b_ch { 0 } else { 1 };
|
||||
let delete = prev[j + 1] + 1;
|
||||
let insert = curr[j] + 1;
|
||||
let substitute = prev[j] + cost;
|
||||
curr[j + 1] = delete.min(insert).min(substitute);
|
||||
}
|
||||
std::mem::swap(&mut prev, &mut curr);
|
||||
}
|
||||
|
||||
prev[b_chars.len()]
|
||||
}
|
||||
|
||||
pub(super) fn suggest_command_names(input: &str, limit: usize) -> Vec<String> {
|
||||
let query = input.trim().to_ascii_lowercase();
|
||||
if query.is_empty() || limit == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let mut scored: Vec<(u8, usize, String)> = Vec::new();
|
||||
for command in COMMANDS {
|
||||
let mut best: Option<(u8, usize)> = None;
|
||||
for candidate in std::iter::once(command.name).chain(command.aliases.iter().copied()) {
|
||||
let prefix_match = candidate.starts_with(&query) || query.starts_with(candidate);
|
||||
let contains_match = candidate.contains(&query) || query.contains(candidate);
|
||||
let distance = edit_distance(candidate, &query);
|
||||
let close_typo = distance <= 2;
|
||||
if !(prefix_match || contains_match || close_typo) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let rank = if prefix_match {
|
||||
0
|
||||
} else if contains_match {
|
||||
1
|
||||
} else {
|
||||
2
|
||||
};
|
||||
|
||||
match best {
|
||||
Some((best_rank, best_distance))
|
||||
if rank > best_rank || (rank == best_rank && distance >= best_distance) => {}
|
||||
_ => best = Some((rank, distance)),
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((rank, distance)) = best {
|
||||
scored.push((rank, distance, command.name.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
scored.sort_by(|a, b| {
|
||||
a.0.cmp(&b.0)
|
||||
.then_with(|| a.1.cmp(&b.1))
|
||||
.then_with(|| a.2.cmp(&b.2))
|
||||
});
|
||||
scored
|
||||
.into_iter()
|
||||
.take(limit)
|
||||
.map(|(_, _, name)| name)
|
||||
.collect()
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
//! Command traits and registry support.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::localization::{Locale, MessageId, tr};
|
||||
use crate::tui::app::App;
|
||||
|
||||
use super::CommandResult;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct CommandInfo {
|
||||
pub name: &'static str,
|
||||
pub aliases: &'static [&'static str],
|
||||
pub usage: &'static str,
|
||||
pub description_id: MessageId,
|
||||
}
|
||||
|
||||
impl CommandInfo {
|
||||
pub fn requires_argument(&self) -> bool {
|
||||
self.usage.contains('<') || self.usage.contains('[')
|
||||
}
|
||||
|
||||
pub fn palette_command(&self) -> String {
|
||||
if self.requires_argument() {
|
||||
format!("/{} ", self.name)
|
||||
} else {
|
||||
format!("/{}", self.name)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn description_for(&self, locale: Locale) -> &'static str {
|
||||
tr(locale, self.description_id)
|
||||
}
|
||||
|
||||
pub fn palette_description_for(&self, locale: Locale) -> String {
|
||||
let desc = self.description_for(locale);
|
||||
if self.aliases.is_empty() {
|
||||
desc.to_string()
|
||||
} else {
|
||||
format!("{} aliases: {}", desc, self.aliases.join(", "))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Command: Send + Sync {
|
||||
fn info(&self) -> &'static CommandInfo;
|
||||
fn execute(&self, app: &mut App, args: Option<&str>) -> CommandResult;
|
||||
}
|
||||
|
||||
pub trait CommandGroup: Send + Sync {
|
||||
fn commands(&self) -> Vec<Box<dyn Command>>;
|
||||
}
|
||||
|
||||
pub(crate) type CommandHandler = fn(&mut App, Option<&str>) -> CommandResult;
|
||||
|
||||
pub(crate) struct FunctionCommand {
|
||||
info: &'static CommandInfo,
|
||||
handler: CommandHandler,
|
||||
}
|
||||
|
||||
impl FunctionCommand {
|
||||
pub(crate) const fn new(info: &'static CommandInfo, handler: CommandHandler) -> Self {
|
||||
Self { info, handler }
|
||||
}
|
||||
}
|
||||
|
||||
impl Command for FunctionCommand {
|
||||
fn info(&self) -> &'static CommandInfo {
|
||||
self.info
|
||||
}
|
||||
|
||||
fn execute(&self, app: &mut App, args: Option<&str>) -> CommandResult {
|
||||
(self.handler)(app, args)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CommandRegistry {
|
||||
commands: Vec<Box<dyn Command>>,
|
||||
name_to_index: HashMap<&'static str, usize>,
|
||||
}
|
||||
|
||||
impl CommandRegistry {
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
commands: Vec::new(),
|
||||
name_to_index: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register(&mut self, command: Box<dyn Command>) {
|
||||
let index = self.commands.len();
|
||||
let info = command.info();
|
||||
self.name_to_index.insert(info.name, index);
|
||||
for alias in info.aliases {
|
||||
self.name_to_index.insert(alias, index);
|
||||
}
|
||||
self.commands.push(command);
|
||||
}
|
||||
|
||||
pub fn register_group(&mut self, group: &dyn CommandGroup) {
|
||||
for command in group.commands() {
|
||||
self.register(command);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&self, name: &str) -> Option<&dyn Command> {
|
||||
let name = name.strip_prefix('/').unwrap_or(name);
|
||||
self.name_to_index
|
||||
.get(name)
|
||||
.and_then(|index| self.commands.get(*index))
|
||||
.map(Box::as_ref)
|
||||
}
|
||||
|
||||
pub fn get_info(&self, name: &str) -> Option<&'static CommandInfo> {
|
||||
self.get(name).map(Command::info)
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &dyn Command> {
|
||||
self.commands.iter().map(Box::as_ref)
|
||||
}
|
||||
|
||||
pub fn infos(&self) -> Vec<&'static CommandInfo> {
|
||||
self.iter().map(Command::info).collect()
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,7 @@ pub fn build_entries(
|
||||
) -> Vec<CommandPaletteEntry> {
|
||||
let mut entries = Vec::new();
|
||||
|
||||
for command in commands::COMMANDS {
|
||||
for command in commands::command_infos() {
|
||||
let mut description = command.palette_description_for(locale);
|
||||
if command.requires_argument() {
|
||||
description.push_str(" ");
|
||||
@@ -1163,9 +1163,9 @@ mod tests {
|
||||
.iter()
|
||||
.filter(|entry| entry.section == PaletteSection::Command)
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(command_entries.len(), commands::COMMANDS.len());
|
||||
assert_eq!(command_entries.len(), commands::command_infos().len());
|
||||
|
||||
for command in commands::COMMANDS {
|
||||
for command in commands::command_infos() {
|
||||
let label = format!("/{}", command.name);
|
||||
let matching = command_entries
|
||||
.iter()
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//!
|
||||
//! Renders two stacked sections — *Slash commands* and *Keybindings* — with
|
||||
//! a live substring filter applied as the user types in the search box. The
|
||||
//! command list is sourced from [`crate::commands::COMMANDS`] and the
|
||||
//! command list is sourced from [`crate::commands::command_infos()`] and the
|
||||
//! keybinding list from [`crate::tui::keybindings::KEYBINDINGS`] so neither
|
||||
//! can drift from the wired-up handlers.
|
||||
//!
|
||||
@@ -202,7 +202,7 @@ impl HelpView {
|
||||
fn build_entries(locale: Locale) -> Vec<HelpEntry> {
|
||||
let mut entries = Vec::new();
|
||||
|
||||
for command in commands::COMMANDS {
|
||||
for command in commands::command_infos() {
|
||||
let label = format!("/{}", command.name);
|
||||
let localized = command.description_for(locale);
|
||||
let description = if command.aliases.is_empty() {
|
||||
@@ -515,7 +515,7 @@ mod tests {
|
||||
fn empty_filter_lists_all_entries() {
|
||||
let view = HelpView::new();
|
||||
// Total = registered slash commands + catalogued keybindings.
|
||||
let expected = commands::COMMANDS.len() + KEYBINDINGS.len();
|
||||
let expected = commands::command_infos().len() + KEYBINDINGS.len();
|
||||
assert_eq!(view.filtered.len(), expected);
|
||||
assert_eq!(view.entries.len(), expected);
|
||||
}
|
||||
|
||||
@@ -2313,7 +2313,7 @@ pub(crate) fn slash_completion_hints(
|
||||
// ── Phase 2: contains (substring) matches ─────────────────────────
|
||||
// Medium priority — broader catching.
|
||||
if completing_skill_arg.is_none() {
|
||||
for cmd in commands::COMMANDS {
|
||||
for cmd in commands::command_infos() {
|
||||
let name = format!("/{}", cmd.name);
|
||||
if seen.contains(&name) {
|
||||
continue;
|
||||
@@ -2340,7 +2340,7 @@ pub(crate) fn slash_completion_hints(
|
||||
// ── Phase 3: fuzzy subsequence matches ────────────────────────────
|
||||
// Lowest priority — characters in order, not necessarily consecutive.
|
||||
if completing_skill_arg.is_none() {
|
||||
for cmd in commands::COMMANDS {
|
||||
for cmd in commands::command_infos() {
|
||||
let name = format!("/{}", cmd.name);
|
||||
if seen.contains(&name) {
|
||||
continue;
|
||||
@@ -2457,7 +2457,7 @@ fn all_command_names_matching_loaded(
|
||||
user_commands: &[(String, String)],
|
||||
) -> Vec<String> {
|
||||
let prefix = prefix.strip_prefix('/').unwrap_or(prefix).to_lowercase();
|
||||
let mut result: Vec<String> = commands::COMMANDS
|
||||
let mut result: Vec<String> = commands::command_infos()
|
||||
.iter()
|
||||
.filter(|cmd| {
|
||||
cmd.name.starts_with(&prefix) || cmd.aliases.iter().any(|a| a.starts_with(&prefix))
|
||||
|
||||
Reference in New Issue
Block a user