Add customizable keyboard shortcuts and new panel/service bindings (#457)

* feat(shortcuts): centralize shortcut registry and matching

* feat(ui-store): persist customizable shortcut overrides

* feat(shortcuts): wire effective shortcuts into global handlers

* feat(help): render shortcut hints from effective mappings

* feat(command-palette): show and trigger panel shortcut actions

* feat(settings): add keyboard shortcuts customization section

* feat(settings): add shortcuts section to OpenChamber sidebar

* feat(settings): render keyboard shortcuts section content

* feat(header): add shortcut-driven services and plan controls

* fix(shortcuts): support unassigned overrides and bracket key matching

* fix(settings): clear overwritten shortcut conflicts safely

* fix: shortcut conflicts, focus retention, and theme-safe warning text

---------

Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
Nelson Pires
2026-02-20 15:40:25 +02:00
committed by GitHub
co-authored by Bohdan Triapitsyn
parent 47cecfc356
commit d9370f3af5
9 changed files with 1305 additions and 140 deletions
+45 -1
View File
@@ -3,6 +3,7 @@ import { devtools, persist, createJSONStorage } from 'zustand/middleware';
import type { SidebarSection } from '@/constants/sidebar';
import { getSafeStorage } from './utils/safeStorage';
import { SEMANTIC_TYPOGRAPHY, getTypographyVariable, type SemanticTypographyKey } from '@/lib/typography';
import type { ShortcutCombo } from '@/lib/shortcuts';
export type MainTab = 'chat' | 'plan' | 'git' | 'diff' | 'terminal' | 'files';
export type RightSidebarTab = 'git' | 'files';
@@ -214,6 +215,8 @@ interface UIStore {
persistChatDraft: boolean;
isMobileSessionStatusBarCollapsed: boolean;
shortcutOverrides: Record<string, ShortcutCombo>;
setTheme: (theme: 'light' | 'dark' | 'system') => void;
toggleSidebar: () => void;
setSidebarOpen: (open: boolean) => void;
@@ -297,6 +300,9 @@ interface UIStore {
setIsMobileSessionStatusBarCollapsed: (value: boolean) => void;
openMultiRunLauncher: () => void;
openMultiRunLauncherWithPrompt: (prompt: string) => void;
setShortcutOverride: (actionId: string, combo: ShortcutCombo) => void;
clearShortcutOverride: (actionId: string) => void;
resetAllShortcutOverrides: () => void;
}
@@ -384,6 +390,7 @@ export const useUIStore = create<UIStore>()(
showTerminalQuickKeysOnDesktop: false,
persistChatDraft: true,
isMobileSessionStatusBarCollapsed: false,
shortcutOverrides: {},
setTheme: (theme) => {
set({ theme });
@@ -1103,11 +1110,32 @@ export const useUIStore = create<UIStore>()(
setIsMobileSessionStatusBarCollapsed: (value) => {
set({ isMobileSessionStatusBarCollapsed: value });
},
setShortcutOverride: (actionId, combo) => {
set((state) => ({
shortcutOverrides: {
...state.shortcutOverrides,
[actionId]: combo,
},
}));
},
clearShortcutOverride: (actionId) => {
set((state) => {
const rest = { ...state.shortcutOverrides };
delete rest[actionId];
return { shortcutOverrides: rest };
});
},
resetAllShortcutOverrides: () => {
set({ shortcutOverrides: {} });
},
}),
{
name: 'ui-store',
storage: createJSONStorage(() => getSafeStorage()),
version: 4,
version: 5,
migrate: (persistedState, version) => {
if (!persistedState || typeof persistedState !== 'object') {
return persistedState;
@@ -1155,6 +1183,21 @@ export const useUIStore = create<UIStore>()(
state.contextPanelByDirectory = {};
}
if (version < 5) {
if (!state.shortcutOverrides || typeof state.shortcutOverrides !== 'object') {
state.shortcutOverrides = {};
} else {
const overrides = state.shortcutOverrides as Record<string, unknown>;
const cleaned: Record<string, string> = {};
for (const [key, value] of Object.entries(overrides)) {
if (typeof key === 'string' && typeof value === 'string') {
cleaned[key] = value;
}
}
state.shortcutOverrides = cleaned;
}
}
return state;
},
partialize: (state) => ({
@@ -1205,6 +1248,7 @@ export const useUIStore = create<UIStore>()(
maxLastMessageLength: state.maxLastMessageLength,
persistChatDraft: state.persistChatDraft,
isMobileSessionStatusBarCollapsed: state.isMobileSessionStatusBarCollapsed,
shortcutOverrides: state.shortcutOverrides,
})
}
),