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/).
4.7 KiB
Story: No reply autoscroll
Phase: 42_no_reply_autoscroll · Source: TODO.md L5 ·
E2E: tests/e2e/test_no_reply_autoscroll.py
Bug report (verbatim, TODO.md L5)
"Get rid of the chat reply autoscroll, it's breaking things like making it impossible for the user to scroll while a reply generates."
Narrative
As a user reading a long answer, I want full control of the viewport while Brain replies. Today the page auto-scrolls to follow the stream (phase 18 "follow-the-bottom"): while I'm in the 200px near-bottom band the page is yanked down on every thinking / tool / delta frame, which fights my own scrolling mid-answer. The reply autoscroll is being removed — the page only scrolls when I explicitly cause it.
- Given a reply is streaming (thinking, tool calls, or answer text)
- When I scroll up to read earlier context
- Then the viewport stays exactly where I put it for the rest of the turn — no frame yanks it back.
Acceptance criteria
- No streaming autoscroll: during a long thinking stream, a tool
call, and a long answer, the page never auto-scrolls — sampled
window.scrollYis stable (within 1px) across frames while the viewport is away from the bottom. - Submit reveals my message: sending a question while scrolled up still scrolls the viewport down so my own message is visible (user-initiated — kept by owner decision).
- Restore landing kept: reloading a persisted conversation (phase 14) still lands one-shot on the latest message.
- The phase-18 gate is gone:
NEAR_BOTTOM_PX/isNearBottom()and the per-framescrollRevealcalls in the thinking / tool / delta handlers are removed fromapp.js; the unit pin (tests/unit/test_frontend_scroll.py) is rewritten for the new contract (scrolls happen only on submit + restore landing). - Everything else unchanged: the thinking window's internal
bottom-pin (phase 17 —
textEl.scrollTop, not the page) is untouched in this phase (phase 43 reworks it separately); message rendering, persistence, UI states, and the 120 s guard are unchanged.
Owner-confirmed (2026-08-27, roadmap A1)
- "Reply autoscroll" = the phase-18 follow-the-bottom auto-follow on thinking / tool / delta frames. Removed.
- Kept: scroll-on-submit (reveal the user's own message) and the one-shot restore landing on page load.
UI Visualization & Structure
- The functional change is in
frontend/assets/app.jsonly:- delete
export const NEAR_BOTTOM_PX = 200,isNearBottom(), and theforce-optional gating inscrollReveal— the helper becomes an unconditionalscrollIntoView(still smooth, still still underprefers-reduced-motionvia the existingSCROLLconstant); addMessage(...)gains an explicit "scroll" intent: the user submit path scrolls (shows my message), brain bubble creation and the typing indicator do not;- the thinking / tool / delta handlers drop their
scrollReveal(wrap)calls (the thinking handler keeps itstextEl.scrollTopwindow-pin — phase 17, reworked in phase 43); - the phase-14 restore landing keeps its one-shot forced scroll;
- module docstrings updated (the phase-18 contract block is replaced by the new "no reply autoscroll (owner direction 2026-08-27)" contract).
- delete
- Non-goals: no new UI element, no "↓ new content" pill (the owner wants silence, not a substitute affordance), no change to the composer / safe-area layout.
Playwright Mapping Rule
Test Scenario → tests/e2e/test_no_reply_autoscroll.py (mock LLM;
DB up; the phase-18 suite tests/e2e/test_follow_bottom_scroll.py is
deleted in this phase — its behavior is intentionally removed):
test_no_autoscroll_during_long_answer—LONG_ANSWER_TRIGGERquestion; once the answer starts, scroll the viewport up ~2× the answer height; samplewindow.scrollYacross ≥10 streaming frames: stable within 1px; afterdonethe viewport is still where it was.test_no_autoscroll_during_thinking—THINKING_TRIGGERquestion; scroll up during the ~4.5 s thinking stream; the viewport stays pinned (no per-chunk page follow).test_submit_reveals_user_message— scroll to the very top of a populated conversation, send a question; the viewport ends with the user's message visible (bottom in view).test_restore_landing_one_shot(phase 14 regression) — settle a conversation, reload; the page lands on the latest message one-shot and stays there while no stream is active.test_answer_content_intact(regression) — the long answer streams to completion with sources and (for thinking) the collapsed block, persisted and restorable.