Skip to content

fix: keep split zoom when the zoomed pane outlives a tab close - #12853

Merged
teamleaderleo merged 3 commits into
manaflow-ai:mainfrom
kugesh-Rajasekaran:issue-8363-persist-pane-zoom-tab-close
Sep 25, 2026
Merged

teamleaderleo merged 3 commits into
manaflow-ai:mainfrom
kugesh-Rajasekaran:issue-8363-persist-pane-zoom-tab-close

Conversation

@kugesh-Rajasekaran

@kugesh-Rajasekaran kugesh-Rajasekaran commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Summary

What changed? Workspace.recordPostCloseState() now marks the split zoom for clearing only when the tab being closed is the last tab in the zoomed pane.

Why? Closing a tab inside a zoomed pane dropped the zoom and snapped the window back to the split layout, even though the pane was still there with another tab selected.

The close path is splitTabBar(_:shouldCloseTab:inPane:) → recordPostCloseState() → postCloseClearSplitZoomTabIds → splitTabBar(_:didCloseTab:fromPane:) → clearSplitZoom(). The mark was recorded from two conditions only:

if controller.zoomedPaneId == pane,
   controller.selectedTab(inPane: pane)?.id == tab.id {
    postCloseClearSplitZoomTabIds.insert(tab.id)
}

"the closed tab is the selected tab of the zoomed pane" is true for every close in that pane, not just the one that removes the pane. recordPostCloseState() runs before Bonsplit removes the tab, so the pane's tab count is still authoritative there — adding tabs.count <= 1 narrows the mark to the case the clear was meant for.

Repro before this change:

┌──────────┬──────────┐      double-click      ┌─────────────────────┐
│  pane A  │  pane B  │  ───────────────────▶  │       pane A        │   zoomed
│  [tab 1] │  [tab x] │       (zoom A)         │       [tab 1]       │
└──────────┴──────────┘                        └─────────────────────┘
                                                          │ open tab 2 in pane A
                                                          ▼
                       ┌──────────┬──────────┐   close    ┌─────────────────────┐
                  was: │  pane A  │  pane B  │  ◀───────  │       pane A        │
                       │  [tab 1] │  [tab x] │   tab 2    │    [tab 1][tab 2]   │
                       └──────────┴──────────┘            └─────────────────────┘
                        ▲ zoom lost, pane B reappears

                       ┌─────────────────────┐
                  now: │       pane A        │  zoom kept, pane A still fills the window
                       │       [tab 1]       │
                       └─────────────────────┘

Closing the last tab of the zoomed pane still ends the zoom, so the surviving pane is never left hidden behind a zoom pointing at a pane that no longer exists.

Scope note: this is the closing half of #8363. The tab-bar + button already preserves the zoom (it routes through didRequestNewTab, which never clears it), but Cmd+T still clears it via TabManager.newSurface(). That opening half touches a different, multi-entrypoint path (~20 call sites deliberately clear the zoom), so per the shared-behavior policy it belongs in its own change rather than being bolted onto this one. Leaving the issue open for it.

Testing

Two commits, per the regression-test policy in AGENTS.md / skills/cmux-testing:

  1. test: keep split zoom when a zoomed pane still has tabs — the test alone, expected red.
  2. fix: keep split zoom when the zoomed pane outlives a tab close — expected green.

New file cmuxTests/WorkspaceSplitZoomTabCloseTests.swift (Swift Testing, @MainActor, .serialized), wired into cmux.xcodeproj/project.pbxproj with all four entries:

  • closingSelectedTabKeepsZoomWhilePaneStillHasTabs — split into two panes, add a second tab to pane A, zoom pane A, close the selected tab via workspace.closePanel(_:force:) (the real BonsplitController.closeTab path, not a hand-called delegate), assert the pane still has one tab and zoomedPaneId is unchanged. This is the assertion that fails without the fix.
  • closingLastTabInZoomedPaneEndsZoom — guards the other direction: closing the pane's only tab still clears zoomedPaneId, and the sibling panel survives.

