mod anchor;
mod attachment;
mod change;
mod config;
mod core;
mod cycle;
mod debug;
mod feedback;
mod goal;
mod hooks;
mod init;
mod jobs;
mod mcp;
mod memory;
mod network;
mod note;
mod provider;
mod queue;
mod rename;
mod restore;
mod review;
mod session;
pub mod share;
mod skills;
mod stash;
mod status;
mod task;
mod user_commands;
use std::fmt::Write as _;
use crate::localization::{Locale, MessageId, tr};
use crate::tui::app::{App, AppAction};
#[derive(Debug, Clone)]
pub struct CommandResult {
pub message: Option<String>,
pub action: Option<AppAction>,
pub is_error: bool,
}
impl CommandResult {
pub fn ok() -> Self {
Self {
message: None,
action: None,
is_error: false,
}
}
pub fn message(msg: impl Into<String>) -> Self {
Self {
message: Some(msg.into()),
action: None,
is_error: false,
}
}
pub fn action(action: AppAction) -> Self {
Self {
message: None,
action: Some(action),
is_error: false,
}
}
#[allow(dead_code)]
pub fn with_message_and_action(msg: impl Into<String>, action: AppAction) -> Self {
Self {
message: Some(msg.into()),
action: Some(action),
is_error: false,
}
}
pub fn error(msg: impl Into<String>) -> Self {
Self {
message: Some(format!("Error: {}", msg.into())),
action: None,
is_error: true,
}
}
}
#[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 const COMMANDS: &[CommandInfo] = &[
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]",
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: "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,
},
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: "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: "relay",
aliases: &["batonpass", "接力"],
usage: "/relay [focus]",
description_id: MessageId::CmdRelayDescription,
},
CommandInfo {
name: "context",
aliases: &["ctx"],
usage: "/context",
description_id: MessageId::CmdContextDescription,
},
CommandInfo {
name: "cycles",
aliases: &["zhouqi"],
usage: "/cycles",
description_id: MessageId::CmdCyclesDescription,
},
CommandInfo {
name: "cycle",
aliases: &[],
usage: "/cycle <n>",
description_id: MessageId::CmdCycleDescription,
},
CommandInfo {
name: "recall",
aliases: &[],
usage: "/recall <query>",
description_id: MessageId::CmdRecallDescription,
},
CommandInfo {
name: "export",
aliases: &["daochu"],
usage: "/export [path]",
description_id: MessageId::CmdExportDescription,
},
CommandInfo {
name: "config",
aliases: &[],
usage: "/config",
description_id: MessageId::CmdConfigDescription,
},
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,
},
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: &["mubiao"],
usage: "/goal [objective] [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,
},
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]",
description_id: MessageId::CmdRestoreDescription,
},
CommandInfo {
name: "rlm",
aliases: &["recursive", "digui"],
usage: "/rlm [N] <file_or_text>",
description_id: MessageId::CmdRlmDescription,
},
CommandInfo {
name: "cost",
aliases: &[],
usage: "/cost",
description_id: MessageId::CmdCostDescription,
},
CommandInfo {
name: "profile",
aliases: &["dangan"],
usage: "/profile <name>",
description_id: MessageId::CmdHelpDescription, },
CommandInfo {
name: "cache",
aliases: &[],
usage: "/cache [count|inspect|warmup]",
description_id: MessageId::CmdCacheDescription,
},
];
pub fn execute(cmd: &str, app: &mut App) -> CommandResult {
let parts: Vec<&str> = cmd.trim().splitn(2, ' ').collect();
let command = parts[0].to_lowercase();
let command = command.strip_prefix('/').unwrap_or(&command);
let arg = parts.get(1).map(|s| s.trim());
if let Some(result) = user_commands::try_dispatch_user_command(app, cmd.trim()) {
return result;
}
match command {
"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" | "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" | "zhuye" | "shouye" => core::home_dashboard(app),
"workspace" | "cwd" => core::workspace_switch(app, arg),
"note" => note::note(app, arg),
"memory" => memory::memory(app, arg),
"attach" | "image" | "media" | "fujian" => attachment::attach(app, arg),
"task" | "tasks" => task::task(app, arg),
"jobs" | "job" | "zuoye" => jobs::jobs(app, arg),
"mcp" => mcp::mcp(app, arg),
"network" => network::network(app, arg),
"rename" | "gaiming" | "chongmingming" => rename::rename(app, arg),
"save" => session::save(app, arg),
"sessions" | "resume" => session::sessions(app, arg),
"relay" | "batonpass" | "接力" => relay(app, arg),
"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" | "daochu" => session::export(app, arg),
"config" => config::config_command(app, arg),
"settings" => config::show_settings(app),
"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" | "xinren" => config::trust(app, arg),
"logout" => config::logout(app),
"translate" | "translation" | "transale" => core::translate(app),
"tokens" => debug::tokens(app),
"cost" => debug::cost(app),
"cache" => debug::cache(app, arg),
"change" => change::change(app, arg),
"system" | "xitong" => debug::system_prompt(app),
"context" | "ctx" => debug::context(app),
"edit" => debug::edit(app),
"diff" => debug::diff(app),
"undo" => {
let result = debug::patch_undo(app);
if result.message.as_deref().is_none_or(|m| {
m.starts_with("No snapshots found")
|| m.starts_with("No tool or pre-turn")
|| m.starts_with("Snapshot repo")
}) {
debug::undo_conversation(app)
} else {
result
}
}
"retry" | "chongshi" => debug::retry(app),
"init" => init::init(app),
"lsp" => config::lsp_command(app, arg),
"share" => share::share(app, arg),
"goal" | "mubiao" => goal::goal(app, arg),
"skills" | "jinengliebiao" => skills::list_skills(app, arg),
"skill" | "jineng" => skills::run_skill(app, arg),
"review" | "shencha" => review::review(app, arg),
"restore" => restore::restore(app, arg),
"profile" | "dangan" => core::profile_switch(app, arg),
"rlm" | "recursive" | "digui" => rlm(app, arg),
"set" => CommandResult::error(
"The /set command was retired. Use /config to edit settings and /settings to inspect current values.",
),
"deepseek" => CommandResult::error(
"The /deepseek command was renamed. Use /links (aliases: /dashboard, /api).",
),
_ => {
if skills::run_skill_by_name(app, command, arg).is_some() {
return skills::run_skill_by_name(app, command, arg).unwrap();
}
let suggestions = suggest_command_names(command, 3);
if suggestions.is_empty() {
CommandResult::error(format!(
"Unknown command: /{command}. Type /help for available commands."
))
} else {
let list = suggestions
.into_iter()
.map(|name| format!("/{name}"))
.collect::<Vec<_>>()
.join(", ");
CommandResult::error(format!(
"Unknown command: /{command}. Did you mean: {list}? Type /help for available commands."
))
}
}
}
}
pub fn set_config_value(app: &mut App, key: &str, value: &str, persist: bool) -> CommandResult {
config::set_config_value(app, key, value, persist)
}
pub fn persist_status_items(
items: &[crate::config::StatusItem],
) -> anyhow::Result<std::path::PathBuf> {
config::persist_status_items(items)
}
pub fn persist_root_string_key(key: &str, value: &str) -> anyhow::Result<std::path::PathBuf> {
config::persist_root_string_key(key, value)
}
pub fn switch_mode(app: &mut App, mode: crate::tui::app::AppMode) -> String {
config::switch_mode(app, mode)
}
pub fn auto_model_heuristic(input: &str, current_model: &str) -> String {
config::auto_model_heuristic(input, current_model)
}
pub use config::{
AutoRouteRecommendation, AutoRouteSelection, normalize_auto_route_effort,
parse_auto_route_recommendation, resolve_auto_route_with_flash,
};
pub fn rlm(app: &mut App, arg: Option<&str>) -> CommandResult {
let (max_depth, target) = match parse_depth_prefixed_arg(arg, 1) {
Ok(parsed) => parsed,
Err(message) => return CommandResult::error(message),
};
let target = match target {
Some(p) if !p.trim().is_empty() => p.trim().to_string(),
_ => {
return CommandResult::error(
"Usage: /rlm [N] <file_or_text>\n\n\
Opens a persistent RLM context with sub_rlm depth N (0-3, default 1)."
.to_string(),
);
}
};
let source_arg = if resolves_to_existing_file(app, &target) {
format!(r#"file_path: "{target}""#)
} else {
format!("content: {:?}", target)
};
let message = format!(
"Open and use a persistent RLM session for this request. Call `rlm_open` with name `slash_rlm` and {source_arg}. Then call `rlm_configure` with `sub_rlm_max_depth: {max_depth}`. Use `rlm_eval` to inspect the context through `peek`, `search`, and `chunk`, and call `finalize(...)` from the REPL when ready. If a `var_handle` is returned, use `handle_read` for bounded slices or projections before answering."
);
CommandResult::with_message_and_action(
format!("Opening persistent RLM context at depth {max_depth}..."),
AppAction::SendMessage(message),
)
}
pub fn agent(_app: &mut App, arg: Option<&str>) -> CommandResult {
let (max_depth, task) = match parse_depth_prefixed_arg(arg, 1) {
Ok(parsed) => parsed,
Err(message) => return CommandResult::error(message),
};
let task = match task {
Some(task) if !task.trim().is_empty() => task.trim().to_string(),
_ => {
return CommandResult::error(
"Usage: /agent [N] <task>\n\n\
Opens a persistent sub-agent session with recursive agent depth N (0-3, default 1).",
);
}
};
let message = format!(
"Open a persistent sub-agent session for this task. Call `agent_open` with name `slash_agent`, `prompt: {:?}`, and `max_depth: {max_depth}`. Use `agent_eval` to wait for the next terminal/current projection and `handle_read` on the returned transcript_handle if you need more detail. Verify any claimed side effects before reporting success.",
task
);
CommandResult::with_message_and_action(
format!("Opening persistent sub-agent at depth {max_depth}..."),
AppAction::SendMessage(message),
)
}
pub fn relay(app: &mut App, arg: Option<&str>) -> CommandResult {
let focus = arg.map(str::trim).filter(|value| !value.is_empty());
let message = build_relay_instruction(app, focus);
CommandResult::with_message_and_action(
"Preparing session relay at .deepseek/handoff.md...",
AppAction::SendMessage(message),
)
}
fn build_relay_instruction(app: &App, focus: Option<&str>) -> String {
let mut out = String::new();
let _ = writeln!(
out,
"Create a compact session relay (接力) for a future DeepSeek TUI thread."
);
let _ = writeln!(out);
let _ = writeln!(out, "Write or update `.deepseek/handoff.md`.");
let _ = writeln!(
out,
"Keep the existing file path for compatibility, but title the artifact `# Session relay`."
);
let _ = writeln!(out);
let _ = writeln!(out, "Current session snapshot:");
let _ = writeln!(out, "- Workspace: {}", app.workspace.display());
let _ = writeln!(out, "- Mode: {}", app.mode.label());
let _ = writeln!(out, "- Model: {}", app.model_display_label());
if let Some(focus) = focus {
let _ = writeln!(out, "- Requested relay focus: {focus}");
}
if let Some(goal) = app.goal.goal_objective.as_deref() {
let _ = writeln!(out, "- Goal: {goal}");
}
if let Some(budget) = app.goal.goal_token_budget {
let _ = writeln!(out, "- Goal token budget: {budget}");
}
if app.cycle_count > 0 {
let _ = writeln!(out, "- Cycle count: {}", app.cycle_count);
}
if let Ok(todos) = app.todos.try_lock() {
let snapshot = todos.snapshot();
if !snapshot.items.is_empty() {
let _ = writeln!(
out,
"\nWork checklist (primary progress surface, {}% complete):",
snapshot.completion_pct
);
for item in snapshot.items {
let _ = writeln!(
out,
"- #{} [{}] {}",
item.id,
item.status.as_str(),
item.content
);
}
}
} else {
let _ = writeln!(
out,
"\nWork checklist: unavailable because the checklist is busy."
);
}
if let Ok(plan) = app.plan_state.try_lock() {
let snapshot = plan.snapshot();
if snapshot.explanation.is_some() || !snapshot.items.is_empty() {
let _ = writeln!(out, "\nOptional strategy metadata from update_plan:");
if let Some(explanation) = snapshot.explanation.as_deref() {
let _ = writeln!(out, "- Explanation: {explanation}");
}
for item in snapshot.items {
let _ = writeln!(out, "- [{}] {}", plan_status_label(&item.status), item.step);
}
}
} else {
let _ = writeln!(
out,
"\nStrategy metadata: unavailable because plan state is busy."
);
}
let _ = writeln!(
out,
"\nBefore writing, inspect the current transcript context and any live tool evidence you need. Do not invent test results, file changes, blockers, or decisions."
);
let _ = writeln!(
out,
"\nUse this compact structure:\n\
# Session relay\n\
\n\
## Goal\n\
[the user's objective and any explicit constraints]\n\
\n\
## Current work\n\
[the active Work checklist item, progress, and what is mid-flight]\n\
\n\
## Files and state\n\
[changed files, important paths, sub-agents/RLM sessions, commands run]\n\
\n\
## Decisions\n\
[why key choices were made]\n\
\n\
## Verification\n\
[what passed, what failed, what was not run]\n\
\n\
## Next action\n\
[one concrete action for the next thread]"
);
let _ = writeln!(
out,
"\nKeep it under about 900 words unless the session genuinely needs more. After writing, report the path and the single next action."
);
out
}
fn plan_status_label(status: &crate::tools::plan::StepStatus) -> &'static str {
match status {
crate::tools::plan::StepStatus::Pending => "pending",
crate::tools::plan::StepStatus::InProgress => "in_progress",
crate::tools::plan::StepStatus::Completed => "completed",
}
}
fn parse_depth_prefixed_arg(
arg: Option<&str>,
default_depth: u32,
) -> Result<(u32, Option<&str>), String> {
let Some(raw) = arg.map(str::trim).filter(|raw| !raw.is_empty()) else {
return Ok((default_depth, None));
};
let mut parts = raw.splitn(2, char::is_whitespace);
let first = parts.next().unwrap_or_default();
if first.chars().all(|ch| ch.is_ascii_digit()) {
let depth: u32 = first
.parse()
.map_err(|_| "Depth must be an integer from 0 to 3".to_string())?;
if depth > 3 {
return Err("Depth must be between 0 and 3".to_string());
}
Ok((depth, parts.next().map(str::trim)))
} else {
Ok((default_depth, Some(raw)))
}
}
fn resolves_to_existing_file(app: &App, input: &str) -> bool {
let path = std::path::Path::new(input);
let candidate = if path.is_absolute() {
path.to_path_buf()
} else {
app.workspace.join(path)
};
candidate.is_file()
}
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))
}
pub fn all_command_names_matching(
prefix: &str,
workspace: Option<&std::path::Path>,
) -> Vec<String> {
let prefix = prefix.strip_prefix('/').unwrap_or(prefix).to_lowercase();
let mut result: Vec<String> = COMMANDS
.iter()
.filter(|cmd| {
cmd.name.starts_with(&prefix) || cmd.aliases.iter().any(|a| a.starts_with(&prefix))
})
.map(|cmd| format!("/{}", cmd.name))
.collect();
result.extend(user_commands::user_commands_matching(&prefix, workspace));
result.sort();
result.dedup();
result
}
#[allow(dead_code)]
pub fn commands_matching(prefix: &str) -> Vec<&'static CommandInfo> {
let prefix = prefix.strip_prefix('/').unwrap_or(prefix).to_lowercase();
COMMANDS
.iter()
.filter(|cmd| {
cmd.name.starts_with(&prefix) || cmd.aliases.iter().any(|a| a.starts_with(&prefix))
})
.collect()
}
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()]
}
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 candidate = candidate.to_ascii_lowercase();
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::*;
use crate::config::Config;
use crate::tools::plan::{PlanItemArg, StepStatus, UpdatePlanArgs};
use crate::tools::todo::TodoStatus;
use crate::tui::app::{App, AppAction, TuiOptions};
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::sync::MutexGuard;
use tempfile::tempdir;
fn create_test_app() -> App {
let options = TuiOptions {
model: "deepseek-v4-pro".to_string(),
workspace: PathBuf::from("."),
config_path: None,
config_profile: None,
allow_shell: false,
use_alt_screen: true,
use_mouse_capture: false,
use_bracketed_paste: true,
max_subagents: 1,
skills_dir: PathBuf::from("."),
memory_path: PathBuf::from("memory.md"),
notes_path: PathBuf::from("notes.txt"),
mcp_config_path: PathBuf::from("mcp.json"),
use_memory: false,
start_in_agent_mode: false,
skip_onboarding: true,
yolo: false,
resume_session_id: None,
initial_input: None,
};
App::new(options, &Config::default())
}
#[test]
fn command_registry_contains_config_and_links_but_not_set_or_deepseek() {
assert!(COMMANDS.iter().any(|cmd| cmd.name == "config"));
assert!(COMMANDS.iter().any(|cmd| cmd.name == "links"));
assert!(COMMANDS.iter().any(|cmd| cmd.name == "memory"));
assert!(!COMMANDS.iter().any(|cmd| cmd.name == "set"));
assert!(!COMMANDS.iter().any(|cmd| cmd.name == "deepseek"));
}
#[test]
fn links_command_has_dashboard_and_api_aliases() {
let links = COMMANDS
.iter()
.find(|cmd| cmd.name == "links")
.expect("links command should exist");
assert_eq!(links.aliases, &["dashboard", "api", "lianjie"]);
}
#[test]
fn rlm_slash_command_routes_to_persistent_tool_instruction() {
let mut app = create_test_app();
let result = execute("/rlm 2 inspect this long corpus", &mut app);
assert!(!result.is_error);
assert!(result.message.as_deref().unwrap_or("").contains("depth 2"));
let Some(AppAction::SendMessage(message)) = result.action else {
panic!("expected SendMessage action");
};
assert!(message.contains("rlm_open"));
assert!(message.contains("rlm_configure"));
assert!(message.contains("sub_rlm_max_depth: 2"));
}
#[test]
fn agent_slash_command_routes_to_persistent_tool_instruction() {
let mut app = create_test_app();
let result = execute("/agent 0 inspect the parser", &mut app);
assert!(!result.is_error);
let Some(AppAction::SendMessage(message)) = result.action else {
panic!("expected SendMessage action");
};
assert!(message.contains("agent_open"));
assert!(message.contains("max_depth: 0"));
}
#[test]
fn relay_slash_command_routes_to_session_relay_instruction() {
let mut app = create_test_app();
app.goal.goal_objective = Some("Unify the work surface".to_string());
app.goal.goal_token_budget = Some(12_000);
app.cycle_count = 2;
{
let mut todos = app.todos.try_lock().expect("todo lock");
todos.add("inspect workspace".to_string(), TodoStatus::Completed);
todos.add("patch relay command".to_string(), TodoStatus::InProgress);
}
{
let mut plan = app.plan_state.try_lock().expect("plan lock");
plan.update(UpdatePlanArgs {
explanation: Some("RLM-style strategy".to_string()),
plan: vec![PlanItemArg {
step: "keep checklist primary".to_string(),
status: StepStatus::InProgress,
}],
});
}
let result = execute("/relay verify install", &mut app);
assert!(!result.is_error);
assert!(
result
.message
.as_deref()
.unwrap_or_default()
.contains(".deepseek/handoff.md")
);
let Some(AppAction::SendMessage(message)) = result.action else {
panic!("expected SendMessage action");
};
assert!(message.contains("session relay"));
assert!(message.contains("接力"));
assert!(message.contains("Write or update `.deepseek/handoff.md`"));
assert!(message.contains("# Session relay"));
assert!(message.contains("Requested relay focus: verify install"));
assert!(message.contains("Goal: Unify the work surface"));
assert!(message.contains("Goal token budget: 12000"));
assert!(message.contains("Cycle count: 2"));
assert!(message.contains("Work checklist (primary progress surface, 50% complete)"));
assert!(message.contains("#1 [completed] inspect workspace"));
assert!(message.contains("#2 [in_progress] patch relay command"));
assert!(message.contains("Optional strategy metadata from update_plan"));
assert!(message.contains("Explanation: RLM-style strategy"));
assert!(message.contains("[in_progress] keep checklist primary"));
}
#[test]
fn relay_command_has_bilingual_aliases() {
let relay = COMMANDS
.iter()
.find(|cmd| cmd.name == "relay")
.expect("relay command should exist");
assert_eq!(relay.aliases, &["batonpass", "接力"]);
assert!(relay.description_for(Locale::ZhHans).contains("接力"));
assert!(relay.description_for(Locale::ZhHant).contains("接力"));
let mut app = create_test_app();
let result = execute("/接力 next hand", &mut app);
assert!(!result.is_error);
let Some(AppAction::SendMessage(message)) = result.action else {
panic!("expected SendMessage action");
};
assert!(message.contains("Requested relay focus: next hand"));
}
#[test]
fn command_registry_has_unique_names_and_aliases() {
let mut names = std::collections::BTreeSet::new();
for command in COMMANDS {
assert!(
names.insert(command.name),
"duplicate command name /{}",
command.name
);
}
let mut aliases = std::collections::BTreeSet::new();
for command in COMMANDS {
for alias in command.aliases {
assert!(
!names.contains(alias),
"alias /{} collides with a command name",
alias
);
assert!(aliases.insert(*alias), "duplicate command alias /{alias}");
}
}
}
#[test]
fn context_command_opens_inspector_and_keeps_ctx_alias() {
let context = COMMANDS
.iter()
.find(|cmd| cmd.name == "context")
.expect("context command should exist");
assert_eq!(context.aliases, &["ctx"]);
assert!(context.description_for(Locale::En).contains("inspector"));
let mut app = create_test_app();
let result = execute("/ctx", &mut app);
assert!(matches!(
result.action,
Some(AppAction::OpenContextInspector)
));
}
#[test]
fn cache_inspect_dispatches_through_cache_command() {
let mut app = create_test_app();
let result = execute("/cache inspect", &mut app);
let msg = result.message.expect("cache inspect should return text");
assert!(msg.contains("Cache Inspect"));
assert!(msg.contains("Base static prefix hash:"));
assert!(msg.contains("Full request prefix hash:"));
assert!(result.action.is_none());
}
#[test]
fn cache_warmup_dispatches_action() {
let mut app = create_test_app();
let result = execute("/cache warmup", &mut app);
assert!(result.message.is_none());
assert!(matches!(result.action, Some(AppAction::CacheWarmup)));
}
#[test]
fn execute_config_opens_config_view_action() {
let mut app = create_test_app();
let result = execute("/config", &mut app);
assert!(result.message.is_none());
assert!(matches!(result.action, Some(AppAction::OpenConfigView)));
}
#[test]
fn execute_verbose_toggles_live_transcript_detail() {
let mut app = create_test_app();
assert!(!app.verbose_transcript);
let result = execute("/verbose on", &mut app);
assert!(!result.is_error);
assert!(app.verbose_transcript);
assert!(result.message.unwrap().contains("on"));
let result = execute("/verbose off", &mut app);
assert!(!result.is_error);
assert!(!app.verbose_transcript);
assert!(result.message.unwrap().contains("off"));
}
#[test]
fn execute_links_and_aliases_return_links_message() {
let mut app = create_test_app();
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"));
assert!(result.action.is_none());
}
}
#[test]
fn execute_workspace_alias_switches_workspace() {
let dir = tempdir().expect("temp dir");
let mut app = create_test_app();
let result = execute(&format!("/cwd {}", dir.path().display()), &mut app);
assert!(matches!(
result.action,
Some(AppAction::SwitchWorkspace { workspace }) if workspace == dir.path().canonicalize().unwrap()
));
}
#[test]
fn removed_set_and_deepseek_commands_show_migration_hints() {
let mut app = create_test_app();
let set_result = execute("/set model deepseek-v4-pro", &mut app);
let set_msg = set_result
.message
.expect("legacy command should return an error message");
assert!(set_msg.contains("The /set command was retired"));
assert!(set_msg.contains("/config"));
assert!(set_msg.contains("/settings"));
assert!(set_result.action.is_none());
let deepseek_result = execute("/deepseek", &mut app);
let deepseek_msg = deepseek_result
.message
.expect("legacy command should return an error message");
assert!(deepseek_msg.contains("The /deepseek command was renamed"));
assert!(deepseek_msg.contains("/links"));
assert!(deepseek_msg.contains("/dashboard"));
assert!(deepseek_msg.contains("/api"));
assert!(deepseek_result.action.is_none());
}
struct ConfigPathGuard {
previous: Option<OsString>,
_lock: MutexGuard<'static, ()>,
}
impl ConfigPathGuard {
fn new(config_path: &Path) -> Self {
let lock = crate::test_support::lock_test_env();
let previous = std::env::var_os("DEEPSEEK_CONFIG_PATH");
unsafe {
std::env::set_var("DEEPSEEK_CONFIG_PATH", config_path);
}
Self {
previous,
_lock: lock,
}
}
}
impl Drop for ConfigPathGuard {
fn drop(&mut self) {
unsafe {
if let Some(previous) = self.previous.take() {
std::env::set_var("DEEPSEEK_CONFIG_PATH", previous);
} else {
std::env::remove_var("DEEPSEEK_CONFIG_PATH");
}
}
}
}
fn create_isolated_test_app() -> (App, tempfile::TempDir, ConfigPathGuard) {
let tmpdir = tempfile::TempDir::new().expect("tempdir for smoke test");
let workspace = tmpdir.path().to_path_buf();
let config_path = workspace.join(".deepseek").join("config.toml");
std::fs::create_dir_all(config_path.parent().expect("config parent")).expect("config dir");
let guard = ConfigPathGuard::new(&config_path);
let options = TuiOptions {
model: "deepseek-v4-pro".to_string(),
workspace: workspace.clone(),
config_path: Some(config_path),
config_profile: None,
allow_shell: false,
use_alt_screen: true,
use_mouse_capture: false,
use_bracketed_paste: true,
max_subagents: 1,
skills_dir: workspace.join("skills"),
memory_path: workspace.join("memory.md"),
notes_path: workspace.join("notes.txt"),
mcp_config_path: workspace.join("mcp.json"),
use_memory: false,
start_in_agent_mode: false,
skip_onboarding: true,
yolo: false,
resume_session_id: None,
initial_input: None,
};
let app = App::new(options, &Config::default());
(app, tmpdir, guard)
}
fn invocation_for(command_name: &str, alias_or_name: &str, tmpdir: &std::path::Path) -> String {
match command_name {
"save" => format!("/{alias_or_name} {}", tmpdir.join("session.json").display()),
"export" => format!("/{alias_or_name} {}", tmpdir.join("chat.md").display()),
_ => format!("/{alias_or_name}"),
}
}
fn skip_in_dispatch_smoke(name: &str) -> bool {
name == "restore"
}
#[test]
fn every_registered_command_dispatches_to_a_handler() {
for command in COMMANDS {
if skip_in_dispatch_smoke(command.name) {
continue;
}
let (mut app, tmpdir, _guard) = create_isolated_test_app();
let invocation = invocation_for(command.name, command.name, tmpdir.path());
let result = execute(&invocation, &mut app);
if let Some(msg) = &result.message {
assert!(
!msg.contains("Unknown command"),
"/{} fell through to the unknown-command branch: {msg}",
command.name,
);
}
}
}
#[test]
fn every_command_alias_dispatches_to_a_handler() {
for command in COMMANDS {
if skip_in_dispatch_smoke(command.name) {
continue;
}
for alias in command.aliases {
let (mut app, tmpdir, _guard) = create_isolated_test_app();
let invocation = invocation_for(command.name, alias, tmpdir.path());
let result = execute(&invocation, &mut app);
if let Some(msg) = &result.message {
assert!(
!msg.contains("Unknown command"),
"/{alias} (alias of /{}) fell through to unknown: {msg}",
command.name,
);
}
}
}
}
#[test]
fn unknown_command_suggests_nearest_match() {
let mut app = create_test_app();
let result = execute("/modle", &mut app);
let msg = result
.message
.expect("unknown command should return an error message");
assert!(msg.contains("Unknown command: /modle"));
assert!(msg.contains("Did you mean:"));
assert!(msg.contains("/model"));
}
#[test]
fn unknown_command_without_close_match_keeps_help_guidance() {
let mut app = create_test_app();
let result = execute("/zzzzzz", &mut app);
let msg = result
.message
.expect("unknown command should return an error message");
assert!(msg.contains("Unknown command: /zzzzzz"));
assert!(msg.contains("Type /help for available commands."));
}
}