feat(desktop): shortcut keys settings panel#4202
Conversation
Add shortcuts.ts with ShortcutAction type, SHORTCUT_DEFAULTS mapping, localStorage persistence, formatKeyCombo, matchesShortcut, and useGlobalHotkey React hook.
Add ShortcutsSection component to the Settings center with: - Shortcut listing with labels and descriptions - Click-to-edit recording mode for custom key bindings - Conflict detection dialog when a combo is already assigned - Reset all to defaults button Registers 'shortcuts' tab in SettingsTab type and SETTINGS_TABS.
Replace hardcoded keybindings in ShellHotkeys and TextSizeHotkeys with useGlobalHotkey calls. Add NewSessionHotkeys (⌘N), TabHotkeys (⌘W), YoloToggleHotkeys (⌘Y), palette (⌘K), and nextUnread (⌘G) handlers.
Replace hardcoded hotkey listeners with useGlobalHotkey/matchesShortcut:
- ShellHotkeys: useGlobalHotkey("shortcuts.shellExpand")
- TextSizeHotkeys: three useGlobalHotkey calls for +/-/reset
- NewSessionHotkeys: new component, useGlobalHotkey("shortcuts.newSession")
- TabHotkeys: new component, useGlobalHotkey("shortcuts.closeTab") + Wails app:close-tab event
- YoloToggleHotkeys: new component, useGlobalHotkey("shortcuts.yoloToggle")
- Palette ⌘K: matchesShortcut("shortcuts.palette")
- Next unread ⌘G: matchesShortcut("shortcuts.nextUnread")
Fixes the blocking review issue: YoloToggleHotkeys was duplicating the toggle logic and bypassing local profile state (patchActiveComposerProfile, yoloRestoreToolApprovalModesRef). Now the component simply calls the existing toggleYoloApprovalMode callback which handles all state correctly. Also fixes: composerProfile.mode → toolApprovalMode (mode field does not exist on ComposerProfile).
Remove shortcuts.nextUnread from ShortcutAction, SHORTCUT_DEFAULTS, and i18n since the upstream TabMeta type does not have an 'unread' field. Can be re-added when backend support is available.
Previously settingsTabMeta('shortcuts') returned empty string,
so the sidebar showed no helper text under the Shortcuts tab.
Now it shows the pageDesc text, consistent with other tabs.
Remove background, border, border-radius, gap from .shortcuts-row. Use border-bottom divider between rows instead. Last row has no divider.
Move the reset-to-defaults control from a danger chip above the list to a RotateCcw icon button in the SettingsSection header actions area (right side of the title). Uses existing Tooltip for the label.
Move Cmd+K handler from useEffect with unstable deps into a dedicated PaletteHotkeys component using useGlobalHotkey, matching the pattern of all other shortcuts. Escape close stays as a stable empty-deps useEffect.
…de, capture - Remove orphaned shortcuts.cycleMode/cycleModeDesc i18n keys (en, zh) - PaletteHotkeys checks paletteOpen before triggering openPalette - SettingsPanel imports loadCustomShortcuts from shortcuts.ts, drops duplicate - useGlobalHotkey adds capture: true to survive stopPropagation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 68ba5101ef
ℹ️ 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".
|
|
||
| const confirmConflict = () => { | ||
| if (!conflict) return; | ||
| doCommit(conflict.pendingLabel, conflict.combo); |
There was a problem hiding this comment.
Remove the old binding when confirming a conflict
When a user confirms the conflict dialog, this only saves the new action's combo and leaves the existing action resolved to the same combo (either via customKeys or its default). In that state pressing the shortcut matches both registered global handlers, e.g. reassigning the palette to Ctrl+N still leaves New Session on Ctrl+N, so a single keypress opens the palette and creates a tab instead of truly reassigning the shortcut.
Useful? React with 👍 / 👎.
| "shortcuts.settings": { mac: "⌘,", win: "Ctrl+," }, | ||
| "shortcuts.shellExpand": { mac: "⌘B", win: "Ctrl+B" }, | ||
| "shortcuts.yoloToggle": { mac: "⌘Y", win: "Ctrl+Y" }, | ||
| "shortcuts.textSizeIncrease": { mac: "⌘=", win: "Ctrl+=" }, |
There was a problem hiding this comment.
Allow Ctrl++ to keep increasing text size
The previous text-size handler accepted both = and +, but the new exact shortcut matcher only defaults to Ctrl+=/⌘= and includes Shift in the formatted combo. On layouts where + is typed as Shift+=, the common Ctrl++/⌘++ shortcut now formats with Shift and no longer matches, so users lose the existing zoom-in keypath unless they press the less-common unshifted = shortcut.
Useful? React with 👍 / 👎.
| "shortcuts.newSession": { mac: "⌘N", win: "Ctrl+N" }, | ||
| "shortcuts.settings": { mac: "⌘,", win: "Ctrl+," }, | ||
| "shortcuts.shellExpand": { mac: "⌘B", win: "Ctrl+B" }, | ||
| "shortcuts.yoloToggle": { mac: "⌘Y", win: "Ctrl+Y" }, |
There was a problem hiding this comment.
Avoid stealing Ctrl+Y from text editing
On Windows/Linux, Ctrl+Y is the standard redo shortcut in text fields, but this new default is registered as a document-level hotkey whose handler calls preventDefault(). When focus is in the composer or another editable control, pressing redo will toggle YOLO mode instead of redoing text edits, so the default should avoid a common editing chord or skip global shortcuts for editable targets.
Useful? React with 👍 / 👎.
| if (e.key === "Escape") { setEditingKey(null); return; } | ||
| if (["Meta", "Control", "Alt", "Shift"].includes(e.key)) return; | ||
| const combo = formatKeyCombo(e, platform as "darwin" | "win"); | ||
| if (!combo) return; | ||
| commitCombo(labelKey, combo); |
There was a problem hiding this comment.
Require a modifier before saving global shortcuts
The recorder accepts any non-modifier key as a complete global shortcut, so a user can accidentally save a bare printable key such as N or K. Since the registered handlers listen on document and call preventDefault(), that assignment fires while typing in the composer and can create sessions/open panels instead of entering text; global shortcuts should reject bare character keys or require an explicit confirmation for them.
Useful? React with 👍 / 👎.
| <TabHotkeys | ||
| tabBarHidden={false} | ||
| activeTabId={activeTabId} | ||
| onCloseTab={(id) => void closeTab(id)} |
There was a problem hiding this comment.
Route close-tab shortcut through tab close handler
Closing a tab via the new shortcut bypasses handleTabClose, which is the path used by the tab UI to prune tabMetas/composer profile state, refresh tab metadata, and reveal the next active tab. In practice, pressing Cmd/Ctrl+W can close the backend tab but leave the closed tab rendered until the periodic metadata refresh catches up, so the shortcut should call the same handler as AppChrome.
Useful? React with 👍 / 👎.
| useGlobalHotkey("shortcuts.settings", (e) => { | ||
| e.preventDefault(); | ||
| onOpen(); | ||
| }, [onOpen]); |
There was a problem hiding this comment.
Honor settings shortcut changes in the macOS menu
This adds a customizable JS handler for opening Settings, but I checked desktop/menu.go and the macOS native Settings menu still has a hard-coded keys.CmdOrCtrl(",") accelerator that emits app:open-settings. After a user changes or reassigns the Settings shortcut, Cmd+, will still open Settings outside this matcher, and if Cmd+, is reassigned it can also trigger the new action.
Useful? React with 👍 / 👎.
1. conflict: clear old binding when user confirms reassign 2. textSize: add fallback for Ctrl+⇧+= (Ctrl++ on non-numpad) 3. yoloToggle: skip in editable elements to preserve Ctrl+Y redo 4. recorder: require at least one modifier key 5. closeTab: use handleTabClose instead of raw closeTab 6. menu.go: remove hardcoded Cmd+, accelerator for Settings
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6e0310a9d6
ℹ️ 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".
| handler(e); | ||
| } | ||
| }; | ||
| document.addEventListener("keydown", onKey, { capture: true }); |
There was a problem hiding this comment.
Suppress global hotkeys while recording shortcuts
Because the global handlers are installed in the capture phase, they run before the shortcuts page's <kbd onKeyDown> handler can call stopPropagation(). In the shortcuts settings page, pressing an already-bound combo while recording (for example Ctrl+W/⌘W to assign close-tab, or Ctrl+K/⌘K) executes the existing action—closing the current tab or opening the palette—before the recorder handles the key, so editing shortcuts can unexpectedly mutate app state.
Useful? React with 👍 / 👎.
| const confirmConflict = () => { | ||
| if (!conflict) return; | ||
| doCommit(conflict.pendingLabel, conflict.combo, conflict.conflictKey); | ||
| setConflict(null); |
There was a problem hiding this comment.
Clear default shortcut conflicts on reassignment
When the conflict is with another action's default binding, conflict.conflictKey is undefined, so confirming the reassign only saves the pending action and leaves the other action still resolving to the same default combo. For example, assigning Close tab to Ctrl+K and confirming leaves Palette at its default Ctrl+K too, so both handlers match the same keypress despite the conflict dialog saying it will reassign.
Useful? React with 👍 / 👎.
| if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === "=") { | ||
| e.preventDefault(); | ||
| applyTextSize(nextTextSize(getTextSize(), 1)); | ||
| } |
There was a problem hiding this comment.
Match the shifted plus key for text-size increase
On standard keyboards where + is produced by Shift+=, KeyboardEvent.key is "+", so Ctrl/Cmd+Shift+= no longer matches either the default shortcuts.textSizeIncrease combo or this fallback. The previous handler accepted e.key === "+", so users pressing the conventional Ctrl/Cmd++ zoom shortcut on a non-numpad keyboard now get no text-size change unless they know to press the unshifted = key.
Useful? React with 👍 / 👎.
| const tag = (e.target as HTMLElement)?.tagName; | ||
| if (tag === "INPUT" || tag === "TEXTAREA" || (e.target as HTMLElement)?.isContentEditable) return; | ||
| e.preventDefault(); |
There was a problem hiding this comment.
Honor custom YOLO shortcuts in the composer
When focus is in the composer textarea, this guard skips the configurable global shortcut and the composer still uses its hard-coded Ctrl/Cmd+Y check (isYoloToggleShortcut). After a user changes the YOLO shortcut, the new combo therefore does nothing while typing in the main composer, while Ctrl/Cmd+Y continues to toggle YOLO instead of behaving like redo.
Useful? React with 👍 / 👎.
…null safety, restore text-size fallback branches - ShellHotkeys: drop dead !shellExpand guard inside handler (enabled=false already skips registration) - ShellHotkeys: use shellExpand?.toggleLast() to fix TS null check - TextSizeHotkeys fallback: restore - and 0 handling; catch '+' key (some keyboards send it for numpad-plus)
|
Thanks @ttmouse for the shortcut settings work here. I integrated this direction into #4515 on top of the latest Authorship note: @ttmouse remains the primary author of the shortcut settings direction. @SivanCola contributed as a collaborator by integrating it with the current shortcut registry, folding in the related help/a11y direction from #3125, and verifying the final state. Closing this PR as superseded by #4515. |
新增快捷键设置面板(Shortcut Keys Settings Panel),包含:
核心库
lib/shortcuts.ts(新增):定义ShortcutAction类型、SHORTCUT_DEFAULTS默认映射、localStorage 持久化、formatKeyCombo/matchesShortcut/useGlobalHotkey全局快捷键集成
useGlobalHotkey模式:⌘K打开命令面板(PaletteHotkeys)⌘N新建会话(NewSessionHotkeys)⌘,打开设置(SettingsHotkeys)⌘W关闭标签页(TabHotkeys)⌘B展开/折叠 Shell(ShellHotkeys)⌘Y切换 YOLO 模式(YoloToggleHotkeys)⌘=/⌘-/⌘0调整文字大小(TextSizeHotkeys)useGlobalHotkey调用设置面板 UI
SettingsPanel.tsx新增ShortcutsSection组件RotateCcw图标按钮在标题栏右侧)i18n / CSS
Closes #