diff --git a/internal/config/config_load.go b/internal/config/config_load.go index ff81b4bb..49dc93f1 100644 --- a/internal/config/config_load.go +++ b/internal/config/config_load.go @@ -156,9 +156,18 @@ func (c *Config) applyEnvOverrides() { envStr("GOCLAW_CLAUDE_CLI_MODEL", &c.Providers.ClaudeCLI.Model) envStr("GOCLAW_CLAUDE_CLI_WORK_DIR", &c.Providers.ClaudeCLI.BaseWorkDir) - // Default provider/model: env overrides config. - envStr("GOCLAW_PROVIDER", &c.Agents.Defaults.Provider) - envStr("GOCLAW_MODEL", &c.Agents.Defaults.Model) + // Default provider/model: env is fallback only (applied when config has no value). + // The onboard wizard sets these in .env for initial bootstrap; once the user + // saves a provider/model via the Dashboard, the config-file value wins. + envFallback := func(key string, dst *string) { + if *dst == "" { + if v := os.Getenv(key); v != "" { + *dst = v + } + } + } + envFallback("GOCLAW_PROVIDER", &c.Agents.Defaults.Provider) + envFallback("GOCLAW_MODEL", &c.Agents.Defaults.Model) // Workspace & sessions envStr("GOCLAW_WORKSPACE", &c.Agents.Defaults.Workspace) diff --git a/ui/web/src/components/shared/provider-model-select.tsx b/ui/web/src/components/shared/provider-model-select.tsx index bc7551df..736218be 100644 --- a/ui/web/src/components/shared/provider-model-select.tsx +++ b/ui/web/src/components/shared/provider-model-select.tsx @@ -1,4 +1,4 @@ -import { useMemo, useEffect } from "react"; +import { useMemo, useEffect, useRef } from "react"; import { useTranslation } from "react-i18next"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -57,14 +57,23 @@ export function ProviderModelSelect({ }: ProviderModelSelectProps) { const { t } = useTranslation("common"); const { providers } = useProviders(); - const enabledProviders = providers.filter((p) => p.enabled); + const enabledProviders = useMemo( + () => providers.filter((p) => p.enabled), + [providers], + ); + + // Stable ref for callback — prevents the auto-select effect from re-running + // on every parent render (inline onProviderChange creates a new ref each time). + const onProviderChangeRef = useRef(onProviderChange); + onProviderChangeRef.current = onProviderChange; - // Auto-select first enabled provider when none is set (unless allowEmpty) + // Auto-select first enabled provider when none is set (unless allowEmpty). + // Uses ref for callback so this only re-runs when provider or providers actually change. useEffect(() => { if (!allowEmpty && !provider && enabledProviders.length > 0) { - onProviderChange(enabledProviders[0]!.name); + onProviderChangeRef.current(enabledProviders[0]!.name); } - }, [allowEmpty, provider, enabledProviders, onProviderChange]); + }, [allowEmpty, provider, enabledProviders]); const selectedProvider = useMemo( () => enabledProviders.find((p) => p.name === provider), diff --git a/ui/web/src/pages/config/hooks/use-config.ts b/ui/web/src/pages/config/hooks/use-config.ts index 2ff35d75..effb114d 100644 --- a/ui/web/src/pages/config/hooks/use-config.ts +++ b/ui/web/src/pages/config/hooks/use-config.ts @@ -4,6 +4,7 @@ import { useWs } from "@/hooks/use-ws"; import { useAuthStore } from "@/stores/use-auth-store"; import { Methods } from "@/api/protocol"; import { queryKeys } from "@/lib/query-keys"; +import { toast } from "@/stores/use-toast-store"; interface ConfigData { config: Record; @@ -50,8 +51,11 @@ export function useConfig() { }); hashRef.current = res.hash; await invalidate(); + toast.success("Config saved"); } catch (err) { - setError(err instanceof Error ? err.message : "Failed to apply config"); + const msg = err instanceof Error ? err.message : "Failed to apply config"; + setError(msg); + toast.error("Failed to save config", msg); throw err; } finally { setSaving(false); @@ -71,8 +75,11 @@ export function useConfig() { }); hashRef.current = res.hash; await invalidate(); + toast.success("Config saved"); } catch (err) { - setError(err instanceof Error ? err.message : "Failed to patch config"); + const msg = err instanceof Error ? err.message : "Failed to patch config"; + setError(msg); + toast.error("Failed to save config", msg); throw err; } finally { setSaving(false);