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:
@@ -78,27 +78,34 @@ def _branch(js: str, open_type: str, close_type: str) -> str:
|
||||
# ---------- the run builders (D1/D2/D3/D5) ----------
|
||||
|
||||
|
||||
def test_ensure_thinking_run_builds_a_new_open_block_in_the_container() -> None:
|
||||
"""D1/D3/D5: ``ensureThinkingRun`` returns the active thinking
|
||||
run's block or — a NEW ``details.thinking`` (the phase-17 static
|
||||
structure via ``createThinkingBlock``) created OPEN (D5),
|
||||
appended into the phase-161 reasoning container's
|
||||
def test_ensure_thinking_run_builds_a_new_div_block_in_the_container() -> None:
|
||||
"""D1/D3 + the D5 refinement (owner 2026-10-06):
|
||||
``ensureThinkingRun`` returns the active thinking run's block or
|
||||
— a NEW plain ``div.thinking`` (the run builder —
|
||||
``createThinkingRunEl``: the static ``.thinking-summary``
|
||||
"Thinking" literal + the ``.thinking-text``; NEVER open/foldable
|
||||
— the container is the ONE disclosure, always visible while it
|
||||
is open), appended into the phase-161 reasoning container's
|
||||
``.reasoning-body`` (D1 — the container sits before the bubble,
|
||||
a new run appends after the previous run's block, so arrival
|
||||
order is preserved; the per-run bubble anchor is superseded). The
|
||||
plain ``Thinking`` literal (D3 — the
|
||||
phase-17 literal, not a numbered variant) rides the static
|
||||
template; the fresh run joins segRuns + currentRun."""
|
||||
order is preserved; the per-run bubble anchor is superseded).
|
||||
The plain ``Thinking`` literal (D3 — the
|
||||
phase-17 literal, not a numbered variant) rides the builder;
|
||||
the fresh run joins segRuns + currentRun."""
|
||||
js = _js()
|
||||
body = _fn(js, "ensureThinkingRun")
|
||||
assert 'currentRun?.kind === "thinking"' in body, (
|
||||
"the active thinking run is extended, not recreated"
|
||||
)
|
||||
assert "closeCurrentRun()" in body, (
|
||||
"a frame of a new kind CLOSES the previous run first (D1)"
|
||||
"a frame of a new kind ends the previous run first (D1)"
|
||||
)
|
||||
assert "createThinkingRunEl()" in body, (
|
||||
"the D5 refinement: a plain div — never collapsible"
|
||||
)
|
||||
assert ".open =" not in body, (
|
||||
"the in-container runs are non-collapsible divs"
|
||||
)
|
||||
assert "createThinkingBlock()" in body, "the shared builder is reused"
|
||||
assert "block.open = true" in body, "D5: created OPEN while its frames stream"
|
||||
assert (
|
||||
'ensureReasoningWrap(wrap).querySelector(".reasoning-body")'
|
||||
".appendChild(block)" in body
|
||||
@@ -139,13 +146,15 @@ def test_create_thinking_block_is_the_phase_17_static_template_without_data() ->
|
||||
)
|
||||
|
||||
|
||||
def test_ensure_tool_run_builds_a_new_open_disclosure_in_the_container() -> None:
|
||||
"""D1/D5: ``ensureToolRun`` returns the active tool run's
|
||||
disclosure or — a NEW ``details.tool-calls-disclosure`` (the
|
||||
phase-117 structure via ``createToolDisclosure`` —
|
||||
summary.tool-calls-summary + the .tool-calls list, role=list +
|
||||
aria-label "Tool calls") created OPEN (D5), appended into the
|
||||
phase-161 reasoning container's ``.reasoning-body`` (D1 — the
|
||||
def test_ensure_tool_run_builds_a_new_div_disclosure_in_the_container() -> None:
|
||||
"""D1 + the D5 refinement (owner 2026-10-06): ``ensureToolRun``
|
||||
returns the active tool run's disclosure or — a NEW plain
|
||||
``div.tool-calls-disclosure`` (the run builder —
|
||||
``createToolRunEl``: the static ``.tool-calls-summary`` + the
|
||||
.tool-calls list, role=list + aria-label "Tool calls"; NEVER
|
||||
open/foldable — the container is the ONE disclosure, always
|
||||
visible while it is open), appended into the phase-161
|
||||
reasoning container's ``.reasoning-body`` (D1 — the
|
||||
container sits before the bubble, arrival order preserved; the
|
||||
per-run bubble anchor is superseded). One disclosure per
|
||||
CONTIGUOUS tool-call run; the per-run "Tool call(s) (N)" count
|
||||
@@ -154,8 +163,12 @@ def test_ensure_tool_run_builds_a_new_open_disclosure_in_the_container() -> None
|
||||
body = _fn(js, "ensureToolRun")
|
||||
assert 'currentRun?.kind === "tool"' in body
|
||||
assert "closeCurrentRun()" in body
|
||||
assert "createToolDisclosure()" in body
|
||||
assert "disc.open = true" in body, "D5: created OPEN while its frames stream"
|
||||
assert "createToolRunEl()" in body, (
|
||||
"the D5 refinement: a plain div — never collapsible"
|
||||
)
|
||||
assert ".open =" not in body, (
|
||||
"the in-container runs are non-collapsible divs"
|
||||
)
|
||||
assert (
|
||||
'ensureReasoningWrap(wrap).querySelector(".reasoning-body")'
|
||||
".appendChild(disc)" in body
|
||||
@@ -321,14 +334,19 @@ def test_the_arm_follows_the_line_append_with_the_new_signature() -> None:
|
||||
|
||||
|
||||
def test_close_current_run_and_close_all_runs_are_defined() -> None:
|
||||
"""D5's fold side: ``closeCurrentRun`` folds the ACTIVE run
|
||||
(idempotent; a no-op without a run) and ``closeAllRuns`` closes
|
||||
EVERY open ``details.thinking`` + ``details.tool-calls-disclosure``
|
||||
in the wrap (defensive — a late double-close is a no-op; a no-op
|
||||
without a wrap)."""
|
||||
"""The D5 refinement's (owner 2026-10-06) fold side:
|
||||
``closeCurrentRun`` ENDS the ACTIVE run (nothing folds — the
|
||||
in-container runs are non-collapsible divs; idempotent; a
|
||||
no-op without a run) and ``closeAllRuns`` closes EVERY open
|
||||
``details.thinking`` + ``details.tool-calls-disclosure`` in the
|
||||
wrap (the DEFENSIVE legacy guard — the pre-phase-161
|
||||
details blocks the restore path creates, already closed; a
|
||||
no-op in every live path)."""
|
||||
js = _js()
|
||||
cur = _fn(js, "closeCurrentRun")
|
||||
assert "currentRun.el.open = false" in cur
|
||||
assert ".open" not in cur, (
|
||||
"D5 refinement: nothing folds — the run simply ends"
|
||||
)
|
||||
assert "currentRun = null" in cur, "the run is cleared (a later close is a no-op)"
|
||||
all_runs = _fn(js, "closeAllRuns")
|
||||
assert "details.thinking, details.tool-calls-disclosure" in all_runs
|
||||
@@ -581,34 +599,35 @@ def test_the_pre_phase_narrative_is_replaced() -> None:
|
||||
|
||||
def test_restore_timeline_builds_the_collapsed_per_run_structure() -> None:
|
||||
"""D5: ``restoreTimeline`` renders the record's ``segments``
|
||||
COLLAPSED — a ``kind:"thinking"`` entry gets its OWN
|
||||
``details.thinking`` (createElement structure, the plain
|
||||
``Thinking`` literal via textContent — D3; NO ``open = true`` —
|
||||
created closed), and CONSECUTIVE ``kind:"tool"`` entries group
|
||||
into ONE ``details.tool-calls-disclosure`` (the shared
|
||||
phase-117 builder — ``createToolDisclosure``, innerHTML ABSENT —
|
||||
created closed). Phase 161 (task 02, D5/D6): the per-run blocks
|
||||
nest inside ONE details.reasoning container — the shared
|
||||
task-01 builder, created CLOSED and inserted ONCE before the
|
||||
bubble (the per-run bubble anchor is superseded; arrival order
|
||||
inside the container COLLAPSED — a ``kind:"thinking"`` entry
|
||||
gets its OWN plain ``div.thinking`` (the D5 refinement — the
|
||||
run builder, never collapsible), and CONSECUTIVE
|
||||
``kind:"tool"`` entries group into ONE plain
|
||||
``div.tool-calls-disclosure`` (the run builder — the static
|
||||
label + the .tool-calls list; innerHTML ABSENT). Phase 161
|
||||
(task 02, D5/D6): the per-run blocks nest inside ONE
|
||||
details.reasoning container — the shared task-01 builder,
|
||||
created CLOSED and inserted ONCE before the bubble (the
|
||||
per-run bubble anchor is superseded; arrival order
|
||||
preserved). The per-run count rides ``appendToolLine``'s summary
|
||||
update; a kind change seals the tool run (one disclosure per
|
||||
run, D1)."""
|
||||
body = _fn(_js(), "restoreTimeline")
|
||||
assert "document.createElement(\"details\")" in body, "the structure is createElement"
|
||||
assert 'block.className = "thinking"' in body
|
||||
assert "block.open = true" not in body, "D5: created COLLAPSED (no open state)"
|
||||
assert 'summary.textContent = "Thinking"' in body, (
|
||||
"the plain Thinking literal (D3 — the phase-17 literal; "
|
||||
"position distinguishes the runs) via textContent"
|
||||
assert "createThinkingRunEl()" in body, (
|
||||
"the D5 refinement: the in-container run is a plain div "
|
||||
"(never collapsible — the container is the ONE disclosure)"
|
||||
)
|
||||
assert "createToolRunEl()" in body, (
|
||||
"the D5 refinement: the in-container run is a plain div "
|
||||
"(never collapsible)"
|
||||
)
|
||||
assert "block.open = true" not in body, "the in-container runs never open"
|
||||
assert "toolDisc.open" not in body, (
|
||||
"the in-container runs are non-collapsible divs"
|
||||
)
|
||||
assert 'textEl.className = "thinking-text"' in body
|
||||
# the consecutive-tool grouping (one disclosure per run — the
|
||||
# grouping loop): create on the first entry, seal on a kind
|
||||
# change, insert before the bubble.
|
||||
assert "createToolDisclosure()" in body, "the shared phase-117 builder (innerHTML ABSENT)"
|
||||
assert "toolDisc.open = true" not in body, "D5: created COLLAPSED (no open state)"
|
||||
assert "toolDisc.open = false" in body
|
||||
# change, append into the container's body.
|
||||
assert "if (toolDisc) toolDisc = null" in body, (
|
||||
"a thinking entry seals the tool run — ONE disclosure per run (D1)"
|
||||
)
|
||||
@@ -682,11 +701,14 @@ def _single_escape_first_write(body: str) -> None:
|
||||
into ``.thinking-text`` (the existing restore convention;
|
||||
renderMarkdown's escape-first contract keeps storage
|
||||
XSS-safe)."""
|
||||
writes = re.findall(r"\b(\w+)\.innerHTML\s*=\s*([^;\n]+)", body)
|
||||
writes = re.findall(r"([\w.$()\"'-]+\.?)\.innerHTML\s*=\s*([^;\n]+)", body)
|
||||
assert len(writes) == 1, (
|
||||
f"exactly ONE innerHTML write (the structure is createElement): {writes}"
|
||||
)
|
||||
assert writes[0][0] == "textEl", "the single write is into .thinking-text"
|
||||
assert (
|
||||
writes[0][0].rstrip(".") == "textEl"
|
||||
or 'querySelector(".thinking-text")' in writes[0][0]
|
||||
), "the single write is into .thinking-text"
|
||||
assert writes[0][1].strip() == "renderMarkdown(entry.text)", (
|
||||
"the single write is the escape-first renderMarkdown"
|
||||
)
|
||||
@@ -694,19 +716,22 @@ def _single_escape_first_write(body: str) -> None:
|
||||
|
||||
def test_restore_timeline_structure_is_create_element_and_the_only_write_is_escape_first() -> None:
|
||||
"""innerHTML ABSENT from the STRUCTURE — it is built via
|
||||
createElement/textContent (the thinking block's summary is
|
||||
textContent; the disclosure rides the innerHTML-absent
|
||||
``createToolDisclosure`` — the task-02 pin). The ONLY HTML
|
||||
createElement/textContent (the in-container runs ride the
|
||||
innerHTML-absent D5-refinement run builders — the static
|
||||
label via textContent; the .tool-calls list, role=list +
|
||||
aria-label "Tool calls" — the task-02 pin). The ONLY HTML
|
||||
write in the function is the escape-first renderMarkdown into
|
||||
``.thinking-text`` (the existing restore convention — the
|
||||
phase-17 pattern; renderMarkdown's escape-first contract keeps
|
||||
storage XSS-safe)."""
|
||||
body = _fn(_js(), "restoreTimeline")
|
||||
_single_escape_first_write(body)
|
||||
assert "document.createElement(\"summary\")" in body, (
|
||||
"the summary is createElement + textContent"
|
||||
assert "createThinkingRunEl()" in body, (
|
||||
"the in-container run rides the innerHTML-absent run builder"
|
||||
)
|
||||
assert "createToolRunEl()" in body, (
|
||||
"the in-container run rides the innerHTML-absent run builder"
|
||||
)
|
||||
assert "document.createElement(\"div\")" in body, "the .thinking-text is createElement"
|
||||
|
||||
|
||||
def test_the_restore_dispatch_gives_segments_precedence_over_the_legacy_path() -> None:
|
||||
@@ -756,31 +781,37 @@ def test_the_restore_dispatch_gives_segments_precedence_over_the_legacy_path() -
|
||||
|
||||
def test_add_segments_builds_the_collapsed_parity_timeline() -> None:
|
||||
"""The shared-page parity copy (D5 — pure render, always
|
||||
COLLAPSED): the same structure (a per-run details.thinking with
|
||||
the plain ``Thinking`` literal via textContent + the phase-117
|
||||
disclosure structure — summary.tool-calls-summary + the
|
||||
.tool-calls list, role=list + aria-label "Tool calls"), the same
|
||||
LINE BYTES as ``addToolLines`` (the four pinned label literals
|
||||
+ the phase-70 legacy names + the phase-139 readRangeSuffix +
|
||||
the phase-95 truncation note), the per-run
|
||||
``Tool call(s) (N)`` count, and nesting inside the phase-161
|
||||
details.reasoning container (the local parity builder, created
|
||||
CLOSED and inserted ONCE before the bubble — the per-run bubble
|
||||
anchor is superseded, arrival order preserved) — all created
|
||||
CLOSED (no ``open = true`` anywhere; the addThinkingBlock /
|
||||
addToolLines collapsed convention)."""
|
||||
COLLAPSED): the same structure (a per-run plain div.thinking
|
||||
with the static ``.thinking-summary`` "Thinking" label via
|
||||
textContent + the phase-117 disclosure structure as a plain
|
||||
div — ``.tool-calls-summary`` + the .tool-calls list,
|
||||
role=list + aria-label "Tool calls"; the D5 refinement —
|
||||
NEVER collapsible, always visible while the container is
|
||||
open), the same LINE BYTES as ``addToolLines`` (the four
|
||||
pinned label literals + the phase-70 legacy names + the
|
||||
phase-139 readRangeSuffix + the phase-95 truncation note), the
|
||||
per-run ``Tool call(s) (N)`` count, and nesting inside the
|
||||
phase-161 details.reasoning container (the local parity
|
||||
builder, created CLOSED and inserted ONCE before the bubble —
|
||||
the per-run bubble anchor is superseded, arrival order
|
||||
preserved) — all created COLLAPSED (the container is the ONE
|
||||
disclosure; the addThinkingBlock / addToolLines collapsed
|
||||
convention)."""
|
||||
body = _fn(_js_shared(), "addSegments")
|
||||
assert "document.createElement(\"details\")" in body, "the structure is createElement"
|
||||
assert "document.createElement(\"div\")" in body, "the structure is createElement"
|
||||
assert 'block.className = "thinking"' in body
|
||||
assert "block.open = true" not in body, "D5: created COLLAPSED"
|
||||
assert "block.open = false" in body
|
||||
assert 'summary.textContent = "Thinking"' in body, "the plain Thinking literal (D3)"
|
||||
assert "block.open" not in body, (
|
||||
"D5 refinement: the in-container runs are non-collapsible divs"
|
||||
)
|
||||
assert 'label.textContent = "Thinking"' in body, "the plain Thinking literal (D3)"
|
||||
assert 'textEl.className = "thinking-text"' in body
|
||||
assert "renderMarkdown(entry.text)" in body, "the escape-first renderer (parity)"
|
||||
# the phase-117 disclosure structure.
|
||||
# the phase-117 disclosure structure (the D5 refinement — a plain
|
||||
# div, never collapsible).
|
||||
assert 'toolDisc.className = "tool-calls-disclosure"' in body
|
||||
assert "toolDisc.open = true" not in body, "D5: created COLLAPSED"
|
||||
assert "toolDisc.open = false" in body
|
||||
assert "toolDisc.open" not in body, (
|
||||
"D5 refinement: the in-container runs are non-collapsible divs"
|
||||
)
|
||||
assert 'summary.className = "tool-calls-summary"' in body
|
||||
assert 'container.className = "tool-calls"' in body
|
||||
assert 'setAttribute("role", "list")' in body
|
||||
|
||||
Reference in New Issue
Block a user