import { useEffect, useRef, useState, type HTMLAttributes } from 'react' import { Sidebar } from './Sidebar' import { ContentRouter } from './ContentRouter' import { ToastContainer } from '../shared/Toast' import { UpdateChecker } from '../shared/UpdateChecker' import { useSettingsStore } from '../../stores/settingsStore' import { useUIStore, type SettingsTab } from '../../stores/uiStore' import { useKeyboardShortcuts } from '../../hooks/useKeyboardShortcuts' import { H5ConnectionRequiredError, initializeDesktopServerUrl, isH5ConnectionRequiredError, isTauriRuntime, } from '../../lib/desktopRuntime' import { TabBar } from './TabBar' import { StartupErrorView } from './StartupErrorView' import { useTabStore, SETTINGS_TAB_ID } from '../../stores/tabStore' import { useChatStore } from '../../stores/chatStore' import { useSessionStore } from '../../stores/sessionStore' import { useTranslation } from '../../i18n' import { H5ConnectionView } from './H5ConnectionView' import { useMobileViewport } from '../../hooks/useMobileViewport' import type { Tab } from '../../stores/tabStore' function isChatTab(tab: Tab | undefined) { return tab?.type === 'session' } export function AppShell() { const fetchSettings = useSettingsStore((s) => s.fetchAll) const sidebarOpen = useUIStore((s) => s.sidebarOpen) const toggleSidebar = useUIStore((s) => s.toggleSidebar) const setSidebarOpen = useUIStore((s) => s.setSidebarOpen) const [ready, setReady] = useState(false) const [startupError, setStartupError] = useState(null) const [h5StartupError, setH5StartupError] = useState(null) const [bootstrapNonce, setBootstrapNonce] = useState(0) const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false) const t = useTranslation() const tauriRuntime = isTauriRuntime() const isMobileShell = useMobileViewport() && !tauriRuntime const tabs = useTabStore((s) => s.tabs) const activeTabId = useTabStore((s) => s.activeTabId) const setActiveTab = useTabStore((s) => s.setActiveTab) const activeSession = useSessionStore((s) => activeTabId ? s.sessions.find((session) => session.id === activeTabId) ?? null : null, ) const wasMobileShellRef = useRef(false) const effectiveSidebarOpen = isMobileShell ? mobileSidebarOpen : sidebarOpen const activeTab = tabs.find((tab) => tab.sessionId === activeTabId) const isActiveChatTab = isChatTab(activeTab) const mobileSessionTitle = activeSession?.title || activeTab?.title || t('session.untitled') const mobileSessionUpdated = (() => { if (!activeSession?.modifiedAt) return '' const diff = Date.now() - new Date(activeSession.modifiedAt).getTime() if (diff < 60000) return t('session.timeJustNow') if (diff < 3600000) return t('session.timeMinutes', { n: Math.floor(diff / 60000) }) if (diff < 86400000) return t('session.timeHours', { n: Math.floor(diff / 3600000) }) return t('session.timeDays', { n: Math.floor(diff / 86400000) }) })() const sidebarHiddenProps: HTMLAttributes & { inert?: '' } = isMobileShell && !effectiveSidebarOpen ? { 'aria-hidden': true, inert: '' } : {} useEffect(() => { let cancelled = false const bootstrap = async () => { if (!cancelled) { setReady(false) setStartupError(null) setH5StartupError(null) } try { await initializeDesktopServerUrl() await fetchSettings() if (!cancelled) { setReady(true) } void (async () => { await useTabStore.getState().restoreTabs() if (cancelled) return const { activeTabId: activeId, tabs } = useTabStore.getState() const activeTab = tabs.find((tab) => tab.sessionId === activeId) if (activeId && activeTab?.type === 'session') { useChatStore.getState().connectToSession(activeId) } })().catch(() => {}) } catch (error) { if (!cancelled) { if (!tauriRuntime && isH5ConnectionRequiredError(error)) { setH5StartupError(error) setStartupError(null) } else { setStartupError(error instanceof Error ? error.message : String(error)) setH5StartupError(null) } setReady(false) } } } void bootstrap() return () => { cancelled = true } }, [bootstrapNonce, fetchSettings, tauriRuntime]) // Listen for macOS native menu navigation events (About / Settings) useEffect(() => { if (!tauriRuntime) return let unlisten: (() => void) | undefined import('@tauri-apps/api/event') .then(({ listen }) => listen('native-menu-navigate', (event) => { const target = event.payload as SettingsTab | 'settings' if (target === 'about') { useUIStore.getState().setPendingSettingsTab('about') } useTabStore.getState().openTab(SETTINGS_TAB_ID, 'Settings', 'settings') }), ) .then((fn) => { unlisten = fn }) .catch(() => {}) return () => { unlisten?.() } }, []) useKeyboardShortcuts() useEffect(() => { if (isMobileShell && !wasMobileShellRef.current) { setMobileSidebarOpen(false) setSidebarOpen(false) } if (!isMobileShell && wasMobileShellRef.current) { setMobileSidebarOpen(false) } wasMobileShellRef.current = isMobileShell }, [isMobileShell, setSidebarOpen]) useEffect(() => { if (!ready || !isMobileShell) return if (isChatTab(activeTab) || (!activeTab && !activeTabId)) return const nextChatTab = tabs.find(isChatTab) if (nextChatTab) { setActiveTab(nextChatTab.sessionId) return } useTabStore.setState({ activeTabId: null }) }, [activeTab, activeTabId, isMobileShell, ready, setActiveTab, tabs]) const setEffectiveSidebarOpen = (open: boolean) => { if (isMobileShell) { setMobileSidebarOpen(open) setSidebarOpen(open) return } setSidebarOpen(open) } const toggleEffectiveSidebar = () => { if (isMobileShell) { setEffectiveSidebarOpen(!mobileSidebarOpen) return } toggleSidebar() } if (!tauriRuntime && h5StartupError) { return ( setBootstrapNonce((value) => value + 1)} /> ) } if (startupError) { return } if (!ready) { return (
{t('app.launching')}
) } return (
{isMobileShell && effectiveSidebarOpen ? ( {isActiveChatTab ? (

{mobileSessionTitle}

{activeTab?.status === 'running' ? ( {t('session.active')} ) : null} {activeSession?.messageCount !== undefined && activeSession.messageCount > 0 ? ( <> {activeTab?.status === 'running' ? : null} {t('session.messages', { count: activeSession.messageCount })} ) : null} {mobileSessionUpdated ? ( <> {(activeTab?.status === 'running') || ((activeSession?.messageCount ?? 0) > 0) ? : null} {t('session.lastUpdated', { time: mobileSessionUpdated })} ) : null}
) : null}
) : null} {!isMobileShell ? : null} ) }