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:
committed by
GitHub
co-authored by
Bohdan Triapitsyn
parent
47cecfc356
commit
d9370f3af5
@@ -36,6 +36,7 @@ import { formatPercent, formatWindowLabel, QUOTA_PROVIDERS, calculatePace, calcu
|
||||
import { UsageProgressBar } from '@/components/sections/usage/UsageProgressBar';
|
||||
import { PaceIndicator } from '@/components/sections/usage/PaceIndicator';
|
||||
import { updateDesktopSettings } from '@/lib/persistence';
|
||||
import { eventMatchesShortcut, formatShortcutForDisplay, getEffectiveShortcutCombo } from '@/lib/shortcuts';
|
||||
import {
|
||||
getAllModelFamilies,
|
||||
getDisplayModelName,
|
||||
@@ -148,6 +149,7 @@ export const Header: React.FC = () => {
|
||||
const setSettingsDialogOpen = useUIStore((state) => state.setSettingsDialogOpen);
|
||||
const activeMainTab = useUIStore((state) => state.activeMainTab);
|
||||
const setActiveMainTab = useUIStore((state) => state.setActiveMainTab);
|
||||
const shortcutOverrides = useUIStore((state) => state.shortcutOverrides);
|
||||
|
||||
const { getCurrentModel } = useConfigStore();
|
||||
const runtimeApis = useRuntimeAPIs();
|
||||
@@ -1064,6 +1066,10 @@ export const Header: React.FC = () => {
|
||||
return base;
|
||||
}, [diffFileCount, isMobile, showPlanTab]);
|
||||
|
||||
const shortcutLabel = React.useCallback((actionId: string) => {
|
||||
return formatShortcutForDisplay(getEffectiveShortcutCombo(actionId, shortcutOverrides));
|
||||
}, [shortcutOverrides]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isMobile && (activeMainTab === 'git' || activeMainTab === 'terminal' || activeMainTab === 'diff' || activeMainTab === 'files')) {
|
||||
setActiveMainTab('chat');
|
||||
@@ -1114,6 +1120,65 @@ export const Header: React.FC = () => {
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [tabs, setActiveMainTab, showProjectTabs, projects, activeProjectId, setActiveProject]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
const toggleServicesCombo = getEffectiveShortcutCombo('toggle_services_menu', shortcutOverrides);
|
||||
if (eventMatchesShortcut(e, toggleServicesCombo)) {
|
||||
e.preventDefault();
|
||||
|
||||
if (isDesktopServicesOpen) {
|
||||
setIsDesktopServicesOpen(false);
|
||||
} else {
|
||||
setIsDesktopServicesOpen(true);
|
||||
void refreshCurrentInstanceLabel();
|
||||
if (desktopServicesTab === 'usage' && quotaResults.length === 0) {
|
||||
void fetchAllQuotas();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const cycleServicesCombo = getEffectiveShortcutCombo('cycle_services_tab', shortcutOverrides);
|
||||
if (eventMatchesShortcut(e, cycleServicesCombo)) {
|
||||
e.preventDefault();
|
||||
|
||||
const tabValues = servicesTabs.map((tab) => tab.value) as Array<'instance' | 'usage' | 'mcp'>;
|
||||
if (tabValues.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentIndex = tabValues.indexOf(desktopServicesTab);
|
||||
const nextIndex = currentIndex === -1 ? 0 : (currentIndex + 1) % tabValues.length;
|
||||
const nextTab = tabValues[nextIndex];
|
||||
setDesktopServicesTab(nextTab);
|
||||
setIsDesktopServicesOpen(true);
|
||||
void refreshCurrentInstanceLabel();
|
||||
if (nextTab === 'usage' && quotaResults.length === 0) {
|
||||
void fetchAllQuotas();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const toggleContextPlanCombo = getEffectiveShortcutCombo('toggle_context_plan', shortcutOverrides);
|
||||
if (eventMatchesShortcut(e, toggleContextPlanCombo)) {
|
||||
e.preventDefault();
|
||||
handleOpenContextPlan();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [
|
||||
shortcutOverrides,
|
||||
isDesktopServicesOpen,
|
||||
desktopServicesTab,
|
||||
servicesTabs,
|
||||
quotaResults.length,
|
||||
fetchAllQuotas,
|
||||
refreshCurrentInstanceLabel,
|
||||
handleOpenContextPlan,
|
||||
]);
|
||||
|
||||
const renderTab = (tab: TabConfig) => {
|
||||
const isActive = activeMainTab === tab.id;
|
||||
const isDiffTab = tab.icon === 'diff';
|
||||
@@ -1453,14 +1518,21 @@ export const Header: React.FC = () => {
|
||||
role="tablist"
|
||||
aria-label="Main navigation"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleOpenSessionSwitcher}
|
||||
aria-label="Open sessions"
|
||||
className={`${headerIconButtonClass} mr-2 shrink-0`}
|
||||
>
|
||||
<RiLayoutLeftLine className="h-5 w-5" />
|
||||
</button>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleOpenSessionSwitcher}
|
||||
aria-label="Open sessions"
|
||||
className={`${headerIconButtonClass} mr-2 shrink-0`}
|
||||
>
|
||||
<RiLayoutLeftLine className="h-5 w-5" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Open sessions ({shortcutLabel('toggle_sidebar')})</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
{/* Project tabs */}
|
||||
{showProjectTabs && (
|
||||
@@ -1683,7 +1755,7 @@ export const Header: React.FC = () => {
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Plan</p>
|
||||
<p>Plan ({shortcutLabel('toggle_context_plan')})</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
@@ -1721,7 +1793,9 @@ export const Header: React.FC = () => {
|
||||
</DropdownMenuTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{isDesktopApp ? `Current instance: ${currentInstanceLabel}` : 'Services'}</p>
|
||||
<p>
|
||||
{isDesktopApp ? `Current instance: ${currentInstanceLabel}` : 'Services'} ({shortcutLabel('toggle_services_menu')}; next tab {shortcutLabel('cycle_services_tab')})
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<DropdownMenuContent
|
||||
@@ -1954,7 +2028,7 @@ export const Header: React.FC = () => {
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Terminal panel</p>
|
||||
<p>Terminal panel ({shortcutLabel('toggle_terminal')})</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
@@ -1970,7 +2044,7 @@ export const Header: React.FC = () => {
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Right sidebar</p>
|
||||
<p>Right sidebar ({shortcutLabel('toggle_right_sidebar')})</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
@@ -2407,7 +2481,7 @@ export const Header: React.FC = () => {
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{updateAvailable ? 'Settings (Update available)' : 'Settings'}</p>
|
||||
<p>{updateAvailable ? `Settings (Update available) (${shortcutLabel('open_settings')})` : `Settings (${shortcutLabel('open_settings')})`}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user