feat: command surface follow-up integration

This commit is contained in:
Yeachan-Heo
2026-04-01 08:10:23 +00:00
parent 0755a36811
commit 7464302fd3
2 changed files with 154 additions and 35 deletions

View File

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

View File

@@ -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." "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); let mut progress = InternalPromptProgressRun::start_ultraplan(task);
match self.run_internal_prompt_text_with_progress( match self.run_internal_prompt_text_with_progress(&prompt, true, Some(progress.reporter()))
&prompt, {
true,
Some(progress.reporter()),
) {
Ok(plan) => { Ok(plan) => {
progress.finish_success(); progress.finish_success();
println!("{plan}"); println!("{plan}");
@@ -2466,8 +2463,7 @@ impl InternalPromptProgressReporter {
fn emit(&self, event: InternalPromptProgressEvent, error: Option<&str>) { fn emit(&self, event: InternalPromptProgressEvent, error: Option<&str>) {
let snapshot = self.snapshot(); let snapshot = self.snapshot();
let line = let line = format_internal_prompt_progress_line(event, &snapshot, self.elapsed(), error);
format_internal_prompt_progress_line(event, &snapshot, self.elapsed(), error);
self.write_line(&line); self.write_line(&line);
} }
@@ -2586,13 +2582,11 @@ impl InternalPromptProgressRun {
let (heartbeat_stop, heartbeat_rx) = mpsc::channel(); let (heartbeat_stop, heartbeat_rx) = mpsc::channel();
let heartbeat_reporter = reporter.clone(); let heartbeat_reporter = reporter.clone();
let heartbeat_handle = thread::spawn(move || { let heartbeat_handle = thread::spawn(move || loop {
loop {
match heartbeat_rx.recv_timeout(INTERNAL_PROGRESS_HEARTBEAT_INTERVAL) { match heartbeat_rx.recv_timeout(INTERNAL_PROGRESS_HEARTBEAT_INTERVAL) {
Ok(()) | Err(RecvTimeoutError::Disconnected) => break, Ok(()) | Err(RecvTimeoutError::Disconnected) => break,
Err(RecvTimeoutError::Timeout) => heartbeat_reporter.emit_heartbeat(), Err(RecvTimeoutError::Timeout) => heartbeat_reporter.emit_heartbeat(),
} }
}
}); });
Self { Self {
@@ -2608,7 +2602,8 @@ impl InternalPromptProgressRun {
fn finish_success(&mut self) { fn finish_success(&mut self) {
self.stop_heartbeat(); self.stop_heartbeat();
self.reporter.emit(InternalPromptProgressEvent::Complete, None); self.reporter
.emit(InternalPromptProgressEvent::Complete, None);
} }
fn finish_failure(&mut self, error: &str) { fn finish_failure(&mut self, error: &str) {
@@ -2646,13 +2641,20 @@ fn format_internal_prompt_progress_line(
format!("current step {}", snapshot.step) format!("current step {}", snapshot.step)
}; };
let mut status_bits = vec![step_label, format!("phase {}", snapshot.phase)]; 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()); status_bits.push(detail.to_string());
} }
let status = status_bits.join(" · "); let status = status_bits.join(" · ");
match event { match event {
InternalPromptProgressEvent::Started => { InternalPromptProgressEvent::Started => {
format!("🧭 {} status · planning started · {status}", snapshot.command_label) format!(
"🧭 {} status · planning started · {status}",
snapshot.command_label
)
} }
InternalPromptProgressEvent::Update => { InternalPromptProgressEvent::Update => {
format!("{} status · {status}", snapshot.command_label) format!("{} status · {status}", snapshot.command_label)
@@ -2663,8 +2665,7 @@ fn format_internal_prompt_progress_line(
), ),
InternalPromptProgressEvent::Complete => format!( InternalPromptProgressEvent::Complete => format!(
"{} status · completed · {elapsed_seconds}s elapsed · {} steps total", "{} status · completed · {elapsed_seconds}s elapsed · {} steps total",
snapshot.command_label, snapshot.command_label, snapshot.step
snapshot.step
), ),
InternalPromptProgressEvent::Failed => format!( InternalPromptProgressEvent::Failed => format!(
"{} status · failed · {elapsed_seconds}s elapsed · {}", "{} status · failed · {elapsed_seconds}s elapsed · {}",
@@ -3756,15 +3757,14 @@ fn print_help() {
mod tests { mod tests {
use super::{ use super::{
describe_tool_progress, filter_tool_specs, format_compact_report, format_cost_report, describe_tool_progress, filter_tool_specs, format_compact_report, format_cost_report,
format_internal_prompt_progress_line, format_model_report, format_internal_prompt_progress_line, format_model_report, format_model_switch_report,
format_model_switch_report, format_permissions_report, format_permissions_report, format_permissions_switch_report, format_resume_report,
format_permissions_switch_report, format_resume_report, format_status_report, format_status_report, format_tool_call_start, format_tool_result,
format_tool_call_start, format_tool_result, normalize_permission_mode, parse_args, normalize_permission_mode, parse_args, parse_git_status_metadata, permission_policy,
parse_git_status_metadata, permission_policy, print_help_to, push_output_block, print_help_to, push_output_block, render_config_report, render_memory_report,
render_config_report, render_memory_report, render_repl_help, resolve_model_alias, render_repl_help, resolve_model_alias, response_to_events, resume_supported_slash_commands,
response_to_events, resume_supported_slash_commands, status_context, CliAction, status_context, CliAction, CliOutputFormat, InternalPromptProgressEvent,
CliOutputFormat, InternalPromptProgressEvent, InternalPromptProgressState, SlashCommand, InternalPromptProgressState, SlashCommand, StatusUsage, DEFAULT_MODEL,
StatusUsage, DEFAULT_MODEL,
}; };
use api::{MessageResponse, OutputContentBlock, Usage}; use api::{MessageResponse, OutputContentBlock, Usage};
use plugins::{PluginTool, PluginToolDefinition, PluginToolPermission}; use plugins::{PluginTool, PluginToolDefinition, PluginToolPermission};
@@ -4449,6 +4449,69 @@ mod tests {
assert!(output.contains("raw 119")); 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] #[test]
fn push_output_block_renders_markdown_text() { fn push_output_block_renders_markdown_text() {
let mut out = Vec::new(); let mut out = Vec::new();