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
+45 -8
View File
@@ -144,6 +144,10 @@ SUMMARY = "#messages .msg.brain .tool-calls-summary"
DISCLOSURE = "#messages .msg.brain .tool-calls-disclosure"
LINES = "#messages .msg.brain .tool-call"
#: Phase 161 (D1/D2): the ONE reasoning container — created on the
#: turn's first tool frame, OPEN live, folded at the settle (D2).
REASONING = "#messages .msg.brain details.reasoning"
# --------------------------------------------------------------------------
# DB seeding (TRUNCATE-then-seed, cf. test_agent_document_tools.py)
# --------------------------------------------------------------------------
@@ -261,7 +265,12 @@ 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."""
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."""
page.locator(f"{REASONING} > summary").click()
page.locator(SUMMARY).click()
@@ -274,16 +283,27 @@ def test_tool_calls_fold_to_one_line_after_turn(
page: Page, app_url: str, mock_llm: int, db_ready: None
) -> None:
"""Pin 1 (the space fix): after the deterministic ``ls`` →
``ls(scoped)`` → ``read`` turn COMPLETES, the record is ONE visible
"Tool calls (3)" summary — the disclosure is CLOSED (no ``open``
``ls(scoped)`` → ``read`` turn COMPLETES, the record is ONE
visible summary line — the disclosure is CLOSED (no ``open``
attribute: the first answer delta folded it, D3), the three
``.tool-call`` lines are still in the DOM (the permanent record)
but hidden (the first line is not visible), and the answer bubble
is present."""
is present. Phase 161 (D2): the at-rest record is wrapped in the
FOLDED reasoning container — the at-rest visible line is the
container's "Reasoning" summary; the disclosure's "Tool calls (3)"
count is the in-container record, revealed by opening it."""
page.set_default_timeout(30_000)
_reset_db(_seed)
_submit_tools_turn(page, app_url)
# Phase 161 (D2): the container exists (the tool frames created
# it) and folds at done — the at-rest visible line is its summary.
container = page.locator(REASONING)
expect(container).to_have_count(1)
expect(container).not_to_have_attribute("open")
page.locator(f"{REASONING} > summary").click()
expect(container).to_have_attribute("open", "")
# One compact summary line, visible, with the live count.
summary = page.locator(SUMMARY)
expect(summary).to_be_visible()
@@ -396,10 +416,15 @@ def test_restored_turn_renders_folded(
record is written at the save points, so the reload sees the full
conversation), a same-context RELOAD re-renders the persisted
record through the restore path (``renderStoredMessage`` →
``appendToolLine`` → the after-loop fold): the disclosure is
``restoreTimeline`` → the after-loop fold): the disclosure is
present, CLOSED (no ``open`` attribute — no auto-expand on load),
the summary reads "Tool calls (3)", the three lines are in the DOM
and the first is hidden (folded on load)."""
and the first is hidden (folded on load). Phase 161 (D2/D5): the
restored record is wrapped in the FOLDED reasoning container —
the at-rest visible line is the container's "Reasoning" summary;
the disclosure's "Tool calls (3)" count is the in-container
record, revealed by opening the container (the same two-native-
toggle user flow as the live at-rest pins)."""
page.set_default_timeout(30_000)
_reset_db(_seed)
_submit_tools_turn(page, app_url)
@@ -413,10 +438,22 @@ def test_restored_turn_renders_folded(
page.locator(".msg.brain .bubble").last
).to_contain_text(MOCK_ANSWER_MARKER, timeout=30_000)
# Folded on load: one compact summary line, disclosure closed.
# Folded on load (phase 161, D2/D5): the record is wrapped in the
# FOLDED reasoning container — the at-rest visible line is the
# container's "Reasoning" summary.
container = page.locator(REASONING)
expect(container).to_have_count(1)
expect(container).not_to_have_attribute("open")
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
page.locator(f"{REASONING} > summary").click()
expect(page.locator(SUMMARY)).to_be_visible()
expect(page.locator(SUMMARY)).to_contain_text("Tool calls (3)")
assert page.get_attribute(DISCLOSURE, "open") is None
# The three lines are in the DOM (the permanent record) but folded:
# the first line is not rendered — no auto-expand.