Skip to content

One chat in two tabs: Session Info and the transcript place are kept per tab; a Session Info popover gets its glass - #34492

Merged
teamleaderleo merged 2 commits into
feat-cmux-nextfrom
lane/session-state-per-tab
Nov 4, 2026
Merged

teamleaderleo merged 2 commits into
feat-cmux-nextfrom
lane/session-state-per-tab

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Nov 4, 2026 •

Copy link
Copy Markdown
Collaborator

Leo, 2026-11-03, on one chat open as a tab in two workspaces: "i feel like right split is fine on click by default (extra view doesn't really affect anything), and you can riht click to go to it. but of course we just have to be prepared to mitigate oddities"

So two views of one session have to be safe. Two pieces of per-view state were kept by session, not by tab, so a reload of one tab took the other tab's state:

  1. Session Info (open, width, folds) in the page's localStorage, cmux.agentPane.inspector.pane.<sessionId>. Session keying came from cmux NEXT agent chat: Session Info opens and closes per pane #25169 ("Session Info opens and closes per pane", 2026-10-25), carried through the rename in carry #29651: Session Info is called Session Info in its code, not Inspector #29921.
  2. The transcript's place (AgentPaneScrollAnchors, chat.scrollAnchor / chat.scrollAnchor.get), keyed by sessionId since cmux-next: chats reopen where you left them after a relaunch (cx-6bgzj) #19445 (cx-6bgzj); moved to the state files in cmux NEXT: agent pane state files save off the main thread #27749 and fix(quit): state files have one owner each and no quit blocks the main thread on them #28275.

What changes

  • The host names the pane's tab: AgentPaneModel.tabKey (the store's tab id, resolved at each call). AgentUnsentDrafts.wire already gets the tab key for every tab's model, a spare's too when a tab adopts it, so it sets it there. No App-target lines added.
  • Scroll places: both hosts (AgentPaneBridge and AgentPageProvider) pass the pane's tab to the store. Entries now carry tab, and an at-latest entry is kept for its tab, so that tab's reload doesn't fall back to the other tab's place. Lookup order: the tab's own entry, then the chat's most recent entry from any tab. That includes old tabless entries, so existing places still restore, and a tab opened on the chat later still opens where the chat last rested. The page never names its own tab.
  • Session Info: the handshake carries tab. A spare that a tab adopts after its handshake gets acpmux-pane-tab. sessionInfoState.ts keeps a tab's state under tab:<tab>:<paneKey>, and each save also writes the chat's key. So the chat key holds the latest state in any tab, which is the fallback for a tab with no state of its own (including chats saved before this change). Both keys go in the index and are pruned the same way. Split keys (split:) work as before. With no tab (an older host, the gallery, tests), nothing changes.
  • Ghost panel: see below.

Ghost panel (confirmed in code, fixed here)

Leo saw a faint copy of Session Info ("Background tasks", "Connectors", "Agent memory"...) mixed into the chat transcript. One path does this. AgentPaneSessionInfoPanel.updateBacking() gave the native panel its overlay glass only when the page sent foot. But a short pane's popover (pane under POPOVER_BELOW_HEIGHT, stage overlay) lies over the transcript and sends no foot: foot={stopsAboveComposer ? dockHeight : undefined} with stopsAboveComposer = overChat && !popover. So that panel's text sat over the transcript text with no fill (the panel itself has backgroundColor = nil). Now the page sends overChat whenever the panel lies over the chat, and the panel takes its glass on overChat || foot. Pages that predate overChat behave as before.

I found no path where the page says closed while the native panel stays shown: every pane.sessionInfo update, reload (pageLoaded()) and unmount clears it. One gap I did not verify: a reload that never reaches ready (a load failure) would leave the old panel up. Session keying (item 1) could make a reloaded tab show the other tab's open state, but each pane's native panel still follows its own page.

