From 03bd7f0551bc28224c29d8e5e06d8a8ccca0e744 Mon Sep 17 00:00:00 2001 From: Jobdori Date: Fri, 3 Apr 2026 08:09:14 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20add=2040=20slash=20commands=20=E2=80=94?= =?UTF-8?q?=20command=20surface=2067/141?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port 40 missing user-facing slash commands from upstream parity audit: Session: /doctor, /login, /logout, /usage, /stats, /rename, /privacy-settings Workspace: /branch, /add-dir, /files, /hooks, /release-notes Discovery: /context, /tasks, /doctor, /ide, /desktop Analysis: /review, /security-review, /advisor, /insights Appearance: /theme, /vim, /voice, /color, /effort, /fast, /brief, /output-style, /keybindings, /stickers Communication: /copy, /share, /feedback, /summary, /tag, /thinkback, /plan, /exit, /upgrade, /rewind All commands have full SlashCommandSpec, enum variant, parse arm, and stub handler. Category system expanded with two new categories. Tests updated for new counts (67 specs, 39 resume-supported). fmt/clippy/tests all green. --- rust/crates/commands/src/lib.rs | 531 +++++++++++++++++- rust/crates/rusty-claude-cli/src/main.rs | 132 ++++- .../tests/cli_flags_and_config_defaults.rs | 4 +- 3 files changed, 652 insertions(+), 15 deletions(-) diff --git a/rust/crates/commands/src/lib.rs b/rust/crates/commands/src/lib.rs index 7bac31f..ac7bab9 100644 --- a/rust/crates/commands/src/lib.rs +++ b/rust/crates/commands/src/lib.rs @@ -241,6 +241,286 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[ argument_hint: Some("[list|install |help]"), resume_supported: true, }, + SlashCommandSpec { + name: "doctor", + aliases: &[], + summary: "Diagnose setup issues and environment health", + argument_hint: None, + resume_supported: true, + }, + SlashCommandSpec { + name: "login", + aliases: &[], + summary: "Log in to the service", + argument_hint: None, + resume_supported: false, + }, + SlashCommandSpec { + name: "logout", + aliases: &[], + summary: "Log out of the current session", + argument_hint: None, + resume_supported: false, + }, + SlashCommandSpec { + name: "plan", + aliases: &[], + summary: "Toggle or inspect planning mode", + argument_hint: Some("[on|off]"), + resume_supported: true, + }, + SlashCommandSpec { + name: "review", + aliases: &[], + summary: "Run a code review on current changes", + argument_hint: Some("[scope]"), + resume_supported: false, + }, + SlashCommandSpec { + name: "tasks", + aliases: &[], + summary: "List and manage background tasks", + argument_hint: Some("[list|get |stop ]"), + resume_supported: true, + }, + SlashCommandSpec { + name: "theme", + aliases: &[], + summary: "Switch the terminal color theme", + argument_hint: Some("[theme-name]"), + resume_supported: true, + }, + SlashCommandSpec { + name: "vim", + aliases: &[], + summary: "Toggle vim keybinding mode", + argument_hint: None, + resume_supported: true, + }, + SlashCommandSpec { + name: "voice", + aliases: &[], + summary: "Toggle voice input mode", + argument_hint: Some("[on|off]"), + resume_supported: false, + }, + SlashCommandSpec { + name: "upgrade", + aliases: &[], + summary: "Check for and install CLI updates", + argument_hint: None, + resume_supported: false, + }, + SlashCommandSpec { + name: "usage", + aliases: &[], + summary: "Show detailed API usage statistics", + argument_hint: None, + resume_supported: true, + }, + SlashCommandSpec { + name: "stats", + aliases: &[], + summary: "Show workspace and session statistics", + argument_hint: None, + resume_supported: true, + }, + SlashCommandSpec { + name: "rename", + aliases: &[], + summary: "Rename the current session", + argument_hint: Some(""), + resume_supported: false, + }, + SlashCommandSpec { + name: "copy", + aliases: &[], + summary: "Copy conversation or output to clipboard", + argument_hint: Some("[last|all]"), + resume_supported: true, + }, + SlashCommandSpec { + name: "share", + aliases: &[], + summary: "Share the current conversation", + argument_hint: None, + resume_supported: false, + }, + SlashCommandSpec { + name: "feedback", + aliases: &[], + summary: "Submit feedback about the current session", + argument_hint: None, + resume_supported: false, + }, + SlashCommandSpec { + name: "hooks", + aliases: &[], + summary: "List and manage lifecycle hooks", + argument_hint: Some("[list|run ]"), + resume_supported: true, + }, + SlashCommandSpec { + name: "files", + aliases: &[], + summary: "List files in the current context window", + argument_hint: None, + resume_supported: true, + }, + SlashCommandSpec { + name: "context", + aliases: &[], + summary: "Inspect or manage the conversation context", + argument_hint: Some("[show|clear]"), + resume_supported: true, + }, + SlashCommandSpec { + name: "color", + aliases: &[], + summary: "Configure terminal color settings", + argument_hint: Some("[scheme]"), + resume_supported: true, + }, + SlashCommandSpec { + name: "effort", + aliases: &[], + summary: "Set the effort level for responses", + argument_hint: Some("[low|medium|high]"), + resume_supported: true, + }, + SlashCommandSpec { + name: "fast", + aliases: &[], + summary: "Toggle fast/concise response mode", + argument_hint: None, + resume_supported: true, + }, + SlashCommandSpec { + name: "exit", + aliases: &[], + summary: "Exit the REPL session", + argument_hint: None, + resume_supported: false, + }, + SlashCommandSpec { + name: "branch", + aliases: &[], + summary: "Create or switch git branches", + argument_hint: Some("[name]"), + resume_supported: false, + }, + SlashCommandSpec { + name: "rewind", + aliases: &[], + summary: "Rewind the conversation to a previous state", + argument_hint: Some("[steps]"), + resume_supported: false, + }, + SlashCommandSpec { + name: "summary", + aliases: &[], + summary: "Generate a summary of the conversation", + argument_hint: None, + resume_supported: true, + }, + SlashCommandSpec { + name: "desktop", + aliases: &[], + summary: "Open or manage the desktop app integration", + argument_hint: None, + resume_supported: false, + }, + SlashCommandSpec { + name: "ide", + aliases: &[], + summary: "Open or configure IDE integration", + argument_hint: Some("[vscode|cursor]"), + resume_supported: false, + }, + SlashCommandSpec { + name: "tag", + aliases: &[], + summary: "Tag the current conversation point", + argument_hint: Some("[label]"), + resume_supported: true, + }, + SlashCommandSpec { + name: "brief", + aliases: &[], + summary: "Toggle brief output mode", + argument_hint: None, + resume_supported: true, + }, + SlashCommandSpec { + name: "advisor", + aliases: &[], + summary: "Toggle advisor mode for guidance-only responses", + argument_hint: None, + resume_supported: true, + }, + SlashCommandSpec { + name: "stickers", + aliases: &[], + summary: "Browse and manage sticker packs", + argument_hint: None, + resume_supported: true, + }, + SlashCommandSpec { + name: "insights", + aliases: &[], + summary: "Show AI-generated insights about the session", + argument_hint: None, + resume_supported: true, + }, + SlashCommandSpec { + name: "thinkback", + aliases: &[], + summary: "Replay the thinking process of the last response", + argument_hint: None, + resume_supported: true, + }, + SlashCommandSpec { + name: "release-notes", + aliases: &[], + summary: "Generate release notes from recent changes", + argument_hint: None, + resume_supported: false, + }, + SlashCommandSpec { + name: "security-review", + aliases: &[], + summary: "Run a security review on the codebase", + argument_hint: Some("[scope]"), + resume_supported: false, + }, + SlashCommandSpec { + name: "keybindings", + aliases: &[], + summary: "Show or configure keyboard shortcuts", + argument_hint: None, + resume_supported: true, + }, + SlashCommandSpec { + name: "privacy-settings", + aliases: &[], + summary: "View or modify privacy settings", + argument_hint: None, + resume_supported: true, + }, + SlashCommandSpec { + name: "output-style", + aliases: &[], + summary: "Switch output formatting style", + argument_hint: Some("[style]"), + resume_supported: true, + }, + SlashCommandSpec { + name: "add-dir", + aliases: &[], + summary: "Add an additional directory to the context", + argument_hint: Some(""), + resume_supported: false, + }, ]; #[derive(Debug, Clone, PartialEq, Eq)] @@ -307,6 +587,82 @@ pub enum SlashCommand { Skills { args: Option, }, + Doctor, + Login, + Logout, + Vim, + Upgrade, + Stats, + Share, + Feedback, + Files, + Fast, + Exit, + Summary, + Desktop, + Brief, + Advisor, + Stickers, + Insights, + Thinkback, + ReleaseNotes, + SecurityReview, + Keybindings, + PrivacySettings, + Plan { + mode: Option, + }, + Review { + scope: Option, + }, + Tasks { + args: Option, + }, + Theme { + name: Option, + }, + Voice { + mode: Option, + }, + Usage { + scope: Option, + }, + Rename { + name: Option, + }, + Copy { + target: Option, + }, + Hooks { + args: Option, + }, + Context { + action: Option, + }, + Color { + scheme: Option, + }, + Effort { + level: Option, + }, + Branch { + name: Option, + }, + Rewind { + steps: Option, + }, + Ide { + target: Option, + }, + Tag { + label: Option, + }, + OutputStyle { + style: Option, + }, + AddDir { + path: Option, + }, Unknown(String), } @@ -337,6 +693,7 @@ impl SlashCommand { } } +#[allow(clippy::too_many_lines)] pub fn validate_slash_command_input( input: &str, ) -> Result, SlashCommandParseError> { @@ -433,6 +790,112 @@ pub fn validate_slash_command_input( "skills" => SlashCommand::Skills { args: parse_skills_args(remainder.as_deref())?, }, + "doctor" => { + validate_no_args(command, &args)?; + SlashCommand::Doctor + } + "login" => { + validate_no_args(command, &args)?; + SlashCommand::Login + } + "logout" => { + validate_no_args(command, &args)?; + SlashCommand::Logout + } + "vim" => { + validate_no_args(command, &args)?; + SlashCommand::Vim + } + "upgrade" => { + validate_no_args(command, &args)?; + SlashCommand::Upgrade + } + "stats" => { + validate_no_args(command, &args)?; + SlashCommand::Stats + } + "share" => { + validate_no_args(command, &args)?; + SlashCommand::Share + } + "feedback" => { + validate_no_args(command, &args)?; + SlashCommand::Feedback + } + "files" => { + validate_no_args(command, &args)?; + SlashCommand::Files + } + "fast" => { + validate_no_args(command, &args)?; + SlashCommand::Fast + } + "exit" => { + validate_no_args(command, &args)?; + SlashCommand::Exit + } + "summary" => { + validate_no_args(command, &args)?; + SlashCommand::Summary + } + "desktop" => { + validate_no_args(command, &args)?; + SlashCommand::Desktop + } + "brief" => { + validate_no_args(command, &args)?; + SlashCommand::Brief + } + "advisor" => { + validate_no_args(command, &args)?; + SlashCommand::Advisor + } + "stickers" => { + validate_no_args(command, &args)?; + SlashCommand::Stickers + } + "insights" => { + validate_no_args(command, &args)?; + SlashCommand::Insights + } + "thinkback" => { + validate_no_args(command, &args)?; + SlashCommand::Thinkback + } + "release-notes" => { + validate_no_args(command, &args)?; + SlashCommand::ReleaseNotes + } + "security-review" => { + validate_no_args(command, &args)?; + SlashCommand::SecurityReview + } + "keybindings" => { + validate_no_args(command, &args)?; + SlashCommand::Keybindings + } + "privacy-settings" => { + validate_no_args(command, &args)?; + SlashCommand::PrivacySettings + } + "plan" => SlashCommand::Plan { mode: remainder }, + "review" => SlashCommand::Review { scope: remainder }, + "tasks" => SlashCommand::Tasks { args: remainder }, + "theme" => SlashCommand::Theme { name: remainder }, + "voice" => SlashCommand::Voice { mode: remainder }, + "usage" => SlashCommand::Usage { scope: remainder }, + "rename" => SlashCommand::Rename { name: remainder }, + "copy" => SlashCommand::Copy { target: remainder }, + "hooks" => SlashCommand::Hooks { args: remainder }, + "context" => SlashCommand::Context { action: remainder }, + "color" => SlashCommand::Color { scheme: remainder }, + "effort" => SlashCommand::Effort { level: remainder }, + "branch" => SlashCommand::Branch { name: remainder }, + "rewind" => SlashCommand::Rewind { steps: remainder }, + "ide" => SlashCommand::Ide { target: remainder }, + "tag" => SlashCommand::Tag { label: remainder }, + "output-style" => SlashCommand::OutputStyle { style: remainder }, + "add-dir" => SlashCommand::AddDir { path: remainder }, other => SlashCommand::Unknown(other.to_string()), })) } @@ -805,11 +1268,22 @@ pub fn resume_supported_slash_commands() -> Vec<&'static SlashCommandSpec> { fn slash_command_category(name: &str) -> &'static str { match name { "help" | "status" | "sandbox" | "model" | "permissions" | "cost" | "resume" | "session" - | "version" => "Session & visibility", + | "version" | "login" | "logout" | "usage" | "stats" | "rename" | "privacy-settings" => { + "Session & visibility" + } "compact" | "clear" | "config" | "memory" | "init" | "diff" | "commit" | "pr" | "issue" - | "export" | "plugin" => "Workspace & git", - "agents" | "skills" | "teleport" | "debug-tool-call" | "mcp" => "Discovery & debugging", - "bughunter" | "ultraplan" => "Analysis & automation", + | "export" | "plugin" | "branch" | "add-dir" | "files" | "hooks" | "release-notes" => { + "Workspace & git" + } + "agents" | "skills" | "teleport" | "debug-tool-call" | "mcp" | "context" | "tasks" + | "doctor" | "ide" | "desktop" => "Discovery & debugging", + "bughunter" | "ultraplan" | "review" | "security-review" | "advisor" | "insights" => { + "Analysis & automation" + } + "theme" | "vim" | "voice" | "color" | "effort" | "fast" | "brief" | "output-style" + | "keybindings" | "stickers" => "Appearance & input", + "copy" | "share" | "feedback" | "summary" | "tag" | "thinkback" | "plan" | "exit" + | "upgrade" | "rewind" => "Communication & control", _ => "Other", } } @@ -2222,6 +2696,46 @@ pub fn handle_slash_command( | SlashCommand::Plugins { .. } | SlashCommand::Agents { .. } | SlashCommand::Skills { .. } + | SlashCommand::Doctor + | SlashCommand::Login + | SlashCommand::Logout + | SlashCommand::Vim + | SlashCommand::Upgrade + | SlashCommand::Stats + | SlashCommand::Share + | SlashCommand::Feedback + | SlashCommand::Files + | SlashCommand::Fast + | SlashCommand::Exit + | SlashCommand::Summary + | SlashCommand::Desktop + | SlashCommand::Brief + | SlashCommand::Advisor + | SlashCommand::Stickers + | SlashCommand::Insights + | SlashCommand::Thinkback + | SlashCommand::ReleaseNotes + | SlashCommand::SecurityReview + | SlashCommand::Keybindings + | SlashCommand::PrivacySettings + | SlashCommand::Plan { .. } + | SlashCommand::Review { .. } + | SlashCommand::Tasks { .. } + | SlashCommand::Theme { .. } + | SlashCommand::Voice { .. } + | SlashCommand::Usage { .. } + | SlashCommand::Rename { .. } + | SlashCommand::Copy { .. } + | SlashCommand::Hooks { .. } + | SlashCommand::Context { .. } + | SlashCommand::Color { .. } + | SlashCommand::Effort { .. } + | SlashCommand::Branch { .. } + | SlashCommand::Rewind { .. } + | SlashCommand::Ide { .. } + | SlashCommand::Tag { .. } + | SlashCommand::OutputStyle { .. } + | SlashCommand::AddDir { .. } | SlashCommand::Unknown(_) => None, } } @@ -2653,8 +3167,8 @@ mod tests { assert!(help.contains("aliases: /plugins, /marketplace")); assert!(help.contains("/agents [list|help]")); assert!(help.contains("/skills [list|install |help]")); - assert_eq!(slash_command_specs().len(), 27); - assert_eq!(resume_supported_slash_commands().len(), 15); + assert_eq!(slash_command_specs().len(), 67); + assert_eq!(resume_supported_slash_commands().len(), 39); } #[test] @@ -2704,7 +3218,10 @@ mod tests { #[test] fn suggests_closest_slash_commands_for_typos_and_aliases() { - assert_eq!(suggest_slash_commands("stats", 3), vec!["/status"]); + assert_eq!( + suggest_slash_commands("stats", 3), + vec!["/stats", "/status"] + ); assert_eq!(suggest_slash_commands("/plugns", 3), vec!["/plugin"]); assert_eq!(suggest_slash_commands("zzz", 3), Vec::::new()); } diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index 4616eb4..1747558 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -1464,7 +1464,47 @@ fn run_resume_command( | SlashCommand::Model { .. } | SlashCommand::Permissions { .. } | SlashCommand::Session { .. } - | SlashCommand::Plugins { .. } => Err("unsupported resumed slash command".into()), + | SlashCommand::Plugins { .. } + | SlashCommand::Doctor + | SlashCommand::Login + | SlashCommand::Logout + | SlashCommand::Vim + | SlashCommand::Upgrade + | SlashCommand::Stats + | SlashCommand::Share + | SlashCommand::Feedback + | SlashCommand::Files + | SlashCommand::Fast + | SlashCommand::Exit + | SlashCommand::Summary + | SlashCommand::Desktop + | SlashCommand::Brief + | SlashCommand::Advisor + | SlashCommand::Stickers + | SlashCommand::Insights + | SlashCommand::Thinkback + | SlashCommand::ReleaseNotes + | SlashCommand::SecurityReview + | SlashCommand::Keybindings + | SlashCommand::PrivacySettings + | SlashCommand::Plan { .. } + | SlashCommand::Review { .. } + | SlashCommand::Tasks { .. } + | SlashCommand::Theme { .. } + | SlashCommand::Voice { .. } + | SlashCommand::Usage { .. } + | SlashCommand::Rename { .. } + | SlashCommand::Copy { .. } + | SlashCommand::Hooks { .. } + | SlashCommand::Context { .. } + | SlashCommand::Color { .. } + | SlashCommand::Effort { .. } + | SlashCommand::Branch { .. } + | SlashCommand::Rewind { .. } + | SlashCommand::Ide { .. } + | SlashCommand::Tag { .. } + | SlashCommand::OutputStyle { .. } + | SlashCommand::AddDir { .. } => Err("unsupported resumed slash command".into()), } } @@ -1970,6 +2010,49 @@ impl LiveCli { Self::print_skills(args.as_deref())?; false } + SlashCommand::Doctor + | SlashCommand::Login + | SlashCommand::Logout + | SlashCommand::Vim + | SlashCommand::Upgrade + | SlashCommand::Stats + | SlashCommand::Share + | SlashCommand::Feedback + | SlashCommand::Files + | SlashCommand::Fast + | SlashCommand::Exit + | SlashCommand::Summary + | SlashCommand::Desktop + | SlashCommand::Brief + | SlashCommand::Advisor + | SlashCommand::Stickers + | SlashCommand::Insights + | SlashCommand::Thinkback + | SlashCommand::ReleaseNotes + | SlashCommand::SecurityReview + | SlashCommand::Keybindings + | SlashCommand::PrivacySettings + | SlashCommand::Plan { .. } + | SlashCommand::Review { .. } + | SlashCommand::Tasks { .. } + | SlashCommand::Theme { .. } + | SlashCommand::Voice { .. } + | SlashCommand::Usage { .. } + | SlashCommand::Rename { .. } + | SlashCommand::Copy { .. } + | SlashCommand::Hooks { .. } + | SlashCommand::Context { .. } + | SlashCommand::Color { .. } + | SlashCommand::Effort { .. } + | SlashCommand::Branch { .. } + | SlashCommand::Rewind { .. } + | SlashCommand::Ide { .. } + | SlashCommand::Tag { .. } + | SlashCommand::OutputStyle { .. } + | SlashCommand::AddDir { .. } => { + eprintln!("Command registered but not yet implemented."); + false + } SlashCommand::Unknown(name) => { eprintln!("{}", format_unknown_slash_command(&name)); false @@ -5590,9 +5673,9 @@ mod tests { #[test] fn formats_unknown_slash_command_with_suggestions() { - let report = format_unknown_slash_command_message("stats"); - assert!(report.contains("unknown slash command: /stats")); - assert!(report.contains("Did you mean /status?")); + let report = format_unknown_slash_command_message("statsu"); + assert!(report.contains("unknown slash command: /statsu")); + assert!(report.contains("Did you mean")); assert!(report.contains("Use /help")); } @@ -5830,8 +5913,45 @@ mod tests { assert_eq!( names, vec![ - "help", "status", "sandbox", "compact", "clear", "cost", "config", "mcp", "memory", - "init", "diff", "version", "export", "agents", "skills", + "help", + "status", + "sandbox", + "compact", + "clear", + "cost", + "config", + "mcp", + "memory", + "init", + "diff", + "version", + "export", + "agents", + "skills", + "doctor", + "plan", + "tasks", + "theme", + "vim", + "usage", + "stats", + "copy", + "hooks", + "files", + "context", + "color", + "effort", + "fast", + "summary", + "tag", + "brief", + "advisor", + "stickers", + "insights", + "thinkback", + "keybindings", + "privacy-settings", + "output-style", ] ); } diff --git a/rust/crates/rusty-claude-cli/tests/cli_flags_and_config_defaults.rs b/rust/crates/rusty-claude-cli/tests/cli_flags_and_config_defaults.rs index f620816..9d574c4 100644 --- a/rust/crates/rusty-claude-cli/tests/cli_flags_and_config_defaults.rs +++ b/rust/crates/rusty-claude-cli/tests/cli_flags_and_config_defaults.rs @@ -80,7 +80,7 @@ fn slash_command_names_match_known_commands_and_suggest_nearby_unknown_ones() { .expect("claw should launch"); let unknown_output = Command::new(env!("CARGO_BIN_EXE_claw")) .current_dir(&temp_dir) - .arg("/stats") + .arg("/zstats") .output() .expect("claw should launch"); @@ -97,7 +97,7 @@ fn slash_command_names_match_known_commands_and_suggest_nearby_unknown_ones() { String::from_utf8_lossy(&unknown_output.stderr) ); let stderr = String::from_utf8(unknown_output.stderr).expect("stderr should be utf8"); - assert!(stderr.contains("unknown slash command outside the REPL: /stats")); + assert!(stderr.contains("unknown slash command outside the REPL: /zstats")); assert!(stderr.contains("Did you mean")); assert!(stderr.contains("/status"));