fix: improve startup readiness performance
This commit is contained in:
+10
-2
@@ -56,6 +56,7 @@ import { SyncAppEffects } from '@/apps/AppEffects';
|
|||||||
import { useAppFontEffects } from '@/apps/useAppFontEffects';
|
import { useAppFontEffects } from '@/apps/useAppFontEffects';
|
||||||
import { resetStreamingState } from '@/sync/streaming';
|
import { resetStreamingState } from '@/sync/streaming';
|
||||||
import { OpenCodeUpdateToast } from '@/components/update/OpenCodeUpdateToast';
|
import { OpenCodeUpdateToast } from '@/components/update/OpenCodeUpdateToast';
|
||||||
|
import { markStartupTrace, startupTraceEnabled } from '@/lib/startupTrace';
|
||||||
|
|
||||||
// Lazy-loaded heavy views — loaded on demand to reduce initial bundle size.
|
// Lazy-loaded heavy views — loaded on demand to reduce initial bundle size.
|
||||||
const OnboardingScreen = lazyWithChunkRecovery(() =>
|
const OnboardingScreen = lazyWithChunkRecovery(() =>
|
||||||
@@ -201,6 +202,13 @@ const EmbeddedSessionChatContent: React.FC<{
|
|||||||
};
|
};
|
||||||
|
|
||||||
function App({ apis }: AppProps) {
|
function App({ apis }: AppProps) {
|
||||||
|
React.useEffect(() => {
|
||||||
|
markStartupTrace('App:mounted');
|
||||||
|
if (startupTraceEnabled()) {
|
||||||
|
console.info('[startup-trace] enabled. Run console.table(window.__OPENCHAMBER_STARTUP_TRACE__) after startup.');
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const initializeApp = useConfigStore((s) => s.initializeApp);
|
const initializeApp = useConfigStore((s) => s.initializeApp);
|
||||||
const isInitialized = useConfigStore((s) => s.isInitialized);
|
const isInitialized = useConfigStore((s) => s.isInitialized);
|
||||||
const isConnected = useConfigStore((s) => s.isConnected);
|
const isConnected = useConfigStore((s) => s.isConnected);
|
||||||
@@ -470,8 +478,8 @@ function App({ apis }: AppProps) {
|
|||||||
const state = useConfigStore.getState();
|
const state = useConfigStore.getState();
|
||||||
if (state.providers.length > 0 && state.agents.length > 0) return;
|
if (state.providers.length > 0 && state.agents.length > 0) return;
|
||||||
try {
|
try {
|
||||||
if (state.providers.length === 0) await loadProviders();
|
if (state.providers.length === 0) await loadProviders({ source: 'startupRecovery' });
|
||||||
if (useConfigStore.getState().agents.length === 0) await loadAgents();
|
if (useConfigStore.getState().agents.length === 0) await loadAgents({ source: 'startupRecovery' });
|
||||||
} catch { /* retry next interval */ }
|
} catch { /* retry next interval */ }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -114,8 +114,8 @@ const MiniChatBootstrap: React.FC<{ config: MiniChatConfig }> = ({ config }) =>
|
|||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!isConnected) return;
|
if (!isConnected) return;
|
||||||
if (providersCount === 0) void loadProviders();
|
if (providersCount === 0) void loadProviders({ source: 'electronMiniChat:recovery' });
|
||||||
if (agentsCount === 0) void loadAgents();
|
if (agentsCount === 0) void loadAgents({ source: 'electronMiniChat:recovery' });
|
||||||
}, [agentsCount, isConnected, loadAgents, loadProviders, providersCount]);
|
}, [agentsCount, isConnected, loadAgents, loadProviders, providersCount]);
|
||||||
|
|
||||||
const sessionBootstrappedRef = React.useRef(false);
|
const sessionBootstrappedRef = React.useRef(false);
|
||||||
|
|||||||
@@ -373,8 +373,8 @@ export function MobileApp({ apis }: MobileAppProps) {
|
|||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!isConnected) return;
|
if (!isConnected) return;
|
||||||
if (providersCount === 0) void loadProviders();
|
if (providersCount === 0) void loadProviders({ source: 'mobileApp:recovery' });
|
||||||
if (agentsCount === 0) void loadAgents();
|
if (agentsCount === 0) void loadAgents({ source: 'mobileApp:recovery' });
|
||||||
}, [agentsCount, isConnected, loadAgents, loadProviders, providersCount]);
|
}, [agentsCount, isConnected, loadAgents, loadProviders, providersCount]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
|||||||
@@ -49,14 +49,15 @@ export const AgentMentionAutocomplete = React.forwardRef<AgentMentionAutocomplet
|
|||||||
const itemRefs = React.useRef<(HTMLDivElement | null)[]>([]);
|
const itemRefs = React.useRef<(HTMLDivElement | null)[]>([]);
|
||||||
const ignoreTabClickRef = React.useRef(false);
|
const ignoreTabClickRef = React.useRef(false);
|
||||||
const getVisibleAgents = useConfigStore((state) => state.getVisibleAgents);
|
const getVisibleAgents = useConfigStore((state) => state.getVisibleAgents);
|
||||||
|
const configAgentsCount = useConfigStore((state) => state.agents.length);
|
||||||
const agentsWithMetadata = useAgentsStore((state) => state.agents);
|
const agentsWithMetadata = useAgentsStore((state) => state.agents);
|
||||||
const loadAgents = useAgentsStore((state) => state.loadAgents);
|
const loadAgents = useAgentsStore((state) => state.loadAgents);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (agentsWithMetadata.length === 0) {
|
if (agentsWithMetadata.length === 0 && configAgentsCount === 0) {
|
||||||
void loadAgents();
|
void loadAgents();
|
||||||
}
|
}
|
||||||
}, [loadAgents, agentsWithMetadata.length]);
|
}, [loadAgents, agentsWithMetadata.length, configAgentsCount]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const visibleAgents = getVisibleAgents();
|
const visibleAgents = getVisibleAgents();
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import { formatEffortLabel, getCycledPrimaryAgentName, type MobileControlsPanel
|
|||||||
import { useI18n } from '@/lib/i18n';
|
import { useI18n } from '@/lib/i18n';
|
||||||
import { useOpenCodeReadiness } from '@/hooks/useOpenCodeReadiness';
|
import { useOpenCodeReadiness } from '@/hooks/useOpenCodeReadiness';
|
||||||
import { eventMatchesShortcut, getEffectiveShortcutCombo, normalizeCombo } from '@/lib/shortcuts';
|
import { eventMatchesShortcut, getEffectiveShortcutCombo, normalizeCombo } from '@/lib/shortcuts';
|
||||||
|
import { markStartupTrace } from '@/lib/startupTrace';
|
||||||
|
|
||||||
type IconComponent = IconName;
|
type IconComponent = IconName;
|
||||||
|
|
||||||
@@ -312,6 +313,19 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
// Use visible agents (excludes hidden internal agents)
|
// Use visible agents (excludes hidden internal agents)
|
||||||
const agents = getVisibleAgents();
|
const agents = getVisibleAgents();
|
||||||
const primaryAgents = React.useMemo(() => agents.filter((agent) => agent.mode === 'primary'), [agents]);
|
const primaryAgents = React.useMemo(() => agents.filter((agent) => agent.mode === 'primary'), [agents]);
|
||||||
|
const tracedReadyRef = React.useRef(false);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (tracedReadyRef.current || !isReady) return;
|
||||||
|
tracedReadyRef.current = true;
|
||||||
|
markStartupTrace('ModelControls:ready', {
|
||||||
|
providers: providers.length,
|
||||||
|
agents: agents.length,
|
||||||
|
currentProviderId,
|
||||||
|
currentModelId,
|
||||||
|
currentAgentName,
|
||||||
|
});
|
||||||
|
}, [agents.length, currentAgentName, currentModelId, currentProviderId, isReady, providers.length]);
|
||||||
|
|
||||||
const currentSessionId = useSessionUIStore((s) => s.currentSessionId);
|
const currentSessionId = useSessionUIStore((s) => s.currentSessionId);
|
||||||
const getDirectoryForSession = useSessionUIStore((s) => s.getDirectoryForSession);
|
const getDirectoryForSession = useSessionUIStore((s) => s.getDirectoryForSession);
|
||||||
|
|||||||
@@ -320,10 +320,10 @@ export const VSCodeLayout: React.FC = () => {
|
|||||||
// Keep trying to fetch core datasets on cold starts.
|
// Keep trying to fetch core datasets on cold starts.
|
||||||
if (configStore.isConnected) {
|
if (configStore.isConnected) {
|
||||||
if (configStore.providers.length === 0) {
|
if (configStore.providers.length === 0) {
|
||||||
await configStore.loadProviders();
|
await configStore.loadProviders({ source: 'vscodeLayout:bootstrap' });
|
||||||
}
|
}
|
||||||
if (configStore.agents.length === 0) {
|
if (configStore.agents.length === 0) {
|
||||||
await configStore.loadAgents();
|
await configStore.loadAgents({ source: 'vscodeLayout:bootstrap' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,8 +50,8 @@ export const AgentSelector: React.FC<AgentSelectorProps> = ({
|
|||||||
|
|
||||||
// Load agents on mount
|
// Load agents on mount
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
loadAgents();
|
if (agents.length === 0) void loadAgents();
|
||||||
}, [loadAgents]);
|
}, [agents.length, loadAgents]);
|
||||||
|
|
||||||
// Ensure we always have a valid selection (defaults to current default agent, then first selectable agent).
|
// Ensure we always have a valid selection (defaults to current default agent, then first selectable agent).
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { cn } from '@/lib/utils';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Icon } from "@/components/icon/Icon";
|
import { Icon } from "@/components/icon/Icon";
|
||||||
import { useI18n } from '@/lib/i18n';
|
import { useI18n } from '@/lib/i18n';
|
||||||
|
import { runtimeFetch } from '@/lib/runtime-fetch';
|
||||||
|
|
||||||
const GITHUB_URL = 'https://github.com/btriapitsyn/openchamber';
|
const GITHUB_URL = 'https://github.com/btriapitsyn/openchamber';
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ export const AboutSettings: React.FC = () => {
|
|||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const [updateDialogOpen, setUpdateDialogOpen] = React.useState(false);
|
const [updateDialogOpen, setUpdateDialogOpen] = React.useState(false);
|
||||||
const [showChecking, setShowChecking] = React.useState(false);
|
const [showChecking, setShowChecking] = React.useState(false);
|
||||||
|
const [openCodeVersion, setOpenCodeVersion] = React.useState<string | null>(null);
|
||||||
const updateStore = useUpdateStore(useShallow((s) => ({
|
const updateStore = useUpdateStore(useShallow((s) => ({
|
||||||
info: s.info,
|
info: s.info,
|
||||||
checking: s.checking,
|
checking: s.checking,
|
||||||
@@ -34,6 +36,33 @@ export const AboutSettings: React.FC = () => {
|
|||||||
|
|
||||||
const currentVersion = updateStore.info?.currentVersion || 'unknown';
|
const currentVersion = updateStore.info?.currentVersion || 'unknown';
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
const loadOpenCodeVersion = async () => {
|
||||||
|
try {
|
||||||
|
const response = await runtimeFetch('/api/opencode/version', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: { Accept: 'application/json' },
|
||||||
|
});
|
||||||
|
if (!response.ok) return;
|
||||||
|
const data = await response.json().catch(() => null) as { version?: unknown } | null;
|
||||||
|
const version = typeof data?.version === 'string' && data.version.trim().length > 0
|
||||||
|
? data.version.trim()
|
||||||
|
: null;
|
||||||
|
if (!cancelled) setOpenCodeVersion(version);
|
||||||
|
} catch {
|
||||||
|
if (!cancelled) setOpenCodeVersion(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void loadOpenCodeVersion();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Track if we initiated a check to show toast on completion
|
// Track if we initiated a check to show toast on completion
|
||||||
const didInitiateCheck = React.useRef(false);
|
const didInitiateCheck = React.useRef(false);
|
||||||
|
|
||||||
@@ -91,6 +120,11 @@ export const AboutSettings: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="typography-meta text-muted-foreground">{t('settings.openchamber.about.field.openCodeVersion')}</span>
|
||||||
|
<span className="typography-meta text-muted-foreground font-mono">{openCodeVersion || t('settings.openchamber.about.state.unknown')}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
{updateStore.error && (
|
{updateStore.error && (
|
||||||
<p className="typography-micro text-[var(--status-error)] truncate">{updateStore.error}</p>
|
<p className="typography-micro text-[var(--status-error)] truncate">{updateStore.error}</p>
|
||||||
)}
|
)}
|
||||||
@@ -159,6 +193,10 @@ export const AboutSettings: React.FC = () => {
|
|||||||
<span className="typography-ui-label text-foreground">{t('settings.openchamber.about.field.version')}</span>
|
<span className="typography-ui-label text-foreground">{t('settings.openchamber.about.field.version')}</span>
|
||||||
<span className="typography-meta text-muted-foreground font-mono">{currentVersion}</span>
|
<span className="typography-meta text-muted-foreground font-mono">{currentVersion}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex min-w-0 flex-col">
|
||||||
|
<span className="typography-ui-label text-foreground">{t('settings.openchamber.about.field.openCodeVersion')}</span>
|
||||||
|
<span className="typography-meta text-muted-foreground font-mono">{openCodeVersion || t('settings.openchamber.about.state.unknown')}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{updateStore.checking && (
|
{updateStore.checking && (
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export function ForkSessionDialog(props: ForkSessionDialogProps) {
|
|||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
void loadProviders({ directory: projectDirectory });
|
void loadProviders({ directory: projectDirectory, source: 'forkSessionDialog' });
|
||||||
void loadConfigAgents({ directory: projectDirectory });
|
void loadConfigAgents({ directory: projectDirectory });
|
||||||
void loadAgentsStoreAgents();
|
void loadAgentsStoreAgents();
|
||||||
}, [open, loadProviders, loadConfigAgents, loadAgentsStoreAgents, projectDirectory]);
|
}, [open, loadProviders, loadConfigAgents, loadAgentsStoreAgents, projectDirectory]);
|
||||||
|
|||||||
@@ -662,8 +662,8 @@ export function ScheduledTaskEditorDialog(props: {
|
|||||||
if (!open) {
|
if (!open) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
void loadProviders();
|
void loadProviders({ source: 'scheduledTaskEditor' });
|
||||||
void loadAgents();
|
void loadAgents({ source: 'scheduledTaskEditor' });
|
||||||
}, [open, loadProviders, loadAgents]);
|
}, [open, loadProviders, loadAgents]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export function TodoSendDialog(props: TodoSendDialogProps) {
|
|||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
void loadProviders({ directory: projectDirectory });
|
void loadProviders({ directory: projectDirectory, source: 'todoSendDialog' });
|
||||||
void loadConfigAgents({ directory: projectDirectory });
|
void loadConfigAgents({ directory: projectDirectory });
|
||||||
void loadAgentsStoreAgents();
|
void loadAgentsStoreAgents();
|
||||||
}, [open, loadProviders, loadConfigAgents, loadAgentsStoreAgents, projectDirectory]);
|
}, [open, loadProviders, loadConfigAgents, loadAgentsStoreAgents, projectDirectory]);
|
||||||
|
|||||||
@@ -360,8 +360,10 @@ export const settingsDict = {
|
|||||||
'settings.projects.actions.toast.saved': 'Project actions saved',
|
'settings.projects.actions.toast.saved': 'Project actions saved',
|
||||||
'settings.openchamber.about.title': 'About OpenChamber',
|
'settings.openchamber.about.title': 'About OpenChamber',
|
||||||
'settings.openchamber.about.field.version': 'Version',
|
'settings.openchamber.about.field.version': 'Version',
|
||||||
|
'settings.openchamber.about.field.openCodeVersion': 'OpenCode version',
|
||||||
'settings.openchamber.about.state.checking': 'Checking...',
|
'settings.openchamber.about.state.checking': 'Checking...',
|
||||||
'settings.openchamber.about.state.upToDate': 'Up to date',
|
'settings.openchamber.about.state.upToDate': 'Up to date',
|
||||||
|
'settings.openchamber.about.state.unknown': 'unknown',
|
||||||
'settings.openchamber.about.actions.checkUpdates': 'Check updates',
|
'settings.openchamber.about.actions.checkUpdates': 'Check updates',
|
||||||
'settings.openchamber.about.actions.update': 'Update',
|
'settings.openchamber.about.actions.update': 'Update',
|
||||||
'settings.openchamber.about.actions.updateToVersion': 'Update to {version}',
|
'settings.openchamber.about.actions.updateToVersion': 'Update to {version}',
|
||||||
|
|||||||
@@ -327,8 +327,10 @@ export const settingsDict = {
|
|||||||
"settings.projects.actions.toast.saved": "Acciones del proyecto guardadas",
|
"settings.projects.actions.toast.saved": "Acciones del proyecto guardadas",
|
||||||
"settings.openchamber.about.title": "Acerca de OpenChamber",
|
"settings.openchamber.about.title": "Acerca de OpenChamber",
|
||||||
"settings.openchamber.about.field.version": "Versión",
|
"settings.openchamber.about.field.version": "Versión",
|
||||||
|
"settings.openchamber.about.field.openCodeVersion": "Versión de OpenCode",
|
||||||
"settings.openchamber.about.state.checking": "Comprobando...",
|
"settings.openchamber.about.state.checking": "Comprobando...",
|
||||||
"settings.openchamber.about.state.upToDate": "Actualizado",
|
"settings.openchamber.about.state.upToDate": "Actualizado",
|
||||||
|
"settings.openchamber.about.state.unknown": "desconocido",
|
||||||
"settings.openchamber.about.actions.checkUpdates": "Comprobar actualizaciones",
|
"settings.openchamber.about.actions.checkUpdates": "Comprobar actualizaciones",
|
||||||
"settings.openchamber.about.actions.update": "Actualizar",
|
"settings.openchamber.about.actions.update": "Actualizar",
|
||||||
"settings.openchamber.about.actions.updateToVersion": "Actualizar a {version}",
|
"settings.openchamber.about.actions.updateToVersion": "Actualizar a {version}",
|
||||||
|
|||||||
@@ -327,8 +327,10 @@ export const settingsDict = {
|
|||||||
'settings.projects.actions.toast.saved': '프로젝트 작업이 저장되었습니다',
|
'settings.projects.actions.toast.saved': '프로젝트 작업이 저장되었습니다',
|
||||||
'settings.openchamber.about.title': 'OpenChamber 정보',
|
'settings.openchamber.about.title': 'OpenChamber 정보',
|
||||||
'settings.openchamber.about.field.version': '버전',
|
'settings.openchamber.about.field.version': '버전',
|
||||||
|
'settings.openchamber.about.field.openCodeVersion': 'OpenCode 버전',
|
||||||
'settings.openchamber.about.state.checking': '확인 중...',
|
'settings.openchamber.about.state.checking': '확인 중...',
|
||||||
'settings.openchamber.about.state.upToDate': '최신 상태',
|
'settings.openchamber.about.state.upToDate': '최신 상태',
|
||||||
|
'settings.openchamber.about.state.unknown': '알 수 없음',
|
||||||
'settings.openchamber.about.actions.checkUpdates': '업데이트 확인',
|
'settings.openchamber.about.actions.checkUpdates': '업데이트 확인',
|
||||||
'settings.openchamber.about.actions.update': '업데이트',
|
'settings.openchamber.about.actions.update': '업데이트',
|
||||||
'settings.openchamber.about.actions.updateToVersion': '{version}으로 업데이트',
|
'settings.openchamber.about.actions.updateToVersion': '{version}으로 업데이트',
|
||||||
|
|||||||
@@ -640,8 +640,10 @@ export const settingsDict = {
|
|||||||
'settings.openchamber.about.actions.update': 'Aktualizuj',
|
'settings.openchamber.about.actions.update': 'Aktualizuj',
|
||||||
'settings.openchamber.about.actions.updateToVersion': 'Aktualizuj do wersji {version}',
|
'settings.openchamber.about.actions.updateToVersion': 'Aktualizuj do wersji {version}',
|
||||||
'settings.openchamber.about.field.version': 'Wersja',
|
'settings.openchamber.about.field.version': 'Wersja',
|
||||||
|
'settings.openchamber.about.field.openCodeVersion': 'Wersja OpenCode',
|
||||||
'settings.openchamber.about.state.checking': 'Sprawdzanie...',
|
'settings.openchamber.about.state.checking': 'Sprawdzanie...',
|
||||||
'settings.openchamber.about.state.upToDate': 'Aktualna wersja',
|
'settings.openchamber.about.state.upToDate': 'Aktualna wersja',
|
||||||
|
'settings.openchamber.about.state.unknown': 'nieznane',
|
||||||
'settings.openchamber.about.title': 'O OpenChamber',
|
'settings.openchamber.about.title': 'O OpenChamber',
|
||||||
'settings.openchamber.about.toast.latestVersion': 'Używasz najnowszej wersji',
|
'settings.openchamber.about.toast.latestVersion': 'Używasz najnowszej wersji',
|
||||||
'settings.openchamber.defaults.field.defaultAgent': 'Domyślny Agent',
|
'settings.openchamber.defaults.field.defaultAgent': 'Domyślny Agent',
|
||||||
|
|||||||
@@ -327,8 +327,10 @@ export const settingsDict = {
|
|||||||
"settings.projects.actions.toast.saved": "Ações do projeto salvas",
|
"settings.projects.actions.toast.saved": "Ações do projeto salvas",
|
||||||
"settings.openchamber.about.title": "Sobre o OpenChamber",
|
"settings.openchamber.about.title": "Sobre o OpenChamber",
|
||||||
"settings.openchamber.about.field.version": "Versão",
|
"settings.openchamber.about.field.version": "Versão",
|
||||||
|
"settings.openchamber.about.field.openCodeVersion": "Versão do OpenCode",
|
||||||
"settings.openchamber.about.state.checking": "Verificando...",
|
"settings.openchamber.about.state.checking": "Verificando...",
|
||||||
"settings.openchamber.about.state.upToDate": "Atualizado",
|
"settings.openchamber.about.state.upToDate": "Atualizado",
|
||||||
|
"settings.openchamber.about.state.unknown": "desconhecido",
|
||||||
"settings.openchamber.about.actions.checkUpdates": "Verificar atualizações",
|
"settings.openchamber.about.actions.checkUpdates": "Verificar atualizações",
|
||||||
"settings.openchamber.about.actions.update": "Atualizar",
|
"settings.openchamber.about.actions.update": "Atualizar",
|
||||||
"settings.openchamber.about.actions.updateToVersion": "Atualizar a {version}",
|
"settings.openchamber.about.actions.updateToVersion": "Atualizar a {version}",
|
||||||
|
|||||||
@@ -327,8 +327,10 @@ export const settingsDict = {
|
|||||||
"settings.projects.actions.toast.saved": "Дії проєкту збережено",
|
"settings.projects.actions.toast.saved": "Дії проєкту збережено",
|
||||||
"settings.openchamber.about.title": "Про OpenChamber",
|
"settings.openchamber.about.title": "Про OpenChamber",
|
||||||
"settings.openchamber.about.field.version": "Версія",
|
"settings.openchamber.about.field.version": "Версія",
|
||||||
|
"settings.openchamber.about.field.openCodeVersion": "Версія OpenCode",
|
||||||
"settings.openchamber.about.state.checking": "Перевірка...",
|
"settings.openchamber.about.state.checking": "Перевірка...",
|
||||||
"settings.openchamber.about.state.upToDate": "В актуальному стані",
|
"settings.openchamber.about.state.upToDate": "В актуальному стані",
|
||||||
|
"settings.openchamber.about.state.unknown": "невідомо",
|
||||||
"settings.openchamber.about.actions.checkUpdates": "Перевірити оновлення",
|
"settings.openchamber.about.actions.checkUpdates": "Перевірити оновлення",
|
||||||
"settings.openchamber.about.actions.update": "Оновити",
|
"settings.openchamber.about.actions.update": "Оновити",
|
||||||
"settings.openchamber.about.actions.updateToVersion": "Оновити до {version}",
|
"settings.openchamber.about.actions.updateToVersion": "Оновити до {version}",
|
||||||
|
|||||||
@@ -327,8 +327,10 @@ export const settingsDict = {
|
|||||||
'settings.projects.actions.toast.saved': '项目操作已保存',
|
'settings.projects.actions.toast.saved': '项目操作已保存',
|
||||||
'settings.openchamber.about.title': '关于 OpenChamber',
|
'settings.openchamber.about.title': '关于 OpenChamber',
|
||||||
'settings.openchamber.about.field.version': '版本',
|
'settings.openchamber.about.field.version': '版本',
|
||||||
|
'settings.openchamber.about.field.openCodeVersion': 'OpenCode 版本',
|
||||||
'settings.openchamber.about.state.checking': '检查中...',
|
'settings.openchamber.about.state.checking': '检查中...',
|
||||||
'settings.openchamber.about.state.upToDate': '已是最新',
|
'settings.openchamber.about.state.upToDate': '已是最新',
|
||||||
|
'settings.openchamber.about.state.unknown': '未知',
|
||||||
'settings.openchamber.about.actions.checkUpdates': '检查更新',
|
'settings.openchamber.about.actions.checkUpdates': '检查更新',
|
||||||
'settings.openchamber.about.actions.update': '更新',
|
'settings.openchamber.about.actions.update': '更新',
|
||||||
'settings.openchamber.about.actions.updateToVersion': '更新到 {version}',
|
'settings.openchamber.about.actions.updateToVersion': '更新到 {version}',
|
||||||
|
|||||||
@@ -324,8 +324,10 @@
|
|||||||
'settings.projects.actions.toast.saved': '專案操作已儲存',
|
'settings.projects.actions.toast.saved': '專案操作已儲存',
|
||||||
'settings.openchamber.about.title': '關於 OpenChamber',
|
'settings.openchamber.about.title': '關於 OpenChamber',
|
||||||
'settings.openchamber.about.field.version': '版本',
|
'settings.openchamber.about.field.version': '版本',
|
||||||
|
'settings.openchamber.about.field.openCodeVersion': 'OpenCode 版本',
|
||||||
'settings.openchamber.about.state.checking': '檢查中...',
|
'settings.openchamber.about.state.checking': '檢查中...',
|
||||||
'settings.openchamber.about.state.upToDate': '已是最新',
|
'settings.openchamber.about.state.upToDate': '已是最新',
|
||||||
|
'settings.openchamber.about.state.unknown': '未知',
|
||||||
'settings.openchamber.about.actions.checkUpdates': '檢查更新',
|
'settings.openchamber.about.actions.checkUpdates': '檢查更新',
|
||||||
'settings.openchamber.about.actions.update': '更新',
|
'settings.openchamber.about.actions.update': '更新',
|
||||||
'settings.openchamber.about.actions.updateToVersion': '更新到 {version}',
|
'settings.openchamber.about.actions.updateToVersion': '更新到 {version}',
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ export const buildOpenCodeStatusReport = async (): Promise<string> => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const probeTargets: Array<{ label: string; path: string; includeDirectory?: boolean; timeoutMs?: number }> = [
|
const probeTargets: Array<{ label: string; path: string; includeDirectory?: boolean; timeoutMs?: number }> = [
|
||||||
{ label: 'health', path: '/global/health', includeDirectory: false },
|
{ label: 'health', path: '/api/health', includeDirectory: false },
|
||||||
{ label: 'config', path: '/config', includeDirectory: true },
|
{ label: 'config', path: '/config', includeDirectory: true },
|
||||||
{ label: 'providers', path: '/config/providers', includeDirectory: true },
|
{ label: 'providers', path: '/config/providers', includeDirectory: true },
|
||||||
{ label: 'agents', path: '/agent', includeDirectory: true, timeoutMs: 12000 },
|
{ label: 'agents', path: '/agent', includeDirectory: true, timeoutMs: 12000 },
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { waitForWorktreeBootstrap } from "@/lib/worktrees/worktreeBootstrap";
|
|||||||
import { getRuntimeUrlResolver } from "@/lib/runtime-url";
|
import { getRuntimeUrlResolver } from "@/lib/runtime-url";
|
||||||
import { runtimeFetch } from "@/lib/runtime-fetch";
|
import { runtimeFetch } from "@/lib/runtime-fetch";
|
||||||
import { getRegisteredRuntimeAPIs } from "@/contexts/runtimeAPIRegistry";
|
import { getRegisteredRuntimeAPIs } from "@/contexts/runtimeAPIRegistry";
|
||||||
|
import { markStartupTrace } from "@/lib/startupTrace";
|
||||||
import {
|
import {
|
||||||
assertProviderCircuitClosed,
|
assertProviderCircuitClosed,
|
||||||
recordProviderSuccess,
|
recordProviderSuccess,
|
||||||
@@ -346,7 +347,14 @@ class OpencodeService {
|
|||||||
|
|
||||||
// Set the current working directory for all API calls
|
// Set the current working directory for all API calls
|
||||||
setDirectory(directory: string | undefined) {
|
setDirectory(directory: string | undefined) {
|
||||||
this.currentDirectory = this.normalizeCandidatePath(directory) ?? directory;
|
const normalized = this.normalizeCandidatePath(directory) ?? directory;
|
||||||
|
if (this.currentDirectory !== normalized) {
|
||||||
|
markStartupTrace('opencodeClient:setDirectory', {
|
||||||
|
previous: this.currentDirectory ?? null,
|
||||||
|
next: normalized ?? null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.currentDirectory = normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
getDirectory(): string | undefined {
|
getDirectory(): string | undefined {
|
||||||
@@ -1414,33 +1422,24 @@ class OpencodeService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Health Check - using /health endpoint for detailed status
|
// Lightweight readiness check. Full diagnostics still live at /health.
|
||||||
async checkHealth(): Promise<boolean> {
|
async checkHealth(): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
// Health endpoint is at root, not under /api
|
|
||||||
let healthUrl: string;
|
|
||||||
const normalizedBase = this.baseUrl.endsWith('/') ? this.baseUrl.replace(/\/+$/, '') : this.baseUrl;
|
const normalizedBase = this.baseUrl.endsWith('/') ? this.baseUrl.replace(/\/+$/, '') : this.baseUrl;
|
||||||
if (normalizedBase === '/api') {
|
const healthUrl = normalizedBase === '/api' || normalizedBase.endsWith('/api')
|
||||||
healthUrl = '/health';
|
? '/api/opencode/health'
|
||||||
} else if (normalizedBase.endsWith('/api')) {
|
: `${normalizedBase}/opencode/health`;
|
||||||
// Desktop: http://127.0.0.1:PORT/api -> http://127.0.0.1:PORT/health
|
markStartupTrace('opencodeClient.checkHealth:url', { baseUrl: this.baseUrl, healthUrl });
|
||||||
healthUrl = `${normalizedBase.slice(0, -4)}/health`;
|
|
||||||
} else {
|
|
||||||
healthUrl = `${normalizedBase}/health`;
|
|
||||||
}
|
|
||||||
const response = await runtimeFetch(healthUrl);
|
const response = await runtimeFetch(healthUrl);
|
||||||
|
markStartupTrace('opencodeClient.checkHealth:response', { status: response.status });
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const healthData = await response.json();
|
const healthData = await response.json();
|
||||||
|
markStartupTrace('opencodeClient.checkHealth:result', { healthy: healthData?.healthy });
|
||||||
|
|
||||||
// Check if the upstream API is ready (not just OpenChamber server)
|
return healthData?.healthy === true;
|
||||||
if (healthData.isOpenCodeReady === false) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,14 +126,12 @@ export const runtimeFetch = async (input: string | URL | Request, init: RuntimeF
|
|||||||
const inputHeaders = resolvedInput instanceof Request ? resolvedInput.headers : undefined;
|
const inputHeaders = resolvedInput instanceof Request ? resolvedInput.headers : undefined;
|
||||||
const headers = await mergeHeaders(inputHeaders, requestInit.headers, shouldAttachRuntimeAuth(resolvedInput));
|
const headers = await mergeHeaders(inputHeaders, requestInit.headers, shouldAttachRuntimeAuth(resolvedInput));
|
||||||
|
|
||||||
if (resolvedInput instanceof Request) {
|
return resolvedInput instanceof Request
|
||||||
return fetch(new Request(resolvedInput, { ...requestInit, headers }));
|
? fetch(new Request(resolvedInput, { ...requestInit, headers }))
|
||||||
}
|
: fetch(resolvedInput, {
|
||||||
|
...requestInit,
|
||||||
return fetch(resolvedInput, {
|
headers,
|
||||||
...requestInit,
|
});
|
||||||
headers,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let runtimeFetchBridgeInstalled = false;
|
let runtimeFetchBridgeInstalled = false;
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
export type StartupTraceEvent = {
|
||||||
|
t: number;
|
||||||
|
name: string;
|
||||||
|
data?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
__OPENCHAMBER_STARTUP_TRACE__?: StartupTraceEvent[];
|
||||||
|
__OPENCHAMBER_STARTUP_TRACE_START__?: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_STARTUP_TRACE_EVENTS = 500;
|
||||||
|
|
||||||
|
const enabled = () => {
|
||||||
|
if (typeof window === 'undefined') return false;
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
return params.get('startupTrace') === '1' || window.localStorage?.getItem('OPENCHAMBER_STARTUP_TRACE') === '1';
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const startupTraceEnabled = () => enabled();
|
||||||
|
|
||||||
|
export const markStartupTrace = (name: string, data?: Record<string, unknown>) => {
|
||||||
|
if (!enabled()) return;
|
||||||
|
const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
||||||
|
window.__OPENCHAMBER_STARTUP_TRACE_START__ ??= now;
|
||||||
|
window.__OPENCHAMBER_STARTUP_TRACE__ ??= [];
|
||||||
|
window.__OPENCHAMBER_STARTUP_TRACE__.push({
|
||||||
|
t: Math.round(now - window.__OPENCHAMBER_STARTUP_TRACE_START__),
|
||||||
|
name,
|
||||||
|
...(data ? { data } : {}),
|
||||||
|
});
|
||||||
|
if (window.__OPENCHAMBER_STARTUP_TRACE__.length > MAX_STARTUP_TRACE_EVENTS) {
|
||||||
|
window.__OPENCHAMBER_STARTUP_TRACE__.splice(
|
||||||
|
0,
|
||||||
|
window.__OPENCHAMBER_STARTUP_TRACE__.length - MAX_STARTUP_TRACE_EVENTS,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getStartupTraceSummary = () => {
|
||||||
|
const trace = typeof window !== 'undefined' ? window.__OPENCHAMBER_STARTUP_TRACE__ ?? [] : [];
|
||||||
|
const readyIndex = trace.findIndex((event) => event.name === 'ModelControls:ready');
|
||||||
|
const endIndex = readyIndex >= 0 ? Math.min(trace.length, readyIndex + 8) : trace.length;
|
||||||
|
return trace.slice(0, endIndex).filter((event) => (
|
||||||
|
event.name.includes('checkConnection')
|
||||||
|
|| event.name.includes('checkHealth')
|
||||||
|
|| event.name.includes('initializeApp')
|
||||||
|
|| event.name.includes('initApp')
|
||||||
|
|| event.name.includes('loadProviders')
|
||||||
|
|| event.name.includes('loadAgents')
|
||||||
|
|| event.name.includes('config.defaults')
|
||||||
|
|| event.name.includes('modelsMetadata')
|
||||||
|
|| event.name.includes('ModelControls')
|
||||||
|
|| event.name.includes('activateDirectory')
|
||||||
|
|| event.name.includes('opencodeClient:setDirectory')
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
(window as typeof window & { __OPENCHAMBER_STARTUP_TRACE_SUMMARY__?: typeof getStartupTraceSummary })
|
||||||
|
.__OPENCHAMBER_STARTUP_TRACE_SUMMARY__ = getStartupTraceSummary;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const measureStartupTrace = async <T>(
|
||||||
|
name: string,
|
||||||
|
fn: () => Promise<T>,
|
||||||
|
data?: Record<string, unknown>,
|
||||||
|
): Promise<T> => {
|
||||||
|
const started = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
||||||
|
markStartupTrace(`${name}:start`, data);
|
||||||
|
try {
|
||||||
|
const result = await fn();
|
||||||
|
const ended = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
||||||
|
markStartupTrace(`${name}:end`, { durationMs: Math.round(ended - started) });
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
const ended = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
||||||
|
markStartupTrace(`${name}:error`, {
|
||||||
|
durationMs: Math.round(ended - started),
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -620,10 +620,10 @@ async function performConfigRefresh(options: {
|
|||||||
const sdkRefreshTasks: Promise<void>[] = [];
|
const sdkRefreshTasks: Promise<void>[] = [];
|
||||||
for (const directory of directoriesToRefresh) {
|
for (const directory of directoriesToRefresh) {
|
||||||
if (refreshProviders) {
|
if (refreshProviders) {
|
||||||
sdkRefreshTasks.push(configStore.loadProviders({ directory }).then(() => undefined));
|
sdkRefreshTasks.push(configStore.loadProviders({ directory, source: 'agentsStore:refreshConfig' }).then(() => undefined));
|
||||||
}
|
}
|
||||||
if (refreshSdkAgents) {
|
if (refreshSdkAgents) {
|
||||||
sdkRefreshTasks.push(configStore.loadAgents({ directory }).then(() => undefined));
|
sdkRefreshTasks.push(configStore.loadAgents({ directory, source: 'agentsStore:refreshConfig' }).then(() => undefined));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { useDirectoryStore } from "@/stores/useDirectoryStore";
|
|||||||
import { streamDebugEnabled } from "@/stores/utils/streamDebug";
|
import { streamDebugEnabled } from "@/stores/utils/streamDebug";
|
||||||
import { parseModelIdentifier } from "@/lib/modelIdentifier";
|
import { parseModelIdentifier } from "@/lib/modelIdentifier";
|
||||||
import { runtimeFetch } from "@/lib/runtime-fetch";
|
import { runtimeFetch } from "@/lib/runtime-fetch";
|
||||||
|
import { markStartupTrace, measureStartupTrace } from "@/lib/startupTrace";
|
||||||
|
|
||||||
const MODELS_DEV_API_URL = "https://models.dev/api.json";
|
const MODELS_DEV_API_URL = "https://models.dev/api.json";
|
||||||
const MODELS_DEV_PROXY_URL = "/api/openchamber/models-metadata";
|
const MODELS_DEV_PROXY_URL = "/api/openchamber/models-metadata";
|
||||||
@@ -61,6 +62,18 @@ interface OpenChamberDefaults {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fetchOpenChamberDefaults = async (): Promise<OpenChamberDefaults> => {
|
const fetchOpenChamberDefaults = async (): Promise<OpenChamberDefaults> => {
|
||||||
|
markStartupTrace('config.defaults:start');
|
||||||
|
const started = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
||||||
|
const finish = (source: string, result: OpenChamberDefaults) => {
|
||||||
|
const ended = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
||||||
|
markStartupTrace('config.defaults:end', {
|
||||||
|
source,
|
||||||
|
durationMs: Math.round(ended - started),
|
||||||
|
hasDefaultModel: Boolean(result.defaultModel),
|
||||||
|
hasDefaultAgent: Boolean(result.defaultAgent),
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
};
|
||||||
try {
|
try {
|
||||||
// 1. Runtime settings API (VSCode)
|
// 1. Runtime settings API (VSCode)
|
||||||
const runtimeSettings = getRegisteredRuntimeAPIs()?.settings;
|
const runtimeSettings = getRegisteredRuntimeAPIs()?.settings;
|
||||||
@@ -86,7 +99,7 @@ const fetchOpenChamberDefaults = async (): Promise<OpenChamberDefaults> => {
|
|||||||
const sttSilenceThresholdDb = normalizeSttSilenceThresholdDb(data?.sttSilenceThresholdDb);
|
const sttSilenceThresholdDb = normalizeSttSilenceThresholdDb(data?.sttSilenceThresholdDb);
|
||||||
const sttSilenceHoldMs = normalizeSttSilenceHoldMs(data?.sttSilenceHoldMs);
|
const sttSilenceHoldMs = normalizeSttSilenceHoldMs(data?.sttSilenceHoldMs);
|
||||||
|
|
||||||
return {
|
return finish('runtime-settings', {
|
||||||
defaultModel: defaultModel.length > 0 ? defaultModel : undefined,
|
defaultModel: defaultModel.length > 0 ? defaultModel : undefined,
|
||||||
defaultVariant: defaultVariant.length > 0 ? defaultVariant : undefined,
|
defaultVariant: defaultVariant.length > 0 ? defaultVariant : undefined,
|
||||||
defaultAgent: defaultAgent.length > 0 ? defaultAgent : undefined,
|
defaultAgent: defaultAgent.length > 0 ? defaultAgent : undefined,
|
||||||
@@ -101,7 +114,7 @@ const fetchOpenChamberDefaults = async (): Promise<OpenChamberDefaults> => {
|
|||||||
sttLanguage,
|
sttLanguage,
|
||||||
sttSilenceThresholdDb,
|
sttSilenceThresholdDb,
|
||||||
sttSilenceHoldMs,
|
sttSilenceHoldMs,
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Fall through to fetch
|
// Fall through to fetch
|
||||||
@@ -114,7 +127,7 @@ const fetchOpenChamberDefaults = async (): Promise<OpenChamberDefaults> => {
|
|||||||
headers: { Accept: 'application/json' },
|
headers: { Accept: 'application/json' },
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
return {};
|
return finish('settings-route-not-ok', {});
|
||||||
}
|
}
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const defaultModel = typeof data?.defaultModel === 'string' ? data.defaultModel.trim() : '';
|
const defaultModel = typeof data?.defaultModel === 'string' ? data.defaultModel.trim() : '';
|
||||||
@@ -134,7 +147,7 @@ const fetchOpenChamberDefaults = async (): Promise<OpenChamberDefaults> => {
|
|||||||
const sttSilenceThresholdDb = normalizeSttSilenceThresholdDb(data?.sttSilenceThresholdDb);
|
const sttSilenceThresholdDb = normalizeSttSilenceThresholdDb(data?.sttSilenceThresholdDb);
|
||||||
const sttSilenceHoldMs = normalizeSttSilenceHoldMs(data?.sttSilenceHoldMs);
|
const sttSilenceHoldMs = normalizeSttSilenceHoldMs(data?.sttSilenceHoldMs);
|
||||||
|
|
||||||
return {
|
return finish('settings-route', {
|
||||||
defaultModel: defaultModel.length > 0 ? defaultModel : undefined,
|
defaultModel: defaultModel.length > 0 ? defaultModel : undefined,
|
||||||
defaultVariant: defaultVariant.length > 0 ? defaultVariant : undefined,
|
defaultVariant: defaultVariant.length > 0 ? defaultVariant : undefined,
|
||||||
defaultAgent: defaultAgent.length > 0 ? defaultAgent : undefined,
|
defaultAgent: defaultAgent.length > 0 ? defaultAgent : undefined,
|
||||||
@@ -149,9 +162,10 @@ const fetchOpenChamberDefaults = async (): Promise<OpenChamberDefaults> => {
|
|||||||
sttLanguage,
|
sttLanguage,
|
||||||
sttSilenceThresholdDb,
|
sttSilenceThresholdDb,
|
||||||
sttSilenceHoldMs,
|
sttSilenceHoldMs,
|
||||||
};
|
});
|
||||||
} catch {
|
} catch (error) {
|
||||||
return {};
|
markStartupTrace('config.defaults:error', { error: error instanceof Error ? error.message : String(error) });
|
||||||
|
return finish('error', {});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -458,9 +472,11 @@ const ensureModelsMetadataFetch = (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
modelsMetadataInFlight = fetchModelsDevMetadata()
|
markStartupTrace('modelsMetadata:queued');
|
||||||
|
modelsMetadataInFlight = measureStartupTrace('modelsMetadata', fetchModelsDevMetadata)
|
||||||
.then((metadata) => {
|
.then((metadata) => {
|
||||||
if (metadata.size > 0) {
|
if (metadata.size > 0) {
|
||||||
|
markStartupTrace('modelsMetadata:set', { entries: metadata.size });
|
||||||
setModelsMetadata(metadata);
|
setModelsMetadata(metadata);
|
||||||
}
|
}
|
||||||
return metadata;
|
return metadata;
|
||||||
@@ -602,8 +618,8 @@ interface ConfigStore {
|
|||||||
|
|
||||||
activateDirectory: (directory: string | null | undefined) => Promise<void>;
|
activateDirectory: (directory: string | null | undefined) => Promise<void>;
|
||||||
|
|
||||||
loadProviders: (options?: { directory?: string | null }) => Promise<void>;
|
loadProviders: (options?: { directory?: string | null; source?: string }) => Promise<void>;
|
||||||
loadAgents: (options?: { directory?: string | null }) => Promise<boolean>;
|
loadAgents: (options?: { directory?: string | null; source?: string }) => Promise<boolean>;
|
||||||
invalidateModelMetadataCache: () => void;
|
invalidateModelMetadataCache: () => void;
|
||||||
setProvider: (providerId: string) => void;
|
setProvider: (providerId: string) => void;
|
||||||
setModel: (modelId: string) => void;
|
setModel: (modelId: string) => void;
|
||||||
@@ -906,10 +922,14 @@ export const useConfigStore = create<ConfigStore>()(
|
|||||||
})(),
|
})(),
|
||||||
activateDirectory: async (directory) => {
|
activateDirectory: async (directory) => {
|
||||||
const directoryKey = toDirectoryKey(directory);
|
const directoryKey = toDirectoryKey(directory);
|
||||||
|
let snapshotHadProviders = false;
|
||||||
|
let snapshotHadAgents = false;
|
||||||
|
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const snapshot = state.directoryScoped[directoryKey];
|
const snapshot = state.directoryScoped[directoryKey];
|
||||||
if (snapshot) {
|
if (snapshot) {
|
||||||
|
snapshotHadProviders = snapshot.providers.length > 0;
|
||||||
|
snapshotHadAgents = snapshot.agents.length > 0;
|
||||||
return {
|
return {
|
||||||
activeDirectoryKey: directoryKey,
|
activeDirectoryKey: directoryKey,
|
||||||
providers: snapshot.providers,
|
providers: snapshot.providers,
|
||||||
@@ -941,18 +961,36 @@ export const useConfigStore = create<ConfigStore>()(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await get().loadProviders({ directory: fromDirectoryKey(directoryKey) });
|
if (snapshotHadProviders) {
|
||||||
await get().loadAgents({ directory: fromDirectoryKey(directoryKey) });
|
markStartupTrace('activateDirectory:skipProviders', { directoryKey });
|
||||||
|
} else {
|
||||||
|
await get().loadProviders({ directory: fromDirectoryKey(directoryKey), source: 'activateDirectory' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshotHadAgents) {
|
||||||
|
markStartupTrace('activateDirectory:skipAgents', { directoryKey });
|
||||||
|
} else {
|
||||||
|
await get().loadAgents({ directory: fromDirectoryKey(directoryKey), source: 'activateDirectory' });
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
loadProviders: async (options) => {
|
loadProviders: async (options) => {
|
||||||
const directoryKey = toDirectoryKey(options?.directory ?? fromDirectoryKey(get().activeDirectoryKey));
|
const requestedDirectory = options?.directory ?? fromDirectoryKey(get().activeDirectoryKey);
|
||||||
|
const effectiveDirectory = requestedDirectory ?? opencodeClient.getDirectory() ?? null;
|
||||||
|
const directoryKey = toDirectoryKey(requestedDirectory);
|
||||||
|
const source = options?.source ?? 'unknown';
|
||||||
|
markStartupTrace('loadProviders:called', { directoryKey, source, requestedDirectory, effectiveDirectory });
|
||||||
|
|
||||||
// Dedup: if a load is already in-flight for this directory, reuse it
|
// Dedup: if a load is already in-flight for this directory, reuse it
|
||||||
const existing = _inFlightProviders.get(directoryKey);
|
const existing = _inFlightProviders.get(directoryKey);
|
||||||
if (existing) return existing;
|
if (existing) {
|
||||||
|
markStartupTrace('loadProviders:deduped', { directoryKey, source, requestedDirectory, effectiveDirectory });
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
const promise = (async () => {
|
const promise = (async () => {
|
||||||
|
const loaderStarted = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
||||||
|
markStartupTrace('loadProviders:start', { directoryKey, source, requestedDirectory, effectiveDirectory });
|
||||||
const existingSnapshot = get().directoryScoped[directoryKey];
|
const existingSnapshot = get().directoryScoped[directoryKey];
|
||||||
const previousProviders = existingSnapshot?.providers ?? (get().activeDirectoryKey === directoryKey ? get().providers : []);
|
const previousProviders = existingSnapshot?.providers ?? (get().activeDirectoryKey === directoryKey ? get().providers : []);
|
||||||
const previousDefaults = existingSnapshot?.defaultProviders ?? (get().activeDirectoryKey === directoryKey ? get().defaultProviders : {});
|
const previousDefaults = existingSnapshot?.defaultProviders ?? (get().activeDirectoryKey === directoryKey ? get().defaultProviders : {});
|
||||||
@@ -964,9 +1002,13 @@ export const useConfigStore = create<ConfigStore>()(
|
|||||||
() => get().modelsMetadata,
|
() => get().modelsMetadata,
|
||||||
(metadata) => set({ modelsMetadata: metadata }),
|
(metadata) => set({ modelsMetadata: metadata }),
|
||||||
);
|
);
|
||||||
const apiResult = await opencodeClient.withDirectory(
|
const apiResult = await measureStartupTrace(
|
||||||
fromDirectoryKey(directoryKey),
|
'loadProviders:api',
|
||||||
() => opencodeClient.getProviders()
|
() => opencodeClient.withDirectory(
|
||||||
|
fromDirectoryKey(directoryKey),
|
||||||
|
() => opencodeClient.getProviders()
|
||||||
|
),
|
||||||
|
{ directoryKey, source, requestedDirectory, effectiveDirectory, attempt: attempt + 1 },
|
||||||
);
|
);
|
||||||
const providers = Array.isArray(apiResult?.providers) ? apiResult.providers : [];
|
const providers = Array.isArray(apiResult?.providers) ? apiResult.providers : [];
|
||||||
const defaults = apiResult?.default || {};
|
const defaults = apiResult?.default || {};
|
||||||
@@ -1036,15 +1078,40 @@ export const useConfigStore = create<ConfigStore>()(
|
|||||||
return nextState;
|
return nextState;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const loaderEnded = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
||||||
|
markStartupTrace('loadProviders:end', {
|
||||||
|
directoryKey,
|
||||||
|
source,
|
||||||
|
requestedDirectory,
|
||||||
|
effectiveDirectory,
|
||||||
|
durationMs: Math.round(loaderEnded - loaderStarted),
|
||||||
|
providers: processedProviders.length,
|
||||||
|
models: processedProviders.reduce((count, provider) => count + provider.models.length, 0),
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
lastError = error;
|
lastError = error;
|
||||||
|
markStartupTrace('loadProviders:attemptError', {
|
||||||
|
directoryKey,
|
||||||
|
source,
|
||||||
|
requestedDirectory,
|
||||||
|
effectiveDirectory,
|
||||||
|
attempt: attempt + 1,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
const waitMs = 200 * (attempt + 1);
|
const waitMs = 200 * (attempt + 1);
|
||||||
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error("Failed to load providers:", lastError);
|
console.error("Failed to load providers:", lastError);
|
||||||
|
markStartupTrace('loadProviders:error', {
|
||||||
|
directoryKey,
|
||||||
|
source,
|
||||||
|
requestedDirectory,
|
||||||
|
effectiveDirectory,
|
||||||
|
error: lastError instanceof Error ? lastError.message : String(lastError),
|
||||||
|
});
|
||||||
|
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const baseSnapshot: DirectoryScopedConfig = state.directoryScoped[directoryKey] ?? {
|
const baseSnapshot: DirectoryScopedConfig = state.directoryScoped[directoryKey] ?? {
|
||||||
@@ -1314,13 +1381,22 @@ export const useConfigStore = create<ConfigStore>()(
|
|||||||
},
|
},
|
||||||
|
|
||||||
loadAgents: async (options) => {
|
loadAgents: async (options) => {
|
||||||
const directoryKey = toDirectoryKey(options?.directory ?? fromDirectoryKey(get().activeDirectoryKey));
|
const requestedDirectory = options?.directory ?? fromDirectoryKey(get().activeDirectoryKey);
|
||||||
|
const effectiveDirectory = requestedDirectory ?? opencodeClient.getDirectory() ?? null;
|
||||||
|
const directoryKey = toDirectoryKey(requestedDirectory);
|
||||||
|
const source = options?.source ?? 'unknown';
|
||||||
|
markStartupTrace('loadAgents:called', { directoryKey, source, requestedDirectory, effectiveDirectory });
|
||||||
|
|
||||||
// Dedup: if a load is already in-flight for this directory, reuse it
|
// Dedup: if a load is already in-flight for this directory, reuse it
|
||||||
const existing = _inFlightAgents.get(directoryKey);
|
const existing = _inFlightAgents.get(directoryKey);
|
||||||
if (existing) return existing;
|
if (existing) {
|
||||||
|
markStartupTrace('loadAgents:deduped', { directoryKey, source, requestedDirectory, effectiveDirectory });
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
const promise = (async (): Promise<boolean> => {
|
const promise = (async (): Promise<boolean> => {
|
||||||
|
const loaderStarted = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
||||||
|
markStartupTrace('loadAgents:start', { directoryKey, source, requestedDirectory, effectiveDirectory });
|
||||||
const existingSnapshot = get().directoryScoped[directoryKey];
|
const existingSnapshot = get().directoryScoped[directoryKey];
|
||||||
const previousAgents = existingSnapshot?.agents ?? (get().activeDirectoryKey === directoryKey ? get().agents : []);
|
const previousAgents = existingSnapshot?.agents ?? (get().activeDirectoryKey === directoryKey ? get().agents : []);
|
||||||
let lastError: unknown = null;
|
let lastError: unknown = null;
|
||||||
@@ -1329,12 +1405,22 @@ export const useConfigStore = create<ConfigStore>()(
|
|||||||
try {
|
try {
|
||||||
// Fetch agents and OpenChamber settings in parallel
|
// Fetch agents and OpenChamber settings in parallel
|
||||||
const [agents, openChamberDefaults] = await Promise.all([
|
const [agents, openChamberDefaults] = await Promise.all([
|
||||||
opencodeClient.withDirectory(fromDirectoryKey(directoryKey), () => opencodeClient.listAgents()),
|
measureStartupTrace(
|
||||||
|
'loadAgents:api',
|
||||||
|
() => opencodeClient.withDirectory(fromDirectoryKey(directoryKey), () => opencodeClient.listAgents()),
|
||||||
|
{ directoryKey, source, requestedDirectory, effectiveDirectory, attempt: attempt + 1 },
|
||||||
|
),
|
||||||
fetchOpenChamberDefaults(),
|
fetchOpenChamberDefaults(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const safeAgents = Array.isArray(agents) ? agents : [];
|
const safeAgents = Array.isArray(agents) ? agents : [];
|
||||||
|
|
||||||
|
const providerLoad = _inFlightProviders.get(directoryKey);
|
||||||
|
if (providerLoad) {
|
||||||
|
markStartupTrace('loadAgents:awaitProviders', { directoryKey, source });
|
||||||
|
await providerLoad;
|
||||||
|
}
|
||||||
|
|
||||||
const providers = get().activeDirectoryKey === directoryKey
|
const providers = get().activeDirectoryKey === directoryKey
|
||||||
? get().providers
|
? get().providers
|
||||||
: (get().directoryScoped[directoryKey]?.providers ?? []);
|
: (get().directoryScoped[directoryKey]?.providers ?? []);
|
||||||
@@ -1452,6 +1538,15 @@ export const useConfigStore = create<ConfigStore>()(
|
|||||||
return nextState;
|
return nextState;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const loaderEnded = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
||||||
|
markStartupTrace('loadAgents:end', {
|
||||||
|
directoryKey,
|
||||||
|
source,
|
||||||
|
requestedDirectory,
|
||||||
|
effectiveDirectory,
|
||||||
|
durationMs: Math.round(loaderEnded - loaderStarted),
|
||||||
|
agents: safeAgents.length,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1592,15 +1687,39 @@ export const useConfigStore = create<ConfigStore>()(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loaderEnded = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
||||||
|
markStartupTrace('loadAgents:end', {
|
||||||
|
directoryKey,
|
||||||
|
source,
|
||||||
|
requestedDirectory,
|
||||||
|
effectiveDirectory,
|
||||||
|
durationMs: Math.round(loaderEnded - loaderStarted),
|
||||||
|
agents: safeAgents.length,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
lastError = error;
|
lastError = error;
|
||||||
|
markStartupTrace('loadAgents:attemptError', {
|
||||||
|
directoryKey,
|
||||||
|
source,
|
||||||
|
requestedDirectory,
|
||||||
|
effectiveDirectory,
|
||||||
|
attempt: attempt + 1,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
const waitMs = 200 * (attempt + 1);
|
const waitMs = 200 * (attempt + 1);
|
||||||
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error("Failed to load agents:", lastError);
|
console.error("Failed to load agents:", lastError);
|
||||||
|
markStartupTrace('loadAgents:error', {
|
||||||
|
directoryKey,
|
||||||
|
source,
|
||||||
|
requestedDirectory,
|
||||||
|
effectiveDirectory,
|
||||||
|
error: lastError instanceof Error ? lastError.message : String(lastError),
|
||||||
|
});
|
||||||
|
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const providers = state.activeDirectoryKey === directoryKey
|
const providers = state.activeDirectoryKey === directoryKey
|
||||||
@@ -2064,13 +2183,31 @@ export const useConfigStore = create<ConfigStore>()(
|
|||||||
},
|
},
|
||||||
|
|
||||||
checkConnection: async () => {
|
checkConnection: async () => {
|
||||||
|
markStartupTrace('checkConnection:start');
|
||||||
const maxAttempts = 5;
|
const maxAttempts = 5;
|
||||||
let attempt = 0;
|
let attempt = 0;
|
||||||
let lastError: unknown = null;
|
let lastError: unknown = null;
|
||||||
|
|
||||||
while (attempt < maxAttempts) {
|
while (attempt < maxAttempts) {
|
||||||
try {
|
try {
|
||||||
const isHealthy = await opencodeClient.checkHealth();
|
markStartupTrace('checkConnection:attempt', { attempt: attempt + 1 });
|
||||||
|
const isHealthy = await measureStartupTrace(
|
||||||
|
'checkConnection:health',
|
||||||
|
() => opencodeClient.checkHealth(),
|
||||||
|
{ attempt: attempt + 1 },
|
||||||
|
);
|
||||||
|
if (!isHealthy && attempt < maxAttempts - 1) {
|
||||||
|
const hasEverConnected = get().hasEverConnected;
|
||||||
|
set({
|
||||||
|
isConnected: false,
|
||||||
|
connectionPhase: hasEverConnected ? "reconnecting" : "connecting",
|
||||||
|
lastDisconnectReason: 'health_check_unhealthy',
|
||||||
|
});
|
||||||
|
attempt += 1;
|
||||||
|
await sleep(400 * attempt);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const hasEverConnected = get().hasEverConnected;
|
const hasEverConnected = get().hasEverConnected;
|
||||||
set(isHealthy
|
set(isHealthy
|
||||||
? { isConnected: true, hasEverConnected: true, connectionPhase: "connected" }
|
? { isConnected: true, hasEverConnected: true, connectionPhase: "connected" }
|
||||||
@@ -2079,6 +2216,7 @@ export const useConfigStore = create<ConfigStore>()(
|
|||||||
connectionPhase: hasEverConnected ? "reconnecting" : "connecting",
|
connectionPhase: hasEverConnected ? "reconnecting" : "connecting",
|
||||||
lastDisconnectReason: 'health_check_unhealthy',
|
lastDisconnectReason: 'health_check_unhealthy',
|
||||||
});
|
});
|
||||||
|
markStartupTrace('checkConnection:end', { healthy: isHealthy, attempts: attempt + 1 });
|
||||||
return isHealthy;
|
return isHealthy;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
lastError = error;
|
lastError = error;
|
||||||
@@ -2096,15 +2234,19 @@ export const useConfigStore = create<ConfigStore>()(
|
|||||||
connectionPhase: get().hasEverConnected ? "reconnecting" : "connecting",
|
connectionPhase: get().hasEverConnected ? "reconnecting" : "connecting",
|
||||||
lastDisconnectReason: 'health_check_failed',
|
lastDisconnectReason: 'health_check_failed',
|
||||||
});
|
});
|
||||||
|
markStartupTrace('checkConnection:end', { healthy: false, attempts: maxAttempts });
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
initializeApp: async () => {
|
initializeApp: async () => {
|
||||||
if (_initializeAppInFlight) {
|
if (_initializeAppInFlight) {
|
||||||
|
markStartupTrace('initializeApp:deduped');
|
||||||
return _initializeAppInFlight;
|
return _initializeAppInFlight;
|
||||||
}
|
}
|
||||||
|
|
||||||
const run = (async () => {
|
const run = (async () => {
|
||||||
|
const initStarted = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
||||||
|
markStartupTrace('initializeApp:start');
|
||||||
try {
|
try {
|
||||||
const debug = streamDebugEnabled();
|
const debug = streamDebugEnabled();
|
||||||
if (debug) console.log("Starting app initialization...");
|
if (debug) console.log("Starting app initialization...");
|
||||||
@@ -2123,15 +2265,21 @@ export const useConfigStore = create<ConfigStore>()(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (debug) console.log("Initializing app...");
|
if (debug) console.log("Initializing app...");
|
||||||
await opencodeClient.initApp();
|
markStartupTrace('initApp:skipped', { reason: 'checkConnection already verified health' });
|
||||||
|
|
||||||
if (debug) console.log("Loading providers...");
|
if (debug) console.log("Loading providers and agents...");
|
||||||
await get().loadProviders();
|
await Promise.all([
|
||||||
|
get().loadProviders({ source: 'initializeApp' }),
|
||||||
if (debug) console.log("Loading agents...");
|
get().loadAgents({ source: 'initializeApp' }),
|
||||||
await get().loadAgents();
|
]);
|
||||||
|
|
||||||
set({ isInitialized: true, isConnected: true, hasEverConnected: true, connectionPhase: "connected" });
|
set({ isInitialized: true, isConnected: true, hasEverConnected: true, connectionPhase: "connected" });
|
||||||
|
const initEnded = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
||||||
|
markStartupTrace('initializeApp:end', {
|
||||||
|
durationMs: Math.round(initEnded - initStarted),
|
||||||
|
providers: get().providers.length,
|
||||||
|
agents: get().agents.length,
|
||||||
|
});
|
||||||
if (debug) console.log("App initialized successfully");
|
if (debug) console.log("App initialized successfully");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to initialize app:", error);
|
console.error("Failed to initialize app:", error);
|
||||||
@@ -2141,6 +2289,7 @@ export const useConfigStore = create<ConfigStore>()(
|
|||||||
connectionPhase: get().hasEverConnected ? "reconnecting" : "connecting",
|
connectionPhase: get().hasEverConnected ? "reconnecting" : "connecting",
|
||||||
lastDisconnectReason: 'init_error',
|
lastDisconnectReason: 'init_error',
|
||||||
});
|
});
|
||||||
|
markStartupTrace('initializeApp:error', { error: error instanceof Error ? error.message : String(error) });
|
||||||
}
|
}
|
||||||
})().finally(() => {
|
})().finally(() => {
|
||||||
_initializeAppInFlight = null;
|
_initializeAppInFlight = null;
|
||||||
@@ -2235,16 +2384,16 @@ let unsubscribeConfigStoreChanges: (() => void) | null = null;
|
|||||||
|
|
||||||
if (!unsubscribeConfigStoreChanges) {
|
if (!unsubscribeConfigStoreChanges) {
|
||||||
unsubscribeConfigStoreChanges = subscribeToConfigChanges(async (event) => {
|
unsubscribeConfigStoreChanges = subscribeToConfigChanges(async (event) => {
|
||||||
const tasks: Promise<void>[] = [];
|
const tasks: Promise<void>[] = [];
|
||||||
|
|
||||||
if (scopeMatches(event, "agents")) {
|
if (scopeMatches(event, "agents")) {
|
||||||
const { loadAgents } = useConfigStore.getState();
|
const { loadAgents } = useConfigStore.getState();
|
||||||
tasks.push(loadAgents().then(() => {}));
|
tasks.push(loadAgents({ source: 'configChange:agents' }).then(() => {}));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scopeMatches(event, "providers")) {
|
if (scopeMatches(event, "providers")) {
|
||||||
const { loadProviders } = useConfigStore.getState();
|
const { loadProviders } = useConfigStore.getState();
|
||||||
tasks.push(loadProviders());
|
tasks.push(loadProviders({ source: 'configChange:providers' }));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tasks.length > 0) {
|
if (tasks.length > 0) {
|
||||||
@@ -2263,6 +2412,7 @@ if (typeof window !== "undefined" && !unsubscribeConfigStoreDirectoryChanges) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
markStartupTrace('directoryStore:changed', { previous: prevKey, next: nextKey });
|
||||||
void useConfigStore.getState().activateDirectory(state.currentDirectory);
|
void useConfigStore.getState().activateDirectory(state.currentDirectory);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -241,6 +241,32 @@ export async function handleSystemBridgeMessage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'api:opencode/version': {
|
||||||
|
try {
|
||||||
|
const apiUrl = ctx?.manager?.getApiUrl();
|
||||||
|
if (!apiUrl) {
|
||||||
|
return { id, type, success: true, data: { version: null, error: 'OpenCode manager unavailable' } };
|
||||||
|
}
|
||||||
|
const base = `${apiUrl.replace(/\/+$/, '')}/`;
|
||||||
|
const response = await fetch(new URL('global/health', base).toString(), {
|
||||||
|
method: 'GET',
|
||||||
|
headers: { Accept: 'application/json', ...ctx?.manager?.getOpenCodeAuthHeaders() },
|
||||||
|
});
|
||||||
|
const health = await response.json().catch(() => null) as { version?: unknown; error?: unknown } | null;
|
||||||
|
if (!response.ok) {
|
||||||
|
const message = typeof health?.error === 'string' ? health.error : response.statusText || 'Failed to read OpenCode version';
|
||||||
|
return { id, type, success: true, data: { version: null, error: message } };
|
||||||
|
}
|
||||||
|
const version = typeof health?.version === 'string' && health.version.trim().length > 0
|
||||||
|
? health.version.trim().replace(/^v/, '')
|
||||||
|
: null;
|
||||||
|
return { id, type, success: true, data: { version } };
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
return { id, type, success: true, data: { version: null, error: errorMessage } };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
case 'api:session-activity:get': {
|
case 'api:session-activity:get': {
|
||||||
return { id, type, success: true, data: getSessionActivitySnapshot() };
|
return { id, type, success: true, data: getSessionActivitySnapshot() };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -547,7 +547,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const probeTargets: Array<{ label: string; path: string; includeDirectory?: boolean; timeoutMs?: number }> = [
|
const probeTargets: Array<{ label: string; path: string; includeDirectory?: boolean; timeoutMs?: number }> = [
|
||||||
{ label: 'health', path: '/global/health', includeDirectory: false },
|
{ label: 'health', path: '/api/health', includeDirectory: false },
|
||||||
{ label: 'config', path: '/config', includeDirectory: true },
|
{ label: 'config', path: '/config', includeDirectory: true },
|
||||||
{ label: 'providers', path: '/config/providers', includeDirectory: true },
|
{ label: 'providers', path: '/config/providers', includeDirectory: true },
|
||||||
// Can be slower on large configs; keep the probe from producing false negatives.
|
// Can be slower on large configs; keep the probe from producing false negatives.
|
||||||
|
|||||||
@@ -568,7 +568,7 @@ async function waitForReady(
|
|||||||
const timeout = setTimeout(() => controller.abort(), 3000);
|
const timeout = setTimeout(() => controller.abort(), 3000);
|
||||||
|
|
||||||
// OpenCode readiness check.
|
// OpenCode readiness check.
|
||||||
const url = new URL(`${baseUrl}/global/health`);
|
const url = new URL(`${baseUrl}/api/health`);
|
||||||
const res = await fetch(url.toString(), {
|
const res = await fetch(url.toString(), {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: { Accept: 'application/json', ...authHeaders },
|
headers: { Accept: 'application/json', ...authHeaders },
|
||||||
|
|||||||
@@ -968,6 +968,24 @@ const handleLocalApiRequest = async (input: RequestInfo | URL, url: URL, init: R
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pathname === '/api/opencode/version' && method === 'GET') {
|
||||||
|
try {
|
||||||
|
const data = await sendBridgeMessage('api:opencode/version');
|
||||||
|
return new Response(JSON.stringify(data), { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
return new Response(JSON.stringify({ version: null, error: message }), { status: 502, headers: { 'Content-Type': 'application/json' } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathname === '/api/opencode/health' && method === 'GET') {
|
||||||
|
const connectionStatus = window.__OPENCHAMBER_CONNECTION__?.status;
|
||||||
|
return new Response(JSON.stringify({ healthy: connectionStatus === 'connected' }), {
|
||||||
|
status: 200,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (pathname === '/api/zen/models' && method === 'GET') {
|
if (pathname === '/api/zen/models' && method === 'GET') {
|
||||||
try {
|
try {
|
||||||
const data = await sendBridgeMessage('api:zen:models');
|
const data = await sendBridgeMessage('api:zen:models');
|
||||||
|
|||||||
@@ -392,7 +392,7 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(buildOpenCodeUrl('/global/health', ''), {
|
const response = await fetch(buildOpenCodeUrl('/api/health', ''), {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
@@ -417,7 +417,7 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
|
|||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeout = setTimeout(() => controller.abort(), 3000);
|
const timeout = setTimeout(() => controller.abort(), 3000);
|
||||||
const base = origin ?? `http://127.0.0.1:${port}`;
|
const base = origin ?? `http://127.0.0.1:${port}`;
|
||||||
const response = await fetch(`${base}/global/health`, {
|
const response = await fetch(`${base}/api/health`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
@@ -659,51 +659,40 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
|
|||||||
let lastError = null;
|
let lastError = null;
|
||||||
|
|
||||||
while (Date.now() < deadline) {
|
while (Date.now() < deadline) {
|
||||||
|
let timeout = null;
|
||||||
try {
|
try {
|
||||||
const [configResult, agentResult] = await Promise.all([
|
const controller = new AbortController();
|
||||||
fetch(buildOpenCodeUrl('/config', ''), {
|
timeout = setTimeout(() => controller.abort(), HEALTH_CHECK_TIMEOUT_MS);
|
||||||
method: 'GET',
|
const response = await fetch(buildOpenCodeUrl('/api/health', ''), {
|
||||||
headers: { Accept: 'application/json', ...getOpenCodeAuthHeaders() },
|
method: 'GET',
|
||||||
}).catch((error) => error),
|
headers: { Accept: 'application/json', ...getOpenCodeAuthHeaders() },
|
||||||
fetch(buildOpenCodeUrl('/agent', ''), {
|
signal: controller.signal,
|
||||||
method: 'GET',
|
});
|
||||||
headers: { Accept: 'application/json', ...getOpenCodeAuthHeaders() },
|
clearTimeout(timeout);
|
||||||
}).catch((error) => error),
|
timeout = null;
|
||||||
]);
|
|
||||||
|
|
||||||
if (configResult instanceof Error) {
|
if (!response.ok) {
|
||||||
lastError = configResult;
|
lastError = new Error(`OpenCode health endpoint responded with status ${response.status}`);
|
||||||
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!configResult.ok) {
|
const body = await response.json().catch(() => null);
|
||||||
lastError = new Error(`OpenCode config endpoint responded with status ${configResult.status}`);
|
if (body?.healthy !== true) {
|
||||||
|
lastError = new Error('OpenCode health endpoint returned unhealthy response');
|
||||||
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
await configResult.json().catch(() => null);
|
|
||||||
|
|
||||||
if (agentResult instanceof Error) {
|
|
||||||
lastError = agentResult;
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!agentResult.ok) {
|
|
||||||
lastError = new Error(`Agent endpoint responded with status ${agentResult.status}`);
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
await agentResult.json().catch(() => []);
|
|
||||||
|
|
||||||
state.isOpenCodeReady = true;
|
state.isOpenCodeReady = true;
|
||||||
state.lastOpenCodeError = null;
|
state.lastOpenCodeError = null;
|
||||||
return;
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
lastError = error;
|
lastError = error;
|
||||||
|
} finally {
|
||||||
|
if (timeout) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export const createOpenCodeNetworkRuntime = (deps) => {
|
|||||||
try {
|
try {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
timeout = setTimeout(() => controller.abort(), 3000);
|
timeout = setTimeout(() => controller.abort(), 3000);
|
||||||
const response = await fetch(`${url.replace(/\/+$/, '')}/global/health`, {
|
const response = await fetch(`${url.replace(/\/+$/, '')}/api/health`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
|
|||||||
@@ -212,6 +212,51 @@ export const registerOpenCodeRoutes = (app, dependencies) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.get('/api/opencode/health', async (_req, res) => {
|
||||||
|
try {
|
||||||
|
const healthResponse = await fetch(buildOpenCodeUrl('/api/health', ''), {
|
||||||
|
method: 'GET',
|
||||||
|
headers: { Accept: 'application/json', ...getOpenCodeAuthHeaders() },
|
||||||
|
});
|
||||||
|
const health = await healthResponse.json().catch(() => null);
|
||||||
|
if (!healthResponse.ok) {
|
||||||
|
return res.status(healthResponse.status).json({
|
||||||
|
healthy: false,
|
||||||
|
error: health?.error || healthResponse.statusText || 'OpenCode health check failed',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return res.json({ healthy: health?.healthy === true });
|
||||||
|
} catch (error) {
|
||||||
|
return res.status(503).json({
|
||||||
|
healthy: false,
|
||||||
|
error: error instanceof Error ? error.message : 'OpenCode health check failed',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/opencode/version', async (_req, res) => {
|
||||||
|
try {
|
||||||
|
const healthResponse = await fetch(buildOpenCodeUrl('/global/health', ''), {
|
||||||
|
method: 'GET',
|
||||||
|
headers: { Accept: 'application/json', ...getOpenCodeAuthHeaders() },
|
||||||
|
});
|
||||||
|
const health = await healthResponse.json().catch(() => null);
|
||||||
|
if (!healthResponse.ok) {
|
||||||
|
return res.status(healthResponse.status).json({
|
||||||
|
version: null,
|
||||||
|
error: health?.error || healthResponse.statusText || 'Failed to read OpenCode version',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const version = typeof health?.version === 'string' ? health.version.replace(/^v/, '') : null;
|
||||||
|
return res.json({ version });
|
||||||
|
} catch (error) {
|
||||||
|
return res.status(500).json({
|
||||||
|
version: null,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to read OpenCode version',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.put('/api/config/settings', async (req, res) => {
|
app.put('/api/config/settings', async (req, res) => {
|
||||||
console.log('[API:PUT /api/config/settings] Received request');
|
console.log('[API:PUT /api/config/settings] Received request');
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user