fix(ui): rename the Memory Palace browser to Memory Wiki and document it (#115954)

* fix(ui): rename the Memory Palace browser to Memory Wiki and document it

* refactor(memory-wiki): drop the old memory-palace module files

* docs: refresh generated docs map

* test(ui): align dream-diary hub-tab assertion with the wiki sub-tab id

* docs: describe memory wiki clustering behavior accurately

* docs(memory-wiki): record the wiki.overview rename decision at the registration site

* test(ui): query the Agents channels hub tab by tab role after the hub-tabs refactor
This commit is contained in:
Peter Steinberger
2026-07-29 12:51:37 -04:00
committed by GitHub
parent a4aaa1cd74
commit c87e545c88
18 changed files with 282 additions and 252 deletions

View File

@@ -283,6 +283,19 @@ When enabled, the Gateway **Dreams** tab shows:
- a distinct grounded Scene lane for staged historical replay entries
- an expandable Dream Diary reader backed by `doctor.memory.dreamDiary`
When the bundled [`memory-wiki`](/plugins/memory-wiki) plugin is enabled, the
Diary view gains two more sub-tabs next to Dreams:
- **Imported Insights**: clustered insights surfaced by external-history
imports (for example `openclaw wiki chatgpt import`), for review before any
of it graduates into durable memory
- **Memory Wiki**: the compiled wiki the memory system can search and reason
over — synthesis, entity, and concept pages (plus sources and reports that
carry claims, open questions, or contradictions) with per-page counts, a
full-vault breakdown, and inline page previews
Both sub-tabs show an enable hint instead when `memory-wiki` is off.
## Related
- [Memory](/concepts/memory)

View File

@@ -216,7 +216,9 @@ dashboards, compiled digests, and wiki-native tools (`wiki_status`,
`memory-wiki` does not replace the active memory plugin; the active memory
plugin still owns recall, promotion, and dreaming. `memory-wiki` adds a
provenance-rich knowledge layer beside it.
provenance-rich knowledge layer beside it. You can browse the compiled wiki
in the Control UI under Memory → Dreams → Diary → **Memory Wiki**
([details](/plugins/memory-wiki#browsing-the-wiki-in-the-control-ui)).
<CardGroup cols={1}>
<Card title="Memory Wiki" icon="book" href="/plugins/memory-wiki">

View File

@@ -6219,6 +6219,7 @@ Do not edit it by hand; run `pnpm docs:map:gen`.
- H2: Dashboards and health reports
- H2: Search and retrieval
- H2: Agent tools
- H2: Browsing the wiki in the Control UI
- H2: Prompt and context behavior
- H2: Configuration
- H3: Per-agent vaults

View File

@@ -283,6 +283,23 @@ The plugin also registers a non-exclusive memory corpus supplement, so shared
`memory_search` and `memory_get` can reach the wiki when the active memory
plugin supports corpus selection.
## Browsing the wiki in the Control UI
The [Control UI](/web/control-ui) can browse the compiled wiki directly: open
the Memory page, then **Dreams → Diary → Memory Wiki**. The tab clusters
synthesis, entity, and concept pages — plus source and report pages that
carry claims, open questions, or contradictions — with per-page counts and a
full-vault page breakdown, and opens full page content inline. Raw sources
and reports without that metadata count toward the breakdown but are not
listed as cards; open them from the **Imported Insights** sub-tab, which
reviews what external-history imports surfaced before promotion.
Both sub-tabs appear once the plugin is enabled; in agent-scoped vault setups
they show the selected agent's own vault. The UI reads through the plugin's
gateway methods (`wiki.overview`, `wiki.get`, `wiki.importInsights`); inline
page previews use `wiki.get`, the same lookup agents reach through the
`wiki_get` tool.
## Prompt and context behavior
When `context.includeCompiledDigestPrompt` is enabled, memory prompt sections

View File

@@ -266,7 +266,7 @@ select it to open the owning Approvals page.
- Threads (a workspace page at `/sessions`, with a **Worktrees** tab alongside it): list configured-agent sessions by default, pin frequent sessions, rename them, archive or restore inactive sessions, fall back from stale unconfigured agent session keys, and apply per-session model/thinking/fast/verbose/trace/reasoning overrides (`sessions.list`, `sessions.patch`). A three-way **Active / Archived / All** filter controls both this page and the sidebar; All dims archived rows and labels them explicitly. Archived sessions keep their transcripts, are never auto-pruned, and remain shelved until explicitly unarchived or deleted. Rows show an unread dot for active sessions with activity since they were last read, with mark-unread/mark-read actions (`sessions.patch { unread }`), and a Fork action that branches the transcript into a new session (`sessions.create { parentSessionKey, fork: true }`). Overview tiles above the table summarize the loaded roster (session count, live runs, unread sessions, total tokens, and archived count when available), each row carries a kind glyph with a live-run dot, status renders as a plain dot plus label, and the Tokens column shows a context-window usage meter when the session reports token and context sizes. Row management actions live in a per-row menu (kebab button or right-click) mirroring the sidebar's session menu, and the row drawer carries the agent runtime and run duration alongside the other session details.
- Native Claude and Codex sidebar catalogs stream one host at a time, then reconcile after node connectivity changes, on page focus, and at most every 30 seconds while visible. Catalog changes trigger a faster follow-up pass, so sessions created in the native tools appear without reloading the Control UI. Claude Desktop rows also retain their local custom-group label when present; OpenClaw reads that mapping from Desktop's local store and never writes it.
- Session grouping: a Group by control organizes the sessions table into sections by custom groups, channel, kind, agent, or date. Custom groups persist per session via `sessions.patch` (`category`), so sessions started from message channels (Discord, Telegram, WhatsApp, ...) can be categorized too; assign groups by dragging rows onto a section, or with the per-row group selector, and create groups with the New group action.
- Memory (a tab on the Agents page, scoped to the selected agent): dreaming status, enable/disable toggle, and Dream Diary reader (`doctor.memory.status`, `doctor.memory.dreamDiary`, `config.patch`).
- Memory (a tab on the Agents page, scoped to the selected agent): dreaming status, enable/disable toggle, and Dream Diary reader (`doctor.memory.status`, `doctor.memory.dreamDiary`, `config.patch`). When the `memory-wiki` plugin is enabled, the Diary view adds **Imported Insights** and **Memory Wiki** sub-tabs that browse imported source chats and the compiled wiki — clustered synthesis, entity, and concept pages plus annotated sources and reports, with claims, open questions, contradictions, and inline page previews (`wiki.importInsights`, `wiki.overview`, `wiki.get`).
- Import Memory (`/memory-import`, reached from the Agents page's Memory tab): preview and copy local Claude Code auto-memory, Codex consolidated memory, or Hermes memory files into the selected agent workspace (`migrations.memory.plan`, `migrations.memory.apply`).
- Onboarding memory offer: when the Control UI opens in [onboarding mode](/web/urls#special-documents-and-startup-modes), a one-page dialog offers to import detected memories with the same plan/apply flow; skipping leaves the settings page as the later entry point.

View File

@@ -65,7 +65,7 @@ describe("memory-wiki plugin", () => {
"wiki.status",
"wiki.importRuns",
"wiki.importInsights",
"wiki.palace",
"wiki.overview",
"wiki.init",
"wiki.doctor",
"wiki.compile",

View File

@@ -6,11 +6,11 @@ import { registerMemoryWikiGatewayMethods } from "./gateway.js";
import { listMemoryWikiImportInsights } from "./import-insights.js";
import { listMemoryWikiImportRuns } from "./import-runs.js";
import { ingestMemoryWikiSource } from "./ingest.js";
import { listMemoryWikiPalace } from "./memory-palace.js";
import { searchMemoryWiki } from "./query.js";
import { syncMemoryWikiImportedSources } from "./source-sync.js";
import { resolveMemoryWikiStatus } from "./status.js";
import { createMemoryWikiTestHarness } from "./test-helpers.js";
import { listMemoryWikiOverview } from "./wiki-overview.js";
type ApplyMemoryWikiMutation = ReturnType<typeof normalizeMemoryWikiMutationInput>;
@@ -39,8 +39,8 @@ vi.mock("./lint.js", () => ({
lintMemoryWikiVault: vi.fn(),
}));
vi.mock("./memory-palace.js", () => ({
listMemoryWikiPalace: vi.fn(),
vi.mock("./wiki-overview.js", () => ({
listMemoryWikiOverview: vi.fn(),
}));
vi.mock("./obsidian.js", () => ({
@@ -108,7 +108,7 @@ const VAULT_BACKED_GATEWAY_CASES = [
["wiki.status", {}],
["wiki.importRuns", {}],
["wiki.importInsights", {}],
["wiki.palace", {}],
["wiki.overview", {}],
["wiki.init", {}],
["wiki.doctor", {}],
["wiki.compile", {}],
@@ -164,7 +164,7 @@ describe("memory-wiki gateway methods", () => {
totalClusters: 0,
clusters: [],
} as never);
vi.mocked(listMemoryWikiPalace).mockResolvedValue({
vi.mocked(listMemoryWikiOverview).mockResolvedValue({
totalItems: 0,
totalClaims: 0,
totalQuestions: 0,
@@ -540,10 +540,10 @@ describe("memory-wiki gateway methods", () => {
});
});
it("returns memory palace overview over the gateway", async () => {
it("returns the wiki overview over the gateway", async () => {
const { config } = await createVault({ prefix: "memory-wiki-gateway-" });
const { api, registerGatewayMethod } = createPluginApi();
vi.mocked(listMemoryWikiPalace).mockResolvedValue({
vi.mocked(listMemoryWikiOverview).mockResolvedValue({
totalItems: 1,
totalPages: 3,
pageCounts: {
@@ -582,9 +582,9 @@ describe("memory-wiki gateway methods", () => {
} as never);
registerMemoryWikiGatewayMethods({ api, config });
const handler = findGatewayHandler(registerGatewayMethod, "wiki.palace");
const handler = findGatewayHandler(registerGatewayMethod, "wiki.overview");
if (!handler) {
throw new Error("wiki.palace handler missing");
throw new Error("wiki.overview handler missing");
}
const respond = vi.fn();
@@ -594,7 +594,7 @@ describe("memory-wiki gateway methods", () => {
});
expect(syncMemoryWikiImportedSources).toHaveBeenCalledWith({ config, appConfig: undefined });
expect(listMemoryWikiPalace).toHaveBeenCalledWith(config);
expect(listMemoryWikiOverview).toHaveBeenCalledWith(config);
expect(readRespondPayload(respond)).toEqual({
totalItems: 1,
totalPages: 3,

View File

@@ -15,7 +15,6 @@ import { listMemoryWikiImportInsights } from "./import-insights.js";
import { listMemoryWikiImportRuns } from "./import-runs.js";
import { ingestMemoryWikiSource } from "./ingest.js";
import { lintMemoryWikiVault } from "./lint.js";
import { listMemoryWikiPalace } from "./memory-palace.js";
import {
probeObsidianCli,
runObsidianCommand,
@@ -27,6 +26,7 @@ import { getMemoryWikiPage, searchMemoryWiki, WIKI_SEARCH_MODES } from "./query.
import { syncMemoryWikiImportedSources } from "./source-sync.js";
import { buildMemoryWikiDoctorReport, resolveMemoryWikiStatus } from "./status.js";
import { initializeMemoryWikiVault } from "./vault.js";
import { listMemoryWikiOverview } from "./wiki-overview.js";
const READ_SCOPE = "operator.read" as const;
const WRITE_SCOPE = "operator.write" as const;
@@ -175,13 +175,16 @@ export function registerMemoryWikiGatewayMethods(params: {
{ scope: READ_SCOPE },
);
// Renamed from wiki.palace without an alias by maintainer decision: the method was
// undocumented, its only known consumer is the version-locked Control UI, and stale
// callers get an explicit unknown-method error rather than a silent failure.
api.registerGatewayMethod(
"wiki.palace",
"wiki.overview",
async ({ params: requestParams, respond }) => {
try {
const { appConfig, config } = resolveRequestContext(requestParams);
await syncImportedSourcesIfNeeded(config, appConfig);
respond(true, await listMemoryWikiPalace(config));
respond(true, await listMemoryWikiOverview(config));
} catch (error) {
respondError(respond, error);
}

View File

@@ -1,17 +1,17 @@
// Memory Wiki tests cover memory palace plugin behavior.
// Memory Wiki tests cover the wiki overview plugin behavior.
import fs from "node:fs/promises";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { renderWikiMarkdown } from "./markdown.js";
import { listMemoryWikiPalace } from "./memory-palace.js";
import { createMemoryWikiTestHarness } from "./test-helpers.js";
import { listMemoryWikiOverview } from "./wiki-overview.js";
const { createVault } = createMemoryWikiTestHarness();
describe("listMemoryWikiPalace", () => {
describe("listMemoryWikiOverview", () => {
it("groups wiki pages by kind and surfaces claims, questions, and contradictions", async () => {
const { rootDir, config } = await createVault({
prefix: "memory-wiki-palace-",
prefix: "memory-wiki-overview-",
initialize: true,
});
@@ -73,7 +73,7 @@ describe("listMemoryWikiPalace", () => {
"utf8",
);
const result = await listMemoryWikiPalace(config);
const result = await listMemoryWikiOverview(config);
expect(result.totalItems).toBe(2);
expect(result.totalPages).toBe(3);

View File

@@ -1,11 +1,11 @@
// Memory Wiki plugin module implements memory palace behavior.
// Memory Wiki plugin module implements the memory wiki overview.
import type { ResolvedMemoryWikiConfig } from "./config.js";
import { parseWikiMarkdown, type WikiPageKind } from "./markdown.js";
import { readQueryableWikiPages } from "./query.js";
const PALACE_KIND_ORDER: WikiPageKind[] = ["synthesis", "entity", "concept", "source", "report"];
const PRIMARY_PALACE_KINDS = new Set<WikiPageKind>(["synthesis", "entity", "concept"]);
const PALACE_KIND_LABELS: Record<WikiPageKind, string> = {
const OVERVIEW_KIND_ORDER: WikiPageKind[] = ["synthesis", "entity", "concept", "source", "report"];
const PRIMARY_OVERVIEW_KINDS = new Set<WikiPageKind>(["synthesis", "entity", "concept"]);
const OVERVIEW_KIND_LABELS: Record<WikiPageKind, string> = {
synthesis: "Syntheses",
entity: "Entities",
concept: "Concepts",
@@ -13,7 +13,7 @@ const PALACE_KIND_LABELS: Record<WikiPageKind, string> = {
report: "Reports",
};
type MemoryWikiPalaceItem = {
type MemoryWikiOverviewItem = {
pagePath: string;
title: string;
kind: WikiPageKind;
@@ -29,7 +29,7 @@ type MemoryWikiPalaceItem = {
snippet?: string;
};
type MemoryWikiPalaceCluster = {
type MemoryWikiOverviewCluster = {
key: WikiPageKind;
label: string;
itemCount: number;
@@ -37,22 +37,22 @@ type MemoryWikiPalaceCluster = {
questionCount: number;
contradictionCount: number;
updatedAt?: string;
items: MemoryWikiPalaceItem[];
items: MemoryWikiOverviewItem[];
};
type MemoryWikiPalacePageCounts = Record<WikiPageKind, number>;
type MemoryWikiOverviewPageCounts = Record<WikiPageKind, number>;
type MemoryWikiPalaceStatus = {
type MemoryWikiOverviewStatus = {
totalItems: number;
totalPages: number;
pageCounts: MemoryWikiPalacePageCounts;
pageCounts: MemoryWikiOverviewPageCounts;
totalClaims: number;
totalQuestions: number;
totalContradictions: number;
clusters: MemoryWikiPalaceCluster[];
clusters: MemoryWikiOverviewCluster[];
};
function createEmptyPalacePageCounts(): MemoryWikiPalacePageCounts {
function createEmptyOverviewPageCounts(): MemoryWikiOverviewPageCounts {
return {
synthesis: 0,
entity: 0,
@@ -88,7 +88,7 @@ function extractSnippet(body: string): string | undefined {
return undefined;
}
function comparePalaceItems(left: MemoryWikiPalaceItem, right: MemoryWikiPalaceItem): number {
function compareOverviewItems(left: MemoryWikiOverviewItem, right: MemoryWikiOverviewItem): number {
const leftKey = left.updatedAt ?? "";
const rightKey = right.updatedAt ?? "";
if (rightKey !== leftKey) {
@@ -100,14 +100,14 @@ function comparePalaceItems(left: MemoryWikiPalaceItem, right: MemoryWikiPalaceI
return left.title.localeCompare(right.title);
}
export async function listMemoryWikiPalace(
export async function listMemoryWikiOverview(
config: ResolvedMemoryWikiConfig,
): Promise<MemoryWikiPalaceStatus> {
): Promise<MemoryWikiOverviewStatus> {
const pages = await readQueryableWikiPages(config.vault.path);
const pageCounts = pages.reduce<MemoryWikiPalacePageCounts>((counts, page) => {
const pageCounts = pages.reduce<MemoryWikiOverviewPageCounts>((counts, page) => {
counts[page.kind] += 1;
return counts;
}, createEmptyPalacePageCounts());
}, createEmptyOverviewPageCounts());
const totalClaims = pages.reduce((sum, page) => sum + page.claims.length, 0);
const totalQuestions = pages.reduce((sum, page) => sum + page.questions.length, 0);
const totalContradictions = pages.reduce((sum, page) => sum + page.contradictions.length, 0);
@@ -130,18 +130,18 @@ export async function listMemoryWikiPalace(
contradictions: page.contradictions.slice(0, 3),
},
extractSnippet(parsed.body) ? { snippet: extractSnippet(parsed.body) } : {},
) satisfies MemoryWikiPalaceItem;
) satisfies MemoryWikiOverviewItem;
})
.filter(
(item) =>
PRIMARY_PALACE_KINDS.has(item.kind) ||
PRIMARY_OVERVIEW_KINDS.has(item.kind) ||
item.claimCount > 0 ||
item.questionCount > 0 ||
item.contradictionCount > 0,
)
.toSorted(comparePalaceItems);
.toSorted(compareOverviewItems);
const clusters = PALACE_KIND_ORDER.map((kind) => {
const clusters = OVERVIEW_KIND_ORDER.map((kind) => {
const clusterItems = items.filter((item) => item.kind === kind);
if (clusterItems.length === 0) {
return null;
@@ -149,7 +149,7 @@ export async function listMemoryWikiPalace(
return Object.assign(
{
key: kind,
label: PALACE_KIND_LABELS[kind],
label: OVERVIEW_KIND_LABELS[kind],
itemCount: clusterItems.length,
claimCount: clusterItems.reduce((sum, item) => sum + item.claimCount, 0),
questionCount: clusterItems.reduce((sum, item) => sum + item.questionCount, 0),
@@ -157,8 +157,8 @@ export async function listMemoryWikiPalace(
},
clusterItems[0]?.updatedAt ? { updatedAt: clusterItems[0].updatedAt } : {},
{ items: clusterItems },
) satisfies MemoryWikiPalaceCluster;
}).filter((entry): entry is MemoryWikiPalaceCluster => entry !== null);
) satisfies MemoryWikiOverviewCluster;
}).filter((entry): entry is MemoryWikiOverviewCluster => entry !== null);
return {
totalItems: items.length,

View File

@@ -97,7 +97,7 @@ describeControlUiE2e("Control UI Agents channel status", () => {
try {
const response = await page.goto(`${server.baseUrl}agents`);
expect(response?.status()).toBe(200);
await page.getByRole("button", { name: /^Channels/ }).click();
await page.getByRole("tab", { name: /^Channels/ }).click();
const discordRow = page.locator(".settings-row").filter({ hasText: "Discord" });
await expect.poll(() => discordRow.count()).toBe(1);

View File

@@ -3718,12 +3718,12 @@ export const en: TranslationMap = {
loadingPage: "Loading wiki page…",
dreamsTab: "Dreams",
insightsTab: "Imported Insights",
palaceTab: "Memory Palace",
wikiTab: "Memory Wiki",
dreamsExplainer:
"This is the raw dream diary the system writes while replaying and consolidating memory; use it to inspect what the memory system is noticing, and where it still looks noisy or thin.",
insightsExplainer:
"These are imported insights clustered from external history; use them to review what imports surfaced before any of it graduates into durable memory.",
palaceExplainer:
wikiExplainer:
"This is the compiled memory wiki surface the system can search and reason over; use it to inspect actual memory pages, claims, open questions, and contradictions rather than raw imported source chats.",
copyArchivePath: "Copy archive path",
loadingInsights: "Loading imported insights…",
@@ -3739,9 +3739,9 @@ export const en: TranslationMap = {
riskReasons: "Risk reasons:",
labels: "Labels:",
openSourcePage: "Open source page",
loadingPalace: "Loading memory palace…",
emptyPalace: "Memory palace is not populated yet",
emptyPalaceHint:
loadingWiki: "Loading memory wiki…",
emptyWiki: "Memory wiki is not populated yet",
emptyWikiHint:
"Right now the wiki mostly has raw source imports and operational reports. This tab becomes useful once syntheses, entities, or concepts start getting written.",
claims: "Claims",
openQuestions: "Open questions",
@@ -3751,7 +3751,7 @@ export const en: TranslationMap = {
id: "Id:",
openWikiPage: "Open wiki page",
unavailable: "Memory Wiki is not enabled",
unavailablePluginPrefix: "Imported Insights and Memory Palace are provided by the bundled",
unavailablePluginPrefix: "Imported Insights and Memory Wiki are provided by the bundled",
unavailablePluginSuffix: "plugin.",
enablePrefix: "Enable",
enableSuffix: ", then reload this tab.",
@@ -3817,7 +3817,7 @@ export const en: TranslationMap = {
tidyingKnowledgeGraph: "tidying the knowledge graph…",
replayingConversations: "replaying today's conversations…",
weavingShortTerm: "weaving short-term into long-term…",
defragmentingMindPalace: "defragmenting the mind palace…",
defragmentingMemoryLane: "defragmenting memory lane…",
filingLooseThoughts: "filing away loose thoughts…",
connectingDots: "connecting distant dots…",
compostingContext: "composting old context windows…",

View File

@@ -12,7 +12,7 @@ import {
loadDreamDiary,
loadDreamingStatus,
loadWikiImportInsights,
loadWikiMemoryPalace,
loadWikiOverview,
repairDreamingArtifacts,
resetGroundedShortTerm,
resetDreamDiary,
@@ -580,13 +580,13 @@ describe("dreaming controller", () => {
expect(state.wikiImportInsightsLoading).toBe(false);
});
it("loads and normalizes the wiki memory palace", async () => {
it("loads and normalizes the wiki wiki overview", async () => {
const { state, request } = createState();
state.hello = {
type: "hello-ok",
protocol: 4,
auth: { role: "operator", scopes: [] },
features: { methods: ["wiki.palace"] },
features: { methods: ["wiki.overview"] },
};
state.configSnapshot = {
hash: "hash-1",
@@ -639,43 +639,41 @@ describe("dreaming controller", () => {
],
});
await loadWikiMemoryPalace(state);
await loadWikiOverview(state);
expect(request).toHaveBeenCalledWith("wiki.palace", {});
expect(state.wikiMemoryPalace?.totalItems).toBe(1);
expect(state.wikiMemoryPalace?.totalPages).toBe(2);
expect(state.wikiMemoryPalace?.pageCounts.source).toBe(1);
expect(state.wikiMemoryPalace?.pageCounts.synthesis).toBe(1);
expect(state.wikiMemoryPalace?.totalClaims).toBe(2);
expect(state.wikiMemoryPalace?.clusters).toHaveLength(1);
expect(state.wikiMemoryPalace?.clusters[0]?.key).toBe("synthesis");
expect(state.wikiMemoryPalace?.clusters[0]?.label).toBe("Syntheses");
expect(state.wikiMemoryPalace?.clusters[0]?.items).toHaveLength(1);
expect(state.wikiMemoryPalace?.clusters[0]?.items[0]?.title).toBe("Travel system");
expect(state.wikiMemoryPalace?.clusters[0]?.items[0]?.claims).toEqual([
"prefers direct receipts",
]);
expect(state.wikiMemoryPalaceError).toBeNull();
expect(state.wikiMemoryPalaceLoading).toBe(false);
expect(request).toHaveBeenCalledWith("wiki.overview", {});
expect(state.wikiOverview?.totalItems).toBe(1);
expect(state.wikiOverview?.totalPages).toBe(2);
expect(state.wikiOverview?.pageCounts.source).toBe(1);
expect(state.wikiOverview?.pageCounts.synthesis).toBe(1);
expect(state.wikiOverview?.totalClaims).toBe(2);
expect(state.wikiOverview?.clusters).toHaveLength(1);
expect(state.wikiOverview?.clusters[0]?.key).toBe("synthesis");
expect(state.wikiOverview?.clusters[0]?.label).toBe("Syntheses");
expect(state.wikiOverview?.clusters[0]?.items).toHaveLength(1);
expect(state.wikiOverview?.clusters[0]?.items[0]?.title).toBe("Travel system");
expect(state.wikiOverview?.clusters[0]?.items[0]?.claims).toEqual(["prefers direct receipts"]);
expect(state.wikiOverviewError).toBeNull();
expect(state.wikiOverviewLoading).toBe(false);
});
it("loads the wiki memory palace for the selected agent", async () => {
it("loads the wiki wiki overview for the selected agent", async () => {
const { state, request } = createState();
state.selectedAgentId = "marketing";
state.hello = {
type: "hello-ok",
protocol: 4,
auth: { role: "operator", scopes: [] },
features: { methods: ["wiki.palace"] },
features: { methods: ["wiki.overview"] },
};
request.mockResolvedValue({ totalItems: 1, clusters: [] });
await loadWikiMemoryPalace(state);
await loadWikiOverview(state);
expect(request).toHaveBeenCalledWith("wiki.palace", { agentId: "marketing" });
expect(request).toHaveBeenCalledWith("wiki.overview", { agentId: "marketing" });
});
it("starts a new selected-agent palace load and ignores stale completions", async () => {
it("starts a new selected-agent wiki overview load and ignores stale completions", async () => {
const { state, request } = createState();
const agentA = createDeferred<unknown>();
const agentB = createDeferred<unknown>();
@@ -683,7 +681,7 @@ describe("dreaming controller", () => {
type: "hello-ok",
protocol: 4,
auth: { role: "operator", scopes: [] },
features: { methods: ["wiki.palace"] },
features: { methods: ["wiki.overview"] },
};
request.mockImplementation(async (_method: string, payload?: unknown) => {
const agentId =
@@ -694,29 +692,29 @@ describe("dreaming controller", () => {
});
state.selectedAgentId = "agent-a";
const firstLoad = loadWikiMemoryPalace(state);
const firstLoad = loadWikiOverview(state);
state.selectedAgentId = "agent-b";
const secondLoad = loadWikiMemoryPalace(state);
const secondLoad = loadWikiOverview(state);
agentB.resolve({ totalItems: 2, clusters: [] });
await secondLoad;
agentA.resolve({ totalItems: 1, clusters: [] });
await firstLoad;
expect(request).toHaveBeenCalledWith("wiki.palace", { agentId: "agent-a" });
expect(request).toHaveBeenCalledWith("wiki.palace", { agentId: "agent-b" });
expect(state.wikiMemoryPalace?.totalItems).toBe(2);
expect(state.wikiMemoryPalaceLoading).toBe(false);
expect(state.wikiMemoryPalaceError).toBeNull();
expect(request).toHaveBeenCalledWith("wiki.overview", { agentId: "agent-a" });
expect(request).toHaveBeenCalledWith("wiki.overview", { agentId: "agent-b" });
expect(state.wikiOverview?.totalItems).toBe(2);
expect(state.wikiOverviewLoading).toBe(false);
expect(state.wikiOverviewError).toBeNull();
});
it("derives legacy wiki memory palace page counts from clusters", async () => {
it("derives legacy wiki wiki overview page counts from clusters", async () => {
const { state, request } = createState();
state.hello = {
type: "hello-ok",
protocol: 4,
auth: { role: "operator", scopes: [] },
features: { methods: ["wiki.palace"] },
features: { methods: ["wiki.overview"] },
};
state.configSnapshot = {
hash: "hash-1",
@@ -748,10 +746,10 @@ describe("dreaming controller", () => {
],
});
await loadWikiMemoryPalace(state);
await loadWikiOverview(state);
expect(state.wikiMemoryPalace?.totalPages).toBe(1);
expect(state.wikiMemoryPalace?.pageCounts).toEqual({
expect(state.wikiOverview?.totalPages).toBe(1);
expect(state.wikiOverview?.pageCounts).toEqual({
synthesis: 1,
entity: 0,
concept: 0,
@@ -760,7 +758,7 @@ describe("dreaming controller", () => {
});
});
it("falls back to config gating for wiki memory palace when methods are not advertised", async () => {
it("falls back to config gating for wiki wiki overview when methods are not advertised", async () => {
const { state, request } = createState();
state.configSnapshot = {
hash: "hash-1",
@@ -782,24 +780,24 @@ describe("dreaming controller", () => {
clusters: [],
});
await loadWikiMemoryPalace(state);
await loadWikiOverview(state);
expect(request).toHaveBeenCalledWith("wiki.palace", {});
expect(state.wikiMemoryPalace?.totalItems).toBe(1);
expect(state.wikiMemoryPalace?.totalPages).toBe(1);
expect(state.wikiMemoryPalace?.pageCounts).toEqual({
expect(request).toHaveBeenCalledWith("wiki.overview", {});
expect(state.wikiOverview?.totalItems).toBe(1);
expect(state.wikiOverview?.totalPages).toBe(1);
expect(state.wikiOverview?.pageCounts).toEqual({
synthesis: 0,
entity: 0,
concept: 0,
source: 0,
report: 0,
});
expect(state.wikiMemoryPalace?.totalClaims).toBe(2);
expect(state.wikiMemoryPalaceError).toBeNull();
expect(state.wikiMemoryPalaceLoading).toBe(false);
expect(state.wikiOverview?.totalClaims).toBe(2);
expect(state.wikiOverviewError).toBeNull();
expect(state.wikiOverviewLoading).toBe(false);
});
it("skips wiki memory palace when memory-wiki is not enabled", async () => {
it("skips wiki wiki overview when memory-wiki is not enabled", async () => {
const { state, request } = createState();
state.configSnapshot = {
hash: "hash-1",
@@ -807,7 +805,7 @@ describe("dreaming controller", () => {
plugins: {},
},
};
state.wikiMemoryPalace = {
state.wikiOverview = {
totalItems: 1,
totalPages: 1,
pageCounts: {
@@ -822,17 +820,17 @@ describe("dreaming controller", () => {
totalContradictions: 0,
clusters: [],
};
state.wikiMemoryPalaceError = "unknown method: wiki.palace";
state.wikiOverviewError = "unknown method: wiki.overview";
await loadWikiMemoryPalace(state);
await loadWikiOverview(state);
expect(request).not.toHaveBeenCalled();
expect(state.wikiMemoryPalace).toBeNull();
expect(state.wikiMemoryPalaceError).toBeNull();
expect(state.wikiMemoryPalaceLoading).toBe(false);
expect(state.wikiOverview).toBeNull();
expect(state.wikiOverviewError).toBeNull();
expect(state.wikiOverviewLoading).toBe(false);
});
it("skips wiki memory palace when the gateway does not advertise the method", async () => {
it("skips wiki wiki overview when the gateway does not advertise the method", async () => {
const { state, request } = createState();
state.hello = {
type: "hello-ok",
@@ -852,7 +850,7 @@ describe("dreaming controller", () => {
},
},
};
state.wikiMemoryPalace = {
state.wikiOverview = {
totalItems: 1,
totalPages: 1,
pageCounts: {
@@ -867,14 +865,14 @@ describe("dreaming controller", () => {
totalContradictions: 0,
clusters: [],
};
state.wikiMemoryPalaceError = "unknown method: wiki.palace";
state.wikiOverviewError = "unknown method: wiki.overview";
await loadWikiMemoryPalace(state);
await loadWikiOverview(state);
expect(request).not.toHaveBeenCalled();
expect(state.wikiMemoryPalace).toBeNull();
expect(state.wikiMemoryPalaceError).toBeNull();
expect(state.wikiMemoryPalaceLoading).toBe(false);
expect(state.wikiOverview).toBeNull();
expect(state.wikiOverviewError).toBeNull();
expect(state.wikiOverviewLoading).toBe(false);
});
it("patches config to update global dreaming enablement", async () => {

View File

@@ -127,7 +127,7 @@ export type WikiImportInsights = {
clusters: WikiImportInsightCluster[];
};
type WikiMemoryPalaceItem = {
type WikiOverviewItem = {
pagePath: string;
title: string;
kind: "entity" | "concept" | "source" | "synthesis" | "report";
@@ -143,27 +143,27 @@ type WikiMemoryPalaceItem = {
snippet?: string;
};
type WikiMemoryPalaceCluster = {
key: WikiMemoryPalaceItem["kind"];
type WikiOverviewCluster = {
key: WikiOverviewItem["kind"];
label: string;
itemCount: number;
claimCount: number;
questionCount: number;
contradictionCount: number;
updatedAt?: string;
items: WikiMemoryPalaceItem[];
items: WikiOverviewItem[];
};
type WikiMemoryPalacePageCounts = Record<WikiMemoryPalaceItem["kind"], number>;
type WikiOverviewPageCounts = Record<WikiOverviewItem["kind"], number>;
export type WikiMemoryPalace = {
export type WikiOverview = {
totalItems: number;
totalPages: number;
pageCounts: WikiMemoryPalacePageCounts;
pageCounts: WikiOverviewPageCounts;
totalClaims: number;
totalQuestions: number;
totalContradictions: number;
clusters: WikiMemoryPalaceCluster[];
clusters: WikiOverviewCluster[];
};
type DoctorMemoryStatusPayload = {
@@ -199,7 +199,7 @@ type WikiImportInsightsPayload = {
clusters?: unknown;
};
type WikiMemoryPalacePayload = {
type WikiOverviewPayload = {
totalItems?: unknown;
totalPages?: unknown;
pageCounts?: unknown;
@@ -244,13 +244,13 @@ export type DreamingState = {
wikiImportInsightsLoading: boolean;
wikiImportInsightsError: string | null;
wikiImportInsights: WikiImportInsights | null;
wikiMemoryPalaceRequestAgentId?: string | null;
wikiMemoryPalaceRequestGeneration?: number;
wikiMemoryPalaceActiveRequestGeneration?: number | null;
wikiMemoryPalaceAgentId?: string | null;
wikiMemoryPalaceLoading: boolean;
wikiMemoryPalaceError: string | null;
wikiMemoryPalace: WikiMemoryPalace | null;
wikiOverviewRequestAgentId?: string | null;
wikiOverviewRequestGeneration?: number;
wikiOverviewActiveRequestGeneration?: number | null;
wikiOverviewAgentId?: string | null;
wikiOverviewLoading: boolean;
wikiOverviewError: string | null;
wikiOverview: WikiOverview | null;
lastError: string | null;
};
@@ -283,9 +283,9 @@ export function createDreamingState(
wikiImportInsightsLoading: false,
wikiImportInsightsError: null,
wikiImportInsights: null,
wikiMemoryPalaceLoading: false,
wikiMemoryPalaceError: null,
wikiMemoryPalace: null,
wikiOverviewLoading: false,
wikiOverviewError: null,
wikiOverview: null,
lastError: null,
};
}
@@ -636,7 +636,7 @@ function normalizeWikiImportInsights(raw: unknown): WikiImportInsights {
};
}
function normalizeWikiPageKind(value: unknown): WikiMemoryPalaceItem["kind"] | undefined {
function normalizeWikiPageKind(value: unknown): WikiOverviewItem["kind"] | undefined {
return value === "entity" ||
value === "concept" ||
value === "source" ||
@@ -646,7 +646,7 @@ function normalizeWikiPageKind(value: unknown): WikiMemoryPalaceItem["kind"] | u
: undefined;
}
function createEmptyWikiMemoryPalacePageCounts(): WikiMemoryPalacePageCounts {
function createEmptyWikiOverviewPageCounts(): WikiOverviewPageCounts {
return {
synthesis: 0,
entity: 0,
@@ -656,10 +656,10 @@ function createEmptyWikiMemoryPalacePageCounts(): WikiMemoryPalacePageCounts {
};
}
function normalizeWikiMemoryPalacePageCounts(
function normalizeWikiOverviewPageCounts(
raw: unknown,
fallback: WikiMemoryPalacePageCounts,
): WikiMemoryPalacePageCounts {
fallback: WikiOverviewPageCounts,
): WikiOverviewPageCounts {
const record = asRecord(raw);
return {
synthesis: normalizeFiniteInt(record?.synthesis, fallback.synthesis),
@@ -670,7 +670,7 @@ function normalizeWikiMemoryPalacePageCounts(
};
}
function sumWikiMemoryPalacePageCounts(pageCounts: WikiMemoryPalacePageCounts): number {
function sumWikiOverviewPageCounts(pageCounts: WikiOverviewPageCounts): number {
return (
pageCounts.synthesis +
pageCounts.entity +
@@ -680,7 +680,7 @@ function sumWikiMemoryPalacePageCounts(pageCounts: WikiMemoryPalacePageCounts):
);
}
function normalizeWikiMemoryPalaceItem(raw: unknown): WikiMemoryPalaceItem | null {
function normalizeWikiOverviewItem(raw: unknown): WikiOverviewItem | null {
const record = asRecord(raw);
const pagePath = normalizeTrimmedString(record?.pagePath);
const title = normalizeTrimmedString(record?.title);
@@ -711,7 +711,7 @@ function normalizeWikiMemoryPalaceItem(raw: unknown): WikiMemoryPalaceItem | nul
};
}
function normalizeWikiMemoryPalaceCluster(raw: unknown): WikiMemoryPalaceCluster | null {
function normalizeWikiOverviewCluster(raw: unknown): WikiOverviewCluster | null {
const record = asRecord(raw);
const key = normalizeWikiPageKind(record?.key);
const label = normalizeTrimmedString(record?.label);
@@ -720,8 +720,8 @@ function normalizeWikiMemoryPalaceCluster(raw: unknown): WikiMemoryPalaceCluster
}
const items = Array.isArray(record?.items)
? record.items
.map((entry) => normalizeWikiMemoryPalaceItem(entry))
.filter((entry): entry is WikiMemoryPalaceItem => entry !== null)
.map((entry) => normalizeWikiOverviewItem(entry))
.filter((entry): entry is WikiOverviewItem => entry !== null)
: [];
return {
key,
@@ -746,23 +746,23 @@ function normalizeWikiMemoryPalaceCluster(raw: unknown): WikiMemoryPalaceCluster
};
}
function normalizeWikiMemoryPalace(raw: unknown): WikiMemoryPalace {
function normalizeWikiOverview(raw: unknown): WikiOverview {
const record = asRecord(raw);
const clusters = Array.isArray(record?.clusters)
? record.clusters
.map((entry) => normalizeWikiMemoryPalaceCluster(entry))
.filter((entry): entry is WikiMemoryPalaceCluster => entry !== null)
.map((entry) => normalizeWikiOverviewCluster(entry))
.filter((entry): entry is WikiOverviewCluster => entry !== null)
: [];
const totalItems = normalizeFiniteInt(
record?.totalItems,
clusters.reduce((sum, cluster) => sum + cluster.itemCount, 0),
);
const fallbackPageCounts = createEmptyWikiMemoryPalacePageCounts();
const fallbackPageCounts = createEmptyWikiOverviewPageCounts();
for (const cluster of clusters) {
fallbackPageCounts[cluster.key] += cluster.itemCount;
}
const pageCounts = normalizeWikiMemoryPalacePageCounts(record?.pageCounts, fallbackPageCounts);
const fallbackTotalPages = sumWikiMemoryPalacePageCounts(pageCounts) || totalItems;
const pageCounts = normalizeWikiOverviewPageCounts(record?.pageCounts, fallbackPageCounts);
const fallbackTotalPages = sumWikiOverviewPageCounts(pageCounts) || totalItems;
return {
totalItems,
totalPages: normalizeFiniteInt(record?.totalPages, fallbackTotalPages),
@@ -1022,58 +1022,58 @@ export async function loadWikiImportInsights(state: DreamingState): Promise<void
}
}
export async function loadWikiMemoryPalace(state: DreamingState): Promise<void> {
export async function loadWikiOverview(state: DreamingState): Promise<void> {
if (!state.client || !state.connected) {
return;
}
const agentId = resolveSelectedAgentId(state);
if (state.wikiMemoryPalaceLoading && state.wikiMemoryPalaceRequestAgentId === agentId) {
if (state.wikiOverviewLoading && state.wikiOverviewRequestAgentId === agentId) {
return;
}
if (state.wikiMemoryPalaceAgentId !== agentId) {
state.wikiMemoryPalace = null;
if (state.wikiOverviewAgentId !== agentId) {
state.wikiOverview = null;
}
if (!canCallMemoryWikiMethod(state, "wiki.palace")) {
state.wikiMemoryPalaceActiveRequestGeneration = null;
state.wikiMemoryPalaceRequestAgentId = null;
state.wikiMemoryPalaceLoading = false;
state.wikiMemoryPalace = null;
state.wikiMemoryPalaceError = null;
if (!canCallMemoryWikiMethod(state, "wiki.overview")) {
state.wikiOverviewActiveRequestGeneration = null;
state.wikiOverviewRequestAgentId = null;
state.wikiOverviewLoading = false;
state.wikiOverview = null;
state.wikiOverviewError = null;
return;
}
const requestGeneration = (state.wikiMemoryPalaceRequestGeneration ?? 0) + 1;
state.wikiMemoryPalaceRequestGeneration = requestGeneration;
state.wikiMemoryPalaceActiveRequestGeneration = requestGeneration;
state.wikiMemoryPalaceRequestAgentId = agentId;
state.wikiMemoryPalaceLoading = true;
state.wikiMemoryPalaceError = null;
const requestGeneration = (state.wikiOverviewRequestGeneration ?? 0) + 1;
state.wikiOverviewRequestGeneration = requestGeneration;
state.wikiOverviewActiveRequestGeneration = requestGeneration;
state.wikiOverviewRequestAgentId = agentId;
state.wikiOverviewLoading = true;
state.wikiOverviewError = null;
try {
const payload = await state.client.request<WikiMemoryPalacePayload>(
"wiki.palace",
const payload = await state.client.request<WikiOverviewPayload>(
"wiki.overview",
buildSelectedAgentPayloadForAgentId(agentId),
);
if (
state.wikiMemoryPalaceActiveRequestGeneration !== requestGeneration ||
state.wikiMemoryPalaceRequestAgentId !== agentId ||
state.wikiOverviewActiveRequestGeneration !== requestGeneration ||
state.wikiOverviewRequestAgentId !== agentId ||
resolveSelectedAgentId(state) !== agentId
) {
return;
}
state.wikiMemoryPalace = normalizeWikiMemoryPalace(payload);
state.wikiMemoryPalaceAgentId = agentId;
state.wikiOverview = normalizeWikiOverview(payload);
state.wikiOverviewAgentId = agentId;
} catch (err) {
if (
state.wikiMemoryPalaceActiveRequestGeneration === requestGeneration &&
state.wikiMemoryPalaceRequestAgentId === agentId &&
state.wikiOverviewActiveRequestGeneration === requestGeneration &&
state.wikiOverviewRequestAgentId === agentId &&
resolveSelectedAgentId(state) === agentId
) {
state.wikiMemoryPalaceError = String(err);
state.wikiOverviewError = String(err);
}
} finally {
if (state.wikiMemoryPalaceActiveRequestGeneration === requestGeneration) {
state.wikiMemoryPalaceLoading = false;
state.wikiMemoryPalaceRequestAgentId = null;
state.wikiMemoryPalaceActiveRequestGeneration = null;
if (state.wikiOverviewActiveRequestGeneration === requestGeneration) {
state.wikiOverviewLoading = false;
state.wikiOverviewRequestAgentId = null;
state.wikiOverviewActiveRequestGeneration = null;
}
}
}

View File

@@ -21,7 +21,7 @@ import {
loadDreamDiary,
loadDreamingStatus,
loadWikiImportInsights,
loadWikiMemoryPalace,
loadWikiOverview,
repairDreamingArtifacts,
resetGroundedShortTerm,
resetDreamDiary,
@@ -298,7 +298,7 @@ class AgentMemoryPanel extends OpenClawLightDomElement {
this.runDreamingTask(loadDreamingStatus, scope),
this.runDreamingTask(loadDreamDiary, scope),
this.runDreamingTask(loadWikiImportInsights, scope),
this.runDreamingTask(loadWikiMemoryPalace, scope),
this.runDreamingTask(loadWikiOverview, scope),
]);
}
@@ -470,13 +470,13 @@ class AgentMemoryPanel extends OpenClawLightDomElement {
wikiImportInsightsLoading: dreaming.wikiImportInsightsLoading,
wikiImportInsightsError: dreaming.wikiImportInsightsError,
wikiImportInsights: dreaming.wikiImportInsights,
wikiMemoryPalaceLoading: dreaming.wikiMemoryPalaceLoading,
wikiMemoryPalaceError: dreaming.wikiMemoryPalaceError,
wikiMemoryPalace: dreaming.wikiMemoryPalace,
wikiOverviewLoading: dreaming.wikiOverviewLoading,
wikiOverviewError: dreaming.wikiOverviewError,
wikiOverview: dreaming.wikiOverview,
onRefresh: () => void this.loadAll(true),
onRefreshDiary: () => void this.runDreamingTask(loadDreamDiary),
onRefreshImports: () => void this.refreshWikiData(loadWikiImportInsights),
onRefreshMemoryPalace: () => void this.refreshWikiData(loadWikiMemoryPalace),
onRefreshWikiOverview: () => void this.refreshWikiData(loadWikiOverview),
onOpenConfig: () => void this.context.runtimeConfig.openFile(),
onOpenWikiPage: (lookup) => this.openWikiPage(lookup),
onBackfillDiary: () => void this.runDreamingTask(backfillDreamDiary),

View File

@@ -231,9 +231,9 @@ function buildProps(overrides?: Partial<DreamingProps>): DreamingProps {
},
],
},
wikiMemoryPalaceLoading: false,
wikiMemoryPalaceError: null,
wikiMemoryPalace: {
wikiOverviewLoading: false,
wikiOverviewError: null,
wikiOverview: {
totalItems: 1,
totalPages: 2,
pageCounts: {
@@ -278,7 +278,7 @@ function buildProps(overrides?: Partial<DreamingProps>): DreamingProps {
onRefresh: () => {},
onRefreshDiary: () => {},
onRefreshImports: () => {},
onRefreshMemoryPalace: () => {},
onRefreshWikiOverview: () => {},
onOpenConfig: () => {},
onOpenWikiPage: async () => null,
onBackfillDiary: () => {},
@@ -430,12 +430,12 @@ describe("dreaming view", () => {
expect(subtabs).toEqual([
{ label: "Dreams", active: false },
{ label: "Imported Insights", active: true },
{ label: "Memory Palace", active: false },
{ label: "Memory Wiki", active: false },
]);
container
.querySelector("#dream-diary-tab-palace")
.querySelector("#dream-diary-tab-wiki")
?.dispatchEvent(new MouseEvent("click", { detail: 1, bubbles: true }));
expect(viewState.activeDiarySubTab).toBe("palace");
expect(viewState.activeDiarySubTab).toBe("wiki");
expect(onViewStateChange).toHaveBeenCalledOnce();
expect(compactText(container.querySelector(".dreams-diary__date"))).toBe(
"Travel · 1 chats · 1 signals",
@@ -550,9 +550,9 @@ describe("dreaming view", () => {
setDreamSubTab("scene");
});
it("renders the memory palace inside the diary tab", () => {
it("renders the wiki overview inside the diary tab", () => {
setDreamSubTab("diary");
setDreamDiarySubTab("palace");
setDreamDiarySubTab("wiki");
const container = renderInto(buildProps());
expect(compactText(container.querySelector(".dreams-diary__date"))).toBe(
"Vault · 2 pages · 2 claim rows · 1 open question · 1 contradiction",
@@ -583,15 +583,15 @@ describe("dreaming view", () => {
setDreamSubTab("scene");
});
it("keeps non-report memory palace card clicks on details", () => {
it("keeps non-report wiki overview card clicks on details", () => {
setDreamSubTab("diary");
setDreamDiarySubTab("palace");
setDreamDiarySubTab("wiki");
const container = document.createElement("div");
const rerender = () => render(renderDreaming(props), container);
const props: DreamingProps = buildProps({ onViewStateChange: rerender });
rerender();
const card = expectElement(container, "[data-palace-page='syntheses/travel-system.md']");
const card = expectElement(container, "[data-wiki-page='syntheses/travel-system.md']");
card.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(textItems(container, ".dreams-diary__insight-list strong")).toContain("Page details");
@@ -599,9 +599,9 @@ describe("dreaming view", () => {
setDreamSubTab("scene");
});
it("opens report memory palace cards on primary click", async () => {
it("opens report wiki overview cards on primary click", async () => {
setDreamSubTab("diary");
setDreamDiarySubTab("palace");
setDreamDiarySubTab("wiki");
const onOpenWikiPage = vi.fn().mockResolvedValue({
title: "Weekly stock report",
path: "reports/weekly-stock.md",
@@ -614,7 +614,7 @@ describe("dreaming view", () => {
const props: DreamingProps = buildProps({
onOpenWikiPage,
onViewStateChange: rerender,
wikiMemoryPalace: {
wikiOverview: {
totalItems: 1,
totalPages: 1,
pageCounts: {
@@ -656,7 +656,7 @@ describe("dreaming view", () => {
});
rerender();
const card = expectElement(container, "[data-palace-page='reports/weekly-stock.md']");
const card = expectElement(container, "[data-wiki-page='reports/weekly-stock.md']");
card.dispatchEvent(new MouseEvent("click", { bubbles: true }));
await Promise.resolve();
await Promise.resolve();
@@ -683,7 +683,7 @@ describe("dreaming view", () => {
it("shows a memory-wiki enablement CTA when wiki subtabs are selected but the plugin is disabled", () => {
setDreamSubTab("diary");
setDreamDiarySubTab("palace");
setDreamDiarySubTab("wiki");
const onOpenConfig = vi.fn();
const container = renderInto(
buildProps({
@@ -697,7 +697,7 @@ describe("dreaming view", () => {
expect(
[...container.querySelectorAll(".dreams-diary__empty-hint")].map((node) => compactText(node)),
).toEqual([
"Imported Insights and Memory Palace are provided by the bundled memory-wiki plugin.",
"Imported Insights and Memory Wiki are provided by the bundled memory-wiki plugin.",
"Enable plugins.entries.memory-wiki.enabled = true, then reload this tab.",
]);

View File

@@ -13,7 +13,7 @@ import "../../../components/modal-dialog.ts";
import { toSanitizedMarkdownHtml } from "../../../components/markdown.ts";
import { t } from "../../../i18n/index.ts";
import "../../../styles/dreams.css";
import type { DreamingEntry, WikiImportInsights, WikiMemoryPalace } from "./dreaming.ts";
import type { DreamingEntry, WikiImportInsights, WikiOverview } from "./dreaming.ts";
// ── Diary entry parser ─────────────────────────────────────────────────
@@ -130,13 +130,13 @@ type DreamingProps = {
wikiImportInsightsLoading: boolean;
wikiImportInsightsError: string | null;
wikiImportInsights: WikiImportInsights | null;
wikiMemoryPalaceLoading: boolean;
wikiMemoryPalaceError: string | null;
wikiMemoryPalace: WikiMemoryPalace | null;
wikiOverviewLoading: boolean;
wikiOverviewError: string | null;
wikiOverview: WikiOverview | null;
onRefresh: () => void;
onRefreshDiary: () => void;
onRefreshImports: () => void;
onRefreshMemoryPalace: () => void;
onRefreshWikiOverview: () => void;
onOpenConfig: () => void;
onOpenWikiPage: (lookup: string) => Promise<{
title: string;
@@ -160,7 +160,7 @@ const DREAM_PHRASE_KEYS = [
"dreaming.phrases.tidyingKnowledgeGraph",
"dreaming.phrases.replayingConversations",
"dreaming.phrases.weavingShortTerm",
"dreaming.phrases.defragmentingMindPalace",
"dreaming.phrases.defragmentingMemoryLane",
"dreaming.phrases.filingLooseThoughts",
"dreaming.phrases.connectingDots",
"dreaming.phrases.compostingContext",
@@ -189,10 +189,10 @@ export type DreamingViewState = {
dreamIndex: number;
dreamLastSwap: number;
activeSubTab: "scene" | "diary" | "advanced";
activeDiarySubTab: "dreams" | "insights" | "palace";
activeDiarySubTab: "dreams" | "insights" | "wiki";
advancedWaitingSort: "recent" | "signals";
expandedInsightCards: Set<string>;
expandedPalaceCards: Set<string>;
expandedWikiCards: Set<string>;
diaryPage: number;
wikiPreviewRequestId: number;
wikiPreviewOpen: boolean;
@@ -214,7 +214,7 @@ export function createDreamingViewState(): DreamingViewState {
activeDiarySubTab: "dreams",
advancedWaitingSort: "recent",
expandedInsightCards: new Set(),
expandedPalaceCards: new Set(),
expandedWikiCards: new Set(),
diaryPage: 0,
wikiPreviewRequestId: 0,
wikiPreviewOpen: false,
@@ -516,7 +516,7 @@ function formatMessageCount(count: number): string {
return t("dreaming.wiki.counts.messages", { count: String(count) });
}
const MEMORY_PALACE_PAGE_COUNT_ORDER: Array<keyof WikiMemoryPalace["pageCounts"]> = [
const WIKI_OVERVIEW_PAGE_COUNT_ORDER: Array<keyof WikiOverview["pageCounts"]> = [
"source",
"synthesis",
"report",
@@ -524,7 +524,7 @@ const MEMORY_PALACE_PAGE_COUNT_ORDER: Array<keyof WikiMemoryPalace["pageCounts"]
"concept",
];
function formatMemoryPalacePageCountLabel(kind: keyof WikiMemoryPalace["pageCounts"]): string {
function formatWikiOverviewPageCountLabel(kind: keyof WikiOverview["pageCounts"]): string {
switch (kind) {
case "source":
return t("dreaming.wiki.pageGroups.sources");
@@ -540,12 +540,12 @@ function formatMemoryPalacePageCountLabel(kind: keyof WikiMemoryPalace["pageCoun
return kind;
}
function formatMemoryPalacePageBreakdown(pageCounts: WikiMemoryPalace["pageCounts"]): string {
const parts = MEMORY_PALACE_PAGE_COUNT_ORDER.map((kind) => {
function formatWikiOverviewPageBreakdown(pageCounts: WikiOverview["pageCounts"]): string {
const parts = WIKI_OVERVIEW_PAGE_COUNT_ORDER.map((kind) => {
const count = pageCounts[kind];
return count > 0
? t("dreaming.wiki.pageGroupSummary", {
label: formatMemoryPalacePageCountLabel(kind),
label: formatWikiOverviewPageCountLabel(kind),
count: formatPageCount(count),
})
: null;
@@ -553,7 +553,7 @@ function formatMemoryPalacePageBreakdown(pageCounts: WikiMemoryPalace["pageCount
return parts.length > 0 ? parts.join("; ") : t("dreaming.wiki.noPagesYet");
}
function formatMemoryPalaceClusterSummary(cluster: WikiMemoryPalace["clusters"][number]): string {
function formatWikiOverviewClusterSummary(cluster: WikiOverview["clusters"][number]): string {
const parts = [
t("dreaming.wiki.sectionPageSummary", {
label: cluster.label,
@@ -730,8 +730,8 @@ function renderDiarySubtabExplainer(activeDiarySubTab: DreamingViewState["active
return html` <p class="dreams-diary__explainer">${t("dreaming.wiki.dreamsExplainer")}</p> `;
case "insights":
return html` <p class="dreams-diary__explainer">${t("dreaming.wiki.insightsExplainer")}</p> `;
case "palace":
return html` <p class="dreams-diary__explainer">${t("dreaming.wiki.palaceExplainer")}</p> `;
case "wiki":
return html` <p class="dreams-diary__explainer">${t("dreaming.wiki.wikiExplainer")}</p> `;
}
return nothing;
}
@@ -1242,15 +1242,15 @@ function renderDiaryImportsSection(props: DreamingProps) {
`;
}
function renderMemoryPalaceSection(props: DreamingProps) {
function renderWikiOverviewSection(props: DreamingProps) {
const state = props.viewState;
const palace = props.wikiMemoryPalace;
const clusters = palace?.clusters ?? [];
const overview = props.wikiOverview;
const clusters = overview?.clusters ?? [];
if (props.wikiMemoryPalaceLoading && clusters.length === 0) {
if (props.wikiOverviewLoading && clusters.length === 0) {
return html`
<div class="dreams-diary__empty">
<div class="dreams-diary__empty-text">${t("dreaming.wiki.loadingPalace")}</div>
<div class="dreams-diary__empty-text">${t("dreaming.wiki.loadingWiki")}</div>
</div>
`;
}
@@ -1258,22 +1258,22 @@ function renderMemoryPalaceSection(props: DreamingProps) {
if (clusters.length === 0) {
return html`
<div class="dreams-diary__empty">
<div class="dreams-diary__empty-text">${t("dreaming.wiki.emptyPalace")}</div>
<div class="dreams-diary__empty-hint">${t("dreaming.wiki.emptyPalaceHint")}</div>
<div class="dreams-diary__empty-text">${t("dreaming.wiki.emptyWiki")}</div>
<div class="dreams-diary__empty-hint">${t("dreaming.wiki.emptyWikiHint")}</div>
</div>
`;
}
const clusterIndex = Math.max(0, Math.min(state.diaryPage, clusters.length - 1));
const cluster = expectDefined(clusters[clusterIndex], "selected memory palace cluster");
const totalPages = palace?.totalPages ?? palace?.totalItems ?? 0;
const totalClaims = palace?.totalClaims ?? 0;
const totalQuestions = palace?.totalQuestions ?? 0;
const totalContradictions = palace?.totalContradictions ?? 0;
const pageBreakdown = palace
? formatMemoryPalacePageBreakdown(palace.pageCounts)
const cluster = expectDefined(clusters[clusterIndex], "selected memory overview cluster");
const totalPages = overview?.totalPages ?? overview?.totalItems ?? 0;
const totalClaims = overview?.totalClaims ?? 0;
const totalQuestions = overview?.totalQuestions ?? 0;
const totalContradictions = overview?.totalContradictions ?? 0;
const pageBreakdown = overview
? formatWikiOverviewPageBreakdown(overview.pageCounts)
: t("dreaming.wiki.noPagesYet");
const clusterSummary = formatMemoryPalaceClusterSummary(cluster);
const clusterSummary = formatWikiOverviewClusterSummary(cluster);
const vaultMeta = [
formatPageCount(totalPages),
...(totalClaims > 0 ? [formatClaimRowCount(totalClaims)] : []),
@@ -1300,7 +1300,7 @@ function renderMemoryPalaceSection(props: DreamingProps) {
)}
</div>
<article class="dreams-diary__entry" key="palace-${cluster.key}">
<article class="dreams-diary__entry" key="wiki-${cluster.key}">
<div class="dreams-diary__accent"></div>
<div class="dreams-diary__date">${t("dreaming.wiki.vault")} · ${vaultMeta.join(" · ")}</div>
<div class="dreams-diary__prose">
@@ -1318,26 +1318,22 @@ function renderMemoryPalaceSection(props: DreamingProps) {
</div>
<div class="dreams-diary__insights">
${cluster.items.map((item) => {
const expanded = state.expandedPalaceCards.has(item.pagePath);
const expanded = state.expandedWikiCards.has(item.pagePath);
return html`
<article
class="dreams-diary__insight-card dreams-diary__insight-card--clickable"
data-palace-page=${item.pagePath}
data-wiki-page=${item.pagePath}
@click=${() => {
if (item.kind === "report") {
void openWikiPreview(item.pagePath, props);
return;
}
toggleExpandedCard(
state.expandedPalaceCards,
item.pagePath,
props.onViewStateChange,
);
toggleExpandedCard(state.expandedWikiCards, item.pagePath, props.onViewStateChange);
}}
>
<div class="dreams-diary__insight-topline">
<div class="dreams-diary__insight-title">${item.title}</div>
<span class="dreams-diary__insight-badge dreams-diary__insight-badge--palace">
<span class="dreams-diary__insight-badge dreams-diary__insight-badge--wiki">
${formatKindLabel(item.kind)}
</span>
</div>
@@ -1403,7 +1399,7 @@ function renderMemoryPalaceSection(props: DreamingProps) {
@click=${(event: Event) => {
event.stopPropagation();
toggleExpandedCard(
state.expandedPalaceCards,
state.expandedWikiCards,
item.pagePath,
props.onViewStateChange,
);
@@ -1498,14 +1494,14 @@ function renderDreamDiaryEntries(props: DreamingProps) {
function renderDiarySection(props: DreamingProps) {
const state = props.viewState;
const activeDiarySubTab = state.activeDiarySubTab;
const wikiTabSelected = activeDiarySubTab === "insights" || activeDiarySubTab === "palace";
const wikiTabSelected = activeDiarySubTab === "insights" || activeDiarySubTab === "wiki";
const memoryWikiUnavailable = wikiTabSelected && !props.memoryWikiEnabled;
const diaryError =
activeDiarySubTab === "dreams"
? props.dreamDiaryError
: activeDiarySubTab === "insights"
? props.wikiImportInsightsError
: props.wikiMemoryPalaceError;
: props.wikiOverviewError;
if (diaryError && !memoryWikiUnavailable) {
return html`
<section class="dreams-diary">
@@ -1525,7 +1521,7 @@ function renderDiarySection(props: DreamingProps) {
tabs: [
{ value: "dreams", label: t("dreaming.wiki.dreamsTab") },
{ value: "insights", label: t("dreaming.wiki.insightsTab") },
{ value: "palace", label: t("dreaming.wiki.palaceTab") },
{ value: "wiki", label: t("dreaming.wiki.wikiTab") },
],
ariaLabel: t("dreaming.diary.title"),
panelId: "dream-diary-panel",
@@ -1546,7 +1542,7 @@ function renderDiarySection(props: DreamingProps) {
? props.dreamDiaryLoading
: activeDiarySubTab === "insights"
? props.wikiImportInsightsLoading
: props.wikiMemoryPalaceLoading)}
: props.wikiOverviewLoading)}
@click=${() => {
state.diaryPage = 0;
if (memoryWikiUnavailable) {
@@ -1556,7 +1552,7 @@ function renderDiarySection(props: DreamingProps) {
} else if (activeDiarySubTab === "insights") {
props.onRefreshImports();
} else {
props.onRefreshMemoryPalace();
props.onRefreshWikiOverview();
}
}}
>
@@ -1570,7 +1566,7 @@ function renderDiarySection(props: DreamingProps) {
? props.wikiImportInsightsLoading
? "Reloading…"
: "Reload"
: props.wikiMemoryPalaceLoading
: props.wikiOverviewLoading
? "Reloading…"
: "Reload"}
</button>
@@ -1608,7 +1604,7 @@ function renderDiarySection(props: DreamingProps) {
? renderDreamDiaryEntries(props)
: activeDiarySubTab === "insights"
? renderDiaryImportsSection(props)
: renderMemoryPalaceSection(props)}
: renderWikiOverviewSection(props)}
</div>
${renderWikiPreviewOverlay(props)}
</section>

View File

@@ -930,7 +930,7 @@
color: var(--muted);
}
.dreams-diary__insight-badge--palace {
.dreams-diary__insight-badge--wiki {
color: var(--accent);
background: color-mix(in oklab, var(--accent-subtle) 72%, transparent);
border-color: color-mix(in oklab, var(--accent) 24%, transparent);