2025-12-07 19:32:53 +02:00
|
|
|
import React from 'react';
|
|
|
|
|
import { MainLayout } from '@/components/layout/MainLayout';
|
2025-12-13 16:34:17 +02:00
|
|
|
import { VSCodeLayout } from '@/components/layout/VSCodeLayout';
|
2026-01-03 22:11:16 +01:00
|
|
|
import { AgentManagerView } from '@/components/views/agent-manager';
|
2025-12-07 19:32:53 +02:00
|
|
|
import { FireworksProvider } from '@/contexts/FireworksContext';
|
|
|
|
|
import { Toaster } from '@/components/ui/sonner';
|
|
|
|
|
import { MemoryDebugPanel } from '@/components/ui/MemoryDebugPanel';
|
|
|
|
|
import { ErrorBoundary } from '@/components/ui/ErrorBoundary';
|
|
|
|
|
import { useEventStream } from '@/hooks/useEventStream';
|
|
|
|
|
import { useKeyboardShortcuts } from '@/hooks/useKeyboardShortcuts';
|
2025-12-19 17:57:50 +02:00
|
|
|
import { useMenuActions } from '@/hooks/useMenuActions';
|
2025-12-07 19:32:53 +02:00
|
|
|
import { useSessionStatusBootstrap } from '@/hooks/useSessionStatusBootstrap';
|
2025-12-20 02:06:00 +02:00
|
|
|
import { useSessionAutoCleanup } from '@/hooks/useSessionAutoCleanup';
|
2026-01-22 11:52:22 -08:00
|
|
|
import { useRouter } from '@/hooks/useRouter';
|
2026-01-22 01:19:24 +02:00
|
|
|
import { usePushVisibilityBeacon } from '@/hooks/usePushVisibilityBeacon';
|
2025-12-07 19:32:53 +02:00
|
|
|
import { GitPollingProvider } from '@/hooks/useGitPolling';
|
|
|
|
|
import { useConfigStore } from '@/stores/useConfigStore';
|
2026-01-03 01:18:57 +01:00
|
|
|
import { hasModifier } from '@/lib/utils';
|
2025-12-07 19:32:53 +02:00
|
|
|
import { useSessionStore } from '@/stores/useSessionStore';
|
|
|
|
|
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
|
|
|
|
import { opencodeClient } from '@/lib/opencode/client';
|
|
|
|
|
import { useFontPreferences } from '@/hooks/useFontPreferences';
|
|
|
|
|
import { CODE_FONT_OPTION_MAP, DEFAULT_MONO_FONT, DEFAULT_UI_FONT, UI_FONT_OPTION_MAP } from '@/lib/fontOptions';
|
|
|
|
|
import { ConfigUpdateOverlay } from '@/components/ui/ConfigUpdateOverlay';
|
2025-12-19 18:59:08 +02:00
|
|
|
import { AboutDialog } from '@/components/ui/AboutDialog';
|
2025-12-07 19:32:53 +02:00
|
|
|
import { RuntimeAPIProvider } from '@/contexts/RuntimeAPIProvider';
|
|
|
|
|
import { registerRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
|
|
|
|
|
import { OnboardingScreen } from '@/components/onboarding/OnboardingScreen';
|
|
|
|
|
import { isCliAvailable } from '@/lib/desktop';
|
2025-12-19 18:59:08 +02:00
|
|
|
import { useUIStore } from '@/stores/useUIStore';
|
2026-01-26 11:28:55 +02:00
|
|
|
import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore';
|
2025-12-07 19:32:53 +02:00
|
|
|
import type { RuntimeAPIs } from '@/lib/api/types';
|
|
|
|
|
|
2025-12-19 18:59:08 +02:00
|
|
|
const AboutDialogWrapper: React.FC = () => {
|
|
|
|
|
const { isAboutDialogOpen, setAboutDialogOpen } = useUIStore();
|
|
|
|
|
return (
|
|
|
|
|
<AboutDialog
|
|
|
|
|
open={isAboutDialogOpen}
|
|
|
|
|
onOpenChange={setAboutDialogOpen}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-07 19:32:53 +02:00
|
|
|
type AppProps = {
|
|
|
|
|
apis: RuntimeAPIs;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function App({ apis }: AppProps) {
|
2026-01-06 21:31:04 +02:00
|
|
|
const { initializeApp, isInitialized, isConnected } = useConfigStore();
|
2025-12-07 19:32:53 +02:00
|
|
|
const { error, clearError, loadSessions } = useSessionStore();
|
|
|
|
|
const currentDirectory = useDirectoryStore((state) => state.currentDirectory);
|
|
|
|
|
const isSwitchingDirectory = useDirectoryStore((state) => state.isSwitchingDirectory);
|
|
|
|
|
const [showMemoryDebug, setShowMemoryDebug] = React.useState(false);
|
|
|
|
|
const { uiFont, monoFont } = useFontPreferences();
|
2026-01-26 11:28:55 +02:00
|
|
|
const refreshGitHubAuthStatus = useGitHubAuthStore((state) => state.refreshStatus);
|
2025-12-07 19:32:53 +02:00
|
|
|
const [isDesktopRuntime, setIsDesktopRuntime] = React.useState<boolean>(() => apis.runtime.isDesktop);
|
2025-12-13 16:34:17 +02:00
|
|
|
const [isVSCodeRuntime, setIsVSCodeRuntime] = React.useState<boolean>(() => apis.runtime.isVSCode);
|
2025-12-07 19:32:53 +02:00
|
|
|
const [cliAvailable, setCliAvailable] = React.useState<boolean>(() => {
|
|
|
|
|
if (!apis.runtime.isDesktop) return true;
|
|
|
|
|
return isCliAvailable();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
setIsDesktopRuntime(apis.runtime.isDesktop);
|
2025-12-13 16:34:17 +02:00
|
|
|
setIsVSCodeRuntime(apis.runtime.isVSCode);
|
|
|
|
|
}, [apis.runtime.isDesktop, apis.runtime.isVSCode]);
|
2025-12-07 19:32:53 +02:00
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
registerRuntimeAPIs(apis);
|
|
|
|
|
return () => registerRuntimeAPIs(null);
|
|
|
|
|
}, [apis]);
|
|
|
|
|
|
2026-01-26 11:28:55 +02:00
|
|
|
React.useEffect(() => {
|
|
|
|
|
void refreshGitHubAuthStatus(apis.github, { force: true });
|
|
|
|
|
}, [apis.github, refreshGitHubAuthStatus]);
|
|
|
|
|
|
2025-12-07 19:32:53 +02:00
|
|
|
React.useEffect(() => {
|
|
|
|
|
if (typeof document === 'undefined') {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const root = document.documentElement;
|
|
|
|
|
const uiStack = UI_FONT_OPTION_MAP[uiFont]?.stack ?? UI_FONT_OPTION_MAP[DEFAULT_UI_FONT].stack;
|
|
|
|
|
const monoStack = CODE_FONT_OPTION_MAP[monoFont]?.stack ?? CODE_FONT_OPTION_MAP[DEFAULT_MONO_FONT].stack;
|
|
|
|
|
|
|
|
|
|
root.style.setProperty('--font-sans', uiStack);
|
|
|
|
|
root.style.setProperty('--font-heading', uiStack);
|
|
|
|
|
root.style.setProperty('--font-family-sans', uiStack);
|
|
|
|
|
root.style.setProperty('--font-mono', monoStack);
|
|
|
|
|
root.style.setProperty('--font-family-mono', monoStack);
|
|
|
|
|
root.style.setProperty('--ui-regular-font-weight', '400');
|
|
|
|
|
|
|
|
|
|
if (document.body) {
|
|
|
|
|
document.body.style.fontFamily = uiStack;
|
|
|
|
|
}
|
|
|
|
|
}, [uiFont, monoFont]);
|
|
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
if (isInitialized) {
|
|
|
|
|
const hideInitialLoading = () => {
|
|
|
|
|
const loadingElement = document.getElementById('initial-loading');
|
|
|
|
|
if (loadingElement) {
|
|
|
|
|
loadingElement.classList.add('fade-out');
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
loadingElement.remove();
|
|
|
|
|
}, 300);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const timer = setTimeout(hideInitialLoading, 150);
|
|
|
|
|
return () => clearTimeout(timer);
|
|
|
|
|
}
|
|
|
|
|
}, [isInitialized]);
|
|
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
const fallbackTimer = setTimeout(() => {
|
|
|
|
|
const loadingElement = document.getElementById('initial-loading');
|
|
|
|
|
if (loadingElement && !isInitialized) {
|
|
|
|
|
loadingElement.classList.add('fade-out');
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
loadingElement.remove();
|
|
|
|
|
}, 300);
|
|
|
|
|
}
|
|
|
|
|
}, 5000);
|
|
|
|
|
|
|
|
|
|
return () => clearTimeout(fallbackTimer);
|
|
|
|
|
}, [isInitialized]);
|
|
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
const init = async () => {
|
2026-01-06 21:31:04 +02:00
|
|
|
// VS Code runtime bootstraps config + sessions after the managed OpenCode instance reports "connected".
|
|
|
|
|
// Doing the default initialization here can race with startup and lead to one-shot failures.
|
|
|
|
|
if (isVSCodeRuntime) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-12-07 19:32:53 +02:00
|
|
|
await initializeApp();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
init();
|
2026-01-06 21:31:04 +02:00
|
|
|
}, [initializeApp, isVSCodeRuntime]);
|
2025-12-07 19:32:53 +02:00
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
if (isSwitchingDirectory) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const syncDirectoryAndSessions = async () => {
|
2026-01-06 21:31:04 +02:00
|
|
|
// VS Code runtime loads sessions via VSCodeLayout bootstrap to avoid startup races.
|
|
|
|
|
if (isVSCodeRuntime) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!isConnected) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-12-07 19:32:53 +02:00
|
|
|
opencodeClient.setDirectory(currentDirectory);
|
|
|
|
|
|
|
|
|
|
await loadSessions();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
syncDirectoryAndSessions();
|
2026-01-06 21:31:04 +02:00
|
|
|
}, [currentDirectory, isSwitchingDirectory, loadSessions, isConnected, isVSCodeRuntime]);
|
2025-12-07 19:32:53 +02:00
|
|
|
|
|
|
|
|
useEventStream();
|
|
|
|
|
|
2026-01-22 01:19:24 +02:00
|
|
|
usePushVisibilityBeacon();
|
|
|
|
|
|
2026-01-22 11:52:22 -08:00
|
|
|
useRouter();
|
2026-01-22 01:19:24 +02:00
|
|
|
|
2025-12-07 19:32:53 +02:00
|
|
|
useKeyboardShortcuts();
|
|
|
|
|
|
2025-12-19 17:57:50 +02:00
|
|
|
const handleToggleMemoryDebug = React.useCallback(() => {
|
|
|
|
|
setShowMemoryDebug(prev => !prev);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
useMenuActions(handleToggleMemoryDebug);
|
|
|
|
|
|
2026-01-28 23:54:23 +02:00
|
|
|
|
2025-12-07 19:32:53 +02:00
|
|
|
|
|
|
|
|
useSessionStatusBootstrap();
|
2025-12-20 02:06:00 +02:00
|
|
|
useSessionAutoCleanup();
|
2025-12-07 19:32:53 +02:00
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
2026-01-03 02:27:10 +02:00
|
|
|
if (hasModifier(e) && e.shiftKey && e.key === 'D') {
|
2025-12-07 19:32:53 +02:00
|
|
|
e.preventDefault();
|
|
|
|
|
setShowMemoryDebug(prev => !prev);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
window.addEventListener('keydown', handleKeyDown);
|
|
|
|
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
if (error) {
|
|
|
|
|
|
|
|
|
|
setTimeout(() => clearError(), 5000);
|
|
|
|
|
}
|
|
|
|
|
}, [error, clearError]);
|
|
|
|
|
|
|
|
|
|
const handleCliAvailable = React.useCallback(() => {
|
|
|
|
|
setCliAvailable(true);
|
|
|
|
|
window.location.reload();
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
if (isDesktopRuntime && !cliAvailable) {
|
|
|
|
|
return (
|
|
|
|
|
<ErrorBoundary>
|
|
|
|
|
<div className={`h-full text-foreground bg-transparent`}>
|
|
|
|
|
<OnboardingScreen onCliAvailable={handleCliAvailable} />
|
|
|
|
|
</div>
|
|
|
|
|
</ErrorBoundary>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-13 16:34:17 +02:00
|
|
|
// VS Code runtime - simplified layout without git/terminal views
|
|
|
|
|
if (isVSCodeRuntime) {
|
2026-01-03 22:11:16 +01:00
|
|
|
// Check if this is the Agent Manager panel
|
|
|
|
|
const panelType = typeof window !== 'undefined'
|
|
|
|
|
? (window as { __OPENCHAMBER_PANEL_TYPE__?: 'chat' | 'agentManager' }).__OPENCHAMBER_PANEL_TYPE__
|
|
|
|
|
: 'chat';
|
|
|
|
|
|
|
|
|
|
if (panelType === 'agentManager') {
|
|
|
|
|
return (
|
|
|
|
|
<ErrorBoundary>
|
|
|
|
|
<RuntimeAPIProvider apis={apis}>
|
|
|
|
|
<div className="h-full text-foreground bg-background">
|
|
|
|
|
<AgentManagerView />
|
|
|
|
|
<Toaster />
|
|
|
|
|
</div>
|
|
|
|
|
</RuntimeAPIProvider>
|
|
|
|
|
</ErrorBoundary>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-13 16:34:17 +02:00
|
|
|
return (
|
|
|
|
|
<ErrorBoundary>
|
|
|
|
|
<RuntimeAPIProvider apis={apis}>
|
|
|
|
|
<FireworksProvider>
|
|
|
|
|
<div className="h-full text-foreground bg-background">
|
|
|
|
|
<VSCodeLayout />
|
|
|
|
|
<Toaster />
|
|
|
|
|
</div>
|
|
|
|
|
</FireworksProvider>
|
|
|
|
|
</RuntimeAPIProvider>
|
|
|
|
|
</ErrorBoundary>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-07 19:32:53 +02:00
|
|
|
return (
|
|
|
|
|
<ErrorBoundary>
|
|
|
|
|
<RuntimeAPIProvider apis={apis}>
|
|
|
|
|
<GitPollingProvider>
|
|
|
|
|
<FireworksProvider>
|
2026-01-25 19:16:26 +02:00
|
|
|
<div className="h-full text-foreground bg-background">
|
2025-12-07 19:32:53 +02:00
|
|
|
<MainLayout />
|
|
|
|
|
<Toaster />
|
|
|
|
|
<ConfigUpdateOverlay />
|
2025-12-19 18:59:08 +02:00
|
|
|
<AboutDialogWrapper />
|
2025-12-07 19:32:53 +02:00
|
|
|
{showMemoryDebug && (
|
|
|
|
|
<MemoryDebugPanel onClose={() => setShowMemoryDebug(false)} />
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</FireworksProvider>
|
|
|
|
|
</GitPollingProvider>
|
|
|
|
|
</RuntimeAPIProvider>
|
|
|
|
|
</ErrorBoundary>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default App;
|