feat: command surface and slash completion wiring

This commit is contained in:
Yeachan-Heo
2026-04-01 08:05:22 +00:00
parent 24fea5db9e
commit 7f33569f3a
2 changed files with 474 additions and 12 deletions

View File

@@ -1,3 +1,8 @@
use std::collections::BTreeMap;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use plugins::{PluginError, PluginManager, PluginSummary}; use plugins::{PluginError, PluginManager, PluginSummary};
use runtime::{compact_session, CompactionConfig, Session}; use runtime::{compact_session, CompactionConfig, Session};
@@ -34,6 +39,7 @@ impl CommandRegistry {
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SlashCommandSpec { pub struct SlashCommandSpec {
pub name: &'static str, pub name: &'static str,
pub aliases: &'static [&'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, pub resume_supported: bool,
@@ -581,9 +587,10 @@ pub fn handle_slash_command(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{ use super::{
handle_plugins_slash_command, handle_slash_command, render_plugins_report, handle_plugins_slash_command, handle_slash_command, load_agents_from_roots,
load_skills_from_roots, render_agents_report, render_plugins_report, render_skills_report,
render_slash_command_help, resume_supported_slash_commands, slash_command_specs, render_slash_command_help, resume_supported_slash_commands, slash_command_specs,
SlashCommand, DefinitionSource, SlashCommand,
}; };
use plugins::{PluginKind, PluginManager, PluginManagerConfig, PluginMetadata, PluginSummary}; use plugins::{PluginKind, PluginManager, PluginManagerConfig, PluginMetadata, PluginSummary};
use runtime::{CompactionConfig, ContentBlock, ConversationMessage, MessageRole, Session}; use runtime::{CompactionConfig, ContentBlock, ConversationMessage, MessageRole, Session};
@@ -921,6 +928,86 @@ mod tests {
assert!(rendered.contains("disabled")); assert!(rendered.contains("disabled"));
} }
#[test]
fn lists_agents_from_project_and_user_roots() {
let workspace = temp_dir("agents-workspace");
let project_agents = workspace.join(".codex").join("agents");
let user_home = temp_dir("agents-home");
let user_agents = user_home.join(".codex").join("agents");
write_agent(
&project_agents,
"planner",
"Project planner",
"gpt-5.4",
"medium",
);
write_agent(
&user_agents,
"planner",
"User planner",
"gpt-5.4-mini",
"high",
);
write_agent(
&user_agents,
"verifier",
"Verification agent",
"gpt-5.4-mini",
"high",
);
let roots = vec![
(DefinitionSource::ProjectCodex, project_agents),
(DefinitionSource::UserCodex, user_agents),
];
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"));
assert!(report.contains("Project (.codex):"));
assert!(report.contains("planner · Project planner · gpt-5.4 · medium"));
assert!(report.contains("User (~/.codex):"));
assert!(report.contains("(shadowed by Project (.codex)) planner · User planner"));
assert!(report.contains("verifier · Verification agent · gpt-5.4-mini · high"));
let _ = fs::remove_dir_all(workspace);
let _ = fs::remove_dir_all(user_home);
}
#[test]
fn lists_skills_from_project_and_user_roots() {
let workspace = temp_dir("skills-workspace");
let project_skills = workspace.join(".codex").join("skills");
let user_home = temp_dir("skills-home");
let user_skills = user_home.join(".codex").join("skills");
write_skill(&project_skills, "plan", "Project planning guidance");
write_skill(&user_skills, "plan", "User planning guidance");
write_skill(&user_skills, "help", "Help guidance");
let roots = vec![
(DefinitionSource::ProjectCodex, project_skills),
(DefinitionSource::UserCodex, user_skills),
];
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"));
assert!(report.contains("Project (.codex):"));
assert!(report.contains("plan · Project planning guidance"));
assert!(report.contains("User (~/.codex):"));
assert!(report.contains("(shadowed by Project (.codex)) plan · User planning guidance"));
assert!(report.contains("help · Help guidance"));
let _ = fs::remove_dir_all(workspace);
let _ = fs::remove_dir_all(user_home);
}
#[test] #[test]
fn installs_plugin_from_path_and_lists_it() { fn installs_plugin_from_path_and_lists_it() {
let config_home = temp_dir("home"); let config_home = temp_dir("home");

View File

@@ -987,6 +987,7 @@ impl LiveCli {
true, true,
allowed_tools.clone(), allowed_tools.clone(),
permission_mode, permission_mode,
None,
)?; )?;
let cli = Self { let cli = Self {
model, model,
@@ -1084,6 +1085,7 @@ impl LiveCli {
false, false,
self.allowed_tools.clone(), self.allowed_tools.clone(),
self.permission_mode, self.permission_mode,
None,
)?; )?;
let mut permission_prompter = CliPermissionPrompter::new(self.permission_mode); let mut permission_prompter = CliPermissionPrompter::new(self.permission_mode);
let summary = runtime.run_turn(input, Some(&mut permission_prompter))?; let summary = runtime.run_turn(input, Some(&mut permission_prompter))?;
@@ -1265,6 +1267,7 @@ impl LiveCli {
true, true,
self.allowed_tools.clone(), self.allowed_tools.clone(),
self.permission_mode, self.permission_mode,
None,
)?; )?;
self.model.clone_from(&model); self.model.clone_from(&model);
println!( println!(
@@ -1308,6 +1311,7 @@ impl LiveCli {
true, true,
self.allowed_tools.clone(), self.allowed_tools.clone(),
self.permission_mode, self.permission_mode,
None,
)?; )?;
println!( println!(
"{}", "{}",
@@ -1333,6 +1337,7 @@ impl LiveCli {
true, true,
self.allowed_tools.clone(), self.allowed_tools.clone(),
self.permission_mode, self.permission_mode,
None,
)?; )?;
println!( println!(
"Session cleared\n Mode fresh session\n Preserved model {}\n Permission mode {}\n Session {}", "Session cleared\n Mode fresh session\n Preserved model {}\n Permission mode {}\n Session {}",
@@ -1368,6 +1373,7 @@ impl LiveCli {
true, true,
self.allowed_tools.clone(), self.allowed_tools.clone(),
self.permission_mode, self.permission_mode,
None,
)?; )?;
self.session = handle; self.session = handle;
println!( println!(
@@ -1440,6 +1446,7 @@ impl LiveCli {
true, true,
self.allowed_tools.clone(), self.allowed_tools.clone(),
self.permission_mode, self.permission_mode,
None,
)?; )?;
self.session = handle; self.session = handle;
println!( println!(
@@ -1483,6 +1490,7 @@ impl LiveCli {
true, true,
self.allowed_tools.clone(), self.allowed_tools.clone(),
self.permission_mode, self.permission_mode,
None,
)?; )?;
self.persist_session() self.persist_session()
} }
@@ -1500,16 +1508,18 @@ impl LiveCli {
true, true,
self.allowed_tools.clone(), self.allowed_tools.clone(),
self.permission_mode, self.permission_mode,
None,
)?; )?;
self.persist_session()?; self.persist_session()?;
println!("{}", format_compact_report(removed, kept, skipped)); println!("{}", format_compact_report(removed, kept, skipped));
Ok(()) Ok(())
} }
fn run_internal_prompt_text( fn run_internal_prompt_text_with_progress(
&self, &self,
prompt: &str, prompt: &str,
enable_tools: bool, enable_tools: bool,
progress: Option<InternalPromptProgressReporter>,
) -> Result<String, Box<dyn std::error::Error>> { ) -> Result<String, Box<dyn std::error::Error>> {
let session = self.runtime.session().clone(); let session = self.runtime.session().clone();
let mut runtime = build_runtime( let mut runtime = build_runtime(
@@ -1520,12 +1530,21 @@ impl LiveCli {
false, false,
self.allowed_tools.clone(), self.allowed_tools.clone(),
self.permission_mode, self.permission_mode,
progress,
)?; )?;
let mut permission_prompter = CliPermissionPrompter::new(self.permission_mode); let mut permission_prompter = CliPermissionPrompter::new(self.permission_mode);
let summary = runtime.run_turn(prompt, Some(&mut permission_prompter))?; let summary = runtime.run_turn(prompt, Some(&mut permission_prompter))?;
Ok(final_assistant_text(&summary).trim().to_string()) Ok(final_assistant_text(&summary).trim().to_string())
} }
fn run_internal_prompt_text(
&self,
prompt: &str,
enable_tools: bool,
) -> Result<String, Box<dyn std::error::Error>> {
self.run_internal_prompt_text_with_progress(prompt, enable_tools, None)
}
fn run_bughunter(&self, scope: Option<&str>) -> Result<(), Box<dyn std::error::Error>> { fn run_bughunter(&self, scope: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
let scope = scope.unwrap_or("the current repository"); let scope = scope.unwrap_or("the current repository");
let prompt = format!( let prompt = format!(
@@ -1540,8 +1559,22 @@ impl LiveCli {
let prompt = format!( let prompt = format!(
"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."
); );
println!("{}", self.run_internal_prompt_text(&prompt, true)?); let mut progress = InternalPromptProgressRun::start_ultraplan(task);
Ok(()) match self.run_internal_prompt_text_with_progress(
&prompt,
true,
Some(progress.reporter()),
) {
Ok(plan) => {
progress.finish_success();
println!("{plan}");
Ok(())
}
Err(error) => {
progress.finish_failure(&error.to_string());
Err(error)
}
}
} }
#[allow(clippy::unused_self)] #[allow(clippy::unused_self)]
@@ -2375,6 +2408,330 @@ fn resolve_plugin_path(cwd: &Path, config_home: &Path, value: &str) -> PathBuf {
} }
} }
#[derive(Debug, Clone, PartialEq, Eq)]
struct InternalPromptProgressState {
command_label: &'static str,
task_label: String,
step: usize,
phase: String,
detail: Option<String>,
saw_final_text: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum InternalPromptProgressEvent {
Started,
Update,
Heartbeat,
Complete,
Failed,
}
#[derive(Debug)]
struct InternalPromptProgressShared {
state: Mutex<InternalPromptProgressState>,
output_lock: Mutex<()>,
started_at: Instant,
}
#[derive(Debug, Clone)]
struct InternalPromptProgressReporter {
shared: Arc<InternalPromptProgressShared>,
}
#[derive(Debug)]
struct InternalPromptProgressRun {
reporter: InternalPromptProgressReporter,
heartbeat_stop: Option<mpsc::Sender<()>>,
heartbeat_handle: Option<thread::JoinHandle<()>>,
}
impl InternalPromptProgressReporter {
fn ultraplan(task: &str) -> Self {
Self {
shared: Arc::new(InternalPromptProgressShared {
state: Mutex::new(InternalPromptProgressState {
command_label: "Ultraplan",
task_label: task.to_string(),
step: 0,
phase: "planning started".to_string(),
detail: Some(format!("task: {task}")),
saw_final_text: false,
}),
output_lock: Mutex::new(()),
started_at: Instant::now(),
}),
}
}
fn emit(&self, event: InternalPromptProgressEvent, error: Option<&str>) {
let snapshot = self.snapshot();
let line =
format_internal_prompt_progress_line(event, &snapshot, self.elapsed(), error);
self.write_line(&line);
}
fn mark_model_phase(&self) {
let snapshot = {
let mut state = self
.shared
.state
.lock()
.expect("internal prompt progress state poisoned");
state.step += 1;
state.phase = if state.step == 1 {
"analyzing request".to_string()
} else {
"reviewing findings".to_string()
};
state.detail = Some(format!("task: {}", state.task_label));
state.clone()
};
self.write_line(&format_internal_prompt_progress_line(
InternalPromptProgressEvent::Update,
&snapshot,
self.elapsed(),
None,
));
}
fn mark_tool_phase(&self, name: &str, input: &str) {
let detail = describe_tool_progress(name, input);
let snapshot = {
let mut state = self
.shared
.state
.lock()
.expect("internal prompt progress state poisoned");
state.step += 1;
state.phase = format!("running {name}");
state.detail = Some(detail);
state.clone()
};
self.write_line(&format_internal_prompt_progress_line(
InternalPromptProgressEvent::Update,
&snapshot,
self.elapsed(),
None,
));
}
fn mark_text_phase(&self, text: &str) {
let trimmed = text.trim();
if trimmed.is_empty() {
return;
}
let detail = truncate_for_summary(first_visible_line(trimmed), 120);
let snapshot = {
let mut state = self
.shared
.state
.lock()
.expect("internal prompt progress state poisoned");
if state.saw_final_text {
return;
}
state.saw_final_text = true;
state.step += 1;
state.phase = "drafting final plan".to_string();
state.detail = (!detail.is_empty()).then_some(detail);
state.clone()
};
self.write_line(&format_internal_prompt_progress_line(
InternalPromptProgressEvent::Update,
&snapshot,
self.elapsed(),
None,
));
}
fn emit_heartbeat(&self) {
let snapshot = self.snapshot();
self.write_line(&format_internal_prompt_progress_line(
InternalPromptProgressEvent::Heartbeat,
&snapshot,
self.elapsed(),
None,
));
}
fn snapshot(&self) -> InternalPromptProgressState {
self.shared
.state
.lock()
.expect("internal prompt progress state poisoned")
.clone()
}
fn elapsed(&self) -> Duration {
self.shared.started_at.elapsed()
}
fn write_line(&self, line: &str) {
let _guard = self
.shared
.output_lock
.lock()
.expect("internal prompt progress output lock poisoned");
let mut stdout = io::stdout();
let _ = writeln!(stdout, "{line}");
let _ = stdout.flush();
}
}
impl InternalPromptProgressRun {
fn start_ultraplan(task: &str) -> Self {
let reporter = InternalPromptProgressReporter::ultraplan(task);
reporter.emit(InternalPromptProgressEvent::Started, None);
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(),
}
}
});
Self {
reporter,
heartbeat_stop: Some(heartbeat_stop),
heartbeat_handle: Some(heartbeat_handle),
}
}
fn reporter(&self) -> InternalPromptProgressReporter {
self.reporter.clone()
}
fn finish_success(&mut self) {
self.stop_heartbeat();
self.reporter.emit(InternalPromptProgressEvent::Complete, None);
}
fn finish_failure(&mut self, error: &str) {
self.stop_heartbeat();
self.reporter
.emit(InternalPromptProgressEvent::Failed, Some(error));
}
fn stop_heartbeat(&mut self) {
if let Some(sender) = self.heartbeat_stop.take() {
let _ = sender.send(());
}
if let Some(handle) = self.heartbeat_handle.take() {
let _ = handle.join();
}
}
}
impl Drop for InternalPromptProgressRun {
fn drop(&mut self) {
self.stop_heartbeat();
}
}
fn format_internal_prompt_progress_line(
event: InternalPromptProgressEvent,
snapshot: &InternalPromptProgressState,
elapsed: Duration,
error: Option<&str>,
) -> String {
let elapsed_seconds = elapsed.as_secs();
let step_label = if snapshot.step == 0 {
"current step pending".to_string()
} else {
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()) {
status_bits.push(detail.to_string());
}
let status = status_bits.join(" · ");
match event {
InternalPromptProgressEvent::Started => {
format!("🧭 {} status · planning started · {status}", snapshot.command_label)
}
InternalPromptProgressEvent::Update => {
format!("{} status · {status}", snapshot.command_label)
}
InternalPromptProgressEvent::Heartbeat => format!(
"{} heartbeat · {elapsed_seconds}s elapsed · {status}",
snapshot.command_label
),
InternalPromptProgressEvent::Complete => format!(
"{} status · completed · {elapsed_seconds}s elapsed · {} steps total",
snapshot.command_label,
snapshot.step
),
InternalPromptProgressEvent::Failed => format!(
"{} status · failed · {elapsed_seconds}s elapsed · {}",
snapshot.command_label,
error.unwrap_or("unknown error")
),
}
}
fn describe_tool_progress(name: &str, input: &str) -> String {
let parsed: serde_json::Value =
serde_json::from_str(input).unwrap_or(serde_json::Value::String(input.to_string()));
match name {
"bash" | "Bash" => {
let command = parsed
.get("command")
.and_then(|value| value.as_str())
.unwrap_or_default();
if command.is_empty() {
"running shell command".to_string()
} else {
format!("command {}", truncate_for_summary(command.trim(), 100))
}
}
"read_file" | "Read" => format!("reading {}", extract_tool_path(&parsed)),
"write_file" | "Write" => format!("writing {}", extract_tool_path(&parsed)),
"edit_file" | "Edit" => format!("editing {}", extract_tool_path(&parsed)),
"glob_search" | "Glob" => {
let pattern = parsed
.get("pattern")
.and_then(|value| value.as_str())
.unwrap_or("?");
let scope = parsed
.get("path")
.and_then(|value| value.as_str())
.unwrap_or(".");
format!("glob `{pattern}` in {scope}")
}
"grep_search" | "Grep" => {
let pattern = parsed
.get("pattern")
.and_then(|value| value.as_str())
.unwrap_or("?");
let scope = parsed
.get("path")
.and_then(|value| value.as_str())
.unwrap_or(".");
format!("grep `{pattern}` in {scope}")
}
"web_search" | "WebSearch" => parsed
.get("query")
.and_then(|value| value.as_str())
.map_or_else(
|| "running web search".to_string(),
|query| format!("query {}", truncate_for_summary(query, 100)),
),
_ => {
let summary = summarize_tool_payload(input);
if summary.is_empty() {
format!("running {name}")
} else {
format!("{name}: {summary}")
}
}
}
}
#[allow(clippy::needless_pass_by_value)] #[allow(clippy::needless_pass_by_value)]
fn build_runtime( fn build_runtime(
session: Session, session: Session,
@@ -2384,6 +2741,7 @@ fn build_runtime(
emit_output: bool, emit_output: bool,
allowed_tools: Option<AllowedToolSet>, allowed_tools: Option<AllowedToolSet>,
permission_mode: PermissionMode, permission_mode: PermissionMode,
progress_reporter: Option<InternalPromptProgressReporter>,
) -> Result<ConversationRuntime<AnthropicRuntimeClient, CliToolExecutor>, Box<dyn std::error::Error>> ) -> Result<ConversationRuntime<AnthropicRuntimeClient, CliToolExecutor>, Box<dyn std::error::Error>>
{ {
let (feature_config, plugin_registry, tool_registry) = build_runtime_plugin_state()?; let (feature_config, plugin_registry, tool_registry) = build_runtime_plugin_state()?;
@@ -2395,6 +2753,7 @@ fn build_runtime(
emit_output, emit_output,
allowed_tools.clone(), allowed_tools.clone(),
tool_registry.clone(), tool_registry.clone(),
progress_reporter,
)?, )?,
CliToolExecutor::new(allowed_tools.clone(), emit_output, tool_registry.clone()), CliToolExecutor::new(allowed_tools.clone(), emit_output, tool_registry.clone()),
permission_policy(permission_mode, &tool_registry), permission_policy(permission_mode, &tool_registry),
@@ -2458,6 +2817,7 @@ struct AnthropicRuntimeClient {
emit_output: bool, emit_output: bool,
allowed_tools: Option<AllowedToolSet>, allowed_tools: Option<AllowedToolSet>,
tool_registry: GlobalToolRegistry, tool_registry: GlobalToolRegistry,
progress_reporter: Option<InternalPromptProgressReporter>,
} }
impl AnthropicRuntimeClient { impl AnthropicRuntimeClient {
@@ -2467,6 +2827,7 @@ impl AnthropicRuntimeClient {
emit_output: bool, emit_output: bool,
allowed_tools: Option<AllowedToolSet>, allowed_tools: Option<AllowedToolSet>,
tool_registry: GlobalToolRegistry, tool_registry: GlobalToolRegistry,
progress_reporter: Option<InternalPromptProgressReporter>,
) -> Result<Self, Box<dyn std::error::Error>> { ) -> Result<Self, Box<dyn std::error::Error>> {
Ok(Self { Ok(Self {
runtime: tokio::runtime::Runtime::new()?, runtime: tokio::runtime::Runtime::new()?,
@@ -2477,6 +2838,7 @@ impl AnthropicRuntimeClient {
emit_output, emit_output,
allowed_tools, allowed_tools,
tool_registry, tool_registry,
progress_reporter,
}) })
} }
} }
@@ -2494,6 +2856,9 @@ fn resolve_cli_auth_source() -> Result<AuthSource, Box<dyn std::error::Error>> {
impl ApiClient for AnthropicRuntimeClient { impl ApiClient for AnthropicRuntimeClient {
#[allow(clippy::too_many_lines)] #[allow(clippy::too_many_lines)]
fn stream(&mut self, request: ApiRequest) -> Result<Vec<AssistantEvent>, RuntimeError> { fn stream(&mut self, request: ApiRequest) -> Result<Vec<AssistantEvent>, RuntimeError> {
if let Some(progress_reporter) = &self.progress_reporter {
progress_reporter.mark_model_phase();
}
let message_request = MessageRequest { let message_request = MessageRequest {
model: self.model.clone(), model: self.model.clone(),
max_tokens: max_tokens_for_model(&self.model), max_tokens: max_tokens_for_model(&self.model),
@@ -2548,6 +2913,9 @@ impl ApiClient for AnthropicRuntimeClient {
ApiStreamEvent::ContentBlockDelta(delta) => match delta.delta { ApiStreamEvent::ContentBlockDelta(delta) => match delta.delta {
ContentBlockDelta::TextDelta { text } => { ContentBlockDelta::TextDelta { text } => {
if !text.is_empty() { if !text.is_empty() {
if let Some(progress_reporter) = &self.progress_reporter {
progress_reporter.mark_text_phase(&text);
}
if let Some(rendered) = markdown_stream.push(&renderer, &text) { if let Some(rendered) = markdown_stream.push(&renderer, &text) {
write!(out, "{rendered}") write!(out, "{rendered}")
.and_then(|()| out.flush()) .and_then(|()| out.flush())
@@ -2571,6 +2939,9 @@ impl ApiClient for AnthropicRuntimeClient {
.map_err(|error| RuntimeError::new(error.to_string()))?; .map_err(|error| RuntimeError::new(error.to_string()))?;
} }
if let Some((id, name, input)) = pending_tool.take() { if let Some((id, name, input)) = pending_tool.take() {
if let Some(progress_reporter) = &self.progress_reporter {
progress_reporter.mark_tool_phase(&name, &input);
}
// Display tool call now that input is fully accumulated // Display tool call now that input is fully accumulated
writeln!(out, "\n{}", format_tool_call_start(&name, &input)) writeln!(out, "\n{}", format_tool_call_start(&name, &input))
.and_then(|()| out.flush()) .and_then(|()| out.flush())
@@ -3384,19 +3755,23 @@ fn print_help() {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{ use super::{
filter_tool_specs, format_compact_report, format_cost_report, format_model_report, describe_tool_progress, filter_tool_specs, format_compact_report, format_cost_report,
format_model_switch_report, format_permissions_report, format_permissions_switch_report, format_internal_prompt_progress_line, format_model_report,
format_resume_report, format_status_report, format_tool_call_start, format_tool_result, format_model_switch_report, format_permissions_report,
normalize_permission_mode, parse_args, parse_git_status_metadata, permission_policy, format_permissions_switch_report, format_resume_report, format_status_report,
print_help_to, push_output_block, render_config_report, render_memory_report, format_tool_call_start, format_tool_result, normalize_permission_mode, parse_args,
render_repl_help, resolve_model_alias, response_to_events, resume_supported_slash_commands, parse_git_status_metadata, permission_policy, print_help_to, push_output_block,
status_context, CliAction, CliOutputFormat, SlashCommand, StatusUsage, DEFAULT_MODEL, 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 api::{MessageResponse, OutputContentBlock, Usage};
use plugins::{PluginTool, PluginToolDefinition, PluginToolPermission}; use plugins::{PluginTool, PluginToolDefinition, PluginToolPermission};
use runtime::{AssistantEvent, ContentBlock, ConversationMessage, MessageRole, PermissionMode}; use runtime::{AssistantEvent, ContentBlock, ConversationMessage, MessageRole, PermissionMode};
use serde_json::json; use serde_json::json;
use std::path::PathBuf; use std::path::PathBuf;
use std::time::Duration;
use tools::GlobalToolRegistry; use tools::GlobalToolRegistry;
fn registry_with_plugin_tool() -> GlobalToolRegistry { fn registry_with_plugin_tool() -> GlobalToolRegistry {