feat(settings): add default model and agent settings with UI integration
This commit is contained in:
@@ -131,6 +131,16 @@ fn sanitize_settings_update(payload: &Value) -> Value {
|
||||
result_obj.insert("markdownDisplayMode".to_string(), json!(s));
|
||||
}
|
||||
}
|
||||
if let Some(Value::String(s)) = obj.get("defaultModel") {
|
||||
if !s.is_empty() {
|
||||
result_obj.insert("defaultModel".to_string(), json!(s));
|
||||
}
|
||||
}
|
||||
if let Some(Value::String(s)) = obj.get("defaultAgent") {
|
||||
if !s.is_empty() {
|
||||
result_obj.insert("defaultAgent".to_string(), json!(s));
|
||||
}
|
||||
}
|
||||
|
||||
// Boolean fields
|
||||
if let Some(Value::Bool(b)) = obj.get("useSystemTheme") {
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
import React from 'react';
|
||||
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 { updateDesktopSettings } from '@/lib/persistence';
|
||||
import { isDesktopRuntime, getDesktopSettings } from '@/lib/desktop';
|
||||
import { useConfigStore } from '@/stores/useConfigStore';
|
||||
import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
|
||||
|
||||
export const DefaultsSettings: React.FC = () => {
|
||||
const setProvider = useConfigStore((state) => state.setProvider);
|
||||
const setModel = useConfigStore((state) => state.setModel);
|
||||
const setAgent = useConfigStore((state) => state.setAgent);
|
||||
const setSettingsDefaultModel = useConfigStore((state) => state.setSettingsDefaultModel);
|
||||
const setSettingsDefaultAgent = useConfigStore((state) => state.setSettingsDefaultAgent);
|
||||
const providers = useConfigStore((state) => state.providers);
|
||||
|
||||
const [defaultModel, setDefaultModel] = React.useState<string | undefined>();
|
||||
const [defaultAgent, setDefaultAgent] = React.useState<string | undefined>();
|
||||
const [isLoading, setIsLoading] = React.useState(true);
|
||||
|
||||
// Parse "provider/model" string into separate parts
|
||||
const parsedModel = React.useMemo(() => {
|
||||
if (!defaultModel) return { providerId: '', modelId: '' };
|
||||
const parts = defaultModel.split('/');
|
||||
if (parts.length !== 2) return { providerId: '', modelId: '' };
|
||||
return { providerId: parts[0] || '', modelId: parts[1] || '' };
|
||||
}, [defaultModel]);
|
||||
|
||||
// Load current settings
|
||||
React.useEffect(() => {
|
||||
const loadSettings = async () => {
|
||||
try {
|
||||
let data: { defaultModel?: string; defaultAgent?: string } | null = null;
|
||||
|
||||
// 1. Desktop runtime (Tauri)
|
||||
if (isDesktopRuntime()) {
|
||||
data = await getDesktopSettings();
|
||||
} else {
|
||||
// 2. Runtime settings API (VSCode)
|
||||
const runtimeSettings = getRegisteredRuntimeAPIs()?.settings;
|
||||
if (runtimeSettings) {
|
||||
try {
|
||||
const result = await runtimeSettings.load();
|
||||
const settings = result?.settings;
|
||||
if (settings) {
|
||||
data = {
|
||||
defaultModel: typeof settings.defaultModel === 'string' ? settings.defaultModel : undefined,
|
||||
defaultAgent: typeof settings.defaultAgent === 'string' ? settings.defaultAgent : undefined,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// Fall through to fetch
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Fetch API (Web)
|
||||
if (!data) {
|
||||
const response = await fetch('/api/config/settings', {
|
||||
method: 'GET',
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
if (response.ok) {
|
||||
data = await response.json();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (data) {
|
||||
setDefaultModel(data.defaultModel);
|
||||
setDefaultAgent(data.defaultAgent);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load defaults settings:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
loadSettings();
|
||||
}, []);
|
||||
|
||||
const handleModelChange = React.useCallback(async (providerId: string, modelId: string) => {
|
||||
const newValue = providerId && modelId ? `${providerId}/${modelId}` : undefined;
|
||||
setDefaultModel(newValue);
|
||||
|
||||
// Update config store settings default (used by setAgent logic)
|
||||
setSettingsDefaultModel(newValue);
|
||||
|
||||
// Also update current model immediately so new sessions use this model
|
||||
if (providerId && modelId) {
|
||||
const provider = providers.find((p) => p.id === providerId);
|
||||
if (provider) {
|
||||
setProvider(providerId);
|
||||
setModel(modelId);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await updateDesktopSettings({
|
||||
defaultModel: newValue ?? '',
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('Failed to save default model:', error);
|
||||
}
|
||||
}, [providers, setProvider, setModel, setSettingsDefaultModel]);
|
||||
|
||||
const handleAgentChange = React.useCallback(async (agentName: string) => {
|
||||
const newValue = agentName || undefined;
|
||||
setDefaultAgent(newValue);
|
||||
|
||||
// Update config store settings default
|
||||
setSettingsDefaultAgent(newValue);
|
||||
|
||||
// Update current agent (setAgent will respect settingsDefaultModel)
|
||||
if (agentName) {
|
||||
setAgent(agentName);
|
||||
}
|
||||
|
||||
try {
|
||||
await updateDesktopSettings({
|
||||
defaultAgent: newValue ?? '',
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('Failed to save default agent:', error);
|
||||
}
|
||||
}, [setAgent, setSettingsDefaultAgent]);
|
||||
|
||||
if (isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="typography-ui-header font-semibold text-foreground">Default model & agent</h3>
|
||||
<Tooltip delayDuration={1000}>
|
||||
<TooltipTrigger asChild>
|
||||
<RiInformationLine className="h-3.5 w-3.5 text-muted-foreground/60 cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent sideOffset={8} className="max-w-xs">
|
||||
Set the default model and agent for new sessions.<br />
|
||||
When not set, uses agent's preferred model or opencode/big-pickle as fallback.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="typography-ui-label text-muted-foreground">Default model</label>
|
||||
<ModelSelector
|
||||
providerId={parsedModel.providerId}
|
||||
modelId={parsedModel.modelId}
|
||||
onChange={handleModelChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="typography-ui-label text-muted-foreground">Default agent</label>
|
||||
<AgentSelector
|
||||
agentName={defaultAgent || ''}
|
||||
onChange={handleAgentChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(defaultModel || defaultAgent) && (
|
||||
<div className="typography-meta text-muted-foreground">
|
||||
New sessions will start with:{' '}
|
||||
{defaultModel && <span className="text-foreground">{defaultModel}</span>}
|
||||
{defaultModel && defaultAgent && ' / '}
|
||||
{defaultAgent && <span className="text-foreground">{defaultAgent}</span>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { AppearanceSettings } from './AppearanceSettings';
|
||||
import { AboutSettings } from './AboutSettings';
|
||||
import { SessionRetentionSettings } from './SessionRetentionSettings';
|
||||
import { DefaultsSettings } from './DefaultsSettings';
|
||||
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
||||
import { useDeviceInfo } from '@/lib/device';
|
||||
import { isWebRuntime } from '@/lib/desktop';
|
||||
@@ -16,6 +17,9 @@ export const SettingsPage: React.FC = () => {
|
||||
className="settings-page-body mx-auto max-w-3xl space-y-3 p-3 sm:space-y-6 sm:p-6"
|
||||
>
|
||||
<AppearanceSettings />
|
||||
<div className="border-t border-border/40 pt-6">
|
||||
<DefaultsSettings />
|
||||
</div>
|
||||
<div className="border-t border-border/40 pt-6">
|
||||
<SessionRetentionSettings />
|
||||
</div>
|
||||
|
||||
@@ -41,6 +41,8 @@ export type DesktopSettings = {
|
||||
showReasoningTraces?: boolean;
|
||||
autoDeleteEnabled?: boolean;
|
||||
autoDeleteAfterDays?: number;
|
||||
defaultModel?: string; // format: "provider/model"
|
||||
defaultAgent?: string;
|
||||
};
|
||||
|
||||
export type DesktopSettingsApi = {
|
||||
|
||||
@@ -125,6 +125,12 @@ const sanitizeWebSettings = (payload: unknown): DesktopSettings | null => {
|
||||
if (typeof candidate.autoDeleteAfterDays === 'number' && Number.isFinite(candidate.autoDeleteAfterDays)) {
|
||||
result.autoDeleteAfterDays = candidate.autoDeleteAfterDays;
|
||||
}
|
||||
if (typeof candidate.defaultModel === 'string' && candidate.defaultModel.length > 0) {
|
||||
result.defaultModel = candidate.defaultModel;
|
||||
}
|
||||
if (typeof candidate.defaultAgent === 'string' && candidate.defaultAgent.length > 0) {
|
||||
result.defaultAgent = candidate.defaultAgent;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -31,15 +31,22 @@ export interface AgentConfig {
|
||||
}
|
||||
|
||||
// Extended Agent type for API properties not in SDK types
|
||||
export type AgentWithExtras = Agent & { native?: boolean; hidden?: boolean };
|
||||
export type AgentWithExtras = Agent & {
|
||||
native?: boolean;
|
||||
hidden?: boolean;
|
||||
options?: { hidden?: boolean };
|
||||
};
|
||||
|
||||
// Helper to check if agent is built-in (handles both SDK 'builtIn' and API 'native')
|
||||
export const isAgentBuiltIn = (agent: Agent): boolean =>
|
||||
agent.builtIn || (agent as AgentWithExtras).native === true;
|
||||
|
||||
// Helper to check if agent is hidden (internal agents like title, compaction, summary)
|
||||
export const isAgentHidden = (agent: Agent): boolean =>
|
||||
(agent as AgentWithExtras).hidden === true;
|
||||
// Checks both top-level hidden and options.hidden (OpenCode API inconsistency workaround)
|
||||
export const isAgentHidden = (agent: Agent): boolean => {
|
||||
const extended = agent as AgentWithExtras;
|
||||
return extended.hidden === true || extended.options?.hidden === true;
|
||||
};
|
||||
|
||||
// Helper to filter only visible (non-hidden) agents
|
||||
export const filterVisibleAgents = (agents: Agent[]): Agent[] =>
|
||||
|
||||
@@ -8,10 +8,78 @@ 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";
|
||||
|
||||
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;
|
||||
defaultAgent?: string;
|
||||
}
|
||||
|
||||
const fetchOpenChamberDefaults = async (): Promise<OpenChamberDefaults> => {
|
||||
try {
|
||||
// 1. Desktop runtime (Tauri)
|
||||
if (isDesktopRuntime()) {
|
||||
const settings = await getDesktopSettings();
|
||||
return {
|
||||
defaultModel: settings?.defaultModel,
|
||||
defaultAgent: settings?.defaultAgent,
|
||||
};
|
||||
}
|
||||
|
||||
// 2. Runtime settings API (VSCode)
|
||||
const runtimeSettings = getRegisteredRuntimeAPIs()?.settings;
|
||||
if (runtimeSettings) {
|
||||
try {
|
||||
const result = await runtimeSettings.load();
|
||||
const data = result?.settings;
|
||||
if (data) {
|
||||
return {
|
||||
defaultModel: typeof data?.defaultModel === 'string' ? data.defaultModel : undefined,
|
||||
defaultAgent: typeof data?.defaultAgent === 'string' ? data.defaultAgent : undefined,
|
||||
};
|
||||
}
|
||||
} 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();
|
||||
return {
|
||||
defaultModel: typeof data?.defaultModel === 'string' ? data.defaultModel : undefined,
|
||||
defaultAgent: typeof data?.defaultAgent === 'string' ? data.defaultAgent : undefined,
|
||||
};
|
||||
} 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;
|
||||
@@ -217,12 +285,17 @@ interface ConfigStore {
|
||||
isConnected: boolean;
|
||||
isInitialized: boolean;
|
||||
modelsMetadata: Map<string, ModelMetadata>;
|
||||
// OpenChamber settings-based defaults (take precedence over agent preferences)
|
||||
settingsDefaultModel: string | undefined; // format: "provider/model"
|
||||
settingsDefaultAgent: string | undefined;
|
||||
|
||||
loadProviders: () => Promise<void>;
|
||||
loadAgents: () => Promise<boolean>;
|
||||
setProvider: (providerId: string) => void;
|
||||
setModel: (modelId: string) => void;
|
||||
setAgent: (agentName: string | undefined) => void;
|
||||
setSettingsDefaultModel: (model: string | undefined) => void;
|
||||
setSettingsDefaultAgent: (agent: string | undefined) => void;
|
||||
saveAgentModelSelection: (agentName: string, providerId: string, modelId: string) => void;
|
||||
getAgentModelSelection: (agentName: string) => { providerId: string; modelId: string } | null;
|
||||
checkConnection: () => Promise<boolean>;
|
||||
@@ -257,12 +330,12 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
isConnected: false,
|
||||
isInitialized: false,
|
||||
modelsMetadata: new Map<string, ModelMetadata>(),
|
||||
settingsDefaultModel: undefined,
|
||||
settingsDefaultAgent: undefined,
|
||||
|
||||
loadProviders: async () => {
|
||||
const previousProviders = get().providers;
|
||||
const previousDefaults = get().defaultProviders;
|
||||
const previousProviderId = get().currentProviderId;
|
||||
const previousModelId = get().currentModelId;
|
||||
let lastError: unknown = null;
|
||||
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
@@ -281,16 +354,10 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
};
|
||||
});
|
||||
|
||||
const defaultProviderId = defaults.provider || processedProviders[0]?.id || "";
|
||||
const provider = processedProviders.find((p) => p.id === defaultProviderId);
|
||||
const defaultModelId = defaults.model || provider?.models?.[0]?.id || "";
|
||||
|
||||
// Only store providers and defaults - model/agent selection handled in loadAgents
|
||||
set({
|
||||
providers: processedProviders,
|
||||
defaultProviders: defaults,
|
||||
|
||||
currentProviderId: defaultProviderId,
|
||||
currentModelId: defaultModelId,
|
||||
});
|
||||
|
||||
const metadata = await metadataPromise;
|
||||
@@ -310,8 +377,6 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
set({
|
||||
providers: previousProviders,
|
||||
defaultProviders: previousDefaults,
|
||||
currentProviderId: previousProviderId,
|
||||
currentModelId: previousModelId,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -355,9 +420,18 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
try {
|
||||
const agents = await opencodeClient.listAgents();
|
||||
// Fetch agents and OpenChamber settings in parallel
|
||||
const [agents, openChamberDefaults] = await Promise.all([
|
||||
opencodeClient.listAgents(),
|
||||
fetchOpenChamberDefaults(),
|
||||
]);
|
||||
const safeAgents = Array.isArray(agents) ? agents : [];
|
||||
set({ agents: safeAgents });
|
||||
set({
|
||||
agents: safeAgents,
|
||||
// Store settings defaults so setAgent can respect them
|
||||
settingsDefaultModel: openChamberDefaults.defaultModel,
|
||||
settingsDefaultAgent: openChamberDefaults.defaultAgent,
|
||||
});
|
||||
|
||||
const { providers } = get();
|
||||
|
||||
@@ -366,33 +440,101 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- 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 defaultAgent = buildAgent || primaryAgents[0] || safeAgents[0];
|
||||
const fallbackAgent = buildAgent || primaryAgents[0] || safeAgents[0];
|
||||
|
||||
const existingAgentName = get().currentAgentName;
|
||||
const existingAgent = existingAgentName ? safeAgents.find((agent) => agent.name === existingAgentName) : undefined;
|
||||
const resolvedAgentName = existingAgent ? existingAgentName : defaultAgent.name;
|
||||
let resolvedAgent: Agent | undefined;
|
||||
|
||||
if (resolvedAgentName !== existingAgentName) {
|
||||
set({ currentAgentName: resolvedAgentName });
|
||||
}
|
||||
// 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);
|
||||
};
|
||||
|
||||
const agentForDefaults = existingAgent || defaultAgent;
|
||||
if (agentForDefaults?.model?.providerID && agentForDefaults?.model?.modelID) {
|
||||
const agentProvider = providers.find((p) => p.id === agentForDefaults.model!.providerID);
|
||||
if (agentProvider) {
|
||||
const agentModel = agentProvider.models.find((model) => model.id === agentForDefaults.model!.modelID);
|
||||
// Track invalid settings to clear
|
||||
const invalidSettings: { defaultModel?: string; defaultAgent?: string } = {};
|
||||
|
||||
if (agentModel) {
|
||||
set({
|
||||
currentProviderId: agentForDefaults.model!.providerID,
|
||||
currentModelId: agentForDefaults.model!.modelID,
|
||||
});
|
||||
// 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 = '';
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Fall back to default logic
|
||||
if (!resolvedAgent) {
|
||||
resolvedAgent = fallbackAgent;
|
||||
}
|
||||
|
||||
set({ currentAgentName: resolvedAgent.name });
|
||||
|
||||
// --- Model Selection ---
|
||||
// Priority: settings.defaultModel → agent's preferred model → opencode/big-pickle
|
||||
let resolvedProviderId: string | undefined;
|
||||
let resolvedModelId: 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 = '';
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Fall back to agent's preferred model
|
||||
if (!resolvedProviderId && resolvedAgent?.model?.providerID && resolvedAgent?.model?.modelID) {
|
||||
if (validateModel(resolvedAgent.model.providerID, resolvedAgent.model.modelID)) {
|
||||
resolvedProviderId = resolvedAgent.model.providerID;
|
||||
resolvedModelId = resolvedAgent.model.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];
|
||||
if (firstProvider && firstProvider.models[0]) {
|
||||
resolvedProviderId = firstProvider.id;
|
||||
resolvedModelId = firstProvider.models[0].id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (resolvedProviderId && resolvedModelId) {
|
||||
set({
|
||||
currentProviderId: resolvedProviderId,
|
||||
currentModelId: resolvedModelId,
|
||||
});
|
||||
}
|
||||
|
||||
// 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,
|
||||
});
|
||||
updateDesktopSettings(invalidSettings).catch(() => {
|
||||
// Ignore errors - best effort cleanup
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
@@ -407,7 +549,7 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
},
|
||||
|
||||
setAgent: (agentName: string | undefined) => {
|
||||
const { agents, providers } = get();
|
||||
const { agents, providers, settingsDefaultModel } = get();
|
||||
|
||||
set({ currentAgentName: agentName });
|
||||
|
||||
@@ -452,6 +594,22 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
}
|
||||
}
|
||||
|
||||
// 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({
|
||||
currentProviderId: parsed.providerId,
|
||||
currentModelId: parsed.modelId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to agent's preferred model
|
||||
const agent = agents.find((candidate) => candidate.name === agentName);
|
||||
if (agent?.model?.providerID && agent?.model?.modelID) {
|
||||
const agentProvider = providers.find((provider) => provider.id === agent.model!.providerID);
|
||||
@@ -469,6 +627,14 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
}
|
||||
},
|
||||
|
||||
setSettingsDefaultModel: (model: string | undefined) => {
|
||||
set({ settingsDefaultModel: model });
|
||||
},
|
||||
|
||||
setSettingsDefaultAgent: (agent: string | undefined) => {
|
||||
set({ settingsDefaultAgent: agent });
|
||||
},
|
||||
|
||||
checkConnection: async () => {
|
||||
const maxAttempts = 5;
|
||||
let attempt = 0;
|
||||
|
||||
@@ -126,10 +126,20 @@ export const useSessionStore = create<SessionStore>()(
|
||||
try {
|
||||
const configState = useConfigStore.getState();
|
||||
const visibleAgents = configState.getVisibleAgents();
|
||||
const agentName =
|
||||
configState.currentAgentName ||
|
||||
visibleAgents.find((agent) => agent.name === 'build')?.name ||
|
||||
visibleAgents[0]?.name;
|
||||
|
||||
// Priority: settingsDefaultAgent → build → first visible
|
||||
let agentName: string | undefined;
|
||||
if (configState.settingsDefaultAgent) {
|
||||
const settingsAgent = visibleAgents.find((a) => a.name === configState.settingsDefaultAgent);
|
||||
if (settingsAgent) {
|
||||
agentName = settingsAgent.name;
|
||||
}
|
||||
}
|
||||
if (!agentName) {
|
||||
agentName =
|
||||
visibleAgents.find((agent) => agent.name === 'build')?.name ||
|
||||
visibleAgents[0]?.name;
|
||||
}
|
||||
|
||||
if (agentName) {
|
||||
configState.setAgent(agentName);
|
||||
|
||||
@@ -415,6 +415,13 @@ const sanitizeSettingsUpdate = (payload) => {
|
||||
result.typographySizes = typography;
|
||||
}
|
||||
|
||||
if (typeof candidate.defaultModel === 'string' && candidate.defaultModel.length > 0) {
|
||||
result.defaultModel = candidate.defaultModel;
|
||||
}
|
||||
if (typeof candidate.defaultAgent === 'string' && candidate.defaultAgent.length > 0) {
|
||||
result.defaultAgent = candidate.defaultAgent;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user