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/).
5.1 KiB
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
- User scroll restored: computed
overflow-yof.thinking-textisauto; wheel / mouse-drag / keyboard move the window (phase 21'soverflow-y: hiddenis gone). - 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). - Paused on scroll-up: scrolled up, the window stops being re-pinned
—
scrollTopstays stable across subsequent chunks (within 1px). - Resumes on return: scrolling back to the bottom (within the band) resumes tail-following on the next chunk.
- Kept from phases 17/21: the fixed 320px
max-heightwindow, 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)
- The 320px window stays — only the overflow mode and the pinning logic change.
- 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: 320pxand all other declarations untouched. - JS (
frontend/assets/app.js, thethinkingSSE 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).
- new exported
- 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:
test_thinking_window_user_scrollable— frozen live tail (4 s hesitation): focus.thinking-text, wheel up,Home, mouse-drag up —scrollTopmoves; the window shows earlier content.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.test_thinking_window_stops_on_scroll_up— mid-stream: scroll up ~half the window; over the next ≥5 chunksscrollTopstays stable (within 1px) — no re-pin.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).test_thinking_window_css_contract— computedoverflow-y: auto,max-height: 320px, and the clip is real (scrollHeight > clientHeightfor the long scratchpad).test_answer_bubble_still_scrollable(phase 11 regression) — a long answer: the page scrolls, the bubble's overflow is untouched.test_restored_collapsed_thinking_unaffected(phase 17 regression) — a settled thinking turn reloads as a collapsed block with its full text.