Skip to content

refactor(desktop): rewrite hotkey system with react-hotkeys-hook#3178

Merged
saddlepaddle merged 17 commits intomainfrom
saddlepaddle/acidic-honeycrisp
Apr 5, 2026
Merged

refactor(desktop): rewrite hotkey system with react-hotkeys-hook#3178
saddlepaddle merged 17 commits intomainfrom
saddlepaddle/acidic-honeycrisp

Conversation

@saddlepaddle
Copy link
Copy Markdown
Collaborator

@saddlepaddle saddlepaddle commented Apr 5, 2026

Summary

Rewrites the desktop app's hotkey system from scratch, replacing 1,400 lines of custom key parsing/matching/normalization with react-hotkeys-hook as the listener layer.

  • New renderer/hotkeys/ module: registry with explicit per-platform keys, useHotkey hook wrapping react-hotkeys-hook, Zustand + localStorage for user overrides, HotkeyLabel component, display formatting
  • Deleted shared/hotkeys.ts (1,042 lines): custom matchesHotkeyEvent, parseHotkeyString, normalizeKey, deriveNonMacDefault, toElectronAccelerator, and 27 copy-paste numbered entries
  • Deleted stores/hotkeys/store.ts (357 lines): old Zustand store with tRPC persistence, multi-window sync, and useAppHotkey hook
  • Simplified Electron menu: 4 accelerators hardcoded instead of syncing from customizable bindings
  • Simplified xterm handler: terminal-reserved chords + CLEAR_TERMINAL only, everything else bubbles to react-hotkeys-hook
  • Migration script: reads old overrides from main process JSON file via tRPC, writes to new localStorage store on first launch

83 files changed, 1,421 insertions, 2,737 deletions (net -1,316 lines)

Test plan

  • Cold start: all hotkeys work (⌘T, ⌘N, ⌘L, ⌘P, ⌘1-9, ⌘,, ⌘/)
  • Terminal: ctrl+c/d/z pass through, ⌘K clears, ⌘T opens new tab
  • Hotkeys work from textareas (chat input, terminal)
  • Settings → Keyboard Shortcuts: displays correctly, recording works, conflict detection works, reset works
  • Migration: old customized hotkeys carry over on first launch
  • Electron menu: Reload (⌘R), Close Window (⌘⇧Q), Settings (⌘,), Shortcuts (⌘/) all work from native menu

Summary by cubic

Rewrote the desktop hotkey system to use react-hotkeys-hook, replacing the custom parser and store. Shortcuts now work in inputs and terminal panes, display correctly across platforms, and user overrides persist in localStorage with a one-time migration on startup.

  • Refactors

    • Added renderer/hotkeys/ with a per-platform registry, hooks (useHotkey, useHotkeyDisplay, useRecordHotkeys), HotkeyLabel, display formatting, and terminal utils.
    • Replaced all useAppHotkey/useHotkeyText with new hooks; replaced HotkeyTooltipContent with HotkeyLabel.
    • Removed legacy hotkey code, store, tRPC routers, and related tests; simplified the Electron menu with fixed accelerators; terminal keeps a small set of reserved chords and lets others bubble to React.
    • Enabled hotkeys on form tags; switched to code-based key names (slash, comma, bracketleft/right) with proper display mapping; patched tests for document/window.addEventListener.
    • Rewrote the Keyboard Settings page to use the new registry and recording hook with conflict detection.
  • Bug Fixes

    • Prevent crash when HotkeyLabel is rendered without an id.
    • Fixed stale callback in useHotkey to avoid missed handlers.
    • Corrected terminal reserved chord matching (including backslash).
    • In settings, Backspace/Delete now disables a binding (sets null); per-row Reset also cancels recording.
    • Migration now handles promise rejections and logs all paths.

Written for commit 93bccc7. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Client-side hotkey overrides persisted locally with a one‑time migration.
    • Hotkey recording UI with conflict detection, reserved-key warnings, and assign/unassign actions.
    • Compact hotkey display component for tooltips, menus, and inline labels.
  • Refactor

    • Hotkey surface moved to a hook-based registry and display utilities used across the app.
    • Remote hotkey sync/import/export removed; application menus use fixed default accelerators.

New hotkey module at renderer/hotkeys/ with:
- Registry: explicit per-platform key definitions (mac/windows/linux)
- Hooks: useHotkey (listener), useHotkeyDisplay (display), useRecordHotkeys (settings recording)
- Store: Zustand + localStorage persist for user overrides
- Display: formatHotkeyDisplay returning { keys, text } for Kbd components and tooltip text
- Components: HotkeyLabel for rendering label + shortcut badge
- Utils: isTerminalReservedEvent for xterm handler

Old system in shared/hotkeys.ts + stores/hotkeys/ not yet removed — migration pending.
16 files updated:
- useAppHotkey → useHotkey from renderer/hotkeys
- HotkeyTooltipContent → HotkeyLabel
- useHotkeyText → useHotkeyDisplay
- Removed deps arrays and undefined option placeholders
…mplify menu/xterm

- Migrate all display-only files to new hotkey system (useHotkeyDisplay, HotkeyLabel)
- Delete old stores/hotkeys/ and components/HotkeyTooltipContent/
- Remove useHotkeysSync from authenticated layout
- Hardcode Electron menu accelerators (no longer customizable)
- Simplify xterm handler to use getBinding + isTerminalReservedEvent
- Remove trpcHotkeysStorage adapter
- Add migration script for old hotkey overrides
- Replace manual recording logic with useRecordHotkeys hook
- Derive hotkeys-by-category from HOTKEYS registry (no store hook)
- Remove import/export functionality
- All renderer code now uses renderer/hotkeys exclusively
- Add OPEN_SETTINGS and SHOW_HOTKEYS back to registry (used by renderer)
- Remove stores/hotkeys re-export from stores/index.ts
- Migrate 3 files using useHotkeysStore for platform to PLATFORM const
- Remove leftover deps arrays from 2 chat search hooks
- Make HotkeyLabel id prop optional for conditional rendering
… routers)

- Delete shared/hotkeys.ts (1042 lines) and its test file
- Delete routers/hotkeys/ (import/export router)
- Delete hotkeys-events.ts (emitter for menu sync)
- Strip hotkeys set/subscribe from ui-state router (keep read-only get for migration)
- Inline legacy hotkeys type in app-state schemas
…hotkeys-hook

xterm uses a textarea for keyboard input, which causes react-hotkeys-hook
to ignore events (it skips form tags by default). Instead of enabling
form tags globally, dispatch a clean synthetic KeyboardEvent on document
when the xterm handler detects a ctrl/meta combo that should be an app
hotkey. The synthetic event's target is document (not textarea), so
react-hotkeys-hook processes it normally.
…bility

react-hotkeys-hook matches against event.code (not event.key), so special
characters need to use their code names:
- "/" → "slash"
- "," → "comma"
- "[" → "bracketleft"
- "]" → "bracketright"

Added corresponding display mappings so they render correctly in the UI.
…patch

- Add enableOnFormTags: true as default in useHotkey so hotkeys work
  when focused in textareas (chat input, etc.)
- Remove synthetic event dispatch from xterm handler — no longer needed
  since react-hotkeys-hook now processes form tag events directly
- Fix key format: use code-based names (slash, comma, bracketleft/right)
  for react-hotkeys-hook compatibility
Replace react-hotkeys-hook's useRecordHotkeys (caused infinite update loop
from reactive Set state) with a simple window keydown listener in useEffect.
Same approach as the old settings page, but extracted as a reusable hook.
@coderabbitai
Copy link
Copy Markdown
Contributor

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

Deleted legacy shared/main hotkeys and TRPC sync; added a renderer-only hotkeys subsystem (registry, types, display, hooks, override store, migration, UI components), replaced renderer usages to the new APIs, and added dependency react-hotkeys-hook.

Changes

