feat(control-ui): confirm dreaming restart changes

Require explicit confirmation before applying restart-impacting Dreaming mode changes in the Control UI.

- Add pending/confirm/loading state for the Dreaming toggle path
- Render a restart confirmation dialog before sending the config patch
- Sync Control UI locale metadata and cover the confirmation flow in browser tests

Fixes #63804
This commit is contained in:
bbddbb
2026-04-27 16:08:59 +08:00
committed by GitHub
parent 276291d399
commit 563718c2e4
33 changed files with 537 additions and 59 deletions

View File

@@ -14,6 +14,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- CLI/plugins: stop security-blocked plugin installs from retrying as hook packs, so normal plugin packages report the scanner failure without a misleading "not a valid hook pack" follow-up. Fixes #61175; supersedes #64102. Thanks @KonsultDigital and @ziyincody.
- Control UI/Dreaming: require explicit confirmation before applying restart-impacting Dreaming mode changes, with restart warning copy and loading feedback. Fixes #63804. (#63807) Thanks @bbddbb1.
- CLI/update: keep the automatic post-update completion refresh on the core-command tree so it no longer stages bundled plugin runtime deps before the Gateway restart path, avoiding `.24` update hangs and 1006 disconnect cascades. Fixes #72665. Thanks @sakalaboator and @He-Pin.
- Agents/Bedrock: stop heartbeat runs from persisting blank user transcript turns and repair existing blank user text messages before replay, preventing AWS Bedrock `ContentBlock` blank-text validation failures. Fixes #72640 and #72622. Thanks @goldzulu.
- Agents/LM Studio: promote standalone bracketed local-model tool requests into registered tool calls and hide unsupported bracket blocks from visible replies, so MemPalace MCP lookups do not print raw `[tool]` JSON scaffolding in chat. Fixes #66178. Thanks @detroit357.

View File

@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-04-26T21:47:11.631Z",
"generatedAt": "2026-04-27T07:37:20.795Z",
"locale": "de",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "0b1690213c6431759bd87ed8a231c4f523c79bac42dfac74028698fb18e7ebba",
"totalKeys": 752,
"translatedKeys": 752,
"sourceHash": "802e1bbb6a0e64584ec06cab4c61b65808c23669206a3a216646cf1a558ba657",
"totalKeys": 758,
"translatedKeys": 758,
"workflow": 1
}

View File

@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-04-26T21:47:11.941Z",
"generatedAt": "2026-04-27T07:37:21.116Z",
"locale": "es",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "0b1690213c6431759bd87ed8a231c4f523c79bac42dfac74028698fb18e7ebba",
"totalKeys": 752,
"translatedKeys": 752,
"sourceHash": "802e1bbb6a0e64584ec06cab4c61b65808c23669206a3a216646cf1a558ba657",
"totalKeys": 758,
"translatedKeys": 758,
"workflow": 1
}

View File

@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-04-26T21:47:12.883Z",
"generatedAt": "2026-04-27T07:37:22.097Z",
"locale": "fr",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "0b1690213c6431759bd87ed8a231c4f523c79bac42dfac74028698fb18e7ebba",
"totalKeys": 752,
"translatedKeys": 752,
"sourceHash": "802e1bbb6a0e64584ec06cab4c61b65808c23669206a3a216646cf1a558ba657",
"totalKeys": 758,
"translatedKeys": 758,
"workflow": 1
}

View File

@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-04-26T21:47:13.865Z",
"generatedAt": "2026-04-27T07:37:23.072Z",
"locale": "id",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "0b1690213c6431759bd87ed8a231c4f523c79bac42dfac74028698fb18e7ebba",
"totalKeys": 752,
"translatedKeys": 752,
"sourceHash": "802e1bbb6a0e64584ec06cab4c61b65808c23669206a3a216646cf1a558ba657",
"totalKeys": 758,
"translatedKeys": 758,
"workflow": 1
}

View File

