Skip to content

feat: configurable keyboard shortcuts#2321

Open
Basit-Balogun10 wants to merge 7 commits into
PostHog:mainfrom
Basit-Balogun10:posthog-code/configurable-shortcuts
Open

feat: configurable keyboard shortcuts#2321
Basit-Balogun10 wants to merge 7 commits into
PostHog:mainfrom
Basit-Balogun10:posthog-code/configurable-shortcuts

Conversation

@Basit-Balogun10
Copy link
Copy Markdown

@Basit-Balogun10 Basit-Balogun10 commented May 23, 2026

Problem

Users cannot remap keyboard shortcuts to suit their workflow or resolve conflicts with OS/app bindings on their machine. Closes #300

Changes

Core store (keybindingsStore.ts)

Persisted Zustand store backed by electronStorage — bindings survive app restarts. Stores custom overrides as Partial<Record<ConfigurableShortcutId, string[]>>.

  • addKeybinding / updateKeybinding / removeKeybinding — per-binding CRUD, max 2 custom bindings per shortcut
  • resetShortcut / resetAll — revert one or all shortcuts to defaults
  • resolveKey — falls back to the static default when no custom entry exists
  • splitBindings — splits comma-separated binding strings while correctly handling mod+, (the Settings shortcut key contains a literal comma — uses negative lookbehind regex (?<!\+), to distinguish separator commas from key commas)
  • findConflict — checks the candidate key against all other configurable shortcuts (with live overrides applied) and all non-configurable static shortcuts; returns the conflicting description and whether it is a fixed shortcut

Definitions (keyboard-shortcuts.ts)

  • 20 of 28 shortcuts marked configurable: true with an explicit CONFIGURABLE_SHORTCUT_IDS array and DEFAULT_KEYBINDINGS record
  • eventToCombo — converts a KeyboardEvent to a normalised combo string (mod+shift+v); requires at least one non-shift modifier; normalises ArrowUp → up etc.
  • tiptapEventToCombo — like eventToCombo but also accepts shift-only combos, needed for prompt-history-prev/next whose defaults are shift+up / shift+down
  • Platform-aware display formatting throughout: Mac shows ⌘⇧⌥↩↑↓, Windows shows Ctrl+Shift+Alt+Enter+↑+↓. This also fixed a bug where hardcoded shortcut labels in the editor (e.g. the paste-as-file hint toast) were showing the Mac symbol on Windows instead of Ctrl

Shortcut recorder UI (ShortcutRecorder.tsx)

ShortcutRecordingModal — a fullscreen capture overlay:

  • Window-level keydown listener (no element focus required)
  • Live conflict detection with amber highlight and description message
  • Enter to confirm (blocked when conflict exists), Escape to cancel, click-backdrop to dismiss
  • Title changes between "Add new binding for…" and "Edit binding for…" depending on mode

BindingChip — individual keycap with left-click to edit, right-click context menu:

  • Edit binding / Add another binding / Remove binding / Reset to default
  • "Remove binding" disabled (with tooltip) when it is the only binding
  • "Reset to default" disabled (with tooltip) when already at default

ShortcutRecorder — row-level component:

  • Multi-chip display with "or" separator; splits multi-binding defaults (e.g. mod+n,mod+t) into individual chips
  • canAddMore based on custom count only — defaults don't consume the 2-binding budget
  • Removing a default binding materialises the remaining defaults as custom, so sibling defaults are not lost
  • Editing a default preserves all other defaults — updateKeybinding copies them into the custom array before replacing only the target

Shortcuts sheet (KeyboardShortcutsSheet.tsx)

  • Self-referential header: shows the current live binding used to open the sheet itself
  • Configurable rows render ShortcutRecorder; non-configurable rows render static ShortcutKeys with a "This shortcut cannot be customized" tooltip on hover
  • Sticky "Reset all shortcuts to defaults" footer — only rendered when there are any customisations

Editor integration (useTiptapEditor.ts)

  • paste-as-file reads from the store dynamically; supports multiple bindings
  • prompt-history-prev/next resolved from store via tiptapEventToCombo; replaces the old hardcoded event.shiftKey check
  • Paste hint toast shows the live current paste-as-file binding (was hardcoded mod+shift+v)

Non-configurable shortcuts — decisions

Shortcut Reason
Switch to task 1–9 (mod+1-9) Range binding across 9 keys; not representable as a single bindable combo
Switch to tab 1–9 (ctrl+1-9) Same
Find in conversation (mod+f) Integrated with the browser find mechanism
Bold / Italic / Underline / Code (mod+b/i/u/e) Tiptap ProseMirror commands — rebinding requires patching Tiptap extension keymaps, not just the app hotkey system

How did you test this?

Unit tests — 30 tests in keybindingsStore.test.ts covering: resolveKey, addKeybinding (dedup, max-limit enforcement), removeKeybinding, resetShortcut, resetAll, getKey, updateKeybinding (editing a default preserves siblings, editing custom replaces only target), and findConflict (configurable defaults, custom overrides on other shortcuts, non-configurable fixed shortcuts, mod+, edge case, excluded shortcut own key not flagged as conflict). All 30 pass.

Typecheck — clean (tsc --noEmit on both tsconfig.node.json and tsconfig.web.json).

E2E tests (shortcuts.spec.ts) — wrote a full E2E suite covering:

  • Sheet opens, all categories visible
  • Configurable chips clickable; non-configurable shows tooltip on hover
  • Recording modal: opens on click, Escape cancels, backdrop click cancels, bare letter ignored, Enter without combo does nothing
  • Saving: valid combo + Enter saves and modal closes
  • Right-click context menu: Edit / Add another / Remove / Reset
  • Add second binding; Add another disappears at 2-binding limit
  • Conflict detection: amber message shown, Enter blocked, resolving conflict re-enables save
  • Remove binding; Remove disabled (with tooltip) when only one binding remains
  • Reset to default; Reset disabled when already at default
  • Reset all: hidden with no custom bindings, appears after add, clears all on click

Manual testing:

1. Open the Keyboard Shortcuts Sheet

Default shortcut: Cmd+/ (Mac) / Ctrl+/ (Windows)

  • Press the shortcut. The "Keyboard Combos" sheet opens.

2. The Sheet Layout — Configurable vs Fixed

Scroll through all four categories: General, Navigation, Panels & Tabs, Editor.

Configurable rows (20 shortcuts) show interactive keycap chips you can click.
Fixed rows (8 shortcuts) show static chips. Hover one — tooltip reads "This shortcut cannot be customized."

Fixed shortcuts and why:

Shortcut Keys Reason non-configurable
Switch to task 1-9 Mod+1-9 Range binding, not a single key
Switch to tab 1-9 Ctrl+1-9 Range binding
Find in conversation Mod+F Tight browser find integration
Bold / Italic / Underline / Inline code Mod+B/I/U/E Tiptap rich-text internals, hardwired into editor command system
configurable-vs-fixed-shortcuts-posthog-code-configurable-shortcuts.mp4

3. Edit a Shortcut — Click to Edit

  1. In General, find "Open command menu" (Cmd+K / Ctrl+K).
  2. Left-click the keycap chip. A recording modal appears with a blurred backdrop.
  3. Press any key combination, e.g. Cmd+Shift+P. The modal shows the combo formatted with platform symbols (Mac: ⌘⇧P, Windows: Ctrl+Shift+P).
  4. The instruction reads "Press Enter to confirm, Escape to cancel."
  5. Press Enter to save. The chip in the sheet updates immediately.
  6. Dismiss the sheet. Press your new shortcut — the command menu opens. ✅
editing-a-shortcut-posthog-code-configurable-shortcuts.mp4.mp4

4. Conflict Detection — Amber Warning

  1. Open the sheet, click a shortcut chip to edit it.
  2. Press a key that is already used — e.g. Cmd+N (which is "New task").
  3. The modal turns amber: the input field highlights in amber, and below reads "Conflicts with 'New task' — press a different combination."
  4. While conflicting, pressing Enter does nothing — you cannot save a conflicting binding.
  5. Press a different combo to clear the conflict. Press Enter to save.

Edge case — mod+, (Settings): The comma is also the separator character used internally. Try to bind Cmd+, to something — it correctly detects it conflicts with "Open settings" despite the comma in the key name.

Non-configurable conflict: Try to bind Mod+B (Bold, which is a fixed Tiptap shortcut). The conflict detection catches this too — you will see it warns "Conflicts with 'Bold'."

conflict-detection-posthog-code-configurable-shortcuts.mp4.mp4.mp4

5. Escape / Backdrop to Cancel

  1. Open the recording modal.
  2. Press Escape — modal closes without saving.
  3. Open it again, press a valid combo.
  4. Click the backdrop (outside the modal card) — modal closes without saving.
escape-to-cancel-posthog-code-configurable-shortcuts.mp4.mp4.mp4

