Skip to content

fix(desktop/hotkeys): adaptive-layout toggle now reaches every dispatch consumer#4091

Merged
Kitenite merged 1 commit into
mainfrom
debug-hotkeys-smoke-test
May 5, 2026
Merged

fix(desktop/hotkeys): adaptive-layout toggle now reaches every dispatch consumer#4091
Kitenite merged 1 commit into
mainfrom
debug-hotkeys-smoke-test

Conversation

@Kitenite
Copy link
Copy Markdown
Collaborator

@Kitenite Kitenite commented May 5, 2026

Summary

PR #4078 added an "Adaptive layout mapping" toggle but it didn't actually do anything for shipped shortcuts. Two stacked defects, plus a default-direction call:

  1. Toggle didn't gate registration. useHotkey (the hook that wires react-hotkeys-hook) and the recorder's conflict detector read the layout map unconditionally. The toggle moved labels in Settings; it never moved which key fired the action.
  2. Toggle had nothing to act on. Every shipped registry default was a bare-string (physical) chord. bindingToDispatchChord short-circuits physical mode, so the layout map was never consulted regardless of toggle state — the 75 built-in shortcuts ignored the preference completely.
  3. Wrong default. adaptiveLayoutEnabled: false shipped as the default, locking non-US users into physical-position dispatch as the out-of-the-box experience.

