# 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.