fix(rag): stop the per-run blocks from folding — the container is the one disclosure
The owner refinement (2026-10-06): the per-run thinking and tool-call blocks inside the Reasoning bubble were individually collapsible details elements — a fold inside the fold. A tool frame closed the previous thinking block, so the visible thinking vanished mid-turn; and the pinned-tail follow died on the append of every run after the first (the pin was captured AFTER the new block's height growth contaminated the 32px reading). - app.js: the in-container runs are now plain divs — createThinkingRunEl (div.thinking + static .thinking-summary label + .thinking-text) and createToolRunEl (div.tool-calls-disclosure + static .tool-calls-summary label + the .tool-calls list); they never open or fold (the container is the ONE open/fold control, open through the turn, folded at settle — unchanged); the pin capture moves BEFORE ensureThinkingRun/ensureToolRun append (plus the first-frame edge: no container yet -> always pinned); closeCurrentRun just ends the run; closeAllRuns stays the defensive legacy guard; restoreTimeline/addSegments (shared.js) use the div builders; the legacy no-segments restore path keeps the old details-based blocks byte-identical - styles.css: the in-container runs are distinct by label + rail (brand-ink Thinking label on a brand-soft rail; accent-ink Tool call(s) (N) label on a neutral rail); the interactive treatment (cursor + chevron) is scoped to the legacy <summary>; the inner windows stay disabled (the container's .reasoning-body is the ONE live scroll surface) - unit + E2E: the D5-refinement contract is pinned — one details.reasoning disclosure, per-run blocks as plain divs, zero nested <details> inside the container, the container as the only open/fold control, the .reasoning-body window auto-follows the tail while streaming unit+integration 3738 passed (coverage 99%), ruff + pyright clean, all five affected E2E suites pass in isolation (reasoning_bubble 6, thinking_timeline 6, thinking_display 5, thinking_scroll 8, tool_call_compact 4), verified live: the follow holds through a long thinking stream, the tool-frame transition, and the second thinking run; restore renders the div structure without client-side errors LLM-Generated: true
This commit is contained in:
@@ -935,27 +935,46 @@ PINNED_RUN_TURN = """function runTurn(text, { reask = false, image = null } = {}
|
||||
// is no longer "processing"; settle its "(Ns)" suffix (the
|
||||
// visible indication moves to the thinking block).
|
||||
settleToolLine();
|
||||
// Phase 17: model reasoning — stream it live into the collapsible
|
||||
// Thinking block. No setUiState here: the UI state stays
|
||||
// Phase 17: model reasoning — stream it live into the per-run
|
||||
// Thinking block (the D5 refinement, owner 2026-10-06 — a
|
||||
// plain div, always visible; the container is the ONE
|
||||
// collapsible). No setUiState here: the UI state stays
|
||||
// "thinking" (the button stays the enabled "Stop" control —
|
||||
// phase 48 — #send-status unchanged) — the live block simply
|
||||
// replaces the typing dots as the visible feedback.
|
||||
// Phase 158 (D1, owner direction 2026-10-04): PER-RUN blocks —
|
||||
// the D15 "re-open the one shared block" is superseded. Each
|
||||
// CONTIGUOUS thinking run streams into its OWN
|
||||
// details.thinking (ensureThinkingRun: a frame of a new kind
|
||||
// closes the previous run and opens a new block; D5 — the new
|
||||
// block is OPEN while its frames stream). A post-delta
|
||||
// thinking frame (the next agent round: the model answered,
|
||||
// called a tool, then thinks again) lands in a NEW block above
|
||||
// the answer — not a re-open of the block that sits on top of
|
||||
// it (the owner's "thinking resumed above the answer" awkwardness
|
||||
// is gone; the runs stay in arrival order, D2).
|
||||
// CONTIGUOUS thinking run streams into its OWN block
|
||||
// (ensureThinkingRun: a frame of a new kind ends the previous
|
||||
// run and opens a new block — always visible inside the
|
||||
// container; D5 refinement). A post-delta thinking frame (the
|
||||
// next agent round: the model answered, called a tool, then
|
||||
// thinks again) lands in a NEW block — not a re-open of the
|
||||
// block that sits on top of it (the owner's "thinking resumed
|
||||
// above the answer" awkwardness is gone; the runs stay in
|
||||
// arrival order, D2).
|
||||
thinkingAcc += ev.text || "";
|
||||
sawThinking = true;
|
||||
clearTurnTimeout(); // the stream is alive — as the first delta says
|
||||
if (!wrap) wrap = addMessage("brain", "");
|
||||
removeTyping(); // the live block replaces the dots as feedback
|
||||
// Phase 161 (D3): the container window's PRE-update pin
|
||||
// capture — BEFORE ensureThinkingRun appends the (new) block:
|
||||
// the append's own height growth must not leak into the
|
||||
// "pre" reading (an append taller than the 32px band would
|
||||
// read as "the user scrolled up" and kill the follow at the
|
||||
// first frame of every run after the first — the D5
|
||||
// refinement, owner 2026-10-06, exposed the ordering defect:
|
||||
// the old details collapse used to reset the window to a
|
||||
// fitting state, masking it). The phase-17 pre-measure
|
||||
// convention, verbatim.
|
||||
// convention, verbatim. A turn's FIRST frame (the container
|
||||
// did not exist yet — reasoningWrapEl is null) is always
|
||||
// pinned: the fresh window starts at its bottom (st=0), and
|
||||
// a first chunk taller than the 320px window must still
|
||||
// land on the tail, not the top.
|
||||
const rbody = reasoningWrapEl?.querySelector(".reasoning-body");
|
||||
const rPinned = rbody ? isReasoningPinned(rbody) : true;
|
||||
const block = ensureThinkingRun(wrap);
|
||||
// Phase 161 (D6): the container's live status — "Thinking…"
|
||||
// while a thinking frame streams (a tool frame overwrites it
|
||||
@@ -970,18 +989,17 @@ PINNED_RUN_TURN = """function runTurn(text, { reask = false, image = null } = {}
|
||||
// grows the window's content below the old bottom, so measuring
|
||||
// the distance AFTER the update reads the chunk's height, not
|
||||
// the user's position, and the follow died at the first
|
||||
// 2-newline gap. Pre-update, the distance is where the user
|
||||
// actually is. Phase 158: the same pin logic, now operating on
|
||||
// THIS block (per-run — each run's block pins its own tail;
|
||||
// the pre-update capture is verbatim).
|
||||
// 2-newline gap. The container capture above is taken BEFORE
|
||||
// the block append for the same reason (the D5 refinement's
|
||||
// exposed ordering defect). Phase 158: the same pin logic, now
|
||||
// operating on THIS block (per-run — each run's block pins its
|
||||
// own tail; the pre-update capture is verbatim). The D5
|
||||
// refinement (owner 2026-10-06): the in-container block is a
|
||||
// plain div (block.open is undefined → the gate is false) and
|
||||
// its .thinking-text is not scrollable, so this capture
|
||||
// no-ops there — the container window's follow below is the
|
||||
// live scroll surface.
|
||||
const pinned = block.open && isThinkingNearBottom(textEl);
|
||||
// Phase 161 (D3): the container window's PRE-update pin
|
||||
// capture — the phase-17 pre-measure convention, BEFORE the
|
||||
// re-render grows the content (the inner per-block follow
|
||||
// above stays — inside the container the .thinking-text is
|
||||
// no longer scrollable, so it no-ops there; task 03).
|
||||
const rbody = reasoningWrapEl?.querySelector(".reasoning-body");
|
||||
const rPinned = !!rbody && isReasoningPinned(rbody);
|
||||
textEl.innerHTML = renderMarkdown(currentRun.run.text); // escape-first, XSS-safe
|
||||
if (pinned) {
|
||||
// Follow the live tail only while the user was pinned to the
|
||||
@@ -1063,24 +1081,37 @@ PINNED_RUN_TURN = """function runTurn(text, { reask = false, image = null } = {}
|
||||
?.setAttribute("aria-label", toolStatus);
|
||||
}
|
||||
// Phase 158 (D1): PER-RUN disclosure — each CONTIGUOUS tool-call
|
||||
// run streams into its OWN details.tool-calls-disclosure
|
||||
// (ensureToolRun: a frame of a new kind closes the previous run
|
||||
// and opens a new disclosure; D5 — OPEN while its frames stream),
|
||||
// inserted BEFORE the bubble in arrival order (D2). The record
|
||||
// run streams into its OWN block (ensureToolRun: a frame of a
|
||||
// new kind ends the previous run and opens a new disclosure —
|
||||
// the D5 refinement, owner 2026-10-06: a plain div, always
|
||||
// visible inside the container; the container is the ONE
|
||||
// collapsible), inserted in arrival order (D2). The record
|
||||
// rides the run's `records` too — the SAME toolAcc objects, so
|
||||
// the phase-95 tool_result stamp (truncated / chars_shown /
|
||||
// chars_total) flows into the segments payload with no extra
|
||||
// wiring.
|
||||
// Phase 161 (D3): the container window's PRE-update pin
|
||||
// capture — BEFORE ensureToolRun appends the (new) block:
|
||||
// the append's own height growth must not leak into the
|
||||
// "pre" reading (an append taller than the 32px band would
|
||||
// read as "the user scrolled up" and kill the follow — the
|
||||
// D5 refinement, owner 2026-10-06, exposed the ordering
|
||||
// defect: the old details collapse used to reset the window
|
||||
// to a fitting state, masking it). The phase-17 pre-measure
|
||||
// convention, verbatim.
|
||||
// convention, verbatim. A turn's FIRST frame (the container
|
||||
// did not exist yet — reasoningWrapEl is null) is always
|
||||
// pinned: the fresh window starts at its bottom (st=0), and
|
||||
// a first line taller than the 320px window must still land
|
||||
// on the tail, not the top.
|
||||
const tbody = reasoningWrapEl?.querySelector(".reasoning-body");
|
||||
const tPinned = tbody ? isReasoningPinned(tbody) : true;
|
||||
const disc = ensureToolRun(wrap);
|
||||
currentRun.run.records.push(toolRecord);
|
||||
// Phase 161 (D6): "Calling <name>…" — the display name the
|
||||
// tool line already shows (the phase-70 legacy-name mapping
|
||||
// produced `name` before this point).
|
||||
setReasoningStatus(`Calling ${name}…`);
|
||||
// Phase 161 (D3): the container window's PRE-update pin
|
||||
// capture — before the line append grows the content.
|
||||
const tbody = reasoningWrapEl?.querySelector(".reasoning-body");
|
||||
const tPinned = !!tbody && isReasoningPinned(tbody);
|
||||
const line = appendToolLine(
|
||||
disc,
|
||||
name,
|
||||
@@ -1132,18 +1163,22 @@ PINNED_RUN_TURN = """function runTurn(text, { reask = false, image = null } = {}
|
||||
acc += ev.text || "";
|
||||
if (uiState === UI_STATE.thinking) setUiState(UI_STATE.streaming);
|
||||
if (!wrap) wrap = addMessage("brain", ""); // first token: live bubble in
|
||||
// Phase 158 (D5): the first delta FOLDS the open run (the
|
||||
// active block closes; a later thinking frame opens a NEW run
|
||||
// above the bubble — the D15 re-open of the shared block is
|
||||
// gone). Idempotent — a no-op without an open run.
|
||||
// Phase 158 (D5): the first delta ENDS the active run (the
|
||||
// D5 refinement, owner 2026-10-06 — the in-container runs are
|
||||
// non-collapsible divs, so nothing folds; the run simply
|
||||
// stops accumulating frames — a later thinking frame opens a
|
||||
// NEW run in the container — the D15 re-open of the shared
|
||||
// block is gone). Idempotent — a no-op without an active run.
|
||||
closeCurrentRun();
|
||||
wrap.querySelector(".bubble").innerHTML = renderMarkdown(acc);
|
||||
// No page scroll (phase 42): the answer never follows the viewport.
|
||||
} else if (ev.type === "done") {
|
||||
sawDone = true;
|
||||
// Phase 158 (D5): the turn is over — fold EVERY open run
|
||||
// (defensive: by construction at most the last run is open; a
|
||||
// late double-close is a no-op). Replaces the phase-117
|
||||
// Phase 158 (D5): the turn is over — the D5 refinement (owner
|
||||
// 2026-10-06) made the in-container runs non-collapsible divs,
|
||||
// so this is the DEFENSIVE legacy guard (the pre-phase-161
|
||||
// details blocks the restore path creates, already closed — a
|
||||
// no-op in every live path). Replaces the phase-117
|
||||
// closeToolCalls fold + the D15 block close.
|
||||
closeAllRuns(wrap);
|
||||
// Phase 161 (D2): the turn settled — fold the container too
|
||||
@@ -1468,13 +1503,21 @@ def test_run_turn_is_pinned_at_phase_159_state() -> None:
|
||||
the live container, like segRuns; the D6 status writes + the D3
|
||||
pre/post follow in the thinking/tool frame branches; the
|
||||
``settleReasoning(wrap)`` fold at the done / stop / failed
|
||||
settle sites — the container's live-path contract, D1/D2/D3/D6):
|
||||
the full source must match the pin — the ``reask`` path, the
|
||||
turn-local resets, the SSE contract, the save points, and the
|
||||
sanctioned additions (the task-01 ``lastUserWrap`` assignment +
|
||||
the idle-settle mark pass, the task-03 aborted-settle mark pair,
|
||||
the phase-160 done-settle pair, and the phase-161 live-container
|
||||
additions) included."""
|
||||
settle sites — the container's live-path contract, D1/D2/D3/D6)
|
||||
and the D5 refinement's (owner 2026-10-06) sanctioned changes
|
||||
(the per-run blocks are plain divs — ``ensureThinkingRun`` /
|
||||
``ensureToolRun`` create ``div``s through the run builders, never
|
||||
open/foldable; the container window's PRE-update pin capture
|
||||
moves BEFORE the block append — the append's height growth must
|
||||
not leak into the "pre" reading; the first-frame edge case — a
|
||||
fresh window is always pinned; the legacy block-level pin
|
||||
capture stays, no-op'ing on the divs): the full source must
|
||||
match the pin — the ``reask`` path, the turn-local resets, the
|
||||
SSE contract, the save points, and the sanctioned additions (the
|
||||
task-01 ``lastUserWrap`` assignment + the idle-settle mark
|
||||
pass, the task-03 aborted-settle mark pair, the phase-160
|
||||
done-settle pair, the phase-161 live-container additions, and
|
||||
the D5 refinement) included."""
|
||||
assert _fn_source(_js(), "runTurn") == PINNED_RUN_TURN
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user