diff --git a/rust/crates/commands/src/lib.rs b/rust/crates/commands/src/lib.rs index b1aa69c..4362194 100644 --- a/rust/crates/commands/src/lib.rs +++ b/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(""), 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(""), 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 ]"), 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 |enable |disable |uninstall |update ]", ), 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")); diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index 7c3179c..b00951f 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/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();