6. Add a Second Binding (Up to 2 Custom Bindings)

  1. Open the sheet. Find "New task."
  2. Right-click its chip → "Add another binding."
  3. Record a new key, e.g. Cmd+Shift+N. Press Enter.
  4. The row now shows two chips: Cmd+N or Cmd+Shift+N (separated by "or").
  5. Both trigger "New task" in the app.
  6. Right-click the first chip → "Add another binding" — this option is gone because you are now at the 2-binding limit.
adding-a-second-binding-posthog-code-configurable-shortcuts.mp4.mp4

7. Default Shortcuts with Multiple Bindings

Some shortcuts ship with 2 default bindings (e.g. New task: Cmd+N or Cmd+T).

  1. Find "New task" at its defaults.
  2. Notice both chips show independently.
  3. Remove one default chip (right-click → Remove binding). Only the other default remains.
  4. This works by materialising the remaining defaults as custom bindings — so removing Cmd+N stores ["mod+t"] in the custom array. The other default is preserved.
  5. Reset to default on either chip restores both Cmd+N and Cmd+T together.
default-shortcuts-with-multiple-binding-posthog-code-configurable-shortcuts.mp4

8. Edit One Default Binding — Preserves the Other

  1. Reset "New task" to defaults (both Cmd+N and Cmd+T chips visible).
  2. Left-click Cmd+N to edit it. Record Cmd+Shift+X. Press Enter.
  3. The row now shows Cmd+Shift+X or Cmd+T — only the edited key changed, the sibling default (Cmd+T) is preserved.

Uploading editing-one-default-binding-posthog-code-configurable-shortcuts.mp4…


9. Reset ALL Shortcuts

  1. Customise a few shortcuts across categories.
  2. At the bottom of the sheet, a sticky footer appears: "Reset all shortcuts to defaults" button (only visible when there are any customisations).
  3. Click it — all bindings reset. The footer disappears.
resetting-all-shortcuts-posthog-code-configurable-shortcuts.mp4

10. Paste-as-File Shortcut (Configurable, Live Hint)

In the Editor category, find "Paste as file attachment" (Cmd+Shift+V / Ctrl+Shift+V).

  1. Copy some text to clipboard.
  2. In the message editor, paste normally — if text > 200 chars, a toast appears: "Use Ctrl+Shift+V to paste as a file attachment instead." (shows the live current binding).
  3. Open the sheet and change the paste-as-file shortcut to Ctrl+Shift+G.
  4. Paste again — the toast now reads "Use Ctrl+Shift+G to paste as a file attachment instead." ✅
paste-as-file-shortcut-posthog-code-configurable-shortcuts.mp4

11. Persistence Across App Restarts

  1. Customise several shortcuts.
  2. Quit and relaunch the app.
  3. All customisations are preserved — stored via electronStorage (Electron user data directory).

Publish to changelog?

Probably? Users can now remap any of the 20 configurable shortcuts directly inside the app via the Keyboard Shortcuts sheet (⌘/ / Ctrl+/) - and potentially any new additions to the app shortcuts can now be configurable

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 23, 2026

Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
apps/code/src/renderer/stores/keybindingsStore.ts:118-121
`updateKeybinding` does not deduplicate the result, so editing one binding to a value that already exists as another binding for the same shortcut silently produces `["ctrl+q", "ctrl+q"]`. The conflict-detection in the recording modal excludes the shortcut being edited (the `excludeId` skip), so the duplicate slips through undetected. The UI then renders two chips with identical `key` props, producing a React duplicate-key warning and potentially broken reconciliation.

```suggestion
        const updated = base.map((k) => (k === oldKey ? newKey : k));
        // Deduplicate in case newKey already exists elsewhere in the array.
        const deduped = [...new Set(updated)];
        set({
          customKeybindings: { ...get().customKeybindings, [id]: deduped },
        });
```

### Issue 2 of 2
apps/code/src/renderer/features/message-editor/tiptap/useTiptapEditor.ts:326-365
`prompt-history-prev/next` shortcuts silently break when remapped to non-arrow-key combos. The outer `event.key === "ArrowUp"` / `event.key === "ArrowDown"` guards are still hardcoded, so `forcePrev`/`forceNext` can be `true` from the store but the handler never fires unless the physical key is an arrow. A user who remaps `prompt-history-prev` to e.g. `ctrl+k` gets a chip in the UI that saves correctly but does nothing when pressed.

Reviews (1): Last reviewed commit: "feat: make prompt-history shortcuts conf..." | Re-trigger Greptile

Comment thread apps/code/src/renderer/stores/keybindingsStore.ts
Comment thread apps/code/src/renderer/features/message-editor/tiptap/useTiptapEditor.ts Outdated
Users can now remap any of the 17 configurable shortcuts via Settings >
Shortcuts (or the ⌘/ sheet). Custom bindings fully replace all defaults
(including alternates) and multiple custom combos per action are supported.
Bindings persist across sessions via electronStorage.

