From 00bb1d3ff3b9a9993a4481be5edfe8a0f19c869d Mon Sep 17 00:00:00 2001 From: markchang <526582458@qq.com> Date: Sat, 9 May 2026 21:06:50 +0800 Subject: [PATCH] feat(commands): add pinyin aliases for all commands --- crates/tui/src/commands/mod.rs | 116 +++++++++++++++--------------- crates/tui/src/modules/text.rs | 9 ++- crates/tui/src/tui/ui/tests.rs | 5 +- crates/tui/src/tui/widgets/mod.rs | 74 ++++++++++++++++--- 4 files changed, 133 insertions(+), 71 deletions(-) diff --git a/crates/tui/src/commands/mod.rs b/crates/tui/src/commands/mod.rs index 093a61aa..c2405457 100644 --- a/crates/tui/src/commands/mod.rs +++ b/crates/tui/src/commands/mod.rs @@ -137,37 +137,37 @@ pub const COMMANDS: &[CommandInfo] = &[ // Core commands CommandInfo { name: "anchor", - aliases: &[], + aliases: &["maodian"], usage: "/anchor | /anchor list | /anchor remove ", 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 ", description_id: MessageId::CmdAttachDescription, }, @@ -239,7 +239,7 @@ pub const COMMANDS: &[CommandInfo] = &[ }, CommandInfo { name: "jobs", - aliases: &["job"], + aliases: &["job", "zuoye"], usage: "/jobs [list|show |poll |wait |stdin |cancel ]", description_id: MessageId::CmdJobsDescription, }, @@ -258,7 +258,7 @@ pub const COMMANDS: &[CommandInfo] = &[ // Session commands CommandInfo { name: "rename", - aliases: &[], + aliases: &["gaiming", "chongmingming"], usage: "/rename ", 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 |remove |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 |update |uninstall |trust >", description_id: MessageId::CmdSkillDescription, }, CommandInfo { name: "review", - aliases: &[], + aliases: &["shencha"], usage: "/review ", description_id: MessageId::CmdReviewDescription, }, @@ -454,7 +454,7 @@ pub const COMMANDS: &[CommandInfo] = &[ // RLM command CommandInfo { name: "rlm", - aliases: &["recursive"], + aliases: &["recursive", "digui"], usage: "/rlm ", description_id: MessageId::CmdRlmDescription, }, @@ -468,7 +468,7 @@ pub const COMMANDS: &[CommandInfo] = &[ // Profile switching (#390) CommandInfo { name: "profile", - aliases: &[], + aliases: &["dangan"], usage: "/profile ", 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")); diff --git a/crates/tui/src/modules/text.rs b/crates/tui/src/modules/text.rs index 66553112..fe071141 100644 --- a/crates/tui/src/modules/text.rs +++ b/crates/tui/src/modules/text.rs @@ -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> { 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) -} +} \ No newline at end of file diff --git a/crates/tui/src/tui/ui/tests.rs b/crates/tui/src/tui/ui/tests.rs index 608f8878..b89e7867 100644 --- a/crates/tui/src/tui/ui/tests.rs +++ b/crates/tui/src/tui/ui/tests.rs @@ -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)")); -} +} \ No newline at end of file diff --git a/crates/tui/src/tui/widgets/mod.rs b/crates/tui/src/tui/widgets/mod.rs index 3dd1633a..98583a74 100644 --- a/crates/tui/src/tui/widgets/mod.rs +++ b/crates/tui/src/tui/widgets/mod.rs @@ -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, } 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::>() + .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::::new(); @@ -3047,4 +3103,4 @@ mod tests { ); } } -} +} \ No newline at end of file