Explorar el Código

feat: command surface follow-up integration

Yeachan-Heo hace 2 meses
padre
commit
5b95e0cfe5
Se han modificado 2 ficheros con 154 adiciones y 35 borrados
  1. 64 8
      rust/crates/commands/src/lib.rs
  2. 90 27
      rust/crates/rusty-claude-cli/src/main.rs

+ 64 - 8
rust/crates/commands/src/lib.rs

@@ -48,144 +48,181 @@ pub struct SlashCommandSpec {
 const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
     SlashCommandSpec {
         name: "help",
+        aliases: &[],
         summary: "Show available slash commands",
         argument_hint: None,
         resume_supported: true,
     },
     SlashCommandSpec {
         name: "status",
+        aliases: &[],
         summary: "Show current session status",
         argument_hint: None,
         resume_supported: true,
     },
     SlashCommandSpec {
         name: "compact",
+        aliases: &[],
         summary: "Compact local session history",
         argument_hint: None,
         resume_supported: true,
     },
     SlashCommandSpec {
         name: "model",
+        aliases: &[],
         summary: "Show or switch the active model",
         argument_hint: Some("[model]"),
         resume_supported: false,
     },
     SlashCommandSpec {
         name: "permissions",
+        aliases: &[],
         summary: "Show or switch the active permission mode",
         argument_hint: Some("[read-only|workspace-write|danger-full-access]"),
         resume_supported: false,
     },
     SlashCommandSpec {
         name: "clear",
+        aliases: &[],
         summary: "Start a fresh local session",
         argument_hint: Some("[--confirm]"),
         resume_supported: true,
     },
     SlashCommandSpec {
         name: "cost",
+        aliases: &[],
         summary: "Show cumulative token usage for this session",
         argument_hint: None,
         resume_supported: true,
     },
     SlashCommandSpec {
         name: "resume",
+        aliases: &[],
         summary: "Load a saved session into the REPL",
         argument_hint: Some("<session-path>"),
         resume_supported: false,
     },
     SlashCommandSpec {
         name: "config",
+        aliases: &[],
         summary: "Inspect Claude config files or merged sections",
         argument_hint: Some("[env|hooks|model|plugins]"),
         resume_supported: true,
     },
     SlashCommandSpec {
         name: "memory",
+        aliases: &[],
         summary: "Inspect loaded Claude instruction memory files",
         argument_hint: None,
         resume_supported: true,
     },
     SlashCommandSpec {
         name: "init",
+        aliases: &[],
         summary: "Create a starter CLAUDE.md for this repo",
         argument_hint: None,
         resume_supported: true,
     },
     SlashCommandSpec {
         name: "diff",
+        aliases: &[],
         summary: "Show git diff for current workspace changes",
         argument_hint: None,
         resume_supported: true,
     },
     SlashCommandSpec {
         name: "version",
+        aliases: &[],
         summary: "Show CLI version and build information",
         argument_hint: None,
         resume_supported: true,
     },
     SlashCommandSpec {
         name: "bughunter",
+        aliases: &[],
         summary: "Inspect the codebase for likely bugs",
         argument_hint: Some("[scope]"),
         resume_supported: false,
     },
     SlashCommandSpec {
         name: "commit",
+        aliases: &[],
         summary: "Generate a commit message and create a git commit",
         argument_hint: None,
         resume_supported: false,
     },
     SlashCommandSpec {
         name: "pr",
+        aliases: &[],
         summary: "Draft or create a pull request from the conversation",
         argument_hint: Some("[context]"),
         resume_supported: false,
     },
     SlashCommandSpec {
         name: "issue",
+        aliases: &[],
         summary: "Draft or create a GitHub issue from the conversation",
         argument_hint: Some("[context]"),
         resume_supported: false,
     },
     SlashCommandSpec {
         name: "ultraplan",
+        aliases: &[],
         summary: "Run a deep planning prompt with multi-step reasoning",
         argument_hint: Some("[task]"),
         resume_supported: false,
     },
     SlashCommandSpec {
         name: "teleport",
+        aliases: &[],
         summary: "Jump to a file or symbol by searching the workspace",
         argument_hint: Some("<symbol-or-path>"),
         resume_supported: false,
     },
     SlashCommandSpec {
         name: "debug-tool-call",
+        aliases: &[],
         summary: "Replay the last tool call with debug details",
         argument_hint: None,
         resume_supported: false,
     },
     SlashCommandSpec {
         name: "export",
+        aliases: &[],
         summary: "Export the current conversation to a file",
         argument_hint: Some("[file]"),
         resume_supported: true,
     },
     SlashCommandSpec {
         name: "session",
+        aliases: &[],
         summary: "List or switch managed local sessions",
         argument_hint: Some("[list|switch <session-id>]"),
         resume_supported: false,
     },
     SlashCommandSpec {
-        name: "plugins",
-        summary: "List or manage plugins",
+        name: "plugin",
+        aliases: &["plugins", "marketplace"],
+        summary: "Manage Claude Code plugins",
         argument_hint: Some(
             "[list|install <path>|enable <name>|disable <name>|uninstall <id>|update <id>]",
         ),
         resume_supported: false,
     },
+    SlashCommandSpec {
+        name: "agents",
+        aliases: &[],
+        summary: "Manage agent configurations",
+        argument_hint: None,
+        resume_supported: false,
+    },
+    SlashCommandSpec {
+        name: "skills",
+        aliases: &[],
+        summary: "List available skills",
+        argument_hint: None,
+        resume_supported: false,
+    },
 ];
 
 #[derive(Debug, Clone, PartialEq, Eq)]
@@ -629,6 +666,27 @@ mod tests {
         .expect("write bundled manifest");
     }
 
+    fn write_agent(root: &Path, name: &str, description: &str, model: &str, reasoning: &str) {
+        fs::create_dir_all(root).expect("agent root");
+        fs::write(
+            root.join(format!("{name}.toml")),
+            format!(
+                "name = \"{name}\"\ndescription = \"{description}\"\nmodel = \"{model}\"\nmodel_reasoning_effort = \"{reasoning}\"\n"
+            ),
+        )
+        .expect("write agent");
+    }
+
+    fn write_skill(root: &Path, name: &str, description: &str) {
+        let skill_root = root.join(name);
+        fs::create_dir_all(&skill_root).expect("skill root");
+        fs::write(
+            skill_root.join("SKILL.md"),
+            format!("---\nname: {name}\ndescription: {description}\n---\n\n# {name}\n"),
+        )
+        .expect("write skill");
+    }
+
     #[allow(clippy::too_many_lines)]
     #[test]
     fn parses_supported_slash_commands() {
@@ -961,9 +1019,8 @@ mod tests {
             (DefinitionSource::ProjectCodex, project_agents),
             (DefinitionSource::UserCodex, user_agents),
         ];
-        let report = render_agents_report(
-            &load_agents_from_roots(&roots).expect("agent roots should load"),
-        );
+        let report =
+            render_agents_report(&load_agents_from_roots(&roots).expect("agent roots should load"));
 
         assert!(report.contains("Agents"));
         assert!(report.contains("2 active agents"));
@@ -992,9 +1049,8 @@ mod tests {
             (DefinitionSource::ProjectCodex, project_skills),
             (DefinitionSource::UserCodex, user_skills),
         ];
-        let report = render_skills_report(
-            &load_skills_from_roots(&roots).expect("skill roots should load"),
-        );
+        let report =
+            render_skills_report(&load_skills_from_roots(&roots).expect("skill roots should load"));
 
         assert!(report.contains("Skills"));
         assert!(report.contains("2 available skills"));

+ 90 - 27
rust/crates/rusty-claude-cli/src/main.rs

@@ -1560,11 +1560,8 @@ impl LiveCli {
             "You are /ultraplan. Produce a deep multi-step execution plan for {task}. Include goals, risks, implementation sequence, verification steps, and rollback considerations. Use tools if needed."
         );
         let mut progress = InternalPromptProgressRun::start_ultraplan(task);
-        match self.run_internal_prompt_text_with_progress(
-            &prompt,
-            true,
-            Some(progress.reporter()),
-        ) {
+        match self.run_internal_prompt_text_with_progress(&prompt, true, Some(progress.reporter()))
+        {
             Ok(plan) => {
                 progress.finish_success();
                 println!("{plan}");
@@ -2466,8 +2463,7 @@ impl InternalPromptProgressReporter {
 
     fn emit(&self, event: InternalPromptProgressEvent, error: Option<&str>) {
         let snapshot = self.snapshot();
-        let line =
-            format_internal_prompt_progress_line(event, &snapshot, self.elapsed(), error);
+        let line = format_internal_prompt_progress_line(event, &snapshot, self.elapsed(), error);
         self.write_line(&line);
     }
 
@@ -2586,12 +2582,10 @@ impl InternalPromptProgressRun {
 
         let (heartbeat_stop, heartbeat_rx) = mpsc::channel();
         let heartbeat_reporter = reporter.clone();
-        let heartbeat_handle = thread::spawn(move || {
-            loop {
-                match heartbeat_rx.recv_timeout(INTERNAL_PROGRESS_HEARTBEAT_INTERVAL) {
-                    Ok(()) | Err(RecvTimeoutError::Disconnected) => break,
-                    Err(RecvTimeoutError::Timeout) => heartbeat_reporter.emit_heartbeat(),
-                }
+        let heartbeat_handle = thread::spawn(move || loop {
+            match heartbeat_rx.recv_timeout(INTERNAL_PROGRESS_HEARTBEAT_INTERVAL) {
+                Ok(()) | Err(RecvTimeoutError::Disconnected) => break,
+                Err(RecvTimeoutError::Timeout) => heartbeat_reporter.emit_heartbeat(),
             }
         });
 
@@ -2608,7 +2602,8 @@ impl InternalPromptProgressRun {
 
     fn finish_success(&mut self) {
         self.stop_heartbeat();
-        self.reporter.emit(InternalPromptProgressEvent::Complete, None);
+        self.reporter
+            .emit(InternalPromptProgressEvent::Complete, None);
     }
 
     fn finish_failure(&mut self, error: &str) {
@@ -2646,13 +2641,20 @@ fn format_internal_prompt_progress_line(
         format!("current step {}", snapshot.step)
     };
     let mut status_bits = vec![step_label, format!("phase {}", snapshot.phase)];
-    if let Some(detail) = snapshot.detail.as_deref().filter(|detail| !detail.is_empty()) {
+    if let Some(detail) = snapshot
+        .detail
+        .as_deref()
+        .filter(|detail| !detail.is_empty())
+    {
         status_bits.push(detail.to_string());
     }
     let status = status_bits.join(" · ");
     match event {
         InternalPromptProgressEvent::Started => {
-            format!("🧭 {} status · planning started · {status}", snapshot.command_label)
+            format!(
+                "🧭 {} status · planning started · {status}",
+                snapshot.command_label
+            )
         }
         InternalPromptProgressEvent::Update => {
             format!("… {} status · {status}", snapshot.command_label)
@@ -2663,8 +2665,7 @@ fn format_internal_prompt_progress_line(
         ),
         InternalPromptProgressEvent::Complete => format!(
             "✔ {} status · completed · {elapsed_seconds}s elapsed · {} steps total",
-            snapshot.command_label,
-            snapshot.step
+            snapshot.command_label, snapshot.step
         ),
         InternalPromptProgressEvent::Failed => format!(
             "✘ {} status · failed · {elapsed_seconds}s elapsed · {}",
@@ -3756,15 +3757,14 @@ fn print_help() {
 mod tests {
     use super::{
         describe_tool_progress, filter_tool_specs, format_compact_report, format_cost_report,
-        format_internal_prompt_progress_line, format_model_report,
-        format_model_switch_report, format_permissions_report,
-        format_permissions_switch_report, format_resume_report, format_status_report,
-        format_tool_call_start, format_tool_result, normalize_permission_mode, parse_args,
-        parse_git_status_metadata, permission_policy, print_help_to, push_output_block,
-        render_config_report, render_memory_report, render_repl_help, resolve_model_alias,
-        response_to_events, resume_supported_slash_commands, status_context, CliAction,
-        CliOutputFormat, InternalPromptProgressEvent, InternalPromptProgressState, SlashCommand,
-        StatusUsage, DEFAULT_MODEL,
+        format_internal_prompt_progress_line, format_model_report, format_model_switch_report,
+        format_permissions_report, format_permissions_switch_report, format_resume_report,
+        format_status_report, format_tool_call_start, format_tool_result,
+        normalize_permission_mode, parse_args, parse_git_status_metadata, permission_policy,
+        print_help_to, push_output_block, render_config_report, render_memory_report,
+        render_repl_help, resolve_model_alias, response_to_events, resume_supported_slash_commands,
+        status_context, CliAction, CliOutputFormat, InternalPromptProgressEvent,
+        InternalPromptProgressState, SlashCommand, StatusUsage, DEFAULT_MODEL,
     };
     use api::{MessageResponse, OutputContentBlock, Usage};
     use plugins::{PluginTool, PluginToolDefinition, PluginToolPermission};
@@ -4449,6 +4449,69 @@ mod tests {
         assert!(output.contains("raw 119"));
     }
 
+    #[test]
+    fn ultraplan_progress_lines_include_phase_step_and_elapsed_status() {
+        let snapshot = InternalPromptProgressState {
+            command_label: "Ultraplan",
+            task_label: "ship plugin progress".to_string(),
+            step: 3,
+            phase: "running read_file".to_string(),
+            detail: Some("reading rust/crates/rusty-claude-cli/src/main.rs".to_string()),
+            saw_final_text: false,
+        };
+
+        let started = format_internal_prompt_progress_line(
+            InternalPromptProgressEvent::Started,
+            &snapshot,
+            Duration::from_secs(0),
+            None,
+        );
+        let heartbeat = format_internal_prompt_progress_line(
+            InternalPromptProgressEvent::Heartbeat,
+            &snapshot,
+            Duration::from_secs(9),
+            None,
+        );
+        let completed = format_internal_prompt_progress_line(
+            InternalPromptProgressEvent::Complete,
+            &snapshot,
+            Duration::from_secs(12),
+            None,
+        );
+        let failed = format_internal_prompt_progress_line(
+            InternalPromptProgressEvent::Failed,
+            &snapshot,
+            Duration::from_secs(12),
+            Some("network timeout"),
+        );
+
+        assert!(started.contains("planning started"));
+        assert!(started.contains("current step 3"));
+        assert!(heartbeat.contains("heartbeat"));
+        assert!(heartbeat.contains("9s elapsed"));
+        assert!(heartbeat.contains("phase running read_file"));
+        assert!(completed.contains("completed"));
+        assert!(completed.contains("3 steps total"));
+        assert!(failed.contains("failed"));
+        assert!(failed.contains("network timeout"));
+    }
+
+    #[test]
+    fn describe_tool_progress_summarizes_known_tools() {
+        assert_eq!(
+            describe_tool_progress("read_file", r#"{"path":"src/main.rs"}"#),
+            "reading src/main.rs"
+        );
+        assert!(
+            describe_tool_progress("bash", r#"{"command":"cargo test -p rusty-claude-cli"}"#)
+                .contains("cargo test -p rusty-claude-cli")
+        );
+        assert_eq!(
+            describe_tool_progress("grep_search", r#"{"pattern":"ultraplan","path":"rust"}"#),
+            "grep `ultraplan` in rust"
+        );
+    }
+
     #[test]
     fn push_output_block_renders_markdown_text() {
         let mut out = Vec::new();