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:
CodeWhale Agent
2026-06-12 14:04:22 -07:00
parent 89a9981bf9
commit 4200b64365
16 changed files with 1126 additions and 639 deletions
@@ -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,
+217
View File
@@ -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,
+127
View File
@@ -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,
+19 -5
View File
@@ -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
View File
@@ -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;
}
-19
View File
@@ -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 }
}
-549
View File
@@ -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()
}
+125
View File
@@ -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()
}
}
+3 -3
View File
@@ -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()
+3 -3
View File
@@ -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);
}
+3 -3
View File
@@ -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))