fix: make Cmd+D new-tab shortcut rebindable via KeyboardShortcutSettings (#2524) - #3338
Conversation
The regression captures the desired shortcut contract before the implementation exists: clearing the split-right shortcut must make Cmd-D fall through instead of creating another pane. Constraint: Local direct xcodebuild is forbidden for this task; verification runs through allowed reload/CI paths. Confidence: medium Scope-risk: narrow Tested: Not run locally; this test-only commit is expected to fail before the implementation adds the unbound shortcut state. Not-tested: Local cmux-unit execution because the user explicitly forbade direct xcodebuild.
Cmd-D was registered in multiple layers: KeyboardShortcutSettings covered the app-level matcher, while SwiftUI/AppKit menu equivalents and Ghostty's default super+d split binding could still create a split after the setting was cleared or remapped. Persist an explicit unbound shortcut, expose Clear in the keyboard shortcut recorder, source split menus from KeyboardShortcutSettings, suppress stale default menu equivalents, and remove Ghostty's default split fallbacks so KeyboardShortcutSettings is the single owner of split shortcuts. The reload helper also now selects a target-matching Zig binary for Ghostty helper builds, because the required reload command otherwise picked the x86 Zig from /usr/local on this arm64 host and failed before launch. Constraint: Direct xcodebuild is forbidden; build verification uses reload.sh only Rejected: Treat clear as reset | reset restores Cmd-D and keeps terminal apps from receiving it Rejected: Only suppress the Swift app shortcut matcher | stale AppKit menu equivalents and Ghostty super+d still split Rejected: Leave Ghostty defaults active | forwarded Cmd-D still triggers Ghostty new_split:right Confidence: high Scope-risk: moderate Tested: git diff --check; bash -n scripts/reload.sh; bash -n scripts/build-ghostty-cli-helper.sh; Localizable.xcstrings JSON parse; cmux settings schema JSON parse; ./scripts/reload.sh --tag issue-2524-cmd-d-rebindable --launch; debug socket remap Cmd+J kept Cmd+D pane count at 15 and Cmd+J increased it to 16; debug socket clear kept Cmd+D pane count at 16; debug socket default restored Cmd+D split to 17 panes Not-tested: Local cmux-unit via xcodebuild because explicitly forbidden; SwiftPM test path cannot build app target because CMUXDebugLog is not in Package.swift; XCUITests are CI-only; Settings UI automation blocked by macOS Apple Events permission -1743 Related: #2524
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThis PR makes the hardcoded Cmd+D split-right shortcut configurable by implementing stale menu shortcut suppression logic. Events matching outdated menu shortcuts are forwarded directly to the ghostty terminal view instead of the menu system, while supporting unbinding and remapping the shortcut through configuration. Changes
Sequence Diagram(s)sequenceDiagram
participant AppKit as NSApplication/NSWindow
participant Delegate as AppDelegate
participant GhosttyView as ghosttyView
participant Menu as NSMenu
AppKit->>Delegate: sendEvent(keyDown) / performKeyEquivalent
Delegate->>Delegate: shouldSuppressStaleCmuxMenuShortcut(event)?
alt Stale Cmd-key menu shortcut detected
Delegate->>GhosttyView: Forward keyDown event
Delegate-->>AppKit: Return early (suppress menu routing)
else Current or non-Cmd shortcut
Delegate->>AppKit: Proceed with normal routing
AppKit->>Menu: Route to responder chain
end
GhosttyView->>GhosttyView: Process keyDown for terminal app
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Review rate limit: 6/8 reviews remaining, refill in 14 minutes and 22 seconds.Comment |
Bring the PR branch up to date with origin/main and resolve the only content conflict in AppDelegate.swift by preserving both main's minimal-mode/right-sidebar routing and the stale shortcut suppression needed for cleared or remapped Cmd-D passthrough. Constraint: User explicitly requested pulling from origin main and resolving merge conflicts Rejected: Rebase branch | pull request already uses the required two visible test/fix commits, and the user specifically requested pull from origin main Confidence: high Scope-risk: moderate Tested: git diff --check; bash -n scripts/reload.sh; bash -n scripts/build-ghostty-cli-helper.sh; Localizable.xcstrings JSON parse; cmux settings schema JSON parse; no unresolved merge paths Not-tested: Local xcodebuild tests remain forbidden by task instruction; final tagged reload to follow Related: #2524
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@Sources/AppDelegate.swift`:
- Around line 11884-11907: The function shouldSuppressStaleCmuxMenuShortcut is
currently returning true even when a non-terminal responder (e.g., browser
address bar) is focused; change it so it only considers stale shortcut
suppression when the current key responder belongs to a terminal view: check the
key window's firstResponder (or walk the responder chain) and confirm it is a
GhosttyNSView or a descendant/owned responder before running the existing
shortcut-matching loops; if the responder is not terminal-owned, return false
early. Use symbols mentioned (shouldSuppressStaleCmuxMenuShortcut,
matchesKeyboardShortcutEvent, KeyboardShortcutSettings.Action, GhosttyNSView) to
locate and implement the responder-type check.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: 8e86f827-238f-4465-b385-155f200f8a37
📒 Files selected for processing (12)
Resources/Localizable.xcstringsSources/AppDelegate.swiftSources/CmuxConfig.swiftSources/GhosttyTerminalView.swiftSources/KeyboardShortcutSettings.swiftSources/TerminalController.swiftSources/cmuxApp.swiftcmuxTests/AppDelegateShortcutRoutingTests.swiftscripts/build-ghostty-cli-helper.shscripts/reload.shweb/app/[locale]/docs/keyboard-shortcuts/page.tsxweb/data/cmux-settings.schema.json
Greptile SummaryThis PR makes Cmd+D's split shortcut fully rebindable by introducing The overall approach is well-structured: the Confidence Score: 4/5Safe to merge; all three findings are P2 — no runtime breakage in the changed paths. No P0 or P1 issues found. The core suppression logic is sound and is covered by three targeted regression tests. Two P2s flag a silent socket-API semantic change and a test that always fails in non-Debug builds. The third P2 notes a documentation gap around the
Important Files Changed
Sequence DiagramsequenceDiagram
participant User
participant NSApp as NSApplication.sendEvent
participant AppDelegate
participant NSWindow as NSWindow.performKeyEquivalent
participant Menu as NSMenu (stale Cmd+D)
participant GhosttyView as GhosttyNSView
User->>NSApp: Cmd+D keyDown
NSApp->>AppDelegate: shouldSuppressStaleCmuxMenuShortcut?
Note over AppDelegate: Check 1: current shortcut matches? NO (cleared/remapped)<br/>Check 2: default shortcut matches? YES (Cmd+D was default)
AppDelegate-->>NSApp: true (suppress)
alt Ghostty view focused
NSApp->>GhosttyView: keyDown(Cmd+D) forwarded to terminal
else No Ghostty view
NSApp-->>User: event dropped
end
Note over NSApp: Never reaches cmux_applicationSendEvent
rect rgb(200, 240, 200)
Note over NSWindow: Defense-in-depth path
NSWindow->>AppDelegate: shouldSuppressStaleCmuxMenuShortcut?
AppDelegate-->>NSWindow: true
alt Ghostty view is first responder
NSWindow->>GhosttyView: keyDown(Cmd+D)
NSWindow-->>User: return true
else Non-Ghostty first responder
NSWindow-->>User: return false
end
end
|
CI failed in workflow-guard-tests because the shortcut regression tests and pass-through fix intentionally grew several already-budgeted Swift files. Refresh the checked-in budget with scripts/swift_file_length_budget.py so the guard reflects the accepted debt from this PR and the current origin/main merge. Constraint: User requested making the PR pass CI/CD after merging origin/main Rejected: Remove the regression coverage | the added tests are the proof that clearing/remapping Cmd-D no longer splits Confidence: high Scope-risk: narrow Tested: python3 scripts/swift_file_length_budget.py --budget .github/swift-file-length-budget.tsv --write-budget; python3 scripts/swift_file_length_budget.py --budget .github/swift-file-length-budget.tsv; ./tests/test_ci_swift_file_length_budget.sh; git diff --check Not-tested: xcodebuild/unit tests locally remain forbidden by task instruction Related: #3338
Bring the Cmd-D shortcut PR branch up to current origin/main and resolve the generated Swift file length budget conflict by regenerating .github/swift-file-length-budget.tsv from the merged tree. Constraint: User requested resolving PR #3338 merge conflicts Rejected: Hand-edit budget conflict markers | the budget is generated from current Swift line counts Confidence: high Scope-risk: moderate Tested: git diff --check; no unresolved merge paths; no real conflict marker lines; python3 scripts/swift_file_length_budget.py --budget .github/swift-file-length-budget.tsv; ./tests/test_ci_swift_file_length_budget.sh; Localizable.xcstrings JSON parse; cmux settings schema JSON parse; bash -n scripts/reload.sh; bash -n scripts/build-ghostty-cli-helper.sh Not-tested: Local xcodebuild/unit tests remain forbidden by task instruction; final tagged reload to follow Related: #3338
Bring PR #3338 up to current origin/main. Main moved the keyboard shortcut recorder and settings rows into dedicated files, so this resolution adopts that split while preserving this PR's Cmd-D passthrough behavior, Ghostty default unbind overlay, debug clear shortcut path, and settings JSON clear/unbound compatibility. Constraint: User requested resolving GitHub PR merge conflicts Rejected: Keep old recorder definitions in KeyboardShortcutSettings/cmuxApp | main now owns the split-file implementation and duplicate SwiftUI types would conflict Confidence: high Scope-risk: moderate Tested: git diff --check; no unresolved merge paths; no true conflict markers; python3 scripts/swift_file_length_budget.py --budget .github/swift-file-length-budget.tsv; ./tests/test_ci_swift_file_length_budget.sh; Localizable.xcstrings JSON parse; cmux settings schema JSON parse; bash -n scripts/reload.sh; bash -n scripts/build-ghostty-cli-helper.sh Not-tested: Local xcodebuild/unit tests remain forbidden by task instruction; final tagged reload to follow Related: #3338
There was a problem hiding this comment.
♻️ Duplicate comments (1)
Sources/AppDelegate.swift (1)
12368-12391:⚠️ Potential issue | 🟠 Major | ⚡ Quick winLimit stale-shortcut suppression to terminal-owned responders.
This helper can still return
truewhile a browser/address-bar/non-terminal responder is focused. Downstream, Line 13512 then returns early, finds noGhosttyNSView, and drops the key instead of letting the current responder handle it. Clearing or remappingCmd+Dcan still swallow the shortcut outside terminal panes.Suggested fix
func shouldSuppressStaleCmuxMenuShortcut(event: NSEvent) -> Bool { guard event.type == .keyDown else { return false } if event.window is NSPanel || NSApp.keyWindow is NSPanel || NSApp.modalWindow != nil || NSApp.keyWindow?.attachedSheet != nil { return false } let flags = event.modifierFlags .intersection(.deviceIndependentFlagsMask) .subtracting([.numericPad, .function, .capsLock]) guard flags.contains(.command) else { return false } + let responder = event.window?.firstResponder + ?? NSApp.keyWindow?.firstResponder + ?? NSApp.mainWindow?.firstResponder + guard cmuxOwningGhosttyView(for: responder) != nil else { return false } for action in KeyboardShortcutSettings.Action.allCases where action != .showHideAllWindows { let currentShortcut = KeyboardShortcutSettings.shortcut(for: action) if matchesKeyboardShortcutEvent(event, action: action, shortcut: currentShortcut) { return false🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/AppDelegate.swift` around lines 12368 - 12391, shouldSuppressStaleCmuxMenuShortcut currently can return true even when a non-terminal responder (e.g., browser/address-bar) is focused, causing terminal shortcuts to be swallowed; before concluding suppression (the loop that checks action.defaultShortcut), verify the focused responder belongs to a terminal view: inspect the key window's firstResponder (or event.window?.firstResponder) and ensure it is a GhosttyNSView or has a GhosttyNSView ancestor; if not, return false. Update shouldSuppressStaleCmuxMenuShortcut to perform this responder ownership check (using GhosttyNSView type checks or a small helper that walks responder.superview/superchain) before returning true while still using matchesKeyboardShortcutEvent and KeyboardShortcutSettings.Action.defaultShortcut for matching.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@Sources/AppDelegate.swift`:
- Around line 12368-12391: shouldSuppressStaleCmuxMenuShortcut currently can
return true even when a non-terminal responder (e.g., browser/address-bar) is
focused, causing terminal shortcuts to be swallowed; before concluding
suppression (the loop that checks action.defaultShortcut), verify the focused
responder belongs to a terminal view: inspect the key window's firstResponder
(or event.window?.firstResponder) and ensure it is a GhosttyNSView or has a
GhosttyNSView ancestor; if not, return false. Update
shouldSuppressStaleCmuxMenuShortcut to perform this responder ownership check
(using GhosttyNSView type checks or a small helper that walks
responder.superview/superchain) before returning true while still using
matchesKeyboardShortcutEvent and KeyboardShortcutSettings.Action.defaultShortcut
for matching.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: a4b4aa90-0786-4286-818c-4597bce3aac4
⛔ Files ignored due to path filters (1)
.github/swift-file-length-budget.tsvis excluded by!**/*.tsv
📒 Files selected for processing (2)
Sources/AppDelegate.swiftSources/KeyboardShortcutSettings.swift
🚧 Files skipped from review as they are similar to previous changes (1)
- Sources/KeyboardShortcutSettings.swift
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 6e0278d. Configure here.
| action: KeyboardShortcutSettings.Action | ||
| ) -> Int? { | ||
| let shortcut = KeyboardShortcutSettings.shortcut(for: action) | ||
| guard !shortcut.isUnbound else { return nil } |
There was a problem hiding this comment.
Redundant isUnbound guard check is dead code
Low Severity
The !shortcut.isUnbound condition in the guard at line 12201 is redundant because the early guard at line 12193 already returns nil if shortcut.isUnbound is true. Any execution reaching line 12201 is guaranteed to have isUnbound == false, making that part of the compound guard dead code that adds confusion about whether the property could change between the two checks.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 6e0278d. Configure here.
…ngs (#2524) (#3338) * Prove cleared Cmd-D split shortcut should pass through The regression captures the desired shortcut contract before the implementation exists: clearing the split-right shortcut must make Cmd-D fall through instead of creating another pane. Constraint: Local direct xcodebuild is forbidden for this task; verification runs through allowed reload/CI paths. Confidence: medium Scope-risk: narrow Tested: Not run locally; this test-only commit is expected to fail before the implementation adds the unbound shortcut state. Not-tested: Local cmux-unit execution because the user explicitly forbade direct xcodebuild. * Make cleared split shortcuts persist as pass-through Cmd-D was registered in multiple layers: KeyboardShortcutSettings covered the app-level matcher, while SwiftUI/AppKit menu equivalents and Ghostty's default super+d split binding could still create a split after the setting was cleared or remapped. Persist an explicit unbound shortcut, expose Clear in the keyboard shortcut recorder, source split menus from KeyboardShortcutSettings, suppress stale default menu equivalents, and remove Ghostty's default split fallbacks so KeyboardShortcutSettings is the single owner of split shortcuts. The reload helper also now selects a target-matching Zig binary for Ghostty helper builds, because the required reload command otherwise picked the x86 Zig from /usr/local on this arm64 host and failed before launch. Constraint: Direct xcodebuild is forbidden; build verification uses reload.sh only Rejected: Treat clear as reset | reset restores Cmd-D and keeps terminal apps from receiving it Rejected: Only suppress the Swift app shortcut matcher | stale AppKit menu equivalents and Ghostty super+d still split Rejected: Leave Ghostty defaults active | forwarded Cmd-D still triggers Ghostty new_split:right Confidence: high Scope-risk: moderate Tested: git diff --check; bash -n scripts/reload.sh; bash -n scripts/build-ghostty-cli-helper.sh; Localizable.xcstrings JSON parse; cmux settings schema JSON parse; ./scripts/reload.sh --tag issue-2524-cmd-d-rebindable --launch; debug socket remap Cmd+J kept Cmd+D pane count at 15 and Cmd+J increased it to 16; debug socket clear kept Cmd+D pane count at 16; debug socket default restored Cmd+D split to 17 panes Not-tested: Local cmux-unit via xcodebuild because explicitly forbidden; SwiftPM test path cannot build app target because CMUXDebugLog is not in Package.swift; XCUITests are CI-only; Settings UI automation blocked by macOS Apple Events permission -1743 Related: manaflow-ai/cmux#2524 * Refresh Swift length budget for Cmd-D shortcut tests CI failed in workflow-guard-tests because the shortcut regression tests and pass-through fix intentionally grew several already-budgeted Swift files. Refresh the checked-in budget with scripts/swift_file_length_budget.py so the guard reflects the accepted debt from this PR and the current origin/main merge. Constraint: User requested making the PR pass CI/CD after merging origin/main Rejected: Remove the regression coverage | the added tests are the proof that clearing/remapping Cmd-D no longer splits Confidence: high Scope-risk: narrow Tested: python3 scripts/swift_file_length_budget.py --budget .github/swift-file-length-budget.tsv --write-budget; python3 scripts/swift_file_length_budget.py --budget .github/swift-file-length-budget.tsv; ./tests/test_ci_swift_file_length_budget.sh; git diff --check Not-tested: xcodebuild/unit tests locally remain forbidden by task instruction Related: manaflow-ai/cmux#3338


Closes #2524
Root cause
Cmd+D was not owned by a single shortcut layer.
KeyboardShortcutSettingshad split actions, but Cmd+D could still be consumed by stale SwiftUI/AppKit menu equivalents after remap/clear, and forwarding Cmd+D into Ghostty still hit Ghostty's defaultsuper+d=new_split:rightbinding. That meant terminal apps never saw Cmd+D even when the cmux binding was cleared or moved to Cmd+J.Fix
StoredShortcut.unbound) and show it asNonein the shortcut recorder.KeyboardShortcutSettingsvalue instead of stale defaults.super+dandsuper+shift+dsplit fallbacks so cmux-owned split shortcuts are governed only byKeyboardShortcutSettings.settings.jsonschema/docs.Test commits
24c32365- failing regression test for cleared Cmd+D pass-throughba39f7e6- implementation fixVerification
git diff --checkbash -n scripts/reload.shbash -n scripts/build-ghostty-cli-helper.shpython3 -m json.tool Resources/Localizable.xcstrings >/dev/nullpython3 -m json.tool web/data/cmux-settings.schema.json >/dev/null./scripts/reload.sh --tag issue-2524-cmd-d-rebindable --launchNot run locally
xcodebuild.CMUXDebugLogis not exposed throughPackage.swift.-1743; the recorder integration is covered in source and the runtime shortcut behavior was verified on the launched tagged build.Note
Medium Risk
Modifies key event routing and menu-equivalent suppression in
AppDelegate/window dispatch, which can affect global keyboard handling and terminal input. Includes Ghostty keybind overrides and config encoding changes that could impact shortcut persistence and default behavior.Overview
Ensures cmux split shortcuts (notably
Cmd+D/Cmd+Shift+D) are fully owned byKeyboardShortcutSettings: shortcuts can now be explicitly unbound and, when cleared or remapped, the old default menu equivalents are suppressed and the key event can be forwarded to the focused terminal instead of creating a split.Updates config/CLI/schema/docs to support unbinding via empty string (and tokens like
clear/none/unbound), and adjusts encoding/decoding accordingly. Ghostty configs now unbind its defaultsuper+dsplit fallbacks, context menu split items pick up the configured shortcuts, build scripts select a Zig binary matching the requested arch, and new tests cover stale-menu and cleared/remappedCmd+Drouting behavior.Reviewed by Cursor Bugbot for commit 6e0278d. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by cubic
Make the Cmd+D split shortcut fully rebindable and clearable. When cleared, Cmd+D now passes through to terminal apps instead of splitting.
Bug Fixes
KeyboardShortcutSettings; suppress stale AppKit menu equivalents in both app and window dispatch after remap/clear, and forward the event to the focused terminal.super+dandsuper+shift+dso cleared/remapped splits don’t fire.New Features
none/clear/unbound; accept action idssplit_rightandsplit_down. Docs and schema updated.Written for commit c20f6f2. Summary will update on new commits. Review in cubic
Summary by CodeRabbit
New Features
"clear","unbound","none", or empty string syntaxBug Fixes
Documentation