feat(app): allow unbinding default keyboard shortcuts in settings#2183
feat(app): allow unbinding default keyboard shortcuts in settings#2183isundaylee wants to merge 2 commits into
Conversation
The shortcut engine already treated an empty override as unbound, but the settings pane offered no way to store one. Add an Unbind action per row, render the empty override as "Not set", and stop shortcut hint surfaces (tooltips, command center) from falling back to default keys when a binding is explicitly unbound. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
| Filename | Overview |
|---|---|
| packages/app/src/keyboard/keyboard-shortcuts.ts | Adds early-exit branch in buildEffectiveBindings for empty override string, producing an empty chord that the existing matcher already skips — correct and minimal change. |
| packages/app/src/screens/settings/keyboard-shortcuts-section.tsx | Adds Unbind action and 'Not set' display; displayChord derivation now correctly handles empty override and empty default row.keys; Unbind button guard displayChord.length > 0 correctly suppresses it when already unbound. |
| packages/app/src/hooks/use-shortcut-keys.ts | Switches from truthiness check to !== undefined + explicit empty-string test, correctly hiding shortcut hint when a shortcut is explicitly unbound. |
| packages/app/src/hooks/use-command-center.ts | Same truthiness-to-strict-equality fix as use-shortcut-keys.ts; command center now hides the shortcut hint for explicitly unbound actions. |
| packages/app/src/keyboard/keyboard-shortcuts.test.ts | Adds a well-scoped test: verifies empty override produces an empty chord via buildEffectiveBindings and that resolveKeyboardShortcut returns no match for the default combo — crosses the module interface cleanly. |
| packages/app/src/i18n/resources/en.ts | Adds unbound and actions.unbind keys to all eight locale files; parity test confirmed passing per PR description. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[User clicks Unbind] --> B[handleUnbind: setOverride bindingId, '']
B --> C[AsyncStorage stores empty override]
C --> D[buildEffectiveBindings]
D --> E{override === ''}
E -- yes --> F[parsedChord: empty array]
F --> H[Matcher skips binding - shortcut never fires]
E -- no --> G[parse override string]
C --> I[useShortcutKeys / resolveActionShortcutKeys]
I --> J{override !== undefined}
J -- yes --> K{override === ''}
K -- yes --> L[return null/undefined - hint hidden]
K -- no --> M[parse override for display]
C --> N[ShortcutRow displayChord]
N --> O{overrideCombo === ''}
O -- yes --> P[displayChord empty - show Not set - Unbind hidden, Reset shown]
O -- no --> Q[show parsed chord keys]
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A[User clicks Unbind] --> B[handleUnbind: setOverride bindingId, '']
B --> C[AsyncStorage stores empty override]
C --> D[buildEffectiveBindings]
D --> E{override === ''}
E -- yes --> F[parsedChord: empty array]
F --> H[Matcher skips binding - shortcut never fires]
E -- no --> G[parse override string]
C --> I[useShortcutKeys / resolveActionShortcutKeys]
I --> J{override !== undefined}
J -- yes --> K{override === ''}
K -- yes --> L[return null/undefined - hint hidden]
K -- no --> M[parse override for display]
C --> N[ShortcutRow displayChord]
N --> O{overrideCombo === ''}
O -- yes --> P[displayChord empty - show Not set - Unbind hidden, Reset shown]
O -- no --> Q[show parsed chord keys]
Reviews (2): Last reviewed commit: "Address review: strict empty-override ch..." | Re-trigger Greptile
…lt-unbound rows Use the same `override === ""` convention in buildEffectiveBindings as in the consuming hooks, and treat rows with no default keys as unbound in the settings display so Unbind cannot appear for a shortcut with no binding. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Linked issue
None — small settings-pane enhancement.
Type of change
What does this PR do
The keyboard shortcut settings pane only allowed rebinding a shortcut or resetting an override back to the default — there was no way to disable a shortcut that ships with a default binding.
Each bound shortcut row now has an Unbind action. It stores an empty override, the row shows "Not set", and the existing Reset action restores the default.
buildEffectiveBindingsmaps an empty override to an empty chord, which the matcher already skips, so an unbound shortcut simply never resolves.Shortcut hint surfaces (
useShortcutKeys, command center items) previously used truthiness checks on overrides and would have kept showing default key hints for an explicitly unbound shortcut; they now treat an empty override as unbound and hide the hint. The new "Not set" and "Unbind" labels are added to all eight locales.How did you verify it
npx vitest run packages/app/src/keyboard/keyboard-shortcuts.test.ts --bail=1— 74 passed.npx vitest run packages/app/src/i18n/resources.test.ts --bail=1— 32 passed (locale key parity).npm run typecheckpassed for all workspaces.npm run lintpassed with zero warnings.npm run formatran, including the pre-commit format check.Risk surface
Web/desktop only — the settings pane renders a mobile notice on native. Overrides are stored client-side in AsyncStorage; no protocol or persisted-daemon-data shapes change. An empty override on an older client build falls into the existing
parseChordStringcatch and is ignored, leaving the default binding active.Checklist
npm run typecheckpassesnpm run lintpassesnpm run formatran (Biome)