feat(settings): configurable keyboard shortcuts with a Shortcuts section - #149
feat(settings): configurable keyboard shortcuts with a Shortcuts section#149omiinaya wants to merge 3 commits into
Conversation
83f169b to
48a8332
Compare
Every hotkey the app ships with is now user-rebindable from a new settings section (Settings -> Shortcuts). The combos live in a shared registry (src/shared/shortcuts.ts) that seeds Settings.shortcuts and drives both the section's capture rows and the ShortcutsPanel badge rows, so a rebind takes effect immediately and the reference panel reflects it. Wiring: dispatch sites (canvas palette/settings/toggles/undo/redo/new-node, main-process markdown-close intercepts, terminal find, source-control commit) read settings.shortcuts via the existing matchesShortcut engine instead of hardcoded key checks. Merge handles a missing .shortcuts on old settings.json like .speech, so new actions simply appear with their shipped default. Extracts the Speech section's ShortcutCaptureField into a shared settings control (gains allowChord/defaultValue knobs; dictation keeps its hold-to-talk shape). Conflict detection flags a combo mapped to two actions inline. Mouse gestures stay fixed; dictation stays in Speech (already configurable).
…reads live settings - ShortcutsPanel.test.tsx: renders a row per SHORTCUT_DEFS grouped, shows the current combo from settings.shortcuts, and a rebind in the store is reflected in the panel immediately (no re-open); dictates lead the General group and the fixed gesture rows still render. - shortcuts-dispatch-wiring.test.ts: source-pins the rebind contract at every dispatch site (Canvas keydown handlers, TerminalNode find, SourceControlPanel commit, main-process markdown/close intercepts) — each reads the LIVE settings/shortcuts map via useSettings.getState()/settingsStore.get() on every event, so a Settings rebind applies on the next keypress without a listener re-run or reload. House pattern per canvas-wiring.test.tsx (Canvas monolith has no render harness).
48a8332 to
6cba8d7
Compare
|
Thanks for this — the registry shape is good, the capture field correctly mandates a primary modifier, and I have to flag one thing up front, because it is the reason this cannot land as-is and it is not visible as a merge conflict. Blocker: after merging main, the
|
|
Following up on item 1 specifically, because it is the one that a rebase makes worse. Symptom after rebasing onto current Why. } else if (input.code === 'Digit0' && !input.shift && !input.alt) {because the guard at the top of the same handler ( if (input.type !== 'keyDown' || !(input.meta || input.control)) returnThis PR replaces that with This is invisible as a merge conflict. Merging Measured on the merged tree, by lifting the real The same test on Fix — one line, the - } else if (input.code === 'Digit0' && !input.shift && !input.alt) {
+ } else if ((input.meta || input.control) && input.code === 'Digit0' && !input.shift && !input.alt) {That is Our half of this. #193's |
|
Heads up on a Practically, this means your rebase will show a red test instead of a silent breakage: Sorry for the extra churn — this was our own test debt from #193, not anything wrong with your branch. |
What changed
Adds a new Settings → Shortcuts section that lets you rebind every hotkey the app ships with. The combos live in a single shared registry (
src/shared/shortcuts.ts) that seedsSettings.shortcutsand drives both the section's capture rows and the ShortcutsPanel (⌘/) badge rows — so a rebind applies immediately and the reference panel always reflects it.Rebindable actions: command palette (
⌘K), settings (⌘,), shortcuts panel (⌘/), undo/redo, canvas toggles, new terminal/agent, close node, markdown view, find, commit, copy.Scope of the change (19 files, +858 / −202):
src/shared/shortcuts.ts(+shortcuts.test.ts) — the registry of defaults and the both-branch cross-platform proof.ShortcutsSection.tsx(+ test). Extracts the Speech section's capture field into a shared controlsrc/renderer/components/settings/ShortcutCaptureField.tsxwithallowChord/defaultValueknobs, so dictation (hold-to-talk) and plain hotkeys share one control. Conflict detection flags a combo mapped to two actions inline.settings.shortcutsthrough the existingmatchesShortcutengine instead of hardcoded key checks —Canvas.tsx(chords), main-process⌘M/⌘Wintercepts insrc/main/index.ts,TerminalNode.tsx(⌘F),SourceControlPanel.tsx(commit).src/shared/types.ts—Settings.shortcutsfield, seeded inDEFAULT_SETTINGSfrom the registry.src/core/settings-store.ts— deep-merges.shortcutsexactly like.speech, so an existingsettings.jsonwithout the new key keeps the shipped defaults (new actions just appear).nav.ts,SettingsIcons.tsx,SettingsPage.tsx,nav.test.ts.The fix / design
The existing shared shortcut engine already abstracts platform variance via an
isMacflag —Cmdrenders and matches as⌘/metaKeyon macOS andCtrl/ctrlKeyon Windows/Linux, so one stored combo behaves and displays correctly on every OS. This is the same mechanism the already-shipped dictation chord uses. No new OS-specific code paths were added; each dispatch just passes itsisMacthrough the engine.Dictionary-style conflict detection: if a user binds a combo that already maps to another action, both rows show an inline conflict flag rather than silently overwriting.
Measured
tsconfig.node.jsonandtsconfig.web.json.shortcuts.test.tsround-trips every shipped default through parse → format → match → capture on both the macOS and Windows/Linux branches (pins punctuation combos likeCmd+,).ShortcutsSection.test.tsxcomponent test (jsdom).test/server/shortcuts-e2e.test.ts— a server-level E2E driving a settings round-trip through the real HTTP + WS-RPC stack, including a legacysettings.jsonmigration case.npm run build(electron-vite): ✓.Not in scope
Testing
npm run typecheck·npx vitest(5079 passed / 12 skipped / 0 failed) ·npm run build, plus a manual pass on a running Server Edition via the browser.References
matchesShortcut/formatShortcut) predates this PR and is the same one the dictation (Speech) setting uses.