mirror of
https://github.com/sdwolf4103/opencode-working-memory.git
synced 2026-06-02 06:19:36 +02:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e708e77e61 | |||
| 9114b57dc1 | |||
| 2ff17ea1b3 | |||
| 65b3b2f2c3 | |||
| 49bf866de2 |
@@ -5,6 +5,27 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.6.1] - 2026-05-08
|
||||
|
||||
### Added
|
||||
|
||||
- Native OpenCode TUI `/memory` submenu for local memory statistics, searchable 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.
|
||||
- Pre-release hyphenated TUI commands were consolidated into `/memory` because native submenu/list dialogs provide better bounded navigation with less slash-menu clutter.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Replaced a literal NUL byte in `workspace-memory.ts` regex source with a `\0` escape so source search tools treat the file as text.
|
||||
|
||||
### Notes / Known UX
|
||||
|
||||
- TUI memory command output opens in transcript-free native TUI dialogs and does not call the LLM.
|
||||
|
||||
## [1.6.0] - 2026-05-08
|
||||
|
||||
### Added
|
||||
|
||||
@@ -23,28 +23,73 @@ Use it when you want your agent to remember things like:
|
||||
- Important file paths or references
|
||||
- Current active files and unresolved errors
|
||||
|
||||
## Features
|
||||
## What You Get
|
||||
|
||||
- **Workspace memory** — durable project facts, preferences, decisions, and references across sessions.
|
||||
- **Hot session state** — active files, open errors, and current working context for the current session.
|
||||
- **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.
|
||||
- **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.
|
||||
| Need | Feature |
|
||||
|---|---|
|
||||
| Remember durable context | Workspace memory keeps project facts, preferences, decisions, and references across sessions. |
|
||||
| Capture what matters | Say `remember this` or `記住` to explicitly save important rules and preferences. |
|
||||
| Inspect memory locally | Use `/memory` in the OpenCode TUI to browse status, help, and searchable current `[M#]` memories. |
|
||||
| Stay out of the way | Memory is injected automatically and piggybacks on OpenCode compaction — no manual tools, no extra LLM/API calls. |
|
||||
| Keep memory clean | Quality guards filter noise, redact credentials, dedupe repeats, and let weak memories fade. |
|
||||
|
||||
```text
|
||||
remember this ──► workspace memory ──► /memory
|
||||
▲ │ searchable [M#] refs
|
||||
│ ▼
|
||||
compaction ─────► reinforce / replace ──► selective prompt context
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
Add OpenCode Working Memory to your OpenCode config:
|
||||
New users: add OpenCode Working Memory to both OpenCode plugin configs.
|
||||
|
||||
`.opencode/opencode.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"plugin": ["opencode-working-memory"]
|
||||
}
|
||||
```
|
||||
|
||||
Then restart OpenCode. It activates automatically.
|
||||
`.opencode/tui.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://opencode.ai/tui.json",
|
||||
"plugin": ["opencode-working-memory"]
|
||||
}
|
||||
```
|
||||
|
||||
Existing users: keep your current `.opencode/opencode.json` config and add only the `.opencode/tui.json` block above to enable the native `/memory` TUI menu.
|
||||
|
||||
Then restart OpenCode. Memory activates automatically, and `/memory` appears in the TUI slash command menu.
|
||||
|
||||
## Native TUI Memory Menu
|
||||
|
||||
The TUI plugin adds one display-only local memory command:
|
||||
|
||||
- `/memory` — open a native memory submenu.
|
||||
|
||||
Submenu entries:
|
||||
|
||||
- Status — show status counts for workspace memory, rendered memories, pending memory, open errors, and recent decisions.
|
||||
- Current memories — browse a searchable grouped list of current active workspace memories with display-local `[M1]` refs.
|
||||
- Help — show command help.
|
||||
|
||||
This menu is read-only and local-only. It reads local memory files and opens native TUI dialogs, so it does not create conversation history entries and does not make an LLM/API call.
|
||||
|
||||
```text
|
||||
/memory
|
||||
├─ Status
|
||||
├─ Current memories ← searchable, grouped [M#] refs
|
||||
└─ Help
|
||||
```
|
||||
|
||||
Use `/memory` when you want to inspect what the agent currently remembers without asking the model or polluting the transcript.
|
||||
|
||||
Compaction output already appears through OpenCode's built-in conversation flow. This plugin does not add duplicate compaction notices.
|
||||
|
||||
## How It Works
|
||||
|
||||
@@ -148,112 +193,64 @@ Memories decay over time. The strongest stay visible in the prompt; weaker ones
|
||||
|
||||
## Explicit Memory Triggers
|
||||
|
||||
You can explicitly ask the agent to remember durable facts.
|
||||
|
||||
Examples:
|
||||
Most memory is extracted automatically during compaction. When something is especially important, tell the agent directly:
|
||||
|
||||
```md
|
||||
Remember this: we prefer Vitest for new frontend tests.
|
||||
記住:這個 repo 發 release 前要先跑 npm test。
|
||||
覚えておいて: API clients should use the shared retry helper.
|
||||
기억해줘: this project uses pnpm, not npm.
|
||||
```
|
||||
|
||||
Supported trigger languages include:
|
||||
Use explicit triggers for stable preferences, project rules, architecture decisions, or important references. Then inspect active workspace memory with:
|
||||
|
||||
| Language | Examples |
|
||||
|---|---|
|
||||
| English | `remember this`, `save to memory`, `from now on`, `my preference` |
|
||||
| Chinese | `記住`, `记住`, `記得`, `请帮我记住` |
|
||||
| Japanese | `覚えて`, `覚えておいて`, `メモして` |
|
||||
| Korean | `기억해`, `기억해줘`, `메모해줘` |
|
||||
|
||||
Negative requests are respected too:
|
||||
|
||||
```md
|
||||
Don't remember this.
|
||||
不要記住這個。
|
||||
覚えないで。
|
||||
기억하지 마.
|
||||
```text
|
||||
/memory → Current memories
|
||||
```
|
||||
|
||||
Avoid saving:
|
||||
Trigger phrases include `remember this`, `save to memory`, `from now on`, `my preference`, `記住`, `記得`, `覚えて`, and `기억해`.
|
||||
|
||||
- Secrets, passwords, tokens, or credentials
|
||||
- Temporary progress updates
|
||||
- Raw command output
|
||||
- Short-lived session details
|
||||
Negative requests are respected too: `Don't remember this`, `不要記住這個`, `覚えないで`, `기억하지 마`.
|
||||
|
||||
Avoid asking memory to save secrets, temporary progress, raw command output, or short-lived session details.
|
||||
|
||||
## Quality Guards
|
||||
|
||||
OpenCode Working Memory tries to keep memory useful and low-noise.
|
||||
**Good memory is selective memory.**
|
||||
|
||||
It includes guards for:
|
||||
OpenCode Working Memory is designed to be selective. Its strength is not storing more; it is keeping the prompt focused on durable facts that still help.
|
||||
|
||||
- Credential redaction
|
||||
- Duplicate memory cleanup
|
||||
- Accounting for promoted, absorbed, superseded, and rejected memories
|
||||
- Strength-based retention so useful memories stay visible without hard age pruning
|
||||
- Filtering stack traces, git hashes, raw errors, and noisy path-heavy facts
|
||||
- Rejecting temporary project progress snapshots
|
||||
It protects memory quality in three ways:
|
||||
|
||||
- **Selective** — filters temporary progress, raw errors, stack traces, git hashes, noisy debug fragments, and duplicate restatements.
|
||||
- **Safe** — redacts credentials and protects manual or explicit memories from unsafe automatic replacement.
|
||||
- **Diagnosable** — tracks promoted, absorbed, superseded, rejected, reinforced, and replaced memory outcomes.
|
||||
|
||||
The goal is to remember durable facts, not every detail.
|
||||
|
||||
**Good memory is selective memory.**
|
||||
|
||||
Historical cleanup is intentionally conservative: extraction-time filtering may reject more aggressively, but one-time migration cleanup only supersedes high-confidence garbage patterns. This protects existing durable memories written in declarative style, such as "API endpoint is X" or "Product branding is Y".
|
||||
|
||||
### Numbered Memory Refs
|
||||
|
||||
During compaction, existing workspace memories may be shown as numbered refs such as `[M1]` or `[M2]`. The model can use these refs to maintain memory without duplicating it:
|
||||
During compaction, existing workspace memories may be shown as numbered refs such as `[M1]` or `[M2]`. The model can reinforce a still-useful memory or propose a protected replacement instead of copying the same fact again.
|
||||
|
||||
```md
|
||||
REINFORCE [M1]
|
||||
REPLACE [M2] project Updated durable project fact.
|
||||
```
|
||||
|
||||
- `REINFORCE [M#]` strengthens an existing memory's retention signal without changing its text.
|
||||
- `REPLACE [M#] [type] text` supersedes a safe compaction-sourced memory and adds a replacement.
|
||||
- Manual, explicit, and already-reinforced memories are protected from automatic replacement.
|
||||
- Stale or mismatched numbered refs are rejected instead of mutating the wrong memory.
|
||||
|
||||
Use `memory-diag commands` to inspect command outcomes and `memory-diag revert` to dry-run and apply manual recovery for successful numbered replacements.
|
||||
Protected memories and stale refs are rejected rather than mutated. Use `memory-diag commands` for detailed command outcomes and recovery guidance.
|
||||
|
||||
### Memory Diagnostics CLI
|
||||
|
||||
Use the read-only diagnostics CLI when you want to understand what memory is doing for the current workspace.
|
||||
|
||||
| Question | Command |
|
||||
|---|---|
|
||||
| Is memory healthy? | `npx --package opencode-working-memory memory-diag` or `npx --package opencode-working-memory memory-diag status` |
|
||||
| Why was something rejected? | `npx --package opencode-working-memory memory-diag rejected` |
|
||||
| Where did my memory go? | `npx --package opencode-working-memory memory-diag missing` |
|
||||
| Why is this memory shown or hidden? | `npx --package opencode-working-memory memory-diag explain <memory-id>` |
|
||||
| How are numbered memory commands behaving? | `npx --package opencode-working-memory memory-diag commands` |
|
||||
| Revert a numbered replacement? | `npx --package opencode-working-memory memory-diag revert --memory <replacement-memory-id>` |
|
||||
|
||||
Global options:
|
||||
|
||||
- `--workspace <path>` — inspect another workspace; defaults to the current directory.
|
||||
- `--verbose` — show detailed diagnostics.
|
||||
- `--json` — print machine-readable output where supported.
|
||||
|
||||
Examples:
|
||||
For deeper troubleshooting, use the read-only `memory-diag` CLI:
|
||||
|
||||
```bash
|
||||
npx --package opencode-working-memory memory-diag status
|
||||
npx --package opencode-working-memory memory-diag rejected --verbose
|
||||
npx --package opencode-working-memory memory-diag missing --workspace /path/to/project
|
||||
npx --package opencode-working-memory memory-diag status --json
|
||||
npx --package opencode-working-memory memory-diag commands --verbose
|
||||
npx --package opencode-working-memory memory-diag revert --memory <replacement-memory-id>
|
||||
npx --package opencode-working-memory memory-diag rejected
|
||||
npx --package opencode-working-memory memory-diag missing
|
||||
npx --package opencode-working-memory memory-diag explain <memory-id>
|
||||
```
|
||||
|
||||
`memory-diag revert` is dry-run by default. Add `--apply` only after reviewing the planned original/replacement status changes.
|
||||
|
||||
The npm package is opencode-working-memory; the installed bin is memory-diag, so package-qualified npx avoids resolving a different package named memory-diag.
|
||||
|
||||
Maintainer-only diagnostics and cleanup commands are intentionally not documented here. Future work: move those internal commands to `docs/development.md`.
|
||||
See [Diagnostics](docs/diagnostics.md) for the full command reference, numbered memory command reports, and dry-run recovery workflow.
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -281,18 +278,9 @@ Current focus:
|
||||
|
||||
- [Architecture Overview](docs/architecture.md)
|
||||
- [Configuration](docs/configuration.md)
|
||||
- [Diagnostics](docs/diagnostics.md)
|
||||
- [Installation Guide](docs/installation.md)
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
git clone https://github.com/sdwolf4103/opencode-working-memory.git
|
||||
cd opencode-working-memory
|
||||
npm install
|
||||
npm test
|
||||
npm run typecheck
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- OpenCode plugin API `>=1.2.0 <2.0.0`
|
||||
|
||||
@@ -1,5 +1,52 @@
|
||||
# Release Notes
|
||||
|
||||
## 1.6.1 (2026-05-08)
|
||||
|
||||
### Native TUI Memory Menu
|
||||
|
||||
This release adds a native OpenCode TUI memory menu so users can inspect local working memory without asking the model and without adding command output to the conversation transcript.
|
||||
|
||||
Open `/memory` in the TUI to browse memory status, current workspace memories, and help from native dialogs.
|
||||
|
||||
> Memory should stay visible when you need it — and stay out of the transcript when you are only inspecting it.
|
||||
|
||||
```text
|
||||
/memory
|
||||
│
|
||||
├─ Status
|
||||
│ local counts and memory health
|
||||
│
|
||||
├─ Current memories
|
||||
│ searchable grouped [M#] refs
|
||||
│
|
||||
└─ Help
|
||||
local usage notes
|
||||
```
|
||||
|
||||
### What Changed
|
||||
|
||||
- **Single TUI entry point**: `/memory` opens a native submenu instead of exposing multiple memory slash commands.
|
||||
- **Searchable current memory list**: `Current memories` uses OpenCode's native select dialog for bounded scrolling, filtering, and grouping.
|
||||
- **Transcript-free inspection**: memory status, list, help, empty states, and errors render in native dialogs instead of user-style session messages.
|
||||
- **Server and TUI plugin exports**: the package exposes `./server` and `./tui` entry points for OpenCode plugin loading.
|
||||
- **User docs refreshed**: README highlights the `/memory` workflow and moves the full diagnostics CLI reference to `docs/diagnostics.md`.
|
||||
|
||||
### Upgrade Notes
|
||||
|
||||
- Add `.opencode/tui.json` if you want the native `/memory` TUI menu. Existing server-only configuration continues to work.
|
||||
- Restart OpenCode after adding the TUI plugin config.
|
||||
- The TUI menu is read-only and local-only. It does not call the LLM.
|
||||
- Individual memory row selection is intentionally a no-op in this release; use the list for inspection and search.
|
||||
|
||||
### Validation
|
||||
|
||||
- `npm run typecheck` — `TYPECHECK_PASS`
|
||||
- `npm test` — 421 tests passing, `TEST_PASS`
|
||||
- `npm pack --dry-run`
|
||||
- Real OpenCode TUI smoke test for `/memory` menu, searchable current memories, and transcript-free output.
|
||||
|
||||
---
|
||||
|
||||
## 1.6.0 (2026-05-08)
|
||||
|
||||
### Numbered Memory Refs
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# Memory Diagnostics
|
||||
|
||||
Use the read-only diagnostics CLI when you want to understand what OpenCode Working Memory is doing for the current workspace.
|
||||
|
||||
The npm package is `opencode-working-memory`; the installed bin is `memory-diag`, so package-qualified `npx` avoids resolving a different package named `memory-diag`.
|
||||
|
||||
## Commands
|
||||
|
||||
| Question | Command |
|
||||
|---|---|
|
||||
| Is memory healthy? | `npx --package opencode-working-memory memory-diag` or `npx --package opencode-working-memory memory-diag status` |
|
||||
| Why was something rejected? | `npx --package opencode-working-memory memory-diag rejected` |
|
||||
| Where did my memory go? | `npx --package opencode-working-memory memory-diag missing` |
|
||||
| Why is this memory shown or hidden? | `npx --package opencode-working-memory memory-diag explain <memory-id>` |
|
||||
| How are numbered memory commands behaving? | `npx --package opencode-working-memory memory-diag commands` |
|
||||
| Revert a numbered replacement? | `npx --package opencode-working-memory memory-diag revert --memory <replacement-memory-id>` |
|
||||
|
||||
## Global Options
|
||||
|
||||
- `--workspace <path>` — inspect another workspace; defaults to the current directory.
|
||||
- `--verbose` — show detailed diagnostics.
|
||||
- `--json` — print machine-readable output where supported.
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
npx --package opencode-working-memory memory-diag status
|
||||
npx --package opencode-working-memory memory-diag rejected --verbose
|
||||
npx --package opencode-working-memory memory-diag missing --workspace /path/to/project
|
||||
npx --package opencode-working-memory memory-diag status --json
|
||||
npx --package opencode-working-memory memory-diag commands --verbose
|
||||
npx --package opencode-working-memory memory-diag revert --memory <replacement-memory-id>
|
||||
```
|
||||
|
||||
## Numbered Memory Command Reports
|
||||
|
||||
Use `memory-diag commands` to inspect `REINFORCE [M#]` and `REPLACE [M#]` outcomes from compaction.
|
||||
|
||||
```bash
|
||||
npx --package opencode-working-memory memory-diag commands
|
||||
npx --package opencode-working-memory memory-diag commands --verbose
|
||||
```
|
||||
|
||||
The report includes successful reinforcements, successful replacements, malformed commands, stale refs, protected replacement blocks, and latest command events in verbose mode.
|
||||
|
||||
## Dry-run Recovery
|
||||
|
||||
`memory-diag revert` is dry-run by default. Add `--apply` only after reviewing the planned original/replacement status changes.
|
||||
|
||||
```bash
|
||||
npx --package opencode-working-memory memory-diag revert --memory <replacement-memory-id>
|
||||
npx --package opencode-working-memory memory-diag revert --memory <replacement-memory-id> --apply
|
||||
```
|
||||
|
||||
You can also target a replacement evidence event directly:
|
||||
|
||||
```bash
|
||||
npx --package opencode-working-memory memory-diag revert --event <event-id>
|
||||
```
|
||||
@@ -0,0 +1,491 @@
|
||||
# Native TUI Memory Command UX Implementation Plan
|
||||
|
||||
> **For agentic workers:** Use `agenthub-writing-plans-skill` to create this plan and `agenthub-executing-plans-skill` to execute it task-by-task. Steps use checkbox (`- [ ]`) syntax. Wave checkpoints are gates.
|
||||
|
||||
**Goal:** Replace the current ambiguous native OpenCode TUI memory slash-command surface with three visibly distinct hyphenated commands before commit/push.
|
||||
|
||||
**User outcome:** OpenCode users see only `/memory-status`, `/memory-list`, and `/memory-help` in slash autocomplete; status shows memory statistics, list shows current active workspace memories as display-local `[M1]` refs, and duplicate recent-activity commands are no longer user-facing.
|
||||
|
||||
**Architecture:** Keep the existing TUI plugin and no-reply session-message injection path. Change only the command registration/routing layer (`src/tui-plugin.ts`) and the local read/format core (`src/memory-visibility.ts`), then update focused tests and docs. Do not add storage, background jobs, LLM calls, command mutation, or a parallel UI surface.
|
||||
|
||||
**Tech stack:** TypeScript ESM on Node >=22.6, OpenCode TUI plugin API, local JSON stores, Node built-in test runner with `--experimental-strip-types`.
|
||||
|
||||
**Scope mode:** COMPLETE for the approved UX correction; no implementation code is changed by this plan.
|
||||
|
||||
---
|
||||
|
||||
## Scope Challenge
|
||||
|
||||
- Existing leverage: Reuse `src/tui-plugin.ts` command registration and `api.client.session.prompt({ noReply: true })`; reuse `src/memory-visibility.ts` local read snapshots, redaction helper, and `accountWorkspaceMemoryRender()`/`accountWorkspaceMemoryCompactionRefs()` accounting instead of creating a separate diagnostic subsystem.
|
||||
- Minimum complete change: Register three unique top-level slash names, add/route a list formatter, remove visible activity/last commands, make status stats-only, and update README/CHANGELOG/tests to match the new public surface.
|
||||
- Scope smell check: Expected code/docs/test touch set is 6 files: `src/tui-plugin.ts`, `src/memory-visibility.ts`, `tests/tui-plugin.test.ts`, `tests/memory-visibility.test.ts`, `README.md`, and `CHANGELOG.md`. `RELEASE_NOTES.md`, `docs/installation.md`, and `docs/configuration.md` do not currently mention the TUI memory commands and should stay unchanged unless implementation reveals new command mentions.
|
||||
- Lake vs ocean: The lake is display-only status/list/help for existing local data. Ocean-sized extras remain out of scope: `/memory delete`, `/memory edit`, stable memory IDs in the TUI, interactive list selection, evidence activity dashboards, assistant-style output APIs, and upstream OpenCode TUI changes.
|
||||
- Out of scope: No activity/last user-facing commands, no `/memory` space-subcommand autocomplete entries, no new aliases unless OpenCode proves they do not create duplicate menu rows, no persistence schema changes, no LLM/API calls, and no server plugin behavior changes.
|
||||
|
||||
## Search and Prior Art
|
||||
|
||||
- Layer 1 choices:
|
||||
- `src/tui-plugin.ts:113-148` currently registers four commands with the same `slash.name: "memory"`, causing OpenCode to show multiple identical `/memory` rows.
|
||||
- `src/tui-plugin.ts:58-63` maps internal values `memory.activity` and `memory.last` to the same `"activity"` command, confirming duplication.
|
||||
- `src/memory-visibility.ts:12` currently exposes `MemoryVisibilityCommand = "status" | "activity" | "help"`; `formatMemoryHelp()` at lines 273-288 documents `/memory activity` and `/memory last`.
|
||||
- `src/memory-visibility.ts:213-238` already formats status counts but also includes preview lines; the approved UX wants status focused on statistics and delegates memory content to `/memory-list`.
|
||||
- `src/workspace-memory.ts:937-997` already has numbered ref accounting (`accountWorkspaceMemoryCompactionRefs`) that returns rendered entries, omitted entries, and `refs` with `M1`, `M2`, ... display labels. This is the best existing source for list refs because it respects the same selection/cap logic as compaction refs.
|
||||
- `tests/tui-plugin.test.ts:98-153` covers TUI registration, no-reply injection, routing, no-session warning, dialog clearing, and injection failure.
|
||||
- `tests/memory-visibility.test.ts:53-195` covers status/activity/help formatting and read-only redaction behavior.
|
||||
- `README.md:64-76` and `CHANGELOG.md:8-25` document the current `/memory` status/activity/help UX and must be updated before commit/push.
|
||||
- Layer 2 choices: None required. Do not add dependencies.
|
||||
- Layer 3 choices: A small `MemoryListModel`/`formatMemoryList()` in `src/memory-visibility.ts` is justified because the TUI needs a user-facing grouped list shape that differs from compaction prompt text.
|
||||
- Eureka findings: OpenCode's current slash menu does not visibly distinguish trailing subcommand text, so the technically elegant `/memory status` model is worse UX than three hyphenated top-level commands for this release.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Components and responsibilities
|
||||
|
||||
- `src/tui-plugin.ts`: Owns visible TUI command names and active-session/no-reply injection. It should register exactly three commands with `slash.name` values `memory-status`, `memory-list`, and `memory-help`; internal `value` strings may remain dot-form (`memory.status`, `memory.list`, `memory.help`) because they are not displayed to users.
|
||||
- `src/memory-visibility.ts`: Owns read-only local models and markdown/plain-text formatting. It should expose command variants `status`, `list`, and `help`; status should report stats only; list should show active rendered workspace memories with display-local `[M#]` refs; help should list only the three public commands. `MemoryVisibilityCommand` is currently consumed by `src/tui-plugin.ts`; verify no other consumers exist before changing/removing exported command variants.
|
||||
- `src/workspace-memory.ts`: No planned edit. Reuse `accountWorkspaceMemoryRender()` for stats and `accountWorkspaceMemoryCompactionRefs()` for capped/ref-capable list selection. If imports need updating, import only existing exported functions.
|
||||
- `tests/tui-plugin.test.ts`: Assert unique slash names and removal of user-facing activity/last registrations.
|
||||
- `tests/memory-visibility.test.ts`: Assert status/list/help output contracts, redaction/truncation, caps, and fallback routing.
|
||||
- `README.md` and `CHANGELOG.md`: Align public docs with the new three-command UX. `RELEASE_NOTES.md` has no 1.6.1 TUI command section today; leave it unchanged unless a later release-notes pass adds one.
|
||||
|
||||
### Data flow
|
||||
|
||||
```text
|
||||
User selects /memory-status, /memory-list, or /memory-help in OpenCode TUI
|
||||
-> src/tui-plugin.ts registered command onSelect
|
||||
-> determine active sessionID from api.route/current session route
|
||||
-> commandFromValue(value) returns "status" | "list" | "help"
|
||||
-> src/memory-visibility.ts renderMemoryCommand(root, sessionID, command)
|
||||
status: read workspace/session/pending snapshots + render accounting counts
|
||||
list: read workspace snapshot + accountWorkspaceMemoryCompactionRefs() + safePreview()
|
||||
help: static command help
|
||||
-> api.client.session.prompt({ sessionID, noReply: true, parts: [{ type: "text", text }] })
|
||||
-> OpenCode renders the report as local no-reply session text; no LLM call is made
|
||||
```
|
||||
|
||||
### Output contracts
|
||||
|
||||
#### `/memory-status`
|
||||
|
||||
Required shape:
|
||||
|
||||
```md
|
||||
## Memory status
|
||||
|
||||
Workspace:
|
||||
- Active memories: <n>
|
||||
- Rendered in prompt: <n>
|
||||
- Omitted active memories: <n>
|
||||
- Superseded memories: <n>
|
||||
|
||||
Pending:
|
||||
- Pending in this session: <n>
|
||||
- Pending journal memories: <n>
|
||||
|
||||
Session:
|
||||
- Open errors: <n>
|
||||
- Recent decisions: <n>
|
||||
|
||||
Use /memory-list to view current [M1]-[M28] memory refs.
|
||||
|
||||
Local only: no LLM request was made.
|
||||
```
|
||||
|
||||
Notes:
|
||||
- Remove preview lines from status.
|
||||
- Keep zero/empty counts visible.
|
||||
- Keep the local-only footer.
|
||||
|
||||
#### `/memory-list`
|
||||
|
||||
Required shape:
|
||||
|
||||
```md
|
||||
## Current workspace memories
|
||||
|
||||
Display refs are local to this output and may change after memory updates.
|
||||
|
||||
feedback:
|
||||
- [M1] <redacted/truncated text>
|
||||
|
||||
project:
|
||||
- [M2] <redacted/truncated text>
|
||||
|
||||
decision:
|
||||
- [M3] <redacted/truncated text>
|
||||
|
||||
reference:
|
||||
- [M4] <redacted/truncated text>
|
||||
|
||||
Shown: <rendered> of <active> active memories.
|
||||
Omitted active memories: <omitted-active>.
|
||||
|
||||
Local only: no LLM request was made.
|
||||
```
|
||||
|
||||
Notes:
|
||||
- Use refs that are explicitly display-local, not stable IDs.
|
||||
- Group by memory type/kind in the existing order: `feedback`, `project`, `decision`, `reference`.
|
||||
- Show only active, non-superseded memories selected by the same caps/budget used for rendered memory refs. The default global cap is 28 (`src/types.ts` via `LONG_TERM_LIMITS.maxEntries`).
|
||||
- Apply `safePreview()` or equivalent credential redaction/truncation to every displayed memory text. Do not dump raw JSON or full unbounded memory text.
|
||||
- Empty state: `No active workspace memories are stored yet.` plus the local-only footer.
|
||||
|
||||
#### `/memory-help`
|
||||
|
||||
Required shape:
|
||||
|
||||
```md
|
||||
## Memory help
|
||||
|
||||
Commands:
|
||||
- /memory-status — show local memory statistics.
|
||||
- /memory-list — show current workspace memories as display-local [M1]-[M28] refs.
|
||||
- /memory-help — show this help.
|
||||
|
||||
These commands are read-only, local-only, and do not call the LLM.
|
||||
```
|
||||
|
||||
Notes:
|
||||
- Do not mention `/memory`, `/memory status`, `/memory activity`, or `/memory last` as available commands.
|
||||
- It is acceptable to keep a short note that mutation commands such as delete/edit are not available, but do not expand scope.
|
||||
|
||||
### Error flow
|
||||
|
||||
- No active session route: preserve existing warning toast behavior and do not write a message.
|
||||
- Local read/format error with a session: preserve existing `## Memory error` stream-visible report.
|
||||
- `api.client.session.prompt()` failure: preserve existing error toast and no retry.
|
||||
- Unknown internal command value: route to help. Do not register unknown/legacy values in the visible command list.
|
||||
|
||||
### Security and permissions
|
||||
|
||||
- Commands are read-only over local memory/session/pending files and write only the user-invoked no-reply session output.
|
||||
- Display memory text only after redaction and truncation.
|
||||
- Do not introduce shell execution, network calls, LLM calls, or file writes to memory stores.
|
||||
- Do not treat display-local `[M#]` refs as authorization or stable identity; they are only labels in the printed list.
|
||||
|
||||
### Performance
|
||||
|
||||
- Status remains O(number of workspace/session/pending entries), using existing bounded stores.
|
||||
- List should format at most the rendered/ref-selected memories and must respect existing caps/budgets; avoid full evidence lifecycle joins.
|
||||
- Removing activity from the visible UX avoids querying/formatting evidence logs during normal command use.
|
||||
|
||||
### Production failure scenarios
|
||||
|
||||
- OpenCode still displays aliases or duplicate slash names unexpectedly: keep only three primary `slash.name` values and avoid aliases until verified.
|
||||
- User expects `/memory` from an unreleased local build: because this is before commit/push, prefer clean UX over compatibility debt; docs should clearly advertise the three hyphenated commands.
|
||||
- Very long memories or credentials appear in stored data: list formatter must redacted/truncate via `safePreview()` and tests should assert credential-like fixture text is absent.
|
||||
- More than 28 active memories exist: list reports shown vs active and omitted count; it must not imply refs cover hidden memories.
|
||||
|
||||
## Backwards Compatibility Stance
|
||||
|
||||
- Treat the current `/memory` space-subcommand surface as pre-public/unshipped for this commit because it produces duplicate-looking menu entries in OpenCode.
|
||||
- Remove visible registrations for `/memory`, `/memory status`, `/memory activity`, `/memory last`, and `/memory help`.
|
||||
- Do not document old spellings.
|
||||
- Internal fallback may continue routing unknown values to help, but do not preserve hidden legacy command entries if OpenCode would show them in autocomplete.
|
||||
- If a reviewer requests aliases, add only after confirming aliases do not create extra duplicate menu rows; otherwise defer aliases to a later OpenCode API capability discussion.
|
||||
|
||||
## File Plan
|
||||
|
||||
- Modify: `src/tui-plugin.ts:58-63` — route `memory.status`, `memory.list`, and `memory.help`; remove activity/last mapping from the public path.
|
||||
- Modify: `src/tui-plugin.ts:113-148` — register exactly three commands with `slash.name` values `memory-status`, `memory-list`, `memory-help`; remove `Memory activity` and `Memory last` command objects.
|
||||
- Modify: `src/memory-visibility.ts:12-40` — change command/model types from status/activity/help to status/list/help; add `MemoryListModel`; remove or unexport activity-only types/functions if no longer used.
|
||||
- Modify: `src/memory-visibility.ts:190-238` — keep stats model but format status as grouped statistics with no previews.
|
||||
- Modify: `src/memory-visibility.ts:240-271` — replace activity reader/formatter with list reader/formatter or remove activity code and add list code nearby.
|
||||
- Modify: `src/memory-visibility.ts:273-288` — update help to list only `/memory-status`, `/memory-list`, `/memory-help`.
|
||||
- Modify: `src/memory-visibility.ts:291-302` — route `"list"` to the new list formatter and remove `"activity"` routing.
|
||||
- Modify: `tests/tui-plugin.test.ts` — assert three unique visible commands and route status/list/help.
|
||||
- Modify: `tests/memory-visibility.test.ts` — replace activity tests with list tests; update status/help assertions.
|
||||
- Modify: `README.md:33,51-76` — update feature copy and Native TUI command docs.
|
||||
- Modify: `CHANGELOG.md:8-25` — amend unreleased/current 1.6.1 entry from status/activity/help to status/list/help and note hyphenated names.
|
||||
- No planned change: `RELEASE_NOTES.md` — no 1.6.1 TUI command mention exists in current evidence.
|
||||
- No planned change: `docs/installation.md`, `docs/configuration.md` — current grep found no TUI command mentions.
|
||||
|
||||
## Test Strategy
|
||||
|
||||
- Framework: Node built-in test runner via `npm test`; TypeScript via `npm run typecheck`.
|
||||
- Unit coverage:
|
||||
- `memory-visibility.ts` status counts with active/superseded/rendered/omitted entries, pending memories, pending journal entries, open errors, and recent decisions; assert no preview section remains.
|
||||
- `memory-visibility.ts` list output with active memories grouped by type, display-local `[M#]` labels, shown/active/omitted summary, redacted credential-like text, empty state, and local-only footer.
|
||||
- `memory-visibility.ts` help text lists only three hyphenated commands and omits `/memory activity` and `/memory last`.
|
||||
- `renderMemoryCommand()` routes `status`, `list`, and `help`; unknown values fall back to help.
|
||||
- TUI integration-style unit coverage:
|
||||
- Registers exactly three command values.
|
||||
- Slash names are exactly `memory-status`, `memory-list`, `memory-help` and unique.
|
||||
- No registered command value is `memory.activity` or `memory.last`.
|
||||
- Selecting `memory.status`, `memory.list`, and `memory.help` injects no-reply text with the expected heading.
|
||||
- Existing no-session warning, dialog clearing, and prompt-injection failure behavior still passes.
|
||||
- Docs verification:
|
||||
- Grep for old public spellings in markdown and source tests after implementation; old spellings should remain only in this plan or intentionally in negative assertions.
|
||||
|
||||
## Implementation Tasks
|
||||
|
||||
## Wave 1: Failing Tests for the New Public Contract
|
||||
|
||||
### Task 1.1: Update TUI command registration/routing tests first
|
||||
|
||||
**Purpose:** Prove the slash command menu no longer contains duplicate-looking `/memory` entries.
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/tui-plugin.test.ts`
|
||||
|
||||
**Behavior:**
|
||||
- Given the TUI plugin registers commands, there are exactly three memory commands.
|
||||
- Given autocomplete displays slash names, the names are unique hyphenated top-level commands.
|
||||
- Given a command is selected, status/list/help route to distinct headings.
|
||||
|
||||
- [ ] **Step 1: Write failing tests**
|
||||
|
||||
Add or update assertions equivalent to:
|
||||
|
||||
```ts
|
||||
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, mockMeta);
|
||||
|
||||
const slashNames = api.commands.map(command => command.slash?.name).filter(Boolean);
|
||||
assert.deepEqual(slashNames, ["memory-status", "memory-list", "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);
|
||||
});
|
||||
```
|
||||
|
||||
Update the routing test to select `memory.list` and expect `## Current workspace memories`.
|
||||
|
||||
- [ ] **Step 2: Run expected failure**
|
||||
|
||||
Run: `node --import ./tests/setup-xdg-data-home.ts --test --experimental-strip-types tests/tui-plugin.test.ts`
|
||||
|
||||
Expected: FAIL because current `src/tui-plugin.ts` registers repeated `slash.name: "memory"` and does not register `memory.list`.
|
||||
|
||||
### Task 1.2: Update memory visibility formatter tests first
|
||||
|
||||
**Purpose:** Lock the approved status/list/help output shape before implementation.
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/memory-visibility.test.ts`
|
||||
|
||||
**Behavior:**
|
||||
- Status is stats-only and points to `/memory-list`.
|
||||
- List prints current active memories grouped by type with display-local refs and redaction.
|
||||
- Help lists only three hyphenated commands.
|
||||
|
||||
- [ ] **Step 1: Write failing tests**
|
||||
|
||||
Required assertions:
|
||||
|
||||
```ts
|
||||
assert.match(output, /^## Memory status/);
|
||||
assert.match(output, /Workspace:/);
|
||||
assert.match(output, /Pending:/);
|
||||
assert.match(output, /Session:/);
|
||||
assert.match(output, /Use \/memory-list to view current \[M1\]-\[M28\] memory refs\./);
|
||||
assert.equal(output.includes("Recent active memory previews"), false);
|
||||
```
|
||||
|
||||
Replace activity tests with list tests that create at least one memory for each type and one superseded memory. The redaction fixture must include at least one short credential-like active memory that is guaranteed to render, such as `Remember password: sushi for the fake test.`, so `output.includes("sushi") === false` proves redaction rather than omission by caps/budget. Assert:
|
||||
|
||||
```ts
|
||||
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.equal(output.includes("sushi"), false);
|
||||
assert.equal(output.includes("Superseded memory should not be active"), false);
|
||||
```
|
||||
|
||||
Update help assertions:
|
||||
|
||||
```ts
|
||||
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);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run expected failure**
|
||||
|
||||
Run: `node --import ./tests/setup-xdg-data-home.ts --test --experimental-strip-types tests/memory-visibility.test.ts`
|
||||
|
||||
Expected: FAIL because current implementation still exposes activity/last and lacks list output.
|
||||
|
||||
### Wave 1 Checkpoint
|
||||
|
||||
- [ ] Confirm both focused test files fail for the expected missing behavior, not unrelated setup errors.
|
||||
- [ ] Do not proceed if failures indicate fixture/storage regressions unrelated to command UX.
|
||||
|
||||
## Wave 2: Implement the Visibility Core
|
||||
|
||||
### Task 2.1: Add list model/formatter and simplify status/help
|
||||
|
||||
**Purpose:** Make the local rendering core match the approved command set independently of TUI registration.
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/memory-visibility.ts`
|
||||
|
||||
**Implementation instructions:**
|
||||
- Change `MemoryVisibilityCommand` to `"status" | "list" | "help"`.
|
||||
- Add a `MemoryListModel` that contains:
|
||||
- `activeMemories: number`
|
||||
- `renderedMemories: number`
|
||||
- `omittedActiveMemories: number`
|
||||
- `groups: Record<LongTermMemoryEntry["type"], Array<{ ref: string; text: string }>>` or equivalent typed structure preserving `feedback`, `project`, `decision`, `reference` order.
|
||||
- Implement `getMemoryList(root: string)` using `readWorkspaceMemorySnapshot(root)` and `accountWorkspaceMemoryCompactionRefs(store)`.
|
||||
- Count active memories from the raw/snapshot store by `status !== "superseded"`.
|
||||
- Use accounting `refs` plus `rendered` entries to build display-local refs.
|
||||
- Only use the `refs`, `rendered`, and `omitted` fields from `accountWorkspaceMemoryCompactionRefs()` for the list formatter; discard its `evidence` and `prompt` fields and do not call `appendEvidenceEvents()` from `/memory-list`.
|
||||
- Display text must pass through `safePreview(ref.textPreview)`.
|
||||
- `omittedActiveMemories` should count only `accounting.omitted` entries whose memory is not superseded; `accounting.omitted` can include superseded entries from selection accounting and those must not inflate active omissions.
|
||||
- Implement `formatMemoryList(model)` with the required output contract.
|
||||
- Update `formatMemoryStatus()` to remove preview output and use grouped stat sections.
|
||||
- Update `formatMemoryHelp()` to list only `/memory-status`, `/memory-list`, `/memory-help`.
|
||||
- Update `renderMemoryCommand()` switch to route `"list"`.
|
||||
- Remove `MemoryActivityModel`, `DEFAULT_ACTIVITY_LIMIT`, `MAX_ACTIVITY_LIMIT`, `clampLimit`, `getMemoryActivity()`, `formatMemoryActivity()`, `formatActivityEvent()`, and `summarizeReasons()` if they become unused. Also remove unused `EvidenceEventV1`/`queryEvidenceEvents` imports.
|
||||
- Before deleting activity-only exports/helpers, grep `src/` for `MemoryActivityModel`, `getMemoryActivity`, `formatMemoryActivity`, `formatActivityEvent`, and `summarizeReasons()`; remove them only after confirming there are no cross-module consumers outside `memory-visibility.ts`.
|
||||
|
||||
- [ ] **Step 1: Implement minimal code**
|
||||
|
||||
Do not modify `src/workspace-memory.ts` unless TypeScript proves an export is missing. Current evidence shows `accountWorkspaceMemoryCompactionRefs` is exported.
|
||||
|
||||
- [ ] **Step 2: Run focused verification**
|
||||
|
||||
Run: `node --import ./tests/setup-xdg-data-home.ts --test --experimental-strip-types tests/memory-visibility.test.ts`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 3: Run typecheck for dead imports/types**
|
||||
|
||||
Run: `npm run typecheck`
|
||||
|
||||
Expected: PASS and output includes `TYPECHECK_PASS`.
|
||||
|
||||
### Wave 2 Checkpoint
|
||||
|
||||
- [ ] Status output contains no memory preview content.
|
||||
- [ ] List output includes display-local `[M#]` refs, grouped by type, with redacted/truncated text.
|
||||
- [ ] Activity/last formatter exports are either removed or no longer referenced by user-facing code.
|
||||
|
||||
## Wave 3: Implement the TUI Command Surface
|
||||
|
||||
### Task 3.1: Register only three hyphenated slash commands
|
||||
|
||||
**Purpose:** Fix OpenCode autocomplete by ensuring visible slash names are unique top-level commands.
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/tui-plugin.ts`
|
||||
|
||||
**Implementation instructions:**
|
||||
- Update `commandFromValue(value)`:
|
||||
- `memory.status` -> `"status"`
|
||||
- `memory.list` -> `"list"`
|
||||
- `memory.help` -> `"help"`
|
||||
- default -> `"help"`
|
||||
- Update `memoryCommands(api)` to return exactly three objects:
|
||||
- title `Memory status`, value `memory.status`, description `Show working memory statistics in the current session.`, category `Memory`, suggested `true`, `slash: { name: "memory-status" }`
|
||||
- title `Memory list`, value `memory.list`, description `Show current workspace memories with display-local refs.`, category `Memory`, `slash: { name: "memory-list" }`
|
||||
- title `Memory help`, value `memory.help`, description `Show working memory help.`, category `Memory`, `slash: { name: "memory-help" }`
|
||||
- Remove `Memory activity` and `Memory last` command objects.
|
||||
- Do not include `aliases: ["mem"]` in this wave; aliases can be reconsidered only after verifying they do not create duplicate menu entries.
|
||||
|
||||
- [ ] **Step 1: Implement minimal code**
|
||||
|
||||
Keep existing active-session guard, no-reply injection, dialog clearing, and prompt failure toast logic unchanged.
|
||||
|
||||
- [ ] **Step 2: Run focused verification**
|
||||
|
||||
Run: `node --import ./tests/setup-xdg-data-home.ts --test --experimental-strip-types tests/tui-plugin.test.ts`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 3: Run adjacent focused verification**
|
||||
|
||||
Run: `node --import ./tests/setup-xdg-data-home.ts --test --experimental-strip-types tests/tui-plugin.test.ts tests/memory-visibility.test.ts`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
### Wave 3 Checkpoint
|
||||
|
||||
- [ ] TUI registration tests prove slash names are unique.
|
||||
- [ ] No test expects or selects `memory.activity` or `memory.last`.
|
||||
- [ ] No implementation path requires OpenCode to render trailing subcommand text.
|
||||
|
||||
## Wave 4: Documentation and Release Metadata Alignment
|
||||
|
||||
### Task 4.1: Update user-facing docs
|
||||
|
||||
**Purpose:** Ensure install/usage docs no longer advertise broken or removed command spellings.
|
||||
|
||||
**Files:**
|
||||
- Modify: `README.md`
|
||||
- Modify: `CHANGELOG.md`
|
||||
- Verify/no change unless needed: `RELEASE_NOTES.md`, `docs/installation.md`, `docs/configuration.md`
|
||||
|
||||
**Implementation instructions:**
|
||||
- In `README.md` feature bullets, replace “status, recent activity, and help” with “status, current memory list, and help”.
|
||||
- In the Native TUI Memory Command section, document:
|
||||
- `/memory-status` — status counts/statistics
|
||||
- `/memory-list` — current active workspace memories with display-local `[M1]` refs
|
||||
- `/memory-help` — help
|
||||
- Keep the existing local-only/no LLM/no-reply transcript caveat.
|
||||
- Remove docs for `/memory`, `/memory status`, `/memory activity`, `/memory last`, and `/memory help` as available user commands.
|
||||
- In `CHANGELOG.md` 1.6.1 entry, amend the current TUI command bullet to say hyphenated `/memory-status`, `/memory-list`, `/memory-help`, and note recent activity/last were removed before release because duplicate entries were not useful.
|
||||
- `RELEASE_NOTES.md` currently has no 1.6.1 TUI command mention in the evidence read; do not add a release note unless the release process requires a 1.6.1 section.
|
||||
|
||||
- [ ] **Step 1: Update markdown docs**
|
||||
|
||||
Use exact command names consistently.
|
||||
|
||||
- [ ] **Step 2: Run docs/source grep**
|
||||
|
||||
Run equivalent local search:
|
||||
|
||||
```bash
|
||||
rg "/memory activity|/memory last|/memory status|/memory help|slash: \{ name: \"memory\"|memory\.activity|memory\.last" README.md CHANGELOG.md src tests
|
||||
```
|
||||
|
||||
Expected: no matches except this plan file if searching the whole repo, or negative test assertions that intentionally verify old commands are absent. The space-separated forms in this grep are obsolete spellings; the correct hyphenated commands `/memory-status`, `/memory-list`, and `/memory-help` should remain present.
|
||||
|
||||
### Wave 4 Checkpoint
|
||||
|
||||
- [ ] Docs advertise only `/memory-status`, `/memory-list`, `/memory-help`.
|
||||
- [ ] Changelog matches the pre-release UX correction.
|
||||
- [ ] No release docs mention stale activity/last commands.
|
||||
|
||||
## Final Verification
|
||||
|
||||
- [ ] Run: `npm run typecheck`
|
||||
Expected: PASS and output includes `TYPECHECK_PASS`.
|
||||
- [ ] Run: `npm test`
|
||||
Expected: PASS and output includes `TEST_PASS`.
|
||||
- [ ] Run: `npm pack --dry-run`
|
||||
Expected: package contains `index.ts`, `src/tui-plugin.ts`, `src/memory-visibility.ts`, README, LICENSE, and no unexpected generated artifacts.
|
||||
- [ ] Manual OpenCode TUI smoke before commit/push:
|
||||
- Configure `.opencode/tui.json` to load the local plugin target.
|
||||
- Open slash command menu and confirm exactly three visible memory commands: `/memory-status`, `/memory-list`, `/memory-help`.
|
||||
- Select `/memory-status`; expected no-reply session text headed `## Memory status`, no assistant response, no LLM/provider activity.
|
||||
- Select `/memory-list`; expected no-reply session text headed `## Current workspace memories`, display-local `[M#]` refs, grouped memory types, redacted/truncated text.
|
||||
- Select `/memory-help`; expected help lists only the three hyphenated commands.
|
||||
- [ ] Review changed files for placeholders, dead code, unused activity imports, debug logging, stale docs, raw secret output, and accidental storage writes.
|
||||
|
||||
## Review Readiness
|
||||
|
||||
- [ ] Scope challenge resolved: this is a focused UX correction, not a memory subsystem rewrite.
|
||||
- [ ] Architecture and data flow are explicit.
|
||||
- [ ] Every changed behavior has a focused test or manual TUI smoke check.
|
||||
- [ ] Failure paths and user-visible states are covered.
|
||||
- [ ] Commands are exact and runnable.
|
||||
- [ ] Backwards compatibility stance is explicit and pre-release-safe.
|
||||
- [ ] Plan has no placeholders.
|
||||
|
||||
## Risks and Mitigations
|
||||
|
||||
- Risk: Hyphenated names are less elegant than `/memory status` subcommands. Mitigation: current OpenCode menu behavior makes hyphenated top-level names the only visible unambiguous option.
|
||||
- Risk: Users may interpret `[M#]` as stable memory IDs. Mitigation: list output must explicitly say refs are display-local and may change after memory updates.
|
||||
- Risk: Activity formatter code may be left as unused dead code. Mitigation: typecheck plus source grep should catch unused imports/references; remove activity-only exports unless a maintainer-only consumer is introduced later.
|
||||
- Risk: List output may leak long or sensitive memory text. Mitigation: use redaction/truncation for each line and add regression assertions that credential-like fixture text is absent.
|
||||
- Risk: Docs drift with the just-added 1.6.1 changelog. Mitigation: amend the same 1.6.1 entry before commit/push rather than adding contradictory release notes.
|
||||
+4
-2
@@ -1,11 +1,13 @@
|
||||
{
|
||||
"name": "opencode-working-memory",
|
||||
"version": "1.6.0",
|
||||
"version": "1.6.1",
|
||||
"description": "Three-layer memory architecture for OpenCode with workspace memory and hot session state",
|
||||
"type": "module",
|
||||
"main": "index.ts",
|
||||
"exports": {
|
||||
".": "./index.ts"
|
||||
".": "./index.ts",
|
||||
"./server": "./index.ts",
|
||||
"./tui": "./src/tui-plugin.ts"
|
||||
},
|
||||
"bin": {
|
||||
"memory-diag": "./scripts/memory-diag-bin.cjs"
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
// 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 { 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 { accountWorkspaceMemoryCompactionRefs, accountWorkspaceMemoryRender } from "./workspace-memory.ts";
|
||||
|
||||
export type MemoryVisibilityCommand = "status" | "list" | "help";
|
||||
|
||||
type MemoryListItem = {
|
||||
ref: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
export type MemoryStatusModel = {
|
||||
activeMemories: number;
|
||||
supersededMemories: number;
|
||||
renderedInPrompt: number;
|
||||
omittedActiveMemories: number;
|
||||
pendingInSession: number;
|
||||
pendingJournalMemories: number;
|
||||
openErrors: number;
|
||||
recentDecisions: number;
|
||||
};
|
||||
|
||||
export type MemoryListModel = {
|
||||
activeMemories: number;
|
||||
renderedMemories: number;
|
||||
omittedActiveMemories: number;
|
||||
groups: Record<LongTermMemoryEntry["type"], MemoryListItem[]>;
|
||||
};
|
||||
|
||||
const MAX_PREVIEW_CHARS = 120;
|
||||
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();
|
||||
if (clean.length <= maxChars) return clean;
|
||||
return `${clean.slice(0, Math.max(0, maxChars - 1)).trimEnd()}…`;
|
||||
}
|
||||
|
||||
async function readJSONSnapshot(path: string): Promise<unknown | undefined> {
|
||||
try {
|
||||
return JSON.parse(await readFile(path, "utf8"));
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function isLongTermType(value: unknown): value is LongTermMemoryEntry["type"] {
|
||||
return value === "feedback" || value === "project" || value === "decision" || value === "reference";
|
||||
}
|
||||
|
||||
function isLongTermSource(value: unknown): value is LongTermMemoryEntry["source"] {
|
||||
return value === "explicit" || value === "compaction" || value === "manual";
|
||||
}
|
||||
|
||||
function isLongTermMemoryEntry(value: unknown): value is LongTermMemoryEntry {
|
||||
if (!isRecord(value)) return false;
|
||||
if (typeof value.id !== "string") return false;
|
||||
if (!isLongTermType(value.type)) return false;
|
||||
if (typeof value.text !== "string") return false;
|
||||
if (!isLongTermSource(value.source)) return false;
|
||||
if (typeof value.confidence !== "number") return false;
|
||||
if (value.status !== "active" && value.status !== "superseded") return false;
|
||||
if (typeof value.createdAt !== "string") return false;
|
||||
return typeof value.updatedAt === "string";
|
||||
}
|
||||
|
||||
function memoryEntries(value: unknown): LongTermMemoryEntry[] {
|
||||
return Array.isArray(value) ? value.filter(isLongTermMemoryEntry) : [];
|
||||
}
|
||||
|
||||
async function emptyWorkspaceMemorySnapshot(root: string): Promise<WorkspaceMemoryStore> {
|
||||
const nowIso = new Date().toISOString();
|
||||
return {
|
||||
version: 1,
|
||||
workspace: { root, key: await workspaceKey(root) },
|
||||
limits: {
|
||||
maxRenderedChars: LONG_TERM_LIMITS.maxRenderedChars,
|
||||
maxEntries: LONG_TERM_LIMITS.maxEntries,
|
||||
},
|
||||
entries: [],
|
||||
migrations: [],
|
||||
updatedAt: nowIso,
|
||||
lastActivityAt: nowIso,
|
||||
};
|
||||
}
|
||||
|
||||
async function readWorkspaceMemorySnapshot(root: string): Promise<WorkspaceMemoryStore> {
|
||||
const fallback = await emptyWorkspaceMemorySnapshot(root);
|
||||
const loaded = await readJSONSnapshot(await workspaceMemoryPath(root));
|
||||
if (!isRecord(loaded)) return fallback;
|
||||
const limits = isRecord(loaded.limits) ? loaded.limits : {};
|
||||
|
||||
return {
|
||||
version: 1,
|
||||
workspace: fallback.workspace,
|
||||
limits: {
|
||||
maxRenderedChars: typeof limits.maxRenderedChars === "number" ? limits.maxRenderedChars : LONG_TERM_LIMITS.maxRenderedChars,
|
||||
maxEntries: typeof limits.maxEntries === "number" ? limits.maxEntries : LONG_TERM_LIMITS.maxEntries,
|
||||
},
|
||||
entries: memoryEntries(loaded.entries),
|
||||
migrations: Array.isArray(loaded.migrations) ? loaded.migrations.filter(item => typeof item === "string") : [],
|
||||
updatedAt: typeof loaded.updatedAt === "string" ? loaded.updatedAt : fallback.updatedAt,
|
||||
lastActivityAt: typeof loaded.lastActivityAt === "string" ? loaded.lastActivityAt : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async function emptyPendingJournalSnapshot(root: string): Promise<PendingMemoryJournalStore> {
|
||||
return {
|
||||
version: 1,
|
||||
workspace: { root, key: await workspaceKey(root) },
|
||||
entries: [],
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
async function readPendingJournalSnapshot(root: string): Promise<PendingMemoryJournalStore> {
|
||||
const fallback = await emptyPendingJournalSnapshot(root);
|
||||
const loaded = await readJSONSnapshot(await workspacePendingJournalPath(root));
|
||||
if (!isRecord(loaded)) return fallback;
|
||||
return {
|
||||
version: 1,
|
||||
workspace: fallback.workspace,
|
||||
entries: memoryEntries(loaded.entries),
|
||||
updatedAt: typeof loaded.updatedAt === "string" ? loaded.updatedAt : fallback.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
function emptySessionStateSnapshot(sessionID: string): SessionState {
|
||||
return {
|
||||
version: 1,
|
||||
sessionID,
|
||||
turn: 0,
|
||||
updatedAt: new Date().toISOString(),
|
||||
activeFiles: [],
|
||||
openErrors: [],
|
||||
recentDecisions: [],
|
||||
pendingMemories: [],
|
||||
compactionMemoryRefs: [],
|
||||
};
|
||||
}
|
||||
|
||||
async function readSessionStateSnapshot(root: string, sessionID: string): Promise<SessionState> {
|
||||
const fallback = emptySessionStateSnapshot(sessionID);
|
||||
const loaded = await readJSONSnapshot(await sessionStatePath(root, sessionID));
|
||||
if (!isRecord(loaded)) return fallback;
|
||||
return {
|
||||
...fallback,
|
||||
turn: typeof loaded.turn === "number" ? loaded.turn : fallback.turn,
|
||||
updatedAt: typeof loaded.updatedAt === "string" ? loaded.updatedAt : fallback.updatedAt,
|
||||
activeFiles: Array.isArray(loaded.activeFiles) ? loaded.activeFiles as SessionState["activeFiles"] : [],
|
||||
openErrors: Array.isArray(loaded.openErrors) ? loaded.openErrors as SessionState["openErrors"] : [],
|
||||
recentDecisions: Array.isArray(loaded.recentDecisions) ? loaded.recentDecisions as SessionState["recentDecisions"] : [],
|
||||
pendingMemories: memoryEntries(loaded.pendingMemories),
|
||||
compactionMemoryRefs: Array.isArray(loaded.compactionMemoryRefs) ? loaded.compactionMemoryRefs as SessionState["compactionMemoryRefs"] : [],
|
||||
};
|
||||
}
|
||||
|
||||
export async function getMemoryStatus(root: string, sessionID: string): Promise<MemoryStatusModel> {
|
||||
const [store, sessionState, pendingJournal] = await Promise.all([
|
||||
readWorkspaceMemorySnapshot(root),
|
||||
readSessionStateSnapshot(root, sessionID),
|
||||
readPendingJournalSnapshot(root),
|
||||
]);
|
||||
const renderAccounting = accountWorkspaceMemoryRender(store);
|
||||
const activeEntries = store.entries.filter(entry => entry.status !== "superseded");
|
||||
const supersededEntries = store.entries.filter(entry => entry.status === "superseded");
|
||||
|
||||
return {
|
||||
activeMemories: activeEntries.length,
|
||||
supersededMemories: supersededEntries.length,
|
||||
renderedInPrompt: renderAccounting.rendered.length,
|
||||
omittedActiveMemories: renderAccounting.omitted.filter(item => item.memory.status !== "superseded").length,
|
||||
pendingInSession: sessionState.pendingMemories.length,
|
||||
pendingJournalMemories: pendingJournal.entries.length,
|
||||
openErrors: sessionState.openErrors.filter(error => error.status === "open").length,
|
||||
recentDecisions: sessionState.recentDecisions.length,
|
||||
};
|
||||
}
|
||||
|
||||
export function formatMemoryStatus(model: MemoryStatusModel): string {
|
||||
return [
|
||||
"## Memory status",
|
||||
"",
|
||||
"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 → Current memories to browse current [M1]-[M${LONG_TERM_LIMITS.maxEntries}] memory refs.`,
|
||||
"",
|
||||
"Local only: no LLM request was made.",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function emptyMemoryListGroups(): MemoryListModel["groups"] {
|
||||
return { feedback: [], project: [], decision: [], reference: [] };
|
||||
}
|
||||
|
||||
export async function getMemoryList(root: string): Promise<MemoryListModel> {
|
||||
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 {
|
||||
activeMemories: store.entries.filter(entry => entry.status !== "superseded").length,
|
||||
renderedMemories,
|
||||
omittedActiveMemories: accounting.omitted.filter(item => item.memory.status !== "superseded").length,
|
||||
groups,
|
||||
};
|
||||
}
|
||||
|
||||
export function formatMemoryList(model: MemoryListModel): string {
|
||||
const lines = [
|
||||
"## Current workspace memories",
|
||||
"",
|
||||
];
|
||||
|
||||
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("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");
|
||||
}
|
||||
|
||||
export function formatMemoryHelp(): string {
|
||||
return [
|
||||
"## Memory help",
|
||||
"",
|
||||
"Command:",
|
||||
"- /memory — open the local memory menu.",
|
||||
"",
|
||||
"Menu entries:",
|
||||
"- Status — show local memory statistics.",
|
||||
`- Current memories — browse active workspace memories as display-local [M1]-[M${LONG_TERM_LIMITS.maxEntries}] refs.`,
|
||||
"- Help — show this help.",
|
||||
"",
|
||||
"These commands are read-only, local-only, and do not call the LLM.",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
export async function renderMemoryCommand(root: string, sessionID: string, command: MemoryVisibilityCommand): Promise<string> {
|
||||
switch (command) {
|
||||
case "status":
|
||||
return formatMemoryStatus(await getMemoryStatus(root, sessionID));
|
||||
case "list":
|
||||
return formatMemoryList(await getMemoryList(root));
|
||||
case "help":
|
||||
return formatMemoryHelp();
|
||||
default:
|
||||
return formatMemoryHelp();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
import {
|
||||
formatMemoryHelp,
|
||||
formatMemoryList,
|
||||
getMemoryList,
|
||||
renderMemoryCommand,
|
||||
type MemoryVisibilityCommand,
|
||||
} from "./memory-visibility.ts";
|
||||
|
||||
type DialogContext = {
|
||||
clear?: () => void;
|
||||
};
|
||||
|
||||
type DialogSize = "medium" | "large" | "xlarge";
|
||||
type DialogElement = unknown;
|
||||
type DialogStackContext = {
|
||||
clear?: () => void;
|
||||
replace?: (render: () => DialogElement, onClose?: () => void) => void;
|
||||
setSize?: (size: DialogSize) => void;
|
||||
};
|
||||
type DialogAlertComponent = (props: { title: string; message: string; onConfirm?: () => void }) => DialogElement;
|
||||
type DialogSelectOption<Value = string> = {
|
||||
title: string;
|
||||
value: Value;
|
||||
description?: string;
|
||||
footer?: string;
|
||||
category?: string;
|
||||
disabled?: boolean;
|
||||
onSelect?: () => void | Promise<void>;
|
||||
};
|
||||
type DialogSelectProps<Value = string> = {
|
||||
title: string;
|
||||
placeholder?: string;
|
||||
options: DialogSelectOption<Value>[];
|
||||
onSelect?: (option: DialogSelectOption<Value>) => void | Promise<void>;
|
||||
skipFilter?: boolean;
|
||||
};
|
||||
type DialogSelectComponent = <Value = string>(props: DialogSelectProps<Value>) => DialogElement;
|
||||
|
||||
type TuiCommand = {
|
||||
title: string;
|
||||
value: string;
|
||||
description?: string;
|
||||
category?: string;
|
||||
suggested?: boolean;
|
||||
slash?: {
|
||||
name: string;
|
||||
aliases?: string[];
|
||||
};
|
||||
onSelect?: (dialog?: DialogContext) => void | Promise<void>;
|
||||
};
|
||||
|
||||
type TuiRouteCurrent =
|
||||
| { name: "home" }
|
||||
| { name: "session"; params: { sessionID: string; prompt?: unknown } }
|
||||
| { name: string; params?: Record<string, unknown> };
|
||||
|
||||
type TuiPluginApi = {
|
||||
command: {
|
||||
register: (cb: () => TuiCommand[]) => () => void;
|
||||
};
|
||||
route: ({ readonly current: TuiRouteCurrent } | TuiRouteCurrent);
|
||||
ui: {
|
||||
DialogAlert?: DialogAlertComponent;
|
||||
DialogSelect?: DialogSelectComponent;
|
||||
toast: (input: { variant?: "info" | "success" | "warning" | "error"; message: string }) => void;
|
||||
dialog?: DialogStackContext;
|
||||
};
|
||||
state: {
|
||||
path: {
|
||||
directory: string;
|
||||
};
|
||||
};
|
||||
client?: unknown;
|
||||
};
|
||||
|
||||
type TuiPlugin = (api: TuiPluginApi, options: unknown, meta: unknown) => Promise<void>;
|
||||
|
||||
function currentRoute(api: TuiPluginApi): TuiRouteCurrent {
|
||||
const route = api.route as ({ readonly current?: TuiRouteCurrent } & Partial<TuiRouteCurrent>);
|
||||
return route.current ?? (route as TuiRouteCurrent);
|
||||
}
|
||||
|
||||
function renderErrorReport(error: unknown): string {
|
||||
const detail = error instanceof Error ? error.message : String(error);
|
||||
return [
|
||||
"## Memory error",
|
||||
"",
|
||||
"Unable to render local memory visibility output.",
|
||||
`Error: ${detail}`,
|
||||
"",
|
||||
"Local only: no LLM request was made.",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function dialogSizeForCommand(command: MemoryVisibilityCommand): DialogSize {
|
||||
if (command === "list") return "xlarge";
|
||||
if (command === "status") return "large";
|
||||
return "medium";
|
||||
}
|
||||
|
||||
function fallbackTitleForCommand(command: MemoryVisibilityCommand): string {
|
||||
if (command === "list") return "Current workspace memories";
|
||||
if (command === "status") return "Memory status";
|
||||
return "Memory help";
|
||||
}
|
||||
|
||||
function dialogCopyFromMarkdown(text: string, fallbackTitle: string): { title: string; message: string } {
|
||||
const match = /^##\s+(.+)$/m.exec(text);
|
||||
if (!match) return { title: fallbackTitle, message: text };
|
||||
|
||||
const headingStart = match.index;
|
||||
const headingEnd = text.indexOf("\n", headingStart);
|
||||
const before = text.slice(0, headingStart);
|
||||
const after = headingEnd === -1 ? "" : text.slice(headingEnd + 1);
|
||||
|
||||
return {
|
||||
title: match[1].trim(),
|
||||
message: `${before}${after}`.replace(/^\s+/, ""),
|
||||
};
|
||||
}
|
||||
|
||||
function getDialogApi(api: TuiPluginApi): {
|
||||
DialogAlert: DialogAlertComponent;
|
||||
DialogSelect: DialogSelectComponent;
|
||||
dialog: Required<Pick<DialogStackContext, "replace" | "setSize">>;
|
||||
} | undefined {
|
||||
if (
|
||||
typeof api.ui.DialogAlert !== "function" ||
|
||||
typeof api.ui.DialogSelect !== "function" ||
|
||||
typeof api.ui.dialog?.replace !== "function" ||
|
||||
typeof api.ui.dialog?.setSize !== "function"
|
||||
) {
|
||||
api.ui.toast({
|
||||
variant: "error",
|
||||
message: "Memory dialog UI is unavailable in this OpenCode runtime.",
|
||||
});
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
DialogAlert: api.ui.DialogAlert,
|
||||
DialogSelect: api.ui.DialogSelect,
|
||||
dialog: {
|
||||
replace: api.ui.dialog.replace,
|
||||
setSize: api.ui.dialog.setSize,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function showDialogError(api: TuiPluginApi, error: unknown): void {
|
||||
const detail = error instanceof Error ? error.message : String(error);
|
||||
api.ui.toast({
|
||||
variant: "error",
|
||||
message: `Unable to show memory dialog: ${detail}`,
|
||||
});
|
||||
}
|
||||
|
||||
function showAlertFromMarkdown(api: TuiPluginApi, text: string, fallbackTitle: string, size: DialogSize): void {
|
||||
const dialogApi = getDialogApi(api);
|
||||
if (!dialogApi) return;
|
||||
const { title, message } = dialogCopyFromMarkdown(text, fallbackTitle);
|
||||
|
||||
try {
|
||||
dialogApi.dialog.replace(() => dialogApi.DialogAlert({ title, message }));
|
||||
dialogApi.dialog.setSize(size);
|
||||
} catch (error) {
|
||||
showDialogError(api, error);
|
||||
}
|
||||
}
|
||||
|
||||
function showMemoryMenu(api: TuiPluginApi, dialog?: DialogContext): void {
|
||||
const dialogApi = getDialogApi(api);
|
||||
if (!dialogApi) return;
|
||||
|
||||
const options: DialogSelectOption[] = [
|
||||
{
|
||||
title: "Status",
|
||||
value: "memory.status",
|
||||
description: "Show local memory statistics",
|
||||
onSelect: () => showMemoryStatus(api),
|
||||
},
|
||||
{
|
||||
title: "Current memories",
|
||||
value: "memory.list",
|
||||
description: "Browse active workspace memories with display-local refs",
|
||||
onSelect: () => showMemoryList(api),
|
||||
},
|
||||
{
|
||||
title: "Help",
|
||||
value: "memory.help",
|
||||
description: "Show memory command help",
|
||||
onSelect: () => showMemoryHelp(api),
|
||||
},
|
||||
];
|
||||
|
||||
try {
|
||||
dialog?.clear?.();
|
||||
dialogApi.dialog.replace(() => dialogApi.DialogSelect({
|
||||
title: "Memory",
|
||||
placeholder: "Search memory actions",
|
||||
options,
|
||||
}));
|
||||
dialogApi.dialog.setSize("large");
|
||||
} catch (error) {
|
||||
showDialogError(api, error);
|
||||
}
|
||||
}
|
||||
|
||||
async function showMemoryStatus(api: TuiPluginApi): Promise<void> {
|
||||
const route = currentRoute(api);
|
||||
|
||||
if (route.name !== "session" || typeof route.params?.sessionID !== "string") {
|
||||
api.ui.toast({
|
||||
variant: "warning",
|
||||
message: "Open a session to use memory commands.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const sessionID = route.params.sessionID;
|
||||
const dialogApi = getDialogApi(api);
|
||||
if (!dialogApi) return;
|
||||
|
||||
let text: string;
|
||||
let fallbackTitle = fallbackTitleForCommand("status");
|
||||
|
||||
try {
|
||||
text = await renderMemoryCommand(api.state.path.directory, sessionID, "status");
|
||||
} catch (error) {
|
||||
text = renderErrorReport(error);
|
||||
fallbackTitle = "Memory error";
|
||||
}
|
||||
|
||||
const { title, message } = dialogCopyFromMarkdown(text, fallbackTitle);
|
||||
|
||||
try {
|
||||
dialogApi.dialog.replace(() => dialogApi.DialogAlert({ title, message }));
|
||||
dialogApi.dialog.setSize(dialogSizeForCommand("status"));
|
||||
} catch (error) {
|
||||
showDialogError(api, error);
|
||||
}
|
||||
}
|
||||
|
||||
function showMemoryHelp(api: TuiPluginApi): void {
|
||||
showAlertFromMarkdown(api, formatMemoryHelp(), "Memory help", "medium");
|
||||
}
|
||||
|
||||
const MEMORY_TYPE_ORDER = ["feedback", "project", "decision", "reference"] as const;
|
||||
|
||||
async function showMemoryList(api: TuiPluginApi): Promise<void> {
|
||||
const dialogApi = getDialogApi(api);
|
||||
if (!dialogApi) return;
|
||||
|
||||
try {
|
||||
const model = await getMemoryList(api.state.path.directory);
|
||||
if (model.renderedMemories === 0) {
|
||||
showAlertFromMarkdown(api, formatMemoryList(model), "Current workspace memories", "medium");
|
||||
return;
|
||||
}
|
||||
|
||||
const options: DialogSelectOption[] = [];
|
||||
for (const type of MEMORY_TYPE_ORDER) {
|
||||
for (const item of model.groups[type]) {
|
||||
options.push({
|
||||
title: `[${item.ref}] ${item.text}`,
|
||||
value: item.ref,
|
||||
category: type,
|
||||
footer: "display-local",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
dialogApi.dialog.replace(() => dialogApi.DialogSelect({
|
||||
title: "Current workspace memories",
|
||||
placeholder: "Search memory refs",
|
||||
options,
|
||||
}));
|
||||
dialogApi.dialog.setSize("xlarge");
|
||||
} catch (error) {
|
||||
showAlertFromMarkdown(api, renderErrorReport(error), "Memory error", "medium");
|
||||
}
|
||||
}
|
||||
|
||||
function memoryCommands(api: TuiPluginApi): TuiCommand[] {
|
||||
return [
|
||||
{
|
||||
title: "Memory",
|
||||
value: "memory.menu",
|
||||
description: "Browse local working memory.",
|
||||
category: "Memory",
|
||||
slash: { name: "memory" },
|
||||
onSelect: (dialog?: DialogContext) => showMemoryMenu(api, dialog),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export const MemoryTuiPlugin: TuiPlugin = async (api) => {
|
||||
api.command.register(() => memoryCommands(api));
|
||||
};
|
||||
|
||||
export default {
|
||||
id: "working-memory-tui",
|
||||
tui: MemoryTuiPlugin,
|
||||
};
|
||||
@@ -528,7 +528,7 @@ function extractConcreteIdentityKey(text: string): string | null {
|
||||
if (pathIdentity) return pathIdentity;
|
||||
}
|
||||
|
||||
const pathMatch = text.match(/(?:\/[^ | ||||