Skip to content

fix(desktop): keyboard settings — Ctrl bindings, event.code unification, terminal override respect#3391

Merged
Kitenite merged 10 commits intomainfrom
fix-keyboard-ctrl-binding
Apr 13, 2026
Merged

fix(desktop): keyboard settings — Ctrl bindings, event.code unification, terminal override respect#3391
Kitenite merged 10 commits intomainfrom
fix-keyboard-ctrl-binding

Conversation

@Kitenite
Copy link
Copy Markdown
Collaborator

@Kitenite Kitenite commented Apr 13, 2026

Summary

Fixes the user-reported bug that the keyboard settings recorder wouldn't let you bind shortcuts starting with Ctrl, plus four related issues found during the dig.

Bugs fixed

  1. Ctrl press auto-committed ctrl+controlevent.key for the Control key lowercases to "control", not "ctrl", so the pure-modifier filter missed it and immediately saved a broken chord before the user could press the second key.
  2. Recorder/resolver key-source mismatch — recorder used event.key while registry + dispatch use event.code. Shift+digit, Alt+letter on Mac, and non-US layouts silently recorded unmatchable strings (ctrl+shift+@ vs ctrl+shift+2, alt+¬ vs alt+l). Unified on event.code via shared normalizeToken helper.
  3. String comparisons didn't canonicalize — reset-to-default, conflict check, and reserved-list lookups used raw ===, so meta+alt+upmeta+alt+arrowup even though they're the same chord. Added canonicalizeChord and applied at all three sites.
  4. Terminal swallowed hotkeys using stale defaultsREGISTERED_APP_CHORDS was a module-load-time constant built from defaults only, so after any rebind the terminal would (a) swallow the old default into the void and (b) let xterm consume the new binding. Now rebuilds on every override-store change.
  5. Migration carried forward corrupt pre-fix overrides — added a sanitizer that canonicalizes each migrated value and drops malformed ones.

Related decisions

  • Meta on Windows/Linux: removed the blanket reject. Extended OS_RESERVED for Windows with common shell intercepts (meta+d/e/l/r/tab) so users get a warning instead of a silent block.
  • mod alias: skipped — the registry already uses per-platform {mac, windows, linux} bindings.

See apps/desktop/plans/20260412-keyboard-recorder-ctrl-binding-fix.md for the full write-up with upstream react-hotkeys-hook source citations and MDN references.

Tests

45 passing tests across 4 new files covering pure helpers (normalizeToken, canonicalizeChord, isIgnorableKey, formatHotkeyDisplay), recorder capture (captureHotkeyFromEvent), live override-aware resolver, and migration sanitizer.

Test plan

  • On macOS: open Settings → Keyboard, press Record, hold Cmd then press a letter — chord captures correctly
  • Press Ctrl alone — no auto-commit, still waiting for second key
  • Press Ctrl+Shift+2 — captures ctrl+shift+2 (not ctrl+shift+@)
  • Press Meta+[ — captures meta+bracketleft (not meta+[)
  • Rebind any hotkey, then try pressing it while focused inside a terminal — fires correctly (not swallowed)
  • Press the OLD default of a rebound hotkey inside a terminal — no longer swallowed
  • Unassign a hotkey (Backspace while recording) — its default chord no longer swallowed in terminal
  • On Windows: try binding Win+R — allowed with "Reserved by OS" warning
  • Fresh install with pre-existing old-format overrides (via main-process tRPC) — migration drops invalid entries and logs a count

Summary by cubic

Fixes Ctrl-based shortcut recording and unifies hotkey matching across record/resolve/display; the terminal now respects live overrides. Allows Meta (Win/Super) bindings on Windows/Linux with an OS-reserved warning, and re-migrates/sanitizes old overrides to drop corrupt entries.

  • Bug Fixes

    • Recording/display: use event.code, ignore lone modifiers/lock keys, and compare via a canonical chord; arrows/punctuation render correctly; reserved lists canonicalized (e.g., alt+ctrl+delete warns) and include ctrl+backslash.
    • Terminal: rebuilds the hotkey index on override changes so new bindings work in-terminal; old defaults and unassigned keys aren’t swallowed.
    • Migration: bumps marker to hotkey-overrides-migrated-v2 so users who migrated pre-sanitizer re-run once; canonicalizes overrides, preserves null (unassign), and drops malformed strings; marker set only on success or when nothing to migrate.
  • Refactors

    • Unified event↔chord matching via shared helpers (eventToChord, matchesChord, canonicalizeChord, normalizeToken, TERMINAL_RESERVED_CHORDS); OS_RESERVED switched to canonicalized Sets; rewrote isTerminalReservedEvent and replaced terminal matchesKey with matchesChord.
    • Display cleanup: normalize tokens in formatHotkeyDisplay, add punctuation/arrow mappings, treat control as ctrl; tightened comments/JSDocs.

Written for commit a3275ee. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes

    • Fix Ctrl-based and layout-sensitive hotkey recording/matching by normalizing to a single token form; ensure conflict detection, reset-to-default, and reserved-key checks use the same canonicalization
    • Ensure terminal reserved shortcuts and shortcut matching behave consistently (including deterministic modifier ordering)
  • Documentation

    • Add plan describing recorder/resolver consolidation, normalization, and migration behavior
  • Migration

    • Sanitize stored overrides at migration, preserving explicit unassignments and dropping invalid entries
  • Tests

    • Add unit tests for normalization, recording capture, display formatting, matching, migration sanitization, and override behavior

- Rebuild the hotkey reverse index on override store changes so the
  terminal forwards the user's current bindings instead of frozen
  defaults. Fixes swallowed-keystroke on rebound-away defaults and
  dead-binding on new overrides.
- Sanitize migrated overrides: canonicalize and drop malformed strings
  (`ctrl+control`, `ctrl+shift+@`, `meta+[`) that the pre-fix recorder
  could produce.
- Document the meta-on-non-Mac policy (Windows OS intercept, Linux WM
  ownership).
…ning

Drop the blanket reject and extend OS_RESERVED for Windows shell
intercepts (meta+d/e/l/r/tab) so users get a warning instead of a
silent block. Linux WM configs vary too much to predict — trust the
user and let them rebind if a chord doesn't fire.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 13, 2026

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

Recorder, resolver, display, and migration logic unified to use KeyboardEvent.code with shared token normalization and canonical chord formatting; overrides are indexed live and sanitized at migration; terminal-reserved checks and display rendering use canonical forms; tests and plan doc added.

Changes

Cohort / File(s) Summary
Plan Documentation
apps/desktop/plans/20260412-keyboard-recorder-ctrl-binding-fix.md
Adds plan describing consolidation on event.code, canonical chord format, display parity, migration sanitizer, live override index, and test matrix.
Display Layer
apps/desktop/src/renderer/hotkeys/display.ts, apps/desktop/src/renderer/hotkeys/display.test.ts
Normalize display tokens via normalizeToken, map controlctrl, expand arrow/punctuation glyph mappings, and add platform display tests.
Recorder / Capture
apps/desktop/src/renderer/hotkeys/hooks/useRecordHotkeys/useRecordHotkeys.ts, .../useRecordHotkeys.test.ts
Exported captureHotkeyFromEvent; switch capture to event.code + normalizeToken/isIgnorableKey; build modifiers via ordered set; canonicalize chords for reserved/conflict/default comparisons; add recorder tests.
Normalization & Resolution Utils
apps/desktop/src/renderer/hotkeys/utils/resolveHotkeyFromEvent.ts, .../resolveHotkeyFromEvent.test.ts, apps/desktop/src/renderer/hotkeys/utils/index.ts
Export MODIFIERS, normalizeToken, isIgnorableKey, canonicalizeChord, eventToChord, matchesChord, and TERMINAL_RESERVED_CHORDS; replace static reverse index with live buildRegisteredAppChords(overrides); add resolver tests.
Migration & Sanitizer
apps/desktop/src/renderer/hotkeys/migrate.ts, apps/desktop/src/renderer/hotkeys/utils/sanitizeOverride.ts, .../overrideSanitizer.test.ts
Add sanitizeOverride to canonicalize/validate migrated overrides, drop invalid entries (preserve explicit null), persist cleaned overrides, and test sanitizer behavior.
Terminal & Consumers
apps/desktop/src/renderer/hotkeys/utils/utils.ts, apps/desktop/src/renderer/screens/.../Terminal/helpers.ts
Remove local matchesKey; make isTerminalReservedEvent use eventToChord and TERMINAL_RESERVED_CHORDS; callers updated to use exported matchesChord.
Exports & Tests
apps/desktop/src/renderer/hotkeys/index.ts, apps/desktop/src/renderer/hotkeys/utils/index.ts, apps/desktop/src/renderer/hotkeys/**/*.test.ts
Re-export matchesChord, update utils index, and add multiple unit tests covering normalization, recorder capture, display formatting, sanitizer, resolver, and override-index behavior.

Sequence Diagram(s)

sequenceDiagram
  participant Keyboard as KeyboardEvent
  participant Recorder as Recorder (captureHotkeyFromEvent)
  participant Normalizer as normalizeToken / eventToChord
  participant Canon as canonicalizeChord
  participant Store as HotkeyOverridesStore
  participant Builder as buildRegisteredAppChords
  participant Resolver as resolveHotkeyFromEvent
  participant Terminal as Terminal handler
  participant UI as formatHotkeyDisplay

  Keyboard->>Recorder: keydown (event.code + modifier flags)
  Recorder->>Normalizer: normalizeToken(event.code)
  Normalizer->>Canon: canonicalizeChord(tokens)
  Canon->>Store: request overrides / check reserved/conflicts
  Store->>Builder: overrides changed -> buildRegisteredAppChords(overrides)
  Builder->>Resolver: provide live registeredAppChords
  Resolver->>Canon: compare canonical forms (matchesChord)
  Resolver->>Terminal: isTerminalReservedEvent(event)
  Resolver->>UI: resolved chord -> formatHotkeyDisplay
  UI-->>Keyboard: render hotkey text
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇
I tapped the keys with whiskers keen,
Codes aligned where gaps had been.
Chords canonical, tidy, clear,
Ctrl and arrows now appear.
A happy hop — shortcuts near!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and specifically summarizes the main changes: fixing Ctrl bindings in keyboard settings, unifying event.code, and ensuring terminal respects overrides.
Description check ✅ Passed The PR description comprehensively covers all required sections: a clear summary of changes, related issues/bugs fixed, type of change (bug fix), testing information, and additional context including decisions and test plans.

✏️ 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 fix-keyboard-ctrl-binding

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.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

No issues found across 9 files

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 13, 2026

Greptile Summary

This PR fixes five compounding bugs in the desktop keyboard-shortcut recorder and resolver, with the primary user-reported issue being that Ctrl-based chords could not be recorded in Settings → Keyboard.

Key changes:

  • Bug 1 (Ctrl auto-commit): event.key for the Control key is "Control" not "Ctrl", so the old modifier filter missed it and immediately committed ctrl+control. Fixed by switching to event.code-based isIgnorableKey() which correctly aliases ControlLeft/Right → ctrl.
  • Bug 2 (recorder/resolver key-source mismatch): Recorder used event.key (layout-dependent) while the registry and dispatch used event.code. Unified on event.code via a shared normalizeToken helper, fixing incorrect captures like ctrl+shift+@ instead of ctrl+shift+2.
  • Bug 3 (uncanonicalized string comparisons): Reset-to-default, conflict check, and reserved-list lookups used raw ===. A new canonicalizeChord (alphabetically sorted modifiers + alias normalization) is now applied at all three sites.
  • Bug 4 (stale terminal override index): The reverse index registeredAppChords was a module-load-time constant; it is now a let rebuilt on every useHotkeyOverridesStore change, so the terminal's attachCustomKeyEventHandler always sees current bindings.
  • Bug 5 (corrupt migration): The migration from the old main-process store now sanitizes each value through canonicalizeChord and drops entries whose key token contains non-[a-z0-9] characters (e.g. ctrl+shift+@, meta+[).

Additional: The blanket meta rejection on Windows/Linux is replaced by OS-level reserved-chord warnings; Windows OS_RESERVED extended with common Win+key intercepts. 45 new tests across 4 files cover all fixed paths.

Confidence Score: 4/5

Safe to merge — all five stated bugs are correctly fixed with 45 new passing tests; only P2 findings remain.

The core bug fixes are sound and well-tested. The three findings are all P2: (1) "ctrl+alt+delete" in OS_RESERVED["windows"] is not alphabetically sorted so its reserved-warning check silently fails — but the OS intercepts that chord at the kernel level so it can never reach the recorder in practice. (2) sanitizeOverride is duplicated between migrate.ts and the test file, creating a latent divergence risk. (3) The module-level Zustand subscribe call discards the unsubscribe handle, which is benign in production but noisy under HMR. None of these affect the primary user paths fixed by the PR.

useRecordHotkeys.ts (OS_RESERVED canonical form) and overrideSanitizer.test.ts (sanitizer duplication) deserve a second look before the next migration or OS-reserved-list expansion.

Important Files Changed

Filename Overview
apps/desktop/src/renderer/hotkeys/utils/resolveHotkeyFromEvent.ts Core fix: reverse index rebuilt on store changes (Bug 4); exports normalizeToken, isIgnorableKey, canonicalizeChord; module-level subscription discards unsubscribe handle (minor)
apps/desktop/src/renderer/hotkeys/hooks/useRecordHotkeys/useRecordHotkeys.ts Recorder unified on event.code, lone-modifier guard fixed, canonicalizeChord applied at all comparison sites; OS_RESERVED "ctrl+alt+delete" entry is not in canonical form and will silently fail the reserved check
apps/desktop/src/renderer/hotkeys/migrate.ts Migration now sanitizes pre-fix corrupt overrides (canonicalizes and drops invalid); sanitizeOverride is a private function duplicated by the test
apps/desktop/src/renderer/hotkeys/display.ts Applies normalizeToken to chord parts so canonical arrow names (arrowup) and legacy short forms (up) both render correctly; adds missing punctuation tokens to KEY_DISPLAY
apps/desktop/src/renderer/hotkeys/utils/overrideSanitizer.test.ts Good test coverage of migration validation rules, but sanitizeOverride is duplicated from migrate.ts rather than imported, creating a silent divergence risk
apps/desktop/src/renderer/hotkeys/utils/resolveHotkeyFromEvent.test.ts Comprehensive tests for normalizeToken, canonicalizeChord, isIgnorableKey, and live override-aware resolveHotkeyFromEvent including rebind and unassign cases
apps/desktop/src/renderer/hotkeys/hooks/useRecordHotkeys/useRecordHotkeys.test.ts Thorough coverage of all three recorder bugs: lone Ctrl, event.code vs event.key divergence, and modifier ordering; platform-dependency acknowledged in comments
apps/desktop/src/renderer/hotkeys/display.test.ts Tests display parity between canonical and legacy arrow forms, punctuation rendering, and null/modifier-only inputs returning Unassigned
apps/desktop/plans/20260412-keyboard-recorder-ctrl-binding-fix.md Detailed plan document with root-cause analysis, MDN/upstream citations, and deliberate design decisions; placed correctly under apps/desktop/plans/

Sequence Diagram

sequenceDiagram
    participant User
    participant Recorder as useRecordHotkeys
    participant Cap as captureHotkeyFromEvent
    participant Canon as canonicalizeChord
    participant Store as useHotkeyOverridesStore
    participant Index as registeredAppChords (let)
    participant Terminal as terminal attachCustomKeyEventHandler
    participant Resolver as resolveHotkeyFromEvent

    User->>Recorder: keydown (e.g. Ctrl+Shift+2)
    Recorder->>Cap: captureHotkeyFromEvent(event)
    Note over Cap: Uses event.code → normalizeToken()<br/>isIgnorableKey() guards lone modifiers
    Cap-->>Recorder: "ctrl+shift+2"
    Recorder->>Canon: canonicalizeChord("ctrl+shift+2")
    Canon-->>Recorder: "ctrl+shift+2" (sorted)
    Recorder->>Recorder: checkReserved / getHotkeyConflict (canonicalized)
    Recorder->>Store: setOverride(id, "ctrl+shift+2")
    Store-->>Index: subscribe callback fires
    Index->>Index: buildRegisteredAppChords(overrides)
    Note over Index: normalizeChord(override) → sorted keys in map

    User->>Terminal: keydown (same chord)
    Terminal->>Resolver: resolveHotkeyFromEvent(event)
    Resolver->>Resolver: eventToChord() → mods.sort() → "ctrl+shift+2"
    Resolver->>Index: registeredAppChords.get("ctrl+shift+2")
    Index-->>Resolver: HotkeyId
    Resolver-->>Terminal: HotkeyId (forward to app, not swallowed)
Loading

Reviews (1): Last reviewed commit: "feat(desktop): allow meta (Win/Super) bi..." | Re-trigger Greptile

Comment thread apps/desktop/src/renderer/hotkeys/hooks/useRecordHotkeys/useRecordHotkeys.ts Outdated
Comment thread apps/desktop/src/renderer/hotkeys/utils/overrideSanitizer.test.ts Outdated
Comment thread apps/desktop/src/renderer/hotkeys/utils/resolveHotkeyFromEvent.ts Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (2)
apps/desktop/src/renderer/hotkeys/migrate.ts (1)

10-34: Extract the sanitizer into a shared, side-effect-free utility.

overrideSanitizer.test.ts currently has to fork this logic because sanitizeOverride() is local here. That makes the migration path and its test easy to drift apart, and it also pulls pure chord helpers from resolveHotkeyFromEvent.ts, which now has module-load store subscription side effects.

As per coding guidelines **/*.test.{ts,tsx}: Co-locate tests with their source files; tests should be in the same directory as the component/utility they test.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/hotkeys/migrate.ts` around lines 10 - 34, Extract
sanitizeOverride into a new side-effect-free utility module and update tests to
import it: move the function (including its use of canonicalizeChord and
MODIFIERS) out of apps/desktop/src/renderer/hotkeys/migrate.ts into a new file
(e.g., hotkeys/sanitizeOverride.ts) that re-exports a pure
sanitizeOverride(value: unknown): string | null | undefined; update
overrideSanitizer.test.ts to colocate with and import the new utility, and
adjust migrate.ts to import sanitizeOverride from the new module so
resolveHotkeyFromEvent.ts no longer needs to be imported (avoiding its
module-load side effects).
apps/desktop/plans/20260412-keyboard-recorder-ctrl-binding-fix.md (1)

401-403: Pin upstream source links to a commit SHA for doc stability.

These citations point to main, which can drift and make this plan harder to verify later. Prefer permalink URLs pinned to a specific commit.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/plans/20260412-keyboard-recorder-ctrl-binding-fix.md` around
lines 401 - 403, Replace the three upstream links that point to "main" with
permalinks pinned to a specific commit SHA to ensure doc stability: update the
GitHub repository link and the two raw file links (`parseHotkeys.ts` and
`useRecordHotkeys.ts`) so they reference the exact commit SHA (e.g., change
"/main/" to "/commit/<SHA>/" or use the raw file URL that includes the commit
hash) rather than the branch name.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/desktop/plans/20260412-keyboard-recorder-ctrl-binding-fix.md`:
- Line 165: The snippet that returns null when PLATFORM !== "mac" &&
event.metaKey is contradictory with the later policy allowing Meta on non-Mac
with OS-reserved warnings; update the behavior so the check in the keyboard
handling code (the PLATFORM constant and the event.metaKey gate) either permits
Meta on non-Mac and defers to the new OS-reserved warning flow, or explicitly
mark the existing condition as historical/commented and add a clear note linking
it to the legacy behavior described later (the section that explains allowing
Meta with OS-reserved warnings). Ensure you modify the code branch that
currently short-circuits on event.metaKey (or its surrounding comment) so the
plan is internally consistent with the later policy text.

In
`@apps/desktop/src/renderer/hotkeys/hooks/useRecordHotkeys/useRecordHotkeys.test.ts`:
- Around line 22-33: The test stub function ev currently coerces undefined
init.code into an empty string which prevents exercising the undefined code
path; update ev (the KeyboardEvent stub used in useRecordHotkeys.test) to
preserve a missing code field (e.g., set code: init.code as unknown as string |
undefined or omit the defaulting ?? "") so that when tests construct ev({}) the
event.code can be undefined and the undefined guard in the assertion will be
exercised. Ensure only code's defaulting is removed (you can keep other fields'
coercions).

In
`@apps/desktop/src/renderer/hotkeys/hooks/useRecordHotkeys/useRecordHotkeys.ts`:
- Around line 56-67: OS_RESERVED entries are raw literals so checkReserved
compares canonicalized input against non-canonical table entries (e.g.,
"ctrl+alt+delete" vs "alt+ctrl+delete"), so canonicalization never matches;
update the reserved-chord handling by canonicalizing each chord in the
OS_RESERVED map (and the other nearby reserved-chord table) with the same
canonicalization routine used by checkReserved before storing or comparing, or
precompute a canonicalized version of those arrays when the module initializes
so checkReserved can reliably detect reserved chords (refer to OS_RESERVED and
the checkReserved function).

In `@apps/desktop/src/renderer/hotkeys/utils/resolveHotkeyFromEvent.test.ts`:
- Around line 138-145: The test should not early-return when the first HOTKEYS
entry lacks a default key; instead locate a hotkey with a bound key and assert
it exists before using it. Replace the current first-entry logic with a search
like Object.entries(HOTKEYS).find(([, hotkey]) => hotkey.key) and add an expect
that the found entry is defined, then use its key to build the event with
buildEventFromChord and assert resolveHotkeyFromEvent(event) equals the found
id; apply the same change to the other test block covering lines 156-174 to
avoid silent no-ops.

---

Nitpick comments:
In `@apps/desktop/plans/20260412-keyboard-recorder-ctrl-binding-fix.md`:
- Around line 401-403: Replace the three upstream links that point to "main"
with permalinks pinned to a specific commit SHA to ensure doc stability: update
the GitHub repository link and the two raw file links (`parseHotkeys.ts` and
`useRecordHotkeys.ts`) so they reference the exact commit SHA (e.g., change
"/main/" to "/commit/<SHA>/" or use the raw file URL that includes the commit
hash) rather than the branch name.

In `@apps/desktop/src/renderer/hotkeys/migrate.ts`:
- Around line 10-34: Extract sanitizeOverride into a new side-effect-free
utility module and update tests to import it: move the function (including its
use of canonicalizeChord and MODIFIERS) out of
apps/desktop/src/renderer/hotkeys/migrate.ts into a new file (e.g.,
hotkeys/sanitizeOverride.ts) that re-exports a pure sanitizeOverride(value:
unknown): string | null | undefined; update overrideSanitizer.test.ts to
colocate with and import the new utility, and adjust migrate.ts to import
sanitizeOverride from the new module so resolveHotkeyFromEvent.ts no longer
needs to be imported (avoiding its module-load side effects).
🪄 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: 4b6691f6-4d2e-4caa-930c-a350bb7aff0a

📥 Commits

Reviewing files that changed from the base of the PR and between 41cd804 and 1b60d0d.

📒 Files selected for processing (9)
  • apps/desktop/plans/20260412-keyboard-recorder-ctrl-binding-fix.md
  • apps/desktop/src/renderer/hotkeys/display.test.ts
  • apps/desktop/src/renderer/hotkeys/display.ts
  • apps/desktop/src/renderer/hotkeys/hooks/useRecordHotkeys/useRecordHotkeys.test.ts
  • apps/desktop/src/renderer/hotkeys/hooks/useRecordHotkeys/useRecordHotkeys.ts
  • apps/desktop/src/renderer/hotkeys/migrate.ts
  • apps/desktop/src/renderer/hotkeys/utils/overrideSanitizer.test.ts
  • apps/desktop/src/renderer/hotkeys/utils/resolveHotkeyFromEvent.test.ts
  • apps/desktop/src/renderer/hotkeys/utils/resolveHotkeyFromEvent.ts

Comment thread apps/desktop/plans/20260412-keyboard-recorder-ctrl-binding-fix.md Outdated
Comment thread apps/desktop/src/renderer/hotkeys/hooks/useRecordHotkeys/useRecordHotkeys.ts Outdated
Audit against react-hotkeys-hook and internal usages found two more
consumers comparing via event.key, which breaks the same punctuation /
layout / rebind cases fixed in the recorder:

- utils/utils.ts isTerminalReservedEvent had its own event.key-based
  TERMINAL_RESERVED set with ctrl+\\.
- Terminal/helpers.ts matchesKey used event.key to check the
  CLEAR_TERMINAL rebind — silently wrong for any punctuation rebind.

Consolidation:

- Export eventToChord and new matchesChord(event, chord) as the single
  canonical event↔chord matcher.
- Export TERMINAL_RESERVED_CHORDS as the single source of truth.
- Rewrite isTerminalReservedEvent around them.
- Replace matchesKey() with matchesChord().
- Remove duplicated TERMINAL_RESERVED from useRecordHotkeys.ts.

Also adds tests for eventToChord, matchesChord, and
isTerminalReservedEvent parity. Updates plan doc with the cleanup.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
apps/desktop/plans/20260412-keyboard-recorder-ctrl-binding-fix.md (2)

18-18: Clarify Bug 1 fix description for accuracy.

The current description "Filter against "control" (the lowercased event.key name)" could be misleading, as the actual fix switches from event.key to event.code via normalizeToken and isIgnorableKey. Consider rewording to clarify that the solution uses normalized event.code tokens, not lowercased event.key values.

💡 Suggested rewording
-| 1 | Lone Ctrl auto-committed `ctrl+control` before the user pressed key 2        | Filter against `"control"` (the lowercased `event.key` name) + lock keys + altgraph |
+| 1 | Lone Ctrl auto-committed `ctrl+control` before the user pressed key 2        | Use `normalizeToken(event.code)` + `isIgnorableKey` to filter modifiers, lock keys, and synthetic events |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/plans/20260412-keyboard-recorder-ctrl-binding-fix.md` at line
18, Update the Bug 1 description to state that the fix uses normalized
event.code tokens rather than lowercased event.key values: mention
normalizeToken and isIgnorableKey are used to convert and filter based on
event.code (plus lock keys and AltGraph) instead of filtering against the
lowercased event.key string; keep the note that it prevents lone Ctrl
auto-commit but replace "Filter against `\"control\"` (the lowercased
`event.key` name)" with wording that explicitly references normalized event.code
handling via normalizeToken and isIgnorableKey.

153-168: Add language identifier to fenced code block.

The code block should specify a language identifier for proper markdown formatting. Since this is a file listing rather than executable code, use text or leave the language identifier empty.

📝 Suggested fix
-```
+```text
 apps/desktop/plans/20260412-keyboard-recorder-ctrl-binding-fix.md  (this doc)
 apps/desktop/src/renderer/hotkeys/
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/plans/20260412-keyboard-recorder-ctrl-binding-fix.md` around
lines 153 - 168, In the markdown file
apps/desktop/plans/20260412-keyboard-recorder-ctrl-binding-fix.md update the
fenced code block that contains the file listing so it includes a language
identifier (use "text" or leave it blank) after the opening triple backticks;
locate the triple-backtick block that starts before the line
"apps/desktop/plans/20260412-keyboard-recorder-ctrl-binding-fix.md  (this doc)"
and change it to "```text" (or "```") to ensure proper markdown formatting.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/desktop/plans/20260412-keyboard-recorder-ctrl-binding-fix.md`:
- Line 138: Update the mismatched test count string in the markdown: locate the
sentence containing "Everything fixed is in pure functions over primitives. **62
tests across 4" and replace the "62 tests across 4 files" fragment with the
accurate summary ("45 passing tests across four new files") or the verified
current test count; ensure the surrounding phrasing and bold markup remain
consistent with the PR objectives section.

---

Nitpick comments:
In `@apps/desktop/plans/20260412-keyboard-recorder-ctrl-binding-fix.md`:
- Line 18: Update the Bug 1 description to state that the fix uses normalized
event.code tokens rather than lowercased event.key values: mention
normalizeToken and isIgnorableKey are used to convert and filter based on
event.code (plus lock keys and AltGraph) instead of filtering against the
lowercased event.key string; keep the note that it prevents lone Ctrl
auto-commit but replace "Filter against `\"control\"` (the lowercased
`event.key` name)" with wording that explicitly references normalized event.code
handling via normalizeToken and isIgnorableKey.
- Around line 153-168: In the markdown file
apps/desktop/plans/20260412-keyboard-recorder-ctrl-binding-fix.md update the
fenced code block that contains the file listing so it includes a language
identifier (use "text" or leave it blank) after the opening triple backticks;
locate the triple-backtick block that starts before the line
"apps/desktop/plans/20260412-keyboard-recorder-ctrl-binding-fix.md  (this doc)"
and change it to "```text" (or "```") to ensure proper markdown formatting.
🪄 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: 9207fafa-72b7-4395-81c5-2f03015b0747

📥 Commits

Reviewing files that changed from the base of the PR and between 1e0cd32 and 3cd6026.

📒 Files selected for processing (1)
  • apps/desktop/plans/20260412-keyboard-recorder-ctrl-binding-fix.md

Comment thread apps/desktop/plans/20260412-keyboard-recorder-ctrl-binding-fix.md
- Merge canonicalizeChord / normalizeChord into one exported function
  (the wrapper was pointless indirection).
- Drop section-banner comments (`// Helpers`, `// Hook`) and comments
  that restated code (`// Must include ctrl or meta…`).
- Tighten JSDocs to convey intent in one line where possible.
- display.ts: drop duplicate short arrow entries (`up: "↑"`) — the
  normalizeToken call aliases them to canonical names already. Extract
  isModifier type guard to kill the cast repetition.

Net -35 LOC, same behavior, same 62 tests passing.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
apps/desktop/src/renderer/hotkeys/utils/resolveHotkeyFromEvent.ts (1)

71-74: Consider strengthening the event.code check.

The check event.code === undefined catches undefined values, but synthetic events or unusual edge cases might produce an empty string. The isIgnorableKey on line 74 handles empty strings via !normalized, so this is safe, but the early return could be more explicit.

 export function eventToChord(event: KeyboardEvent): string | null {
-	if (event.code === undefined) return null;
+	if (!event.code) return null;
 	const key = normalizeToken(event.code);
 	if (isIgnorableKey(key)) return null;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/hotkeys/utils/resolveHotkeyFromEvent.ts` around
lines 71 - 74, The early guard in eventToChord checks only for event.code ===
undefined which misses empty-string or falsy values; update the guard to treat
any falsy or non-string code as invalid (e.g., check if !event?.code or typeof
event.code !== 'string') before calling normalizeToken, referencing the
eventToChord function and the normalizeToken/isIgnorableKey flow so empty or
non-string codes short-circuit consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/desktop/src/renderer/hotkeys/utils/resolveHotkeyFromEvent.ts`:
- Around line 71-74: The early guard in eventToChord checks only for event.code
=== undefined which misses empty-string or falsy values; update the guard to
treat any falsy or non-string code as invalid (e.g., check if !event?.code or
typeof event.code !== 'string') before calling normalizeToken, referencing the
eventToChord function and the normalizeToken/isIgnorableKey flow so empty or
non-string codes short-circuit consistently.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2654ee11-aec8-4223-9518-f919926af969

📥 Commits

Reviewing files that changed from the base of the PR and between 3cd6026 and 7ba18d4.

📒 Files selected for processing (4)
  • apps/desktop/src/renderer/hotkeys/display.ts
  • apps/desktop/src/renderer/hotkeys/hooks/useRecordHotkeys/useRecordHotkeys.ts
  • apps/desktop/src/renderer/hotkeys/migrate.ts
  • apps/desktop/src/renderer/hotkeys/utils/resolveHotkeyFromEvent.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/desktop/src/renderer/hotkeys/hooks/useRecordHotkeys/useRecordHotkeys.ts

…anups

Addresses PR review feedback:

- Bug: OS_RESERVED[\"windows\"] had \"ctrl+alt+delete\" which never matched
  because canonicalization sorts modifiers alphabetically. Wrap both
  OS_RESERVED and TERMINAL_RESERVED_CHORDS in .map(canonicalizeChord)
  at build time so multi-modifier entries can't silently miss the
  reserved warning. OS_RESERVED values switched from array to Set.
- Extract sanitizeOverride into utils/sanitizeOverride.ts so the test
  imports the real implementation instead of a near-copy.
- Test stub: preserve explicit \`code: undefined\` so the synthetic-event
  guard in captureHotkeyFromEvent is actually exercised (not the
  empty-string branch).
- Resolver tests: resolve a sample hotkey once at describe scope and
  throw if HOTKEYS has no bound defaults, instead of each test silently
  no-op-ing via \`if (!def) return\`.
- Add regression test asserting canonicalizeChord(\"ctrl+alt+delete\")
  sorts to \"alt+ctrl+delete\".
… pre-fix

The original guard short-circuited whenever \`hotkey-overrides\` existed in
localStorage, which meant any user who ran the migration before the
Bug 5 sanitizer shipped would keep their corrupt entries
(\`ctrl+control\`, \`ctrl+shift+@\`, \`meta+[\`) forever.

Gate migration/sanitization on a separate \`hotkey-overrides-sanitized-v1\`
marker instead:

- No marker + no store  → tRPC migration with sanitizer.
- No marker + has store → in-place re-sanitization of existing entries.
- Marker present        → done, skip.

The marker is only set once per user; new bindings written by the fixed
recorder can't be corrupt so no further passes are needed.
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/desktop/src/renderer/hotkeys/migrate.ts">

<violation number="1" location="apps/desktop/src/renderer/hotkeys/migrate.ts:35">
P2: Only mark re-sanitization complete when it actually succeeds; current logic suppresses future retries after a failure.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread apps/desktop/src/renderer/hotkeys/migrate.ts Outdated
…s re-migrate

Use a dedicated `hotkey-overrides-migrated-v2` marker separate from the
Zustand persist key. Users who ran the migration on the pre-sanitizer
build (~1 day window) will re-run once and get their corrupt entries
(`ctrl+control`, `ctrl+shift+@`, `meta+[`) dropped.

Mark set only on success paths (or when there's nothing to migrate), not
in the catch branch — transient tRPC failures at boot retry next launch
instead of silently giving up on the user's legacy bindings.
@Kitenite Kitenite merged commit 7893007 into main Apr 13, 2026
7 checks passed
@Kitenite Kitenite deleted the fix-keyboard-ctrl-binding branch April 13, 2026 06:19
@github-actions
Copy link
Copy Markdown
Contributor

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ⚠️ Neon database branch
  • ⚠️ Electric Fly.io app

Thank you for your contribution! 🎉

MocA-Love pushed a commit to MocA-Love/superset that referenced this pull request Apr 13, 2026
…on, terminal override respect (superset-sh#3391)

* attempt fix

* Test and doc

* fix(desktop): terminal & migration respect hotkey overrides

- Rebuild the hotkey reverse index on override store changes so the
  terminal forwards the user's current bindings instead of frozen
  defaults. Fixes swallowed-keystroke on rebound-away defaults and
  dead-binding on new overrides.
- Sanitize migrated overrides: canonicalize and drop malformed strings
  (`ctrl+control`, `ctrl+shift+@`, `meta+[`) that the pre-fix recorder
  could produce.
- Document the meta-on-non-Mac policy (Windows OS intercept, Linux WM
  ownership).

* feat(desktop): allow meta (Win/Super) bindings on non-Mac with OS warning

Drop the blanket reject and extend OS_RESERVED for Windows shell
intercepts (meta+d/e/l/r/tab) so users get a warning instead of a
silent block. Linux WM configs vary too much to predict — trust the
user and let them rebind if a chord doesn't fire.

* refactor(desktop): unify event↔chord matching via shared helpers

Audit against react-hotkeys-hook and internal usages found two more
consumers comparing via event.key, which breaks the same punctuation /
layout / rebind cases fixed in the recorder:

- utils/utils.ts isTerminalReservedEvent had its own event.key-based
  TERMINAL_RESERVED set with ctrl+\\.
- Terminal/helpers.ts matchesKey used event.key to check the
  CLEAR_TERMINAL rebind — silently wrong for any punctuation rebind.

Consolidation:

- Export eventToChord and new matchesChord(event, chord) as the single
  canonical event↔chord matcher.
- Export TERMINAL_RESERVED_CHORDS as the single source of truth.
- Rewrite isTerminalReservedEvent around them.
- Replace matchesKey() with matchesChord().
- Remove duplicated TERMINAL_RESERVED from useRecordHotkeys.ts.

Also adds tests for eventToChord, matchesChord, and
isTerminalReservedEvent parity. Updates plan doc with the cleanup.

* docs(desktop): rewrite hotkey fix plan for concise review

* chore(desktop/hotkeys): deslop — tighten comments, remove dead wrappers

- Merge canonicalizeChord / normalizeChord into one exported function
  (the wrapper was pointless indirection).
- Drop section-banner comments (`// Helpers`, `// Hook`) and comments
  that restated code (`// Must include ctrl or meta…`).
- Tighten JSDocs to convey intent in one line where possible.
- display.ts: drop duplicate short arrow entries (`up: "↑"`) — the
  normalizeToken call aliases them to canonical names already. Extract
  isModifier type guard to kill the cast repetition.

Net -35 LOC, same behavior, same 62 tests passing.

* fix(desktop/hotkeys): canonicalize reserved-chord tables + review cleanups

Addresses PR review feedback:

- Bug: OS_RESERVED[\"windows\"] had \"ctrl+alt+delete\" which never matched
  because canonicalization sorts modifiers alphabetically. Wrap both
  OS_RESERVED and TERMINAL_RESERVED_CHORDS in .map(canonicalizeChord)
  at build time so multi-modifier entries can't silently miss the
  reserved warning. OS_RESERVED values switched from array to Set.
- Extract sanitizeOverride into utils/sanitizeOverride.ts so the test
  imports the real implementation instead of a near-copy.
- Test stub: preserve explicit \`code: undefined\` so the synthetic-event
  guard in captureHotkeyFromEvent is actually exercised (not the
  empty-string branch).
- Resolver tests: resolve a sample hotkey once at describe scope and
  throw if HOTKEYS has no bound defaults, instead of each test silently
  no-op-ing via \`if (!def) return\`.
- Add regression test asserting canonicalizeChord(\"ctrl+alt+delete\")
  sorts to \"alt+ctrl+delete\".

* fix(desktop/hotkeys): re-sanitize localStorage for users who migrated pre-fix

The original guard short-circuited whenever \`hotkey-overrides\` existed in
localStorage, which meant any user who ran the migration before the
Bug 5 sanitizer shipped would keep their corrupt entries
(\`ctrl+control\`, \`ctrl+shift+@\`, \`meta+[\`) forever.

Gate migration/sanitization on a separate \`hotkey-overrides-sanitized-v1\`
marker instead:

- No marker + no store  → tRPC migration with sanitizer.
- No marker + has store → in-place re-sanitization of existing entries.
- Marker present        → done, skip.

The marker is only set once per user; new bindings written by the fixed
recorder can't be corrupt so no further passes are needed.

* fix(desktop/hotkeys): bump migration marker key so pre-sanitizer users re-migrate

Use a dedicated `hotkey-overrides-migrated-v2` marker separate from the
Zustand persist key. Users who ran the migration on the pre-sanitizer
build (~1 day window) will re-run once and get their corrupt entries
(`ctrl+control`, `ctrl+shift+@`, `meta+[`) dropped.

Mark set only on success paths (or when there's nothing to migrate), not
in the catch branch — transient tRPC failures at boot retry next launch
instead of silently giving up on the user's legacy bindings.
saddlepaddle added a commit that referenced this pull request Apr 13, 2026
…workspace

Widen PlatformKey and HotkeyDefinition so hotkey entries can register
with a null chord per platform. Downstream consumers were already
null-safe from #3391 (useBinding, buildRegisteredAppChords,
formatHotkeyDisplay, sanitizeOverride, HotkeyMenuShortcut), so the
schema widening is the only structural change needed.

Re-introduce PREV_TAB, NEXT_TAB, PREV_WORKSPACE, NEXT_WORKSPACE as
registered-but-unbound entries so users who want tab/workspace neighbor
navigation can rebind them in Settings → Keyboard. PR #3403 removed
these to free Cmd+Alt+Arrow for directional pane focus; this restores
the hotkey IDs (and their v1/v2 handlers) without claiming any default
chord. Users with pre-#3403 overrides for these IDs will transparently
get their bindings back since the override is preserved in localStorage.

- Null-guard canonicalizeChord(defaultKey) in useRecordHotkeys so
  recording a new chord for an unbound hotkey no longer throws.
- Replace the force-cast in resolveHotkeyFromEvent.test.ts sample
  picker with a type predicate so sampleDef.key narrows to string
  honestly instead of lying about the widened schema.
saddlepaddle added a commit that referenced this pull request Apr 13, 2026
…kspace (#3422)

* feat(desktop/hotkeys): allow unbound defaults; restore PREV/NEXT tab+workspace

Widen PlatformKey and HotkeyDefinition so hotkey entries can register
with a null chord per platform. Downstream consumers were already
null-safe from #3391 (useBinding, buildRegisteredAppChords,
formatHotkeyDisplay, sanitizeOverride, HotkeyMenuShortcut), so the
schema widening is the only structural change needed.

Re-introduce PREV_TAB, NEXT_TAB, PREV_WORKSPACE, NEXT_WORKSPACE as
registered-but-unbound entries so users who want tab/workspace neighbor
navigation can rebind them in Settings → Keyboard. PR #3403 removed
these to free Cmd+Alt+Arrow for directional pane focus; this restores
the hotkey IDs (and their v1/v2 handlers) without claiming any default
chord. Users with pre-#3403 overrides for these IDs will transparently
get their bindings back since the override is preserved in localStorage.

- Null-guard canonicalizeChord(defaultKey) in useRecordHotkeys so
  recording a new chord for an unbound hotkey no longer throws.
- Replace the force-cast in resolveHotkeyFromEvent.test.ts sample
  picker with a type predicate so sampleDef.key narrows to string
  honestly instead of lying about the widened schema.

* fix(desktop/hotkeys): restore prevIndex in v2 PREV_WORKSPACE handler
MocA-Love pushed a commit to MocA-Love/superset that referenced this pull request Apr 14, 2026
…kspace (superset-sh#3422)

* feat(desktop/hotkeys): allow unbound defaults; restore PREV/NEXT tab+workspace

Widen PlatformKey and HotkeyDefinition so hotkey entries can register
with a null chord per platform. Downstream consumers were already
null-safe from superset-sh#3391 (useBinding, buildRegisteredAppChords,
formatHotkeyDisplay, sanitizeOverride, HotkeyMenuShortcut), so the
schema widening is the only structural change needed.

Re-introduce PREV_TAB, NEXT_TAB, PREV_WORKSPACE, NEXT_WORKSPACE as
registered-but-unbound entries so users who want tab/workspace neighbor
navigation can rebind them in Settings → Keyboard. PR superset-sh#3403 removed
these to free Cmd+Alt+Arrow for directional pane focus; this restores
the hotkey IDs (and their v1/v2 handlers) without claiming any default
chord. Users with pre-superset-sh#3403 overrides for these IDs will transparently
get their bindings back since the override is preserved in localStorage.

- Null-guard canonicalizeChord(defaultKey) in useRecordHotkeys so
  recording a new chord for an unbound hotkey no longer throws.
- Replace the force-cast in resolveHotkeyFromEvent.test.ts sample
  picker with a type predicate so sampleDef.key narrows to string
  honestly instead of lying about the widened schema.

* fix(desktop/hotkeys): restore prevIndex in v2 PREV_WORKSPACE handler
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.

1 participant