Agent chat: the prompt rail lays out every turn from the first frame (acpmux prompt index)

Merged
TE
7d ago
+475-21

Leo (2026-10-31), on the transcript's left prompt rail: "likewise i hate how this keeps shifting all the time when things grow + the fact that things grow instead of just showing me a big preview of the stuff from teh start. we should be able to have some sort of count that just rapidly is able to figure out the # of chats and whatnot be able to associate stuff better".

What changed

  • acpmux (hub/paging.rs prompt_index, _acpmux/events promptIndex: true): one read returns every prompt (user_message) before beforeSeq, oldest first, each cut to its seq, promptId, steer flag and the first 240 characters of its text. The store skips every other line by kind without parsing it, so the read stays fast however much tool output the chat holds. Protocol spec and generated types updated (EventsParams.promptIndex).
  • Pane (direct.ts): when a chat with older history opens, the client makes that one read and puts the prompts older than the held rows in the snapshot as earlierPrompts. A daemon without the index rejects the unknown param, and the rail then grows as it did before. loadThrough(seq) pages history in (through loadOlder and its guards) until a prompt is held.
  • Rail (threadMinimap/model.ts, ThreadMinimap.tsx): carries the rail half of #25160 (02e62c5a7c65) onto this data source. Indexed prompts become ticks above the loaded turns, at their final places. An older page that loads turns the matching placeholder (same row id user-<seq>, same promptId key) into a real turn, so no tick is added, removed or moved, and the prepend glide never fires. A click on an unloaded tick pages history in and then jumps. The hover card shows the indexed prompt text. The full-height map still reads loaded turns only.

This replaces #26033's client-side backward paging and turn-start reducer, which kept diverging from the transcript's own reducer (its blocker comment): the index comes from the daemon in one reply, and the tick keys match the loaded rows' keys.

Design spec: leo-workspace design/session-rail.md gains "Every turn from the first frame" and the rule "Never reflow as history loads" (teamleaderleo/leo-workspace PR linked below).

Performance

Same harness, cargo test --release -p acpmux --lib prompt_index_perf (a 1,000-turn Codex chat with 40 output updates per command, LocalStore on disk, best of 3, fleet Linux):

metricbeforeafterdeltaharness
time until the rail has every turn's tick122.9 ms of daemon reads across 38 loadOlder pages, and those pages only load as the reader scrolls to the top18.7 ms, one read at open-85% daemon time, 38 round trips down to 1hub/paging_tests.rs prompt_index_perf
tick reflows while history pages inone per older page (38 for this chat)0-38webviews/test/thread-rail-earlier-prompts-layout.test.ts

No new per-token work: the earlier list is read once per chat. minimapTurns keeps its incremental path, and the earlier turns are reused while their list is unchanged.

Tests

  • hub/paging_tests.rs: the_prompt_index_holds_every_prompt_cut_short_oldest_first (cut text, no images or pastes, steers kept with their flag, before bounds it).
  • thread-rail-earlier-prompts-layout.test.ts: a 12-turn chat opened at turn 10, then paged back three times. The tick keys equal the whole chat's at every step, and the held turns become real.
  • agent-pane-history-paging.test.ts: the open now makes exactly one prompt index read, and the older-page assertions exclude it.

Risk: a prompt the transcript draws as continuing a turn (a send-now continuation) is a tick in the index. At worst the rail shows one extra tick for it, with no reflow.

Agent session: agent session

šŸ¤– Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.


Summary by cubic

The prompt rail now lays out every turn of a long chat from the first frame: acpmux gains a prompt index (_acpmux/events with promptIndex: true) that returns every prompt before a sequence in one read, oldest first, each cut to its seq, promptId, steer flag, and the first 240 characters of its text. The store skips non-prompt lines by kind without parsing them, so the read stays fast however much tool output the chat holds. The pane makes that one read at open and passes the older prompts to the rail, which draws their ticks at their final places; an older page that loads a prompt turns its placeholder into a real row instead of adding a tick, so the rail never reflows. A click on an unloaded tick pages history in and then jumps. A daemon without the index ignores the param and the rail grows as it did before.

Performance

  • A 1,000-turn chat's rail goes from 122.9 ms of daemon reads across 38 loadOlder pages to one 18.7 ms read.
  • Tick reflows while history pages in drop from one per older page to zero.

Tests

  • hub/paging_tests.rs covers the index's shape: cut text, steer flag kept, before bounds, oldest first.
  • thread-rail-earlier-prompts-layout.test.ts checks a 12-turn chat opened at turn 10: the tick keys equal the whole chat's at every page step, and held prompts become real turns.
  • agent-pane-history-paging.test.ts asserts the open makes exactly one prompt index read.

One caveat: a send-now continuation (a prompt drawn as continuing a turn) appears as an extra tick, with no reflow.