Cohort / File(s) Summary
Removed main/shared hotkeys infra
apps/desktop/src/shared/hotkeys.ts, apps/desktop/src/shared/hotkeys.test.ts, apps/desktop/src/lib/trpc/routers/hotkeys/index.ts, apps/desktop/src/lib/trpc/routers/index.ts, apps/desktop/src/lib/trpc/routers/ui-state/index.ts, apps/desktop/src/main/lib/hotkeys-events.ts, apps/desktop/src/main/lib/menu.ts, apps/desktop/src/main/windows/main.ts, apps/desktop/src/main/lib/app-state/schemas.ts, apps/desktop/src/renderer/lib/trpc-storage.ts, apps/desktop/src/renderer/stores/hotkeys/*
Removed centralized hotkeys module, tests, main-process router/emitter, menu hotkey wiring, TRPC-backed renderer persistence/store; app-state hotkeys shape replaced with a legacy shape.
Renderer hotkeys core
apps/desktop/src/renderer/hotkeys/index.ts, apps/desktop/src/renderer/hotkeys/registry.ts, apps/desktop/src/renderer/hotkeys/types.ts, apps/desktop/src/renderer/hotkeys/display.ts, apps/desktop/src/renderer/hotkeys/utils/utils.ts
Added HOTKEYS registry, PLATFORM detection, types, display formatter, and terminal-reserved helper.
Override store & migration
apps/desktop/src/renderer/hotkeys/stores/hotkeyOverridesStore.ts, apps/desktop/src/renderer/hotkeys/migrate.ts
Added persisted localStorage overrides store and a one-time migration from legacy main-process hotkey state.
Hooks: binding/registration/display/recording
apps/desktop/src/renderer/hotkeys/hooks/...
Added useBinding, useHotkey, useHotkeyDisplay, useRecordHotkeys, and barrel exports; useHotkey integrates react-hotkeys-hook.
UI additions & replacements
apps/desktop/src/renderer/hotkeys/components/HotkeyLabel/..., apps/desktop/src/renderer/components/HotkeyTooltipContent/*, many apps/desktop/src/renderer/... files
Added HotkeyLabel and replaced numerous usages of deleted tooltip/display/store APIs with new hooks/components and adjusted prop shapes.
Terminal and keyboard handling
apps/desktop/src/renderer/screens/.../Terminal/helpers.ts, apps/desktop/src/renderer/screens/.../useTerminalHotkeys.ts
Reworked terminal key matching to use getBinding + new key matcher; retained terminal-reserved logic but removed older app-hotkey matching flow.
Settings & recording UI
apps/desktop/src/renderer/routes/_authenticated/settings/keyboard/page.tsx
Rewrote keyboard settings to use useRecordHotkeys and override-store APIs; removed TRPC import/export UI.
Bulk hook replacements
many apps/desktop/src/renderer/... files (chat, sidebar, topbar, workspace, tasks, panes, menus, etc.)
Replaced store-based APIs (useAppHotkey, useHotkeyText, useHotkeyDisplay from stores) with new useHotkey, useHotkeyDisplay, getBinding, and HotkeyLabel usages; adjusted call shapes and props.
Tests & setup changes
apps/desktop/test-setup.ts, several deleted tests
Extended test setup to mock event listeners for react-hotkeys-hook; removed multiple hotkey-related test suites.
Package
apps/desktop/package.json
Added dependency react-hotkeys-hook ^5.2.4.

Sequence Diagram(s)

sequenceDiagram
    participant Component
    participant useHotkey as useHotkey Hook
    participant useBinding as useBinding
    participant Store as Overrides Store
    participant Registry as HOTKEYS
    participant ReactHotkeys as react-hotkeys-hook
    participant Keyboard as KeyboardEvent

    Component->>useHotkey: call useHotkey("ID", callback, options)
    useHotkey->>useBinding: resolve binding for "ID"
    useBinding->>Store: read overrides
    alt override exists
        Store-->>useBinding: return override
    else
        Registry-->>useBinding: return default key
    end
    useBinding-->>useHotkey: resolved key string
    useHotkey->>ReactHotkeys: register handler for resolved key
    Keyboard-->>ReactHotkeys: key pressed
    ReactHotkeys-->>Component: invoke callback
    useHotkey-->>Component: returns HotkeyDisplay {keys, text}
Loading
sequenceDiagram
    participant User
    participant RecorderUI as Recording UI
    participant useRecord as useRecordHotkeys
    participant Store as Overrides Store
    participant Registry as HOTKEYS

    User->>RecorderUI: start recording for ID
    RecorderUI->>useRecord: attach keydown listener
    User->>useRecord: press combination
    useRecord->>useRecord: normalize + validate
    useRecord->>Registry: check conflicts
    alt conflict
        useRecord-->>RecorderUI: onConflict(conflictId)
    else reserved-warning
        useRecord-->>RecorderUI: onReserved(warning)
    else valid
        useRecord->>Store: setOverride(ID, keys)
        Store-->>localStorage: persist
        useRecord-->>RecorderUI: onSave()
    end
Loading

Estimated Code Review Effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Poem

🐰 I hopped through code both old and new,
Tucked shared hotkeys in a tidy drawer,
Hooks now hum where registries grew,
Local overrides keep shortcuts sure—
A rabbit’s nibble, neat and pure.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.52% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: rewriting the hotkey system to use react-hotkeys-hook instead of the custom implementation.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering the summary, changes made, deletions, simplifications, migration, test plan, and includes an auto-generated summary with detailed refactoring notes and bug fixes.

✏️ 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 saddlepaddle/acidic-honeycrisp

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.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 5, 2026

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ✅ Neon database branch
  • ✅ Electric Fly.io app

Thank you for your contribution! 🎉

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.

8 issues found across 83 files

Note: This PR contains a large number of files. cubic only reviews up to 75 files per PR, so some files may not have been reviewed.

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

<violation number="1" location="apps/desktop/src/renderer/hotkeys/hooks/useRecordHotkeys/useRecordHotkeys.ts:13">
P1: Normalize `event.key` before composing the hotkey string; raw browser key values can mismatch registry token names and break conflict/default/reserved comparisons.</violation>
</file>

<file name="apps/desktop/src/renderer/routes/_authenticated/layout.tsx">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/layout.tsx:71">
P2: Handle the async migration promise explicitly in the effect to avoid unhandled promise rejections.

(Based on your team's feedback about handling async calls/errors explicitly.) [FEEDBACK_USED]</violation>
</file>

<file name="apps/desktop/src/renderer/hotkeys/components/HotkeyLabel/HotkeyLabel.tsx">

<violation number="1" location="apps/desktop/src/renderer/hotkeys/components/HotkeyLabel/HotkeyLabel.tsx:6">
P1: Passing `""` as a fallback hotkey id can crash at runtime (`HOTKEYS[id]` is undefined). Use a valid fallback id instead of an invalid cast.</violation>
</file>

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

<violation number="1" location="apps/desktop/src/renderer/hotkeys/utils/utils.ts:15">
P2: Backslash matching is over-escaped, so `Ctrl+\\` never matches the terminal-reserved set.</violation>
</file>

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

<violation number="1" location="apps/desktop/src/renderer/hotkeys/hooks/useHotkey/useHotkey.ts:14">
P1: The hotkey callback is memoized with incomplete dependencies, which can invoke stale closures when component state changes.</violation>
</file>

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

<violation number="1" location="apps/desktop/src/renderer/hotkeys/registry.ts:38">
P2: Navigation hotkeys are assigned to a category that the keyboard settings UI does not render, so they won’t appear for users.</violation>
</file>

<file name="apps/desktop/src/renderer/routes/_authenticated/settings/keyboard/page.tsx">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/settings/keyboard/page.tsx:131">
P2: Backspace/Delete unassign now resets to default instead of disabling the shortcut.</violation>

<violation number="2" location="apps/desktop/src/renderer/routes/_authenticated/settings/keyboard/page.tsx:168">
P2: Conflict reassignment no longer warns when the chosen key is OS-reserved.</violation>
</file>

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

const MODIFIER_ORDER = ["meta", "ctrl", "alt", "shift"] as const;

function captureHotkeyFromEvent(event: KeyboardEvent): string | null {
const key = event.key.toLowerCase();
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 5, 2026

Choose a reason for hiding this comment

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

P1: Normalize event.key before composing the hotkey string; raw browser key values can mismatch registry token names and break conflict/default/reserved comparisons.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/hotkeys/hooks/useRecordHotkeys/useRecordHotkeys.ts, line 13:

<comment>Normalize `event.key` before composing the hotkey string; raw browser key values can mismatch registry token names and break conflict/default/reserved comparisons.</comment>

<file context>
@@ -0,0 +1,146 @@
+const MODIFIER_ORDER = ["meta", "ctrl", "alt", "shift"] as const;
+
+function captureHotkeyFromEvent(event: KeyboardEvent): string | null {
+	const key = event.key.toLowerCase();
+	if (["shift", "ctrl", "alt", "meta", "dead", "unidentified"].includes(key))
+		return null;
</file context>
Fix with Cubic

import type { HotkeyId } from "../../registry";

export function HotkeyLabel({ label, id }: { label: string; id?: HotkeyId }) {
const { keys } = useHotkeyDisplay(id ?? ("" as HotkeyId));
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 5, 2026

Choose a reason for hiding this comment

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

P1: Passing "" as a fallback hotkey id can crash at runtime (HOTKEYS[id] is undefined). Use a valid fallback id instead of an invalid cast.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/hotkeys/components/HotkeyLabel/HotkeyLabel.tsx, line 6:

<comment>Passing `""` as a fallback hotkey id can crash at runtime (`HOTKEYS[id]` is undefined). Use a valid fallback id instead of an invalid cast.</comment>

<file context>
@@ -0,0 +1,18 @@
+import type { HotkeyId } from "../../registry";
+
+export function HotkeyLabel({ label, id }: { label: string; id?: HotkeyId }) {
+	const { keys } = useHotkeyDisplay(id ?? ("" as HotkeyId));
+	if (!id || keys[0] === "Unassigned") return <span>{label}</span>;
+	return (
</file context>
Fix with Cubic

Comment thread apps/desktop/src/renderer/hotkeys/hooks/useHotkey/useHotkey.ts Outdated
Comment thread apps/desktop/src/renderer/routes/_authenticated/layout.tsx Outdated
Comment thread apps/desktop/src/renderer/hotkeys/utils/utils.ts Outdated
linux: "ctrl+shift+bracketleft",
},
label: "Navigate Back",
category: "Navigation",
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 5, 2026

Choose a reason for hiding this comment

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

P2: Navigation hotkeys are assigned to a category that the keyboard settings UI does not render, so they won’t appear for users.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/hotkeys/registry.ts, line 38:

<comment>Navigation hotkeys are assigned to a category that the keyboard settings UI does not render, so they won’t appear for users.</comment>

<file context>
@@ -0,0 +1,576 @@
+			linux: "ctrl+shift+bracketleft",
+		},
+		label: "Navigate Back",
+		category: "Navigation",
+		description: "Go back to the previous page in history",
+	},
</file context>
Fix with Cubic

toast.warning("This shortcut may be reserved by your OS.");
}
setOverride(pendingConflict.conflictId, null);
setOverride(pendingConflict.targetId, pendingConflict.keys);
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 5, 2026

Choose a reason for hiding this comment

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

P2: Conflict reassignment no longer warns when the chosen key is OS-reserved.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/routes/_authenticated/settings/keyboard/page.tsx, line 168:

<comment>Conflict reassignment no longer warns when the chosen key is OS-reserved.</comment>

<file context>
@@ -132,118 +156,16 @@ function KeyboardShortcutsPage() {
-			toast.warning("This shortcut may be reserved by your OS.");
-		}
+		setOverride(pendingConflict.conflictId, null);
+		setOverride(pendingConflict.targetId, pendingConflict.keys);
 		setPendingConflict(null);
 	};
</file context>
Fix with Cubic

useRecordHotkeys(recordingId, {
onSave: () => setRecordingId(null),
onCancel: () => setRecordingId(null),
onUnassign: () => setRecordingId(null),
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 5, 2026

Choose a reason for hiding this comment

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

P2: Backspace/Delete unassign now resets to default instead of disabling the shortcut.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/routes/_authenticated/settings/keyboard/page.tsx, line 131:

<comment>Backspace/Delete unassign now resets to default instead of disabling the shortcut.</comment>

<file context>
@@ -92,34 +85,65 @@ export const Route = createFileRoute("/_authenticated/settings/keyboard/")({
+	useRecordHotkeys(recordingId, {
+		onSave: () => setRecordingId(null),
+		onCancel: () => setRecordingId(null),
+		onUnassign: () => setRecordingId(null),
+		onConflict: (targetId, keys, conflictId) => {
+			setPendingConflict({ targetId, keys, conflictId });
</file context>
Suggested change
onUnassign: () => setRecordingId(null),
onUnassign: (id) => {
setOverride(id, null);
setRecordingId(null);
},
Fix with Cubic

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

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/routes/_authenticated/_dashboard/layout.tsx (1)

79-91: ⚠️ Potential issue | 🟠 Major

Same stale closure concern for CLOSE_WORKSPACE callback.

The callback references currentWorkspaceId and currentWorkspace, which are captured at registration time. When the hotkey fires later, these values may be stale.

Since enabled already guards activation via !!currentWorkspaceId, you could read the current workspace inside the callback or use a ref.

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

In `@apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx` around
lines 79 - 91, The CLOSE_WORKSPACE hotkey callback closes over
currentWorkspaceId and currentWorkspace at registration time causing stale
values; change the callback to read the latest workspace state when invoked (or
use a ref) instead of relying on the closed-over variables. Specifically, update
the useHotkey registration for "CLOSE_WORKSPACE" so the handler queries the
current workspace (e.g., from the same state selector or a
currentWorkspaceRef.current) and then calls setDeleteTarget({ workspaceId:
latestId, workspaceName: latest.name, workspaceType: latest.type }) only when
that fresh value exists; keep the enabled: !!currentWorkspaceId guard but do not
rely on those closed-over variables inside the callback.
🧹 Nitpick comments (7)
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/NavigationControls/NavigationControls.tsx (1)

15-16: Optional: guard hotkey actions with navigation availability.

To mirror button disabled behavior, consider no-op’ing when back/forward is unavailable.

♻️ Suggested refinement
- useHotkey("NAVIGATE_BACK", () => router.history.back());
- useHotkey("NAVIGATE_FORWARD", () => router.history.forward());
+ useHotkey("NAVIGATE_BACK", () => {
+   if (!canGoBack) return;
+   router.history.back();
+ });
+ useHotkey("NAVIGATE_FORWARD", () => {
+   if (!canGoForward) return;
+   router.history.forward();
+ });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/NavigationControls/NavigationControls.tsx`
around lines 15 - 16, The hotkey handlers call router.history.back() and
router.history.forward() unconditionally; update the useHotkey callbacks for
"NAVIGATE_BACK" and "NAVIGATE_FORWARD" to first check that navigation is
available and no-op otherwise (e.g., test a capability on router.history such as
a canGoBack/canGoForward method if present, or check history length/index before
calling router.history.back() / router.history.forward()). Modify the useHotkey
callbacks in NavigationControls.tsx to guard the calls to router.history.back
and router.history.forward so they mirror the buttons' disabled behavior.
apps/desktop/src/renderer/hotkeys/hooks/useHotkeyDisplay/useHotkeyDisplay.ts (1)

7-9: Consider using HotkeyId type instead of string for better type safety.

The function accepts string but immediately casts it to Parameters<typeof useBinding>[0] (which is likely HotkeyId). Accepting HotkeyId directly would provide compile-time validation at call sites and eliminate the need for the cast.

♻️ Proposed fix
+import type { HotkeyId } from "../../types";
+
-export function useHotkeyDisplay(id: string): HotkeyDisplay {
-	const binding = useBinding(id as Parameters<typeof useBinding>[0]);
+export function useHotkeyDisplay(id: HotkeyId): HotkeyDisplay {
+	const binding = useBinding(id);
 	return useMemo(() => formatHotkeyDisplay(binding, PLATFORM), [binding]);
 }
🤖 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 7 - 9, Change the useHotkeyDisplay parameter from a plain string to
the specific HotkeyId type so callers get compile-time validation and you can
remove the cast to Parameters<typeof useBinding>[0]; update the function
signature for useHotkeyDisplay(id: HotkeyId): HotkeyDisplay, adjust any
necessary imports (e.g., HotkeyId) and then call useBinding(id) directly (no
cast) and keep the useMemo(formatHotkeyDisplay(binding, PLATFORM), [binding])
logic as-is.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarShortcuts/useDashboardSidebarShortcuts.ts (1)

21-28: Hardcoded symbol may be inconsistent with user overrides and platform.

The workspaceShortcutLabels map uses a hardcoded ⌘${index + 1} format, but the new hotkey system supports user overrides and platform-specific display formatting via useHotkeyDisplay. If a user customizes these workspace shortcuts, this label won't reflect their changes.

Consider using useHotkeyDisplay for each workspace shortcut to maintain consistency with the rest of the hotkey system, or verify this is intentional given the new architecture.

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

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarShortcuts/useDashboardSidebarShortcuts.ts`
around lines 21 - 28, The workspaceShortcutLabels map currently hardcodes labels
as `⌘${index + 1}` which ignores user overrides and platform formatting; update
the logic in useDashboardSidebarShortcuts to compute each label using the hotkey
formatting utility instead of a literal string: for each workspace in
flattenedWorkspaces.slice(0, MAX_SHORTCUT_COUNT) call useHotkeyDisplay (or the
project's equivalent) with the same accelerator/keybinding used when registering
the workspace shortcut and use its returned string as the map value (keep keys
as workspace.id); ensure this replaces the hardcoded template so labels reflect
user and platform-specific formatting.
apps/desktop/src/renderer/components/HotkeyMenuShortcut/HotkeyMenuShortcut.tsx (1)

10-14: Consider using a dedicated flag instead of string comparison for "Unassigned" check.

Comparing against the magic string "Unassigned" is fragile—if the display text changes, this check would silently break. Consider having useHotkeyDisplay return an isAssigned boolean alongside text, or exporting the UNASSIGNED_TEXT constant from the hotkeys module for consistent comparison.

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

In
`@apps/desktop/src/renderer/components/HotkeyMenuShortcut/HotkeyMenuShortcut.tsx`
around lines 10 - 14, The component HotkeyMenuShortcut currently checks for the
magic string "Unassigned" from useHotkeyDisplay; change the implementation so
the hook returns a stable indicator (e.g., { text, isAssigned }) or export a
shared UNASSIGNED_TEXT constant and use that instead of a string literal; update
HotkeyMenuShortcut to read isAssigned (or compare against UNASSIGNED_TEXT) and
return null when not assigned, ensuring you modify useHotkeyDisplay (and the
hotkeys module if adding a constant) and update all callers to use the new
boolean/constant.
apps/desktop/src/renderer/hotkeys/components/HotkeyLabel/HotkeyLabel.tsx (1)

12-14: Potential duplicate React keys if keys array contains repeated values.

Using k (the key name string) as the React key could cause warnings if the same modifier appears twice in the display. Consider using the index or a composite key:

♻️ Suggested fix
 <KbdGroup>
-	{keys.map((k) => (
-		<Kbd key={k}>{k}</Kbd>
+	{keys.map((k, i) => (
+		<Kbd key={`${k}-${i}`}>{k}</Kbd>
 	))}
 </KbdGroup>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/hotkeys/components/HotkeyLabel/HotkeyLabel.tsx`
around lines 12 - 14, The HotkeyLabel component uses keys.map((k) => <Kbd
key={k}>{k}</Kbd>), which can produce duplicate React keys when the same key
string repeats; change the map to include the index or a composite key (e.g.,
use keys.map((k, i) => <Kbd key={`${k}-${i}`}>{k}</Kbd>)) so each Kbd has a
unique key and React warnings are avoided.
apps/desktop/src/renderer/hotkeys/display.ts (1)

1-5: Minor documentation inconsistency: "mod" alias not handled.

The file docstring mentions "mod+ctrl+enter" as an example input, but "mod" isn't included in MODIFIER_ORDER or MODIFIER_DISPLAY. If react-hotkeys-hook uses "mod" as a platform-agnostic alias for meta (mac) or ctrl (windows/linux), the formatter should handle it; otherwise, the docstring example should be updated.

💡 Option A: Update docstring to reflect actual supported format
 /**
  * Display formatting for hotkey bindings.
- * Converts key strings like "mod+ctrl+enter" into platform-specific symbols.
+ * Converts key strings like "meta+shift+n" into platform-specific symbols.
  */
