From a2a1cedf8d95f079cf5952644d1508651b398235 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Tue, 16 Jun 2026 19:23:38 +0300 Subject: [PATCH] perf: streamline provider and agent startup loading #185 Avoids loading the full provider catalog on startup Prewarms project config in the background Prevents duplicate worktree-scoped config requests --- AGENTS.md | 2 +- .../sections/providers/ProvidersPage.test.ts | 9 ++ .../sections/providers/ProvidersPage.tsx | 16 +- .../providers/providerAvailability.ts | 1 + packages/ui/src/lib/opencode/client.ts | 64 ++++++-- packages/ui/src/stores/useConfigStore.test.ts | 18 ++- packages/ui/src/stores/useConfigStore.ts | 140 ++++++++++++++++-- .../src/stores/useSessionFoldersStore.test.ts | 79 ++++++++++ packages/ui/src/sync/bootstrap.ts | 9 +- packages/ui/src/sync/sync-context.tsx | 21 ++- 10 files changed, 307 insertions(+), 52 deletions(-) create mode 100644 packages/ui/src/components/sections/providers/ProvidersPage.test.ts create mode 100644 packages/ui/src/components/sections/providers/providerAvailability.ts create mode 100644 packages/ui/src/stores/useSessionFoldersStore.test.ts diff --git a/AGENTS.md b/AGENTS.md index 186cc4f7..1ec26a2e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -451,7 +451,7 @@ A single store with N properties means every subscriber re-evaluates on every st ## Validation expectations -- Run `bun run type-check` and `bun run lint` before finalizing source-code changes that can affect TypeScript, runtime behavior, builds, lint rules, package resolution, or generated assets. Use a sufficiently long tool timeout for these workspace-wide checks (for example 240000ms) so successful package-level results are not lost to a tool timeout. For docs-only or isolated config-only changes, run the narrowest relevant validation instead (for example JSON/schema validation) and do not run the full checks unless the change can affect code execution. +- Run type-check/lint validation before finalizing source-code changes that can affect TypeScript, runtime behavior, builds, lint rules, package resolution, or generated assets, but keep validation scoped to the edited workspace by default. Prefer the package-level command for the package you changed (for example the relevant workspace's `type-check`/`lint`) instead of workspace-wide `bun run type-check` / `bun run lint`. Use workspace-wide checks only when the change spans multiple workspaces, shared package contracts, root tooling/config, dependency resolution, generated assets used across packages, or when a narrower command cannot cover the risk. Use a sufficiently long tool timeout for any broad checks (for example 240000ms) so successful package-level results are not lost to a tool timeout. For docs-only or isolated config-only changes, run the narrowest relevant validation instead (for example JSON/schema validation) and do not run full checks unless the change can affect code execution. - For hot-path changes, verify behavior under streaming or repeated events, not just static render. - For sync or startup changes, verify fresh load, retry/failure, and restart behavior. - For session changes, verify create, stream, abort, permission, archive/delete, and revisit flows when relevant. diff --git a/packages/ui/src/components/sections/providers/ProvidersPage.test.ts b/packages/ui/src/components/sections/providers/ProvidersPage.test.ts new file mode 100644 index 00000000..e45fb969 --- /dev/null +++ b/packages/ui/src/components/sections/providers/ProvidersPage.test.ts @@ -0,0 +1,9 @@ +import { describe, expect, test } from 'bun:test'; +import { shouldLoadAvailableProviders } from './providerAvailability'; + +describe('ProvidersPage available provider loading', () => { + test('loads available providers only in add-provider mode', () => { + expect(shouldLoadAvailableProviders(false)).toBe(false); + expect(shouldLoadAvailableProviders(true)).toBe(true); + }); +}); diff --git a/packages/ui/src/components/sections/providers/ProvidersPage.tsx b/packages/ui/src/components/sections/providers/ProvidersPage.tsx index 63aca1d2..4fb42c3d 100644 --- a/packages/ui/src/components/sections/providers/ProvidersPage.tsx +++ b/packages/ui/src/components/sections/providers/ProvidersPage.tsx @@ -23,6 +23,7 @@ import type { ModelMetadata } from '@/types'; import { getCurrentIntlLocale, useI18n } from '@/lib/i18n'; import { runtimeFetch } from '@/lib/runtime-fetch'; import { opencodeClient } from '@/lib/opencode/client'; +import { shouldLoadAvailableProviders } from './providerAvailability'; const formatCompactNumber = (value: number) => new Intl.NumberFormat(getCurrentIntlLocale(), { notation: 'compact', @@ -169,6 +170,7 @@ export const ProvidersPage: React.FC = () => { const [providerDropdownOpen, setProviderDropdownOpen] = React.useState(false); const [providerSources, setProviderSources] = React.useState>({}); const [showAuthPanel, setShowAuthPanel] = React.useState(false); + const isAddMode = selectedProviderId === ADD_PROVIDER_ID; React.useEffect(() => { if (!selectedProviderId && providers.length > 0) { @@ -177,6 +179,10 @@ export const ProvidersPage: React.FC = () => { }, [providers, selectedProviderId, setSelectedProvider]); React.useEffect(() => { + if (!isAddMode) { + return; + } + let isMounted = true; const loadAuthMethods = async () => { @@ -204,9 +210,13 @@ export const ProvidersPage: React.FC = () => { return () => { isMounted = false; }; - }, [t]); + }, [isAddMode, t]); React.useEffect(() => { + if (!shouldLoadAvailableProviders(isAddMode)) { + return; + } + let isMounted = true; const loadAvailableProviders = async () => { @@ -235,7 +245,7 @@ export const ProvidersPage: React.FC = () => { return () => { isMounted = false; }; - }, [t]); + }, [isAddMode, t]); const connectedProviderIds = React.useMemo( () => new Set(providers.map((provider) => provider.id)), @@ -482,8 +492,6 @@ export const ProvidersPage: React.FC = () => { } }; - const isAddMode = selectedProviderId === ADD_PROVIDER_ID; - if (!isAddMode && providers.length === 0) { return (
diff --git a/packages/ui/src/components/sections/providers/providerAvailability.ts b/packages/ui/src/components/sections/providers/providerAvailability.ts new file mode 100644 index 00000000..ea6f0386 --- /dev/null +++ b/packages/ui/src/components/sections/providers/providerAvailability.ts @@ -0,0 +1 @@ +export const shouldLoadAvailableProviders = (isAddMode: boolean): boolean => isAddMode; diff --git a/packages/ui/src/lib/opencode/client.ts b/packages/ui/src/lib/opencode/client.ts index b7701bbe..3ba0984d 100644 --- a/packages/ui/src/lib/opencode/client.ts +++ b/packages/ui/src/lib/opencode/client.ts @@ -229,6 +229,7 @@ class OpencodeService { private currentDirectory: string | undefined = undefined; private directoryContextQueue: Promise = Promise.resolve(); private listDirectoryInFlight: Map> = new Map(); + private configProvidersInFlight: Map> = new Map(); private listAgentsInFlight: Map> = new Map(); private listDirectoryCache: Map = new Map(); @@ -253,6 +254,8 @@ class OpencodeService { this.client = createRuntimeOpencodeClient({ baseUrl: this.baseUrl }); this.scopedClients.clear(); this.listDirectoryInFlight.clear(); + this.configProvidersInFlight.clear(); + this.listAgentsInFlight.clear(); this.listDirectoryCache.clear(); } @@ -1263,11 +1266,34 @@ class OpencodeService { providers: Provider[]; default: { [key: string]: string }; }> { - const response = await this.client.config.providers( - this.currentDirectory ? { directory: this.currentDirectory } : undefined - ); - if (!response.data) throw new Error('Failed to get providers'); - return response.data; + return this.getProvidersForConfig(this.currentDirectory); + } + + async getProvidersForConfig(directory?: string | null): Promise<{ + providers: Provider[]; + default: { [key: string]: string }; + }> { + const effectiveDirectory = this.normalizeCandidatePath(directory) ?? directory ?? this.currentDirectory ?? undefined; + const key = effectiveDirectory ?? ''; + + const existing = this.configProvidersInFlight.get(key); + if (existing) { + return existing; + } + + const request = (async () => { + const response = await this.client.config.providers( + effectiveDirectory ? { directory: effectiveDirectory } : undefined, + ); + return unwrapSdkData(response, 'config.providers'); + })(); + + this.configProvidersInFlight.set(key, request); + try { + return await request; + } finally { + this.configProvidersInFlight.delete(key); + } } // App Management - using config endpoint since /app doesn't exist in this version @@ -1308,13 +1334,29 @@ class OpencodeService { } const request = (async () => { - const response = await this.client.app.agents( - effectiveDirectory ? { directory: effectiveDirectory } : undefined - ); - if (response.error) { - throw new Error(`app.agents failed: ${formatSdkError(response.error)}`); + const params = effectiveDirectory ? { directory: effectiveDirectory } : undefined; + const response = await this.client.app.agents(params); + if (!response.error && Array.isArray(response.data) && response.data.length > 0) { + return response.data; } - return response.data || []; + + // SDK gap / endpoint drift: current OpenCode exposes the authoritative + // agent list at /agent, while app.agents can be empty on some runtimes. + const fallbackResponse = await runtimeFetch('/api/agent', { + ...(effectiveDirectory ? { query: { directory: effectiveDirectory } } : {}), + }); + if (!fallbackResponse.ok) { + if (response.error) { + throw new Error(`app.agents failed${response.response?.status ? ` (${response.response.status})` : ''}: ${formatSdkError(response.error)}`); + } + throw new Error(`agent.list failed (${fallbackResponse.status})`); + } + + const fallbackData = await fallbackResponse.json().catch(() => null) as unknown; + if (!Array.isArray(fallbackData)) { + throw new Error('agent.list failed: invalid response'); + } + return fallbackData as Agent[]; })(); this.listAgentsInFlight.set(key, request); diff --git a/packages/ui/src/stores/useConfigStore.test.ts b/packages/ui/src/stores/useConfigStore.test.ts index 22636027..7bb5ade3 100644 --- a/packages/ui/src/stores/useConfigStore.test.ts +++ b/packages/ui/src/stores/useConfigStore.test.ts @@ -106,6 +106,18 @@ mock.module('@/stores/utils/safeStorage', () => ({ getSafeStorage: () => makeStorage(), })); +mock.module('@/stores/useProjectsStore', () => ({ + useProjectsStore: { + getState: () => ({ + activeProjectId: 'project', + projects: [ + { id: 'project', path: DIRECTORY, label: 'Project' }, + { id: 'other', path: OTHER_DIRECTORY, label: 'Other' }, + ], + }), + }, +})); + mock.module('@/lib/opencode/client', () => ({ opencodeClient: { setDirectory: mock(() => undefined), @@ -126,6 +138,11 @@ mock.module('@/lib/opencode/client', () => ({ const id = liveProviderIdsByDirectory.get(currentFetchDirectory ?? '') ?? liveProviderId; return { providers: [providerResponse(id, `${id}-model`, liveProviderVariants)], default: { default: id } }; }), + getProvidersForConfig: mock(async (directory?: string | null) => { + getProvidersCalls += 1; + const id = liveProviderIdsByDirectory.get(directory ?? '') ?? liveProviderId; + return { providers: [providerResponse(id, `${id}-model`, liveProviderVariants)], default: { default: id } }; + }), listAgents: mock(async () => []), }, })); @@ -286,7 +303,6 @@ describe('useConfigStore provider persistence', () => { const state = useConfigStore.getState(); expect(getProvidersCalls).toBe(2); - expect(new Set(withDirectoryCalls)).toEqual(new Set([DIRECTORY, OTHER_DIRECTORY])); expect(state.directoryScoped[DIRECTORY]?.providers.map((entry) => entry.id)).toEqual(['active-live']); expect(state.directoryScoped[OTHER_DIRECTORY]?.providers.map((entry) => entry.id)).toEqual(['inactive-live']); expect(state.directoryScoped[OTHER_DIRECTORY]?.defaultProviders).toEqual({ default: 'inactive-live' }); diff --git a/packages/ui/src/stores/useConfigStore.ts b/packages/ui/src/stores/useConfigStore.ts index d4b1c0c8..cffafd0b 100644 --- a/packages/ui/src/stores/useConfigStore.ts +++ b/packages/ui/src/stores/useConfigStore.ts @@ -734,6 +734,34 @@ const rememberWorktreeProject = (worktree: string, project: string): void => { } }; +const normalizeConfigPath = (value: string | null | undefined): string | null => { + const trimmed = typeof value === 'string' ? value.trim() : ''; + if (!trimmed) return null; + return trimmed.replace(/\\/g, '/').replace(/\/+$/, '') || '/'; +}; + +const getKnownProjectDirectories = (): string[] => { + try { + return useProjectsStore.getState().projects + .map((project) => normalizeConfigPath(project.path)) + .filter((path): path is string => Boolean(path)); + } catch { + return []; + } +}; + +const getFallbackProjectDirectory = (): string | null => { + try { + const { projects, activeProjectId } = useProjectsStore.getState(); + const active = activeProjectId + ? projects.find((project) => project.id === activeProjectId) + : null; + return normalizeConfigPath(active?.path ?? projects[0]?.path ?? null); + } catch { + return null; + } +}; + /** * Map a directory to its CONFIG scope. Providers/agents/defaults are defined at * the PROJECT level (opencode.json), so a worktree must inherit its parent @@ -742,11 +770,14 @@ const rememberWorktreeProject = (worktree: string, project: string): void => { * a known worktree, else the directory unchanged. */ const resolveConfigDirectory = (directory: string | null | undefined): string | null => { - const dir = typeof directory === 'string' && directory.trim().length > 0 ? directory : null; - if (!dir) return dir; - // 1. Persisted mapping — resolves synchronously at startup, before the async - // git worktree discovery has populated the runtime map. - const cached = getWorktreeProjectMap()[dir]; + const dir = normalizeConfigPath(directory); + const projects = getKnownProjectDirectories(); + if (!dir) return null; + if (projects.includes(dir)) return dir; + + // 1. Persisted mapping — resolves synchronously when the async worktree + // discovery has not populated the runtime map yet. + const cached = normalizeConfigPath(getWorktreeProjectMap()[dir]); if (cached) return cached; // 2. Live resolution via projects + discovered worktree map; cache the hit. try { @@ -755,14 +786,15 @@ const resolveConfigDirectory = (directory: string | null | undefined): string | useSessionUIStore.getState().availableWorktreesByProject, dir, ); - if (project?.path && project.path !== dir) { - rememberWorktreeProject(dir, project.path); - return project.path; + const projectPath = normalizeConfigPath(project?.path ?? null); + if (projectPath && projectPath !== dir) { + rememberWorktreeProject(dir, projectPath); + return projectPath; } } catch { - return dir; + return null; } - return dir; + return null; }; const toConfigDirectoryKey = (directory: string | null | undefined): string => @@ -775,6 +807,7 @@ const toConfigDirectoryKey = (directory: string | null | undefined): string => const _providersLoadedAt = new Map(); const _agentsLoadedAt = new Map(); const CONFIG_REFRESH_TTL_MS = 30_000; +const PROJECT_CONFIG_PREWARM_DELAY_MS = 1_000; const isConfigFresh = (loadedAt: Map, key: string): boolean => { const at = loadedAt.get(key); return typeof at === 'number' && Date.now() - at < CONFIG_REFRESH_TTL_MS; @@ -945,6 +978,7 @@ interface ConfigStore { probeConnection: (options?: { timeoutMs?: number }) => Promise; checkConnection: () => Promise; initializeApp: () => Promise; + prewarmProjectConfigs: (initialDirectory?: string | null) => Promise; getCurrentProvider: () => ProviderWithModelList | undefined; getCurrentModel: () => ProviderModel | undefined; getCurrentAgent: () => Agent | undefined; @@ -1237,7 +1271,12 @@ export const useConfigStore = create()( // active key + snapshot key always match and stay project-scoped. // Everything below operates on this key unchanged; the OpenCode // working directory (opencodeClient.getDirectory()) is separate. - const directoryKey = toConfigDirectoryKey(directory); + const configDirectory = resolveConfigDirectory(directory); + if (!configDirectory) { + markStartupTrace('activateDirectory:skippedUnknownDirectory', { directory }); + return; + } + const directoryKey = toDirectoryKey(configDirectory); let snapshotHadProviders = false; let snapshotHadAgents = false; @@ -1360,6 +1399,10 @@ export const useConfigStore = create()( // Providers are project-scoped: resolve a worktree to its project // so it reuses one shared snapshot instead of its own. const configDirectory = resolveConfigDirectory(requestedDirectory); + if (!configDirectory) { + markStartupTrace('loadProviders:skippedUnknownDirectory', { requestedDirectory, source: options?.source ?? 'unknown' }); + return; + } const effectiveDirectory = configDirectory ?? opencodeClient.getDirectory() ?? null; const directoryKey = toDirectoryKey(configDirectory); const source = options?.source ?? 'unknown'; @@ -1388,10 +1431,7 @@ export const useConfigStore = create()( ); const apiResult = await measureStartupTrace( 'loadProviders:api', - () => opencodeClient.withDirectory( - fromDirectoryKey(directoryKey), - () => opencodeClient.getProviders() - ), + () => opencodeClient.getProvidersForConfig(fromDirectoryKey(directoryKey)), { directoryKey, source, requestedDirectory, effectiveDirectory, attempt: attempt + 1 }, ); const providers = Array.isArray(apiResult?.providers) ? apiResult.providers : []; @@ -1779,6 +1819,10 @@ export const useConfigStore = create()( // Agents are project-scoped: resolve a worktree to its project // so it reuses one shared snapshot instead of its own. const configDirectory = resolveConfigDirectory(requestedDirectory); + if (!configDirectory) { + markStartupTrace('loadAgents:skippedUnknownDirectory', { requestedDirectory, source: options?.source ?? 'unknown' }); + return false; + } const effectiveDirectory = configDirectory ?? opencodeClient.getDirectory() ?? null; const directoryKey = toDirectoryKey(configDirectory); const source = options?.source ?? 'unknown'; @@ -2748,7 +2792,21 @@ export const useConfigStore = create()( useSessionUIStore.getState().availableWorktreesByProject, initialDirectory ?? null, ); - const configDirectory = resolvedProject?.path ?? initialDirectory ?? null; + const resolvedInitialDirectory = resolveConfigDirectory(resolvedProject?.path ?? initialDirectory ?? null); + const configDirectory = resolvedInitialDirectory ?? getFallbackProjectDirectory(); + if (!configDirectory) { + markStartupTrace('initializeApp:noProjectConfigDirectory'); + set({ isInitialized: true, isConnected: true, hasEverConnected: true, connectionPhase: "connected" }); + return; + } + if (!resolvedInitialDirectory && initialDirectory !== configDirectory) { + markStartupTrace('initializeApp:normalizedUnknownDirectoryToProject', { + initialDirectory, + configDirectory, + }); + opencodeClient.setDirectory(configDirectory); + useDirectoryStore.getState().setDirectory(configDirectory, { showOverlay: false }); + } const configDirectoryKey = toDirectoryKey(configDirectory); if (get().activeDirectoryKey !== configDirectoryKey) { set({ activeDirectoryKey: configDirectoryKey }); @@ -2761,6 +2819,7 @@ export const useConfigStore = create()( ]); set({ isInitialized: true, isConnected: true, hasEverConnected: true, connectionPhase: "connected" }); + void get().prewarmProjectConfigs(configDirectory); const initEnded = typeof performance !== 'undefined' ? performance.now() : Date.now(); markStartupTrace('initializeApp:end', { durationMs: Math.round(initEnded - initStarted), @@ -2786,6 +2845,55 @@ export const useConfigStore = create()( return run; }, + prewarmProjectConfigs: async (initialDirectory?: string | null) => { + if (!get().isConnected) { + return; + } + + const initialKey = toConfigDirectoryKey(initialDirectory ?? fromDirectoryKey(get().activeDirectoryKey)); + const projectDirectories = useProjectsStore.getState().projects + .map((project) => project.path) + .filter((path): path is string => typeof path === 'string' && path.trim().length > 0); + const seen = new Set([initialKey]); + const queuedDirectories: string[] = []; + + for (const directory of projectDirectories) { + const directoryKey = toConfigDirectoryKey(directory); + if (seen.has(directoryKey)) { + continue; + } + seen.add(directoryKey); + + const snapshot = get().directoryScoped[directoryKey]; + if (snapshot?.providers.length && snapshot.agents.length) { + continue; + } + const scopedDirectory = fromDirectoryKey(directoryKey); + if (scopedDirectory) { + queuedDirectories.push(scopedDirectory); + } + } + + for (const directory of queuedDirectories) { + await sleep(PROJECT_CONFIG_PREWARM_DELAY_MS); + if (!get().isConnected) { + return; + } + const directoryKey = toConfigDirectoryKey(directory); + const snapshot = get().directoryScoped[directoryKey]; + const tasks: Promise[] = []; + if (!snapshot?.providers.length) { + tasks.push(get().loadProviders({ directory, source: 'projectConfigPrewarm' })); + } + if (!snapshot?.agents.length) { + tasks.push(get().loadAgents({ directory, source: 'projectConfigPrewarm' })); + } + if (tasks.length > 0) { + await Promise.allSettled(tasks); + } + } + }, + getCurrentProvider: () => { const { providers, currentProviderId } = get(); return providers.find((p) => p.id === currentProviderId); diff --git a/packages/ui/src/stores/useSessionFoldersStore.test.ts b/packages/ui/src/stores/useSessionFoldersStore.test.ts new file mode 100644 index 00000000..42264081 --- /dev/null +++ b/packages/ui/src/stores/useSessionFoldersStore.test.ts @@ -0,0 +1,79 @@ +import { beforeEach, describe, expect, mock, test } from 'bun:test'; + +const storage = new Map(); +let storageSetCount = 0; + +const safeStorage = { + getItem: (key: string) => storage.get(key) ?? null, + setItem: (key: string, value: string) => { + storageSetCount += 1; + storage.set(key, value); + }, + removeItem: (key: string) => { + storage.delete(key); + }, + clear: () => { + storage.clear(); + }, + key: (index: number) => Array.from(storage.keys())[index] ?? null, + get length() { + return storage.size; + }, +} as Storage; + +mock.module('./utils/safeStorage', () => ({ + getSafeStorage: () => safeStorage, +})); + +mock.module('@/lib/desktop', () => ({ + isVSCodeRuntime: () => false, +})); + +mock.module('@/lib/runtime-fetch', () => ({ + runtimeFetch: mock(async () => new Response('{}', { headers: { 'Content-Type': 'application/json' } })), +})); + +const { useSessionFoldersStore } = await import('./useSessionFoldersStore'); + +const waitForPersist = () => new Promise((resolve) => setTimeout(resolve, 350)); + +describe('useSessionFoldersStore folder assignments', () => { + beforeEach(() => { + storage.clear(); + storageSetCount = 0; + useSessionFoldersStore.setState({ + foldersMap: {}, + collapsedFolderIds: new Set(), + }); + }); + + test('repeated addSessionToFolder to the same folder preserves foldersMap reference', async () => { + const store = useSessionFoldersStore.getState(); + const folder = store.createFolder('/workspace/project', 'Work'); + store.addSessionToFolder('/workspace/project', folder.id, 'ses_1'); + await waitForPersist(); + storageSetCount = 0; + + const before = useSessionFoldersStore.getState().foldersMap; + useSessionFoldersStore.getState().addSessionToFolder('/workspace/project', folder.id, 'ses_1'); + await waitForPersist(); + + expect(useSessionFoldersStore.getState().foldersMap).toBe(before); + expect(storageSetCount).toBe(0); + }); + + test('repeated addSessionsToFolder to the same folder preserves foldersMap reference', async () => { + const store = useSessionFoldersStore.getState(); + const folder = store.createFolder('/workspace/project', 'Batch'); + store.addSessionsToFolder('/workspace/project', folder.id, ['ses_1', 'ses_2']); + await waitForPersist(); + storageSetCount = 0; + + const before = useSessionFoldersStore.getState().foldersMap; + useSessionFoldersStore.getState().addSessionsToFolder('/workspace/project', folder.id, ['ses_1', 'ses_2']); + await waitForPersist(); + + expect(useSessionFoldersStore.getState().foldersMap).toBe(before); + expect(storageSetCount).toBe(0); + }); +}); diff --git a/packages/ui/src/sync/bootstrap.ts b/packages/ui/src/sync/bootstrap.ts index 0b37c630..b82022cf 100644 --- a/packages/ui/src/sync/bootstrap.ts +++ b/packages/ui/src/sync/bootstrap.ts @@ -80,7 +80,6 @@ export async function bootstrapGlobal( set({ projects }) }), ), - retry(() => sdk.provider.list().then((x) => set({ providers: unwrap(x, "provider.list") }))), ]) const errors = results @@ -125,7 +124,6 @@ export async function bootstrapDirectory(input: { global: { config: Record projects: Project[] - providers: { all: unknown[]; connected: unknown[]; default: Record } } loadSessions: (directory: string) => Promise | void }) { @@ -136,9 +134,6 @@ export async function bootstrapDirectory(input: { // Seed from global state while we fetch directory-specific data const seededProject = projectID(directory, g.projects) if (seededProject) set({ project: seededProject }) - if (state.provider.all.length === 0 && g.providers.all.length > 0) { - set({ provider: g.providers as State["provider"] }) - } if (Object.keys(state.config ?? {}).length === 0 && Object.keys(g.config ?? {}).length > 0) { set({ config: g.config as State["config"] }) } @@ -152,7 +147,6 @@ export async function bootstrapDirectory(input: { seededProject ? Promise.resolve() : retry(() => sdk.project.current().then((x) => set({ project: unwrap(x, "project.current").id }))), - retry(() => sdk.provider.list().then((x) => set({ provider: unwrap(x, "provider.list") }))), retry(() => sdk.config.get().then((x) => set({ config: unwrap(x, "config.get") }))), retry(() => sdk.path.get().then((x) => { @@ -177,7 +171,7 @@ export async function bootstrapDirectory(input: { // - path.get feeds project resolution, but if we already resolved a project // (from global projects) its failure is tolerable; the worktree path is // refreshed by later events. - const [, , , pathResult] = phase1Results + const [, , pathResult] = phase1Results const pathFailedWithoutProject = pathResult.status === "rejected" && !getState().project @@ -194,7 +188,6 @@ export async function bootstrapDirectory(input: { // These enrich the UI but aren't required for basic functionality. // --------------------------------------------------------------------------- void Promise.allSettled([ - retry(() => sdk.app.agents().then((x) => set({ agent: unwrap(x, "app.agents") }))), retry(() => sdk.command.list().then((x) => set({ command: unwrap(x, "command.list") }))), retry(() => sdk.mcp.status().then((x) => set({ mcp: unwrap(x, "mcp.status") }))), retry(() => sdk.lsp.status().then((x) => set({ lsp: unwrap(x, "lsp.status") }))), diff --git a/packages/ui/src/sync/sync-context.tsx b/packages/ui/src/sync/sync-context.tsx index 92736e17..5b6ea932 100644 --- a/packages/ui/src/sync/sync-context.tsx +++ b/packages/ui/src/sync/sync-context.tsx @@ -1657,11 +1657,10 @@ export function SyncProvider(props: { ingestDirectoryStateIntoRoutingIndex(routingIndex, directory, store.getState()) } }, - global: { - config: globalState.config, - projects: globalState.projects, - providers: globalState.providers, - }, + global: { + config: globalState.config, + projects: globalState.projects, + }, loadSessions: (dir) => retry(async () => { const rootSessions = (await listGlobalSessionPages(props.sdk, { directory: dir, @@ -1752,11 +1751,11 @@ export function SyncProvider(props: { const generation = ++globalBootstrapGeneration bootingRoot = true const globalActions = useGlobalSyncStore.getState().actions - bootstrapGlobal(props.sdk, (patch) => { - if (globalBootstrapGeneration === generation) { - globalActions.set(patch) - } - }) + bootstrapGlobal(props.sdk, (patch) => { + if (globalBootstrapGeneration === generation) { + globalActions.set(patch) + } + }) .then(() => { if (globalBootstrapGeneration === generation) { bootedAt = Date.now() @@ -1772,7 +1771,7 @@ export function SyncProvider(props: { bootingRoot = false } } - }, [props.sdk]) + }, [props.sdk]) // Event pipeline — created once per mount. No class, no start/stop. // Abort controller owned by the pipeline closure. Cleanup aborts + flushes.