diff --git a/packages/desktop/src-tauri/src/commands/settings.rs b/packages/desktop/src-tauri/src/commands/settings.rs index 35d2d633..e9c416e2 100644 --- a/packages/desktop/src-tauri/src/commands/settings.rs +++ b/packages/desktop/src-tauri/src/commands/settings.rs @@ -234,13 +234,27 @@ fn sanitize_settings_update(payload: &Value) -> Value { } } if let Some(Value::String(s)) = obj.get("defaultModel") { - if !s.is_empty() { - result_obj.insert("defaultModel".to_string(), json!(s)); + let trimmed = s.trim(); + if trimmed.is_empty() { + result_obj.insert("defaultModel".to_string(), Value::Null); + } else { + result_obj.insert("defaultModel".to_string(), json!(trimmed)); + } + } + if let Some(Value::String(s)) = obj.get("defaultVariant") { + let trimmed = s.trim(); + if trimmed.is_empty() { + result_obj.insert("defaultVariant".to_string(), Value::Null); + } else { + result_obj.insert("defaultVariant".to_string(), json!(trimmed)); } } if let Some(Value::String(s)) = obj.get("defaultAgent") { - if !s.is_empty() { - result_obj.insert("defaultAgent".to_string(), json!(s)); + let trimmed = s.trim(); + if trimmed.is_empty() { + result_obj.insert("defaultAgent".to_string(), Value::Null); + } else { + result_obj.insert("defaultAgent".to_string(), json!(trimmed)); } } diff --git a/packages/ui/src/components/chat/ModelControls.tsx b/packages/ui/src/components/chat/ModelControls.tsx index 13af6f2d..d4f9e410 100644 --- a/packages/ui/src/components/chat/ModelControls.tsx +++ b/packages/ui/src/components/chat/ModelControls.tsx @@ -276,6 +276,7 @@ export const ModelControls: React.FC = ({ className }) => { currentModelId, currentVariant, currentAgentName, + settingsDefaultVariant, setProvider, setModel, setCurrentVariant, @@ -865,7 +866,7 @@ export const ModelControls: React.FC = ({ className }) => { }, [currentAgentName, currentSessionId, getAgentModelForSession, tryApplyModelSelection, agents, contextHydrated]); React.useEffect(() => { - if (!contextHydrated || !currentSessionId || !currentAgentName) { + if (!contextHydrated || !currentAgentName) { setCurrentVariant(undefined); return; } @@ -875,6 +876,28 @@ export const ModelControls: React.FC = ({ className }) => { return; } + if (availableVariants.length === 0) { + setCurrentVariant(undefined); + return; + } + + if (currentVariant && !availableVariants.includes(currentVariant)) { + setCurrentVariant(undefined); + return; + } + + // Draft state (no session yet): seed from settings default, but don't override + // user selection while drafting. + if (!currentSessionId) { + if (!currentVariant) { + const desired = settingsDefaultVariant && availableVariants.includes(settingsDefaultVariant) + ? settingsDefaultVariant + : undefined; + setCurrentVariant(desired); + } + return; + } + const savedVariant = getAgentModelVariantForSession( currentSessionId, currentAgentName, @@ -882,12 +905,11 @@ export const ModelControls: React.FC = ({ className }) => { currentModelId, ); - if (savedVariant && !availableVariants.includes(savedVariant)) { - setCurrentVariant(undefined); - return; - } + const resolvedSaved = savedVariant && availableVariants.includes(savedVariant) + ? savedVariant + : undefined; - setCurrentVariant(savedVariant); + setCurrentVariant(resolvedSaved); }, [ availableVariants, contextHydrated, @@ -895,8 +917,10 @@ export const ModelControls: React.FC = ({ className }) => { currentAgentName, currentProviderId, currentModelId, + currentVariant, getAgentModelVariantForSession, setCurrentVariant, + settingsDefaultVariant, ]); const handleVariantSelect = React.useCallback((variant: string | undefined) => { diff --git a/packages/ui/src/components/sections/openchamber/DefaultsSettings.tsx b/packages/ui/src/components/sections/openchamber/DefaultsSettings.tsx index c55e5e91..2f8cce33 100644 --- a/packages/ui/src/components/sections/openchamber/DefaultsSettings.tsx +++ b/packages/ui/src/components/sections/openchamber/DefaultsSettings.tsx @@ -3,6 +3,7 @@ import { RiInformationLine } from '@remixicon/react'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { ModelSelector } from '@/components/sections/agents/ModelSelector'; import { AgentSelector } from '@/components/sections/commands/AgentSelector'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { updateDesktopSettings } from '@/lib/persistence'; import { isDesktopRuntime, getDesktopSettings } from '@/lib/desktop'; import { useConfigStore } from '@/stores/useConfigStore'; @@ -13,13 +14,16 @@ export const DefaultsSettings: React.FC = () => { const setProvider = useConfigStore((state) => state.setProvider); const setModel = useConfigStore((state) => state.setModel); const setAgent = useConfigStore((state) => state.setAgent); + const setCurrentVariant = useConfigStore((state) => state.setCurrentVariant); const setSettingsDefaultModel = useConfigStore((state) => state.setSettingsDefaultModel); + const setSettingsDefaultVariant = useConfigStore((state) => state.setSettingsDefaultVariant); const setSettingsDefaultAgent = useConfigStore((state) => state.setSettingsDefaultAgent); const settingsAutoCreateWorktree = useConfigStore((state) => state.settingsAutoCreateWorktree); const setSettingsAutoCreateWorktree = useConfigStore((state) => state.setSettingsAutoCreateWorktree); const providers = useConfigStore((state) => state.providers); const [defaultModel, setDefaultModel] = React.useState(); + const [defaultVariant, setDefaultVariant] = React.useState(); const [defaultAgent, setDefaultAgent] = React.useState(); const [isLoading, setIsLoading] = React.useState(true); @@ -35,7 +39,7 @@ export const DefaultsSettings: React.FC = () => { React.useEffect(() => { const loadSettings = async () => { try { - let data: { defaultModel?: string; defaultAgent?: string } | null = null; + let data: { defaultModel?: string; defaultVariant?: string; defaultAgent?: string } | null = null; // 1. Desktop runtime (Tauri) if (isDesktopRuntime()) { @@ -50,6 +54,7 @@ export const DefaultsSettings: React.FC = () => { if (settings) { data = { defaultModel: typeof settings.defaultModel === 'string' ? settings.defaultModel : undefined, + defaultVariant: typeof (settings as Record).defaultVariant === 'string' ? ((settings as Record).defaultVariant as string) : undefined, defaultAgent: typeof settings.defaultAgent === 'string' ? settings.defaultAgent : undefined, }; } @@ -71,8 +76,13 @@ export const DefaultsSettings: React.FC = () => { } if (data) { - setDefaultModel(data.defaultModel); - setDefaultAgent(data.defaultAgent); + const model = typeof data.defaultModel === 'string' && data.defaultModel.trim().length > 0 ? data.defaultModel.trim() : undefined; + const variant = typeof data.defaultVariant === 'string' && data.defaultVariant.trim().length > 0 ? data.defaultVariant.trim() : undefined; + const agent = typeof data.defaultAgent === 'string' && data.defaultAgent.trim().length > 0 ? data.defaultAgent.trim() : undefined; + + setDefaultModel(model); + setDefaultVariant(variant); + setDefaultAgent(agent); } } catch (error) { console.warn('Failed to load defaults settings:', error); @@ -87,6 +97,11 @@ export const DefaultsSettings: React.FC = () => { const newValue = providerId && modelId ? `${providerId}/${modelId}` : undefined; setDefaultModel(newValue); + // Reset variant when model changes (model-specific) + setDefaultVariant(undefined); + setSettingsDefaultVariant(undefined); + setCurrentVariant(undefined); + // Update config store settings default (used by setAgent logic) setSettingsDefaultModel(newValue); @@ -102,11 +117,29 @@ export const DefaultsSettings: React.FC = () => { try { await updateDesktopSettings({ defaultModel: newValue ?? '', + defaultVariant: '', }); } catch (error) { console.warn('Failed to save default model:', error); } - }, [providers, setProvider, setModel, setSettingsDefaultModel]); + }, [providers, setCurrentVariant, setProvider, setModel, setSettingsDefaultModel, setSettingsDefaultVariant]); + + const DEFAULT_VARIANT_VALUE = '__default__'; + + const handleVariantChange = React.useCallback(async (variant: string) => { + const newValue = variant === DEFAULT_VARIANT_VALUE ? undefined : (variant || undefined); + setDefaultVariant(newValue); + setSettingsDefaultVariant(newValue); + setCurrentVariant(newValue); + + try { + await updateDesktopSettings({ + defaultVariant: newValue ?? '', + }); + } catch (error) { + console.warn('Failed to save default variant:', error); + } + }, [setCurrentVariant, setSettingsDefaultVariant]); const handleAgentChange = React.useCallback(async (agentName: string) => { const newValue = agentName || undefined; @@ -129,6 +162,31 @@ export const DefaultsSettings: React.FC = () => { } }, [setAgent, setSettingsDefaultAgent]); + const availableVariants = React.useMemo(() => { + const provider = providers.find((p) => p.id === parsedModel.providerId); + const model = provider?.models.find((m: Record) => (m as { id?: string }).id === parsedModel.modelId) as + | { variants?: Record } + | undefined; + const variants = model?.variants; + if (!variants) { + return []; + } + return Object.keys(variants); + }, [parsedModel.modelId, parsedModel.providerId, providers]); + + const supportsVariants = availableVariants.length > 0; + + React.useEffect(() => { + if (!supportsVariants && defaultVariant) { + setDefaultVariant(undefined); + setSettingsDefaultVariant(undefined); + setCurrentVariant(undefined); + updateDesktopSettings({ defaultVariant: '' }).catch(() => { + // best effort + }); + } + }, [defaultVariant, setCurrentVariant, setSettingsDefaultVariant, supportsVariants]); + const handleAutoWorktreeChange = React.useCallback(async (e: React.ChangeEvent) => { const enabled = e.target.checked; setSettingsAutoCreateWorktree(enabled); @@ -161,29 +219,53 @@ export const DefaultsSettings: React.FC = () => { -
-
- - -
+
+
+ + +
-
- - -
-
+ {supportsVariants && ( +
+ + +
+ )} + +
+ + +
+
{(defaultModel || defaultAgent) && (
New sessions will start with:{' '} - {defaultModel && {defaultModel}} + {defaultModel && ( + + {defaultModel} + {supportsVariants ? ` (${defaultVariant ?? 'default'})` : ''} + + )} {defaultModel && defaultAgent && ' / '} {defaultAgent && {defaultAgent}}
diff --git a/packages/ui/src/lib/desktop.ts b/packages/ui/src/lib/desktop.ts index 3455ca5f..019ba68d 100644 --- a/packages/ui/src/lib/desktop.ts +++ b/packages/ui/src/lib/desktop.ts @@ -54,6 +54,7 @@ export type DesktopSettings = { autoDeleteEnabled?: boolean; autoDeleteAfterDays?: number; defaultModel?: string; // format: "provider/model" + defaultVariant?: string; defaultAgent?: string; autoCreateWorktree?: boolean; queueModeEnabled?: boolean; diff --git a/packages/ui/src/lib/persistence.ts b/packages/ui/src/lib/persistence.ts index ff05eb3d..c0ed0b29 100644 --- a/packages/ui/src/lib/persistence.ts +++ b/packages/ui/src/lib/persistence.ts @@ -252,6 +252,9 @@ const sanitizeWebSettings = (payload: unknown): DesktopSettings | null => { if (typeof candidate.defaultModel === 'string' && candidate.defaultModel.length > 0) { result.defaultModel = candidate.defaultModel; } + if (typeof candidate.defaultVariant === 'string' && candidate.defaultVariant.length > 0) { + result.defaultVariant = candidate.defaultVariant; + } if (typeof candidate.defaultAgent === 'string' && candidate.defaultAgent.length > 0) { result.defaultAgent = candidate.defaultAgent; } diff --git a/packages/ui/src/lib/worktreeSessionCreator.ts b/packages/ui/src/lib/worktreeSessionCreator.ts index 74ce4e63..7670ae76 100644 --- a/packages/ui/src/lib/worktreeSessionCreator.ts +++ b/packages/ui/src/lib/worktreeSessionCreator.ts @@ -160,6 +160,24 @@ export async function createWorktreeSession(): Promise<{ id: string } | null> { useContextStore.getState().saveSessionModelSelection(session.id, providerId, modelId); // Also save the specific agent's model preference for this session useContextStore.getState().saveAgentModelForSession(session.id, agentName, providerId, modelId); + + // Seed default variant into session context so ModelControls restore logic + // doesn't wipe it on first switch to the new session. + const settingsDefaultVariant = configState.settingsDefaultVariant; + if (settingsDefaultVariant) { + const provider = configState.providers.find((p) => p.id === providerId); + const model = provider?.models.find((m: Record) => (m as { id?: string }).id === modelId) as + | { variants?: Record } + | undefined; + const variants = model?.variants; + + if (variants && Object.prototype.hasOwnProperty.call(variants, settingsDefaultVariant)) { + configState.setCurrentVariant(settingsDefaultVariant); + useContextStore + .getState() + .saveAgentModelVariantForSession(session.id, agentName, providerId, modelId, settingsDefaultVariant); + } + } } } } diff --git a/packages/ui/src/stores/useConfigStore.ts b/packages/ui/src/stores/useConfigStore.ts index 880b8d60..b4e666ff 100644 --- a/packages/ui/src/stores/useConfigStore.ts +++ b/packages/ui/src/stores/useConfigStore.ts @@ -22,6 +22,7 @@ const FALLBACK_MODEL_ID = "big-pickle"; interface OpenChamberDefaults { defaultModel?: string; + defaultVariant?: string; defaultAgent?: string; autoCreateWorktree?: boolean; } @@ -33,6 +34,7 @@ const fetchOpenChamberDefaults = async (): Promise => { const settings = await getDesktopSettings(); return { defaultModel: settings?.defaultModel, + defaultVariant: settings?.defaultVariant, defaultAgent: settings?.defaultAgent, autoCreateWorktree: settings?.autoCreateWorktree, }; @@ -45,9 +47,14 @@ const fetchOpenChamberDefaults = async (): Promise => { 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() : ''; + return { - defaultModel: typeof data?.defaultModel === 'string' ? data.defaultModel : undefined, - defaultAgent: typeof data?.defaultAgent === 'string' ? data.defaultAgent : undefined, + 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, }; } @@ -65,9 +72,14 @@ const fetchOpenChamberDefaults = async (): Promise => { 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() : ''; + return { - defaultModel: typeof data?.defaultModel === 'string' ? data.defaultModel : undefined, - defaultAgent: typeof data?.defaultAgent === 'string' ? data.defaultAgent : undefined, + 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, }; } catch { @@ -326,10 +338,12 @@ const resolveInitialDirectoryKey = (): string => { }; 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 } }; @@ -355,6 +369,7 @@ interface ConfigStore { 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; @@ -370,6 +385,7 @@ interface ConfigStore { 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; saveAgentModelSelection: (agentName: string, providerId: string, modelId: string) => void; @@ -412,6 +428,7 @@ export const useConfigStore = create()( isInitialized: false, modelsMetadata: new Map(), settingsDefaultModel: undefined, + settingsDefaultVariant: undefined, settingsDefaultAgent: undefined, settingsAutoCreateWorktree: false, @@ -427,6 +444,7 @@ export const useConfigStore = create()( agents: snapshot.agents, currentProviderId: snapshot.currentProviderId, currentModelId: snapshot.currentModelId, + currentVariant: snapshot.currentVariant, currentAgentName: snapshot.currentAgentName, selectedProviderId: snapshot.selectedProviderId, agentModelSelections: snapshot.agentModelSelections, @@ -564,14 +582,14 @@ export const useConfigStore = create()( 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] ?? { @@ -579,6 +597,7 @@ export const useConfigStore = create()( agents: state.agents, currentProviderId: state.currentProviderId, currentModelId: state.currentModelId, + currentVariant: state.currentVariant, currentAgentName: state.currentAgentName, selectedProviderId: state.selectedProviderId, agentModelSelections: state.agentModelSelections, @@ -612,6 +631,7 @@ export const useConfigStore = create()( agents: state.agents, currentProviderId: state.currentProviderId, currentModelId: state.currentModelId, + currentVariant: state.currentVariant, currentAgentName: state.currentAgentName, selectedProviderId: state.selectedProviderId, agentModelSelections: state.agentModelSelections, @@ -638,7 +658,32 @@ export const useConfigStore = create()( if (state.currentVariant === variant) { return state; } - return { currentVariant: variant }; + + 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, + }, + }; }); }, @@ -659,18 +704,17 @@ export const useConfigStore = create()( const current = get().currentVariant; if (!current) { - set((state) => (state.currentVariant === variantKeys[0] ? state : { currentVariant: variantKeys[0] })); - return; - } - - const index = variantKeys.indexOf(current); - if (index === -1 || index === variantKeys.length - 1) { - set((state) => (state.currentVariant === undefined ? state : { currentVariant: undefined })); + get().setCurrentVariant(variantKeys[0]); return; } - const nextVariant = variantKeys[index + 1]; - set((state) => (state.currentVariant === nextVariant ? state : { currentVariant: nextVariant })); + const index = variantKeys.indexOf(current); + if (index === -1 || index === variantKeys.length - 1) { + get().setCurrentVariant(undefined); + return; + } + + get().setCurrentVariant(variantKeys[index + 1]); }, setSelectedProvider: (providerId: string) => { @@ -780,9 +824,10 @@ export const useConfigStore = create()( }; const nextState: Partial = { - settingsDefaultModel: openChamberDefaults.defaultModel, - settingsDefaultAgent: openChamberDefaults.defaultAgent, - settingsAutoCreateWorktree: openChamberDefaults.autoCreateWorktree ?? false, + settingsDefaultModel: openChamberDefaults.defaultModel, + settingsDefaultVariant: openChamberDefaults.defaultVariant, + settingsDefaultAgent: openChamberDefaults.defaultAgent, + settingsAutoCreateWorktree: openChamberDefaults.autoCreateWorktree ?? false, directoryScoped: { ...state.directoryScoped, [directoryKey]: nextSnapshot, @@ -801,9 +846,10 @@ export const useConfigStore = create()( const baseSnapshot: DirectoryScopedConfig = state.directoryScoped[directoryKey] ?? { providers, agents: [], - currentProviderId: "", - currentModelId: "", - currentAgentName: undefined, + currentProviderId: "", + currentModelId: "", + currentVariant: undefined, + currentAgentName: undefined, selectedProviderId: "", agentModelSelections: {}, defaultProviders: {}, @@ -849,7 +895,7 @@ export const useConfigStore = create()( let resolvedAgent: Agent = fallbackAgent; // Track invalid settings to clear - const invalidSettings: { defaultModel?: string; defaultAgent?: string } = {}; + const invalidSettings: { defaultModel?: string; defaultVariant?: string; defaultAgent?: string } = {}; // 1. Check OpenChamber settings for default agent if (openChamberDefaults.defaultAgent) { @@ -862,22 +908,34 @@ export const useConfigStore = create()( } } - // --- Model Selection --- - // Priority: settings.defaultModel → agent's preferred model → opencode/big-pickle - let resolvedProviderId: string | undefined; - let resolvedModelId: string | undefined; + // --- 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; - } else { - // Model no longer exists - mark for clearing - invalidSettings.defaultModel = ''; - } - } + // 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) { @@ -919,8 +977,9 @@ export const useConfigStore = create()( providers, agents: safeAgents, currentAgentName: resolvedAgent.name, - currentProviderId: resolvedProviderId ?? baseSnapshot.currentProviderId, - currentModelId: resolvedModelId ?? baseSnapshot.currentModelId, + currentProviderId: resolvedProviderId ?? baseSnapshot.currentProviderId, + currentModelId: resolvedModelId ?? baseSnapshot.currentModelId, + currentVariant: resolvedVariant, }; const nextState: Partial = { @@ -930,13 +989,14 @@ export const useConfigStore = create()( }, }; - if (state.activeDirectoryKey === directoryKey) { - nextState.currentAgentName = resolvedAgent.name; - if (resolvedProviderId && resolvedModelId) { - nextState.currentProviderId = resolvedProviderId; - nextState.currentModelId = resolvedModelId; - } - } + if (state.activeDirectoryKey === directoryKey) { + nextState.currentAgentName = resolvedAgent.name; + if (resolvedProviderId && resolvedModelId) { + nextState.currentProviderId = resolvedProviderId; + nextState.currentModelId = resolvedModelId; + nextState.currentVariant = resolvedVariant; + } + } return nextState; }); @@ -944,10 +1004,11 @@ export const useConfigStore = create()( // 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, - settingsDefaultAgent: invalidSettings.defaultAgent !== undefined ? undefined : get().settingsDefaultAgent, - }); + 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 }); @@ -1003,7 +1064,7 @@ export const useConfigStore = create()( }, setAgent: (agentName: string | undefined) => { - const { agents, providers, settingsDefaultModel } = get(); + const { agents, providers, settingsDefaultModel, settingsDefaultVariant } = get(); set((state) => { const directoryKey = state.activeDirectoryKey; @@ -1086,21 +1147,34 @@ export const useConfigStore = create()( 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, @@ -1156,13 +1230,17 @@ export const useConfigStore = create()( } }, - setSettingsDefaultModel: (model: string | undefined) => { - set({ settingsDefaultModel: model }); - }, + setSettingsDefaultModel: (model: string | undefined) => { + set({ settingsDefaultModel: model }); + }, - setSettingsDefaultAgent: (agent: string | undefined) => { - set({ settingsDefaultAgent: agent }); - }, + setSettingsDefaultVariant: (variant: string | undefined) => { + set({ settingsDefaultVariant: variant }); + }, + + setSettingsDefaultAgent: (agent: string | undefined) => { + set({ settingsDefaultAgent: agent }); + }, setSettingsAutoCreateWorktree: (enabled: boolean) => { set({ settingsAutoCreateWorktree: enabled }); diff --git a/packages/vscode/src/bridge.ts b/packages/vscode/src/bridge.ts index d75ddbb3..82ff93fa 100644 --- a/packages/vscode/src/bridge.ts +++ b/packages/vscode/src/bridge.ts @@ -80,6 +80,15 @@ const persistSettings = async (changes: Record, ctx?: BridgeCon const current = readSettings(ctx); const restChanges = { ...(changes || {}) }; delete restChanges.lastDirectory; + + // Normalize empty-string clears to key removal (match web/desktop behavior) + for (const key of ['defaultModel', 'defaultVariant', 'defaultAgent']) { + const value = restChanges[key]; + if (typeof value === 'string' && value.trim().length === 0) { + delete restChanges[key]; + } + } + const merged = { ...current, ...restChanges, lastDirectory: current.lastDirectory }; await ctx?.context?.globalState.update(SETTINGS_KEY, merged); return merged; diff --git a/packages/web/server/index.js b/packages/web/server/index.js index fc5079b5..de5803e9 100644 --- a/packages/web/server/index.js +++ b/packages/web/server/index.js @@ -590,11 +590,17 @@ const sanitizeSettingsUpdate = (payload) => { result.typographySizes = typography; } - if (typeof candidate.defaultModel === 'string' && candidate.defaultModel.length > 0) { - result.defaultModel = candidate.defaultModel; + if (typeof candidate.defaultModel === 'string') { + const trimmed = candidate.defaultModel.trim(); + result.defaultModel = trimmed.length > 0 ? trimmed : undefined; } - if (typeof candidate.defaultAgent === 'string' && candidate.defaultAgent.length > 0) { - result.defaultAgent = candidate.defaultAgent; + if (typeof candidate.defaultVariant === 'string') { + const trimmed = candidate.defaultVariant.trim(); + result.defaultVariant = trimmed.length > 0 ? trimmed : undefined; + } + if (typeof candidate.defaultAgent === 'string') { + const trimmed = candidate.defaultAgent.trim(); + result.defaultAgent = trimmed.length > 0 ? trimmed : undefined; } if (typeof candidate.queueModeEnabled === 'boolean') { result.queueModeEnabled = candidate.queueModeEnabled;