Files
openchamber/packages/ui/src/stores/useConfigStore.ts
T
Bohdan Triapitsyn 494da1315c refactoring: switched to gpt-5-nano for commit message generation
Remove commitMessageModel from persisted settings and defaults
Eliminate UI controls and state for commitMessageModel in GitSettings and config store
Update backend and persistence layers to stop reading or writing commitMessageModel
2026-01-19 03:42:34 +02:00

1462 lines
68 KiB
TypeScript

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 { isDesktopRuntime, getDesktopSettings } from "@/lib/desktop";
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<OpenChamberDefaults> => {
try {
// 1. Desktop runtime (Tauri)
if (isDesktopRuntime()) {
const settings = await getDesktopSettings();
return {
defaultModel: settings?.defaultModel,
defaultVariant: settings?.defaultVariant,
defaultAgent: settings?.defaultAgent,
autoCreateWorktree: settings?.autoCreateWorktree,
gitmojiEnabled: settings?.gitmojiEnabled,
};
}
// 2. 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
}
}
// 3. Fetch API (Web)
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<Provider, "models"> & { 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<string, ModelsDevModelEntry | undefined>;
}
const isRecord = (value: unknown): value is Record<string, unknown> =>
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<string, ModelMetadata> => {
const metadataMap = new Map<string, ModelMetadata>();
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<Map<string, ModelMetadata>> => {
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<Map<string, ModelMetadata>> | null = null;
const ensureModelsMetadataFetch = (
getModelsMetadata: () => Map<string, ModelMetadata>,
setModelsMetadata: (metadata: Map<string, ModelMetadata>) => 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<string, ModelMetadata>())
.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<string, 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 };
isConnected: boolean;
isInitialized: boolean;
modelsMetadata: Map<string, ModelMetadata>;
// 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<void>;
loadProviders: (options?: { directory?: string | null }) => Promise<void>;
loadAgents: (options?: { directory?: string | null }) => Promise<boolean>;
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<boolean>;
initializeApp: () => Promise<void>;
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<StoreApi<ConfigStore>>;
__zustand_session_store__?: UseBoundStore<StoreApi<SessionStore>>;
}
}
export const useConfigStore = create<ConfigStore>()(
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<string, ModelMetadata>(),
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<ConfigStore> = {
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<string, unknown> } | 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<ConfigStore> = {
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<string, unknown> } | 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<string, unknown> } | 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<ConfigStore> = {
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<ConfigStore> = {
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<string, unknown> } | 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<ConfigStore> = {
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<ConfigStore> = {
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<string, unknown> } | 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<void>[] = [];
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);
});
}