Skip to content

perf(next): closing the last tab of a split pane removes the pane at once, not after the program read - #32898

Merged
teamleaderleo merged 3 commits into
feat-cmux-nextfrom
lane/right-pane-close-fast
Nov 3, 2026
Merged

teamleaderleo merged 3 commits into
feat-cmux-nextfrom
lane/right-pane-close-fast

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

Leo, 2026-11-03: "it's so slow to open and close idk if there's some sort of threading issue but it's nigh instant for claude and codex so we nede to be at least as fast". Leo, 2026-10-25: "closing the right pane seems be like... waiting for the right tab's closing animation to play before closing".

leo-named-fix

What was slow

Cmd-W (or a tab's close button) on the last terminal of a split's pane (the right pane, the usual case) kept the pane on screen until the daemon's process read answered (TabCloseGuard: does a program still run in it? up to 1 s per terminal). #31875 already hid the closing tab in the key's turn, but only when its pane kept another tab; a pane the close emptied waited for the read and only then left (prepareClose -> showPaneClose).

Fix

  • TabCloseGuard.close: a pane the close empties now hides with its tabs in the key's turn too, when its layout can show it leaving (LayoutModel.canShowPaneClose: a split's pane or a column's only pane; never the screen's last pane, which would show empty) and it holds no browser page (asking a page starts its close; the browser path is perf(browser): closing a Chromium tab, or a pane's last one, takes effect on the click (beforeunload still asks) #32876). The terminal stays alive: the read still decides.
    • No program: the hide lifts and the close's own intent shows the pane close again in the same turn (HiddenTabClose.lift runs the new lifted hooks: LayoutModel.withdrawPaneClose drops the hold unshown), so nothing is drawn in between.
    • A program runs: the pane comes back with its tab, its selection and its focus before the question; Cancel keeps it.
  • TabStripModel.cmdWTarget(_:closing:skipped:): the pure Cmd-W target pick moved from the App into CmuxNextTabs (no behavior change), which pays for the App lines this adds.

Design spec: plans/cmux-next/close-animation/DESIGN.md (the pane leaves in the key's frame); this extends it to a close the daemon's read still decides.

Risk

risk:normal. Close-path state: the hide/restore of an emptied pane while the read decides. A rolled-back close shows the pane again through the same cancel path a refused daemon close uses. Not covered: the screen's last pane (nothing to show in its place) and the warnings-off path where only the undo-toast decision reads (CloseUndoToasts.decideFirst, follow-up). A pane in a column with rows is #32907.

Performance

metric before after delta harness or run link
Cmd-W on a split's last idle terminal -> pane gone from the shown layout, p50 (15 rounds) 8.52 ms 1.96 ms -6.56 ms (-77%) CmdWEmptiedSplitPaneLeavesInItsTurnTests.measureCmdWToEmptiedPaneGone; before at 83ba468 (fleet step 2c8b77668d07787212926acb), after at cea57c4
same, p95 25.31 ms 2.23 ms -23.08 ms (-91%) same
same, max 39.13 ms 3.14 ms -35.99 ms same

The before is an idle same-tree daemon on an idle Mac; under load the read waits up to its 1 s bound, while the after does not depend on the read at all. The two runs landed on different fleet Macs (the controller picked them).

Tests

CmdWEmptiedSplitPaneLeavesInItsTurnTests (same-tree daemon, held read): the emptied pane leaves in the key's turn and its close lands; with sleep 600 running, the pane, tab and focus come back before the question and Cancel keeps it.
cmux-ci run --class light --script scripts/ci/package-test-lane.sh --ref SHA --arg=suite --arg=Packages/macOS/CmuxNext --arg=CmdWEmptiedSplitPaneLeavesInItsTurnTests,CmdWIdleTerminalClosesInItsTurnTests,CmdWTargetTests --env CMUX_CI_XCODE_APP=/Applications/Xcode_26.6.app

Observed / Not run

Ran: compile-check --package CmuxNext at 191b5ab (passed, 213 s); the suites above. Not run: a STRESS burst-bench row, a DEV build by hand.

Changelog

Fixed: Closing the last tab of a split pane (the right pane) removes the pane at once instead of after a short wait.

No capture: timing only.

Agent session: agent session

🤖 Generated with Claude Code

teamleaderleo and others added 3 commits November 3, 2026 02:12
…y's turn (red: it waits for the daemon's read)

Leo 2026-10-25: "closing the right pane seems be like... waiting". The
emptied pane stays on screen until TabCloseGuard's process read answers.
Adds a measuring test (Cmd-W -> pane gone, 15 rounds, p50/p95).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-ed9226b71907d8aaa0b6
…y's turn; a running program brings it back

TabCloseGuard hid the closing tabs in the key's turn (#31875) only when
their pane kept another tab: closing the last terminal of a split (the
right pane) kept the pane on screen until the daemon's process read
answered (up to 1 s). Now a pane the close empties leaves its window at
once too (PaneController.showPaneClose), when the layout can show it
leaving (never the screen's last pane) and it holds no browser page.
The read decides as before: no program, the hide lifts and the close's
own intent shows the pane close again in the same turn (no frame between);
a program, the pane comes back with its tab, selection and focus before
the question.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-ed9226b71907d8aaa0b6
…h the coordinator's

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 3, 2026 •

Copy link
Copy Markdown
Contributor

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

Review cea57c4: 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=pr32898 -f ref=lane/right-pane-close-fast -f platform=macos -f backend_mode=local

Default changes

This pull request changes 4 defaults at cea57c4b0dc0 (what everyone gets without configuring anything):

  • tunable-source.CmuxNextApp/Handlers/TabCloseGuard.swift:close: "ff4f87c8afce74ad8da9b9dda88590492db377f6baa0b60638959cc6f32735a1" -> "3ab97a26fb36de3dcc046c63324df0be9a25e746b4429e4cb5185daa7fc75eed"
  • tunable-source.CmuxNextApp/Handlers/TabCloseGuard.swift:cmdWTarget: "d29ba6e7cab703fe04757d54069cbefd51971b1b91c50925b768a0046c004983" -> "5c1a2f56f09527b4786b545245e4c8b6790e8ef7b341c6869d43841083125ff1"
  • tunable-source.CmuxNextApp/Handlers/TabCloseGuard.swift:cmdWTarget#: "53db97b2e9367f0056ca46ef4d25ceecc97c9a154390ae230eee85ef40124827" -> (none)
  • tunable-source.CmuxNextDaemonService/HiddenTabClose.swift:hide: "4026f23e5433a40958f931ca03200aea19cb5339f66ca1ec8836811a37a6306e" -> "035937427d1e6eb6959d90cd9975255222f56e1092c30dcb014df6b79767e3bc"

Signed off on this commit by review (codex, router-review).

Merge receipt: merged as 6ee46ce93804 with these defaults changed, signed off by review (codex, router-review).

Regenerate the snapshot with python3 scripts/ci/defaults_guard.py snapshot.

Shipped in:

@github-actions github-actions Bot added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Nov 3, 2026
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (codex, router-review): SHIP at cea57c4

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

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

SHIP at cea57c4
Visual verdict: no images; Leo tries the build

  1. [P2] [follow-up] TabCloseGuard.swift:51: Restore must account for another close hiding the pane. With two tabs A/B, quickly close A then B while both reads wait. A captures emptied == false; B hides the pane. If A reports a running program first, its restore skips withdrawing B’s pane hold, so A’s confirmation appears while its pane remains hidden. Reconcile pane visibility when any tab returns, and add a test controlling both reply orders.

  2. [P2] [follow-up] TabCloseGuard.swift:50: No images demonstrate the hide/restore transition. Verify it in the native build, including Cancel and focus restoration. AppKit tests could not run in this Linux box.

Follow-ups:

  • Reconcile overlapping close restores and test both reply orders.
  • Verify native pane motion and restored focus.

@cmux-bot cmux-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cmux-bot approves cea57c4: an independent review passed this exact head (router-review receipt by teamleaderleo). A push needs a new review; a later BLOCK at this head dismisses this approval.

@teamleaderleo
teamleaderleo merged commit 6ee46ce into feat-cmux-next Nov 3, 2026
209 of 225 checks passed
@teamleaderleo
teamleaderleo deleted the lane/right-pane-close-fast branch November 3, 2026 03:42
@cmux-bot cmux-bot Bot removed the stack-blocker label Nov 3, 2026
teamleaderleo added a commit that referenced this pull request Nov 3, 2026
…se of the pane's last other tab hid it (review of #32898) (#32973)

Two quick Cmd-Ws in one pane, the first on a running program: the second
emptied the pane, which hid; the first's rollback brought its tab back
into a hidden pane, so its question showed with no pane. A rolled-back
tab now withdraws whichever close hid its pane.


Agent-Session: https://cmux.com/deeplink/agent?id=aid-ed9226b71907d8aaa0b6

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@lawrencecchen

Copy link
Copy Markdown
Contributor

Swift logic follow-up (tip watcher swift-logic row, STATUS 13027 "SWIFT LOGIC CULPRITS"; lead order).

This PR raised swift_logic_lines by +40 at its merge commit 6ee46ce: scripts/cmux-next/swift-logic-lines.py before and after the merge, with the same table (the tip's, c6be943). The largest declarations of the rise:

  • extension LayoutModel in Packages/macOS/CmuxNext/Sources/CmuxNextLayout/Model/LayoutModel+PaneClose.swift: +21 lines counted as Swift logic (its row counts it as logic)
  • extension TabStripModel in Packages/macOS/CmuxNext/Sources/CmuxNextTabs/TabStripModel+CmdWTarget.swift: +12 lines counted as Swift logic (no row in plans/cmux-next/swift-logic-classes.tsv)
  • struct HiddenTabClose in Packages/macOS/CmuxNext/Sources/CmuxNextDaemonService/HiddenTabClose.swift: +8 lines counted as Swift logic (no row in plans/cmux-next/swift-logic-classes.tsv)

plans/cmux-next/layer-ownership.md says Rust owns every state, list and rule and Swift is the platform shell. Please move the new logic of this PR in a follow-up: to Rust (the crate that owns the area; the owner lists in STATUS 12147-12165 name the target per area), or at least out of the app into its package. If a declaration above is platform code (AppKit, Core Animation, OS calls, strings, tokens, DEBUG tooling), reply here and the table gets a PLATFORM row with your reason instead.

The swift-logic row is a report, not a gate: nothing blocks on this comment.

This branch was previously deployed

1 inactive deployment
ci-routing-agent — cea57c4b Deployed Nov 3, 2026 by teamleaderleo via namespace autofix #12859
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.

2 participants