cmux-next: thread minimap matches the Codex rail; bookmarks show and jump

Closed
TE
17d ago
+3679-1703

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 on cubic-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 entry agent-pane.thread-minimap shows 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.

Codexcmux (this PR)
Px per turn156153.5
Turns per viewport4.24.07
Rail rows / tick10 px rows, 6x2 px ticksame
Rail cap (70% of height)57 rows57 rows
Lit ticks mid-thread / bottom5 / 45 / 5
Lit per frame while scrolling4-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:

ours light middle

ours dark middle

At the bottom:

ours light bottom

ours dark bottom

Hovering the rail mid-thread:

ours dark hover middle

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

strip ours scroll

ours dark scroll

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.

base-vs-head

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 at bun run check on a base formatting red in test/ime-composition-keys.test.ts, which this PR does not touch.
  • Swift (fleet): AgentPaneTranscriptStepTests red at 2261f9c (does not compile: no AgentTranscriptStep), green at c6a863f (2/2). Full suites at c6a863f: 296/297 pass. NowPlayingCardTests fails; this PR does not touch that sidebar card. AcpmuxBundleReplacedTests was 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's direct.ts conflict is resolved as the union: the attach kinds list keeps #20556's BOOKMARKS_KIND and gains the base's OFFER_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

d1 rest

d2 hover

d3 clicked

d4 left

c2 card marked

r1 row

dot toggle frames

Full quality:

Open the live gallery

🤖 Generated with Claude Code