Agent chat: the thread rail shows every prompt of the chat, read in the background

TE
9d ago
+231-23

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

  • minimapTurns prepends indexed prompts with index: -1 above the loaded window, counts the complete prompt set through each tick's aria-setsize, and retains the current promptId/row-id keys and #25960 card metadata.
  • ThreadMinimap subscribes to client.earlier. Clicking or scrubbing an unloaded prompt asks loadThrough(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.ts is a real Chromium browser test: it pages a tail-only attach, verifies the full aria-setsize, clicks the first unloaded tick, and waits for user-1 to 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.ts Chromium browser lane passes with CMUX_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 f6262e04 and 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, and git diff --check pass.
  • python3 scripts/cmux-next/prepush-gate.py passes 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)

chatbuildticksworst frame gaplong tasks (total, longest)
500 prompts, 50 loaded, 3 sbase 02151f8c5016.8 ms0
stacked A + B 61bd0b5c50016.8 ms0
2,000 prompts, 50 loaded, 6 sbase 02151f8c5016.8 ms0
stacked A + B 61bd0b5c (68 ticks drawn by the window)2,00016.8 ms0

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.
  • loadThrough uses the existing history guards and stops on exhaustion, no progress, stale selection/epoch, or client close; it never writes scrollTop.

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