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:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user