Skip to content

Remove custom hotkeys handler in favor of react-hotkeys-hook#114

Merged
saddlepaddle merged 1 commit intomainfrom
switch-to-react-hotkeys-hook
Nov 21, 2025
Merged

Remove custom hotkeys handler in favor of react-hotkeys-hook#114
saddlepaddle merged 1 commit intomainfrom
switch-to-react-hotkeys-hook

Conversation

@saddlepaddle
Copy link
Copy Markdown
Collaborator

@saddlepaddle saddlepaddle commented Nov 21, 2025

Description

Related Issues

Type of Change

  • Bug fix
  • New feature
  • Documentation
  • Refactor
  • Other (please describe):

Testing

Screenshots (if applicable)

Additional Notes

Summary by CodeRabbit

  • New Features
    • Keyboard shortcuts now available for workspace navigation (Meta+Alt+Left/Right), tab management (Meta+T, Meta+W, Meta+1-9), sidebar toggle (Meta+S), and pane splitting (Meta+D, Meta+Shift+D).

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Nov 21, 2025

Walkthrough

This PR migrates keyboard shortcut handling from a centralized custom system (useGlobalShortcuts hook with keyboard-shortcuts and shortcuts modules) to a distributed component-based approach using react-hotkeys-hook library. The old infrastructure is removed entirely and hotkey registration is moved into individual components.

Changes

Cohort / File(s) Summary
Dependency Management
apps/desktop/package.json
Added react-hotkeys-hook ^5.2.1 to dependencies.
Removed Custom Shortcut Infrastructure
apps/desktop/src/renderer/lib/keyboard-shortcuts.ts, apps/desktop/src/renderer/lib/shortcuts.ts, apps/desktop/src/renderer/hooks/useGlobalShortcuts.ts
Deleted keyboard shortcut utility module (matchesShortcut, createShortcutHandler, formatShortcut), shortcuts factory functions (createWorkspaceShortcuts, createTabShortcuts, createSplitPaneShortcuts, createTerminalShortcuts), and centralized global shortcuts hook.
Component-Level Hotkey Integration
apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/index.tsx, apps/desktop/src/renderer/screens/main/components/WorkspaceView/index.tsx, apps/desktop/src/renderer/screens/main/index.tsx
Integrated react-hotkeys-hook into components for workspace switching (meta+alt+left/right), tab management (meta+t, meta+w, meta+1-9, meta+alt+up/down), and sidebar/split pane toggling (meta+s, meta+d, meta+shift+d).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Key areas requiring attention:
    • Verify no orphaned imports of removed keyboard-shortcuts.ts, shortcuts.ts, or useGlobalShortcuts remain elsewhere in the codebase
    • Confirm hotkey sequences in each component are consistent and non-conflicting (meta+alt+left/right, meta+t, meta+w, meta+d, meta+s, meta+1-9, etc.)
    • Check guard conditions in WorkspaceTabs and WorkspaceView (handling missing activeWorkspaceId, boundary checks for array indices)
    • Validate useMemo and dependency arrays are correctly specified to prevent stale closures
    • Ensure hotkey handlers properly interact with TRPC mutations (useSetActiveWorkspace, tab state updates)

Possibly related PRs

  • PR #108: Introduced the global keyboard shortcut system (useGlobalShortcuts, keyboard-shortcuts.ts, shortcuts.ts) that is being removed and replaced by this distributed hotkey approach.

Poem

🐰 Hops to the beat of keys anew,
From central rules to scattered few,
React hooks dance where shortcuts belong,
Each component sings its own sweet song! 🎹✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is entirely an unfilled template with no actual content, rationale, testing details, or implementation notes provided by the author. Fill in the Description section explaining the refactoring rationale, link any related issues, mark as Refactor, document testing performed, and add context about the library migration.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main change—migration from a custom keyboard shortcut handler to the react-hotkeys-hook library.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch switch-to-react-hotkeys-hook

Comment @coderabbitai help to get the list of available commands and usage tips.

@saddlepaddle saddlepaddle merged commit 9392f9c into main Nov 21, 2025
0 of 5 checks passed
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
apps/desktop/src/renderer/screens/main/index.tsx (1)

2-34: Align modifiers with mod and consider preventDefault for the sidebar shortcut

The wiring itself looks correct, but two details are worth tightening up:

  • meta+... vs mod+...: react-hotkeys-hook supports a mod modifier that maps to ctrl on Windows/Linux and meta on macOS, which is usually what users expect for editor‑style shortcuts. Using meta directly means Windows users need the OS key instead of Ctrl. (react-hotkeys-hook.vercel.app)
  • meta+s default behavior: in Chromium environments, cmd/ctrl+s is typically bound to “Save page”. The library docs recommend preventing the default if you are overriding that shortcut; otherwise you can end up both toggling the sidebar and triggering the browser/Electron save behavior, depending on menu configuration. (react-hotkeys-hook.vercel.app)

