Skip to content

feat(app): allow unbinding default keyboard shortcuts in settings#2183

Open
isundaylee wants to merge 2 commits into
getpaseo:mainfrom
isundaylee:unbind-default-shortcuts
Open

feat(app): allow unbinding default keyboard shortcuts in settings#2183
isundaylee wants to merge 2 commits into
getpaseo:mainfrom
isundaylee:unbind-default-shortcuts

Conversation

@isundaylee

@isundaylee isundaylee commented Jul 18, 2026

Copy link
Copy Markdown

Linked issue

None — small settings-pane enhancement.

Type of change

  • Bug fix
  • New feature (with prior issue + design alignment)
  • Refactor / code improvement
  • Docs

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. buildEffectiveBindings maps 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

  • Added a unit test: an empty override produces an empty chord, and pressing the default combo (Ctrl+Shift+T, new terminal) no longer resolves a match.
  • 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 typecheck passed for all workspaces.
  • npm run lint passed with zero warnings.
  • npm run format ran, including the pre-commit format check.
  • Built and ran local app, tried unbinding "Open Project" - verified that Command+O no longer triggers that.

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 parseChordString catch and is ignored, leaving the default binding active.

Checklist

  • One focused change. Unrelated cleanups split out.
  • npm run typecheck passes
  • npm run lint passes
  • npm run format ran (Biome)
  • UI changes include screenshots or video for every affected platform
  • Tests added or updated where it made sense
Screenshot 2026-07-17 at 11 58 50 PM

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>
@isundaylee isundaylee changed the title feat(app): allow unbinding default keyboard shortcuts in settings Allow unbinding default keyboard shortcuts in settings Jul 18, 2026
@isundaylee isundaylee changed the title Allow unbinding default keyboard shortcuts in settings feat(app): allow unbinding default keyboard shortcuts in settings Jul 18, 2026
@greptile-apps

greptile-apps Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds an Unbind action to the keyboard shortcuts settings pane, letting users disable any shortcut that ships with a default binding. An empty string override is stored in AsyncStorage, buildEffectiveBindings maps it to an empty chord (which the matcher already skips), and shortcut hint surfaces (useShortcutKeys, command center) now treat override === "" as explicitly unbound and hide the hint.

  • buildEffectiveBindings gains a short early-exit for override === ""parsedChord: []; the existing truthiness checks in use-shortcut-keys and use-command-center are tightened to !== undefined + an explicit empty-string test, so the engine and the UI surfaces stay consistent.
  • The settings row's displayChord derivation is updated to return [] for an empty override (fixing a latent bug where the default keys would still display for an unbound shortcut) and also guards row.keys.length > 0 for shortcuts with no default binding.
  • New "Not set" / "Unbind" i18n keys are added to all eight locale files and verified by the existing locale-parity test.

Confidence Score: 5/5

Safe to merge — the change is narrowly scoped to the keyboard shortcut settings UI and its associated hint-display hooks, with no protocol or persisted-schema changes.

The engine change (empty chord → no match) is a single-branch addition in buildEffectiveBindings with a unit test confirming both the chord shape and the absence of a match. The hook changes replace truthiness checks with precise === "" guards, eliminating a latent bug where the default hint would still show for an unbound shortcut. The UI correctly gates the Unbind button on displayChord.length > 0, so it disappears once a shortcut is already unbound. Overrides are stored client-side only; an empty override on an older build falls into the existing parseChordString catch and the default binding remains active, so the fallback is safe.

No files require special attention.

Important Files Changed

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]
Loading
%%{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]
Loading

Reviews (2): Last reviewed commit: "Address review: strict empty-override ch..." | Re-trigger Greptile

Comment thread packages/app/src/keyboard/keyboard-shortcuts.ts Outdated
Comment thread packages/app/src/screens/settings/keyboard-shortcuts-section.tsx
…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>
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