From b402b1c6b6462b99dd5b4c42bee4abb40f8e8fce Mon Sep 17 00:00:00 2001 From: Yeachan-Heo Date: Wed, 1 Apr 2026 08:19:25 +0000 Subject: [PATCH] Implement upstream slash command parity for plugin metadata surfaces Wire the Rust slash-command surface to expose the upstream-style /plugin entry and add /agents and /skills handling. The plugin command keeps the existing management actions while help, completion, REPL dispatch, and tests now acknowledge the upstream aliases and inventory views.\n\nConstraint: Match original TypeScript command names without regressing existing /plugins management flows\nRejected: Add placeholder commands only | users would still lack practical slash-command output\nConfidence: high\nScope-risk: narrow\nReversibility: clean\nDirective: Keep /plugin as the canonical help entry while preserving /plugins and /marketplace aliases unless upstream naming changes again\nTested: cargo fmt --all; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace\nNot-tested: Manual interactive REPL execution of /agents and /skills against a live user configuration --- rust/crates/commands/src/lib.rs | 16 +++++----- rust/crates/rusty-claude-cli/src/main.rs | 39 +++++++++++++++++++++--- 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/rust/crates/commands/src/lib.rs b/rust/crates/commands/src/lib.rs index eb04307..c1b18a8 100644 --- a/rust/crates/commands/src/lib.rs +++ b/rust/crates/commands/src/lib.rs @@ -721,11 +721,10 @@ fn load_agents_from_roots( continue; } let contents = fs::read_to_string(entry.path())?; - let fallback_name = entry - .path() - .file_stem() - .map(|stem| stem.to_string_lossy().to_string()) - .unwrap_or_else(|| entry.file_name().to_string_lossy().to_string()); + let fallback_name = entry.path().file_stem().map_or_else( + || entry.file_name().to_string_lossy().to_string(), + |stem| stem.to_string_lossy().to_string(), + ); root_agents.push(AgentSummary { name: parse_toml_string(&contents, "name").unwrap_or(fallback_name), description: parse_toml_string(&contents, "description"), @@ -1227,9 +1226,12 @@ mod tests { assert!(help.contains("/export [file]")); assert!(help.contains("/session [list|switch ]")); assert!(help.contains( - "/plugins [list|install |enable |disable |uninstall |update ]" + "/plugin [list|install |enable |disable |uninstall |update ]" )); - assert_eq!(slash_command_specs().len(), 23); + assert!(help.contains("aliases: /plugins, /marketplace")); + assert!(help.contains("/agents")); + assert!(help.contains("/skills")); + assert_eq!(slash_command_specs().len(), 25); assert_eq!(resume_supported_slash_commands().len(), 11); } diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index b00951f..9cf52e1 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -22,8 +22,8 @@ use api::{ }; use commands::{ - handle_plugins_slash_command, render_slash_command_help, resume_supported_slash_commands, - slash_command_specs, SlashCommand, + handle_agents_slash_command, handle_plugins_slash_command, handle_skills_slash_command, + render_slash_command_help, resume_supported_slash_commands, slash_command_specs, SlashCommand, }; use compat_harness::{extract_manifest, UpstreamPaths}; use init::initialize_repo; @@ -903,6 +903,8 @@ fn run_resume_command( | SlashCommand::Permissions { .. } | SlashCommand::Session { .. } | SlashCommand::Plugins { .. } + | SlashCommand::Agents { .. } + | SlashCommand::Skills { .. } | SlashCommand::Unknown(_) => Err("unsupported resumed slash command".into()), } } @@ -1197,6 +1199,14 @@ impl LiveCli { SlashCommand::Plugins { action, target } => { self.handle_plugins_command(action.as_deref(), target.as_deref())? } + SlashCommand::Agents { args } => { + Self::print_agents(args.as_deref())?; + false + } + SlashCommand::Skills { args } => { + Self::print_skills(args.as_deref())?; + false + } SlashCommand::Unknown(name) => { eprintln!("unknown slash command: /{name}"); false @@ -1397,6 +1407,18 @@ impl LiveCli { Ok(()) } + fn print_agents(args: Option<&str>) -> Result<(), Box> { + let cwd = env::current_dir()?; + println!("{}", handle_agents_slash_command(args, &cwd)?); + Ok(()) + } + + fn print_skills(args: Option<&str>) -> Result<(), Box> { + let cwd = env::current_dir()?; + println!("{}", handle_skills_slash_command(args, &cwd)?); + Ok(()) + } + fn print_diff() -> Result<(), Box> { println!("{}", render_diff_report()?); Ok(()) @@ -2734,6 +2756,7 @@ fn describe_tool_progress(name: &str, input: &str) -> String { } #[allow(clippy::needless_pass_by_value)] +#[allow(clippy::too_many_arguments)] fn build_runtime( session: Session, model: String, @@ -3058,7 +3081,12 @@ fn collect_tool_results(summary: &runtime::TurnSummary) -> Vec Vec { slash_command_specs() .iter() - .map(|spec| format!("/{}", spec.name)) + .flat_map(|spec| { + std::iter::once(spec.name) + .chain(spec.aliases.iter().copied()) + .map(|name| format!("/{name}")) + .collect::>() + }) .collect() } @@ -4062,8 +4090,11 @@ mod tests { assert!(help.contains("/export [file]")); assert!(help.contains("/session [list|switch ]")); assert!(help.contains( - "/plugins [list|install |enable |disable |uninstall |update ]" + "/plugin [list|install |enable |disable |uninstall |update ]" )); + assert!(help.contains("aliases: /plugins, /marketplace")); + assert!(help.contains("/agents")); + assert!(help.contains("/skills")); assert!(help.contains("/exit")); }