import { useEffect, useRef } 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'; 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, latestMessage, isConnected } = useWebSocket(); const wasConnectedRef = useRef(false); const { activeSessions, processingSessions, markSessionAsActive, markSessionAsInactive, markSessionAsProcessing, markSessionAsNotProcessing, replaceTemporarySession, } = useSessionProtection(); const { selectedProject, selectedSession, activeTab, sidebarOpen, isLoadingProjects, externalMessageUpdate, newSessionTrigger, setActiveTab, setSidebarOpen, setIsInputFocused, setShowSettings, openSettings, refreshProjectsSilently, sidebarSharedProps, handleNewSession, } = useProjectsState({ sessionId, navigate, latestMessage, isMobile, activeSessions, }); 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]); // Permission recovery: query pending permissions on WebSocket reconnect or session change useEffect(() => { const isReconnect = isConnected && !wasConnectedRef.current; if (isReconnect) { wasConnectedRef.current = true; } else if (!isConnected) { wasConnectedRef.current = false; } if (isConnected && selectedSession?.id) { sendMessage({ type: 'get-pending-permissions', sessionId: selectedSession.id }); } }, [isConnected, selectedSession?.id, sendMessage]); // 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} onSessionActive={markSessionAsActive} onSessionInactive={markSessionAsInactive} onSessionProcessing={markSessionAsProcessing} onSessionNotProcessing={markSessionAsNotProcessing} processingSessions={processingSessions} onReplaceTemporarySession={replaceTemporarySession} onNavigateToSession={(targetSessionId: string, options) => navigate(`/session/${targetSessionId}`, { replace: Boolean(options?.replace) }) } onShowSettings={() => setShowSettings(true)} externalMessageUpdate={externalMessageUpdate} newSessionTrigger={newSessionTrigger} />
openSettings()} onShowTab={setActiveTab} />
); }