diff --git a/CHANGELOG.md b/CHANGELOG.md index 660f813..a06f2c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,12 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Native OpenCode TUI `/memory` display commands for local memory status, recent activity, and help. +- Native OpenCode TUI `/memory-status`, `/memory-list`, and `/memory-help` display commands for local memory statistics, current workspace memory refs, and help. - Package `./tui` export for OpenCode TUI plugin loading. ### Changed - README documents separate server and TUI plugin configuration. +- Recent activity/last TUI commands were removed before release because duplicate-looking slash menu entries were not useful. ### Fixed @@ -22,7 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Notes / Known UX -- `/memory` output is injected as no-reply user-style conversation text and does not call the LLM. +- TUI memory command output is injected as no-reply user-style conversation text and does not call the LLM. ## [1.6.0] - 2026-05-08 diff --git a/README.md b/README.md index e620a2f..32f1e32 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Use it when you want your agent to remember things like: - **Explicit memory triggers** — users can say “remember this”, “記住”, “覚えて”, or “기억해” to save durable facts. - **Compaction-based extraction** — memory extraction piggybacks on OpenCode’s existing compaction flow. - **Numbered memory refs** — compaction can `REINFORCE [M#]` useful memories or safely `REPLACE [M#]` obsolete compaction memories. -- **Native TUI `/memory` display** — show local memory status, recent activity, and help from the OpenCode TUI without an LLM/API call. +- **Native TUI memory commands** — show local memory status, current memory list, and help from the OpenCode TUI without an LLM/API call. - **No manual tools** — memory is injected automatically into the system prompt. - **Quality guards** — filters noisy memories, temporary progress snapshots, stack traces, raw errors, and credentials. - **Retention decay** — keeps the strongest memories in prompt context while older or weaker memories fade out naturally; important and reinforced memories decay more slowly. @@ -48,7 +48,7 @@ Add OpenCode Working Memory to your server plugin config: } ``` -To enable the native TUI `/memory` display command, also add the TUI plugin config: +To enable the native TUI memory display commands, also add the TUI plugin config: `.opencode/tui.json`: @@ -65,13 +65,13 @@ Then restart OpenCode. Server memory activates automatically; TUI memory command The TUI plugin adds display-only local memory commands: -- `/memory` or `/memory status` — show status counts for workspace memory, rendered memories, pending memory, open errors, and recent decisions. -- `/memory activity` or `/memory last` — show recent local evidence activity. Due to the current OpenCode command model, some versions may show separate autocomplete entries instead of typed subargs. -- `/memory help` — show command help. +- `/memory-status` — show status counts for workspace memory, rendered memories, pending memory, open errors, and recent decisions. +- `/memory-list` — show current active workspace memories with display-local `[M1]` refs. +- `/memory-help` — show command help. These commands are read-only and local-only. They read local memory files and inject output with OpenCode's no-reply session prompt path, so they do not make an LLM/API call. -Current OpenCode plugins do not expose an assistant-style command-output surface, so `/memory` output appears as a user-style conversation message. The output becomes part of the session transcript and may be included in future compaction summaries; this is expected command output. +Current OpenCode plugins do not expose an assistant-style command-output surface, so TUI memory command output appears as a user-style conversation message. The output becomes part of the session transcript and may be included in future compaction summaries; this is expected command output. Compaction output already appears through OpenCode's built-in conversation flow. This plugin does not add duplicate compaction notices. diff --git a/src/memory-visibility.ts b/src/memory-visibility.ts index dd32b5e..ab533ec 100644 --- a/src/memory-visibility.ts +++ b/src/memory-visibility.ts @@ -1,20 +1,16 @@ -// No OpenCode SDK or TUI imports. Uses only local file-system reads from workspace memory, session state, pending journal, and evidence log. +// No OpenCode SDK or TUI imports. Uses only local file-system reads from workspace memory, session state, and pending journal. import { readFile } from "node:fs/promises"; -import type { EvidenceEventV1 } from "./evidence-log.ts"; -import { queryEvidenceEvents } from "./evidence-log.ts"; import { sessionStatePath, workspaceKey, workspaceMemoryPath, workspacePendingJournalPath } from "./paths.ts"; import { redactCredentials } from "./redaction.ts"; import type { LongTermMemoryEntry, PendingMemoryJournalStore, SessionState, WorkspaceMemoryStore } from "./types.ts"; import { LONG_TERM_LIMITS } from "./types.ts"; -import { accountWorkspaceMemoryRender } from "./workspace-memory.ts"; +import { accountWorkspaceMemoryCompactionRefs, accountWorkspaceMemoryRender } from "./workspace-memory.ts"; -export type MemoryVisibilityCommand = "status" | "activity" | "help"; +export type MemoryVisibilityCommand = "status" | "list" | "help"; -export type MemoryPreview = { - id: string; - type: LongTermMemoryEntry["type"]; - source: LongTermMemoryEntry["source"]; +type MemoryListItem = { + ref: string; text: string; }; @@ -27,23 +23,17 @@ export type MemoryStatusModel = { pendingJournalMemories: number; openErrors: number; recentDecisions: number; - previews: MemoryPreview[]; }; -export type MemoryActivityModel = { - events: EvidenceEventV1[]; - limit: number; +export type MemoryListModel = { + activeMemories: number; + renderedMemories: number; + omittedActiveMemories: number; + groups: Record; }; -const DEFAULT_ACTIVITY_LIMIT = 10; -const MAX_ACTIVITY_LIMIT = 50; -const MAX_PREVIEWS = 3; const MAX_PREVIEW_CHARS = 120; - -function clampLimit(limit: number | undefined): number { - if (!Number.isFinite(limit)) return DEFAULT_ACTIVITY_LIMIT; - return Math.max(0, Math.min(MAX_ACTIVITY_LIMIT, Math.trunc(limit ?? DEFAULT_ACTIVITY_LIMIT))); -} +const MEMORY_TYPE_ORDER = ["feedback", "project", "decision", "reference"] as const satisfies readonly LongTermMemoryEntry["type"][]; function safePreview(text: string | undefined, maxChars = MAX_PREVIEW_CHARS): string { const clean = redactCredentials(text ?? "").replace(/\s+/g, " ").trim(); @@ -51,19 +41,6 @@ function safePreview(text: string | undefined, maxChars = MAX_PREVIEW_CHARS): st return `${clean.slice(0, Math.max(0, maxChars - 1)).trimEnd()}…`; } -function summarizeReasons(reasons: string[] | undefined): string { - return reasons && reasons.length > 0 ? reasons.join(", ") : "no_reason_recorded"; -} - -function memoryPreview(memory: LongTermMemoryEntry): MemoryPreview { - return { - id: memory.id, - type: memory.type, - source: memory.source, - text: safePreview(memory.text), - }; -} - async function readJSONSnapshot(path: string): Promise { try { return JSON.parse(await readFile(path, "utf8")); @@ -206,67 +183,91 @@ export async function getMemoryStatus(root: string, sessionID: string): Promise< pendingJournalMemories: pendingJournal.entries.length, openErrors: sessionState.openErrors.filter(error => error.status === "open").length, recentDecisions: sessionState.recentDecisions.length, - previews: activeEntries.slice(0, MAX_PREVIEWS).map(memoryPreview), }; } export function formatMemoryStatus(model: MemoryStatusModel): string { - const lines = [ + return [ "## Memory status", "", - `Active memories: ${model.activeMemories}`, - `Rendered in prompt: ${model.renderedInPrompt}`, - `Omitted active memories: ${model.omittedActiveMemories}`, - `Superseded memories: ${model.supersededMemories}`, - `Pending in this session: ${model.pendingInSession}`, - `Pending journal memories: ${model.pendingJournalMemories}`, - `Open errors: ${model.openErrors}`, - `Recent decisions: ${model.recentDecisions}`, - ]; - - if (model.previews.length > 0) { - lines.push("", "Recent active memory previews:"); - for (const preview of model.previews) { - lines.push(`- ${preview.type}/${preview.source}: ${preview.text}`); - } - } else { - lines.push("", "No active workspace memories are stored yet."); - } - - lines.push("", "Local only: no LLM request was made."); - return lines.join("\n"); + "Workspace:", + `- Active memories: ${model.activeMemories}`, + `- Rendered in prompt: ${model.renderedInPrompt}`, + `- Omitted active memories: ${model.omittedActiveMemories}`, + `- Superseded memories: ${model.supersededMemories}`, + "", + "Pending:", + `- Pending in this session: ${model.pendingInSession}`, + `- Pending journal memories: ${model.pendingJournalMemories}`, + "", + "Session:", + `- Open errors: ${model.openErrors}`, + `- Recent decisions: ${model.recentDecisions}`, + "", + `Use /memory-list to view current [M1]-[M${LONG_TERM_LIMITS.maxEntries}] memory refs.`, + "", + "Local only: no LLM request was made.", + ].join("\n"); } -export async function getMemoryActivity(root: string, options: { limit?: number } = {}): Promise { - const limit = clampLimit(options.limit); +function emptyMemoryListGroups(): MemoryListModel["groups"] { + return { feedback: [], project: [], decision: [], reference: [] }; +} + +export async function getMemoryList(root: string): Promise { + const store = await readWorkspaceMemorySnapshot(root); + const accounting = accountWorkspaceMemoryCompactionRefs(store); + const groups = emptyMemoryListGroups(); + const renderedMemoryIds = new Set(accounting.rendered.map(memory => memory.id)); + + for (const ref of accounting.refs) { + if (!renderedMemoryIds.has(ref.memoryId)) continue; + groups[ref.type].push({ + ref: ref.ref, + text: safePreview(ref.textPreview), + }); + } + + const renderedMemories = MEMORY_TYPE_ORDER.reduce((total, type) => total + groups[type].length, 0); + return { - events: await queryEvidenceEvents(root, { newestFirst: true, limit }), - limit, + activeMemories: store.entries.filter(entry => entry.status !== "superseded").length, + renderedMemories, + omittedActiveMemories: accounting.omitted.filter(item => item.memory.status !== "superseded").length, + groups, }; } -function formatActivityEvent(event: EvidenceEventV1): string { - const time = event.createdAt || "unknown_time"; - const memoryType = event.memory?.type ? ` ${event.memory.type}` : ""; - const memoryId = event.memory?.memoryId ? ` ${event.memory.memoryId}` : ""; - const preview = safePreview(event.textPreview); - const previewText = preview ? ` — ${preview}` : ""; - return `- ${time} — ${event.outcome}/${event.phase}${memoryType}${memoryId} — ${summarizeReasons(event.reasonCodes)}${previewText}`; -} - -export function formatMemoryActivity(model: MemoryActivityModel): string { +export function formatMemoryList(model: MemoryListModel): string { const lines = [ - "## Recent memory activity", + "## Current workspace memories", "", ]; - if (model.events.length === 0) { - lines.push(`No retained memory activity exists in the local evidence log for the last ${model.limit} events.`); - } else { - lines.push(...model.events.map(formatActivityEvent)); + if (model.renderedMemories === 0) { + lines.push("No active workspace memories are stored yet.", "", "Local only: no LLM request was made."); + return lines.join("\n"); } - lines.push("", "Local only: no LLM request was made."); + lines.push("Display refs are local to this output and may change after memory updates.", ""); + + for (const type of MEMORY_TYPE_ORDER) { + const group = model.groups[type]; + if (group.length === 0) continue; + lines.push(`${type}:`); + for (const item of group) { + lines.push(`- [${item.ref}] ${item.text}`); + } + lines.push(""); + } + + lines.push( + `Shown: ${model.renderedMemories} of ${model.activeMemories} active memories.`, + `Omitted active memories: ${model.omittedActiveMemories}.`, + "", + "Local only: no LLM request was made.", + ); + return lines.join("\n"); } @@ -274,17 +275,12 @@ export function formatMemoryHelp(): string { return [ "## Memory help", "", - "Available display commands:", - "- /memory status — show local workspace/session memory counts.", - "- /memory activity — show recent local memory evidence activity.", - "- /memory last — alias for /memory activity.", - "- /memory help — show this help text.", + "Commands:", + "- /memory-status — show local memory statistics.", + `- /memory-list — show current workspace memories as display-local [M1]-[M${LONG_TERM_LIMITS.maxEntries}] refs.`, + "- /memory-help — show this help.", "", - "Compaction output already appears in the conversation through OpenCode's built-in flow.", - "This command reads local memory files and does not call the LLM.", - "Future commands such as /memory delete and /memory edit are not available in v1.6.1.", - "", - "Local only: no LLM request was made.", + "These commands are read-only, local-only, and do not call the LLM.", ].join("\n"); } @@ -292,8 +288,8 @@ export async function renderMemoryCommand(root: string, sessionID: string, comma switch (command) { case "status": return formatMemoryStatus(await getMemoryStatus(root, sessionID)); - case "activity": - return formatMemoryActivity(await getMemoryActivity(root)); + case "list": + return formatMemoryList(await getMemoryList(root)); case "help": return formatMemoryHelp(); default: diff --git a/src/tui-plugin.ts b/src/tui-plugin.ts index 259eced..7c4119e 100644 --- a/src/tui-plugin.ts +++ b/src/tui-plugin.ts @@ -57,7 +57,7 @@ function currentRoute(api: TuiPluginApi): TuiRouteCurrent { function commandFromValue(value: string): MemoryVisibilityCommand { if (value === "memory.status") return "status"; - if (value === "memory.activity" || value === "memory.last") return "activity"; + if (value === "memory.list") return "list"; if (value === "memory.help") return "help"; return "help"; } @@ -115,34 +115,26 @@ function memoryCommands(api: TuiPluginApi): TuiCommand[] { { title: "Memory status", value: "memory.status", - description: "Show working memory status in the current session.", + description: "Show working memory statistics in the current session.", category: "Memory", suggested: true, - slash: { name: "memory", aliases: ["mem"] }, + slash: { name: "memory-status" }, onSelect: (dialog?: DialogContext) => injectMemoryOutput(api, "memory.status", dialog), }, { - title: "Memory activity", - value: "memory.activity", - description: "Show recent working memory activity.", + title: "Memory list", + value: "memory.list", + description: "Show current workspace memories with display-local refs.", category: "Memory", - slash: { name: "memory", aliases: ["mem"] }, - onSelect: (dialog?: DialogContext) => injectMemoryOutput(api, "memory.activity", dialog), - }, - { - title: "Memory last", - value: "memory.last", - description: "Show recent working memory activity.", - category: "Memory", - slash: { name: "memory", aliases: ["mem"] }, - onSelect: (dialog?: DialogContext) => injectMemoryOutput(api, "memory.last", dialog), + slash: { name: "memory-list" }, + onSelect: (dialog?: DialogContext) => injectMemoryOutput(api, "memory.list", dialog), }, { title: "Memory help", value: "memory.help", description: "Show working memory help.", category: "Memory", - slash: { name: "memory", aliases: ["mem"] }, + slash: { name: "memory-help" }, onSelect: (dialog?: DialogContext) => injectMemoryOutput(api, "memory.help", dialog), }, ]; diff --git a/tests/memory-visibility.test.ts b/tests/memory-visibility.test.ts index 32e6099..f3f27d5 100644 --- a/tests/memory-visibility.test.ts +++ b/tests/memory-visibility.test.ts @@ -3,18 +3,16 @@ import assert from "node:assert/strict"; import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { dirname, join } from "node:path"; import { tmpdir } from "node:os"; -import type { EvidenceEventInput } from "../src/evidence-log.ts"; -import { appendEvidenceEvents } from "../src/evidence-log.ts"; import { appendPendingMemories } from "../src/pending-journal.ts"; import { saveSessionState } from "../src/session-state.ts"; import type { LongTermMemoryEntry, WorkspaceMemoryStore } from "../src/types.ts"; import { workspaceMemoryPath } from "../src/paths.ts"; import { saveWorkspaceMemory } from "../src/workspace-memory.ts"; import { - formatMemoryActivity, formatMemoryHelp, + formatMemoryList, formatMemoryStatus, - getMemoryActivity, + getMemoryList, getMemoryStatus, renderMemoryCommand, } from "../src/memory-visibility.ts"; @@ -38,18 +36,6 @@ function memory(id: string, text: string, overrides: Partial = {}): EvidenceEventInput { - return { - type: "promotion_promoted", - phase: "promotion", - outcome: "promoted", - reasonCodes: ["new_workspace_entry"], - memory: { memoryId: "mem-a", type: "decision", source: "compaction", status: "active" }, - textPreview: "Use npm test before release", - ...overrides, - }; -} - test("formats status counts from workspace, session, and pending journal stores", async () => { const root = await tempRoot(); try { @@ -92,14 +78,21 @@ test("formats status counts from workspace, session, and pending journal stores" const output = formatMemoryStatus(await getMemoryStatus(root, "ses_status")); assert.match(output, /^## Memory status/); - assert.match(output, /Active memories: 2/); - assert.match(output, /Rendered in prompt: 1/); - assert.match(output, /Pending in this session: 1/); - assert.match(output, /Pending journal memories: 1/); - assert.match(output, /Open errors: 1/); - assert.match(output, /Recent decisions: 1/); + assert.match(output, /Workspace:/); + assert.match(output, /- Active memories: 2/); + assert.match(output, /- Rendered in prompt: 1/); + assert.match(output, /- Omitted active memories: 1/); + assert.match(output, /- Superseded memories: 1/); + assert.match(output, /Pending:/); + assert.match(output, /- Pending in this session: 1/); + assert.match(output, /- Pending journal memories: 1/); + assert.match(output, /Session:/); + assert.match(output, /- Open errors: 1/); + assert.match(output, /- Recent decisions: 1/); + assert.match(output, /Use \/memory-list to view current \[M1\]-\[M28\] memory refs\./); assert.match(output, /Local only: no LLM request was made\./); - assert.equal(output.includes("sushi"), false, "credential-like previews should be redacted"); + assert.equal(output.includes("Recent active memory previews"), false); + assert.equal(output.includes("sushi"), false, "status output should not include memory previews"); } finally { await rm(root, { recursive: true, force: true }); } @@ -125,63 +118,87 @@ test("getMemoryStatus redacts previews without rewriting workspace memory", asyn const output = formatMemoryStatus(await getMemoryStatus(root, "ses_readonly")); const after = await readFile(path, "utf8"); - assert.match(output, /Active memories: 1/); - assert.equal(output.includes("sushi"), false, "status output should redact credential-like previews"); + assert.match(output, /- Active memories: 1/); + assert.equal(output.includes("Recent active memory previews"), false); + assert.equal(output.includes("sushi"), false, "status output should not include memory previews"); assert.equal(after, before, "status display must not persist normalization, migration, or redaction changes"); } finally { await rm(root, { recursive: true, force: true }); } }); -test("formats recent memory activity newest first with reason summaries", async () => { +test("formats current workspace memories grouped by type with display-local refs", async () => { const root = await tempRoot(); try { - await appendEvidenceEvents(root, [ - evidence({ - type: "render_omitted", - phase: "render", - outcome: "omitted", - reasonCodes: ["char_budget"], - memory: { memoryId: "old-render", type: "reference", source: "compaction", status: "active" }, - textPreview: "Older preview", - }), - evidence({ - type: "promotion_promoted", - phase: "promotion", - outcome: "promoted", - reasonCodes: ["new_workspace_entry"], - memory: { memoryId: "new-memory", type: "decision", source: "explicit", status: "active" }, - textPreview: "Newest password: sushi preview", - }), - ]); + const now = new Date().toISOString(); + await saveWorkspaceMemory(root, { + version: 1, + workspace: { root, key: "test" }, + limits: { maxRenderedChars: 3600, maxEntries: 28 }, + entries: [ + memory("mem-feedback", "Remember password: sushi for the fake test.", { type: "feedback" }), + memory("mem-project", "Project memory should render in its own group.", { type: "project" }), + memory("mem-decision", "Decision memory should render in its own group.", { type: "decision" }), + memory("mem-reference", "Reference memory should render in its own group.", { type: "reference" }), + memory("mem-superseded", "Superseded memory should not be active", { type: "reference", status: "superseded" }), + ], + migrations: [], + updatedAt: now, + }); - const output = formatMemoryActivity(await getMemoryActivity(root, { limit: 2 })); + const output = formatMemoryList(await getMemoryList(root)); - assert.match(output, /^## Recent memory activity/); - assert.ok(output.indexOf("promoted") < output.indexOf("omitted"), "newest event should be formatted first"); - assert.match(output, /new_workspace_entry/); - assert.match(output, /char_budget/); - assert.equal(output.includes("sushi"), false, "activity previews should be redacted"); + assert.match(output, /^## Current workspace memories/); + assert.match(output, /Display refs are local to this output/); + assert.match(output, /feedback:\n- \[M\d+\]/); + assert.match(output, /project:\n- \[M\d+\]/); + assert.match(output, /decision:\n- \[M\d+\]/); + assert.match(output, /reference:\n- \[M\d+\]/); + assert.match(output, /Shown: \d+ of \d+ active memories\./); + assert.match(output, /Shown: 4 of 4 active memories\./); + assert.match(output, /Omitted active memories: 0\./); + assert.equal(output.includes("[M1]"), true, "at least one display-local ref should render"); + assert.equal(output.includes("sushi"), false, "list previews should redact credential-like text"); + assert.equal(output.includes("Superseded memory should not be active"), false); } finally { await rm(root, { recursive: true, force: true }); } }); -test("formats empty activity state", () => { - const output = formatMemoryActivity({ events: [], limit: 10 }); - assert.match(output, /^## Recent memory activity/); - assert.match(output, /No retained memory activity exists/); +test("formats empty memory list state", () => { + const output = formatMemoryList({ + activeMemories: 0, + renderedMemories: 0, + omittedActiveMemories: 0, + groups: { feedback: [], project: [], decision: [], reference: [] }, + }); + assert.match(output, /^## Current workspace memories/); + assert.match(output, /No active workspace memories are stored yet\./); + assert.match(output, /Local only: no LLM request was made\./); + assert.equal(output.includes("feedback:"), false); }); test("formats help text for available display commands", () => { const output = formatMemoryHelp(); assert.match(output, /^## Memory help/); - assert.match(output, /\/memory status/); - assert.match(output, /\/memory activity/); - assert.match(output, /\/memory last/); - assert.match(output, /\/memory help/); - assert.match(output, /Future commands such as \/memory delete and \/memory edit are not available in v1\.6\.1\./); - assert.match(output, /does not call the LLM/); + assert.match(output, /\/memory-status/); + assert.match(output, /\/memory-list/); + assert.match(output, /\/memory-help/); + assert.equal(output.includes("/memory activity"), false); + assert.equal(output.includes("/memory last"), false); + assert.equal(output.includes("/memory status"), false); + assert.equal(output.includes("/memory help"), false); + assert.match(output, /do not call the LLM/); +}); + +test("renderMemoryCommand routes list output", async () => { + const root = await tempRoot(); + try { + const output = await renderMemoryCommand(root, "ses_list", "list"); + assert.match(output, /^## Current workspace memories/); + } finally { + await rm(root, { recursive: true, force: true }); + } }); test("renderMemoryCommand falls back to help for unknown command values", async () => { diff --git a/tests/tui-plugin.test.ts b/tests/tui-plugin.test.ts index 638e13f..27024bb 100644 --- a/tests/tui-plugin.test.ts +++ b/tests/tui-plugin.test.ts @@ -95,10 +95,17 @@ const { MemoryTuiPlugin } = await import("../src/tui-plugin.ts"); // Tests // --------------------------------------------------------------------------- -test("registers /memory slash command", async () => { +test("registers three unique hyphenated memory slash commands", async () => { const api = makeMockTuiApi({ route: { name: "session", params: { sessionID: "ses_1" } } }); await MemoryTuiPlugin(api as any, undefined, { id: "test", source: "file", spec: "test", target: "./tui", first_time: Date.now(), last_time: Date.now(), time_changed: Date.now(), load_count: 1, fingerprint: "test", state: "first" }); - assert.ok(api.commands.some(command => command.slash?.name === "memory")); + + const slashNames = api.commands.map(command => command.slash?.name).filter(Boolean); + assert.deepEqual(slashNames, ["memory-status", "memory-list", "memory-help"]); + assert.deepEqual(api.commands.map(command => command.slash), [{ name: "memory-status" }, { name: "memory-list" }, { name: "memory-help" }]); + assert.equal(new Set(slashNames).size, slashNames.length); + assert.deepEqual(api.commands.map(command => command.value), ["memory.status", "memory.list", "memory.help"]); + assert.equal(api.commands.some(command => command.value === "memory.activity"), false); + assert.equal(api.commands.some(command => command.value === "memory.last"), false); }); test("injects no-reply text into the active session", async () => { @@ -114,15 +121,15 @@ test("injects no-reply text into the active session", async () => { assert.equal(api.prompts[0].parts![0].synthetic, undefined); }); -test("routes memory subcommands to status, activity, and help output", async () => { +test("routes memory commands to status, list, and help output", async () => { const api = makeMockTuiApi({ route: { name: "session", params: { sessionID: "ses_1" } } }); await MemoryTuiPlugin(api as any, undefined, { id: "test", source: "file", spec: "test", target: "./tui", first_time: Date.now(), last_time: Date.now(), time_changed: Date.now(), load_count: 1, fingerprint: "test", state: "first" }); await selectCommand(api, "memory.status"); assert.match(api.prompts.at(-1)?.parts?.[0]?.text ?? "", /^## Memory status/); - await selectCommand(api, "memory.activity"); - assert.match(api.prompts.at(-1)?.parts?.[0]?.text ?? "", /^## Recent memory activity/); + await selectCommand(api, "memory.list"); + assert.match(api.prompts.at(-1)?.parts?.[0]?.text ?? "", /^## Current workspace memories/); await selectCommand(api, "memory.help"); assert.match(api.prompts.at(-1)?.parts?.[0]?.text ?? "", /^## Memory help/);