From 467d1d1f11df907aaffff5d87cb02d57c19656eb Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 25 Jul 2026 18:29:10 -0700 Subject: [PATCH] fix(ui): stop claiming a gateway restart when toggling dreaming (#113960) The dreaming toggle writes `plugins.entries..config.dreaming.enabled`, which matches the `{ prefix: "plugins", kind: "hot" }` reload rule in src/gateway/config-reload-plan.ts, so `restartGateway` stays false and no restart ever happens. The confirmation modal nonetheless showed a red danger callout promising a Gateway restart that "may temporarily interrupt chats, automations, and connected channels". Replace it with an accurate confirmation that states the real consequence: dreaming is global, so the single managed cron sweep it schedules covers every configured agent workspace (resolveMemoryDreamingWorkspaces iterates all agent entries), not just the agent whose page the panel is on. Copy is now direction-aware, since turning it off is the consequential direction. Renames the i18n block from `dreaming.restartConfirmation` to `dreaming.toggleConfirmation` and the component from `restart-confirmation.ts` to `toggle-confirmation.ts`; memory-panel was its only caller. No runtime or server behavior changes. --- ui/src/i18n/locales/en.ts | 18 +++-- .../pages/agents/memory/memory-panel.test.ts | 20 +++--- ui/src/pages/agents/memory/memory-panel.ts | 47 ++++++------- .../agents/memory/restart-confirmation.ts | 56 --------------- .../agents/memory/toggle-confirmation.test.ts | 61 ++++++++++++++++ .../agents/memory/toggle-confirmation.ts | 69 +++++++++++++++++++ 6 files changed, 175 insertions(+), 96 deletions(-) delete mode 100644 ui/src/pages/agents/memory/restart-confirmation.ts create mode 100644 ui/src/pages/agents/memory/toggle-confirmation.test.ts create mode 100644 ui/src/pages/agents/memory/toggle-confirmation.ts diff --git a/ui/src/i18n/locales/en.ts b/ui/src/i18n/locales/en.ts index 94fb8122f032..e5aceaee6f48 100644 --- a/ui/src/i18n/locales/en.ts +++ b/ui/src/i18n/locales/en.ts @@ -3142,13 +3142,17 @@ export const en: TranslationMap = { on: "Dreaming On", off: "Dreaming Off", }, - restartConfirmation: { - title: "Restart Gateway to Apply Change", - subtitle: "Changing Dreaming mode restarts the gateway.", - warning: - "This action will restart the Gateway and may temporarily interrupt chats, automations, and connected channels.", - confirm: "Confirm Restart", - restarting: "Restarting…", + toggleConfirmation: { + subtitle: "Dreaming is a global setting; it is not scoped to this agent.", + enableTitle: "Turn On Dreaming for All Agents", + enableDetail: + "The nightly dreaming sweep will run across every configured agent workspace, promoting short-term recalls into long-term memory. This applies right away.", + enableConfirm: "Turn On Dreaming", + disableTitle: "Turn Off Dreaming for All Agents", + disableDetail: + "The nightly dreaming sweep will stop for every configured agent, not just this one. Memories already written stay; nothing new gets promoted. This applies right away.", + disableConfirm: "Turn Off Dreaming", + saving: "Saving…", failed: "Could not apply change. Check your connection and try again.", }, status: { diff --git a/ui/src/pages/agents/memory/memory-panel.test.ts b/ui/src/pages/agents/memory/memory-panel.test.ts index 19a3f61d4d67..c8aa429f4736 100644 --- a/ui/src/pages/agents/memory/memory-panel.test.ts +++ b/ui/src/pages/agents/memory/memory-panel.test.ts @@ -13,8 +13,8 @@ type TestMemoryPanel = HTMLElement & { agentId: string; dreaming: DreamingState; viewState: DreamingViewState; - restartConfirmOpen: boolean; - restartConfirmLoading: boolean; + toggleConfirmOpen: boolean; + toggleConfirmLoading: boolean; pendingEnabled: boolean | null; applyAgentId: () => void; applyGatewaySnapshot: (snapshot: ApplicationGatewaySnapshot) => void; @@ -120,8 +120,8 @@ describe("AgentMemoryPanel gateway lifecycle", () => { page.viewState.wikiPreviewLoading = true; page.viewState.wikiPreviewTitle = "Old page"; page.viewState.wikiPreviewContent = "old wiki"; - page.restartConfirmOpen = true; - page.restartConfirmLoading = true; + page.toggleConfirmOpen = true; + page.toggleConfirmLoading = true; page.pendingEnabled = true; await replaceContext(page, contextWithGateway(client, false)); @@ -132,19 +132,19 @@ describe("AgentMemoryPanel gateway lifecycle", () => { expect(page.viewState.wikiPreviewLoading).toBe(false); expect(page.viewState.wikiPreviewTitle).toBe(""); expect(page.viewState.wikiPreviewContent).toBe(""); - expect(page.restartConfirmOpen).toBe(false); - expect(page.restartConfirmLoading).toBe(false); + expect(page.toggleConfirmOpen).toBe(false); + expect(page.toggleConfirmLoading).toBe(false); expect(page.pendingEnabled).toBeNull(); page.viewState.wikiPreviewOpen = true; - page.restartConfirmOpen = true; - page.restartConfirmLoading = true; + page.toggleConfirmOpen = true; + page.toggleConfirmLoading = true; page.pendingEnabled = false; page.remove(); expect(page.viewState.wikiPreviewOpen).toBe(false); - expect(page.restartConfirmOpen).toBe(false); - expect(page.restartConfirmLoading).toBe(false); + expect(page.toggleConfirmOpen).toBe(false); + expect(page.toggleConfirmLoading).toBe(false); expect(page.pendingEnabled).toBeNull(); }); diff --git a/ui/src/pages/agents/memory/memory-panel.ts b/ui/src/pages/agents/memory/memory-panel.ts index a2f157d26d19..87e5eea48697 100644 --- a/ui/src/pages/agents/memory/memory-panel.ts +++ b/ui/src/pages/agents/memory/memory-panel.ts @@ -29,7 +29,7 @@ import { updateDreamingEnabled, type DreamingState, } from "./dreaming.ts"; -import { renderDreamingRestartConfirmation } from "./restart-confirmation.ts"; +import { renderDreamingToggleConfirmation } from "./toggle-confirmation.ts"; import { createDreamingViewState, renderDreaming, type DreamingViewState } from "./view.ts"; type WikiPagePreview = { @@ -104,8 +104,8 @@ class AgentMemoryPanel extends OpenClawLightDomElement { @property({ attribute: false }) agentId = ""; @state() private dreaming = createDreamingState(); - @state() private restartConfirmOpen = false; - @state() private restartConfirmLoading = false; + @state() private toggleConfirmOpen = false; + @state() private toggleConfirmLoading = false; @state() private pendingEnabled: boolean | null = null; private readonly viewState: DreamingViewState = createDreamingViewState(); @@ -187,8 +187,8 @@ class AgentMemoryPanel extends OpenClawLightDomElement { private resetTransientState() { this.resetWikiPreview(); - this.restartConfirmOpen = false; - this.restartConfirmLoading = false; + this.toggleConfirmOpen = false; + this.toggleConfirmLoading = false; this.pendingEnabled = null; } @@ -305,37 +305,37 @@ class AgentMemoryPanel extends OpenClawLightDomElement { private setEnabled(enabled: boolean, dreamingOn: boolean) { if ( this.dreaming.dreamingModeSaving || - this.restartConfirmLoading || - this.restartConfirmOpen || + this.toggleConfirmLoading || + this.toggleConfirmOpen || dreamingOn === enabled ) { return; } this.pendingEnabled = enabled; - this.restartConfirmOpen = true; + this.toggleConfirmOpen = true; this.dreaming.dreamingStatusError = null; } - private cancelRestart() { - if (this.restartConfirmLoading) { + private cancelToggle() { + if (this.toggleConfirmLoading) { return; } - this.restartConfirmOpen = false; + this.toggleConfirmOpen = false; this.pendingEnabled = null; this.dreaming.dreamingStatusError = null; } - private async confirmRestart() { + private async confirmToggle() { const enabled = this.pendingEnabled; - if (enabled == null || this.restartConfirmLoading) { + if (enabled == null || this.toggleConfirmLoading) { return; } - this.restartConfirmLoading = true; + this.toggleConfirmLoading = true; this.dreaming.dreamingStatusError = null; const scope = this.captureTaskScope(); const runtimeConfig = this.context.runtimeConfig; if (!scope) { - this.restartConfirmLoading = false; + this.toggleConfirmLoading = false; return; } try { @@ -347,7 +347,7 @@ class AgentMemoryPanel extends OpenClawLightDomElement { return; } if (!updated) { - this.dreaming.dreamingStatusError ??= t("dreaming.restartConfirmation.failed"); + this.dreaming.dreamingStatusError ??= t("dreaming.toggleConfirmation.failed"); return; } await runtimeConfig.refresh(); @@ -359,11 +359,11 @@ class AgentMemoryPanel extends OpenClawLightDomElement { if (!this.isTaskScopeCurrent(scope)) { return; } - this.restartConfirmOpen = false; + this.toggleConfirmOpen = false; this.pendingEnabled = null; } finally { if (this.isTaskScopeCurrent(scope)) { - this.restartConfirmLoading = false; + this.toggleConfirmLoading = false; } } } @@ -487,11 +487,12 @@ class AgentMemoryPanel extends OpenClawLightDomElement { onRepairDreamingArtifacts: () => void this.runDreamingTask(repairDreamingArtifacts), onViewStateChange: () => this.requestUpdate(), })} - ${renderDreamingRestartConfirmation({ - open: this.restartConfirmOpen, - loading: this.restartConfirmLoading, - onConfirm: () => void this.confirmRestart(), - onCancel: () => this.cancelRestart(), + ${renderDreamingToggleConfirmation({ + open: this.toggleConfirmOpen, + enabling: this.pendingEnabled === true, + loading: this.toggleConfirmLoading, + onConfirm: () => void this.confirmToggle(), + onCancel: () => this.cancelToggle(), hasError: Boolean(dreaming.dreamingStatusError), })} `; diff --git a/ui/src/pages/agents/memory/restart-confirmation.ts b/ui/src/pages/agents/memory/restart-confirmation.ts deleted file mode 100644 index e095b66f6f3e..000000000000 --- a/ui/src/pages/agents/memory/restart-confirmation.ts +++ /dev/null @@ -1,56 +0,0 @@ -// Control UI view renders dreaming restart confirmation screen content. -import { html, nothing } from "lit"; -import { t } from "../../../i18n/index.ts"; -import "../../../components/modal-dialog.ts"; - -type DreamingRestartConfirmationProps = { - open: boolean; - loading: boolean; - onConfirm: () => void; - onCancel: () => void; - hasError: boolean; -}; - -export function renderDreamingRestartConfirmation(props: DreamingRestartConfirmationProps) { - if (!props.open) { - return nothing; - } - const titleId = "dreaming-restart-confirmation-title"; - const descriptionId = "dreaming-restart-confirmation-description"; - const title = t("dreaming.restartConfirmation.title"); - const description = t("dreaming.restartConfirmation.subtitle"); - const handleCancel = () => { - if (!props.loading) { - props.onCancel(); - } - }; - - return html` - -
-
-
-
${title}
-
${description}
-
-
-
- ${t("dreaming.restartConfirmation.warning")} -
- ${props.hasError - ? html`
${t("dreaming.restartConfirmation.failed")}
` - : nothing} -
- - -
-
-
- `; -} diff --git a/ui/src/pages/agents/memory/toggle-confirmation.test.ts b/ui/src/pages/agents/memory/toggle-confirmation.test.ts new file mode 100644 index 000000000000..503fa5bfd24a --- /dev/null +++ b/ui/src/pages/agents/memory/toggle-confirmation.test.ts @@ -0,0 +1,61 @@ +/* @vitest-environment jsdom */ + +import { render } from "lit"; +import { describe, expect, it, vi } from "vitest"; +import { renderDreamingToggleConfirmation } from "./toggle-confirmation.ts"; + +type ToggleProps = Parameters[0]; + +function renderToggle(overrides?: Partial): HTMLElement { + const props: ToggleProps = { + open: true, + enabling: true, + loading: false, + onConfirm: vi.fn(), + onCancel: vi.fn(), + hasError: false, + ...overrides, + }; + const host = document.createElement("div"); + render(renderDreamingToggleConfirmation(props), host); + return host; +} + +describe("renderDreamingToggleConfirmation", () => { + it("renders nothing while closed", () => { + expect(renderToggle({ open: false }).textContent?.trim()).toBe(""); + }); + + it("states the global scope and never promises a gateway restart", () => { + for (const enabling of [true, false]) { + const text = renderToggle({ enabling }).textContent ?? ""; + expect(text).toContain("All Agents"); + expect(text).toContain("global setting"); + expect(text.toLowerCase()).not.toContain("restart"); + expect(text.toLowerCase()).not.toContain("interrupt"); + } + }); + + it("uses direction-specific copy for enabling and disabling", () => { + expect(renderToggle({ enabling: true }).textContent).toContain("Turn On Dreaming"); + const disabling = renderToggle({ enabling: false }); + expect(disabling.textContent).toContain("Turn Off Dreaming"); + // Disabling is the destructive direction: it stops the sweep for every agent. + expect(disabling.querySelector("button.btn.danger")).not.toBeNull(); + expect(renderToggle({ enabling: true }).querySelector("button.btn.danger")).toBeNull(); + }); + + it("swaps the confirm label for a saving label while the write is in flight", () => { + const host = renderToggle({ loading: true }); + const confirm = host.querySelector("button"); + expect(confirm?.textContent?.trim()).toBe("Saving…"); + expect(confirm?.hasAttribute("disabled")).toBe(true); + }); + + it("ignores backdrop cancel while saving", () => { + const onCancel = vi.fn(); + const host = renderToggle({ loading: true, onCancel }); + host.querySelector("openclaw-modal-dialog")?.dispatchEvent(new CustomEvent("modal-cancel")); + expect(onCancel).not.toHaveBeenCalled(); + }); +}); diff --git a/ui/src/pages/agents/memory/toggle-confirmation.ts b/ui/src/pages/agents/memory/toggle-confirmation.ts new file mode 100644 index 000000000000..4f2e54eb8d4f --- /dev/null +++ b/ui/src/pages/agents/memory/toggle-confirmation.ts @@ -0,0 +1,69 @@ +// Control UI view renders the dreaming on/off confirmation screen content. +import { html, nothing } from "lit"; +import { t } from "../../../i18n/index.ts"; +import "../../../components/modal-dialog.ts"; + +type DreamingToggleConfirmationProps = { + open: boolean; + // Direction of the pending write. Copy differs because turning dreaming off + // stops the sweep for every agent, not just the one this panel is showing. + enabling: boolean; + loading: boolean; + onConfirm: () => void; + onCancel: () => void; + hasError: boolean; +}; + +export function renderDreamingToggleConfirmation(props: DreamingToggleConfirmationProps) { + if (!props.open) { + return nothing; + } + const titleId = "dreaming-toggle-confirmation-title"; + const descriptionId = "dreaming-toggle-confirmation-description"; + const title = props.enabling + ? t("dreaming.toggleConfirmation.enableTitle") + : t("dreaming.toggleConfirmation.disableTitle"); + const description = t("dreaming.toggleConfirmation.subtitle"); + const detail = props.enabling + ? t("dreaming.toggleConfirmation.enableDetail") + : t("dreaming.toggleConfirmation.disableDetail"); + const confirmLabel = props.enabling + ? t("dreaming.toggleConfirmation.enableConfirm") + : t("dreaming.toggleConfirmation.disableConfirm"); + const handleCancel = () => { + if (!props.loading) { + props.onCancel(); + } + }; + + return html` + +
+
+
+
${title}
+
${description}
+
+
+
+ ${detail} +
+ ${props.hasError + ? html`
${t("dreaming.toggleConfirmation.failed")}
` + : nothing} +
+ + +
+
+
+ `; +}