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
This commit is contained in:
Yeachan-Heo
2026-03-31 20:01:48 +00:00
parent c996eb7b1b
commit a8f5da6427
2 changed files with 72 additions and 10 deletions

View File

@@ -35,6 +35,7 @@ pub struct SlashCommandSpec {
pub name: &'static str, pub name: &'static str,
pub summary: &'static str, pub summary: &'static str,
pub argument_hint: Option<&'static str>, pub argument_hint: Option<&'static str>,
pub resume_supported: bool,
} }
const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
@@ -42,56 +43,67 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
name: "help", name: "help",
summary: "Show available slash commands", summary: "Show available slash commands",
argument_hint: None, argument_hint: None,
resume_supported: true,
}, },
SlashCommandSpec { SlashCommandSpec {
name: "status", name: "status",
summary: "Show current session status", summary: "Show current session status",
argument_hint: None, argument_hint: None,
resume_supported: true,
}, },
SlashCommandSpec { SlashCommandSpec {
name: "compact", name: "compact",
summary: "Compact local session history", summary: "Compact local session history",
argument_hint: None, argument_hint: None,
resume_supported: true,
}, },
SlashCommandSpec { SlashCommandSpec {
name: "model", name: "model",
summary: "Show or switch the active model", summary: "Show or switch the active model",
argument_hint: Some("[model]"), argument_hint: Some("[model]"),
resume_supported: false,
}, },
SlashCommandSpec { SlashCommandSpec {
name: "permissions", name: "permissions",
summary: "Show or switch the active permission mode", summary: "Show or switch the active permission mode",
argument_hint: Some("[read-only|workspace-write|danger-full-access]"), argument_hint: Some("[read-only|workspace-write|danger-full-access]"),
resume_supported: false,
}, },
SlashCommandSpec { SlashCommandSpec {
name: "clear", name: "clear",
summary: "Start a fresh local session", summary: "Start a fresh local session",
argument_hint: None, argument_hint: None,
resume_supported: true,
}, },
SlashCommandSpec { SlashCommandSpec {
name: "cost", name: "cost",
summary: "Show cumulative token usage for this session", summary: "Show cumulative token usage for this session",
argument_hint: None, argument_hint: None,
resume_supported: true,
}, },
SlashCommandSpec { SlashCommandSpec {
name: "resume", name: "resume",
summary: "Load a saved session into the REPL", summary: "Load a saved session into the REPL",
argument_hint: Some("<session-path>"), argument_hint: Some("<session-path>"),
resume_supported: false,
}, },
SlashCommandSpec { SlashCommandSpec {
name: "config", name: "config",
summary: "Inspect discovered Claude config files", summary: "Inspect discovered Claude config files",
argument_hint: None, argument_hint: None,
resume_supported: true,
}, },
SlashCommandSpec { SlashCommandSpec {
name: "memory", name: "memory",
summary: "Inspect loaded Claude instruction memory files", summary: "Inspect loaded Claude instruction memory files",
argument_hint: None, argument_hint: None,
resume_supported: true,
}, },
SlashCommandSpec { SlashCommandSpec {
name: "init", name: "init",
summary: "Create a starter CLAUDE.md for this repo", summary: "Create a starter CLAUDE.md for this repo",
argument_hint: None, argument_hint: None,
resume_supported: true,
}, },
]; ];
@@ -149,15 +161,31 @@ pub fn slash_command_specs() -> &'static [SlashCommandSpec] {
SLASH_COMMAND_SPECS 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] #[must_use]
pub fn render_slash_command_help() -> String { 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() { for spec in slash_command_specs() {
let name = match spec.argument_hint { let name = match spec.argument_hint {
Some(argument_hint) => format!("/{} {}", spec.name, argument_hint), Some(argument_hint) => format!("/{} {}", spec.name, argument_hint),
None => format!("/{}", spec.name), 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") lines.join("\n")
} }
@@ -210,7 +238,8 @@ pub fn handle_slash_command(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{ 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}; use runtime::{CompactionConfig, ContentBlock, ConversationMessage, MessageRole, Session};
@@ -250,6 +279,7 @@ mod tests {
#[test] #[test]
fn renders_help_from_shared_specs() { fn renders_help_from_shared_specs() {
let help = render_slash_command_help(); let help = render_slash_command_help();
assert!(help.contains("resume-safe commands"));
assert!(help.contains("/help")); assert!(help.contains("/help"));
assert!(help.contains("/status")); assert!(help.contains("/status"));
assert!(help.contains("/compact")); assert!(help.contains("/compact"));
@@ -262,6 +292,7 @@ mod tests {
assert!(help.contains("/memory")); assert!(help.contains("/memory"));
assert!(help.contains("/init")); assert!(help.contains("/init"));
assert_eq!(slash_command_specs().len(), 11); assert_eq!(slash_command_specs().len(), 11);
assert_eq!(resume_supported_slash_commands().len(), 8);
} }
#[test] #[test]

View File

@@ -12,7 +12,9 @@ use api::{
ToolResultContentBlock, 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 compat_harness::{extract_manifest, UpstreamPaths};
use render::{Spinner, TerminalRenderer}; use render::{Spinner, TerminalRenderer};
use runtime::{ use runtime::{
@@ -1065,21 +1067,38 @@ fn print_help() {
println!("rusty-claude-cli"); println!("rusty-claude-cli");
println!(); println!();
println!("Usage:"); println!("Usage:");
println!(" rusty-claude-cli [--model MODEL] Start interactive REPL"); println!(" rusty-claude-cli [--model MODEL]");
println!( println!(" Start interactive REPL");
" rusty-claude-cli [--model MODEL] prompt TEXT Send one prompt and stream the response" 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 dump-manifests");
println!(" rusty-claude-cli bootstrap-plan"); println!(" rusty-claude-cli bootstrap-plan");
println!(" rusty-claude-cli system-prompt [--cwd PATH] [--date YYYY-MM-DD]"); 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::<Vec<_>>()
.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)] #[cfg(test)]
mod tests { mod tests {
use super::{ use super::{
format_status_line, normalize_permission_mode, parse_args, render_init_claude_md, 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 runtime::{ContentBlock, ConversationMessage, MessageRole};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@@ -1182,6 +1201,18 @@ mod tests {
assert!(help.contains("/exit")); 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::<Vec<_>>();
assert_eq!(
names,
vec!["help", "status", "compact", "clear", "cost", "config", "memory", "init",]
);
}
#[test] #[test]
fn status_line_reports_model_and_token_totals() { fn status_line_reports_model_and_token_totals() {
let status = format_status_line( let status = format_status_line(