User report context: a Dvorak user (Tony, in #ext-superset-aytuncext-mastra) installed the v1.8.1 release with the toggle, found that flipping it changed the Settings labels but not which keys actually fired, and confirmed the symptom for every shortcut.

Changes

  • Single chokepoint. New useEffectiveLayoutMap() / getEffectiveLayoutMap() in keyboardPreferencesStore that gates by adaptiveLayoutEnabled. All five consumers (registration, resolver, display hooks, imperative dispatch, recorder conflict detector) route through it. Banner on keyboardLayoutStore warns against reading the raw store directly — that pattern was the root cause of feat(desktop): toggle to disable adaptive keyboard-layout mapping #4078's miss.
  • Logical defaults. Re-authored printable chords in registry.ts as mode: "logical" via a new L() helper. Named-key chords (arrows / Enter / Backspace / Tab) stay as bare strings — they're layout-stable.
  • Default flipped to ON to match macOS / VS Code / Cursor convention: pressing the key with "T" printed on it fires ⌘T, regardless of layout. Existing users who explicitly persisted a value keep it; users who never touched the toggle pick up the new default on upgrade.
  • Settings page copy rewritten in user-facing terms.
  • Regression test (registry.test.ts) locks in the logical-default authoring convention so a future PR can't silently revert printable chords to bare strings.
  • Smoke test doc (apps/desktop/plans/20260505-hotkey-adaptive-toggle-smoke-test.md) documents the manual flow for non-US layouts.

Migration story is clean: US users see no behavior change at all (identity layout map). Non-US users with no override get behavior closer to OS convention. Non-US users with custom overrides keep them.

Test plan

Verified manually on Dvorak with QWERTY-labeled hardware (Tests 1–7 in the smoke-test doc).

  • bun run typecheck passes
  • bunx biome check apps/desktop/src/renderer/hotkeys passes
  • bun test apps/desktop/src/renderer/hotkeys/registry.test.ts passes
  • Manual: toggle ON, Dvorak — ⌘T fires on the K-cap (where "t" lives on Dvorak)
  • Manual: toggle OFF, Dvorak — ⌘T fires on the T-cap (physical KeyT, types "y" on Dvorak)
  • Manual: live toggle flip swaps the firing key without reload
  • Manual: recorder conflict detection consistent across both toggle states
  • Manual: persistence — new installs land on ON; explicit OFF survives reload
  • Manual: terminal reservation — Ctrl+C reaches PTY, app hotkeys don't leak

Follow-ups (not blocking)

  • One-shot migration toast for existing non-US users on first launch post-upgrade. Skipped here because it needs copy/timing decisions; current change is a fix toward OS convention, not silent breakage.
  • Hotkey vitest infra. The existing bun test suite for hotkeys is broken at module init via a transitive electronTrpcClient import (pre-existing on main). Worth a separate ticket so we can write a real useHotkey integration test next time.

Summary by CodeRabbit

  • New Features

    • Adaptive layout mapping is now enabled by default for new installations, ensuring keyboard shortcuts automatically adapt to non-US keyboard layouts.
  • Improvements

    • Enhanced keyboard shortcut behavior to consistently use logical-mode mapping for proper key relabeling on QWERTZ and other non-standard layouts.
    • Improved help text in settings to clarify how adaptive layout matching works with physical keyboard labels.

…h consumer

The "Adaptive layout mapping" toggle in #4078 was wired into the resolver
index, the display hooks, and `getDispatchChord`, but missed the actual
hotkey registration in `useHotkey` and the recorder's conflict detector.
Worse, every shipped registry default was a bare-string (physical) chord,
which `bindingToDispatchChord` short-circuits — so flipping the toggle did
nothing for the 75 built-in shortcuts regardless of any other fix. Net
result: a Dvorak / QWERTZ user pressing the labeled-T key for `⌘T` saw
the menu update but no hotkey fire.

- Route every layout-map consumer through a single
  `useEffectiveLayoutMap` / `getEffectiveLayoutMap` chokepoint that gates
  by `adaptiveLayoutEnabled`. Adds a banner on `keyboardLayoutStore`
  warning future contributors not to read the raw store.
- Re-author printable defaults in `registry.ts` as `mode: "logical"`
  via an `L()` helper so the toggle actually moves them on non-US
  layouts. Named-key chords (arrows, Enter, Backspace, Tab) stay as
  bare strings since they're layout-stable.
- Flip `adaptiveLayoutEnabled` to default `true` to match macOS / VS
  Code / Cursor convention. Existing users who explicitly persisted a
  value keep it (zustand persist is write-on-set).
- Update the keyboard settings copy to describe the toggle in
  user-facing terms ("match shortcuts to printed key labels" vs
  "anchor to physical positions").
- Add `registry.test.ts` locking in the logical-default authoring
  convention so a future PR can't silently revert printable chords to
  bare strings.
- `apps/desktop/plans/20260505-hotkey-adaptive-toggle-smoke-test.md`
  documents the manual smoke test for non-US layouts.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 5, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

This PR implements an adaptive layout toggle for desktop hotkeys, defaulting enabled. A new useEffectiveLayoutMap() hook centralizes preference-gated access to keyboard layout maps, and the registry wraps printable bindings in a L() helper specifying logical mode. Consumers (display, dispatch, recorder) uniformly read the effective map, enabling label-based remapping on non-US keyboards when enabled, positional behavior when disabled.

Changes

Adaptive Layout Toggle Implementation

Layer / File(s) Summary
Type Definitions
apps/desktop/src/renderer/hotkeys/types.ts
PlatformKey and HotkeyDefinition.key change from string | null to ShortcutBinding | null. Documentation updated to clarify logical/physical/named binding modes.
Store Foundation
apps/desktop/src/renderer/hotkeys/stores/keyboardPreferencesStore.ts, keyboardLayoutStore.ts
adaptiveLayoutEnabled now defaults to true. New useEffectiveLayoutMap() and getEffectiveLayoutMap() conditionally return layout map when adaptive layout is enabled, else null; guard comment added to discourage direct layout store access.
Store Exports
apps/desktop/src/renderer/hotkeys/stores/index.ts
Added re-exports for getEffectiveLayoutMap and useEffectiveLayoutMap.
Registry & Binding Helper
apps/desktop/src/renderer/hotkeys/registry.ts
Introduced L(chord: string): ShortcutBinding helper (version 2, mode "logical"). Wrapped all platform-specific chord strings across navigation, workspace, layout, terminal, chat, window, and help hotkeys with L("..."), converting HOTKEYS output from string chords to ShortcutBinding objects.
Hook Integration
apps/desktop/src/renderer/hotkeys/hooks/useHotkey/useHotkey.ts, useHotkeyDisplay/useHotkeyDisplay.ts, useBinding/useBinding.ts, useRecordHotkeys/useRecordHotkeys.ts
All hooks switched to derive layoutMap via useEffectiveLayoutMap() or getEffectiveLayoutMap() instead of direct store reads, centralizing the adaptive layout toggle gate.
Resolver Simplification
apps/desktop/src/renderer/hotkeys/utils/resolveHotkeyFromEvent.ts
Refactored to call getEffectiveLayoutMap() directly; removed local activeLayoutMap() helper and consolidated three separate subscription callbacks into a single rebuild() function called by all store subscriptions.
Tests & Documentation
apps/desktop/src/renderer/hotkeys/registry.test.ts, utils/resolveHotkeyFromEvent.test.ts, apps/desktop/plans/20260505-hotkey-adaptive-toggle-smoke-test.md, apps/desktop/src/renderer/routes/_authenticated/settings/keyboard/page.tsx
Registry tests enforce logical mode for printable chords and validate canary defaults. Resolver tests updated to parse canonical chords. Smoke test plan documents ON/OFF behavior, live toggle, migration, and PTY parity. Settings UI text clarified to explain label-based matching with QWERTZ example.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Hops with joy through QWERTZ plains,
Logical chords break keyboard chains,
Toggle ON—labels match each key,
Toggle OFF—positions stay carefree,
One true map to rule them all,
Adaptive layout heeds the call! 🎹✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.36% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: the adaptive-layout toggle now reaches every dispatch consumer, fixing the defect where it previously only updated Settings labels.
Description check ✅ Passed The description is comprehensive and well-structured, covering the problem, changes, test plan, and follow-ups. All required template sections are present and appropriately filled.
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.

✏️ 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 debug-hotkeys-smoke-test

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
Copy link
Copy Markdown

greptile-apps Bot commented May 5, 2026

Greptile Summary

  • Fixes the feat(desktop): toggle to disable adaptive keyboard-layout mapping #4078 "toggle does nothing" regression by routing all five dispatch consumers (useHotkey, resolver index, display hooks, getDispatchChord, recorder conflict detector) through a new useEffectiveLayoutMap / getEffectiveLayoutMap chokepoint in keyboardPreferencesStore, and re-authoring all 75 printable registry defaults as mode: \"logical\" v2 objects via the L() helper so the toggle actually moves which key fires.
  • Flips the adaptiveLayoutEnabled default from false to true to match macOS / VS Code / Chrome convention; existing users with an explicit persisted value are unaffected.
  • Adds a registry.test.ts regression suite that locks in the logical-mode authoring convention for printable defaults, preventing silent regression back to bare strings.

Confidence Score: 4/5

Safe to merge — fix is correct and complete; only P2 documentation inconsistencies remain

All five dispatch consumers are properly unified through the new chokepoint, the default flip is safe with persist middleware, and the regression test locks in the convention. The only findings are P2 documentation issues: a stale JSDoc in useBinding.ts, a warning banner that slightly overstates what's forbidden (import vs. raw map read), and a smoke-test doc that still says a helper needs to be added as a follow-up when it was already added in this PR.

apps/desktop/src/renderer/hotkeys/utils/resolveHotkeyFromEvent.ts (direct useKeyboardLayoutStore import contradicts new banner), apps/desktop/src/renderer/hotkeys/hooks/useBinding/useBinding.ts (stale JSDoc)

Important Files Changed

Filename Overview
apps/desktop/src/renderer/hotkeys/stores/keyboardPreferencesStore.ts Adds useEffectiveLayoutMap / getEffectiveLayoutMap as the single chokepoint for toggle-gated layout access; flips default to true; correctly persists only adaptiveLayoutEnabled
apps/desktop/src/renderer/hotkeys/registry.ts All 75 printable chord defaults migrated from bare strings to L() (v2 logical objects); layout-stable keys (arrows, Enter, F-keys) correctly left as bare strings
apps/desktop/src/renderer/hotkeys/utils/resolveHotkeyFromEvent.ts Resolver index now rebuilt via shared rebuild() helper using getEffectiveLayoutMap(); still directly imports useKeyboardLayoutStore for subscription (contradicts new banner warning)
apps/desktop/src/renderer/hotkeys/hooks/useBinding/useBinding.ts Correctly routes getDispatchChord through getEffectiveLayoutMap(); JSDoc on useBinding still describes defaults as bare strings (now stale)
apps/desktop/src/renderer/hotkeys/registry.test.ts New regression suite locks in logical-mode authoring convention for printable defaults and includes canary checks for letter, digit, and punctuation shapes
apps/desktop/src/renderer/hotkeys/hooks/useHotkey/useHotkey.ts Replaces raw useKeyboardLayoutStore read with useEffectiveLayoutMap(); registration now correctly gates on toggle state
apps/desktop/src/renderer/hotkeys/hooks/useHotkeyDisplay/useHotkeyDisplay.ts Removes local useActiveLayoutMap helper and unifies both display hooks through useEffectiveLayoutMap()
apps/desktop/src/renderer/hotkeys/hooks/useRecordHotkeys/useRecordHotkeys.ts Recorder conflict detector now reads through getEffectiveLayoutMap(); toggle state is correctly reflected on each keypress
apps/desktop/src/renderer/hotkeys/types.ts PlatformKey and HotkeyDefinition.key correctly widened from string to ShortcutBinding; BindingMode doc reordered to reflect logical-first convention
apps/desktop/src/renderer/hotkeys/utils/resolveHotkeyFromEvent.test.ts Updated to extract chord from ShortcutBinding via parseBinding so tests work correctly now that defaults are v2 objects instead of bare strings
apps/desktop/src/renderer/hotkeys/stores/keyboardLayoutStore.ts Adds warning banner directing consumers to use useEffectiveLayoutMap instead of reading the store directly; no functional change
apps/desktop/src/renderer/routes/_authenticated/settings/keyboard/page.tsx UI copy for Adaptive layout mapping clarified from technical jargon to user-facing language describing label-matching behaviour

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[KeyboardEvent] --> B{adaptiveLayoutEnabled?}
    B -- "true (default)" --> C[getEffectiveLayoutMap\nreturns OS layout map]
    B -- "false" --> D[getEffectiveLayoutMap\nreturns null]

    C --> E[bindingToDispatchChord\ntranslates logical chord\nthrough OS map]
    D --> F[bindingToDispatchChord\nfalls back to authored chord\nphysical mode]

    E --> G[Dispatch consumers]
    F --> G

    G --> H[useHotkey\nregistration]
    G --> I[resolveHotkeyFromEvent\nreverse index]
    G --> J[useHotkeyDisplay\ndisplay hooks]
    G --> K[getDispatchChord\nimperative dispatch]
    G --> L[getHotkeyConflict\nrecorder detector]
Loading

Comments Outside Diff (2)

  1. apps/desktop/src/renderer/hotkeys/hooks/useBinding/useBinding.ts, line 7-9 (link)

    P2 Stale JSDoc after default-mode migration

    The useBinding JSDoc still says "bare chord string (legacy / shipped defaults, treated as physical mode)" — but every shipped default is now a v2 logical object via L(). A developer reading this comment will get a wrong mental model of what HOTKEYS[id].key returns.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/desktop/src/renderer/hotkeys/hooks/useBinding/useBinding.ts
    Line: 7-9
    
    Comment:
    **Stale JSDoc after default-mode migration**
    
    The `useBinding` JSDoc still says "bare chord string (legacy / shipped defaults, treated as physical mode)" — but every shipped default is now a v2 logical object via `L()`. A developer reading this comment will get a wrong mental model of what `HOTKEYS[id].key` returns.
    
    How can I resolve this? If you propose a fix, please make it concise.
  2. apps/desktop/plans/20260505-hotkey-adaptive-toggle-smoke-test.md, line 134-136 (link)

    P2 Stale "follow-up" suggestion — helper already shipped in this PR

    Lines 134–136 say "consider a useEffectiveLayoutMap() helper as a follow-up to make this unmissable," but useEffectiveLayoutMap (and its imperative sibling getEffectiveLayoutMap) are both implemented and exported in this very PR. The suggestion reads as if the helper still needs to be created.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/desktop/plans/20260505-hotkey-adaptive-toggle-smoke-test.md
    Line: 134-136
    
    Comment:
    **Stale "follow-up" suggestion — helper already shipped in this PR**
    
    Lines 134–136 say "consider a `useEffectiveLayoutMap()` helper as a follow-up to make this unmissable," but `useEffectiveLayoutMap` (and its imperative sibling `getEffectiveLayoutMap`) are both implemented and exported in this very PR. The suggestion reads as if the helper still needs to be created.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
apps/desktop/src/renderer/hotkeys/hooks/useBinding/useBinding.ts:7-9
**Stale JSDoc after default-mode migration**

The `useBinding` JSDoc still says "bare chord string (legacy / shipped defaults, treated as physical mode)" — but every shipped default is now a v2 logical object via `L()`. A developer reading this comment will get a wrong mental model of what `HOTKEYS[id].key` returns.

### Issue 2 of 3
apps/desktop/src/renderer/hotkeys/utils/resolveHotkeyFromEvent.ts:3
**Direct `useKeyboardLayoutStore` import contradicts the new banner warning**

`keyboardLayoutStore.ts` now warns "Do not import this store directly from dispatch / display / recorder code." The resolver still imports it directly — albeit only to call `useKeyboardLayoutStore.subscribe(rebuild)`, not to read `.getState().map`. The warning should clarify "do not read `.map` directly" rather than "do not import," or the subscription should be encapsulated (e.g. a `subscribeToEffectiveLayoutMap` helper in `keyboardPreferencesStore`). As written, a future developer sees the import, reads the banner, and may incorrectly treat the resolver as a violation to be "fixed" by removing the subscription — which would silently break live layout-switch reactivity.

### Issue 3 of 3
apps/desktop/plans/20260505-hotkey-adaptive-toggle-smoke-test.md:134-136
**Stale "follow-up" suggestion — helper already shipped in this PR**

Lines 134–136 say "consider a `useEffectiveLayoutMap()` helper as a follow-up to make this unmissable," but `useEffectiveLayoutMap` (and its imperative sibling `getEffectiveLayoutMap`) are both implemented and exported in this very PR. The suggestion reads as if the helper still needs to be created.

Reviews (1): Last reviewed commit: "fix(desktop/hotkeys): make adaptive-layo..." | Re-trigger Greptile

@@ -1,19 +1,13 @@
import { HOTKEYS, type HotkeyId } from "../registry";
import { useHotkeyOverridesStore } from "../stores/hotkeyOverridesStore";
import { useKeyboardLayoutStore } from "../stores/keyboardLayoutStore";
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Direct useKeyboardLayoutStore import contradicts the new banner warning

keyboardLayoutStore.ts now warns "Do not import this store directly from dispatch / display / recorder code." The resolver still imports it directly — albeit only to call useKeyboardLayoutStore.subscribe(rebuild), not to read .getState().map. The warning should clarify "do not read .map directly" rather than "do not import," or the subscription should be encapsulated (e.g. a subscribeToEffectiveLayoutMap helper in keyboardPreferencesStore). As written, a future developer sees the import, reads the banner, and may incorrectly treat the resolver as a violation to be "fixed" by removing the subscription — which would silently break live layout-switch reactivity.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/hotkeys/utils/resolveHotkeyFromEvent.ts
Line: 3

Comment:
**Direct `useKeyboardLayoutStore` import contradicts the new banner warning**

`keyboardLayoutStore.ts` now warns "Do not import this store directly from dispatch / display / recorder code." The resolver still imports it directly — albeit only to call `useKeyboardLayoutStore.subscribe(rebuild)`, not to read `.getState().map`. The warning should clarify "do not read `.map` directly" rather than "do not import," or the subscription should be encapsulated (e.g. a `subscribeToEffectiveLayoutMap` helper in `keyboardPreferencesStore`). As written, a future developer sees the import, reads the banner, and may incorrectly treat the resolver as a violation to be "fixed" by removing the subscription — which would silently break live layout-switch reactivity.

How can I resolve this? If you propose a fix, please make it concise.

@Kitenite Kitenite merged commit 611ab0c into main May 5, 2026
9 of 10 checks passed
@Kitenite Kitenite deleted the debug-hotkeys-smoke-test branch May 5, 2026 16:02
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ✅ Neon database branch

Thank you for your contribution! 🎉

saddlepaddle pushed a commit that referenced this pull request May 6, 2026
…h consumer (#4091)

The "Adaptive layout mapping" toggle in #4078 was wired into the resolver
index, the display hooks, and `getDispatchChord`, but missed the actual
hotkey registration in `useHotkey` and the recorder's conflict detector.
Worse, every shipped registry default was a bare-string (physical) chord,
which `bindingToDispatchChord` short-circuits — so flipping the toggle did
nothing for the 75 built-in shortcuts regardless of any other fix. Net
result: a Dvorak / QWERTZ user pressing the labeled-T key for `⌘T` saw
the menu update but no hotkey fire.

- Route every layout-map consumer through a single
  `useEffectiveLayoutMap` / `getEffectiveLayoutMap` chokepoint that gates
  by `adaptiveLayoutEnabled`. Adds a banner on `keyboardLayoutStore`
  warning future contributors not to read the raw store.
- Re-author printable defaults in `registry.ts` as `mode: "logical"`
  via an `L()` helper so the toggle actually moves them on non-US
  layouts. Named-key chords (arrows, Enter, Backspace, Tab) stay as
  bare strings since they're layout-stable.
- Flip `adaptiveLayoutEnabled` to default `true` to match macOS / VS
  Code / Cursor convention. Existing users who explicitly persisted a
  value keep it (zustand persist is write-on-set).
- Update the keyboard settings copy to describe the toggle in
  user-facing terms ("match shortcuts to printed key labels" vs
  "anchor to physical positions").
- Add `registry.test.ts` locking in the logical-default authoring
  convention so a future PR can't silently revert printable chords to
  bare strings.
- `apps/desktop/plans/20260505-hotkey-adaptive-toggle-smoke-test.md`
  documents the manual smoke test for non-US layouts.
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