Agent chat: the thread rail shows every prompt of the chat, read in the background
HOLD: base red web / react-apps-check hides webview test signal on feat-cmux-next (owner nx-ci); this PR touches webviews/ and lands once that check is green again.
Leo (2026-10-25, the thread rail): "if you haven't loaded in chats ... the session rails ... should be able to still, like, gradually load that stuff in".
This is PR #25160, the rail integration stacked on PR #26033 (rail-opus-earlier-prompts-index). PR A owns the prompt index, backward event paging, client.earlier, and loadThrough(seq); this PR lands after A and consumes that contract. The branch is mine and was rebuilt from feat-cmux-next.
What changed
minimapTurnsprepends indexed prompts withindex: -1above the loaded window, counts the complete prompt set through each tick'saria-setsize, and retains the current promptId/row-id keys and #25960 card metadata.ThreadMinimapsubscribes toclient.earlier. Clicking or scrubbing an unloaded prompt asksloadThrough(seq)to materialize it, then hands the existing jump path the real row; no second scroll compensator is introduced.- The full-height map receives loaded turns only, so the windowed map keeps its bounded work while the compact rail can represent the full prompt history.
thread-rail-every-prompt.test.tsis a real Chromium browser test: it pages a tail-only attach, verifies the fullaria-setsize, clicks the first unloaded tick, and waits foruser-1to materialize through history paging.
Coordination
big-red/nx-chat acknowledged ACK-nx-history-contract-1027. nx-chat owns logical/physical history coordinates and the existing row-id prepend anchor (preserveScrollAnchorTop in conversation/scrollAnchor.ts, called by VirtualTranscript). PR A owns the complementary prompt index.
Public shapes consumed from PR A:
client.earlier: EarlierPromptsStore = {
subscribe(listener: () => void): () => void,
get(): readonly AcpmuxRow[],
}
client.loadThrough(seq: number): Promise<void>
Tests
Rebased onto A 181d9797847473707e927b7e4cda26e60090a22f. The rail-only commit is unchanged (git range-diff). A's unknown-boundary and equal-count regressions fail on 8885fc97 and pass on the current stack. The 235 generated page/byte splits still match whole-transcript keys. The failed-result regressions for read failure and the 200-page cap fail on 1c1fd5d4 and pass on this stack; the next prompt appears and is published exactly once.
Current stack: 31 focused tests pass, one history browser-lane test is skipped in the ordinary local run, and CMUX_BROWSER_TESTS=1 bun test test/thread-rail-every-prompt.test.ts passes in Chromium. A's PR body records the 320,000 worst-case transition bound and cap retention measurements (5,749,799 serialized transition bytes while unresolved, zero on completion; prompt display/refresh storage and JS overhead excluded).
thread-rail-every-prompt.test.tsChromium browser lane passes withCMUX_BROWSER_TESTS=1.- Related earlier-prompt, long-chat, scroll-map, card-metadata, turn-key, and history-paging suites pass locally; the normal local run skips browser lanes outside CI/Freestyle.
- PR A regression tests fail on
f6262e04and pass on the new stacked head: cross-page continuation state and final failed-read batch publication. bun run typecheck,check:fixed-waits,check:layers,check:design-tokens, targeted formatting, andgit diff --checkpass.python3 scripts/cmux-next/prepush-gate.pypasses on the exact stacked head; no native build was run.
Perf (same harness, windowed rail, readDelay 300 ms, cmux-ci Chromium, 3 runs each; identical across runs)
| chat | build | ticks | worst frame gap | long tasks (total, longest) |
|---|---|---|---|---|
| 500 prompts, 50 loaded, 3 s | base 02151f8c | 50 | 16.8 ms | 0 |
stacked A + B 61bd0b5c | 500 | 16.8 ms | 0 | |
| 2,000 prompts, 50 loaded, 6 s | base 02151f8c | 50 | 16.8 ms | 0 |
stacked A + B 61bd0b5c (68 ticks drawn by the window) | 2,000 | 16.8 ms | 0 |
These are the same-harness windowed-rail measurements as the original PR, with the rebuilt stacked head recorded above.
Timers and stop conditions
- One idle callback at a time per pane; each callback reads at most one 400-record page.
- Index reading stops on no older records, 200 pages, failed read, chat switch/reset, or
close(); every stop cancels pending idle work. - A waiting click/scrub ends on reader input, another jump, loading ending without the target row, or chat switch.
loadThroughuses the existing history guards and stops on exhaustion, no progress, stale selection/epoch, or client close; it never writesscrollTop.
Changelog
Changed: the left prompt rail now represents every prompt in a long chat and materializes an unloaded prompt through the existing history/anchor path.
Agent session: agent session