Skip to content

feat(desktop): layout-aware keyboard shortcuts + dual-mode bindings#3839

Merged
Kitenite merged 22 commits into
mainfrom
keyboard-shortcut-analysi
Apr 28, 2026
Merged

feat(desktop): layout-aware keyboard shortcuts + dual-mode bindings#3839
Kitenite merged 22 commits into
mainfrom
keyboard-shortcut-analysi

Conversation

@Kitenite
Copy link
Copy Markdown
Collaborator

@Kitenite Kitenite commented Apr 28, 2026

Summary

  • Layout-aware hotkey display and dispatch driven by native-keymap (replaces hand-rolled US-layout heuristic) — picks up macOS input-source switches reliably.
  • New dual-mode binding type (ShortcutBinding v2): logical (the key labeled "P"), physical (event.code), and named, with translation through the current keymap at registration time so logical bindings work across layouts (e.g. logical meta+p registers as meta+r on Dvorak).
  • Settings UI: logical-mode recorder, cross-mode conflict detection (translates to dispatch chord before comparing), and removal of the per-binding mode badge in favor of the displayed glyph.

Test plan

  • Switch macOS input source between US and Dvorak and verify shortcut display updates live
  • Bind a logical shortcut on US layout, switch to Dvorak, verify it still triggers when pressing the labeled key
  • Record a binding using an F-key and a named key — verify they're forced to "named" mode
  • Try to bind two different logical/physical chords that map to the same physical key on the active layout — verify conflict is detected
  • Existing v1 string bindings continue to dispatch unchanged

Summary by cubic

Make desktop hotkeys layout-aware and add dual‑mode bindings so shortcuts display and fire correctly on any keyboard layout. Removes the old v1→v2 migration and adds reliable live OS layout updates via native-keymap.

  • New Features

    • Live layout service via native-keymap in main, streamed to the renderer over tRPC. Shows the printed glyph for each physical key and updates on input‑source changes.
    • ShortcutBinding v2 with modes: physical, logical, and named. Logical chords translate through the current keymap for dispatch and display (e.g., logical meta+p registers/renders as meta+r on Dvorak). Shipped defaults stay physical; the recorder defaults to logical for printable keys and forces named for F‑keys/special keys.
    • Settings: logical‑mode recorder, cross‑mode conflict detection (compares translated dispatch chords), and a cleaner list without the per‑binding mode badge.
  • Bug Fixes & Cleanup

    • AltGr no longer matches Ctrl+Alt, and hotkeys don’t fire during IME composition.
    • Removed the US‑layout heuristic; use authoritative layout id/map from native-keymap. v1 string bindings continue to work.
    • Display correctness on non‑US layouts: conflict dialog shows layout‑aware glyphs, and logical bindings render the correct key. Terminal forwarding and UI share the same translation step.
    • Deleted legacy v1→v2 migration and sanitizer code (and its call site); no behavior change for already‑migrated users.
    • Search bar click now dispatches the layout‑translated chord, so QUICK_OPEN works on non‑US layouts.
    • Layout subscription auto‑retries with capped backoff to keep live glyph updates and logical translation working after transient errors.
    • Edge cases: '+' no longer creates malformed logical chords, and keycap casing only uppercases ASCII letters to avoid multi‑character glyphs.
    • Docs: consolidated keyboard shortcut architecture into a single KEYBOARD_SYSTEM.md; moved prior plans to done/.
    • Build/CI: register native-keymap as a runtime‑external with asar‑unpack, and install Linux libx11-dev/libxkbfile-dev to fix node‑gyp builds.

Written for commit e9b1d20. Summary will update on new commits. Review in cubic

Summary by CodeRabbit

  • New Features

    • Live, layout-aware keyboard service with renderer sync for accurate hotkey glyphs and display.
    • New binding model (physical/logical/named) with structured persisted bindings and a formatting hook for UI.
  • Bug Fixes

    • Suppress hotkeys during IME composition and AltGr.
    • Conflict detection and reserved-key handling now use layout-aware dispatch chords.
  • Tests

    • Expanded coverage for capture, translation, parsing, and equality.
  • Documentation

    • Added design and QA plans.
  • Chores

    • Removed legacy one-time migration and obsolete sanitization utilities.

navigator.keyboard.layoutchange doesn't fire for macOS input-source
switches; native-keymap hooks the OS-level notification VSCode uses,
giving reliable on-the-fly layout updates. Replaces the hand-rolled
US-layout fingerprint heuristic with authoritative layout data from
main, exposed to the renderer via tRPC observable.
Merge utils/utils.ts into resolveHotkeyFromEvent.ts (one helper, shared
deps), route all top-level barrel imports through nested barrels, and
flag migrate.ts + MAC_US_DEAD_KEYS for sunset removal once the v1→v2
window closes.
Adds ShortcutBinding versioned shape (string for legacy/physical,
v2 object for logical/named) plus parseBinding/serializeBinding/
bindingsEqual helpers. All current bindings still match by event.code
— no behavior change. Sets up commit 2 to add layout-aware
logical-mode dispatch.
Logical-mode bindings ("the key labeled P, regardless of layout") are
translated through the current keymap to the equivalent event.code-based
chord at registration time. On Dvorak, logical meta+p registers as
meta+r so react-hotkeys-hook's event.code matcher fires when the user
presses physical KeyR (which prints P on Dvorak).

Translation re-runs on layout change for both useHotkey's react-hotkeys-
hook registration and resolveHotkeyFromEvent's reverse index used by
terminal forwarding.
…icts

Recorder now captures both event.code (codeChord) and event.key
(keyChord) plus a classification ("named" | "fkey" | "printable").
F-keys and named keys force "named" mode regardless of preference;
printable bindings honor preferredMode (defaults to "logical" so the
recorded chord follows the printed character).

Settings page passes preferredMode: "logical" and shows a small
"Letter / Position / Named" badge per binding so users can see how
each one matches.

Cross-mode conflict detection translates each binding to its dispatch
chord (event.code form, via the current keymap for logical bindings)
before comparing, so a logical and a physical binding that resolve to
the same physical key on this layout collide as expected.
Mode (physical / logical / named) is an implementation detail. The
badge was useful while debugging Phase 2 but adds clutter for users —
the displayed glyph and recording behavior already convey what matters.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 28, 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

Adds OS keyboard-layout support: main-process native-keymap reader and change events, a tRPC router/subscription, renderer layout store, new binding types and translation/serialize utilities, layout-aware recording/display/hooks, many tests and docs, and removes legacy sanitizer/migration/detect-US utilities.

Changes

Cohort / File(s) Summary
Dependency & Plans
apps/desktop/package.json, apps/desktop/plans/20260427-keyboard-layout-plan.md, apps/desktop/plans/20260428-keyboard-qa-plan.md
Adds native-keymap@^3.3.9; introduces two planning/QA docs describing phased architecture, testing, and rollout.
Main process layout reader
apps/desktop/src/main/lib/keyboardLayout.ts
New module that lazily loads native-keymap, reads/caches layout snapshot (layoutId + unshifted glyph map), exposes getKeyboardLayoutSnapshot() and onKeyboardLayoutChange(), and logs/falls back on errors.
tRPC router
apps/desktop/src/lib/trpc/routers/keyboardLayout.ts, apps/desktop/src/lib/trpc/routers/index.ts
Adds keyboardLayout router with get query and changes observable; root router exposes keyboardLayout.
Renderer layout store
apps/desktop/src/renderer/hotkeys/stores/keyboardLayoutStore.ts
New Zustand store useKeyboardLayoutStore holding `{ map: ReadonlyMap<string,string>
Binding types & core utils
apps/desktop/src/renderer/hotkeys/types.ts, apps/desktop/src/renderer/hotkeys/utils/binding.ts, apps/desktop/src/renderer/hotkeys/utils/binding.test.ts
Adds BindingMode, ShortcutBinding, ParsedBinding; implements parseBinding, serializeBinding, bindingToDispatchChord, translateLogicalChord, bindingsEqual, NAMED_KEYS, isFunctionKey, defaultModeForChord and comprehensive tests for classification, translation, canonicalization, and equality.
Display & glyph mapping
apps/desktop/src/renderer/hotkeys/display.ts, apps/desktop/src/renderer/hotkeys/display.test.ts
formatHotkeyDisplay gains optional layoutMap; new glyphForCode returns single-character glyphs or null; tests added for layout-aware display and fallbacks.
Recording & resolution
apps/desktop/src/renderer/hotkeys/hooks/useRecordHotkeys/useRecordHotkeys.ts, .../useRecordHotkeys.test.ts
Recording now returns CapturedHotkey (with codeChord, optional keyChord, classification); adds resolveCapturedBinding, preferredMode option, conflict detection using dispatch chords, and updates callbacks to use serialized ShortcutBinding.
Hooks, stores & re-exports
apps/desktop/src/renderer/hotkeys/hooks/..., apps/desktop/src/renderer/hotkeys/index.ts, apps/desktop/src/renderer/hotkeys/hooks/useHotkeyDisplay/index.ts
Hooks updated to return/consume ShortcutBinding; added useFormatBinding and getDispatchChord; useHotkey/useHotkeyDisplay become layout-aware and ignore IME/AltGraph; barrel exports expanded for new types/utilities.
Event resolution & terminal handling
apps/desktop/src/renderer/hotkeys/utils/resolveHotkeyFromEvent.ts, .../resolveHotkeyFromEvent.test.ts, apps/desktop/src/renderer/hotkeys/utils/index.ts
Chord generation suppressed during IME composition and AltGr; adds isTerminalReservedEvent; registered-app chord index rebuilt on layout changes using dispatch chords derived from ShortcutBinding.
Overrides store & persistence typing
apps/desktop/src/renderer/hotkeys/stores/hotkeyOverridesStore.ts
Overrides state retyped to `Record<string, ShortcutBinding
Removed migration & sanitizers
apps/desktop/src/renderer/hotkeys/migrate.ts, apps/desktop/src/renderer/hotkeys/utils/sanitizeOverride.ts, apps/desktop/src/renderer/hotkeys/utils/detectUSLayout.ts, apps/desktop/src/renderer/hotkeys/utils/utils.ts
Removed hotkey override migration, sanitizeOverride, US-layout detection utility, and legacy isTerminalReservedEvent util; associated tests deleted.
Consumer updates
apps/desktop/src/renderer/routes/.../SearchBarTrigger.tsx, apps/desktop/src/renderer/routes/.../settings/keyboard/page.tsx
Consumers dispatch dispatch-ready chords via getDispatchChord(...); settings use preferredMode: "logical" and display bindings via useFormatBinding.
Tests & removed tests
apps/desktop/src/renderer/hotkeys/.../*.test.ts, apps/desktop/src/renderer/hotkeys/utils/overrideSanitizer.test.ts
Added/updated tests across binding, display, record/capture, resolveHotkeyFromEvent; removed long-running sanitizeOverride tests.
Docs/JSDoc
apps/desktop/src/renderer/lib/terminal/line-edit-translations.ts
JSDoc clarifies contract for using event.key vs event.code in line-edit translations.

Sequence Diagram(s)

sequenceDiagram
    actor OS
    participant NativeKeymap as NativeKeymap
    participant Main as Main Process
    participant tRPC as tRPC Router
    participant Renderer as Renderer
    participant Store as Layout Store

    OS->>NativeKeymap: keyboard layout change
    NativeKeymap-->>Main: onDidChangeKeyboardLayout
    Main->>Main: getKeyboardLayoutSnapshot()
    Main->>tRPC: emit layout snapshot (changes observable)
    tRPC-->>Renderer: subscription payload
    Renderer->>Store: applySnapshot(map, layoutId)
    Store-->>Renderer: layout state updated
Loading
sequenceDiagram
    actor User
    participant Browser as Browser Event
    participant Hook as useHotkey Hook
    participant Binding as Binding Utils
    participant Layout as KeyboardLayout Store
    participant Display as formatHotkeyDisplay

    User->>Browser: press key
    Browser-->>Hook: KeyboardEvent (key, code)
    Hook->>Layout: get layoutMap
    Hook->>Binding: bindingToDispatchChord(storedBinding, layoutMap)
    alt binding.mode == "logical" and layoutMap present
        Binding->>Hook: translated dispatch chord
    else
        Hook-->>Hook: use physical/named chord
    end
    Hook->>Display: formatHotkeyDisplay(chord, layoutMap)
    Display-->>Hook: formatted display
    Hook-->>User: show hotkey UI
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~70 minutes

Possibly related PRs

Poem

🐰 I hopped through keys and maps tonight,

I turned each code to glyphs just right,
I chased AltGr and IME with care,
I stitched chords from main to renderer air,
Tiny paws, layout magic bright.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% 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 clearly and concisely summarizes the main change: introducing layout-aware keyboard shortcuts and dual-mode bindings. It accurately reflects the primary objectives of the changeset.
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.
Description check ✅ Passed PR description is mostly complete with clear summary, test plan checklist, and comprehensive auto-generated summary by cubic covering features, bug fixes, and cleanup.

✏️ 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 keyboard-shortcut-analysi

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 Apr 28, 2026

Greptile Summary

This PR replaces the hand-rolled US-layout heuristic with native-keymap (via a new main-process service bridged over tRPC), adds a v2 ShortcutBinding type with physical / logical / named modes, and wires layout-aware display and cross-mode conflict detection into the settings UI. The architecture and the core useHotkey / recorder / resolver logic are sound and well-tested.

Two P1 bugs affect non-US layout users with logical bindings:

  • useHotkeyDisplay skips the logical→physical translation step — it calls formatHotkeyDisplay(parseBinding(binding).chord, ...) directly. For a logical \"meta+p\" on Dvorak this resolves glyphForCode(\"p\") → \"KeyP\" → \"l\", showing \"⌘L\" in the Settings page instead of the correct \"⌘P\". useHotkey correctly translates first and shows \"⌘P\".
  • SearchBarTrigger dispatches the raw logical chorddispatchHotkeyEvent(parseBinding(binding).chord) fires a KeyboardEvent with code=\"KeyP\", but react-hotkeys-hook is registered with the translated \"meta+r\" (code=\"KeyR\"), so the QUICK\_OPEN handler never fires on a non-US layout when the user clicks the button.

Confidence Score: 4/5

Safe to merge for US-layout users; two P1 bugs break logical-binding display and programmatic dispatch on non-US layouts.

Core architecture (main-process keymap service, tRPC bridge, binding translation, recorder, conflict detection) is solid and well-tested. Two P1 issues exist for the non-US layout + logical binding combination: useHotkeyDisplay shows wrong glyphs, and SearchBarTrigger programmatic dispatch fails. Both are two-line fixes following the same pattern useHotkey already implements correctly.

apps/desktop/src/renderer/hotkeys/hooks/useHotkeyDisplay/useHotkeyDisplay.ts and apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/SearchBarTrigger/SearchBarTrigger.tsx

Important Files Changed

Filename Overview
apps/desktop/src/renderer/hotkeys/hooks/useHotkeyDisplay/useHotkeyDisplay.ts Passes raw logical chord to formatHotkeyDisplay without translating to physical first — shows wrong glyph on non-US layouts for logical bindings (P1)
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/SearchBarTrigger/SearchBarTrigger.tsx dispatchHotkeyEvent uses logical chord (code=KeyP on Dvorak) instead of translated physical chord (code=KeyR), programmatic trigger fails for logical bindings on non-US layouts (P1)
apps/desktop/src/renderer/hotkeys/utils/binding.ts New binding utilities (parseBinding, serializeBinding, translateLogicalChord, bindingsEqual) — well-tested; minor inconsistency in named-key early-return and duplicated NAMED_KEYS set
apps/desktop/src/renderer/hotkeys/hooks/useHotkey/useHotkey.ts Correctly translates logical bindings via translateLogicalChord before registering with react-hotkeys-hook; adds IME/AltGr suppression via ignoreEventWhen
apps/desktop/src/renderer/hotkeys/hooks/useRecordHotkeys/useRecordHotkeys.ts Captures both codeChord and keyChord; resolveCapturedBinding correctly forces F-key/named to named mode; cross-mode conflict detection via dispatchChordFor is correct
apps/desktop/src/main/lib/keyboardLayout.ts New main-process service wrapping native-keymap — lazy init, snapshot cache, EventEmitter for layout-change notifications, graceful EMPTY fallback on load failure
apps/desktop/src/renderer/hotkeys/stores/keyboardLayoutStore.ts Process-lifetime Zustand store subscribed to main-process layout changes via tRPC; correctly primed with initial snapshot by the subscription observable
apps/desktop/src/lib/trpc/routers/keyboardLayout.ts New tRPC router exposing get (query) and changes (subscription); subscription primes subscriber with current snapshot; cleanup via returned unsubscribe function is correct
apps/desktop/src/renderer/hotkeys/utils/resolveHotkeyFromEvent.ts Added IME composition guard, AltGr suppression, logical-binding translation in buildRegisteredAppChords, and isTerminalReservedEvent helper — all correct
apps/desktop/src/renderer/hotkeys/migrate.ts Replaces hand-rolled detectUSLayout with native-keymap query; inlines isUSCompatibleKeymap with SUNSET comment; migration logic unchanged
Prompt To Fix All With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/hotkeys/hooks/useHotkeyDisplay/useHotkeyDisplay.ts
Line: 7-16

Comment:
**Wrong glyph for logical bindings on non-US layouts**

`parseBinding(binding).chord` for a logical binding (e.g. `"meta+p"`) is passed directly to `formatHotkeyDisplay`. That function calls `glyphForCode("p", layoutMap)`, which resolves to `canonicalToScanCode("p") → "KeyP"`, then does `layoutMap.get("KeyP")`. On Dvorak, `"KeyP"` prints `"l"`, so the settings page shows `"⌘L"` instead of the correct `"⌘P"`. `useHotkey` correctly translates first (`translateLogicalChord("meta+p", dvorakMap) → "meta+r"`, which maps to `"p"` on Dvorak, yielding `"⌘P"`), but `useHotkeyDisplay` skips that step — so the Settings list and every `HotkeyLabel` rendering a logical binding on a non-US layout will show the wrong key glyph.

Fix: resolve the chord the same way `useHotkey` does before passing to `formatHotkeyDisplay`.

```ts
const parsed = binding ? parseBinding(binding) : null;
const chord =
  parsed?.mode === "logical"
    ? (translateLogicalChord(parsed.chord, layoutMap) ?? parsed.chord)
    : (parsed?.chord ?? null);
return useMemo(
  () => formatHotkeyDisplay(chord, PLATFORM, layoutMap),
  [chord, layoutMap],
);
```

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

---

This is a comment left during a code review.
Path: apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/SearchBarTrigger/SearchBarTrigger.tsx
Line: 42-47

Comment:
**Programmatic dispatch fires wrong event for logical bindings on non-US layouts**

`dispatchHotkeyEvent(parseBinding(binding).chord)` passes the raw **logical** chord (e.g. `"meta+p"`) to `dispatchHotkeyEvent`, which sets `code = keyToCode("p") = "KeyP"`. On Dvorak, `useHotkey` registers react-hotkeys-hook with the **translated physical** chord `"meta+r"` (via `translateLogicalChord`). Because react-hotkeys-hook matches by `event.code`, the dispatched `code="KeyP"` event does not match the `"KeyR"` registration — the QUICK_OPEN handler never fires when the user clicks the button on a non-US layout.

```ts
const handleClick = useCallback(() => {
  const binding = getBinding("QUICK_OPEN");
  if (binding) {
    const parsed = parseBinding(binding);
    const layoutMap = useKeyboardLayoutStore.getState().map;
    const chord =
      parsed.mode === "logical"
        ? (translateLogicalChord(parsed.chord, layoutMap) ?? parsed.chord)
        : parsed.chord;
    dispatchHotkeyEvent(chord);
  }
}, []);
```

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

---

This is a comment left during a code review.
Path: apps/desktop/src/renderer/hotkeys/utils/binding.ts
Line: 3-25

Comment:
**Duplicate named-key set with `NAMED_CODES` in `useRecordHotkeys.ts`**

`NAMED_KEYS` here and `NAMED_CODES` in `useRecordHotkeys.ts` serve similar roles but are defined independently. They diverge: `NAMED_KEYS` includes aliases (`"return"`, `"esc"`, `"up"`, `"down"`, `"left"`, `"right"`) absent from `NAMED_CODES`. A future edit to one set that misses the other will silently produce inconsistent `defaultModeForChord` / classification results. Consider exporting `NAMED_KEYS` from `binding.ts` and reusing it in `useRecordHotkeys.ts`.

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

---

This is a comment left during a code review.
Path: apps/desktop/src/renderer/hotkeys/utils/binding.ts
Line: 118-120

Comment:
**`translateLogicalChord` returns un-canonicalized chord for named / F-keys**

When the key is in `NAMED_KEYS` or is an F-key, the function returns the original `chord` argument unchanged (preserving input modifier order). For all other keys it returns `parts.join("+")` from a `canonicalizeChord`-sorted split. Callers in `buildRegisteredAppChords` and `dispatchChordFor` always apply `canonicalizeChord` to the result, so there is no observable bug today — but it is a latent footgun for any future caller that compares without canonicalizing.

```ts
// Canonicalize before the early-return too
if (NAMED_KEYS.has(key) || isFunctionKey(key)) return canonicalizeChord(chord);
```

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

Reviews (1): Last reviewed commit: "chore(desktop): drop mode badge from key..." | Re-trigger Greptile

Comment thread apps/desktop/src/renderer/hotkeys/utils/binding.ts
Comment thread apps/desktop/src/renderer/hotkeys/utils/binding.ts
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 28, 2026

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ✅ Neon database branch

Thank you for your contribution! 🎉

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: 9

Caution

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

⚠️ Outside diff range comments (1)
apps/desktop/src/renderer/hotkeys/hooks/useRecordHotkeys/useRecordHotkeys.ts (1)

232-236: ⚠️ Potential issue | 🟠 Major

Only use Backspace/Delete for unassign on the bare keypress.

This branch also catches meta+Backspace, ctrl+Delete, etc., so those named bindings can never be recorded even though the new binding model supports them. Restrict unassign to the unmodified keypress, or move it to an explicit UI action.

Minimal fix
-			if (event.key === "Backspace" || event.key === "Delete") {
+			if (
+				(event.key === "Backspace" || event.key === "Delete") &&
+				!event.metaKey &&
+				!event.ctrlKey &&
+				!event.altKey &&
+				!event.shiftKey
+			) {
 				setOverride(recordingId, null);
 				optionsRef.current?.onUnassign?.(recordingId);
 				return;
 			}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/hotkeys/hooks/useRecordHotkeys/useRecordHotkeys.ts`
around lines 232 - 236, The unassign branch in useRecordHotkeys is triggered for
any Backspace/Delete keypress even when modifiers are held, blocking recording
of bindings like Meta+Backspace or Ctrl+Delete; update the condition in the
handler that checks event.key === "Backspace" || event.key === "Delete" to also
require no modifier keys (e.g. event.metaKey, event.ctrlKey, event.altKey,
event.shiftKey are all false) before calling setOverride(recordingId, null) and
optionsRef.current?.onUnassign?.(recordingId); ensure this logic lives inside
the same function in useRecordHotkeys so only a bare Backspace/Delete unassigns
and modified key combos can still be recorded.
🧹 Nitpick comments (1)
apps/desktop/src/renderer/hotkeys/hooks/useHotkeyDisplay/useHotkeyDisplay.ts (1)

4-6: Prefer tsconfig alias imports for consistency.

These new imports can be switched to alias-based paths to match repository conventions and keep import style uniform.

As per coding guidelines: “apps/desktop/**/*.{ts,tsx}: Use alias as defined in tsconfig.json when possible”.

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

