Merge remote-tracking branch 'origin/pr/1306' into work/v0.8.34

# Conflicts:
#	crates/tui/src/commands/mod.rs
#	crates/tui/src/tui/ui/tests.rs
#	crates/tui/src/tui/widgets/mod.rs
This commit is contained in:
Hunter Bown
2026-05-12 23:26:13 -05:00
4 changed files with 130 additions and 66 deletions
+58 -56
View File
@@ -142,37 +142,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,
},
@@ -196,25 +196,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: "agent",
aliases: &[],
aliases: &["daili"],
usage: "/agent [N] <task>",
description_id: MessageId::CmdAgentDescription,
},
CommandInfo {
name: "links",
aliases: &["dashboard", "api"],
aliases: &["dashboard", "api", "lianjie"],
usage: "/links",
description_id: MessageId::CmdLinksDescription,
},
@@ -226,7 +226,7 @@ pub const COMMANDS: &[CommandInfo] = &[
},
CommandInfo {
name: "home",
aliases: &["stats", "overview"],
aliases: &["stats", "overview", "zhuye", "shouye"],
usage: "/home",
description_id: MessageId::CmdHomeDescription,
},
@@ -244,7 +244,7 @@ pub const COMMANDS: &[CommandInfo] = &[
},
CommandInfo {
name: "attach",
aliases: &["image", "media"],
aliases: &["image", "media", "fujian"],
usage: "/attach <path>",
description_id: MessageId::CmdAttachDescription,
},
@@ -256,7 +256,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,
},
@@ -275,7 +275,7 @@ pub const COMMANDS: &[CommandInfo] = &[
// Session commands
CommandInfo {
name: "rename",
aliases: &[],
aliases: &["gaiming", "chongmingming"],
usage: "/rename <new title>",
description_id: MessageId::CmdRenameDescription,
},
@@ -293,13 +293,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,
},
@@ -317,7 +317,7 @@ pub const COMMANDS: &[CommandInfo] = &[
},
CommandInfo {
name: "cycles",
aliases: &[],
aliases: &["zhouqi"],
usage: "/cycles",
description_id: MessageId::CmdCyclesDescription,
},
@@ -335,7 +335,7 @@ pub const COMMANDS: &[CommandInfo] = &[
},
CommandInfo {
name: "export",
aliases: &[],
aliases: &["daochu"],
usage: "/export [path]",
description_id: MessageId::CmdExportDescription,
},
@@ -348,7 +348,7 @@ pub const COMMANDS: &[CommandInfo] = &[
},
CommandInfo {
name: "mode",
aliases: &[],
aliases: &["jihua", "zidong"],
usage: "/mode [agent|plan|yolo|1|2|3]",
description_id: MessageId::CmdModeDescription,
},
@@ -366,7 +366,7 @@ pub const COMMANDS: &[CommandInfo] = &[
},
CommandInfo {
name: "trust",
aliases: &[],
aliases: &["xinren"],
usage: "/trust [on|off|add <path>|remove <path>|list]",
description_id: MessageId::CmdTrustDescription,
},
@@ -391,7 +391,7 @@ pub const COMMANDS: &[CommandInfo] = &[
},
CommandInfo {
name: "system",
aliases: &[],
aliases: &["xitong"],
usage: "/system",
description_id: MessageId::CmdSystemDescription,
},
@@ -421,7 +421,7 @@ pub const COMMANDS: &[CommandInfo] = &[
},
CommandInfo {
name: "retry",
aliases: &[],
aliases: &["chongshi"],
usage: "/retry",
description_id: MessageId::CmdRetryDescription,
},
@@ -445,7 +445,7 @@ pub const COMMANDS: &[CommandInfo] = &[
},
CommandInfo {
name: "goal",
aliases: &[],
aliases: &["mubiao"],
usage: "/goal [objective] [budget: N]",
description_id: MessageId::CmdGoalDescription,
},
@@ -470,19 +470,19 @@ pub const COMMANDS: &[CommandInfo] = &[
// Skills commands
CommandInfo {
name: "skills",
aliases: &[],
aliases: &["jinengliebiao"],
usage: "/skills [--remote|sync|<prefix>]",
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,
},
@@ -495,7 +495,7 @@ pub const COMMANDS: &[CommandInfo] = &[
// RLM command
CommandInfo {
name: "rlm",
aliases: &["recursive"],
aliases: &["recursive", "digui"],
usage: "/rlm [N] <file_or_text>",
description_id: MessageId::CmdRlmDescription,
},
@@ -509,7 +509,7 @@ pub const COMMANDS: &[CommandInfo] = &[
// Profile switching (#390)
CommandInfo {
name: "profile",
aliases: &[],
aliases: &["dangan"],
usage: "/profile <name>",
description_id: MessageId::CmdHelpDescription, // reuse for now
},
@@ -537,40 +537,40 @@ pub fn execute(cmd: &str, app: &mut App) -> CommandResult {
// Match command or alias
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),
"anchor" | "maodian" => anchor::anchor(app, arg),
"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),
"agent" => agent(app, arg),
"links" | "dashboard" | "api" => core::deepseek_links(app),
"hooks" | "hook" | "gouzi" => hooks::hooks(app, arg),
"subagents" | "agents" | "zhinengti" => core::subagents(app),
"agent" | "daili" => agent(app, arg),
"links" | "dashboard" | "api" | "lianjie" => core::deepseek_links(app),
"feedback" => feedback::feedback(app, arg),
"home" | "stats" | "overview" => core::home_dashboard(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),
"relay" | "batonpass" | "接力" => relay(app, arg),
"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),
@@ -578,9 +578,11 @@ pub fn execute(cmd: &str, app: &mut App) -> CommandResult {
"status" => status::status(app),
"statusline" => config::status_line(app),
"mode" => config::mode(app, arg),
"jihua" => config::mode(app, Some("plan")),
"zidong" => config::mode(app, Some("yolo")),
"theme" => config::theme(app, arg),
"verbose" => config::verbose(app, arg),
"trust" => config::trust(app, arg),
"trust" | "xinren" => config::trust(app, arg),
"logout" => config::logout(app),
// Debug commands
@@ -591,7 +593,7 @@ pub fn execute(cmd: &str, app: &mut App) -> CommandResult {
// ChangeLog command
"change" => change::change(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),
@@ -610,25 +612,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(
@@ -1096,7 +1098,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]
@@ -1281,7 +1283,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"));
+5 -2
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(),
+3
View File
@@ -2521,11 +2521,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;
@@ -2553,6 +2555,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));
+64 -8
View File
@@ -846,8 +846,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()
@@ -881,12 +897,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;
@@ -902,7 +926,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(' ');
}
@@ -2010,6 +2034,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(
@@ -2035,17 +2062,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, workspace) {
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,
});
}
}
@@ -2062,6 +2114,7 @@ pub(crate) fn slash_completion_hints(
name: format!("/skill {skill_name}"),
description: skill_desc.clone(),
is_skill: true,
alias_hint: None,
});
}
}
@@ -2074,6 +2127,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,
});
}
}
@@ -2639,12 +2693,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();