agent-ui: collapse streamed thinking_tokens ticks into one live row

claude streams a running 'estimated_tokens' counter as system/thinking_tokens
events — many per turn (thousands in a long turn). renderStream rendered each
as a '⚙ thinking_tokens' note, flooding the terminal scrollback. Coalesce
consecutive ticks into a single '🧠 thinking … ~N tokens' note row that
updates in place; reuse the row only while it's still the last one rendered
(nextElementSibling == null) so any other event starts a fresh one. Closes
#1818.
This commit is contained in:
iris 2026-06-21 21:50:53 +02:00
commit 63a79d76ba
2 changed files with 31 additions and 0 deletions

View file

@ -71,6 +71,13 @@ suffix, so the terminal degrades cleanly against older event shapes.
1. Drops `system/init`, `rate_limit_event`, `result` (noise /
handled elsewhere — `result` powers the `cost` badge).
1a. `system/thinking_tokens` (claude streams a running
`estimated_tokens` counter while thinking — many per turn) →
collapses into a **single** `🧠 thinking … ~N tokens` `.note`
row that updates in place. Consecutive ticks reuse the row only
while it's still the last one rendered (`nextElementSibling ==
null`); any other event after it makes the next tick start a
fresh row. Avoids a note-per-tick scrollback flood.
2. `subtype == "task_started" | "task_notification"`
`renderTaskEvent` (subagent activity gets the `⌁` glyph).
3. `type == "assistant"` → walk `message.content[]`:

View file

@ -1695,6 +1695,12 @@ window.marked = marked;
}
return false;
}
// Coalescing target for the live `thinking_tokens` counter.
// Holds the row currently showing the running estimate so consecutive
// ticks update in place instead of appending a note each. Reset
// implicitly: we only reuse it while it's still the last row rendered
// (see the nextElementSibling check), so any other event starts fresh.
let thinkingTokensRow = null;
function renderStream(v, api) {
// Drop claude's result line and rate-limit — noise. TurnEnd
// communicates pass/fail; rate-limit events are noisy status chatter.
@ -1722,6 +1728,24 @@ window.marked = marked;
api.row('note stderr', '✗ api error · ' + msg);
return;
}
// Live thinking-token counter — claude streams many of these per
// turn (a running `estimated_tokens` total while it thinks). Collapse
// consecutive ticks into ONE in-place-updating row instead of a note
// per tick. Reuse our row only while it's still the last one
// in the scrollback; once any other event renders after it, the next
// tick starts a fresh row.
if (v.subtype === 'thinking_tokens') {
const n = v.estimated_tokens;
const label = '🧠 thinking … '
+ (n != null ? '~' + Number(n).toLocaleString() + ' tokens' : '');
if (thinkingTokensRow && thinkingTokensRow.isConnected
&& thinkingTokensRow.nextElementSibling === null) {
thinkingTokensRow.textContent = label;
} else {
thinkingTokensRow = api.row('note', label);
}
return;
}
// Other system subtypes (context_window_exceeded, etc.) — render a
// muted note with the subtype label; reserve the loud orange `sys`
// catch-all for truly unrecognised top-level types.