In `@apps/desktop/src/renderer/hotkeys/hooks/useHotkeyDisplay/useHotkeyDisplay.ts`
around lines 4 - 6, Update the three imports to use the repo's tsconfig path
aliases instead of relative paths: replace the import of useKeyboardLayoutStore,
the type HotkeyDisplay, and the parseBinding utility with their aliased module
paths (keep the same symbol names: useKeyboardLayoutStore, HotkeyDisplay,
parseBinding) so the file useHotkeyDisplay.ts imports via the configured
tsconfig aliases rather than "../../stores/keyboardLayoutStore", "../../types",
and "../../utils/binding".
🤖 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/20260427-keyboard-layout-plan.md`:
- Line 4: The branch name in the plan is truncated: replace the broken branch
identifier `keyboard-shortcut-analysi` with the correct, full branch name (e.g.,
`keyboard-shortcut-analysis` or the intended branch) in the document so the
Branch line reflects the true branch name; update the `**Branch:**` value in the
file to the corrected branch string.
- Line 325: The acceptance criteria line "No new runtime deps" contradicts Phase
1 which adds the runtime dependency native-keymap; update the acceptance wording
to reflect the addition (e.g., "Adds native-keymap as a new runtime dependency"
or relax to "Allows one new runtime dep: native-keymap") so Phase 1 and the
acceptance criteria are consistent—search for the phrase "No new runtime deps"
and the Phase 1 section referencing native-keymap to make the edit.
- Around line 227-252: The fenced code blocks in the markdown (showing the
keyboard layout architecture diagram and the listed source files) are missing
language tags and trigger MD040; update each triple-backtick fence to include an
appropriate language (e.g., text or ts) for the blocks that contain plain
diagrams or TypeScript paths. Locate the fences around the diagram and the file
references (mentions of apps/desktop/src/main/lib/keyboardLayout.ts,
apps/desktop/src/renderer/hotkeys/stores/keyboardLayoutStore.ts,
apps/desktop/src/lib/trpc/routers/keyboardLayout.ts, and display.ts →
formatHotkeyDisplay) and add the language tag after the opening ``` so the
linter recognizes them.

In `@apps/desktop/src/renderer/hotkeys/display.ts`:
- Around line 77-80: The code currently unconditionally calls v.toUpperCase(),
which can expand some single glyphs (e.g., "ß" → "SS") and break keycap
rendering; change the logic to first compute the uppercased form (const upper =
v.toUpperCase()) and only use upper if it remains a single code unit
(upper.length === 1); otherwise fall back to the original single-character glyph
(v) or null as appropriate so you never return a multi-character string for the
key display (update the expression that currently returns v.length === 1 ?
v.toUpperCase() : null).

In
`@apps/desktop/src/renderer/hotkeys/hooks/useRecordHotkeys/useRecordHotkeys.ts`:
- Around line 86-96: The code currently treats any single non-whitespace
event.key as a logical token, which allows separator characters like "+" to
produce malformed logical chords (e.g., "meta++"); instead, detect unencodable
glyphs (at minimum the chord separator "+", and any other characters that would
collide with the joiner) when building keyChord from produced and if detected,
revert to physical mode: set keyChord = codeChord and set classification (or the
value used to indicate mode) to "physical". Apply the same change in both places
that build logical keyChord (the block using produced at lines ~86-96 and the
similar block at ~103-111) so resolveCapturedBinding no longer receives
malformed logical bindings.

In `@apps/desktop/src/renderer/hotkeys/migrate.ts`:
- Around line 27-39: The current isUSCompatibleKeymap heuristic is too narrow
and can falsely allow non-US remaps; update the logic in isUSCompatibleKeymap to
either (a) check navigator.keyboard/layoutId or the platform-specific layout
identifier against an explicit allowlist of known US-compatible macOS layouts,
or (b) validate every event.code key that MAC_US_DEAD_KEYS relies on by
comparing unshifted[code] for all keys present in MAC_US_DEAD_KEYS rather than
only KeyA/KeyQ/KeyW/KeyZ/Semicolon/Quote so sanitizeOverride(..., {
assumeUSMacLayout: true }) only runs when the full set matches; reference the
isUSCompatibleKeymap function, MAC_US_DEAD_KEYS, and sanitizeOverride to locate
and change the gate logic.

