import { create } from "zustand"; import type { StoreApi, UseBoundStore } from "zustand"; import { devtools, persist, createJSONStorage } from "zustand/middleware"; import type { Provider, Agent } from "@opencode-ai/sdk/v2"; import { opencodeClient } from "@/lib/opencode/client"; import { scopeMatches, subscribeToConfigChanges } from "@/lib/configSync"; import type { ModelMetadata } from "@/types"; import { getSafeStorage } from "./utils/safeStorage"; import type { SessionStore } from "./types/sessionTypes"; import { filterVisibleAgents } from "./useAgentsStore"; import { getRegisteredRuntimeAPIs } from "@/contexts/runtimeAPIRegistry"; import { updateDesktopSettings } from "@/lib/persistence"; import { useDirectoryStore } from "@/stores/useDirectoryStore"; import { streamDebugEnabled } from "@/stores/utils/streamDebug"; const MODELS_DEV_API_URL = "https://models.dev/api.json"; const MODELS_DEV_PROXY_URL = "/api/openchamber/models-metadata"; const FALLBACK_PROVIDER_ID = "opencode"; const FALLBACK_MODEL_ID = "big-pickle"; interface OpenChamberDefaults { defaultModel?: string; defaultVariant?: string; defaultAgent?: string; autoCreateWorktree?: boolean; gitmojiEnabled?: boolean; } const fetchOpenChamberDefaults = async (): Promise => { try { // 1. Runtime settings API (VSCode) const runtimeSettings = getRegisteredRuntimeAPIs()?.settings; if (runtimeSettings) { try { const result = await runtimeSettings.load(); const data = result?.settings; if (data) { const defaultModel = typeof data?.defaultModel === 'string' ? data.defaultModel.trim() : ''; const defaultVariant = typeof data?.defaultVariant === 'string' ? data.defaultVariant.trim() : ''; const defaultAgent = typeof data?.defaultAgent === 'string' ? data.defaultAgent.trim() : ''; const gitmojiEnabled = typeof data?.gitmojiEnabled === 'boolean' ? data.gitmojiEnabled : undefined; return { defaultModel: defaultModel.length > 0 ? defaultModel : undefined, defaultVariant: defaultVariant.length > 0 ? defaultVariant : undefined, defaultAgent: defaultAgent.length > 0 ? defaultAgent : undefined, autoCreateWorktree: typeof data?.autoCreateWorktree === 'boolean' ? data.autoCreateWorktree : undefined, gitmojiEnabled, }; } } catch { // Fall through to fetch } } // 2. Fetch API (Web/server) const response = await fetch('/api/config/settings', { method: 'GET', headers: { Accept: 'application/json' }, }); if (!response.ok) { return {}; } const data = await response.json(); const defaultModel = typeof data?.defaultModel === 'string' ? data.defaultModel.trim() : ''; const defaultVariant = typeof data?.defaultVariant === 'string' ? data.defaultVariant.trim() : ''; const defaultAgent = typeof data?.defaultAgent === 'string' ? data.defaultAgent.trim() : ''; const gitmojiEnabled = typeof data?.gitmojiEnabled === 'boolean' ? data.gitmojiEnabled : undefined; return { defaultModel: defaultModel.length > 0 ? defaultModel : undefined, defaultVariant: defaultVariant.length > 0 ? defaultVariant : undefined, defaultAgent: defaultAgent.length > 0 ? defaultAgent : undefined, autoCreateWorktree: typeof data?.autoCreateWorktree === 'boolean' ? data.autoCreateWorktree : undefined, gitmojiEnabled, }; } catch { return {}; } }; const parseModelString = (modelString: string): { providerId: string; modelId: string } | null => { if (!modelString || typeof modelString !== 'string') { return null; } const parts = modelString.split('/'); if (parts.length !== 2 || !parts[0] || !parts[1]) { return null; } return { providerId: parts[0], modelId: parts[1] }; }; const normalizeProviderId = (value: string) => value?.toLowerCase?.() ?? ''; const isPrimaryMode = (mode?: string) => mode === "primary" || mode === "all" || mode === undefined || mode === null; type ProviderModel = Provider["models"][string]; type ProviderWithModelList = Omit & { models: ProviderModel[] }; interface ModelsDevModelEntry { id?: string; name?: string; tool_call?: boolean; reasoning?: boolean; temperature?: boolean; attachment?: boolean; modalities?: { input?: string[]; output?: string[]; }; cost?: { input?: number; output?: number; cache_read?: number; cache_write?: number; }; limit?: { context?: number; output?: number; }; knowledge?: string; release_date?: string; last_updated?: string; } interface ModelsDevProviderEntry { id?: string; models?: Record; } const isRecord = (value: unknown): value is Record => typeof value === "object" && value !== null; const isStringArray = (value: unknown): value is string[] => Array.isArray(value) && value.every((item) => typeof item === "string"); const isModelsDevModelEntry = (value: unknown): value is ModelsDevModelEntry => { if (!isRecord(value)) { return false; } const candidate = value as ModelsDevModelEntry; if (candidate.modalities) { const { input, output } = candidate.modalities; if (input && !isStringArray(input)) { return false; } if (output && !isStringArray(output)) { return false; } } return true; }; const isModelsDevProviderEntry = (value: unknown): value is ModelsDevProviderEntry => { if (!isRecord(value)) { return false; } const candidate = value as ModelsDevProviderEntry; return candidate.models === undefined || isRecord(candidate.models); }; const buildModelMetadataKey = (providerId: string, modelId: string) => { const normalizedProvider = normalizeProviderId(providerId); if (!normalizedProvider || !modelId) { return ''; } return `${normalizedProvider}/${modelId}`; }; const transformModelsDevResponse = (payload: unknown): Map => { const metadataMap = new Map(); if (!isRecord(payload)) { return metadataMap; } for (const [providerKey, providerValue] of Object.entries(payload)) { if (!isModelsDevProviderEntry(providerValue)) { continue; } const providerId = typeof providerValue.id === 'string' && providerValue.id.length > 0 ? providerValue.id : providerKey; const models = providerValue.models; if (!models || !isRecord(models)) { continue; } for (const [modelKey, modelValue] of Object.entries(models)) { if (!isModelsDevModelEntry(modelValue)) { continue; } const resolvedModelId = typeof modelKey === 'string' && modelKey.length > 0 ? modelKey : modelValue.id; if (!resolvedModelId || typeof resolvedModelId !== 'string' || resolvedModelId.length === 0) { continue; } const metadata: ModelMetadata = { id: typeof modelValue.id === 'string' && modelValue.id.length > 0 ? modelValue.id : resolvedModelId, providerId, name: typeof modelValue.name === 'string' ? modelValue.name : undefined, tool_call: typeof modelValue.tool_call === 'boolean' ? modelValue.tool_call : undefined, reasoning: typeof modelValue.reasoning === 'boolean' ? modelValue.reasoning : undefined, temperature: typeof modelValue.temperature === 'boolean' ? modelValue.temperature : undefined, attachment: typeof modelValue.attachment === 'boolean' ? modelValue.attachment : undefined, modalities: modelValue.modalities ? { input: isStringArray(modelValue.modalities.input) ? modelValue.modalities.input : undefined, output: isStringArray(modelValue.modalities.output) ? modelValue.modalities.output : undefined, } : undefined, cost: modelValue.cost, limit: modelValue.limit, knowledge: typeof modelValue.knowledge === 'string' ? modelValue.knowledge : undefined, release_date: typeof modelValue.release_date === 'string' ? modelValue.release_date : undefined, last_updated: typeof modelValue.last_updated === 'string' ? modelValue.last_updated : undefined, }; const key = buildModelMetadataKey(providerId, resolvedModelId); if (key) { metadataMap.set(key, metadata); } } } return metadataMap; }; const fetchModelsDevMetadata = async (): Promise> => { if (typeof fetch !== 'function') { return new Map(); } const sources = [MODELS_DEV_PROXY_URL, MODELS_DEV_API_URL]; for (const source of sources) { const controller = typeof AbortController !== 'undefined' ? new AbortController() : undefined; const timeout = controller ? setTimeout(() => controller.abort(), 8000) : undefined; try { const isAbsoluteUrl = /^https?:\/\//i.test(source); const requestInit: RequestInit = { signal: controller?.signal, headers: { Accept: 'application/json', }, cache: 'no-store', }; if (isAbsoluteUrl) { requestInit.mode = 'cors'; } else { requestInit.credentials = 'same-origin'; } const response = await fetch(source, requestInit); if (!response.ok) { throw new Error(`Metadata request to ${source} returned status ${response.status}`); } const data = await response.json(); return transformModelsDevResponse(data); } catch (error: unknown) { if ((error as Error)?.name === 'AbortError') { console.warn(`Model metadata request aborted (${source})`); } else { console.warn(`Failed to fetch model metadata from ${source}:`, error); } } finally { if (timeout) { clearTimeout(timeout); } } } return new Map(); }; let modelsMetadataInFlight: Promise> | null = null; const ensureModelsMetadataFetch = ( getModelsMetadata: () => Map, setModelsMetadata: (metadata: Map) => void, ) => { const existing = getModelsMetadata(); if (existing.size > 0) { return; } if (modelsMetadataInFlight) { return; } modelsMetadataInFlight = fetchModelsDevMetadata() .then((metadata) => { if (metadata.size > 0) { setModelsMetadata(metadata); } return metadata; }) .catch(() => new Map()) .finally(() => { modelsMetadataInFlight = null; }); }; const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); const DIRECTORY_KEY_GLOBAL = "__global__"; const toDirectoryKey = (directory: string | null | undefined): string => { const trimmed = typeof directory === 'string' ? directory.trim() : ''; return trimmed.length > 0 ? trimmed : DIRECTORY_KEY_GLOBAL; }; const fromDirectoryKey = (key: string): string | null => (key === DIRECTORY_KEY_GLOBAL ? null : key); const resolveInitialDirectoryKey = (): string => { if (typeof window === 'undefined') { return DIRECTORY_KEY_GLOBAL; } const directory = opencodeClient.getDirectory() ?? useDirectoryStore.getState().currentDirectory; return toDirectoryKey(directory); }; interface DirectoryScopedConfig { providers: ProviderWithModelList[]; agents: Agent[]; currentProviderId: string; currentModelId: string; currentVariant?: string | undefined; currentAgentName: string | undefined; selectedProviderId: string; agentModelSelections: { [agentName: string]: { providerId: string; modelId: string } }; defaultProviders: { [key: string]: string }; } interface ConfigStore { activeDirectoryKey: string; directoryScoped: Record; providers: ProviderWithModelList[]; agents: Agent[]; currentProviderId: string; currentModelId: string; currentVariant: string | undefined; currentAgentName: string | undefined; selectedProviderId: string; agentModelSelections: { [agentName: string]: { providerId: string; modelId: string } }; defaultProviders: { [key: string]: string }; isConnected: boolean; isInitialized: boolean; modelsMetadata: Map; // OpenChamber settings-based defaults (take precedence over agent preferences) settingsDefaultModel: string | undefined; // format: "provider/model" settingsDefaultVariant: string | undefined; settingsDefaultAgent: string | undefined; settingsAutoCreateWorktree: boolean; settingsGitmojiEnabled: boolean; activateDirectory: (directory: string | null | undefined) => Promise; loadProviders: (options?: { directory?: string | null }) => Promise; loadAgents: (options?: { directory?: string | null }) => Promise; setProvider: (providerId: string) => void; setModel: (modelId: string) => void; setCurrentVariant: (variant: string | undefined) => void; cycleCurrentVariant: () => void; getCurrentModelVariants: () => string[]; setAgent: (agentName: string | undefined) => void; setSelectedProvider: (providerId: string) => void; setSettingsDefaultModel: (model: string | undefined) => void; setSettingsDefaultVariant: (variant: string | undefined) => void; setSettingsDefaultAgent: (agent: string | undefined) => void; setSettingsAutoCreateWorktree: (enabled: boolean) => void; setSettingsGitmojiEnabled: (enabled: boolean) => void; saveAgentModelSelection: (agentName: string, providerId: string, modelId: string) => void; getAgentModelSelection: (agentName: string) => { providerId: string; modelId: string } | null; checkConnection: () => Promise; initializeApp: () => Promise; getCurrentProvider: () => ProviderWithModelList | undefined; getCurrentModel: () => ProviderModel | undefined; getCurrentAgent: () => Agent | undefined; getModelMetadata: (providerId: string, modelId: string) => ModelMetadata | undefined; // Returns only visible agents (excludes hidden internal agents like title, compaction, summary) getVisibleAgents: () => Agent[]; } declare global { interface Window { __zustand_config_store__?: UseBoundStore>; __zustand_session_store__?: UseBoundStore>; } } export const useConfigStore = create()( devtools( persist( (set, get) => ({ activeDirectoryKey: resolveInitialDirectoryKey(), directoryScoped: {}, providers: [], agents: [], currentProviderId: "", currentModelId: "", currentVariant: undefined, currentAgentName: undefined, selectedProviderId: "", agentModelSelections: {}, defaultProviders: {}, isConnected: false, isInitialized: false, modelsMetadata: new Map(), settingsDefaultModel: undefined, settingsDefaultVariant: undefined, settingsDefaultAgent: undefined, settingsAutoCreateWorktree: false, settingsGitmojiEnabled: false, activateDirectory: async (directory) => { const directoryKey = toDirectoryKey(directory); set((state) => { const snapshot = state.directoryScoped[directoryKey]; if (snapshot) { return { activeDirectoryKey: directoryKey, providers: snapshot.providers, agents: snapshot.agents, currentProviderId: snapshot.currentProviderId, currentModelId: snapshot.currentModelId, currentVariant: snapshot.currentVariant, currentAgentName: snapshot.currentAgentName, selectedProviderId: snapshot.selectedProviderId, agentModelSelections: snapshot.agentModelSelections, defaultProviders: snapshot.defaultProviders, }; } return { activeDirectoryKey: directoryKey, providers: [], agents: [], currentProviderId: "", currentModelId: "", currentAgentName: undefined, selectedProviderId: "", agentModelSelections: {}, defaultProviders: {}, }; }); if (!get().isConnected) { return; } await get().loadProviders({ directory: fromDirectoryKey(directoryKey) }); await get().loadAgents({ directory: fromDirectoryKey(directoryKey) }); }, loadProviders: async (options) => { const directoryKey = toDirectoryKey(options?.directory ?? fromDirectoryKey(get().activeDirectoryKey)); const existingSnapshot = get().directoryScoped[directoryKey]; const previousProviders = existingSnapshot?.providers ?? (get().activeDirectoryKey === directoryKey ? get().providers : []); const previousDefaults = existingSnapshot?.defaultProviders ?? (get().activeDirectoryKey === directoryKey ? get().defaultProviders : {}); let lastError: unknown = null; for (let attempt = 0; attempt < 3; attempt++) { try { ensureModelsMetadataFetch( () => get().modelsMetadata, (metadata) => set({ modelsMetadata: metadata }), ); const apiResult = await opencodeClient.withDirectory( fromDirectoryKey(directoryKey), () => opencodeClient.getProviders() ); const providers = Array.isArray(apiResult?.providers) ? apiResult.providers : []; const defaults = apiResult?.default || {}; const processedProviders: ProviderWithModelList[] = providers.map((provider) => { const modelRecord = provider.models ?? {}; const models: ProviderModel[] = Object.keys(modelRecord).map((modelId) => modelRecord[modelId]); return { ...provider, models, }; }); set((state) => { const baseSnapshot: DirectoryScopedConfig = state.directoryScoped[directoryKey] ?? { providers: [], agents: [], currentProviderId: "", currentModelId: "", currentAgentName: undefined, selectedProviderId: "", agentModelSelections: {}, defaultProviders: {}, }; const nextSnapshot: DirectoryScopedConfig = { ...baseSnapshot, providers: processedProviders, defaultProviders: defaults, }; const nextState: Partial = { directoryScoped: { ...state.directoryScoped, [directoryKey]: nextSnapshot, }, }; if (state.activeDirectoryKey === directoryKey) { nextState.providers = processedProviders; nextState.defaultProviders = defaults; if (!state.currentProviderId && !state.currentModelId && state.settingsDefaultModel) { const parsed = parseModelString(state.settingsDefaultModel); if (parsed) { const settingsProvider = processedProviders.find((p) => p.id === parsed.providerId); if (settingsProvider?.models.some((m) => m.id === parsed.modelId)) { const model = settingsProvider.models.find((m) => m.id === parsed.modelId); const currentVariant = state.settingsDefaultVariant && (model as { variants?: Record } | undefined)?.variants?.[state.settingsDefaultVariant] ? state.settingsDefaultVariant : undefined; nextState.currentProviderId = parsed.providerId; nextState.currentModelId = parsed.modelId; nextState.currentVariant = currentVariant; nextState.selectedProviderId = parsed.providerId; nextSnapshot.currentProviderId = parsed.providerId; nextSnapshot.currentModelId = parsed.modelId; nextSnapshot.currentVariant = currentVariant; nextSnapshot.selectedProviderId = parsed.providerId; } } } } return nextState; }); return; } catch (error) { lastError = error; const waitMs = 200 * (attempt + 1); await new Promise((resolve) => setTimeout(resolve, waitMs)); } } console.error("Failed to load providers:", lastError); set((state) => { const baseSnapshot: DirectoryScopedConfig = state.directoryScoped[directoryKey] ?? { providers: [], agents: [], currentProviderId: "", currentModelId: "", currentAgentName: undefined, selectedProviderId: "", agentModelSelections: {}, defaultProviders: {}, }; const nextSnapshot: DirectoryScopedConfig = { ...baseSnapshot, providers: previousProviders, defaultProviders: previousDefaults, }; const nextState: Partial = { directoryScoped: { ...state.directoryScoped, [directoryKey]: nextSnapshot, }, }; if (state.activeDirectoryKey === directoryKey) { nextState.providers = previousProviders; nextState.defaultProviders = previousDefaults; if (!state.currentProviderId && !state.currentModelId && state.settingsDefaultModel) { const parsed = parseModelString(state.settingsDefaultModel); if (parsed) { const settingsProvider = previousProviders.find((p) => p.id === parsed.providerId); if (settingsProvider?.models.some((m) => m.id === parsed.modelId)) { const model = settingsProvider.models.find((m) => m.id === parsed.modelId); const currentVariant = state.settingsDefaultVariant && (model as { variants?: Record } | undefined)?.variants?.[state.settingsDefaultVariant] ? state.settingsDefaultVariant : undefined; nextState.currentProviderId = parsed.providerId; nextState.currentModelId = parsed.modelId; nextState.currentVariant = currentVariant; nextState.selectedProviderId = parsed.providerId; nextSnapshot.currentProviderId = parsed.providerId; nextSnapshot.currentModelId = parsed.modelId; nextSnapshot.currentVariant = currentVariant; nextSnapshot.selectedProviderId = parsed.providerId; } } } } return nextState; }); }, setProvider: (providerId: string) => { const { providers } = get(); const provider = providers.find((p) => p.id === providerId); if (!provider) { return; } const firstModel = provider.models[0]; const newModelId = firstModel?.id || ""; set((state) => { const directoryKey = state.activeDirectoryKey; const baseSnapshot: DirectoryScopedConfig = state.directoryScoped[directoryKey] ?? { providers: state.providers, agents: state.agents, currentProviderId: state.currentProviderId, currentModelId: state.currentModelId, currentVariant: state.currentVariant, currentAgentName: state.currentAgentName, selectedProviderId: state.selectedProviderId, agentModelSelections: state.agentModelSelections, defaultProviders: state.defaultProviders, }; const nextSnapshot: DirectoryScopedConfig = { ...baseSnapshot, currentProviderId: providerId, currentModelId: newModelId, selectedProviderId: providerId, }; return { currentProviderId: providerId, currentModelId: newModelId, selectedProviderId: providerId, directoryScoped: { ...state.directoryScoped, [directoryKey]: nextSnapshot, }, }; }); }, setModel: (modelId: string) => { set((state) => { const directoryKey = state.activeDirectoryKey; const baseSnapshot: DirectoryScopedConfig = state.directoryScoped[directoryKey] ?? { providers: state.providers, agents: state.agents, currentProviderId: state.currentProviderId, currentModelId: state.currentModelId, currentVariant: state.currentVariant, currentAgentName: state.currentAgentName, selectedProviderId: state.selectedProviderId, agentModelSelections: state.agentModelSelections, defaultProviders: state.defaultProviders, }; const nextSnapshot: DirectoryScopedConfig = { ...baseSnapshot, currentModelId: modelId, }; return { currentModelId: modelId, directoryScoped: { ...state.directoryScoped, [directoryKey]: nextSnapshot, }, }; }); }, setCurrentVariant: (variant: string | undefined) => { set((state) => { if (state.currentVariant === variant) { return state; } const directoryKey = state.activeDirectoryKey; const baseSnapshot: DirectoryScopedConfig = state.directoryScoped[directoryKey] ?? { providers: state.providers, agents: state.agents, currentProviderId: state.currentProviderId, currentModelId: state.currentModelId, currentVariant: state.currentVariant, currentAgentName: state.currentAgentName, selectedProviderId: state.selectedProviderId, agentModelSelections: state.agentModelSelections, defaultProviders: state.defaultProviders, }; const nextSnapshot: DirectoryScopedConfig = { ...baseSnapshot, currentVariant: variant, }; return { currentVariant: variant, directoryScoped: { ...state.directoryScoped, [directoryKey]: nextSnapshot, }, }; }); }, getCurrentModelVariants: () => { const model = get().getCurrentModel(); const variants = (model as { variants?: Record } | undefined)?.variants; if (!variants) { return []; } return Object.keys(variants); }, cycleCurrentVariant: () => { const variantKeys = get().getCurrentModelVariants(); if (variantKeys.length === 0) { return; } const current = get().currentVariant; if (!current) { get().setCurrentVariant(variantKeys[0]); return; } const index = variantKeys.indexOf(current); if (index === -1 || index === variantKeys.length - 1) { get().setCurrentVariant(undefined); return; } get().setCurrentVariant(variantKeys[index + 1]); }, setSelectedProvider: (providerId: string) => { set((state) => { const directoryKey = state.activeDirectoryKey; const baseSnapshot: DirectoryScopedConfig = state.directoryScoped[directoryKey] ?? { providers: state.providers, agents: state.agents, currentProviderId: state.currentProviderId, currentModelId: state.currentModelId, currentAgentName: state.currentAgentName, selectedProviderId: state.selectedProviderId, agentModelSelections: state.agentModelSelections, defaultProviders: state.defaultProviders, }; const nextSnapshot: DirectoryScopedConfig = { ...baseSnapshot, selectedProviderId: providerId, }; return { selectedProviderId: providerId, directoryScoped: { ...state.directoryScoped, [directoryKey]: nextSnapshot, }, }; }); }, saveAgentModelSelection: (agentName: string, providerId: string, modelId: string) => { set((state) => { const directoryKey = state.activeDirectoryKey; const nextSelections = { ...state.agentModelSelections, [agentName]: { providerId, modelId }, }; const baseSnapshot: DirectoryScopedConfig = state.directoryScoped[directoryKey] ?? { providers: state.providers, agents: state.agents, currentProviderId: state.currentProviderId, currentModelId: state.currentModelId, currentAgentName: state.currentAgentName, selectedProviderId: state.selectedProviderId, agentModelSelections: state.agentModelSelections, defaultProviders: state.defaultProviders, }; const nextSnapshot: DirectoryScopedConfig = { ...baseSnapshot, agentModelSelections: nextSelections, }; return { agentModelSelections: nextSelections, directoryScoped: { ...state.directoryScoped, [directoryKey]: nextSnapshot, }, }; }); }, getAgentModelSelection: (agentName: string) => { const { agentModelSelections } = get(); return agentModelSelections[agentName] || null; }, loadAgents: async (options) => { const directoryKey = toDirectoryKey(options?.directory ?? fromDirectoryKey(get().activeDirectoryKey)); const existingSnapshot = get().directoryScoped[directoryKey]; const previousAgents = existingSnapshot?.agents ?? (get().activeDirectoryKey === directoryKey ? get().agents : []); let lastError: unknown = null; for (let attempt = 0; attempt < 3; attempt++) { try { // Fetch agents and OpenChamber settings in parallel const [agents, openChamberDefaults] = await Promise.all([ opencodeClient.withDirectory(fromDirectoryKey(directoryKey), () => opencodeClient.listAgents()), fetchOpenChamberDefaults(), ]); const safeAgents = Array.isArray(agents) ? agents : []; const providers = get().activeDirectoryKey === directoryKey ? get().providers : (get().directoryScoped[directoryKey]?.providers ?? []); set((state) => { const baseSnapshot: DirectoryScopedConfig = state.directoryScoped[directoryKey] ?? { providers, agents: previousAgents, currentProviderId: "", currentModelId: "", currentAgentName: undefined, selectedProviderId: "", agentModelSelections: {}, defaultProviders: {}, }; const nextSnapshot: DirectoryScopedConfig = { ...baseSnapshot, providers, agents: safeAgents, }; const nextState: Partial = { settingsDefaultModel: openChamberDefaults.defaultModel, settingsDefaultVariant: openChamberDefaults.defaultVariant, settingsDefaultAgent: openChamberDefaults.defaultAgent, settingsAutoCreateWorktree: openChamberDefaults.autoCreateWorktree ?? false, settingsGitmojiEnabled: openChamberDefaults.gitmojiEnabled ?? false, directoryScoped: { ...state.directoryScoped, [directoryKey]: nextSnapshot, }, }; if (state.activeDirectoryKey === directoryKey) { nextState.agents = safeAgents; } return nextState; }); if (safeAgents.length === 0) { set((state) => { const baseSnapshot: DirectoryScopedConfig = state.directoryScoped[directoryKey] ?? { providers, agents: [], currentProviderId: "", currentModelId: "", currentVariant: undefined, currentAgentName: undefined, selectedProviderId: "", agentModelSelections: {}, defaultProviders: {}, }; const nextSnapshot: DirectoryScopedConfig = { ...baseSnapshot, providers, agents: [], currentAgentName: undefined, }; const nextState: Partial = { directoryScoped: { ...state.directoryScoped, [directoryKey]: nextSnapshot, }, }; if (state.activeDirectoryKey === directoryKey) { nextState.currentAgentName = undefined; } return nextState; }); return true; } // Helper to validate model exists in providers const validateModel = (providerId: string, modelId: string): boolean => { const provider = providers.find((p) => p.id === providerId); if (!provider) return false; return provider.models.some((m) => m.id === modelId); }; // --- Agent Selection --- // Priority: settings.defaultAgent → build → first primary → first agent const primaryAgents = safeAgents.filter((agent) => isPrimaryMode(agent.mode)); const buildAgent = primaryAgents.find((agent) => agent.name === "build"); const fallbackAgent = buildAgent || primaryAgents[0] || safeAgents[0]; let resolvedAgent: Agent = fallbackAgent; // Track invalid settings to clear const invalidSettings: { defaultModel?: string; defaultVariant?: string; defaultAgent?: string } = {}; // 1. Check OpenChamber settings for default agent if (openChamberDefaults.defaultAgent) { const settingsAgent = safeAgents.find((agent) => agent.name === openChamberDefaults.defaultAgent); if (settingsAgent) { resolvedAgent = settingsAgent; } else { // Agent no longer exists - mark for clearing invalidSettings.defaultAgent = ''; } } // --- Model Selection --- // Priority: settings.defaultModel → agent's preferred model → opencode/big-pickle let resolvedProviderId: string | undefined; let resolvedModelId: string | undefined; let resolvedVariant: string | undefined; // 1. Check OpenChamber settings for default model if (openChamberDefaults.defaultModel) { const parsed = parseModelString(openChamberDefaults.defaultModel); if (parsed && validateModel(parsed.providerId, parsed.modelId)) { resolvedProviderId = parsed.providerId; resolvedModelId = parsed.modelId; if (openChamberDefaults.defaultVariant) { const provider = providers.find((p) => p.id === parsed.providerId); const model = provider?.models.find((m) => m.id === parsed.modelId) as { variants?: Record } | undefined; const variants = model?.variants; if (variants && Object.prototype.hasOwnProperty.call(variants, openChamberDefaults.defaultVariant)) { resolvedVariant = openChamberDefaults.defaultVariant; } else { invalidSettings.defaultVariant = ''; } } } else { // Model no longer exists - mark for clearing invalidSettings.defaultModel = ''; } } // 2. Fall back to agent's preferred model if (!resolvedProviderId && resolvedAgent?.model?.providerID && resolvedAgent?.model?.modelID) { const { providerID, modelID } = resolvedAgent.model; if (validateModel(providerID, modelID)) { resolvedProviderId = providerID; resolvedModelId = modelID; } } // 3. Fall back to opencode/big-pickle if (!resolvedProviderId) { if (validateModel(FALLBACK_PROVIDER_ID, FALLBACK_MODEL_ID)) { resolvedProviderId = FALLBACK_PROVIDER_ID; resolvedModelId = FALLBACK_MODEL_ID; } else { // Last resort: first provider's first model const firstProvider = providers[0]; const firstModel = firstProvider?.models[0]; if (firstProvider && firstModel) { resolvedProviderId = firstProvider.id; resolvedModelId = firstModel.id; } } } set((state) => { const baseSnapshot: DirectoryScopedConfig = state.directoryScoped[directoryKey] ?? { providers, agents: safeAgents, currentProviderId: "", currentModelId: "", currentAgentName: undefined, selectedProviderId: "", agentModelSelections: {}, defaultProviders: {}, }; const nextSnapshot: DirectoryScopedConfig = { ...baseSnapshot, providers, agents: safeAgents, currentAgentName: resolvedAgent.name, currentProviderId: resolvedProviderId ?? baseSnapshot.currentProviderId, currentModelId: resolvedModelId ?? baseSnapshot.currentModelId, currentVariant: resolvedVariant, }; const nextState: Partial = { directoryScoped: { ...state.directoryScoped, [directoryKey]: nextSnapshot, }, }; if (state.activeDirectoryKey === directoryKey) { nextState.currentAgentName = resolvedAgent.name; if (resolvedProviderId && resolvedModelId) { nextState.currentProviderId = resolvedProviderId; nextState.currentModelId = resolvedModelId; nextState.currentVariant = resolvedVariant; } } return nextState; }); // Clear invalid settings from storage (best-effort cleanup) if (Object.keys(invalidSettings).length > 0) { // Also clear from store state set({ settingsDefaultModel: invalidSettings.defaultModel !== undefined ? undefined : get().settingsDefaultModel, settingsDefaultVariant: invalidSettings.defaultVariant !== undefined ? undefined : get().settingsDefaultVariant, settingsDefaultAgent: invalidSettings.defaultAgent !== undefined ? undefined : get().settingsDefaultAgent, }); updateDesktopSettings(invalidSettings).catch(() => { // Ignore errors - best effort cleanup }); } return true; } catch (error) { lastError = error; const waitMs = 200 * (attempt + 1); await new Promise((resolve) => setTimeout(resolve, waitMs)); } } console.error("Failed to load agents:", lastError); set((state) => { const providers = state.activeDirectoryKey === directoryKey ? state.providers : (state.directoryScoped[directoryKey]?.providers ?? []); const baseSnapshot: DirectoryScopedConfig = state.directoryScoped[directoryKey] ?? { providers, agents: [], currentProviderId: "", currentModelId: "", currentAgentName: undefined, selectedProviderId: "", agentModelSelections: {}, defaultProviders: {}, }; const nextSnapshot: DirectoryScopedConfig = { ...baseSnapshot, providers, agents: previousAgents, }; const nextState: Partial = { directoryScoped: { ...state.directoryScoped, [directoryKey]: nextSnapshot, }, }; if (state.activeDirectoryKey === directoryKey) { nextState.agents = previousAgents; } return nextState; }); return false; }, setAgent: (agentName: string | undefined) => { const { agents, providers, settingsDefaultModel, settingsDefaultVariant } = get(); set((state) => { const directoryKey = state.activeDirectoryKey; const baseSnapshot: DirectoryScopedConfig = state.directoryScoped[directoryKey] ?? { providers: state.providers, agents: state.agents, currentProviderId: state.currentProviderId, currentModelId: state.currentModelId, currentAgentName: state.currentAgentName, selectedProviderId: state.selectedProviderId, agentModelSelections: state.agentModelSelections, defaultProviders: state.defaultProviders, }; const nextSnapshot: DirectoryScopedConfig = { ...baseSnapshot, currentAgentName: agentName, }; return { currentAgentName: agentName, directoryScoped: { ...state.directoryScoped, [directoryKey]: nextSnapshot, }, }; }); if (agentName && typeof window !== "undefined") { const sessionStore = window.__zustand_session_store__; if (sessionStore) { const sessionState = sessionStore.getState(); const { currentSessionId, isOpenChamberCreatedSession, initializeNewOpenChamberSession, getAgentModelForSession } = sessionState; if (currentSessionId) { sessionStore.setState((state) => { const newAgentContext = new Map(state.currentAgentContext); newAgentContext.set(currentSessionId, agentName); return { currentAgentContext: newAgentContext }; }); } if (currentSessionId && isOpenChamberCreatedSession(currentSessionId)) { const existingAgentModel = getAgentModelForSession(currentSessionId, agentName); if (!existingAgentModel) { initializeNewOpenChamberSession(currentSessionId, agents); } } } } if (agentName && typeof window !== "undefined") { const sessionStore = window.__zustand_session_store__; if (sessionStore?.getState) { const { currentSessionId, getAgentModelForSession } = sessionStore.getState(); if (currentSessionId) { const existingAgentModel = getAgentModelForSession(currentSessionId, agentName); if (existingAgentModel) { return; } } } // If settings has a default model, use it instead of agent's preferred if (settingsDefaultModel) { const parsed = parseModelString(settingsDefaultModel); if (parsed) { const settingsProvider = providers.find((p) => p.id === parsed.providerId); if (settingsProvider?.models.some((m) => m.id === parsed.modelId)) { set((state) => { const directoryKey = state.activeDirectoryKey; const baseSnapshot: DirectoryScopedConfig = state.directoryScoped[directoryKey] ?? { providers: state.providers, agents: state.agents, currentProviderId: state.currentProviderId, currentModelId: state.currentModelId, currentVariant: state.currentVariant, currentAgentName: state.currentAgentName, selectedProviderId: state.selectedProviderId, agentModelSelections: state.agentModelSelections, defaultProviders: state.defaultProviders, }; let nextVariant: string | undefined; if (settingsDefaultVariant) { const settingsProvider = providers.find((p) => p.id === parsed.providerId); const model = settingsProvider?.models.find((m) => m.id === parsed.modelId) as { variants?: Record } | undefined; const variants = model?.variants; if (variants && Object.prototype.hasOwnProperty.call(variants, settingsDefaultVariant)) { nextVariant = settingsDefaultVariant; } } const nextSnapshot: DirectoryScopedConfig = { ...baseSnapshot, currentProviderId: parsed.providerId, currentModelId: parsed.modelId, currentVariant: nextVariant, }; return { currentProviderId: parsed.providerId, currentModelId: parsed.modelId, currentVariant: nextVariant, directoryScoped: { ...state.directoryScoped, [directoryKey]: nextSnapshot, }, }; }); return; } } } // Fall back to agent's preferred model const agent = agents.find((candidate) => candidate.name === agentName); const agentModelSelection = agent?.model; if (agentModelSelection?.providerID && agentModelSelection?.modelID) { const { providerID, modelID } = agentModelSelection; const agentProvider = providers.find((provider) => provider.id === providerID); const agentModel = agentProvider?.models.find((model) => model.id === modelID); if (agentModel) { set((state) => { const directoryKey = state.activeDirectoryKey; const baseSnapshot: DirectoryScopedConfig = state.directoryScoped[directoryKey] ?? { providers: state.providers, agents: state.agents, currentProviderId: state.currentProviderId, currentModelId: state.currentModelId, currentAgentName: state.currentAgentName, selectedProviderId: state.selectedProviderId, agentModelSelections: state.agentModelSelections, defaultProviders: state.defaultProviders, }; const nextSnapshot: DirectoryScopedConfig = { ...baseSnapshot, currentProviderId: providerID, currentModelId: modelID, selectedProviderId: providerID, }; return { currentProviderId: providerID, currentModelId: modelID, selectedProviderId: providerID, directoryScoped: { ...state.directoryScoped, [directoryKey]: nextSnapshot, }, }; }); } } } }, setSettingsDefaultModel: (model: string | undefined) => { set({ settingsDefaultModel: model }); }, setSettingsDefaultVariant: (variant: string | undefined) => { set({ settingsDefaultVariant: variant }); }, setSettingsDefaultAgent: (agent: string | undefined) => { set({ settingsDefaultAgent: agent }); }, setSettingsAutoCreateWorktree: (enabled: boolean) => { set({ settingsAutoCreateWorktree: enabled }); }, setSettingsGitmojiEnabled: (enabled: boolean) => { set({ settingsGitmojiEnabled: enabled }); }, checkConnection: async () => { const maxAttempts = 5; let attempt = 0; let lastError: unknown = null; while (attempt < maxAttempts) { try { const isHealthy = await opencodeClient.checkHealth(); set({ isConnected: isHealthy }); return isHealthy; } catch (error) { lastError = error; attempt += 1; const delay = 400 * attempt; await sleep(delay); } } if (lastError) { console.warn("[ConfigStore] Failed to reach OpenCode after retrying:", lastError); } set({ isConnected: false }); return false; }, initializeApp: async () => { try { const debug = streamDebugEnabled(); if (debug) console.log("Starting app initialization..."); const isConnected = await get().checkConnection(); if (debug) console.log("Connection check result:", isConnected); if (!isConnected) { if (debug) console.log("Server not connected"); set({ isConnected: false }); return; } if (debug) console.log("Initializing app..."); await opencodeClient.initApp(); if (debug) console.log("Loading providers..."); await get().loadProviders(); if (debug) console.log("Loading agents..."); await get().loadAgents(); set({ isInitialized: true, isConnected: true }); if (debug) console.log("App initialized successfully"); } catch (error) { console.error("Failed to initialize app:", error); set({ isInitialized: false, isConnected: false }); } }, getCurrentProvider: () => { const { providers, currentProviderId } = get(); return providers.find((p) => p.id === currentProviderId); }, getCurrentModel: () => { const provider = get().getCurrentProvider(); const { currentModelId } = get(); if (!provider) { return undefined; } return provider.models.find((model) => model.id === currentModelId); }, getCurrentAgent: () => { const { agents, currentAgentName } = get(); if (!currentAgentName) return undefined; return agents.find((a) => a.name === currentAgentName); }, getModelMetadata: (providerId: string, modelId: string) => { const key = buildModelMetadataKey(providerId, modelId); if (!key) { return undefined; } const { modelsMetadata } = get(); return modelsMetadata.get(key); }, getVisibleAgents: () => { const { agents } = get(); return filterVisibleAgents(agents); }, }), { name: "config-store", storage: createJSONStorage(() => getSafeStorage()), partialize: (state) => ({ activeDirectoryKey: state.activeDirectoryKey, directoryScoped: state.directoryScoped, currentProviderId: state.currentProviderId, currentModelId: state.currentModelId, currentVariant: state.currentVariant, currentAgentName: state.currentAgentName, selectedProviderId: state.selectedProviderId, agentModelSelections: state.agentModelSelections, defaultProviders: state.defaultProviders, settingsDefaultModel: state.settingsDefaultModel, settingsDefaultVariant: state.settingsDefaultVariant, settingsDefaultAgent: state.settingsDefaultAgent, settingsAutoCreateWorktree: state.settingsAutoCreateWorktree, settingsGitmojiEnabled: state.settingsGitmojiEnabled, }), }, ), ), ); if (typeof window !== "undefined") { window.__zustand_config_store__ = useConfigStore; } let unsubscribeConfigStoreChanges: (() => void) | null = null; if (!unsubscribeConfigStoreChanges) { unsubscribeConfigStoreChanges = subscribeToConfigChanges(async (event) => { const tasks: Promise[] = []; if (scopeMatches(event, "agents")) { const { loadAgents } = useConfigStore.getState(); tasks.push(loadAgents().then(() => {})); } if (scopeMatches(event, "providers")) { const { loadProviders } = useConfigStore.getState(); tasks.push(loadProviders()); } if (tasks.length > 0) { await Promise.all(tasks); } }); } let unsubscribeConfigStoreDirectoryChanges: (() => void) | null = null; if (typeof window !== "undefined" && !unsubscribeConfigStoreDirectoryChanges) { unsubscribeConfigStoreDirectoryChanges = useDirectoryStore.subscribe((state, prevState) => { const nextKey = toDirectoryKey(state.currentDirectory); const prevKey = toDirectoryKey(prevState.currentDirectory); if (nextKey === prevKey) { return; } void useConfigStore.getState().activateDirectory(state.currentDirectory); }); }