refactor(ui): register application shortcuts centrally
This commit is contained in:
@@ -33,11 +33,12 @@ import { useFeatureFlagsStore } from '@/stores/useFeatureFlagsStore';
|
|||||||
import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore';
|
import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore';
|
||||||
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
||||||
import { useDesktopWindowControlsLayout } from '@/hooks/useDesktopWindowControlsLayout';
|
import { useDesktopWindowControlsLayout } from '@/hooks/useDesktopWindowControlsLayout';
|
||||||
|
import { useKeybinds } from '@/hooks/useKeybind';
|
||||||
import { ContextUsageDisplay } from '@/components/ui/ContextUsageDisplay';
|
import { ContextUsageDisplay } from '@/components/ui/ContextUsageDisplay';
|
||||||
import { WindowsWindowControls } from '@/components/desktop/WindowsWindowControls';
|
import { WindowsWindowControls } from '@/components/desktop/WindowsWindowControls';
|
||||||
import { UpdateDialog } from '@/components/ui/UpdateDialog';
|
import { UpdateDialog } from '@/components/ui/UpdateDialog';
|
||||||
import { useDeviceInfo, useTabletStandalonePwaRuntime } from '@/lib/device';
|
import { useDeviceInfo, useTabletStandalonePwaRuntime } from '@/lib/device';
|
||||||
import { cn, hasModifier } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { McpDropdownContent } from '@/components/mcp/McpDropdown';
|
import { McpDropdownContent } from '@/components/mcp/McpDropdown';
|
||||||
import { McpIcon } from '@/components/icons/McpIcon';
|
import { McpIcon } from '@/components/icons/McpIcon';
|
||||||
import { ProviderLogo } from '@/components/ui/ProviderLogo';
|
import { ProviderLogo } from '@/components/ui/ProviderLogo';
|
||||||
@@ -46,7 +47,11 @@ import { UsageProgressBar } from '@/components/sections/usage/UsageProgressBar';
|
|||||||
import { PaceIndicator } from '@/components/sections/usage/PaceIndicator';
|
import { PaceIndicator } from '@/components/sections/usage/PaceIndicator';
|
||||||
import { updateDesktopSettings } from '@/lib/persistence';
|
import { updateDesktopSettings } from '@/lib/persistence';
|
||||||
import { formatTimeForPreference } from '@/lib/timeFormat';
|
import { formatTimeForPreference } from '@/lib/timeFormat';
|
||||||
import { eventMatchesShortcut, formatShortcutForDisplay, getEffectiveShortcutCombo } from '@/lib/shortcuts';
|
import {
|
||||||
|
formatShortcutForDisplay,
|
||||||
|
getEffectiveShortcutCombo,
|
||||||
|
type ShortcutActionId,
|
||||||
|
} from '@/lib/shortcuts';
|
||||||
import type { TimeFormatPreference } from '@/stores/useUIStore';
|
import type { TimeFormatPreference } from '@/stores/useUIStore';
|
||||||
import {
|
import {
|
||||||
getAllModelFamilies,
|
getAllModelFamilies,
|
||||||
@@ -285,7 +290,7 @@ type DesktopServicesMenuProps = {
|
|||||||
rateLimitGroups: RateLimitGroup[];
|
rateLimitGroups: RateLimitGroup[];
|
||||||
expandedFamilies: Record<string, string[]>;
|
expandedFamilies: Record<string, string[]>;
|
||||||
toggleFamilyExpanded: (providerId: string, familyId: string) => void;
|
toggleFamilyExpanded: (providerId: string, familyId: string) => void;
|
||||||
shortcutLabel: (actionId: string) => string;
|
shortcutLabel: (actionId: ShortcutActionId) => string;
|
||||||
showDevShutdown: boolean;
|
showDevShutdown: boolean;
|
||||||
isDevShutdownInFlight: boolean;
|
isDevShutdownInFlight: boolean;
|
||||||
onDevShutdown: () => Promise<void>;
|
onDevShutdown: () => Promise<void>;
|
||||||
@@ -1935,7 +1940,7 @@ export const Header: React.FC<HeaderProps> = ({
|
|||||||
return [];
|
return [];
|
||||||
}, [isMobile, showPlanTab, t]);
|
}, [isMobile, showPlanTab, t]);
|
||||||
|
|
||||||
const shortcutLabel = React.useCallback((actionId: string) => {
|
const shortcutLabel = React.useCallback((actionId: ShortcutActionId) => {
|
||||||
return formatShortcutForDisplay(getEffectiveShortcutCombo(actionId, shortcutOverrides));
|
return formatShortcutForDisplay(getEffectiveShortcutCombo(actionId, shortcutOverrides));
|
||||||
}, [shortcutOverrides]);
|
}, [shortcutOverrides]);
|
||||||
|
|
||||||
@@ -2043,82 +2048,53 @@ export const Header: React.FC<HeaderProps> = ({
|
|||||||
];
|
];
|
||||||
}, [t]);
|
}, [t]);
|
||||||
|
|
||||||
useEffect(() => {
|
const switchToIndexedTab = (index: number) => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const tab = tabs[index];
|
||||||
if (hasModifier(e) && !e.shiftKey && !e.altKey) {
|
if (!tab) return false;
|
||||||
const num = parseInt(e.key, 10);
|
if (isMobile) {
|
||||||
if (num >= 1 && num <= tabs.length) {
|
blurActiveElement();
|
||||||
e.preventDefault();
|
closeMobileHeaderPanels();
|
||||||
if (isMobile) {
|
}
|
||||||
blurActiveElement();
|
setActiveMainTab(tab.id);
|
||||||
closeMobileHeaderPanels();
|
};
|
||||||
}
|
|
||||||
setActiveMainTab(tabs[num - 1].id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
|
||||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
||||||
}, [blurActiveElement, closeMobileHeaderPanels, isMobile, setActiveMainTab, tabs]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useKeybinds({
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
switch_tab_1: () => switchToIndexedTab(0),
|
||||||
const toggleServicesCombo = getEffectiveShortcutCombo('toggle_services_menu', shortcutOverrides);
|
switch_tab_2: () => switchToIndexedTab(1),
|
||||||
if (eventMatchesShortcut(e, toggleServicesCombo)) {
|
switch_tab_3: () => switchToIndexedTab(2),
|
||||||
e.preventDefault();
|
switch_tab_4: () => switchToIndexedTab(3),
|
||||||
|
switch_tab_5: () => switchToIndexedTab(4),
|
||||||
if (isDesktopServicesOpen) {
|
switch_tab_6: () => switchToIndexedTab(5),
|
||||||
setIsDesktopServicesOpen(false);
|
switch_tab_7: () => switchToIndexedTab(6),
|
||||||
} else {
|
switch_tab_8: () => switchToIndexedTab(7),
|
||||||
setIsDesktopServicesOpen(true);
|
switch_tab_9: () => switchToIndexedTab(8),
|
||||||
void refreshCurrentInstanceLabel();
|
toggle_services_menu: () => {
|
||||||
if (desktopServicesTab === 'usage' && quotaResults.length === 0) {
|
if (isDesktopServicesOpen) {
|
||||||
void fetchAllQuotas();
|
setIsDesktopServicesOpen(false);
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
setIsDesktopServicesOpen(true);
|
||||||
const cycleServicesCombo = getEffectiveShortcutCombo('cycle_services_tab', shortcutOverrides);
|
void refreshCurrentInstanceLabel();
|
||||||
if (eventMatchesShortcut(e, cycleServicesCombo)) {
|
if (desktopServicesTab === 'usage' && quotaResults.length === 0) {
|
||||||
e.preventDefault();
|
void fetchAllQuotas();
|
||||||
|
|
||||||
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);
|
cycle_services_tab: () => {
|
||||||
if (eventMatchesShortcut(e, toggleContextPlanCombo)) {
|
const tabValues = servicesTabs.map((tab) => tab.value) as Array<'instance' | 'usage' | 'mcp'>;
|
||||||
e.preventDefault();
|
if (tabValues.length === 0) return false;
|
||||||
handleOpenContextPlan();
|
const currentIndex = tabValues.indexOf(desktopServicesTab);
|
||||||
|
const nextTab = tabValues[currentIndex === -1 ? 0 : (currentIndex + 1) % tabValues.length];
|
||||||
|
setDesktopServicesTab(nextTab);
|
||||||
|
setIsDesktopServicesOpen(true);
|
||||||
|
void refreshCurrentInstanceLabel();
|
||||||
|
if (nextTab === 'usage' && quotaResults.length === 0) {
|
||||||
|
void fetchAllQuotas();
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
toggle_context_plan: () => {
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
handleOpenContextPlan();
|
||||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
},
|
||||||
}, [
|
});
|
||||||
shortcutOverrides,
|
|
||||||
isDesktopServicesOpen,
|
|
||||||
desktopServicesTab,
|
|
||||||
servicesTabs,
|
|
||||||
quotaResults.length,
|
|
||||||
fetchAllQuotas,
|
|
||||||
refreshCurrentInstanceLabel,
|
|
||||||
handleOpenContextPlan,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const renderTab = (tab: TabConfig) => {
|
const renderTab = (tab: TabConfig) => {
|
||||||
const isActive = activeMainTab === tab.id;
|
const isActive = activeMainTab === tab.id;
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ import {
|
|||||||
import { useDebouncedValue } from '@/hooks/useDebouncedValue';
|
import { useDebouncedValue } from '@/hooks/useDebouncedValue';
|
||||||
import { useFileSearchStore } from '@/stores/useFileSearchStore';
|
import { useFileSearchStore } from '@/stores/useFileSearchStore';
|
||||||
import { useDeviceInfo } from '@/lib/device';
|
import { useDeviceInfo } from '@/lib/device';
|
||||||
import { cn, getModifierLabel, getRevealLabelKey, hasModifier } from '@/lib/utils';
|
import { cn, getModifierLabel, getRevealLabelKey } from '@/lib/utils';
|
||||||
import { getLanguageFromExtension, getImageMimeType, isBinaryFile, isDrawioFile, isImageFile, isPdfFile, isSvgFile, looksLikeBinaryText } from '@/lib/toolHelpers';
|
import { getLanguageFromExtension, getImageMimeType, isBinaryFile, isDrawioFile, isImageFile, isPdfFile, isSvgFile, looksLikeBinaryText } from '@/lib/toolHelpers';
|
||||||
import { shouldAllowFileDraftSave, shouldScheduleFileAutosave } from '@/lib/fileEditorAutosave';
|
import { shouldAllowFileDraftSave, shouldScheduleFileAutosave } from '@/lib/fileEditorAutosave';
|
||||||
import { getRuntimeUrlResolver } from '@/lib/runtime-url';
|
import { getRuntimeUrlResolver } from '@/lib/runtime-url';
|
||||||
@@ -52,6 +52,7 @@ import { getRuntimeApiBaseUrl } from '@/lib/runtime-switch';
|
|||||||
import { getOutsideFileGrant } from '@/lib/outsideFileGrants';
|
import { getOutsideFileGrant } from '@/lib/outsideFileGrants';
|
||||||
import { DiagramEditor } from '@/components/diagram';
|
import { DiagramEditor } from '@/components/diagram';
|
||||||
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
||||||
|
import { useKeybind, useKeybinds } from '@/hooks/useKeybind';
|
||||||
import { EditorView } from '@codemirror/view';
|
import { EditorView } from '@codemirror/view';
|
||||||
import type { Extension } from '@codemirror/state';
|
import type { Extension } from '@codemirror/state';
|
||||||
import { useThemeSystem } from '@/contexts/useThemeSystem';
|
import { useThemeSystem } from '@/contexts/useThemeSystem';
|
||||||
@@ -72,7 +73,6 @@ import { ensurePierreThemeRegistered } from '@/lib/shiki/appThemeRegistry';
|
|||||||
import { getDefaultTheme } from '@/lib/theme/themes';
|
import { getDefaultTheme } from '@/lib/theme/themes';
|
||||||
import { isBrowserClientRuntime, openDesktopFileInApp, openDesktopPath } from '@/lib/desktop';
|
import { isBrowserClientRuntime, openDesktopFileInApp, openDesktopPath } from '@/lib/desktop';
|
||||||
import { useOpenInAppsStore } from '@/stores/useOpenInAppsStore';
|
import { useOpenInAppsStore } from '@/stores/useOpenInAppsStore';
|
||||||
import { eventMatchesShortcut, getEffectiveShortcutCombo } from '@/lib/shortcuts';
|
|
||||||
import { useI18n } from '@/lib/i18n';
|
import { useI18n } from '@/lib/i18n';
|
||||||
import { sessionEvents } from '@/lib/sessionEvents';
|
import { sessionEvents } from '@/lib/sessionEvents';
|
||||||
|
|
||||||
@@ -1022,7 +1022,6 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
const setPendingFileNavigation = useUIStore((state) => state.setPendingFileNavigation);
|
const setPendingFileNavigation = useUIStore((state) => state.setPendingFileNavigation);
|
||||||
const pendingFileFocusPath = useUIStore((state) => state.pendingFileFocusPath);
|
const pendingFileFocusPath = useUIStore((state) => state.pendingFileFocusPath);
|
||||||
const setPendingFileFocusPath = useUIStore((state) => state.setPendingFileFocusPath);
|
const setPendingFileFocusPath = useUIStore((state) => state.setPendingFileFocusPath);
|
||||||
const shortcutOverrides = useUIStore((state) => state.shortcutOverrides);
|
|
||||||
const fileEditorKeymap = useUIStore((state) => state.fileEditorKeymap);
|
const fileEditorKeymap = useUIStore((state) => state.fileEditorKeymap);
|
||||||
const settingsDefaultFileViewerPreview = useConfigStore((state) => state.settingsDefaultFileViewerPreview);
|
const settingsDefaultFileViewerPreview = useConfigStore((state) => state.settingsDefaultFileViewerPreview);
|
||||||
const showMessageTTSButtons = useConfigStore((state) => state.showMessageTTSButtons);
|
const showMessageTTSButtons = useConfigStore((state) => state.showMessageTTSButtons);
|
||||||
@@ -1767,35 +1766,28 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
setAutoSaveStatus('idle');
|
setAutoSaveStatus('idle');
|
||||||
}, [selectedFile?.path]);
|
}, [selectedFile?.path]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
useKeybinds({
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
save_file: (event) => {
|
||||||
if (!hasModifier(e)) {
|
if (!(event.target instanceof Node) || !editorWrapperRef.current?.contains(event.target)) return false;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.key.toLowerCase() === 's') {
|
// Cancel pending auto-save because the explicit save should run immediately.
|
||||||
e.preventDefault();
|
if (autoSaveTimerRef.current) {
|
||||||
// Cancel pending auto-save; user wants immediate save
|
clearTimeout(autoSaveTimerRef.current);
|
||||||
if (autoSaveTimerRef.current) {
|
autoSaveTimerRef.current = null;
|
||||||
clearTimeout(autoSaveTimerRef.current);
|
|
||||||
autoSaveTimerRef.current = null;
|
|
||||||
}
|
|
||||||
if (!isSaving) {
|
|
||||||
void saveDraft().then((saved) => {
|
|
||||||
if (!saved) return;
|
|
||||||
setAutoSaveStatus('saved');
|
|
||||||
setTimeout(() => setAutoSaveStatus('idle'), 2000);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (e.key.toLowerCase() === 'f') {
|
|
||||||
e.preventDefault();
|
|
||||||
setIsSearchOpen(true);
|
|
||||||
}
|
}
|
||||||
};
|
if (!isSaving) {
|
||||||
|
void saveDraft().then((saved) => {
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
if (!saved) return;
|
||||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
setAutoSaveStatus('saved');
|
||||||
}, [isSaving, saveDraft]);
|
setTimeout(() => setAutoSaveStatus('idle'), 2000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
find_in_file: (event) => {
|
||||||
|
if (!(event.target instanceof Node) || !editorWrapperRef.current?.contains(event.target)) return false;
|
||||||
|
setIsSearchOpen(true);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const loadSelectedFile = React.useCallback(async (node: FileNode) => {
|
const loadSelectedFile = React.useCallback(async (node: FileNode) => {
|
||||||
const loadId = activeFileLoadIdRef.current + 1;
|
const loadId = activeFileLoadIdRef.current + 1;
|
||||||
@@ -2908,42 +2900,21 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
};
|
};
|
||||||
}, [isMobile, nudgeEditorSelectionAboveKeyboard]);
|
}, [isMobile, nudgeEditorSelectionAboveKeyboard]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
useKeybind('open_go_to_line', (event) => {
|
||||||
if (!canEdit || textViewMode !== 'edit' || isMobile) {
|
if (!canEdit || textViewMode !== 'edit' || isMobile) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const goToLineCombo = getEffectiveShortcutCombo('open_go_to_line', shortcutOverrides);
|
const target = event.target as Element | null;
|
||||||
|
if (target?.closest('[role="dialog"]')) return false;
|
||||||
|
if (!(target instanceof Node) || !editorWrapperRef.current?.contains(target)) return false;
|
||||||
|
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const isEditorTarget = Boolean(target?.closest('.cm-editor'));
|
||||||
const target = event.target as Element | null;
|
const isTypingTarget = Boolean(target?.closest('input, textarea, [contenteditable="true"], [role="textbox"]'));
|
||||||
if (target?.closest('[role="dialog"]')) {
|
if (isTypingTarget && !isEditorTarget) return false;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isEditorTarget = Boolean(target?.closest('.cm-editor'));
|
setIsGoToLineOpen(true);
|
||||||
const isTypingTarget = Boolean(
|
});
|
||||||
target?.closest('input, textarea, [contenteditable="true"], [role="textbox"]')
|
|
||||||
);
|
|
||||||
if (isTypingTarget && !isEditorTarget) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const activeElement = document.activeElement as Element | null;
|
|
||||||
const editorHasFocus = Boolean(activeElement?.closest('.cm-editor'));
|
|
||||||
if (!editorHasFocus) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventMatchesShortcut(event, goToLineCombo)) {
|
|
||||||
event.preventDefault();
|
|
||||||
setIsGoToLineOpen(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
|
||||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
||||||
}, [canEdit, isMobile, shortcutOverrides, textViewMode]);
|
|
||||||
|
|
||||||
const editorFontSize = useUIStore((state) => state.editorFontSize);
|
const editorFontSize = useUIStore((state) => state.editorFontSize);
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,102 +1,115 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { focusChatInput } from '@/components/chat/composer/editor/dom';
|
import { focusChatInput } from '@/components/chat/composer/editor/dom';
|
||||||
import { canUseElectronDesktopIPC, invokeDesktop } from '@/lib/desktop';
|
import { canUseElectronDesktopIPC, invokeDesktop } from '@/lib/desktop';
|
||||||
import { eventMatchesShortcut, getEffectiveShortcutCombo } from '@/lib/shortcuts';
|
import { ShortcutDispatcher } from '@/lib/shortcutDispatcher';
|
||||||
|
import { shortcutRegistry } from '@/lib/shortcutRegistry';
|
||||||
|
import { getEffectiveShortcutCombo } from '@/lib/shortcuts';
|
||||||
import { useConfigStore } from '@/stores/useConfigStore';
|
import { useConfigStore } from '@/stores/useConfigStore';
|
||||||
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
||||||
import { useProjectsStore } from '@/stores/useProjectsStore';
|
import { useProjectsStore } from '@/stores/useProjectsStore';
|
||||||
import { useUIStore } from '@/stores/useUIStore';
|
import { useUIStore } from '@/stores/useUIStore';
|
||||||
import { useSelectionStore } from '@/sync/selection-store';
|
import { useSelectionStore } from '@/sync/selection-store';
|
||||||
import { useSessionUIStore } from '@/sync/session-ui-store';
|
import { useSessionUIStore } from '@/sync/session-ui-store';
|
||||||
|
import { useKeybinds } from './useKeybind';
|
||||||
|
|
||||||
export const useMiniChatKeyboardShortcuts = () => {
|
export const useMiniChatKeyboardShortcuts = () => {
|
||||||
const shortcutOverrides = useUIStore((state) => state.shortcutOverrides);
|
|
||||||
const currentDirectory = useDirectoryStore((state) => state.currentDirectory);
|
const currentDirectory = useDirectoryStore((state) => state.currentDirectory);
|
||||||
const activeProject = useProjectsStore((state) => state.getActiveProject());
|
const activeProject = useProjectsStore((state) => state.getActiveProject());
|
||||||
const openNewSessionDraft = useSessionUIStore((state) => state.openNewSessionDraft);
|
const openNewSessionDraft = useSessionUIStore((state) => state.openNewSessionDraft);
|
||||||
|
const dispatcherRef = React.useRef<ShortcutDispatcher | null>(null);
|
||||||
|
|
||||||
|
if (!dispatcherRef.current) {
|
||||||
|
dispatcherRef.current = new ShortcutDispatcher({
|
||||||
|
registry: shortcutRegistry,
|
||||||
|
getBinding: (actionId) => getEffectiveShortcutCombo(
|
||||||
|
actionId,
|
||||||
|
useUIStore.getState().shortcutOverrides,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const dispatcher = dispatcherRef.current;
|
||||||
|
|
||||||
|
const cycleFavoriteModel = (delta: number): boolean | void => {
|
||||||
|
const { favoriteModels, addRecentModel } = useUIStore.getState();
|
||||||
|
if (favoriteModels.length === 0) return false;
|
||||||
|
|
||||||
|
const {
|
||||||
|
currentProviderId,
|
||||||
|
currentModelId,
|
||||||
|
setProvider,
|
||||||
|
setModel,
|
||||||
|
} = useConfigStore.getState();
|
||||||
|
const currentIndex = favoriteModels.findIndex(
|
||||||
|
(favorite) => favorite.providerID === currentProviderId && favorite.modelID === currentModelId,
|
||||||
|
);
|
||||||
|
const next = favoriteModels[(currentIndex + delta + favoriteModels.length) % favoriteModels.length];
|
||||||
|
setProvider(next.providerID);
|
||||||
|
setModel(next.modelID);
|
||||||
|
addRecentModel(next.providerID, next.modelID);
|
||||||
|
};
|
||||||
|
|
||||||
|
useKeybinds({
|
||||||
|
focus_input: () => {
|
||||||
|
focusChatInput();
|
||||||
|
},
|
||||||
|
new_mini_chat: () => {
|
||||||
|
if (!canUseElectronDesktopIPC()) return false;
|
||||||
|
void invokeDesktop('desktop_open_draft_mini_chat_window', {
|
||||||
|
directory: currentDirectory || activeProject?.path || '',
|
||||||
|
projectId: activeProject?.id ?? null,
|
||||||
|
})?.catch((error) => {
|
||||||
|
console.warn('[mini-chat-shortcuts] failed to open draft mini chat window', error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
new_chat: () => {
|
||||||
|
openNewSessionDraft({
|
||||||
|
selectedProjectId: activeProject?.id ?? null,
|
||||||
|
directoryOverride: currentDirectory || activeProject?.path || null,
|
||||||
|
preserveDirectoryOverride: Boolean(currentDirectory || activeProject?.path),
|
||||||
|
});
|
||||||
|
focusChatInput();
|
||||||
|
},
|
||||||
|
open_model_selector: () => {
|
||||||
|
const { isModelSelectorOpen, setModelSelectorOpen } = useUIStore.getState();
|
||||||
|
setModelSelectorOpen(!isModelSelectorOpen);
|
||||||
|
},
|
||||||
|
cycle_thinking_variant: () => {
|
||||||
|
const configState = useConfigStore.getState();
|
||||||
|
if (configState.getCurrentModelVariants().length === 0) return false;
|
||||||
|
|
||||||
|
configState.cycleCurrentVariant();
|
||||||
|
const sessionId = useSessionUIStore.getState().currentSessionId;
|
||||||
|
const {
|
||||||
|
currentVariant,
|
||||||
|
currentAgentName,
|
||||||
|
currentProviderId,
|
||||||
|
currentModelId,
|
||||||
|
} = useConfigStore.getState();
|
||||||
|
if (sessionId && currentAgentName && currentProviderId && currentModelId) {
|
||||||
|
useSelectionStore.getState().saveAgentModelVariantForSession(
|
||||||
|
sessionId,
|
||||||
|
currentAgentName,
|
||||||
|
currentProviderId,
|
||||||
|
currentModelId,
|
||||||
|
currentVariant,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cycle_favorite_model_forward: () => cycleFavoriteModel(1),
|
||||||
|
cycle_favorite_model_backward: () => cycleFavoriteModel(-1),
|
||||||
|
});
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const combo = (actionId: string) => getEffectiveShortcutCombo(actionId, shortcutOverrides);
|
|
||||||
|
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
if (eventMatchesShortcut(event, combo('focus_input'))) {
|
if (dispatcher.dispatch(event)) event.preventDefault();
|
||||||
event.preventDefault();
|
|
||||||
focusChatInput();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (canUseElectronDesktopIPC() && eventMatchesShortcut(event, combo('new_mini_chat'))) {
|
|
||||||
event.preventDefault();
|
|
||||||
void invokeDesktop('desktop_open_draft_mini_chat_window', {
|
|
||||||
directory: currentDirectory || activeProject?.path || '',
|
|
||||||
projectId: activeProject?.id ?? null,
|
|
||||||
})?.catch((error) => {
|
|
||||||
console.warn('[mini-chat-shortcuts] failed to open draft mini chat window', error);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventMatchesShortcut(event, combo('new_chat'))) {
|
|
||||||
event.preventDefault();
|
|
||||||
openNewSessionDraft({
|
|
||||||
selectedProjectId: activeProject?.id ?? null,
|
|
||||||
directoryOverride: currentDirectory || activeProject?.path || null,
|
|
||||||
preserveDirectoryOverride: Boolean(currentDirectory || activeProject?.path),
|
|
||||||
});
|
|
||||||
focusChatInput();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventMatchesShortcut(event, combo('open_model_selector'))) {
|
|
||||||
event.preventDefault();
|
|
||||||
const { isModelSelectorOpen, setModelSelectorOpen } = useUIStore.getState();
|
|
||||||
setModelSelectorOpen(!isModelSelectorOpen);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventMatchesShortcut(event, combo('cycle_thinking_variant'))) {
|
|
||||||
const configState = useConfigStore.getState();
|
|
||||||
const variants = configState.getCurrentModelVariants();
|
|
||||||
if (variants.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
configState.cycleCurrentVariant();
|
|
||||||
|
|
||||||
const nextVariant = useConfigStore.getState().currentVariant;
|
|
||||||
const sessionId = useSessionUIStore.getState().currentSessionId;
|
|
||||||
const agentName = useConfigStore.getState().currentAgentName;
|
|
||||||
const providerId = useConfigStore.getState().currentProviderId;
|
|
||||||
const modelId = useConfigStore.getState().currentModelId;
|
|
||||||
|
|
||||||
if (sessionId && agentName && providerId && modelId) {
|
|
||||||
useSelectionStore.getState().saveAgentModelVariantForSession(sessionId, agentName, providerId, modelId, nextVariant);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cyclesForward = eventMatchesShortcut(event, combo('cycle_favorite_model_forward'));
|
|
||||||
const cyclesBackward = eventMatchesShortcut(event, combo('cycle_favorite_model_backward'));
|
|
||||||
if (cyclesForward || cyclesBackward) {
|
|
||||||
const { favoriteModels, addRecentModel } = useUIStore.getState();
|
|
||||||
if (favoriteModels.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
const { currentProviderId, currentModelId, setProvider, setModel } = useConfigStore.getState();
|
|
||||||
const currentIndex = favoriteModels.findIndex((favorite) => favorite.providerID === currentProviderId && favorite.modelID === currentModelId);
|
|
||||||
const delta = cyclesForward ? 1 : -1;
|
|
||||||
const next = favoriteModels[(currentIndex + delta + favoriteModels.length) % favoriteModels.length];
|
|
||||||
|
|
||||||
setProvider(next.providerID);
|
|
||||||
setModel(next.modelID);
|
|
||||||
addRecentModel(next.providerID, next.modelID);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
const handleBlur = () => dispatcher.handleBlur();
|
||||||
|
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
window.addEventListener('blur', handleBlur);
|
||||||
}, [activeProject?.id, activeProject?.path, currentDirectory, openNewSessionDraft, shortcutOverrides]);
|
return () => {
|
||||||
|
window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
window.removeEventListener('blur', handleBlur);
|
||||||
|
};
|
||||||
|
}, [dispatcher]);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user