In `@apps/desktop/src/renderer/hotkeys/stores/keyboardLayoutStore.ts`:
- Around line 38-43: The subscription error handler currently only logs errors
so the store's map can remain null permanently; change the handler on
electronTrpcClient.keyboardLayout.changes.subscribe to attempt recovery: call
the existing applySnapshot path or a new bootstrap function (e.g.,
bootstrapKeyboardLayout/loadKeyboardLayout) to fetch the latest keyboard layout
once on error, then schedule/resume a re-subscribe retry with backoff (or at
least a fixed retry) so the store's map is set instead of staying null; ensure
the handler replaces the console.error with logic that: (1) invokes the same
snapshot application used on successful onData, (2) triggers a one-time fetch of
keyboardLayout via electronTrpcClient.keyboardLayout.query (or the existing
bootstrap function) to populate map immediately, and (3) re-establishes the
subscription (using electronTrpcClient.keyboardLayout.changes.subscribe and
applySnapshot) with exponential or capped retry to avoid permanent failure.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/SearchBarTrigger/SearchBarTrigger.tsx`:
- Around line 43-45: The click handler currently takes
parseBinding(binding).chord and dispatches it directly, which fails on non‑US
layouts; instead, after getBinding("QUICK_OPEN") and parseBinding(binding), pass
the parsed chord through the same layout-aware translation used for hotkey
registration (the code path that converts logical binding tokens into dispatch
form / event.code tokens) and then call dispatchHotkeyEvent with that translated
dispatch-chord. Locate where hotkeys are registered (the layout-aware
translation function used there) and reuse it so SearchBarTrigger synthesizes
the same dispatch chord the runtime expects rather than the raw parsed chord.

In `@apps/desktop/src/renderer/routes/_authenticated/settings/keyboard/page.tsx`:
- Around line 286-289: The conflict dialog is calling
formatHotkeyDisplay(parseBinding(pendingConflict.binding).chord, PLATFORM)
without the layoutMap, causing printable bindings to render as US glyphs; update
that invocation to pass the current layoutMap (e.g., formatHotkeyDisplay(...,
PLATFORM, layoutMap)) so parseBinding/pendingConflict display uses the active
key layout; ensure any other nearby calls in the same reassignment/dialog
rendering (references: formatHotkeyDisplay, parseBinding, pendingConflict,
PLATFORM, HOTKEYS) also receive layoutMap for consistent layout-aware rendering.

---

Outside diff comments:
In
`@apps/desktop/src/renderer/hotkeys/hooks/useRecordHotkeys/useRecordHotkeys.ts`:
- Around line 232-236: The unassign branch in useRecordHotkeys is triggered for
any Backspace/Delete keypress even when modifiers are held, blocking recording
of bindings like Meta+Backspace or Ctrl+Delete; update the condition in the
handler that checks event.key === "Backspace" || event.key === "Delete" to also
require no modifier keys (e.g. event.metaKey, event.ctrlKey, event.altKey,
event.shiftKey are all false) before calling setOverride(recordingId, null) and
optionsRef.current?.onUnassign?.(recordingId); ensure this logic lives inside
the same function in useRecordHotkeys so only a bare Backspace/Delete unassigns
and modified key combos can still be recorded.

---

Nitpick comments:
In
`@apps/desktop/src/renderer/hotkeys/hooks/useHotkeyDisplay/useHotkeyDisplay.ts`:
- Around line 4-6: Update the three imports to use the repo's tsconfig path
aliases instead of relative paths: replace the import of useKeyboardLayoutStore,
the type HotkeyDisplay, and the parseBinding utility with their aliased module
paths (keep the same symbol names: useKeyboardLayoutStore, HotkeyDisplay,
parseBinding) so the file useHotkeyDisplay.ts imports via the configured
tsconfig aliases rather than "../../stores/keyboardLayoutStore", "../../types",
and "../../utils/binding".
🪄 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: a0084a12-af81-4370-9c07-ab4557c63f61

📥 Commits

Reviewing files that changed from the base of the PR and between a8e8556 and 5749f28.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (29)
  • apps/desktop/package.json
  • apps/desktop/plans/20260427-keyboard-layout-plan.md
  • apps/desktop/src/lib/trpc/routers/index.ts
  • apps/desktop/src/lib/trpc/routers/keyboardLayout.ts
  • apps/desktop/src/main/lib/keyboardLayout.ts
  • apps/desktop/src/renderer/hotkeys/display.test.ts
  • apps/desktop/src/renderer/hotkeys/display.ts
  • apps/desktop/src/renderer/hotkeys/hooks/index.ts
  • apps/desktop/src/renderer/hotkeys/hooks/useBinding/useBinding.ts
  • apps/desktop/src/renderer/hotkeys/hooks/useHotkey/useHotkey.ts
  • apps/desktop/src/renderer/hotkeys/hooks/useHotkeyDisplay/useHotkeyDisplay.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/index.ts
  • apps/desktop/src/renderer/hotkeys/migrate.ts
  • apps/desktop/src/renderer/hotkeys/stores/hotkeyOverridesStore.ts
  • apps/desktop/src/renderer/hotkeys/stores/keyboardLayoutStore.ts
  • apps/desktop/src/renderer/hotkeys/types.ts
  • apps/desktop/src/renderer/hotkeys/utils/binding.test.ts
  • apps/desktop/src/renderer/hotkeys/utils/binding.ts
  • apps/desktop/src/renderer/hotkeys/utils/detectUSLayout.ts
  • apps/desktop/src/renderer/hotkeys/utils/index.ts
  • apps/desktop/src/renderer/hotkeys/utils/resolveHotkeyFromEvent.test.ts
  • apps/desktop/src/renderer/hotkeys/utils/resolveHotkeyFromEvent.ts
  • apps/desktop/src/renderer/hotkeys/utils/sanitizeOverride.ts
  • apps/desktop/src/renderer/hotkeys/utils/utils.ts
  • apps/desktop/src/renderer/lib/terminal/line-edit-translations.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/SearchBarTrigger/SearchBarTrigger.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/keyboard/page.tsx
💤 Files with no reviewable changes (2)
  • apps/desktop/src/renderer/hotkeys/utils/utils.ts
  • apps/desktop/src/renderer/hotkeys/utils/detectUSLayout.ts

Comment thread apps/desktop/plans/done/20260427-keyboard-layout-plan.md
Comment thread apps/desktop/plans/done/20260427-keyboard-layout-plan.md
Comment thread apps/desktop/plans/20260427-keyboard-layout-plan.md Outdated
Comment thread apps/desktop/src/renderer/hotkeys/display.ts Outdated
Comment thread apps/desktop/src/renderer/hotkeys/migrate.ts Outdated
Comment thread apps/desktop/src/renderer/hotkeys/stores/keyboardLayoutStore.ts Outdated
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.

6 issues found across 30 files

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/display.ts">

<violation number="1" location="apps/desktop/src/renderer/hotkeys/display.ts:79">
P3: Uppercasing can expand some single-character layout glyphs (for example `ß` → `SS`), causing multi-character hotkey labels despite the single-glyph guard.</violation>
</file>

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

<violation number="1" location="apps/desktop/src/renderer/hotkeys/hooks/useHotkeyDisplay/useHotkeyDisplay.ts:12">
P2: Logical-mode bindings are displayed without layout translation, so displayed keys can be wrong on non-US layouts.</violation>
</file>

<file name="apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/SearchBarTrigger/SearchBarTrigger.tsx">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/SearchBarTrigger/SearchBarTrigger.tsx:45">
P1: The trigger dispatches `parseBinding(binding).chord` directly, which skips logical->dispatch translation and can break Search click activation for logical bindings on non-US keyboard layouts.</violation>
</file>

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

<violation number="1" location="apps/desktop/src/renderer/hotkeys/hooks/useRecordHotkeys/useRecordHotkeys.ts:92">
P1: The `+` character passes both `length === 1` and `/\S/` checks but will produce a malformed chord like `meta++` since `+` is the separator in chord syntax. This binding can't be round-tripped by `canonicalizeChord` or `parseBinding`. Filter out `+` here and let `keyChord` fall back to `codeChord`.</violation>

<violation number="2" location="apps/desktop/src/renderer/hotkeys/hooks/useRecordHotkeys/useRecordHotkeys.ts:93">
P1: Don't store shifted symbol glyphs as logical chord tokens. Shortcuts like Ctrl+Shift+2 record as `ctrl+shift+@`, which cannot be translated by the unshifted layout map and won't dispatch as the pressed key.</violation>

<violation number="3" location="apps/desktop/src/renderer/hotkeys/hooks/useRecordHotkeys/useRecordHotkeys.ts:168">
P2: Mirror the hotkey registration fallback here. Returning `null` skips conflict detection for logical chords that still register with `parsed.chord`.</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/hooks/useRecordHotkeys/useRecordHotkeys.ts Outdated
Comment thread apps/desktop/src/renderer/hotkeys/hooks/useHotkeyDisplay/useHotkeyDisplay.ts Outdated
Comment thread apps/desktop/src/renderer/hotkeys/hooks/useRecordHotkeys/useRecordHotkeys.ts Outdated
Comment thread apps/desktop/src/renderer/hotkeys/display.ts Outdated
The conflict dialog called formatHotkeyDisplay without the layout map,
so a captured chord shown there used US glyphs even on German/QWERTZ.
Added a useFormatChord hook for the "format an arbitrary chord, before
it's saved" case and used it here.
…patch

A logical binding records the produced character ("z"), not a scan-code
token. useHotkey was already translating to event.code form before
registering with react-hotkeys-hook, but useHotkeyDisplay /
useFormatChord were passing the literal "z" to the keymap-based glyph
lookup, which treats it as the canonical token for KeyZ — rendering
"⌘Y" on QWERTZ for what should display as "⌘Z".

Extract bindingToDispatchChord into utils/binding.ts and use it from
every site that needs to resolve a binding to its event.code chord
(useHotkey, useHotkeyDisplay, useFormatBinding, conflict detection,
terminal forwarding reverse index). Renames useFormatChord →
useFormatBinding now that it accepts a binding shape rather than a
raw chord.
Manual test plan covering Phase 0–2: smoke regression on US, layout-
aware display, logical bindings (the Phase 2 payoff), edge cases (IME,
AltGr, terminal forwarding, migration), storage shape, and triage notes
for failure modes.
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.

♻️ Duplicate comments (1)
apps/desktop/src/renderer/hotkeys/hooks/useRecordHotkeys/useRecordHotkeys.ts (1)

85-95: ⚠️ Potential issue | 🟠 Major

Fall back to physical mode when the logical glyph contains the chord separator.

The check at line 91 allows "+" through since it's a single non-whitespace character. When a user records Meta++ (meta plus the plus key), keyChord becomes "meta++", which cannot be parsed correctly because + is the chord separator. The comment on line 89-90 says "caller falls back to physical," but resolveCapturedBinding doesn't implement this fallback.

Proposed fix
 	if (classification === "printable") {
 		const produced = (event.key ?? "").toLowerCase();
 		// Single printable char only — strings like "Dead", "Process" or
 		// multi-char IME output stay on codeChord (caller falls back to
 		// physical when forced into logical mode).
-		if (produced.length === 1 && /\S/.test(produced)) {
+		if (produced.length === 1 && /\S/.test(produced) && produced !== "+") {
 			keyChord = [...ordered, produced].join("+");
+		} else {
+			// Unencodable glyph — force physical fallback
+			keyChord = codeChord;
 		}
 	}

And in resolveCapturedBinding:

 export function resolveCapturedBinding(
 	captured: CapturedHotkey,
 	preferredMode: "physical" | "logical",
 ): ParsedBinding {
 	if (captured.classification === "fkey" || captured.classification === "named")
 		return { mode: "named", chord: captured.codeChord };
-	const mode: BindingMode = preferredMode;
-	const chord = mode === "logical" ? captured.keyChord : captured.codeChord;
-	return { mode, chord };
+	// Fall back to physical if keyChord couldn't encode the logical glyph
+	if (preferredMode === "logical" && captured.keyChord !== captured.codeChord) {
+		return { mode: "logical", chord: captured.keyChord };
+	}
+	return { mode: "physical", chord: captured.codeChord };
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/hotkeys/hooks/useRecordHotkeys/useRecordHotkeys.ts`
around lines 85 - 95, The recorded logical glyph can contain the chord separator
"+" (e.g., pressing Meta+Plus), so update useRecordHotkeys: in the printable
branch (where produced is derived) ensure produced.length === 1 &&
/\S/.test(produced) && produced !== "+" (or more generally: does not include the
chord separator) before building keyChord, so we don't create ambiguous keyChord
strings; and update resolveCapturedBinding to detect when a logical glyph
contains the separator ("+") and in that case fall back to using the
physical/code-based binding (i.e., treat the capture as non-printable and
resolve via the codeChord path). Ensure you reference/use useRecordHotkeys,
produced, keyChord, codeChord, classification and resolveCapturedBinding when
making these changes.
🧹 Nitpick comments (4)
apps/desktop/src/renderer/hotkeys/utils/binding.test.ts (1)

170-175: Minor: Test name may be misleading.

The test is named "preserves modifier order from the input chord" but canonicalizeChord sorts modifiers alphabetically. The test actually verifies that modifiers are canonicalized correctly (alt+meta+shift+...), not that input order is preserved.

Suggested rename
-	it("preserves modifier order from the input chord", () => {
-		// Verify modifiers stay in their input order; canonicalizeChord sorts them
+	it("canonicalizes modifier order in output chord", () => {
+		// Verify modifiers are sorted alphabetically by canonicalizeChord
 		const result = translateLogicalChord("alt+meta+shift+p", dvorakMap);
 		expect(result).toBe("alt+meta+shift+r");
 	});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/hotkeys/utils/binding.test.ts` around lines 170 -
175, Rename the test to reflect that modifiers are canonicalized rather than
preserved; update the test name from "preserves modifier order from the input
chord" to something like "canonicalizes modifier order (alphabetical) via
canonicalizeChord" and ensure it still calls translateLogicalChord with the same
inputs (translateLogicalChord and canonicalizeChord are the relevant symbols) so
the assertion expecting "alt+meta+shift+r" remains correct.
apps/desktop/src/renderer/hotkeys/utils/binding.ts (1)

4-26: Consider consolidating NAMED_KEYS with NAMED_CODES from useRecordHotkeys.ts.

Both sets serve similar purposes (identifying named/special keys) but have different entries. NAMED_KEYS includes aliases like "return", "esc", "up", "down", while NAMED_CODES doesn't. If the aliasing is intentional (to handle both raw and normalized tokens), consider extracting a shared constant or documenting the distinction.

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

In `@apps/desktop/src/renderer/hotkeys/utils/binding.ts` around lines 4 - 26,
NAMED_KEYS in binding.ts largely overlaps with NAMED_CODES in
useRecordHotkeys.ts but contains aliases ("return", "esc", "up", "down") causing
duplication and potential divergence; consolidate by extracting a single shared
constant (e.g., NAMED_KEY_SET) that includes all normalized names and aliases or
clearly document/justify the alias differences, then update binding.ts's
NAMED_KEYS and useRecordHotkeys.ts's NAMED_CODES to import and use that shared
constant (or replace one with the other) so both tokenization paths use the same
canonical set.
apps/desktop/src/renderer/hotkeys/hooks/useHotkeyDisplay/useHotkeyDisplay.ts (1)

25-34: Consider extracting useFormatBinding to its own file.

The coding guidelines recommend one component per file. While useFormatBinding is closely related to useHotkeyDisplay, consider extracting it to useFormatBinding.ts for consistency.

As per coding guidelines: "Do not create multi-component files; use one component per file."

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

In `@apps/desktop/src/renderer/hotkeys/hooks/useHotkeyDisplay/useHotkeyDisplay.ts`
around lines 25 - 34, Extract the hook useFormatBinding into its own file named
useFormatBinding.ts: move the exported function useFormatBinding(binding:
ShortcutBinding | null): HotkeyDisplay into the new file and keep its imports
(useKeyboardLayoutStore, bindingToDispatchChord, formatHotkeyDisplay, PLATFORM,
useMemo) at the top of that file; update the original file to import
useFormatBinding from the new module and ensure the function signature and
behavior (calling useKeyboardLayoutStore, computing chord with
bindingToDispatchChord, and memoizing formatHotkeyDisplay) remain identical so
existing callers of useFormatBinding continue to work.
apps/desktop/src/renderer/hotkeys/utils/resolveHotkeyFromEvent.ts (1)

144-155: Consider combining subscriptions to avoid redundant rebuilds.

When both stores update in quick succession (e.g., during initialization), this triggers two separate rebuilds. Consider consolidating into a single subscription or debouncing if this becomes a performance concern.

🤖 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 144 - 155, Currently two separate subscriptions
(useHotkeyOverridesStore.subscribe and useKeyboardLayoutStore.subscribe) each
call buildRegisteredAppChords and assign registeredAppChords, causing redundant
rebuilds; consolidate by creating a single coordinated updater that reads both
stores and sets registeredAppChords (e.g., a function updateRegisteredAppChords
that calls buildRegisteredAppChords with
useHotkeyOverridesStore.getState().overrides and
useKeyboardLayoutStore.getState().map) and subscribe that function to both
stores, or add a short debounce around updateRegisteredAppChords to coalesce
rapid successive updates.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@apps/desktop/src/renderer/hotkeys/hooks/useRecordHotkeys/useRecordHotkeys.ts`:
- Around line 85-95: The recorded logical glyph can contain the chord separator
"+" (e.g., pressing Meta+Plus), so update useRecordHotkeys: in the printable
branch (where produced is derived) ensure produced.length === 1 &&
/\S/.test(produced) && produced !== "+" (or more generally: does not include the
chord separator) before building keyChord, so we don't create ambiguous keyChord
strings; and update resolveCapturedBinding to detect when a logical glyph
contains the separator ("+") and in that case fall back to using the
physical/code-based binding (i.e., treat the capture as non-printable and
resolve via the codeChord path). Ensure you reference/use useRecordHotkeys,
produced, keyChord, codeChord, classification and resolveCapturedBinding when
making these changes.

---

Nitpick comments:
In
`@apps/desktop/src/renderer/hotkeys/hooks/useHotkeyDisplay/useHotkeyDisplay.ts`:
- Around line 25-34: Extract the hook useFormatBinding into its own file named
useFormatBinding.ts: move the exported function useFormatBinding(binding:
ShortcutBinding | null): HotkeyDisplay into the new file and keep its imports
(useKeyboardLayoutStore, bindingToDispatchChord, formatHotkeyDisplay, PLATFORM,
useMemo) at the top of that file; update the original file to import
useFormatBinding from the new module and ensure the function signature and
behavior (calling useKeyboardLayoutStore, computing chord with
bindingToDispatchChord, and memoizing formatHotkeyDisplay) remain identical so
existing callers of useFormatBinding continue to work.

In `@apps/desktop/src/renderer/hotkeys/utils/binding.test.ts`:
- Around line 170-175: Rename the test to reflect that modifiers are
canonicalized rather than preserved; update the test name from "preserves
modifier order from the input chord" to something like "canonicalizes modifier
order (alphabetical) via canonicalizeChord" and ensure it still calls
translateLogicalChord with the same inputs (translateLogicalChord and
canonicalizeChord are the relevant symbols) so the assertion expecting
"alt+meta+shift+r" remains correct.

In `@apps/desktop/src/renderer/hotkeys/utils/binding.ts`:
- Around line 4-26: NAMED_KEYS in binding.ts largely overlaps with NAMED_CODES
in useRecordHotkeys.ts but contains aliases ("return", "esc", "up", "down")
causing duplication and potential divergence; consolidate by extracting a single
shared constant (e.g., NAMED_KEY_SET) that includes all normalized names and
aliases or clearly document/justify the alias differences, then update
binding.ts's NAMED_KEYS and useRecordHotkeys.ts's NAMED_CODES to import and use
that shared constant (or replace one with the other) so both tokenization paths
use the same canonical set.

In `@apps/desktop/src/renderer/hotkeys/utils/resolveHotkeyFromEvent.ts`:
- Around line 144-155: Currently two separate subscriptions
(useHotkeyOverridesStore.subscribe and useKeyboardLayoutStore.subscribe) each
call buildRegisteredAppChords and assign registeredAppChords, causing redundant
rebuilds; consolidate by creating a single coordinated updater that reads both
stores and sets registeredAppChords (e.g., a function updateRegisteredAppChords
that calls buildRegisteredAppChords with
useHotkeyOverridesStore.getState().overrides and
useKeyboardLayoutStore.getState().map) and subscribe that function to both
stores, or add a short debounce around updateRegisteredAppChords to coalesce
rapid successive updates.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5ebfedc7-85a3-45e7-9df4-e4b8dfecc875

📥 Commits

Reviewing files that changed from the base of the PR and between 5749f28 and bf10082.

📒 Files selected for processing (12)
  • apps/desktop/plans/20260428-keyboard-qa-plan.md
  • apps/desktop/src/renderer/hotkeys/hooks/index.ts
  • apps/desktop/src/renderer/hotkeys/hooks/useHotkey/useHotkey.ts
  • apps/desktop/src/renderer/hotkeys/hooks/useHotkeyDisplay/index.ts
  • apps/desktop/src/renderer/hotkeys/hooks/useHotkeyDisplay/useHotkeyDisplay.ts
  • apps/desktop/src/renderer/hotkeys/hooks/useRecordHotkeys/useRecordHotkeys.ts
  • apps/desktop/src/renderer/hotkeys/index.ts
  • apps/desktop/src/renderer/hotkeys/utils/binding.test.ts
  • apps/desktop/src/renderer/hotkeys/utils/binding.ts
  • apps/desktop/src/renderer/hotkeys/utils/index.ts
  • apps/desktop/src/renderer/hotkeys/utils/resolveHotkeyFromEvent.ts
  • apps/desktop/src/renderer/routes/_authenticated/settings/keyboard/page.tsx
✅ Files skipped from review due to trivial changes (3)
  • apps/desktop/src/renderer/hotkeys/hooks/useHotkeyDisplay/index.ts
  • apps/desktop/plans/20260428-keyboard-qa-plan.md
  • apps/desktop/src/renderer/hotkeys/hooks/index.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/desktop/src/renderer/hotkeys/utils/index.ts
  • apps/desktop/src/renderer/hotkeys/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/settings/keyboard/page.tsx
  • apps/desktop/src/renderer/hotkeys/hooks/useHotkey/useHotkey.ts

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 12 files (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/plans/20260428-keyboard-qa-plan.md">

<violation number="1" location="apps/desktop/plans/20260428-keyboard-qa-plan.md:57">
P2: The QA assertion for `⌘,` being layout-invariant is incorrect; on layouts like French AZERTY, comma is not on the same physical key, so the displayed glyph may change.</violation>
</file>

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

Comment thread apps/desktop/plans/20260428-keyboard-qa-plan.md 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: 1

Caution

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

⚠️ Outside diff range comments (2)
apps/desktop/src/renderer/hotkeys/hooks/useRecordHotkeys/useRecordHotkeys.ts (2)

201-205: ⚠️ Potential issue | 🟠 Major

Don't use modified Delete/Backspace as the unassign shortcut.

This early return swallows Meta+Backspace, Ctrl+Delete, etc. before captureHotkeyFromEvent, so those named bindings can never be recorded even though NAMED_KEYS and resolveCapturedBinding support them. Restrict unassign to the unmodified keys or move it behind capture parsing.

Possible fix
-			if (event.key === "Backspace" || event.key === "Delete") {
+			if (
+				(event.key === "Backspace" || event.key === "Delete") &&
+				!event.metaKey &&
+				!event.ctrlKey &&
+				!event.altKey &&
+				!event.shiftKey
+			) {
 				setOverride(recordingId, null);
 				optionsRef.current?.onUnassign?.(recordingId);
 				return;
 			}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/hotkeys/hooks/useRecordHotkeys/useRecordHotkeys.ts`
around lines 201 - 205, The current early-return treats any Backspace/Delete as
an unassign and therefore swallows modified combos like Meta+Backspace or
Ctrl+Delete before they reach captureHotkeyFromEvent; change the logic so
unassign only triggers for unmodified Backspace/Delete (i.e., require no
modifier keys) or alternatively perform captureHotkeyFromEvent first and then
treat an unmodified Backspace/Delete binding as an unassign; update the block
that calls setOverride(recordingId, null) and
optionsRef.current?.onUnassign?.(recordingId) to check modifier keys
(event.metaKey, event.ctrlKey, event.altKey, event.shiftKey) or move it after
captureHotkeyFromEvent/resolveCapturedBinding/NAMED_KEYS handling so modified
Delete/Backspace are allowed to be recorded.

46-60: ⚠️ Potential issue | 🟠 Major

Check for AltGraph before recording ctrl+alt modifier combinations.

The comment on lines 48–49 correctly identifies that AltGr masquerades as ctrl+alt on Linux/international layouts, but the recorder does not actually guard against this. Other parts of the hotkeys system (resolveHotkeyFromEvent.ts, useHotkey.ts) explicitly check event.getModifierState?.("AltGraph"), but captureHotkeyFromEvent does not. This causes AltGr key presses to be incorrectly recorded as ctrl+alt chords. Bail out or suppress the alt modifier when AltGraph is detected, consistent with the rest of the system.

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

In `@apps/desktop/src/renderer/hotkeys/hooks/useRecordHotkeys/useRecordHotkeys.ts`
around lines 46 - 60, The handler incorrectly records AltGr as ctrl+alt; update
the AltGraph handling in this block by using
event.getModifierState?.("AltGraph") to detect AltGraph and suppress it: change
the altIsAppModifier computation (currently using PLATFORM === "mac" &&
event.altKey) to take AltGraph into account (e.g., treat event.altKey as
not-app-modifier when getModifierState("AltGraph") is true), and when building
the modifiers Set (the code that adds "alt") skip adding "alt" if AltGraph is
detected (or return null early to bail out if that matches the behavior in
resolveHotkeyFromEvent/useHotkey). Ensure the checks reference isFKey,
altIsAppModifier, and the modifiers Set so the recorded chord matches the rest
of the hotkey system.
♻️ Duplicate comments (3)
apps/desktop/plans/20260427-keyboard-layout-plan.md (2)

4-4: ⚠️ Potential issue | 🟡 Minor

Branch name appears truncated.

keyboard-shortcut-analysi looks incomplete; verify the full branch name matches the PR branch for traceability.

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

In `@apps/desktop/plans/20260427-keyboard-layout-plan.md` at line 4, The branch
name in the plan file is truncated: replace the string
`keyboard-shortcut-analysi` with the full branch name used for the PR (e.g.,
`keyboard-shortcut-analysis` or the exact PR branch) so the Branch: line
accurately matches the source branch for traceability; update the Branch value
in the file where `keyboard-shortcut-analysi` appears to the correct full branch
name.

227-252: ⚠️ Potential issue | 🟡 Minor

Add language tags to fenced code blocks.

The architecture diagram and file path blocks should specify a language (e.g., text) to satisfy markdown linting (MD040).

🧹 Suggested fix
-```
+```text
 ┌──────────────────────────────────────────────┐
 ...
 └──────────────────────────────────────────────┘

Also applies to lines 266-268, 274-276, and 282-284.
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @apps/desktop/plans/20260427-keyboard-layout-plan.md around lines 227 - 252,
Add language tags (e.g., "text") to the fenced code blocks containing the ASCII
architecture diagram and the file-path blocks (the block starting with
"┌──────────────────────────────────────────────┐" and the smaller file
path/code blocks that reference apps/desktop/src/... and display.ts →
formatHotkeyDisplay) so markdown lint MD040 is satisfied; update each
triple-backtick fence to ```text for those blocks (also apply the same change to
the other similar fenced blocks mentioned in the comment).


</details>

</blockquote></details>
<details>
<summary>apps/desktop/src/renderer/hotkeys/hooks/useRecordHotkeys/useRecordHotkeys.ts (1)</summary><blockquote>

`30-36`: _⚠️ Potential issue_ | _🟠 Major_

**Don't keep the `"+"` fallback in logical mode.**

When `event.key` is unencodable, `captureHotkeyFromEvent` copies `codeChord` into `keyChord`, but `resolveCapturedBinding` still returns `mode: "logical"`. `Meta+Shift+=` therefore gets stored as logical `meta+shift+equal`, which will later translate as the `=` key on other layouts instead of the `+` glyph the user actually recorded. Represent “no logical chord” explicitly and downgrade those captures to physical.
 

<details>
<summary>Possible fix</summary>

```diff
 export interface CapturedHotkey {
 	/** Modifiers + canonical(event.code). Always meaningful. */
 	codeChord: string;
-	/** Modifiers + lowercased event.key for printable letters/digits/punctuation;
-	 *  identical to codeChord for named keys / F-keys. */
-	keyChord: string;
+	/** Null when the produced glyph cannot be encoded safely in chord syntax. */
+	keyChord: string | null;
 	classification: "named" | "fkey" | "printable";
 }
@@
-	let keyChord = codeChord;
+	let keyChord: string | null = codeChord;
 	if (classification === "printable") {
 		const produced = (event.key ?? "").toLowerCase();
+		keyChord = null;
 		if (produced.length === 1 && /\S/.test(produced) && produced !== "+") {
 			keyChord = [...ordered, produced].join("+");
 		}
 	}
@@
 	if (captured.classification === "fkey" || captured.classification === "named")
 		return { mode: "named", chord: captured.codeChord };
-	const mode: BindingMode = preferredMode;
-	const chord = mode === "logical" ? captured.keyChord : captured.codeChord;
-	return { mode, chord };
+	if (preferredMode === "logical" && captured.keyChord) {
+		return { mode: "logical", chord: captured.keyChord };
+	}
+	return { mode: "physical", chord: captured.codeChord };
 }
```
</details>


Also applies to: 69-79, 86-94

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/hotkeys/hooks/useRecordHotkeys/useRecordHotkeys.ts`
around lines 30 - 36, The code currently copies codeChord into keyChord when
event.key is unencodable, but resolveCapturedBinding still returns mode:
"logical", causing recorded glyphs like "+" to be misinterpreted as the physical
"=" on other layouts; update captureHotkeyFromEvent so that when event.key is
unencodable you do NOT set a logical fallback (represent absence of a logical
chord explicitly—e.g. set keyChord to null/undefined or a dedicated flag) and
ensure resolveCapturedBinding downgrades such captures to mode: "physical" (or
returns mode: "physical" when keyChord is empty); change handling around
CapturedHotkey.codeChord, CapturedHotkey.keyChord, captureHotkeyFromEvent and
resolveCapturedBinding so logical mode is only used when a true logical key is
encodable.
```

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🤖 Prompt for all review comments with AI agents</summary>

Verify each finding against the current code and only fix it if needed.

Inline comments:
In @apps/desktop/plans/20260427-keyboard-layout-plan.md:

  • Line 32: The documentation currently includes local dev paths (e.g.,
    ~/workplace/native-keymap/src/keyboard_mac.mm:182-189 and
    ~/workplace/vscode/src/vs/platform/keyboardLayout/electron-main/keyboardLayoutMainService.ts:48-60)
    which are not accessible to others; update the paragraph to replace those local
    filesystem references with public GitHub URLs pointing to the equivalent
    files/lines or, if exact line links are unstable, use generic repository + file
    references (repo name, path, and function/symbol names such as
    kTISNotifySelectedKeyboardInputSourceChanged, keyboard_mac.mm, and
    keyboardLayoutMainService.ts) so readers can find the implementations without
    local paths.

Outside diff comments:
In
@apps/desktop/src/renderer/hotkeys/hooks/useRecordHotkeys/useRecordHotkeys.ts:

  • Around line 201-205: The current early-return treats any Backspace/Delete as
    an unassign and therefore swallows modified combos like Meta+Backspace or
    Ctrl+Delete before they reach captureHotkeyFromEvent; change the logic so
    unassign only triggers for unmodified Backspace/Delete (i.e., require no
    modifier keys) or alternatively perform captureHotkeyFromEvent first and then
    treat an unmodified Backspace/Delete binding as an unassign; update the block
    that calls setOverride(recordingId, null) and
    optionsRef.current?.onUnassign?.(recordingId) to check modifier keys
    (event.metaKey, event.ctrlKey, event.altKey, event.shiftKey) or move it after
    captureHotkeyFromEvent/resolveCapturedBinding/NAMED_KEYS handling so modified
    Delete/Backspace are allowed to be recorded.
  • Around line 46-60: The handler incorrectly records AltGr as ctrl+alt; update
    the AltGraph handling in this block by using
    event.getModifierState?.("AltGraph") to detect AltGraph and suppress it: change
    the altIsAppModifier computation (currently using PLATFORM === "mac" &&
    event.altKey) to take AltGraph into account (e.g., treat event.altKey as
    not-app-modifier when getModifierState("AltGraph") is true), and when building
    the modifiers Set (the code that adds "alt") skip adding "alt" if AltGraph is
    detected (or return null early to bail out if that matches the behavior in
    resolveHotkeyFromEvent/useHotkey). Ensure the checks reference isFKey,
    altIsAppModifier, and the modifiers Set so the recorded chord matches the rest
    of the hotkey system.

Duplicate comments:
In @apps/desktop/plans/20260427-keyboard-layout-plan.md:

  • Line 4: The branch name in the plan file is truncated: replace the string
    keyboard-shortcut-analysi with the full branch name used for the PR (e.g.,
    keyboard-shortcut-analysis or the exact PR branch) so the Branch: line
    accurately matches the source branch for traceability; update the Branch value
    in the file where keyboard-shortcut-analysi appears to the correct full branch
    name.
  • Around line 227-252: Add language tags (e.g., "text") to the fenced code
    blocks containing the ASCII architecture diagram and the file-path blocks (the
    block starting with "┌──────────────────────────────────────────────┐" and the
    smaller file path/code blocks that reference apps/desktop/src/... and display.ts
    → formatHotkeyDisplay) so markdown lint MD040 is satisfied; update each
    triple-backtick fence to ```text for those blocks (also apply the same change to
    the other similar fenced blocks mentioned in the comment).

In
@apps/desktop/src/renderer/hotkeys/hooks/useRecordHotkeys/useRecordHotkeys.ts:

  • Around line 30-36: The code currently copies codeChord into keyChord when
    event.key is unencodable, but resolveCapturedBinding still returns mode:
    "logical", causing recorded glyphs like "+" to be misinterpreted as the physical
    "=" on other layouts; update captureHotkeyFromEvent so that when event.key is
    unencodable you do NOT set a logical fallback (represent absence of a logical
    chord explicitly—e.g. set keyChord to null/undefined or a dedicated flag) and
    ensure resolveCapturedBinding downgrades such captures to mode: "physical" (or
    returns mode: "physical" when keyChord is empty); change handling around
    CapturedHotkey.codeChord, CapturedHotkey.keyChord, captureHotkeyFromEvent and
    resolveCapturedBinding so logical mode is only used when a true logical key is
    encodable.

</details>

<details>
<summary>🪄 Autofix (Beta)</summary>

Fix all unresolved CodeRabbit comments on this PR:

- [ ] <!-- {"checkboxId": "4b0d0e0a-96d7-4f10-b296-3a18ea78f0b9"} --> Push a commit to this branch (recommended)
- [ ] <!-- {"checkboxId": "ff5b1114-7d8c-49e6-8ac1-43f82af23a33"} --> Create a new PR with the fixes

</details>

---

<details>
<summary>ℹ️ Review info</summary>

<details>
<summary>⚙️ Run configuration</summary>

**Configuration used**: defaults

**Review profile**: CHILL

**Plan**: Pro

**Run ID**: `dc987add-6d11-433b-b14b-84bbb898e8a9`

</details>

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 16f0da83e2001f3fc4f366d0f1b03f2dbea34803 and 1326becb2e91173a6f22a44d4ff78a2c4b4f39ee.

</details>

<details>
<summary>📒 Files selected for processing (13)</summary>

* `apps/desktop/plans/20260427-keyboard-layout-plan.md`
* `apps/desktop/plans/20260428-keyboard-qa-plan.md`
* `apps/desktop/src/renderer/hotkeys/display.test.ts`
* `apps/desktop/src/renderer/hotkeys/display.ts`
* `apps/desktop/src/renderer/hotkeys/hooks/index.ts`
* `apps/desktop/src/renderer/hotkeys/hooks/useBinding/index.ts`
* `apps/desktop/src/renderer/hotkeys/hooks/useBinding/useBinding.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/index.ts`
* `apps/desktop/src/renderer/hotkeys/stores/keyboardLayoutStore.ts`
* `apps/desktop/src/renderer/hotkeys/utils/binding.ts`
* `apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/SearchBarTrigger/SearchBarTrigger.tsx`

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (1)</summary>

* apps/desktop/src/renderer/hotkeys/hooks/useBinding/index.ts

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (4)</summary>

* apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/SearchBarTrigger/SearchBarTrigger.tsx
* apps/desktop/src/renderer/hotkeys/hooks/index.ts
* apps/desktop/plans/20260428-keyboard-qa-plan.md
* apps/desktop/src/renderer/hotkeys/stores/keyboardLayoutStore.ts

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Comment thread apps/desktop/plans/done/20260427-keyboard-layout-plan.md
native-keymap's Linux backend uses libxkbfile and Xlib via pkg-config;
node-gyp fails on Ubuntu runners without those headers. Add the apt
install before each `bun run install:deps` step on Linux:

- ci.yml Test job
- ci.yml Build job
- build-desktop.yml Build - Linux (x64)

macOS jobs are unaffected (native-keymap uses Cocoa frameworks there).
validate:native-runtime flagged native-keymap in the main bundle as an
unexpected external require — it wasn't on the runtime allowlist.
Adding it to externalizedRuntimeModules wires up everything in one
place: keeps the bundle externalization, materializes the module into
apps/desktop/node_modules during build, includes the .node binary in
the package, and unpacks it from asar so the native binding is
loadable at runtime. Same pattern as better-sqlite3 / node-pty.
… doc

Three shipped/active plans cover overlapping ground (April refactor
retrospective, the dual-mode plan, the QA plan). Replace them with a
single canonical reference at apps/desktop/docs/KEYBOARD_SYSTEM.md
covering: architecture, public API, binding model, layout-aware
translation, recording flow, cross-cutting guards (AltGr / IME / PTY-
reserved), brief decision history, and known gaps.

Move the three shipped plans to plans/done/ per AGENTS.md convention.
20260409-tui-hotkey-forwarding.md stays in plans/ — v1 terminal
migration is still remaining work.
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