mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-04 07:01:38 +00:00
fix(ui): stop claiming a gateway restart when toggling dreaming (#113960)
The dreaming toggle writes `plugins.entries.<memoryPluginId>.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.
This commit is contained in:
committed by
GitHub
parent
977c8e9d37
commit
467d1d1f11
@@ -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: {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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),
|
||||
})}
|
||||
`;
|
||||
|
||||
@@ -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`
|
||||
<openclaw-modal-dialog label=${title} description=${description} @modal-cancel=${handleCancel}>
|
||||
<div class="exec-approval-card">
|
||||
<div class="exec-approval-header">
|
||||
<div>
|
||||
<div id=${titleId} class="exec-approval-title">${title}</div>
|
||||
<div id=${descriptionId} class="exec-approval-sub">${description}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="callout danger" style="margin-top: 12px;">
|
||||
${t("dreaming.restartConfirmation.warning")}
|
||||
</div>
|
||||
${props.hasError
|
||||
? html`<div class="exec-approval-error">${t("dreaming.restartConfirmation.failed")}</div>`
|
||||
: nothing}
|
||||
<div class="exec-approval-actions">
|
||||
<button class="btn danger" ?disabled=${props.loading} @click=${props.onConfirm}>
|
||||
${props.loading
|
||||
? t("dreaming.restartConfirmation.restarting")
|
||||
: t("dreaming.restartConfirmation.confirm")}
|
||||
</button>
|
||||
<button class="btn" ?disabled=${props.loading} @click=${props.onCancel}>
|
||||
${t("common.cancel")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</openclaw-modal-dialog>
|
||||
`;
|
||||
}
|
||||
61
ui/src/pages/agents/memory/toggle-confirmation.test.ts
Normal file
61
ui/src/pages/agents/memory/toggle-confirmation.test.ts
Normal file
@@ -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<typeof renderDreamingToggleConfirmation>[0];
|
||||
|
||||
function renderToggle(overrides?: Partial<ToggleProps>): 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();
|
||||
});
|
||||
});
|
||||
69
ui/src/pages/agents/memory/toggle-confirmation.ts
Normal file
69
ui/src/pages/agents/memory/toggle-confirmation.ts
Normal file
@@ -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`
|
||||
<openclaw-modal-dialog label=${title} description=${description} @modal-cancel=${handleCancel}>
|
||||
<div class="exec-approval-card">
|
||||
<div class="exec-approval-header">
|
||||
<div>
|
||||
<div id=${titleId} class="exec-approval-title">${title}</div>
|
||||
<div id=${descriptionId} class="exec-approval-sub">${description}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="callout ${props.enabling ? "info" : "warn"}" style="margin-top: 12px;">
|
||||
${detail}
|
||||
</div>
|
||||
${props.hasError
|
||||
? html`<div class="exec-approval-error">${t("dreaming.toggleConfirmation.failed")}</div>`
|
||||
: nothing}
|
||||
<div class="exec-approval-actions">
|
||||
<button
|
||||
class="btn ${props.enabling ? "primary" : "danger"}"
|
||||
?disabled=${props.loading}
|
||||
@click=${props.onConfirm}
|
||||
>
|
||||
${props.loading ? t("dreaming.toggleConfirmation.saving") : confirmLabel}
|
||||
</button>
|
||||
<button class="btn" ?disabled=${props.loading} @click=${props.onCancel}>
|
||||
${t("common.cancel")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</openclaw-modal-dialog>
|
||||
`;
|
||||
}
|
||||
Reference in New Issue
Block a user