feat(ui): land the centralized shortcuts core from #2532 with review fixes
The schema/config/bindings/registry/dispatcher module, useKeybind hooks, recording dialog, reworked shortcuts settings page, help dialog, and the localized action labels — re-based onto current main rather than merged (the branch predates 440+ commits including the session-tabs shortcuts). Review fixes applied on top of the original: - close_session_tab (alt+w) joins the schema with labels in every locale; it shipped on main after the PR's base and would otherwise silently die. - switch_context_surface's special-case in conflict resolution is now a declared prefixStyle config property instead of a magic id string. - Duplicate handler registration warns in dev builds. - The risky-browser-shortcut warning inspects every chord and covers mod+q/d/h/j/o/u plus mod+shift+w/q. - The dispatcher remembers which target armed a two-chord prefix so the window-level completion handler can distinguish a deliberate sequence from typing in an editable field (guard lands with the dispatch hook). - Schema tests: unique normalized default bindings enforced, and the flat-file-era override format proven to keep resolving.
This commit is contained in:
@@ -0,0 +1,286 @@
|
||||
import type { ShortcutCombo } from './bindings';
|
||||
|
||||
type ShortcutCategory = 'session' | 'models' | 'panels' | 'navigation' | 'application';
|
||||
|
||||
type ShortcutConfig = {
|
||||
id: string;
|
||||
defaultBinding: ShortcutCombo;
|
||||
allowsSequenceFallback?: true;
|
||||
/** The binding is a bare-modifier chord prefix (completed by another key);
|
||||
conflict resolution compares its prefix rather than a full combo. */
|
||||
prefixStyle?: true;
|
||||
} & (
|
||||
| { customizable: false }
|
||||
| {
|
||||
customizable: true;
|
||||
settingsLabelKey: `settings.openchamber.keyboardShortcuts.action.${string}.label`;
|
||||
}
|
||||
);
|
||||
|
||||
const SHORTCUT_GROUPS = {
|
||||
session: [
|
||||
{
|
||||
id: 'add_selection_to_chat',
|
||||
defaultBinding: 'mod+l',
|
||||
allowsSequenceFallback: true,
|
||||
customizable: true,
|
||||
settingsLabelKey:
|
||||
'settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label',
|
||||
},
|
||||
{
|
||||
id: 'focus_input',
|
||||
defaultBinding: 'mod+i',
|
||||
customizable: true,
|
||||
settingsLabelKey: 'settings.openchamber.keyboardShortcuts.action.focus_input.label',
|
||||
},
|
||||
{
|
||||
id: 'open_timeline_dialog',
|
||||
defaultBinding: 'mod+t',
|
||||
customizable: true,
|
||||
settingsLabelKey:
|
||||
'settings.openchamber.keyboardShortcuts.action.open_timeline_dialog.label',
|
||||
},
|
||||
{
|
||||
id: 'new_chat',
|
||||
defaultBinding: 'mod+n',
|
||||
customizable: true,
|
||||
settingsLabelKey: 'settings.openchamber.keyboardShortcuts.action.new_chat.label',
|
||||
},
|
||||
{
|
||||
id: 'close_session_tab',
|
||||
defaultBinding: 'alt+w',
|
||||
customizable: true,
|
||||
settingsLabelKey: 'settings.openchamber.keyboardShortcuts.action.close_session_tab.label',
|
||||
},
|
||||
{
|
||||
id: 'open_draft_project_picker',
|
||||
defaultBinding: 'mod+s p',
|
||||
customizable: true,
|
||||
settingsLabelKey:
|
||||
'settings.openchamber.keyboardShortcuts.action.open_draft_project_picker.label',
|
||||
},
|
||||
{
|
||||
id: 'open_draft_worktree_picker',
|
||||
defaultBinding: 'mod+s g',
|
||||
customizable: true,
|
||||
settingsLabelKey:
|
||||
'settings.openchamber.keyboardShortcuts.action.open_draft_worktree_picker.label',
|
||||
},
|
||||
{
|
||||
id: 'open_session_list',
|
||||
defaultBinding: 'mod+s l',
|
||||
customizable: true,
|
||||
settingsLabelKey:
|
||||
'settings.openchamber.keyboardShortcuts.action.open_session_list.label',
|
||||
},
|
||||
{
|
||||
id: 'new_chat_worktree',
|
||||
defaultBinding: 'mod+shift+n',
|
||||
customizable: true,
|
||||
settingsLabelKey:
|
||||
'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label',
|
||||
},
|
||||
{
|
||||
id: 'new_mini_chat',
|
||||
defaultBinding: 'mod+alt+n',
|
||||
customizable: true,
|
||||
settingsLabelKey: 'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label',
|
||||
},
|
||||
{
|
||||
id: 'expand_input',
|
||||
defaultBinding: 'mod+shift+e',
|
||||
customizable: true,
|
||||
settingsLabelKey: 'settings.openchamber.keyboardShortcuts.action.expand_input.label',
|
||||
},
|
||||
{
|
||||
id: 'toggle_dictation',
|
||||
defaultBinding: 'mod+alt+v',
|
||||
customizable: true,
|
||||
settingsLabelKey: 'settings.openchamber.keyboardShortcuts.action.toggle_dictation.label',
|
||||
},
|
||||
{ id: 'abort_run', defaultBinding: 'escape', customizable: false },
|
||||
],
|
||||
models: [
|
||||
{
|
||||
id: 'open_model_selector',
|
||||
defaultBinding: 'mod+shift+m',
|
||||
customizable: true,
|
||||
settingsLabelKey:
|
||||
'settings.openchamber.keyboardShortcuts.action.open_model_selector.label',
|
||||
},
|
||||
{ id: 'cycle_thinking_variant', defaultBinding: 'mod+shift+t', customizable: false },
|
||||
{
|
||||
id: 'cycle_agent',
|
||||
defaultBinding: 'tab',
|
||||
customizable: true,
|
||||
settingsLabelKey: 'settings.openchamber.keyboardShortcuts.action.cycle_agent.label',
|
||||
},
|
||||
{
|
||||
id: 'cycle_favorite_model_forward',
|
||||
defaultBinding: 'ctrl+]',
|
||||
customizable: true,
|
||||
settingsLabelKey:
|
||||
'settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_forward.label',
|
||||
},
|
||||
{
|
||||
id: 'cycle_favorite_model_backward',
|
||||
defaultBinding: 'ctrl+[',
|
||||
customizable: true,
|
||||
settingsLabelKey:
|
||||
'settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_backward.label',
|
||||
},
|
||||
],
|
||||
panels: [
|
||||
{
|
||||
id: 'toggle_terminal',
|
||||
defaultBinding: 'mod+j',
|
||||
customizable: true,
|
||||
settingsLabelKey: 'settings.openchamber.keyboardShortcuts.action.toggle_terminal.label',
|
||||
},
|
||||
{
|
||||
id: 'toggle_terminal_expanded',
|
||||
defaultBinding: 'mod+shift+j',
|
||||
customizable: true,
|
||||
settingsLabelKey:
|
||||
'settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label',
|
||||
},
|
||||
{
|
||||
id: 'toggle_sidebar',
|
||||
defaultBinding: 'mod+alt+l',
|
||||
customizable: true,
|
||||
settingsLabelKey: 'settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label',
|
||||
},
|
||||
{
|
||||
id: 'toggle_prompt_navigator',
|
||||
defaultBinding: 'mod+alt+p',
|
||||
customizable: true,
|
||||
settingsLabelKey:
|
||||
'settings.openchamber.keyboardShortcuts.action.toggle_prompt_navigator.label',
|
||||
},
|
||||
{
|
||||
id: 'toggle_right_sidebar',
|
||||
defaultBinding: 'mod+b',
|
||||
customizable: true,
|
||||
settingsLabelKey:
|
||||
'settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label',
|
||||
},
|
||||
{
|
||||
id: 'open_right_sidebar_git',
|
||||
defaultBinding: 'mod+shift+g',
|
||||
customizable: true,
|
||||
settingsLabelKey:
|
||||
'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_git.label',
|
||||
},
|
||||
{
|
||||
id: 'open_right_sidebar_files',
|
||||
defaultBinding: 'mod+shift+f',
|
||||
customizable: true,
|
||||
settingsLabelKey:
|
||||
'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_files.label',
|
||||
},
|
||||
{
|
||||
id: 'switch_context_surface',
|
||||
defaultBinding: 'mod',
|
||||
// The binding is a bare modifier acting as a chord prefix (completed by
|
||||
// a digit); conflict resolution must compare its PREFIX, not a combo.
|
||||
prefixStyle: true,
|
||||
customizable: true,
|
||||
settingsLabelKey:
|
||||
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label',
|
||||
},
|
||||
{
|
||||
id: 'toggle_context_plan',
|
||||
defaultBinding: 'mod+shift+p',
|
||||
customizable: true,
|
||||
settingsLabelKey:
|
||||
'settings.openchamber.keyboardShortcuts.action.toggle_context_plan.label',
|
||||
},
|
||||
{
|
||||
id: 'toggle_services_menu',
|
||||
defaultBinding: 'mod+shift+s',
|
||||
customizable: true,
|
||||
settingsLabelKey:
|
||||
'settings.openchamber.keyboardShortcuts.action.toggle_services_menu.label',
|
||||
},
|
||||
],
|
||||
navigation: [
|
||||
{ id: 'save_file', defaultBinding: 'mod+s', customizable: false, allowsSequenceFallback: true },
|
||||
{ id: 'find_in_file', defaultBinding: 'mod+f', customizable: false },
|
||||
{
|
||||
id: 'open_go_to_line',
|
||||
defaultBinding: 'alt+g',
|
||||
customizable: true,
|
||||
settingsLabelKey: 'settings.openchamber.keyboardShortcuts.action.open_go_to_line.label',
|
||||
},
|
||||
{
|
||||
id: 'cycle_services_tab',
|
||||
defaultBinding: 'mod+shift+[',
|
||||
customizable: true,
|
||||
settingsLabelKey:
|
||||
'settings.openchamber.keyboardShortcuts.action.cycle_services_tab.label',
|
||||
},
|
||||
{ id: 'switch_tab_1', defaultBinding: 'mod+1', customizable: false },
|
||||
// Mobile tab shortcuts may share numeric bindings with desktop-only panel commands.
|
||||
{ id: 'switch_tab_2', defaultBinding: 'mod+2', customizable: false },
|
||||
{ id: 'switch_tab_3', defaultBinding: 'mod+3', customizable: false },
|
||||
{ id: 'switch_tab_4', defaultBinding: 'mod+4', customizable: false },
|
||||
{ id: 'switch_tab_5', defaultBinding: 'mod+5', customizable: false },
|
||||
{ id: 'switch_tab_6', defaultBinding: 'mod+6', customizable: false },
|
||||
{ id: 'switch_tab_7', defaultBinding: 'mod+7', customizable: false },
|
||||
{ id: 'switch_tab_8', defaultBinding: 'mod+8', customizable: false },
|
||||
{ id: 'switch_tab_9', defaultBinding: 'mod+9', customizable: false },
|
||||
],
|
||||
application: [
|
||||
{
|
||||
id: 'open_command_palette',
|
||||
defaultBinding: 'mod+p',
|
||||
customizable: true,
|
||||
settingsLabelKey:
|
||||
'settings.openchamber.keyboardShortcuts.action.open_command_palette.label',
|
||||
},
|
||||
{ id: 'open_status', defaultBinding: 'mod+shift+o', customizable: false },
|
||||
{
|
||||
id: 'open_settings',
|
||||
defaultBinding: 'mod+comma',
|
||||
customizable: true,
|
||||
settingsLabelKey: 'settings.openchamber.keyboardShortcuts.action.open_settings.label',
|
||||
},
|
||||
{
|
||||
id: 'open_help',
|
||||
defaultBinding: 'mod+.',
|
||||
customizable: true,
|
||||
settingsLabelKey: 'settings.openchamber.keyboardShortcuts.action.open_help.label',
|
||||
},
|
||||
{
|
||||
id: 'cycle_theme',
|
||||
defaultBinding: 'mod+/',
|
||||
customizable: true,
|
||||
settingsLabelKey: 'settings.openchamber.keyboardShortcuts.action.cycle_theme.label',
|
||||
},
|
||||
{ id: 'toggle_memory_debug', defaultBinding: 'mod+shift+d', customizable: false },
|
||||
],
|
||||
} as const satisfies Record<ShortcutCategory, readonly ShortcutConfig[]>;
|
||||
|
||||
/** All application shortcuts, flattened in the same order used by Settings. */
|
||||
export const SHORTCUT_SCHEMA = [
|
||||
...SHORTCUT_GROUPS.session.map((shortcut) => ({
|
||||
...shortcut,
|
||||
category: 'session' as const,
|
||||
})),
|
||||
...SHORTCUT_GROUPS.models.map((shortcut) => ({
|
||||
...shortcut,
|
||||
category: 'models' as const,
|
||||
})),
|
||||
...SHORTCUT_GROUPS.panels.map((shortcut) => ({
|
||||
...shortcut,
|
||||
category: 'panels' as const,
|
||||
})),
|
||||
...SHORTCUT_GROUPS.navigation.map((shortcut) => ({
|
||||
...shortcut,
|
||||
category: 'navigation' as const,
|
||||
})),
|
||||
...SHORTCUT_GROUPS.application.map((shortcut) => ({
|
||||
...shortcut,
|
||||
category: 'application' as const,
|
||||
})),
|
||||
] as const;
|
||||
Reference in New Issue
Block a user