Files
brain-of-reese/.agent/user_stories/thinking-scroll-back.md
T
ducoterra 02c76ad328 chore(agent): phase roadmap from TODO.md — 8 phases (40–47), 24 tasks
Converts the 9 TODO items into an executable phase roadmap (Protocol B,
appended after phase 39):

- 40 tuning toggle anonymous flash (TODO L3)
- 41 sync fail-fast + modal when a model is down (TODO L4)
- 42 no reply autoscroll (TODO L5)
- 43 thinking scroll back — user scroll + gated autoscroll (TODO L7)
- 44 markdown tables (TODO L6)
- 45 agent unlimited tool calls behind BOR_AGENT_MAX_ROUNDS (TODO L8)
- 46 mobile hamburger nav (TODO L9)
- 47 quadlet + jinja import formats, A9 revision (TODO L10–L11)

Each phase carries a user story, a dedicated Playwright E2E suite plan,
and owner-locked decisions (R1 A9 format extension, R2 phase-37 budget
revision, A1–A5 scope decisions) confirmed 2026-08-27.

Also records the completed phases 30–39 todo/ -> complete/ moves that
were pending in the working tree. TODO.md is cleared (items now live in
.agent/phases/todo/).
2026-08-27 18:25:53 -04:00

99 lines
5.1 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 scroll back (user scroll + generate-time autoscroll)
**Phase:** `43_thinking_scroll_back` · **Source:** `TODO.md` L7 ·
**E2E:** `tests/e2e/test_thinking_scroll.py`
## Bug report (verbatim, `TODO.md` L7)
> "Add scrolling back to the thinking block, but have it autoscroll
> while thinking content is generating."
## Narrative
As **a user watching Brain reason**, the Thinking block should work like
a well-behaved live console: it **follows the tail while the reasoning
is generating** — but the moment I scroll up to re-read an earlier line,
it must **stop yanking me down**, and it must let me scroll the window
freely (phase 21's no-scroll clip is being reversed by owner direction).
- **Given** the Thinking block is streaming reasoning content
- **When** I'm at the bottom of the 320px window
- **Then** each new chunk keeps the window pinned to the live tail.
- **When** I scroll up to read earlier reasoning
- **Then** the window stays where I put it (no more re-pinning);
- **When** I return to the bottom
- **Then** tail-following resumes on the next chunk.
## Acceptance criteria
1. **User scroll restored:** computed `overflow-y` of
`.thinking-text` is `auto`; wheel / mouse-drag / keyboard move the
window (phase 21's `overflow-y: hidden` is gone).
2. **Follow while generating:** with the user pinned at the window's
bottom (within a small near-bottom band — the phase-18 pattern,
now applied to the *window* instead of the page, exported constant
`THINKING_NEAR_BOTTOM_PX = 32`), each streamed chunk re-pins the
window to the tail (within 1px).
3. **Paused on scroll-up:** scrolled up, the window stops being re-pinned
— `scrollTop` stays stable across subsequent chunks (within 1px).
4. **Resumes on return:** scrolling back to the bottom (within the band)
resumes tail-following on the next chunk.
5. **Kept from phases 17/21:** the fixed 320px `max-height` window, the
auto-collapse on the first answer token, the reduced-motion stillness,
and the answer-bubble scroll behavior (phase 11) are all unchanged.
## Owner-confirmed (2026-08-27, roadmap A2)
1. **The 320px window stays** — only the overflow mode and the pinning
logic change.
2. **Follow-the-bottom for the window:** autoscroll only while the user
is pinned near the window's bottom (≈32px band); scroll-up pauses,
return-to-bottom resumes. (The phase-18 page-level band is removed in
phase 42; this is its window-level successor.)
## UI Visualization & Structure
- **CSS** (`frontend/assets/styles.css`): `details.thinking
.thinking-text` — `overflow-y: hidden` → `overflow-y: auto`; the
owner-choice comment is replaced with the 2026-08-27 direction
(user-scrollable window; JS follows the tail only while pinned).
`max-height: 320px` and all other declarations untouched.
- **JS** (`frontend/assets/app.js`, the `thinking` SSE handler):
- new exported `const THINKING_NEAR_BOTTOM_PX = 32` +
`isThinkingNearBottom(textEl)` (`scrollHeight - scrollTop -
clientHeight <= band`);
- the phase-17 unconditional pin
(`textEl.scrollTop = textEl.scrollHeight`) becomes gated:
`if (block.open && isThinkingNearBottom(textEl)) { textEl.scrollTop
= textEl.scrollHeight; }` — a scrolled-up user is never re-pinned,
and returning to the bottom re-arms the pin automatically (the check
runs on every chunk).
- **Non-goals:** no "↓ more" affordance, no auto-height growth, no change
to the summary/chevron, the tool-call lines, or the answer bubble.
## Playwright Mapping Rule
**Test Scenario → `tests/e2e/test_thinking_scroll.py`** (mock LLM; DB
up; phase 21 lengthened `mock_llm.compose_thinking` to ~2 700 chars ≈
4.5 s of paced frames so the scratchpad overflows the 320px window by
~2×, and the phase-20 hesitation trigger gives a deterministic 4 s
frozen-tail state with the block open). The phase-21 suite
`tests/e2e/test_thinking_no_scroll.py` is **deleted** in this phase:
1. `test_thinking_window_user_scrollable` — frozen live tail (4 s
hesitation): focus `.thinking-text`, wheel up, `Home`, mouse-drag up
— `scrollTop` moves; the window shows earlier content.
2. `test_thinking_window_follows_while_pinned` — during the live stream:
with the user at the bottom, after the 2nd-to-last and the last chunk
the window is pinned to the tail (within 1px) and the last chunk's
text renders inside the visible rectangle.
3. `test_thinking_window_stops_on_scroll_up` — mid-stream: scroll up
~half the window; over the next ≥5 chunks `scrollTop` stays stable
(within 1px) — no re-pin.
4. `test_thinking_window_resumes_on_return` — from the paused state,
scroll the window back to its bottom; on the next chunk the window is
re-pinned to the tail (within 1px).
5. `test_thinking_window_css_contract` — computed `overflow-y: auto`,
`max-height: 320px`, and the clip is real (`scrollHeight >
clientHeight` for the long scratchpad).
6. `test_answer_bubble_still_scrollable` (phase 11 regression) — a long
answer: the page scrolls, the bubble's overflow is untouched.
7. `test_restored_collapsed_thinking_unaffected` (phase 17 regression) —
a settled thinking turn reloads as a collapsed block with its full
text.