Skip to content

feat: make keyboard shortcuts configurable - #31

Merged
nonlooped merged 2 commits into
mainfrom
feat/configurable-shortcuts
Jul 31, 2026
Merged

feat: make keyboard shortcuts configurable#31
nonlooped merged 2 commits into
mainfrom
feat/configurable-shortcuts

Conversation

@nonlooped

Copy link
Copy Markdown
Owner

Summary

  • Every shortcut in Settings > Keybindings can now be rebound in place: click a shortcut, press the new combo, Escape cancels.
  • Rebinding a combo already in use unbinds whoever held it, with a toast naming what got unbound.
  • Per-shortcut and "reset all" controls restore defaults. Overrides persist in NativePi's own state file (keybindingOverrides), separate from Pi's settings.
  • Tooltips that show a shortcut hint (composer, sidebar, header, transcript) now reflect the user's actual binding, not just the hardcoded default.

Test plan

  • bun test src/renderer/lib/shortcuts.test.ts src/renderer/lib/shortcutBindings.test.ts — registry, override, conflict, and keystroke-parsing behavior
  • bun test src/shared/rpc-schema.test.ts — new keybindingOverrides field defaults and corrupt-value handling
  • bun run typecheck — no new errors (one pre-existing, unrelated electron.vite.config.ts error from a vite/rolldown version mismatch)
  • Manual click-through of the Settings > Keybindings screen (not run in this environment)

@vercel

vercel Bot commented Jul 31, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
nativepi Ignored Ignored Preview Jul 31, 2026 10:01am

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 752e1746e9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

export function parseKeyEvent(event: KeyboardEvent): string | null {
if (MODIFIER_CODES.has(event.code)) return null;
const parts: string[] = [];
if (event.ctrlKey || event.metaKey) parts.push("$mod");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve Ctrl and Meta as distinct modifiers

On Windows, metaKey represents the Windows key, while $mod resolves to Ctrl, so recording Win+G saves and displays Ctrl+G and the combination the user actually pressed will not trigger the shortcut. Record Meta separately from Ctrl on non-macOS platforms rather than collapsing both flags into $mod.

AGENTS.md reference: AGENTS.md:L9-L10

Useful? React with 👍 / 👎.

Comment on lines +74 to +78
resetKeybinding: (id) => {
set((s) => {
const { [id]: _removed, ...rest } = s.keybindingOverrides;
return { keybindingOverrides: rest };
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Resolve conflicts when resetting one shortcut

If a user assigns Import chat's default combination to New chat, Import chat is unbound; clicking Import chat's reset button then removes that empty override and restores its default without checking that New chat still owns it. Both actions consequently advertise the same key, while bindings() retains only one handler for that map entry, so one shortcut silently stops working. Resetting an individual binding needs the same collision handling as assigning it.

Useful? React with 👍 / 👎.


cycleThinking: withProject(() => void cycleThinkingLevel()),
}),
}, keybindingOverrides),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Apply the input exception only to Escape

When Stop turn is rebound to a non-Escape combination and focus is in an HTMLInputElement, the existing guard at line 443 still suppresses the action because it was written specifically to let Escape clear or close fields. For example, rebinding Stop turn to Ctrl+X and pressing it in sidebar search edits the field instead of aborting the run; gate that exception on the pressed/effective Escape binding.

Useful? React with 👍 / 👎.

const openTerminal = useAppStore((s) => s.openTerminal);
const editorId = useAppStore((s) => s.preferences.preferredEditorId);
const searchFocusRequest = useAppStore((s) => s.searchFocusRequest);
const keybindingOverrides = useAppStore((s) => s.keybindingOverrides);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Use the override in the empty-sidebar hint

After New chat is rebound or displaced by a collision, the nested ChatList still renders hintFor("newChat") without overrides at line 410, so an empty project instructs the user to press the default key even though the global handler listens elsewhere. Have ChatList subscribe to the binding override when rendering this copy.

AGENTS.md reference: AGENTS.md:L125-L125

Useful? React with 👍 / 👎.

<button
type="button"
onClick={() => setRecordingId(recording ? null : shortcut.id)}
aria-label={`Change shortcut for ${shortcut.label}`}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Expose the current binding to assistive technology

For screen-reader users, this aria-label replaces the accessible name derived from the <Kbd> children, so every recorder is announced only as “Change shortcut for …” and its assigned, unassigned, or recording state is unavailable. Include the current binding and recording state in the accessible name or description so the editable shortcut list remains usable without sight.

AGENTS.md reference: AGENTS.md:L138-L138

Useful? React with 👍 / 👎.

@nonlooped
nonlooped merged commit 4f4b823 into main Jul 31, 2026
7 checks passed
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