@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-04-26T21:47:12.252Z",
"generatedAt": "2026-04-27T07:37:21.442Z",
"locale": "ja-JP",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "0b1690213c6431759bd87ed8a231c4f523c79bac42dfac74028698fb18e7ebba",
"totalKeys": 752,
"translatedKeys": 752,
"sourceHash": "802e1bbb6a0e64584ec06cab4c61b65808c23669206a3a216646cf1a558ba657",
"totalKeys": 758,
"translatedKeys": 758,
"workflow": 1
}

View File

@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-04-26T21:47:12.563Z",
"generatedAt": "2026-04-27T07:37:21.771Z",
"locale": "ko",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "0b1690213c6431759bd87ed8a231c4f523c79bac42dfac74028698fb18e7ebba",
"totalKeys": 752,
"translatedKeys": 752,
"sourceHash": "802e1bbb6a0e64584ec06cab4c61b65808c23669206a3a216646cf1a558ba657",
"totalKeys": 758,
"translatedKeys": 758,
"workflow": 1
}

View File

@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-04-26T21:47:14.204Z",
"generatedAt": "2026-04-27T07:37:23.392Z",
"locale": "pl",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "0b1690213c6431759bd87ed8a231c4f523c79bac42dfac74028698fb18e7ebba",
"totalKeys": 752,
"translatedKeys": 752,
"sourceHash": "802e1bbb6a0e64584ec06cab4c61b65808c23669206a3a216646cf1a558ba657",
"totalKeys": 758,
"translatedKeys": 758,
"workflow": 1
}

View File

@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-04-26T21:47:11.305Z",
"generatedAt": "2026-04-27T07:37:20.461Z",
"locale": "pt-BR",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "0b1690213c6431759bd87ed8a231c4f523c79bac42dfac74028698fb18e7ebba",
"totalKeys": 752,
"translatedKeys": 752,
"sourceHash": "802e1bbb6a0e64584ec06cab4c61b65808c23669206a3a216646cf1a558ba657",
"totalKeys": 758,
"translatedKeys": 758,
"workflow": 1
}

View File

@@ -1,11 +1,18 @@
{
"fallbackKeys": [],
"generatedAt": "2026-04-26T21:47:14.524Z",
"fallbackKeys": [
"dreaming.restartConfirmation.confirm",
"dreaming.restartConfirmation.failed",
"dreaming.restartConfirmation.restarting",
"dreaming.restartConfirmation.subtitle",
"dreaming.restartConfirmation.title",
"dreaming.restartConfirmation.warning"
],
"generatedAt": "2026-04-27T07:37:23.707Z",
"locale": "th",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "0b1690213c6431759bd87ed8a231c4f523c79bac42dfac74028698fb18e7ebba",
"totalKeys": 752,
"sourceHash": "802e1bbb6a0e64584ec06cab4c61b65808c23669206a3a216646cf1a558ba657",
"totalKeys": 758,
"translatedKeys": 752,
"workflow": 1
}

View File

@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-04-26T21:47:13.204Z",
"generatedAt": "2026-04-27T07:37:22.430Z",
"locale": "tr",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "0b1690213c6431759bd87ed8a231c4f523c79bac42dfac74028698fb18e7ebba",
"totalKeys": 752,
"translatedKeys": 752,
"sourceHash": "802e1bbb6a0e64584ec06cab4c61b65808c23669206a3a216646cf1a558ba657",
"totalKeys": 758,
"translatedKeys": 758,
"workflow": 1
}

View File

@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-04-26T21:47:13.531Z",
"generatedAt": "2026-04-27T07:37:22.755Z",
"locale": "uk",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "0b1690213c6431759bd87ed8a231c4f523c79bac42dfac74028698fb18e7ebba",
"totalKeys": 752,
"translatedKeys": 752,
"sourceHash": "802e1bbb6a0e64584ec06cab4c61b65808c23669206a3a216646cf1a558ba657",
"totalKeys": 758,
"translatedKeys": 758,
"workflow": 1
}

View File

