Skip to content

fix(app): a script's new workspace does not take the person's keys from Home's composer - #30071

Open
lawrencecchen wants to merge 9 commits into
feat-cmux-nextfrom
perfx/composer-focus-2
Open

lawrencecchen wants to merge 9 commits into
feat-cmux-nextfrom
perfx/composer-focus-2

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

Summary

With Home showing, keys typed for a new terminal could land in Home's Chief message box, and Return sent the shell command to the Chief as a chat message (dogfood v2.2.3, P0 journey scripted-newtab-keeps-person-keys). A script's newTab with focus could also take the window from a person typing in a field. Both are fixed:

  • The person's own new terminal holds their keys until it shows. A terminal-first newTab from the person (menu, palette, key), shown in the window they type in, opens a key hold (CreationInputCoordinator.beginWorkspace). Keys wait until the window shows that workspace. If the create fails or the workspace never shows, those keys are dropped, never typed into whatever has the focus.
  • The window's keyboard stays the new terminal's until the person's next intent. That is a click, Escape, a focus move by mouse, keyboard or palette, or another of their actions (KeyboardClaims). A timeout never ends it, and neither does automation. While the claim holds, no primary input takes the keyboard by itself (NSWindow.pendingCreationOwnsKeyboard), and keys that run no binding are kept from the window.
  • A script never takes the person's window. A script's, the CLI's, MCP's or the socket's newTab with focus is created without view change. Once it exists, the window shows it only if no text input has the person's keyboard in the active key window (WindowController.hasTextInputKeyboard: an editable text view or field, an input method composing, or an editable page element). A person who runs cmux open --focus from a terminal or from another app still gets the workspace shown.
  • Who asked is the run's own origin. A script's nested newTab, built with the default user invocation, opens no New Tab page and no key hold.

Created but not shown (open behavior note, big-changes.md). When a script's new workspace arrives while the person types in a field, the workspace is created and listed in the sidebar, and the window stays where the person is typing. The check runs when the workspace exists, not when the script asks for it, so a person who started typing meanwhile keeps their input. This PR adds no badge or toast for a workspace that is listed but not shown. Its sidebar row is the only sign. That stays an open UX question.

Follow-up (lead decision). If the person's own claimed create fails, this PR keeps the claim until their next intent, so keys typed meanwhile are kept from the window and nothing says why (review finding 1). A follow-up PR, red first, ends the claim when its own create fails and shows the failure through the create-failure notice, which says the typed keys were not delivered. Keys held for that create still never reach an agent's composer.

Ported from cp/fix-chief-composer-focus (perf merge 943ac39), changed to fit feat's code. Feat has no KeyRouter modes, so the hold is read from CreationInputCoordinator.isHolding(in:).

App size. CmuxNextApp shrinks by 27 lines over the PR. The new types are in CmuxNextFocusUI (KeyboardClaims, the new-workspace tickets of CreationInputBuffer, NSWindow.firstResponderTypesText) and CmuxNextFocus (FocusEvent.isPersonsOwn). Commit b1fc9a3 only moves five focus and key files (DecidedKeyEvents, ShortcutHintModifierPolicy, PaneNavigationHintPlan, BrowserFocusModeEscapeExit, NSResponder.ownsTypedText) from CmuxNextApp to CmuxNextFocusUI. Its only edits are the public access words and the public inits that a move across modules needs. So the fix commit can be read on its own.

Owner note. Windows/WindowController.swift is in the PL-switch area. The change is 4 lines: the focus coordinator's personIntent hook and ending the window's claim in teardown(). No switch path changes.

Commits: c65b87a (red test), b1fc9a3 (move only), bd887fa (fix), af500ef (test only: the keys test closes the Home's Chief Settings popover panel it made).

Testing

  • Red: HomePendingTerminalFocusTests on c65b87a (feat plus the test), fleet step 9324962aac1d94d400bb65c2 on aws-m4pro-9, CMUX_TEST_REQUIRE_GUI=1: failed with the tests run, showing the P0. keysTypedForAPersonsNewTerminalOverHomeNeverReachTheChiefMessageBox found no hold, Home's message box took the keyboard, and "echo ok" went into the Chief box.
  • Green, first run: the same 3 suites on bd887fa, fleet step f28ce3bdca7354ac9eaa15b5 on aws-m4pro-7. Every test passed (HomePendingTerminalFocusTests 5/5, PersonTerminalKeyboardClaimTests 8/8, CreationInputHoldTests 6/6, 0 skipped). The suite failed only on the window-leak check: the keys test left the Home's Chief Settings NativePopupPanel open. af500ef closes it in the test's teardown. The leak check is unchanged. Green rerun on af500ef: fleet step 2549eaa093d29b23b5c0d47b on aws-m4pro-2, CMUX_TEST_REQUIRE_GUI=1: all 3 suites passed (HomePendingTerminalFocusTests, PersonTerminalKeyboardClaimTests, CreationInputHoldTests), 0 skipped, no window leak, no FLAKY, exit 0.
  • Compile: cmux-ci compile-check --app --package CmuxNext --recipe-version 2 on bd887fa, job 7c2baf4ef55ccb7ebbec6281: CmuxNext, app and cmux-tui ok. On af500ef: job 706ef390c8e5167d9d836c9e: CmuxNext, app and cmux-tui ok.
  • HomePendingTerminalFocusTests needs this tree's cmux-tui. It used to skip without it (skips audit row 3). Its suite trait now also runs it when CMUX_TEST_REQUIRE_GUI=1, so a run without the binary fails at withApp's #require(SameTreeDaemonBinary.url) and does not skip. package-test-lane.sh already counts a "needs the same-tree cmux-tui" skip as no result.

Observed / Not run

  • Observed (display, at the PR head 7688e8b): fleet step 899cd57b1887b17c35461ea7 on a gui-session-v1 worker (cmuxs-Mac-mini), in cmux's console session (launchctl asuser 501), CMUX_TEST_REQUIRE_GUI=1: HomePendingTerminalFocusTests 5/5, PersonTerminalKeyboardClaimTests 8/8, CreationInputHoldTests 7/7, SecondCreationKeysTests 11/11. All 4 suites passed, exit 0 (log read with cmux-ci log; added by the ready-queue unblocker).
  • Observed: the compile job, the red step and the first green step above (logs read with cmux-ci log --full), and prepush-gate.py ("no new violations", app_lines -27).
  • Observed: the green rerun and the compile on af500ef (logs read).
  • Not run: a P0 journey or display run of scripted-newtab-keeps-person-keys on a feat build, and a live check in a tagged build. No perf claim.

Changelog

Fixed: Keys typed for a new terminal while Home shows no longer reach Home's message box, and a script's new workspace no longer takes the window from a person typing in a field

Proof

No capture: keyboard routing and focus behavior with no visual change. The evidence is the fleet test steps above.

…ch the Chief message box; a script's new terminal never takes a typing person's keys (red)

Ported from cp/fix-chief-composer-focus (4aefbf6, 228e8b0, c3b322c,
abe55d9, f9ec8a6; perf merge 943ac39), adapted to feat-cmux-next:
feat has no KeyRouter modes, so the hold is read from
CreationInputCoordinator.isHolding(in:).

Red on feat: a person's terminal-first newTab opens no key hold over Home, a
script's newTab with focus shows over a person typing in a field, and a
script's nested newTab opens the New Tab page hold.
…uxNextFocusUI

A move only, so the fix after it reads alone. DecidedKeyEvents (key
dispatch), ShortcutHintModifierPolicy and PaneNavigationHintPlan (the
modifier-hold shortcut hints, beside FocusUI's hint overlay and chip fader),
BrowserFocusModeEscapeExit (the focus-mode Escape gesture) and
NSResponder.ownsTypedText (who has the keyboard) are focus code. The only
edits are the access words a move across modules needs: `public` on the
types and members the App and its tests use, and public memberwise inits.
No logic changes. CmuxNextApp reaches them through its existing
`@_exported import CmuxNextFocusUI` (FocusUIExport.swift).
…om Home's composer

Dogfood v2.2.3 (P0 journey scripted-newtab-keeps-person-keys): with Home
showing, keys typed for a new terminal went into Home's Chief message box and
Return sent the shell command to the Chief as a message. A script's newTab
with focus also took the window from a person typing in a field.

- A person's terminal-first newTab shown in the window they type in opens a
  key hold (CreationInputCoordinator.beginWorkspace) that waits until the
  window shows that workspace; keys typed for a workspace that fails or never
  shows are dropped, never typed into what has the focus.
- The window's keyboard stays claimed (KeyboardClaims) until the person's next
  intent there: a click, Escape, a focus move by mouse, keyboard or palette,
  or another of their actions. Never a timeout, never automation. While the
  claim holds, no primary input takes the keyboard by itself
  (NSWindow.pendingCreationOwnsKeyboard) and keys that run no binding are kept
  from the window.
- A script's, the CLI's, MCP's or the socket's newTab with focus is created
  and listed in the sidebar without view change. Once it exists, the window
  shows it only if no text input has the person's keyboard then
  (WindowController.hasTextInputKeyboard: an editable text view or field, an
  input method composing, or an editable page element, in the active key
  window). Otherwise it stays listed and not shown.
- The origin is the run's own (a nested run keeps its outer script origin):
  a script's nested newTab opens no New Tab page and no key hold.

Ported from cp/fix-chief-composer-focus (f664686, 23248fd,
2be9a0c, a4c01b0, 8072355; perf merge 943ac39). The new code
lives in CmuxNextFocusUI (KeyboardClaims, the CreationInputBuffer workspace
tickets, NSWindow.firstResponderTypesText) and CmuxNextFocus
(FocusEvent.isPersonsOwn). The move commit before this one
moved five focus files out of CmuxNextApp, so the App does not grow over
the PR (app-size rule).
@lawrencecchen lawrencecchen added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Nov 1, 2026
@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

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

Review 9923d35: 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=pr30071 -f ref=perfx/composer-focus-2 -f platform=macos -f backend_mode=local

red: cmux-next checks (god files, concurrency, crash safety, l10n); base red: the same failure on the base 6eb6839; does not block

Default changes

This pull request changes 5 defaults at 9923d35519e4 (what everyone gets without configuring anything):

  • tunable-source.CmuxNextApp/AppActions+Workspaces.swift:newWorkspace: "980fd5e1935e0d748560c4ad61718bbc57a944247976d2c9d32d04fd9ce5d821" -> "8883dc0ba4591cc30cc9923ad8cb115a461626fd98a2567f14c8884c7d283cd2"
  • tunable-source.CmuxNextApp/AppActions.swift:bind: "811e82d35056290db891eb3387712ae6004b1ffc5aaf6d93b086f9f007f282b9" -> "be3e474308722e3a2641c8298b4aaa07e8f297572179067014929cdec6635325"
  • tunable-source.CmuxNextApp/AppActions.swift:bindApp: "fe3856eefe1e399a840d6a5c4f2cdb45be0f2e0372ba0ec425e8f739fab1ffef" -> "9b41159d4d9872003f6eb5df297fc5608a7aacccaec7588e6534369ea67fc21e"
  • tunable-source.CmuxNextApp/Focus/CreationInputCoordinator.swift:installPersonIntent: (none) -> "f309303b210dd730a101c5a77bfd316821e65af00562b940d009c08208781132"
  • tunable-source.CmuxNextApp/WorkspaceCreation.swift:opensPage: (none) -> "4915456288086f8ab284920b074eece02dcb51f5707c5c9eed78a62b7d9c5eb7"

Needs a review. It passes with an independent review receipt on this commit: a comment or review whose first line is Review (<reviewer>): SHIP at 9923d35519e411f4e4c3cf74ac2b81b339fbf95c, from a member, not a self-review (the newest verdict naming the head decides; a later BLOCK beats an earlier SHIP), or with an approving review on this commit from @austinywang or @teamleaderleo (the call owners in .github/defaults-call-owners.json, never the author). A receipt posted as a review re-runs this check; after a plain comment, re-run it. The needs a call label alone does not pass, and a new push needs a new receipt.

Regenerate the snapshot with python3 scripts/ci/defaults_guard.py snapshot.

@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.

7 issues found across 19 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/Sources/CmuxNextApp/AppServices.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Sources/CmuxNextApp/AppServices.swift:742">
P2: This click handler ends the new terminal's keyboard claim for any mouse-down, including clicks the app posts for automation (`debug.mouse`, agent pane clicks). Skip the call when `currentEventIsSynthetic` is true, as the notifications call on the line above already does.</violation>
</file>

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

<violation number="1" location="Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/HomePendingTerminalFocusTests.swift:228">
P2: The result of the script's nested `newTab` is discarded, so `!scriptHolds` passes even when the action never runs. Assert the returned Bool, as the control call does with `#expect(control)`.</violation>
</file>

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

<violation number="1" location="Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/PersonTerminalKeyboardClaimTests.swift:146">
P3: This test does not check its precondition or the action's result. Assert that the claim holds before the action and that `perform` returned true, so the final `!claimed` check actually tests the claim ending.</violation>

<violation number="2" location="Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/PersonTerminalKeyboardClaimTests.swift:158">
P2: This test has no positive control. It discards the result of `perform("newTab")` and asserts only that no hold or claim exists, so it still passes if `newTab` never runs. Assert that the perform call returned true before checking the negatives.</violation>
</file>

<file name="Packages/macOS/CmuxNext/Sources/CmuxNextFocusUI/NSResponder+OwnsTypedText.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Sources/CmuxNextFocusUI/NSResponder+OwnsTypedText.swift:27">
P2: The IME branch of `firstResponderTypesText` skips the visibility check that `ownsTypedText` applies. A hidden field that still holds the keyboard with uncommitted marked text counts as typing, so a script's workspace is not shown. Apply the same window and hidden checks before the `hasMarkedText()` test.</violation>

<violation number="2" location="Packages/macOS/CmuxNext/Sources/CmuxNextFocusUI/NSResponder+OwnsTypedText.swift:27">
P2: This only recognizes custom input surfaces while an IME preedit is active, so ordinary typing in `RemoteInputCaptureView` is treated as having no text-input keyboard and an automated `newTab` can steal focus. Include active custom keyboard owners in the ownership check, while retaining the intentional terminal exception.</violation>
</file>

<file name="Packages/macOS/CmuxNext/Sources/CmuxNextFocusUI/CreationInputBuffer.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Sources/CmuxNextFocusUI/CreationInputBuffer.swift:100">
P1: A key arriving after a failed workspace reply but before the scheduled flush is captured as a normal post-creation key and replayed into the current responder, so it can still reach Home's composer. Keep the failed workspace ticket marked unshown until the buffer is flushed, so `take(shown:)` drops keys captured in this gap.</violation>
</file>

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

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

if awaitsFocus, let workspace {
madeWorkspaces[ticket.id] = workspace
} else {
workspaceTickets.remove(ticket.id)

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: A key arriving after a failed workspace reply but before the scheduled flush is captured as a normal post-creation key and replayed into the current responder, so it can still reach Home's composer. Keep the failed workspace ticket marked unshown until the buffer is flushed, so take(shown:) drops keys captured in this gap.

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/CmuxNextFocusUI/CreationInputBuffer.swift, line 100:

<comment>A key arriving after a failed workspace reply but before the scheduled flush is captured as a normal post-creation key and replayed into the current responder, so it can still reach Home's composer. Keep the failed workspace ticket marked unshown until the buffer is flushed, so `take(shown:)` drops keys captured in this gap.</comment>

<file context>
@@ -68,19 +84,29 @@ public final class CreationInputBuffer {
+        if awaitsFocus, let workspace {
+            madeWorkspaces[ticket.id] = workspace
+        } else {
+            workspaceTickets.remove(ticket.id)
+            events.removeAll { $0.creation == ticket.id }
+        }
</file context>

self?.linkHints.cancel()
self?.windowHints.cancel()
self?.keyRouter.cancelChord()
self?.keyRouter.creationInputCoordinator.personActed(in: event.window, event: event) // a click: the person's next intent

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 click handler ends the new terminal's keyboard claim for any mouse-down, including clicks the app posts for automation (debug.mouse, agent pane clicks). Skip the call when currentEventIsSynthetic is true, as the notifications call on the line above already does.

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/AppServices.swift, line 742:

<comment>This click handler ends the new terminal's keyboard claim for any mouse-down, including clicks the app posts for automation (`debug.mouse`, agent pane clicks). Skip the call when `currentEventIsSynthetic` is true, as the notifications call on the line above already does.</comment>

<file context>
@@ -739,6 +739,7 @@ final class AppServices {
             self?.linkHints.cancel()
             self?.windowHints.cancel()
             self?.keyRouter.cancelChord()
+            self?.keyRouter.creationInputCoordinator.personActed(in: event.window, event: event) // a click: the person's next intent
         }
         (NSApp as? CmuxApplication)?.keyDownObserver = { [weak self] _ in self?.notifications.endViewHolds() }
</file context>
Suggested change
self?.keyRouter.creationInputCoordinator.personActed(in: event.window, event: event) // a click: the person's next intent
if (NSApp as? CmuxApplication)?.currentEventIsSynthetic != true { self?.keyRouter.creationInputCoordinator.personActed(in: event.window, event: event) } // a click: the person's next intent (not the app's own posted input)

Comment on lines +228 to +231
_ = ActionRunScope.carrying(script) {
services.registry.perform("newTab", invocation: ActionInvocation(arguments: ["name": .string("scripts-page")], origin: .user))
}
let scriptHolds = input.isHolding(in: shell)

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 result of the script's nested newTab is discarded, so !scriptHolds passes even when the action never runs. Assert the returned Bool, as the control call does with #expect(control).

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/HomePendingTerminalFocusTests.swift, line 228:

<comment>The result of the script's nested `newTab` is discarded, so `!scriptHolds` passes even when the action never runs. Assert the returned Bool, as the control call does with `#expect(control)`.</comment>

<file context>
@@ -0,0 +1,235 @@
+            try await PasswordManagerRouteTests.until({ !input.isHolding(in: shell) }, within: .seconds(30), report: { "the person's page hold never ended: \(input.log)" })
+
+            let script = ActionRunScope(origin: .script, allowsViewChange: true)
+            _ = ActionRunScope.carrying(script) {
+                services.registry.perform("newTab", invocation: ActionInvocation(arguments: ["name": .string("scripts-page")], origin: .user))
+            }
</file context>
Suggested change
_ = ActionRunScope.carrying(script) {
services.registry.perform("newTab", invocation: ActionInvocation(arguments: ["name": .string("scripts-page")], origin: .user))
}
let scriptHolds = input.isHolding(in: shell)
#expect(ActionRunScope.carrying(script) {
services.registry.perform("newTab", invocation: ActionInvocation(arguments: ["name": .string("scripts-page")], origin: .user))
})

try await Self.withWindow { h in
let request = ActionInvocation(arguments: ["command": .string("/bin/sh"), "name": .string("nested")], origin: .user)
let script = ActionRunScope(origin: .script, allowsViewChange: true)
_ = ActionRunScope.carrying(script) { h.services.registry.perform("newTab", invocation: request) }

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 test has no positive control. It discards the result of perform("newTab") and asserts only that no hold or claim exists, so it still passes if newTab never runs. Assert that the perform call returned true before checking the negatives.

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/PersonTerminalKeyboardClaimTests.swift, line 158:

<comment>This test has no positive control. It discards the result of `perform("newTab")` and asserts only that no hold or claim exists, so it still passes if `newTab` never runs. Assert that the perform call returned true before checking the negatives.</comment>

<file context>
@@ -0,0 +1,208 @@
+        try await Self.withWindow { h in
+            let request = ActionInvocation(arguments: ["command": .string("/bin/sh"), "name": .string("nested")], origin: .user)
+            let script = ActionRunScope(origin: .script, allowsViewChange: true)
+            _ = ActionRunScope.carrying(script) { h.services.registry.perform("newTab", invocation: request) }
+            let holding = h.services.keyRouter.creationInputCoordinator.isHolding(in: h.window)
+            let claimed = h.window.pendingCreationOwnsKeyboard
</file context>
Suggested change
_ = ActionRunScope.carrying(script) { h.services.registry.perform("newTab", invocation: request) }
let ran = ActionRunScope.carrying(script) { h.services.registry.perform("newTab", invocation: request) }
#expect(ran, "the nested newTab did not run, so the negatives below prove nothing")

@MainActor public func firstResponderTypesText(excluding excluded: (NSResponder) -> Bool) -> Bool {
guard let responder = firstResponder else { return false }
if responder.ownsTypedText { return true }
return !excluded(responder) && (responder as? any NSTextInputClient)?.hasMarkedText() == true

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 IME branch of firstResponderTypesText skips the visibility check that ownsTypedText applies. A hidden field that still holds the keyboard with uncommitted marked text counts as typing, so a script's workspace is not shown. Apply the same window and hidden checks before the hasMarkedText() 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/Sources/CmuxNextFocusUI/NSResponder+OwnsTypedText.swift, line 27:

<comment>The IME branch of `firstResponderTypesText` skips the visibility check that `ownsTypedText` applies. A hidden field that still holds the keyboard with uncommitted marked text counts as typing, so a script's workspace is not shown. Apply the same window and hidden checks before the `hasMarkedText()` test.</comment>

<file context>
@@ -0,0 +1,29 @@
+    @MainActor public func firstResponderTypesText(excluding excluded: (NSResponder) -> Bool) -> Bool {
+        guard let responder = firstResponder else { return false }
+        if responder.ownsTypedText { return true }
+        return !excluded(responder) && (responder as? any NSTextInputClient)?.hasMarkedText() == true
+    }
+}
</file context>
Suggested change
return !excluded(responder) && (responder as? any NSTextInputClient)?.hasMarkedText() == true
guard !excluded(responder), let view = responder as? NSView, view.window != nil, !view.isHiddenOrHasHiddenAncestor else { return false }
return (responder as? any NSTextInputClient)?.hasMarkedText() == true

@MainActor public func firstResponderTypesText(excluding excluded: (NSResponder) -> Bool) -> Bool {
guard let responder = firstResponder else { return false }
if responder.ownsTypedText { return true }
return !excluded(responder) && (responder as? any NSTextInputClient)?.hasMarkedText() == true

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 only recognizes custom input surfaces while an IME preedit is active, so ordinary typing in RemoteInputCaptureView is treated as having no text-input keyboard and an automated newTab can steal focus. Include active custom keyboard owners in the ownership check, while retaining the intentional terminal exception.

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/CmuxNextFocusUI/NSResponder+OwnsTypedText.swift, line 27:

<comment>This only recognizes custom input surfaces while an IME preedit is active, so ordinary typing in `RemoteInputCaptureView` is treated as having no text-input keyboard and an automated `newTab` can steal focus. Include active custom keyboard owners in the ownership check, while retaining the intentional terminal exception.</comment>

<file context>
@@ -0,0 +1,29 @@
+    @MainActor public func firstResponderTypesText(excluding excluded: (NSResponder) -> Bool) -> Bool {
+        guard let responder = firstResponder else { return false }
+        if responder.ownsTypedText { return true }
+        return !excluded(responder) && (responder as? any NSTextInputClient)?.hasMarkedText() == true
+    }
+}
</file context>

let ticket = try #require(coordinator.beginWorkspace(in: h.controller))
coordinator.resolve(ticket, workspace: nil, in: h.window)
try await PasswordManagerRouteTests.until({ !coordinator.isHolding(in: h.window) }, report: { "the hold never ended" })
_ = h.services.registry.perform("toggleSidebar", invocation: ActionInvocation(origin: .user))

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: This test does not check its precondition or the action's result. Assert that the claim holds before the action and that perform returned true, so the final !claimed check actually tests the claim ending.

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/PersonTerminalKeyboardClaimTests.swift, line 146:

<comment>This test does not check its precondition or the action's result. Assert that the claim holds before the action and that `perform` returned true, so the final `!claimed` check actually tests the claim ending.</comment>

<file context>
@@ -0,0 +1,208 @@
+            let ticket = try #require(coordinator.beginWorkspace(in: h.controller))
+            coordinator.resolve(ticket, workspace: nil, in: h.window)
+            try await PasswordManagerRouteTests.until({ !coordinator.isHolding(in: h.window) }, report: { "the hold never ended" })
+            _ = h.services.registry.perform("toggleSidebar", invocation: ActionInvocation(origin: .user))
+            let claimed = h.window.pendingCreationOwnsKeyboard
+            #expect(!claimed, "the claim outlived the person's next action")
</file context>
Suggested change
_ = h.services.registry.perform("toggleSidebar", invocation: ActionInvocation(origin: .user))
#expect(h.window.pendingCreationOwnsKeyboard, "no claim before the person's action")
#expect(h.services.registry.perform("toggleSidebar", invocation: ActionInvocation(origin: .user)), "toggleSidebar did not run")

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (review-agent-composer-focus): SHIP at bd887fa
reviewer-session: 28b60fe3-5897-49a4-a449-9357bbf1c35a
author-session: a1121df0cfe085bf2
R9: pass. The PR adds no new cap, queue or deadline. The key hold uses the existing 2 s holdLimit. The keyboard claim has no timeout (by design).
perf claim: none

Scope: git diff cba94d55a6a4 bd887fafd57, read in full, plus the code it calls (WorkspaceSpawner, WorkspaceLanes, WindowManager.claimNew/claim, FocusCoordinator.send/currentState, KeyRouter.interceptKeyDown, ActionRegistry.runScoped). I did not build or run tests. Compile job 7c2baf4ef55ccb7ebbec6281 passed (CmuxNext, app, cmux-tui). At review time, red step 9324962aac1d94d400bb65c2 (c65b87a) and green step f28ce3bdca7354ac9eaa15b5 (bd887fa) were both still running (aws-m4pro-9 and aws-m4pro-7). I did not read their results.

What I checked

  • Move commit b1fc9a3 is a move only. It renames five files and makes only these edits: public, public import and public memberwise or empty inits. Both targets use uiSwiftSettings (MainActor by default), so isolation does not change. The logic does not change.
  • Success path: the claim does not swallow the new terminal's keys. The workspace id is the locally made WorkspaceKey, which is the same string as FocusTopology.workspace. Show before the reply: resolve calls noteMade, then claimHolds, and the claim ends. Show after the reply: focusDidSettle calls claimHolds and the claim ends. Also, keepsFromWindow checks shown again on every key, so a missed settle cannot hold keys after the workspace shows.
  • Automation does not start a claim. beginWorkspace runs only when byPerson (the run's own origin, so a nested .user invocation inside a script run is not the person). Automation does not end a claim either: isPersonsOwn accepts only the mouse, keyboard and palette sources, and willRunObservers acts only on a top-level .user run (the observer runs before the run scope is bound, so current == nil is correct). The focusSidebar change from .keyboard to .intent keeps a CLI focusSidebar from counting as the person's intent.
  • Window close or window-number reuse. teardown() ends the claim. holds checks isOpen and claim.window === window, and the weak window drops the claim. Only the claim's own ticket can name its workspace (noteMade).
  • Quiet path. The Task is created inside carrying(quiet), so the lane task inherits the scope. Because of this, named.create → claimNew files the workspace in quietClaims/backgroundClaims, so it is listed and not shown. After the reply, when no text input has the keyboard, the code removes both marks and calls claimNew again with view change. That calls claim(…) → select when the workspace is already mirrored, or sets pendingClaims when it is not. So the workspace is listed and shown. hasTextInputKeyboard requires appActive and this window (or a window it owns) to be key, so cmux open --focus from another app still shows the workspace and activates the app.
  • Main thread. No blocking calls. facts(in:) reads cached state. The new hot-path work in capture costs only an isEmpty check while no claim exists. No animation, no new action bind, no new CI gate. CmuxNextApp shrinks by 27 lines.
  • Tests. Before the fix, these tests fail for the reasons the PR states: the Home hold test (no hold before the fix), the script-over-field test (it showed) and the nested-newTab test (page hold). The claim tests exercise the KeyboardClaims rules directly. The suite trait change does not change any lane result: with CMUX_TEST_REQUIRE_GUI=1, package-test-lane.sh already fails a skip.

Findings (none block)

  1. Medium (UX, deliberate and tested): a failed create leaves a claim that silently drops typed text. In CreationInputCoordinator.swift:170, when resolve(…, workspace: nil) runs (the create failed, the daemon is not connected, or a script's close cancels the pending create through CancellationError at AppActions+Workspaces.swift:208), the claim stays with workspace == nil. KeyboardClaims.holds (KeyboardClaims.swift:64) then returns true until the person clicks, presses Escape, uses a binding or moves focus. In this state, every key that runs no binding is dropped in that window. This includes keys typed into the person's existing terminal when Cmd-N fails during a daemon reconnect, and Cmd-C/Cmd-V/arrow keys in Home's message box. Nothing tells the person why. aPersonsNewTerminalThatNeverShowsKeepsTheWindowsKeysUntilEscape asserts this behavior, so it is a design choice. Suggested follow-up: end the claim when its own create fails and the focused responder is not an agent input. At a minimum, show the failure where the person is typing.
  2. Low: synthetic clicks end the claim. The mouseDownObserver (AppServices.swift:742) calls personActed for every mouse-down, including synthetic ones (Computer Use, CGEvent automation), although currentEventIsSynthetic is available on the next lines. This only ends a claim early (the safe direction), but it means automation can end a person's claim.
  3. Low: any top-level .user registry run ends the claim. This includes app code that calls perform with the default invocation outside a run scope (for example a page or bridge callback). It only ends the claim early, but that reopens the window in which keys go to Home's box. I did not find a concrete caller in the newTab flow, because its own continuations keep the run scope.
  4. Low (behavior note, already in the PR's open note): Home's message box is an editable NSTextView and takes the keyboard by itself. So while Home shows and cmux is active, a script's, MCP's or the Chief's newTab --focus is never shown, only listed. This is consistent with the stated rule, but the effect is wider than "a person typing in a field".
  5. Low (test gap): No test checks, on the real show-after-reply path, that the claim is gone and that a key typed after the new terminal shows reaches that terminal. The Home test stops when the terminal is the first responder. The per-key shown check makes a regression here unlikely, but a later change could break it without any test failing.
  6. Note: the second branch of claimsKeyboard (CreationInputCoordinator.swift:105) now keeps any primary input from taking the keyboard during any newest-generation creation hold (splits, new terminal tab, browser stand-in), not only during newTab. After a normal hold end, only the forced "unshown" path sends contentPresented again. Today this is correct, because the current callers create terminals or browser tabs and neither has a PrimaryInputTarget. A future creation of a primary-input surface through begin(...) would not get the keyboard.

…ttings popover panel it made

Green step f28ce3bdca7354ac9eaa15b5 passed every test and failed the suite on
the window-leak check: keysTypedForAPersonsNewTerminalOverHomeNeverReachTheChiefMessageBox
left the conversation's NativePopupPanel open (the Home makes it up front and
only hides it). Closed in the test's own teardown, as
HomeOpenFocusesComposerTests does.
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (review-agent-composer-focus-2): SHIP at af500ef
reviewer-session: 143cfba7-e710-41c3-b08b-497a0779cd55
author-session: a1121df0cfe085bf2
R9: pass. No new cap, queue or deadline. The new commit changes one test only.
perf claim: none

Scope: git diff bd887fafd57 af500efe389 (one commit, af500ef). The previous independent review gave SHIP at bd887fa (#30071 (comment)). I read the code. I did not build or run tests.

Findings:

  1. Test-only: pass. git diff bd887fafd57 af500efe389 --stat shows 1 file, 4 added lines, in Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/HomePendingTerminalFocusTests.swift. No product code, no script and no CI file changes. The suite keeps its .closesItsWindows trait. The leak check is not weaker: the test now closes the panel that it caused, it does not hide or skip the check.
  2. Mirror path: correct on this head. HomeHostView.swift line 29 has private let settings: HomeChiefSettingsPopover. HomeChiefSettingsPopover.swift line 15 has private let popup = NativePopupHost(panel: NativePopupPanel(identifier: "cmux.home.chiefSettings")). The panel is made when the host is made, which agrees with the leak report ("NativePopupPanel ... 0 on screen"). NativePopupHost.closePanel() (CmuxNextDesign/Overlay/NativePopupHost.swift line 204) hides the popup, removes the child window and calls panel.close(). This is the same pattern as close() in HomeOpenFocusesComposerTests.swift line 104.
  3. Defer coverage: the defer is registered just after let host = try #require(page.host). After that line it runs on every exit, including a failed #require (a throw) and a failed #expect. It does not run when withApp, showHome, the two until waits or #require(page.host) fail first. On those paths the test already fails, and the panel leak is only a second message. Low, not blocking. If you want full coverage, register the defer on page.host right after page.show(...), as HomeOpenFocusesComposerTests does with if let host = page.host.
  4. import CmuxNextDesign: valid. Package.swift lists "CmuxNextDesign" in the CmuxNextAppTests test target dependencies (about line 1694), and 535 files in Tests/CmuxNextAppTests import it already. NativePopupHost is public in that module.
  5. Full PR diff: git diff cba94d55a6a4 af500efe389 is the code that the previous SHIP reviewed, plus these 4 test lines. The PR head on GitHub is af500ef.

Evidence:

  • Compile check 706ef390c8e5167d9d836c9e on af500ef: done (cmux-next-swift-check, --package CmuxNext --app).
  • Green rerun 2549eaa093d29b23b5c0d47b on af500ef (suites HomePendingTerminalFocusTests, PersonTerminalKeyboardClaimTests, CreationInputHoldTests, CMUX_TEST_REQUIRE_GUI=1): still queued when I wrote this (waiting for a worker). I did not see its test or leak results. The previous green f28ce3bdca7354ac9eaa15b5 on bd887fa passed every test and failed only on this one panel leak.
  • Red 9324962aac1d94d400bb65c2 on c65b87a failed with tests run (as the author reports; I did not read its log again).

Verdict: SHIP. Land when 2549eaa093d29b23b5c0d47b is green with no window leak.

@lawrencecchen lawrencecchen added the ready-to-land Reviewed at this head; next-autoland merges it into feat-cmux-next when its only reds are base reds label Nov 1, 2026
Conflicts with #29301 (373f346, held keys carry a lane: the creation whose
tab the focus named):
- CreationInputBuffer: open takes both `machine` (feat) and `newWorkspace`
  (this PR). A key whose focus names no creation's tab while a new workspace
  of the hold is on its way goes to that workspace's lane (Home showed, so
  the focus named no new tab). take(shown:) becomes dropKeys(ofWorkspacesOtherThan:)
  before feat's take(focused:isShortcut:); take(shown:) stays as drop + take.
- CreationInputCoordinator: feat's CreationHoldLog (holdEnds) replaces the
  local failedCreation/lastHoldEnd; the flush drops unshown workspace keys,
  then takes the lanes as feat does. One comment line folded to keep the file
  at the 400-line limit.
- DecidedKeyEvents: feat made the same move to CmuxNextFocusUI; feat's copy.
- CreationInputHoldTests: both tests kept; this PR's captures name no
  focused tab.
A move only. KeyWindowRole says which window the key window acts for and its
close rule (keyboard focus code; it reads WindowKindRegistry and
isChromiumPageWindow, both already in CmuxNextDesign and CmuxNextFocusUI).
The only edits are the access words a move across modules needs (public on
the type, its fields and resolve; public imports) and dropping the import of
its new module. KindlessWindowAudit stays internal beside it. Feat moved
DecidedKeyEvents itself in #29301, so this PR needs one more move to keep
CmuxNextApp from growing (app-size rule).
@lawrencecchen lawrencecchen removed the ready-to-land Reviewed at this head; next-autoland merges it into feat-cmux-next when its only reds are base reds label Nov 1, 2026
@cmux-bot cmux-bot Bot removed the stack-blocker label Nov 1, 2026
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (review-agent-composer-merge): SHIP at 7688e8b
reviewer-session: 68807981-e2a5-499d-b6d7-0e86769c7332
author-session: a1121df0cfe085bf2
R9: pass
perf claim: none

Scope: the merge of feat-cmux-next 2d8fbe0 (9591708) and the move 7688e8b, against the SHIP at af500ef and against #29301 (373f346).

Merge scope

  • git diff 2d8fbe07376 7688e8b9a7e --stat has the same 19 files as git diff cba94d55a6a4 af500efe389. The only difference is that DecidedKeyEvents is replaced by KeyWindowRole. 16 of these files have a -U0 patch that is identical to the pre-merge patch. Only CreationInputBuffer, CreationInputCoordinator, CreationInputHoldTests and KeyWindowRole differ.
  • DecidedKeyEvents is feat's copy, byte for byte. It differs from the PR's old move only in where init() sits.
  • No fix(app): each held key goes only to the terminal it was typed for (P0 1193, Cmd-D twice) #29301 code is lost. Outside this PR's hunks, both merged files are equal to feat's. The diff against feat removes only the two lines that the PR replaces (return false in capture's no-buffer guard, resolve(ticket, awaitsFocus:)). CreationHoldLog (creationFailed, end(in:forced:)) gives the same results as the PR's old failedCreation/lastHoldEnd.

Keys for a new workspace that is not shown, or whose creation failed (CreationInputBuffer.swift, CreationInputCoordinator.swift)

  • A new-workspace ticket has no tab, no earlyTab and no laneTabs entry. So heldLane(id) is nil, and in take(focused:) its keys would become .run(_, lane: nil), which means the focused view. The flush prevents this: it calls dropKeys(ofWorkspacesOtherThan: shown) before take(focused:) (coordinator lines 344-346), on forced ends and on normal ends. After the drop, a workspace key reaches .run only when the window shows that workspace. This is the same rule as the old take(shown:).
  • take(lane:): earlyLane reads only earlyTabs, so a workspace ticket is never an early lane. No path is open here.
  • resolve(_, awaitsFocus: false) deletes the failed ticket's keys. This is the same as before the merge.
  • cancel(tab:) cannot cancel a workspace ticket, because the ticket has no tab. When the hold waits for nothing more, cancel deletes the workspace's keys (laneTabs[$0] == nil). They are not delivered.
  • runAgain -> prepend(_:issuer:): unshown workspace keys are dropped before replay, so they never get to prepend. If the replayed key is Cmd-N and opens a new workspace hold, then creation = nextID is that workspace ticket. The keys after Cmd-N on the issuer lane, and the lane-less keys, become the new workspace's keys. This is correct.
  • Forced flush from capture (.focusLeft) and the window-gone branch: both drop the keys or run the same drop. No path sends a key typed for an unshown or failed workspace to the focus, so Home's message box cannot get one.

The workspaceTickets.max() fallback compared with #29301's lane-less keys

  • SecondCreationKeysTests opens no new-workspace ticket. In heldKeysReachTheirTerminalAfterItsWorkspaceWasSwitchedAway the keys are typed before newTab and get lane 1 from focusedTab. The fallback is nil, or it never applies, so every scenario has the same result as on feat. Cmd-N's ticket resolves, the window shows the new workspace, dropKeys drops nothing, and lane 1 still gets .type into the hidden first terminal.
  • In a hold that also has a new workspace, a lane-less key (fix(app): each held key goes only to the terminal it was typed for (P0 1193, Cmd-D twice) #29301: "typed after the person moved on") now goes to the workspace. It is dropped when that workspace is not shown. Before the merge, the PR assigned every key to the newest creation, so this is not a regression. It is also the safe direction: the key is lost, but it is never typed into the wrong place. Not blocking.
  • Optional follow-up, not blocking: a menu or palette New Workspace that opens a ticket in a buffer whose split focus already resolved to that split's tab (typingTab = TA). In this case a key typed after it gets lane(TA), not the workspace. A key path cannot get here: a held Cmd-N replays through prepend, which gives the later keys to the workspace. max(lane(focusedTab:), workspaceTickets.max()), by ticket number, would give the newest intent's creation in all cases.

7688e8b

  • This is a pure move (similarity 91%). The changes are: public on the struct, its three fields and resolve; public import AppKit and public import CmuxNextDesign; and the import of CmuxNextFocusUI is removed. KindlessWindowAudit stays an internal caseless enum. KeyWindowRole is a public struct with instance fields, so the namespace rule (which applies to public or package types) passes. lint-ios-package-conventions.sh does not scan macOS CmuxNext. The only caller is WindowKeyTable.keyWindowRole, which imports CmuxNextFocusUI.

Tests

  • CreationInputHoldTests: the merge changes only capture(_, focusedTab: nil). The test still proves each claim: no keys while Home shows, the keys go on when ws-new shows, and a failure deletes them. "Typed after the reply: still that terminal's" now depends on the fallback, which is the intended rule. One gap, not blocking: the test calls take(shown:) (drop + take()). That function now has no production caller, and the coordinator's real path (drop + take(focused:)) is covered only by the GUI suites.
  • HomePendingTerminalFocusTests and PersonTerminalKeyboardClaimTests are unchanged by the merge and still test what they claim. In the Home test the focus is on the dock page (typingTab nil), and in the claim test it is on pane 0's terminal, which is not a creation tab. In both, the fallback gives the keys the workspace lane. Pre-existing, not blocking: aPersonsNewTerminalThatNeverShowsKeepsTheWindowsKeysUntilEscape says "the keys typed for it are deleted", but it does not assert that "e" never reaches pane 0's terminal.

Evidence read once

  • cf2e38710c76d81f88d13ed1 (cmux-next-swift-check at 7688e8b, --package CmuxNext --app): state done.
  • 899cd57b1887b17c35461ea7 (GUI suite HomePendingTerminalFocusTests, PersonTerminalKeyboardClaimTests, CreationInputHoldTests, SecondCreationKeysTests, CMUX_TEST_REQUIRE_GUI=1, on cmuxs-Mac-mini): state running at 05:38Z. No result yet, and I did not wait for it.

@lawrencecchen lawrencecchen added ready-to-land Reviewed at this head; next-autoland merges it into feat-cmux-next when its only reds are base reds and removed ready-to-land Reviewed at this head; next-autoland merges it into feat-cmux-next when its only reds are base reds labels Nov 1, 2026
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

DIRTY since the 07:02Z merge round: the merge with feat needs App-size work (ready-queue unblocker)

I added the display proof 899cd57b1887b17c35461ea7 (gui-session-v1, all 4 suites, at 7688e8b) to the Observed section; with it the PR passed the P0, receipt and policy checks. Then feat moved, and the 07:02Z round says "DIRTY on GitHub: merge feat, resolve, new receipt".

What I found when I merged origin/feat-cmux-next (local only, not pushed):

  1. One text conflict: CmuxNextFocusUI/ShortcutHintModifierPolicy.swift. Feat's fix(app): keys typed right after Cmd-W go to the pane that gets the focus, never into the closing terminal (P0 close-then-type) #29409 (P0 close-then-type, 06:31Z) made the same move of this file and of the other four focus files out of CmuxNextApp. The two copies differ only in where public init() {} sits; feat's copy is fine.
  2. The pre-push gate then refuses the merge: arch-ratchet [app_lines] CmuxNextApp Swift lines: +26. This PR's "-27 App lines" came from those five moves, which feat now already has. What stays in the App is this PR's new code: Focus/CreationInputCoordinator.swift +54, AppActions+Workspaces.swift +27, Focus/PrimaryInputTarget.swift +7.

So the owner of this branch must move about 26 lines of the new code out of CmuxNextApp (the Focus row of architecture-refactor-DESIGN.md section 4 names the destination package), or add a reviewed app_lines exception. Then push, run a new display run at the new head, and get a new receipt. I did not do this: it is a design choice in a P0 focus path. Note: the pre-commit hook also adds a tests/test-execution.toml entry for tests/test_nightly_tree_source_marker.py on that merge.

One conflict: CmuxNextFocusUI/ShortcutHintModifierPolicy.swift. Feat's #29409
moved it (and DecidedKeyEvents, PaneNavigationHintPlan) out of CmuxNextApp
too; the copies differ only in where public init() sits: feat's copy.
A move of pure data, beside the other key and focus types in CmuxNextFocusUI:
the facts about the focused surface that KeyRouter reads besides the focus
state (no App dependency, Foundation only). The edits are the access words a
move across modules needs (public on the struct and its fields, a public
import) and a public init with every field defaulted, in declaration order,
which is what the App's memberwise init gave every call site (KeyRouter's
Facts(...) with any subset of labels). No logic changes.

Feat's #29409 moved three of the five focus files this PR moved, so the
merge left CmuxNextApp +26 lines over feat (arch ratchet app_lines); this
move pays for it (app-size rule).
One conflict: AppActions+Workspaces.swift newWorkspace, with #26078 (Run New
Workspaces in Cloud). Feat's code is kept as is: the source window (the
invocation's window target, else the active one) in place of windows.active,
the Cloud route before the create, and the Cloud notice toast. This PR's
rules sit on it: byPerson (the run's own origin), no New Tab page for
automation, the person's keyboard claim in the source window, and a
script's quiet create shown only when no text input has the keyboard; the
Cloud notice shows after either show path.
@blacksmith-sh

blacksmith-sh Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Found 13 test failures on Blacksmith runners:

Failures

Test View Logs
/tmp/tmp32tfcg0j/summary-missing.json (live):/summary-missing.json (live): View Logs
/tmp/tmpr4a5mr0g/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
nightly must build and hand off the macOS 26 helper and Xcode 26.5 app before publishin
g/
nightly must build and hand off the macOS 26 helper and Xcode 26.5 app before publishin
g
View Logs
test_cmux_next_follows_the_committed_target_graph (main.FocusedTestMap.test_cmux_ne
xt_follows_the_committed_target_graph)/
test_cmux_next_follows_the_committed_target_graph (main.FocusedTestMap.test_cmux_ne
xt_follows_the_committed_target_graph)
View Logs
test_every_part_of_bun_run_check_runs_in_the_warn_branch (main.WebviewsWarnSplitCov
ersCheck.test_every_part_of_bun_run_check_runs_in_the_warn_branch)/
test_every_part_of_bun_run_check_runs_in_the_warn_branch (main.WebviewsWarnSplitCov
ersCheck.test_every_part_of_bun_run_check_runs_in_the_warn_branch)
View Logs
test_helper_never_takes_a_hosted_mac_and_skips_compile_on_hit (main.WorkflowTests.t
est_helper_never_takes_a_hosted_mac_and_skips_compile_on_hit)/
test_helper_never_takes_a_hosted_mac_and_skips_compile_on_hit (main.WorkflowTests.t
est_helper_never_takes_a_hosted_mac_and_skips_compile_on_hit)
View Logs

...and 3 more test failures. View all on Blacksmith: run 41285212937, run 41285213049

Fix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

lawrencecchen added a commit that referenced this pull request Nov 1, 2026
…30476)

* test(hooks): a merge commit registers none of the base's tests

The pre-commit hook reads the cached diff against HEAD, which in a merge holds
every test the base added since the branch point, so it wrote the base's
unregistered test into the pull request (tests/test_nightly_dedicated_mini.py
in the merge commits of #30071 and #30351). This test fails on the base:
python3 tests/test_install_git_hooks.py InstallGitHooksTests.test_pre_commit_registers_no_test_in_a_merge_commit

* fix(hooks): a merge commit registers no tests in tests/test-execution.toml

In a merge, the cached diff against HEAD holds every test file and workflow
line the other parent added since the branch point, so the pre-commit hook
registered the base's tests in the pull request's merge commit
(tests/test_nightly_dedicated_mini.py in #30071 and #30351). A merge adds no
test of its own: the hook now detects MERGE_HEAD first and skips registration
as it already skipped the formatters.

Red on the base, green here:
python3 tests/test_install_git_hooks.py InstallGitHooksTests.test_pre_commit_registers_no_test_in_a_merge_commit
Whole file: python3 tests/test_install_git_hooks.py (41 tests OK).
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (review-agent-composer-merge-2): SHIP at 9923d35
reviewer-session: d17815ed-0ab3-4712-aeb3-e6ace8707d62
author-session: a1121df0cfe085bf2
R9: pass
perf claim: none

Scope: the two merges and 10f80bb since the SHIP at 7688e8b. Diffs are against the merged feat parent 6eb6839 (feat moved after the merge).

  1. newWorkspace resolution (AppActions+Workspaces.swift:129-226) is correct.
    • Feat's code is kept as it was: sourceWindow, the Cloud route and its guard, the Cloud toast. The PR's rules sit on top, and windows.active changes to sourceWindow/sourceID the same way feat changed the New Tab key hold (keys).
    • Claim in a window that is not the active one: terminalWindow is set only when target == sourceWindow?.state.id, and sourceWindow is the window the invocation names, else windows.active (the event's window, else the key window). CreationInputCoordinator.beginWorkspace opens the buffer and the claim per window (buffer(in: window), claims.begin(in: window)). When the person names window B and types in window A, A's keys are not held. B's claim only gives B's keyboard to the new terminal of the workspace the person asked B for, and it ends at resolve/holdLimit. This is the same rule as feat's own keys line. No person's key goes to a window they did not choose.
    • Early returns: the Cloud guard case .thisMac return and the machine-not-connected return both come before beginWorkspace and beginIncoming. No ticket or claim opens before a return, so nothing is left unresolved. beginWorkspace is after the Cloud guard.
    • Cloud toast: it is shown after both show branches, in target's window, and also when the quiet path keeps the workspace hidden. A notice comes only when isPersonsOwnRun is true, so a socket/CLI script never gets one. The toast is an overlay and does not take the keyboard. Acceptable.
  2. 10f80bb is a pure move. The 20 fields have the same names, types, order and defaults (false/nil) as the old CmuxNextApp/Focus/KeyFacts.swift. The public init lists them in declaration order with the same defaults, so every KeyFacts(...)/Facts(...) call that compiled against the memberwise init still compiles. CmuxNextApp re-exports CmuxNextFocusUI (FocusUIExport.swift), so KeyRouter+Context.swift and the @testable import CmuxNextApp tests see the type. The type has an instance surface (stored properties and an init), so it obeys the namespace convention.
  3. cb32825: CmuxNextFocusUI/ShortcutHintModifierPolicy.swift, PaneNavigationHintPlan.swift and tests/test-execution.toml at the head are byte-identical to feat (6eb6839 and the current tip).
  4. git diff 6eb68397ef9 9923d35519e --stat lists 18 files: the 17 files the previous SHIP read (less the two hint files feat now has), plus the KeyFacts move. The PR's patch on every other file is line-identical to the patch at 7688e8b.

Cloud follow-up (not this PR's): the Cloud route still uses invocation.isPersonsOwnRun, which a script's nested newTab can pass. This PR does not make it worse. It slightly reduces it: byPerson now gates inheritedCwd, so a nested script run no longer sends the person's focused folder into the Cloud plan's cwd.

Evidence: compile 21e438d0d02304bb2cbd8a7e on this head is done, with receipts ok:true for CmuxNext (CmuxNextAppTests compiled), app and cmux-tui. Display green 03c125f17b5ba43ae7e38807 (HomePendingTerminalFocusTests, PersonTerminalKeyboardClaimTests, CreationInputHoldTests, SecondCreationKeysTests, CMUX_TEST_REQUIRE_GUI=1) is still queued (waiting_for_worker). Not run, not read.

@teamleaderleo

Copy link
Copy Markdown
Collaborator

For awareness: this PR has a SHIP review at its head, but GitHub reports it as conflicting with feat-cmux-next, so the lander's update-branch can't merge it and it can't land as is. Files that conflict in a test merge against the current base: Packages/macOS/CmuxNext/Sources/CmuxNextApp/AppActions+Workspaces.swift. Once feat-cmux-next is merged into the branch and the conflicts resolved, it needs only a quick review of the resolution before it can land. (From the conflict sweep; no push was made to this PR.)

This branch was previously deployed

1 inactive deployment
ci-routing-agent — 9923d355 Deployed Nov 1, 2026 by lawrencecchen via wait for same-key tree publisher #94630
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.

2 participants