- Add `configurable` flag + `DEFAULT_KEYBINDINGS` map to keyboard-shortcuts.ts
- New `keybindingsStore` (persist + electronStorage) with array-based custom combos,
  conflict detection helper, and individual/bulk reset
- New `useShortcut(id)` hook — reactive Zustand selector, feeds useHotkeys
- New `Keycap` component extracted to avoid circular imports
- New `ShortcutRecorder` component: click + to enter recording mode, captures
  keydown, shows conflict toast, per-binding × remove, per-shortcut ↩ reset
- Update all useHotkeys call sites (GlobalEventHandlers, SpaceSwitcher,
  usePanelKeyboardShortcuts, ExternalAppsOpener) to use useShortcut()
- KeyboardShortcutsSheet: configurable rows render ShortcutRecorder instead of
  static keycaps; "Reset all shortcuts" button shown when customisations exist

Generated-By: PostHog Code
Task-Id: 80405bf7-239f-4b60-a1cf-5a4777fb7218
Bare letter keys (e.g. just "k") would fire every time that character is
typed anywhere in the app. Require at least mod/ctrl/alt to be held.

Generated-By: PostHog Code
Task-Id: 80405bf7-239f-4b60-a1cf-5a4777fb7218
24 tests covering resolveKey, addKeybinding, removeKeybinding,
resetShortcut, resetAll, getKey, and findConflict — including
conflict detection against comma-separated default alternates.

Generated-By: PostHog Code
Task-Id: 80405bf7-239f-4b60-a1cf-5a4777fb7218
- KeyboardShortcutsSheet header now reads the "shortcuts" key via
  useShortcut() so the trigger keycap updates when remapped
- ExternalAppsOpener dropdown labels for open-in-editor and copy-path
  now derive from useShortcut() + formatHotkeyParts() instead of
  hardcoded Mac-only symbols

test(e2e): add Playwright shortcut sheet tests

Covers sheet open/close, category sections, hover controls, recording
mode entry/cancellation, bare-key rejection, saving bindings, conflict
detection, removing bindings, per-shortcut reset, and reset-all.

Generated-By: PostHog Code
Task-Id: 80405bf7-239f-4b60-a1cf-5a4777fb7218
Hardcoded Cmd glyphs were leaking onto Windows in the send-messages
dropdown and the tiptap paste hint, and two handlers were gated on
metaKey only so the corresponding shortcut never fired on Windows
(mod+1..9 task switching, Cmd/Ctrl-click multi-select in the inbox).

Generated-By: PostHog Code
Task-Id: 80405bf7-239f-4b60-a1cf-5a4777fb7218
- Add prompt-history-prev/next to CONFIGURABLE_SHORTCUT_IDS and
  DEFAULT_KEYBINDINGS so they appear in the shortcuts sheet and
  can be rebound like any other shortcut
- Add tiptapEventToCombo() — accepts shift-only combos (no Ctrl/Meta
  required) so shift+up/down can be matched against live bindings
- Fix eventToCombo() to normalise Arrow-prefixed key names (ArrowUp to up)
- Wire useTiptapEditor to resolve prompt-history keys from the store
  instead of hardcoding event.shiftKey
- Fix paste hint toast to show the live paste-as-file binding instead
  of the hardcoded mod+shift+v string
- Fix noStaticElementInteractions lint on recording modal backdrop
- Rewrite E2E shortcut tests to match the current recording modal UI
  (chips + right-click context menu) rather than the old hover-button
  and inline-input design
- Deduplicate in updateKeybinding — conflict detection excludes the
  shortcut being edited so editing one binding to match another on the
  same shortcut could produce ["ctrl+q","ctrl+q"], duplicate React keys
  and broken chip reconciliation
- Remove ArrowUp/Down gate around prompt-history navigation so custom
  non-arrow bindings (e.g. Ctrl+K) actually fire when pressed, not just
  when the physical key is an arrow
- Remove obvious section-divider comments and redundant JSX labels
  (Header, Scrollable list, Sticky footer); keep non-obvious rationale
  comments (window-level capture, backdrop dismiss, canAddMore budget,
  dedup note, ArrowKey gate explanation)
@Basit-Balogun10 Basit-Balogun10 force-pushed the posthog-code/configurable-shortcuts branch from 5a62f10 to aa353a1 Compare May 23, 2026 02:33
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.

Ability to configure shortcuts

1 participant