fix(agent): strip tool_use_error tags and render errors in red

When a tool call fails, claude wraps the result text in
<tool_use_error>...</tool_use_error> XML tags. The terminal was
displaying these raw, producing output like:
  '<tool_use_error>File has not been read yet.</tool_use_error>'

Fix renderToolResult in app.js:
- Check c.is_error on the tool_result content block.
- Strip the <tool_use_error>...</tool_use_error> wrapper from the text.
- Render error results with a '✗' prefix under '.tool-result.error'
  (flat, ≤120c) or '.tool-result-block.error' (<details>, longer text).

Add .live .tool-result.error { color: var(--red); } to terminal.css
so error results are visually distinct (red, same as turn-end-fail).

Update terminal-rendering.md row taxonomy to document the two new
error row classes.

Closes #2104.
This commit is contained in:
iris 2026-07-01 17:06:12 +02:00 committed by mara
commit 57f936af63
3 changed files with 20 additions and 2 deletions

View file

@ -63,6 +63,8 @@ parent's negative pull.
| `.tool-result` (flat) | `← <txt>` | muted | short `tool_result` (≤120c, non-recv) | stream-json |
| `.tool-result-block` `<details>` | `Nl · headline` | muted, body is text | long generic `tool_result` | stream-json |
| `.tool-result-block` `<details open>` | `recv ← <txt>` | muted, body is markdown | `tool_result` correlated to a prior `recv` tool_use via id | stream-json |
| `.tool-result.error` (flat) | `✗ <msg>` | red | `tool_result` with `is_error: true` (≤120c); `<tool_use_error>` wrapper stripped | stream-json |
| `.tool-result-block.error` `<details>` | `Nl · headline` | red, body is text | long error `tool_result` (`is_error: true`); wrapper stripped | stream-json |
| `.tool-use` | `⌁ task <id> started · <desc> [type]` | cyan | claude Task-tool subagent start (dead path — `Task` omitted from agent allow-list) | `renderTaskEvent` |
| `.turn-end-ok` / `.turn-end-fail` / `.tool-result` | `⌁ task <id> ✓/✗/◌ <status> · <desc> · → <output_file>` | green / red / muted | claude Task-tool result (dead path for agents) | `renderTaskEvent` |
| `.note` | `· <text>` | muted | harness chatter | `LiveEvent::Note` |

View file

@ -1684,9 +1684,16 @@ window.marked = marked;
// grows with the session — entries are tiny strings.
const toolNameById = new Map();
function renderToolResult(c, api) {
const txt = Array.isArray(c.content)
const rawTxt = Array.isArray(c.content)
? c.content.map(p => p.text || '').join('')
: (c.content || '');
// Strip the <tool_use_error>…</tool_use_error> wrapper that claude
// emits when a tool call fails — the tags are an implementation detail
// and add noise to the terminal display.
const isError = !!c.is_error;
const txt = isError
? rawTxt.replace(/^<tool_use_error>([\s\S]*)<\/tool_use_error>$/, '$1').trim()
: rawTxt;
const sourceName = c.tool_use_id ? toolNameById.get(c.tool_use_id) : null;
const isMessageBearing = sourceName === 'mcp__hyperhive__recv';
// When an ask's tool_result lands the broker has just
@ -1707,6 +1714,14 @@ window.marked = marked;
const headline = trimmed.slice(0, 90) + '…';
return `${lines}L · ${headline}`;
})();
if (isError) {
if (!txt.trim() || txt.length <= 120) {
api.row('tool-result error', '✗ ' + summaryBody);
} else {
api.details('tool-result-block error', summaryBody, txt);
}
return;
}
// Flat row: keep the `←` glyph in the prefix column. Details rows
// drop it — the `▸/▾` disclosure marker sits in that column via CSS.
if (isMessageBearing && txt.trim()) {

View file

@ -120,7 +120,8 @@
.live .text { color: var(--fg); }
.live .thinking { color: var(--muted); font-style: italic; }
.live .tool-use { color: var(--cyan); }
.live .tool-result { color: var(--muted); }
.live .tool-result { color: var(--muted); }
.live .tool-result.error { color: var(--red); }
.live .result { color: var(--green); }
.live .note { color: var(--muted); }
/* Distinguish stderr lines (orange) and operator-initiated notes