Skip to content

Fix CLI commands briefly stealing focus#2464

Merged
austinywang merged 2 commits intomainfrom
issue-140-cli-focus-steal
Apr 1, 2026
Merged

Fix CLI commands briefly stealing focus#2464
austinywang merged 2 commits intomainfrom
issue-140-cli-focus-steal

Conversation

@lawrencecchen
Copy link
Copy Markdown
Contributor

@lawrencecchen lawrencecchen commented Apr 1, 2026

Summary

  • enforce the socket focus policy in AppDelegate so non-focus CLI commands cannot invoke AppKit activation/fronting helpers
  • gate remaining TerminalController activation paths for settings.open, feedback.open, debug.type, and synthetic input through the same focus-intent whitelist
  • extend the debug socket policy test coverage for the regressed non-focus commands

Testing

  • ./scripts/reload.sh --tag cli-focus-steal

Closes #140.


Summary by cubic

Stops CLI commands from briefly stealing focus by enforcing the socket focus policy and blocking unintended app/window activation. Addresses #140.

  • Bug Fixes
    • Enforce focus policy in AppDelegate: bringToFront/focusMainWindow early-exit under suppression; update active main window without activating UI.
    • Gate settings.open, feedback.open, debug.type, synthetic input, and shortcut simulation behind socketCommandAllowsInAppFocusMutations; use v2FocusAllowed for V2 activation flags; skip NSApp.activate/makeKeyAndOrderFront when disallowed.
    • Expand debug tests to cover policy for simulate_shortcut, settings.open, feedback.open, and debug.type.

Written for commit 5642bb1. Summary will update on new commits.

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Improved window focus and activation behavior when socket command restrictions are active
    • Enhanced focus mutation handling to properly respect security policies
  • Tests

    • Expanded socket command policy verification test coverage
  • Chores

    • Updated dependencies

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 1, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Apr 1, 2026 7:29am

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 1, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

These changes add focus suppression guards to prevent CLI socket commands from stealing focus. When socket command activation suppression is enabled and focus mutations are disallowed, window activation and ordering operations are bypassed or early-return, while submodule dependencies are also updated.

Changes

Cohort / File(s) Summary
Focus Suppression Guards
Sources/AppDelegate.swift, Sources/TerminalController.swift
Modified window activation and ordering logic to respect shouldSuppressSocketCommandActivation() and socketCommandAllowsInAppFocusMutations() checks. focusMainWindow() and bringToFront() now early-return when suppression is active, bypassing miniaturization handling and NSRunningApplication.activate(). V2 feedback/settings activation now gates through v2FocusAllowed() instead of raw boolean defaults. Synthetic input window preparation now guards activation/order-front operations behind focus mutation allowance checks.
Socket Command Policy Tests
cmuxTests/TerminalControllerSocketSecurityTests.swift
Extended testSocketCommandPolicyDistinguishesFocusIntent with additional assertions verifying that simulate_shortcut, settings.open, feedback.open, and debug.type socket commands are marked with insideSuppressed == true and insideAllowsFocus == false.
Submodule Updates
ghostty, vendor/bonsplit
Updated submodule references to newer upstream commits.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 Hop along, dear focus—stay where you are!
Our CLI commands won't move you afar,
With guards and with gates, we keep you in place,
No more stolen focus—just calm, quiet grace.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the primary change: preventing CLI commands from stealing focus through policy enforcement.
Description check ✅ Passed The description covers what changed, why, and the testing approach (./scripts/reload.sh --tag cli-focus-steal), though lacks evidence of local testing completion.
Linked Issues check ✅ Passed Code changes enforce socket focus policy to prevent CLI commands from stealing focus, directly addressing #140's core requirement that CLI commands remain side-effect-free regarding focus state.
Out of Scope Changes check ✅ Passed All changes (AppDelegate focus handling, TerminalController activation gating, submodule updates, test coverage) are directly scoped to preventing CLI focus-stealing as defined in #140.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-140-cli-focus-steal

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c778946c75

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +4992 to 4996
!TerminalController.socketCommandAllowsInAppFocusMutations() {
setActiveMainWindow(window)
return true
}
bringToFront(window)
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 Badge Keep socket-suppressed focus commands from activating the app

This change routes socket-suppressed focus-intent commands (for example window.focus / workspace.select) through bringToFront, which now performs full app activation. In the suppressed path this used to avoid AppKit activation, so running these commands while the user is in another macOS app can steal OS focus again. That regresses the socket focus policy and makes automation intrusive in normal desktop use.

Useful? React with 👍 / 👎.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 1, 2026

Greptile Summary

This PR fixes CLI commands briefly stealing macOS focus by consistently enforcing the socket focus-intent policy across AppDelegate and TerminalController. It closes issue #140.

Key changes:

  • focusMainWindow (AppDelegate) — simplified to an early-return for non-focus socket commands; falls through to bringToFront for focus-intent commands and the non-socket path.
  • bringToFront (AppDelegate) — now guards against non-focus socket commands at the top of the method, and setActiveMainWindow is moved into this function (affecting all ~10 call sites).
  • v2FeedbackOpen / v2SettingsOpen (TerminalController) — shouldActivate now flows through v2FocusAllowed, preventing settings.open and feedback.open from causing activation side effects.
  • v2DebugTypeNSApp.activate + makeKeyAndOrderFront are gated behind socketCommandAllowsInAppFocusMutations().
  • prepareWindowForSyntheticInput — the entire function is now a no-op for non-focus socket commands, stopping simulate_shortcut from stealing focus.
  • Tests — four new assertions in testSocketCommandPolicyDistinguishesFocusIntent verify the policy snapshot for simulate_shortcut, settings.open, feedback.open, and debug.type.

Process note: Per the regression test commit policy in CLAUDE.md, regression tests for a bug fix should use a two-commit structure (test-only commit first so CI goes red, then the fix). This PR lands both the test additions and the fix in a single commit (c778946), so CI never demonstrates the tests failing without the fix.

Confidence Score: 5/5

Safe to merge — all remaining findings are P2 observations about side-effect scope and a process policy note, none block correctness.

The core logic is sound: the new guard conditions correctly prevent non-focus socket commands from calling AppKit activation helpers. All TerminalController activation paths are properly gated. The only open items are a P2 observation about setActiveMainWindow being implicitly added to several previously-omitted bringToFront call sites (likely a coincidental improvement), a P2 note that focus-intent commands now receive full NSApp.activate instead of the lighter orderFront (probably correct), and a process-level P2 that the two-commit regression test policy from CLAUDE.md was not followed.

Sources/AppDelegate.swift — confirm setActiveMainWindow side effect at the three bringToFront call sites that previously lacked it (lines ~5132, 11693, 11771) is intentional.

Important Files Changed

Filename Overview
Sources/AppDelegate.swift Refactors focusMainWindow to skip deminiaturize for non-focus socket commands and gates bringToFront behind the same focus-intent check; incidentally embeds setActiveMainWindow inside bringToFront, touching all ~10 call sites.
Sources/TerminalController.swift Gates v2FeedbackOpen, v2SettingsOpen, v2DebugType, and prepareWindowForSyntheticInput activation paths through the focus-intent whitelist via v2FocusAllowed / socketCommandAllowsInAppFocusMutations.
cmuxTests/TerminalControllerSocketSecurityTests.swift Extends testSocketCommandPolicyDistinguishesFocusIntent with correct policy assertions for simulate_shortcut, settings.open, feedback.open, and debug.type.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[CLI Socket Command arrives] --> B{withSocketCommandPolicy\npushes suppressed=true\nallowsFocus=?}
    B --> C{Is command in\nfocusIntentWhitelist?}
    C -- Yes --> D[allowsFocus = true]
    C -- No --> E[allowsFocus = false]

    D --> F[focusMainWindow called]
    E --> G[focusMainWindow called]

    F --> H{suppressed &&\n!allowsFocus?}
    H -- No --> I[bringToFront\nsetActiveMainWindow\ndeminiaturize\nmakeKeyAndOrderFront\nNSApp.activate]
    H -- Yes --> J[setActiveMainWindow only\nreturn — no AppKit activation]

    G --> K{suppressed &&\n!allowsFocus?}
    K -- No --> I
    K -- Yes --> J

    style J fill:#f9a,stroke:#c00
    style I fill:#9fa,stroke:#060
Loading

Comments Outside Diff (2)

  1. Sources/AppDelegate.swift, line 11887-11899 (link)

    P2 setActiveMainWindow now called at every bringToFront call site

    Adding setActiveMainWindow(window) inside bringToFront is a wider change than described. bringToFront has ~10 call sites in AppDelegate:

    • Dual-call sites — several callers already invoke setActiveMainWindow immediately before bringToFront (e.g. focusScriptableMainWindow at ~line 4602, openWelcomeWorkspace at ~line 6296, addWorkspace at ~line 4613). After this change they call setActiveMainWindow twice, which is idempotent but redundant.
    • Previously missing call sites — a few callers never called setActiveMainWindow before bringToFront (e.g. the cross-window drag-reassert closure at ~line 5132, and both openJumpToUnreadNotification paths at lines 11693 and 11771). Those paths now silently gain an AppDelegate.tabManager / TerminalController.setActiveTabManager side effect that didn't exist before.

    The redundant calls are harmless, and the newly-added calls at the omitted sites are probably the right fix. But if setActiveMainWindow at the three previously-omitted sites was deliberately left out, this could change routing behaviour for shortcut dispatch and sidebar state in those paths. Worth a quick confirmation that the side effect is intentional everywhere.

  2. Sources/AppDelegate.swift, line 4989-4998 (link)

    P2 Focus-intent socket commands now get full NSApp.activate

    In the old code, when focusMainWindow was called inside a focus-intent socket command (suppressed + allowed), the path was:

    deminiaturize → window.orderFront(nil) → setActiveMainWindow → return
    

    bringToFront (and therefore NSRunningApplication.current.activate(options: [.activateAllWindows, .activateIgnoringOtherApps])) was deliberately skipped.

    After this change, the same path falls through to bringToFront, which calls the full NSRunningApplication.current.activate. This upgrades focus-intent socket commands to do system-level activation, which is likely the right behaviour (a window.focus command should bring the app to front), but it is an undocumented behaviour change. If the lighter orderFront-only path was intentional (e.g. to avoid disrupting Space/Mission Control transitions in tests), it's worth noting explicitly.

Reviews (1): Last reviewed commit: "wok" | Re-trigger Greptile

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

2 issues found across 3 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Sources/AppDelegate.swift">

<violation number="1" location="Sources/AppDelegate.swift:4993">
P2: Suppressed socket commands now mutate active in-app window routing even when focus mutations are disallowed.</violation>
</file>

<file name="Sources/TerminalController.swift">

<violation number="1" location="Sources/TerminalController.swift:10460">
P2: socketCommandAllowsInAppFocusMutations() relies on a global shared stack across all socket threads, so concurrent commands can read another command’s focus policy and incorrectly allow/suppress focus changes in newly gated paths.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

}
if TerminalController.shouldSuppressSocketCommandActivation(),
!TerminalController.socketCommandAllowsInAppFocusMutations() {
setActiveMainWindow(window)
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Apr 1, 2026

Choose a reason for hiding this comment

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

P2: Suppressed socket commands now mutate active in-app window routing even when focus mutations are disallowed.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/AppDelegate.swift, line 4993:

<comment>Suppressed socket commands now mutate active in-app window routing even when focus mutations are disallowed.</comment>

<file context>
@@ -4988,14 +4988,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
-            }
+        if TerminalController.shouldSuppressSocketCommandActivation(),
+           !TerminalController.socketCommandAllowsInAppFocusMutations() {
+            setActiveMainWindow(window)
             return true
         }
</file context>
Fix with Cubic

}
NSApp.activate(ignoringOtherApps: true)
window.makeKeyAndOrderFront(nil)
if socketCommandAllowsInAppFocusMutations() {
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Apr 1, 2026

Choose a reason for hiding this comment

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

P2: socketCommandAllowsInAppFocusMutations() relies on a global shared stack across all socket threads, so concurrent commands can read another command’s focus policy and incorrectly allow/suppress focus changes in newly gated paths.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/TerminalController.swift, line 10460:

<comment>socketCommandAllowsInAppFocusMutations() relies on a global shared stack across all socket threads, so concurrent commands can read another command’s focus policy and incorrectly allow/suppress focus changes in newly gated paths.</comment>

<file context>
@@ -10457,8 +10457,10 @@ class TerminalController {
             }
-            NSApp.activate(ignoringOtherApps: true)
-            window.makeKeyAndOrderFront(nil)
+            if socketCommandAllowsInAppFocusMutations() {
+                NSApp.activate(ignoringOtherApps: true)
+                window.makeKeyAndOrderFront(nil)
</file context>
Fix with Cubic

@austinywang austinywang merged commit f1acbcb into main Apr 1, 2026
20 of 21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CLI commands should not change focus state

2 participants