Both drive observable runtime behavior through the executable close path; no source-text or project-metadata assertions.

Verified locally:

$ ./scripts/lint-pbxproj-test-wiring.sh
lint-pbxproj-test-wiring: ok (checked 961 test files)

$ python3 scripts/normalize-pbxproj.py
normalized: cmux.xcodeproj/project.pbxproj     # idempotent, diff stays at +4 lines

$ swiftc -parse cmuxTests/WorkspaceSplitZoomTabCloseTests.swift   # exit 0
$ swiftc -parse Sources/Workspace.swift                           # exit 0

What I could not run locally, and why. This machine is macOS 15.7.5 / Xcode 16.2 (Swift 6.0.3), not the pinned Xcode 26. scripts/setup.sh + the prebuilt GhosttyKit.xcframework fetch both succeed, but ./scripts/reload.sh --tag issue-8363 fails on current main before it reaches any of my code, on Swift 6.2-only semantics in packages the app links:

Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SSHPTY/SSHPTYAttachSignalMonitor.swift:11:27:
  error: use of protocol 'DispatchSourceSignal' as a type must be written 'any DispatchSourceSignal'

Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/CmxRetryAfterPolicy.swift:105:15:
  error: converting non-sendable function value to '@Sendable (TimeInterval) async throws -> Void'
  may introduce data races

(Both are pre-existing on main and untouched by this PR — flagging them only to explain the gap, and because AGENTS.md still describes Xcode 16.2 as a best-effort pathway for the macOS app.)

So I have no locally built app and therefore no demo video. The red→green transition across the two commits on CI is the behavioral proof here, and I'll attach the CI evidence as a comment once the checks report. Happy to add a recorded demo if a maintainer can point me at a build lane I can use.

Demo Video

Not attached — see the note above: the pinned Xcode 26 toolchain is unavailable on my machine, so I could not produce a running build to record. The two-commit CI red/green covers the behavior instead.

Review Trigger (Copy/Paste as PR comment)

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

Checklist

  • I tested the change locally — to the extent the toolchain allows (lints, parse, package tests); app-target tests run on CI, see Testing
  • I added or updated tests for behavior changes
  • I updated docs/changelog if needed — no user-facing strings, settings, menus or shortcuts changed, so no localization or changelog entry applies
  • I requested bot reviews after my latest commit
  • All code review bot comments are resolved
  • All human review comments are resolved

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


Summary by cubic

Fixes closing a tab in a zoomed pane dropping the zoom, so the window no longer snaps back to the split layout while the pane still has other tabs. recordPostCloseState() now clears the zoom only when the closed tab is the pane's last one; closing that last tab still ends the zoom as before.

Covers the closing half of #8363. The tab-bar + button already preserved zoom, but Cmd+T still clears it via TabManager.newSurface(); that opening half stays open in issue #8363.

Testing

  • Adds WorkspaceSplitZoomTabCloseTests covering both directions: zoom survives a close that leaves tabs, and ends when the pane's last tab closes.

Written for commit 49fd3ed. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes

    • Split-pane zoom now remains active when closing the selected tab if other tabs remain in that pane.
    • Split-pane zoom is cleared when closing the pane’s final tab.
  • Tests

    • Added coverage for split-pane zoom behavior when closing selected and last remaining tabs.

Closing the selected tab of a zoomed pane currently clears the split zoom
even when the pane survives the close, so the window snaps back to the
split layout. Cover both halves of the behavior: the zoom must survive a
close that leaves tabs behind, and must still end when the close takes
the zoomed pane with it.

Fails without the fix in the following commit.

Issue: manaflow-ai#8363
@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@github-actions

github-actions Bot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@coderabbitai

coderabbitai Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

Review Change StackReview Change Stack

Warning

Review limit reached

Next included review available in 2 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 10 included reviews currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Repository: manaflow-ai/cmux/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 68268499-54b0-4557-afa8-0fe5a275be04

📥 Commits

Reviewing files that changed from the base of the PR and between 65752ee and 49fd3ed.

