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
+58 -40
View File
@@ -10,12 +10,18 @@ item breaking mid-word (``Rea``/``ding``) and the path wrapping to
three lines indented to the right of that broken label.
Phase 117's fix (frontend-only, D1–D6 in ``00_phase.md``): the
per-call lines ride in ONE native ``<details>`` disclosure — a compact
"Tool calls (N)" summary, open while the turn is live and FOLDED when
the answer begins (the first ``delta``), on ``done``, on stop/abort,
and on restore — and the lines themselves are deboxed inline-flow
text (no card, no flex: the label + ``<code>`` run as one continuous
sentence and the path wraps to the left edge).
per-call lines ride in ONE compact "Tool calls (N)" disclosure, open
while the turn is live and FOLDED when the answer begins (the first
``delta``), on ``done``, on stop/abort, and on restore — and the lines
themselves are deboxed inline-flow text (no card, no flex: the label
+ ``<code>`` run as one continuous sentence and the path wraps to the
left edge). Phase 161 (D2): the disclosure sits INSIDE the ONE
reasoning container (``details.reasoning``), which is the only
collapsible. The D5 refinement (owner 2026-10-06) goes one step
further: the per-run disclosure is now a PLAIN div (no ``<details>``)
— always visible while the container is open; the container is the
ONE disclosure (open while the turn is live, folded at done/stop/
restore).
Run in isolation (DB must be up: ``podman compose up -d db``):
@@ -41,22 +47,26 @@ already folded (phase 117, D3: the fold rides the first delta).
Test → phase mapping (Playwright Mapping Rule):
1. ``test_tool_calls_fold_to_one_line_after_turn`` — pin 1: a
completed 3-call turn renders ONE visible "Tool calls (3)" summary;
the disclosure is CLOSED (no ``open`` attribute); the three
``.tool-call`` lines are present in the DOM but folded (the first
line is not visible); the answer is present.
completed 3-call turn renders ONE visible "Tool calls (3)" summary
(revealed by opening the container — the ONE disclosure, folded at
rest); the in-container runs are plain divs (zero ``<details>``
nested in the container — the D5 refinement); the three
``.tool-call`` lines are present in the DOM but hidden (the
container is folded); the answer is present.
2. ``test_summary_click_expands_the_calls`` — pin 2: tapping the
native ``<summary>`` (a real focusable toggle, AA) opens the
disclosure; the three lines become visible, in order, with the
phase-37/94 pinned text.
container's native ``<summary>`` (the ONE real focusable toggle,
AA) opens the container; the three lines become visible, in order,
with the phase-37/94 pinned text.
3. ``test_expanded_line_is_deboxed_inline_flow`` — pin 3: the
expanded "Reading" line's computed ``display`` is NOT ``flex``
(the label + path are one inline run, not two flex items — the
debox), and the label runs directly into the path in the same run.
4. ``test_restored_turn_renders_folded`` — pin 4: a same-context
RELOAD (the phase-14/50 persisted conversation restores) renders
the turn FOLDED — closed disclosure, "Tool calls (3)" summary,
three lines in the DOM, first line hidden — no auto-expand on load.
the turn FOLDED — folded container, plain div runs (zero nested
``<details>``), "Tool calls (3)" summary (revealed by opening the
container), three lines in the DOM, first line hidden — no
auto-expand on load.
"""
from __future__ import annotations
@@ -232,11 +242,13 @@ def _reset_db(seed: Callable[[Session], None] | None = None) -> None:
# --------------------------------------------------------------------------
def _disclosure_open(page: Page) -> bool:
"""The disclosure's open state: a closed ``<details>`` has NO
def _container_open(page: Page) -> bool:
"""The container's open state: a closed ``<details>`` has NO
``open`` attribute (``get_attribute`` → ``None``); an open one
carries it (any value — the native boolean attribute is empty)."""
return page.get_attribute(DISCLOSURE, "open") is not None
carries it (any value — the native boolean attribute is empty).
Phase 161 (D2): the container is the ONE disclosure — the
per-run disclosure is a plain div (the D5 refinement)."""
return page.get_attribute(REASONING, "open") is not None
def _submit_tools_turn(page: Page, app_url: str) -> None:
@@ -264,14 +276,11 @@ def _submit_tools_turn(page: Page, app_url: str) -> None:
def _expand_disclosure(page: Page) -> None:
"""Tap the summary — the native ``<summary>`` is a real focusable
toggle (AA), so a plain click is the user contract. Phase 161
(D2): at rest the disclosure sits INSIDE the folded reasoning
container, so the user flow is two native toggles — open the
container first (its own summary — a direct child, so the inner
runs' summaries are not matched), then the disclosure's summary."""
"""Tap the container's summary — the native ``<summary>`` is a
real focusable toggle (AA), so a plain click is the user
contract. The D5 refinement: ONE toggle — the per-run disclosure
is a plain div, always visible while the container is open."""
page.locator(f"{REASONING} > summary").click()
page.locator(SUMMARY).click()
# --------------------------------------------------------------------------
@@ -301,6 +310,11 @@ def test_tool_calls_fold_to_one_line_after_turn(
container = page.locator(REASONING)
expect(container).to_have_count(1)
expect(container).not_to_have_attribute("open")
# Folded at rest: the record's lines are hidden (the D5 refinement —
# the at-rest fold is the container's; the per-run runs are plain
# divs that render on reveal).
expect(page.locator(LINES)).to_have_count(3)
expect(page.locator(LINES).first).not_to_be_visible()
page.locator(f"{REASONING} > summary").click()
expect(container).to_have_attribute("open", "")
@@ -309,13 +323,15 @@ def test_tool_calls_fold_to_one_line_after_turn(
expect(summary).to_be_visible()
expect(summary).to_contain_text("Tool calls (3)")
# Folded at rest: a closed <details> carries no `open` attribute.
assert page.get_attribute(DISCLOSURE, "open") is None
# The D5 refinement: the in-container runs are plain divs — the
# per-run disclosure is NOT a <details> (the container is the ONE
# disclosure).
expect(page.locator(f"{REASONING} details")).to_have_count(0)
# The lines are present in the DOM (the permanent record) but
# folded: the FIRST line is not rendered.
# The lines are present in the DOM (the permanent record) and
# render on reveal — always visible inside the open container (D5).
expect(page.locator(LINES)).to_have_count(3)
expect(page.locator(LINES).first).not_to_be_visible()
expect(page.locator(LINES).first).to_be_visible()
# The answer is present (the mock's deterministic single-read quote).
expect(page.locator(".msg.brain .bubble").last).to_contain_text(
@@ -339,13 +355,13 @@ def test_summary_click_expands_the_calls(
page.set_default_timeout(30_000)
_reset_db(_seed)
_submit_tools_turn(page, app_url)
assert not _disclosure_open(page) # folded after the turn (pin 1)
assert not _container_open(page) # folded after the turn (pin 1)
_expand_disclosure(page)
# The native boolean attribute is now present (any value — it is
# empty in the DOM; "true" is the attribute-present shorthand).
assert page.get_attribute(DISCLOSURE, "open") is not None
assert _container_open(page)
# All three lines, in order, visible with their pinned text.
lines = page.locator(LINES)
@@ -447,15 +463,17 @@ def test_restored_turn_renders_folded(
expect(container.locator("> summary")).to_be_visible()
expect(container.locator("> summary")).to_contain_text("Reasoning")
# The disclosure is CLOSED (no auto-expand on load) — and the
# in-container record is revealed by opening the container (the
# two-native-toggle user flow).
assert page.get_attribute(DISCLOSURE, "open") is None
# No auto-expand on load (the container is folded) — and the
# in-container record is revealed by opening the ONE toggle.
assert not _container_open(page)
# The D5 refinement: the restored runs are plain divs (no nested
# <details>).
expect(page.locator(f"{REASONING} details")).to_have_count(0)
page.locator(f"{REASONING} > summary").click()
expect(page.locator(SUMMARY)).to_be_visible()
expect(page.locator(SUMMARY)).to_contain_text("Tool calls (3)")
# The three lines are in the DOM (the permanent record) but folded:
# the first line is not rendered — no auto-expand.
# The three lines are in the DOM (the permanent record) and render
# on reveal — always visible inside the open container (D5).
expect(page.locator(LINES)).to_have_count(3)
expect(page.locator(LINES).first).not_to_be_visible()
expect(page.locator(LINES).first).to_be_visible()