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

5.1 KiB
Raw Blame History

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.