Files
openchamber/packages/ui/src/apps/VSCodeApp.tsx
T
Bohdan Triapitsyn 59fa91309a fix(ui): keep the app root pinned when the caret scrolls it
Chromium scrolls overflow:hidden ancestors when a textarea caret moves out
of view (PageUp/PageDown in the prompt box, long prompts), shifting the whole
app up and hiding the title bar with no way to scroll back. Snap html/body/#root
back to zero on any root scroll event in the web, desktop, VS Code and mini-chat
apps.

Claude-Session: https://claude.ai/code/session_017TK5JAYDfT3Fotc23UEg98
2026-08-28 20:08:58 +03:00

148 lines
5.5 KiB
TypeScript

import React from 'react';
import { AgentManagerView } from '@/components/views/agent-manager';
import { FireworksProvider } from '@/contexts/FireworksContext';
import { RuntimeAPIProvider } from '@/contexts/RuntimeAPIProvider';
import { registerRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
import { TooltipProvider } from '@/components/ui/tooltip';
import { Toaster } from '@/components/ui/sonner';
import { ConfigUpdateOverlay } from '@/components/ui/ConfigUpdateOverlay';
import { ErrorBoundary } from '@/components/ui/ErrorBoundary';
import { OpenCodeUpdateToast } from '@/components/update/OpenCodeUpdateToast';
import { AppLinkConfirmDialog } from '@/components/chat/AppLinkConfirmDialog';
import { VSCodeLayout } from '@/components/layout/VSCodeLayout';
import { usePushVisibilityBeacon } from '@/hooks/usePushVisibilityBeacon';
import { useGlobalSessionsPolling } from '@/hooks/useGlobalSessionsPolling';
import { useRouter } from '@/hooks/useRouter';
import { useWindowTitle } from '@/hooks/useWindowTitle';
import { useRootScrollLock } from '@/hooks/useRootScrollLock';
import { opencodeClient } from '@/lib/opencode/client';
import type { RuntimeAPIs } from '@/lib/api/types';
import { runtimeFetch } from '@/lib/runtime-fetch';
import { useFeatureFlagsStore } from '@/stores/useFeatureFlagsStore';
import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore';
import { useDirectoryStore } from '@/stores/useDirectoryStore';
import { useUIStore } from '@/stores/useUIStore';
import { useSessionUIStore } from '@/sync/session-ui-store';
import { SyncProvider } from '@/sync/sync-context';
import { SyncAppEffects } from './AppEffects';
import { useAppFontEffects } from './useAppFontEffects';
type VSCodePanelType = 'chat' | 'agentManager';
declare global {
interface Window {
__OPENCHAMBER_PANEL_TYPE__?: VSCodePanelType;
}
}
type VSCodeAppProps = {
apis: RuntimeAPIs;
};
export function VSCodeApp({ apis }: VSCodeAppProps) {
const currentDirectory = useDirectoryStore((state) => state.currentDirectory);
const error = useSessionUIStore((state) => state.error);
const clearError = useSessionUIStore((state) => state.clearError);
const wideChatLayoutEnabled = useUIStore((state) => state.wideChatLayoutEnabled);
const refreshGitHubAuthStatus = useGitHubAuthStore((state) => state.refreshStatus);
const setPlanModeEnabled = useFeatureFlagsStore((state) => state.setPlanModeEnabled);
const panelType = typeof window !== 'undefined'
? window.__OPENCHAMBER_PANEL_TYPE__
: 'chat';
React.useEffect(() => {
registerRuntimeAPIs(apis);
return () => registerRuntimeAPIs(null);
}, [apis]);
useAppFontEffects();
usePushVisibilityBeacon({ enabled: true });
useWindowTitle();
useRootScrollLock();
useRouter();
useGlobalSessionsPolling(panelType !== 'agentManager');
React.useEffect(() => {
document.documentElement.classList.toggle('wide-chat-layout', wideChatLayoutEnabled);
return () => {
document.documentElement.classList.remove('wide-chat-layout');
};
}, [wideChatLayoutEnabled]);
React.useEffect(() => {
void refreshGitHubAuthStatus(apis.github, { force: true });
}, [apis.github, refreshGitHubAuthStatus]);
React.useEffect(() => {
let cancelled = false;
const run = async () => {
const res = await runtimeFetch('/health', { method: 'GET' }).catch(() => null);
if (!res || !res.ok || cancelled) return;
const data = (await res.json().catch(() => null)) as null | {
planModeExperimentalEnabled?: unknown;
};
if (!data || cancelled) return;
const raw = data.planModeExperimentalEnabled;
const enabled = raw === true || raw === 1 || raw === '1' || raw === 'true';
setPlanModeEnabled(enabled);
};
void run();
return () => {
cancelled = true;
};
}, [setPlanModeEnabled]);
React.useEffect(() => {
if (!error) {
return;
}
const timeout = window.setTimeout(() => clearError(), 5000);
return () => window.clearTimeout(timeout);
}, [clearError, error]);
if (panelType === 'agentManager') {
return (
<ErrorBoundary>
<SyncProvider sdk={opencodeClient.getSdkClient()} directory={currentDirectory || ''}>
<RuntimeAPIProvider apis={apis}>
<TooltipProvider delayDuration={300} skipDelayDuration={150}>
<div className="h-full text-foreground bg-background">
<SyncAppEffects embeddedBackgroundWorkEnabled={true} />
<AgentManagerView />
<AppLinkConfirmDialog />
<OpenCodeUpdateToast />
<Toaster position="top-center" />
</div>
</TooltipProvider>
</RuntimeAPIProvider>
</SyncProvider>
</ErrorBoundary>
);
}
return (
<ErrorBoundary>
<SyncProvider sdk={opencodeClient.getSdkClient()} directory={currentDirectory || ''}>
<RuntimeAPIProvider apis={apis}>
<FireworksProvider>
<TooltipProvider delayDuration={300} skipDelayDuration={150}>
<div className="h-full text-foreground bg-background">
<SyncAppEffects embeddedBackgroundWorkEnabled={true} />
<VSCodeLayout />
<AppLinkConfirmDialog />
<OpenCodeUpdateToast />
<Toaster position="top-center" />
<ConfigUpdateOverlay />
</div>
</TooltipProvider>
</FireworksProvider>
</RuntimeAPIProvider>
</SyncProvider>
</ErrorBoundary>
);
}