diff --git a/CHANGELOG.md b/CHANGELOG.md index 28e8b9df8cb..42447bcc1c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -88,6 +88,7 @@ Docs: https://docs.openclaw.ai - CLI/gateway: reuse cached paired-device auth during `gateway probe` and report post-connect diagnostic failures as degraded reachability, so healthy local gateways are no longer marked unreachable after loopback auth or read timeouts. Fixes #48360. Thanks @RacecarGuy. - Channels/Discord: give Discord Gateway WebSocket handshakes a 30s timeout so stalled TLS/network transitions emit an error and Carbon can continue its reconnect loop instead of leaving the bot silent until restart. Refs #50046. Thanks @codexGW. - Channels/Telegram: suppress standalone failed edit/write warning payloads when a user-facing assistant error reply already covers the turn, while keeping unresolved mutating failures visible behind success-looking or suppressed-error replies. Fixes #39631; refs #73750; carries forward #39636 and #39717; leaves #39406 for configurable delivery policy. Thanks @Bartok9 and @Bortlesboat. +- Control UI/agents: persist the Set Default action through `agents.list[].default` instead of writing the unsupported `agents.defaultId` field, so saved default-agent changes survive config validation. Fixes #65565; carries forward #72585. Thanks @luyao618. - NVIDIA/NIM: persist the `NVIDIA_API_KEY` provider marker and mark bundled NVIDIA Chat Completions models as string-content compatible, so NIM models load from `models.json` and OpenAI-compatible subagent calls send plain text content. Fixes #73013 and #50107; refs #73014. Thanks @bautrey, @iot2edge, @ifearghal, and @futhgar. - Channels/Discord: let text-only configs drop the `GuildVoiceStates` gateway intent and expose a bounded `/gateway/bot` metadata timeout with rate-limited fallback logs, reducing idle CPU and warning floods. Fixes #73709 and #73585. Thanks @sanchezm86 and @trac3r00. - Agents/sessions: mark same-turn `sessions_send` and A2A reply prompts with an inter-session `isUser=false` envelope before they reach the model, so foreign session output no longer lands as bare active user text. Fixes #73702; refs #73698, #73609, #73595, and #73622. Thanks @alvelda. diff --git a/ui/src/ui/app-render.ts b/ui/src/ui/app-render.ts index be195040720..0b2ee0cc82c 100644 --- a/ui/src/ui/app-render.ts +++ b/ui/src/ui/app-render.ts @@ -40,6 +40,7 @@ import { resetConfigPendingChanges, runUpdate, saveConfig, + stageDefaultAgentConfigEntry, stageConfigPreset, updateConfigFormValue, removeConfigFormValue, @@ -2148,10 +2149,7 @@ export function renderApp(state: AppViewState) { updateConfigFormValue(state, basePath, { primary, fallbacks: normalized }); }, onSetDefault: (agentId) => { - if (!configValue) { - return; - } - updateConfigFormValue(state, ["agents", "defaultId"], agentId); + stageDefaultAgentConfigEntry(state, agentId); }, }), ) diff --git a/ui/src/ui/controllers/config.test.ts b/ui/src/ui/controllers/config.test.ts index 2923586b8e5..dea41b310a7 100644 --- a/ui/src/ui/controllers/config.test.ts +++ b/ui/src/ui/controllers/config.test.ts @@ -8,6 +8,7 @@ import { resetConfigPendingChanges, runUpdate, saveConfig, + stageDefaultAgentConfigEntry, stageConfigPreset, updateConfigFormValue, type ConfigState, @@ -477,6 +478,50 @@ describe("agent config helpers", () => { }, }); }); + + it("sets default via agents.list[].default instead of agents.defaultId", () => { + const state = createState(); + state.configSnapshot = { + config: { + agents: { + list: [{ id: "alpha", default: true }, { id: "beta" }], + }, + }, + valid: true, + issues: [], + raw: "{\n}\n", + }; + + const updated = stageDefaultAgentConfigEntry(state, "beta"); + + expect(updated).toBe(true); + expect(state.configFormDirty).toBe(true); + expect(state.configForm).toEqual({ + agents: { + list: [{ id: "alpha" }, { id: "beta", default: true }], + }, + }); + }); + + it("does not stage agents.defaultId when the target agent is absent", () => { + const state = createState(); + state.configSnapshot = { + config: { + agents: { + list: [{ id: "alpha", default: true }], + }, + }, + valid: true, + issues: [], + raw: "{\n}\n", + }; + + const updated = stageDefaultAgentConfigEntry(state, "beta"); + + expect(updated).toBe(false); + expect(state.configFormDirty).toBe(false); + expect(state.configForm).toBeNull(); + }); }); describe("applyConfig", () => { diff --git a/ui/src/ui/controllers/config.ts b/ui/src/ui/controllers/config.ts index 5434e5833de..015e843fbf3 100644 --- a/ui/src/ui/controllers/config.ts +++ b/ui/src/ui/controllers/config.ts @@ -359,6 +359,38 @@ export function ensureAgentConfigEntry(state: ConfigState, agentId: string): num return nextIndex; } +export function stageDefaultAgentConfigEntry(state: ConfigState, agentId: string): boolean { + const normalizedAgentId = agentId.trim(); + if (!normalizedAgentId) { + return false; + } + const source = + state.configForm ?? (state.configSnapshot?.config as Record | null); + const targetIndex = findAgentConfigEntryIndex(source, normalizedAgentId); + if (targetIndex < 0) { + return false; + } + mutateConfigForm(state, (draft) => { + const list = (draft as { agents?: { list?: unknown[] } } | null)?.agents?.list; + if (!Array.isArray(list)) { + return; + } + for (let i = 0; i < list.length; i++) { + const entry = list[i]; + if (!entry || typeof entry !== "object" || Array.isArray(entry)) { + continue; + } + const record = entry as Record; + if (i === targetIndex) { + record.default = true; + } else { + delete record.default; + } + } + }); + return true; +} + export async function openConfigFile(state: ConfigState): Promise { if (!state.client || !state.connected) { return;