fix(tui): compact tool-call UI and context

This commit is contained in:
Hunter Bown
2026-06-01 16:55:10 -07:00
parent c52769e5f5
commit 57c10c78d6
7 changed files with 504 additions and 15 deletions
+184
View File
@@ -8,6 +8,7 @@ use crate::compaction::estimate_tokens;
use crate::error_taxonomy::ErrorCategory;
use crate::models::{Message, SystemPrompt, context_window_for_model};
use crate::tools::spec::ToolResult;
use serde_json::Value;
/// Max output tokens requested for normal agent turns. Generous on purpose:
/// V4 thinking models can produce tens of thousands of reasoning tokens on
@@ -126,6 +127,12 @@ fn tool_result_is_noisy(tool_name: &str) -> bool {
"exec_shell"
| "exec_shell_wait"
| "exec_shell_interact"
| "exec_shell_cancel"
| "task_shell_start"
| "task_shell_wait"
| "run_tests"
| "run_verifiers"
| "task_gate_run"
| "multi_tool_use.parallel"
| "web_search"
)
@@ -259,6 +266,179 @@ fn compact_subagent_tool_result_for_context(tool_name: &str, raw: &str) -> Optio
Some(out.trim_end().to_string())
}
fn json_text<'a>(value: &'a Value, key: &str) -> Option<&'a str> {
value
.get(key)
.and_then(Value::as_str)
.map(str::trim)
.filter(|s| !s.is_empty())
}
fn json_number_text(value: &Value, key: &str) -> Option<String> {
value
.get(key)
.and_then(|value| {
value
.as_i64()
.map(|n| n.to_string())
.or_else(|| value.as_u64().map(|n| n.to_string()))
})
.or_else(|| {
value
.get(key)
.and_then(Value::as_str)
.map(str::trim)
.filter(|s| !s.is_empty())
.map(ToString::to_string)
})
}
fn compact_run_tests_result_for_context(raw: &str) -> Option<String> {
let parsed: Value = serde_json::from_str(raw).ok()?;
let success = parsed.get("success")?.as_bool()?;
let exit_code = json_number_text(&parsed, "exit_code").unwrap_or_else(|| "?".to_string());
let command = json_text(&parsed, "command").unwrap_or("(unknown command)");
let stdout = json_text(&parsed, "stdout");
let stderr = json_text(&parsed, "stderr");
let stream_limit = if success { 500 } else { 1_000 };
let mut lines = vec![
"[run_tests result summarized for context]".to_string(),
format!(
"status: {}, exit_code: {exit_code}",
if success { "passed" } else { "failed" }
),
format!("command: {}", summarize_text(command, 300)),
];
if let Some(stderr) = stderr {
lines.push(format!(
"stderr: {}",
summarize_text_head_tail(stderr, stream_limit)
));
}
if let Some(stdout) = stdout {
lines.push(format!(
"stdout: {}",
summarize_text_head_tail(stdout, stream_limit)
));
}
Some(lines.join("\n"))
}
fn run_verifier_status_rank(status: Option<&str>) -> u8 {
match status.unwrap_or_default() {
"failed" | "timeout" => 0,
"skipped" => 1,
"passed" => 2,
_ => 3,
}
}
fn compact_run_verifiers_result_for_context(raw: &str) -> Option<String> {
let parsed: Value = serde_json::from_str(raw).ok()?;
let gates = parsed.get("gates")?.as_array()?;
let summary = json_text(&parsed, "summary")
.map(ToString::to_string)
.unwrap_or_else(|| {
let passed = json_number_text(&parsed, "passed").unwrap_or_else(|| "?".to_string());
let failed = json_number_text(&parsed, "failed").unwrap_or_else(|| "?".to_string());
let skipped = json_number_text(&parsed, "skipped").unwrap_or_else(|| "?".to_string());
format!("{passed} passed, {failed} failed, {skipped} skipped")
});
let mut ordered: Vec<&Value> = gates.iter().collect();
ordered.sort_by(|a, b| {
run_verifier_status_rank(json_text(a, "status"))
.cmp(&run_verifier_status_rank(json_text(b, "status")))
.then_with(|| json_text(a, "name").cmp(&json_text(b, "name")))
});
let mut lines = vec![
"[run_verifiers result summarized for context]".to_string(),
format!("summary: {summary}"),
];
let profile = json_text(&parsed, "profile");
let level = json_text(&parsed, "level");
if profile.is_some() || level.is_some() {
lines.push(format!(
"selection: profile={}, level={}",
profile.unwrap_or("?"),
level.unwrap_or("?")
));
}
for (idx, gate) in ordered.iter().enumerate() {
if idx >= 12 {
lines.push(format!(
"- ... {} more gate(s) omitted from context summary",
ordered.len().saturating_sub(idx)
));
break;
}
let name = json_text(gate, "name").unwrap_or("gate");
let ecosystem = json_text(gate, "ecosystem").unwrap_or("unknown");
let status = json_text(gate, "status").unwrap_or("unknown");
let exit = json_number_text(gate, "exit_code")
.map(|code| format!(" exit={code}"))
.unwrap_or_default();
lines.push(format!("- {name} ({ecosystem}): {status}{exit}"));
if status != "passed" {
if let Some(command) = json_text(gate, "command") {
lines.push(format!(" command: {}", summarize_text(command, 240)));
}
if let Some(detail) = json_text(gate, "skipped_reason")
.or_else(|| json_text(gate, "stderr"))
.or_else(|| json_text(gate, "stdout"))
{
lines.push(format!(
" detail: {}",
summarize_text_head_tail(detail, 600)
));
}
}
}
Some(lines.join("\n"))
}
fn compact_task_gate_run_result_for_context(raw: &str) -> Option<String> {
let parsed: Value = serde_json::from_str(raw).ok()?;
let gate = parsed.get("gate")?;
let gate_name = json_text(gate, "gate").unwrap_or("gate");
let status = json_text(gate, "status").unwrap_or("unknown");
let command = json_text(gate, "command").unwrap_or("(unknown command)");
let summary = json_text(gate, "summary")
.or_else(|| json_text(&parsed, "stderr_summary"))
.or_else(|| json_text(&parsed, "stdout_summary"));
let exit = json_number_text(gate, "exit_code")
.map(|code| format!(", exit_code: {code}"))
.unwrap_or_default();
let mut lines = vec![
"[task_gate_run result summarized for context]".to_string(),
format!("gate: {gate_name}, status: {status}{exit}"),
format!("command: {}", summarize_text(command, 300)),
];
if let Some(summary) = summary {
lines.push(format!("summary: {}", summarize_text(summary, 800)));
}
if let Some(log_path) = json_text(gate, "log_path") {
lines.push(format!("log_path: {log_path}"));
}
Some(lines.join("\n"))
}
fn compact_structured_tool_result_for_context(tool_name: &str, raw: &str) -> Option<String> {
match tool_name {
"run_tests" => compact_run_tests_result_for_context(raw),
"run_verifiers" => compact_run_verifiers_result_for_context(raw),
"task_gate_run" => compact_task_gate_run_result_for_context(raw),
_ => None,
}
}
fn tool_result_context_limits_for_model(model: &str) -> ToolResultContextLimits {
let is_large_context =
context_window_for_model(model).is_some_and(|window| window >= LARGE_CONTEXT_WINDOW_TOKENS);
@@ -292,6 +472,10 @@ pub(crate) fn compact_tool_result_for_context(
return summary;
}
if let Some(summary) = compact_structured_tool_result_for_context(tool_name, raw) {
return summary;
}
let limits = tool_result_context_limits_for_model(model);
let raw_chars = raw.chars().count();
let should_compact = raw_chars > limits.hard_limit_chars
+137
View File
@@ -1524,6 +1524,143 @@ fn subagent_results_are_summarized_before_parent_context_insertion() {
assert!(context.contains("handle_read"));
}
#[test]
fn run_verifiers_results_are_structured_before_context_insertion() {
let noisy_failure = "node lint failure detail\n".repeat(300);
let noisy_success = "successful check output\n".repeat(300);
let output = ToolResult::success(
json!({
"success": false,
"profile": "auto",
"level": "quick",
"workspace": "/repo",
"gate_count": 3,
"passed": 1,
"failed": 1,
"skipped": 1,
"summary": "1 passed, 1 failed, 1 skipped",
"gates": [
{
"name": "rust-check",
"ecosystem": "rust",
"status": "passed",
"command": "cargo check --workspace --locked",
"cwd": "/repo",
"exit_code": 0,
"duration_ms": 110,
"stdout": noisy_success.clone(),
"stderr": "",
"stdout_truncated": false,
"stderr_truncated": false,
"skipped_reason": null
},
{
"name": "node-lint",
"ecosystem": "node",
"status": "failed",
"command": "npm run lint",
"cwd": "/repo",
"exit_code": 1,
"duration_ms": 220,
"stdout": "",
"stderr": noisy_failure,
"stdout_truncated": false,
"stderr_truncated": false,
"skipped_reason": null
},
{
"name": "python-pytest",
"ecosystem": "python",
"status": "skipped",
"command": "",
"cwd": "/repo",
"exit_code": null,
"duration_ms": 0,
"stdout": "",
"stderr": "",
"stdout_truncated": false,
"stderr_truncated": false,
"skipped_reason": "pytest is not installed"
}
]
})
.to_string(),
);
let context = compact_tool_result_for_context("deepseek-v4-pro", "run_verifiers", &output);
assert!(context.contains("[run_verifiers result summarized for context]"));
assert!(context.contains("summary: 1 passed, 1 failed, 1 skipped"));
assert!(context.contains("selection: profile=auto, level=quick"));
assert!(context.contains("- node-lint (node): failed exit=1"));
assert!(context.contains("command: npm run lint"));
assert!(context.contains("- python-pytest (python): skipped"));
assert!(context.contains("pytest is not installed"));
assert!(context.contains("- rust-check (rust): passed exit=0"));
assert!(context.len() < output.content.len());
assert!(
!context.contains(&noisy_success),
"successful gate stdout should not be copied into parent context"
);
}
#[test]
fn run_tests_results_are_structured_before_context_insertion() {
let stdout = "running test suite\n".repeat(500);
let stderr = "error[E0425]: cannot find value `missing`\n".repeat(500);
let output = ToolResult::success(
json!({
"success": false,
"exit_code": 101,
"stdout": stdout,
"stderr": stderr,
"command": "(cd /repo && cargo test --workspace --all-features)"
})
.to_string(),
);
let context = compact_tool_result_for_context("deepseek-v4-pro", "run_tests", &output);
assert!(context.contains("[run_tests result summarized for context]"));
assert!(context.contains("status: failed, exit_code: 101"));
assert!(context.contains("cargo test --workspace --all-features"));
assert!(context.contains("error[E0425]"));
assert!(context.contains("running test suite"));
assert!(context.len() < output.content.len());
}
#[test]
fn task_gate_run_results_are_structured_before_context_insertion() {
let output = ToolResult::success(
json!({
"gate": {
"id": "gate_abcd1234",
"gate": "clippy",
"command": "cargo clippy -p codewhale-tui --all-targets --all-features --locked -- -D warnings",
"cwd": "/repo",
"exit_code": 1,
"status": "failed",
"classification": "compile_failure",
"duration_ms": 5000,
"summary": "warning promoted to error in verifier.rs",
"log_path": "/repo/.codewhale/runtime/gate.log",
"recorded_at": "2026-06-01T12:00:00Z"
},
"stdout_summary": "",
"stderr_summary": "warning promoted to error"
})
.to_string(),
);
let context = compact_tool_result_for_context("deepseek-v4-pro", "task_gate_run", &output);
assert!(context.contains("[task_gate_run result summarized for context]"));
assert!(context.contains("gate: clippy, status: failed, exit_code: 1"));
assert!(context.contains("cargo clippy -p codewhale-tui"));
assert!(context.contains("summary: warning promoted to error"));
assert!(context.contains("log_path: /repo/.codewhale/runtime/gate.log"));
}
#[test]
fn refresh_system_prompt_leaves_working_set_out_of_system_prompt() {
let tmp = tempdir().expect("tempdir");
+6 -1
View File
@@ -17,6 +17,7 @@ use crate::tui::ui::{
status_color,
};
use crate::tui::ui_text::{concise_shell_command_label, truncate_line_to_width};
use crate::tui::widgets::tool_card::tool_activity_label_for_name;
use crate::tui::widgets::{FooterProps, FooterToast, FooterWidget, Renderable};
use crate::tui::workspace_context;
@@ -399,7 +400,11 @@ fn collect_active_tool_status(cell: &HistoryCell, snapshot: &mut ActiveToolStatu
if matches!(generic.name.as_str(), "agent_open" | "agent_spawn") {
return;
}
snapshot.record(format!("tool {}", generic.name), generic.status, None);
snapshot.record(
tool_activity_label_for_name(&generic.name),
generic.status,
None,
);
}
}
}
+97 -6
View File
@@ -1279,6 +1279,16 @@ pub struct GenericToolCell {
pub is_diff: bool,
}
fn should_show_raw_tool_name(
name: &str,
family: crate::tui::widgets::tool_card::ToolFamily,
mode: RenderMode,
) -> bool {
matches!(mode, RenderMode::Transcript)
|| matches!(family, crate::tui::widgets::tool_card::ToolFamily::Generic)
|| name.starts_with("mcp_")
}
impl GenericToolCell {
/// Render the generic tool cell into lines.
///
@@ -1329,12 +1339,14 @@ impl GenericToolCell {
None,
low_motion,
));
lines.extend(render_compact_kv(
"name",
&self.name,
tool_value_style(),
width,
));
if should_show_raw_tool_name(&self.name, family, mode) {
lines.extend(render_compact_kv(
"name",
&self.name,
tool_value_style(),
width,
));
}
// Prefer per-prompt rows over the generic args summary when the tool
// exposes a list of child prompts. One row per child with a `[i]`
@@ -1878,6 +1890,18 @@ pub fn summarize_tool_args(input: &Value) -> Option<String> {
summarize_inline_value(value, 40, false)
));
}
if let Some(value) = obj.get("profile") {
parts.push(format!(
"profile: {}",
summarize_inline_value(value, 40, false)
));
}
if let Some(value) = obj.get("level") {
parts.push(format!(
"level: {}",
summarize_inline_value(value, 40, false)
));
}
if let Some(value) = obj.get("file_id") {
parts.push(format!(
"file_id: {}",
@@ -4792,6 +4816,73 @@ mod tests {
assert!(text.contains("query: foo"));
}
#[test]
fn known_generic_tool_hides_raw_name_in_live_mode() {
let cell = HistoryCell::Tool(ToolCell::Generic(GenericToolCell {
name: "run_verifiers".to_string(),
status: ToolStatus::Running,
input_summary: Some("profile: auto, level: quick".to_string()),
output: None,
prompts: None,
spillover_path: None,
output_summary: None,
is_diff: false,
}));
let text = lines_text(&cell.lines(80));
assert!(text.contains("verify running"), "{text}");
assert!(text.contains("profile: auto"), "{text}");
assert!(
!text.contains("name: run_verifiers"),
"live card should not spend a row on internal tool id: {text}"
);
assert!(
!text.contains("run_verifiers"),
"known tool id should not leak into compact live card: {text}"
);
}
#[test]
fn known_generic_tool_keeps_raw_name_in_transcript_mode() {
let cell = HistoryCell::Tool(ToolCell::Generic(GenericToolCell {
name: "run_verifiers".to_string(),
status: ToolStatus::Running,
input_summary: Some("profile: auto, level: quick".to_string()),
output: None,
prompts: None,
spillover_path: None,
output_summary: None,
is_diff: false,
}));
let text = lines_text(&cell.transcript_lines(80));
assert!(text.contains("verify running"), "{text}");
assert!(
text.contains("name: run_verifiers"),
"transcript replay should preserve exact tool id: {text}"
);
}
#[test]
fn unknown_generic_tool_keeps_raw_name_in_live_mode() {
let cell = HistoryCell::Tool(ToolCell::Generic(GenericToolCell {
name: "future_private_tool".to_string(),
status: ToolStatus::Running,
input_summary: Some("query: foo".to_string()),
output: None,
prompts: None,
spillover_path: None,
output_summary: None,
is_diff: false,
}));
let text = lines_text(&cell.lines(80));
assert!(
text.contains("name: future_private_tool"),
"unknown tools should remain identifiable: {text}"
);
}
#[test]
fn generic_tool_cell_preserves_multi_line_output_in_transcript() {
// Repro for #80: a `git diff --stat`-shaped tool result should keep
+10 -1
View File
@@ -4407,6 +4407,10 @@ async fn tool_result_content_for_api_message(
return String::new();
}
if matches!(name, "run_tests" | "run_verifiers" | "task_gate_run") {
return crate::core::engine::compact_tool_result_for_context(&app.model, name, output);
}
if raw.chars().count() > crate::tool_output_receipts::RAW_TOOL_OUTPUT_RECEIPT_THRESHOLD_CHARS {
let messages = live_tool_receipt_messages(app, id, raw, output.success);
let artifacts = app.session_artifacts.clone();
@@ -8327,6 +8331,9 @@ fn activity_cell_label(app: &App, cell_index: usize, cell: &HistoryCell) -> Stri
HistoryCell::Thinking { .. } => "thinking".to_string(),
HistoryCell::Error { .. } => "error".to_string(),
HistoryCell::SubAgent(_) => "sub-agent".to_string(),
HistoryCell::Tool(ToolCell::Generic(generic)) => {
crate::tui::widgets::tool_card::tool_activity_label_for_name(&generic.name)
}
HistoryCell::Tool(_) => {
detail_target_label(app, cell_index).unwrap_or_else(|| "tool activity".to_string())
}
@@ -8752,7 +8759,9 @@ pub(crate) fn detail_target_label(app: &App, cell_index: usize) -> Option<String
Some(format!("image {}", image.path.display()))
}
HistoryCell::Tool(ToolCell::WebSearch(search)) => Some(format!("search {}", search.query)),
HistoryCell::Tool(ToolCell::Generic(generic)) => Some(format!("tool {}", generic.name)),
HistoryCell::Tool(ToolCell::Generic(generic)) => {
Some(crate::tui::widgets::tool_card::tool_activity_label_for_name(&generic.name))
}
HistoryCell::SubAgent(_) => Some("sub-agent".to_string()),
_ => None,
}
+2 -1
View File
@@ -1918,7 +1918,8 @@ fn active_tool_status_label_counts_foreground_rlm_work() {
let label = active_tool_status_label(&app).expect("status label");
assert!(label.contains("tool rlm"), "label: {label}");
assert!(label.contains("rlm"), "label: {label}");
assert!(!label.contains("tool rlm"), "label: {label}");
assert!(label.contains("1 active"), "label: {label}");
}
+68 -6
View File
@@ -8,7 +8,7 @@
//!
//! This module owns:
//!
//! - [`ToolFamily`] — the seven canonical families plus a `Generic`
//! - [`ToolFamily`] — the canonical semantic families plus a `Generic`
//! fallback for anything we don't have a family for yet.
//! - [`tool_family_for_title`] — maps the legacy `render_tool_header` title
//! string (`"Shell"`, `"Patch"`, `"Workspace"`, etc.) to a family. Lets
@@ -41,6 +41,8 @@ pub enum ToolFamily {
Fanout,
/// Recursive language model work. `⋮⋮ rlm`.
Rlm,
/// Verification gates, tests, and validators. `✓ verify`.
Verify,
/// Reasoning / chain-of-thought. `… think`. Reasoning has its own
/// render path (`render_thinking` in `history.rs`); the family is
/// declared here for completeness so any future code that reaches for
@@ -77,16 +79,46 @@ pub fn tool_family_for_name(name: &str) -> ToolFamily {
match name {
"read_file" | "list_dir" | "view_image" => ToolFamily::Read,
"edit_file" | "apply_patch" | "write_file" => ToolFamily::Patch,
"exec_shell" | "exec_shell_wait" | "exec_shell_interact" => ToolFamily::Run,
"exec_shell"
| "exec_shell_wait"
| "exec_shell_interact"
| "exec_shell_cancel"
| "task_shell_start"
| "task_shell_wait" => ToolFamily::Run,
"grep_files" | "file_search" | "web_search" | "fetch_url" => ToolFamily::Find,
"agent_open" | "agent_eval" | "agent_close" | "agent_spawn" | "tool_agent" => {
ToolFamily::Delegate
}
"rlm_open" | "rlm_eval" | "rlm_configure" | "rlm_close" | "rlm" => ToolFamily::Rlm,
"run_tests" | "run_verifiers" | "task_gate_run" | "validate_data" => ToolFamily::Verify,
_ => ToolFamily::Generic,
}
}
/// User-facing label for an arbitrary tool name. Known tools collapse to the
/// semantic verb; unknown tools keep their exact name for debugging.
#[must_use]
pub fn tool_display_label_for_name(name: &str) -> String {
let family = tool_family_for_name(name);
if matches!(family, ToolFamily::Generic) {
name.to_string()
} else {
family_label(family).to_string()
}
}
/// Compact activity/status label for arbitrary tool names. Known built-ins use
/// the semantic verb; unknown tools keep the `tool NAME` form.
#[must_use]
pub fn tool_activity_label_for_name(name: &str) -> String {
let family = tool_family_for_name(name);
if matches!(family, ToolFamily::Generic) {
format!("tool {name}")
} else {
tool_display_label_for_name(name)
}
}
/// Build a compact semantic summary for a tool header from the public tool
/// name and the already-sanitized argument summary.
#[must_use]
@@ -103,6 +135,7 @@ pub fn tool_header_summary_for_name(name: &str, input_summary: Option<&str>) ->
ToolFamily::Delegate | ToolFamily::Fanout | ToolFamily::Rlm => {
["prompt", "task", "model"].as_slice()
}
ToolFamily::Verify => ["profile", "level", "command", "args", "path"].as_slice(),
ToolFamily::Think | ToolFamily::Generic => {
["query", "path", "command", "prompt"].as_slice()
}
@@ -144,8 +177,9 @@ pub fn family_glyph(family: ToolFamily) -> &'static str {
ToolFamily::Delegate => "\u{25D0}", // ◐
ToolFamily::Fanout => "\u{22EE}\u{22EE}", // ⋮⋮ (two cells)
ToolFamily::Rlm => "\u{22EE}\u{22EE}", // ⋮⋮ (two cells)
ToolFamily::Think => "\u{2026}", // …
ToolFamily::Generic => "\u{2022}", //
ToolFamily::Verify => "\u{2713}",
ToolFamily::Think => "\u{2026}", //
ToolFamily::Generic => "\u{2022}", // •
}
}
@@ -162,6 +196,7 @@ pub fn family_label(family: ToolFamily) -> &'static str {
ToolFamily::Delegate => "delegate",
ToolFamily::Fanout => "fanout",
ToolFamily::Rlm => "rlm",
ToolFamily::Verify => "verify",
ToolFamily::Think => "think",
ToolFamily::Generic => "tool",
}
@@ -198,8 +233,9 @@ pub fn rail_glyph(rail: CardRail) -> &'static str {
#[cfg(test)]
mod tests {
use super::{
CardRail, ToolFamily, family_glyph, family_label, rail_glyph, tool_family_for_name,
tool_family_for_title, tool_header_summary_for_name,
CardRail, ToolFamily, family_glyph, family_label, rail_glyph, tool_activity_label_for_name,
tool_display_label_for_name, tool_family_for_name, tool_family_for_title,
tool_header_summary_for_name,
};
#[test]
@@ -218,15 +254,35 @@ mod tests {
assert_eq!(tool_family_for_name("read_file"), ToolFamily::Read);
assert_eq!(tool_family_for_name("apply_patch"), ToolFamily::Patch);
assert_eq!(tool_family_for_name("exec_shell"), ToolFamily::Run);
assert_eq!(tool_family_for_name("task_shell_start"), ToolFamily::Run);
assert_eq!(tool_family_for_name("grep_files"), ToolFamily::Find);
assert_eq!(tool_family_for_name("agent_open"), ToolFamily::Delegate);
assert_eq!(tool_family_for_name("rlm_eval"), ToolFamily::Rlm);
assert_eq!(tool_family_for_name("run_verifiers"), ToolFamily::Verify);
assert_eq!(
tool_family_for_name("totally_new_tool"),
ToolFamily::Generic
);
}
#[test]
fn tool_display_label_collapses_known_tools_to_user_verbs() {
assert_eq!(tool_display_label_for_name("exec_shell"), "run");
assert_eq!(tool_display_label_for_name("run_verifiers"), "verify");
assert_eq!(tool_display_label_for_name("file_search"), "find");
assert_eq!(
tool_display_label_for_name("future_private_tool"),
"future_private_tool"
);
assert_eq!(tool_activity_label_for_name("exec_shell"), "run");
assert_eq!(tool_activity_label_for_name("run_verifiers"), "verify");
assert_eq!(
tool_activity_label_for_name("future_private_tool"),
"tool future_private_tool"
);
}
#[test]
fn tool_header_summary_prefers_family_specific_arguments() {
assert_eq!(
@@ -244,6 +300,11 @@ mod tests {
.as_deref(),
Some("TODO")
);
assert_eq!(
tool_header_summary_for_name("run_verifiers", Some("profile: auto, level: quick"))
.as_deref(),
Some("auto")
);
assert_eq!(
tool_header_summary_for_name("unknown", Some("alpha: beta")).as_deref(),
Some("alpha: beta")
@@ -261,6 +322,7 @@ mod tests {
ToolFamily::Delegate,
ToolFamily::Fanout,
ToolFamily::Rlm,
ToolFamily::Verify,
ToolFamily::Think,
ToolFamily::Generic,
] {