Tests

  • webviews/test/session-info-per-tab-state.test.ts (new): two tabs of one chat keep their own open, width and folds across a reload, including the first frame before the bind. A tab with no entry of its own reads the chat's old session-keyed entry and then keeps its own. Both entries are in the index. A spare that a tab adopts after binding re-reads under its tab and notifies listeners.
  • webviews/test/native-session-info-page.test.tsx: a panel over the chat posts overChat: true even with no foot.
  • ChatScrollRestoreTests: two tabs of one chat keep their own place across a reload, and a tab at its latest message stays there instead of taking the other tab's place. An old tabless file entry is the fallback for a tab with none and is then replaced by the tab's own.
  • AgentPaneNativeSessionInfoTests.aPanelOverTheChatGetsItsGlassEvenWithoutAFoot.

Red on base (the first commit, tests only): cd webviews && bun test test/session-info-per-tab-state.test.ts test/native-session-info-page.test.tsx gives 2 fail and 1 error. Green at the head: 31 pass across those files plus session-info-split-starts-closed and session-info-per-pane-state. The Swift tests reference API that doesn't exist on base (tab: and liesOverChat), so they fail to build there.

Observed / Not run

  • Observed: the focused Bun tests above (red on base, green on head), vp fmt and vp lint on the changed web files, the cmux-next prepush gate, and the app compile check at the head.
  • Not run: the Swift suites themselves (CmuxNextAgentPaneTests). I have not run two real tabs of one chat in a DEV build.

Downgrade note: an older app that reads a places file containing an at-latest tab entry (no anchor) fails to decode it and starts with no kept places. It loses scroll places only, nothing else.

Changelog

  • Fixed: one chat open in two tabs keeps each tab's Session Info and its transcript's place across a reload, and a short pane's Session Info popover no longer shows the chat through it.

Agent session: agent session

🤖 Generated with Claude Code


Autofix with [code]smith
Open in [code]smith · Need help on this PR? Tag @codesmith-bot with what you need.


Summary by cubic

Keeping two tabs of one chat safe now: each tab stores its own Session Info (open state, width, folds) and transcript scroll position, so a reload in one tab no longer picks up the other tab's state. A Session Info popover over the transcript also gets its proper glass background, fixing the chat showing through it.

  • Scroll places and Session Info are keyed by tab, with the chat's latest state as fallback for tabs with no entry of their own (including older saved data).
  • The host passes the pane's tab ID through the handshake and scroll anchor calls; pages adopted after handshake get it via a new event.
  • The native Session Info panel receives overChat from the page so a short-pane popover (which sends no foot) gets its glass.

Written for commit 6e56746. Summary will update on new commits.

View guided diff Turn on auto-fix

teamleaderleo and others added 2 commits November 4, 2026 01:01
… place; a popover over the chat gets its glass

Red on base: the per-tab store API, the tab-aware place and the
panel's overChat do not exist, and the popover model carries no
overChat (bun: 2 fail, 1 error).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-ed9226b71907d8aaa0b6
…t; a popover over the chat gets its glass

One chat open as a tab in two workspaces: each tab's reload now shows its
own Session Info (open, width, folds) and reopens where that tab's
transcript rested, instead of the other tab's. A tab with no state of its
own reads the chat's old entry. The host names the tab (handshake tab,
acpmux-pane-tab for an adopted spare; the pane's tab for chat.scrollAnchor).

The native Session Info panel as a short pane's popover lay over the
transcript with no glass (only foot turned it on); the page now sends
overChat and the panel takes its glass whenever it lies over the chat.

Focused bun: test/session-info-per-tab-state.test.ts and
test/native-session-info-page.test.tsx red on base, green here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-ed9226b71907d8aaa0b6
@github-actions

github-actions Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

cmux status (one comment, edited in place; each line is updated by the job that owns it)

Review 6e56746: HQ feed card · gallery before/after runs · checks

App of this head (app.zip, kept 1 day)

gh workflow run reload-build.yml -R manaflow-ai/cmux --ref feat-cmux-next -f tag=pr34492 -f ref=lane/session-state-per-tab -f platform=macos -f backend_mode=local

Not verified: macOS jobs did not run: compile and app tests were skipped on 6e56746fdf; ci-status is green without them.

CI passes on 6e56746fdf (run 41752283762 attempt 1).
macOS jobs did not run: compile and app tests were skipped: Fast static checks skipped (the macos job needs both to succeed). Re-run CI once they pass, before merging.

