cmux-next: new splits no longer vanish when made in quick succession

AU
4d ago
+801-32

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 job abc473a11c7aeb03f2c717f0, tag 26695-splits-vanish): FAIL. cmd-d presses settles at 3 or 4 of 5 panes (view counts 2,3,4,2,3), and cmd-d then browser split settles at 2 of 3.
  • Head 1140f975d19a9 live check (fleet build e09c6c2096f6be8f262c5551 at 56928e56, same app code): PASS 15/15 phases over 3 rounds. Every Cmd-D burst settles at 5 panes; cmd-d then browser split settles 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, .terminalSplits and .mixed bursts at RapidSplitsKeepPanesTests.swift:54 (Expectation failed).
  • Green: head e545b3cde7f137cb7b5a3d638a821d68ecce34b1 (step d45637dd0710eab15a281ade): 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 (step 035383c28ffb7abe87e91e5f), 4983fb35 (step f2df026b33103173b92668ec).

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; BrowserStandInIntentTests passed at 56928e56; scripts/cmux-next/prepush-gate.py clean.
  • Not re-run at e545b3cd: the live check (last at 56928e56); 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 0a7c1b94c669b and 5b671f6b0eca4 (the RapidSplitsKeepPanesTests suite and rapid-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 Autofix with [code]smith
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

  • RapidSplitsKeepPanesTests presses splits back to back with no gap; InstantCreateIntentTests covers the wait, refusal, cancellation, tab-on-pending-split, and Cmd-N pane paths.
  • scripts/cmux-next/split-vanish-live.py (and the same surface via rapid-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.