Переглянути джерело

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
Yeachan-Heo 2 місяців тому
батько
коміт
a8f5da6427

+ 34 - 3
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("<session-path>"),
+        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]

+ 38 - 7
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::<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)]
 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::<Vec<_>>();
+        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(