Remove the blanket .agent/ gitignore so the phase roadmap, user stories, reports, and PLAN.md are versioned with the code. Only runtime artifacts (.agent/phase-sessions/, .agent/pipeline.log) remain ignored. Update AGENTS.md git protocol rule to match.
5.5 KiB
5.5 KiB
Story: Thinking Display
Phase: 17_thinking_display · E2E: tests/e2e/test_thinking_display.py
Narrative
As a user, my local reasoning model thinks before it answers — and today those 10–30 seconds are pure silence. I want to see Brain think: its reasoning streaming live, tucked away once the answer starts — so long turns feel transparent instead of frozen.
- Given I submit a question the model reasons about
- When the turn streams reasoning ahead of (or around) the answer
- Then a collapsible "Thinking" block streams open above the answer bubble, auto-collapses on the first answer token, and stays user-toggleable afterwards — while turns that emit no reasoning render exactly as before.
Acceptance criteria
- Turns whose model stream carries reasoning show a "Thinking" block (collapsible, above the answer bubble) that streams open and auto-collapses on the first answer token; always user-toggleable afterwards.
- Turns without reasoning render exactly as before (no block, no layout shift).
- Thinking-without-answer (reasoning exhausted the token budget) shows the existing empty-answer fallback with the thinking block preserved.
- Thinking persists with the message (phase 14) and restores collapsed after reload; "New chat" clears it with everything else.
- Deflected turns show the amber bubble + "Maybe try" chips alongside the thinking block (honesty gate untouched).
- A stream that dies mid-thinking/mid-answer ends in the error state (retry hint) — never a silent half bubble.
BOR_STREAM_THINKING=0suppressesthinkingevents server-side (log line still countsthinking_chars).
UI Visualization & Structure
- DOM contract (built dynamically —
index.htmlis unchanged): inside.msg-body, before.bubble:The block is created open at the first<details class="thinking" open> <summary>Thinking</summary> <div class="thinking-text"></div> </details>thinkingevent and auto-collapses at the first answer token; a late/interleavedthinkingevent only appends to the block — it never reopens once the answer started. Restored messages (phase 14) render the block collapsed. - Styling (Phase-08 tokens):
details.thinking—--surfacebackground, 1px--lineborder, 3px--brand-softleft accent,--radius-sm;summary—--brand-inkon surface (≈8.7:1), 0.9rem, min-height 44px touch target, CSS chevron▸rotating 90° when open (0.15s transform, stills underprefers-reduced-motion),:focus-visible3px--brandoutline;.thinking-text—--ink-softon surface (≈6.9:1), 0.875rem/1.55, max-height 320px withoverflow-y: auto(long scratchpads scroll instead of pushing the answer off-screen) and tightened paragraph/list margins. - Accessibility: native
<details>/<summary>— a real, keyboard-focusable control with native open/closed announcements. No live region on the scratchpad (announcing every reasoning chunk would be hostile to screen readers); the existing#send-statusregion announces turn state as before. - Feedback contract: while reasoning streams, the block is the
visible "thinking" feedback (the typing dots are removed); the UI state
stays
thinking(button disabled with "Thinking…", status text unchanged). The 120s pre-token guard clears on the firstthinkingordeltaevent. - Stream-drop guard: a stream that ends without a
doneevent after at least one thinking/delta frame settles into the error state ("The stream ended before my answer finished — try again?") — never a silent half bubble.
Playwright Mapping Rule
Test Scenario → tests/e2e/test_thinking_display.py (mock LLM,
"think out loud" trigger):
test_thinking_block_streams_open_then_collapses— submitTHINK_QUESTION. Assertdetails.thinkinginside the last.msg.brainattaches within 10s (it appears at the firstthinkingevent); immediately after attach, assert it is open (the mock's ~800-char thinking stream gives a multi-second open window — see the suite's determinism note) and.thinking-textis non-empty; once.bubbletext is non-empty, assert the block is closed; at settle:.thinking-textcontainsStep 2: Check my notes, the bubble contains the mock's deterministic answer sentence,.source-chipcount ≥ 1, send button re-enabled.test_thinking_toggle_after_done— after a settledTHINK_QUESTIONturn, the block is closed; clicksummary→details[open]and the full thinking text is visible; click again → closed. (Real keyboard-focusable control.)test_thinking_restored_after_reload— settle aTHINK_QUESTIONturn; capture the thinking text;page.reload(); the restored conversation contains the brain message with a closeddetails.thinkingwhose.thinking-textmatches the captured text, and the answer bubble + source chips are intact (phase-14 restore path).test_no_thinking_block_without_trigger— submitPLAIN_QUESTION; at settle:page.locator("details.thinking")count is 0 (a model that doesn't think renders exactly as before — no layout regression).test_thinking_with_deflection— submitTHINK_DEFLECT_QUESTION; at settle: the brain message has.is-deflected, a.maybe-trygroup with chips, and a closeddetails.thinkingwhose text containsStep 2: Check my notes(thinking and the honesty gate coexist).