agent-ecosystem/src/renderer/App.tsx

110 lines
4.1 KiB
TypeScript

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 (
<ErrorBoundary>
<TooltipProvider delayDuration={150} skipDelayDuration={1500}>
<ContextSwitchOverlay />
<TabbedLayout />
<ConfirmDialog />
<ToolApprovalSheet />
</TooltipProvider>
</ErrorBoundary>
);
};