phase: 161_reasoning_bubble
Build and Push Containers / build-and-push-app (push) Successful in 1m55s
Build and Push Containers / build-and-push-db (push) Successful in 14s

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:
2026-10-06 06:36:09 -04:00
parent 8172956c15
commit 051f62deb7
48 changed files with 4194 additions and 166 deletions
+27 -11
View File
@@ -438,16 +438,31 @@ whole git-source refresh in one click:
The self-hosted `turbo` model reasons before it answers. That reasoning is
streamed with the turn as `thinking` SSE events and shown in **
collapsible "Thinking" blocks** above the answer bubble — one block per
contiguous thinking run, in arrival order (phase 158): the turn renders
as a chronological timeline (Thinking → Tool call (1) → Thinking → … →
answer). Each block opens and fills in live while its run streams,
and the runs tuck themselves away when the first answer token lands,
when the turn ends, or when the turn is stopped/failed — and every
block and tool-call disclosure stays click-toggleable afterwards. A
thinking run that starts after the answer began (the model replied,
called a tool, then thinks again) gets its own block above the answer
instead of resuming on top of it.
collapsible "Thinking" blocks** — one block per
contiguous thinking run, in arrival order (phase 158) — all wrapped in
**one collapsible "Reasoning" bubble** above the answer bubble
(phase 161): the turn renders as a chronological timeline
(Thinking → Tool call (1) → Thinking → … → answer), condensed into a
single bounded live window (320 px, one scroll surface) that follows
the tail while you watch.
The bubble is created on the turn's first thinking (or tool) frame and
stays **open** from creation until the turn settles — including a
mid-round reply (the model answered, called a tool, then thinks again:
the reply streams into the answer while the bubble stays open, and the
new thinking run gets its own block inside it instead of resuming on
top of it). It folds when the turn ends, is stopped, or fails. While
open, its summary carries a live status ("Thinking…" /
"Calling <name>…") and reads "Reasoning — N steps" once the turn
settles; a turn with no thinking and no tools renders no bubble at all
(the answer layout is byte-identical to pre-phase).
The per-run blocks keep their own open/fold behavior: each block opens
and fills in live while its run streams, and the runs tuck themselves
away when the first answer token lands, when the turn ends, or when the
turn is stopped/failed — and every block and tool-call disclosure stays
click-toggleable afterwards (the bubble's summary is the at-rest entry
point).
The tool calls ride the same timeline: each contiguous run of tool
calls gets its own collapsed-when-idle "Tool call(s) (N)" disclosure
@@ -457,7 +472,8 @@ suffix targets the newest line of the active run.
The timeline is persisted with the saved turn (an optional `segments`
array — the per-run order; the concatenated `thinking` and the `tools`
record stay for history and older clients), so a reload and the shared
page re-render the same timeline, collapsed.
page re-render the same timeline **collapsed inside the Reasoning
bubble**, with the step-count summary.
To hide it, set `BOR_STREAM_THINKING=0`.