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

TE
16h ago
+359-37

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:

  • The dock's fresh-tab stand-in (#33455's ProvisionalDock). When a lone chat is docked left, its old pane shows the fresh terminal's stand-in tab. CancelledCreation covered only .createTab and .splitPane, so closing that stand-in went to the daemon, failed with the toast, and the daemon's fresh tab still showed when it landed. Fixes #33829.
  • Close Pane on a stand-in pane that is not a split's: a dock's pane, or Cmd-N's first pane. A split's stand-in was already cancelled.

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 #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