Repository navigation
cmux-next: thread minimap matches the Codex rail; bookmarks show and jump - #20560
teamleaderleo wants to merge 46 commits into
Conversation
Behavior tests for _acpmux/bookmark_get and _acpmux/bookmark_set before they exist: restart, prompt-only seqs, fork, transfer, peer forwarding, the pane relay's scope and params, and the minimap popover's store. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
_acpmux/bookmark_get and _acpmux/bookmark_set keep the thread minimap's marks in SessionMeta.bookmarks: seqs of the chat's user_message records, checked against its log and capped at 1000. A change logs a mux bookmarks record, as tags do, so attached panes hear it; the chat keeps its place in the session list. A fork carries the marks of the prompts it copies under its own seqs; a move carries them (the log keeps its seqs) and the source keeps none. Peers' chats forward to their owner. The pane relay allows both methods for a chat in the pane's scope with only sessionId, seq and bookmarked. The minimap's store reads them from acpmux, toggles optimistically with rollback, and imports this pane's old localStorage marks for the chat once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…(red) Behavior tests for the rail's thresholds, hover magnification, card timing, keyboard focus, scrub, the bookmark dot and row button, and the bookmark steps, plus a WebKit motion test and the Swift action/page-event test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…jump The rail now follows the Codex desktop app's measured behavior: it shows from 4 prompts with 48 px left of the column, 16 px in and centered, with 10 px x 36 px rows and 6 px ticks that scroll past 64 rows or 70% of the window. Hover magnifies the rows (26/20/14/10 px) in 160 ms on Codex's spring, the preview card opens after 250 ms (at once within 300 ms of a close, or on keyboard focus) and closes 100 ms after leaving. A click scrolls on cubic-bezier(0.35, 0.05, 0, 1) (~400 ms for ~650 px), landing the bubble 35 px below the top, and highlights it for 1400 ms; an unmounted target jumps. Dragging scrubs. Every row is a tab stop. Bookmarks show as a 2 px dot past the tick (a click on its spot toggles it), in the card, and in a new button on the prompt's own row. Previous and Next Bookmarked Prompt are palette actions with no default key; the turn steps keep Ctrl-Opt-Up/Down, now on Codex's 200 ms scroll curve. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ead-minimap-codex
Bookmarks now live in acpmux (#20556). The rail's dot, its clickable spot, the card button and the prompt row's button read and toggle them through bookmarks.ts's useBookmarks/promptSeq (threadMinimap/promptMarks.ts), and show only for prompts acpmux has logged. Tests set a bookmark transport and use user-<seq> prompt ids. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Generated by scripts/measure/export-action-surfaces.sh on the fleet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…for acpmux (red) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bugbot needs on-demand usage enabledBugbot uses usage-based billing for this team and requires on-demand usage to be enabled. A team admin can enable on-demand usage in the Cursor dashboard. |
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configuration
You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
… holds them set_bookmark changes, logs and saves the marks under the chat's move lock, so concurrent sets log in the order they change the marks and a move begins before or after a change. The prompt check scans the log on a blocking thread. The pane forgets an old localStorage mark only once bookmark_set succeeds for it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Bead |
This comment has been minimized.
This comment has been minimized.
|
cmux status (one comment, edited in place; each line is updated by the job that owns it) Review App of this head (app.zip, kept 1 day)
Pending: CI is running on Last result: Unclear: Written by red: cmux-next checks (god files, concurrency, crash safety, l10n); base red: the same failure on the base 204546c; does not block
Agents: |
|
Agents: |
|
cmux-next tour of The tour job failed before the tour ran (build or setup); the run log has the output. The tour produced no media. Tours are data in |
…mux-turn-bookmarks
WebKit does not focus a clicked button, so in the app Tab after a click left the rail for the transcript. Codex's rail (Chromium) focuses it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…amples CI's headless WebKit paced frames unevenly, so a sample 40 ms into the transition could come long after it ended. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ead-minimap-codex # Conflicts: # webviews/src/agent-session/acpmux/direct.ts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ead-minimap-codex
The flag cleared on a 0 ms timer, which a slow frame could run between the pointerup and its click, so the drag's click jumped (CI web shard 2). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
#22228 carries this branch forward: it merges current feat-cmux-next into it (conflicts in App.tsx, ThreadMinimap.tsx, ActionCatalog.xcstrings, actions.md) and fixes what still measured worse than Codex in a 60-turn chat (band lag while scrolling, 466 ms card, far jumps landing 262 px off, older pages moving the band 163 px). I'll close this one once #22228 lands. |
[Medium impact] The changes affect navigation and bookmarking within agent-chat transcripts. The PR should not merge yet because a minimap jump can keep scrolling after the user switches chats. Findings
SummaryThe PR changes the thread minimap to a fixed-pitch prompt rail with previews, scrolling, keyboard navigation, and scrubbing.
Diagram%%{init: {'theme': 'neutral'}}%%
flowchart LR
Palette["Command palette"] --> Step["Transcript step"]
Rail["Rail click or scrub"] --> Jump["Jump to prompt"]
Step --> Jump
Jump --> Scroll["Scroll transcript"]
Dot["Rail dot"] --> Marks["Session bookmarks"]
Card["Preview button"] --> Marks
Row["Prompt row button"] --> Marks
Marks --> Rail
Reviews (4) · Last reviewed commit: "chore(next): PR autofix at 1d86039b308d" · Reviewed by Greptile |
| const target = () => | ||
| Math.min(turnScrollTop(turn, live.current.tops), Math.max(0, live.current.total - node.clientHeight)); | ||
| const row = mountedRow(node, turn.id); | ||
| const to = target(); | ||
| if (how === "scrub" || !row || reducedMotion()) { | ||
| stopScroll(node); | ||
| node.scrollTop = to; | ||
| } else if (how === "step") animateScroll(node, target, STEP_MS, STEP_CURVE); | ||
| else animateScroll(node, target, jumpDuration(to - node.scrollTop), JUMP_CURVE); |
There was a problem hiding this comment.
Switching chats during a minimap jump leaves the old scroll running. target() keeps the old turn but reads the new chat's layout through live.current. Because VirtualTranscript reuses the same scroller, later frames overwrite the new chat's opening position. Stop the animation when sessionId changes and when the minimap unmounts.
| const windowHeight = globalThis.innerHeight || viewportHeight; | ||
| const onScreen = visibleTurns(turns, layout.tops, layout.totalHeight, scrollTop, viewportHeight); | ||
| const active = hovered ?? current; | ||
| const turn: MinimapTurn | undefined = hovered === undefined ? undefined : turns[hovered]; | ||
| const shown = turns.length >= MIN_PROMPTS && gutter(width) >= MIN_GUTTER; | ||
| const height = railHeight(turns.length, windowHeight); | ||
| const overflow = turns.length * ROW_PITCH > height; |
There was a problem hiding this comment.
The rail's height uses innerHeight, but its position uses viewportHeight. The header and expanded composer leave less room for the transcript. A 600 px webview can therefore produce a 420 px rail in a transcript shorter than 420 px. Its top becomes negative, clipping the end rows and their click targets. Cap the rail against the transcript's available height too.
| <div | ||
| ref={card} | ||
| id={cardId} | ||
| className="acpmux-minimap__card" | ||
| style={{ transform: `translate3d(${RAIL_LEFT + ROW_WIDTH}px, ${cardTop}px, 0)` }} | ||
| onPointerLeave={(event) => { | ||
| // Back onto the rail keeps it (its row takes over); anywhere else it goes. | ||
| if (!within(rail.current, event.relatedTarget)) intend(null); | ||
| }} |
There was a problem hiding this comment.
Moving the card outside the nav drops its keyboard dismissal path. Tab from the last rail row into the bookmark button, then Tab out: the nav leaves the card open, and the card has no onBlur to close it. Escape on that button also misses the handler attached to ticks. Close the card when focus leaves it and handle Escape within it.
| onPointerDown={(event) => { | ||
| if (event.button !== 0 || event.pointerType === "touch") return; | ||
| pressing.current = true; | ||
| swallowClick.current = false; | ||
| scrub.current = { pointer: event.pointerId, y: event.clientY, moved: false }; | ||
| }} | ||
| onPointerMove={(event) => { | ||
| const state = scrub.current; | ||
| if (!state) return; | ||
| if (!state.moved) { | ||
| if (Math.abs(event.clientY - state.y) < SCRUB_SLOP) return; | ||
| state.moved = true; | ||
| setScrubbing(true); | ||
| // Captured only once it is a drag, so a plain click still lands on its row. | ||
| event.currentTarget.setPointerCapture?.(state.pointer); |
There was a problem hiding this comment.
Pressing a row, moving horizontally outside the rail, and releasing leaves pressing.current and scrub.current set. Pointer capture starts only after 3 px of vertical movement, so the list misses that release. Later keyboard focus is ignored, and returning the pointer can start a scrub without a pressed button. Track releases outside the list or otherwise clear the press when it ends.
| if (swallowClick.current) { | ||
| swallowClick.current = false; | ||
| return; |
There was a problem hiding this comment.
First keyboard jump disappears
swallowClick can consume a later keyboard jump instead of the drag's click. A scrub captures the pointer on the list, so its ending click can target the list rather than a tick. That click never reaches actions.click, leaving the flag set. The next Enter or Space on a tick then does nothing. Consume the ending pointer click at the list, or limit suppression to that pointer click.
| if (rowTop >= node.scrollTop && rowTop + ROW_PITCH <= node.scrollTop + height) return; | ||
| node.scrollTop = Math.max(0, rowTop - height / 3); | ||
| setListTop(node.scrollTop); | ||
| }, [onScreen.first, overflow, height, hovered, focused]); |
There was a problem hiding this comment.
When a narrow pane becomes wide enough to show the rail, it mounts a fresh list. This effect previously skipped the hidden rail because list.current was null. Since neither shown nor width is a dependency, an unchanged height and current prompt do not rerun it. Long chats reopen the rail at the first prompt instead of the reader's position. Run the effect when the rail becomes visible.
- plans/cmux-next/action-surfaces.json - plans/cmux-next/actions.md Autofix-Run: https://github.com/manaflow-ai/cmux/actions/runs/39360247633
| width: 50.5px; | ||
| font: 13px -apple-system, BlinkMacSystemFont, "SF Pro Text", system-ui, sans-serif; | ||
| margin: 0; | ||
| animation: acpmux-minimap-in 150ms cubic-bezier(0.23, 1, 0.32, 1); |
There was a problem hiding this comment.
The rail still fades in when ui.animationSpeed is off. This new animation uses a fixed 150 ms duration, and only the system Reduce Motion setting disables it. The app sends zero motion durations when animations are off, but this rule ignores those values. Use the app's motion setting for the rail's entrance too.
- Packages/macOS/CmuxNext/ci-target-graph.json Autofix-Run: https://github.com/manaflow-ai/cmux/actions/runs/39367009636
Bugbot needs on-demand usage enabledBugbot uses usage-based billing for this team and requires on-demand usage to be enabled. A team admin can enable on-demand usage in the Cursor dashboard. |
|
Found 38 test failures on Blacksmith runners: Failures
...and 28 more test failures. View all on Blacksmith: run 39368818708, run 39368818670
|
|
From the subsession-visibility lane, which owns the thread minimap work now: this PR's content is already on feat-cmux-next.
#22879 follows up with Leo's feedback. Nothing here is left to adopt, so this PR is superseded. I'm leaving the close to its owner. |
|
owner: 01a15ba2-88b0-7880-a07f-1f6bd7f401ed on cosmic-gray |
|
Adopting this orphan for live-reload-loop-dd. Next: check base against the transcript-density and scroll-jitter changes, then one base merge, conflicts, gallery before/after, review. [live-reload-loop-dd] |
|
Closing as superseded. This branch landed as #22228 (d85ec8a), a squash of every commit here through Current feat-cmux-next has both halves of this PR: the Codex-style minimap ( [live-reload-loop-0f] Agent session: cosmic-gray |
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-bookmarksand reads marks only through itsbookmarks.tsAPI, so #20556 lands first.What changed
linear()spring (~0.8% overshoot), all rows at once, and ease back in 150 ms on leave (was 50/35 ms).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).cubic-bezier(0.30, 0.20, 0, 1)with no highlight.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:
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;The Codex frames are in the private archive (
references/codex-prompt-rail-2026-10-14/), not here.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-texttokens). 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: ours-dark-scroll.mp4
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.mp4
Verification:
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.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 acpmux: prompt bookmarks persist in the session #20556'sBOOKMARKS_KINDand gains the base'sOFFER_KINDS. acpmux: prompt bookmarks persist in the session #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.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: dot-toggle-frames.mp4
Open the live gallery
🤖 Generated with Claude Code