diff --git a/CHANGELOG.md b/CHANGELOG.md index 90c3e6ee..35dc73da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ All notable changes to this project will be documented in this file. - Sessions: fixed a bug where a running session would briefly flicker as idle (in the sidebar, the send/stop button, and the status row) when the app is protected by a password. - Desktop: you can now open developer tools from the Help menu. - Sessions: new draft sessions now start from the default model and agent instead of inheriting the previous session's selection, and fall back to OpenCode's own `default_agent` (and its model) when no OpenChamber default is set. +- Startup: the model and agent now appear faster on the initial draft — config loads under the project key up front (no reload when the draft opens) and the agent list is fetched once instead of per consumer. ## [1.12.4] - 2026-06-11 diff --git a/packages/ui/src/lib/opencode/client.ts b/packages/ui/src/lib/opencode/client.ts index 58713305..b7701bbe 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 listAgentsInFlight: Map> = new Map(); private listDirectoryCache: Map = new Map(); constructor(baseUrl: string = DEFAULT_BASE_URL) { @@ -1294,14 +1295,34 @@ class OpencodeService { * useAgentsStore) can observe failure and retry; silently returning an * empty list would defeat retries and clear the cached agent list. */ - async listAgents(): Promise { - const response = await this.client.app.agents( - this.currentDirectory ? { directory: this.currentDirectory } : undefined - ); - if (response.error) { - throw new Error(`app.agents failed: ${formatSdkError(response.error)}`); + async listAgents(directory?: string | null): Promise { + // Pass the directory explicitly so we don't depend on (and serialize behind) + // withDirectory's shared context queue. Concurrent callers for the same + // directory (e.g. config store + agents store at startup) share one request. + const effectiveDirectory = this.normalizeCandidatePath(directory) ?? directory ?? this.currentDirectory ?? undefined; + const key = effectiveDirectory ?? ''; + + const existing = this.listAgentsInFlight.get(key); + if (existing) { + return existing; + } + + 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)}`); + } + return response.data || []; + })(); + + this.listAgentsInFlight.set(key, request); + try { + return await request; + } finally { + this.listAgentsInFlight.delete(key); } - return response.data || []; } // SSE infrastructure removed — EventPipeline in sync/event-pipeline.ts handles diff --git a/packages/ui/src/stores/useAgentsStore.ts b/packages/ui/src/stores/useAgentsStore.ts index 742d2343..49923828 100644 --- a/packages/ui/src/stores/useAgentsStore.ts +++ b/packages/ui/src/stores/useAgentsStore.ts @@ -245,8 +245,10 @@ export const useAgentsStore = create()( try { const queryParams = configDirectory ? `?directory=${encodeURIComponent(configDirectory)}` : ''; - // Ensure we list agents using the correct project context - const agents = await opencodeClient.withDirectory(configDirectory, () => opencodeClient.listAgents()); + // Ensure we list agents using the correct project context. Pass the + // directory directly so this shares the in-flight request with the config + // store instead of issuing a duplicate agents fetch at startup. + const agents = await opencodeClient.listAgents(configDirectory); const agentsWithScope = await Promise.all( agents.map(async (agent) => { diff --git a/packages/ui/src/stores/useConfigStore.ts b/packages/ui/src/stores/useConfigStore.ts index 07038eb0..4dd93052 100644 --- a/packages/ui/src/stores/useConfigStore.ts +++ b/packages/ui/src/stores/useConfigStore.ts @@ -12,6 +12,8 @@ import { useSelectionStore } from "@/sync/selection-store"; import { getRegisteredRuntimeAPIs } from "@/contexts/runtimeAPIRegistry"; import { updateDesktopSettings } from "@/lib/persistence"; import { useDirectoryStore } from "@/stores/useDirectoryStore"; +import { useProjectsStore } from "@/stores/useProjectsStore"; +import { resolveProjectForSessionDirectory } from "@/lib/projectResolution"; import { streamDebugEnabled } from "@/stores/utils/streamDebug"; import { parseModelIdentifier } from "@/lib/modelIdentifier"; import { runtimeFetch } from "@/lib/runtime-fetch"; @@ -1710,7 +1712,7 @@ export const useConfigStore = create()( const [agents, openChamberDefaults, opencodeConfig] = await Promise.all([ measureStartupTrace( 'loadAgents:api', - () => opencodeClient.withDirectory(fromDirectoryKey(directoryKey), () => opencodeClient.listAgents()), + () => opencodeClient.listAgents(fromDirectoryKey(directoryKey)), { directoryKey, source, requestedDirectory, effectiveDirectory, attempt: attempt + 1 }, ), fetchOpenChamberDefaults(), @@ -2634,10 +2636,28 @@ export const useConfigStore = create()( get().invalidateProviderCache(); + // Config (providers/agents/defaults) lives at the PROJECT level. If the + // app starts on a worktree directory, load config under the owning + // project's key so the initial draft — which activates the project — finds + // a ready snapshot instead of triggering a second provider/agent load. + const initialDirectory = opencodeClient.getDirectory() + ?? useDirectoryStore.getState().currentDirectory + ?? fromDirectoryKey(get().activeDirectoryKey); + const resolvedProject = resolveProjectForSessionDirectory( + useProjectsStore.getState().projects, + useSessionUIStore.getState().availableWorktreesByProject, + initialDirectory ?? null, + ); + const configDirectory = resolvedProject?.path ?? initialDirectory ?? null; + const configDirectoryKey = toDirectoryKey(configDirectory); + if (get().activeDirectoryKey !== configDirectoryKey) { + set({ activeDirectoryKey: configDirectoryKey }); + } + if (debug) console.log("Loading providers and agents..."); await Promise.all([ - get().loadProviders({ source: 'initializeApp' }), - get().loadAgents({ source: 'initializeApp' }), + get().loadProviders({ directory: configDirectory, source: 'initializeApp' }), + get().loadAgents({ directory: configDirectory, source: 'initializeApp' }), ]); set({ isInitialized: true, isConnected: true, hasEverConnected: true, connectionPhase: "connected" });