Repository navigation
cmux NEXT: the thread minimap feels like the Codex rail (band tracks every frame, jumps land, 250 ms card) - #22228
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>
… 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>
…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>
- vp fmt webviews/test/ime-composition-keys.test.ts - cargo fmt in Native/OptChat/optchat-chief Autofix-Run: https://github.com/manaflow-ai/cmux/actions/runs/38566033765
…mux-turn-bookmarks # Conflicts: # cmux-tui/crates/acpmux/tests/peer_integration.rs # webviews/src/agent-session/acpmux/direct.ts
…ead-minimap-codex
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ead-minimap-codex # Conflicts: # Packages/macOS/CmuxNext/Sources/CmuxNextActions/ActionCatalog.swift # cmux-tui/crates/acpmux/src/hub/fork.rs # cmux-tui/crates/acpmux/src/hub/resolve.rs # cmux-tui/crates/acpmux/src/server/requests.rs # cmux-tui/crates/acpmux/src/store.rs # cmux-tui/crates/cmux-agent-pane-policy/policy.json # cmux-tui/crates/cmux-agent-pane-policy/tests/cases/params.json # cmux-tui/crates/cmux-agent-pane-policy/tests/cases/sessions.json # webviews/src/agent-session/acpmux/App.tsx # webviews/src/agent-session/acpmux/direct.ts # webviews/src/agent-session/acpmux/threadMinimap/ThreadMinimap.tsx # webviews/src/agent-session/acpmux/threadMinimap/model.ts # webviews/src/agent-session/acpmux/threadMinimap/threadMinimap.css # webviews/test/thread-bookmarks.test.tsx
Base's hover-intent and elevation tests now drive the rewritten rail (.acpmux-minimap__card, the rows' pointerenter/pointerleave), and the rail test pins the app hover intent (450 ms delay, warm 500 ms, gone as the pointer leaves unless it enters the card) instead of CardTiming's 250/300/100 ms. heavy --mem 3G bun test test/hover-intent-cards.test.tsx test/subtle-elevation.test.ts test/thread-minimap-rail.test.tsx (fail) the card follows the app's hover intent: it waits the hover delay, follows the pointer at once while warm, and goes as the pointer leaves thread-minimap-rail.test.tsx:210 expect(card()).toBeNull() after 400 ms: the card opened at 250 ms (fail) the minimap's turn preview waits the hover delay, then follows the pointer at once until the rail cools hover-intent-cards.test.tsx:117 expect(visible()).toBe(false) right after leaving: CardTiming keeps it 100 ms (fail) one subtle elevation scale > .lg-sheet casts --elevation-2 (base red, ledger.css untouched) The .acpmux-minimap__card casts --elevation-2 case passes (the merge already moved the card to it). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The preview card now runs on ui/hoverIntent's useHoverIntent, as base's minimap did: the first waits the hover delay, the card then follows the pointer from row to row at once while the rail is warm, and it goes as the pointer leaves the rail (moving into the card, or back, keeps it). Keyboard focus still shows it at once; Escape dismisses it and focus-out closes it on that frame. cardTiming.ts and the CARD_* constants are gone. thread-bookmarks.test.tsx goes back to the PR's version: the merge took base's half, which drives the old rail's pointermove on two prompts. heavy --mem 3G bun test test/hover-intent-cards.test.tsx test/subtle-elevation.test.ts test/thread-minimap-rail.test.tsx 46 pass, 1 fail (.lg-sheet casts --elevation-2: base red, ledger.css untouched) heavy --mem 3G bun test test/thread-minimap-current.test.tsx test/thread-bookmarks.test.tsx test/thread-turn-keys.test.tsx test/hover-intent-parity.test.ts test/hover-row-sweep.test.ts test/thread-minimap-motion.test.ts 7 pass, 0 fail (browser lanes skipped locally) heavy --mem 4G bun run check: pass Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
export-action-surfaces.sh on 6cf5407 (fleet step a2d9f3a7): 961 actions (base's chat-identifier actions plus this PR's transcript actions). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ead-minimap-codex
export-action-surfaces.sh on eec9482 (fleet step fa811efc): 963 actions (base's Music actions added). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ead-minimap-codex
…y captures debug.agent_pane seed_rows fixture "short": one-line prompts answered "ok N", the same content as the Codex reference chat, so tick and highlight density compare like for like. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ead-minimap-codex
…ead-minimap-codex
… row (tab-switch-rewrite) syncSelection walked every cell twice per switch: the full separator pass and the occlusion pass (TabOcclusion samples each cell's motion path). At rest no tab overlaps another and no mask is on, so occlusion runs only while a strip move is in flight (movesInFlight, cleared by the move's completion), and the separator rule is applied to the lines next to the old and new selected tab through a slot index built once per layout (TabSlotIndex), which reveal also reads. A multi-selection change keeps the full pass. TabStripSwitchSeparatorTests proves the lines equal a full pass after any switch order. Benches (fleet, aws-m4pro-5, same host A/B against a9439ce): TabSwitchPathBench apply p50 60 tabs 0.42 -> 0.31 ms, 30 tabs 0.21 -> 0.15 ms; strip separators 38 us -> 2.5 us at 60 tabs, constant. TabSwitchPathBench now prints the per-stage split (BenchSpans switch.*), and TabStripSelectionBench each strip step. scripts/cmux-next/tab-switch-matrix.py also reports each Chromium and WebKit page's own first frame after a switch (content_frame_ms). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fleet render at 39b0b0b still drew the Name field at its ideal width: the Grid sized to its cells, so maxWidth on a cell had nothing to fill. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Grid kept the Name field at its ideal width (the maxWidth frame sat outside the drawn card) and wrapped the Chief checkbox. Captions over the controls need no label column, so long labels in any language fit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… them (AgentTranscriptActionCatalog order)
Review of base-merge 7938b34 and fix 6521796 (subagent, correctness first)I reviewed both commits by reading, grepping and running small Python checks. Nothing was built or run.
6521796 (the fix)
Verdict: MERGE |
…k is one flat menu (red) Lawrence 2026-10-20: Move to Top left a workspace under groups that were pinned (the Pinned band lists above the machine sections), but nothing showed they were pinned and Pin/Unpin could only pin. And the header's right-click opened the editor, whose More Group Actions row held the menu. Red: SidebarDocPinnedGroupTests (a Pinned-band group maps pinned), WorkspaceGroupFlatMenuTests (common rows top-level, no catch-all folder, every row in the palette), GroupPaletteDefaultTests (right-click shows the menu, editor rows without More). Design: plans/cmux-next/sidebar-ordering-DESIGN.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ight-click is one flat menu Root cause (plans/cmux-next/sidebar-ordering-DESIGN.md): the groups Move to Top left a workspace under were pinned. Pin Group moves a group into the Pinned band, above every machine section, but SidebarDocSections mapped every document group with pinned: false, so no pin glyph showed and Pin/Unpin always pinned. A Pinned-band group now maps pinned (the header's pin, Unpin Group in the menu and editor), and Pin/Unpin toggles from what the sidebar shows. The header's right-click opens the group's menu at once (no editor, no More Group Actions row): Color, Set/Remove Icon, Pin/Unpin, Collapse All/Expand All/Collapse Others, Move to Top, Ungroup, Close Group, Delete Group and Close Other Groups are top-level; Move and Sort keep their lists. The editor offers Pin and Move to Top itself. New Workspace in Group, Pin/Unpin Group, Mark Read/Unread, Clear Notifications and Edit Group Config join the palette. Pin and Collapse rows read as the change they make. Strings in all 21 locales (19 need review). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ActionSurfaceParityTests.everyMenuKeepsAShortTopLevel (Lawrence 2026-10-01: at most 12 top-level rows) failed at 21. The rows Lawrence named stay top-level (New Workspace in Group, Rename, Color, Pin/Unpin, Collapse/Expand, Move to Top, Ungroup, Close Group) with Delete Group and Close Other Groups; Options ▸ takes icon, read state, collapse-all, config and Duplicate, and Move ▸ takes the group sorts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…p-level rows Its one-row Copy folder showed inline as a 13th row. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Regenerated on the fleet (key a219-exports-820ba663b9c, scripts/measure/export-action-surfaces.sh). check-group-menus.py passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… (the pattern that broke CEFPumpScheduleTests) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…minimap-feel # Conflicts: # plans/cmux-next/actions.md
Review of base-merge 452faf1 (subagent, correctness first)I checked this merge with git and Python by reading only; nothing was built. The only files both sides changed are the generated exports.
Verdict: MERGE |
| /// Passes over every cell, by kind (`occlusion`, `separators`, `frames`): what a change cost, for | ||
| /// the switch work budget (a tab switch at rest runs none, at any tab count). | ||
| var rowPasses = TabStripRowPasses() |
There was a problem hiding this comment.
TabStripView.rowPasses adds counters to shipping code solely for TabSwitchWorkBudgetTests; no production code reads them. This violates the repository requirement that test-only seams stay out of production Sources/.
Move this test scaffolding into the test target and inspect existing internal state through @testable import, following the reference fix in #6452.
Rule Used: Flag Swift files under a production Sources path (matching /Sources/ and not under /Tests/) that add a test-only or debug-only seam: a #if DEBUG (or other test-build-guarded) extension/member exposing internal state only for tests or a debugg... (source)
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
|
Shipped in:
|
… the chat (#24058) * test(webviews): the wave rail is the default and its band reaches the bottom (red) Leo 2026-10-22: "it wouldn't show the highlighted bottom text where I was scrolling at the bottom, and it just didn't go to the bottom" and "I didn't want the mini map." These fail until the compact rail (the wave from #22228) is the default arm again and the rail's own list keeps the whole band in view, reaching its end at the bottom of the chat. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15cfc-5eec-7700-94d7-9a3a1f7d889a&host=Air-Blue * fix(webviews): the wave rail is the thread rail again, and its band reaches the bottom Leo 2026-10-22: "Why did you turn it into a whole mini map? ... I didn't want the mini map." and "IT'S JUST THE FUCKING LINES THEY DONT FUCKIG ANIMTATE". His "the rail feels short" meant the band never reached the bottom ticks: "it wouldn't show the highlighted bottom text where I was scrolling at the bottom, and it just didn't go to the bottom." - thread-minimap-layout defaults to compact again: the Codex rail from #22228, one tick per prompt, the band that follows the scroll and the Dock-style magnify under the pointer. The full-height map (#22784) and off stay selectable from cmux.experiments. - The rail's own list (a chat with more prompts than the rail has room for) kept only the band's first row in view, a third of the way down, so the prompts below it sat under the bottom fade or past the rail's end. railScrollTop keeps the whole band in view and clear of the fades, and a band holding the last prompt scrolls the list to its end, where no fade draws: at the bottom of the chat the bottom ticks show lit. - The list keeps overflow: hidden (#23735), so it never draws a scroller, and the rail stays sticky with no height: no layout shift on a tab switch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15cfc-5eec-7700-94d7-9a3a1f7d889a&host=Air-Blue * test(webviews): the rail's lit ticks stay clear of the transcript's faded edges (red) Review of #24058: the rail is inside .acpmux-scroll, whose mask fades rows out 18 px under its top once scrolled and 18 px (56 px with Jump to latest) above its foot while more lies below. In a short pane the rail filled the view less 8 px each end, so the first and last lit ticks drew inside those fades at every offset short of the chat's ends: the bottom ticks Leo asked to see lit were hidden again. - thread-minimap-bottom: a 320 px pane, every offset; and the model's fade constants must cover the CSS's bands. - thread-minimap-feel (Chromium): a 420 px window, the real mask's bands read from the scroller's computed style. - thread-minimap-rail: railHeight leaves room for the bands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15cfc-5eec-7700-94d7-9a3a1f7d889a&host=Air-Blue * fix(webviews): the rail keeps its ticks clear of the transcript's faded edges The rail sits inside the transcript, so the transcript's mask fades it: 18 px under the top once scrolled, 18 px (56 px with Jump to latest) above the view's foot while more lies below. railRoom keeps the rail's rows out of both bands at their widest (so the rail never moves as they come and go), and the rail centers on the view as before, moved only as far as that takes. App passes the mask's foot (the bottom stack's top), so a composer grown while following does not cover the bottom ticks either. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15cfc-5eec-7700-94d7-9a3a1f7d889a&host=Air-Blue * test(webviews): every lit tick draws whole in the rail, down to a 96 px pane (red) Review round 2 of #24058: in a 96 px transcript (the CSS minimum) with four prompts the rail was 18 px tall, and a band in the middle of the chat put its lit tick at 10..20 px, past the rail's end. The list's own 40 px edge fades also covered a rail that short. - thread-minimap-room: model sweep over pane heights 96 to 640 px, 1 to 70 prompts, every band of 1 to 4 prompts scrolled down and back up. Every lit tick must sit whole inside the rail, and, where the pane has room, clear of the transcript's faded edges. railHeight takes the band's size (a new fifth argument; the red commit ignores it). - thread-minimap-tiny: renders panes 96 to 336 px, 4 to 70 prompts, offsets down the whole chat, and checks every lit tick against the rail, the list's fades and the transcript's mask. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15cfc-5eec-7700-94d7-9a3a1f7d889a&host=Air-Blue * fix(webviews): the rail is never shorter than the band, down to a 96 px pane Review round 2 of #24058: in a 96 px transcript the rail was 18 px, shorter than its own 40 px edge fades, and a band in the middle of the chat drew its lit tick past the rail's end (railScrollTop fell back to 0 when the fade clearances conflicted). - railHeight takes the number of prompts on screen and never returns less than their rows: in a pane too short for the band and the transcript mask's clearance, the clearance gives way first, up to the whole view. railTop then centers the rail in the view. - The list's edge fades are a quarter of a short rail at most (railFade, --acpmux-minimap-fade) and never draw over the band (railFades). - railScrollTop keeps the band whole inside the list when the fade clearances conflict, instead of falling back to 0. - The rail hides only if the view has no room for one row; the transcript's 96 px minimum always has room. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15cfc-5eec-7700-94d7-9a3a1f7d889a&host=Air-Blue * test(webviews): the bottom rail test reads only the mask fades that draw, and has room to run maskFades took the smallest --acpmux-fade-bottom in conversation.css, which is one of the 0px rules (find open, Reduce Transparency, Increase Contrast) that turn the fade off, so the CSS check failed at once. Those rules draw nothing; only the nonzero bands count. The two render sweeps take about 5 and 7 s in jsdom, past bun's 5 s default when run with plain `bun test` (CI's runner raises it to 30 s), so the file sets 60 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15cfc-5eec-7700-94d7-9a3a1f7d889a&host=Air-Blue * test(webviews): the short-pane rail sweeps run in a full-height window, as the app does The rail caps its height at 70% of the window (globalThis.innerHeight), and bun's globalThis has none, so the rail fell back to 70% of the pane. In a 320 px pane that is 224 px, centered, nowhere near the transcript's 18 px fades, so the short-pane test passed on the code it was written against (34b6d18) as well as on the fix. In the app the window is taller than its transcript and the rail filled the pane less 8 px a side, into the fades. Both sweeps now run with a 900 px window: the 320 px test fails on 34b6d18 again, and the tiny sweep fails at 96 to 176 px on 3eec766 (it failed at 96 to 128 before). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15cfc-5eec-7700-94d7-9a3a1f7d889a&host=Air-Blue * test(webviews): a token streaming into the last reply builds no tick again (red) #24058 review round 3: every streamed token gives the transcript a new rows snapshot, and the rail maps every prompt again for it, building each tick's label and element: O(prompts) work per token on the main thread, now that the rail is the default. thread-minimap-stream renders a 300-prompt chat and streams 20 tokens into the last reply, counting the tick labels the rail builds (each tick it renders reads its label string once). It expects none per token, the card to still preview the streamed reply, and a new prompt to add its tick while the earlier tick elements stay. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15cfc-5eec-7700-94d7-9a3a1f7d889a&host=Air-Blue * fix(webviews): a streamed token renders no tick on the rail #24058 review round 3: each streamed token is a new rows snapshot, and the rail mapped every prompt again for it, building each tick's label and element: O(prompts) per token. - minimapTurns also returns `ticks` (each prompt's key, id and text), which keeps the previous list, by identity, while the prompts are the same. It compares only the turns from the first changed one on; the earlier ones are the previous objects. - The rail draws its rows through a memoized TickList that takes `ticks` and the band, hover, focus and card state, so a render that changes none of these (a token in the live reply) renders no row. `turns` still changes with the reply, so the card previews it as it streams. - Per token the rail now does the reply's turn (as before) plus a compare from the changed turn to the end; the ticks are not visited. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15cfc-5eec-7700-94d7-9a3a1f7d889a&host=Air-Blue * test(webviews): a token reads rows bounded by what changed, and a hovered rail keeps the band (red) #24058 review round 4. - thread-minimap-stream: minimapTurns reads (through a counting proxy) the same few rows for a streamed token in a 300- and a 3000-prompt chat, given the transcript's change point; it used to compare every row from the first. A mid-chat edit and an inserted row read the same turns as a fresh read, and a change point for other rows is ignored. A new prompt keeps every earlier tick element, not only the first. - thread-minimap-bottom: with a tick hovered, and with one focused, scrolling the 90-prompt chat from top to bottom keeps every lit tick inside the rail's unfaded area. The rail used to stop following the band while hovered or focused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15cfc-5eec-7700-94d7-9a3a1f7d889a&host=Air-Blue * fix(webviews): a token reads only the live turn's rows, and a hovered rail keeps the band #24058 review round 4. - minimapTurns took the first changed row by comparing every row from the first, O(rows) per token. VirtualTranscript already finds that point once per snapshot (its arriving rows); it now keeps it (RowsChange) and passes it to the rail, which uses it when it is the step from the rail's last rows to these and compares from the start otherwise. The kept turns are found by binary search. A streamed token now reads the live turn's rows only: 1202 row reads for both a 300- and a 3000-prompt chat in the test (12002 for 3000 before). - A turn whose reply streamed on past its preview (replyPreview stops once full) keeps its object, so the turn list stays the same object too. - The rail followed the band only with no tick hovered or focused, so scrolling the chat under a hovered or focused rail took the band out of it. It follows always now; railScrollTop keeps the list where it is while the whole band shows clear of the fades, so the tick under the pointer moves only when the band would leave. Only a scrub holds the list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15cfc-5eec-7700-94d7-9a3a1f7d889a&host=Air-Blue * test(webviews): the magnified tick follows a still pointer, and a token copies no turns (red) #24058 review round 5. - thread-minimap-bottom: hover tick 3 with the pointer at y 35, then wheel the chat with the pointer still: once the rail's list moves, the magnified tick must be the one under the pointer at every offset. It stayed tick 3. - thread-minimap-stream: the previous turns behind a counting proxy: a streamed token whose preview is still filling must update the list in place (same array) with about log2(prompts) reads, at 300 and 3000 prompts. It copied every earlier turn (slice). And, through VirtualTranscript: another chat with repeating row ids, the same chat replaced wholesale, then cut, each show their own tick labels. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15cfc-5eec-7700-94d7-9a3a1f7d889a&host=Air-Blue * fix(webviews): the magnified tick follows a still pointer, and a token copies no turns #24058 review round 5. - The rail's list follows the band with a tick hovered, but hover is a row index set on pointerenter, and a list that scrolls under a still pointer fires none: the old tick stayed magnified while another sat under the pointer. The list records the pointer's last clientY (pointerover, pointermove; cleared when the pointer leaves the rail), and when it moves the list while hovered, the hovered row and the card's are the row now under that point. - minimapTurns copied every earlier turn (slice) for each snapshot, O(prompts) per token while a reply's preview fills. It now reads only the turns from the changed one, compares them with the previous list's, and updates that list in place when they differ (the previous result is spent once passed in). The live turn is found by binary search: about log2(prompts) reads. ThreadMinimap renders on each snapshot and reads the list then; `ticks` keeps its own identity rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15cfc-5eec-7700-94d7-9a3a1f7d889a&host=Air-Blue * Test: the rail keeps committed turns, and follows the pointer when focus scrolls it Red at this commit (#24058 review round 6): - a dropped render (the next rows read, never committed) leaves the committed turns as they were; minimapTurns writes the previous list in place, so the committed list takes the dropped prompt; - End or an arrow key that focuses a tick past the list's end, with the pointer still on the rail, magnifies the tick under the pointer; the focus scroll only moves listTop, so the old tick stays magnified. The still-pointer test now puts the list 140 px down, so the rail's own offset counts, and the default test reads a stored cmux.experiments choice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15cfc-5eec-7700-94d7-9a3a1f7d889a&host=Air-Blue * Agent chat: the rail leaves committed turns alone and follows the pointer on any list scroll Two findings from #24058 review round 6: - minimapTurns wrote the previous turns list in place, during render. A render React drops then left the committed rail holding rows it never committed. The result now carries a spare list (the one before its turns) and changed turns go there, never into the previous list; a record of how far the spare still holds the committed turns means a streamed token writes the live turn alone, still no copy of the others. ThreadMinimap stores the committed result in a layout effect, not in the memo. - A key that focuses a tick past the list's end scrolls the list under a pointer that did not move, and only listTop followed. The list's scroll handler now remaps the hovered tick from the last pointer position, as the follow effect does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15cfc-5eec-7700-94d7-9a3a1f7d889a&host=Air-Blue --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…%, and stays lit on hover Carries over the parts of #24065 that #24058 does not cover, built on #24058's rail (railScrollTop, followPointer). The full-height map stays as #24058 left it. - At rest a row coming on screen brightens, and one leaving dims, over the motion scale's micro step (120 ms), so the band glides down the rail. Hover, scrub, data-reduce and prefers-reduced-motion switch at once; the full map keeps its look. - Lit ticks draw at --acpmux-minimap-lit, 75% by default (was 60%), over rest at 40%. lit.experiment.ts keeps 60/70/75/85% for the gallery's compare view and dogfood. - The prompts on screen stay lit while the pointer is on the rail (#22228 dimmed them back to rest there). Tests: thread-minimap-default-wave (default arm, each arm reaches the rail, the CSS); thread-minimap-motion reads the bar's transitions as a list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16030-bf28-7072-b1f6-b83bbd0beacc&host=cosmic-gray
…and glides at rest, shows at 75%, and stays lit on hover (#24526) * cmux NEXT agent chat: the rail's lit band glides at rest, shows at 75%, and stays lit on hover Carries over the parts of #24065 that #24058 does not cover, built on #24058's rail (railScrollTop, followPointer). The full-height map stays as #24058 left it. - At rest a row coming on screen brightens, and one leaving dims, over the motion scale's micro step (120 ms), so the band glides down the rail. Hover, scrub, data-reduce and prefers-reduced-motion switch at once; the full map keeps its look. - Lit ticks draw at --acpmux-minimap-lit, 75% by default (was 60%), over rest at 40%. lit.experiment.ts keeps 60/70/75/85% for the gallery's compare view and dogfood. - The prompts on screen stay lit while the pointer is on the rail (#22228 dimmed them back to rest there). Tests: thread-minimap-default-wave (default arm, each arm reaches the rail, the CSS); thread-minimap-motion reads the bar's transitions as a list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16030-bf28-7072-b1f6-b83bbd0beacc&host=cosmic-gray * cmux NEXT agent chat: only the scroll's band moves glide; focus, hover and leaving snap Codex review of #24526 (BLOCK at 61de4e9): - A key focusing a tick made it active while the rail stayed at rest, so the focused row faded in. Active ticks never take the glide now. - The pointer leaving the rail dropped data-hovering in the same commit that changed the hovered row's tone, so leaving faded too. A commit that changes the active row, the hover or the scrub marks the rail data-snap and its rows switch at once; the rail's next render that changes none of these drops the mark. - thread-minimap-motion asserted durations with a pattern a single 0.12s also matched. It now reads transitionProperty: exactly "transform, opacity", each 0.12s with no delay. thread-minimap-default-wave: the glide's selectors exclude data-snap and the active tick, and a VirtualTranscript render marks data-snap on focus and blur and clears it on the next band move. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16030-bf28-7072-b1f6-b83bbd0beacc&host=cosmic-gray --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Leo (10-19), on the agent chat's left-edge rail: "it's been shit and has stayed shit for a while, it doesn't feel good like codex's". This makes the thread minimap behave like the Codex desktop app's prompt rail, in every state.
It builds on #20560, which rebuilt the rail from Codex's source and 60 Hz recordings but stopped at merge conflicts on 10-15. This branch merges current
feat-cmux-nextinto it and fixes what still felt worse than Codex, measured in Chromium with a 60-turn chat. This PR replaces #20560.The reference, per state
The source is the Codex reference spec in the private screenshots archive (
references/codex-prompt-rail-2026-10-14/SPEC.md). It was read from Codex 26.930's shipped webview code (thread-user-message-navigation-rail-app-*.js) and checked against 60 Hz screen recordings. Leo's screenshots from 10-19 show the same states. Verified means seen in both the source and the frames. Assumed means it wasn't captured.linear(), about 0.8% overshoot), all rows together. Leaving eases back in 150 ms. The band dims to rest while the pointer is on the rail.cubic-bezier(0.35, 0.05, 0, 1). The turn's block lands at the top with its bubble 35 px down, then the bubble fades from a highlight over 1400 ms. An unmounted target jumps at once.overflow-y: auto). Codex portals the rail outside the scroll element; what a wheel does there wasn't recorded. Ours scrolls the transcript, or the rail's list when it overflows.cubic-bezier(0.3, 0.2, 0, 1)). Rows are Tab stops: a focused row grows alone and opens its card at once. Return jumps and Escape closes.Defects, measured
Each column is a Chromium run of the same 60-turn chat (replies of 1-9 paragraphs) in a 1280 × 820 window, measured with the real transcript and rail. "Band correct" compares the lit rows with the turns on screen by Codex's rule, read from the drawn rows each frame.
feat-cmux-next(what Leo uses)What this PR changes on top of #20560
scrollTopdown after React schedules the scroll's render, which is 1-3 frames late. The rail now listens to the scroller itself, in capture. When a scroll moves the band, it renders the rail synchronously (flushSync; only rows whose state changed draw again). A scroll that doesn't move the band renders nothing.delayOpen), set for the rail only. The rest of the app keeps the 450 ms hover delay from Hover intent: tooltips and hover cards wait, then stay warm, app-wide #20528. Under 200 ms still counts as passing over, so the 10-14 complaint (cards opening on every row the pointer crossed) stays fixed.Tests
test/thread-minimap-feel.test.ts(new, Chromium browser lane) covers four things:check:browser-lanesred.thread-minimap-motion(WebKit),thread-minimap-rail,-current,thread-bookmarks,thread-turn-keys,hover-intent-cards,transcript-followandtranscript-flingall pass on Huge Blue. The rail's card tests now expect 250 ms.tsc --noEmitis clean andvp fmtis clean on the touched files. Other parts ofbun run checkare red on base and outside this PR:agent-pane-older-rows-fade.test.ts(browser-lanes),syncMotion.gallery.css(type-scale), a text-focus ring, andsettings/icons.tsxformatting.Gallery
agent-pane.thread-minimapgains three states:rest: mid-thread, the band lit, every tick equal;scrubbing: a press on row 3, dragged to row 12;seventy-prompts: the rail at its cap, scrolled, with both edge fades.It keeps
long-chat(hover: fisheye plus card) andbookmarked. The gallery advisory run captures them.Not in this PR
plans/cmux-next/actions.md) take base's copy here, and the autofix bot regenerates them.FYI @lawrencecchen, since you wrote the original rail.
Changelog
Changed: the agent chat's thread minimap looks and feels like the Codex rail: equal ticks with the on-screen turns lit, the lit band following the scroll every frame, a dock-style magnification and preview card on hover, and jumps that land exactly
🤖 Generated with Claude Code