diff --git a/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx b/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx index 34272d77..42ea45bb 100644 --- a/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx +++ b/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx @@ -175,10 +175,21 @@ function ChatInputInner({ loadModelConfig() }, [isActive, loadModelConfig]) + // Reload when model config changes (e.g. from settings dialog) + useEffect(() => { + const handler = () => { loadModelConfig() } + window.addEventListener('models-config-changed', handler) + return () => window.removeEventListener('models-config-changed', handler) + }, [loadModelConfig]) + const handleModelChange = useCallback(async (key: string) => { const entry = configuredModels.find((m) => `${m.flavor}/${m.model}` === key) if (!entry) return setActiveModelKey(key) + // Collect all models for this provider so the full list is preserved + const providerModels = configuredModels + .filter((m) => m.flavor === entry.flavor) + .map((m) => m.model) try { await window.ipc.invoke('models:saveConfig', { provider: { @@ -188,6 +199,7 @@ function ChatInputInner({ headers: entry.headers, }, model: entry.model, + models: providerModels, knowledgeGraphModel: entry.knowledgeGraphModel, }) } catch { diff --git a/apps/x/apps/renderer/src/components/settings-dialog.tsx b/apps/x/apps/renderer/src/components/settings-dialog.tsx index 3c129e80..7cb0a011 100644 --- a/apps/x/apps/renderer/src/components/settings-dialog.tsx +++ b/apps/x/apps/renderer/src/components/settings-dialog.tsx @@ -278,17 +278,20 @@ function ModelSettings({ dialogOpen }: { dialogOpen: boolean }) { } } } - // Active provider takes precedence from top-level config - const existingModels = next[flavor].models; - const activeModels = existingModels[0] === parsed.model - ? existingModels - : [parsed.model, ...existingModels.filter((m: string) => m && m !== parsed.model)]; - next[flavor] = { - apiKey: parsed.provider.apiKey || "", - baseURL: parsed.provider.baseURL || (defaultBaseURLs[flavor] || ""), - models: activeModels.length > 0 ? activeModels : [""], - knowledgeGraphModel: parsed.knowledgeGraphModel || "", - }; + // Active provider takes precedence from top-level config, + // but only if it exists in the providers map (wasn't deleted) + if (parsed.providers?.[flavor]) { + const existingModels = next[flavor].models; + const activeModels = existingModels[0] === parsed.model + ? existingModels + : [parsed.model, ...existingModels.filter((m: string) => m && m !== parsed.model)]; + next[flavor] = { + apiKey: parsed.provider.apiKey || "", + baseURL: parsed.provider.baseURL || (defaultBaseURLs[flavor] || ""), + models: activeModels.length > 0 ? activeModels : [""], + knowledgeGraphModel: parsed.knowledgeGraphModel || "", + }; + } return next; }) } @@ -366,6 +369,7 @@ function ModelSettings({ dialogOpen }: { dialogOpen: boolean }) { await window.ipc.invoke("models:saveConfig", providerConfig) setDefaultProvider(provider) setTestState({ status: "success" }) + window.dispatchEvent(new Event('models-config-changed')) toast.success("Model configuration saved") } else { setTestState({ status: "error", error: result.error }) @@ -393,12 +397,50 @@ function ModelSettings({ dialogOpen }: { dialogOpen: boolean }) { knowledgeGraphModel: config.knowledgeGraphModel.trim() || undefined, }) setDefaultProvider(prov) + window.dispatchEvent(new Event('models-config-changed')) toast.success("Default provider updated") } catch { toast.error("Failed to set default provider") } }, [providerConfigs]) + const handleDeleteProvider = useCallback(async (prov: LlmProviderFlavor) => { + try { + const result = await window.ipc.invoke("workspace:readFile", { path: "config/models.json" }) + const parsed = JSON.parse(result.data) + if (parsed?.providers?.[prov]) { + delete parsed.providers[prov] + } + // If the deleted provider is the current top-level active one, + // switch top-level config to the current default provider + if (parsed?.provider?.flavor === prov && defaultProvider && defaultProvider !== prov) { + const defConfig = providerConfigs[defaultProvider] + const defModels = defConfig.models.map(m => m.trim()).filter(Boolean) + parsed.provider = { + flavor: defaultProvider, + apiKey: defConfig.apiKey.trim() || undefined, + baseURL: defConfig.baseURL.trim() || undefined, + } + parsed.model = defModels[0] || "" + parsed.models = defModels + parsed.knowledgeGraphModel = defConfig.knowledgeGraphModel.trim() || undefined + } + await window.ipc.invoke("workspace:writeFile", { + path: "config/models.json", + data: JSON.stringify(parsed, null, 2), + }) + setProviderConfigs(prev => ({ + ...prev, + [prov]: { apiKey: "", baseURL: defaultBaseURLs[prov] || "", models: [""], knowledgeGraphModel: "" }, + })) + setTestState({ status: "idle" }) + window.dispatchEvent(new Event('models-config-changed')) + toast.success("Provider configuration removed") + } catch { + toast.error("Failed to remove provider") + } + }, [defaultProvider, providerConfigs]) + const renderProviderCard = (p: { id: LlmProviderFlavor; name: string; description: string }) => { const isDefault = defaultProvider === p.id const isSelected = provider === p.id @@ -427,16 +469,28 @@ function ModelSettings({ dialogOpen }: { dialogOpen: boolean }) {