📒 Files selected for processing (3)
  • Sources/Workspace.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/WorkspaceSplitZoomTabCloseTests.swift

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 8191af45-f1bb-4fee-bc91-3441ca8b12db

📥 Commits

Reviewing files that changed from the base of the PR and between 8c43c63 and b5a314a.

📒 Files selected for processing (3)
  • Sources/Workspace.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/WorkspaceSplitZoomTabCloseTests.swift

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.


📝 Walkthrough

Walkthrough

Workspace now preserves split zoom when closing a selected tab leaves other tabs in the pane. New tests cover this case and clearing zoom when the closed tab was the pane’s last tab.

Changes

Split zoom tab-close behavior

Layer / File(s) Summary
Preserve zoom for surviving tabs
Sources/Workspace.swift
recordPostCloseState now checks the pane tab count. It clears split zoom only when the closed selected tab leaves one or fewer tabs.
Validate tab-close outcomes
cmuxTests/WorkspaceSplitZoomTabCloseTests.swift, cmux.xcodeproj/project.pbxproj
Tests cover preserving zoom with a surviving tab and ending zoom when the pane loses its last tab. The test file is added to the Xcode test target.

Priority: ⬇️ Low

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix

Merge Risk: ⚪ Minimal · up to b5a31

Split zoom now remains active when tabs survive and clears when the zoomed pane is removed. The covered change is ready to merge.

