Skip to content

P0 port: keys typed after Cmd-N then Ctrl-` reach the new terminal (#30827, #29530 keys part) - #30271

Merged
lawrencecchen merged 10 commits into
cp/dogfood-v2-2-12-optfrom
fix/p0-ctrlbt-after-cmdn-dogfood
Nov 1, 2026
Merged

lawrencecchen merged 10 commits into
cp/dogfood-v2-2-12-optfrom
fix/p0-ctrlbt-after-cmdn-dogfood

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Testing

  • Compile at the head 16891d1: bc1c6d1a65cee7dc17ba0a9c passed (CmuxNext with tests, app, cmux-tui).
  • Red bac8b631e19bb81a12332906 at 95dbc94 (the same tests and harness, New Tab hold release off): CmdNThenCreateKeysTests failed 2/2 on the screen check (sh-3.2$; every key "New Tab hold true"). fix(app): Ctrl-` right after Cmd-N opens its terminal; a create on a pane still opening waits for it (P0 STATUS 2080) #29530's key test passed there: it does not discriminate this fix.
  • Probe 7ec3a8abe34e5cbc20966aae at 66e36b7 (the head's product code): the 5 key tests passed.
  • Green 0f44c923195bc87faf36079e at the head (display, CMUX_TEST_REQUIRE_GUI=1; CmdNThenCreateKeysTests, CmdNThenNewTerminalTabTests, CmdNStandInTargetTests, PaneVerbOnPendingPaneTests, EarlyTerminalKeysTests, NewTerminalOnPendingSplitTests): PASSED, all 6 suites (2+3+5+5+9+1 tests), gui-session-v1 console line present, no FLAKY. CmdNThenCreateKeysTests 2/2: the typed line and its answer show in the new terminal for Ctrl-` and Cmd-D.

Observed / Not run

  • Observed: the fleet results above, read in their logs.
  • Not run: the other withApp suites at the head (the harness now starts the runtime in all of them, as the app does); a real Ctrl-` key press (smoke rounds); New Column and New Row after Cmd-N.

Changelog

  • Fixed: after Cmd-N then Ctrl-` (or Cmd-D), the keys typed next reach the new terminal, not the New Tab page.

No capture: key routing; the evidence is the display-worker screen oracle.
perf claim: none.

@lawrencecchen lawrencecchen added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Nov 1, 2026

@cubic-dev-ai cubic-dev-ai 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.

5 issues found across 15 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Packages/macOS/CmuxNext/Tests/CmuxNextDaemonTests/CmdNStandInTargetTests.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Tests/CmuxNextDaemonTests/CmdNStandInTargetTests.swift:59">
P2: These five yields do not establish that `daemonTarget` entered its stand-in wait, so this regression test can pass without exercising the race. Park until `store.standInWaits` contains the wait and assert that state (and apply the same change to the identical loop in the failure test).</violation>
</file>

<file name="Packages/macOS/CmuxNext/Sources/CmuxNextDaemonService/NewTerminalTabCommand.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Sources/CmuxNextDaemonService/NewTerminalTabCommand.swift:62">
P2: The `target` closure now suspends, so a cancelled wait throws CancellationError. The `send` closure maps every non-PaneNotReady/PendingPaneNotCreated failure to NewTerminalTabFailure, so the person sees 'the daemon refused the new tab' even though no request was sent. Handle the cancellation explicitly, or make the message reflect that nothing was sent.</violation>
</file>

<file name="Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/PaneVerbOnPendingPaneTests.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/PaneVerbOnPendingPaneTests.swift:114">
P2: This assertion does not verify that the new tab landed in the pending pane's daemon pane; a tab opened in another pane would still satisfy the global `tabsBefore + 2` count. Assert that the pending pane itself gained the tab, and apply the same target-specific check to the repeated assertion below.</violation>
</file>

<file name="Packages/macOS/CmuxNext/Sources/CmuxNextDaemonService/NewRowCommand.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Sources/CmuxNextDaemonService/NewRowCommand.swift:36">
P3: `daemonTarget` can now throw `PendingPaneNotCreated`, but this command still converts it to `PaneSplitFailure`, falsely reporting a daemon refusal. Preserve and rethrow `PendingPaneNotCreated` like `PaneSplitCommand` so a failed parent create reports that the target pane was not created.</violation>
</file>

<file name="Packages/macOS/CmuxNext/Sources/CmuxNextDaemonService/NewColumnCommand.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Sources/CmuxNextDaemonService/NewColumnCommand.swift:48">
P3: `daemonTarget` can now throw `PendingPaneNotCreated`, but this command still converts it to `PaneSplitFailure`, falsely reporting a daemon refusal. Preserve and rethrow `PendingPaneNotCreated` like `PaneSplitCommand` so a failed parent create reports that the target pane was not created.</violation>
</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

let send = Task { @MainActor in
do { outcome.pane = try await store.daemonTarget(workspace.pane) } catch { outcome.error = error }
}
for _ in 0..<5 { await Task.yield() }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: These five yields do not establish that daemonTarget entered its stand-in wait, so this regression test can pass without exercising the race. Park until store.standInWaits contains the wait and assert that state (and apply the same change to the identical loop in the failure test).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Tests/CmuxNextDaemonTests/CmdNStandInTargetTests.swift, line 59:

<comment>These five yields do not establish that `daemonTarget` entered its stand-in wait, so this regression test can pass without exercising the race. Park until `store.standInWaits` contains the wait and assert that state (and apply the same change to the identical loop in the failure test).</comment>

<file context>
@@ -0,0 +1,170 @@
+        let send = Task { @MainActor in
+            do { outcome.pane = try await store.daemonTarget(workspace.pane) } catch { outcome.error = error }
+        }
+        for _ in 0..<5 { await Task.yield() }
+        #expect(outcome.error == nil, "the create on Cmd-N's stand-in was refused: \(String(describing: outcome.error))")
+        #expect(outcome.pane == nil, "the create named a pane before the daemon had one: \(String(describing: outcome.pane))")
</file context>
Suggested change
for _ in 0..<5 { await Task.yield() }
for _ in 0..<100 where store.standInWaits.isEmpty { await Task.yield() }
#expect(store.standInWaits.count == 1, "daemonTarget did not park while Cmd-N was opening")

// The target at send time (as Cmd-D, New Column and New Row): a Ctrl-` pressed on a stand-in whose create is in
// flight (a split's, Cmd-N's first pane) waits for it and names the daemon's pane that took it over, never the
// stand-in (``DaemonStore/daemonTarget(_:)``). P0 STATUS 2080: refused there before, no terminal.
let target: @MainActor @Sendable () async throws -> PaneID = { try await daemon.store.daemonTarget(pane) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: The target closure now suspends, so a cancelled wait throws CancellationError. The send closure maps every non-PaneNotReady/PendingPaneNotCreated failure to NewTerminalTabFailure, so the person sees 'the daemon refused the new tab' even though no request was sent. Handle the cancellation explicitly, or make the message reflect that nothing was sent.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextDaemonService/NewTerminalTabCommand.swift, line 62:

<comment>The `target` closure now suspends, so a cancelled wait throws CancellationError. The `send` closure maps every non-PaneNotReady/PendingPaneNotCreated failure to NewTerminalTabFailure, so the person sees 'the daemon refused the new tab' even though no request was sent. Handle the cancellation explicitly, or make the message reflect that nothing was sent.</comment>

<file context>
@@ -55,9 +56,10 @@ public struct NewTerminalTabCommand: Sendable {
+        // The target at send time (as Cmd-D, New Column and New Row): a Ctrl-` pressed on a stand-in whose create is in
+        // flight (a split's, Cmd-N's first pane) waits for it and names the daemon's pane that took it over, never the
+        // stand-in (``DaemonStore/daemonTarget(_:)``). P0 STATUS 2080: refused there before, no terminal.
+        let target: @MainActor @Sendable () async throws -> PaneID = { try await daemon.store.daemonTarget(pane) }
         let send = daemon.intendNowReporting("new-tab", intent, transaction: transaction) { connection in
             var sent = sent
</file context>

// The split confirms, and the typed tab opens in its daemon pane: the split's tab plus this one.
try await PinnedPageTests.until({
content.panes.values.allSatisfy { $0.pane.handle.rawValue < ProvisionalPane.firstHandle }
&& content.panes.values.reduce(0) { $0 + $1.pane.tabs.count } == tabsBefore + 2

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: This assertion does not verify that the new tab landed in the pending pane's daemon pane; a tab opened in another pane would still satisfy the global tabsBefore + 2 count. Assert that the pending pane itself gained the tab, and apply the same target-specific check to the repeated assertion below.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/PaneVerbOnPendingPaneTests.swift, line 114:

<comment>This assertion does not verify that the new tab landed in the pending pane's daemon pane; a tab opened in another pane would still satisfy the global `tabsBefore + 2` count. Assert that the pending pane itself gained the tab, and apply the same target-specific check to the repeated assertion below.</comment>

<file context>
@@ -109,23 +108,22 @@ struct PaneVerbOnPendingPaneTests {
+            // The split confirms, and the typed tab opens in its daemon pane: the split's tab plus this one.
+            try await PinnedPageTests.until({
+                content.panes.values.allSatisfy { $0.pane.handle.rawValue < ProvisionalPane.firstHandle }
+                    && content.panes.values.reduce(0) { $0 + $1.pane.tabs.count } == tabsBefore + 2
+            }, report: {
+                "the typed new tab did not open: \(content.panes.values.reduce(0) { $0 + $1.pane.tabs.count }) tabs, \(tabsBefore) before the split; HUD \(services.refusalHUD.message ?? "none")"
</file context>

let intent = Intent.splitPane(pane: pane, direction: .down, ratio: 0.5, provisional: provisional)
// The target at send time (see ``NewColumnCommand``): a stand-in has become the daemon's pane.
let target: @MainActor @Sendable () throws -> PaneID = { try daemon.store.daemonTarget(pane) }
let target: @MainActor @Sendable () async throws -> PaneID = { try await daemon.store.daemonTarget(pane) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: daemonTarget can now throw PendingPaneNotCreated, but this command still converts it to PaneSplitFailure, falsely reporting a daemon refusal. Preserve and rethrow PendingPaneNotCreated like PaneSplitCommand so a failed parent create reports that the target pane was not created.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextDaemonService/NewRowCommand.swift, line 36:

<comment>`daemonTarget` can now throw `PendingPaneNotCreated`, but this command still converts it to `PaneSplitFailure`, falsely reporting a daemon refusal. Preserve and rethrow `PendingPaneNotCreated` like `PaneSplitCommand` so a failed parent create reports that the target pane was not created.</comment>

<file context>
@@ -33,7 +33,7 @@ public struct NewRowCommand: Sendable {
         let intent = Intent.splitPane(pane: pane, direction: .down, ratio: 0.5, provisional: provisional)
         // The target at send time (see ``NewColumnCommand``): a stand-in has become the daemon's pane.
-        let target: @MainActor @Sendable () throws -> PaneID = { try daemon.store.daemonTarget(pane) }
+        let target: @MainActor @Sendable () async throws -> PaneID = { try await daemon.store.daemonTarget(pane) }
         let send = daemon.intendNowReporting("new-row", intent, transaction: transaction) { connection in
             try await RowCommands(connection).newRow(below: try await target(), height: height, options: sent)
</file context>

// daemon's pane by then (``DaemonStore/daemonHandle(showing:)``; creates of one workspace go
// out in order, each once the store holds the one before).
let target: @MainActor @Sendable () throws -> PaneID = { try daemon.store.daemonTarget(pane) }
let target: @MainActor @Sendable () async throws -> PaneID = { try await daemon.store.daemonTarget(pane) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: daemonTarget can now throw PendingPaneNotCreated, but this command still converts it to PaneSplitFailure, falsely reporting a daemon refusal. Preserve and rethrow PendingPaneNotCreated like PaneSplitCommand so a failed parent create reports that the target pane was not created.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextDaemonService/NewColumnCommand.swift, line 48:

<comment>`daemonTarget` can now throw `PendingPaneNotCreated`, but this command still converts it to `PaneSplitFailure`, falsely reporting a daemon refusal. Preserve and rethrow `PendingPaneNotCreated` like `PaneSplitCommand` so a failed parent create reports that the target pane was not created.</comment>

<file context>
@@ -45,7 +45,7 @@ public struct NewColumnCommand: Sendable {
         // daemon's pane by then (``DaemonStore/daemonHandle(showing:)``; creates of one workspace go
         // out in order, each once the store holds the one before).
-        let target: @MainActor @Sendable () throws -> PaneID = { try daemon.store.daemonTarget(pane) }
+        let target: @MainActor @Sendable () async throws -> PaneID = { try await daemon.store.daemonTarget(pane) }
         let send = daemon.intendNowReporting("new-pane-right", intent, transaction: transaction) { connection in
             try await connection.newColumn(rightOf: try await target(), width: width, options: sent)
</file context>

@cursor

cursor Bot commented Nov 1, 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.

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (opus-review-30271): SHIP at 024eb02
reviewer-session: d3e7a506-293c-474f-9146-6fb381853e16
author-session: a7414ded41eeb0207
R9: pass | regression tests come before their fixes (16ab06d before 6e7623f; 2af2e1a before 4894d35), the same order as #29530
perf claim: none
compile: 8a118e33eea4c8b2f33a6495 passed

Findings: none that block.

What I checked (read-only, against #29530's squash 82a70ea and the dogfood base a516f14):

  • Source diff vs fix(app): Ctrl-` right after Cmd-N opens its terminal; a create on a pane still opening waits for it (P0 STATUS 2080) #29530: the +/- lines are the same, except for the three port changes listed in the commits. (1) PaneController+Intents keeps this line's NewTerminalTrace.request wrapper and resolves daemonTarget inside it. (2) withOverlayLifted converts _ = removeDue(...) to the loop that calls noteIntentLeft (this line has no intentSettlements or onIntentSettled). releaseStandInTargets() now runs on every lift, not after the provisional-panes guard. resolveStandInWaits() runs before this line's resumeIdleWaits(). (3) StandInWait moved to its own file and uses OnceContinuation.
  • settleIntent and noteIntentLeft run inside the lift, as on feat. withOverlayLifted has the same call sites on both lines, and intentLog.remove/removeDue are the only ways an intent leaves the log. Every settlement, rejection and apply therefore reaches resolveStandInWaits.
  • Callers of the now-async daemonTarget: NewColumn, NewRow, NewTerminalTab and the optimistic split (@MainActor async closures awaited inside the isolated DaemonConnection body; the connection actor is not held while it waits), PaneSplitCommand.send (now @MainActor; its only caller is a Task in PaneHandlers, which is main-actor) and the typed new-tab path in PaneController. No sync caller remains.
  • OnceContinuation: each wait is taken out of standInWaits before it is resumed, so it is resumed at most once. With .cancellable, a cancelled create throws CancellationError at once. The entry stays until the stand-in's create ends, and that later resume is a fault-free no-op. A task cancelled before the wait is handled by OnceContinuationCancelTarget.attach. A dropped store throws .abandoned instead of hanging. The end result matches feat (CancellationError, request never sent, so R3 is not affected); it only comes sooner.
  • No deadlock: the stand-in's own create (Cmd-N, a split) does not wait on the waiting create. Command tickets and idle waits do not block the settlement.
  • Tests: the new files are the same as feat's except pendingIntents reads .intent from this line's (transaction, intent) pairs. The dogfood-only aPersonsConfigCommandOnAPendingPaneOpensInItsDaemonPane has the same shape as the ported typed-tab test.
  • Main thread: the wait is an async suspension with no timer. resolveStandInWaits returns at once when nothing waits. No try!, no motion and no dialog code touched.

Notes (not blocking):

  • Observed / Not run: the evidence is compile only. The ported tests (CmdNThenNewTerminalTabTests, CmdNStandInTargetTests, PaneVerbOnPendingPaneTests) were not run on this line on the fleet. Run them before the dogfood cut.
  • The port-only cancellable wait (3179dcc) has no dedicated test.
  • On the typed new-tab path, NewTerminalTrace's request_sent stage now also includes the wait for a stand-in's daemon pane. This affects the trace only.

…ys (held, or sent to the Ctrl-` terminal at once)

Green 06886ca01b09669e601a82d7 at 024eb02: CmdNThenNewTerminalTabTests:171 held 0 of
10, while the same test's checks after it passed (no failure, the hold ended .focusLanded,
the focused tab is the new terminal). On this line (107f5da, 050651b,
EarlyTerminalKeysTests) a key typed while a new terminal's early view has the keyboard is
not held: CreationInputCoordinator.capture returns false and AppKit sends the key to that
view, which sends it by the minted terminal id (or by the attachment once the daemon does
not know the id). feat's coordinator holds it instead. The assertion now counts a key as
the new terminal's when it is held, or when it is not dropped and the window's first
responder is the Ctrl-` stand-in terminal (earlyDeliveryState: takesKeysEarly true). Any
other key fails the test with its path (dropped flag, first responder, hold state).
Test-only; product code unchanged.
…hows the typed line (end-result oracle)

Lead 08:57Z: the pass condition is what the person sees, not an internal path. Each key of
"echo held\r" goes through the app's dispatch (KeyRouter.interceptKeyDown, then the window
for a key it does not consume, as CmuxApplication.sendEvent does). After the create settles,
the new terminal's screen (read-screen) must show `echo held` and the shell's answer `held`
(all ten keys, in order, Return included), within 30 s, and no other terminal's screen may
show `echo held`. How each key went (consumed or to which responder, the hold count) is in
the failure text only. Known limit: a key the hold keeps is replayed through
NSApp.sendEvent, which a test (never the key window) does not deliver; such a key would fail
this test and show as "consumed" in its paths. Test-only; product code unchanged.
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (opus-review-30271-test): SHIP at 2afdba7
reviewer-session: 6500fbcf-6e31-4cd8-a3ee-c468522e851a
author-session: a7414ded41eeb0207
R9: pass | regression tests come before their fixes (16ab06d before 6e7623f; 2af2e1a before 4894d35). 5aeb4d5 and 2afdba7 change only the test. Their red evidence is the red ref eefa578 (head's tests on the dogfood tip's Sources), step 2bf3cf539a5b304dc854eb8a, still running.
perf claim: none
compile: b5b969afcc70d7358f3b8a90 passed

Findings: none that block.

What I checked (read-only, whole diff a516f14..2afdba7, with extra care on 024eb02..2afdba7):

  • Sources are unchanged since the SHIP at 024eb02. The two new commits touch only CmdNThenNewTerminalTabTests.swift.
  • The author's claim is true on this line. In CreationInputCoordinator.capture (Focus/CreationInputCoordinator.swift:300-309), when takesKeysEarly is true (focus is on the newest early tab, its pane shows it, and the first responder is that terminal's view), it runs deliverEarly and returns false. The key then goes on to the window, so it is not held. In that case capture returns false and the feat check held == typed must fail (green 06886ca0: 0 of 10).
  • Key path for a key that is not held: TerminalSurfaceView -> DaemonTerminalIO.inputDrain, then CreatingTerminalInput.write by the minted terminal id. The Ctrl-create now waits indaemonTarget, so the first chunk can reach the daemon before the create registers the id. server/creating_input.rsthen answers "unknown terminal",writesetsdoneand returns false, and the drain gives the chunk todriver.input` (the attach queue) before the lane resumes (DaemonTerminalIO.swift:210-224). The later chunks go by the attachment. Order is kept and no chunk is dropped. A chunk that the daemon queued is written after the commit.
  • The new pass rule is the end result. The test reads the selected new terminal's screen (ReadScreenRequest). The screen must contain echo held and also a separate line that is exactly held. A lost, extra or swapped key breaks the contiguous echo held or the held output line. A lost Return gives no held line. A key that goes to the New Tab composer (typesIntoPrimaryInput consumes it) or to the old terminal never reaches the new terminal, so the test fails. Each other confirmed pty is also checked: it must not show echo held. The test keeps the feat checks: the hold is open in the Ctrl-turn, the hold ends by.focusLanded, and the focused tab is the confirmed new terminal. The test is therefore stricter than feat's mechanism count, not weaker. On the tip's Sources the Ctrl- is refused, so the test fails earlier, at failures.isEmpty and at the #require of the terminal.
  • Dispatch fidelity: interceptKeyDown, then window.sendEvent for a key that is not consumed, is the same as CmuxApplication.route -> super.sendEvent -> ShellWindow.sendEvent -> first responder. Only the latency slot and the PerfLab wrapper are left out, and they do not affect routing. HomePendingTerminalFocusTests uses the same pattern.
  • A key that the hold keeps is replayed with NSApp.sendEvent (HeldKeyReplay.Sender.application). That goes to the key window, and a test window is never key, so the key would be lost. This can only give a false red, never a false pass. Any such key shows as consumed (... held N) in the failure text.

Notes (not blocking):

  • Observed / Not run: I did not run anything. Green ba18fd57d338ad5d7c6b801c and red 2bf3cf539a5b304dc854eb8a were running when I checked. The SHIP depends on green passing that test. If green fails with consumed (held N) paths, the cause is the false-red case above, not the P0.
  • The test name still says "AreHeld", but on this line the keys are not held. The doc comment explains this. A rename is optional.
  • Existing risk on this line, not changed here: CreatingTerminalInput.write treats DaemonError.timedOut as delivered. A chunk that times out while the daemon is busy, and that the daemon later refuses as an unknown id, is lost. Now that the Ctrl-` create waits for Cmd-N, the first chunk usually reaches an id the daemon does not know yet. That case is safe only while the daemon answers within the request timeout.

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/CmdNThenNewTerminalTabTests.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/CmdNThenNewTerminalTabTests.swift:211">
P2: This assertion treats a failed screen read as an empty screen, so the test can pass without checking whether another terminal received the command. Propagate the request error so the test fails instead of silently accepting an unverifiable result.</violation>
</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

// New Tab page's composer had none of them either once the new terminal shows all ten).
for other in store.panesByHandle.values.flatMap(\.tabs) where other.kind == .pty && other.surface != terminal
&& !ProvisionalTab.isProvisional(surface: other.surface) {
let text = (try? await connection.request(ReadScreenRequest(surface: other.surface), timeout: .seconds(2)).text) ?? ""

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: This assertion treats a failed screen read as an empty screen, so the test can pass without checking whether another terminal received the command. Propagate the request error so the test fails instead of silently accepting an unverifiable result.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/CmdNThenNewTerminalTabTests.swift, line 211:

<comment>This assertion treats a failed screen read as an empty screen, so the test can pass without checking whether another terminal received the command. Propagate the request error so the test fails instead of silently accepting an unverifiable result.</comment>

<file context>
@@ -199,6 +190,27 @@ struct CmdNThenNewTerminalTabTests {
+            // New Tab page's composer had none of them either once the new terminal shows all ten).
+            for other in store.panesByHandle.values.flatMap(\.tabs) where other.kind == .pty && other.surface != terminal
+                && !ProvisionalTab.isProvisional(surface: other.surface) {
+                let text = (try? await connection.request(ReadScreenRequest(surface: other.surface), timeout: .seconds(2)).text) ?? ""
+                #expect(!text.contains("echo held"), "terminal \(other.surface) also got the typed line: \(text.debugDescription)")
+            }
</file context>
Suggested change
let text = (try? await connection.request(ReadScreenRequest(surface: other.surface), timeout: .seconds(2)).text) ?? ""
let text = try await connection.request(ReadScreenRequest(surface: other.surface), timeout: .seconds(2)).text

…s: Cmd-N then Ctrl-` types into the new terminal

Judged smoke of dogfood v2.2.13-opt, round 2 step 16 (#30271): after Cmd-N, Ctrl-` and a
typed line, the line went to the New Tab page. Cmd-N opens the window's New Tab hold
(NewTabInputCoordinator.beginIncoming): every key waits for the new page's field. The
Ctrl-` (or Cmd-D, New Column, a browser tab) that followed opened its creation hold, but the
New Tab hold runs first in KeyRouter.interceptKeyDown and nothing ended it (its chord
release only fires with keys already held), so every later key still waited for the page
and went to its field when it was ready.

One seam for every creation that moves a window's focus: CreationInputCoordinator.begin ends
that window's New Tab hold once its own hold is open (NewTabInputCoordinator.creationTookKeys,
the same window lookup capture uses). The keys the New Tab hold kept run again through the key
router in typing order, so they go into the creation's hold first and reach the new terminal.
A script's creation (no view change allowed) opens no hold and ends none.
isHolding is the same check on one line (offsets the App lines).

(cherry picked from commit 6bf54eb)
…minal's keys in the creation hold, as feat does

Green 3f0893af4165fda1bde2f4ad at 673b6bf (with #30827's fix): the 10 keys typed after
Cmd-N then Ctrl-` now left the New Tab hold, but went to the Ctrl-` stand-in terminal's view
("to TerminalSurfaceView"), and the new terminal's screen stayed "sh-3.2$". On this line
(107f5da, 050651b) the creation hold gives a new terminal's keys to its early view,
which sends them to the daemon by the terminal id its create minted. With #29530 a create on
a stand-in pane (Cmd-N's first pane, a split in flight) waits for the daemon's pane before it
is sent, so the daemon does not know that id yet when the keys come. feat-cmux-next never gives
the optimistic Ctrl-` an early tab: its keys wait in the creation hold for the daemon's terminal.

DaemonService.queuesCreatingInput(on:) answers false for a stand-in target
(ProvisionalPane.isStandIn); Ctrl-` and Cmd-D use it for their early tab, so those keys wait in
the hold and run again once focus landed on the daemon's terminal. New Column and New Row are
unchanged (not covered by a test here).

Test seam (only this part of feat's HeldKeyReplay, #28436): CreationInputCoordinator.replay,
NSApp.sendEvent by default; the tests set their window's. Tests: the ported #29530 key test and
#30827's CmdNThenCreateKeysTests (Ctrl-` and Cmd-D) set it and keep Cmd-N's page hold open as a
person's page does; the pass condition is the new terminal's screen text.

@cubic-dev-ai cubic-dev-ai 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.

2 issues found across 7 files (changes from recent commits).

You’re at about 94% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Packages/macOS/CmuxNext/Sources/CmuxNextApp/Focus/CreationInputCoordinator.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Sources/CmuxNextApp/Focus/CreationInputCoordinator.swift:367">
P3: The new default for `replay` calls `NSApp.sendEvent` directly and ignores the `Sender` that `HeldKeyReplay` hands in. That breaks the seam's documented rule that a held key is sent only through the `Sender`. Current behavior is the same, but the send step is now outside the seam. Make the seam optional and fall back to `send.application(event)` so production keeps going through the `Sender`.</violation>
</file>

<file name="Packages/macOS/CmuxNext/Sources/CmuxNextDaemonService/DaemonService+Endpoint.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Sources/CmuxNextDaemonService/DaemonService+Endpoint.swift:41">
P1: New Column and New Row still bypass this target-aware check: `createInKeysTurn` uses `daemon.queuesCreatingInput` without the source pane. Pass that handle through and call `queuesCreatingInput(on:)`, otherwise keys typed while a stand-in target is resolving are sent before the daemon knows the terminal.</violation>
</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

/// flight) waits for the daemon's pane before it is sent (`DaemonStore.daemonTarget`, #29530), so the daemon does not know
/// its terminal id yet and refuses keys sent by it: those keys wait in the creation hold for the daemon's terminal, as
/// on feat-cmux-next (#30271: after Cmd-N then Ctrl-`, the line typed into the stand-in never reached the terminal).
public func queuesCreatingInput(on target: PaneID) -> Bool { queuesCreatingInput && !ProvisionalPane.isStandIn(target) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: New Column and New Row still bypass this target-aware check: createInKeysTurn uses daemon.queuesCreatingInput without the source pane. Pass that handle through and call queuesCreatingInput(on:), otherwise keys typed while a stand-in target is resolving are sent before the daemon knows the terminal.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextDaemonService/DaemonService+Endpoint.swift, line 41:

<comment>New Column and New Row still bypass this target-aware check: `createInKeysTurn` uses `daemon.queuesCreatingInput` without the source pane. Pass that handle through and call `queuesCreatingInput(on:)`, otherwise keys typed while a stand-in target is resolving are sent before the daemon knows the terminal.</comment>

<file context>
@@ -34,6 +34,12 @@ extension DaemonService {
+    /// flight) waits for the daemon's pane before it is sent (`DaemonStore.daemonTarget`, #29530), so the daemon does not know
+    /// its terminal id yet and refuses keys sent by it: those keys wait in the creation hold for the daemon's terminal, as
+    /// on feat-cmux-next (#30271: after Cmd-N then Ctrl-`, the line typed into the stand-in never reached the terminal).
+    public func queuesCreatingInput(on target: PaneID) -> Bool { queuesCreatingInput && !ProvisionalPane.isStandIn(target) }
+
     /// The input of a view whose terminal `terminalID` is still being created goes to this daemon
</file context>

replayedCount += 1
router.heldKeys.replay(event, heldBy: .creation, keyWindow: window, throughApplication: true) { $0.application(event) }
(replaying, replayedCount) = (true, replayedCount + 1)
router.heldKeys.replay(event, heldBy: .creation, keyWindow: window, throughApplication: true) { [replay] _ in replay(event, window, router) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: The new default for replay calls NSApp.sendEvent directly and ignores the Sender that HeldKeyReplay hands in. That breaks the seam's documented rule that a held key is sent only through the Sender. Current behavior is the same, but the send step is now outside the seam. Make the seam optional and fall back to send.application(event) so production keeps going through the Sender.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextApp/Focus/CreationInputCoordinator.swift, line 367:

<comment>The new default for `replay` calls `NSApp.sendEvent` directly and ignores the `Sender` that `HeldKeyReplay` hands in. That breaks the seam's documented rule that a held key is sent only through the `Sender`. Current behavior is the same, but the send step is now outside the seam. Make the seam optional and fall back to `send.application(event)` so production keeps going through the `Sender`.</comment>

<file context>
@@ -362,9 +363,8 @@ final class CreationInputCoordinator {
-        replayedCount += 1
-        router.heldKeys.replay(event, heldBy: .creation, keyWindow: window, throughApplication: true) { $0.application(event) }
+        (replaying, replayedCount) = (true, replayedCount + 1)
+        router.heldKeys.replay(event, heldBy: .creation, keyWindow: window, throughApplication: true) { [replay] _ in replay(event, window, router) }
         replaying = false
     }
</file context>

lawrencecchen added a commit that referenced this pull request Nov 1, 2026
…not the New Tab page (P0) (#30827)

* test(app): keys typed after Cmd-N then Ctrl-` (or Cmd-D) must reach the new terminal, not the New Tab page

Judged smoke of dogfood v2.2.13-opt, round 2 step 16 (#30271): after Cmd-N, Ctrl-` and a
typed line, the keys went to the New Tab page. CmdNThenCreateKeysTests types "echo held\r"
through the app's dispatch (KeyRouter.interceptKeyDown, then the window) right after Cmd-N
and Ctrl-` (and Cmd-D), with the daemon stopped, and passes only when the new terminal's
screen shows the line and the shell's answer, and no other terminal shows it.
PasswordManagerRouteTests.withApp takes a heldKeyReplay (a test window is never key).
Red expected on feat-cmux-next: the keys wait in Cmd-N's New Tab hold.

* fix(app): a creation pressed while a New Tab page opens takes the keys: Cmd-N then Ctrl-` types into the new terminal

Judged smoke of dogfood v2.2.13-opt, round 2 step 16 (#30271): after Cmd-N, Ctrl-` and a
typed line, the line went to the New Tab page. Cmd-N opens the window's New Tab hold
(NewTabInputCoordinator.beginIncoming): every key waits for the new page's field. The
Ctrl-` (or Cmd-D, New Column, a browser tab) that followed opened its creation hold, but the
New Tab hold runs first in KeyRouter.interceptKeyDown and nothing ended it (its chord
release only fires with keys already held), so every later key still waited for the page
and went to its field when it was ready.

One seam for every creation that moves a window's focus: CreationInputCoordinator.begin ends
that window's New Tab hold once its own hold is open (NewTabInputCoordinator.creationTookKeys,
the same window lookup capture uses). The keys the New Tab hold kept run again through the key
router in typing order, so they go into the creation's hold first and reach the new terminal.
A script's creation (no view change allowed) opens no hold and ends none.
isHolding is the same check on one line (offsets the App lines).

* test(app): Cmd-N's page hold keeps the keys while the page opens, so the base loses them as a person's does

Red 808acd14b268c72028550d5a at 9856f7f PASSED: the test's New Tab page never reports its
field ready, so on the base the page's incoming hold reached its 4 s bound and handed the keys
on to the new terminal. A person's page reports ready and takes them (smoke step 16). The test
sets incomingHoldLimit to 120 s, so the page's hold keeps its keys while the page opens, as it
does for a person until their page is ready. The pass condition is unchanged: the new terminal's
screen shows the typed line and its answer within 15 s.
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Status at head 28bcb1b (a7414ded41eeb0207): not ready, do not merge.

…nt, and a control line typed after the hold (#30271 diagnostics)

Green 37f96e2b9465316565e74dc5 at 28bcb1b: the creation hold kept all 10 keys and ended
.focusLanded, but the new terminal showed none of them. On failure the test now records, for each
held key at its replay: whether the key router consumed it, the window's first responder and
whether it is the focused pane's shown terminal view, the pane, its shown tab, the view's stand-in
gate and the terminal id its input goes by; and a control: "echo ctl" typed through the same
dispatch after the hold, with the screen it gives. The pass rule is unchanged.
…/p0-ctrlbt-after-cmdn-dogfood

# Conflicts:
#	Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/CmdNThenNewTerminalTabTests.swift
@lawrencecchen lawrencecchen changed the title P0 port: Ctrl-` right after Cmd-N opens its terminal (#29530) P0 port: keys typed after Cmd-N then Ctrl-` reach the new terminal (#30827, #29530 keys part) Nov 1, 2026
…rminal views can type

Diagnostic 12f65c19335cf44d44de0317 at 537cb91: every replayed key went to the new
terminal's own view (first responder, focus on its tab), and a control line typed after the
hold did not show either. PasswordManagerRouteTests.withApp starts no Ghostty runtime on this
line, so a terminal view has no surface and TerminalSurfaceView.keyDown sends nothing to the
PTY. TestHosts.liveDaemonApp (NewTerminalOnPendingSplitTests, which types the same way) calls
GhosttyRuntime.startedForTests(); CmdNThenCreateKeysTests and the #29530 key test now do too.
Test-only. The earlier screen-oracle failures of these tests on this line (37f96e2b, 3f0893af)
did not test the product.
…h Cmd-N key test (lead 17:3xZ)

On feat-cmux-next the Ghostty runtime starts on its first use (product code); on this line the
app starts it at launch, so PasswordManagerRouteTests.withApp, the harness these tests share,
now starts it the way this line's TestHosts.liveDaemonApp does (GhosttyRuntime.startedForTests).
The per-test calls of b3ac9cf go. Test-only.
… it is not needed

Probe 7ec3a8abe34e5cbc20966aae at 66e36b7 (this branch without that rule, with the Ghostty
runtime started in the test harness) passed all five key tests: CmdNThenCreateKeysTests 2/2
(Ctrl-` and Cmd-D after Cmd-N: the typed line shows in the new terminal, no other terminal shows
it) and CmdNThenNewTerminalTabTests 3/3. The failures that led to the rule (3f0893af, 37f96e2b)
came from the harness, whose terminal views had no Ghostty surface. So this line keeps its own
early-key path (107f5da, 050651b) unchanged: queuesCreatingInput(on:) and
ProvisionalPane.isStandIn go. The test replay seam CreationInputCoordinator.replay stays (the
default is NSApp.sendEvent, as before).
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (opus-review-30271-net): SHIP at 16891d1
reviewer-session: 5393e85c-6a22-4916-9317-89576d703a77
author-session: a7414ded41eeb0207
R9: pass | this is a port, so the test (CmdNThenCreateKeysTests, 28bcb1b) came after the cherry-picked fix (673b6bf) in the history. The red ref 95dbc94 has the same tests and harness as the head, and its Sources are the head minus the one creationTookKeys line (the dropped rule is only defined there, never called). Red bac8b631e19bb81a12332906 at that ref failed 2/2 on the screen check, with "New Tab hold true" on every key. Probe 7ec3a8abe34e5cbc20966aae at 66e36b7 (same Sources as the head) passed 5/5.
perf claim: none
compile: bc1c6d1a65cee7dc17ba0a9c passed; red bac8b631e19bb81a12332906 failed as expected; green 0f44c923195bc87faf36079e running when read (19:27Z, no test lines yet)

Findings: none in the code that block.

What I checked (read-only, net diff origin/cp/dogfood-v2-2-12-opt...HEAD, 5 files):

  • The port matches feat d7128e6 line for line. The diff is begin's defer { router?.newTabInputCoordinator.creationTookKeys(in: window) } and creationTookKeys, which does release(in: focus(for:).0?.window ?? window), plus the same one-line release(in:) and isHolding. The defer runs after the creation buffer is open. The New Tab entry is removed before its buffer replays. Its keys go through NewTabInputCoordinator.replay -> interceptKeyDown, where CreationInputCoordinator.capture holds them in typing order before any later key.
  • Only begin ends the New Tab hold, not beginWorkspace. This is correct. In newTab (AppActions+Workspaces.swift:166-174), beginIncoming and beginWorkspace cannot both run, because one needs opensPage and the other needs !opensPage. So a beginWorkspace release could never end its own action's page hold. An older Cmd-T page hold is already ended in two ways: the chord rule ends it when keys are pending, and Entry.owns ends it once its pane is no longer shown. feat has no beginWorkspace.
  • The callers of begin are PaneHandlers.split:132, newTerminalTab:133, newTerminalTabNow:183, createInKeysTurn:151 and PaneBrowserTabOpener.openStandIn:214. None of them opens a New Tab hold in the same turn before it calls begin. NewTabPage.open and NewTabPage.split call only begin(for:) and beginIncoming, never the creation begin. So a New Tab hold never ends at its own start. A browser stand-in opened from the page, or a mouse or menu creation, is a newer intent of the person. Its keys now go to the new tab, as on feat. A script without focus is not affected: viewChangeAllowed gives an early return before the defer. continueCreation does not exist on this line.
  • Cmd-Shift-N (followsNewWindow, found through entry(for:)): a creation in any window ends that hold. The keys are then delivered to pageWindow ?? keyWindow ?? window through the router. Usually that is the creation's hold. feat behaves the same, and the Ctrl-backtick/Cmd-D chord rule already gives the same result when keys are pending.
  • Replay seam: the default { event, _, _ in NSApp.sendEvent(event) } does the same as the old Sender.application(event), inside the same heldKeys.replay(..., throughApplication: true) stage. Product behavior does not change, and nothing in Sources sets the seam.
  • Harness: GhosttyRuntime.startedForTests() is idempotent and process-wide, and the app starts it at launch on this line. 27 suites use PasswordManagerRouteTests.withApp (ActionScopeHop, AgentChatFromHome, BrowserProfileNewTabMenu, CmdNSlowDaemonKeepsWorkspace, CmdNThenCreateKeys, CmdNThenNewTerminalTab, CmdTSpamKeepsNewTabs, HeadlessRenameTakesNoKeyboard, HomeChiefPaneHasNoTabBar, HomeDockNewTerminalFocus, HomeDockOpens, HomePendingTerminalFocus, HomeServiceObservation, HomeSplitKeys, InternalPageLazy, MarkdownOmnibarOpen, NewTabNeverATerminal, NewTabPreload, NewTabPrewarmStall, NewTabSparePageLaziness, NewTabSuccessionKeepsShownTabs, ParkedWorkspacePageTab, PasswordManagerRoute, ScriptPaneFocus, TerminalColorQuery, TerminalFirstNewTab, TopPageCoversWorkspaceFocus). Before this change, whether these suites had a started runtime depended on which suite ran first in the process. Now it is always the app's condition, so the change makes them closer to the app and fixed in result. Their meaning does not change. TerminalColorQuery already starts the runtime itself. The runtime config comes from the test process, not from home:, so it is not affected.
  • Tests check the end result. The new terminal's screen must contain the contiguous echo held and a separate line that is exactly held. No other confirmed pty may show echo held. The focused tab must be the confirmed new terminal. Each key goes through interceptKeyDown, then window.sendEvent. The diagnostic block runs only on failure. In the red run it showed the cause directly ("replays []", New Tab hold true).
  • prepush-gate.py over 1f0153f..16891d1: no new violations. One owner warning: KeyRouter+NewTabInput.swift is owned by PL-typing.

Notes (not blocking the code; do them before ready-to-land):

  • The PR body is out of date with the head. It lists 28bcb1b's rule as content (16891d1 dropped it). Its "STILL OPEN" item and its planned fix(app): each held key goes only to the terminal it was typed for (P0 1193, Cmd-D twice) #29301 port are no longer true. Its Testing section names the old red 4f718214 and the failed green 37f96e2b, not red bac8b631, probe 7ec3a8ab and green 0f44c923. Its Changelog and its "Fixes STATUS 3968" (the Ctrl-backtick itself held in a CefNSWindow) claim a change that the net diff and its tests do not cover, because the tests perform newSurface directly and send no Ctrl-backtick key. Rewrite the body to cover the net change only, with these runs and an Observed / Not run section.
  • CmdNThenNewTerminalTabTests.swift:128-136 (doc comment of keysTypedWhileCtrlBacktickWaitsForCmdNAreHeldForTheNewTerminal): "lost. Now they wait in the creation hold, as on feat" is no longer true. The rule that made them wait is dropped. On this line the keys can go early to the stand-in view by terminal id, and 16891d1 says the earlier losses came from the harness. The test also passed on red bac8b631, so it guards fix(app): Ctrl-` right after Cmd-N opens its terminal; a create on a pane still opening waits for it (P0 STATUS 2080) #29530 but is not a red/green for this fix. Fix the comment in a follow-up.
  • Not run by me: nothing was compiled or tested on this machine. Green 0f44c923 was still running. The SHIP depends on it passing the 6 suites. The other 25 withApp suites were not run at the head with the runtime started.

@lawrencecchen
lawrencecchen merged commit f48d38e into cp/dogfood-v2-2-12-opt Nov 1, 2026
63 of 69 checks passed
@lawrencecchen
lawrencecchen deleted the fix/p0-ctrlbt-after-cmdn-dogfood branch November 1, 2026 20:12
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.

1 participant