import { useCallback, useEffect } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import Sidebar from '../sidebar/view/Sidebar'; import MainContent from '../main-content/view/MainContent'; import CommandPalette from '../command-palette/CommandPalette'; import { useWebSocket } from '../../contexts/WebSocketContext'; import { PaletteOpsProvider, usePaletteOpsRegister } from '../../contexts/PaletteOpsContext'; import { useDeviceSettings } from '../../hooks/useDeviceSettings'; import { useSessionProtection } from '../../hooks/useSessionProtection'; import { useProjectsState } from '../../hooks/useProjectsState'; import { api } from '../../utils/api'; type RunningSessionApiItem = { sessionId?: unknown; startedAt?: unknown; statusText?: unknown; canInterrupt?: unknown; }; type RunningSessionsApiPayload = { data?: { sessions?: RunningSessionApiItem[]; }; }; const parseStartedAt = (value: unknown): number | undefined => { if (typeof value === 'number' && Number.isFinite(value) && value > 0) { return value; } if (typeof value !== 'string') { return undefined; } const parsed = Date.parse(value); return Number.isFinite(parsed) ? parsed : undefined; }; export default function AppContent() { return ( ); } function AppContentInner() { const navigate = useNavigate(); const { sessionId } = useParams<{ sessionId?: string }>(); const { t } = useTranslation('common'); const { isMobile } = useDeviceSettings({ trackPWA: false }); const { ws, sendMessage, subscribe } = useWebSocket(); const { processingSessions, markSessionProcessing, markSessionIdle, syncProcessingSessions, } = useSessionProtection(); const { selectedProject, selectedSession, activeTab, sidebarOpen, isLoadingProjects, externalMessageUpdate, newSessionTrigger, setActiveTab, setSidebarOpen, setIsInputFocused, setShowSettings, openSettings, refreshProjectsSilently, registerOptimisticSession, sidebarSharedProps, handleNewSession, } = useProjectsState({ sessionId, navigate, subscribe, isMobile, activeSessions: processingSessions, }); const refreshRunningSessions = useCallback(async () => { try { const response = await api.runningSessions(); if (!response.ok) { return; } const payload = (await response.json()) as RunningSessionsApiPayload; const sessions = Array.isArray(payload.data?.sessions) ? payload.data.sessions : []; syncProcessingSessions( sessions .map((session) => { if (typeof session.sessionId !== 'string' || !session.sessionId) { return null; } return { sessionId: session.sessionId, startedAt: parseStartedAt(session.startedAt), statusText: typeof session.statusText === 'string' ? session.statusText : undefined, canInterrupt: typeof session.canInterrupt === 'boolean' ? session.canInterrupt : undefined, }; }) .filter((session): session is NonNullable => Boolean(session)), ); } catch (error) { console.error('[AppContent] Failed to sync running sessions:', error); } }, [syncProcessingSessions]); useEffect(() => { void refreshRunningSessions(); }, [refreshRunningSessions]); useEffect(() => { if (processingSessions.size === 0) { return; } const interval = window.setInterval(() => { void refreshRunningSessions(); }, 5000); return () => window.clearInterval(interval); }, [processingSessions.size, refreshRunningSessions]); usePaletteOpsRegister({ openSettings, refreshProjects: refreshProjectsSilently, }); useEffect(() => { if (typeof navigator === 'undefined' || !('serviceWorker' in navigator)) { return undefined; } const handleServiceWorkerMessage = (event: MessageEvent) => { const message = event.data; if (!message || message.type !== 'notification:navigate') { return; } if (typeof message.provider === 'string' && message.provider.trim()) { localStorage.setItem('selected-provider', message.provider); } setActiveTab('chat'); setSidebarOpen(false); void refreshProjectsSilently(); if (typeof message.sessionId === 'string' && message.sessionId) { navigate(`/session/${message.sessionId}`); return; } navigate('/'); }; navigator.serviceWorker.addEventListener('message', handleServiceWorkerMessage); return () => { navigator.serviceWorker.removeEventListener('message', handleServiceWorkerMessage); }; }, [navigate, refreshProjectsSilently, setActiveTab, setSidebarOpen]); // Pending tool permissions are recovered through the `chat.subscribe` flow: // the `chat_subscribed` ack carries them on session open and on reconnect, // so no separate permission-recovery message is needed here. // Adjust the app container to stay above the virtual keyboard on iOS Safari. // On Chrome for Android the layout viewport already shrinks when the keyboard opens, // so inset-0 adjusts automatically. On iOS the layout viewport stays full-height and // the keyboard overlays it — we use the Visual Viewport API to track keyboard height // and apply it as a CSS variable that shifts the container's bottom edge up. useEffect(() => { const vv = window.visualViewport; if (!vv) return; const update = () => { // Only resize matters — keyboard open/close changes vv.height. // Do NOT listen to scroll: on iOS Safari, scrolling content changes // vv.offsetTop which would make --keyboard-height fluctuate during // normal scrolling, causing the container to bounce up and down. const kb = Math.max(0, window.innerHeight - vv.height); document.documentElement.style.setProperty('--keyboard-height', `${kb}px`); }; vv.addEventListener('resize', update); return () => vv.removeEventListener('resize', update); }, []); return (
{!isMobile ? (
) : (
)}
setSidebarOpen(true)} isLoading={isLoadingProjects} onInputFocusChange={setIsInputFocused} onSessionProcessing={markSessionProcessing} onSessionIdle={markSessionIdle} processingSessions={processingSessions} onNavigateToSession={(targetSessionId: string, options) => navigate(`/session/${targetSessionId}`, { replace: Boolean(options?.replace) }) } onSessionEstablished={(targetSessionId, context) => registerOptimisticSession({ sessionId: targetSessionId, ...context }) } onShowSettings={() => setShowSettings(true)} externalMessageUpdate={externalMessageUpdate} newSessionTrigger={newSessionTrigger} />
openSettings()} onShowTab={setActiveTab} />
); }