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/).
99 lines
5.1 KiB
Markdown
99 lines
5.1 KiB
Markdown
# 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.
|