Skip to content

Fix focus history shortcut rebinding - #8853

Merged
austinywang merged 18 commits into
mainfrom
issue-8849-focus-history-rebind
Jul 24, 2026
Merged

austinywang merged 18 commits into
mainfrom
issue-8849-focus-history-rebind

Conversation

@austinywang

@austinywang austinywang commented Jul 24, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • make Focus Back and Focus Forward honor rebound shortcuts through the live configured dispatcher
  • stop the retired Cmd+[ and Cmd+] defaults from matching after rebinding
  • preserve Option-generated text when no configured cmux shortcut owns the event
  • cover Option, Shift+Option, Command+Shift, Control, and Shift+Option+Control rebinds

Fixes #8849

Root cause

The recorder, JSON store, settings reload, menu updates, titlebar actions, and KeyboardShortcutSettings.shortcut(for:) lookup all reflected the rebound shortcut correctly. The event died in the shared runtime routing path before the focus-history action block:

  1. AppDelegate.handleCustomShortcut returned false from shouldBypassPrintableOptionTextForShortcutRouting for printable Option-only bindings.
  2. ShortcutStroke.matches(event:) independently rejected that same event.
  3. NSWindow.cmux_performKeyEquivalent then forwarded the event directly to Ghostty, WebKit, or the text responder without giving the complete configured dispatcher a chance.

That made settings accept bindings such as Option+Y while runtime matching could never own them. The old defaults are not cached; after a rebind they cease matching as expected.

Fix

ShortcutStroke matching is now a pure comparison against the live configured shortcut. The shared AppDelegate dispatcher no longer rejects printable Option input before checking actions. At the NSWindow fallback chokepoint, configured shortcuts get first chance; only unmatched Option input is forwarded as text. This applies to all app-owned actions in the dispatcher rather than special-casing focus history.

Regression proof

The commits intentionally preserve the required red/green structure:

  1. a6e7e5b adds the failing focus-history routing regression test only. On main, the Option and Shift+Option assertions fail before navigation reaches focusHistoryBack or focusHistoryForward.
  2. dd92faa changes routing ownership and updates the pre-existing Option-text expectation.

The regression exercises both directions through AppDelegate, verifies new live bindings navigate history, and verifies Cmd+[ / Cmd+] no longer match or dispatch after rebinding.

Local validation

  • git diff --check
  • Swift frontend parse of all 11 edited Swift files
  • scripts/lint-pbxproj-test-wiring.sh (577 test files)
  • scripts/check-workspace-package-groups.py --check
  • scripts/check-package-resolved-policy.py
  • Focus-history/live-dispatch CI: run 30088253728, all 6 tests passed
  • File Preview marked-text CI: run 30088928656, passed

The requested scripts/swift_file_length_budget.py is absent on current main; no budget TSVs were changed or generated. A diagnostic run of the unchanged current-main TextBox IME layout suite remains runner-failing outside this patch, so its auxiliary responder test was removed and the suite file restored exactly to origin/main. Per task instructions, no local xcodebuild or app build was run. No user-facing strings were introduced; the localization audit found no catalog changes required.


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


Summary by cubic

Fixes Focus Back/Forward rebinds by giving configured shortcuts first pass over printable Option input while preserving IME. Normalizes recorded/stored keys to physical base keys and matches by keyCode so shifted symbols and non‑US layouts record and fire correctly; Cmd+[ / Cmd+] no longer trigger after rebinding (fixes #8849).

  • Bug Fixes
    • Respect rebound Focus Back/Forward via the live dispatcher; drop retired defaults.
    • Route configured Option shortcuts before text; forward only unmatched Option input. Defer when marked text is active in NSWindow, File Preview editor, Text Box input, and right‑sidebar mode lookup.
    • Canonicalize keys on record/load and match by physical key when a keyCode exists to avoid shifted‑symbol collisions and ensure international layout support.
    • Tests/build: add international/IME and shifted‑symbol regressions; expand focus‑history/Option‑shortcut coverage; minor fixture/import fixes.

Written for commit 02ff2ed. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes

    • Improved routing for printable Option-key shortcuts so they properly bypass/defer when a text field has marked text, preventing unintended shortcut actions and preserving the IME/composition state.
    • Ensured configured printable Option shortcuts take precedence over text-input routing when marked text is not present, including correct behavior for Option+Q.
    • Fixed focus-history shortcut handling after shortcut rebinding, with correct matching across modifier variants and window contexts.
  • Tests

    • Added/updated regression tests for marked-text precedence, shortcut routing behavior, focus history rebinding, and more accurate key-event simulation helpers.

@coderabbitai

coderabbitai Bot commented Jul 24, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Shortcut routing now checks configured shortcuts for printable Option events before text-input dispatch. Marked-text inputs defer shortcut handling, matching requires keydown events, and regression tests cover rebinding, modifier variants, window targeting, and editor behavior.

Changes

Shortcut routing

Layer / File(s) Summary
Configured shortcut routing
Sources/AppDelegate.swift, Sources/KeyboardShortcutSettings.swift, Sources/Panels/FilePreviewTextEditor.swift, Sources/TextBoxInput.swift
Printable Option events are checked against configured shortcuts before unmatched input dispatch; marked-text paths clear or bypass shortcut handling, and matching is restricted to keydown events.
Shortcut rebinding and marked-text regression coverage
cmuxTests/AppDelegateOptionDigitShortcutRoutingTests.swift, cmuxTests/AppDelegateShortcutRoutingTests.swift, cmuxTests/FilePreviewTextEditorTextKitTests.swift, cmuxTests/TextBoxIMECompositionLayoutTests.swift
Tests cover rebound focus-history shortcuts, modifier combinations, window-targeted events, Option+Q handling, marked-text preservation, and deferred text-box shortcuts.
Supporting Swift updates
Sources/ControlSurfaceResumeTarget.swift, Sources/DockSplitStore+RestoredAgentLifecycle.swift, Sources/DockSplitStore+SessionSnapshot.swift, cmuxTests/DockWorkingDirectoryInheritanceTests.swift
Resume-policy returns are reformatted, a module import and explicit type annotations are added, and a detached surface transfer initializes its restore-source workspace identifier.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant NSEvent
  participant AppDelegate
  participant ShortcutMatcher
  participant FocusedTextTarget
  NSEvent->>AppDelegate: keyDown printable Option event
  AppDelegate->>ShortcutMatcher: match configured shortcut
  ShortcutMatcher-->>AppDelegate: handled or unmatched result
  AppDelegate->>FocusedTextTarget: dispatch unmatched input when marked text is active
Loading

Possibly related issues

Possibly related PRs

Suggested reviewers: lawrencecchen


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 warning, 1 inconclusive)

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.
Cmux Swift Actor Isolation ❓ Inconclusive placeholder2 Need repo evidence before deciding.
✅ Passed checks (23 passed)
Check name Status Explanation
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 Blocking Runtime ✅ Passed The commit only changes a test file, and the diff adds no semaphores, waits, sleeps, main-queue sync, polling, or locks.
Cmux Browser Automation Off-Main ✅ Passed No browser automation routing code changed; HEAD only modifies a TextBox IME test, so the off-main browser-automation rule is not implicated.
Cmux Expensive Synchronous Load ✅ Passed Production Swift changes only adjust shortcut routing/type imports; no new RestorableAgentSessionIndex.load() or other heavy agent-history sync load is added on an interactive path.
Cmux Cache Substitution Correctness ✅ Passed The edited snapshot/history files only add imports and type annotations; no authoritative read was replaced by a cache/opportunistic value in a persisted snapshot path.
Cmux No Hacky Sleeps ✅ Passed The diff only changes a Swift test file; no non-Swift runtime code or fixed waits/timers/polling were added.
Cmux Algorithmic Complexity ✅ Passed The production edits only add bounded shortcut guards and fixed-size matcher loops; no new scans/sorts/joins over user-sized collections were introduced.
Cmux Swift Concurrency ✅ Passed PASS: The only changed Swift file is an XCTest case; it adds no new DispatchQueue, Combine, completion-handler, or fire-and-forget Task patterns, and @MainActor is a test boundary.
Cmux Swift @Concurrent ✅ Passed PASS: None of the touched Swift files add @concurrent or nonisolated async; the edits are synchronous or @MainActor UI-bound.
Cmux Swift Package Boundaries ✅ Passed Edited Swift is app/AppKit glue and tests; reusable shortcut value logic already lives in CmuxSettings, and no new package-worthy domain logic was added.
Cmux Swiftpm Lockfiles ✅ Passed PR diff vs origin/main has no .gitignore, Package.resolved, project, or workflow changes, so the SwiftPM lockfile policy isn’t violated.
Cmux Swift Logging ✅ Passed The diff adds no print/debugPrint/dump/NSLog/Logger changes in production Swift, and the logging rule only flags diff-introduced or materially changed logging.
Cmux User-Facing Error Privacy ✅ Passed Current commit only changes a test file; no production user-facing errors or alerts were added, so the privacy rule isn’t triggered.
Cmux Full Internationalization ✅ Passed HEAD changes only cmuxTests/TextBoxIMECompositionLayoutTests.swift; no production user-facing text, catalogs, or web locale assets were modified.
Cmux Swiftui State Layout ✅ Passed Diff only adds AppKit NSTextView shortcut guards/tests; no new ObservableObject/@Published/GeometryReader/lazy-row store refs or render-time state writes were introduced.
Cmux Architecture Rethink ✅ Passed Diff only tightens shared shortcut routing around marked-text invariants; no sleeps, polling, locks, observers, or split ownership were introduced.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed Only a test-only NSWindow fixture changed; no user-visible cmux window/controller, identifier, or close-shortcut routing was introduced or worsened.
Cmux Source Artifacts ✅ Passed Only changed path is a hand-written Swift test source file; no logs, caches, build output, temp dirs, or other source artifacts were added.
Cmux No Test Or Debug Seam In Production Source ✅ Passed Production diffs only change routing guards and type annotations; no new #if DEBUG blocks, ForTesting members, or widened accessors were added.
Cmux No Ambient Global State ✅ Passed Only cmuxTests/TextBoxIMECompositionLayoutTests.swift changed in HEAD^..HEAD; no production Swift ambient state was added.
Title check ✅ Passed The title clearly summarizes the main change: fixing focus history shortcut rebinding.
Description check ✅ Passed The description covers summary, root cause, fix, and validation; only some template sections are missing.
✨ 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-8849-focus-history-rebind

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.

@greptile-apps

greptile-apps Bot commented Jul 24, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes focus-history shortcut rebinding (#8849) by routing configured shortcuts through the live dispatcher before Option-generated text input, and by making ShortcutStroke.matches use the recorded key code as the primary matching signal when present.

  • Matching overhaul: ShortcutStroke.matches(keyCode:…) gains a key-code fast path — when the stored stroke has a keyCode, matching is a direct equality check that ignores layout character variants, making rebound Option shortcuts (e.g. Option+Y) reliably fire regardless of what character Option+Y produces on the active layout.
  • Key canonicalization: A new ShortcutKeyCanonicalization module normalizes shifted-symbol keys to physical base keys at record time, conflict-check time, and JSON load time, so Cmd+Shift+< is stored as Cmd+Shift+, and matches and conflicts correctly.
  • NSWindow routing reorder: In NSWindow.cmux_performKeyEquivalent, configured shortcuts now get first priority over text-input forwarding for printable Option events, with an explicit marked-text guard to preserve IME composition; the same marked-text guard is added to handleCustomShortcut, rightSidebarModeShortcut, FilePreviewTextEditor, and TextBoxInput.

Confidence Score: 5/5

The changes are well-scoped to the shortcut routing layer, covered by intentional regression tests, and do not touch persistence schemas or user data.

The routing overhaul is thoroughly tested — six new tests across AppDelegate, FilePreviewTextEditor, TextBoxInput, and the recorder cover Option, Shift+Option, Cmd+Shift, Ctrl, and Shift+Opt+Ctrl rebinds as well as marked-text precedence. The key-code fast path is a pure simplification with no new state. The only observation is a style point in the new canonicalization module.

ShortcutKeyCanonicalization.swift — the two public top-level functions are a minor style point; no correctness concerns in any file.

Important Files Changed

Filename Overview
Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutKeyCanonicalization.swift New file providing physical-key normalization for recording and load-time canonicalization. Logic is correct; two public top-level functions should be namespaced on a type rather than left as global module API.
Sources/KeyboardShortcutSettings.swift Removes the blanket printable-Option bypass from ShortcutStroke.matches(event:) and adds a clean key-code fast path in matches(keyCode:…); delegates recordableKeyParts to the new canonical function. Logic is sound.
Sources/AppDelegate.swift Restructures Option-text routing: removes shouldBypassPrintableOptionTextForShortcutRouting, adds marked-text guards at the handleCustomShortcut and rightSidebarModeShortcut entry points, and in NSWindow fallback gives configured shortcuts first priority before forwarding unmatched Option input to text responders.
Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutStroke.swift Adds canonicalized() helper that normalizes the stored key via canonicalShortcutKey; preserves keyCode through the copy. Correct and minimal.
Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/StoredShortcut.swift Adds canonicalized() that delegates to both strokes. No issues.
Sources/KeyboardShortcutSettingsFileStore.swift Applies canonicalShortcutKey at load time so hand-written bindings with shifted-symbol keys normalise to physical-base keys on first read. Correct.
Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Bindings/ShortcutListModel.swift Canonicalizes recorded strokes before conflict-checking and persisting; applies to both single-stroke assign and chord assign. Correct.
Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Rows/ShortcutRecorderView.swift Replaces raw charactersIgnoringModifiers.lowercased() with recordedShortcutKey(...), ensuring shifted-symbol keys record as physical base keys at recording time.
Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/ShortcutConflict.swift Canonicalizes both strokes before the numbered-aware conflict check, preventing false positives from shifted-symbol representations.
Sources/Panels/FilePreviewTextEditor.swift Adds early-return when the editor text view has marked text and the event is a printable Option input, preventing chord-state corruption during IME composition.
Sources/TextBoxInput.swift Same marked-text guard added in TextBoxInputTextView's shortcut handler as in FilePreviewTextEditor.
cmuxTests/AppDelegateOptionDigitShortcutRoutingTests.swift Adds three new regression tests: focus-history rebinding routes live shortcuts and drops retired defaults, modifier-variant coverage (Cmd+Shift, Ctrl, Shift+Opt+Ctrl), and marked-text precedence over configured printable-Option shortcuts.
cmuxTests/AppDelegateShortcutRoutingTests.swift Inverts the Option+Q test assertion to reflect new behavior: configured Option shortcuts now win over text-input routing when no marked text is active.
cmuxTests/FilePreviewTextEditorTextKitTests.swift Adds a regression test for IME marked-text preservation when a printable Option shortcut fires in the file-preview editor; also fixes the keyEvent helper to accept a separate charactersIgnoringModifiers argument.
cmuxTests/KeyboardShortcutContextSwiftTests.swift Adds a unit test verifying that a shortcut with a stored keyCode matches by keyCode across different layout characters, confirming the new fast-path in ShortcutStroke.matches.
Packages/macOS/CmuxSettingsUI/Tests/CmuxSettingsUITests/ShortcutRecorderViewTests.swift Adds two tests: Cmd+Shift+< is detected as a collision with reloadConfiguration (because < canonicalizes to , + Shift), and an unmapped symbol key (§, keyCode 10) records and stores correctly through the new path.

Sequence Diagram

sequenceDiagram
    participant AppKit
    participant Win as NSWindow.cmux_performKeyEquivalent
    participant Del as AppDelegate.handleCustomShortcut
    participant Resp as Text Responder (Ghostty/WebKit/NSTextView)

    AppKit->>Win: keyDown (Option+Y)
    Win->>Win: shortcutRoutingShouldBypassForPrintableOptionText?
    alt not printable Option (e.g. Cmd+Y)
        Win->>Del: standard shortcut routing path
        Del-->>Win: handled / not handled
    else printable Option event
        Win->>Win: browserWebKitKeyDownReentry? → bail if true
        Win->>Win: firstResponderHasMarkedText?
        alt marked text active
            Win->>Resp: cmuxForceDispatchKeyDownOnce("unmatched Option input")
        else no marked text
            Win->>Del: handleConfiguredShortcutKeyEquivalent
            Del->>Del: browserResponderHasMarkedText? → clearChordState + return false if true
            Del->>Del: ShortcutStroke.matches(keyCode:…)
            note over Del: Fast path: if self.keyCode present,<br/>return keyCode == recordedKeyCode
            alt configured shortcut matched
                Del-->>Win: true (consumed)
            else no match
                Win->>Resp: cmuxForceDispatchKeyDownOnce("unmatched Option input")
            end
        end
    end
Loading

Reviews (11): Last reviewed commit: "Match recorded shortcuts by physical key" | Re-trigger Greptile

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Sources/AppDelegate.swift (1)

17057-17076: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Guard the configurable Option shortcut handler during IME composition.

firstResponderHasMarkedText is available before this branch, and the existing Ghostty composition guard a few lines below relies on ghosttyView.hasMarkedText(). This new bypass calls handleConfiguredShortcutKeyEquivalent(event) first for any printable-Option event, so a configured shortcut can consume an in-progress Option diacritic/composition before that guard runs. Gate this shortcut check on !firstResponderHasMarkedText (or ghosttyView.hasMarkedText() when Ghostty is the responder), matching the existing IME guard.

🤖 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 `@Sources/AppDelegate.swift` around lines 17057 - 17076, Guard the
handleConfiguredShortcutKeyEquivalent call within the
shortcutRoutingShouldBypassForPrintableOptionText branch so it runs only when no
IME composition is active, using firstResponderHasMarkedText or the
responder-specific Ghostty hasMarkedText check already used nearby. Preserve the
existing text-input dispatch and re-entry behavior while preventing configured
shortcuts from consuming in-progress composition input.
🤖 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.

Outside diff comments:
In `@Sources/AppDelegate.swift`:
- Around line 17057-17076: Guard the handleConfiguredShortcutKeyEquivalent call
within the shortcutRoutingShouldBypassForPrintableOptionText branch so it runs
only when no IME composition is active, using firstResponderHasMarkedText or the
responder-specific Ghostty hasMarkedText check already used nearby. Preserve the
existing text-input dispatch and re-entry behavior while preventing configured
shortcuts from consuming in-progress composition input.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 7a581a8b-01fa-4e05-9daa-3ed122b82afb

📥 Commits

Reviewing files that changed from the base of the PR and between 4dc00ae and dd92faa.

📒 Files selected for processing (4)
  • Sources/AppDelegate.swift
  • Sources/KeyboardShortcutSettings.swift
  • cmuxTests/AppDelegateOptionDigitShortcutRoutingTests.swift
  • cmuxTests/AppDelegateShortcutRoutingTests.swift
💤 Files with no reviewable changes (1)
  • Sources/KeyboardShortcutSettings.swift

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.

Focus Forward / Focus Back (focusHistory) shortcuts can't be rebound — new shortcut recorded in Settings doesn't fire

1 participant