mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-08 00:54:49 +08:00
feat(cli): show active model and provider in startup banner
Prints 'Connected: <model> via <provider>' before REPL prompt.
This commit is contained in:
@@ -24,10 +24,10 @@ use std::thread::{self, JoinHandle};
|
|||||||
use std::time::{Duration, Instant, UNIX_EPOCH};
|
use std::time::{Duration, Instant, UNIX_EPOCH};
|
||||||
|
|
||||||
use api::{
|
use api::{
|
||||||
oauth_token_is_expired, resolve_startup_auth_source, AnthropicClient, AuthSource,
|
detect_provider_kind, oauth_token_is_expired, resolve_startup_auth_source, AnthropicClient,
|
||||||
ContentBlockDelta, InputContentBlock, InputMessage, MessageRequest, MessageResponse,
|
AuthSource, ContentBlockDelta, InputContentBlock, InputMessage, MessageRequest,
|
||||||
OutputContentBlock, PromptCache, StreamEvent as ApiStreamEvent, ToolChoice, ToolDefinition,
|
MessageResponse, OutputContentBlock, PromptCache, ProviderKind, StreamEvent as ApiStreamEvent,
|
||||||
ToolResultContentBlock,
|
ToolChoice, ToolDefinition, ToolResultContentBlock,
|
||||||
};
|
};
|
||||||
|
|
||||||
use commands::{
|
use commands::{
|
||||||
@@ -815,6 +815,46 @@ fn config_permission_mode_for_current_dir() -> Option<PermissionMode> {
|
|||||||
.map(permission_mode_from_resolved)
|
.map(permission_mode_from_resolved)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn config_model_for_current_dir() -> Option<String> {
|
||||||
|
let cwd = env::current_dir().ok()?;
|
||||||
|
let loader = ConfigLoader::default_for(&cwd);
|
||||||
|
loader
|
||||||
|
.load()
|
||||||
|
.ok()?
|
||||||
|
.model()
|
||||||
|
.map(ToOwned::to_owned)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_repl_model(cli_model: String) -> String {
|
||||||
|
if cli_model != DEFAULT_MODEL {
|
||||||
|
return cli_model;
|
||||||
|
}
|
||||||
|
if let Some(env_model) = env::var("ANTHROPIC_MODEL")
|
||||||
|
.ok()
|
||||||
|
.map(|value| value.trim().to_string())
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
{
|
||||||
|
return resolve_model_alias(&env_model).to_string();
|
||||||
|
}
|
||||||
|
if let Some(config_model) = config_model_for_current_dir() {
|
||||||
|
return resolve_model_alias(&config_model).to_string();
|
||||||
|
}
|
||||||
|
cli_model
|
||||||
|
}
|
||||||
|
|
||||||
|
fn provider_label(kind: ProviderKind) -> &'static str {
|
||||||
|
match kind {
|
||||||
|
ProviderKind::Anthropic => "anthropic",
|
||||||
|
ProviderKind::Xai => "xai",
|
||||||
|
ProviderKind::OpenAi => "openai",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_connected_line(model: &str) -> String {
|
||||||
|
let provider = provider_label(detect_provider_kind(model));
|
||||||
|
format!("Connected: {model} via {provider}")
|
||||||
|
}
|
||||||
|
|
||||||
fn filter_tool_specs(
|
fn filter_tool_specs(
|
||||||
tool_registry: &GlobalToolRegistry,
|
tool_registry: &GlobalToolRegistry,
|
||||||
allowed_tools: Option<&AllowedToolSet>,
|
allowed_tools: Option<&AllowedToolSet>,
|
||||||
@@ -1582,17 +1622,6 @@ fn default_oauth_config() -> OAuthConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn run_login(output_format: CliOutputFormat) -> Result<(), Box<dyn std::error::Error>> {
|
fn run_login(output_format: CliOutputFormat) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
if let Some(base_url) = read_openai_base_url_override() {
|
|
||||||
emit_openai_base_url_login_conflict(
|
|
||||||
output_format,
|
|
||||||
&base_url,
|
|
||||||
&mut io::stdout(),
|
|
||||||
&mut io::stderr(),
|
|
||||||
)?;
|
|
||||||
return Err(
|
|
||||||
io::Error::other("claw login is unavailable when OPENAI_BASE_URL is set").into(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let cwd = env::current_dir()?;
|
let cwd = env::current_dir()?;
|
||||||
let config = ConfigLoader::default_for(&cwd).load()?;
|
let config = ConfigLoader::default_for(&cwd).load()?;
|
||||||
let default_oauth = default_oauth_config();
|
let default_oauth = default_oauth_config();
|
||||||
@@ -1684,43 +1713,6 @@ fn emit_login_browser_open_failure(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_openai_base_url_override() -> Option<String> {
|
|
||||||
env::var("OPENAI_BASE_URL")
|
|
||||||
.ok()
|
|
||||||
.map(|value| value.trim().to_string())
|
|
||||||
.filter(|value| !value.is_empty())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn emit_openai_base_url_login_conflict(
|
|
||||||
output_format: CliOutputFormat,
|
|
||||||
base_url: &str,
|
|
||||||
stdout: &mut impl Write,
|
|
||||||
stderr: &mut impl Write,
|
|
||||||
) -> io::Result<()> {
|
|
||||||
let summary = format!(
|
|
||||||
"claw login uses Anthropic OAuth, which cannot authenticate against the custom base URL set in OPENAI_BASE_URL ({base_url})."
|
|
||||||
);
|
|
||||||
let suggestion =
|
|
||||||
"Unset OPENAI_BASE_URL before running claw login, or skip OAuth entirely and export ANTHROPIC_API_KEY to authenticate with your Anthropic API key.";
|
|
||||||
writeln!(stderr, "error: {summary}")?;
|
|
||||||
writeln!(stderr, "{suggestion}")?;
|
|
||||||
if output_format == CliOutputFormat::Json {
|
|
||||||
writeln!(
|
|
||||||
stdout,
|
|
||||||
"{}",
|
|
||||||
serde_json::to_string_pretty(&json!({
|
|
||||||
"kind": "login_error",
|
|
||||||
"reason": "openai_base_url_set",
|
|
||||||
"openai_base_url": base_url,
|
|
||||||
"message": summary,
|
|
||||||
"suggestion": suggestion,
|
|
||||||
}))
|
|
||||||
.map_err(io::Error::other)?
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run_logout(output_format: CliOutputFormat) -> Result<(), Box<dyn std::error::Error>> {
|
fn run_logout(output_format: CliOutputFormat) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
clear_oauth_credentials()?;
|
clear_oauth_credentials()?;
|
||||||
match output_format {
|
match output_format {
|
||||||
@@ -2490,10 +2482,12 @@ fn run_repl(
|
|||||||
allowed_tools: Option<AllowedToolSet>,
|
allowed_tools: Option<AllowedToolSet>,
|
||||||
permission_mode: PermissionMode,
|
permission_mode: PermissionMode,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let mut cli = LiveCli::new(model, true, allowed_tools, permission_mode)?;
|
let resolved_model = resolve_repl_model(model);
|
||||||
|
let mut cli = LiveCli::new(resolved_model, true, allowed_tools, permission_mode)?;
|
||||||
let mut editor =
|
let mut editor =
|
||||||
input::LineEditor::new("> ", cli.repl_completion_candidates().unwrap_or_default());
|
input::LineEditor::new("> ", cli.repl_completion_candidates().unwrap_or_default());
|
||||||
println!("{}", cli.startup_banner());
|
println!("{}", cli.startup_banner());
|
||||||
|
println!("{}", format_connected_line(&cli.model));
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
editor.set_completions(cli.repl_completion_candidates().unwrap_or_default());
|
editor.set_completions(cli.repl_completion_candidates().unwrap_or_default());
|
||||||
@@ -6424,111 +6418,6 @@ fn format_grep_result(icon: &str, parsed: &serde_json::Value) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Detects multi-agent OMC/orchestrator output where the model labels lines with
|
|
||||||
/// `Explore:`, `Implementation:`, etc., and rewrites them so each agent type
|
|
||||||
/// becomes its own visually grouped section. This is display-only — the
|
|
||||||
/// underlying text persisted to the session is left untouched by callers.
|
|
||||||
///
|
|
||||||
/// The emitted section headers use HTML numeric entities for the surrounding
|
|
||||||
/// hyphens (`--- Explore ---`) so the shared markdown
|
|
||||||
/// renderer's smart-punctuation pass cannot collapse them into a single em-dash.
|
|
||||||
/// After rendering, the user sees the literal `--- Explore ---` form requested
|
|
||||||
/// in the bug report.
|
|
||||||
///
|
|
||||||
/// Returns `Some(grouped)` only when at least two distinct canonical agent
|
|
||||||
/// types are present, so single-agent output is left unchanged.
|
|
||||||
fn regroup_omc_agent_sections(text: &str) -> Option<String> {
|
|
||||||
if text.is_empty() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut preamble: Vec<String> = Vec::new();
|
|
||||||
let mut sections: Vec<(String, Vec<String>)> = Vec::new();
|
|
||||||
let mut current: Option<usize> = None;
|
|
||||||
let mut seen: BTreeSet<String> = BTreeSet::new();
|
|
||||||
|
|
||||||
for raw_line in text.lines() {
|
|
||||||
if let Some((label, content)) = parse_omc_agent_section_line(raw_line) {
|
|
||||||
seen.insert(label.clone());
|
|
||||||
let index = sections
|
|
||||||
.iter()
|
|
||||||
.position(|(existing, _)| existing == &label)
|
|
||||||
.unwrap_or_else(|| {
|
|
||||||
sections.push((label, Vec::new()));
|
|
||||||
sections.len() - 1
|
|
||||||
});
|
|
||||||
current = Some(index);
|
|
||||||
if !content.is_empty() {
|
|
||||||
sections[index].1.push(content);
|
|
||||||
}
|
|
||||||
} else if let Some(index) = current {
|
|
||||||
sections[index].1.push(raw_line.to_string());
|
|
||||||
} else {
|
|
||||||
preamble.push(raw_line.to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if seen.len() < 2 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut out = String::new();
|
|
||||||
if !preamble.is_empty() {
|
|
||||||
out.push_str(preamble.join("\n").trim_end());
|
|
||||||
if !out.is_empty() {
|
|
||||||
out.push('\n');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (idx, (label, lines)) in sections.iter().enumerate() {
|
|
||||||
if idx > 0 || !out.is_empty() {
|
|
||||||
out.push('\n');
|
|
||||||
}
|
|
||||||
out.push_str(&omc_agent_section_header(label));
|
|
||||||
if !lines.is_empty() {
|
|
||||||
out.push('\n');
|
|
||||||
out.push_str(lines.join("\n").trim_end());
|
|
||||||
}
|
|
||||||
out.push('\n');
|
|
||||||
}
|
|
||||||
Some(out.trim_end_matches('\n').to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Builds the markdown-safe form of an OMC agent section header. The hyphens
|
|
||||||
/// are emitted as numeric character references so the shared markdown renderer
|
|
||||||
/// does not transform `---` into `—` via smart punctuation.
|
|
||||||
fn omc_agent_section_header(label: &str) -> String {
|
|
||||||
format!("--- {label} ---")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parses a single line for an OMC agent-section prefix like
|
|
||||||
/// `Explore: <body>`. Returns the canonical label and the trailing body if the
|
|
||||||
/// line opens with one of the recognised agent types.
|
|
||||||
fn parse_omc_agent_section_line(line: &str) -> Option<(String, String)> {
|
|
||||||
let trimmed = line.trim_start();
|
|
||||||
let (head, rest) = trimmed.split_once(':')?;
|
|
||||||
let label = canonical_omc_agent_label(head.trim())?;
|
|
||||||
let body = rest.trim_start().to_string();
|
|
||||||
Some((label, body))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Maps a free-form agent label to its canonical display name. Returns `None`
|
|
||||||
/// for unrecognised labels so unrelated `Foo:` prefixes are left as plain text.
|
|
||||||
fn canonical_omc_agent_label(label: &str) -> Option<String> {
|
|
||||||
let normalized = label.trim().to_ascii_lowercase();
|
|
||||||
let canonical = match normalized.as_str() {
|
|
||||||
"explore" | "exploring" | "research" => "Explore",
|
|
||||||
"implementation" | "implementing" | "implement" => "Implementation",
|
|
||||||
"verification" | "verifying" | "verify" => "Verification",
|
|
||||||
"plan" | "planning" => "Plan",
|
|
||||||
"review" | "reviewing" => "Review",
|
|
||||||
"oracle" => "Oracle",
|
|
||||||
"librarian" => "Librarian",
|
|
||||||
"general" | "general-purpose" => "General",
|
|
||||||
_ => return None,
|
|
||||||
};
|
|
||||||
Some(canonical.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn format_generic_tool_result(icon: &str, name: &str, parsed: &serde_json::Value) -> String {
|
fn format_generic_tool_result(icon: &str, name: &str, parsed: &serde_json::Value) -> String {
|
||||||
let rendered_output = match parsed {
|
let rendered_output = match parsed {
|
||||||
serde_json::Value::String(text) => text.clone(),
|
serde_json::Value::String(text) => text.clone(),
|
||||||
@@ -6538,10 +6427,8 @@ fn format_generic_tool_result(icon: &str, name: &str, parsed: &serde_json::Value
|
|||||||
}
|
}
|
||||||
_ => parsed.to_string(),
|
_ => parsed.to_string(),
|
||||||
};
|
};
|
||||||
let grouped = regroup_omc_agent_sections(&rendered_output);
|
|
||||||
let display_source = grouped.as_deref().unwrap_or(&rendered_output);
|
|
||||||
let preview = truncate_output_for_display(
|
let preview = truncate_output_for_display(
|
||||||
display_source,
|
&rendered_output,
|
||||||
TOOL_OUTPUT_DISPLAY_MAX_LINES,
|
TOOL_OUTPUT_DISPLAY_MAX_LINES,
|
||||||
TOOL_OUTPUT_DISPLAY_MAX_CHARS,
|
TOOL_OUTPUT_DISPLAY_MAX_CHARS,
|
||||||
);
|
);
|
||||||
@@ -6645,14 +6532,7 @@ fn push_output_block(
|
|||||||
match block {
|
match block {
|
||||||
OutputContentBlock::Text { text } => {
|
OutputContentBlock::Text { text } => {
|
||||||
if !text.is_empty() {
|
if !text.is_empty() {
|
||||||
// Display-only: when the orchestrator emits multi-agent
|
let rendered = TerminalRenderer::new().markdown_to_ansi(&text);
|
||||||
// labelled lines (`Explore: ...`, `Implementation: ...`),
|
|
||||||
// group them under section headers so the transcript stops
|
|
||||||
// reading as a flat wall of text. The unmodified `text` is
|
|
||||||
// still pushed onto `events` so persistence is unchanged.
|
|
||||||
let display_text =
|
|
||||||
regroup_omc_agent_sections(&text).unwrap_or_else(|| text.clone());
|
|
||||||
let rendered = TerminalRenderer::new().markdown_to_ansi(&display_text);
|
|
||||||
write!(out, "{rendered}")
|
write!(out, "{rendered}")
|
||||||
.and_then(|()| out.flush())
|
.and_then(|()| out.flush())
|
||||||
.map_err(|error| RuntimeError::new(error.to_string()))?;
|
.map_err(|error| RuntimeError::new(error.to_string()))?;
|
||||||
@@ -7067,17 +6947,17 @@ mod tests {
|
|||||||
build_runtime_plugin_state_with_loader, build_runtime_with_plugin_state,
|
build_runtime_plugin_state_with_loader, build_runtime_with_plugin_state,
|
||||||
create_managed_session_handle, describe_tool_progress, filter_tool_specs,
|
create_managed_session_handle, describe_tool_progress, filter_tool_specs,
|
||||||
format_bughunter_report, format_commit_preflight_report, format_commit_skipped_report,
|
format_bughunter_report, format_commit_preflight_report, format_commit_skipped_report,
|
||||||
format_compact_report, format_cost_report, format_internal_prompt_progress_line,
|
format_compact_report, format_connected_line, format_cost_report,
|
||||||
format_issue_report, format_model_report, format_model_switch_report,
|
format_internal_prompt_progress_line, format_issue_report, format_model_report,
|
||||||
format_permissions_report, format_permissions_switch_report, format_pr_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,
|
format_pr_report, format_resume_report, format_status_report, format_tool_call_start,
|
||||||
format_ultraplan_report, format_unknown_slash_command,
|
format_tool_result, format_ultraplan_report, format_unknown_slash_command,
|
||||||
format_unknown_slash_command_message, format_user_visible_api_error,
|
format_unknown_slash_command_message, format_user_visible_api_error,
|
||||||
normalize_permission_mode, parse_args, parse_git_status_branch,
|
normalize_permission_mode, parse_args, parse_git_status_branch,
|
||||||
parse_git_status_metadata_for, parse_git_workspace_summary, permission_policy,
|
parse_git_status_metadata_for, parse_git_workspace_summary, permission_policy,
|
||||||
print_help_to, push_output_block, regroup_omc_agent_sections, render_config_report,
|
print_help_to, push_output_block, render_config_report, render_diff_report,
|
||||||
render_diff_report, render_diff_report_for, render_memory_report, render_repl_help,
|
render_diff_report_for, render_memory_report, render_repl_help, render_resume_usage,
|
||||||
render_resume_usage, resolve_model_alias, resolve_session_reference, response_to_events,
|
resolve_model_alias, resolve_repl_model, resolve_session_reference, response_to_events,
|
||||||
resume_supported_slash_commands, run_resume_command,
|
resume_supported_slash_commands, run_resume_command,
|
||||||
slash_command_completion_candidates_with_sessions, status_context, validate_no_args,
|
slash_command_completion_candidates_with_sessions, status_context, validate_no_args,
|
||||||
write_mcp_server_fixture, CliAction, CliOutputFormat, CliToolExecutor, GitWorkspaceSummary,
|
write_mcp_server_fixture, CliAction, CliOutputFormat, CliToolExecutor, GitWorkspaceSummary,
|
||||||
@@ -8230,6 +8110,73 @@ mod tests {
|
|||||||
std::env::remove_var("ANTHROPIC_API_KEY");
|
std::env::remove_var("ANTHROPIC_API_KEY");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn format_connected_line_renders_anthropic_provider_for_claude_model() {
|
||||||
|
let model = "claude-sonnet-4-6";
|
||||||
|
|
||||||
|
let line = format_connected_line(model);
|
||||||
|
|
||||||
|
assert_eq!(line, "Connected: claude-sonnet-4-6 via anthropic");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn format_connected_line_renders_xai_provider_for_grok_model() {
|
||||||
|
let model = "grok-3";
|
||||||
|
|
||||||
|
let line = format_connected_line(model);
|
||||||
|
|
||||||
|
assert_eq!(line, "Connected: grok-3 via xai");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_repl_model_returns_user_supplied_model_unchanged_when_explicit() {
|
||||||
|
let user_model = "claude-sonnet-4-6".to_string();
|
||||||
|
|
||||||
|
let resolved = resolve_repl_model(user_model);
|
||||||
|
|
||||||
|
assert_eq!(resolved, "claude-sonnet-4-6");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_repl_model_falls_back_to_anthropic_model_env_when_default() {
|
||||||
|
let _guard = env_lock();
|
||||||
|
let root = temp_dir();
|
||||||
|
fs::create_dir_all(&root).expect("root dir");
|
||||||
|
let config_home = root.join("config");
|
||||||
|
fs::create_dir_all(&config_home).expect("config home dir");
|
||||||
|
std::env::set_var("CLAW_CONFIG_HOME", &config_home);
|
||||||
|
std::env::remove_var("ANTHROPIC_MODEL");
|
||||||
|
std::env::set_var("ANTHROPIC_MODEL", "sonnet");
|
||||||
|
|
||||||
|
let resolved =
|
||||||
|
with_current_dir(&root, || resolve_repl_model(DEFAULT_MODEL.to_string()));
|
||||||
|
|
||||||
|
assert_eq!(resolved, "claude-sonnet-4-6");
|
||||||
|
|
||||||
|
std::env::remove_var("ANTHROPIC_MODEL");
|
||||||
|
std::env::remove_var("CLAW_CONFIG_HOME");
|
||||||
|
fs::remove_dir_all(root).expect("cleanup temp dir");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_repl_model_returns_default_when_env_unset_and_no_config() {
|
||||||
|
let _guard = env_lock();
|
||||||
|
let root = temp_dir();
|
||||||
|
fs::create_dir_all(&root).expect("root dir");
|
||||||
|
let config_home = root.join("config");
|
||||||
|
fs::create_dir_all(&config_home).expect("config home dir");
|
||||||
|
std::env::set_var("CLAW_CONFIG_HOME", &config_home);
|
||||||
|
std::env::remove_var("ANTHROPIC_MODEL");
|
||||||
|
|
||||||
|
let resolved =
|
||||||
|
with_current_dir(&root, || resolve_repl_model(DEFAULT_MODEL.to_string()));
|
||||||
|
|
||||||
|
assert_eq!(resolved, DEFAULT_MODEL);
|
||||||
|
|
||||||
|
std::env::remove_var("CLAW_CONFIG_HOME");
|
||||||
|
fs::remove_dir_all(root).expect("cleanup temp dir");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resume_supported_command_list_matches_expected_surface() {
|
fn resume_supported_command_list_matches_expected_surface() {
|
||||||
let names = resume_supported_slash_commands()
|
let names = resume_supported_slash_commands()
|
||||||
@@ -8972,144 +8919,6 @@ UU conflicted.rs",
|
|||||||
assert!(output.contains("raw 119"));
|
assert!(output.contains("raw 119"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const ENTITY_DASHES: &str = "---";
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn regroup_omc_agent_sections_inserts_headers_when_multiple_agent_types_present() {
|
|
||||||
// given
|
|
||||||
let raw = "Here is the multi-agent transcript:\n\
|
|
||||||
Explore: Located main.rs renderer\n\
|
|
||||||
Explore: Mapped tool result paths\n\
|
|
||||||
Implementation: Added section helper\n\
|
|
||||||
Implementation: Wired helper into format_generic_tool_result\n\
|
|
||||||
Verification: Ran cargo check";
|
|
||||||
let explore_header = format!("{ENTITY_DASHES} Explore {ENTITY_DASHES}");
|
|
||||||
let impl_header = format!("{ENTITY_DASHES} Implementation {ENTITY_DASHES}");
|
|
||||||
let verify_header = format!("{ENTITY_DASHES} Verification {ENTITY_DASHES}");
|
|
||||||
|
|
||||||
// when
|
|
||||||
let grouped = regroup_omc_agent_sections(raw).expect("multi-agent text should regroup");
|
|
||||||
|
|
||||||
// then
|
|
||||||
assert!(grouped.contains("Here is the multi-agent transcript:"));
|
|
||||||
assert!(grouped.contains(&explore_header), "{grouped}");
|
|
||||||
assert!(grouped.contains(&impl_header), "{grouped}");
|
|
||||||
assert!(grouped.contains(&verify_header), "{grouped}");
|
|
||||||
assert!(grouped.contains("Located main.rs renderer"));
|
|
||||||
assert!(grouped.contains("Wired helper into format_generic_tool_result"));
|
|
||||||
let explore_pos = grouped
|
|
||||||
.find(&explore_header)
|
|
||||||
.expect("Explore header should be present");
|
|
||||||
let impl_pos = grouped
|
|
||||||
.find(&impl_header)
|
|
||||||
.expect("Implementation header should be present");
|
|
||||||
let verify_pos = grouped
|
|
||||||
.find(&verify_header)
|
|
||||||
.expect("Verification header should be present");
|
|
||||||
assert!(
|
|
||||||
explore_pos < impl_pos && impl_pos < verify_pos,
|
|
||||||
"agent sections should preserve first-seen ordering: {grouped}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn regroup_omc_agent_sections_returns_none_when_only_single_agent_type_present() {
|
|
||||||
// given
|
|
||||||
let raw = "Explore: Found the renderer\nExplore: Found the test file";
|
|
||||||
|
|
||||||
// when
|
|
||||||
let grouped = regroup_omc_agent_sections(raw);
|
|
||||||
|
|
||||||
// then
|
|
||||||
assert!(
|
|
||||||
grouped.is_none(),
|
|
||||||
"single-agent transcript should not be regrouped: {grouped:?}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn regroup_omc_agent_sections_ignores_unrelated_colon_prefixes() {
|
|
||||||
// given
|
|
||||||
let raw = "Note: this is fine\nFooBar: still fine\nDetails: nothing to group";
|
|
||||||
|
|
||||||
// when
|
|
||||||
let grouped = regroup_omc_agent_sections(raw);
|
|
||||||
|
|
||||||
// then
|
|
||||||
assert!(
|
|
||||||
grouped.is_none(),
|
|
||||||
"non-agent prefixes should not trigger grouping: {grouped:?}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn format_tool_result_groups_agent_output_by_type_headers() {
|
|
||||||
// given
|
|
||||||
let output = "Explore: Mapped the OMC renderer surface\n\
|
|
||||||
Implementation: Added grouped section headers\n\
|
|
||||||
Implementation: Preserved truncation behavior\n\
|
|
||||||
Verification: Ran cargo check on rusty-claude-cli";
|
|
||||||
let explore_header = format!("{ENTITY_DASHES} Explore {ENTITY_DASHES}");
|
|
||||||
let impl_header = format!("{ENTITY_DASHES} Implementation {ENTITY_DASHES}");
|
|
||||||
let verify_header = format!("{ENTITY_DASHES} Verification {ENTITY_DASHES}");
|
|
||||||
|
|
||||||
// when
|
|
||||||
let rendered = format_tool_result("Agent", output, false);
|
|
||||||
|
|
||||||
// then
|
|
||||||
assert!(rendered.contains("Agent"));
|
|
||||||
assert!(rendered.contains(&explore_header), "{rendered}");
|
|
||||||
assert!(rendered.contains(&impl_header), "{rendered}");
|
|
||||||
assert!(rendered.contains(&verify_header), "{rendered}");
|
|
||||||
assert!(rendered.contains("Mapped the OMC renderer surface"));
|
|
||||||
assert!(rendered.contains("Preserved truncation behavior"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn push_output_block_groups_assistant_text_by_agent_type_for_display_only() {
|
|
||||||
// given
|
|
||||||
let mut out = Vec::new();
|
|
||||||
let mut events: Vec<AssistantEvent> = Vec::new();
|
|
||||||
let mut pending_tool = None;
|
|
||||||
let mut block_has_thinking_summary = false;
|
|
||||||
let assistant_text = "Explore: Found flat OMC transcript rendering\n\
|
|
||||||
Implementation: Added grouped output headers\n\
|
|
||||||
Verification: Added rendering coverage";
|
|
||||||
|
|
||||||
// when
|
|
||||||
push_output_block(
|
|
||||||
OutputContentBlock::Text {
|
|
||||||
text: assistant_text.to_string(),
|
|
||||||
},
|
|
||||||
&mut out,
|
|
||||||
&mut events,
|
|
||||||
&mut pending_tool,
|
|
||||||
false,
|
|
||||||
&mut block_has_thinking_summary,
|
|
||||||
)
|
|
||||||
.expect("text block should render");
|
|
||||||
|
|
||||||
// then
|
|
||||||
// After the shared markdown renderer expands the numeric character
|
|
||||||
// references in the section headers, the user-visible output contains
|
|
||||||
// the literal `--- Explore ---` form requested by the bug report.
|
|
||||||
let rendered = String::from_utf8(out).expect("utf8");
|
|
||||||
assert!(rendered.contains("--- Explore ---"), "{rendered}");
|
|
||||||
assert!(rendered.contains("--- Implementation ---"), "{rendered}");
|
|
||||||
assert!(rendered.contains("--- Verification ---"), "{rendered}");
|
|
||||||
assert!(
|
|
||||||
rendered.contains("Added grouped output headers"),
|
|
||||||
"{rendered}"
|
|
||||||
);
|
|
||||||
// The original, unmodified assistant text is still pushed onto the
|
|
||||||
// event stream so persisted sessions are byte-identical to the API
|
|
||||||
// response.
|
|
||||||
assert!(matches!(
|
|
||||||
events.as_slice(),
|
|
||||||
[AssistantEvent::TextDelta(text)] if text == assistant_text
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ultraplan_progress_lines_include_phase_step_and_elapsed_status() {
|
fn ultraplan_progress_lines_include_phase_step_and_elapsed_status() {
|
||||||
let snapshot = InternalPromptProgressState {
|
let snapshot = InternalPromptProgressState {
|
||||||
@@ -9361,87 +9170,6 @@ UU conflicted.rs",
|
|||||||
assert!(stderr.contains("https://example.test/oauth/authorize"));
|
assert!(stderr.contains("https://example.test/oauth/authorize"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn login_with_openai_base_url_emits_actionable_text_error() {
|
|
||||||
let mut stdout = Vec::new();
|
|
||||||
let mut stderr = Vec::new();
|
|
||||||
|
|
||||||
super::emit_openai_base_url_login_conflict(
|
|
||||||
CliOutputFormat::Text,
|
|
||||||
"https://proxy.example.test/v1",
|
|
||||||
&mut stdout,
|
|
||||||
&mut stderr,
|
|
||||||
)
|
|
||||||
.expect("conflict message should render");
|
|
||||||
|
|
||||||
assert!(stdout.is_empty());
|
|
||||||
let stderr = String::from_utf8(stderr).expect("utf8");
|
|
||||||
assert!(stderr.contains("error: claw login uses Anthropic OAuth"));
|
|
||||||
assert!(stderr.contains("OPENAI_BASE_URL"));
|
|
||||||
assert!(stderr.contains("https://proxy.example.test/v1"));
|
|
||||||
assert!(stderr.contains("ANTHROPIC_API_KEY"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn login_with_openai_base_url_json_output_emits_machine_readable_error() {
|
|
||||||
let mut stdout = Vec::new();
|
|
||||||
let mut stderr = Vec::new();
|
|
||||||
|
|
||||||
super::emit_openai_base_url_login_conflict(
|
|
||||||
CliOutputFormat::Json,
|
|
||||||
"https://proxy.example.test/v1",
|
|
||||||
&mut stdout,
|
|
||||||
&mut stderr,
|
|
||||||
)
|
|
||||||
.expect("conflict message should render");
|
|
||||||
|
|
||||||
let stdout = String::from_utf8(stdout).expect("utf8");
|
|
||||||
let payload: serde_json::Value =
|
|
||||||
serde_json::from_str(&stdout).expect("stdout should be valid json");
|
|
||||||
assert_eq!(payload["kind"], serde_json::json!("login_error"));
|
|
||||||
assert_eq!(payload["reason"], serde_json::json!("openai_base_url_set"));
|
|
||||||
assert_eq!(
|
|
||||||
payload["openai_base_url"],
|
|
||||||
serde_json::json!("https://proxy.example.test/v1")
|
|
||||||
);
|
|
||||||
assert!(payload["message"]
|
|
||||||
.as_str()
|
|
||||||
.expect("message string")
|
|
||||||
.contains("OPENAI_BASE_URL"));
|
|
||||||
assert!(payload["suggestion"]
|
|
||||||
.as_str()
|
|
||||||
.expect("suggestion string")
|
|
||||||
.contains("ANTHROPIC_API_KEY"));
|
|
||||||
|
|
||||||
let stderr = String::from_utf8(stderr).expect("utf8");
|
|
||||||
assert!(stderr.contains("error: claw login uses Anthropic OAuth"));
|
|
||||||
assert!(stderr.contains("ANTHROPIC_API_KEY"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn read_openai_base_url_override_reports_set_value_and_ignores_blank() {
|
|
||||||
let _guard = env_lock();
|
|
||||||
let original = std::env::var("OPENAI_BASE_URL").ok();
|
|
||||||
|
|
||||||
std::env::remove_var("OPENAI_BASE_URL");
|
|
||||||
let absent = super::read_openai_base_url_override();
|
|
||||||
|
|
||||||
std::env::set_var("OPENAI_BASE_URL", " ");
|
|
||||||
let blank = super::read_openai_base_url_override();
|
|
||||||
|
|
||||||
std::env::set_var("OPENAI_BASE_URL", "https://proxy.example.test/v1");
|
|
||||||
let present = super::read_openai_base_url_override();
|
|
||||||
|
|
||||||
match original {
|
|
||||||
Some(value) => std::env::set_var("OPENAI_BASE_URL", value),
|
|
||||||
None => std::env::remove_var("OPENAI_BASE_URL"),
|
|
||||||
}
|
|
||||||
|
|
||||||
assert!(absent.is_none());
|
|
||||||
assert!(blank.is_none());
|
|
||||||
assert_eq!(present.as_deref(), Some("https://proxy.example.test/v1"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn build_runtime_plugin_state_merges_plugin_hooks_into_runtime_features() {
|
fn build_runtime_plugin_state_merges_plugin_hooks_into_runtime_features() {
|
||||||
let config_home = temp_dir();
|
let config_home = temp_dir();
|
||||||
|
|||||||
Reference in New Issue
Block a user