perf: speed up model/agent readiness on the initial draft

Load startup config under the owning project's directory key (resolving from a
worktree directory when needed) so the auto-opened draft, which activates the
project, finds a ready providers/agents snapshot instead of triggering a second
load. Also dedupe app.agents: listAgents now takes the directory directly and
shares an in-flight request, so the config store and agents store no longer
issue duplicate agent fetches at startup.
This commit is contained in:
Bohdan Triapitsyn
2026-06-14 22:04:32 +03:00
parent 9111611bdc
commit 296177357b
4 changed files with 56 additions and 12 deletions
+1
View File
@@ -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
+28 -7
View File
@@ -229,6 +229,7 @@ class OpencodeService {
private currentDirectory: string | undefined = undefined;
private directoryContextQueue: Promise<void> = Promise.resolve();
private listDirectoryInFlight: Map<string, Promise<FilesystemEntry[]>> = new Map();
private listAgentsInFlight: Map<string, Promise<Agent[]>> = new Map();
private listDirectoryCache: Map<string, { entries: FilesystemEntry[]; expiresAt: number }> = 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<Agent[]> {
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<Agent[]> {
// 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
+4 -2
View File
@@ -245,8 +245,10 @@ export const useAgentsStore = create<AgentsStore>()(
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) => {
+23 -3
View File
@@ -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<ConfigStore>()(
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<ConfigStore>()(
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" });