feat(commands): add pinyin aliases for all commands

This commit is contained in:
markchang
2026-05-09 21:06:50 +08:00
parent 39b2d528cd
commit 00bb1d3ff3
4 changed files with 133 additions and 71 deletions
+58 -58
View File
@@ -137,37 +137,37 @@ pub const COMMANDS: &[CommandInfo] = &[
// Core commands
CommandInfo {
name: "anchor",
aliases: &[],
aliases: &["maodian"],
usage: "/anchor <text> | /anchor list | /anchor remove <n>",
description_id: MessageId::CmdAnchorDescription,
},
CommandInfo {
name: "help",
aliases: &["?"],
aliases: &["?", "bangzhu", "帮助"],
usage: "/help [command]",
description_id: MessageId::CmdHelpDescription,
},
CommandInfo {
name: "clear",
aliases: &[],
aliases: &["qingping"],
usage: "/clear",
description_id: MessageId::CmdClearDescription,
},
CommandInfo {
name: "exit",
aliases: &["quit", "q"],
aliases: &["quit", "q", "tuichu"],
usage: "/exit",
description_id: MessageId::CmdExitDescription,
},
CommandInfo {
name: "model",
aliases: &[],
aliases: &["moxing"],
usage: "/model [name]",
description_id: MessageId::CmdModelDescription,
},
CommandInfo {
name: "models",
aliases: &[],
aliases: &["moxingliebiao"],
usage: "/models",
description_id: MessageId::CmdModelsDescription,
},
@@ -191,25 +191,25 @@ pub const COMMANDS: &[CommandInfo] = &[
},
CommandInfo {
name: "hooks",
aliases: &["hook"],
aliases: &["hook", "gouzi"],
usage: "/hooks [list|events]",
description_id: MessageId::CmdHooksDescription,
},
CommandInfo {
name: "subagents",
aliases: &["agents"],
aliases: &["agents", "zhinengti"],
usage: "/subagents",
description_id: MessageId::CmdSubagentsDescription,
},
CommandInfo {
name: "links",
aliases: &["dashboard", "api"],
aliases: &["dashboard", "api", "lianjie"],
usage: "/links",
description_id: MessageId::CmdLinksDescription,
},
CommandInfo {
name: "home",
aliases: &["stats", "overview"],
aliases: &["stats", "overview", "zhuye", "shouye"],
usage: "/home",
description_id: MessageId::CmdHomeDescription,
},
@@ -227,7 +227,7 @@ pub const COMMANDS: &[CommandInfo] = &[
},
CommandInfo {
name: "attach",
aliases: &["image", "media"],
aliases: &["image", "media", "fujian"],
usage: "/attach <path>",
description_id: MessageId::CmdAttachDescription,
},
@@ -239,7 +239,7 @@ pub const COMMANDS: &[CommandInfo] = &[
},
CommandInfo {
name: "jobs",
aliases: &["job"],
aliases: &["job", "zuoye"],
usage: "/jobs [list|show <id>|poll <id>|wait <id>|stdin <id> <input>|cancel <id>]",
description_id: MessageId::CmdJobsDescription,
},
@@ -258,7 +258,7 @@ pub const COMMANDS: &[CommandInfo] = &[
// Session commands
CommandInfo {
name: "rename",
aliases: &[],
aliases: &["gaiming", "chongmingming"],
usage: "/rename <new title>",
description_id: MessageId::CmdRenameDescription,
},
@@ -276,13 +276,13 @@ pub const COMMANDS: &[CommandInfo] = &[
},
CommandInfo {
name: "load",
aliases: &[],
aliases: &["jiazai"],
usage: "/load [path]",
description_id: MessageId::CmdLoadDescription,
},
CommandInfo {
name: "compact",
aliases: &[],
aliases: &["yasuo"],
usage: "/compact",
description_id: MessageId::CmdCompactDescription,
},
@@ -294,7 +294,7 @@ pub const COMMANDS: &[CommandInfo] = &[
},
CommandInfo {
name: "cycles",
aliases: &[],
aliases: &["zhouqi"],
usage: "/cycles",
description_id: MessageId::CmdCyclesDescription,
},
@@ -312,7 +312,7 @@ pub const COMMANDS: &[CommandInfo] = &[
},
CommandInfo {
name: "export",
aliases: &[],
aliases: &["daochu"],
usage: "/export [path]",
description_id: MessageId::CmdExportDescription,
},
@@ -325,25 +325,25 @@ pub const COMMANDS: &[CommandInfo] = &[
},
CommandInfo {
name: "yolo",
aliases: &[],
aliases: &["zidong"],
usage: "/yolo",
description_id: MessageId::CmdYoloDescription,
},
CommandInfo {
name: "agent",
aliases: &[],
aliases: &["daili"],
usage: "/agent",
description_id: MessageId::CmdAgentDescription,
},
CommandInfo {
name: "plan",
aliases: &[],
aliases: &["jihua"],
usage: "/plan",
description_id: MessageId::CmdPlanDescription,
},
CommandInfo {
name: "trust",
aliases: &[],
aliases: &["xinren"],
usage: "/trust [on|off|add <path>|remove <path>|list]",
description_id: MessageId::CmdTrustDescription,
},
@@ -362,7 +362,7 @@ pub const COMMANDS: &[CommandInfo] = &[
},
CommandInfo {
name: "system",
aliases: &[],
aliases: &["xitong"],
usage: "/system",
description_id: MessageId::CmdSystemDescription,
},
@@ -386,7 +386,7 @@ pub const COMMANDS: &[CommandInfo] = &[
},
CommandInfo {
name: "retry",
aliases: &[],
aliases: &["chongshi"],
usage: "/retry",
description_id: MessageId::CmdRetryDescription,
},
@@ -410,7 +410,7 @@ pub const COMMANDS: &[CommandInfo] = &[
},
CommandInfo {
name: "goal",
aliases: &[],
aliases: &["mubiao"],
usage: "/goal [objective] [budget: N]",
description_id: MessageId::CmdGoalDescription,
},
@@ -429,19 +429,19 @@ pub const COMMANDS: &[CommandInfo] = &[
// Skills commands
CommandInfo {
name: "skills",
aliases: &[],
aliases: &["jinengliebiao"],
usage: "/skills [--remote|sync]",
description_id: MessageId::CmdSkillsDescription,
},
CommandInfo {
name: "skill",
aliases: &[],
aliases: &["jineng"],
usage: "/skill <name|install <spec>|update <name>|uninstall <name>|trust <name>>",
description_id: MessageId::CmdSkillDescription,
},
CommandInfo {
name: "review",
aliases: &[],
aliases: &["shencha"],
usage: "/review <target>",
description_id: MessageId::CmdReviewDescription,
},
@@ -454,7 +454,7 @@ pub const COMMANDS: &[CommandInfo] = &[
// RLM command
CommandInfo {
name: "rlm",
aliases: &["recursive"],
aliases: &["recursive", "digui"],
usage: "/rlm <prompt>",
description_id: MessageId::CmdRlmDescription,
},
@@ -468,7 +468,7 @@ pub const COMMANDS: &[CommandInfo] = &[
// Profile switching (#390)
CommandInfo {
name: "profile",
aliases: &[],
aliases: &["dangan"],
usage: "/profile <name>",
description_id: MessageId::CmdHelpDescription, // reuse for now
},
@@ -497,52 +497,52 @@ pub fn execute(cmd: &str, app: &mut App) -> CommandResult {
match command {
// Core commands
"anchor" => anchor::anchor(app, arg),
"help" | "?" => core::help(app, arg),
"clear" => core::clear(app),
"exit" | "quit" | "q" => core::exit(),
"model" => core::model(app, arg),
"models" => core::models(app),
"help" | "?" | "bangzhu" | "帮助" => core::help(app, arg),
"clear" | "qingping" => core::clear(app),
"exit" | "quit" | "q" | "tuichu" => core::exit(),
"model" | "moxing" => core::model(app, arg),
"models" | "moxingliebiao" => core::models(app),
"provider" => provider::provider(app, arg),
"queue" | "queued" => queue::queue(app, arg),
"stash" | "park" => stash::stash(app, arg),
"hooks" | "hook" => hooks::hooks(app, arg),
"subagents" | "agents" => core::subagents(app),
"links" | "dashboard" | "api" => core::deepseek_links(app),
"home" | "stats" | "overview" => core::home_dashboard(app),
"hooks" | "hook" | "gouzi" => hooks::hooks(app, arg),
"subagents" | "agents" | "zhinengti" => core::subagents(app),
"links" | "dashboard" | "api" | "lianjie" => core::deepseek_links(app),
"home" | "stats" | "overview" | "zhuye" | "shouye" => core::home_dashboard(app),
"note" => note::note(app, arg),
"memory" => memory::memory(app, arg),
"attach" | "image" | "media" => attachment::attach(app, arg),
"attach" | "image" | "media" | "fujian" => attachment::attach(app, arg),
"task" | "tasks" => task::task(app, arg),
"jobs" | "job" => jobs::jobs(app, arg),
"jobs" | "job" | "zuoye" => jobs::jobs(app, arg),
"mcp" => mcp::mcp(app, arg),
"network" => network::network(app, arg),
// Session commands
"rename" => rename::rename(app, arg),
"rename" | "gaiming" | "chongmingming" => rename::rename(app, arg),
"save" => session::save(app, arg),
"sessions" | "resume" => session::sessions(app, arg),
"load" => session::load(app, arg),
"compact" => session::compact(app),
"cycles" => cycle::list_cycles(app),
"load" | "jiazai" => session::load(app, arg),
"compact" | "yasuo" => session::compact(app),
"cycles" | "zhouqi" => cycle::list_cycles(app),
"cycle" => cycle::show_cycle(app, arg),
"recall" => cycle::recall_archive(app, arg),
"export" => session::export(app, arg),
"export" | "daochu" => session::export(app, arg),
// Config commands
"config" => config::config_command(app, arg),
"settings" => config::show_settings(app),
"statusline" | "status" => config::status_line(app),
"yolo" => config::yolo(app),
"agent" => config::agent_mode(app),
"plan" => config::plan_mode(app),
"trust" => config::trust(app, arg),
"yolo" | "zidong" => config::yolo(app),
"agent" | "daili" => config::agent_mode(app),
"plan" | "jihua" => config::plan_mode(app),
"trust" | "xinren" => config::trust(app, arg),
"logout" => config::logout(app),
// Debug commands
"tokens" => debug::tokens(app),
"cost" => debug::cost(app),
"cache" => debug::cache(app, arg),
"system" => debug::system_prompt(app),
"system" | "xitong" => debug::system_prompt(app),
"context" | "ctx" => debug::context(app),
"edit" => debug::edit(app),
"diff" => debug::diff(app),
@@ -561,25 +561,25 @@ pub fn execute(cmd: &str, app: &mut App) -> CommandResult {
result
}
}
"retry" => debug::retry(app),
"retry" | "chongshi" => debug::retry(app),
// Project commands
"init" => init::init(app),
"lsp" => config::lsp_command(app, arg),
"share" => share::share(app, arg),
"goal" => goal::goal(app, arg),
"goal" | "mubiao" => goal::goal(app, arg),
// Skills commands
"skills" => skills::list_skills(app, arg),
"skill" => skills::run_skill(app, arg),
"review" => review::review(app, arg),
"skill" | "jineng" => skills::run_skill(app, arg),
"review" | "shencha" => review::review(app, arg),
"restore" => restore::restore(app, arg),
// Profile switch (#390)
"profile" => core::profile_switch(app, arg),
"profile" | "dangan" => core::profile_switch(app, arg),
// RLM command
"rlm" | "recursive" => rlm(app, arg),
"rlm" | "recursive" | "digui" => rlm(app, arg),
// Legacy command migrations (kept out of registry/autocomplete intentionally).
"set" => CommandResult::error(
@@ -863,7 +863,7 @@ mod tests {
.iter()
.find(|cmd| cmd.name == "links")
.expect("links command should exist");
assert_eq!(links.aliases, &["dashboard", "api"]);
assert_eq!(links.aliases, &["dashboard", "api", "lianjie"]);
}
#[test]
@@ -918,7 +918,7 @@ mod tests {
#[test]
fn execute_links_and_aliases_return_links_message() {
let mut app = create_test_app();
for cmd in ["/links", "/dashboard", "/api"] {
for cmd in ["/links", "/dashboard", "/api", "/lianjie"] {
let result = execute(cmd, &mut app);
let msg = result.message.expect("links commands should return text");
assert!(msg.contains("https://platform.deepseek.com"));
+6 -3
View File
@@ -448,7 +448,7 @@ fn handle_command_deepseek(
}
match trimmed {
"/help" => {
"/help" | "/?" | "/bangzhu" | "/帮助" => {
print_help();
}
"/history" => {
@@ -488,7 +488,7 @@ fn handle_command_official(
}
match trimmed {
"/help" => {
"/help" | "/?" | "/bangzhu" | "/帮助" => {
print_help();
}
"/history" => {
@@ -668,6 +668,9 @@ fn create_editor() -> Result<Editor<CommandCompleter, DefaultHistory>> {
let helper = CommandCompleter {
commands: vec![
"/help".to_string(),
"/?".to_string(),
"/bangzhu".to_string(),
"/帮助".to_string(),
"/clear".to_string(),
"/history".to_string(),
"/stats".to_string(),
@@ -753,4 +756,4 @@ async fn handle_line_official(
eprintln!("{} {}", ds_red("Error:").bold(), error);
}
Ok(false)
}
}
+4 -1
View File
@@ -1687,11 +1687,13 @@ fn apply_slash_menu_selection_appends_space_for_arg_commands() {
name: "/model".to_string(),
description: String::new(),
is_skill: false,
alias_hint: None,
},
crate::tui::widgets::SlashMenuEntry {
name: "/settings".to_string(),
description: String::new(),
is_skill: false,
alias_hint: None,
},
];
app.slash_menu_selected = 0;
@@ -1706,6 +1708,7 @@ fn apply_slash_menu_selection_uses_skill_command_form() {
name: "/skill search-files".to_string(),
description: "Search files".to_string(),
is_skill: true,
alias_hint: None,
}];
assert!(apply_slash_menu_selection(&mut app, &entries, true));
@@ -3792,4 +3795,4 @@ fn subagent_completion_notification_can_include_elapsed_summary() {
assert!(msg.contains("deepseek: sub-agent agent_live complete"));
assert!(msg.contains("deepseek: sub-agent complete (1m 5s)"));
}
}
+65 -9
View File
@@ -806,8 +806,24 @@ impl Renderable for ComposerWidget<'_> {
};
let menu_bottom = (menu_top + menu_visible_rows).min(menu_total);
// Label column width for two-column layout (name + description)
let label_width = 22.min(content_width.saturating_sub(4));
// Label column width — grows to fit the widest visible name
// (including alias hint like " or /bangzhu") but stays bounded.
let label_width = self
.slash_menu_entries
.iter()
.take(menu_bottom)
.skip(menu_top)
.map(|e| {
if let Some(ref hint) = e.alias_hint {
format!("{} or /{}", e.name, hint).width()
} else {
e.name.width()
}
})
.max()
.unwrap_or(22)
.min(content_width.saturating_sub(4))
.max(8);
for (idx, entry) in self
.slash_menu_entries
.iter()
@@ -841,12 +857,20 @@ impl Renderable for ComposerWidget<'_> {
Style::default().fg(palette::TEXT_DIM)
};
// Build display name: canonical name, with "or /alias" hint
// when the user typed via a pinyin alias.
let display_name = if let Some(ref hint) = entry.alias_hint {
format!("{} or /{}", entry.name, hint)
} else {
entry.name.clone()
};
let name_display = {
let display_width: usize = entry.name.width();
let display_width: usize = display_name.width();
if display_width > label_width {
let mut s = String::new();
let mut w = 0;
for ch in entry.name.chars() {
for ch in display_name.chars() {
let cw = ch.width().unwrap_or(0);
if w + cw + 1 > label_width {
break;
@@ -862,7 +886,7 @@ impl Renderable for ComposerWidget<'_> {
s
} else {
// pad to label_width display cols
let mut s = entry.name.clone();
let mut s = display_name;
while s.width() < label_width {
s.push(' ');
}
@@ -1797,6 +1821,9 @@ pub(crate) struct SlashMenuEntry {
pub name: String,
pub description: String,
pub is_skill: bool,
/// Matching pinyin/alias prefix hint, e.g. when user types `/bang` and
/// the command `/help` matches via alias `bangzhu`.
pub alias_hint: Option<String>,
}
pub(crate) fn slash_completion_hints(
@@ -1821,17 +1848,42 @@ pub(crate) fn slash_completion_hints(
// built-in ones from the static registry and use a generic label for
// user-defined commands.
if completing_skill_arg.is_none() {
let prefix_lower = prefix.to_ascii_lowercase();
for name in commands::all_command_names_matching(prefix) {
let command_key = name.trim_start_matches('/');
let description = if let Some(info) = commands::get_command_info(command_key) {
info.description_for(locale).to_string()
let (description, alias_hint) = if let Some(info) = commands::get_command_info(command_key) {
// Detect matching alias: if the user typed via pinyin rather
// than the canonical name, record which alias matched.
let hint = if !command_key.to_ascii_lowercase().starts_with(&prefix_lower) {
info.aliases
.iter()
.find(|a| a.to_ascii_lowercase().starts_with(&prefix_lower))
.map(|a| a.to_string())
} else {
None
};
let desc = if info.aliases.is_empty() {
info.description_for(locale).to_string()
} else {
format!(
"{} (aliases: {})",
info.description_for(locale),
info.aliases
.iter()
.map(|a| format!("/{a}"))
.collect::<Vec<_>>()
.join(", ")
)
};
(desc, hint)
} else {
String::from("User-defined command")
(String::from("User-defined command"), None)
};
entries.push(SlashMenuEntry {
name,
description,
is_skill: false,
alias_hint,
});
}
}
@@ -1852,6 +1904,7 @@ pub(crate) fn slash_completion_hints(
name: format!("/skill {skill_name}"),
description: skill_desc.clone(),
is_skill: true,
alias_hint: None,
});
}
}
@@ -1863,6 +1916,7 @@ pub(crate) fn slash_completion_hints(
name: format!("/model {model_name}"),
description: String::from("Switch to this model"),
is_skill: false,
alias_hint: None,
});
}
}
@@ -2426,12 +2480,14 @@ mod tests {
name: format!("/skill{i}"),
description: String::new(),
is_skill: false,
alias_hint: None,
})
.collect();
let one_match = vec![SlashMenuEntry {
name: "/skill".to_string(),
description: String::new(),
is_skill: false,
alias_hint: None,
}];
let no_matches = Vec::<SlashMenuEntry>::new();
@@ -3047,4 +3103,4 @@ mod tests {
);
}
}
}
}