Files
brain-of-reese/.agent/user_stories/no-reply-autoscroll.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

4.7 KiB
Raw Blame History

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.