fix(rag): stop the per-run blocks from folding — the container is the one disclosure
Build and Push Containers / build-and-push-app (push) Successful in 1m43s
Build and Push Containers / build-and-push-db (push) Successful in 15s

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:
2026-10-06 14:44:41 -04:00
parent 051f62deb7
commit 690fd98036
15 changed files with 941 additions and 609 deletions
+87 -44
View File
@@ -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