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/).
This commit is contained in:
2026-08-27 18:25:53 -04:00
parent 492d8275e7
commit 02c76ad328
66 changed files with 1906 additions and 0 deletions
@@ -0,0 +1,91 @@
# 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.