Skip to content

Fix ⌘[ / ⌘] focus-history vs browser Back/Forward default collision (+ regression guard) - #5878

Open
notadev99 wants to merge 2 commits into
manaflow-ai:mainfrom
notadev99:fix/default-shortcut-context-collisions
Open

notadev99 wants to merge 2 commits into
manaflow-ai:mainfrom
notadev99:fix/default-shortcut-context-collisions

Conversation

@notadev99

@notadev99 notadev99 commented Jun 11, 2026 •

Copy link
Copy Markdown

Summary

Adds a regression test that sweeps every factory-default shortcut for an unresolved chord collision, and fixes the one genuine collision it surfaced.

Two actions may share a default chord only when their effective when-contexts are disjoint (the same keystroke drives each in a different focus) or router priority picks a deterministic winner. Anything else is a shipped conflict: one action shadows the other and the duplicate chord is unreachable. This is the class behind #3467 (Rename Tab / Reload Page on ⌘R) and #5810 — but until now nothing guarded against re-introducing it.

The sweep found that focusHistoryBack / focusHistoryForward (⌘[ / ⌘]) collide with the browser pane's Back / Forward. Focus-history navigation ships .application-scoped, so while a browser panel is focused both it and browserBack / browserForward (browser-scoped) match the keystroke. The browser's own Back/Forward is the natural owner of ⌘[ / ⌘] there.

Note on #5810

The pair the reporter screenshotted — Zoom In/Out vs Markdown Viewer: Zoom In/Out on ⌘= / ⌘- — is not a runtime collision: browser-zoom is scoped to browserFocus and markdown-zoom to markdownFocus, which never co-occur, so the sweep correctly treats them as non-colliding. That half of #5810 is a Settings-display matter (no context badge on context-scoped duplicates), not a binding conflict. The sweep instead caught a real one elsewhere. The non-US-layout zoom-reachability half of #5810 is a separate keystroke-resolution concern and is out of scope here.

Changes

  1. Regression test (ShortcutDefaultCollisionTests) — group actions by their defaultShortcut, and for every shared chord assert the pair does not bindingsCollide under their defaultFocusWhenClause + priority routing, except an explicit intentionalOverlaps allowlist. The allowlist is asserted to stay tight (no stale entries).
  2. Scope fix — focusHistoryBack / focusHistoryForward move to the non-browser-panel context, mirrored in both ShortcutAction.defaultFocusWhenClause (CmuxSettings) and the app target's KeyboardShortcutContext.shortcutContext so the existing drift test stays aligned. Outside a browser panel they still navigate focus history; inside one they yield ⌘[ / ⌘] to the browser. The chord stays rebindable in Settings.

Intentional overlap (allowlisted, not changed)

groupSelectedWorkspaces ⇄ toggleReactGrab (⌘⇧G) intentionally share a chord and are resolved at runtime — the group handler propagates the event when there are no eligible workspaces, so React Grab still fires where grouping would no-op. Documented in KeyboardShortcutSettings.swift; the test allowlists exactly this pair.

Two-commit red/green

Per AGENTS.md:

  • Commit 1 (test): goes red on the focus-history ⌘[ / ⌘] collisions.
  • Commit 2 (scope fix): green.

Verification

swift test on Packages/CmuxSettings (Xcode 26 toolchain): full suite green (70 tests, 9 suites); the new suite red at commit 1 as above.

I did not build the full app target locally (it needs the GhosttyKit xcframework / VM setup from CONTRIBUTING.md). The app-target edit is a two-line addition to the existing .nonBrowserPanel case of shortcutContext, and the package and app mappings resolve to the identical when-clause, so testSettingsPackageDefaultWhenClausesMatchRuntimeShortcutContexts stays aligned by construction — flagging it for CI to confirm.

Relates to #5810.


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Summary by cubic

Fixes a default shortcut collision where ⌘[ / ⌘] for focus history conflicted with the browser pane’s Back/Forward, and adds a regression test to prevent future default-chord collisions.

  • Bug Fixes

    • Scoped focusHistoryBack / focusHistoryForward to non-browser panels in both CmuxSettings defaults and runtime KeyboardShortcutContext, so the browser owns ⌘[ / ⌘] when a browser pane is focused; behavior outside browser panels is unchanged and shortcuts remain rebindable.
  • New Features

    • Added ShortcutDefaultCollisionTests to sweep factory defaults for unresolved chord collisions, with a minimal allowlist for the intentional groupSelectedWorkspaces ⇄ toggleReactGrab overlap (⌘⇧G).

Written for commit 3b2e72e. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes

    • Improved focus history navigation shortcuts to properly defer to browser back/forward actions when a browser panel is focused, ensuring expected behavior across different interface contexts.
  • Tests

    • Added validation tests to detect and prevent unintended keyboard shortcut collisions.

notadev99 and others added 2 commits June 11, 2026 12:49
…rd collision

Two actions may share a default chord only when their effective when-contexts
are disjoint or router priority picks a deterministic winner; otherwise one
shadows the other and the duplicate is unreachable. Sweep every shared default
chord and fail on any collision outside an explicit intentional-overlap
allowlist.

Goes red on two shipped collisions: focusHistoryBack/Forward (⌘[ / ⌘],
.application-scoped) shadow the browser pane's Back/Forward (browserBack/Forward,
browser-scoped) whenever a browser panel is focused.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… ⌘] collision

focusHistoryBack/Forward shipped .application-scoped on ⌘[ / ⌘], the same
chords the browser pane binds to Back/Forward (browserBack/browserForward,
browser-scoped). While a browser panel was focused both matched the keystroke,
so the duplicate was an unresolved conflict — the browser's own Back/Forward is
the natural owner of ⌘[ / ⌘] there.

Scope focus-history navigation to .nonBrowserPanel (mirrored in both the
CmuxSettings default-context map and the app target's runtime shortcutContext so
the drift test stays aligned). Outside a browser panel it still navigates focus
history; inside one it yields the chord to the browser. The chord stays
rebindable in Settings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 11, 2026

Copy link
Copy Markdown

@notadev99 is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jun 11, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Focus-history back/forward shortcuts are explicitly configured to exclude browser and sidebar focus contexts. Changes span the action's focus-context definition, context routing in keyboard shortcut settings, and comprehensive tests verifying the routing prevents collisions and yields to browser back/forward behavior.

Changes

Focus History Shortcut Context Routing

Layer / File(s) Summary
ShortcutAction focus context definition
Packages/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction.swift
defaultFocusWhenClause now assigns focusHistoryBack and focusHistoryForward to a context excluding .browserFocus and .sidebarFocus, matching the pattern used by sendCtrlFToTerminal.
KeyboardShortcutContext routing
Sources/KeyboardShortcutContext.swift
shortcutContext explicitly maps focus-history back/forward (along with rename and Ctrl+F actions) to .nonBrowserPanel, with a comment documenting that these shortcuts must yield to browser back/forward when a browser panel is focused.
Shortcut collision and focus-context tests
Packages/CmuxSettings/Tests/CmuxSettingsTests/ShortcutDefaultCollisionTests.swift
New test suite verifies default-shortcut chords collide only for the intentional pair and that focus-history shortcuts evaluate false when browser is focused and true otherwise, confirming the routing logic prevents conflicts.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • manaflow-ai/cmux#4160: Introduces focusHistoryBack and focusHistoryForward actions; this PR adds their focus-context routing and yields-to-browser behavior.
  • manaflow-ai/cmux#5011: Modifies focus-context routing for sendCtrlFToTerminal; this PR extends the same routing pattern to focus-history shortcuts.

Poem

🐰 History hops through focus states with care,
Browser pane back/forth claim the air,
Actions yield in sidebar's gentle plea,
Tests ensure no collisions shall we see! 🎯


Important

Pre-merge checks failed

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

❌ Failed checks (1 error)

Check name Status Explanation Resolution
Cmux Full Internationalization ❌ Error PR adds user-facing display names "Focus Back" and "Focus Forward" for focusHistoryBack/Forward actions in ShortcutAction.swift without using String(localized:) API, violating full-internationaliza... Replace display names with String(localized:defaultValue:) calls: case .focusHistoryBack: return String(localized: "shortcut.focusHistoryBack.label", defaultValue: "Focus Back") and similar for focusHistoryForward.
✅ Passed checks (20 passed)
Check name Status Explanation
Title check ✅ Passed The title concisely summarizes the main change: fixing a focus-history keyboard shortcut collision with browser Back/Forward, plus adding a regression guard.
Description check ✅ Passed The description covers the required sections: a thorough summary of what changed and why, testing methodology (swift test verification), and checklist items are addressed.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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 involve only pure computed properties on value enums (ShortcutAction, KeyboardShortcutSettings.Action) with no actor isolation issues; test file is properly exempt per check rules.
Cmux Swift Blocking Runtime ✅ Passed The PR introduces no blocking or timing-based synchronization primitives; changes are purely logical routing updates to shortcut focus contexts and conditions in two production files and test-only...
Cmux Expensive Synchronous Load ✅ Passed No expensive synchronous loaders added to main actor/interactive paths; changes are shortcut routing configuration and tests only.
Cmux Cache Substitution Correctness ✅ Passed PR modifies computed properties for shortcut routing; no cache substitution or persistence operations involved. Changes are fresh enum-case-based routing logic without caching.
Cmux No Hacky Sleeps ✅ Passed PR contains only Swift code (explicitly out of scope); no TypeScript, JavaScript, shell, or build/runtime scripts modified. Swift timing covered by separate rule.
Cmux Algorithmic Complexity ✅ Passed Production code changes are simple O(1) metadata/routing switch cases with no loops or collection scans; test file is explicitly exempt per algorithmic-complexity.md.
Cmux Swift Concurrency ✅ Passed PR introduces no legacy async patterns: all changes are synchronous switch/enum logic, test assertions, and property routing. No DispatchQueue, Combine, completion handlers, or fire-and-forget Tasks.
Cmux Swift @Concurrent ✅ Passed PR contains no async functions or @concurrent annotations; changes are synchronous computed properties and tests, so check is not applicable.
Cmux Swift File And Package Boundaries ✅ Passed PR adheres to boundaries: ShortcutAction.swift (374 lines, package, +5 lines) under limits; new test (76 lines, allowed); KeyboardShortcutContext.swift (+3 lines, focused routing glue code within 5...
Cmux Swift Logging ✅ Passed No logging violations detected. The PR adds switch cases, comments, and test assertions with no print/NSLog/Logger usage or file/stdout logging.
Cmux User-Facing Error Privacy ✅ Passed No user-facing errors, alerts, or sensitive information exposed; changes are internal routing logic for keyboard shortcuts and developer-only test code with appropriate comments.
Cmux Swiftui State Layout ✅ Passed PR contains no SwiftUI files. All three changed files (ShortcutAction.swift, ShortcutDefaultCollisionTests.swift, KeyboardShortcutContext.swift) are non-SwiftUI: they import Foundation/AppKit/Testi...
Cmux Architecture Rethink ✅ Passed PR implements a small, clear correctness fix with proper ownership and invariants. Changes scope focusHistoryBack/Forward from open context to .nonBrowserPanel, making the collision with browserBac...
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR contains only keyboard shortcut context and test changes with no new/modified standalone windows, NSWindow, NSPanel, NSWindowController, SwiftUI Window, or WindowGroup code requiring cmux.* iden...
Cmux Source Artifacts ✅ Passed All changed files are legitimate hand-written Swift source code and tests in standard Sources/ and Tests/ directories; no generated artifacts, build outputs, caches, or scratch directories detected.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 Jun 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a keyboard shortcut collision where focusHistoryBack/focusHistoryForward (⌘[/⌘]) were firing alongside browserBack/browserForward while a browser panel was focused, and adds a regression-guard test suite that sweeps all factory-default chords for unresolved collisions.

  • Scope fix: focusHistoryBack/focusHistoryForward are moved from .application scope to .nonBrowserPanel in both ShortcutAction.defaultFocusWhenClause (package) and KeyboardShortcutContext.shortcutContext (app target), making the two contexts mutually exclusive and resolving the ⌘[/⌘] double-fire.
  • Regression test (ShortcutDefaultCollisionTests): groups all factory-default shortcuts by chord and asserts every shared chord is either context-disjoint or priority-resolved, with a bidirectional allowlist check ensuring the groupSelectedWorkspaces⇄toggleReactGrab intentional overlap stays documented but doesn't rot.

Confidence Score: 4/5

Safe to merge for the stated browser-collision fix; the only open question is whether silently disabling focus-history navigation while the right sidebar is focused is the intended behavior.

The browser collision fix is correct and well-tested. The only wrinkle is that reusing .nonBrowserPanel also excludes sidebar focus, which was not part of the collision being fixed and may silently remove focus-history navigation in sidebar-focused state without a competing action taking over those chords there.

ShortcutAction.swift — the sidebar-exclusion side-effect of the .nonBrowserPanel scope is worth confirming as intentional before merging.

Important Files Changed

Filename Overview
Packages/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction.swift Adds focusHistoryBack/focusHistoryForward to the defaultFocusWhenClause .nonBrowserPanel arm, scoping them out of browser-panel focus to resolve the ⌘[/⌘] collision; the fix also silently excludes sidebar-focused state, which was not part of the stated collision and may regress focus-history navigation when the right sidebar is focused.
Packages/CmuxSettings/Tests/CmuxSettingsTests/ShortcutDefaultCollisionTests.swift New regression-guard test suite sweeping all factory-default chords for unresolved collisions, with a bidirectional allowlist check and a focused test for browser-Back/Forward yielding.
Sources/KeyboardShortcutContext.swift Adds focusHistoryBack/focusHistoryForward to the .nonBrowserPanel arm of shortcutContext, keeping the runtime mapping aligned with the package-side defaultFocusWhenClause.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    KEY["⌘[ or ⌘] keystroke"]
    CTX{"Focus context?"}
    BROWSER["browserFocus = true"]
    SIDEBAR["sidebarFocus = true"]
    OTHER["Neither browser nor sidebar"]
    KEY --> CTX
    CTX -->|browser panel focused| BROWSER
    CTX -->|right sidebar focused| SIDEBAR
    CTX -->|other| OTHER
    BROWSER -->|fires| BB["browserBack / browserForward (.browserPanel)"]
    BROWSER -->|suppressed| FHB["focusHistoryBack / focusHistoryForward (suppressed)"]
    SIDEBAR -->|no handler| NOOP["no-op (previously focusHistory fired)"]
    OTHER -->|fires| FH["focusHistoryBack / focusHistoryForward (.nonBrowserPanel)"]
Loading

Reviews (1): Last reviewed commit: "Scope focusHistory Back/Forward out of b..." | Re-trigger Greptile

Comment on lines +233 to +237
case .focusHistoryBack, .focusHistoryForward:
// ⌘[ / ⌘] are the browser pane's Back/Forward. Scope workspace
// focus-history navigation out of browser panels so it yields the
// chord there instead of colliding with browserBack/browserForward.
return .and(.not(.atom(.browserFocus)), .not(.atom(.sidebarFocus)))

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 Sidebar-focus exclusion is over-broad for focus-history navigation

The new when-clause inherits .nonBrowserPanel's full condition — !browserFocus && !sidebarFocus — but the PR only motivates excluding browser focus. As a result, ⌘[ / ⌘] now silently stop navigating focus history whenever the right sidebar is focused (previously they fired at .application scope). Unlike renameTab / sendCtrlFToTerminal, there's no obvious sidebar-owned handler for these chords that would justify the exclusion, so this looks like an unintentional behavioral change: sidebar-focused users lose focus-history navigation without any competing action taking their place.

Comment on lines +69 to +75
@Test func focusHistoryNavigationYieldsToBrowserBackForward() {
for action in [ShortcutAction.focusHistoryBack, .focusHistoryForward] {
let clause = action.defaultFocusWhenClause
#expect(!clause.evaluate(ShortcutFocusState(browser: true, markdown: false, sidebar: false)))
#expect(clause.evaluate(ShortcutFocusState(browser: false, markdown: false, sidebar: false)))
}
}

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 Test covers browser-focused state but not sidebar-focused state

focusHistoryNavigationYieldsToBrowserBackForward asserts the clause is false when browser: true and true when browser: false, sidebar: false. Since the underlying when-clause now also evaluates to false when sidebar: true, adding a third assertion (!clause.evaluate(ShortcutFocusState(browser: false, markdown: false, sidebar: true))) would make the test document this behavior explicitly — and catch it if the clause is ever narrowed back to browser-only exclusion without updating the sidebar dimension.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@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
`@Packages/CmuxSettings/Tests/CmuxSettingsTests/ShortcutDefaultCollisionTests.swift`:
- Around line 69-75: The test focusHistoryNavigationYieldsToBrowserBackForward
currently asserts browser focus yields and no-focus passes, but misses asserting
that sidebar focus is excluded; update the test (function
focusHistoryNavigationYieldsToBrowserBackForward) to also check that for each
action in [ShortcutAction.focusHistoryBack, .focusHistoryForward] the clause
(action.defaultFocusWhenClause) evaluates to false when given
ShortcutFocusState(browser: false, markdown: false, sidebar: true) so the
.not(.atom(.sidebarFocus)) part is covered.
🪄 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: 5aa1160e-586e-47a7-b29a-c3f73c85311b

📥 Commits

Reviewing files that changed from the base of the PR and between b311245 and 3b2e72e.

📒 Files selected for processing (3)
  • Packages/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction.swift
  • Packages/CmuxSettings/Tests/CmuxSettingsTests/ShortcutDefaultCollisionTests.swift
  • Sources/KeyboardShortcutContext.swift

Comment on lines +69 to +75
@Test func focusHistoryNavigationYieldsToBrowserBackForward() {
for action in [ShortcutAction.focusHistoryBack, .focusHistoryForward] {
let clause = action.defaultFocusWhenClause
#expect(!clause.evaluate(ShortcutFocusState(browser: true, markdown: false, sidebar: false)))
#expect(clause.evaluate(ShortcutFocusState(browser: false, markdown: false, sidebar: false)))
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add test case for sidebar focus.

The test verifies that focus-history shortcuts yield to browser focus, but the when-clause also excludes sidebar focus (.and(.not(.atom(.browserFocus)), .not(.atom(.sidebarFocus)))). Add a test case to verify sidebar: true evaluates to false.

🧪 Proposed test case addition
     `@Test` func focusHistoryNavigationYieldsToBrowserBackForward() {
         for action in [ShortcutAction.focusHistoryBack, .focusHistoryForward] {
             let clause = action.defaultFocusWhenClause
             `#expect`(!clause.evaluate(ShortcutFocusState(browser: true, markdown: false, sidebar: false)))
             `#expect`(clause.evaluate(ShortcutFocusState(browser: false, markdown: false, sidebar: false)))
+            `#expect`(!clause.evaluate(ShortcutFocusState(browser: false, markdown: false, sidebar: true)))
         }
     }
🤖 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
`@Packages/CmuxSettings/Tests/CmuxSettingsTests/ShortcutDefaultCollisionTests.swift`
around lines 69 - 75, The test focusHistoryNavigationYieldsToBrowserBackForward
currently asserts browser focus yields and no-focus passes, but misses asserting
that sidebar focus is excluded; update the test (function
focusHistoryNavigationYieldsToBrowserBackForward) to also check that for each
action in [ShortcutAction.focusHistoryBack, .focusHistoryForward] the clause
(action.defaultFocusWhenClause) evaluates to false when given
ShortcutFocusState(browser: false, markdown: false, sidebar: true) so the
.not(.atom(.sidebarFocus)) part is covered.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: input Keyboard, shortcuts, IME, mouse, clipboard and paste S3: minor Wrong behavior with a workaround

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants