diff --git a/src/browser/components/ChatInput/index.tsx b/src/browser/components/ChatInput/index.tsx index 2f90052364..06cb87083a 100644 --- a/src/browser/components/ChatInput/index.tsx +++ b/src/browser/components/ChatInput/index.tsx @@ -413,25 +413,40 @@ export const ChatInput: React.FC = (props) => { }, [api]); // Check if OpenAI API key is configured (for voice input) + // Subscribe to config changes so key status updates immediately when set in Settings useEffect(() => { - let isMounted = true; + if (!api) return; + const abortController = new AbortController(); + const signal = abortController.signal; const checkOpenAIKey = async () => { try { - const config = await api?.providers.getConfig(); - if (isMounted) { + const config = await api.providers.getConfig(); + if (!signal.aborted) { setOpenAIKeySet(config?.openai?.apiKeySet ?? false); } - } catch (error) { - console.error("Failed to check OpenAI API key:", error); + } catch { + // Ignore errors fetching config } }; + // Initial fetch void checkOpenAIKey(); - return () => { - isMounted = false; - }; + // Subscribe to provider config changes via oRPC + (async () => { + try { + const iterator = await api.providers.onConfigChanged(undefined, { signal }); + for await (const _ of iterator) { + if (signal.aborted) break; + void checkOpenAIKey(); + } + } catch { + // Subscription cancelled via abort signal - expected on cleanup + } + })(); + + return () => abortController.abort(); }, [api]); // Allow external components (e.g., CommandPalette, Queued message edits) to insert text diff --git a/src/browser/components/Settings/sections/ModelsSection.tsx b/src/browser/components/Settings/sections/ModelsSection.tsx index 0a203b0d47..705c0359b4 100644 --- a/src/browser/components/Settings/sections/ModelsSection.tsx +++ b/src/browser/components/Settings/sections/ModelsSection.tsx @@ -1,11 +1,11 @@ -import React, { useState, useEffect, useCallback } from "react"; +import React, { useState, useCallback } from "react"; import { Plus, Loader2 } from "lucide-react"; -import type { ProvidersConfigMap } from "../types"; import { SUPPORTED_PROVIDERS, PROVIDER_DISPLAY_NAMES } from "@/common/constants/providers"; import { KNOWN_MODELS } from "@/common/constants/knownModels"; import { useModelLRU } from "@/browser/hooks/useModelLRU"; import { ModelRow } from "./ModelRow"; import { useAPI } from "@/browser/contexts/API"; +import { useProvidersConfig } from "@/browser/hooks/useProvidersConfig"; interface NewModelForm { provider: string; @@ -20,22 +20,12 @@ interface EditingState { export function ModelsSection() { const { api } = useAPI(); - const [config, setConfig] = useState(null); + const { config, loading, updateModelsOptimistically } = useProvidersConfig(); const [newModel, setNewModel] = useState({ provider: "", modelId: "" }); - const [saving, setSaving] = useState(false); const [editing, setEditing] = useState(null); const [error, setError] = useState(null); const { defaultModel, setDefaultModel } = useModelLRU(); - // Load config on mount - useEffect(() => { - if (!api) return; - void (async () => { - const cfg = await api.providers.getConfig(); - setConfig(cfg ?? null); - })(); - }, [api]); - // Check if a model already exists (for duplicate prevention) const modelExists = useCallback( (provider: string, modelId: string, excludeOriginal?: string): boolean => { @@ -46,7 +36,7 @@ export function ModelsSection() { [config] ); - const handleAddModel = useCallback(async () => { + const handleAddModel = useCallback(() => { if (!config || !newModel.provider || !newModel.modelId.trim()) return; const trimmedModelId = newModel.modelId.trim(); @@ -59,40 +49,31 @@ export function ModelsSection() { if (!api) return; setError(null); - setSaving(true); - try { - const currentModels = config[newModel.provider]?.models ?? []; - const updatedModels = [...currentModels, trimmedModelId]; - await api.providers.setModels({ provider: newModel.provider, models: updatedModels }); + // Optimistic update - returns new models array for API call + const updatedModels = updateModelsOptimistically(newModel.provider, (models) => [ + ...models, + trimmedModelId, + ]); + setNewModel({ provider: "", modelId: "" }); - // Refresh config - const cfg = await api.providers.getConfig(); - setConfig(cfg ?? null); - setNewModel({ provider: "", modelId: "" }); - } finally { - setSaving(false); - } - }, [api, newModel, config, modelExists]); + // Save in background + void api.providers.setModels({ provider: newModel.provider, models: updatedModels }); + }, [api, newModel, config, modelExists, updateModelsOptimistically]); const handleRemoveModel = useCallback( - async (provider: string, modelId: string) => { + (provider: string, modelId: string) => { if (!config || !api) return; - setSaving(true); - try { - const currentModels = config[provider]?.models ?? []; - const updatedModels = currentModels.filter((m) => m !== modelId); - await api.providers.setModels({ provider, models: updatedModels }); + // Optimistic update - returns new models array for API call + const updatedModels = updateModelsOptimistically(provider, (models) => + models.filter((m) => m !== modelId) + ); - // Refresh config - const cfg = await api.providers.getConfig(); - setConfig(cfg ?? null); - } finally { - setSaving(false); - } + // Save in background + void api.providers.setModels({ provider, models: updatedModels }); }, - [api, config] + [api, config, updateModelsOptimistically] ); const handleStartEdit = useCallback((provider: string, modelId: string) => { @@ -105,7 +86,7 @@ export function ModelsSection() { setError(null); }, []); - const handleSaveEdit = useCallback(async () => { + const handleSaveEdit = useCallback(() => { if (!config || !editing || !api) return; const trimmedModelId = editing.newModelId.trim(); @@ -123,26 +104,19 @@ export function ModelsSection() { } setError(null); - setSaving(true); - try { - const currentModels = config[editing.provider]?.models ?? []; - const updatedModels = currentModels.map((m) => - m === editing.originalModelId ? trimmedModelId : m - ); - await api.providers.setModels({ provider: editing.provider, models: updatedModels }); + // Optimistic update - returns new models array for API call + const updatedModels = updateModelsOptimistically(editing.provider, (models) => + models.map((m) => (m === editing.originalModelId ? trimmedModelId : m)) + ); + setEditing(null); - // Refresh config - const cfg = await api.providers.getConfig(); - setConfig(cfg ?? null); - setEditing(null); - } finally { - setSaving(false); - } - }, [api, editing, config, modelExists]); + // Save in background + void api.providers.setModels({ provider: editing.provider, models: updatedModels }); + }, [api, editing, config, modelExists, updateModelsOptimistically]); // Show loading state while config is being fetched - if (config === null) { + if (loading || !config) { return (
@@ -211,8 +185,8 @@ export function ModelsSection() { />