import React from 'react'; import { useSessionStore } from '@/stores/useSessionStore'; import { useUIStore } from '@/stores/useUIStore'; import { useConfigStore } from '@/stores/useConfigStore'; import { useThemeSystem } from '@/contexts/useThemeSystem'; import { useAssistantStatus } from '@/hooks/useAssistantStatus'; import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry'; export const useKeyboardShortcuts = () => { const { createSession, abortCurrentOperation, initializeNewOpenChamberSession, armAbortPrompt, clearAbortPrompt, currentSessionId } = useSessionStore(); const { toggleCommandPalette, toggleHelpDialog, toggleSidebar, setSessionSwitcherOpen, setSessionCreateDialogOpen, setActiveMainTab, setSettingsDialogOpen, } = useUIStore(); const { agents } = useConfigStore(); const { themeMode, setThemeMode } = useThemeSystem(); const { working } = useAssistantStatus(); const abortPrimedUntilRef = React.useRef(null); const abortPrimedTimeoutRef = React.useRef | null>(null); const isDownloadingLogsRef = React.useRef(false); const resetAbortPriming = React.useCallback(() => { if (abortPrimedTimeoutRef.current) { clearTimeout(abortPrimedTimeoutRef.current); abortPrimedTimeoutRef.current = null; } abortPrimedUntilRef.current = null; clearAbortPrompt(); }, [clearAbortPrompt]); React.useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.ctrlKey && e.key === 'x') { e.preventDefault(); toggleCommandPalette(); } if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 'l') { const runtimeAPIs = getRegisteredRuntimeAPIs(); const diagnostics = runtimeAPIs?.diagnostics; if (!diagnostics) { return; } e.preventDefault(); if (isDownloadingLogsRef.current) { return; } isDownloadingLogsRef.current = true; diagnostics .downloadLogs() .then(({ fileName, content }) => { const blob = new Blob([content], { type: 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const anchor = document.createElement('a'); anchor.href = url; anchor.download = fileName || 'desktop.log'; document.body.appendChild(anchor); anchor.click(); document.body.removeChild(anchor); URL.revokeObjectURL(url); }) .finally(() => { isDownloadingLogsRef.current = false; }); return; } if (e.ctrlKey && e.key === 'h') { e.preventDefault(); toggleHelpDialog(); } if (e.ctrlKey && !e.metaKey && e.key.toLowerCase() === 'n') { e.preventDefault(); if (e.shiftKey) { setSessionCreateDialogOpen(true); return; } createSession().then(session => { if (session) { initializeNewOpenChamberSession(session.id, agents); } }); } if ((e.metaKey || e.ctrlKey) && e.key === '/') { e.preventDefault(); const modes: Array<'light' | 'dark' | 'system'> = ['light', 'dark', 'system']; const currentIndex = modes.indexOf(themeMode); const nextIndex = (currentIndex + 1) % modes.length; setThemeMode(modes[nextIndex]); } if (e.ctrlKey && !e.metaKey && !e.shiftKey && e.key.toLowerCase() === 'g') { e.preventDefault(); const { activeMainTab } = useUIStore.getState(); setActiveMainTab(activeMainTab === 'git' ? 'chat' : 'git'); return; } if (e.ctrlKey && !e.metaKey && !e.shiftKey && e.key.toLowerCase() === 't') { e.preventDefault(); const { activeMainTab } = useUIStore.getState(); setActiveMainTab(activeMainTab === 'terminal' ? 'chat' : 'terminal'); return; } if (e.ctrlKey && !e.metaKey && !e.shiftKey && e.key === ',') { e.preventDefault(); const { isSettingsDialogOpen } = useUIStore.getState(); setSettingsDialogOpen(!isSettingsDialogOpen); return; } if (e.ctrlKey && !e.metaKey && !e.shiftKey && e.key.toLowerCase() === 'l') { e.preventDefault(); const { isMobile, isSessionSwitcherOpen } = useUIStore.getState(); if (isMobile) { setSessionSwitcherOpen(!isSessionSwitcherOpen); } else { toggleSidebar(); } return; } if (e.ctrlKey && !e.metaKey && !e.shiftKey && e.key.toLowerCase() === 'i') { e.preventDefault(); const textarea = document.querySelector('textarea[data-chat-input="true"]'); textarea?.focus(); return; } if (e.key === 'Escape') { const sessionId = currentSessionId; const canAbortNow = working.canAbort && Boolean(sessionId); if (!canAbortNow) { resetAbortPriming(); return; } const now = Date.now(); const primedUntil = abortPrimedUntilRef.current; if (primedUntil && now < primedUntil) { e.preventDefault(); resetAbortPriming(); void abortCurrentOperation(); return; } e.preventDefault(); const expiresAt = armAbortPrompt(3000) ?? now + 3000; abortPrimedUntilRef.current = expiresAt; if (abortPrimedTimeoutRef.current) { clearTimeout(abortPrimedTimeoutRef.current); } const delay = Math.max(expiresAt - now, 0); abortPrimedTimeoutRef.current = setTimeout(() => { if (abortPrimedUntilRef.current && Date.now() >= abortPrimedUntilRef.current) { resetAbortPriming(); } }, delay || 0); return; } }; window.addEventListener('keydown', handleKeyDown); return () => { window.removeEventListener('keydown', handleKeyDown); }; }, [ createSession, abortCurrentOperation, toggleCommandPalette, toggleHelpDialog, toggleSidebar, setSessionSwitcherOpen, setSessionCreateDialogOpen, setActiveMainTab, setSettingsDialogOpen, setThemeMode, themeMode, initializeNewOpenChamberSession, agents, working, armAbortPrompt, resetAbortPriming, currentSessionId, ]); React.useEffect(() => { return () => { resetAbortPriming(); }; }, [resetAbortPriming]); };