Fix dreaming replay, repair polluted artifacts, and gate wiki tabs (#65138)

* fix(active-memory): preserve parent channel context for recall runs

* fix(active-memory): keep recall runs on the resolved channel

* fix(active-memory): prefer resolved recall channel over wrapper hints

* fix(active-memory): trust explicit recall channel hints

* fix(active-memory): rank recall channel fallbacks by trust

* Fix dreaming replay and recovery flows

* fix: prevent dreaming event loss and diary write races

* chore: add changelog entry for memory fixes

* fix: harden dreaming repair and diary writes

* fix: harden dreaming artifact archive naming
This commit is contained in:
Tak Hoffman
2026-04-12 00:25:11 -05:00
committed by GitHub
parent 5543925cd2
commit 847739d82c
45 changed files with 2016 additions and 148 deletions

View File

@@ -293,8 +293,10 @@ export const en: TranslationMap = {
},
scene: {
backfill: "Backfill",
dedupeDiary: "Dedupe Diary",
reset: "Reset",
clearGrounded: "Clear Replayed",
repairCache: "Repair Dream Cache",
working: "Working…",
},
phase: {

View File

@@ -73,10 +73,13 @@ import {
} from "./controllers/devices.ts";
import {
backfillDreamDiary,
copyDreamingArchivePath,
dedupeDreamDiary,
loadDreamDiary,
loadDreamingStatus,
loadWikiImportInsights,
loadWikiMemoryPalace,
repairDreamingArtifacts,
resetGroundedShortTerm,
resetDreamDiary,
resolveConfiguredDreaming,
@@ -233,6 +236,32 @@ function uniquePreserveOrder(values: string[]): string[] {
return output;
}
function isPluginExplicitlyEnabled(
configSnapshot: AppViewState["configSnapshot"],
pluginId: string,
): boolean {
const config = configSnapshot?.config;
if (!config || typeof config !== "object" || Array.isArray(config)) {
return true;
}
const plugins =
"plugins" in config && config.plugins && typeof config.plugins === "object"
? (config.plugins as Record<string, unknown>)
: null;
if (plugins?.enabled === false) {
return false;
}
const entries =
plugins && "entries" in plugins && plugins.entries && typeof plugins.entries === "object"
? (plugins.entries as Record<string, unknown>)
: null;
const entry = entries?.[pluginId];
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
return true;
}
return (entry as { enabled?: unknown }).enabled !== false;
}
type DismissedUpdateBanner = {
latestVersion: string;
channel: string | null;
@@ -1985,9 +2014,12 @@ export function renderApp(state: AppViewState) {
modeSaving: state.dreamingModeSaving,
dreamDiaryLoading: state.dreamDiaryLoading,
dreamDiaryActionLoading: state.dreamDiaryActionLoading,
dreamDiaryActionMessage: state.dreamDiaryActionMessage,
dreamDiaryActionArchivePath: state.dreamDiaryActionArchivePath,
dreamDiaryError: state.dreamDiaryError,
dreamDiaryPath: state.dreamDiaryPath,
dreamDiaryContent: state.dreamDiaryContent,
memoryWikiEnabled: isPluginExplicitlyEnabled(state.configSnapshot, "memory-wiki"),
wikiImportInsightsLoading: state.wikiImportInsightsLoading,
wikiImportInsightsError: state.wikiImportInsightsError,
wikiImportInsights: state.wikiImportInsights,
@@ -1998,10 +2030,16 @@ export function renderApp(state: AppViewState) {
onRefreshDiary: () => loadDreamDiary(state),
onRefreshImports: () => loadWikiImportInsights(state),
onRefreshMemoryPalace: () => loadWikiMemoryPalace(state),
onOpenConfig: () => openConfigFile(state),
onOpenWikiPage: (lookup: string) => openWikiPage(lookup),
onBackfillDiary: () => backfillDreamDiary(state),
onCopyDreamingArchivePath: () => {
void copyDreamingArchivePath(state);
},
onDedupeDreamDiary: () => dedupeDreamDiary(state),
onResetDiary: () => resetDreamDiary(state),
onResetGroundedShortTerm: () => resetGroundedShortTerm(state),
onRepairDreamingArtifacts: () => repairDreamingArtifacts(state),
onRequestUpdate: requestHostUpdate,
})
: nothing}

View File

@@ -70,6 +70,8 @@ type SettingsHost = {
dreamingModeSaving: boolean;
dreamDiaryLoading: boolean;
dreamDiaryActionLoading: boolean;
dreamDiaryActionMessage: { kind: "success" | "error"; text: string } | null;
dreamDiaryActionArchivePath: string | null;
dreamDiaryError: string | null;
dreamDiaryPath: string | null;
dreamDiaryContent: string | null;
@@ -165,6 +167,8 @@ const createHost = (tab: Tab): SettingsHost => ({
dreamingModeSaving: false,
dreamDiaryLoading: false,
dreamDiaryActionLoading: false,
dreamDiaryActionMessage: null,
dreamDiaryActionArchivePath: null,
dreamDiaryError: null,
dreamDiaryPath: null,
dreamDiaryContent: null,

View File

@@ -136,6 +136,8 @@ export type AppViewState = {
dreamingModeSaving: boolean;
dreamDiaryLoading: boolean;
dreamDiaryActionLoading: boolean;
dreamDiaryActionMessage: { kind: "success" | "error"; text: string } | null;
dreamDiaryActionArchivePath: string | null;
dreamDiaryError: string | null;
dreamDiaryPath: string | null;
dreamDiaryContent: string | null;

View File

@@ -238,6 +238,8 @@ export class OpenClawApp extends LitElement {
@state() dreamingModeSaving = false;
@state() dreamDiaryLoading = false;
@state() dreamDiaryActionLoading = false;
@state() dreamDiaryActionMessage: { kind: "success" | "error"; text: string } | null = null;
@state() dreamDiaryActionArchivePath: string | null = null;
@state() dreamDiaryError: string | null = null;
@state() dreamDiaryPath: string | null = null;
@state() dreamDiaryContent: string | null = null;

View File

@@ -1,10 +1,13 @@
import { describe, expect, it, vi } from "vitest";
import {
backfillDreamDiary,
copyDreamingArchivePath,
dedupeDreamDiary,
loadDreamDiary,
loadDreamingStatus,
loadWikiImportInsights,
loadWikiMemoryPalace,
repairDreamingArtifacts,
resetGroundedShortTerm,
resetDreamDiary,
resolveConfiguredDreaming,
@@ -27,6 +30,8 @@ function createState(): { state: DreamingState; request: ReturnType<typeof vi.fn
dreamingModeSaving: false,
dreamDiaryLoading: false,
dreamDiaryActionLoading: false,
dreamDiaryActionMessage: null,
dreamDiaryActionArchivePath: null,
dreamDiaryError: null,
dreamDiaryPath: null,
dreamDiaryContent: null,
@@ -683,4 +688,108 @@ describe("dreaming controller", () => {
expect(state.dreamDiaryContent).toBe("keep existing diary");
expect(state.dreamDiaryActionLoading).toBe(false);
});
it("repairs dreaming artifacts and reloads only dreaming status", async () => {
const { state, request } = createState();
state.dreamDiaryContent = "keep existing diary";
const confirmSpy = vi.spyOn(globalThis, "confirm").mockReturnValue(true);
request.mockImplementation(async (method: string) => {
if (method === "doctor.memory.repairDreamingArtifacts") {
return {
action: "repairDreamingArtifacts",
changed: true,
archiveDir: "/tmp/openclaw/.openclaw-repair/dreaming/2026-04-11T22-10-00-000Z",
archivedSessionCorpus: true,
archivedSessionIngestion: true,
};
}
if (method === "doctor.memory.status") {
return { dreaming: null };
}
return {};
});
const ok = await repairDreamingArtifacts(state);
expect(ok).toBe(true);
expect(confirmSpy).toHaveBeenCalled();
expect(request).toHaveBeenCalledWith("doctor.memory.repairDreamingArtifacts", {});
expect(request).toHaveBeenCalledWith("doctor.memory.status", {});
expect(request).not.toHaveBeenCalledWith("doctor.memory.dreamDiary", {});
expect(state.dreamDiaryContent).toBe("keep existing diary");
expect(state.dreamDiaryActionMessage).toEqual({
kind: "success",
text: "Dream cache repair complete: archived session corpus, archived ingestion state. Archive: /tmp/openclaw/.openclaw-repair/dreaming/2026-04-11T22-10-00-000Z",
});
expect(state.dreamDiaryActionArchivePath).toBe(
"/tmp/openclaw/.openclaw-repair/dreaming/2026-04-11T22-10-00-000Z",
);
expect(state.dreamDiaryActionLoading).toBe(false);
});
it("dedupes dream diary entries and reloads diary plus status", async () => {
const { state, request } = createState();
const confirmSpy = vi.spyOn(globalThis, "confirm").mockReturnValue(true);
request.mockImplementation(async (method: string) => {
if (method === "doctor.memory.dedupeDreamDiary") {
return {
action: "dedupeDreamDiary",
removedEntries: 2,
keptEntries: 5,
};
}
if (method === "doctor.memory.dreamDiary") {
return { found: true, path: "DREAMS.md", content: "deduped diary" };
}
if (method === "doctor.memory.status") {
return { dreaming: null };
}
return {};
});
const ok = await dedupeDreamDiary(state);
expect(ok).toBe(true);
expect(confirmSpy).toHaveBeenCalled();
expect(request).toHaveBeenCalledWith("doctor.memory.dedupeDreamDiary", {});
expect(request).toHaveBeenCalledWith("doctor.memory.dreamDiary", {});
expect(request).toHaveBeenCalledWith("doctor.memory.status", {});
expect(state.dreamDiaryContent).toBe("deduped diary");
expect(state.dreamDiaryActionMessage).toEqual({
kind: "success",
text: "Removed 2 duplicate dream entries and kept 5.",
});
expect(state.dreamDiaryActionArchivePath).toBeNull();
expect(state.dreamDiaryActionLoading).toBe(false);
});
it("copies the dreaming repair archive path", async () => {
const { state } = createState();
state.dreamDiaryActionArchivePath =
"/tmp/openclaw/.openclaw-repair/dreaming/2026-04-11T22-10-00-000Z";
const writeText = vi.fn().mockResolvedValue(undefined);
vi.stubGlobal("navigator", { clipboard: { writeText } } as unknown as Navigator);
const ok = await copyDreamingArchivePath(state);
expect(ok).toBe(true);
expect(writeText).toHaveBeenCalledWith(
"/tmp/openclaw/.openclaw-repair/dreaming/2026-04-11T22-10-00-000Z",
);
expect(state.dreamDiaryActionMessage).toEqual({
kind: "success",
text: "Archive path copied.",
});
});
it("does not run repair when confirmation is cancelled", async () => {
const { state, request } = createState();
vi.spyOn(globalThis, "confirm").mockReturnValue(false);
const ok = await repairDreamingArtifacts(state);
expect(ok).toBe(false);
expect(request).not.toHaveBeenCalled();
expect(state.dreamDiaryActionMessage).toBeNull();
});
});

View File

@@ -168,9 +168,17 @@ type DoctorMemoryDreamDiaryPayload = {
type DoctorMemoryDreamActionPayload = {
action?: unknown;
removedEntries?: unknown;
dedupedEntries?: unknown;
keptEntries?: unknown;
written?: unknown;
replaced?: unknown;
removedShortTermEntries?: unknown;
changed?: unknown;
archiveDir?: unknown;
archivedSessionCorpus?: unknown;
archivedSessionIngestion?: unknown;
archivedDreamsDiary?: unknown;
warnings?: unknown;
};
type WikiImportInsightsPayload = {
@@ -199,6 +207,8 @@ export type DreamingState = {
dreamingModeSaving: boolean;
dreamDiaryLoading: boolean;
dreamDiaryActionLoading: boolean;
dreamDiaryActionMessage: { kind: "success" | "error"; text: string } | null;
dreamDiaryActionArchivePath: string | null;
dreamDiaryError: string | null;
dreamDiaryPath: string | null;
dreamDiaryContent: string | null;
@@ -211,6 +221,64 @@ export type DreamingState = {
lastError: string | null;
};
function confirmDreamingAction(message: string): boolean {
if (typeof globalThis.confirm !== "function") {
return true;
}
return globalThis.confirm(message);
}
function buildDreamDiaryActionSuccessMessage(
method:
| "doctor.memory.backfillDreamDiary"
| "doctor.memory.resetDreamDiary"
| "doctor.memory.resetGroundedShortTerm"
| "doctor.memory.repairDreamingArtifacts"
| "doctor.memory.dedupeDreamDiary",
payload: DoctorMemoryDreamActionPayload | undefined,
): string {
switch (method) {
case "doctor.memory.dedupeDreamDiary": {
const removed =
typeof payload?.dedupedEntries === "number"
? payload.dedupedEntries
: typeof payload?.removedEntries === "number"
? payload.removedEntries
: 0;
const kept = typeof payload?.keptEntries === "number" ? payload.keptEntries : undefined;
return kept !== undefined
? `Removed ${removed} duplicate dream ${removed === 1 ? "entry" : "entries"} and kept ${kept}.`
: `Removed ${removed} duplicate dream ${removed === 1 ? "entry" : "entries"}.`;
}
case "doctor.memory.repairDreamingArtifacts": {
const actions: string[] = [];
const archiveDir = normalizeTrimmedString(payload?.archiveDir);
if (payload?.archivedSessionCorpus === true) {
actions.push("archived session corpus");
}
if (payload?.archivedSessionIngestion === true) {
actions.push("archived ingestion state");
}
if (payload?.archivedDreamsDiary === true) {
actions.push("archived dream diary");
}
if (actions.length === 0) {
return "Dream cache repair finished with no changes.";
}
return archiveDir
? `Dream cache repair complete: ${actions.join(", ")}. Archive: ${archiveDir}`
: `Dream cache repair complete: ${actions.join(", ")}.`;
}
case "doctor.memory.backfillDreamDiary":
return `Backfilled ${typeof payload?.written === "number" ? payload.written : 0} dream diary entries.`;
case "doctor.memory.resetDreamDiary":
return `Removed ${typeof payload?.removedEntries === "number" ? payload.removedEntries : 0} backfilled dream diary entries.`;
case "doctor.memory.resetGroundedShortTerm":
return `Cleared ${typeof payload?.removedShortTermEntries === "number" ? payload.removedShortTermEntries : 0} replayed short-term entries.`;
}
return "Dream diary action complete.";
}
function asRecord(value: unknown): Record<string, unknown> | null {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
@@ -708,7 +776,9 @@ async function runDreamDiaryAction(
method:
| "doctor.memory.backfillDreamDiary"
| "doctor.memory.resetDreamDiary"
| "doctor.memory.resetGroundedShortTerm",
| "doctor.memory.resetGroundedShortTerm"
| "doctor.memory.repairDreamingArtifacts"
| "doctor.memory.dedupeDreamDiary",
options?: {
reloadDiary?: boolean;
},
@@ -716,20 +786,48 @@ async function runDreamDiaryAction(
if (!state.client || !state.connected || state.dreamDiaryActionLoading) {
return false;
}
if (
method === "doctor.memory.repairDreamingArtifacts" &&
!confirmDreamingAction(
"Repair Dream Cache? This archives derived dream cache files and rebuilds them from clean inputs. Your dream diary stays untouched.",
)
) {
return false;
}
if (
method === "doctor.memory.dedupeDreamDiary" &&
!confirmDreamingAction(
"Dedupe Dream Diary? This rewrites DREAMS.md and removes only exact duplicate diary entries.",
)
) {
return false;
}
state.dreamDiaryActionLoading = true;
state.dreamingStatusError = null;
state.dreamDiaryError = null;
state.dreamDiaryActionMessage = null;
state.dreamDiaryActionArchivePath = null;
try {
await state.client.request<DoctorMemoryDreamActionPayload>(method, {});
const payload = await state.client.request<DoctorMemoryDreamActionPayload>(method, {});
if (options?.reloadDiary !== false) {
await loadDreamDiary(state);
}
await loadDreamingStatus(state);
state.dreamDiaryActionArchivePath =
method === "doctor.memory.repairDreamingArtifacts"
? (normalizeTrimmedString(payload?.archiveDir) ?? null)
: null;
state.dreamDiaryActionMessage = {
kind: "success",
text: buildDreamDiaryActionSuccessMessage(method, payload),
};
return true;
} catch (err) {
const message = String(err);
state.dreamingStatusError = message;
state.lastError = message;
state.dreamDiaryActionArchivePath = null;
state.dreamDiaryActionMessage = { kind: "error", text: message };
return false;
} finally {
state.dreamDiaryActionLoading = false;
@@ -750,6 +848,44 @@ export async function resetGroundedShortTerm(state: DreamingState): Promise<bool
});
}
export async function repairDreamingArtifacts(state: DreamingState): Promise<boolean> {
return runDreamDiaryAction(state, "doctor.memory.repairDreamingArtifacts", {
reloadDiary: false,
});
}
export async function copyDreamingArchivePath(state: DreamingState): Promise<boolean> {
const path = state.dreamDiaryActionArchivePath;
if (!path) {
return false;
}
if (!globalThis.navigator?.clipboard?.writeText) {
state.dreamDiaryActionMessage = {
kind: "error",
text: "Could not copy archive path.",
};
return false;
}
try {
await globalThis.navigator.clipboard.writeText(path);
state.dreamDiaryActionMessage = {
kind: "success",
text: "Archive path copied.",
};
return true;
} catch {
state.dreamDiaryActionMessage = {
kind: "error",
text: "Could not copy archive path.",
};
return false;
}
}
export async function dedupeDreamDiary(state: DreamingState): Promise<boolean> {
return runDreamDiaryAction(state, "doctor.memory.dedupeDreamDiary");
}
async function writeDreamingPatch(
state: DreamingState,
patch: Record<string, unknown>,

View File

@@ -63,10 +63,13 @@ function buildProps(overrides?: Partial<DreamingProps>): DreamingProps {
modeSaving: false,
dreamDiaryLoading: false,
dreamDiaryActionLoading: false,
dreamDiaryActionMessage: null,
dreamDiaryActionArchivePath: null,
dreamDiaryError: null,
dreamDiaryPath: "DREAMS.md",
dreamDiaryContent:
"# Dream Diary\n\n<!-- openclaw:dreaming:diary:start -->\n\n---\n\n*April 5, 2026, 3:00 AM*\n\nThe repository whispered of forgotten endpoints tonight.\n\n<!-- openclaw:dreaming:diary:end -->",
memoryWikiEnabled: true,
wikiImportInsightsLoading: false,
wikiImportInsightsError: null,
wikiImportInsights: {
@@ -176,10 +179,14 @@ function buildProps(overrides?: Partial<DreamingProps>): DreamingProps {
onRefreshDiary: () => {},
onRefreshImports: () => {},
onRefreshMemoryPalace: () => {},
onOpenConfig: () => {},
onOpenWikiPage: async () => null,
onBackfillDiary: () => {},
onCopyDreamingArchivePath: () => {},
onDedupeDreamDiary: () => {},
onResetDiary: () => {},
onResetGroundedShortTerm: () => {},
onRepairDreamingArtifacts: () => {},
...overrides,
};
}
@@ -387,6 +394,27 @@ describe("dreaming view", () => {
setDreamSubTab("scene");
});
it("shows a memory-wiki enablement CTA when wiki subtabs are selected but the plugin is disabled", () => {
setDreamSubTab("diary");
setDreamDiarySubTab("palace");
const onOpenConfig = vi.fn();
const container = renderInto(
buildProps({
memoryWikiEnabled: false,
onOpenConfig,
}),
);
expect(container.textContent).toContain("Memory Wiki is not enabled");
expect(container.textContent).toContain("plugins.entries.memory-wiki.enabled = true");
container
.querySelector<HTMLButtonElement>(".dreams-diary__empty-actions .btn")
?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(onOpenConfig).toHaveBeenCalledTimes(1);
setDreamDiarySubTab("dreams");
setDreamSubTab("scene");
});
it("renders dream diary with parsed entry on diary tab", () => {
setDreamSubTab("diary");
setDreamDiarySubTab("dreams");

View File

@@ -113,9 +113,12 @@ export type DreamingProps = {
modeSaving: boolean;
dreamDiaryLoading: boolean;
dreamDiaryActionLoading: boolean;
dreamDiaryActionMessage: { kind: "success" | "error"; text: string } | null;
dreamDiaryActionArchivePath: string | null;
dreamDiaryError: string | null;
dreamDiaryPath: string | null;
dreamDiaryContent: string | null;
memoryWikiEnabled: boolean;
wikiImportInsightsLoading: boolean;
wikiImportInsightsError: string | null;
wikiImportInsights: WikiImportInsights | null;
@@ -126,6 +129,7 @@ export type DreamingProps = {
onRefreshDiary: () => void;
onRefreshImports: () => void;
onRefreshMemoryPalace: () => void;
onOpenConfig: () => void;
onOpenWikiPage: (lookup: string) => Promise<{
title: string;
path: string;
@@ -135,8 +139,11 @@ export type DreamingProps = {
updatedAt?: string;
} | null>;
onBackfillDiary: () => void;
onCopyDreamingArchivePath: () => void;
onDedupeDreamDiary: () => void;
onResetDiary: () => void;
onResetGroundedShortTerm: () => void;
onRepairDreamingArtifacts: () => void;
onRequestUpdate?: () => void;
};
@@ -763,6 +770,20 @@ function renderAdvancedSection(props: DreamingProps) {
<div class="dreams-advanced__summary">${summary}</div>
</div>
<div class="dreams-advanced__actions">
<button
class="btn btn--subtle btn--sm"
?disabled=${props.modeSaving || props.dreamDiaryActionLoading}
@click=${() => props.onDedupeDreamDiary()}
>
${t("dreaming.scene.dedupeDiary")}
</button>
<button
class="btn btn--subtle btn--sm"
?disabled=${props.modeSaving || props.dreamDiaryActionLoading}
@click=${() => props.onRepairDreamingArtifacts()}
>
${t("dreaming.scene.repairCache")}
</button>
<button
class="btn btn--subtle btn--sm"
?disabled=${props.modeSaving || props.dreamDiaryActionLoading}
@@ -788,6 +809,31 @@ function renderAdvancedSection(props: DreamingProps) {
</button>
</div>
</div>
${props.dreamDiaryActionMessage
? html`
<div
class="callout ${props.dreamDiaryActionMessage.kind === "success"
? "success"
: "danger"}"
role="status"
>
<div class="row wrap items-center gap-2">
<span>${props.dreamDiaryActionMessage.text}</span>
${props.dreamDiaryActionArchivePath
? html`
<button
class="btn btn--subtle btn--sm"
?disabled=${props.dreamDiaryActionLoading}
@click=${() => props.onCopyDreamingArchivePath()}
>
Copy archive path
</button>
`
: nothing}
</div>
</div>
`
: nothing}
<div class="dreams-advanced__sections">
${renderAdvancedEntryList({
@@ -1294,13 +1340,15 @@ function renderDreamDiaryEntries(props: DreamingProps) {
// ── Diary section renderer ────────────────────────────────────────────
function renderDiarySection(props: DreamingProps) {
const wikiTabSelected = _diarySubTab === "insights" || _diarySubTab === "palace";
const memoryWikiUnavailable = wikiTabSelected && !props.memoryWikiEnabled;
const diaryError =
_diarySubTab === "dreams"
? props.dreamDiaryError
: _diarySubTab === "insights"
? props.wikiImportInsightsError
: props.wikiMemoryPalaceError;
if (diaryError) {
if (diaryError && !memoryWikiUnavailable) {
return html`
<section class="dreams-diary">
<div class="dreams-diary__error">${diaryError}</div>
@@ -1356,15 +1404,19 @@ function renderDiarySection(props: DreamingProps) {
</div>
<button
class="btn btn--subtle btn--sm"
?disabled=${props.modeSaving ||
(_diarySubTab === "dreams"
? props.dreamDiaryLoading
: _diarySubTab === "insights"
? props.wikiImportInsightsLoading
: props.wikiMemoryPalaceLoading)}
?disabled=${memoryWikiUnavailable
? false
: props.modeSaving ||
(_diarySubTab === "dreams"
? props.dreamDiaryLoading
: _diarySubTab === "insights"
? props.wikiImportInsightsLoading
: props.wikiMemoryPalaceLoading)}
@click=${() => {
_diaryPage = 0;
if (_diarySubTab === "dreams") {
if (memoryWikiUnavailable) {
props.onOpenConfig();
} else if (_diarySubTab === "dreams") {
props.onRefreshDiary();
} else if (_diarySubTab === "insights") {
props.onRefreshImports();
@@ -1373,27 +1425,48 @@ function renderDiarySection(props: DreamingProps) {
}
}}
>
${_diarySubTab === "dreams"
? props.dreamDiaryLoading
? t("dreaming.diary.reloading")
: t("dreaming.diary.reload")
: _diarySubTab === "insights"
? props.wikiImportInsightsLoading
? "Reloading…"
: "Reload"
: props.wikiMemoryPalaceLoading
? "Reloading…"
: "Reload"}
${memoryWikiUnavailable
? "How to enable"
: _diarySubTab === "dreams"
? props.dreamDiaryLoading
? t("dreaming.diary.reloading")
: t("dreaming.diary.reload")
: _diarySubTab === "insights"
? props.wikiImportInsightsLoading
? "Reloading…"
: "Reload"
: props.wikiMemoryPalaceLoading
? "Reloading…"
: "Reload"}
</button>
</div>
${renderDiarySubtabExplainer()}
</div>
${_diarySubTab === "dreams"
? renderDreamDiaryEntries(props)
: _diarySubTab === "insights"
? renderDiaryImportsSection(props)
: renderMemoryPalaceSection(props)}
${memoryWikiUnavailable
? html`
<div class="dreams-diary__empty">
<div class="dreams-diary__empty-text">Memory Wiki is not enabled</div>
<div class="dreams-diary__empty-hint">
Imported Insights and Memory Palace are provided by the bundled
<code>memory-wiki</code> plugin.
</div>
<div class="dreams-diary__empty-hint">
Enable <code>plugins.entries.memory-wiki.enabled = true</code>, then reload this
tab.
</div>
<div class="dreams-diary__empty-actions">
<button class="btn btn--subtle btn--sm" @click=${() => props.onOpenConfig()}>
Open Config
</button>
</div>
</div>
`
: _diarySubTab === "dreams"
? renderDreamDiaryEntries(props)
: _diarySubTab === "insights"
? renderDiaryImportsSection(props)
: renderMemoryPalaceSection(props)}
${renderWikiPreviewOverlay(props)}
</section>
`;