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
+27 -15
View File
@@ -145,20 +145,25 @@ def test_thinking_event_is_a_first_class_turn_branch() -> None:
def test_thinking_run_helpers_are_idempotent() -> None:
"""Phase 158 (D1/D5 — the phase-109 D15 pin, replaced by owner
direction 2026-10-04): ``ensureThinkingRun`` returns the active
run's block or creates a NEW one OPEN (phase 161, D1: appended
into the reasoning container's ``.reasoning-body`` — the
per-run bubble anchor is superseded; a frame of a new kind
closes the previous run first); ``closeCurrentRun`` is a no-op
without a run and folds the active one (the D15 re-open of the
shared block is gone — a post-delta thinking frame opens a NEW
block)."""
run's block or creates a NEW one (the D5 refinement, owner
2026-10-06 — a plain ``div.thinking`` through the run builder,
NEVER open/foldable: the container is the ONE disclosure); it is
appended into the reasoning container's ``.reasoning-body`` (phase
161, D1 — the per-run bubble anchor is superseded; a frame of a
new kind ends the previous run first); ``closeCurrentRun`` is a
no-op without a run and ends the active one (nothing folds — the
D5 refinement; the D15 re-open of the shared block is gone — a
post-delta thinking frame opens a NEW block)."""
js = _js()
fn = js.find("function ensureThinkingRun")
assert fn != -1, "ensureThinkingRun must exist (near addTyping/removeTyping)"
body = js[fn : js.find("\n}\n", fn)]
assert "closeCurrentRun()" in body, ("a frame of a new kind closes the "
assert "closeCurrentRun()" in body, ("a frame of a new kind ends the "
"previous run (D1)")
assert "block.open = true" in body, "D5: created open — the stream is the show"
assert "createThinkingRunEl()" in body, (
"D5 refinement: a plain div — never collapsible (the container "
"is the ONE disclosure)"
)
assert (
'ensureReasoningWrap(wrap).querySelector(".reasoning-body")'
".appendChild(block)" in body
@@ -169,15 +174,20 @@ def test_thinking_run_helpers_are_idempotent() -> None:
fn2 = js.find("function closeCurrentRun")
assert fn2 != -1, "closeCurrentRun must exist"
body2 = js[fn2 : js.find("\n}\n", fn2)]
assert "currentRun.el.open = false" in body2
assert ".open" not in body2, (
"D5 refinement: nothing folds — the run simply ends (the "
"in-container runs are non-collapsible divs)"
)
assert "currentRun = null" in body2, "a no-op without a run (idempotent)"
def test_delta_branch_collapses_block_and_transitions_to_streaming() -> None:
"""The first answer delta transitions thinking → streaming (even when
thinking created the wrap first) and folds the active run — phase 158
(D5): ``closeCurrentRun()`` (a later thinking frame opens a NEW run;
the D15 re-open of the shared block is gone)."""
thinking created the wrap first) and ends the active run — phase 158
(D5): ``closeCurrentRun()`` (the D5 refinement, owner 2026-10-06 —
the in-container runs are non-collapsible divs, so nothing folds;
a later thinking frame opens a NEW run; the D15 re-open of the
shared block is gone)."""
js = _js()
delta_idx = js.find('ev.type === "delta"')
done_idx = js.find('ev.type === "done"')
@@ -190,8 +200,10 @@ def test_delta_branch_collapses_block_and_transitions_to_streaming() -> None:
def test_done_branch_sets_sawdone_and_closes_block() -> None:
"""On `done` the turn marks itself complete (sawDone — the stream-drop
guard keys off it) and settles EVERY open run closed (phase 158, D5:
``closeAllRuns``)."""
guard keys off it) and settles the runs (phase 158, D5:
``closeAllRuns`` — the D5 refinement, owner 2026-10-06 — the
in-container runs are non-collapsible divs, so the defensive
details query no-ops in every live path)."""
js = _js()
done_idx = js.find('ev.type === "done"')
error_idx = js.find('ev.type === "error"')