cmux-next: new splits no longer vanish when made in quick succession
Summary
In cmux NEXT, a split or browser pane made quickly after another split showed up and then vanished (https://github.com/manaflow-ai/cmux/issues/26695, also reported as https://github.com/manaflow-ai/cmux/issues/26689).
Cmd-D shows its new pane at once as a provisional pane and focuses it. That pane's handle exists only in the app (ā„ 2^62) until the daemon's pane takes it over under the same public id. A second Cmd-D, a browser split, a New Tab page split or a new tab pressed in that window sent the provisional handle to the daemon. The daemon refused it (split failed: unknown pane 4611686018427387960 in the app log), and the intent log rolled the create back, so the pane disappeared. New Column and New Row already resolved their target when sending. Cmd-D, browser tabs, agent/New Tab page tabs and new terminal tabs did not.
Fix: DaemonStore.daemonPane(confirming:) returns the daemon's pane behind a stand-in, so every create aimed at a pane names that pane. When the daemon has already taken the stand-in over, it answers at once. Otherwise it waits, event-driven, without polling: its waiters run after each overlay apply and are released when the connection ends. It returns nil when the stand-in's own create is refused, and the dependent create fails the same way it does today. Callers: PaneSplitCommand.target (Cmd-D on both paths, New Column, New Row, new terminal tab), the browser tab open funnel and Split Browser's move, and the agent / New Tab page tab create.
Review follow-ups (https://github.com/manaflow-ai/cmux/pull/26926#issuecomment-6483475197): a new tab pressed onto a pending split's pane, or onto Cmd-N's first pane, now stays shown in the daemon's pane instead of vanishing for a round trip; a cancelled wait sends nothing; a create aimed at a cancelled split gets no pane.
Not in this PR: the late-reply path, where a split whose reply misses its 5 s start deadline on a loaded host is rejected even though the daemon commits it. It needs a keyed retry and is a separate follow-up.
Testing
Regression check: scripts/cmux-next/split-vanish-live.py SOCKET OUT_DIR PID. It attaches to a tagged DEV app, presses Cmd-D back to back through debug.key and fires split action bursts. It fails when the shown pane count goes down or settles short.
- Base
ffa3b4cdd6e0(fleet jobabc473a11c7aeb03f2c717f0, tag26695-splits-vanish): FAIL.cmd-d pressessettles at 3 or 4 of 5 panes (view counts2,3,4,2,3), andcmd-d then browser splitsettles at 2 of 3. - Head
1140f975d19a9live check (fleet builde09c6c2096f6be8f262c5551at56928e56, same app code): PASS 15/15 phases over 3 rounds. Every Cmd-D burst settles at 5 panes;cmd-d then browser splitsettles at 3 (2 terminals + browser), counts never go down.
Unit regression (Tests/CmuxNextAppTests/RapidSplitsKeepPanesTests.swift, back-to-back presses with no wait between them), run on the fleet:
cmux-ci run --class light --script scripts/ci/package-test-lane.sh --arg=suite --arg=Packages/macOS/CmuxNext --arg=RapidSplitsKeepPanesTests --ref <sha>
- Red: side branch at
7ccd183e91765(base fix-free code + this test only): FAIL,.terminalSplitsand.mixedbursts atRapidSplitsKeepPanesTests.swift:54(Expectation failed). - Green: head
e545b3cde7f137cb7b5a3d638a821d68ecce34b1(stepd45637dd0710eab15a281ade): PASS, 6/6 suites:RapidSplitsKeepPanesTests,InstantCreateIntentTests(new: stand-in waits queued, refused, cancelled; a tab on a pending split and on Cmd-N's pane),NewTabIntentTableTests,NewTabSplitRefusalTests,SplitBrowserFailureTests,DragSendsOneIntentTests. Superseded:1140f975d19a9(step035383c28ffb7abe87e91e5f),4983fb35(stepf2df026b33103173b92668ec).
Observed / Not run
- Observed: the live check above on base and head builds (window snapshots before/after each phase: base shows 3 panes after 4 Cmd-D, head shows 5); the fleet unit runs above;
BrowserStandInIntentTestspassed at56928e56;scripts/cmux-next/prepush-gate.pyclean. - Not re-run at
e545b3cd: the live check (last at56928e56); the review follow-ups change only the overlay and the stand-in wait, covered by the unit runs above. - Not run: the full CmuxNext suite. Other creates still send a stand-in handle and were only read, not changed: tab moves, tab groups, workspace content placement, agent/chief/cloud handlers, the non-optimistic New Column path. They are follow-ups if they show the same symptom. The "workspace left empty" variant was not reproduced separately; the live check never showed an empty workspace on either build. Possible overlap with https://github.com/manaflow-ai/cmux/pull/23032 not checked in depth.
- Commits
0a7c1b94c669band5b671f6b0eca4(theRapidSplitsKeepPanesTestssuite andrapid-split-keeps-panes-e2e.py) came from another session working the same issue; I kept and tightened the test.
Changelog
Fixed: New splits and browser panes made in quick succession no longer appear and then vanish
Proof
No capture: no visible UI changes, only panes that no longer disappear. The live check's window snapshots were taken on a local isolated DEV app and are not posted because they show the local account; the pane counts above come from them.
Checklist
- Behavior changes have added or updated tests, or Testing says why not
- No UI, settings or user-facing strings change (an existing refusal string is reused)
š¤ Generated with Claude Code
![View with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v3-3x.png)
![Autofix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark-3x.png)
Need help on this PR? Tag @codesmith-bot with what you need.
Summary by cubic
Fixes new splits, browser panes, tabs, and Cmd-N's first pane vanishing when created in quick succession (#26695): a create pressed while an earlier one still shows its pane provisionally sent the provisional pane's client-only handle to the daemon, which refused it and rolled the create back.
Creates now name their target in the press's turn (DaemonStore.paneTarget(_:)) and resolve it before sending. daemonPane(confirming:) waits event-driven for the daemon's pane to take the stand-in over, or returns nil when the stand-in's create was refused, cancelled, or its waiting task is cancelled. A tab pressed onto a pending split stays shown in the daemon's pane; Cmd-N's first pane resolves through its workspace key. Applies to PaneSplitCommand.target (Cmd-D, New Column, New Row, new terminal tab), the browser tab open funnel and Split Browser's move, and agent / New Tab page tab creation.
Testing
RapidSplitsKeepPanesTestspresses splits back to back with no gap;InstantCreateIntentTestscovers the wait, refusal, cancellation, tab-on-pending-split, and Cmd-N pane paths.scripts/cmux-next/split-vanish-live.py(and the same surface viarapid-split-keeps-panes-e2e.py) fails on the pre-fix base, settling at 3ā4 of 5 panes, and passes on head.- Not covered: the late-reply path is a separate follow-up; creates that still send stand-in handles (tab moves, tab groups, workspace content placement) were only read, not changed.