🚥 Pre-merge checks | ✅ 25
✅ Passed checks (25 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 2 functions across 1 files. (2 skipped: 1 …
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed PASS. The production diff only moves the existing controller.tabs(inPane:) read and adds tabs.count <= 1 to an existing condition in Workspace.splitTabBar(_:shouldCloseTab:inPane:). It adds no `…
Cmux Swift Blocking Runtime ✅ Passed PASS. The authoritative diff changes production Swift only by reading controller.tabs(inPane:) and adding a tab-count condition in recordPostCloseState(). It introduces no semaphore, blocking wait…
Cmux Browser Automation Off-Main ✅ Passed PASS. The PR changes only split-zoom close handling, its regression tests, and Xcode project wiring. The policy-scoped browser automation files are unchanged: Sources/TerminalController.swift, `Cont…
Cmux Expensive Synchronous Load ✅ Passed PASS: The production diff only reads the in-memory Bonsplit tab list, checks the selected tab, and adds a tab-count condition in Workspace.splitTabBar(_:shouldCloseTab:inPane:). It adds no `Restorab…
Cmux Cache Substitution Correctness ✅ Passed PASS. The production diff does not replace a fresh authoritative read with a cache. It moves the existing controller.tabs(inPane: pane) read before the zoom decision and adds tabs.count <= 1; the …
Cmux No Hacky Sleeps ✅ Passed PASS. The pull request changes only Swift source, Swift tests, and Xcode project metadata. The production change adds a tab-count condition to split-zoom close handling; it introduces no sleep, timer,…
Cmux Algorithmic Complexity ✅ Passed PASS. The only production change in Sources/Workspace.swift moves the existing controller.tabs(inPane:) retrieval before the zoom condition and adds tabs.count <= 1. The code still performs one …
Cmux Swift Concurrency ✅ Passed PASS. The authoritative diff changes only synchronous tab-count/state logic in Workspace.swift and adds synchronous @MainActor tests. It introduces no DispatchQueue background work, `DispatchGro…
Cmux Swift @Concurrent ✅ Passed PASS: The PR changes only synchronous tab-close state handling in Workspace.splitTabBar(_:shouldCloseTab:inPane:) by reading tabs and narrowing the zoom-clear condition. The function signature and…
Cmux Swift Package Boundaries ✅ Passed PASS. The only production change is a small condition in Sources/Workspace.swift inside Workspace's BonsplitDelegate close callback. It reads live BonsplitController pane state and updates the…
Cmux Swiftpm Lockfiles ✅ Passed PASS. The PR changes only Sources/Workspace.swift, the test source, and four test-file entries in cmux.xcodeproj/project.pbxproj. The project diff adds a test build file, file reference, group ent…
Cmux Swift Logging ✅ Passed The pull request adds no production logging. The Swift diff only changes tab-count logic and adds tests; it adds no print, debugPrint, dump, NSLog, file/stdout logging, Logger declaration, or …
Cmux User-Facing Error Privacy ✅ Passed PASS: The PR does not add or change user-facing errors, alerts, command output, API error bodies, or recovery copy. The production diff only changes split-zoom close-state logic and adds a developer c…
Cmux Full Internationalization ✅ Passed PASS — the PR changes only split-zoom logic, test wiring, and a new regression-test file. Sources/Workspace.swift adds state handling and developer comments; it adds no user-facing Swift text or loc…
Cmux Swiftui State Layout ✅ Passed PASS. The PR changes Workspace close-state logic, project wiring, and adds a non-SwiftUI regression test. The added Swift code introduces no ObservableObject, @Published, @StateObject, `Geomet…
Cmux Architecture Rethink ✅ Passed PASS. The PR makes a small local correctness change in the existing Bonsplit close delegate. It reads the existing pane tab list before close and clears split zoom only when the zoomed pane has at mos…
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PASS: The pull request changes Workspace tab-close state and adds a test-only Swift suite. The authoritative diff adds no NSWindow, NSPanel, NSWindowController, SwiftUI Window, or `WindowGro…
Cmux Source Artifacts ✅ Passed PASS. The diff changes only Sources/Workspace.swift, cmux.xcodeproj/project.pbxproj, and the hand-written test cmuxTests/WorkspaceSplitZoomTabCloseTests.swift. These are intentional source, test…
Cmux No Test Or Debug Seam In Production Source ✅ Passed PASS. The PR changes only the existing close-state logic in Sources/Workspace.swift: it adds a local tabs value, a product-behavior comment, and a tabs.count <= 1 condition. It adds no `#if DEBU…
Cmux No Ambient Global State ✅ Passed PASS. The production diff only changes the existing nested recordPostCloseState() helper in Sources/Workspace.swift; it adds no file-scope function, mutable global, static-only namespace, or singl…
Title check ✅ Passed The title clearly and concisely describes the main change: preserving split zoom when the zoomed pane remains after a tab close.
Description check ✅ Passed The description is complete and directly aligned with the change. It explains what and why, documents tests and toolchain limitations, includes the required review and checklist sections, and explains…
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@kugesh-Rajasekaran

Copy link
Copy Markdown
Contributor Author

I have read the CLA Document v2.2 and I hereby sign the CLA

github-actions Bot added a commit that referenced this pull request Sep 17, 2026
recordPostCloseState() marked the zoom for clearing whenever the closed
tab was the selected tab of the zoomed pane, regardless of how many tabs
that pane had. Closing one of several tabs therefore dropped the zoom and
snapped the window back to the split, even though the zoomed pane was
still there with another tab selected.

Mark the zoom for clearing only when the closed tab is the pane's last
one, which is the case where the pane itself goes away. Closing the last
tab still ends the zoom, so the surviving pane is never left hidden.

This covers the closing half of the issue only; opening a tab with Cmd+T
still clears the zoom through a separate multi-entrypoint path, so the
issue stays open.

Issue: manaflow-ai#8363
@kugesh-Rajasekaran
kugesh-Rajasekaran force-pushed the issue-8363-persist-pane-zoom-tab-close branch from b5a314a to 65752ee Compare September 17, 2026 11:32
# Conflicts:
#	cmux.xcodeproj/project.pbxproj
@cursor

cursor Bot commented Sep 25, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@teamleaderleo
teamleaderleo merged commit 3bd994a into manaflow-ai:main Sep 25, 2026
56 checks passed
@teamleaderleo

Copy link
Copy Markdown
Collaborator

Merged, thank you @kugesh-Rajasekaran! A zoomed pane now stays zoomed when you close one of its tabs. I merged main in and sorted out the project file conflict for you :D

@github-actions

Copy link
Copy Markdown
Contributor

Merge receipt for 49fd3ed824: every check was green at merge (13 verified; 13 skipped by policy). Full suite runs on main after merge.

rustybret pushed a commit to rustybret/bmux that referenced this pull request Sep 25, 2026
c055747 ci: parse runner expressions in the fork guard, and gate LINUX_RUNNER on fork PRs (manaflow-ai#14192)
fc112a8 docs(testing): describe how run-e2e.sh actually picks the runner (manaflow-ai#14620)
0f6edfa ci(owned): keep seeds until the disk is actually short, not at a fixed 6 (manaflow-ai#14621)
1b78097 CI: one owned-pool rescue sweeper instead of a rescue run per CI run (manaflow-ai#14602)
c7e79f6 Hold Files tree reloads while its context menu is open (manaflow-ai#14451)
dbb24cb ci: read the owned-pool rescue's Actions API through the route App (manaflow-ai#14499)
36b8063 ci: store each app-host product file once in the product archive (manaflow-ai#14601)
0dba677 CI: run CmuxWorkspaces package tests (manaflow-ai#14592)
2244e98 Fix color detection in native tmux mirrors (manaflow-ai#14175)
3d2478e build: keep every built file in a project group so Xcode reuses its build description (manaflow-ai#14486)
a1a5ae9 ci(cmux-tui): cache cargo builds in the Rust jobs (manaflow-ai#14613)
560e640 ci(owned): keep 8 seeds per mini and pick seeds by cost, not a 2-commit cap (manaflow-ai#14607)
561d317 cmux-debug-cli: find a publish-hq build in the HQ tag app cache (manaflow-ai#14579)
c0aaac7 fix(events): survive receive-timeout reconfiguration churn during replay (manaflow-ai#13888)
3bd994a fix: keep split zoom when the zoomed pane outlives a tab close (manaflow-ai#12853)
ca984c7 Session snapshots can send binding actions to ghostty on a surface that is no longer live (manaflow-ai#12623)
a47d65b settings: expose local tmux session persistence (manaflow-ai#13210)
e3acb25 ci: give an owned Mac's full app rebuild 35 minutes to compile (manaflow-ai#14594)

# Conflicts:
#	.github/workflows/ci-artifact-transport.yml
#	.github/workflows/ci-cache-receipts.yml
#	.github/workflows/ci-guards.yml
#	.github/workflows/ci-macos.yml
#	.github/workflows/ci-owned-pool-rescue.yml
#	.github/workflows/ci-web.yml
#	.github/workflows/ci.yml
#	.github/workflows/cloud-machine-tests.yml
#	.github/workflows/cloud-vm-guest-install.yml
#	.github/workflows/cloud-vm-image-contract.yml
#	.github/workflows/cloud-vm-image-reachability.yml
#	.github/workflows/cloudflare-relay.yml
#	.github/workflows/cmux-skill-contract.yml
#	.github/workflows/cmux-tui-sdks.yml
#	.github/workflows/cmux-tui-spec.yml
#	.github/workflows/cmux-tui.yml
#	.github/workflows/indexnow-tests.yml
#	.github/workflows/iroh-v2.yml
#	.github/workflows/localization-catalog.yml
#	.github/workflows/r2-upload-tests.yml
#	.github/workflows/remote-daemon.yml
#	.github/workflows/repair-nightly-appcast-content-types.yml
#	.github/workflows/required-checks-drift.yml
#	.github/workflows/resolve-dispatch-ref.yml
#	.github/workflows/seed-derived-data.yml
#	.github/workflows/terminal-hang-diagnostics.yml
#	.github/workflows/test-e2e.yml
#	.github/workflows/test-ios.yml
#	.github/workflows/testbox-broker-guard.yml
#	.github/workflows/web-validation.yml
lawrencecchen added a commit that referenced this pull request Sep 25, 2026
…14664)

#12853 made a zoomed pane that still has tabs stay zoomed after its selected
tab closes (#8363) and added WorkspaceSplitZoomTabCloseTests for it, but the
older TabManager test still asserted that the close cleared the zoom, so it
fails on main. Keep its focus assertion and expect the new zoom behavior.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants