Skip to content

Fix Cmd+, settings shortcut routing - #3722

Closed
lawrencecchen wants to merge 2 commits into
mainfrom
task-fix-command-comma-shortcut
Closed

lawrencecchen wants to merge 2 commits into
mainfrom
task-fix-command-comma-shortcut

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented May 8, 2026 •

Copy link
Copy Markdown
Contributor

Summary:

  • Route app-level shortcuts that do not need terminal context before the terminal routing sync gate.
  • Add a regression test for Cmd+, from a cmux window without terminal shortcut context.
  • Keep close-window shortcuts on the synchronized path because their AppKit delegates depend on active terminal context.

Tests:

  • CMUX_SKIP_ZIG_BUILD=1 xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux-unit -configuration Debug -destination platform=macOS -derivedDataPath /tmp/cmux-cmdcomma-red test -only-testing:cmuxTests/AppDelegateShortcutRoutingTests/testCmdCommaOpensSettingsFromWindowWithoutTerminalShortcutContext -only-testing:cmuxTests/AppDelegateShortcutRoutingTests/testCmdShiftNCreatesWindowFromEventWindowWithoutAddingWorkspace
  • CMUX_SKIP_ZIG_BUILD=1 ./scripts/reload.sh --tag cmdcomma

Notes:

  • The first commit adds the failing regression test; the second commit applies the fix.

Note

Medium Risk
Adjusts keyboard shortcut routing order, which can change how global/app-level shortcuts are handled across different window contexts. Risk is limited to input/shortcut behavior but could cause regressions in chord handling or window-specific routing.

Overview
Fixes shortcut routing so app-scoped shortcuts (notably Cmd+, settings) are handled even when the key event originates from a window that lacks terminal shortcut context, by routing a small set of context-independent actions (quit, openSettings, reloadConfiguration, newWindow) before the terminal-context synchronization gate.

Adds a regression test ensuring Cmd+, opens Settings from an auxiliary (non-terminal) window, and updates chord arming so these context-independent actions can still participate in configured shortcut chords without requiring terminal window context.

Reviewed by Cursor Bugbot for commit 2eea49a. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Fixes the Settings shortcut (Cmd+,) so it works from non-terminal windows by handling app-level shortcuts before terminal routing. App-scoped actions like Quit, Settings, Reload Config, and New Window now work from any window.

  • Bug Fixes
    • Route context-independent shortcuts early (Quit, Settings, Reload Configuration, New Window).
    • Add regression test to ensure Cmd+, opens Settings from a non-terminal window.
    • Keep close-window shortcuts on the synchronized path due to AppKit delegate requirements.

Written for commit 2eea49a. Summary will update on new commits.

Summary by CodeRabbit

  • Refactor

    • Improved shortcut handling to ensure context-independent actions (quit, Settings, reload configuration, and new window) work consistently across all application contexts and windows.
  • Tests

    • Added test coverage for shortcut routing behavior from non-terminal windows.

@vercel

vercel Bot commented May 8, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment May 8, 2026 5:10am
cmux-staging Building Building Preview, Comment May 8, 2026 5:10am

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@coderabbitai

coderabbitai Bot commented May 8, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

AppDelegate's shortcut handling is refactored to extract context-independent shortcuts (quit, settings, reload configuration, new window) into an early dispatch helper. The main handleCustomShortcut flow now invokes this helper before arming configured chords. A new test verifies that Cmd+, opens settings from non-terminal window contexts.

Changes

Shortcut Routing Refactor

Layer / File(s) Summary
Actions Contract
Sources/AppDelegate.swift
Computed property contextIndependentShortcutActions enumerates which configured actions (quit, settings, reload, new window) are routed early, independent of terminal context.
Handler Implementation
Sources/AppDelegate.swift
New handleContextIndependentShortcut(event:) helper matches keyboard events against context-independent shortcuts and executes their corresponding handlers.
Integration
Sources/AppDelegate.swift
handleCustomShortcut is updated to call the context-independent helper first; if it succeeds, the method returns early; otherwise, configured chord arming proceeds normally.
Verification Test
cmuxTests/AppDelegateShortcutRoutingTests.swift
New test testCmdCommaOpensSettingsFromWindowWithoutTerminalShortcutContext creates an auxiliary non-terminal window, sends a Cmd+, keydown event, and asserts the settings window presenter opens exactly once.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

  • manaflow-ai/cmux#717: Directly modifies AppDelegate shortcut routing for Command-key shortcuts, including Command+, handling and terminal vs. app-level routing.
  • manaflow-ai/cmux#3214: Modifies how the "new window" shortcut action is handled in AppDelegate's shortcut routing and source window determination.
  • manaflow-ai/cmux#3468: Directly modifies AppDelegate's shortcut routing logic, including early dispatch of certain actions and handleCustomShortcut integration.

Poem

🐰 Shortcuts spring early, unburdened and free,
Context need not apply, just Q and Cmd+C,
Settings open swiftly, new windows appear,
The rabbit's refactor brings structure so clear!

🚥 Pre-merge checks | ✅ 13 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (13 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the primary change: refactoring shortcut routing for the Cmd+, settings shortcut to work correctly across different window contexts.
Description check ✅ Passed The description includes most required sections: comprehensive summary of changes, testing approach with specific commands, and notes about commit structure. However, the Demo Video section and Review Trigger checklist items are missing or incomplete.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed Production changes in AppDelegate.swift are private methods/properties within @MainActor class. No new public entities, Sendable types without isolation, or actor isolation issues introduced.
Cmux Swift Blocking Runtime ✅ Passed Production code (AppDelegate.swift) introduces no blocking synchronization. Test code uses deterministic RunLoop.main.run sleeps which are allowed per blocking-runtime rules as test scaffolding.
Cmux No Hacky Sleeps ✅ Passed Rule applies to TypeScript, JavaScript, and shell scripts. PR contains only Swift changes—shortcut routing logic with no new sleeps/timers and test-only RunLoop calls (allowed).
Cmux Swift Concurrency ✅ Passed No new legacy async patterns introduced. Synchronous refactoring of shortcut routing and test using allowed XCTest RunLoop synchronization.
Cmux Swift @Concurrent ✅ Passed PR adds synchronous shortcut routing functions. No new async code introduces concurrent annotation violations. Existing async methods are properly MainActor-isolated.
Cmux Swift File And Package Boundaries ✅ Passed Focused bug fix refactoring shortcut routing. Adds ~22 net lines to oversized file (under 250 threshold), no new responsibilities, reorganizes existing AppKit/Ghostty glue code (allowed case).
Cmux Swift Logging ✅ Passed No logging violations found. Changes refactor shortcut routing without adding print, debugPrint, dump, NSLog, or problematic Logger patterns. Test code uses appropriate XCTest assertions.
Cmux Swiftui State Layout ✅ Passed PR modifies only AppKit AppDelegate and XCTest code. No SwiftUI components, Views, or state patterns are introduced. SwiftUI state layout check is not applicable.
Cmux Architecture Rethink ✅ Passed Early routing of context-independent shortcuts with dedicated handler and declarative action list. No timing repairs, locks, observers, duplicate wiring, or split ownership.

✏️ 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 task-fix-command-comma-shortcut

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.

@greptile-apps

greptile-apps Bot commented May 8, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes the Cmd+, settings shortcut being silently dropped when the key event originates from a window that has no addressable terminal context (e.g. the settings window itself). The fix extracts the four app-global shortcuts (quit, openSettings, reloadConfiguration, newWindow) into a new handleContextIndependentShortcut helper and moves their dispatch — along with chord-prefix detection for the same set — before the synchronizeShortcutRoutingContext gate.

  • AppDelegate.swift: New handleContextIndependentShortcut and contextIndependentShortcutActions are injected before the synchronization gate; the four previously post-gate dispatchers are removed to avoid double-handling.
  • AppDelegateShortcutRoutingTests.swift: Regression test creates a plain NSWindow (no terminal context), synthesizes Cmd+, against it, and asserts the shortcut is consumed and settings open exactly once.

Confidence Score: 4/5

The production routing change is correct and targeted; the main risk is in the new test where uncleaned global presenter state can pollute the XCTest session in non-DEBUG builds.

The production AppDelegate change moves four well-understood shortcuts before the sync gate and is covered by the new regression test. The test itself has a real defect: SettingsWindowPresenter.configure(openWindow:) mutates global state unconditionally while the matching resetForTests() cleanup is compiled away in non-DEBUG, leaving a stale closure in the presenter for the rest of the test run.

cmuxTests/AppDelegateShortcutRoutingTests.swift — the #if DEBUG guard placement around configure/cleanup needs attention.

Important Files Changed

Filename Overview
Sources/AppDelegate.swift Extracts quit/openSettings/reloadConfiguration/newWindow into a new handleContextIndependentShortcut helper and moves their dispatch + chord-detection before the synchronization gate; the action list and dispatch function are structurally decoupled and require manual synchronization.
cmuxTests/AppDelegateShortcutRoutingTests.swift Adds regression test for Cmd+, from a non-terminal window; SettingsWindowPresenter.configure(openWindow:) is called outside the #if DEBUG fence while the matching resetForTests() cleanup is inside it, leaking global presenter state in non-DEBUG test runs.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[NSEvent keyDown arrives at handleCustomShortcut] --> B{Recorder active?}
    B -- yes --> Z[return false]
    B -- no --> C[Notifications popover / sidebar checks]
    C --> D[handleContextIndependentShortcut\nquit · openSettings · reloadConfig · newWindow]
    D -- matched --> Y[return true]
    D -- no match --> E{activeChordPrefix == nil?\narmChordIfNeeded for contextIndependentActions?}
    E -- chord armed --> Y2[return true chord prefix consumed]
    E -- no chord --> F[shortcutEventHasAddressableWindow\nsynchronizeShortcutRoutingContext]
    F -- unresolved window ctx --> Z2[return false]
    F -- synchronized --> G[find · toggleFullScreen · etc. dispatchers]
    G --> H[return true / false]
Loading

Reviews (1): Last reviewed commit: "Fix context-independent shortcut routing" | Re-trigger Greptile

Comment on lines +2819 to +2826
var settingsOpenCount = 0
#if DEBUG
SettingsWindowPresenter.resetForTests()
defer { SettingsWindowPresenter.resetForTests() }
#endif
SettingsWindowPresenter.configure(openWindow: {
settingsOpenCount += 1
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Global presenter state leaked in non-DEBUG test runs

SettingsWindowPresenter.configure(openWindow:) is an unconditional production API that writes directly to the type's static var openWindow. The matching cleanup (SettingsWindowPresenter.resetForTests()) is guarded by #if DEBUG. In a non-DEBUG test run the cleanup is compiled away, leaving the global openWindow closure set for the remainder of the XCTest session — any later test that calls SettingsWindowPresenter.show() (or openPreferencesWindow) would invoke a dangling test closure against a no-longer-valid settingsOpenCount. The configure(openWindow:) call should be placed inside the same #if DEBUG fence as its cleanup.

Comment thread Sources/AppDelegate.swift
Comment on lines +10578 to +10610
private func handleContextIndependentShortcut(event: NSEvent) -> Bool {
if matchConfiguredShortcut(event: event, action: .quit) {
return handleQuitShortcutWarning()
}

if matchConfiguredShortcut(event: event, action: .openSettings) {
openPreferencesWindow(debugSource: "shortcut.openSettings")
return true
}

if matchConfiguredShortcut(event: event, action: .reloadConfiguration) {
GhosttyApp.shared.reloadConfiguration(source: "shortcut.reloadConfiguration")
return true
}

if matchConfiguredShortcut(event: event, action: .newWindow) {
openNewMainWindow(preferredWindow: mainWindowForShortcutEvent(event))
return true
}

return false
}

private var contextIndependentShortcutActions: [KeyboardShortcutSettings.Action] {
// Keep main-window lifecycle commands, such as Close Window, on the synchronized
// path because their AppKit delegates depend on the active terminal context.
[
.quit,
.openSettings,
.reloadConfiguration,
.newWindow,
]
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 handleContextIndependentShortcut and contextIndependentShortcutActions must be kept in sync manually

The dispatch logic in handleContextIndependentShortcut and the chord-detection list in contextIndependentShortcutActions enumerate the same four actions in two separate, unconnected places. If a future change adds a new action to the dispatch function but misses the array (or vice versa), chord-shortcut detection for that action will silently break with no compiler or test coverage to catch it. Consider having handleContextIndependentShortcut driven by the same set declared in the property, or at minimum add a comment explicitly calling out the coupling requirement.

Rule Used: Flag Swift fixes that patch symptoms while leaving... (source)

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmuxTests/AppDelegateShortcutRoutingTests.swift`:
- Around line 2819-2826: The test installs a global handler via
SettingsWindowPresenter.configure(openWindow:) unconditionally while the
teardown SettingsWindowPresenter.resetForTests() and its defer live inside a `#if`
DEBUG block, risking a leaked handler in non-DEBUG test runs; move the
SettingsWindowPresenter.configure(openWindow:) call (and the settingsOpenCount
setup if desired) inside the same `#if` DEBUG / `#endif` region alongside the
resetForTests() and its defer so the configure and reset are always paired
(refer to SettingsWindowPresenter.configure(openWindow:) and
SettingsWindowPresenter.resetForTests()).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: e33ce9cb-17d2-4327-a738-706a6de3d823

📥 Commits

Reviewing files that changed from the base of the PR and between 7142e31 and 2eea49a.

📒 Files selected for processing (2)
  • Sources/AppDelegate.swift
  • cmuxTests/AppDelegateShortcutRoutingTests.swift

Comment on lines +2819 to +2826
var settingsOpenCount = 0
#if DEBUG
SettingsWindowPresenter.resetForTests()
defer { SettingsWindowPresenter.resetForTests() }
#endif
SettingsWindowPresenter.configure(openWindow: {
settingsOpenCount += 1
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

SettingsWindowPresenter.configure called unconditionally while resetForTests() is #if DEBUG-gated — potential state leak in non-Debug test runs.

configure(openWindow:) sets a global handler on SettingsWindowPresenter in all build configurations, but the matching resetForTests() teardown only runs in DEBUG. In a Release-configuration test binary, the closure (capturing settingsOpenCount) is installed and never cleared, and subsequent tests that open settings may observe the stale handler.

Move the configure call inside the same #if DEBUG guard so it is always paired with its cleanup:

🛡️ Proposed fix
 var settingsOpenCount = 0
 `#if` DEBUG
 SettingsWindowPresenter.resetForTests()
+SettingsWindowPresenter.configure(openWindow: {
+    settingsOpenCount += 1
+})
 defer { SettingsWindowPresenter.resetForTests() }
 `#endif`
-SettingsWindowPresenter.configure(openWindow: {
-    settingsOpenCount += 1
-})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmuxTests/AppDelegateShortcutRoutingTests.swift` around lines 2819 - 2826,
The test installs a global handler via
SettingsWindowPresenter.configure(openWindow:) unconditionally while the
teardown SettingsWindowPresenter.resetForTests() and its defer live inside a `#if`
DEBUG block, risking a leaked handler in non-DEBUG test runs; move the
SettingsWindowPresenter.configure(openWindow:) call (and the settingsOpenCount
setup if desired) inside the same `#if` DEBUG / `#endif` region alongside the
resetForTests() and its defer so the configure and reset are always paired
(refer to SettingsWindowPresenter.configure(openWindow:) and
SettingsWindowPresenter.resetForTests()).

@lawrencecchen lawrencecchen added the stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening. label Sep 23, 2026
@github-project-automation github-project-automation Bot moved this from Todo to Done in cmux backlog Sep 23, 2026

This branch was successfully deployed

1 active deployment
Preview – cmux — 2eea49ab Deployed May 8, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants