From a99508719e8151e47e6934c27271b725d2495867 Mon Sep 17 00:00:00 2001 From: iris Date: Sat, 11 Jul 2026 20:30:38 +0200 Subject: [PATCH] refactor: extract makeCoalescer helper for last-row update-in-place pattern thinkingTokensRow/statusRow/pluginInstallRow were three copy-pasted module-level row/text pairs implementing the same 'collapse repeated ticks into one updating row' pattern. Extracted a small makeCoalescer(cls, icon) factory returning an update(api, text) closure; each call site is now a single line instead of the duplicated guard-and-update-or-create block against api.mutableRow. --- frontend/packages/agent/src/app.js | 87 ++++++++++++++---------------- 1 file changed, 39 insertions(+), 48 deletions(-) diff --git a/frontend/packages/agent/src/app.js b/frontend/packages/agent/src/app.js index 3dfa342e..6e6ff5da 100644 --- a/frontend/packages/agent/src/app.js +++ b/frontend/packages/agent/src/app.js @@ -1893,26 +1893,36 @@ window.marked = marked; } return false; } - // Coalescing target for the live `thinking_tokens` counter. - // Holds the row + its text node 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 (nextElementSibling === - // null), so any other event starts fresh. - let thinkingTokensRow = null; - let thinkingTokensText = null; - // Same coalescing trick for claude's generic `status` subtype (bare - // "still working" ticks with no detail beyond the label itself). Without - // this every tick got its own row, so a compaction (which emits a burst - // of these with no other signal) looked like a wall of identical noise - // rather than one "still going" line. - let statusRow = null; - let statusText = null; - // Same coalescing for `plugin_install`: claude emits a `started` tick - // then a `completed` tick per plugin — without coalescing that's two - // separate rows ("loading…" then "✓ done") for what's really one event - // from the operator's point of view; update in place instead. - let pluginInstallRow = null; - let pluginInstallText = null; + // Coalescing helper: collapses a burst of same-kind ticks into ONE + // row that updates in place instead of appending a fresh note per + // tick. Holds the row + its text node in a closure; reuses them only + // while the row is still the last one rendered (nextElementSibling + // === null) — any other event in between starts a fresh row. `cls` + // and `icon` are fixed per coalescer instance (mirrors `api.mutableRow`'s + // signature); `api` is passed per-call since `renderStream` receives a + // fresh one each invocation. + function makeCoalescer(cls, icon) { + let row = null; + let text = null; + return function update(api, newText) { + if (row && row.isConnected && row.nextElementSibling === null) { + text.nodeValue = newText; + } else { + [row, text] = api.mutableRow(cls, newText, icon); + } + }; + } + // Live `thinking_tokens` counter — claude streams many of these per turn. + const updateThinkingTokens = makeCoalescer('note', '🧠'); + // Bare `status` ticks (claude's generic "still working" signal) — the + // only signal during a compaction pass (no dedicated "compacting…" + // event), so without coalescing a compaction looked like a wall of + // identical `⚙ status` rows. + const updateStatus = makeCoalescer('note'); + // `plugin_install`: claude emits a `started` tick then a `completed` + // tick per plugin — without coalescing that's two rows ("loading…" + // then "✓ done") for what's really one event. + const updatePluginInstall = makeCoalescer('note'); function renderStream(v, api) { // Drop claude's result line and rate-limit — noise. TurnEnd // communicates pass/fail; rate-limit events are noisy status chatter. @@ -1943,19 +1953,12 @@ window.marked = marked; // 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. + // per tick (see `makeCoalescer` above). if (v.subtype === 'thinking_tokens') { const n = v.estimated_tokens; const text = 'thinking … ' + (n != null ? '~' + Number(n).toLocaleString() + ' tokens' : ''); - if (thinkingTokensRow && thinkingTokensRow.isConnected - && thinkingTokensRow.nextElementSibling === null) { - thinkingTokensText.nodeValue = text; - } else { - [thinkingTokensRow, thinkingTokensText] = api.mutableRow('note', text, '🧠'); - } + updateThinkingTokens(api, text); return; } // plugin_install: claude is loading/finishing a plugin (MCP server or @@ -1965,13 +1968,7 @@ window.marked = marked; const status = v.status === 'completed' ? '✓ done' : v.status === 'started' ? 'loading…' : (v.status || '?'); - const text = '⚙ plugin install · ' + status; - if (pluginInstallRow && pluginInstallRow.isConnected - && pluginInstallRow.nextElementSibling === null) { - pluginInstallText.nodeValue = text; - } else { - [pluginInstallRow, pluginInstallText] = api.mutableRow('note', text); - } + updatePluginInstall(api, '⚙ plugin install · ' + status); return; } // commands_changed: the set of available slash commands changed (usually @@ -2015,19 +2012,13 @@ window.marked = marked; } // Bare `status` ticks (claude's own generic "still working" signal, // no detail beyond the label) — collapse consecutive ticks into one - // updating row instead of a fresh note each, same pattern as - // `thinking_tokens` above. These are the only signal we get during a - // compaction pass (no dedicated "compacting…" event from claude), so - // without collapsing a compaction looked like a wall of identical - // `⚙ status` rows followed by silence. + // updating row instead of a fresh note each (see `makeCoalescer` + // above). These are the only signal we get during a compaction pass + // (no dedicated "compacting…" event from claude), so without + // collapsing a compaction looked like a wall of identical `⚙ status` + // rows followed by silence. if (v.subtype === 'status') { - const text = '⚙ status'; - if (statusRow && statusRow.isConnected - && statusRow.nextElementSibling === null) { - statusText.nodeValue = text; - } else { - [statusRow, statusText] = api.mutableRow('note', text); - } + updateStatus(api, '⚙ status'); return; } // Other system subtypes (context_window_exceeded, etc.) — render a