import { useStore } from '@nanostores/react' import { useQueryClient } from '@tanstack/react-query' import { lazy, Suspense, useCallback, useEffect, useRef } from 'react' import { Navigate, Route, Routes, useLocation, useNavigate, useParams } from 'react-router-dom' import { DesktopOnboardingOverlay } from '@/components/desktop-onboarding-overlay' import { Pane, PaneMain } from '@/components/pane-shell' import { useSkinCommand } from '@/themes/use-skin-command' import { formatRefValue } from '../components/assistant-ui/directive-text' import { getSessionMessages, listSessions } from '../hermes' import { toChatMessages } from '../lib/chat-messages' import { $pinnedSessionIds, FILE_BROWSER_DEFAULT_WIDTH, FILE_BROWSER_MAX_WIDTH, FILE_BROWSER_MIN_WIDTH, pinSession, SIDEBAR_DEFAULT_WIDTH, SIDEBAR_MAX_WIDTH, unpinSession } from '../store/layout' import { $filePreviewTarget, $previewTarget, closeActiveRightRailTab } from '../store/preview' import { $activeSessionId, $currentCwd, $freshDraftReady, $gatewayState, $selectedStoredSessionId, setAwaitingResponse, setBusy, setCurrentModel, setCurrentProvider, setMessages, setSessions, setSessionsLoading } from '../store/session' import { ChatView } from './chat' import { useComposerActions } from './chat/hooks/use-composer-actions' import { ChatPreviewRail, PREVIEW_RAIL_MAX_WIDTH, PREVIEW_RAIL_MIN_WIDTH, PREVIEW_RAIL_PANE_WIDTH } from './chat/right-rail' import { ChatSidebar } from './chat/sidebar' import { FileBrowserPane } from './file-browser' import { useGatewayBoot } from './gateway/hooks/use-gateway-boot' import { useGatewayRequest } from './gateway/hooks/use-gateway-request' import { ModelPickerOverlay } from './model-picker-overlay' import { NEW_CHAT_ROUTE, routeSessionId, sessionRoute } from './routes' import { useContextSuggestions } from './session/hooks/use-context-suggestions' import { useCwdActions } from './session/hooks/use-cwd-actions' import { useHermesConfig } from './session/hooks/use-hermes-config' import { useMessageStream } from './session/hooks/use-message-stream' import { useModelControls } from './session/hooks/use-model-controls' import { usePreviewRouting } from './session/hooks/use-preview-routing' import { usePromptActions } from './session/hooks/use-prompt-actions' import { useRouteResume } from './session/hooks/use-route-resume' import { useSessionActions } from './session/hooks/use-session-actions' import { useSessionStateCache } from './session/hooks/use-session-state-cache' import { AppShell } from './shell/app-shell' import { useOverlayRouting } from './shell/hooks/use-overlay-routing' import { useStatusSnapshot } from './shell/hooks/use-status-snapshot' import { useStatusbarItems } from './shell/hooks/use-statusbar-items' import type { StatusbarItem } from './shell/statusbar-controls' import type { TitlebarTool } from './shell/titlebar-controls' import { useGroupRegistry } from './shell/use-group-registry' const AgentsView = lazy(async () => ({ default: (await import('./agents')).AgentsView })) const ArtifactsView = lazy(async () => ({ default: (await import('./artifacts')).ArtifactsView })) const CommandCenterView = lazy(async () => ({ default: (await import('./command-center')).CommandCenterView })) const SettingsView = lazy(async () => ({ default: (await import('./settings')).SettingsView })) const SkillsView = lazy(async () => ({ default: (await import('./skills')).SkillsView })) export function DesktopController() { const queryClient = useQueryClient() const location = useLocation() const navigate = useNavigate() const busyRef = useRef(false) const creatingSessionRef = useRef(false) const refreshSessionsRequestRef = useRef(0) const gatewayState = useStore($gatewayState) const activeSessionId = useStore($activeSessionId) const currentCwd = useStore($currentCwd) const freshDraftReady = useStore($freshDraftReady) const filePreviewTarget = useStore($filePreviewTarget) const previewTarget = useStore($previewTarget) const selectedStoredSessionId = useStore($selectedStoredSessionId) const routedSessionId = routeSessionId(location.pathname) const routeToken = `${location.pathname}:${location.search}:${location.hash}` const routeTokenRef = useRef(routeToken) routeTokenRef.current = routeToken const getRouteToken = useCallback(() => routeTokenRef.current, []) const { agentsOpen, chatOpen, closeOverlayToPreviousRoute, commandCenterInitialSection, commandCenterOpen, currentView, openAgents, openCommandCenterSection, settingsOpen, toggleCommandCenter } = useOverlayRouting() const titlebarToolGroups = useGroupRegistry() const statusbarItemGroups = useGroupRegistry() const setTitlebarToolGroup = titlebarToolGroups.set const setStatusbarItemGroup = statusbarItemGroups.set const { activeSessionIdRef, ensureSessionState, runtimeIdByStoredSessionIdRef, selectedStoredSessionIdRef, sessionStateByRuntimeIdRef, syncSessionStateToView, updateSessionState } = useSessionStateCache({ activeSessionId, busyRef, selectedStoredSessionId, setAwaitingResponse, setBusy, setMessages }) const { connectionRef, gatewayRef, requestGateway } = useGatewayRequest() useEffect(() => { window.hermesDesktop?.setPreviewShortcutActive?.(Boolean(chatOpen && (filePreviewTarget || previewTarget))) }, [chatOpen, filePreviewTarget, previewTarget]) useEffect(() => { const onKeyDown = (event: KeyboardEvent) => { if (!$filePreviewTarget.get() && !$previewTarget.get()) { return } if ((event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey && event.key.toLowerCase() === 'w') { event.preventDefault() event.stopPropagation() closeActiveRightRailTab() } } const unsubscribe = window.hermesDesktop?.onClosePreviewRequested?.(closeActiveRightRailTab) window.addEventListener('keydown', onKeyDown, { capture: true }) return () => { unsubscribe?.() window.removeEventListener('keydown', onKeyDown, { capture: true }) } }, []) const refreshSessions = useCallback(async () => { const requestId = refreshSessionsRequestRef.current + 1 refreshSessionsRequestRef.current = requestId setSessionsLoading(true) try { const result = await listSessions(50) if (refreshSessionsRequestRef.current === requestId) { setSessions(result.sessions) } } finally { if (refreshSessionsRequestRef.current === requestId) { setSessionsLoading(false) } } }, []) const toggleSelectedPin = useCallback(() => { const sessionId = $selectedStoredSessionId.get() if (!sessionId) { return } if ($pinnedSessionIds.get().includes(sessionId)) { unpinSession(sessionId) } else { pinSession(sessionId) } }, []) const { gatewayLogLines, statusSnapshot } = useStatusSnapshot(gatewayState) const { browseSessionCwd, changeSessionCwd, refreshProjectBranch } = useCwdActions({ activeSessionId, activeSessionIdRef, currentCwd, requestGateway }) const { refreshHermesConfig, sttEnabled, voiceMaxRecordingSeconds } = useHermesConfig({ activeSessionIdRef, refreshProjectBranch }) const { refreshCurrentModel, selectModel, updateModelOptionsCache } = useModelControls({ activeSessionId, queryClient, requestGateway }) useContextSuggestions({ activeSessionId, activeSessionIdRef, currentCwd, gatewayState, requestGateway }) const hydrateFromStoredSession = useCallback( async ( attempts = 1, storedSessionId = selectedStoredSessionIdRef.current, runtimeSessionId = activeSessionIdRef.current ) => { if (!storedSessionId || !runtimeSessionId) { return } for (let index = 0; index < Math.max(1, attempts); index += 1) { try { const latest = await getSessionMessages(storedSessionId) updateSessionState( runtimeSessionId, state => ({ ...state, messages: toChatMessages(latest.messages) }), storedSessionId ) return } catch { // Best-effort fallback when live stream payloads are empty. } if (index < attempts - 1) { await new Promise(resolve => window.setTimeout(resolve, 250)) } } }, [activeSessionIdRef, selectedStoredSessionIdRef, updateSessionState] ) const { handleGatewayEvent } = useMessageStream({ activeSessionIdRef, hydrateFromStoredSession, queryClient, refreshHermesConfig, refreshSessions, updateSessionState }) const { handleDesktopGatewayEvent, restartPreviewServer } = usePreviewRouting({ activeSessionIdRef, baseHandleGatewayEvent: handleGatewayEvent, currentCwd, currentView, requestGateway, routedSessionId, selectedStoredSessionId }) const { branchCurrentSession, createBackendSessionForSend, openSettings, removeSession, resumeSession, selectSidebarItem, startFreshSessionDraft } = useSessionActions({ activeSessionId, activeSessionIdRef, busyRef, creatingSessionRef, ensureSessionState, getRouteToken, navigate, requestGateway, runtimeIdByStoredSessionIdRef, selectedStoredSessionId, selectedStoredSessionIdRef, sessionStateByRuntimeIdRef, syncSessionStateToView, updateSessionState }) const composer = useComposerActions({ activeSessionId, currentCwd, requestGateway }) const branchInNewChat = useCallback( async (messageId?: string) => { const branched = await branchCurrentSession(messageId) if (branched) { await refreshSessions().catch(() => undefined) } return branched }, [branchCurrentSession, refreshSessions] ) const handleSkinCommand = useSkinCommand() const { cancelRun, editMessage, handleThreadMessagesChange, reloadFromMessage, submitText, transcribeVoiceAudio } = usePromptActions({ activeSessionId, activeSessionIdRef, branchCurrentSession: branchInNewChat, busyRef, createBackendSessionForSend, handleSkinCommand, requestGateway, selectedStoredSessionIdRef, startFreshSessionDraft, sttEnabled, updateSessionState }) useGatewayBoot({ handleGatewayEvent: handleDesktopGatewayEvent, onConnectionReady: c => { connectionRef.current = c }, onGatewayReady: g => { gatewayRef.current = g }, refreshHermesConfig, refreshSessions }) useEffect(() => { if (gatewayState === 'open') { void refreshCurrentModel() void refreshSessions().catch(() => undefined) } }, [gatewayState, refreshCurrentModel, refreshSessions]) useRouteResume({ activeSessionId, activeSessionIdRef, creatingSessionRef, currentView, freshDraftReady, gatewayState, locationPathname: location.pathname, resumeSession, routedSessionId, runtimeIdByStoredSessionIdRef, selectedStoredSessionId, selectedStoredSessionIdRef, startFreshSessionDraft }) const { leftStatusbarItems, statusbarItems } = useStatusbarItems({ agentsOpen, browseSessionCwd, commandCenterOpen, extraLeftItems: statusbarItemGroups.flat.left, extraRightItems: statusbarItemGroups.flat.right, gatewayLogLines, openAgents, openCommandCenterSection, statusSnapshot, toggleCommandCenter }) const sidebar = ( void removeSession(sessionId)} onNavigate={selectSidebarItem} onRefreshSessions={() => void refreshSessions()} onResumeSession={sessionId => navigate(sessionRoute(sessionId))} /> ) const overlays = ( <> { void refreshHermesConfig() void refreshCurrentModel() void queryClient.invalidateQueries({ queryKey: ['model-options'] }) }} requestGateway={requestGateway} /> {settingsOpen && ( { void refreshHermesConfig() void refreshCurrentModel() void queryClient.invalidateQueries({ queryKey: ['model-options'] }) }} /> )} {commandCenterOpen && ( { setCurrentProvider(provider) setCurrentModel(model) updateModelOptionsCache(provider, model, true) void refreshCurrentModel() void queryClient.invalidateQueries({ queryKey: ['model-options'] }) }} onNavigateRoute={path => navigate(path)} onOpenSession={sessionId => navigate(sessionRoute(sessionId))} /> )} {agentsOpen && ( )} ) const chatView = ( composer.addContextRefAttachment(`@url:${formatRefValue(url)}`, url)} onAttachDroppedItems={composer.attachDroppedItems} onAttachImageBlob={composer.attachImageBlob} onBranchInNewChat={messageId => void branchInNewChat(messageId)} onCancel={() => void cancelRun()} onDeleteSelectedSession={() => { if (selectedStoredSessionId) { void removeSession(selectedStoredSessionId) } }} onEdit={editMessage} onPasteClipboardImage={() => void composer.pasteClipboardImage()} onPickFiles={() => void composer.pickContextPaths('file')} onPickFolders={() => void composer.pickContextPaths('folder')} onPickImages={() => void composer.pickImages()} onReload={reloadFromMessage} onRemoveAttachment={id => void composer.removeAttachment(id)} onSubmit={submitText} onThreadMessagesChange={handleThreadMessagesChange} onToggleSelectedPin={toggleSelectedPin} onTranscribeAudio={transcribeVoiceAudio} /> ) return ( {sidebar} } path="skills" /> } path="artifacts" /> } path="new" /> } path="sessions/:sessionId" /> } path="*" /> {chatOpen ? ( ) : null} ) } function LegacySessionRedirect() { const { sessionId } = useParams() return }