You might want to update along these lines:

-	useHotkeys('meta+s', toggleSidebar, [toggleSidebar]);
+	useHotkeys('mod+s', toggleSidebar, { preventDefault: true }, [toggleSidebar]);

-	useHotkeys('meta+d', () => {
+	useHotkeys('mod+d', () => {
 		if (activeWorkspaceId) {
 			splitTabVertical(activeWorkspaceId);
 		}
-	}, [activeWorkspaceId, splitTabVertical]);
+	}, [activeWorkspaceId, splitTabVertical]);
 
-	useHotkeys('meta+shift+d', () => {
+	useHotkeys('mod+shift+d', () => {
 		if (activeWorkspaceId) {
 			splitTabHorizontal(activeWorkspaceId);
 		}
 	}, [activeWorkspaceId, splitTabHorizontal]);
apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/index.tsx (1)

3-42: Workspace hotkey logic looks solid; consider mod for cross‑platform ergonomics

The index/bounds checks around setActiveWorkspace.mutate look correct and safe even if the active workspace disappears or the list changes underneath.

The only thing I’d reconsider is the modifier choice:

  • Using 'meta+alt+left/right' will map to Cmd+Opt+Arrow on macOS, but to Super+Alt+Arrow on Windows/Linux, which is an unusual combo for app‑level navigation.
  • Since react-hotkeys-hook supports mod (Ctrl on Win/Linux, Meta on macOS), 'mod+alt+left/right' would better match common expectations for “previous/next workspace” on all platforms. (react-hotkeys-hook.vercel.app)

If you don’t have strong reasons to stick with the OS key on non‑mac platforms, swapping metamod here would improve consistency.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9a46d07 and 8526e14.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • apps/desktop/package.json (1 hunks)
  • apps/desktop/src/renderer/hooks/useGlobalShortcuts.ts (0 hunks)
  • apps/desktop/src/renderer/lib/keyboard-shortcuts.ts (0 hunks)
  • apps/desktop/src/renderer/lib/shortcuts.ts (0 hunks)
  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/index.tsx (3 hunks)
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/index.tsx (1 hunks)
  • apps/desktop/src/renderer/screens/main/index.tsx (1 hunks)
💤 Files with no reviewable changes (3)
  • apps/desktop/src/renderer/hooks/useGlobalShortcuts.ts
  • apps/desktop/src/renderer/lib/keyboard-shortcuts.ts
  • apps/desktop/src/renderer/lib/shortcuts.ts
🧰 Additional context used
🧬 Code graph analysis (3)
apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/index.tsx (1)
apps/desktop/src/renderer/react-query/workspaces/useSetActiveWorkspace.ts (1)
  • useSetActiveWorkspace (7-23)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/index.tsx (1)
apps/desktop/src/renderer/stores/tabs/hooks.ts (5)
  • useTabs (3-3)
  • useActiveTabIds (4-5)
  • useAddTab (7-7)
  • useSetActiveTab (10-11)
  • useRemoveTab (8-8)
apps/desktop/src/renderer/screens/main/index.tsx (2)
apps/desktop/src/renderer/stores/sidebar-state.ts (1)
  • useSidebarStore (17-37)
apps/desktop/src/renderer/stores/tabs/hooks.ts (2)
  • useSplitTabVertical (19-20)
  • useSplitTabHorizontal (21-22)
🔇 Additional comments (1)
apps/desktop/package.json (1)

55-61: Hotkeys dependency addition looks good; just confirm peer/engine compatibility locally

Adding react-hotkeys-hook at ^5.2.1 aligns with the current stable, non‑vulnerable release and its documented API (including the overload where the third arg is a deps array) matches how it’s used elsewhere in this PR. (secure.software)

Since you’re on React 19.1.1 and Electron 39, it should work fine, but please run your normal install flow and glance at any peer‑dependency warnings to be safe.

Comment on lines +29 to +66
// Tab management shortcuts - work even when sidebar is closed
useHotkeys('meta+t', () => {
if (activeWorkspaceId) {
addTab(activeWorkspaceId);
}
}, [activeWorkspaceId, addTab]);

useHotkeys('meta+w', () => {
if (activeTabId) {
removeTab(activeTabId);
}
}, [activeTabId, removeTab]);

useHotkeys('meta+alt+up', () => {
if (!activeWorkspaceId || !activeTabId) return;
const index = tabs.findIndex((t) => t.id === activeTabId);
if (index > 0) {
setActiveTab(activeWorkspaceId, tabs[index - 1].id);
}
}, [activeWorkspaceId, activeTabId, tabs, setActiveTab]);

useHotkeys('meta+alt+down', () => {
if (!activeWorkspaceId || !activeTabId) return;
const index = tabs.findIndex((t) => t.id === activeTabId);
if (index < tabs.length - 1) {
setActiveTab(activeWorkspaceId, tabs[index + 1].id);
}
}, [activeWorkspaceId, activeTabId, tabs, setActiveTab]);

// Jump to tab by number (Cmd+1 through Cmd+9)
useHotkeys('meta+1,meta+2,meta+3,meta+4,meta+5,meta+6,meta+7,meta+8,meta+9', (_, handler) => {
if (!activeWorkspaceId) return;
const key = handler.keys?.join('');
const num = key ? Number.parseInt(key, 10) : null;
if (num && tabs[num - 1]) {
setActiveTab(activeWorkspaceId, tabs[num - 1].id);
}
}, [activeWorkspaceId, tabs, setActiveTab]);
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

🧩 Analysis chain

Meta+T / Meta+W / Meta+1–9 hotkeys are likely to conflict with browser/Electron defaults

The tab management logic itself is clean:

  • Filtering tabs by workspaceId and !parentId is straightforward.
  • meta+alt+up/down navigation and the numbered tab jump using handler.keys?.join('') follow the patterns shown in the react-hotkeys-hook docs. (react-hotkeys-hook.vercel.app)

However, the specific keybindings chosen are risky with this library:

  • The docs explicitly call out that browsers don’t allow overriding several “safe browsing” shortcuts such as meta+W, meta+N, meta+T, their meta+shift+... variants, and the meta+1..9 tab-selection shortcuts; key events for these combinations may never reach JavaScript at all. (react-hotkeys-hook.vercel.app)
  • Here, those exact combos are used for:
    • meta+t → add tab.
    • meta+w → close tab.
    • meta+1..9 → jump to tab.
  • In a Chrome-based environment (including Electron unless you intercept at webContents level), users are very likely to get the default behavior (closing the window, opening a new tab/window, switching OS/browser tabs) instead of your in‑app tab actions, or the handlers may never fire.

That’s a significant UX and reliability problem, especially for cmd+w, which can close the entire window instead of just the active in‑app tab.

I’d strongly recommend:

  • Verifying on macOS and Windows in both dev (Vite in a browser, if applicable) and packaged Electron builds that these handlers actually fire and don’t accidentally close the app/window.
  • If they don’t behave reliably, either:
    • Move just these reserved combos (cmd+w, cmd+t, cmd+1..9) back to Electron‑level handling (before-input-event / globalShortcut) and dispatch actions into the renderer instead of using react-hotkeys-hook; or
    • Change the bindings to non‑reserved sequences (for example mod+alt+w for close tab, mod+alt+t for new tab, and another pattern for numbered jumps), and surface them in whatever “keyboard shortcuts” UI you expose.

Separately, using meta instead of mod here means Windows/Linux users must press the OS key rather than Ctrl. If you want the usual “Cmd on mac, Ctrl elsewhere” behavior, consider swapping to mod for all these tab shortcuts as well. (react-hotkeys-hook.vercel.app)






🏁 Script executed:

#!/bin/bash
# Step 1: Locate and examine the target file and its imports
find . -name "index.tsx" -path "*/WorkspaceView/*" -type f | head -1 | xargs cat

Length of output: 4394


🏁 Script executed:

#!/bin/bash
# Step 2: Check package.json for react-hotkeys-hook version and Electron
find . -name "package.json" -type f | head -5 | xargs grep -l "react-hotkeys-hook\|electron" 2>/dev/null || echo "Not found in initial scan"

Length of output: 59


🏁 Script executed:

#!/bin/bash
# Step 3: Search for Electron main process configuration and before-input-event handlers
rg -n "before-input-event|globalShortcut|ipcMain" --type=ts --type=js -C3 | head -40

Length of output: 2613


🏁 Script executed:

#!/bin/bash
# Step 4: Check for any existing meta vs mod usage in hotkey definitions
rg -n "useHotkeys|meta\+|mod\+" --type=tsx --type=ts -C2 | grep -E "(useHotkeys|meta\+|mod\+)" | head -30

Length of output: 27


🌐 Web query:

react-hotkeys-hook reserved shortcuts meta+w meta+t limitations 2025

💡 Result:

Short answer: You generally cannot override meta+W or meta+T in browsers using react-hotkeys-hook — the library’s docs and README warn those browser-level shortcuts (close tab / new tab) are not preventable, so preventDefault won’t stop them. For special cases there’s the experimental Keyboard Lock API (navigator.keyboard.lock) but it has limited support and typically only works in fullscreen. [1][2][3]

Sources:
[1] react-hotkeys-hook docs (preventDefault warning).
[2] react-hotkeys-hook GitHub README (note about certain keystrokes not preventable).
[3] Discussion/experiments about navigator.keyboard.lock (Stack Overflow).



---

🏁 Script executed:

```shell
#!/bin/bash
# Find all index.tsx files under apps/desktop and filter for WorkspaceView
fd "index.tsx" apps/desktop --exec grep -l "useHotkeys" {} \;

Length of output: 203


🏁 Script executed:

#!/bin/bash
# Examine the apps/desktop/package.json for react-hotkeys-hook version and Electron
cat apps/desktop/package.json | grep -A2 -B2 "react-hotkeys-hook\|electron"

Length of output: 1236


🏁 Script executed:

#!/bin/bash
# Search for Electron main process and hotkey handlers in active desktop app
find apps/desktop/src/main -type f \( -name "*.ts" -o -name "*.js" \) 2>/dev/null | xargs grep -l "before-input-event\|globalShortcut\|ipcMain" 2>/dev/null || echo "No main process files found"

Length of output: 41


🏁 Script executed:

#!/bin/bash
# Correct search for useHotkeys usage with proper file types
rg "useHotkeys.*meta\+|useHotkeys.*mod\+" -t ts -C2 | head -50

Length of output: 4316


🏁 Script executed:

#!/bin/bash
# Search for the specific hotkey patterns mentioned in the review
rg "meta\+t|meta\+w|meta\+1" -t ts -t js -C3 | head -50

Length of output: 2165



---

🏁 Script executed:

```shell
#!/bin/bash
# Examine the full WorkspaceView/index.tsx to confirm lines 29-66 and context
cat apps/desktop/src/renderer/screens/main/components/WorkspaceView/index.tsx

Length of output: 2384


🏁 Script executed:

#!/bin/bash
# Check if there are any Electron main process files at all
find apps/desktop/src/main -type f -name "*.ts" -o -name "*.js" 2>/dev/null | head -10

Length of output: 309


🏁 Script executed:

#!/bin/bash
# Check for any conditional event handlers or preventDefault logic
rg "preventDefault|keyboard.lock|registerKeyboardEvent" apps/desktop/src --type ts --type js -C2

Length of output: 3670


Reserved hotkeys (meta+t, meta+w, meta+1–9) will not work reliably in Electron

The tab management logic itself is clean:

  • Filtering tabs by workspaceId and !parentId is straightforward.
  • meta+alt+up/down navigation and the numbered tab jump using handler.keys?.join('') follow the patterns shown in the react-hotkeys-hook docs.

However, the specific keybindings chosen are risky with this library:

  • The docs explicitly warn that browsers and Electron do not allow overriding several "safe browsing" shortcuts such as meta+w, meta+t, their meta+shift+... variants, and the meta+1..9 tab-selection shortcuts; key events for these combinations may never reach JavaScript at all.
  • Here, those exact combos are used for:
    • meta+t → add tab.
    • meta+w → close tab.
    • meta+1..9 → jump to tab.
  • In this Electron environment, users will likely get the default behavior (closing the window, opening a new tab, switching OS/browser tabs) instead of your in-app tab actions, or the handlers may never fire.

This is a significant UX and reliability problem, especially for cmd+w, which can close the entire window instead of just the active in-app tab.

I'd strongly recommend:

  • Verifying on macOS and Windows in both dev and packaged Electron builds that these handlers actually fire and don't accidentally close the app/window.
  • If they don't behave reliably, either:
    • Move just these reserved combos (cmd+w, cmd+t, cmd+1..9) back to Electron-level handling (before-input-event / globalShortcut) and dispatch actions into the renderer instead of using react-hotkeys-hook; or
    • Change the bindings to non-reserved sequences (for example mod+alt+w for close tab, mod+alt+t for new tab, and another pattern for numbered jumps).

Separately, using meta instead of mod means Windows/Linux users must press the OS key rather than Ctrl. If you want the usual "Cmd on macOS, Ctrl elsewhere" behavior, consider swapping to mod for all these tab shortcuts.

🤖 Prompt for AI Agents
In apps/desktop/src/renderer/screens/main/components/WorkspaceView/index.tsx
around lines 29–66, the current hotkeys use reserved combos (meta+t, meta+w,
meta+1..9) which may be intercepted by the OS/Electron and never reach
react-hotkeys-hook; replace these usages by either moving handling for these
reserved combos to the Electron main process (use before-input-event or
globalShortcut and send an IPC to the renderer to add/close/switch tabs) or
change the bindings to non-reserved sequences (e.g., mod+alt+t, mod+alt+w,
mod+alt+1..9) and switch from literal "meta" to "mod" so Ctrl is used on
Windows/Linux; ensure you update the useHotkeys calls accordingly and test on
macOS and Windows in both dev and packaged builds to confirm the handlers fire
and the window isn’t closed unexpectedly.

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