From 22b31c90730d689acf479435b947f3942f737047 Mon Sep 17 00:00:00 2001 From: rsh4484 Date: Fri, 24 Apr 2026 17:49:27 -0700 Subject: [PATCH 1/3] Add Thinking default and Dreaming toggles to General menu Persists `agentDefaults.thinking` (off/low/medium/high) and `agentDefaults.dreaming` (boolean) under openclaw.json via a new `/api/agent-defaults` GET/PUT route. Mutations mark the gateway restart-required so OpenClaw picks up new defaults. Adds an "Agent Defaults" card to the General tab with a thinking-level select and a dreaming on/off toggle, plus unit and route tests for the read/write/normalize paths. --- .../js/components/general/agent-defaults.js | 123 +++++++++++++++++ lib/public/js/components/general/index.js | 2 + lib/public/js/lib/api.js | 34 +++++ lib/server/agent-defaults-config.js | 88 ++++++++++++ lib/server/routes/system.js | 60 ++++++++ tests/server/agent-defaults-config.test.js | 108 +++++++++++++++ tests/server/routes-system.test.js | 130 ++++++++++++++++++ 7 files changed, 545 insertions(+) create mode 100644 lib/public/js/components/general/agent-defaults.js create mode 100644 lib/server/agent-defaults-config.js create mode 100644 tests/server/agent-defaults-config.test.js diff --git a/lib/public/js/components/general/agent-defaults.js b/lib/public/js/components/general/agent-defaults.js new file mode 100644 index 00000000..48177821 --- /dev/null +++ b/lib/public/js/components/general/agent-defaults.js @@ -0,0 +1,123 @@ +import { h } from "preact"; +import { useEffect, useState } from "preact/hooks"; +import htm from "htm"; +import { fetchAgentDefaults, updateAgentDefaults } from "../../lib/api.js"; +import { ChevronDownIcon } from "../icons.js"; +import { ToggleSwitch } from "../toggle-switch.js"; +import { showToast } from "../toast.js"; + +const html = htm.bind(h); + +const kThinkingOptions = [ + { value: "off", label: "Off" }, + { value: "low", label: "Low" }, + { value: "medium", label: "Medium" }, + { value: "high", label: "High" }, +]; + +export const AgentDefaults = ({ onRestartRequired = () => {} }) => { + const [loading, setLoading] = useState(true); + const [savingKey, setSavingKey] = useState(""); + const [thinking, setThinking] = useState("medium"); + const [dreaming, setDreaming] = useState(false); + + useEffect(() => { + let cancelled = false; + (async () => { + try { + const data = await fetchAgentDefaults(); + if (cancelled) return; + const next = data.agentDefaults || {}; + if (typeof next.thinking === "string") setThinking(next.thinking); + if (typeof next.dreaming === "boolean") setDreaming(next.dreaming); + } catch (err) { + if (!cancelled) { + showToast(err.message || "Could not load agent defaults", "error"); + } + } finally { + if (!cancelled) setLoading(false); + } + })(); + return () => { + cancelled = true; + }; + }, []); + + const save = async (key, payload, optimistic) => { + const previous = key === "thinking" ? thinking : dreaming; + optimistic(); + setSavingKey(key); + try { + const data = await updateAgentDefaults(payload); + if (!data.ok) { + throw new Error(data.error || "Could not save agent defaults"); + } + const next = data.agentDefaults || {}; + if (typeof next.thinking === "string") setThinking(next.thinking); + if (typeof next.dreaming === "boolean") setDreaming(next.dreaming); + if (data.restartRequired) onRestartRequired(); + showToast("Agent defaults saved", "success"); + } catch (err) { + if (key === "thinking") setThinking(previous); + else setDreaming(previous); + showToast(err.message || "Could not save agent defaults", "error"); + } finally { + setSavingKey(""); + } + }; + + const handleThinkingChange = (event) => { + const nextThinking = String(event.target.value || ""); + save("thinking", { thinking: nextThinking }, () => setThinking(nextThinking)); + }; + + const handleDreamingChange = (nextDreaming) => { + save("dreaming", { dreaming: nextDreaming }, () => setDreaming(nextDreaming)); + }; + + return html` +
+

Agent Defaults

+
+
+
+

Thinking default

+

Default reasoning depth for agent calls.

+
+
+ + <${ChevronDownIcon} + className="pointer-events-none absolute right-2.5 top-1/2 -translate-y-1/2 text-fg-muted" + /> +
+
+
+
+

Dreaming

+

Allow the agent to run background ideation between turns.

+
+ <${ToggleSwitch} + checked=${dreaming} + disabled=${loading || savingKey === "dreaming"} + onChange=${handleDreamingChange} + label=${dreaming ? "On" : "Off"} + /> +
+
+
+ `; +}; diff --git a/lib/public/js/components/general/index.js b/lib/public/js/components/general/index.js index b508c92d..0bd2318a 100644 --- a/lib/public/js/components/general/index.js +++ b/lib/public/js/components/general/index.js @@ -11,6 +11,7 @@ import { Features } from "../features.js"; import { GeneralDoctorWarning } from "../doctor/general-warning.js"; import { ChevronDownIcon } from "../icons.js"; import { UpdateActionButton } from "../update-action-button.js"; +import { AgentDefaults } from "./agent-defaults.js"; import { useGeneralTab } from "./use-general-tab.js"; const html = htm.bind(h); @@ -131,6 +132,7 @@ export const GeneralTab = ({ `} /> <${Features} onSwitchTab=${onSwitchTab} /> + <${AgentDefaults} onRestartRequired=${onRestartRequired} /> <${Google} gatewayStatus=${state.gatewayStatus} onRestartRequired=${onRestartRequired} diff --git a/lib/public/js/lib/api.js b/lib/public/js/lib/api.js index 324851b4..a32c7778 100644 --- a/lib/public/js/lib/api.js +++ b/lib/public/js/lib/api.js @@ -516,6 +516,40 @@ export async function updateAlphaclaw() { return res.json(); } +export async function fetchAgentDefaults() { + const res = await authFetch("/api/agent-defaults"); + const text = await res.text(); + let data; + try { + data = text ? JSON.parse(text) : {}; + } catch { + throw new Error(text || "Could not parse agent defaults response"); + } + if (!res.ok) { + throw new Error(data.error || text || `HTTP ${res.status}`); + } + return data; +} + +export async function updateAgentDefaults(payload) { + const res = await authFetch("/api/agent-defaults", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + const text = await res.text(); + let data; + try { + data = text ? JSON.parse(text) : {}; + } catch { + throw new Error(text || "Could not parse agent defaults response"); + } + if (!res.ok) { + throw new Error(data.error || text || `HTTP ${res.status}`); + } + return data; +} + export async function fetchSyncCron() { const res = await authFetch("/api/sync-cron"); const text = await res.text(); diff --git a/lib/server/agent-defaults-config.js b/lib/server/agent-defaults-config.js new file mode 100644 index 00000000..04e5881a --- /dev/null +++ b/lib/server/agent-defaults-config.js @@ -0,0 +1,88 @@ +const fs = require("fs"); +const { + readOpenclawConfig, + writeOpenclawConfig, +} = require("./openclaw-config"); + +const kThinkingLevels = Object.freeze(["off", "low", "medium", "high"]); +const kDefaultThinkingLevel = "medium"; +const kDefaultDreamingEnabled = false; + +const isThinkingLevel = (value) => + typeof value === "string" && kThinkingLevels.includes(value); + +const normalizeAgentDefaults = (rawConfig = {}) => { + const config = + rawConfig && typeof rawConfig === "object" && !Array.isArray(rawConfig) + ? rawConfig + : {}; + const raw = + config.agentDefaults && + typeof config.agentDefaults === "object" && + !Array.isArray(config.agentDefaults) + ? config.agentDefaults + : {}; + return { + thinking: isThinkingLevel(raw.thinking) ? raw.thinking : kDefaultThinkingLevel, + dreaming: typeof raw.dreaming === "boolean" ? raw.dreaming : kDefaultDreamingEnabled, + }; +}; + +const readAgentDefaults = ({ fsModule = fs, openclawDir } = {}) => { + const config = readOpenclawConfig({ + fsModule, + openclawDir, + fallback: {}, + }); + return normalizeAgentDefaults(config); +}; + +const writeAgentDefaults = ({ + fsModule = fs, + openclawDir, + thinking, + dreaming, +} = {}) => { + const config = readOpenclawConfig({ + fsModule, + openclawDir, + fallback: {}, + }); + const safeConfig = + config && typeof config === "object" && !Array.isArray(config) ? config : {}; + const before = JSON.stringify(safeConfig.agentDefaults || null); + const next = normalizeAgentDefaults(safeConfig); + if (typeof thinking === "string") { + if (!isThinkingLevel(thinking)) { + throw new Error( + `thinking must be one of: ${kThinkingLevels.join(", ")}`, + ); + } + next.thinking = thinking; + } + if (typeof dreaming === "boolean") { + next.dreaming = dreaming; + } + safeConfig.agentDefaults = next; + const after = JSON.stringify(safeConfig.agentDefaults); + const changed = before !== after; + if (changed) { + writeOpenclawConfig({ + fsModule, + openclawDir, + config: safeConfig, + spacing: 2, + }); + } + return { changed, agentDefaults: next }; +}; + +module.exports = { + kThinkingLevels, + kDefaultThinkingLevel, + kDefaultDreamingEnabled, + isThinkingLevel, + normalizeAgentDefaults, + readAgentDefaults, + writeAgentDefaults, +}; diff --git a/lib/server/routes/system.js b/lib/server/routes/system.js index 28817a91..042c3ffe 100644 --- a/lib/server/routes/system.js +++ b/lib/server/routes/system.js @@ -1,5 +1,11 @@ const { buildManagedPaths } = require("../internal-files-migration"); const { readOpenclawConfig } = require("../openclaw-config"); +const { + kThinkingLevels, + isThinkingLevel, + readAgentDefaults, + writeAgentDefaults, +} = require("../agent-defaults-config"); const https = require("https"); const registerSystemRoutes = ({ @@ -576,6 +582,60 @@ const registerSystemRoutes = ({ res.json({ ok: true, syncCron: status }); }); + app.get("/api/agent-defaults", (req, res) => { + try { + const agentDefaults = readAgentDefaults({ + fsModule: fs, + openclawDir: OPENCLAW_DIR, + }); + res.json({ ok: true, agentDefaults, thinkingLevels: kThinkingLevels }); + } catch (err) { + res.status(500).json({ ok: false, error: err.message }); + } + }); + + app.put("/api/agent-defaults", (req, res) => { + const { thinking, dreaming } = req.body || {}; + if (thinking !== undefined && !isThinkingLevel(thinking)) { + return res.status(400).json({ + ok: false, + error: `thinking must be one of: ${kThinkingLevels.join(", ")}`, + }); + } + if (dreaming !== undefined && typeof dreaming !== "boolean") { + return res + .status(400) + .json({ ok: false, error: "dreaming must be a boolean" }); + } + if (thinking === undefined && dreaming === undefined) { + return res.status(400).json({ + ok: false, + error: "At least one of thinking or dreaming must be provided", + }); + } + try { + const result = writeAgentDefaults({ + fsModule: fs, + openclawDir: OPENCLAW_DIR, + thinking, + dreaming, + }); + let restartRequired = false; + if (result.changed && isOnboarded()) { + restartRequiredState.markRequired(); + restartRequired = true; + } + res.json({ + ok: true, + changed: result.changed, + agentDefaults: result.agentDefaults, + restartRequired, + }); + } catch (err) { + res.status(500).json({ ok: false, error: err.message }); + } + }); + app.get("/api/alphaclaw/version", async (req, res) => { const refresh = String(req.query.refresh || "") === "1"; const status = await alphaclawVersionService.getVersionStatus(refresh); diff --git a/tests/server/agent-defaults-config.test.js b/tests/server/agent-defaults-config.test.js new file mode 100644 index 00000000..e0fcb153 --- /dev/null +++ b/tests/server/agent-defaults-config.test.js @@ -0,0 +1,108 @@ +const fs = require("fs"); +const os = require("os"); +const path = require("path"); + +const { + kThinkingLevels, + kDefaultThinkingLevel, + kDefaultDreamingEnabled, + normalizeAgentDefaults, + readAgentDefaults, + writeAgentDefaults, +} = require("../../lib/server/agent-defaults-config"); + +const createTempOpenclawDir = () => + fs.mkdtempSync(path.join(os.tmpdir(), "alphaclaw-agent-defaults-test-")); + +describe("server/agent-defaults-config", () => { + it("falls back to safe defaults when openclaw.json is missing or empty", () => { + const openclawDir = createTempOpenclawDir(); + + expect(readAgentDefaults({ fsModule: fs, openclawDir })).toEqual({ + thinking: kDefaultThinkingLevel, + dreaming: kDefaultDreamingEnabled, + }); + }); + + it("normalizes invalid stored values to safe defaults", () => { + expect( + normalizeAgentDefaults({ + agentDefaults: { thinking: "ultra", dreaming: "yes" }, + }), + ).toEqual({ + thinking: kDefaultThinkingLevel, + dreaming: kDefaultDreamingEnabled, + }); + }); + + it("reads stored values verbatim when valid", () => { + const openclawDir = createTempOpenclawDir(); + fs.writeFileSync( + path.join(openclawDir, "openclaw.json"), + JSON.stringify({ + agentDefaults: { thinking: "high", dreaming: true }, + }), + ); + + expect(readAgentDefaults({ fsModule: fs, openclawDir })).toEqual({ + thinking: "high", + dreaming: true, + }); + }); + + it("writes only the keys provided and preserves existing siblings", () => { + const openclawDir = createTempOpenclawDir(); + const configPath = path.join(openclawDir, "openclaw.json"); + fs.writeFileSync( + configPath, + JSON.stringify({ + tools: { profile: "full" }, + agentDefaults: { thinking: "low", dreaming: true }, + }), + ); + + const result = writeAgentDefaults({ + fsModule: fs, + openclawDir, + thinking: "high", + }); + + expect(result.changed).toBe(true); + expect(result.agentDefaults).toEqual({ thinking: "high", dreaming: true }); + const stored = JSON.parse(fs.readFileSync(configPath, "utf8")); + expect(stored).toEqual({ + tools: { profile: "full" }, + agentDefaults: { thinking: "high", dreaming: true }, + }); + }); + + it("reports changed=false when the write is a no-op", () => { + const openclawDir = createTempOpenclawDir(); + fs.writeFileSync( + path.join(openclawDir, "openclaw.json"), + JSON.stringify({ + agentDefaults: { thinking: "medium", dreaming: false }, + }), + ); + + const result = writeAgentDefaults({ + fsModule: fs, + openclawDir, + thinking: "medium", + dreaming: false, + }); + + expect(result.changed).toBe(false); + }); + + it("rejects invalid thinking values", () => { + const openclawDir = createTempOpenclawDir(); + expect(() => + writeAgentDefaults({ fsModule: fs, openclawDir, thinking: "max" }), + ).toThrow(/thinking must be one of/); + }); + + it("exposes the canonical thinking level list", () => { + expect(kThinkingLevels).toEqual(["off", "low", "medium", "high"]); + }); +}); diff --git a/tests/server/routes-system.test.js b/tests/server/routes-system.test.js index 2cb044f5..af3c5d81 100644 --- a/tests/server/routes-system.test.js +++ b/tests/server/routes-system.test.js @@ -78,6 +78,7 @@ const createSystemDeps = () => { restartInProgress: false, gatewayRunning: true, })), + markRequired: vi.fn(), markRestartInProgress: vi.fn(), clearRequired: vi.fn(), markRestartComplete: vi.fn(), @@ -666,4 +667,133 @@ describe("server/routes/system", () => { }, ]); }); + + describe("/api/agent-defaults", () => { + const buildAgentDefaultsDeps = (storedConfig) => { + const deps = createSystemDeps(); + const stored = { value: storedConfig ? { ...storedConfig } : null }; + deps.fs.readFileSync = vi.fn((filePath) => { + if ( + typeof filePath === "string" && + filePath.endsWith("openclaw.json") && + stored.value + ) { + return JSON.stringify(stored.value); + } + throw new Error("ENOENT"); + }); + deps.fs.writeFileSync = vi.fn((filePath, content) => { + if (typeof filePath === "string" && filePath.endsWith("openclaw.json")) { + stored.value = JSON.parse(content); + } + }); + deps.fs.mkdirSync = vi.fn(); + return { deps, stored }; + }; + + it("returns defaults when openclaw.json is missing", async () => { + const { deps } = buildAgentDefaultsDeps(null); + const app = createApp(deps); + + const res = await request(app).get("/api/agent-defaults"); + + expect(res.status).toBe(200); + expect(res.body).toEqual({ + ok: true, + agentDefaults: { thinking: "medium", dreaming: false }, + thinkingLevels: ["off", "low", "medium", "high"], + }); + }); + + it("returns stored agent defaults", async () => { + const { deps } = buildAgentDefaultsDeps({ + agentDefaults: { thinking: "high", dreaming: true }, + }); + const app = createApp(deps); + + const res = await request(app).get("/api/agent-defaults"); + + expect(res.status).toBe(200); + expect(res.body.agentDefaults).toEqual({ thinking: "high", dreaming: true }); + }); + + it("rejects invalid thinking values on PUT", async () => { + const { deps } = buildAgentDefaultsDeps(null); + const app = createApp(deps); + + const res = await request(app) + .put("/api/agent-defaults") + .send({ thinking: "max" }); + + expect(res.status).toBe(400); + expect(res.body.ok).toBe(false); + expect(res.body.error).toContain("thinking must be one of"); + expect(deps.restartRequiredState.markRequired).not.toHaveBeenCalled(); + }); + + it("rejects non-boolean dreaming values on PUT", async () => { + const { deps } = buildAgentDefaultsDeps(null); + const app = createApp(deps); + + const res = await request(app) + .put("/api/agent-defaults") + .send({ dreaming: "yes" }); + + expect(res.status).toBe(400); + expect(res.body.error).toContain("dreaming must be a boolean"); + }); + + it("rejects empty payloads on PUT", async () => { + const { deps } = buildAgentDefaultsDeps(null); + const app = createApp(deps); + + const res = await request(app).put("/api/agent-defaults").send({}); + + expect(res.status).toBe(400); + expect(res.body.error).toContain("At least one of thinking or dreaming"); + }); + + it("persists changes and signals restart-required when onboarded", async () => { + const { deps, stored } = buildAgentDefaultsDeps({ + tools: { profile: "full" }, + }); + deps.isOnboarded.mockReturnValue(true); + const app = createApp(deps); + + const res = await request(app) + .put("/api/agent-defaults") + .send({ thinking: "high", dreaming: true }); + + expect(res.status).toBe(200); + expect(res.body).toEqual({ + ok: true, + changed: true, + agentDefaults: { thinking: "high", dreaming: true }, + restartRequired: true, + }); + expect(stored.value.agentDefaults).toEqual({ + thinking: "high", + dreaming: true, + }); + expect(stored.value.tools).toEqual({ profile: "full" }); + expect(deps.restartRequiredState.markRequired).toHaveBeenCalledTimes(1); + }); + + it("does not signal restart-required for no-op writes", async () => { + const { deps } = buildAgentDefaultsDeps({ + agentDefaults: { thinking: "medium", dreaming: false }, + }); + deps.isOnboarded.mockReturnValue(true); + const app = createApp(deps); + + const res = await request(app) + .put("/api/agent-defaults") + .send({ thinking: "medium", dreaming: false }); + + expect(res.status).toBe(200); + expect(res.body.changed).toBe(false); + expect(res.body.restartRequired).toBe(false); + expect(deps.restartRequiredState.markRequired).not.toHaveBeenCalled(); + }); + }); }); From 5e805b4b6f59078841844c8440e8ad974da6e02a Mon Sep 17 00:00:00 2001 From: rsh4484 Date: Fri, 24 Apr 2026 18:05:44 -0700 Subject: [PATCH 2/3] Fix two code-review findings on agent-defaults - Pass `true` to onRestartRequired() so the global restart banner renders (matches the pattern in envars/exec-config/setup-wizard). - Add `/api/agent-defaults` to SETUP_API_PREFIXES so the proxy catch-all knows the path is locally served, not gateway-bound. --- lib/public/js/components/general/agent-defaults.js | 2 +- lib/server/constants.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/public/js/components/general/agent-defaults.js b/lib/public/js/components/general/agent-defaults.js index 48177821..050cccec 100644 --- a/lib/public/js/components/general/agent-defaults.js +++ b/lib/public/js/components/general/agent-defaults.js @@ -55,7 +55,7 @@ export const AgentDefaults = ({ onRestartRequired = () => {} }) => { const next = data.agentDefaults || {}; if (typeof next.thinking === "string") setThinking(next.thinking); if (typeof next.dreaming === "boolean") setDreaming(next.dreaming); - if (data.restartRequired) onRestartRequired(); + if (data.restartRequired) onRestartRequired(true); showToast("Agent defaults saved", "success"); } catch (err) { if (key === "thinking") setThinking(previous); diff --git a/lib/server/constants.js b/lib/server/constants.js index 242dd61b..32802652 100644 --- a/lib/server/constants.js +++ b/lib/server/constants.js @@ -408,6 +408,7 @@ const SETUP_API_PREFIXES = [ "/api/channels", "/api/operations", "/api/nodes", + "/api/agent-defaults", ]; module.exports = { From cc28302f542d891ca28a3c86bdad97b7038858ed Mon Sep 17 00:00:00 2001 From: rsh4484 Date: Fri, 24 Apr 2026 18:10:56 -0700 Subject: [PATCH 3/3] Resolve P1 review findings on agent defaults - Refuse to overwrite openclaw.json when it exists but cannot be parsed. writeAgentDefaults now reads with fallback:null, throws MalformedOpenclawConfigError, and the route returns 409. Prevents the cascade where a transient parse failure was silently converted into permanent loss of channels/plugins/secrets. - Wire agentDefaults.thinking into the doctor service so the persisted value actually shapes agent behavior. Drop markRequired() from the PUT handler and remove the unused onRestartRequired prop on AgentDefaults -- the doctor reads the value per call, so no gateway restart is needed. - Refactor AgentDefaults to useCachedFetch (per AGENTS.md), and update the api-cache entry on successful PUT so a stale GET cannot reappear immediately after a write. --- .../js/components/general/agent-defaults.js | 58 +++++++++---------- lib/public/js/components/general/index.js | 2 +- lib/server/agent-defaults-config.js | 21 ++++++- lib/server/doctor/service.js | 4 +- lib/server/routes/system.js | 10 ++-- tests/server/agent-defaults-config.test.js | 30 ++++++++++ tests/server/routes-system.test.js | 28 ++++++--- 7 files changed, 105 insertions(+), 48 deletions(-) diff --git a/lib/public/js/components/general/agent-defaults.js b/lib/public/js/components/general/agent-defaults.js index 050cccec..1c3816b3 100644 --- a/lib/public/js/components/general/agent-defaults.js +++ b/lib/public/js/components/general/agent-defaults.js @@ -2,12 +2,15 @@ import { h } from "preact"; import { useEffect, useState } from "preact/hooks"; import htm from "htm"; import { fetchAgentDefaults, updateAgentDefaults } from "../../lib/api.js"; +import { setCached } from "../../lib/api-cache.js"; +import { useCachedFetch } from "../../hooks/use-cached-fetch.js"; import { ChevronDownIcon } from "../icons.js"; import { ToggleSwitch } from "../toggle-switch.js"; import { showToast } from "../toast.js"; const html = htm.bind(h); +const kAgentDefaultsCacheKey = "/api/agent-defaults"; const kThinkingOptions = [ { value: "off", label: "Off" }, { value: "low", label: "Low" }, @@ -15,47 +18,40 @@ const kThinkingOptions = [ { value: "high", label: "High" }, ]; -export const AgentDefaults = ({ onRestartRequired = () => {} }) => { - const [loading, setLoading] = useState(true); +export const AgentDefaults = () => { + const { data, loading } = useCachedFetch( + kAgentDefaultsCacheKey, + fetchAgentDefaults, + { maxAgeMs: 30000 }, + ); + const serverDefaults = data?.agentDefaults || null; const [savingKey, setSavingKey] = useState(""); const [thinking, setThinking] = useState("medium"); const [dreaming, setDreaming] = useState(false); useEffect(() => { - let cancelled = false; - (async () => { - try { - const data = await fetchAgentDefaults(); - if (cancelled) return; - const next = data.agentDefaults || {}; - if (typeof next.thinking === "string") setThinking(next.thinking); - if (typeof next.dreaming === "boolean") setDreaming(next.dreaming); - } catch (err) { - if (!cancelled) { - showToast(err.message || "Could not load agent defaults", "error"); - } - } finally { - if (!cancelled) setLoading(false); - } - })(); - return () => { - cancelled = true; - }; - }, []); + if (!serverDefaults) return; + if (typeof serverDefaults.thinking === "string") { + setThinking(serverDefaults.thinking); + } + if (typeof serverDefaults.dreaming === "boolean") { + setDreaming(serverDefaults.dreaming); + } + }, [serverDefaults?.thinking, serverDefaults?.dreaming]); const save = async (key, payload, optimistic) => { const previous = key === "thinking" ? thinking : dreaming; optimistic(); setSavingKey(key); try { - const data = await updateAgentDefaults(payload); - if (!data.ok) { - throw new Error(data.error || "Could not save agent defaults"); + const response = await updateAgentDefaults(payload); + if (!response.ok) { + throw new Error(response.error || "Could not save agent defaults"); } - const next = data.agentDefaults || {}; + const next = response.agentDefaults || {}; if (typeof next.thinking === "string") setThinking(next.thinking); if (typeof next.dreaming === "boolean") setDreaming(next.dreaming); - if (data.restartRequired) onRestartRequired(true); + setCached(kAgentDefaultsCacheKey, { ...(data || {}), ...response }); showToast("Agent defaults saved", "success"); } catch (err) { if (key === "thinking") setThinking(previous); @@ -75,6 +71,8 @@ export const AgentDefaults = ({ onRestartRequired = () => {} }) => { save("dreaming", { dreaming: nextDreaming }, () => setDreaming(nextDreaming)); }; + const disabled = loading && !serverDefaults; + return html`

Agent Defaults

@@ -88,8 +86,8 @@ export const AgentDefaults = ({ onRestartRequired = () => {} }) => {