5.3 KiB
Story: Thinking No Scroll (live-tail Thinking window)
Phase: 21_thinking_no_scroll · Source: TODO.md L4 ·
E2E: tests/e2e/test_thinking_no_scroll.py
Bug report (verbatim, TODO.md L4)
"Disable scroll in the thinking window. Users don't need to scroll back through thinking, just see it live."
Narrative
As a user, the Thinking block is a scratchpad, not a transcript: while Brain reasons I just want to watch the newest lines appear — not scroll back through everything it already thought. The block should always show the live tail of the reasoning stream, in its fixed 320px window, with no way to scroll it back.
- Given a turn streams a long reasoning body (longer than the 320px window)
- When I try to scroll back through the window — mouse wheel, mouse drag, or keyboard — while it streams (or while it sits open before the first token)
- Then the window does not move: it stays glued to the live tail, pinned there by the phase-17 per-chunk bottom-pin (the sole scroller).
Acceptance criteria
- With a long thinking stream, wheel / mouse-drag / keyboard on
.thinking-textdo not move it; the visible content is always the live tail (pinned at the bottom, within 1px, after each chunk). - Computed style of
.thinking-text:overflow-y: hidden,max-height: 320px— the 320px clip stays (owner-confirmed: no auto-height growth, no "↓ more" affordance). - A long answer bubble is untouched: it keeps its existing scroll
behavior (phase 11) — phase 21 only changed
.thinking-text. - Restored (collapsed) Thinking blocks are untouched (phase 14/17 restore path): they render collapsed with their text, where overflow is moot.
- No new surface: the block keeps its summary chevron, focus-visible
ring, aria contract, and the reduced-motion stillness (styles.css
prefers-reduced-motionblock).
Owner-confirmed (2026-08-24, roadmap A2)
- Keep the 320px clip — "just see it live" means the window stays a fixed 320px viewport showing the newest lines; no auto-height growth, no "↓ more" affordance.
- The answer bubble is untouched — final answers keep their existing scroll behavior (phase 11 long answers).
- Restored (collapsed) Thinking blocks are untouched — the phase-14 restore renders them collapsed, where overflow is moot.
UI Visualization & Structure
- The whole functional change is one CSS property
(
frontend/assets/styles.css, phase-17 thinking section):details.thinking .thinking-text—overflow-y: auto→overflow-y: hidden(with the owner-choice comment: no user scroll back (owner choice 2026-08-24): the window is a live tail only — the JS bottom-pin is the sole scroller).max-height: 320pxand every other declaration stay byte-identical. - Why this works:
overflow: hiddendisables user scrolling (wheel/drag/keyboard) but still permits programmatic scrolling — so the phase-17 bottom-pin (textEl.scrollTop = textEl.scrollHeighton everythinkingchunk,frontend/assets/app.js) keeps the window glued to the live tail with no JS change. - Non-goals: no change to the answer bubble, the collapsed restore state, the summary/chevron, or the auto-collapse on the first delta (phase 17).
Playwright Mapping Rule
Test Scenario → tests/e2e/test_thinking_no_scroll.py (mock LLM;
phase 21 lengthened mock_llm.compose_thinking to ~2 700 chars ≈ 4.5s
of paced reasoning_content frames so the scratchpad overflows the
320px window; tests 1–2 use the phase-20 think out loud then hesitate
trigger — after the stream ends there is a deterministic 4s pre-content
pause with the block still open and no further pin frames, a frozen live
tail where any (regressed) user scroll would persist and be observable):
test_thinking_window_not_user_scrollable— wait until the window overflows and the thinking stream ends (text stable; block still open, pre-token). Assert the pin left it at the tail; then focus.thinking-text, wheel up ×2,Home+ArrowUp, mouse-drag up:scrollTopmust not move (within 1px) and the window must still show the live tail.test_thinking_window_tracks_live_tail— while the stream is live: once the 2nd-to-last chunk has landed, the window is pinned to the tail; after the last chunk (held by the 4s pause) it is still pinned, the last chunk's text (the final text node) renders inside the visible rectangle, and a hit-test at the box's bottom lands inside.thinking-text.test_thinking_window_css_contract— computed style:overflow-yishidden,max-heightis320px, and the clip is real (scrollHeight > clientHeightfor the long scratchpad).test_answer_bubble_still_scrollable(regression, phase 11) — the long-answer mock question streams to completion; the answer bubble's computedoverflow-yis nothidden, the page is scrollable, and wheel up/down over the answer moves the page scroll both ways.test_restored_collapsed_thinking_unaffected(regression, phase 17) — a settled thinking turn is reloaded; the restored conversation renders the Thinking block collapsed with its full text (matching the persisted raw text), and the answer bubble is intact.