Skip to content

cmux NEXT: a chat shown again from another tab no longer jumps - #23743

Merged
teamleaderleo merged 3 commits into
feat-cmux-nextfrom
glass-regressions/tab-switch-shift
Oct 21, 2026
Merged

teamleaderleo merged 3 commits into
feat-cmux-nextfrom
glass-regressions/tab-switch-shift

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

Leo: "THIS WHOLE THING CAUSES A LAYOUT SHIFT WHEN I SIMPLY SWITCH TO IT FROM ANOTHER TAB."

This PR fixes the native resize behind the shift. A check of the webview's scrollTop and first visible message on show is a follow-up (see below).

Cause

A chat's header tools share the pane's tab row (chrome.tabs.rowTools, added in #20933 and turned on by default in #22655). So while a chat shows, PaneContentView.layout() makes the content host the whole pane. While a terminal or browser shows, the host is only the area under the strip. A parked chat kept [.width, .height] autoresizing (retire: "stays sized with the pane"). As a result:

  • switching away shrank the hidden chat's page by the strip's height;
  • switching back, install sized it to the still-short host, and the next layout grew it to the full pane.

The page laid out at a height it never shows at, and the transcript re-placed itself as the chat came back (the follow/anchor pass in VirtualTranscript runs on every height change). AgentPageRedraw.awaits saw the size come back to the drawn size and ended the paint hold without waiting for a frame, so the move was on screen.

On the red commit, the test records exactly this: the chat's sizes go [(900, 472), (900, 500)], shrinking by the 28 pt strip and then growing back.

Fix

  • PaneContentView.parkedFrame(of:) gives the frame a parked row-sharing page (a chat) shows at: the whole pane when its tools fit the row, otherwise the area under the strip.
  • layoutParked(), called from layout(), keeps hidden chats at that frame. A window resize still reaches them; a tab switch no longer does.
  • retire drops autoresizing for those pages. install leaves a chat that is already at its frame alone, and layout() restores autoresizing once it is the shown content.
  • AgentPageRedraw.awaits compares the page's own frame rather than the host's, so a switch back to a chat already at its size starts no paint hold.

Test

ParkedChatKeepsSizeTests (CmuxNextAppTests) shows a chat, then a terminal-like view, then the chat again. The chat is a real InternalPageView (the branch retire checks) wrapping a row-sharing view, and every frame change is recorded through frameDidChange. The test asserts:

  • the chat never changes size while parked or on the way back;

  • for 500 ms after the switch (16 ms run-loop ticks, a layout pass each tick), its window rect and size never change;

  • a parked chat still follows a pane resize.

  • Red: test commit f112bb3, fleet step 9af3d9782b0e85860818e4cb, failed as expected (above).

  • Green: head d3ac814, fleet step 36d1d4f821a14a6b174bbe9d. Passed: ParkedChatKeepsSizeTests, TabRowToolsStayClickableTests, TabSwitchNoBlankFrameTests, InternalPageParkingTests, PaneParkScalingTests, WorkspaceSwitchRedrawHoldTests.

  • Mutant: retire's parkedFrame clause reverted (c20b228, scratch only), fleet step 660d5b8f7fb3a8bcc8d1bd9d. Fails, sizes [(900, 472), (900, 500)], so the test covers that branch.

  • App compile: cmux-ci compile-check --app job c3201f15ae593ebdd8f81f80 at d3ac814, passed (62 s).

Observed / Not run

  • Observed: the fleet runs above.
  • Not run: a real app tab switch with a live transcript.
  • Follow-up: a webview test that the transcript's scrollTop and first visible message stay put across hide/show with no resize. The webview already has guards for this case (shared/pageReveal.ts stops entrances and glides while hidden or on the first frame back; VirtualTranscript ignores zero-height measurements). This PR removes the resize that drove the transcript's re-placement.

No capture: this removes a resize that happened only during a tab switch; at rest nothing looks different.

Changelog

  • Fixed: switching to a chat tab from a terminal or browser tab no longer shifts the chat's layout.

Agent session: cosmic-gray

🤖 Generated with Claude Code

teamleaderleo and others added 2 commits October 21, 2026 14:08
A chat shares the pane's tab row and fills the pane; a terminal sits under
the strip. The parked chat followed the content host, so switching away
shrank its page by the strip's height and switching back grew it again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16027-a80b-79c0-ab31-500e58883d27&host=cosmic-gray
The content host is the whole pane while a chat shows (its tools share the
tab row) and the area under the strip while a terminal or browser shows. A
parked chat was autoresized with the host, so every switch away shrank its
page by the strip's height and the switch back grew it again: the page laid
out at a size it never shows at and the transcript moved as it came back.
Parked row-sharing pages now keep the frame they show at (layoutParked), and
install keeps that frame so the next layout only moves the host.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16027-a80b-79c0-ab31-500e58883d27&host=cosmic-gray
@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

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

Review d3ac814: 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=pr23743 -f ref=glass-regressions/tab-switch-shift -f platform=macos -f backend_mode=local

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

CI passes on d3ac81470e (run 39684593117 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.

Tour of d3ac81470e (run)

Path-selected interactions

Interaction Changed paths
agent-pane Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPageRedraw.swift
core Packages/macOS/CmuxNext/Sources/CmuxNextApp/PaneContentView\+Parking.swift
Packages/macOS/CmuxNext/Sources/CmuxNextApp/PaneContentView.swift
Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/ParkedChatKeepsSizeTests.swift

Agent pane (before): PASS — 3 ok

tour

Video (mp4)

# Step Result Command
1 Launch ok
2 Open agent chat ok cmux agent new-chat
3 Queue a second chat ok cmux agent new-chat
Screenshots (3)

1. Launch
Launch

2. Open agent chat
Open agent chat

3. Queue a second chat
Queue a second chat

### Agent pane \(after\): PASS — 3 ok

tour

Video (mp4)

# Step Result Command
1 Launch ok
2 Open agent chat ok cmux agent new-chat
3 Queue a second chat ok cmux agent new-chat
Screenshots (3)

1. Launch
Launch

2. Open agent chat
Open agent chat

3. Queue a second chat
Queue a second chat

### Core UI \(before\): DEGRADED — 19 ok, 1 unavailable

tour

Video (mp4)

# Step Result Command
1 Launch ok
2 New workspace ok cmux workspace new --name Tour
3 Type in the terminal ok cmux workspace name:Tour screen current pane current tab current terminal current write --text "echo hello from the cmux-next tour\r"
4 A command reports progress: tab loading spinner ok cmux workspace name:Tour screen current pane current tab current terminal current write --text "clear; printf '\\033]9;4;3\\007'; sleep 900\r"
5 Split right ok cmux action run splitRight
6 Split down ok cmux action run splitDown
7 New terminal tab ok cmux action run newSurface
8 Browser tab ok cmux tab new-browser --url https://example.com
9 Hide the sidebar ok cmux action run toggleSidebar
10 Show the sidebar ok cmux action run toggleSidebar
11 Notification ok cmux notify --title Tour --body "Hello from CI"
12 Notifications panel ok cmux action run showNotifications
13 Agent chat ok cmux agent new-chat
14 Agent workspace ok cmux workspace new --name Agent
15 Agent starts ok cmux workspace name:Agent screen current pane current tab current terminal current write --text "clear; printf '\\033]7501;state=working\\033\\\\'; sleep 8; printf '\\033]7501;state=done\\033\\\\'; sleep 900\r"
16 An agent works: row working spinner ok cmux workspace new --name Notes
17 The agent finishes: done mark in the spinner's place ok
18 Session tree: keycaps on the first rows ok cmux settings set sidebar.sessionTree true
19 Session tree back off for later tours ok cmux settings set sidebar.sessionTree false
20 Profile menu unavailable: exited 1 (the menu shot needs a screen capture route) cmux action run sidebar.profileMenu
Screenshots (15)

1. Launch
Launch

2. New workspace
New workspace

3. Type in the terminal
Type in the terminal

4. A command reports progress: tab loading spinner
A command reports progress: tab loading spinner

5. Split right
Split right

6. Split down
Split down

7. New terminal tab
New terminal tab

8. Browser tab
Browser tab

9. Hide the sidebar
Hide the sidebar

11. Notification
Notification

12. Notifications panel
Notifications panel

13. Agent chat
Agent chat

16. An agent works: row working spinner
An agent works: row working spinner

17. The agent finishes: done mark in the spinner's place
The agent finishes: done mark in the spinner's place

18. Session tree: keycaps on the first rows
Session tree: keycaps on the first rows

### Core UI \(after\): DEGRADED — 19 ok, 1 unavailable

tour

Video (mp4)

# Step Result Command
1 Launch ok
2 New workspace ok cmux workspace new --name Tour
3 Type in the terminal ok cmux workspace name:Tour screen current pane current tab current terminal current write --text "echo hello from the cmux-next tour\r"
4 A command reports progress: tab loading spinner ok cmux workspace name:Tour screen current pane current tab current terminal current write --text "clear; printf '\\033]9;4;3\\007'; sleep 900\r"
5 Split right ok cmux action run splitRight
6 Split down ok cmux action run splitDown
7 New terminal tab ok cmux action run newSurface
8 Browser tab ok cmux tab new-browser --url https://example.com
9 Hide the sidebar ok cmux action run toggleSidebar
10 Show the sidebar ok cmux action run toggleSidebar
11 Notification ok cmux notify --title Tour --body "Hello from CI"
12 Notifications panel ok cmux action run showNotifications
13 Agent chat ok cmux agent new-chat
14 Agent workspace ok cmux workspace new --name Agent
15 Agent starts ok cmux workspace name:Agent screen current pane current tab current terminal current write --text "clear; printf '\\033]7501;state=working\\033\\\\'; sleep 8; printf '\\033]7501;state=done\\033\\\\'; sleep 900\r"
16 An agent works: row working spinner ok cmux workspace new --name Notes
17 The agent finishes: done mark in the spinner's place ok
18 Session tree: keycaps on the first rows ok cmux settings set sidebar.sessionTree true
19 Session tree back off for later tours ok cmux settings set sidebar.sessionTree false
20 Profile menu unavailable: exited 1 (the menu shot needs a screen capture route) cmux action run sidebar.profileMenu
Screenshots (15)

1. Launch
Launch

2. New workspace
New workspace

3. Type in the terminal
Type in the terminal

4. A command reports progress: tab loading spinner
A command reports progress: tab loading spinner

5. Split right
Split right

6. Split down
Split down

7. New terminal tab
New terminal tab

8. Browser tab
Browser tab

9. Hide the sidebar
Hide the sidebar

11. Notification
Notification

12. Notifications panel
Notifications panel

13. Agent chat
Agent chat

16. An agent works: row working spinner
An agent works: row working spinner

17. The agent finishes: done mark in the spinner's place
The agent finishes: done mark in the spinner's place

18. Session tree: keycaps on the first rows
Session tree: keycaps on the first rows

Tours are data in scripts/cmux-next/media/tours: add a step or a tour file to show your feature.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Owned by glass-regressions on cosmic-gray · session

@teamleaderleo teamleaderleo added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Oct 21, 2026
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Bead cx-s9bci created for this PR by the cmux coordinator.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (subagent, correctness first): CHANGES at 1767fa9

The native fix reads correct to me. Parked row-sharing pages stay at the frame they show at (parkedFrame / layoutParked), and install keeps that frame, so the host growing under the chat no longer resizes it. The bottom bar, a hidden strip, tools that don't fit, an active browser band and a resize while parked all work out the same as layout(), and when the frames don't match, install goes back to the old path. The new test does fail without the fix: on the old code, showing the chat again sizes it to the short host first.

What is missing before this ships:

  1. The test doesn't check what was asked. It runs synchronous layout passes and compares sizes. It never watches the chat for a while after the switch (the ask was nothing moving for 500 ms), and it never checks scrollTop or the first visible message. One fix: after the final show(chat), run RunLoop.main.run(until: .now + 0.5) and assert that sizes is still empty and the window rect is unchanged. For scroll, add an app-host or webview test that checks scrollTop and the first visible message, or scope this PR to the native resize and say the scroll check is a follow-up. The body still says "In progress" and "Webview side still being checked for show-time re-measure", so nothing shows yet that the transcript stays still.
  2. The test doesn't cover the retire change. RowChat is not an AgentPaneView or InternalPageView, so retire cleared its autoresizing mask even before this PR. In the old code, for the test fake, the hidden chat never shrank. That means reverting the || parkedFrame(of: view) != nil part of retire, which is the change that stops a real chat shrinking while hidden, would still pass both tests. It needs a test that goes through that branch, or a note on why it can't.
  3. Nit, not blocking: AgentPageRedraw.awaits still compares host.bounds.size with drawnSize. A chat parked at full size behind a terminal (short host) now always reports that it is waiting, so every switch back starts a paint hold. The hold ends on the next layout (the size matches drawnSize), so nothing shows on screen, but it no longer matches the "stays sized with the pane" comment. Comparing the page's own frame would avoid the hold.

CI at this head: the Swift code hasn't compiled yet. cmux-next swift test and Release compile (Xcode 26) are queued, and fast tier and fleet/test-compile are still running, so ParkedChatKeepsSizeTests has not run.

…gh retire's branch

The test's chat is now an InternalPageView (the kind retire keeps sized with
the pane, as an AgentPaneView), so reverting retire's parkedFrame check fails
it, and it watches the chat for 500 ms on the run loop after the switch.
AgentPageRedraw.awaits compares the page's own frame, which the pane keeps at
the size it shows at, so a switch back to a chat at that size holds nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16027-a80b-79c0-ab31-500e58883d27&host=cosmic-gray
@blacksmith-sh

blacksmith-sh Bot commented Oct 21, 2026

Copy link
Copy Markdown

Found 1 test failure on Blacksmith runners:

Failure

Test View Logs
test_the_repository_highlights_validate (main.DigestTests.test_the_repository_highl
ights_validate)/
test_the_repository_highlights_validate (main.DigestTests.test_the_repository_highl
ights_validate)
View Logs

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

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (subagent, correctness first): SHIP at d3ac814

The fix is correct. A hidden page now stays at the frame it will show at (parkedFrame / layoutParked). install keeps that frame, and the next layout() only moves the chat's origin as the host grows under the tab row, so its size and its place in the window stay the same. parkedFrame matches what layout() gives in every case I checked: bottom bar, hidden strip, tools that don't fit, an active browser band (released before install), the strip hiding in the same switch (frames differ, so it falls back to the old path, and awaits still holds because of needsLayout), and a pane resize while parked. A page that is never offered the row stays under the strip, the same as before.

The earlier CHANGES at 1767fa9:

  1. Fixed. watch runs 500 ms of 16 ms ticks after the switch and checks the window rect and recorded sizes. In step 36d1d4f8, each test ran for about 0.5 s, so the watch did run. The body now scopes the PR to the native resize and lists scrollTop and the first visible message as a follow-up.
  2. Fixed. The chat is now a real InternalPageView, so retire takes the AgentPaneView || InternalPageView branch. With the || parkedFrame(of:) != nil part reverted, the parked chat autoresizes to (900, 472), then layoutParked puts it back to (900, 500), and both changes are recorded through frameDidChange, so the test fails. On base it fails at the switch away.
  3. Fixed. awaits compares pane.frame.size with drawnSize, which is the page's own bounds.size from laidOut. A chat parked at full size behind a terminal starts no hold, and a resize while parked still does.

Regressions and races: I found none. Hidden pages still move no view on a switch (PaneParkScalingTests and InternalPageParkingTests pass). Parked non-chat internal pages are now kept under the strip while a chat shows, where before they grew with the host, so the same class of bug is fixed for them too. Between install and the next layout(), the chat sits at y = -strip in the short host. That is the same window rect, and layout() runs before display. If the tab switches away first, retire clears the mask again.

Verified on the fleet:

  • 36d1d4f821a14a6b174bbe9d (aws-m4pro-2) at d3ac814: all 6 suites pass.
  • 9af3d9782b0e85860818e4cb at f112bb3: red with [(900,472),(900,500)].
  • cmux-ci compile-check --app c3201f15ae593ebdd8f81f80 at d3ac814: ok: true, done.

Not blocking:

  • The red run is for the earlier RowChat version of the test. The red for the current InternalPageView test is reasoned above, not run.
  • parkedFrame(of:) is non-nil exactly for AgentPaneView and InternalPageView, so retire's condition is now always true. It could be a plain view.autoresizingMask = [], and the "stays sized with the pane" comment above it is stale.
  • No test covers the awaits change directly.
  • The test's fake has nothing async, so the 500 ms watch guards the native layout, not WebKit. That matches the follow-up the body lists.

@teamleaderleo
teamleaderleo merged commit bc9e7e7 into feat-cmux-next Oct 21, 2026
@teamleaderleo
teamleaderleo deleted the glass-regressions/tab-switch-shift branch October 21, 2026 23:53
azooz2003-bit added a commit that referenced this pull request Oct 22, 2026
…integration

PaneContentView keeps this branch's layout of resized content and the
base's parked-chat autoresizing and layoutParked (#23743); the settings
source stamp was recomputed with defaults_guard.py source-stamp.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

base-red-watch: cmux-next / cmux-next fast tier went red on feat-cmux-next in https://github.com/manaflow-ai/cmux/actions/runs/39696410579 (first red at 4eca5c3, last green at ee6f052). Suspect: #23743 (commit bc9e7e7). If it is yours, fix forward on feat-cmux-next.

@github-actions

Copy link
Copy Markdown
Contributor

Shipped in:

This branch was previously deployed

1 inactive deployment
ci-routing-agent — d3ac8147 Deployed Oct 21, 2026 by teamleaderleo
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:normal Risk tier (docs/ci/risk-tiers.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant