import React, { useEffect } from 'react'; import { TooltipProvider } from '@renderer/components/ui/tooltip'; import { ConfirmDialog } from './components/common/ConfirmDialog'; import { ContextSwitchOverlay } from './components/common/ContextSwitchOverlay'; import { ErrorBoundary } from './components/common/ErrorBoundary'; import { TabbedLayout } from './components/layout/TabbedLayout'; import { type SplashSceneHandle, startSplashScene } from './components/splash/splashScene'; import { ToolApprovalSheet } from './components/team/ToolApprovalSheet'; import { useTheme } from './hooks/useTheme'; import { api } from './api'; import { useStore } from './store'; declare global { interface Window { __claudeTeamsSplashEnhancedStartedAt?: number; __claudeTeamsSplashScene?: SplashSceneHandle; __claudeTeamsSplashStartedAt?: number; } } const SPLASH_MIN_DURATION_MS = 1600; const SPLASH_ENHANCED_HOLD_MS = 600; const SPLASH_FADE_MS = 480; const SPLASH_REDUCED_MIN_DURATION_MS = 320; const SPLASH_REDUCED_HOLD_MS = 120; const SPLASH_REDUCED_FADE_MS = 180; const SPLASH_AVATAR_READY_MAX_WAIT_MS = 900; const SPLASH_REDUCED_AVATAR_READY_MAX_WAIT_MS = 160; export const App = (): React.JSX.Element => { // Initialize theme on app load useTheme(); // Upgrade the static preload splash, then dismiss it after the scene is visible. useEffect(() => { const splash = document.getElementById('splash'); if (splash) { const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; const scene = window.__claudeTeamsSplashScene ?? startSplashScene(splash, { reducedMotion }); const startedAt = window.__claudeTeamsSplashStartedAt ?? performance.now(); const enhancedStartedAt = window.__claudeTeamsSplashEnhancedStartedAt ?? performance.now(); const elapsed = performance.now() - startedAt; const enhancedElapsed = performance.now() - enhancedStartedAt; const minDuration = reducedMotion ? SPLASH_REDUCED_MIN_DURATION_MS : SPLASH_MIN_DURATION_MS; const enhancedHold = reducedMotion ? SPLASH_REDUCED_HOLD_MS : SPLASH_ENHANCED_HOLD_MS; const fadeDuration = reducedMotion ? SPLASH_REDUCED_FADE_MS : SPLASH_FADE_MS; const avatarReadyMaxWait = reducedMotion ? SPLASH_REDUCED_AVATAR_READY_MAX_WAIT_MS : SPLASH_AVATAR_READY_MAX_WAIT_MS; const exitDelay = Math.max(minDuration - elapsed, enhancedHold - enhancedElapsed, 0); let removeTimer: number | undefined; let avatarReadyTimer: number | undefined; let dismissed = false; const dismissSplash = (): void => { if (dismissed) return; dismissed = true; splash.classList.add('splash-exiting'); removeTimer = window.setTimeout(() => { scene.stop(); window.__claudeTeamsSplashScene = undefined; window.__claudeTeamsSplashEnhancedStartedAt = undefined; splash.remove(); }, fadeDuration); }; const exitTimer = window.setTimeout(() => { avatarReadyTimer = window.setTimeout(dismissSplash, avatarReadyMaxWait); void (scene.ready ?? Promise.resolve()).then(dismissSplash, dismissSplash); }, exitDelay); return () => { dismissed = true; window.clearTimeout(exitTimer); if (avatarReadyTimer !== undefined) { window.clearTimeout(avatarReadyTimer); } if (removeTimer !== undefined) { window.clearTimeout(removeTimer); } }; } return undefined; }, []); // Initialize context system lazily when SSH connection state changes. // Local-only users never pay the cost of IndexedDB init + context IPC calls. useEffect(() => { if (!api.ssh?.onStatus) return; const cleanup = api.ssh.onStatus(() => { void useStore.getState().initializeContextSystem(); void useStore.getState().fetchAvailableContexts(); }); return cleanup; }, []); return ( ); };