agent pane: page older turns in as the reader scrolls up (cx-2lv1k)
Scrolling up in a long chat now loads its older turns. Before, the pane opened a chat on its newest 400 records and never asked for more, so anything older was unreachable in the transcript.
What changes
- Older turns page in before the reader reaches them. Whenever the view is within two screens of the oldest loaded row, the pane asks acpmux for the page before it (
_acpmux/eventswithbeforeSeq), one request at a time, with a 2 s backoff after a failure. Attach'shasMoresays when the start is already loaded. - The reader's place holds. The row at the top of the view stays where it is as each page lands. This now also works at offset 0, and when a page completes a turn whose start had been cut off.
- A page costs the same however much is loaded. Each older page is trimmed to whole turns (records before its first prompt wait for the next page) and folded on its own, above the held rows. Re-folding every held event took 105 ms a page at 45k events and kept growing; a page now folds in 1–6 ms. Rows a full rebuild recreates unchanged keep their objects, so drawn heights and memoized rows survive it.
- Bounded downloads. Opening asks for at most 128 KiB and each older page for 256 KiB (
maxBytes). The daemon side of the cap is #20535; a daemon without it ignores the field. hasMore, notmore. The pane read acpmux's paging flag asmore, which acpmux never sends, so missed-event catch-up after a lag also stopped after its first page.
Evidence
test/agent-pane-history-paging.test.ts(Chromium, stand-in daemon): a 500-turn chat with 6 records per turn, so page edges fall mid-turn.- Checks that it opens on its tail with at most 400 records and a capped
maxBytes. - Scrolls to the first turn with the mouse wheel. At every step, the message near the top moves only by what the wheel scrolled.
- Every turn appears exactly once, newest to oldest, and older pages use strictly decreasing cursors.
- Red on base at 211192e (no
maxBytes, no paging), green at the fix.
- Checks that it opens on its tail with at most 400 records and a capped
- Whole non-browser webviews suite, one process: the same 110 tests fail on base and on this branch (that run mode breaks jsdom setup); no difference.
vp fmt --check,vp lintandui-rulespass on the changed files. Typecheck is left to CI (it gets OOM-killed on the shared host).
Measured (Chromium, 3000-turn chat, 183k records)
| base | this PR | |
|---|---|---|
| First paint (navigate to the latest message drawn) | 917–1029 ms | 893–1125 ms |
| JS heap after open | 47 MiB | 47 MiB |
| Reaches older history | no | yes |
| Fold per older page | full re-fold, 105 ms at 45k events | 1–6 ms |
| Renderer memory after GC, 150 pages loaded (~60k records) | n/a | 500 MiB (from 280 at open) |
Open is unchanged because base already fetched only the tail. The daemon side of opening (218 ms down to 3–4 ms on this log) is in #20535.
In this measurement the vite dev server serves React's development build, which records a performance.measure for each render; uncleared, those grew the renderer to 7 GiB. The production bundle the app ships does not record them, so the run clears them between samples.
Changelog
- Fixed: Scrolling up in a long agent chat loads its older turns instead of stopping at the last 400 records.
🤖 Generated with Claude Code
Need help on this PR? Tag @codesmith-bot with what you need.
Summary by cubic
Scrolling up in a long agent chat now pages its older turns in. Before, the pane opened on the newest 400 records, paging stopped after one page because it read acpmux's flag as more instead of hasMore, and anything older was unreachable.
- Older turns page in when the view is within two screens of the oldest loaded row; after a failure the next page waits 2 s, and only one page is in flight at a time.
- The row at the top of the view holds its place as each page lands, including at offset 0 and when a page completes a turn cut at its edge.
- Each page folds on its own without re-folding held events, cutting fold cost from 105 ms per page at 45k events to 1–6 ms.
- A paged-in turn is timed from its own prompt, so its "Worked for" no longer counts from the newest prompt.
- Opening fetches at most 128 KiB and each older page 256 KiB via
maxBytes; daemons without the cap ignore the field. - Fixed the paging flag read so missed-event catch-up after a lag keeps paging instead of stopping at its first page.