Repository navigation
cmux NEXT agent chat: Session Info opens and closes per pane - #25169
Conversation
Leo, 2026-10-25: "basically the video shows the session rail opening and closing it opens and closes on the other side too when you do one... they should all be individual per pane" The Inspector's open state, width and folds were one localStorage key every chat page followed through the storage event. Each pane now keeps its own, saved under its chat's session id (bounded to the newest 200 chats of the last 30 days); no page follows another's writes. The last choice made in any pane (the old shared keys) is the default for a pane with no state yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16f7b-11f6-73e0-8276-7c0e3bb3ead4&host=huge-blue
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. |
|
Owned by rail-opus-01 on huge-blue · session |
|
Bead |
|
Found 88 test failures on Blacksmith runners: Failures
...and 78 more test failures. View all on Blacksmith: run 40247590590, run 40247590279
|
|
|
- One localStorage key per chat (cmux.agentPane.inspector.pane.<sessionId>), so two panes saving at once never overwrite each other's chat. - No session id (a harness switch's new chat): nothing is saved until one comes; the pane keeps what it shows and saves it under the new session. - The chat's state is read before render (useInspectorOpen(sessionId)) and the panel is keyed by chat: switching to a saved-closed chat no longer plays the last chat's panel out. - Pruning always keeps the pane's own chat, then the newest 199 others of the last 30 days. - The unit test runs each pane as its own copy of the module over one storage, so it fails on base for the behaviour, not a missing export. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16f7b-11f6-73e0-8276-7c0e3bb3ead4&host=huge-blue
|
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)
|
|
|
A save no longer lists and parses every localStorage key: a small index (cmux.agentPane.inspector.index, [sessionId, savedAt] pairs, at most 200) is updated on save and pruned from. The one full scan runs once per page, on its first save, and adds a chat key no index lists (a lost index write) to the index so pruning still reaches it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16f7b-11f6-73e0-8276-7c0e3bb3ead4&host=huge-blue
|
|
…ince A pane prunes from the index it read; another pane may have saved one of those chats since. Before removing a chat's key the pane re-reads it and removes it only while its own savedAt is still the one the index named; a chat saved since is listed again with its new time. The pane's own chat is never pruned. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16f7b-11f6-73e0-8276-7c0e3bb3ead4&host=huge-blue
… clock rollback A chat's key is removed only while its savedAt is exactly the time the index named; any other time, an earlier one after the clock was set back included, means another pane saved it since, so it is kept and listed again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16f7b-11f6-73e0-8276-7c0e3bb3ead4&host=huge-blue
The two 500 ms waits become a wait for the other pane's write in the shared storage, then two painted frames (cmuxAfterPaint), so check:fixed-waits stays at its 0 baseline. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16f7b-11f6-73e0-8276-7c0e3bb3ead4&host=huge-blue
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. |
![Fix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/fix-with-codesmith-light-3x.png)
Leo, 2026-10-25, word for word:
("Session rail" is Session Info, the agent chat's floating Inspector.)
Cause
webviews/src/agent-session/acpmux/inspector/inspectorState.tskept open, width and folds in one set of localStorage keys (cmux.agentPane.inspector.openand friends, lines 1-5 and 36-38 on base), "kept in step across panes by thestorageevent" (thesubscribelistener, lines 97-109 on base). Every chat pane is its own page on one origin, so a toggle in one pane wrote the key and every other pane followed its storage event.Fix
inspectorState.tsnow builds a per-page store (createInspectorStore). Each pane's open state, width and section folds are its own. No page listens for another page's writes, so a toggle, a drag or a fold in one pane never reaches another.cmux.agentPane.inspector.pane.<sessionId>({ open, width, sections, at }). A pane writes only its own chat's key, so two panes saving at the same moment cannot overwrite each other. There is no shared blob and no read-modify-write across chats. The session id is what the page already has and it survives a relaunch. The page has no stable pane or tab id: the URL, tab row, header state and page-host payloads carry none (the review confirmed this), so no native change is made. With the same chat in two panes, the panes are independent while they run. After a relaunch both start from that chat's last choice. This is a known gap until the native side passes a pane id.useInspectorOpen(sessionId)anduseInspectorState(sessionId)read the chat's saved state before the pane binds to it), and the panel is keyed by chat. A pane switched from chat A (open) to chat B (saved closed) therefore shows B's state from its first frame, and A's panel does not play its exit animation over B.cmux.agentPane.inspector.index([sessionId, savedAt]pairs, at most 200), is updated on every save. Pruning works from the index: the pane's own chat is always kept, even after the clock is set back or on a timestamp tie, plus the newest 199 others touched in the last 30 days. Dropped chats' keys are deleted. A toggle, fold or resize reads only its chat's key and the index, never the rest of storage. Only the small index is read and rewritten; if another pane's write loses an update to it, pruning is delayed but no chat's state is lost. Before deleting a chat's key, the pane re-reads that key and deletes it only while its storedatis exactly the time the index gave. A chat another pane saved after this pane read the index, even at an earlier time after the clock was set back, is kept and listed again with its new time. The one full key scan runs once per page load, on that page's first save, and adds any chat key no index lists to the index, so pruning still reaches it.Tests
test/inspector-per-pane-state.test.ts(new, jsdom, 11 tests). Each pane is its own copy of the module, imported under its own query the way each page loads its own, over one shared localStorage.key()andlengthzero times andgetItemat most 24 times (spy; red at 82bba1e); a chat key no index lists is still pruned; an eviction interleaving (pane A reads the index, pane B saves chat X fresh, pane A prunes) keeps X's state (red at c81cf1b), including when B's clock was set back to before the index's time (red at 8749acb).test/chat-inspector.test.tsx, new test: "a pane switched to another chat shows that chat's Inspector at once". A MutationObserver records everydata-presencechange. Red at e44cfe5 (shown -> leaving), green here. The old "follows another pane's change" test now asserts the per-pane rule.test/chat-inspector-per-pane.test.ts(new, browser lane,CMUX_BROWSER_TESTS=1). Two papercut panes share one browser context, so they share storage. Closing Session Info in the left pane leaves the right pane's open, and the left pane is still closed after a reload. Red on base (right:panel 0, pressed false), green here (Chromium; WebKit did not launch on huge-blue, and CI runs both).chat-inspector*tests pass alongside the new unit test (101). Typecheck, lint and fmt are clean.inspector-dead-clicks.test.tsx"every clickable thing" at the 5 s default, and the rest of that file then cascades with "overlapping act()". The test takes the same ~3.7 s with and without this change (3 runs each), so the timeouts are host load. A run alone or in a fresh batch passes.Captures
Two panes showing the same chat side by side, after the toggle in the left pane is clicked.
Before (feat-cmux-next): the right pane closed too.

After: only the left pane closes.

Risk
Low, webview only. The only behavior change is that state no longer crosses panes: another chat or a new chat starts from the last choice instead of following live. The Inspector's own UI is not touched. Nearby PRs: #25116 (Session Info stays visible across pane changes) and #25148 (left rail beside Session Info) touch
ChatInspector.tsxin other hunks, and a trial merge with each is clean.Agent session: huge-blue
🤖 Generated with Claude Code