💡 Option B: Add "mod" alias support if needed
-const MODIFIER_ORDER = ["meta", "ctrl", "alt", "shift"] as const;
+const MODIFIER_ORDER = ["meta", "ctrl", "alt", "shift", "mod"] as const;

 const MODIFIER_DISPLAY: Record<Platform, Record<string, string>> = {
-	mac: { meta: "⌘", ctrl: "⌃", alt: "⌥", shift: "⇧" },
-	windows: { meta: "Win", ctrl: "Ctrl", alt: "Alt", shift: "Shift" },
-	linux: { meta: "Super", ctrl: "Ctrl", alt: "Alt", shift: "Shift" },
+	mac: { meta: "⌘", ctrl: "⌃", alt: "⌥", shift: "⇧", mod: "⌘" },
+	windows: { meta: "Win", ctrl: "Ctrl", alt: "Alt", shift: "Shift", mod: "Ctrl" },
+	linux: { meta: "Super", ctrl: "Ctrl", alt: "Alt", shift: "Shift", mod: "Ctrl" },
 };

Also applies to: 31-31

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

In `@apps/desktop/src/renderer/hotkeys/display.ts` around lines 1 - 5, Docstring
mentions "mod" but the code doesn't handle that alias; add support for "mod" by
treating it as a platform-agnostic alias that resolves to "meta" on macOS and
"ctrl" on Windows/Linux: update MODIFIER_DISPLAY to include a "mod" entry (or
add a small normalization step) and ensure MODIFIER_ORDER includes "mod" where
appropriate, and update the hotkey parsing/formatting path (the function that
builds display strings using MODIFIER_ORDER/MODIFIER_DISPLAY) to normalize input
keys by expanding "mod" to the correct platform-specific modifier before
rendering; also adjust the file docstring example if you instead prefer to
document the actual supported tokens.
apps/desktop/src/renderer/routes/_authenticated/settings/keyboard/page.tsx (1)

