Skip to content

fix(next): a stand-in tab or pane closes on the click, never "still opening" - #34367

Merged
teamleaderleo merged 5 commits into
feat-cmux-nextfrom
lane/close-while-opening
Nov 4, 2026
Merged

teamleaderleo merged 5 commits into
feat-cmux-nextfrom
lane/close-while-opening

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Nov 4, 2026 •

Copy link
Copy Markdown
Collaborator

Leo (2026-11-03), about a toast reading "This pane is still opening; try again in a moment.":

this is extremely bad. i should be able to just close immedeiately.

A close takes effect on the click, even while a pane or tab is still opening. Before this, two closes of a stand-in still sent its stand-in id, which is refused before it is sent (PaneNotReady, added in #28331). That showed the toast and brought the closed thing back:

What changed

  • The dock's stand-in tab is now a pending creation's tab (DaemonCreations.isPending/cancel(tab:)). Its close keeps the dock (the chat still docks) and sets a new standInClosed on the intent. From then on the overlay shows the dock without the stand-in. The daemon's fresh tab, found by the terminal id the client minted, is hidden when the records hold it and closed through the existing cancelled-creation close (close-tabs, session_end). The intent stays in the log until that close commits, so the tab does not come back when the dock settles, and not after a snapshot either. If the close fails, the intent is rejected and the records show what the daemon kept.
  • Close Pane on a stand-in pane leaves the records in the click's turn, not only the window. A new .closeStandInPane intent (DaemonStore.intendStandInPaneClose) takes the stand-in pane out of the layout, columns and panes. The daemon's pane that takes it over (same public pane id, or the pane of Cmd-N's workspace that holds its first tab) stays out too, until the daemon's records drop it. Its close-pane waits off the key's turn for that daemon pane (DaemonStore.daemonTarget, the same seam creates use since fix(app): Ctrl-` right after Cmd-N opens its terminal; a create on a pane still opening waits for it (P0 STATUS 2080) #29530), then settles at the reply's event sequence. If the close is refused, the pane comes back. If the stand-in was never made, there is nothing to close. A pane that is its screen's only pane (Cmd-N's lone pane) is not taken out of the records, same as a cancelled split's.
  • Review fix (P1 follow-up at 8cc685e): the close's own wait for its daemon pane (StandInWait.state) now finds the daemon pane a pending stand-in close hides. Before, the hide took that pane out of the index, so once the dock settled the wait threw PendingPaneNotCreated and the refused close brought the pane back.
  • Review fix (P1 at ee6f658): if the daemon's dock pane and layout settle the move before the respawned fresh tab arrives, the closed stand-in's intent waits for that tab (up to a snapshot, which holds everything the daemon made). A late fresh tab is still hidden and closed.

No close path reaches the "still opening" toast now. The toast text itself stays for the non-close pane verbs that #28331 refuses on a stand-in (zoom, rename, swap), so those refusals still show a message.

Tests

ProvisionalDockTests (store level, no daemon binary):

  • closingTheFreshTabsStandInClosesItAtOnceAndItsDaemonTabNeverShows: the stand-in leaves in the close's turn, the chat stays docked, and nothing is sent yet. When the daemon's dock and fresh tab land, the fresh tab is hidden and its close is sent once. It stays hidden after the move settles, and the intent leaves only when the close commits. No mirror violations. On base, cancel(tab:) returns false and the stand-in stays.
  • aFreshTabThatLandsAfterTheDockSettledStaysClosed: the dock pane and layout arrive as events, the move settles, and then the fresh tab lands. It is hidden and closed (the review's P1).
  • closingTheDocksStandInPaneTakesItOutAtOnceAndItsDaemonPaneNeverShows: right after the close intent, the stand-in pane, its dock column and its chat are gone from the model. The daemon's dock pane under the same public id never shows, including after both replies settle. The intent leaves once a snapshot drops the pane. No mirror violations.
  • The same test asks daemonTarget for the stand-in before and after the dock settles and gets the hidden daemon pane both times (the P1 at 8cc685e).
  • aRefusedDockAfterItsStandInClosedShowsTheChatOnly: a refused move after the close leaves the chat in its pane, with no stand-in.

tooling-tests red: the same 32 WebLintPrePushTests fail by name on #34392, a Swift-only PR with no overlap, so it is a base red.

Risk: the overlay changes only for a .dockTab whose stand-in was closed and for the new .closeStandInPane. The other intents are untouched; the standInClosed checks are false for them. hidePane is the cancelled split's pane removal moved out unchanged. The hidden-pane tab count now leaves out provisional tabs, which the conservation check already leaves out of the visible count.

Follow-ups (not in this PR): #34414, and #34432 (check the native layout of the pane close in a build, the review's P2). Closing Cmd-N's lone stand-in pane while the workspace opens is hidden only in the view, not taken out of the model. Cmd-W on Cmd-N's own first tab while it opens still sends the stand-in. Both need a decision on whether the emptied workspace closes or keeps an empty pane (EmptyWorkspaceRepair).

Agent session: agent session

🤖 Generated with Claude Code

…pening"

Closing the dock's fresh-tab stand-in before the daemon confirms now takes
it out at once while the chat still docks; the daemon's fresh tab is hidden
when it lands and closed, never shown (#33829). Close Pane on a stand-in
pane (a dock's, Cmd-N's first pane) waits for the daemon's pane off the
key's turn, as a create does, instead of being refused.

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

Copy link
Copy Markdown
Contributor

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

Review a065072: 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=pr34367 -f ref=lane/close-while-opening -f platform=macos -f backend_mode=local

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

CI passes on bb7eef01ec (run 41752469752 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.

red: cmux-next checks (god files, concurrency, crash safety, l10n); no finished base run on 842ce34 to compare; do not merge until green; see lane-rules

Shipped in:

@teamleaderleo teamleaderleo added the leo-named-fix An obvious fix to something Leo named: lands on push, review runs in parallel label Nov 4, 2026
@github-actions github-actions Bot added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Nov 4, 2026
@blacksmith-sh

blacksmith-sh Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

Found 7 test failures on Blacksmith runners:

Failures

Test View Logs
/tmp/tmpfihtv0td/summary-missing.json (live):/summary-missing.json (live): View Logs
/tmp/tmpor6ugdq6/summary-fail.json (live):/summary-fail.json (live): View Logs
[new_workspaces] debug.new_workspace timed 0 of 50 new workspace rows/
[new_workspaces] debug.new_workspace timed 0 of 50 new workspace rows
View Logs
[new_workspaces] the window draws no frames/[new_workspaces] the window draws no frames View Logs
[tabs] debug.tab_switch timed 0 of 40 switches/
[tabs] debug.tab_switch timed 0 of 40 switches
View Logs
[tabs] the window draws no frames/[tabs] the window draws no frames View Logs
convention violations found (ERROR lines above)./
convention violations found (ERROR lines above).
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 (codex gpt-6-astra high, router-review): BLOCK at ee6f658

BLOCK at ee6f658

Visual verdict: no images; Leo tries the build

  1. [P1] [blocking] In Packages/macOS/CmuxNext/Sources/CmuxNextDaemon/Store/ProvisionalSplit.swift:56-60, a closed stand-in intent is retained only when freshTabHeld is already true. If the daemon confirms the dock pane/layout before its respawn tab appears, the intent becomes due and is removed; restoreClosedStandIn then cannot hide or close the later tab, which reappears despite the user closing it. Keep this intent pending through that pane-first ordering until the fresh tab is observed and its close commits.

…in the click's turn

The close's intent hides the stand-in pane, and the daemon's pane that takes
it over, until the daemon's records drop it; a refused close brings it back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-ed9226b71907d8aaa0b6
…ck pane comes first

Review of #34367 (P1): the daemon's dock pane and layout can settle the move
before the respawned tab arrives. The intent now waits for that tab (up to a
snapshot), so a late fresh tab is still hidden and closed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-ed9226b71907d8aaa0b6
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (codex, router-review): SHIP at 8cc685e

Reviewer: codex gpt-6.1-sol xhigh, 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 8cc685e
Visual verdict: no images; Leo tries the build

  1. [P1] [follow-up] Keep the hidden pane discoverable by daemonTarget — IntentOverlay.swift:106. Closing a dock stand-in removes its replacement pane from the index before resolveStandInWaits() runs. The waiter cannot observe adoption; once the dock settles, it throws PendingPaneNotCreated. The handler then rejects the close, restoring the pane without sending close-pane. Resolve the target against confirmed or hidden records, and test the actual waiter; the added test manually settles the close.

  2. [P2] [follow-up] Verify the native close/restore layout in a build — PaneHandlers+Verbs.swift:63. No after images cover this behavior.

Swift tests could not run because Swift is unavailable in this box.

Follow-ups:

  • [P1] [follow-up] Fix hidden-pane target resolution and add waiter coverage.
  • [P2] [follow-up] Verify the native close/restore layout.

Review of #34367 (P1): the close's overlay took the daemon's pane out of the
index, so daemonTarget never saw the adoption and threw once the dock settled;
the close was refused and the pane came back. The wait now finds the daemon
pane a pending stand-in close hides.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-ed9226b71907d8aaa0b6
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (subagent opus-5.5 medium): SHIP at a065072
Visual verdict: no images; Leo tries the build
I read the whole diff against the checkout, plus the surrounding store code (withOverlayLifted, removeDue, requestCloses, the cancelled-creation close wiring, daemonTarget/StandInWait, DockColumnCommand, ctx.send). What I checked: undo order, conservation counts, how long each intent stays in the log, that each close goes out once to the right surface or pane, the failure paths, actor isolation, exhaustive switches, and whether the tests fail on base.

What holds up:

  • Undo order: .closedStandIn applies the dock overlay then the hide, and undoes them in reverse (unhide, then the dock).
  • Close Pane: .closeStandInPane hides whichever pane is showing at each reapply: the stand-in while it shows, else the daemon pane with the same public id.
  • Conservation: hiddenTabs now leaves out provisional tabs in hidden panes and counts the hidden fresh tab in .closedStandIn. Both match what the confirmed census sees.
  • Lifetimes: the dock intent with a closed stand-in waits for its fresh tab and then for that tab's close to commit. The close intent stays until the records drop the daemon pane. Refusal goes through PendingPaneNotCreated, or rejectIntent from the failure catch, and both remove the intent.
  • One close each: the fresh tab's close is guarded by closeRequested, and close-pane is sent once to the resolved daemon pane. hiddenDaemonPane keeps that pane findable after the dock settles.
  • Isolation: the closure captures only Sendable values (DaemonStore is @MainActor) and hops to the main actor for every store write. await connection.eventSequence() matches the existing pattern in DaemonService+Intents.swift:52.
  • Switches: both Intent switches without a default (IntentOverlay.apply, DaemonStore+Stage.swift:96) and IntentOverlay.undo cover the new cases.
  • Tests: they would fail on base. cancel(tab:) returns false for a dock's stand-in there, and intendStandInPaneClose doesn't exist.

Findings:

  1. [follow-up] Sources/CmuxNextApp/Handlers/PaneHandlers+Verbs.swift:63: intendStandInPaneClose runs before ctx.send. When ctx.send returns early, or DaemonService.failure finds no connection, the body never runs, so the close intent is never settled or rejected. The pane then stays hidden until... nothing; no path removes it, since settlesAtSnapshot is never set. Fix: register the intent only once a connection exists, or reject it on the not-connected path.
  2. [follow-up] Same file, lines 57-63: the intent goes on daemon(for: pane).store, but ctx.send runs on services.activeDaemon's connection. For a pane on another machine's daemon, noteSettled would get a sequence from the wrong connection. The send-on-active-daemon part was already there on base; the PR now ties an intent to it.
  3. [follow-up] Sources/CmuxNextDaemon/Store/ProvisionalSplit.swift:417: a closed stand-in's dock waits for its fresh tab until a snapshot arrives. If the daemon replies to the move but never respawns a fresh tab, the dock intent stays in the log (re-checked on every apply) until the next snapshot.
  4. [follow-up] Sources/CmuxNextDaemon/Store/CancelledCreation.swift:82: when the stand-in was the source pane's only tab, ProvisionalDock.apply(standIn: nil) leaves that pane visibly empty until the fresh tab's close commits and the daemon drops or keeps the pane. The test asserts the empty pane; check what the UI shows in that window.
  5. [follow-up] Sources/CmuxNextDaemonService/DaemonService+CancelledCreation.swift:46: if the fresh tab's close fails, rejectIntent is called on the whole dock transaction. That is correct here, because the daemon must already have docked for the fresh tab to exist, but nothing documents it. A comment or a test would keep it from regressing.

Follow-ups: 1 (intent leaks when the send never runs), 2 (store and connection from different daemons for remote panes), 3 (dock waits until a snapshot when no fresh tab comes), 4 (empty source pane while the fresh tab's close is in flight), 5 (comment or test that a failed fresh-tab close rejects the whole dock).

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

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

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 a065072
Visual verdict: no images; Leo tries the build

  1. [P1] [follow-up] Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/PaneHandlers+Verbs.swift:64: Clean up the close intent when dispatch fails before entering the closure. ctx.send returns immediately when disconnected, and DaemonService.failure also skips the closure if the connection disappears before its task starts. Neither reaches these catch blocks. The unsent intent then has no settlement or rejection and can keep a subsequently reconciled dock pane hidden indefinitely. Handle dispatch failure outside the body and reject the intent there.

  2. [P2] [follow-up] Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/PaneHandlers+Verbs.swift:64: Verify the changed native pane removal and focus behavior in a build; no after images were supplied.

Static review completed; tests could not run because Swift is unavailable.

Follow-ups:

@cursor

cursor Bot commented Nov 4, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

Bugbot uses usage-based billing for this team and requires on-demand usage to be enabled.

A team admin can enable on-demand usage in the Cursor dashboard.

@cmux-bot
cmux-bot Bot temporarily deployed to ci-routing-agent November 4, 2026 01:12 Inactive
@cmux-bot
cmux-bot Bot temporarily deployed to ci-routing-agent November 4, 2026 01:12 Inactive
@cmux-bot
cmux-bot Bot temporarily deployed to ci-routing-agent November 4, 2026 01:18 Inactive
@teamleaderleo
teamleaderleo merged commit 431286d into feat-cmux-next Nov 4, 2026
108 of 121 checks passed
@teamleaderleo
teamleaderleo deleted the lane/close-while-opening branch November 4, 2026 01:27
@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 +126 at its merge commit 431286d: 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:

  • enum CancelledCreation in Packages/macOS/CmuxNext/Sources/CmuxNextDaemon/Store/CancelledCreation.swift: +71 lines counted as Swift logic (its row counts it as logic)
  • extension DaemonStore in Packages/macOS/CmuxNext/Sources/CmuxNextDaemon/Store/DaemonCreations.swift: +20 lines counted as Swift logic (its row counts it as logic)
  • struct ProvisionalSplit in Packages/macOS/CmuxNext/Sources/CmuxNextDaemon/Store/ProvisionalSplit.swift: +10 lines counted as Swift logic (its row counts it as logic)
  • struct IntentLog in Packages/macOS/CmuxNext/Sources/CmuxNextDaemon/Store/IntentLog.swift: +5 lines counted as Swift logic (its row counts it as logic)

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 — bb7eef01 Deployed Nov 4, 2026 by cmux-bot[bot] via wait for same-key tree publisher #108468
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

leo-named-fix An obvious fix to something Leo named: lands on push, review runs in parallel risk:normal Risk tier (docs/ci/risk-tiers.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants