Skip to content

Fix unbound Cmd+Shift forwarding to terminal (#1718) - #3332

Merged
austinywang merged 4 commits into
mainfrom
issue-1718-cmd-shift-swallowed-regression
May 5, 2026
Merged

austinywang merged 4 commits into
mainfrom
issue-1718-cmd-shift-swallowed-regression

Conversation

@austinywang

@austinywang austinywang commented Apr 30, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #1718

Root cause

PR #1959 fixed the second-pass Cmd+Shift fallback by using charactersIgnoringModifiers, but a later window-level command routing path now asks the main menu first and calls Ghostty only after a menu miss. For an unbound Cmd+Shift key such as Cmd+Shift+K, Ghostty recorded the timestamp and returned false expecting AppKit to redispatch the event. When no menu item and no Ghostty binding matched, AppKit could skip that second pass, so the key was silently dropped before it reached the terminal/kitty keyboard protocol.

Fix

When performKeyEquivalentAfterMenuMiss is already on the post-menu-miss path and no Ghostty binding matched, forward the original event directly through keyDown and return true. Bound Ghostty keys still use the existing ghostty_surface_key_is_binding branch first, so keys like Cmd+Shift+J remain routed through their binding path without double dispatch.

Test structure

  • Commit 1 adds GhosttyCommandShiftForwardingTests.testUnboundCommandShiftKeyAfterMenuMissForwardsToGhosttyKeyDown, which exercises the menu-miss path with Cmd+Shift+K and expects the focused Ghostty surface to receive a press event.
  • Commit 2 adds the direct forwarding fix.

Verification

  • git diff --check
  • python3 scripts/swift_file_length_budget.py --budget .github/swift-file-length-budget.tsv
  • GitHub checks pass: tests, workflow-guard-tests, ui-regressions, tests-build-and-lag, both compat lanes, release-build, build-ghosttykit, web checks, and reviewer checks.
  • Tagged dev build launched with CMUX_SKIP_ZIG_BUILD=1 ./scripts/reload.sh --tag issue-1718-cmd-shift-swallowed-regression --launch.
  • The literal requested command with a trailing . was attempted, but this repo's reload.sh rejects . as an unknown option.
  • Manual targeted-window verification in the launched tagged app: ran cat -v in the 1718 dev terminal window and sent Cmd+Shift+K to that app process. /tmp/cmux-debug-issue-1718-cmd-shift-swallowed-regression.log recorded focus.keyRepair ... keyCode=40 mods=1179648 followed by forceRefresh ... reason=keyDown.textInput, confirming the unbound Cmd+Shift+K reached Ghostty keyDown instead of being swallowed.

Note

Medium Risk
Touches terminal key-equivalent routing, which can subtly affect shortcut handling and event dispatch order, but the change is small and covered by a focused regression test.

Overview
Fixes a regression where unbound Cmd+Shift key presses could be swallowed after the app routes through the main menu first: when performKeyEquivalentAfterMenuMiss is used and no Ghostty binding matches, the original event is now forwarded directly to keyDown and consumed.

Adds a new unit test GhosttyCommandShiftForwardingTests and wires it into the Xcode project to verify that an unbound Cmd+Shift+K reaches the terminal exactly once (including expected modifier flags).

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

Summary by CodeRabbit

  • Bug Fixes
    • Improved handling of keyboard events with Command and Shift key combinations.

@vercel

vercel Bot commented Apr 30, 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 5, 2026 9:08am
cmux-staging Building Building Preview, Comment May 5, 2026 9:08am

@coderabbitai

coderabbitai Bot commented Apr 30, 2026 •

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b7f55a01-23df-47de-a8be-2c2a3762461c

📥 Commits

Reviewing files that changed from the base of the PR and between e3a61db and 33eec9b.

📒 Files selected for processing (3)
  • GhosttyTabs.xcodeproj/project.pbxproj
  • Sources/GhosttyTerminalView.swift
  • cmuxTests/GhosttyCommandShiftForwardingTests.swift

📝 Walkthrough

Walkthrough

This PR changes GhosttyNSView's second-pass key-equivalent handling so non-Command key events on the second pass are no longer deferred: the stored timestamp is cleared, the event is redispatched to keyDown(with:), and it's reported handled. It also adds a unit test (and project entries) verifying Command+Shift forwarding.

Changes

Key-equivalent handling fix

Layer / File(s) Summary
Behavior change (core)
Sources/GhosttyTerminalView.swift
On second-pass (shouldRetryMainMenu == false) non-Command key-equivalent branch: clear lastPerformKeyEvent, call keyDown(with: event), and return true instead of storing timestamp and returning false.
Context / callers
Sources/GhosttyTerminalView.swift (near performKeyEquivalent/performKeyEquivalentAfterMenuMiss)
Adjusts how the two-pass AppKit menu-miss/redispatch flow is handled for unbound events.
Tests
cmuxTests/GhosttyCommandShiftForwardingTests.swift
New unit test validates Command+Shift key forwarding reaches Ghostty via performKeyEquivalentAfterMenuMiss calling keyDown.

Test integration

Layer / File(s) Summary
Project wiring
GhosttyTabs.xcodeproj/project.pbxproj
Adds PBXFileReference/PBXBuildFile and inserts GhosttyCommandShiftForwardingTests.swift into cmuxTests group and PBXSourcesBuildPhase.
Test source
cmuxTests/GhosttyCommandShiftForwardingTests.swift
Creates hosted TerminalSurface/NSWindow, finds GhosttyNSView, synthesizes Command+Shift "k" key event, invokes performKeyEquivalentAfterMenuMiss(with:), and asserts forwarded press captured with correct keycode and modifiers.

Sequence Diagram(s)

sequenceDiagram
  participant AppKit
  participant GhosttyNSView
  participant TerminalSurface

  AppKit->>GhosttyNSView: performKeyEquivalent(event) (first pass)
  note right of GhosttyNSView: first pass may store timestamp and return false
  AppKit->>GhosttyNSView: performKeyEquivalentAfterMenuMiss(event) (second pass)
  alt shouldRetryMainMenu == false (second pass for unbound non-Command)
    GhosttyNSView->>GhosttyNSView: clear lastPerformKeyEvent
    GhosttyNSView->>GhosttyNSView: keyDown(with: event)
    GhosttyNSView->>TerminalSurface: forward key event (GHOSTTY_ACTION_PRESS)
    GhosttyNSView-->>AppKit: return true
  else shouldRetryMainMenu == true (preserve prior behavior)
    GhosttyNSView->>GhosttyNSView: set lastPerformKeyEvent = event.timestamp
    GhosttyNSView-->>AppKit: return false
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • manaflow-ai/cmux#2971: Modifies performKeyEquivalentAfterMenuMiss behavior for menu-miss path handling (closely related to this change).
  • manaflow-ai/cmux#1959: Changes redispatch handling for Command-modified key-equivalent events; similar area of key-event routing.
  • manaflow-ai/cmux#717: Earlier changes to key-event routing and forwarding into Ghostty via keyDown(with:).

Poem

🐰 A hop, a tap, a key unbound,
Cmd+Shift found its way around.
Cleared the timestamp, sent it through,
One test to prove the path is true—
Hooray! The keystroke's homeward bound.

🚥 Pre-merge checks | ✅ 12 | ❌ 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 (12 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Fix unbound Cmd+Shift forwarding to terminal (#1718)' clearly and specifically describes the main change—resolving an issue where unbound Cmd+Shift key combinations were silently dropped instead of being forwarded to the terminal.
Description check ✅ Passed The PR description provides comprehensive context including root cause analysis, the fix explanation, test structure, and thorough verification steps. It follows the repository's template with a Summary, Testing section (implicitly covered via verification), and includes both detailed context and an automated review comment.
Linked Issues check ✅ Passed The code changes directly address issue #1718 by fixing the key-routing logic to forward unbound Cmd+Shift events to the terminal and adding a focused regression test. The modification to performKeyEquivalent ensures unbound keys reach keyDown instead of being dropped, and the test verifies this behavior.
Out of Scope Changes check ✅ Passed All changes are tightly scoped to the issue: the GhosttyTerminalView.swift modification addresses the root cause of swallowed keys, the test file validates the fix, and the project file change only registers the test. No unrelated modifications are present.
Cmux Swift Actor Isolation ✅ Passed No actor isolation violations detected. Test properly @MainActor. Production code in NSView (implicitly MainActor). TimeInterval? value type is safe. No new APIs or unsafe Sendable.
Cmux Swift Blocking Runtime ✅ Passed Production code change does not introduce blocking/timing-based synchronization. Test file uses deterministic RunLoop.run(until:) in test-only scaffolding, which is allowed per rule.
Cmux Swift Concurrency ✅ Passed No legacy async patterns introduced. The PR modifies performKeyEquivalent synchronously and adds an @MainActor XCTest without Dispatch queues, Combine, fire-and-forget Tasks, or completion handlers.
Cmux Swift @Concurrent ✅ Passed No Swift @concurrent annotation violations. All modified functions are synchronous. Test class correctly annotated @MainActor. No async work improperly leaving actor isolation. Complies with rules.
Cmux Swift File And Package Boundaries ✅ Passed +2 line bug fix to 13,385-line production file (within budget). 111-line test file in cmuxTests/. No violations; matches allowed exception for focused bug fixes to large files.
Cmux Swift Logging ✅ Passed No logging violations found. Production code changes add 2 lines with no print/NSLog/debugPrint/dump. Test file has no logging. Project file changes not subject to rules.
Cmux Swiftui State Layout ✅ Passed PR contains no new SwiftUI state patterns. Changes are AppKit NSView key routing and XCTest additions only. Pre-existing TerminalSurface ObservableObject is untouched.
Cmux Architecture Rethink ✅ Passed Small correctness fix with clear owner and explicit invariant. No timing repairs, locks, observers, or split lifecycle. Uses allowed test-only synchronization.

✏️ 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-1718-cmd-shift-swallowed-regression

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 Apr 30, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a silent key-drop bug where unbound Cmd+Shift key combinations (e.g. Cmd+Shift+K) were swallowed after the menu-miss path, because performKeyEquivalentAfterMenuMiss re-entered the timestamp-redispatch loop and returned false with no one left to redispatch the event. The fix adds a single guard at the top of the default: branch: when already on the post-menu-miss path and no Ghostty binding matched, forward the original event directly to keyDown and return true.

  • Sources/GhosttyTerminalView.swift: On the !shouldRetryMainMenu path, clear lastPerformKeyEvent and call keyDown(with: event) directly, bypassing the timestamp dance that served no purpose at this stage.
  • cmuxTests/GhosttyCommandShiftForwardingTests.swift: New XCTestCase exercises the performKeyEquivalentAfterMenuMiss path with a synthetic Cmd+Shift+K event and asserts that the Ghostty surface receives exactly one PRESS with the correct keycode, super, and shift modifiers.
  • GhosttyTabs.xcodeproj/project.pbxproj: Wires the new test file into the Xcode target.

Confidence Score: 5/5

Safe to merge — the change is minimal, confined to one branch of the key-routing function, and covered by a new regression test plus manual tagged-build verification.

The production change is a single guard that short-circuits an already-known-to-be-post-menu-miss code path; bound Ghostty keys are handled earlier in the same function and are unaffected. The new test class exercises the exact failure scenario end-to-end, and the debug log from the tagged dev build confirms the key reaches keyDown instead of being dropped.

No files require special attention.

Important Files Changed

Filename Overview
Sources/GhosttyTerminalView.swift Single-line guard added to performKeyEquivalent to short-circuit the timestamp redispatch loop when already on the post-menu-miss path; state is correctly cleared before forwarding to keyDown.
cmuxTests/GhosttyCommandShiftForwardingTests.swift New regression test class that builds a real hosted GhosttyNSView, exercises performKeyEquivalentAfterMenuMiss with a synthetic Cmd+Shift+K event, and asserts the correct key event reaches the Ghostty surface; uses @MainActor correctly throughout.
GhosttyTabs.xcodeproj/project.pbxproj Adds GhosttyCommandShiftForwardingTests.swift to both the file references and the test target sources; Xcode UUIDs follow the existing convention in the project file.

Sequence Diagram

sequenceDiagram
    participant AppKit
    participant Window as Window routing
    participant GNSView as GhosttyNSView
    participant Menu as NSApp.mainMenu
    participant Ghostty as ghostty_surface_key

    AppKit->>Window: performKeyEquivalent(Cmd+Shift+K)
    Window->>GNSView: performKeyEquivalent(shouldRetryMainMenu:true)
    GNSView->>Ghostty: ghostty_surface_key_is_binding?
    Ghostty-->>GNSView: false (unbound)
    GNSView->>GNSView: lastPerformKeyEvent = timestamp
    GNSView-->>Window: false
    Window->>Menu: performKeyEquivalent(Cmd+Shift+K)
    Menu-->>Window: false (no menu item)
    Window->>GNSView: performKeyEquivalentAfterMenuMiss(shouldRetryMainMenu:false)
    GNSView->>Ghostty: ghostty_surface_key_is_binding?
    Ghostty-->>GNSView: false (unbound)
    Note over GNSView: NEW: !shouldRetryMainMenu branch
    GNSView->>GNSView: lastPerformKeyEvent = nil
    GNSView->>GNSView: keyDown(with: event)
    GNSView-->>Window: true (event handled)
Loading

Reviews (2): Last reviewed commit: "Tighten the Cmd+Shift forwarding regress..." | Re-trigger Greptile

Comment thread cmuxTests/CJKIMEInputTests.swift Outdated
Comment thread cmuxTests/CJKIMEInputTests.swift Outdated

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit b79c171. Configure here.

Comment thread Sources/GhosttyTerminalView.swift Outdated
Adds a focused AppKit-hosted Ghostty regression test for the after-menu-miss route. The test drives an unbound Cmd+Shift+K NSEvent through performKeyEquivalentAfterMenuMiss and expects Ghostty keyDown observation instead of an AppKit swallow.

Constraint: Regression tests must exercise runtime behavior rather than source text.

Confidence: high

Scope-risk: narrow

Tested: python3 scripts/swift_file_length_budget.py --budget .github/swift-file-length-budget.tsv

Not-tested: Local XCTest run skipped per repository testing policy; CI owns the executable test run.
When the window-level router has already missed the main menu, Ghostty should not wait for AppKit to redispatch an unbound command-modified event. The fallback now forwards the original event to keyDown immediately on the menu-miss path while keeping the existing two-pass redispatch for callers that still need to retry the main menu.

Constraint: Bound Ghostty actions must stay on the existing ghostty_surface_key_is_binding path.

Rejected: Keep relying on timestamp replay after a menu miss | AppKit can drop unbound Cmd+Shift events without a second pass.

Confidence: high

Scope-risk: narrow

Tested: git diff --check; python3 scripts/swift_file_length_budget.py --budget .github/swift-file-length-budget.tsv

Not-tested: Local XCTest and manual app verification pending after tagged reload/launch.

@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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmuxTests/GhosttyCommandShiftForwardingTests.swift`:
- Around line 68-69: The test ignores the Bool result of
window.makeFirstResponder(surfaceView) so focus assignment failures are hidden;
update the setup to capture and assert that makeFirstResponder(surfaceView)
returns true (or call XCTAssertTrue(...) with a clear failure message) before
asserting XCTAssertNotNil(surfaceView.terminalSurface) so a failed handoff fails
the test; reference window.makeFirstResponder and surfaceView.terminalSurface
when making the assertion.
- Around line 71-77: The current observer captures only the last matching
keyEvent via forwardedKeyEvent, which hides duplicate dispatches; add an Int
counter (e.g., forwardedPressCount) alongside forwardedKeyEvent in the
GhosttyNSView.debugGhosttySurfaceKeyEventObserver closure, increment it when the
guard condition (keyEvent.action == GHOSTTY_ACTION_PRESS && keyEvent.keycode ==
40) matches, store the first matching event if forwardedKeyEvent is nil, and at
the end of the test assert forwardedPressCount == 1 (and that forwardedKeyEvent
is non-nil); update both occurrences around
GhosttyNSView.debugGhosttySurfaceKeyEventObserver (the block using
previousKeyEventObserver and forwardedKeyEvent) and the second similar block at
lines 97-101.
🪄 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: 38b95544-8658-4c98-a51b-31f181acf2f3

📥 Commits

Reviewing files that changed from the base of the PR and between b79c171 and e3a61db.

📒 Files selected for processing (3)
  • GhosttyTabs.xcodeproj/project.pbxproj
  • Sources/GhosttyTerminalView.swift
  • cmuxTests/GhosttyCommandShiftForwardingTests.swift

Comment thread cmuxTests/GhosttyCommandShiftForwardingTests.swift Outdated
Comment thread cmuxTests/GhosttyCommandShiftForwardingTests.swift
austinywang and others added 2 commits May 5, 2026 02:02
Merged the current main branch into the PR branch and resolved the project-file conflict by preserving both new test registrations. The Swift routing fix stayed intact: terminal-focused command events still go to the app menu first, then Ghostty owns delivery after a menu miss.

Constraint: PR branch must preserve unbound Cmd+Shift terminal forwarding while accepting main's project structure

Confidence: high

Scope-risk: narrow

Tested: plutil -lint GhosttyTabs.xcodeproj/project.pbxproj; git diff --check; conflict-marker scan on resolved files

Not-tested: Local XCTest skipped per repository policy; CI will verify after push

Co-authored-by: OmX <omx@oh-my-codex.dev>
The review feedback was correct that capturing only the last Ghostty key event would miss duplicate dispatches. The regression now counts matching press events, preserves the first observed key payload, asserts one delivery, and verifies first-responder setup before exercising the menu-miss path.

Constraint: Local XCTest is owned by CI for this repo

Rejected: Leave duplicate dispatch uncovered | the bug class is specifically about terminal key delivery guarantees

Confidence: high

Scope-risk: narrow

Tested: git diff --check; python3 scripts/swift_file_length_budget.py --budget .github/swift-file-length-budget.tsv

Not-tested: Local XCTest skipped per repository policy

Co-authored-by: OmX <omx@oh-my-codex.dev>
@austinywang
austinywang merged commit 25b0b53 into main May 5, 2026
36 checks passed

This branch was successfully deployed

1 active deployment
Preview – cmux — 33eec9b4 Deployed May 5, 2026 by vercel[bot]
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.

Unbound Cmd+Shift+<key> key combinations are silently swallowed, they never reach terminal/kitty protocol

1 participant