27-33: Keep the category list exhaustive.

getHotkeysByCategory() builds a Navigation bucket, but both filtering and rendering only iterate CATEGORY_ORDER, which omits that category. Any HOTKEYS entry assigned to Navigation will silently disappear from this page. Either include the missing category here or derive the rendered order from the grouped data so these lists cannot drift.

Also applies to: 88-113, 148-159

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

In `@apps/desktop/src/renderer/routes/_authenticated/settings/keyboard/page.tsx`
around lines 27 - 33, CATEGORY_ORDER currently omits the "Navigation" category
so any HOTKEYS assigned to that category are dropped during rendering; update
the code to either add "Navigation" to CATEGORY_ORDER (so CATEGORY_ORDER:
HotkeyCategory[] includes "Navigation") or change the rendering/filtering to
derive the display order from the keys of getHotkeysByCategory(HOTKEYS) (i.e.,
iterate the grouped object’s keys instead of the static CATEGORY_ORDER) and
ensure getHotkeysByCategory, CATEGORY_ORDER, and the render loop remain
consistent.
🤖 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/src/renderer/hotkeys/hooks/useHotkey/useHotkey.ts`:
- Around line 14-16: The hotkey registration currently only depends on keys
which causes stale closures; update the useHotkeys call in useHotkey (the line
invoking useHotkeys(keys ?? "", callback, { enableOnFormTags: true, ...options
}, [keys])) to include callback in the dependency array (e.g., [keys, callback])
so the binding is re-registered when the handler changes, and note that callers
should memoize callback (useCallback) to avoid excessive re-registrations.

In `@apps/desktop/src/renderer/hotkeys/utils/utils.ts`:
- Line 8: The TERMINAL_RESERVED set entry "ctrl+\\" doesn't match the value
produced when handling event.key because the ternary produces "ctrl+\\\\" for a
single backslash key; update the logic so the produced key string and the set
entries use the same representation: either change the set entry to match the
doubled-backslash form or (preferred) remove the special-case ternary and
normalize backslash handling so the set contains "ctrl+\\" (single escaped
backslash literal) and the code that builds the key string uses that same form;
ensure you update the entries referenced (e.g., "ctrl+\\" in TERMINAL_RESERVED)
and the event.key branch that currently emits "\\\\" to stay consistent.

In `@apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx`:
- Around line 62-68: The hotkey callback currently closes over a stale
isWorkspaceSidebarOpen value because useHotkey only depends on keys; update the
callback to read the latest state at invocation (instead of closing over the
variable) by either calling the store getter/read function or using a ref that
you update on state changes. Concretely: in the hotkey handler used with
useHotkey, replace direct use of isWorkspaceSidebarOpen with a live read (e.g.,
call the workspace store getter or selector to get current
isWorkspaceSidebarOpen) or maintain a latestIsWorkspaceSidebarOpen ref updated
in an effect and read that ref inside the callback; continue to call
setWorkspaceSidebarOpen(true) or toggleWorkspaceSidebarCollapsed() as before.

In `@apps/desktop/src/renderer/routes/_authenticated/layout.tsx`:
- Around line 69-72: The effect calls migrateHotkeyOverrides() without handling
rejections, so failures are silent; update the useEffect in layout.tsx to call
migrateHotkeyOverrides() inside an async wrapper (or .then/.catch) and
explicitly handle errors: await the promise returned by
migrateHotkeyOverrides(), catch any thrown error, and log or surface it (e.g.,
processLogger.error / console.error and optionally set a UI-visible flag) so
migration failures are not silent. Ensure you reference the existing useEffect
and migrateHotkeyOverrides function names and do not change their signatures,
just add proper async handling and error handling inside the effect.

In `@apps/desktop/src/renderer/routes/_authenticated/settings/keyboard/page.tsx`:
- Around line 189-195: The per-row "Reset" handler does not clear the global
recording state, so if a row is reset while showing "Recording…" the key
listener remains armed; update the per-row reset handlers (the Button onClick
that calls resetAll in the bulk path and the single-row reset handler used
around lines 236-243) to also call setRecordingId(null) when performing a reset
(i.e., ensure setRecordingId(null) is invoked alongside the reset logic that
clears the binding), so the recordingId is cleared for both the global "Reset
all" path and each per-row reset.

---

Outside diff comments:
In `@apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx`:
- Around line 79-91: The CLOSE_WORKSPACE hotkey callback closes over
currentWorkspaceId and currentWorkspace at registration time causing stale
values; change the callback to read the latest workspace state when invoked (or
use a ref) instead of relying on the closed-over variables. Specifically, update
the useHotkey registration for "CLOSE_WORKSPACE" so the handler queries the
current workspace (e.g., from the same state selector or a
currentWorkspaceRef.current) and then calls setDeleteTarget({ workspaceId:
latestId, workspaceName: latest.name, workspaceType: latest.type }) only when
that fresh value exists; keep the enabled: !!currentWorkspaceId guard but do not
rely on those closed-over variables inside the callback.

---

Nitpick comments:
In
`@apps/desktop/src/renderer/components/HotkeyMenuShortcut/HotkeyMenuShortcut.tsx`:
- Around line 10-14: The component HotkeyMenuShortcut currently checks for the
magic string "Unassigned" from useHotkeyDisplay; change the implementation so
the hook returns a stable indicator (e.g., { text, isAssigned }) or export a
shared UNASSIGNED_TEXT constant and use that instead of a string literal; update
HotkeyMenuShortcut to read isAssigned (or compare against UNASSIGNED_TEXT) and
return null when not assigned, ensuring you modify useHotkeyDisplay (and the
hotkeys module if adding a constant) and update all callers to use the new
boolean/constant.

In `@apps/desktop/src/renderer/hotkeys/components/HotkeyLabel/HotkeyLabel.tsx`:
- Around line 12-14: The HotkeyLabel component uses keys.map((k) => <Kbd
key={k}>{k}</Kbd>), which can produce duplicate React keys when the same key
string repeats; change the map to include the index or a composite key (e.g.,
use keys.map((k, i) => <Kbd key={`${k}-${i}`}>{k}</Kbd>)) so each Kbd has a
unique key and React warnings are avoided.

In `@apps/desktop/src/renderer/hotkeys/display.ts`:
- Around line 1-5: Docstring mentions "mod" but the code doesn't handle that
alias; add support for "mod" by treating it as a platform-agnostic alias that
resolves to "meta" on macOS and "ctrl" on Windows/Linux: update MODIFIER_DISPLAY
to include a "mod" entry (or add a small normalization step) and ensure
MODIFIER_ORDER includes "mod" where appropriate, and update the hotkey
parsing/formatting path (the function that builds display strings using
MODIFIER_ORDER/MODIFIER_DISPLAY) to normalize input keys by expanding "mod" to
the correct platform-specific modifier before rendering; also adjust the file
docstring example if you instead prefer to document the actual supported tokens.

In
`@apps/desktop/src/renderer/hotkeys/hooks/useHotkeyDisplay/useHotkeyDisplay.ts`:
- Around line 7-9: Change the useHotkeyDisplay parameter from a plain string to
the specific HotkeyId type so callers get compile-time validation and you can
remove the cast to Parameters<typeof useBinding>[0]; update the function
signature for useHotkeyDisplay(id: HotkeyId): HotkeyDisplay, adjust any
necessary imports (e.g., HotkeyId) and then call useBinding(id) directly (no
cast) and keep the useMemo(formatHotkeyDisplay(binding, PLATFORM), [binding])
logic as-is.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarShortcuts/useDashboardSidebarShortcuts.ts`:
- Around line 21-28: The workspaceShortcutLabels map currently hardcodes labels
as `⌘${index + 1}` which ignores user overrides and platform formatting; update
the logic in useDashboardSidebarShortcuts to compute each label using the hotkey
formatting utility instead of a literal string: for each workspace in
flattenedWorkspaces.slice(0, MAX_SHORTCUT_COUNT) call useHotkeyDisplay (or the
project's equivalent) with the same accelerator/keybinding used when registering
the workspace shortcut and use its returned string as the map value (keep keys
as workspace.id); ensure this replaces the hardcoded template so labels reflect
user and platform-specific formatting.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/NavigationControls/NavigationControls.tsx`:
- Around line 15-16: The hotkey handlers call router.history.back() and
router.history.forward() unconditionally; update the useHotkey callbacks for
"NAVIGATE_BACK" and "NAVIGATE_FORWARD" to first check that navigation is
available and no-op otherwise (e.g., test a capability on router.history such as
a canGoBack/canGoForward method if present, or check history length/index before
calling router.history.back() / router.history.forward()). Modify the useHotkey
callbacks in NavigationControls.tsx to guard the calls to router.history.back
and router.history.forward so they mirror the buttons' disabled behavior.

In `@apps/desktop/src/renderer/routes/_authenticated/settings/keyboard/page.tsx`:
- Around line 27-33: CATEGORY_ORDER currently omits the "Navigation" category so
any HOTKEYS assigned to that category are dropped during rendering; update the
code to either add "Navigation" to CATEGORY_ORDER (so CATEGORY_ORDER:
HotkeyCategory[] includes "Navigation") or change the rendering/filtering to
derive the display order from the keys of getHotkeysByCategory(HOTKEYS) (i.e.,
iterate the grouped object’s keys instead of the static CATEGORY_ORDER) and
ensure getHotkeysByCategory, CATEGORY_ORDER, and the render loop remain
consistent.
🪄 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: dbc50899-b4e3-49c2-9f8b-6c53767d0fc9

📥 Commits

Reviewing files that changed from the base of the PR and between 864977d and b43d428.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (82)
  • apps/desktop/package.json
  • apps/desktop/src/lib/trpc/routers/hotkeys/index.ts
  • apps/desktop/src/lib/trpc/routers/index.ts
  • apps/desktop/src/lib/trpc/routers/ui-state/index.ts
  • apps/desktop/src/main/lib/app-state/schemas.ts
  • apps/desktop/src/main/lib/hotkeys-events.ts
  • apps/desktop/src/main/lib/menu.ts
  • apps/desktop/src/main/windows/main.ts
  • apps/desktop/src/renderer/components/Chat/ChatInterface/components/ChatInputFooter/ChatInputFooter.tsx
  • apps/desktop/src/renderer/components/Chat/ChatInterface/components/ChatInputFooter/components/ChatShortcuts/ChatShortcuts.tsx
  • apps/desktop/src/renderer/components/HotkeyMenuShortcut/HotkeyMenuShortcut.tsx
  • apps/desktop/src/renderer/components/HotkeyTooltipContent/HotkeyTooltipContent.tsx
  • apps/desktop/src/renderer/components/HotkeyTooltipContent/index.ts
  • apps/desktop/src/renderer/components/NewWorkspaceModal/components/PromptGroup/PromptGroup.tsx
  • apps/desktop/src/renderer/components/OpenInButton/OpenInButton.tsx
  • apps/desktop/src/renderer/hooks/useWorkspaceShortcuts.ts
  • apps/desktop/src/renderer/hotkeys/components/HotkeyLabel/HotkeyLabel.tsx
  • apps/desktop/src/renderer/hotkeys/components/HotkeyLabel/index.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/useHotkey/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/index.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/registry.ts
  • apps/desktop/src/renderer/hotkeys/stores/hotkeyOverridesStore.ts
  • apps/desktop/src/renderer/hotkeys/stores/index.ts
  • apps/desktop/src/renderer/hotkeys/types.ts
  • apps/desktop/src/renderer/hotkeys/utils/index.ts
  • apps/desktop/src/renderer/hotkeys/utils/utils.ts
  • apps/desktop/src/renderer/lib/trpc-storage.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarHeader/DashboardSidebarHeader.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/DashboardSidebarExpandedWorkspaceRow.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceHoverCardContent/DashboardSidebarWorkspaceHoverCardContent.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarShortcuts/useDashboardSidebarShortcuts.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/NavigationControls/NavigationControls.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/OpenInMenuButton/OpenInMenuButton.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/OrganizationDropdown/OrganizationDropdown.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/SearchBarTrigger/SearchBarTrigger.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/SidebarToggle/SidebarToggle.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/WorkspaceRunButton/WorkspaceRunButton.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/TasksTopBar.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/CreateTaskDialog/CreateTaskDialog.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceEmptyState/WorkspaceEmptyState.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/WorkspaceChatInterface/components/ChatInputFooter/ChatInputFooter.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/WorkspaceChatInterface/components/ChatInputFooter/components/ChatShortcuts/ChatShortcuts.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/WorkspaceChatInterface/components/ChatMessageList/hooks/useChatMessageSearch/useChatMessageSearch.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/workspace/$workspaceId/hooks/usePresetHotkeys.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/workspace/$workspaceId/page.tsx
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/PromptGroup/PromptGroup.tsx
  • apps/desktop/src/renderer/routes/_authenticated/layout.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/keyboard/page.tsx
  • apps/desktop/src/renderer/screens/main/components/SettingsButton/SettingsButton.tsx
  • apps/desktop/src/renderer/screens/main/components/SidebarControl/SidebarControl.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/WorkspaceListItem.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/components/WorkspaceHoverCard/WorkspaceHoverCard.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebarHeader/NewWorkspaceButton.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/EmptyTabView.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabContentContextMenu.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatPaneInterface/components/ChatMessageList/hooks/useChatMessageSearch/useChatMessageSearch.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/hooks/useDiffSearch/useDiffSearch.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/hooks/useMarkdownSearch/useMarkdownSearch.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/components/PaneToolbarActions/PaneToolbarActions.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/ScrollToBottomButton/ScrollToBottomButton.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/helpers.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/hooks/useTerminalHotkeys.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/PaneContextMenuItems/PaneContextMenuItems.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/PresetsBar/components/PresetBarItem/PresetBarItem.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/index.tsx
  • apps/desktop/src/renderer/stores/hotkeys/index.ts
  • apps/desktop/src/renderer/stores/hotkeys/store.ts
  • apps/desktop/src/renderer/stores/index.ts
  • apps/desktop/src/shared/hotkeys.test.ts
  • apps/desktop/src/shared/hotkeys.ts
💤 Files with no reviewable changes (11)
  • apps/desktop/src/lib/trpc/routers/index.ts
  • apps/desktop/src/renderer/components/HotkeyTooltipContent/index.ts
  • apps/desktop/src/main/lib/hotkeys-events.ts
  • apps/desktop/src/renderer/stores/hotkeys/index.ts
  • apps/desktop/src/renderer/stores/index.ts
  • apps/desktop/src/renderer/lib/trpc-storage.ts
  • apps/desktop/src/lib/trpc/routers/hotkeys/index.ts
  • apps/desktop/src/shared/hotkeys.test.ts
  • apps/desktop/src/renderer/components/HotkeyTooltipContent/HotkeyTooltipContent.tsx
  • apps/desktop/src/renderer/stores/hotkeys/store.ts
  • apps/desktop/src/shared/hotkeys.ts

Comment thread apps/desktop/src/renderer/hotkeys/hooks/useHotkey/useHotkey.ts Outdated
Comment thread apps/desktop/src/renderer/hotkeys/utils/utils.ts
Comment on lines +62 to +68
useHotkey("TOGGLE_WORKSPACE_SIDEBAR", () => {
if (!isWorkspaceSidebarOpen) {
setWorkspaceSidebarOpen(true);
} else {
toggleWorkspaceSidebarCollapsed();
}
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Callback may capture stale isWorkspaceSidebarOpen state.

The useHotkey hook (per useHotkey.ts lines 14-16) only includes keys in its dependency array, not the callback. This means isWorkspaceSidebarOpen is captured at registration time and won't reflect updates.

Consider using a ref pattern or ensuring the callback reads from the store directly:

🛠️ Suggested fix using store getter
 useHotkey("TOGGLE_WORKSPACE_SIDEBAR", () => {
-	if (!isWorkspaceSidebarOpen) {
+	const isOpen = useWorkspaceSidebarStore.getState().isOpen;
+	if (!isOpen) {
 		setWorkspaceSidebarOpen(true);
 	} else {
 		toggleWorkspaceSidebarCollapsed();
 	}
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx` around
lines 62 - 68, The hotkey callback currently closes over a stale
isWorkspaceSidebarOpen value because useHotkey only depends on keys; update the
callback to read the latest state at invocation (instead of closing over the
variable) by either calling the store getter/read function or using a ref that
you update on state changes. Concretely: in the hotkey handler used with
useHotkey, replace direct use of isWorkspaceSidebarOpen with a live read (e.g.,
call the workspace store getter or selector to get current
isWorkspaceSidebarOpen) or maintain a latestIsWorkspaceSidebarOpen ref updated
in an effect and read that ref inside the callback; continue to call
setWorkspaceSidebarOpen(true) or toggleWorkspaceSidebarCollapsed() as before.

Comment thread apps/desktop/src/renderer/routes/_authenticated/layout.tsx
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 5, 2026

Greptile Summary

This PR replaces ~1,400 lines of custom key-parsing, matching, and normalization with a registry-based hotkey system backed by react-hotkeys-hook. The overall architecture is a clear improvement: explicit per-platform keymaps in registry.ts, a thin useHotkey wrapper with enableOnFormTags on by default, Zustand+localStorage for user overrides, and a one-time migration from the old main-process JSON store via tRPC.

  • New renderer/hotkeys/ module: registry.ts with per-platform keys, useHotkey/useHotkeyDisplay/useRecordHotkeys hooks, Zustand persist store, HotkeyLabel component, display formatting
  • Deleted ~1,400 lines: shared/hotkeys.ts (custom matchesHotkeyEvent, parseHotkeyString, normalizeKey) and stores/hotkeys/store.ts (tRPC persistence + multi-window sync)
  • Simplified Electron menu: 4 hardcoded accelerators, correctly independent of user-customizable bindings
  • One-time migration: reads old overrides from the main-process JSON file via tRPC, writes to the new localStorage store on first launch with proper idempotency and error handling
  • P1 — Conflict detector breaks shared-binding groups: getHotkeyConflict in useRecordHotkeys.ts does a flat scan over all registered hotkeys. Five groups of context-sensitive hotkeys intentionally share the same default binding (e.g. FIND_IN_TERMINAL, FIND_IN_FILE_VIEWER, FIND_IN_CHAT, and FOCUS_TASK_SEARCH all use meta+f). The detector flags these as conflicts, making it impossible for users to record a new binding for any hotkey in those groups without unassigning a sibling
  • P1 — HotkeyLabel crashes when id is undefined: the hook fires before the early-return guard, passing "" to useBinding, which throws a TypeError accessing HOTKEYS[""].key

Confidence Score: 3/5

Not safe to merge — two P1 bugs cause immediate user-facing failures in the keyboard shortcuts settings page and wherever HotkeyLabel is rendered without an id.

The architecture is solid and code quality is high throughout, but the conflict detector in useRecordHotkeys.ts is context-unaware and will break the shortcuts UI for five groups of shared-binding hotkeys, and HotkeyLabel crashes with a TypeError when rendered without an id prop. Both fixes are small but are required before merging.

useRecordHotkeys.ts (conflict detection logic), HotkeyLabel.tsx and useBinding.ts (crash path when id is undefined)

Important Files Changed

Filename Overview
apps/desktop/src/renderer/hotkeys/hooks/useRecordHotkeys/useRecordHotkeys.ts Context-unaware getHotkeyConflict breaks the keyboard shortcuts UI for five shared-binding groups; TERMINAL_RESERVED constant is duplicated from utils/utils.ts
apps/desktop/src/renderer/hotkeys/components/HotkeyLabel/HotkeyLabel.tsx Calls useHotkeyDisplay with an empty string when id is undefined, causing a guaranteed TypeError in useBinding before the early-return guard can run
apps/desktop/src/renderer/hotkeys/hooks/useBinding/useBinding.ts Missing null-guard on HOTKEYS[id] — will throw TypeError for any invalid or empty id, including the empty string passed from HotkeyLabel when id is absent
apps/desktop/src/renderer/hotkeys/registry.ts Clean per-platform registry; intentionally shared bindings across context-sensitive hotkeys are the root cause of the conflict-detection false positives in useRecordHotkeys
apps/desktop/src/renderer/routes/_authenticated/settings/keyboard/page.tsx Conflict UI implementation is solid; will surface false conflicts for all shared-binding groups until the context-group fix in useRecordHotkeys is applied
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/helpers.ts matchesKey correctly handles CLEAR_TERMINAL today but will silently fail if used with any registry binding containing named key tokens like bracketleft or comma
apps/desktop/src/renderer/hotkeys/migrate.ts Clean one-time migration from the old main-process JSON store to localStorage; idempotent, graceful error handling, correct platform key mapping
apps/desktop/src/renderer/hotkeys/stores/hotkeyOverridesStore.ts Straightforward Zustand persist store with correct partialize, createJSONStorage, and immutable update patterns
apps/desktop/src/main/lib/menu.ts Correctly simplified to 4 hardcoded accelerators; independent of the hotkey registry since Electron menu items must work before the renderer is ready
apps/desktop/src/renderer/hotkeys/display.ts Well-structured display formatting with platform-specific symbol maps and complete KEY_DISPLAY coverage for all named tokens used in the registry

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[User clicks hotkey row] --> B[setRecordingId]
    B --> C[User presses keys]
    C --> D[captureHotkeyFromEvent]
    D --> E{Bare modifier or\nunrecognized key?}
    E -- yes --> C
    E -- no --> F[checkReserved]
    F --> G{Terminal reserved?}
    G -- error --> H[onReserved - abort]
    G -- no --> I{OS reserved?}
    I -- warning --> J[onReserved toast - continue]
    I -- no --> J
    J --> K[getHotkeyConflict\nFLAT SCAN over all hotkeys]
    K --> L{Conflict found?}
    L -- yes --> M[onConflict - show dialog]
    M --> N{User choice}
    N -- Reassign --> O[setOverride conflictId null\nsetOverride targetId keys]
    N -- Cancel --> P[No change]
    L -- no --> Q{captured === defaultKey?}
    Q -- yes --> R[resetOverride]
    Q -- no --> S[setOverride]
    R --> T[onSave - setRecordingId null]
    S --> T
    T --> U[useBinding re-reads store]
    U --> V[useHotkeys re-registers]
Loading

Reviews (1): Last reviewed commit: "fix: add addEventListener mock to test s..." | Re-trigger Greptile

- Fix HotkeyLabel crash when id is undefined (add ?. guard in useBinding)
- Fix backslash over-escaping in isTerminalReservedEvent
- Fix stale closure in useHotkey (add callbackRef pattern)
- Fix Backspace/Delete to disable hotkey (set null) instead of resetting to default
- Handle migration promise rejection explicitly
- Cancel recording when per-row Reset is clicked
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (5)
apps/desktop/src/renderer/hotkeys/hooks/useBinding/useBinding.ts (2)

1-2: Prefer tsconfig alias imports instead of deep relative paths.

These imports should use the configured alias form to match project conventions and reduce path fragility.

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/useBinding/useBinding.ts` around
lines 1 - 2, Imports in useBinding.ts use deep relative paths; update the import
statements that bring in HOTKEYS, HotkeyId and useHotkeyOverridesStore to use
the project's tsconfig path aliases instead of "../../registry" and
"../../stores/hotkeyOverridesStore". Locate the import lines that reference
HOTKEYS and HotkeyId and the one that references useHotkeyOverridesStore and
replace their module specifiers with the corresponding tsconfig alias modules
(the alias used for the renderer hotkeys registry and stores) so they conform to
the project's alias convention.

5-19: Deduplicate binding resolution between hook and imperative helper.

useBinding and getBinding currently duplicate the same resolution branch logic. Extracting a shared resolver will prevent drift.

♻️ Suggested refactor
+function resolveBinding(
+	id: HotkeyId,
+	overrides: Record<string, string | null>,
+): string | null {
+	if (!id) return null;
+	if (id in overrides) return overrides[id] ?? null;
+	return HOTKEYS[id]?.key ?? null;
+}
+
 /** Reactive: get the effective key binding for a hotkey (override ?? default) */
 export function useBinding(id: HotkeyId): string | null {
-	return useHotkeyOverridesStore((state) => {
-		if (!id) return null;
-		if (id in state.overrides) return state.overrides[id] ?? null;
-		return HOTKEYS[id]?.key ?? null;
-	});
+	return useHotkeyOverridesStore((state) => resolveBinding(id, state.overrides));
 }
 
 /** Imperative: get the effective key binding (for non-React contexts like xterm) */
 export function getBinding(id: HotkeyId): string | null {
-	const state = useHotkeyOverridesStore.getState();
-	if (!id) return null;
-	if (id in state.overrides) return state.overrides[id] ?? null;
-	return HOTKEYS[id]?.key ?? null;
+	const { overrides } = useHotkeyOverridesStore.getState();
+	return resolveBinding(id, overrides);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/hotkeys/hooks/useBinding/useBinding.ts` around
lines 5 - 19, Extract the duplicate resolution logic used in useBinding and
getBinding into a single shared resolver (e.g., resolveBinding or
getEffectiveBinding) that takes a HotkeyId and an overrides map (or the store
state) and returns the resolved key or null; update useBinding to call
useHotkeyOverridesStore with a selector that delegates to this shared resolver,
and update getBinding to read the store via useHotkeyOverridesStore.getState()
and delegate to the same resolver so both functions use identical logic and
avoid drift.
apps/desktop/src/renderer/routes/_authenticated/settings/keyboard/page.tsx (2)

27-33: Keep category ordering in one source of truth.

getHotkeysByCategory() builds a Navigation bucket, but the page only filters and renders categories from CATEGORY_ORDER. Anything categorized as Navigation will silently disappear from this screen.

♻️ Simple fix
 const CATEGORY_ORDER: HotkeyCategory[] = [
+	"Navigation",
 	"Workspace",
 	"Terminal",
 	"Layout",
 	"Window",
 	"Help",
 ];

Also applies to: 88-113, 148-159, 216-218

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

In `@apps/desktop/src/renderer/routes/_authenticated/settings/keyboard/page.tsx`
around lines 27 - 33, The page filters and renders categories using the
hardcoded CATEGORY_ORDER which omits the "Navigation" bucket produced by
getHotkeysByCategory(), causing those hotkeys to be hidden; update the rendering
to use a single source of truth by either adding "Navigation" to CATEGORY_ORDER
or (preferably) deriving the category list from the keys returned by
getHotkeysByCategory() and then applying your existing ordering as a stable
fallback, ensuring references to CATEGORY_ORDER, getHotkeysByCategory, and the
"Navigation" category are synchronized so no category is dropped.

165-169: Reuse the same default-vs-override normalization when resolving conflicts.

The direct recording path removes the override when the chosen keys equal HOTKEYS[id].key, but this branch always persists a string. That leaves a redundant override behind when the user reassigns a shortcut back to its default.

♻️ Keep the conflict path consistent with the recorder
 const handleConflictReassign = () => {
 	if (!pendingConflict) return;
 	setOverride(pendingConflict.conflictId, null);
-	setOverride(pendingConflict.targetId, pendingConflict.keys);
+	if (pendingConflict.keys === HOTKEYS[pendingConflict.targetId].key) {
+		resetOverride(pendingConflict.targetId);
+	} else {
+		setOverride(pendingConflict.targetId, pendingConflict.keys);
+	}
 	setPendingConflict(null);
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/routes/_authenticated/settings/keyboard/page.tsx`
around lines 165 - 169, The conflict-resolution path in handleConflictReassign
currently always writes a string override for pendingConflict.targetId, leaving
a redundant override when the chosen keys equal the default; change the logic in
handleConflictReassign so it uses the same normalization as the recorder: if
pendingConflict.keys equals HOTKEYS[pendingConflict.targetId].key call
setOverride(pendingConflict.targetId, null), otherwise call
setOverride(pendingConflict.targetId, pendingConflict.keys); keep the existing
setOverride(pendingConflict.conflictId, null) and setPendingConflict(null)
behavior.
apps/desktop/src/renderer/hotkeys/hooks/useRecordHotkeys/useRecordHotkeys.ts (1)

2-4: Use the renderer/* alias for these internal hotkey imports.

The new hotkeys module is already using alias-based imports elsewhere in this PR; keeping these as ../../... makes the tree more brittle to future moves.

As per coding guidelines, "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/useRecordHotkeys/useRecordHotkeys.ts`
around lines 2 - 4, Replace the relative imports with the renderer alias
imports: import HOTKEYS, HotkeyId, and PLATFORM from the renderer hotkeys
registry and import useHotkeyOverridesStore and Platform from their renderer
stores/types instead of using "../../registry",
"../../stores/hotkeyOverridesStore", and "../../types"; update the import
statements that reference HOTKEYS, type HotkeyId, PLATFORM,
useHotkeyOverridesStore, and type Platform to use the "renderer/..." path
aliases defined in tsconfig so the module uses the alias-based imports
consistently.
🤖 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/src/renderer/hotkeys/hooks/useRecordHotkeys/useRecordHotkeys.ts`:
- Around line 12-31: In captureHotkeyFromEvent, normalize event.key before
building the shortcut string: after const key = event.key.toLowerCase(); map
known aliases (e.g. map "control" -> "ctrl" and the literal " " -> "space")
using a small keyAliases lookup and replace key with the normalized value; then
proceed with the existing exclusion list, F-key test, modifier checks, and final
join so you avoid producing duplicates like "ctrl+control" and so space-based
shortcuts become "space" (and thus match reserved combos like "meta+space");
reference function captureHotkeyFromEvent and constants MODIFIER_ORDER and
PLATFORM when making the change.

---

Nitpick comments:
In `@apps/desktop/src/renderer/hotkeys/hooks/useBinding/useBinding.ts`:
- Around line 1-2: Imports in useBinding.ts use deep relative paths; update the
import statements that bring in HOTKEYS, HotkeyId and useHotkeyOverridesStore to
use the project's tsconfig path aliases instead of "../../registry" and
"../../stores/hotkeyOverridesStore". Locate the import lines that reference
HOTKEYS and HotkeyId and the one that references useHotkeyOverridesStore and
replace their module specifiers with the corresponding tsconfig alias modules
(the alias used for the renderer hotkeys registry and stores) so they conform to
the project's alias convention.
- Around line 5-19: Extract the duplicate resolution logic used in useBinding
and getBinding into a single shared resolver (e.g., resolveBinding or
getEffectiveBinding) that takes a HotkeyId and an overrides map (or the store
state) and returns the resolved key or null; update useBinding to call
useHotkeyOverridesStore with a selector that delegates to this shared resolver,
and update getBinding to read the store via useHotkeyOverridesStore.getState()
and delegate to the same resolver so both functions use identical logic and
avoid drift.

In
`@apps/desktop/src/renderer/hotkeys/hooks/useRecordHotkeys/useRecordHotkeys.ts`:
- Around line 2-4: Replace the relative imports with the renderer alias imports:
import HOTKEYS, HotkeyId, and PLATFORM from the renderer hotkeys registry and
import useHotkeyOverridesStore and Platform from their renderer stores/types
instead of using "../../registry", "../../stores/hotkeyOverridesStore", and
"../../types"; update the import statements that reference HOTKEYS, type
HotkeyId, PLATFORM, useHotkeyOverridesStore, and type Platform to use the
"renderer/..." path aliases defined in tsconfig so the module uses the
alias-based imports consistently.

In `@apps/desktop/src/renderer/routes/_authenticated/settings/keyboard/page.tsx`:
- Around line 27-33: The page filters and renders categories using the hardcoded
CATEGORY_ORDER which omits the "Navigation" bucket produced by
getHotkeysByCategory(), causing those hotkeys to be hidden; update the rendering
to use a single source of truth by either adding "Navigation" to CATEGORY_ORDER
or (preferably) deriving the category list from the keys returned by
getHotkeysByCategory() and then applying your existing ordering as a stable
fallback, ensuring references to CATEGORY_ORDER, getHotkeysByCategory, and the
"Navigation" category are synchronized so no category is dropped.
- Around line 165-169: The conflict-resolution path in handleConflictReassign
currently always writes a string override for pendingConflict.targetId, leaving
a redundant override when the chosen keys equal the default; change the logic in
handleConflictReassign so it uses the same normalization as the recorder: if
pendingConflict.keys equals HOTKEYS[pendingConflict.targetId].key call
setOverride(pendingConflict.targetId, null), otherwise call
setOverride(pendingConflict.targetId, pendingConflict.keys); keep the existing
setOverride(pendingConflict.conflictId, null) and setPendingConflict(null)
behavior.
🪄 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: 54812c06-4fd9-462f-a7ec-6198efdbed05

📥 Commits

Reviewing files that changed from the base of the PR and between afbae45 and 93bccc7.

📒 Files selected for processing (6)
  • apps/desktop/src/renderer/hotkeys/hooks/useBinding/useBinding.ts
  • apps/desktop/src/renderer/hotkeys/hooks/useHotkey/useHotkey.ts
  • apps/desktop/src/renderer/hotkeys/hooks/useRecordHotkeys/useRecordHotkeys.ts
  • apps/desktop/src/renderer/hotkeys/utils/utils.ts
  • apps/desktop/src/renderer/routes/_authenticated/layout.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/keyboard/page.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/desktop/src/renderer/routes/_authenticated/layout.tsx
  • apps/desktop/src/renderer/hotkeys/utils/utils.ts
  • apps/desktop/src/renderer/hotkeys/hooks/useHotkey/useHotkey.ts

Comment on lines +12 to +31
function captureHotkeyFromEvent(event: KeyboardEvent): string | null {
const key = event.key.toLowerCase();
if (["shift", "ctrl", "alt", "meta", "dead", "unidentified"].includes(key))
return null;

// Must include ctrl or meta (or be F1-F12)
const isFKey = /^f([1-9]|1[0-2])$/.test(key);
if (!isFKey && !event.ctrlKey && !event.metaKey) return null;

// Reject meta on non-Mac
if (PLATFORM !== "mac" && event.metaKey) return null;

const modifiers: string[] = [];
if (event.metaKey) modifiers.push("meta");
if (event.ctrlKey) modifiers.push("ctrl");
if (event.altKey) modifiers.push("alt");
if (event.shiftKey) modifiers.push("shift");

const ordered = MODIFIER_ORDER.filter((m) => modifiers.includes(m));
return [...ordered, key].join("+");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

In Chromium / Electron, what values does KeyboardEvent.key return for the Control key and the Space key?

💡 Result:

In Chromium and Electron, KeyboardEvent.key returns "Control" for the Control key and " " (space character) for the Space key.

Citations:


🏁 Script executed:

cat -n apps/desktop/src/renderer/hotkeys/hooks/useRecordHotkeys/useRecordHotkeys.ts | head -70

Repository: superset-sh/superset

Length of output: 2837


🏁 Script executed:

rg -n "const HOTKEYS|export.*HOTKEYS" --type ts --type tsx

Repository: superset-sh/superset

Length of output: 91


🏁 Script executed:

rg -n "const HOTKEYS|export.*HOTKEYS" -t ts

Repository: superset-sh/superset

Length of output: 451


🏁 Script executed:

sed -n '571,600p' apps/desktop/src/renderer/hotkeys/registry.ts

Repository: superset-sh/superset

Length of output: 251


🏁 Script executed:

sed -n '29,150p' apps/desktop/src/renderer/hotkeys/registry.ts | head -80

Repository: superset-sh/superset

Length of output: 2417


🏁 Script executed:

rg -n "space\|meta\+space" apps/desktop/src/renderer/hotkeys/registry.ts

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

rg -n "captureHotkeyFromEvent\|checkReserved" apps/desktop/src/renderer/hotkeys/hooks/useRecordHotkeys/useRecordHotkeys.ts

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

cat -n apps/desktop/src/renderer/hotkeys/hooks/useRecordHotkeys/useRecordHotkeys.ts | tail -50

Repository: superset-sh/superset

Length of output: 1769


Normalize event.key to handle Chromium/Electron key names before building shortcut strings.

Chromium/Electron reports the Ctrl key as "Control" (uppercase) and Space as a literal " ". The current code only applies toLowerCase(), which produces invalid shortcuts like "ctrl+control" when pressing Ctrl+K, and prevents Space-based shortcuts from matching reserved keys like "meta+space".

🔧 Add key aliases to normalize "Control" → "ctrl" and " " → "space"
+const KEY_ALIASES: Record<string, string> = {
+	control: "ctrl",
+	" ": "space",
+};
+
 function captureHotkeyFromEvent(event: KeyboardEvent): string | null {
-	const key = event.key.toLowerCase();
+	const key = KEY_ALIASES[event.key.toLowerCase()] ?? event.key.toLowerCase();
 	if (["shift", "ctrl", "alt", "meta", "dead", "unidentified"].includes(key))
 		return null;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function captureHotkeyFromEvent(event: KeyboardEvent): string | null {
const key = event.key.toLowerCase();
if (["shift", "ctrl", "alt", "meta", "dead", "unidentified"].includes(key))
return null;
// Must include ctrl or meta (or be F1-F12)
const isFKey = /^f([1-9]|1[0-2])$/.test(key);
if (!isFKey && !event.ctrlKey && !event.metaKey) return null;
// Reject meta on non-Mac
if (PLATFORM !== "mac" && event.metaKey) return null;
const modifiers: string[] = [];
if (event.metaKey) modifiers.push("meta");
if (event.ctrlKey) modifiers.push("ctrl");
if (event.altKey) modifiers.push("alt");
if (event.shiftKey) modifiers.push("shift");
const ordered = MODIFIER_ORDER.filter((m) => modifiers.includes(m));
return [...ordered, key].join("+");
const KEY_ALIASES: Record<string, string> = {
control: "ctrl",
" ": "space",
};
function captureHotkeyFromEvent(event: KeyboardEvent): string | null {
const key = KEY_ALIASES[event.key.toLowerCase()] ?? event.key.toLowerCase();
if (["shift", "ctrl", "alt", "meta", "dead", "unidentified"].includes(key))
return null;
// Must include ctrl or meta (or be F1-F12)
const isFKey = /^f([1-9]|1[0-2])$/.test(key);
if (!isFKey && !event.ctrlKey && !event.metaKey) return null;
// Reject meta on non-Mac
if (PLATFORM !== "mac" && event.metaKey) return null;
const modifiers: string[] = [];
if (event.metaKey) modifiers.push("meta");
if (event.ctrlKey) modifiers.push("ctrl");
if (event.altKey) modifiers.push("alt");
if (event.shiftKey) modifiers.push("shift");
const ordered = MODIFIER_ORDER.filter((m) => modifiers.includes(m));
return [...ordered, key].join("+");
}
🤖 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 12 - 31, In captureHotkeyFromEvent, normalize event.key before
building the shortcut string: after const key = event.key.toLowerCase(); map
known aliases (e.g. map "control" -> "ctrl" and the literal " " -> "space")
using a small keyAliases lookup and replace key with the normalized value; then
proceed with the existing exclusion list, F-key test, modifier checks, and final
join so you avoid producing duplicates like "ctrl+control" and so space-based
shortcuts become "space" (and thus match reserved combos like "meta+space");
reference function captureHotkeyFromEvent and constants MODIFIER_ORDER and
PLATFORM when making the change.

@saddlepaddle saddlepaddle merged commit 1219200 into main Apr 5, 2026
15 checks passed
MocA-Love added a commit to MocA-Love/superset that referenced this pull request Apr 5, 2026
…erset-sh#3178)

upstream 1219200 の取り込み:
- shared/hotkeys.ts (1042行) + stores/hotkeys/ (357行) を削除
- renderer/hotkeys/ に新システム導入 (react-hotkeys-hook ベース)
- useAppHotkey → useHotkey、HotkeyTooltipContent → HotkeyLabel に全面移行
- tRPC hotkeysルーター削除、メニューacceleratorハードコード化
- キーボード設定ページを新システムで書き直し

フォーク固有の対応:
- BROWSER_RELOAD / BROWSER_HARD_RELOAD を新レジストリに追加
- SEARCH_IN_FILES を新レジストリに追加
- HotkeyCategory に "Browser" カテゴリ追加
- menu.ts のフォーク独自メニュー項目 (Browser Reload) 維持
- browser-shortcut-events.ts 維持
- workspace/page.tsx のコンフリクト解消 (閉じ括弧修正含む)
MocA-Love added a commit to MocA-Love/superset that referenced this pull request Apr 5, 2026
refactor(desktop): ホットキーシステムをreact-hotkeys-hookに全面移行 (superset-sh#3178)
MocA-Love added a commit to MocA-Love/superset that referenced this pull request Apr 5, 2026
cherry-pick + 手動統合で内容を取り込み済み:
- 1219200 superset-sh#3178 — rewrite hotkey system with react-hotkeys-hook ✓

フォーク固有の追加対応:
- BROWSER_RELOAD / BROWSER_HARD_RELOAD / SEARCH_IN_FILES を新レジストリに追加
- HotkeyCategory に "Browser" カテゴリ追加
- 全 useHotkey に enabled: isActive ガード追加 (keep-alive workspace 対策)
- menu.ts accelerator を registry.ts と一致させる修正

これで upstream/main との behind = 0
github-actions Bot added a commit that referenced this pull request Apr 10, 2026
The hotkey rewrite in #3178 introduced a blanket catch-all that bubbles
every Ctrl/Meta combo away from xterm, breaking standard readline
shortcuts (Ctrl+R, Ctrl+L, Ctrl+A, etc.). The TERMINAL_RESERVED
allowlist only contained ctrl+c/d/z/s/q/\, so readline combos were
intercepted by the app layer instead of reaching the terminal.

Add the 12 missing readline shortcuts to TERMINAL_RESERVED in both
utils.ts and useRecordHotkeys.ts so they pass through to xterm.

Closes #3333
darklow added a commit to darklow/superset that referenced this pull request Apr 10, 2026
The hotkey rewrite in superset-sh#3178 bubbles all Ctrl combos away from xterm
unless they're in TERMINAL_RESERVED, which only had 6 entries.
This restores standard readline shortcuts (Ctrl+R, Ctrl+L, Ctrl+A, etc.).
Kitenite added a commit that referenced this pull request Apr 12, 2026
Two bugs in the post-rewrite hotkey migration (#3178):

1. The "already migrated" guard checked for the `hotkey-overrides`
   localStorage key, but Zustand's persist middleware doesn't write it
   until the store mutates. Users who never customized anything re-ran
   the migration (and re-hit the legacy tRPC endpoint) every launch.
   Replaced with a dedicated `hotkey-overrides-migrated` sentinel set
   in every terminal branch.

2. The new registry declares punctuation via code-based names
   (`bracketleft`, `comma`, `slash`, …) because react-hotkeys-hook
   matches on `KeyboardEvent.code`. The old store held the literal
   character ("["). The migration copied overrides verbatim, so
   anything bound to `[ ] , . / \ ; ' backtick - =` silently stopped
   working after upgrade. Added a token-level translation table.

Adds migrate.test.ts covering both fixes plus the existing branches.
@Kitenite Kitenite deleted the saddlepaddle/acidic-honeycrisp branch April 13, 2026 16:36
darklow added a commit to darklow/superset that referenced this pull request Apr 19, 2026
…ll ctrl combos

Instead of allowlisting individual terminal shortcuts in TERMINAL_RESERVED,
flip the logic: keep all ctrl/meta combos in xterm by default and only
bubble events that match a registered app hotkey (isAppHotkeyEvent).
This restores the pre-superset-sh#3178 behavior where unregistered shortcuts like
Ctrl+R, Ctrl+L, Ctrl+O, etc. stay in the terminal.
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