Skip to content

feat(desktop): shortcut keys settings panel#4202

Closed
ttmouse wants to merge 16 commits into
esengine:main-v2from
ttmouse:pr/shortcut-keys-settings
Closed

feat(desktop): shortcut keys settings panel#4202
ttmouse wants to merge 16 commits into
esengine:main-v2from
ttmouse:pr/shortcut-keys-settings

Conversation

@ttmouse

@ttmouse ttmouse commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

新增快捷键设置面板(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 组件
  • 快捷键列表展示(标签 + 说明 + 当前键位)
  • 点击进入录制模式 → 按下新组合键 → 冲突检测弹窗 → 保存至 localStorage
  • 重置为默认值(RotateCcw 图标按钮在标题栏右侧)
  • 分割线样式替代卡片框样式

i18n / CSS

  • 双语词条(en/zh 各 31 条)
  • 快捷键面板样式(分割线布局、录制态闪烁动画、冲突弹窗)

Closes #

ttmouse added 13 commits June 12, 2026 22:23
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.
@github-actions github-actions Bot added v2 Go rewrite (1.x) — main-v2 branch, active development desktop Wails desktop app (desktop/**) labels Jun 12, 2026
…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

@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: 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);

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 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+=" },

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 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" },

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 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 👍 / 👎.

Comment on lines +4408 to +4412
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);

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 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 👍 / 👎.

Comment thread desktop/frontend/src/App.tsx Outdated
<TabHotkeys
tabBarHidden={false}
activeTabId={activeTabId}
onCloseTab={(id) => void closeTab(id)}

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 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 👍 / 👎.

Comment on lines +640 to +643
useGlobalHotkey("shortcuts.settings", (e) => {
e.preventDefault();
onOpen();
}, [onOpen]);

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 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
@ttmouse

ttmouse commented Jun 13, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@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: 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 });

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 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 👍 / 👎.

Comment on lines +4385 to +4388
const confirmConflict = () => {
if (!conflict) return;
doCommit(conflict.pendingLabel, conflict.combo, conflict.conflictKey);
setConflict(null);

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 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 👍 / 👎.

Comment thread desktop/frontend/src/App.tsx Outdated
Comment on lines 621 to 624
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === "=") {
e.preventDefault();
applyTextSize(nextTextSize(getTextSize(), 1));
}

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 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 👍 / 👎.

Comment on lines +697 to +699
const tag = (e.target as HTMLElement)?.tagName;
if (tag === "INPUT" || tag === "TEXTAREA" || (e.target as HTMLElement)?.isContentEditable) return;
e.preventDefault();

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 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)
@SivanCola

Copy link
Copy Markdown
Collaborator

Thanks @ttmouse for the shortcut settings work here.

I integrated this direction into #4515 on top of the latest main-v2, using the existing keyboardShortcuts module as the single source of truth instead of adding a second shortcuts library. The new PR keeps the settings page direction from this PR and adds shared-registry help/docs/tests.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

desktop Wails desktop app (desktop/**) v2 Go rewrite (1.x) — main-v2 branch, active development

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants