Compare commits

..
3 changed files with 59 additions and 9 deletions

View file

@ -1886,11 +1886,12 @@ window.marked = marked;
return false; return false;
} }
// Coalescing target for the live `thinking_tokens` counter. // Coalescing target for the live `thinking_tokens` counter.
// Holds the row currently showing the running estimate so consecutive // Holds the row + its text node so consecutive ticks update in place
// ticks update in place instead of appending a note each. Reset // instead of appending a note each. Reset implicitly: we only reuse
// implicitly: we only reuse it while it's still the last row rendered // it while it's still the last row rendered (nextElementSibling ===
// (see the nextElementSibling check), so any other event starts fresh. // null), so any other event starts fresh.
let thinkingTokensRow = null; let thinkingTokensRow = null;
let thinkingTokensText = null;
function renderStream(v, api) { function renderStream(v, api) {
// Drop claude's result line and rate-limit — noise. TurnEnd // Drop claude's result line and rate-limit — noise. TurnEnd
// communicates pass/fail; rate-limit events are noisy status chatter. // communicates pass/fail; rate-limit events are noisy status chatter.
@ -1926,13 +1927,13 @@ window.marked = marked;
// tick starts a fresh row. // tick starts a fresh row.
if (v.subtype === 'thinking_tokens') { if (v.subtype === 'thinking_tokens') {
const n = v.estimated_tokens; const n = v.estimated_tokens;
const label = '🧠 thinking … ' const text = 'thinking … '
+ (n != null ? '~' + Number(n).toLocaleString() + ' tokens' : ''); + (n != null ? '~' + Number(n).toLocaleString() + ' tokens' : '');
if (thinkingTokensRow && thinkingTokensRow.isConnected if (thinkingTokensRow && thinkingTokensRow.isConnected
&& thinkingTokensRow.nextElementSibling === null) { && thinkingTokensRow.nextElementSibling === null) {
thinkingTokensRow.textContent = label; thinkingTokensText.nodeValue = text;
} else { } else {
thinkingTokensRow = api.row('note', label); [thinkingTokensRow, thinkingTokensText] = api.mutableRow('note', text, '🧠');
} }
return; return;
} }

View file

@ -216,11 +216,44 @@
details.row { details.row {
white-space: normal; white-space: normal;
} }
/* Two-column layout for expandable summary rows.
Left col: fixed-width icon cell. Right col: disclosure chevron +
text, wrapping within itself so no continuation line bleeds under
the icon. Overrides the flat-row hanging-indent metrics (.live .row
sets padding-left: 1.9em; text-indent: -1.4em) with explicit flex
geometry. The negative margin-left on summary cancels the details
container's inherited padding-left so the icon lands at the same
horizontal position as flat-row icons. */
details.row > summary { details.row > summary {
cursor: pointer; cursor: pointer;
list-style: none; list-style: none;
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-word; word-break: break-word;
display: flex;
align-items: baseline;
margin-left: -1.9em;
padding-left: 0.5em;
text-indent: 0;
}
/* Icon column: 2em accommodates wide emoji without ink bleeding into
the chevron column. Overrides the inline-block + width: 1.4em set
on .live .row-glyph for the shared flat-row context. */
details.row > summary > .row-glyph {
flex: 0 0 2em;
width: auto;
text-align: center;
}
/* Content column: chevron (::before) + text, wraps within the cell. */
details.row > summary > .summary-text {
flex: 1;
min-width: 0;
}
/* Icon-less summaries: the CSS-generated / acts as the hanging
marker. Hanging indent keeps wrapped lines under the text body,
not under the chevron. Mirrors the flat-row text-indent metric. */
details.row > summary > .summary-text:only-child {
padding-left: 1.4em;
text-indent: -1.4em;
} }
details.row > summary > .summary-text::before { details.row > summary > .summary-text::before {
content: '▸ '; content: '▸ ';

View file

@ -215,6 +215,22 @@ export function create(opts) {
afterAppend(wasNearBottom); afterAppend(wasNearBottom);
return e; return e;
} }
// Like row(), but returns [element, textNode] so the caller can update
// the text in place via textNode.nodeValue. Use for rows whose content
// changes after initial render (e.g. live-updating counters).
// Text is stored as a plain text node — no linkify, no innerHTML.
function mutableRow(cls, text, icon) {
clearPlaceholder();
const wasNearBottom = isNearBottom();
const e = document.createElement('div');
e.className = 'row ' + (cls || '') + (currentNoAnim ? ' no-anim' : '');
if (icon != null && icon !== '') e.appendChild(glyphSpan(icon));
const tn = document.createTextNode(text == null ? '' : String(text));
e.appendChild(tn);
log.appendChild(e);
afterAppend(wasNearBottom);
return [e, tn];
}
function details(cls, summary, body, icon) { function details(cls, summary, body, icon) {
clearPlaceholder(); clearPlaceholder();
const wasNearBottom = isNearBottom(); const wasNearBottom = isNearBottom();
@ -253,7 +269,7 @@ export function create(opts) {
function api(extra) { function api(extra) {
return Object.assign({ return Object.assign({
row, details, detailsDiff, placeholder, linkify, row, mutableRow, details, detailsDiff, placeholder, linkify,
fromHistory: false, fromHistory: false,
}, extra || {}); }, extra || {});
} }
@ -486,7 +502,7 @@ export function create(opts) {
} }
const ready = start(); const ready = start();
return { row, details, detailsDiff, placeholder, ready }; return { row, mutableRow, details, detailsDiff, placeholder, ready };
} }
// Build a DocumentFragment from `text`, turning bare http(s) URLs into // Build a DocumentFragment from `text`, turning bare http(s) URLs into