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:
@@ -23,12 +23,15 @@ further tool call). The client renders the ENTIRE per-run timeline
|
||||
inside ONE ``details.reasoning`` container (D1): open from creation
|
||||
until the turn settles (D2 — it STAYS OPEN through a mid-round
|
||||
delta; only ``done`` / stop / the failed-turn funnel fold it),
|
||||
condensed in its bounded 320px ``.reasoning-body`` live window (D3),
|
||||
the summary carrying the D6 status literals (``Thinking…`` /
|
||||
``Calling <name>…`` live, ``N steps`` at rest). The restore path and
|
||||
the shared page render the SAME container COLLAPSED (D5) with the
|
||||
at-rest step count (D6). A turn with no thinking and no tools creates
|
||||
NO container (D1/D4 — the byte-identical DOM).
|
||||
condensed in its bounded 320px ``.reasoning-body`` live window (D3,
|
||||
the pinned-tail follow — the window follows the live tail while the
|
||||
user stays near the bottom). The per-run blocks inside the container
|
||||
are plain divs — NOT collapsible (the D5 refinement, owner
|
||||
2026-10-06: the container is the ONE disclosure; a nested fold
|
||||
inside the fold used to hide the live reasoning mid-turn). The
|
||||
restore path and the shared page render the SAME container COLLAPSED
|
||||
(D5) with the at-rest step count (D6). A turn with no thinking and
|
||||
no tools creates NO container (D1/D4 — the byte-identical DOM).
|
||||
|
||||
KB fixture — mirrored from ``test_thinking_timeline.py`` (the
|
||||
deterministic two-document pair): the marker question cosines
|
||||
@@ -53,9 +56,9 @@ Test → phase mapping (Playwright Mapping Rule):
|
||||
``Stop`` control), the container is STILL ``open`` with a run
|
||||
visible.
|
||||
3. ``test_container_folds_at_done`` — D2/D5/D6: after ``done`` the
|
||||
container has NO ``open`` attribute, its summary is visible, EVERY
|
||||
inner run has no ``open`` attribute (the phase-158 fold-at-rest
|
||||
contract preserved inside the fold), and the status is ``3 steps``
|
||||
container has NO ``open`` attribute, its summary is visible, the
|
||||
inner runs are NOT collapsible (zero ``<details>`` inside the
|
||||
container — the D5 refinement), and the status is ``3 steps``
|
||||
(the flow's contiguous-run count — ``thinking (Step 1)``,
|
||||
``tool (ls)``, ``thinking (Step 2)`` — the D6 live-settle
|
||||
literal); the bubble carries the mid-round + closing answer.
|
||||
@@ -281,8 +284,8 @@ CONTAINER_LIVE_JS = """
|
||||
const live = st === 'Thinking…' || st.indexOf('Calling ') === 0;
|
||||
if (!live) return false;
|
||||
return !!cont.querySelector(
|
||||
'.reasoning-body details.thinking, ' +
|
||||
'.reasoning-body details.tool-calls-disclosure');
|
||||
'.reasoning-body div.thinking, ' +
|
||||
'.reasoning-body div.tool-calls-disclosure');
|
||||
}
|
||||
"""
|
||||
|
||||
@@ -305,8 +308,8 @@ MIDROUND_STILL_OPEN_JS = """
|
||||
const cont = wrap.querySelector('details.reasoning');
|
||||
if (!cont || !cont.hasAttribute('open')) return false;
|
||||
return !!cont.querySelector(
|
||||
'.reasoning-body details.thinking, ' +
|
||||
'.reasoning-body details.tool-calls-disclosure');
|
||||
'.reasoning-body div.thinking, ' +
|
||||
'.reasoning-body div.tool-calls-disclosure');
|
||||
}
|
||||
"""
|
||||
|
||||
@@ -322,7 +325,7 @@ TOOL_ONLY_LIVE_JS = """
|
||||
const cont = wrap.querySelector('details.reasoning');
|
||||
if (!cont || !cont.hasAttribute('open')) return false;
|
||||
return !!cont.querySelector(
|
||||
'.reasoning-body details.tool-calls-disclosure');
|
||||
'.reasoning-body div.tool-calls-disclosure');
|
||||
}
|
||||
"""
|
||||
|
||||
@@ -567,17 +570,13 @@ def test_container_folds_at_done(
|
||||
_wait_settled(page)
|
||||
|
||||
# D2/D5: at rest the container has NO ``open`` attribute (the
|
||||
# done settle folded it) — and EVERY inner run has no ``open``
|
||||
# attribute either (the phase-158 fold-at-rest contract
|
||||
# preserved inside the fold).
|
||||
# done settle folded it) — and the inner runs are NOT
|
||||
# collapsible (zero ``<details>`` inside the container — the D5
|
||||
# refinement, owner 2026-10-06: the per-run blocks are plain
|
||||
# divs, always visible while the container is open).
|
||||
expect(page.locator("#messages .msg.brain details.reasoning[open]")).to_have_count(0)
|
||||
expect(
|
||||
page.locator("#messages .msg.brain details.reasoning details.thinking[open]")
|
||||
).to_have_count(0)
|
||||
expect(
|
||||
page.locator(
|
||||
"#messages .msg.brain details.reasoning details.tool-calls-disclosure[open]"
|
||||
)
|
||||
page.locator("#messages .msg.brain details.reasoning details")
|
||||
).to_have_count(0)
|
||||
|
||||
# The collapsed summary is still visible (the bubble stays
|
||||
@@ -649,19 +648,17 @@ def test_restored_container_collapsed_with_step_count(
|
||||
expect(page.locator("#messages .msg.brain details.reasoning")).to_have_count(1)
|
||||
expect(page.locator("#messages .msg.brain details.reasoning[open]")).to_have_count(0)
|
||||
|
||||
# The SAME per-run timeline, nested in the container (all
|
||||
# collapsed — the D5 restore convention).
|
||||
# The SAME per-run timeline, nested in the container (plain divs
|
||||
# — the D5 refinement; the at-rest fold is the container's).
|
||||
expect(
|
||||
page.locator("#messages .msg.brain details.reasoning details.thinking")
|
||||
page.locator("#messages .msg.brain details.reasoning div.thinking")
|
||||
).to_have_count(2)
|
||||
expect(
|
||||
page.locator(
|
||||
"#messages .msg.brain details.reasoning details.thinking[open]"
|
||||
)
|
||||
page.locator("#messages .msg.brain details.reasoning details")
|
||||
).to_have_count(0)
|
||||
expect(
|
||||
page.locator(
|
||||
"#messages .msg.brain details.reasoning details.tool-calls-disclosure"
|
||||
"#messages .msg.brain details.reasoning div.tool-calls-disclosure"
|
||||
)
|
||||
).to_have_count(1)
|
||||
texts = page.locator(
|
||||
@@ -737,30 +734,23 @@ def test_shared_page_container_collapsed(
|
||||
|
||||
# D5 (the shared-page half): ONE ``details.reasoning``, NO
|
||||
# ``open`` attribute (the pure-render convention — the
|
||||
# shared page is a finished conversation), wrapping the SAME
|
||||
# per-run timeline, all collapsed.
|
||||
# shared page is a finished conversation), wrapping the
|
||||
# SAME per-run timeline as plain divs (the D5 refinement —
|
||||
# the inner runs are not collapsible).
|
||||
expect(anon.locator("#messages .msg.brain details.reasoning")).to_have_count(1)
|
||||
expect(
|
||||
anon.locator("#messages .msg.brain details.reasoning[open]")
|
||||
).to_have_count(0)
|
||||
expect(
|
||||
anon.locator(
|
||||
"#messages .msg.brain details.reasoning details.thinking[open]"
|
||||
)
|
||||
anon.locator("#messages .msg.brain details.reasoning details")
|
||||
).to_have_count(0)
|
||||
expect(
|
||||
anon.locator(
|
||||
"#messages .msg.brain details.reasoning "
|
||||
"details.tool-calls-disclosure[open]"
|
||||
)
|
||||
).to_have_count(0)
|
||||
expect(
|
||||
anon.locator("#messages .msg.brain details.reasoning details.thinking")
|
||||
anon.locator("#messages .msg.brain details.reasoning div.thinking")
|
||||
).to_have_count(2)
|
||||
expect(
|
||||
anon.locator(
|
||||
"#messages .msg.brain details.reasoning "
|
||||
"details.tool-calls-disclosure"
|
||||
"div.tool-calls-disclosure"
|
||||
)
|
||||
).to_have_count(1)
|
||||
anon_texts = anon.locator(
|
||||
@@ -807,9 +797,9 @@ def test_no_reasoning_turn_has_no_container(
|
||||
_submit(page, PLAIN_ANSWER_QUESTION)
|
||||
_wait_settled(page)
|
||||
expect(page.locator("#messages .msg.brain details.reasoning")).to_have_count(0)
|
||||
expect(page.locator("#messages .msg.brain details.thinking")).to_have_count(0)
|
||||
expect(page.locator("#messages .msg.brain .thinking")).to_have_count(0)
|
||||
expect(
|
||||
page.locator("#messages .msg.brain details.tool-calls-disclosure")
|
||||
page.locator("#messages .msg.brain .tool-calls-disclosure")
|
||||
).to_have_count(0)
|
||||
expect(page.locator(".msg.brain .bubble").last).not_to_have_text("")
|
||||
|
||||
@@ -851,13 +841,18 @@ def test_no_reasoning_turn_has_no_container(
|
||||
).to_have_text("1 steps")
|
||||
|
||||
# The phase-117 single-disclosure behavior, preserved and now
|
||||
# wrapped: ONE disclosure with the 3 ``.tool-call`` lines, zero
|
||||
# thinking blocks, folded at rest.
|
||||
# wrapped: ONE disclosure — a plain div (the D5 refinement —
|
||||
# not collapsible; zero ``<details>`` inside the container) with
|
||||
# the 3 ``.tool-call`` lines, zero thinking blocks, folded at
|
||||
# rest (the container's fold).
|
||||
expect(
|
||||
page.locator(
|
||||
"#messages .msg.brain details.reasoning details.tool-calls-disclosure"
|
||||
"#messages .msg.brain details.reasoning div.tool-calls-disclosure"
|
||||
)
|
||||
).to_have_count(1)
|
||||
expect(
|
||||
page.locator("#messages .msg.brain details.reasoning details")
|
||||
).to_have_count(0)
|
||||
expect(
|
||||
page.locator(
|
||||
"#messages .msg.brain details.reasoning .tool-call"
|
||||
@@ -869,12 +864,7 @@ def test_no_reasoning_turn_has_no_container(
|
||||
)
|
||||
).to_have_text("Tool calls (3)")
|
||||
expect(
|
||||
page.locator("#messages .msg.brain details.reasoning details.thinking")
|
||||
).to_have_count(0)
|
||||
expect(
|
||||
page.locator(
|
||||
"#messages .msg.brain details.reasoning details.thinking[open]"
|
||||
)
|
||||
page.locator("#messages .msg.brain details.reasoning div.thinking")
|
||||
).to_have_count(0)
|
||||
|
||||
# The phase-37 answer echo (the read document reached the model).
|
||||
|
||||
Reference in New Issue
Block a user