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));
|
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
|
// Boolean fields
|
||||||
if let Some(Value::Bool(b)) = obj.get("useSystemTheme") {
|
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 { AppearanceSettings } from './AppearanceSettings';
|
||||||
import { AboutSettings } from './AboutSettings';
|
import { AboutSettings } from './AboutSettings';
|
||||||
import { SessionRetentionSettings } from './SessionRetentionSettings';
|
import { SessionRetentionSettings } from './SessionRetentionSettings';
|
||||||
|
import { DefaultsSettings } from './DefaultsSettings';
|
||||||
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
||||||
import { useDeviceInfo } from '@/lib/device';
|
import { useDeviceInfo } from '@/lib/device';
|
||||||
import { isWebRuntime } from '@/lib/desktop';
|
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"
|
className="settings-page-body mx-auto max-w-3xl space-y-3 p-3 sm:space-y-6 sm:p-6"
|
||||||
>
|
>
|
||||||
<AppearanceSettings />
|
<AppearanceSettings />
|
||||||
|
<div className="border-t border-border/40 pt-6">
|
||||||
|
<DefaultsSettings />
|
||||||
|
</div>
|
||||||
<div className="border-t border-border/40 pt-6">
|
<div className="border-t border-border/40 pt-6">
|
||||||
<SessionRetentionSettings />
|
<SessionRetentionSettings />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ export type DesktopSettings = {
|
|||||||
showReasoningTraces?: boolean;
|
showReasoningTraces?: boolean;
|
||||||
autoDeleteEnabled?: boolean;
|
autoDeleteEnabled?: boolean;
|
||||||
autoDeleteAfterDays?: number;
|
autoDeleteAfterDays?: number;
|
||||||
|
defaultModel?: string; // format: "provider/model"
|
||||||
|
defaultAgent?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DesktopSettingsApi = {
|
export type DesktopSettingsApi = {
|
||||||
|
|||||||
@@ -125,6 +125,12 @@ const sanitizeWebSettings = (payload: unknown): DesktopSettings | null => {
|
|||||||
if (typeof candidate.autoDeleteAfterDays === 'number' && Number.isFinite(candidate.autoDeleteAfterDays)) {
|
if (typeof candidate.autoDeleteAfterDays === 'number' && Number.isFinite(candidate.autoDeleteAfterDays)) {
|
||||||
result.autoDeleteAfterDays = 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;
|
return result;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -31,15 +31,22 @@ export interface AgentConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Extended Agent type for API properties not in SDK types
|
// 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')
|
// Helper to check if agent is built-in (handles both SDK 'builtIn' and API 'native')
|
||||||
export const isAgentBuiltIn = (agent: Agent): boolean =>
|
export const isAgentBuiltIn = (agent: Agent): boolean =>
|
||||||
agent.builtIn || (agent as AgentWithExtras).native === true;
|
agent.builtIn || (agent as AgentWithExtras).native === true;
|
||||||
|
|
||||||
// Helper to check if agent is hidden (internal agents like title, compaction, summary)
|
// Helper to check if agent is hidden (internal agents like title, compaction, summary)
|
||||||
export const isAgentHidden = (agent: Agent): boolean =>
|
// Checks both top-level hidden and options.hidden (OpenCode API inconsistency workaround)
|
||||||
(agent as AgentWithExtras).hidden === true;
|
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
|
// Helper to filter only visible (non-hidden) agents
|
||||||
export const filterVisibleAgents = (agents: Agent[]): Agent[] =>
|
export const filterVisibleAgents = (agents: Agent[]): Agent[] =>
|
||||||
|
|||||||
@@ -8,10 +8,78 @@ import type { ModelMetadata } from "@/types";
|
|||||||
import { getSafeStorage } from "./utils/safeStorage";
|
import { getSafeStorage } from "./utils/safeStorage";
|
||||||
import type { SessionStore } from "./types/sessionTypes";
|
import type { SessionStore } from "./types/sessionTypes";
|
||||||
import { filterVisibleAgents } from "./useAgentsStore";
|
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_API_URL = "https://models.dev/api.json";
|
||||||
const MODELS_DEV_PROXY_URL = "/api/openchamber/models-metadata";
|
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 normalizeProviderId = (value: string) => value?.toLowerCase?.() ?? '';
|
||||||
|
|
||||||
const isPrimaryMode = (mode?: string) => mode === "primary" || mode === "all" || mode === undefined || mode === null;
|
const isPrimaryMode = (mode?: string) => mode === "primary" || mode === "all" || mode === undefined || mode === null;
|
||||||
@@ -217,12 +285,17 @@ interface ConfigStore {
|
|||||||
isConnected: boolean;
|
isConnected: boolean;
|
||||||
isInitialized: boolean;
|
isInitialized: boolean;
|
||||||
modelsMetadata: Map<string, ModelMetadata>;
|
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>;
|
loadProviders: () => Promise<void>;
|
||||||
loadAgents: () => Promise<boolean>;
|
loadAgents: () => Promise<boolean>;
|
||||||
setProvider: (providerId: string) => void;
|
setProvider: (providerId: string) => void;
|
||||||
setModel: (modelId: string) => void;
|
setModel: (modelId: string) => void;
|
||||||
setAgent: (agentName: string | undefined) => void;
|
setAgent: (agentName: string | undefined) => void;
|
||||||
|
setSettingsDefaultModel: (model: string | undefined) => void;
|
||||||
|
setSettingsDefaultAgent: (agent: string | undefined) => void;
|
||||||
saveAgentModelSelection: (agentName: string, providerId: string, modelId: string) => void;
|
saveAgentModelSelection: (agentName: string, providerId: string, modelId: string) => void;
|
||||||
getAgentModelSelection: (agentName: string) => { providerId: string; modelId: string } | null;
|
getAgentModelSelection: (agentName: string) => { providerId: string; modelId: string } | null;
|
||||||
checkConnection: () => Promise<boolean>;
|
checkConnection: () => Promise<boolean>;
|
||||||
@@ -257,12 +330,12 @@ export const useConfigStore = create<ConfigStore>()(
|
|||||||
isConnected: false,
|
isConnected: false,
|
||||||
isInitialized: false,
|
isInitialized: false,
|
||||||
modelsMetadata: new Map<string, ModelMetadata>(),
|
modelsMetadata: new Map<string, ModelMetadata>(),
|
||||||
|
settingsDefaultModel: undefined,
|
||||||
|
settingsDefaultAgent: undefined,
|
||||||
|
|
||||||
loadProviders: async () => {
|
loadProviders: async () => {
|
||||||
const previousProviders = get().providers;
|
const previousProviders = get().providers;
|
||||||
const previousDefaults = get().defaultProviders;
|
const previousDefaults = get().defaultProviders;
|
||||||
const previousProviderId = get().currentProviderId;
|
|
||||||
const previousModelId = get().currentModelId;
|
|
||||||
let lastError: unknown = null;
|
let lastError: unknown = null;
|
||||||
|
|
||||||
for (let attempt = 0; attempt < 3; attempt++) {
|
for (let attempt = 0; attempt < 3; attempt++) {
|
||||||
@@ -281,16 +354,10 @@ export const useConfigStore = create<ConfigStore>()(
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultProviderId = defaults.provider || processedProviders[0]?.id || "";
|
// Only store providers and defaults - model/agent selection handled in loadAgents
|
||||||
const provider = processedProviders.find((p) => p.id === defaultProviderId);
|
|
||||||
const defaultModelId = defaults.model || provider?.models?.[0]?.id || "";
|
|
||||||
|
|
||||||
set({
|
set({
|
||||||
providers: processedProviders,
|
providers: processedProviders,
|
||||||
defaultProviders: defaults,
|
defaultProviders: defaults,
|
||||||
|
|
||||||
currentProviderId: defaultProviderId,
|
|
||||||
currentModelId: defaultModelId,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const metadata = await metadataPromise;
|
const metadata = await metadataPromise;
|
||||||
@@ -310,8 +377,6 @@ export const useConfigStore = create<ConfigStore>()(
|
|||||||
set({
|
set({
|
||||||
providers: previousProviders,
|
providers: previousProviders,
|
||||||
defaultProviders: previousDefaults,
|
defaultProviders: previousDefaults,
|
||||||
currentProviderId: previousProviderId,
|
|
||||||
currentModelId: previousModelId,
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -355,9 +420,18 @@ export const useConfigStore = create<ConfigStore>()(
|
|||||||
|
|
||||||
for (let attempt = 0; attempt < 3; attempt++) {
|
for (let attempt = 0; attempt < 3; attempt++) {
|
||||||
try {
|
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 : [];
|
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();
|
const { providers } = get();
|
||||||
|
|
||||||
@@ -366,33 +440,101 @@ export const useConfigStore = create<ConfigStore>()(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Agent Selection ---
|
||||||
|
// Priority: settings.defaultAgent → build → first primary → first agent
|
||||||
const primaryAgents = safeAgents.filter((agent) => isPrimaryMode(agent.mode));
|
const primaryAgents = safeAgents.filter((agent) => isPrimaryMode(agent.mode));
|
||||||
const buildAgent = primaryAgents.find((agent) => agent.name === "build");
|
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;
|
let resolvedAgent: Agent | undefined;
|
||||||
const existingAgent = existingAgentName ? safeAgents.find((agent) => agent.name === existingAgentName) : undefined;
|
|
||||||
const resolvedAgentName = existingAgent ? existingAgentName : defaultAgent.name;
|
|
||||||
|
|
||||||
if (resolvedAgentName !== existingAgentName) {
|
// Helper to validate model exists in providers
|
||||||
set({ currentAgentName: resolvedAgentName });
|
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;
|
// Track invalid settings to clear
|
||||||
if (agentForDefaults?.model?.providerID && agentForDefaults?.model?.modelID) {
|
const invalidSettings: { defaultModel?: string; defaultAgent?: string } = {};
|
||||||
const agentProvider = providers.find((p) => p.id === agentForDefaults.model!.providerID);
|
|
||||||
if (agentProvider) {
|
|
||||||
const agentModel = agentProvider.models.find((model) => model.id === agentForDefaults.model!.modelID);
|
|
||||||
|
|
||||||
if (agentModel) {
|
// 1. Check OpenChamber settings for default agent
|
||||||
set({
|
if (openChamberDefaults.defaultAgent) {
|
||||||
currentProviderId: agentForDefaults.model!.providerID,
|
const settingsAgent = safeAgents.find((agent) => agent.name === openChamberDefaults.defaultAgent);
|
||||||
currentModelId: agentForDefaults.model!.modelID,
|
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;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
lastError = error;
|
lastError = error;
|
||||||
@@ -407,7 +549,7 @@ export const useConfigStore = create<ConfigStore>()(
|
|||||||
},
|
},
|
||||||
|
|
||||||
setAgent: (agentName: string | undefined) => {
|
setAgent: (agentName: string | undefined) => {
|
||||||
const { agents, providers } = get();
|
const { agents, providers, settingsDefaultModel } = get();
|
||||||
|
|
||||||
set({ currentAgentName: agentName });
|
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);
|
const agent = agents.find((candidate) => candidate.name === agentName);
|
||||||
if (agent?.model?.providerID && agent?.model?.modelID) {
|
if (agent?.model?.providerID && agent?.model?.modelID) {
|
||||||
const agentProvider = providers.find((provider) => provider.id === agent.model!.providerID);
|
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 () => {
|
checkConnection: async () => {
|
||||||
const maxAttempts = 5;
|
const maxAttempts = 5;
|
||||||
let attempt = 0;
|
let attempt = 0;
|
||||||
|
|||||||
@@ -126,10 +126,20 @@ export const useSessionStore = create<SessionStore>()(
|
|||||||
try {
|
try {
|
||||||
const configState = useConfigStore.getState();
|
const configState = useConfigStore.getState();
|
||||||
const visibleAgents = configState.getVisibleAgents();
|
const visibleAgents = configState.getVisibleAgents();
|
||||||
const agentName =
|
|
||||||
configState.currentAgentName ||
|
// Priority: settingsDefaultAgent → build → first visible
|
||||||
visibleAgents.find((agent) => agent.name === 'build')?.name ||
|
let agentName: string | undefined;
|
||||||
visibleAgents[0]?.name;
|
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) {
|
if (agentName) {
|
||||||
configState.setAgent(agentName);
|
configState.setAgent(agentName);
|
||||||
|
|||||||
@@ -415,6 +415,13 @@ const sanitizeSettingsUpdate = (payload) => {
|
|||||||
result.typographySizes = typography;
|
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;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user