From a8f5da642764f0b55f02dfd44b7fd5796d3a8bc9 Mon Sep 17 00:00:00 2001 From: Yeachan-Heo Date: Tue, 31 Mar 2026 20:01:48 +0000 Subject: [PATCH] Make CLI command discovery closer to Claude Code Improve top-level help and shared slash-command help so the implemented surface is easier to discover, with explicit resume-safe markings and concrete examples for saved-session workflows. This keeps the command registry authoritative while making the CLI feel less skeletal and more like a real operator-facing tool. Constraint: Help text must reflect the actual implemented surface without advertising unsupported offline/runtime behavior Rejected: Separate bespoke help tables for REPL and --resume | would drift from the shared command registry Confidence: high Scope-risk: narrow Reversibility: clean Directive: Add new slash commands to the shared registry first so help and resume capability stay synchronized Tested: cargo fmt --manifest-path ./rust/Cargo.toml --all; cargo clippy --manifest-path ./rust/Cargo.toml --workspace --all-targets -- -D warnings; cargo test --manifest-path ./rust/Cargo.toml --workspace Not-tested: Manual UX comparison against upstream Claude Code help output --- rust/crates/commands/src/lib.rs | 37 +++++++++++++++++-- rust/crates/rusty-claude-cli/src/main.rs | 45 ++++++++++++++++++++---- 2 files changed, 72 insertions(+), 10 deletions(-) diff --git a/rust/crates/commands/src/lib.rs b/rust/crates/commands/src/lib.rs index 8a2e2eb..e090491 100644 --- a/rust/crates/commands/src/lib.rs +++ b/rust/crates/commands/src/lib.rs @@ -35,6 +35,7 @@ pub struct SlashCommandSpec { pub name: &'static str, pub summary: &'static str, pub argument_hint: Option<&'static str>, + pub resume_supported: bool, } const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[ @@ -42,56 +43,67 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[ name: "help", summary: "Show available slash commands", argument_hint: None, + resume_supported: true, }, SlashCommandSpec { name: "status", summary: "Show current session status", argument_hint: None, + resume_supported: true, }, SlashCommandSpec { name: "compact", summary: "Compact local session history", argument_hint: None, + resume_supported: true, }, SlashCommandSpec { name: "model", summary: "Show or switch the active model", argument_hint: Some("[model]"), + resume_supported: false, }, SlashCommandSpec { name: "permissions", summary: "Show or switch the active permission mode", argument_hint: Some("[read-only|workspace-write|danger-full-access]"), + resume_supported: false, }, SlashCommandSpec { name: "clear", summary: "Start a fresh local session", argument_hint: None, + resume_supported: true, }, SlashCommandSpec { name: "cost", summary: "Show cumulative token usage for this session", argument_hint: None, + resume_supported: true, }, SlashCommandSpec { name: "resume", summary: "Load a saved session into the REPL", argument_hint: Some(""), + resume_supported: false, }, SlashCommandSpec { name: "config", summary: "Inspect discovered Claude config files", argument_hint: None, + resume_supported: true, }, SlashCommandSpec { name: "memory", summary: "Inspect loaded Claude instruction memory files", argument_hint: None, + resume_supported: true, }, SlashCommandSpec { name: "init", summary: "Create a starter CLAUDE.md for this repo", argument_hint: None, + resume_supported: true, }, ]; @@ -149,15 +161,31 @@ pub fn slash_command_specs() -> &'static [SlashCommandSpec] { SLASH_COMMAND_SPECS } +#[must_use] +pub fn resume_supported_slash_commands() -> Vec<&'static SlashCommandSpec> { + slash_command_specs() + .iter() + .filter(|spec| spec.resume_supported) + .collect() +} + #[must_use] pub fn render_slash_command_help() -> String { - let mut lines = vec!["Available commands:".to_string()]; + let mut lines = vec![ + "Available commands:".to_string(), + " (resume-safe commands are marked with [resume])".to_string(), + ]; for spec in slash_command_specs() { let name = match spec.argument_hint { Some(argument_hint) => format!("/{} {}", spec.name, argument_hint), None => format!("/{}", spec.name), }; - lines.push(format!(" {name:<20} {}", spec.summary)); + let resume = if spec.resume_supported { + " [resume]" + } else { + "" + }; + lines.push(format!(" {name:<20} {}{}", spec.summary, resume)); } lines.join("\n") } @@ -210,7 +238,8 @@ pub fn handle_slash_command( #[cfg(test)] mod tests { use super::{ - handle_slash_command, render_slash_command_help, slash_command_specs, SlashCommand, + handle_slash_command, render_slash_command_help, resume_supported_slash_commands, + slash_command_specs, SlashCommand, }; use runtime::{CompactionConfig, ContentBlock, ConversationMessage, MessageRole, Session}; @@ -250,6 +279,7 @@ mod tests { #[test] fn renders_help_from_shared_specs() { let help = render_slash_command_help(); + assert!(help.contains("resume-safe commands")); assert!(help.contains("/help")); assert!(help.contains("/status")); assert!(help.contains("/compact")); @@ -262,6 +292,7 @@ mod tests { assert!(help.contains("/memory")); assert!(help.contains("/init")); assert_eq!(slash_command_specs().len(), 11); + assert_eq!(resume_supported_slash_commands().len(), 8); } #[test] diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index 7a3c0e3..95e1b14 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -12,7 +12,9 @@ use api::{ ToolResultContentBlock, }; -use commands::{handle_slash_command, render_slash_command_help, SlashCommand}; +use commands::{ + handle_slash_command, render_slash_command_help, resume_supported_slash_commands, SlashCommand, +}; use compat_harness::{extract_manifest, UpstreamPaths}; use render::{Spinner, TerminalRenderer}; use runtime::{ @@ -1065,21 +1067,38 @@ fn print_help() { println!("rusty-claude-cli"); println!(); println!("Usage:"); - println!(" rusty-claude-cli [--model MODEL] Start interactive REPL"); - println!( - " rusty-claude-cli [--model MODEL] prompt TEXT Send one prompt and stream the response" - ); + println!(" rusty-claude-cli [--model MODEL]"); + println!(" Start interactive REPL"); + println!(" rusty-claude-cli [--model MODEL] prompt TEXT"); + println!(" Send one prompt and stream the response"); + println!(" rusty-claude-cli --resume SESSION.json [/status] [/compact] [...]"); + println!(" Inspect or maintain a saved session without entering the REPL"); println!(" rusty-claude-cli dump-manifests"); println!(" rusty-claude-cli bootstrap-plan"); println!(" rusty-claude-cli system-prompt [--cwd PATH] [--date YYYY-MM-DD]"); - println!(" rusty-claude-cli --resume SESSION.json [/status] [/compact] [...]"); + println!(); + println!("Interactive slash commands:"); + println!("{}", render_slash_command_help()); + println!(); + let resume_commands = resume_supported_slash_commands() + .into_iter() + .map(|spec| match spec.argument_hint { + Some(argument_hint) => format!("/{} {}", spec.name, argument_hint), + None => format!("/{}", spec.name), + }) + .collect::>() + .join(", "); + println!("Resume-safe commands: {resume_commands}"); + println!("Examples:"); + println!(" rusty-claude-cli --resume session.json /status /compact /cost"); + println!(" rusty-claude-cli --resume session.json /memory /config"); } #[cfg(test)] mod tests { use super::{ format_status_line, normalize_permission_mode, parse_args, render_init_claude_md, - render_repl_help, CliAction, SlashCommand, DEFAULT_MODEL, + render_repl_help, resume_supported_slash_commands, CliAction, SlashCommand, DEFAULT_MODEL, }; use runtime::{ContentBlock, ConversationMessage, MessageRole}; use std::path::{Path, PathBuf}; @@ -1182,6 +1201,18 @@ mod tests { assert!(help.contains("/exit")); } + #[test] + fn resume_supported_command_list_matches_expected_surface() { + let names = resume_supported_slash_commands() + .into_iter() + .map(|spec| spec.name) + .collect::>(); + assert_eq!( + names, + vec!["help", "status", "compact", "clear", "cost", "config", "memory", "init",] + ); + } + #[test] fn status_line_reports_model_and_token_totals() { let status = format_status_line(