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.
This commit is contained in:
iris 2026-07-11 20:30:38 +02:00
commit a99508719e

View file

@ -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