Files
brain-of-reese/.agents/user_stories/thinking-display.md
T
ducoterra dbf2af26c6 refactor(agents): migrate .agent/ planning tree to .agents/
Standardize on the .agents/ directory (shared with project skills):
phases/, user_stories/, reports/, screenshots/, validate.sh, and
phase-sessions/ + pipeline.log all move to .agents/ (git mv preserves
history; runtime artifacts move alongside).

Updates every reference in AGENTS.md, README.md, .gitignore, app
docstrings, and test story headers. Historical KB content in data/
and the runtime pipeline.log transcript are left untouched.
2026-09-05 10:57:07 -04:00

104 lines
5.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
1. 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.
2. Turns without reasoning render exactly as before (no block, no layout
shift).
3. Thinking-without-answer (reasoning exhausted the token budget) shows
the existing empty-answer fallback with the thinking block preserved.
4. Thinking persists with the message (phase 14) and restores collapsed
after reload; "New chat" clears it with everything else.
5. Deflected turns show the amber bubble + "Maybe try" chips alongside the
thinking block (honesty gate untouched).
6. A stream that dies mid-thinking/mid-answer ends in the error state
(retry hint) — never a silent half bubble.
7. `BOR_STREAM_THINKING=0` suppresses `thinking` events server-side (log
line still counts `thinking_chars`).
## UI Visualization & Structure
- **DOM contract** (built dynamically — `index.html` is unchanged): inside
`.msg-body`, **before** `.bubble`:
```html
<details class="thinking" open>
<summary>Thinking</summary>
<div class="thinking-text"></div>
</details>
```
The block is created **open** at the first `thinking` event and
auto-collapses at the first answer token; a late/interleaved `thinking`
event 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` — `--surface`
background, 1px `--line` border, 3px `--brand-soft` left accent,
`--radius-sm`; `summary` — `--brand-ink` on surface (≈**8.7:1**), 0.9rem,
**min-height 44px** touch target, CSS chevron `▸` rotating 90° when open
(0.15s transform, stills under `prefers-reduced-motion`),
`:focus-visible` 3px `--brand` outline; `.thinking-text` — `--ink-soft`
on surface (≈**6.9:1**), 0.875rem/1.55, **max-height 320px** with
`overflow-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-status` region
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 first `thinking`
**or** `delta` event.
- **Stream-drop guard:** a stream that ends without a `done` event 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):
1. `test_thinking_block_streams_open_then_collapses` — submit
`THINK_QUESTION`. Assert `details.thinking` inside the last
`.msg.brain` attaches within 10s (it appears at the first `thinking`
event); 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-text` is non-empty; once
`.bubble` text is non-empty, assert the block is **closed**; at
settle: `.thinking-text` contains `Step 2: Check my notes`, the bubble
contains the mock's deterministic answer sentence, `.source-chip`
count ≥ 1, send button re-enabled.
2. `test_thinking_toggle_after_done` — after a settled `THINK_QUESTION`
turn, the block is closed; click `summary` → `details[open]` and the
full thinking text is visible; click again → closed. (Real
keyboard-focusable control.)
3. `test_thinking_restored_after_reload` — settle a `THINK_QUESTION`
turn; capture the thinking text; `page.reload()`; the restored
conversation contains the brain message with a **closed**
`details.thinking` whose `.thinking-text` matches the captured text,
and the answer bubble + source chips are intact (phase-14 restore
path).
4. `test_no_thinking_block_without_trigger` — submit `PLAIN_QUESTION`; at
settle: `page.locator("details.thinking")` count is 0 (a model that
doesn't think renders exactly as before — no layout regression).
5. `test_thinking_with_deflection` — submit `THINK_DEFLECT_QUESTION`; at
settle: the brain message has `.is-deflected`, a `.maybe-try` group
with chips, and a closed `details.thinking` whose text contains
`Step 2: Check my notes` (thinking and the honesty gate coexist).