@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-04-26T21:47:10.673Z",
"generatedAt": "2026-04-27T07:37:19.772Z",
"locale": "zh-CN",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "0b1690213c6431759bd87ed8a231c4f523c79bac42dfac74028698fb18e7ebba",
"totalKeys": 752,
"translatedKeys": 752,
"sourceHash": "802e1bbb6a0e64584ec06cab4c61b65808c23669206a3a216646cf1a558ba657",
"totalKeys": 758,
"translatedKeys": 758,
"workflow": 1
}

View File

@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-04-26T21:47:10.990Z",
"generatedAt": "2026-04-27T07:37:20.133Z",
"locale": "zh-TW",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "0b1690213c6431759bd87ed8a231c4f523c79bac42dfac74028698fb18e7ebba",
"totalKeys": 752,
"translatedKeys": 752,
"sourceHash": "802e1bbb6a0e64584ec06cab4c61b65808c23669206a3a216646cf1a558ba657",
"totalKeys": 758,
"translatedKeys": 758,
"workflow": 1
}

View File

@@ -356,6 +356,15 @@ export const de: TranslationMap = {
on: "Träumen aktiviert",
off: "Träumen deaktiviert",
},
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…",
failed: "Could not apply change. Check your connection and try again.",
},
status: {
active: "Träumen aktiv",
idle: "Träumen im Leerlauf",

View File

@@ -346,6 +346,15 @@ 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…",
failed: "Could not apply change. Check your connection and try again.",
},
status: {
active: "Dreaming Active",
idle: "Dreaming Idle",

View File

@@ -350,6 +350,15 @@ export const es: TranslationMap = {
on: "Sueño activado",
off: "Sueño desactivado",
},
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…",
failed: "Could not apply change. Check your connection and try again.",
},
status: {
active: "Sueño activo",
idle: "Sueño inactivo",

View File

@@ -354,6 +354,15 @@ export const fr: TranslationMap = {
on: "Rêverie activée",
off: "Rêverie désactivée",
},
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…",
failed: "Could not apply change. Check your connection and try again.",
},
status: {
active: "Rêverie active",
idle: "Rêverie inactive",

View File

@@ -350,6 +350,15 @@ export const id: TranslationMap = {
on: "Dreaming Aktif",
off: "Dreaming Nonaktif",
},
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…",
failed: "Could not apply change. Check your connection and try again.",
},
status: {
active: "Dreaming Aktif",
idle: "Dreaming Idle",

View File

@@ -354,6 +354,15 @@ export const ja_JP: TranslationMap = {
on: "Dreaming オン",
off: "Dreaming オフ",
},
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…",
failed: "Could not apply change. Check your connection and try again.",
},
status: {
active: "Dreaming 有効",
idle: "Dreaming 待機中",

View File

@@ -349,6 +349,15 @@ export const ko: TranslationMap = {
on: "드리밍 켜짐",
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…",
failed: "Could not apply change. Check your connection and try again.",
},
status: {
active: "드리밍 활성",
idle: "드리밍 유휴",

View File

@@ -352,6 +352,15 @@ export const pl: TranslationMap = {
on: "Dreaming włączone",
off: "Dreaming wyłączone",
},
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…",
failed: "Could not apply change. Check your connection and try again.",
},
status: {
active: "Dreaming aktywne",
idle: "Dreaming bezczynne",

View File

@@ -350,6 +350,15 @@ export const pt_BR: TranslationMap = {
on: "Dreaming ativado",
off: "Dreaming desativado",
},
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…",
failed: "Could not apply change. Check your connection and try again.",
},
status: {
active: "Dreaming ativo",
idle: "Dreaming inativo",

View File

@@ -343,6 +343,15 @@ export const th: TranslationMap = {
on: "เปิดการฝัน",
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…",
failed: "Could not apply change. Check your connection and try again.",
},
status: {
active: "การฝันกำลังทำงาน",
idle: "การฝันไม่ได้ทำงาน",

View File

@@ -355,6 +355,15 @@ export const tr: TranslationMap = {
on: "Dreaming Açık",
off: "Dreaming Kapalı",
},
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…",
failed: "Could not apply change. Check your connection and try again.",
},
status: {
active: "Dreaming Etkin",
idle: "Dreaming Boşta",

View File

@@ -353,6 +353,15 @@ export const uk: TranslationMap = {
on: "Сновидіння увімкнено",
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…",
failed: "Could not apply change. Check your connection and try again.",
},
status: {
active: "Сновидіння активне",
idle: "Сновидіння неактивне",

View File

@@ -343,6 +343,15 @@ export const zh_CN: TranslationMap = {
on: "Dreaming 已开启",
off: "Dreaming 已关闭",
},
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…",
failed: "Could not apply change. Check your connection and try again.",
},
status: {
active: "Dreaming 运行中",
idle: "Dreaming 空闲",

View File

@@ -343,6 +343,15 @@ export const zh_TW: TranslationMap = {
on: "Dreaming 已開啟",
off: "Dreaming 已關閉",
},
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…",
failed: "Could not apply change. Check your connection and try again.",
},
status: {
active: "Dreaming 進行中",
idle: "Dreaming 閒置中",

View File

@@ -143,6 +143,7 @@ import {
createDefaultDraft,
draftToCronFormPatch,
} from "./views/cron-quick-create.ts";
import { renderDreamingRestartConfirmation } from "./views/dreaming-restart-confirmation.ts";
import { renderDreaming } from "./views/dreaming.ts";
import { renderExecApprovalPrompt } from "./views/exec-approval.ts";
import { renderGatewayUrlConfirmation } from "./views/gateway-url-confirmation.ts";
@@ -738,16 +739,49 @@ export function renderApp(state: AppViewState) {
};
};
const applyDreamingEnabled = (enabled: boolean) => {
if (state.dreamingModeSaving || dreamingOn === enabled) {
if (
state.dreamingModeSaving ||
state.dreamingRestartConfirmLoading ||
state.dreamingRestartConfirmOpen ||
dreamingOn === enabled
) {
return;
}
state.dreamingPendingEnabled = enabled;
state.dreamingRestartConfirmOpen = true;
state.dreamingStatusError = null;
};
const cancelDreamingRestart = () => {
if (state.dreamingRestartConfirmLoading) {
return;
}
state.dreamingRestartConfirmOpen = false;
state.dreamingPendingEnabled = null;
state.dreamingStatusError = null;
};
const confirmDreamingRestart = () => {
const enabled = state.dreamingPendingEnabled;
if (enabled == null || state.dreamingRestartConfirmLoading) {
return;
}
void (async () => {
const updated = await updateDreamingEnabled(state, enabled);
if (!updated) {
return;
state.dreamingRestartConfirmLoading = true;
state.dreamingStatusError = null;
try {
const updated = await updateDreamingEnabled(state, enabled);
if (!updated) {
if (!state.dreamingStatusError) {
state.dreamingStatusError = t("dreaming.restartConfirmation.failed");
}
return;
}
await loadConfig(state);
await loadDreamingStatus(state);
state.dreamingRestartConfirmOpen = false;
state.dreamingPendingEnabled = null;
} finally {
state.dreamingRestartConfirmLoading = false;
}
await loadConfig(state);
await loadDreamingStatus(state);
})();
};
const basePath = normalizeBasePath(state.basePath ?? "");
@@ -2491,7 +2525,15 @@ export function renderApp(state: AppViewState) {
})
: nothing}
</main>
${renderExecApprovalPrompt(state)} ${renderGatewayUrlConfirmation(state)} ${nothing}
${renderExecApprovalPrompt(state)} ${renderGatewayUrlConfirmation(state)}
${renderDreamingRestartConfirmation({
open: state.dreamingRestartConfirmOpen,
loading: state.dreamingRestartConfirmLoading,
onConfirm: confirmDreamingRestart,
onCancel: cancelDreamingRestart,
hasError: Boolean(state.dreamingStatusError),
})}
${nothing}
</div>
`;
}

View File

@@ -155,6 +155,9 @@ export type AppViewState = {
dreamingStatusError: string | null;
dreamingStatus: import("./controllers/dreaming.js").DreamingStatus | null;
dreamingModeSaving: boolean;
dreamingRestartConfirmOpen: boolean;
dreamingRestartConfirmLoading: boolean;
dreamingPendingEnabled: boolean | null;
dreamDiaryLoading: boolean;
dreamDiaryActionLoading: boolean;
dreamDiaryActionMessage: { kind: "success" | "error"; text: string } | null;

View File

@@ -262,6 +262,9 @@ export class OpenClawApp extends LitElement {
@state() dreamingStatusError: string | null = null;
@state() dreamingStatus: DreamingStatus | null = null;
@state() dreamingModeSaving = false;
@state() dreamingRestartConfirmOpen = false;
@state() dreamingRestartConfirmLoading = false;
@state() dreamingPendingEnabled: boolean | null = null;
@state() dreamDiaryLoading = false;
@state() dreamDiaryActionLoading = false;
@state() dreamDiaryActionMessage: { kind: "success" | "error"; text: string } | null = null;

View File

@@ -46,6 +46,257 @@ describe("control UI routing", () => {
const dreamsLink = app.querySelector<HTMLAnchorElement>('a.nav-item[href="/dreaming"]');
expect(dreamsLink).not.toBeNull();
});
it("renders the dreaming view on the /dreaming route", async () => {
const app = mountApp("/dreaming");
app.dreamingStatus = {
enabled: true,
timezone: "Europe/Madrid",
verboseLogging: false,
storageMode: "inline",
separateReports: false,
shortTermCount: 2,
recallSignalCount: 1,
dailySignalCount: 1,
groundedSignalCount: 0,
totalSignalCount: 2,
phaseSignalCount: 0,
lightPhaseHitCount: 0,
remPhaseHitCount: 0,
promotedTotal: 1,
promotedToday: 1,
shortTermEntries: [],
signalEntries: [],
promotedEntries: [],
phases: {
light: { enabled: true, cron: "", managedCronPresent: false, lookbackDays: 7, limit: 20 },
deep: {
enabled: true,
cron: "",
managedCronPresent: false,
limit: 20,
minScore: 0.75,
minRecallCount: 3,
minUniqueQueries: 2,
recencyHalfLifeDays: 7,
},
rem: {
enabled: true,
cron: "",
managedCronPresent: false,
lookbackDays: 7,
limit: 20,
minPatternStrength: 0.6,
},
},
};
app.dreamDiaryPath = "DREAMS.md";
app.dreamDiaryContent = [
"# Dream Diary",
"",
"<!-- openclaw:dreaming:diary:start -->",
"",
"---",
"",
"*January 1, 2026*",
"",
"What Happened",
"1. Stable operator rule surfaced.",
"",
"<!-- openclaw:dreaming:diary:end -->",
].join("\n");
app.requestUpdate();
await app.updateComplete;
expect(app.tab).toBe("dreams");
expect(app.querySelector(".dreams__tab")).not.toBeNull();
expect(app.querySelector(".dreams__lobster")).not.toBeNull();
});
it("requires confirmation before sending dreaming restart patch", async () => {
const app = mountApp("/dreaming");
const request = vi.fn(async (method: string) => {
if (method === "config.schema.lookup") {
return {
schema: {
additionalProperties: true,
},
children: [{ key: "dreaming" }],
};
}
if (method === "config.patch") {
return { ok: true };
}
if (method === "config.get") {
return {
hash: "hash-2",
config: {
plugins: {
slots: {
memory: "memory-core",
},
entries: {
"memory-core": {
config: {
dreaming: {
enabled: true,
},
},
},
},
},
},
};
}
if (method === "doctor.memory.status") {
return {
dreaming: {
enabled: true,
timezone: "UTC",
verboseLogging: false,
storageMode: "inline",
separateReports: false,
shortTermCount: 0,
recallSignalCount: 0,
dailySignalCount: 0,
groundedSignalCount: 0,
totalSignalCount: 0,
phaseSignalCount: 0,
lightPhaseHitCount: 0,
remPhaseHitCount: 0,
promotedTotal: 0,
promotedToday: 0,
shortTermEntries: [],
signalEntries: [],
promotedEntries: [],
phases: {
light: {
enabled: true,
cron: "",
managedCronPresent: false,
lookbackDays: 7,
limit: 20,
},
deep: {
enabled: true,
cron: "",
managedCronPresent: false,
limit: 20,
minScore: 0.75,
minRecallCount: 3,
minUniqueQueries: 2,
recencyHalfLifeDays: 7,
},
rem: {
enabled: true,
cron: "",
managedCronPresent: false,
lookbackDays: 7,
limit: 20,
minPatternStrength: 0.6,
},
},
},
};
}
return {};
});
app.client = {
request,
stop: vi.fn(),
} as unknown as NonNullable<typeof app.client>;
app.connected = true;
app.configSnapshot = {
hash: "hash-1",
config: {
plugins: {
slots: {
memory: "memory-core",
},
entries: {
"memory-core": {
config: {
dreaming: {
enabled: true,
},
},
},
},
},
},
};
app.dreamingStatus = {
enabled: true,
timezone: "UTC",
verboseLogging: false,
storageMode: "inline",
separateReports: false,
shortTermCount: 0,
recallSignalCount: 0,
dailySignalCount: 0,
groundedSignalCount: 0,
totalSignalCount: 0,
phaseSignalCount: 0,
lightPhaseHitCount: 0,
remPhaseHitCount: 0,
promotedTotal: 0,
promotedToday: 0,
shortTermEntries: [],
signalEntries: [],
promotedEntries: [],
phases: {
light: { enabled: true, cron: "", managedCronPresent: false, lookbackDays: 7, limit: 20 },
deep: {
enabled: true,
cron: "",
managedCronPresent: false,
limit: 20,
minScore: 0.75,
minRecallCount: 3,
minUniqueQueries: 2,
recencyHalfLifeDays: 7,
},
rem: {
enabled: true,
cron: "",
managedCronPresent: false,
lookbackDays: 7,
limit: 20,
minPatternStrength: 0.6,
},
},
};
app.requestUpdate();
await app.updateComplete;
const toggle = app.querySelector<HTMLButtonElement>(".dreams__phase-toggle--on");
expect(toggle).not.toBeNull();
toggle?.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
await app.updateComplete;
expect(request).not.toHaveBeenCalledWith("config.patch", expect.anything());
const confirmRestart = Array.from(app.querySelectorAll<HTMLButtonElement>("button")).find(
(button) => button.textContent?.trim() === "Confirm Restart",
);
expect(confirmRestart).not.toBeUndefined();
confirmRestart?.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
await nextFrame();
await app.updateComplete;
expect(request).toHaveBeenCalledWith(
"config.patch",
expect.objectContaining({
baseHash: "hash-1",
}),
);
});
it("renders the refreshed top navigation shell", async () => {
const app = mountApp("/chat");
await app.updateComplete;
expect(app.querySelector(".topnav-shell")).not.toBeNull();
expect(app.querySelector(".topnav-shell__content")).not.toBeNull();

View File

@@ -0,0 +1,45 @@
import { html, nothing } from "lit";
import { t } from "../../i18n/index.ts";
type DreamingRestartConfirmationProps = {
open: boolean;
loading: boolean;
onConfirm: () => void;
onCancel: () => void;
hasError: boolean;
};
export function renderDreamingRestartConfirmation(props: DreamingRestartConfirmationProps) {
if (!props.open) {
return nothing;
}
return html`
<div class="exec-approval-overlay" role="dialog" aria-modal="true" aria-live="polite">
<div class="exec-approval-card">
<div class="exec-approval-header">
<div>
<div class="exec-approval-title">${t("dreaming.restartConfirmation.title")}</div>
<div class="exec-approval-sub">${t("dreaming.restartConfirmation.subtitle")}</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>
</div>
`;
}