From 63a79d76ba31d8d7b9f47dd47010e3eab1afeb48 Mon Sep 17 00:00:00 2001 From: iris Date: Sun, 21 Jun 2026 21:50:53 +0200 Subject: [PATCH] agent-ui: collapse streamed thinking_tokens ticks into one live row MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- docs/terminal-rendering.md | 7 +++++++ frontend/packages/agent/src/app.js | 24 ++++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/docs/terminal-rendering.md b/docs/terminal-rendering.md index ed2946a3..58584e54 100644 --- a/docs/terminal-rendering.md +++ b/docs/terminal-rendering.md @@ -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[]`: diff --git a/frontend/packages/agent/src/app.js b/frontend/packages/agent/src/app.js index 26f3c074..cffc2085 100644 --- a/frontend/packages/agent/src/app.js +++ b/frontend/packages/agent/src/app.js @@ -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.