Written by scripts/ci/classify_failures.py (ci-failure-attribution.yml); signatures are its SIGNATURES table. A machine verdict is the runner's fault, not this PR's; yours means the failing file is one this PR changes, also red on main that main's latest full suite fails the same way, seen on other PRs that it failed on another pull request's run lately.

Shipped in:

@github-actions github-actions Bot added the risk:high Risk tier (docs/ci/risk-tiers.md) label Nov 4, 2026
@teamleaderleo teamleaderleo added leo-named-fix An obvious fix to something Leo named: lands on push, review runs in parallel risk:normal Risk tier (docs/ci/risk-tiers.md) labels Nov 4, 2026
@github-actions github-actions Bot removed the risk:normal Risk tier (docs/ci/risk-tiers.md) label Nov 4, 2026
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (subagent claude-opus-5-5 medium): SHIP at 6e56746

Visual verdict: no images. The only visual change is the Session Info popover's glass backing, a native panel; covered by code and tests.

Checked: scroll-anchors Codable compatibility with old files, fallback order (own tab, then the chat's most recent, at-latest stays nil), the double write and index pruning in sessionInfoState.ts, the path the tab key takes to the page (handshake, acpmux-pane-tab via the AgentUnsentDrafts.wire didSet), both scroll hosts, isolation, and overChat/liesOverChat.

Follow-ups:

  • [follow-up] The handshake reads tabKey() before its awaits; a re-wire in that window (provisional id to store id) is skipped by announceTab's hasHandshake guard. Read the tab last, or announce again after hasHandshake.
  • [follow-up] A recycled spare keeps the closed tab's tabKey until wire runs after adoptSession. Clear it in recycle, or wire first.
  • [follow-up] PANES_KEPT (200) and the scroll-anchor limit (500) now also count tab entries.
  • [follow-up] Downgrade: an older build can't decode an entry with no anchor and drops all scroll places.
  • [follow-up] A page that draws before its handshake may show the chat's state for one frame, then the tab's.
  • [follow-up] No tests yet for announceTab, the handshake tab field, or the acpmux-pane-tab listener.

Checks at the head: app compile check passed. Focused Bun tests are green. Fleet bun run check: fmt, lint and type check are clean; its one red is check:design-tokens on subagents/subagents.css, a file this PR doesn't touch (base red).

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (codex, router-review): SHIP at 6e56746

Reviewer: codex gpt-6-astra high, fast tier.

Follow-ups: 1. Each lands as an issue in this repo that links this receipt; the lane that opened the PR fixes it forward.

SHIP at 6e56746

Visual verdict: no new visual regression in the 16 supplied before/after pairs. Native popover glass is not pictured.

  1. [P2] [follow-up] sessionInfoState.ts:331: Persist inherited state under the tab’s own key when binding. When tab B inherits the chat’s saved state, bind() returns without saving a tab-specific entry. If tab A subsequently changes the panel, reloading untouched tab B adopts A’s changes. Reproduced using the transpiled store: B changed from closed/480px to open/320px after reload. Save B’s initial inherited state and add a test where B makes no explicit changes.

Bun and Swift suites could not run because those runtimes are unavailable.

Follow-ups:

@teamleaderleo teamleaderleo removed the risk:high Risk tier (docs/ci/risk-tiers.md) label Nov 4, 2026
@teamleaderleo
teamleaderleo merged commit dad9790 into feat-cmux-next Nov 4, 2026
273 of 294 checks passed
@teamleaderleo
teamleaderleo deleted the lane/session-state-per-tab branch November 4, 2026 01:39
@github-actions github-actions Bot added the risk:high Risk tier (docs/ci/risk-tiers.md) label Nov 4, 2026

This branch was previously deployed

1 inactive deployment
ci-routing-agent — 6e56746f Deployed Nov 4, 2026 by teamleaderleo via namespace autofix #13740
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

leo-named-fix An obvious fix to something Leo named: lands on push, review runs in parallel risk:high Risk tier (docs/ci/risk-tiers.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant