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} - - - ${props.loading - ? t("dreaming.restartConfirmation.restarting") - : t("dreaming.restartConfirmation.confirm")} - - - ${t("common.cancel")} - - - - - `; -} 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} + + + ${props.loading ? t("dreaming.toggleConfirmation.saving") : confirmLabel} + + + ${t("common.cancel")} + + + + + `; +}