chore: import upstream snapshot with attribution
FreeBSD Smoke / FreeBSD Smoke (x86_64) (push) Has been cancelled
CI / Quality Guardrails (push) Has been cancelled
CI / Build & Test (macos-latest) (push) Has been cancelled
CI / Build & Test (ubuntu-latest) (push) Has been cancelled
CI / Build & Test (windows-latest) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / PowerShell Syntax (push) Has been cancelled
CI / Windows Cross-Target Check (Linux) (push) Has been cancelled
FreeBSD Smoke / FreeBSD Smoke (x86_64) (push) Has been cancelled
CI / Quality Guardrails (push) Has been cancelled
CI / Build & Test (macos-latest) (push) Has been cancelled
CI / Build & Test (ubuntu-latest) (push) Has been cancelled
CI / Build & Test (windows-latest) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / PowerShell Syntax (push) Has been cancelled
CI / Windows Cross-Target Check (Linux) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
|
||||
|
||||
/// Map provider-side tool names to internal display names.
|
||||
/// Mirrors Registry::resolve_tool_name so TUI surfaces show friendly names.
|
||||
pub fn resolve_display_tool_name(name: &str) -> &str {
|
||||
match name {
|
||||
"communicate" => "swarm",
|
||||
"task" | "task_runner" => "subagent",
|
||||
"shell_exec" => "bash",
|
||||
"file_read" => "read",
|
||||
"file_write" => "write",
|
||||
"file_edit" => "edit",
|
||||
"file_glob" => "glob",
|
||||
"file_grep" => "grep",
|
||||
"todo_read" | "todo_write" | "todoread" | "todowrite" => "todo",
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn canonical_tool_name(name: &str) -> &str {
|
||||
match name {
|
||||
"communicate" => "swarm",
|
||||
"Write" => "write",
|
||||
"Edit" => "edit",
|
||||
"MultiEdit" => "multiedit",
|
||||
"Patch" => "patch",
|
||||
"ApplyPatch" => "apply_patch",
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_edit_tool_name(name: &str) -> bool {
|
||||
matches!(
|
||||
canonical_tool_name(name),
|
||||
"write" | "edit" | "multiedit" | "patch" | "apply_patch"
|
||||
)
|
||||
}
|
||||
|
||||
fn parse_nonzero_exit_code_line(line: &str) -> bool {
|
||||
let trimmed = line.trim();
|
||||
if let Some(rest) = trimmed.strip_prefix("Exit code:") {
|
||||
return rest
|
||||
.trim()
|
||||
.parse::<i32>()
|
||||
.map(|code| code != 0)
|
||||
.unwrap_or(false);
|
||||
}
|
||||
if let Some(rest) = trimmed.strip_prefix("--- Command finished with exit code:") {
|
||||
return rest
|
||||
.trim()
|
||||
.trim_end_matches('-')
|
||||
.trim()
|
||||
.parse::<i32>()
|
||||
.map(|code| code != 0)
|
||||
.unwrap_or(false);
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn display_prefix_by_width(s: &str, max_width: usize) -> &str {
|
||||
if max_width == 0 {
|
||||
return "";
|
||||
}
|
||||
let mut used = 0usize;
|
||||
let mut end = 0usize;
|
||||
for (idx, ch) in s.char_indices() {
|
||||
let cw = UnicodeWidthChar::width(ch).unwrap_or(0);
|
||||
if used + cw > max_width {
|
||||
break;
|
||||
}
|
||||
used += cw;
|
||||
end = idx + ch.len_utf8();
|
||||
}
|
||||
&s[..end]
|
||||
}
|
||||
|
||||
fn display_suffix_by_width(s: &str, max_width: usize) -> &str {
|
||||
if max_width == 0 {
|
||||
return "";
|
||||
}
|
||||
let mut used = 0usize;
|
||||
let mut start = s.len();
|
||||
for (idx, ch) in s.char_indices().rev() {
|
||||
let cw = UnicodeWidthChar::width(ch).unwrap_or(0);
|
||||
if used + cw > max_width {
|
||||
break;
|
||||
}
|
||||
used += cw;
|
||||
start = idx;
|
||||
}
|
||||
&s[start..]
|
||||
}
|
||||
|
||||
pub fn truncate_middle_display(s: &str, max_width: usize) -> String {
|
||||
if UnicodeWidthStr::width(s) <= max_width {
|
||||
return s.to_string();
|
||||
}
|
||||
if max_width == 0 {
|
||||
return String::new();
|
||||
}
|
||||
if max_width == 1 {
|
||||
return "…".to_string();
|
||||
}
|
||||
let remaining = max_width.saturating_sub(1);
|
||||
let head = remaining / 2 + remaining % 2;
|
||||
let tail = remaining / 2;
|
||||
format!(
|
||||
"{}…{}",
|
||||
display_prefix_by_width(s, head),
|
||||
display_suffix_by_width(s, tail)
|
||||
)
|
||||
}
|
||||
|
||||
fn normalize_backticked_identifier(text: &str) -> String {
|
||||
text.replace('`', "").trim().to_string()
|
||||
}
|
||||
|
||||
pub fn concise_tool_error_summary(content: &str) -> Option<String> {
|
||||
for raw_line in content.lines() {
|
||||
let line = raw_line.trim();
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let detail = line
|
||||
.strip_prefix("Error:")
|
||||
.or_else(|| line.strip_prefix("error:"))
|
||||
.or_else(|| line.strip_prefix("Failed:"))
|
||||
.map(str::trim);
|
||||
if let Some(detail) = detail {
|
||||
if let Some(field) = detail.strip_prefix("missing field ") {
|
||||
return Some(format!(
|
||||
"invalid input: missing {}",
|
||||
normalize_backticked_identifier(field)
|
||||
));
|
||||
}
|
||||
if detail.starts_with("invalid type") || detail.starts_with("unknown variant") {
|
||||
return Some(format!("invalid input: {}", detail));
|
||||
}
|
||||
if detail.contains("source metadata") && detail.contains("was for") {
|
||||
return Some("build source changed before reload".to_string());
|
||||
}
|
||||
if detail.starts_with("Refusing to publish") {
|
||||
return Some("reload refused: rebuild against current source".to_string());
|
||||
}
|
||||
return Some(format!("error: {}", truncate_middle_display(detail, 80)));
|
||||
}
|
||||
|
||||
if line.contains("Compile terminated by signal") {
|
||||
return Some(line.to_string());
|
||||
}
|
||||
if let Some(rest) = line.strip_prefix("Exit code:")
|
||||
&& let Ok(code) = rest.trim().parse::<i32>()
|
||||
&& code != 0
|
||||
{
|
||||
return Some(format!("exit {}", code));
|
||||
}
|
||||
if let Some(rest) = line.strip_prefix("--- Command finished with exit code:") {
|
||||
let code = rest.trim().trim_end_matches('-').trim();
|
||||
if code != "0" && !code.is_empty() {
|
||||
return Some(format!("exit {}", code));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn tool_output_looks_failed(content: &str) -> bool {
|
||||
let trimmed = content.trim();
|
||||
if trimmed.is_empty() {
|
||||
return false;
|
||||
}
|
||||
let lower = trimmed.to_ascii_lowercase();
|
||||
if concise_tool_error_summary(trimmed).is_some()
|
||||
|| lower.starts_with("error:")
|
||||
|| lower.starts_with("failed:")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
trimmed.lines().any(|line| {
|
||||
let line = line.trim();
|
||||
parse_nonzero_exit_code_line(line)
|
||||
|| line.eq_ignore_ascii_case("Status: failed")
|
||||
|| line.eq_ignore_ascii_case("failed to start")
|
||||
|| line.eq_ignore_ascii_case("terminated")
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn canonicalizes_edit_tool_names() {
|
||||
assert_eq!(canonical_tool_name("ApplyPatch"), "apply_patch");
|
||||
assert!(is_edit_tool_name("MultiEdit"));
|
||||
assert!(!is_edit_tool_name("read"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn summarizes_tool_errors() {
|
||||
assert_eq!(
|
||||
concise_tool_error_summary("Error: missing field `command`").as_deref(),
|
||||
Some("invalid input: missing command")
|
||||
);
|
||||
assert_eq!(
|
||||
concise_tool_error_summary("--- Command finished with exit code: 2 ---").as_deref(),
|
||||
Some("exit 2")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_failed_tool_output() {
|
||||
assert!(tool_output_looks_failed("Status: failed"));
|
||||
assert!(tool_output_looks_failed("Exit code: 1"));
|
||||
assert!(!tool_output_looks_failed("Exit code: 0"));
|
||||
assert!(!tool_output_looks_failed("completed successfully"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user