phase: 161_reasoning_bubble
All gates verified. Final check — AGENTS.md rules against the project (commands, gates, protocols) — the quick-reference commands all exist and the rules (PLAN §2/§7/§9 references, phase protocol, No CDN, Debugpy, git gates) accurately describe the project; the reasoning bubble is a pure UI change with no new commands or env vars, so no AGENTS.md change is needed.
**Verification complete — full results:**
- **Verified** (implemented by prior task passes, all in working tree): `createReasoningWrap`/`ensureReasoningWrap` live container (D1), `settleReasoning` fold at the 3 settle sites + not at delta (D2), 320px `.reasoning-body` window + pinned-tail follow (D3), zero persistence change (D4), collapsed restore + shared-page parity with D6 step counts, `styles.css` family treatment + reduced-motion still, mock `REASONING_MIDROUND_TRIGGER` flow, 6-test story suite, phase-158 pin updates
- **Full suite:** `uv run pytest` → **3738 passed, 0 failed** · `uv run pytest --cov=app --cov-report=term-missing` → **TOTAL 99% (>90%)** · `uv run ruff check .` → all checks passed · `uv run pyright` → **0 errors, 0 warnings**
- **E2E in isolation** (DB up; port-8123 collision with an unrelated `undercooked` service → `E2E_APP_PORT=8125`/`E2E_MOCK_PORT=8902`, no live change made): `test_reasoning_bubble.py` **6/6** · `test_thinking_timeline.py` ✓ · `test_thinking_display.py` ✓ · `test_tool_call_compact.py` ✓ · `test_big_read_progress.py` ✓ · `test_turn_progress_loader.py` ✓ · `test_share_chat.py` ✓ · `test_chat_persistence.py` ✓ · `test_smoke.py` ✓ (all exit 0, zero failures)
- **Completion criteria:** all 9 checked against the code + test outcomes — container open/live/nested ✓, stays open past mid-round delta ✓, folds at done ✓, restore + shared collapsed with "3 steps" ✓, plain turn = no container ✓, regression suites green ✓, suite/coverage/lint green ✓, diff scope limited to `frontend/assets/{app.js,shared.js,styles.css}` + `tests/` + `README.md` + phase files (no `app/` — D4) ✓
- **Documentation:** README.md "Thinking" section names the Reasoning bubble accurately (320px window, open-until-settle incl. mid-round deltas, "N steps", byte-identical no-container, collapsed on restore/shared) — verified consistent, no changes needed; AGENTS.md — verified consistent, no changes needed; all `.env.example` vars documented
- **Decisions:** commit + phase-dir move left to the harness (per pass rules — no git index/history commands executed); `undercooked` dev server on port 8123 untouched, E2E used the env port overrides
- **Next pending phase:** none — `161_reasoning_bubble` is the last in `todo/`
This commit is contained in:
@@ -0,0 +1,422 @@
|
||||
# Phase 161 — One live-open, condensed reasoning bubble
|
||||
|
||||
**Source:** `TODO.md` L3 — "The reasoning process should be one collapsible
|
||||
section that **stays open** during the reasoning process and shows all
|
||||
thinking and tool calls in order. Right now the thinking, tool calling,
|
||||
thinking, tool calling blocks take up a ton of space and obscure the answer.
|
||||
We need to condense this while still letting the user watch the thinking
|
||||
process happen so the latency doesn't feel super low. This new thinking/tool
|
||||
calling bubble should only collapse once the final response has been
|
||||
returned and the session has stopped."
|
||||
**Story:** n/a (owner TODO — 2026-10-05)
|
||||
**Context:** the DIRECT follow-up to completed `158_thinking_timeline`
|
||||
(the per-run `details.thinking` / `details.tool-calls-disclosure`
|
||||
stack, open-live/fold-at-rest) and `117_tool_call_compact` (the
|
||||
disclosure convention) + `17_thinking_display` (the 320px live-tail
|
||||
window + pinned-follow pattern). `frontend/assets/app.js` —
|
||||
`ensureThinkingRun` (~L1103, `body.insertBefore(block,
|
||||
body.querySelector(".bubble"))` — the per-run anchor),
|
||||
`ensureToolRun` (~L1122, the same anchor), `closeCurrentRun` (~L1140, the
|
||||
delta fold), `closeAllRuns` (~L1149, the turn's terminal settle),
|
||||
`segRuns` (~L1464, module scope, reset in `runTurn` ~L3304), the thinking
|
||||
frame branch (~L3381: `ensureThinkingRun` + the per-block
|
||||
`isThinkingNearBottom` pinned follow), the tool frame branch (~L3501:
|
||||
`ensureToolRun` + `appendToolLine` + `armToolLineClock`), the `delta`
|
||||
branch (~L3553: `closeCurrentRun()`), the `done` branch (~L3570:
|
||||
`closeAllRuns(wrap)`), `finalizeFailedTurn` (~L3218: `closeAllRuns(wrap)`),
|
||||
the STOP path (~L3752: `closeAllRuns(wrap)`); `restoreTimeline`
|
||||
(~L2195, the collapsed per-run restore, `insertBefore(block, bubble)`),
|
||||
the `renderStoredMessage` dispatch (~L2292: segments vs the byte-identical
|
||||
legacy path); `frontend/assets/shared.js` — `addSegments` (~L269, the
|
||||
collapsed parity copy), the record dispatch (~L584);
|
||||
`frontend/assets/styles.css` — the `details.thinking` block (L633, the
|
||||
320px window + the phase-17 pinned-follow convention), the
|
||||
`.tool-calls-disclosure` (L689), the reduced-motion still block (L1499);
|
||||
`tests/e2e/mock_llm.py` (`THINK_TOOLS_TRIGGER` L1067 +
|
||||
`_thinking_tools_flow` L1983 + `_tool_call_stream` L3956 — the sequential
|
||||
flow, extended by task 04 with the mid-round-delta flow);
|
||||
`tests/e2e/test_thinking_timeline.py` (the six phase-158 pins — the
|
||||
"direct children before the bubble" pins update in task 05);
|
||||
`tests/unit/test_thinking_timeline.py` (the source pins — the
|
||||
insertion-anchor pins update in task 05; the new container pins are
|
||||
task 01's).
|
||||
|
||||
## Owner decisions (chat, 2026-10-05 — confirmed 2026-10-05 — recorded per AGENTS.md rule 3)
|
||||
- **D1 — One wrapper, runs unchanged.** ONE `details.reasoning` container
|
||||
per brain message wraps the ENTIRE per-run timeline (the phase-158
|
||||
blocks keep their structure, order, and open/fold behavior). Created on
|
||||
the turn's FIRST thinking/tool frame, inserted before the bubble,
|
||||
`open = true` from creation until the turn settles. A turn with no
|
||||
thinking and no tools creates NO container (byte-identical DOM —
|
||||
`BOR_STREAM_THINKING=0` and plain-answer turns are untouched).
|
||||
- **D2 — Stays open through mid-round deltas.** The container folds ONLY
|
||||
at the existing settle points — `done`, stop/abort, failed-turn
|
||||
finalize — alongside `closeAllRuns`. A mid-round `delta` (the React
|
||||
style reply before the next round) still folds the ACTIVE RUN
|
||||
(`closeCurrentRun`, phase 158 D5) but the CONTAINER STAYS OPEN
|
||||
(the TODO's "only collapse once the final response has been returned
|
||||
and the session has stopped").
|
||||
- **D3 — Condensed while open (A2).** The container's `.reasoning-body`
|
||||
is ONE bounded scrollable live window — `max-height: 320px` (the
|
||||
phase-17 window convention) + `overflow-y: auto` — with
|
||||
pinned-tail autoscroll (the phase-17 `isThinkingNearBottom` pattern
|
||||
reused, extended to the container window; the inner per-run blocks are
|
||||
de-bordered inside the container and their OWN 320px scroll windows
|
||||
are disabled — `max-height: none` — so there is ONE scroll surface,
|
||||
not nested boxes; the user watches the live tail move without the
|
||||
timeline pushing the answer off-screen).
|
||||
- **D4 — Zero persistence change (A4).** The `segments` record, the
|
||||
`ChatMessage` schema, the SSE contract, and the history wire are ALL
|
||||
untouched (the container is a pure client render decision — the run
|
||||
order already persists through phase 158's `segments`). Records
|
||||
WITHOUT `segments` restore byte-identically through the untouched
|
||||
legacy path (no container, no new keys). No migration.
|
||||
- **D5 — Collapsed at rest (A5).** The restore path and the shared page
|
||||
render the SAME container COLLAPSED around the per-run timeline, with
|
||||
the at-rest step-count summary (D6). The live open state is
|
||||
exclusively the live turn's.
|
||||
- **D6 — The summary literals (A1).** The summary is the static literal
|
||||
`Reasoning` + a `span.reasoning-status`: live — `Thinking…` while a
|
||||
thinking frame streams, `Calling <name>…` on a tool frame (the
|
||||
display name the tool line already shows — the phase-70 legacy-name
|
||||
mapping); settled — `N steps` (N = the turn's RUN count —
|
||||
`segRuns.length`; restore/shared — the restored run count, the SAME
|
||||
semantics: consecutive thinking entries + one per contiguous tool
|
||||
group).
|
||||
|
||||
## Design (shared by all tasks — the executor reads this, not the chat)
|
||||
|
||||
### The container's STATIC structure (task 01's builder)
|
||||
```
|
||||
details.reasoning[open] (live) / details.reasoning (rest)
|
||||
summary.reasoning-summary
|
||||
(CSS chevron ::before — the family treatment, task 03)
|
||||
Reasoning
|
||||
span.reasoning-status ("Thinking…" / "Calling <name>…" / "N steps")
|
||||
div.reasoning-body (the bounded live window, task 03)
|
||||
details.thinking … (the phase-158 per-run blocks — UNCHANGED
|
||||
details.tool-calls-disclosure … inside, arrival order preserved)
|
||||
```
|
||||
- `createReasoningWrap()` — createElement only (innerHTML ABSENT — the
|
||||
house rule): `details` (class `reasoning`) + `summary` (class
|
||||
`reasoning-summary`, `textContent = "Reasoning"` — D6, the ONLY new
|
||||
static literal; the span is empty until a frame sets it) + `div`
|
||||
(class `reasoning-body`). The caller owns open/fold + insertion.
|
||||
- `ensureReasoningWrap(wrap)` — get-or-create per turn (module scope
|
||||
`reasoningWrapEl`, reset with `segRuns` in `runTurn` ~L3304 — the
|
||||
pagehide partial reads the LIVE container, so the module scope is
|
||||
readable there like `segRuns`): on the turn's first thinking/tool
|
||||
frame — `createReasoningWrap()`, `open = true` (D1), inserted
|
||||
`before` `body.querySelector(".bubble")` (the container is the ONE
|
||||
new anchor; the per-run blocks move from `insertBefore(bubble)` to
|
||||
`reasoningWrapEl.querySelector(".reasoning-body").appendChild(…)` —
|
||||
arrival order preserved: a new run is appended after the previous
|
||||
run's block).
|
||||
- `ensureThinkingRun` / `ensureToolRun` — the insertion line ONLY
|
||||
changes (append into the container's body); everything else (the
|
||||
per-run create, `open = true`, `segRuns` push, `currentRun`) is
|
||||
UNCHANGED.
|
||||
- `settleReasoning(wrap)` — the container fold:
|
||||
`wrap?.querySelector("details.reasoning")?.removeAttribute("open")`
|
||||
(idempotent; a no-op without a container — the plain-answer turn
|
||||
settles byte-identically). Called alongside `closeAllRuns(wrap)` at
|
||||
the THREE settle sites (the `done` branch ~L3570, the STOP path
|
||||
~L3752, `finalizeFailedTurn` ~L3218). The `delta` branch UNCHANGED
|
||||
(`closeCurrentRun()` — the active run folds; the container stays
|
||||
open — D2).
|
||||
- The live status (D6): the thinking frame branch sets
|
||||
`reasoningWrapEl`'s `.reasoning-status` `textContent = "Thinking…"`;
|
||||
the tool frame branch sets `"Calling <name>…"` (the `name` the
|
||||
`appendToolLine` call already receives); the settle sites set
|
||||
`"${segRuns.length} steps"`. The restore/shared builders set
|
||||
`"${N} steps"` directly (D5/D6 — no live state there).
|
||||
- The live follow (D3): the NEW small helpers — `isReasoningPinned(el)`
|
||||
(the pre-update capture — the phase-17 pre-measure convention:
|
||||
`el.scrollTop + el.clientHeight >= el.scrollHeight -
|
||||
REASONING_NEAR_BOTTOM_PX`, the same 32px band — a new constant
|
||||
`REASONING_NEAR_BOTTOM_PX = 32`) and `scrollReasoningToBottom(el)`
|
||||
(the post-append apply: `el.scrollTop = el.scrollHeight`). Called
|
||||
from BOTH the thinking frame branch (pre: before the block
|
||||
re-render; post: after it) and the tool frame branch (pre: before
|
||||
`appendToolLine`; post: after it) — `el` =
|
||||
`reasoningWrapEl?.querySelector(".reasoning-body")` (a no-op when
|
||||
absent). The EXISTING per-block `isThinkingNearBottom(textEl)`
|
||||
follow stays (harmless — the inner `.thinking-text` is no longer
|
||||
scrollable inside the container (task 03's `max-height: none`), so
|
||||
it's a no-op there; it keeps the legacy no-container path's
|
||||
behavior conceptually intact and costs nothing). The phase-87
|
||||
`armToolLineClock` / `settleToolLine` wrap-scoped queries are
|
||||
DESCENDANT-scoped — they work unchanged with the runs nested in
|
||||
the container (no change).
|
||||
- `closeAllRuns`'s selector (`details.thinking,
|
||||
details.tool-calls-disclosure`) is descendant-scoped to the wrap —
|
||||
the nested runs still match. UNCHANGED.
|
||||
|
||||
### `frontend/assets/app.js` — the restore path (task 02)
|
||||
- `restoreTimeline`: BEFORE the loop, get-or-create the container —
|
||||
`createReasoningWrap()`, `open = false` (D5 — the restore renders
|
||||
collapsed), the summary's `.reasoning-status` = `"${runs} steps"`
|
||||
(D6 — computed INCREMENTALLY: each `thinking` entry starts a run
|
||||
(count+1 when the previous is not a thinking entry — the contiguous
|
||||
semantics matching `segRuns.length`); each CONTIGUOUS tool group is
|
||||
ONE run (count+1 at the group's first entry)); the container is
|
||||
inserted `before` the bubble ONCE. Every per-run block appends into
|
||||
the container's `.reasoning-body` (the `insertBefore(block, bubble)`
|
||||
lines become appends — the rest of the loop — the malformed-entry
|
||||
skips, the tOffset/tLimit/tPath guards, the
|
||||
`appendTruncatedNote` call — is UNCHANGED).
|
||||
- The `renderStoredMessage` dispatch (~L2292) is UNCHANGED: a record
|
||||
WITHOUT `segments` never reaches `restoreTimeline` — the legacy path
|
||||
is byte-identical (no container — D4).
|
||||
- `frontend/assets/shared.js` — `addSegments`: the SAME wrapper parity
|
||||
copy (created CLOSED — the pure-render convention; the SAME
|
||||
incremental step count; the line bytes MIRROR `addToolLines` —
|
||||
unchanged). The record dispatch (~L584) is UNCHANGED (no `segments`
|
||||
→ the legacy `addThinkingBlock` + `addToolLines` path, byte-identical).
|
||||
|
||||
### `frontend/assets/styles.css` (task 03)
|
||||
- `details.reasoning` — the family treatment of `details.thinking`
|
||||
(L633): `background: var(--surface); border: 1px solid var(--line);
|
||||
border-left: 3px solid var(--brand-soft); border-radius:
|
||||
var(--radius-sm); margin: 0 0 0.5rem; overflow: hidden;` — a
|
||||
distinct comment block citing this phase.
|
||||
- `summary.reasoning-summary` — `display: flex; align-items: center;
|
||||
gap: 0.5rem; padding: 0.5rem 0.75rem; min-height: 44px;` (the
|
||||
WCAG ≥44px target); `color: var(--brand-ink)` (8.7:1 on
|
||||
`--surface` — AA, the token the `details.thinking` summary uses);
|
||||
`font-size: 0.9rem; cursor: pointer; list-style: none;`; the
|
||||
webkit marker `display: none`; the CSS chevron `::before` `▸`
|
||||
rotating 90° on `[open]` (the `details.thinking` convention; the
|
||||
transition stills in the reduced-motion block — task 03 appends it
|
||||
to the L1499 still list); `:focus-visible` → the house 3px
|
||||
`var(--brand)` ring + `outline-offset: 2px`.
|
||||
- `.reasoning-status` — `color: var(--ink-soft)` (6.9:1 on
|
||||
`--surface` — AA); `font-size: 0.8rem; line-height: 1.4; margin-left:
|
||||
0.25rem;` — text + color, never color alone (B5).
|
||||
- `.reasoning-body` — `padding: 0 0.75rem 0.75rem; max-height: 320px;
|
||||
overflow-y: auto;` (D3 — the ONE bounded live window; a
|
||||
user-scrollable window — the phase-17 convention, the comment
|
||||
cites this phase + D3).
|
||||
- The inner-run compacting (D3): `details.reasoning details.thinking`
|
||||
→ `border: 0; border-left: 2px solid var(--line);
|
||||
border-radius: 0; margin: 0.25rem 0; background: transparent;`
|
||||
(deboxed — the nested-border weight is removed);
|
||||
`details.reasoning details.thinking .thinking-text` → `max-height:
|
||||
none; overflow-y: visible;` (the inner scroll window is disabled —
|
||||
the container's window is the single scroll surface);
|
||||
`details.reasoning .tool-calls-disclosure` → `margin: 0.25rem 0;`
|
||||
(already compact — the rule documents the intent, no visual change).
|
||||
- The reduced-motion block (L1499) gains the `details.reasoning
|
||||
summary::before` / `.reasoning-summary::before` still (the
|
||||
chevron rotation stills — parity with the existing stills).
|
||||
- All colors are EXISTING AA-verified tokens (no new literal colors —
|
||||
the B5 token discipline; contrast ≥4.5:1 by construction).
|
||||
|
||||
### `tests/e2e/mock_llm.py` (task 04)
|
||||
- `REASONING_MIDROUND_TRIGGER = "think out loud, answer, then check
|
||||
your work"` — checked in the streaming branch BEFORE the
|
||||
`_thinking_tools_flow` dispatch (the more-specific-phrase-wins
|
||||
convention — the phrase CONTAINS `think out loud` + `check your
|
||||
work`… the executor MUST verify: (a) no existing trigger phrase is a
|
||||
substring of the new one AND no new phrase is a substring of an
|
||||
existing one in a way that steals an existing question's dispatch
|
||||
(the existing trigger is `think out loud and use your tools` — the
|
||||
new phrase shares only `think out loud`, and NEITHER is a substring
|
||||
of the other — verified: the existing continues `…and use your
|
||||
tools`, the new continues `…answer, then check…`), and (b) no
|
||||
existing E2E question carries the new phrase — verify both before
|
||||
wiring).
|
||||
- `_reasoning_midround_flow(body)` — stateless, shaped like
|
||||
`_thinking_tools_flow` (gated on the `tools` parameter + the
|
||||
`<tools>` section, like the other marker flows):
|
||||
- request 1 (tools offered, no tool results yet): `thinking` =
|
||||
`"Step 1: I think I know the answer — let me check the
|
||||
documents."` → a MID-ROUND `delta` = `"The reference server is
|
||||
ref-server-1."` (content stream — BEFORE the tool call — the
|
||||
React-style reply) → the `_tool_call_stream("ls", {}, "call_0",
|
||||
thinking="")` (the tool call AFTER the delta — the exact
|
||||
mid-round ordering the TODO describes).
|
||||
- request 2 (a `tool`-role result): `thinking` = `"Step 2: the
|
||||
listing confirms it."` → a `delta` = `" — confirmed by the
|
||||
listing."` → the `finish` (the `done` convention — the flow ends
|
||||
with a content finish; NO further tool call).
|
||||
- The mock stays mock-only by design (`E2E_REAL_LLM` note unchanged);
|
||||
the docstring gains the new trigger entry (the phase-161 flow).
|
||||
|
||||
### `tests/e2e/test_reasoning_bubble.py` (task 04 — dedicated suite, run in isolation)
|
||||
The conftest `app_server`/`page`/`seeded_kb` fixtures + the phase-37
|
||||
admin login + the mid-round trigger question (the KB must be seeded so
|
||||
the `ls` result exists — mirror `test_thinking_timeline.py`). Six
|
||||
tests:
|
||||
1. **`test_container_open_during_the_turn`** — during the turn (an
|
||||
`expect.poll` predicate over the mock's 0.1 s pacing window, before
|
||||
the answer bubble is complete): a `details.reasoning` exists, is
|
||||
`open`, contains ≥1 `details.thinking` or
|
||||
`details.tool-calls-disclosure` INSIDE its `.reasoning-body` (the
|
||||
per-run blocks are NESTED — the phase-158 direct-children layout is
|
||||
superseded), and its `.reasoning-status` carries a live suffix
|
||||
(`Thinking…` or `Calling …`).
|
||||
2. **`test_container_stays_open_past_a_mid_round_delta`** — the
|
||||
mid-round trigger: while the answer bubble is NON-EMPTY (the
|
||||
mid-round delta rendered) AND the turn is not `done`, the container
|
||||
is STILL `open` with a run visible (the D2 contract — the TODO's
|
||||
"only collapse once the final response has been returned" — the
|
||||
phase-158 first-delta-fold is superseded at the container level).
|
||||
3. **`test_container_folds_at_done`** — after `done`: the container
|
||||
has NO `open` attribute; its summary is visible; EVERY inner run
|
||||
has no `open` attribute (the D2/D5 settle — the phase-158
|
||||
fold-at-rest contract preserved inside the fold).
|
||||
4. **`test_restored_container_collapsed_with_step_count`** — RELOAD
|
||||
(same context): the restored message renders ONE `details.reasoning`
|
||||
(NO `open` attribute) containing the same per-run timeline, and its
|
||||
`.reasoning-status` is `3 steps` (the mid-round flow's run count —
|
||||
the D6 at-rest count: the flow has thinking → delta → tool →
|
||||
thinking → delta: the runs are `thinking (Step 1)`, `tool (ls)`,
|
||||
`thinking (Step 2)` = 3 contiguous runs — matching
|
||||
`segRuns.length`; the semantics is what's pinned: contiguous-run
|
||||
count).
|
||||
5. **`test_shared_page_container_collapsed`** — the phase-51 save +
|
||||
share flow, a FRESH context on `/shared/<token>`: ONE
|
||||
`details.reasoning` (collapsed) with the same timeline + step count
|
||||
(the `shared.js` parity — `addSegments`).
|
||||
6. **`test_no_reasoning_turn_has_no_container`** — a plain answer
|
||||
question: ZERO `details.reasoning` in the brain wrap (D1/D4 — the
|
||||
byte-identical DOM); and a plain `use your tools` question (NO
|
||||
thinking trigger): the container EXISTS (the tool frames create it),
|
||||
wraps the ONE phase-117 disclosure, is `open` live, folds at `done`
|
||||
(the no-thinking turn's contract — the phase-117 single-disclosure
|
||||
behavior preserved, now wrapped).
|
||||
|
||||
## Tasks
|
||||
1. `01_live_container.md` — the container's static builder + the
|
||||
get-or-create live wrapper (D1) + the re-anchored per-run inserts +
|
||||
the `settleReasoning` fold at the three settle sites (D2) + the
|
||||
summary live status (D6) + the live follow (D3) + the JS source
|
||||
unit pins.
|
||||
2. `02_restore_and_shared.md` — the collapsed restore wrapper (D5/D6,
|
||||
the incremental step count) + the `shared.js` `addSegments` parity +
|
||||
the unit pins.
|
||||
3. `03_container_css.md` — the `details.reasoning` styling family, the
|
||||
bounded live window, the inner-run compacting, the reduced-motion
|
||||
stills.
|
||||
4. `04_mock_and_e2e.md` — the mock `REASONING_MIDROUND_TRIGGER` flow +
|
||||
the dedicated `test_reasoning_bubble.py` story suite, run in
|
||||
isolation.
|
||||
5. `05_regression_update_and_commit.md` — the phase-158 suite pin
|
||||
updates (the "direct children before the bubble" pins → the
|
||||
container-nested contract), the remaining unit-pin updates, the full
|
||||
gate, the documentation pass, the atomic commit, the phase move.
|
||||
|
||||
## Testing & Quality
|
||||
- Unit (new, task 01): `tests/unit/test_reasoning_bubble.py` — the
|
||||
source-level pins: `createReasoningWrap`'s createElement shape
|
||||
(innerHTML ABSENT), `ensureReasoningWrap`'s get-or-create + reset in
|
||||
`runTurn`, `open = true` at creation, the per-run inserts append
|
||||
into `.reasoning-body` (the `insertBefore(bubble)` per-run anchor is
|
||||
GONE from `ensureThinkingRun`/`ensureToolRun`), `settleReasoning`
|
||||
defined + called at the THREE settle sites (done / stop /
|
||||
finalizeFailedTurn) and NOT at the `delta` branch (D2 — the
|
||||
`closeCurrentRun` there is unchanged), the status literals
|
||||
(`Reasoning` / `Thinking…` / `Calling ` / ` steps`), the
|
||||
`followReasoningBody` pin + the `REASONING_NEAR_BOTTOM_PX` constant,
|
||||
and the `innerHTML` ABSENT from the wrapper builder.
|
||||
- Unit (task 02): `tests/unit/test_reasoning_bubble.py` (extended) — the
|
||||
restore wrapper (created CLOSED, the incremental step count — a
|
||||
thinking entry + a contiguous tool group = the run semantics; the
|
||||
legacy no-segments path is byte-identical — `restoreTimeline`
|
||||
unreachable without segments), the `shared.js` `addSegments` parity
|
||||
(the collapsed wrapper + the step count + the unchanged line bytes),
|
||||
and `innerHTML` ABSENT from the restore/shared wrappers.
|
||||
- E2E: `tests/e2e/test_reasoning_bubble.py` (new — the six tests
|
||||
above), green **in isolation** (`uv run pytest
|
||||
tests/e2e/test_reasoning_bubble.py -v --no-cov`), mock-only.
|
||||
- Regression E2E (task 05, in isolation): `test_thinking_timeline.py`
|
||||
(UPDATED in task 05 — the direct-children pins → the container-nested
|
||||
contract; the open/fold + the per-run distinctness contracts
|
||||
unchanged), `test_thinking_display.py`, `test_tool_call_compact.py`
|
||||
(the no-thinking single-disclosure contract — now inside the
|
||||
container), `test_big_read_progress.py` (the phase-87 clock — the
|
||||
wrap-scoped descendant queries are unchanged — must stay green
|
||||
UNCHANGED), `test_turn_progress_loader.py`, `test_share_chat.py`,
|
||||
`test_chat_persistence.py`, `test_smoke.py`.
|
||||
- Existing unit suites: every pin NOT broken by the wrapper (the
|
||||
tool-line label literals, the phase-117 deboxed CSS, the phase-87
|
||||
`.tool-elapsed` behavior, the phase-139/146 suffix pins, the emoji
|
||||
guard) stays green; the phase-158 insertion-anchor pins update in
|
||||
task 05 (the documented change — the per-run anchor moved into the
|
||||
container).
|
||||
- Coverage: `uv run pytest --cov=app --cov-report=term-missing`
|
||||
**>90%** on `app/` (this phase is FRONTEND-ONLY — `app/` is
|
||||
untouched; the gate stays green by the suite).
|
||||
- Lint/types: `uv run ruff check . && uv run pyright` clean.
|
||||
- The phase is "Complete" only when the **entire** project test suite
|
||||
passes 100% (every test in the project, not just this phase's).
|
||||
|
||||
## Documentation & Consistency
|
||||
- The final pass reviews `README.md` and `AGENTS.md` in full against
|
||||
the project as it stands after this phase — fixing every
|
||||
inconsistency found, **including pre-existing ones not caused by the
|
||||
phase**.
|
||||
- Tasks update `README.md`/`AGENTS.md` immediately when their own
|
||||
changes are user-visible (the reasoning bubble is a visible UI
|
||||
change — name it where the chat thinking/tool-call display is
|
||||
described, if anywhere).
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] A live reasoning turn renders ONE `details.reasoning` (open)
|
||||
wrapping the ordered per-run timeline, condensed in the bounded
|
||||
320px live window with pinned-tail follow (E2E tests 1 + 6 —
|
||||
task 04); it STAYS OPEN through a mid-round delta (test 2 — the
|
||||
TODO's core contract) and folds at `done` (test 3).
|
||||
- [ ] A reload restores the collapsed container with the step-count
|
||||
summary (test 4); a fresh context on `/shared/<token>` renders
|
||||
the same (test 5).
|
||||
- [ ] A plain answer turn creates NO container (test 6) — the
|
||||
byte-identical DOM; a pre-phase saved record (no `segments`)
|
||||
restores byte-identically through the untouched legacy path (the
|
||||
regression suites + the unit pin).
|
||||
- [ ] `test_thinking_timeline.py` (updated pins) +
|
||||
`test_thinking_display.py` + `test_tool_call_compact.py` +
|
||||
`test_big_read_progress.py` + `test_turn_progress_loader.py`
|
||||
green in isolation.
|
||||
- [ ] `uv run pytest` green (100% of the project suite);
|
||||
`uv run pytest --cov=app --cov-report=term-missing` > 90%;
|
||||
`tests/e2e/test_reasoning_bubble.py` green in isolation;
|
||||
`uv run ruff check . && uv run pyright` clean.
|
||||
- [ ] `git diff --stat` limited to `frontend/assets/app.js`,
|
||||
`frontend/assets/shared.js`, `frontend/assets/styles.css`,
|
||||
`tests/`, and the phase files (NO `app/` change — D4).
|
||||
- [ ] README.md and AGENTS.md are accurate and consistent with the
|
||||
whole project (verified in the final pass).
|
||||
- [ ] ONE owner-directed exception to "no behavior change in completed
|
||||
phases": the phase-158 per-run blocks' direct-children layout is
|
||||
SUPERSeded by the container nesting (owner TODO direction
|
||||
2026-10-05 — the `test_thinking_timeline.py` pins update in task
|
||||
05); the per-run open/fold behavior itself is UNCHANGED.
|
||||
- [ ] One atomic `--no-gpg-sign` commit (below); the phase dir moved
|
||||
to `.agents/phases/complete/`.
|
||||
|
||||
## Commit
|
||||
```bash
|
||||
git add -A .agents/ frontend/ tests/ README.md && git commit --no-gpg-sign -m "feat(ui): condense the reasoning timeline into one live-open collapsible bubble (folds at turn end, collapsed on restore/shared)"
|
||||
```
|
||||
|
||||
## Locked decisions
|
||||
- **A15 UNCHANGED — the SSE contract is untouched.** No new event type,
|
||||
no frame change; the container is pure client grouping/render.
|
||||
`app/` is untouched (D4).
|
||||
- **D1–D6 (owner, 2026-10-05 — confirmed 2026-10-05)** — the one
|
||||
wrapper with unchanged per-run blocks, the stays-open-past-mid-round
|
||||
deltas, the condensed live window (320px, one scroll surface,
|
||||
pinned-tail follow), the zero persistence change, the collapsed-at
|
||||
rest, and the summary literals.
|
||||
- **A13 UNCHANGED** — no migration (the record is opaque JSONB; no new
|
||||
key — the `segments` field already carries the run order).
|
||||
- **A16 honoured** — one new story E2E suite + the task-05 regression
|
||||
updates; mock-only by design. **A17 honoured** — one atomic
|
||||
`--no-gpg-sign` commit.
|
||||
- **The phase-158 `closeAllRuns` / `closeCurrentRun` / the phase-87
|
||||
clock are UNCHANGED** (descendant-scoped queries — they work with
|
||||
the nesting); the ONLY superseded anchor is the per-run
|
||||
`insertBefore(bubble)` → the container's `.reasoning-body` append.
|
||||
No other anchor changed.
|
||||
Reference in New Issue
Block a user