cmux-next: thread minimap matches the Codex rail; bookmarks show and jump
The agent chat's thread minimap now behaves like the Codex desktop app's prompt rail, measured frame by frame from 60 Hz recordings of Codex.app (26.930). Bookmarks show on the rail, toggle from the rail, the card and the prompt's own row, and two new palette actions jump between them.
Depends on #20556 (bookmarks stored in acpmux). This branch merges feat/acpmux-turn-bookmarks and reads marks only through its bookmarks.ts API, so #20556 lands first.
What changed
- Rail: shows from 4 prompts, only with ≥48 px left of the column (was 2 and 28 px). It sits 16 px in from the transcript's left edge, vertically centered, fixed. Rows are a fixed 10 px pitch, 36 px wide to hit, with 2 × 6 px ticks; past 64 rows or 70% of the window the rail scrolls with 2.5rem edge fades. Every prompt on screen is "current" (brighter, 6 px); the hovered or focused row draws in the text color. Colors are theme tokens (muted text at 40%, text at 60%, text), which give the SPEC's #3d3d3d / ~#747474 / #dadada in the dark reference theme.
- Hover: no delay; rows magnify 26/20/14/10 px in 160 ms on Codex's
linear()spring (~0.8% overshoot), all rows at once, and ease back in 150 ms on leave (was 50/35 ms). - Preview card: opens 250 ms after the pointer reaches the rail, instantly if one closed <300 ms ago, closes 100 ms after leaving; appears and disappears in one frame and moves straight to the hovered row. 320 px wide, 12 px radius, flush right of the row: the prompt on one line, the bookmark button, up to 3 answer lines.
- Keyboard: every row is a tab stop, and a clicked row takes focus (as in Codex), so Tab goes on from the rail. A focused row grows to 26 px with no neighbor magnification and opens its card at once; Escape closes it; Return/Space jumps.
- Click to jump: the prompt's bubble lands 35 px below the transcript's top. A JS scroll on
cubic-bezier(0.35, 0.05, 0, 1)takes ~400 ms for ~650 px; an unmounted (virtualized) target jumps at once. The bubble then fades from a highlight to its own fill over 1400 ms oncubic-bezier(0.23, 1, 0.32, 1). - Scrub: dragging on the rail jumps the transcript to each row crossed, with no tick transition; the drag's click is swallowed.
- Bookmarks: a bookmarked tick is drawn at full opacity with a 2 × 2 px dot 2 px past its end; the card shows a filled icon. Beyond Codex: a click on the dot's spot toggles the mark (the hovered row shows a faint dot there), the prompt row's hover actions gain a bookmark button, and Previous Bookmarked Prompt / Next Bookmarked Prompt are command palette actions with no default key.
- Previous/Next Turn keep Ctrl-Opt-Up/Down. Codex uses Option-Up/Down, but a default key change needs a design call, so none changed here. The step now scrolls on Codex's 200 ms
cubic-bezier(0.30, 0.20, 0, 1)with no highlight. - Reduce Motion (system or the app's): magnification, card and highlight are instant, the scroll jumps.
- The parity dev page (
threadMinimap/parity.html) now replays the Codex reference thread through the real transcript; the gallery entryagent-pane.thread-minimapshows the hover card and a bookmarked variant.
Brief vs Codex: the brief said hover "expands into the list of prompts". Codex does not do that: ticks magnify like the Dock and a preview card opens. This follows Codex.
Short-prompt density (Leo 10-14)
Leo asked whether the rail shows enough prompt highlights at once with short prompts. It does: density matches Codex by construction, and the measurements agree.
Setup:
- a cmux NEXT DEV build of fc30853551dd on a capture mini, in a 1280x820 window;
- the new debug seed
debug.agent_pane seed_rows {"count":180,"fixture":"short"}: 60 turns of "Reply with only the word ok and the number N." / "ok N", the same content as the Codex reference chat; - Codex 26.930 measured from the reference recording, at the same window size.
The Codex frames are in the private archive (references/codex-prompt-rail-2026-10-14/), not here.
| Codex | cmux (this PR) | |
|---|---|---|
| Px per turn | 156 | 153.5 |
| Turns per viewport | 4.2 | 4.07 |
| Rail rows / tick | 10 px rows, 6x2 px tick | same |
| Rail cap (70% of height) | 57 rows | 57 rows |
| Lit ticks mid-thread / bottom | 5 / 4 | 5 / 5 |
| Lit per frame while scrolling | 4-6 (5 in 61% of frames) | 4-6 (5 in 93% of frames) |
The one-tick difference at the bottom comes from where each thread's last scroll position falls. The rest of the rail is the same: the pitch, the cap and the rule that every turn intersecting the viewport is lit.
One difference that isn't density: our rest and current ticks are about 25/255 lighter than Codex's in dark mode (the --agent-muted/--agent-text tokens). That's left as is here and noted as a follow-up.
Light and dark, mid-thread:


At the bottom:


Hovering the rail mid-thread:

Scrolling through, as a 12-frame strip and a clip:


Full quality:
Evidence
Captures: tagged DEV builds from the fleet's Mac builder (head 82f7d7b, base 2e420aa), recorded with real HID input on the capture mini that recorded the Codex reference, same window frame, 20-prompt mock chat. Host names blurred.
The Codex-vs-cmux side-by-side clips (hover, click to jump, scrub, bookmark, keyboard, Previous/Next Turn) are reference captures of another app, so they live only in the private screenshots archive (manaflow-ai/cmux-app-screenshots references/codex-prompt-rail-2026-10-14/), not here. The measured numbers are in the Codex reference spec in the same archive.
Before (feat-cmux-next) and after (this PR) in the cmux NEXT DEV app: hover, click, bookmark, keyboard.

Verification:
- Web regression tests, failing first (jsdom): thresholds, card timing (fake timers), Tab/Escape/Return, scrub, the bookmark dot and row button, the bookmark steps. Red at 2261f9c (1 pass, 8 fail) → green (9 pass); with #20556 merged, 35 pass across 9 files. The clicked row taking focus (found in the capture: WebKit does not focus a clicked button, so Tab left the rail): red at bd45ad5 → green at 82f7d7b (rail 8/8; current, turn keys and bookmarks 13/13).
- WebKit browser test
thread-minimap-motion.test.ts(CI only): at 82f7d7b, the click curve, the landing and highlight, and Reduce Motion passed in react-apps-check. The hover check now seeks the transition instead of sampling frames, because CI's headless WebKit paced frames unevenly (a72dc7e). That rewrite has not run in WebKit yet: since then react-apps-check stops atbun run checkon a base formatting red intest/ime-composition-keys.test.ts, which this PR does not touch. - Swift (fleet):
AgentPaneTranscriptStepTestsred at 2261f9c (does not compile: noAgentTranscriptStep), green at c6a863f (2/2). Full suites at c6a863f: 296/297 pass.NowPlayingCardTestsfails; this PR does not touch that sidebar card.AcpmuxBundleReplacedTestswas skipped by the same-tree guard. Release compile: BUILD SUCCEEDED at c6a863f and again at the merged head 568dba6. bun run check, ui-rules,gen-strings --check, gallery coverage and play, action exports regenerated on the fleet after the latest base merge (568dba6). The base merge'sdirect.tsconflict is resolved as the union: the attach kinds list keeps #20556'sBOOKMARKS_KINDand gains the base'sOFFER_KINDS. #20556 will need the same resolution. check-system-blue, crash_ratchet, check-app-size, check-test-notification-centers and lint-ios-package-conventions are clean. check-no-godfiles passes on every touched Swift file (the catalog split keeps AgentActionCatalog at 375 lines). check-concurrency: 1 base red in ControlWorkspaceInfoCache.swift, not touched here.- Other reds on this PR, none in its files: web/ server shards 1/4 and 3/4 (Cloud namespace, admin audit and Stack invitation tests, which also fail locally on this tree),
src/pages/settings/categories.test.ts, Tour the app (runner worktree error), batch-queue debounce (cancelled).
Changelog
Changed: the thread minimap magnifies, previews and jumps like the Codex app; bookmarks show a dot and can be jumped to
Showcase







Full quality:
🤖 Generated with Claude Code