feat: command surface and slash completion wiring
This commit is contained in:
@@ -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 runtime::{compact_session, CompactionConfig, Session};
|
||||
|
||||
@@ -34,6 +39,7 @@ impl CommandRegistry {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct SlashCommandSpec {
|
||||
pub name: &'static str,
|
||||
pub aliases: &'static [&'static str],
|
||||
pub summary: &'static str,
|
||||
pub argument_hint: Option<&'static str>,
|
||||
pub resume_supported: bool,
|
||||
@@ -581,9 +587,10 @@ pub fn handle_slash_command(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
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,
|
||||
SlashCommand,
|
||||
DefinitionSource, SlashCommand,
|
||||
};
|
||||
use plugins::{PluginKind, PluginManager, PluginManagerConfig, PluginMetadata, PluginSummary};
|
||||
use runtime::{CompactionConfig, ContentBlock, ConversationMessage, MessageRole, Session};
|
||||
@@ -921,6 +928,86 @@ mod tests {
|
||||
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]
|
||||
fn installs_plugin_from_path_and_lists_it() {
|
||||
let config_home = temp_dir("home");
|
||||
|
||||
@@ -987,6 +987,7 @@ impl LiveCli {
|
||||
true,
|
||||
allowed_tools.clone(),
|
||||
permission_mode,
|
||||
None,
|
||||
)?;
|
||||
let cli = Self {
|
||||
model,
|
||||
@@ -1084,6 +1085,7 @@ impl LiveCli {
|
||||
false,
|
||||
self.allowed_tools.clone(),
|
||||
self.permission_mode,
|
||||
None,
|
||||
)?;
|
||||
let mut permission_prompter = CliPermissionPrompter::new(self.permission_mode);
|
||||
let summary = runtime.run_turn(input, Some(&mut permission_prompter))?;
|
||||
@@ -1265,6 +1267,7 @@ impl LiveCli {
|
||||
true,
|
||||
self.allowed_tools.clone(),
|
||||
self.permission_mode,
|
||||
None,
|
||||
)?;
|
||||
self.model.clone_from(&model);
|
||||
println!(
|
||||
@@ -1308,6 +1311,7 @@ impl LiveCli {
|
||||
true,
|
||||
self.allowed_tools.clone(),
|
||||
self.permission_mode,
|
||||
None,
|
||||
)?;
|
||||
println!(
|
||||
"{}",
|
||||
@@ -1333,6 +1337,7 @@ impl LiveCli {
|
||||
true,
|
||||
self.allowed_tools.clone(),
|
||||
self.permission_mode,
|
||||
None,
|
||||
)?;
|
||||
println!(
|
||||
"Session cleared\n Mode fresh session\n Preserved model {}\n Permission mode {}\n Session {}",
|
||||
@@ -1368,6 +1373,7 @@ impl LiveCli {
|
||||
true,
|
||||
self.allowed_tools.clone(),
|
||||
self.permission_mode,
|
||||
None,
|
||||
)?;
|
||||
self.session = handle;
|
||||
println!(
|
||||
@@ -1440,6 +1446,7 @@ impl LiveCli {
|
||||
true,
|
||||
self.allowed_tools.clone(),
|
||||
self.permission_mode,
|
||||
None,
|
||||
)?;
|
||||
self.session = handle;
|
||||
println!(
|
||||
@@ -1483,6 +1490,7 @@ impl LiveCli {
|
||||
true,
|
||||
self.allowed_tools.clone(),
|
||||
self.permission_mode,
|
||||
None,
|
||||
)?;
|
||||
self.persist_session()
|
||||
}
|
||||
@@ -1500,16 +1508,18 @@ impl LiveCli {
|
||||
true,
|
||||
self.allowed_tools.clone(),
|
||||
self.permission_mode,
|
||||
None,
|
||||
)?;
|
||||
self.persist_session()?;
|
||||
println!("{}", format_compact_report(removed, kept, skipped));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_internal_prompt_text(
|
||||
fn run_internal_prompt_text_with_progress(
|
||||
&self,
|
||||
prompt: &str,
|
||||
enable_tools: bool,
|
||||
progress: Option<InternalPromptProgressReporter>,
|
||||
) -> Result<String, Box<dyn std::error::Error>> {
|
||||
let session = self.runtime.session().clone();
|
||||
let mut runtime = build_runtime(
|
||||
@@ -1520,12 +1530,21 @@ impl LiveCli {
|
||||
false,
|
||||
self.allowed_tools.clone(),
|
||||
self.permission_mode,
|
||||
progress,
|
||||
)?;
|
||||
let mut permission_prompter = CliPermissionPrompter::new(self.permission_mode);
|
||||
let summary = runtime.run_turn(prompt, Some(&mut permission_prompter))?;
|
||||
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>> {
|
||||
let scope = scope.unwrap_or("the current repository");
|
||||
let prompt = format!(
|
||||
@@ -1540,8 +1559,22 @@ impl LiveCli {
|
||||
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."
|
||||
);
|
||||
println!("{}", self.run_internal_prompt_text(&prompt, true)?);
|
||||
Ok(())
|
||||
let mut progress = InternalPromptProgressRun::start_ultraplan(task);
|
||||
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)]
|
||||
@@ -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)]
|
||||
fn build_runtime(
|
||||
session: Session,
|
||||
@@ -2384,6 +2741,7 @@ fn build_runtime(
|
||||
emit_output: bool,
|
||||
allowed_tools: Option<AllowedToolSet>,
|
||||
permission_mode: PermissionMode,
|
||||
progress_reporter: Option<InternalPromptProgressReporter>,
|
||||
) -> Result<ConversationRuntime<AnthropicRuntimeClient, CliToolExecutor>, Box<dyn std::error::Error>>
|
||||
{
|
||||
let (feature_config, plugin_registry, tool_registry) = build_runtime_plugin_state()?;
|
||||
@@ -2395,6 +2753,7 @@ fn build_runtime(
|
||||
emit_output,
|
||||
allowed_tools.clone(),
|
||||
tool_registry.clone(),
|
||||
progress_reporter,
|
||||
)?,
|
||||
CliToolExecutor::new(allowed_tools.clone(), emit_output, tool_registry.clone()),
|
||||
permission_policy(permission_mode, &tool_registry),
|
||||
@@ -2458,6 +2817,7 @@ struct AnthropicRuntimeClient {
|
||||
emit_output: bool,
|
||||
allowed_tools: Option<AllowedToolSet>,
|
||||
tool_registry: GlobalToolRegistry,
|
||||
progress_reporter: Option<InternalPromptProgressReporter>,
|
||||
}
|
||||
|
||||
impl AnthropicRuntimeClient {
|
||||
@@ -2467,6 +2827,7 @@ impl AnthropicRuntimeClient {
|
||||
emit_output: bool,
|
||||
allowed_tools: Option<AllowedToolSet>,
|
||||
tool_registry: GlobalToolRegistry,
|
||||
progress_reporter: Option<InternalPromptProgressReporter>,
|
||||
) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
Ok(Self {
|
||||
runtime: tokio::runtime::Runtime::new()?,
|
||||
@@ -2477,6 +2838,7 @@ impl AnthropicRuntimeClient {
|
||||
emit_output,
|
||||
allowed_tools,
|
||||
tool_registry,
|
||||
progress_reporter,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2494,6 +2856,9 @@ fn resolve_cli_auth_source() -> Result<AuthSource, Box<dyn std::error::Error>> {
|
||||
impl ApiClient for AnthropicRuntimeClient {
|
||||
#[allow(clippy::too_many_lines)]
|
||||
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 {
|
||||
model: self.model.clone(),
|
||||
max_tokens: max_tokens_for_model(&self.model),
|
||||
@@ -2548,6 +2913,9 @@ impl ApiClient for AnthropicRuntimeClient {
|
||||
ApiStreamEvent::ContentBlockDelta(delta) => match delta.delta {
|
||||
ContentBlockDelta::TextDelta { text } => {
|
||||
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) {
|
||||
write!(out, "{rendered}")
|
||||
.and_then(|()| out.flush())
|
||||
@@ -2571,6 +2939,9 @@ impl ApiClient for AnthropicRuntimeClient {
|
||||
.map_err(|error| RuntimeError::new(error.to_string()))?;
|
||||
}
|
||||
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
|
||||
writeln!(out, "\n{}", format_tool_call_start(&name, &input))
|
||||
.and_then(|()| out.flush())
|
||||
@@ -3384,19 +3755,23 @@ fn print_help() {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
filter_tool_specs, format_compact_report, format_cost_report, 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, SlashCommand, StatusUsage, DEFAULT_MODEL,
|
||||
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,
|
||||
};
|
||||
use api::{MessageResponse, OutputContentBlock, Usage};
|
||||
use plugins::{PluginTool, PluginToolDefinition, PluginToolPermission};
|
||||
use runtime::{AssistantEvent, ContentBlock, ConversationMessage, MessageRole, PermissionMode};
|
||||
use serde_json::json;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
use tools::GlobalToolRegistry;
|
||||
|
||||
fn registry_with_plugin_tool() -> GlobalToolRegistry {
|
||||
|
||||
Reference in New Issue
Block a user