cmux NEXT agent chat: the left rail stays beside Session Info
Leo's words (2026-10-25)
"So the left rail thing, that still gets hidden. So, like, when that session rail is there, the session rail ends up pushing it to the side, and also when the left pane gets too narrow, the session rail still disappears. We can't have it disappear."
The "left rail" is the transcript's thread rail (threadMinimap/ThreadMinimap.tsx), and the "session rail" is Session Info (inspector/ChatInspector.tsx). This PR fixes the first half: Session Info pushes the rail aside. The second half, where the rail disappears in a narrow pane, is already open as #24525 (cx4-cs24525, alive on cmux16). This PR does not duplicate it. See "Narrow pane" below.
Cause
webviews/src/agent-session/acpmux/inspector/inspector.css:82-87is from #24948 (fafe2d4c1ea). Floating Session Info moves the chat left by--acpmux-chat-shiftby translating.acpmux-main > .acpmux-stage > .acpmux-scroll. The thread rail draws inside that scroller (App.tsx:3801,.acpmux-minimap, sticky), so it moved too:- In a 1200px pane it moved 111px left, from x=16 to x=-95, off the pane.
- In a 1100px pane it moved 146px left.
inspectorChatShift(ChatInspector.tsx:121, #24564 078475554e5, reshaped by #24948) let the shift use the whole left gutter (lacks <= inset). So a moved column could also end up over the rail's place.
Fix
- The rail stays at the pane's left edge.
inspector.cssmoves.acpmux-scroll > .acpmux-minimapback by+--acpmux-chat-shift. It uses the same transition, Reduce Motion rules and Push-always rules as the scroller, so the two moves cancel on every frame. I chose "stays put" over "moves with the column" because the rail belongs to the pane's edge, not to the text. Session Info is on the right, so nothing on the left should move. - The column never slides over the rail.
inspectorChatShiftshifts the column only while the left gutter keeps the rail's room:lacks <= inset - MIN_GUTTER. This applies only beside the transcript (!wide). A handoff review's form takes the transcript's place and draws no rail. Its shift reserves the form's own 18px edge clearance instead (HANDOFF_FORM_EDGE, from handoff/styles.csscalc(100% - 36px)). Base reserved nothing there, so the form could reach the pane's edge (from #24948).- 1200px: still shifts 111px. The gutter is 225px.
- About 1110px and narrower: no shift, so Session Info floats over the chat as it already did at 900px and below.
- Leo's earlier rulings are kept. Session Info floats. The column shifts left only when both fit, and now "both" includes the rail. Push always is unchanged.
Narrow pane (#24525, landed)
On base before #24525, the rail hid under a 48px gutter (MIN_GUTTER). #24525 landed as 0d6c0cb0e7f and draws a minimal narrow rail there. This PR is rebased onto it; the rebase left the reviewed diff byte-identical. It adds only the test widths that #24525 made possible.
Tests
The new browser-lane test is test/thread-minimap-left-rail-stays.test.ts, run on Chromium with CMUX_BROWSER_TESTS=1. In 1600, 1200, 1100 and 1000px panes, and in the 200px minimum pane (layout.minimumPaneWidth), with Session Info closed, open and pushing, it checks that:
- the rail draws;
- the rail stays inside the pane;
- the rail stays clear of the transcript's column;
- opening Session Info moves the rail by 0px.
Results:
- Red on base: 1200 fails with
moved: -111,insidePane: false. 1100 fails withmoved: -146,insidePane: false. 1600 passes. - Green on this head: 5 of 5 pass. The 1000px and 200px cases pass on base too, thanks to #24525. They are there so the rail can't regress.
test/chat-inspector.test.tsx adds three cases:
inspectorChatShift(1100, 320) === 0: moving would take the rail's room.inspectorChatShift(1140, 320) === 126: the column moves and keeps 84px for the rail.inspectorChatShift(1140, 320, undefined, true) === 146: the handoff form moves by everything it lacks and keeps 44px.- Handoff boundary: 1114px shifts 159px and keeps exactly 18px. 1112px and 1100px don't shift; Session Info floats over the form.
Review round 1 (gpt-6-astra, CHANGES at dc88b8b0)
Finding: the rail reserve also applied to wide handoff reviews, which have no rail. At 1140px the form's 190px gutter lacks 146px, but the 142px limit returned 0, so Session Info lay over the form. Fixed in d4c15a39 by reserving the rail's gutter only when !wide. The new handoff case fails before the fix (expected 146, received 0) and passes after. chat-inspector.test.tsx (21), chat-inspector-placement (11) and the new rail test (3) all pass at the new head.
Review round 2 (gpt-6-astra at d4c15a39)
P2: the handoff form's 18px edge clearance wasn't kept. At 1100px the form moved 166px and kept 4px; at 1096px it kept 0. Fixed in 98bf97ac, which reserves HANDOFF_FORM_EDGE for wide. The 1112px boundary test fails at d4c15a39 (received 160) and passes after. All three test files pass again at 98bf97ac (21, 11 and 3).
Rebase onto feat-cmux-next (with #24525)
The reviewed diff is unchanged. f9d4e9ca adds the 1000px and 200px widths and Push always at every width, and drops the 1100px Push-always skip. At f9d4e9ca: the rail test 5/5, chat-inspector 21, chat-inspector-placement 11, thread-minimap-narrow 10.
Each existing file was run on its own, all green: every chat-inspector* file (21 in chat-inspector.test.tsx, 11 in chat-inspector-placement), every thread-minimap*, thread-scroll-map*, minimap-reply-preview, thread-bookmarks and thread-turn-keys. vp fmt, vp lint and tsc are clean, and so are the browser-lanes and fixed-waits checks.
Captures
These come from the pane harness (papercutPane, Chromium, 8 prompts). The red outline marks the rail and was added for the capture only. In each image the rows are Session Info closed, open, and Push always; the columns are before and after.
1100px. Before, opening Session Info pushes the rail off the pane. After, the rail stays and the panel floats.

1200px. Before, the rail is gone with Session Info open. After, the column still moves left and the rail stays at the pane's edge.

1600px. No change; nothing moves here.

Risk
Low. The change is CSS plus a one-term change to the shift's fit check. It adds no new timer, observer or animation loop. The rail's back-translate is a CSS transition that ends at its target.
The visible change: in panes from about 1110 to 1150px, the column used to move left beside Session Info and now stays put, with the panel floating over the chat, as it already does at narrower widths. design/session-info.md's rule "the column moves left only when both fit" still holds, with the rail counted.
Overlaps (pr-overlap):
- #24225 and #24272 share
ChatInspector.tsx/inspector.cssbut not these lines. - #25116 (Session Info stays mounted across splits) does not touch the shift.
Agent session: huge-blue
š¤ Generated with Claude Code