# 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 1. **No streaming autoscroll:** during a long thinking stream, a tool call, and a long answer, the page never auto-scrolls — sampled `window.scrollY` is stable (within 1px) across frames while the viewport is away from the bottom. 2. **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). 3. **Restore landing kept:** reloading a persisted conversation (phase 14) still lands one-shot on the latest message. 4. **The phase-18 gate is gone:** `NEAR_BOTTOM_PX` / `isNearBottom()` and the per-frame `scrollReveal` calls in the thinking / tool / delta handlers are removed from `app.js`; the unit pin (`tests/unit/test_frontend_scroll.py`) is rewritten for the new contract (scrolls happen only on submit + restore landing). 5. **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) 1. **"Reply autoscroll" = the phase-18 follow-the-bottom auto-follow on thinking / tool / delta frames.** Removed. 2. **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.js` only:** - delete `export const NEAR_BOTTOM_PX = 200`, `isNearBottom()`, and the `force`-optional gating in `scrollReveal` — the helper becomes an unconditional `scrollIntoView` (still smooth, still still under `prefers-reduced-motion` via the existing `SCROLL` constant); - `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 its `textEl.scrollTop` window-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). - **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): 1. `test_no_autoscroll_during_long_answer` — `LONG_ANSWER_TRIGGER` question; once the answer starts, scroll the viewport up ~2× the answer height; sample `window.scrollY` across ≥10 streaming frames: stable within 1px; after `done` the viewport is still where it was. 2. `test_no_autoscroll_during_thinking` — `THINKING_TRIGGER` question; scroll up during the ~4.5 s thinking stream; the viewport stays pinned (no per-chunk page follow). 3. `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). 4. `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. 5. `test_answer_content_intact` (regression) — the long answer streams to completion with sources and (for thinking) the collapsed block, persisted and restorable.