From f212f5a1c42cfa743e1a066e82e96892cf592666 Mon Sep 17 00:00:00 2001 From: m-aebrer Date: Fri, 7 Aug 2026 11:38:21 -0400 Subject: [PATCH 1/6] chore: open PR for issue 335 From d0701879472c3948cbe100a61147c5ca7644ef6e Mon Sep 17 00:00:00 2001 From: m-aebrer Date: Fri, 7 Aug 2026 12:56:19 -0400 Subject: [PATCH 2/6] Add dashboard memory management --- README.md | 6 +- packages/coding-agent/README.md | 2 +- packages/coding-agent/docs/dashboard.md | 1 + packages/coding-agent/src/index.ts | 1 + packages/dashboard/README.md | 15 + packages/dashboard/src/client/api.ts | 24 + packages/dashboard/src/client/app.tsx | 4 + .../src/client/components/common.tsx | 5 +- .../dashboard/src/client/screens/memories.tsx | 412 ++++++++++++++++++ packages/dashboard/src/client/state/store.ts | 4 + packages/dashboard/src/client/styles/app.css | 133 ++++++ packages/dashboard/src/server/memories.ts | 397 +++++++++++++++++ packages/dashboard/src/server/server.ts | 52 +++ packages/dashboard/src/shared/protocol.ts | 48 ++ .../dashboard/test/client/screens.test.tsx | 132 ++++++ packages/dashboard/test/client/store.test.ts | 8 + packages/dashboard/test/memories.test.ts | 168 +++++++ packages/dashboard/test/server.test.ts | 58 ++- 18 files changed, 1465 insertions(+), 5 deletions(-) create mode 100644 packages/dashboard/src/client/screens/memories.tsx create mode 100644 packages/dashboard/src/server/memories.ts create mode 100644 packages/dashboard/test/memories.test.ts diff --git a/README.md b/README.md index b0e29f45..4cbeb75b 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Use dreb if you want a coding agent that can run against direct APIs, coding sub ## Why choose dreb? -- **Every session, on every device.** The [web dashboard](#web-dashboard) is a first-party browser UI for the same sessions the terminal runs: a fleet overview of all live and past sessions across projects, full chat with steering, live subagent observability, host file access, and settings — one synchronized state on desktop and mobile. Local-only by default; remote access is Tailscale-gated with device pairing. +- **Every session, on every device.** The [web dashboard](#web-dashboard) is a first-party browser UI for the same sessions the terminal runs: a fleet overview of all live and past sessions across projects, full chat with steering, live subagent observability, host file access, dreb memory management, and settings — one synchronized state on desktop and mobile. Local-only by default; remote access is Tailscale-gated with device pairing. - **Model and provider freedom.** Authenticate with API keys or `/login` subscriptions, switch models at runtime with `/model`, scope model sets, tune thinking levels, route built-in providers through proxies, use cloud providers such as Bedrock/Vertex/Azure, or add local/proxy/custom models through [Custom Models](packages/coding-agent/docs/models.md) and [Custom Providers](packages/coding-agent/docs/custom-provider.md). See [Providers](packages/coding-agent/docs/providers.md) for the current setup list. - **A real development workflow.** [mach6](packages/coding-agent/docs/mach6.md) is a built-in issue-to-merge workflow: assess issues, plan work, open draft PRs, implement, push progress, run multi-agent reviews, independently assess findings, fix CI or review items, and publish. Plans, reviews, and progress live on GitHub as shared memory. - **Composable agent building blocks.** [Skills](packages/coding-agent/docs/skills.md) are markdown workflows loaded on demand; [extensions](packages/coding-agent/docs/extensions.md) are TypeScript modules for custom tools, commands, event hooks, UI components, renderers, keybindings, provider registration, permission gates, and workflow automation; [packages](packages/coding-agent/docs/packages.md) bundle skills, extensions, prompts, and themes for npm, git, or local sharing. @@ -116,7 +116,7 @@ The same agent runtime powers multiple surfaces: - **RPC mode** — strict [JSONL stdin/stdout protocol](packages/coding-agent/docs/rpc.md) for non-Node clients and custom UIs. - **SDK** — import `@dreb/coding-agent` and create agent sessions directly in TypeScript. - **Telegram** — `@dreb/telegram` runs dreb as a bot with sessions, model controls, file upload/download, live tool status, and visible results for user-facing tools. -- **Web dashboard** — `dreb dashboard` serves a browser UI (fleet overview of all sessions, full chat with steering, subagent observability, host file browser); local-only by default, remote via Tailscale + rotating pairing code. See [dashboard docs](packages/coding-agent/docs/dashboard.md). +- **Web dashboard** — `dreb dashboard` serves a browser UI (fleet overview of all sessions, full chat with steering, subagent observability, host file browser, dreb memory editor); local-only by default, remote via Tailscale + rotating pairing code. See [dashboard docs](packages/coding-agent/docs/dashboard.md). ### Web dashboard @@ -143,6 +143,8 @@ The dashboard is the visual face of dreb: every agent session on the host, live **Host files, explicitly.** Browse the host filesystem, upload/download, create folders, and start a new session in any directory — every file operation logged server-side. +**Memories, repairable.** The Memories screen edits dreb memory scopes only: global `~/.dreb/memory` plus `.dreb/memory` for active/disk project roots. It shows the complete `MEMORY.md` index (with a warning when it exceeds the 200-line prompt convention), existing entry metadata or parse errors, sanitized Markdown previews, exact-revision conflict handling that preserves drafts, and entry deletion that synchronously cleans matching index links before unlinking the file. It does not create/rename entries or expose Claude memory paths. + **Curated appearance themes.** A theme gallery in settings offers eight dashboard-native themes (entropist.ca, Dim, Solarized, Gruvbox, Caves of Qud, Van Gogh, plus the colorblind-safe Okabe-Ito and Paul Tol palettes), each with its own light and dark palette, plus a system/light/dark mode toggle. Choices are saved per browser and are independent of your TUI theme. Launch locally: diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index 2e509c4c..35394e41 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -81,7 +81,7 @@ Or use a custom provider (corporate proxy, Bedrock, etc.) — see [Custom provid Then just talk to dreb. All 13 standard built-in tools are enabled by default: `read`, `write`, `edit`, `bash`, `grep`, `find`, `ls`, `web_search`, `web_fetch`, `subagent`, `wait`, `watch_github_ci`, and `ask_user`. Use `--tools` to restrict to a subset (e.g., `--tools read,grep,find,ls` for read-only). Three additional tools — `search`, `skill`, and `tasks_update` — are always active regardless of `--tools`. `suggest_next` is active by default but excluded when `--tools` is specified. The model uses these to fulfill your requests. Add capabilities via [skills](#skills), [prompt templates](#prompt-templates), [extensions](#extensions), or [packages](#packages). -**Also available:** [`@dreb/telegram`](https://www.npmjs.com/package/@dreb/telegram) — run dreb as a Telegram bot with live tool status and visible results for user-facing tools (`npm install -g @dreb/telegram`). [`@dreb/dashboard`](https://www.npmjs.com/package/@dreb/dashboard) — run `dreb dashboard` for a browser UI with fleet overview, full chat steering, generic fail-closed built-in slash-command discovery and execution, inline provider/API failures with partial output preserved, sanitized raster tool images plus sent user uploads retained as bounded transcript previews by default, a bounded all-agent subagent panel with drill-in, host file browser, curated appearance themes (per-browser light/dark), and Tailscale/rotating-code pairing (`npm install -g @dreb/dashboard`; see [docs/dashboard.md](docs/dashboard.md)). Tool images cross browser-facing transport as content-addressed references; browser-local Settings offers placeholders, bounded previews, or informed-opt-in originals, with size disclosure and confirmation above 1 MiB. Full-resolution HTML export remains self-contained. Compact SSE snapshots update live fleet cards without repeatedly fetching the cross-project inventory, and session drill-in hydrates state, messages, and background agents through one ordered snapshot request. Terminal provider failures show their reason on fleet cards, while transient failures clear terminal state when automatic retry begins and remain recorded inline on the failed attempt. Its top bar and persistent session header indicators report connecting, connected, retrying, resyncing, disconnected, or auth failed; bounded SSE replay plus an explicit snapshot barrier restores session state, tasks, and image references after a reload, restart, gap, backpressure disconnect, or stalled stream, while authenticated image routes recover bytes separately from authoritative transcripts. +**Also available:** [`@dreb/telegram`](https://www.npmjs.com/package/@dreb/telegram) — run dreb as a Telegram bot with live tool status and visible results for user-facing tools (`npm install -g @dreb/telegram`). [`@dreb/dashboard`](https://www.npmjs.com/package/@dreb/dashboard) — run `dreb dashboard` for a browser UI with fleet overview, full chat steering, generic fail-closed built-in slash-command discovery and execution, inline provider/API failures with partial output preserved, sanitized raster tool images plus sent user uploads retained as bounded transcript previews by default, a bounded all-agent subagent panel with drill-in, host file browser, dreb memory editor with exact-revision saves and automatic index cleanup on delete, curated appearance themes (per-browser light/dark), and Tailscale/rotating-code pairing (`npm install -g @dreb/dashboard`; see [docs/dashboard.md](docs/dashboard.md)). Tool images cross browser-facing transport as content-addressed references; browser-local Settings offers placeholders, bounded previews, or informed-opt-in originals, with size disclosure and confirmation above 1 MiB. Full-resolution HTML export remains self-contained. The Memories screen is dreb-only (`~/.dreb/memory` and active/on-disk-session project `.dreb/memory`), shows complete indexes with a >200-line warning, surfaces malformed entry frontmatter for repair, preserves drafts on conflicts, and does not create/rename entries or expose Claude paths. Compact SSE snapshots update live fleet cards without repeatedly fetching the cross-project inventory, and session drill-in hydrates state, messages, and background agents through one ordered snapshot request. Terminal provider failures show their reason on fleet cards, while transient failures clear terminal state when automatic retry begins and remain recorded inline on the failed attempt. Its top bar and persistent session header indicators report connecting, connected, retrying, resyncing, disconnected, or auth failed; bounded SSE replay plus an explicit snapshot barrier restores session state, tasks, and image references after a reload, restart, gap, backpressure disconnect, or stalled stream, while authenticated image routes recover bytes separately from authoritative transcripts. **Platform notes:** [Windows](docs/windows.md) | [Termux (Android)](docs/termux.md) | [tmux](docs/tmux.md) | [Terminal setup](docs/terminal-setup.md) | [Shell aliases](docs/shell-aliases.md) diff --git a/packages/coding-agent/docs/dashboard.md b/packages/coding-agent/docs/dashboard.md index 5a8118fd..b990cabc 100644 --- a/packages/coding-agent/docs/dashboard.md +++ b/packages/coding-agent/docs/dashboard.md @@ -123,6 +123,7 @@ networking window above. | **Session view** | Full chat drill-in. Markdown streaming transcript (text, thinking blocks with expand preference, inline provider/API failures with partial output preserved, agent-result cards, tool cards with bespoke read/write/edit/bash bodies plus full expandable inputs, markdown-rendered results for markdown-contract tools like subagent/skill/web_fetch/suggest_next, and inline tool-result images, compaction/branch summaries, custom messages), per-message copy, tasks panel, a bounded scrollable subagent panel that lists every retained agent newest-first with full running/done counts, status line with elapsed time plus ■ stop and compaction/retry aborts, a persistent session-header live indicator, and an info bar with cwd, branch, session name, token breakdown, cost/(sub)/daily rollup, ctx%, median tok/s, and a stats popover. Composer supports auto-grow, history, `/` autocomplete from `get_commands`, image attach/paste with sent images retained as user-message previews, queued-message chips with restore-all, steer/follow-up modes, and suggest-next. Registered built-in slash commands are discovered generically, deduplicated ahead of colliding resource commands, and intercepted before prompting: dashboard actions cover settings, model, scoped-models, export/import, name/session stats, fork/tree, new/compact/dream, resume/reload, and quit. `/scoped-models` deep-links to the Settings editor with the session's current cwd as project context; login/logout show an explicit not-yet-implemented notice, while copy/hotkeys/buddy give terminal-only guidance. Future built-ins are intercepted automatically. The RPC prompt boundary rejects any built-in that reaches it during command-loading races or failures, so slash text cannot leak to the model. Attachments are retained and the command is visibly rejected rather than silently discarded. The ⋯ menu covers export HTML, compact, rename, fork-from-message, loaded context, and tool expand/collapse. Session names update live from manual rename or auto-naming. Extension UI requests for select/confirm/input/editor render as modals; a rich `ask`/`ask_user` request renders inline as a single wizard that presents all its questions together — each with Markdown-formatted question text, choices, optional free text — plus an in-card Stop agent action, Escape-to-stop, and the authoritative auto-stop countdown, and is answered as one batch submit. Pending questions set needs-attention state and use the existing hidden-page notification path. Extension notifications render as toasts. | | **Subagent view** | Read-only transcript of a background agent: live events via the RPC relay, hydrated from the agent's on-disk session log (`/subagents/:agentId/messages`) so the transcript survives browser reloads. Shows the task, streaming output, tool activity, and any safe Dispatch Arbiter changed/unchanged/failure records with the final agent/model/thinking. No raw arbiter output is displayed or transported. No composer — subagents can't be steered yet; the parent session controls them. | | **Files** | Host-wide browser with places shortcuts (home, /tmp, project roots), breadcrumbs to `/`, new-folder, download, drop-zone/picker upload with explicit collision prompts, and "new session here" on any directory. It also shows the **effective global nested-context trust** for the displayed canonical directory: untrusted, trusted by that root, inherited from a granting root, or global expert trust-all. You can trust the displayed folder and descendants, or untrust the actual granting root; untrusting an inherited folder removes that root's trust for all descendants. | +| **Memories** | Dreb-only memory management for `~/.dreb/memory` and `.dreb/memory` under active/disk project roots. It lists existing `MEMORY.md` indexes and direct child `.md` entries only (no Claude paths, create, or rename), shows entry frontmatter or metadata errors so malformed files can be repaired, renders sanitized Markdown preview beside a raw editor, and uses exact SHA-256 revisions so stale saves/deletes return conflicts while preserving drafts. The index view is complete, not truncated, and warns when it exceeds the 200-line memory-index convention. Entry deletion requires both entry and index revisions, removes only matching safe Markdown-link index lines (`file.md` / `./file.md`), writes the cleaned index atomically before unlinking the entry, and rolls back loudly if the unlink fails. | | **Settings** | Persistent defaults (default model, thinking level, steering/follow-up queue modes, auto-compaction, auto-retry) via `get_settings`/`set_settings` — validation errors are shown verbatim. The scoped-models editor controls model cycling for new sessions only: grouped search, model/provider/all toggles, responsive controls, accessible up/down partial-scope ordering, and save/reset. An absent `enabledModels` is future-inclusive all models in registry order and cannot be reordered; a partial scope is a non-empty ordered list of canonical `provider/model` references. Editing legacy glob, fuzzy, or thinking-suffix values saves normalized exact references. The selected context reads effective global + project settings but writes global; a project-level `enabledModels` shadow is warned. The global-only Dispatch Arbiter card exposes enable/disable, exact authenticated model selection, thinking, guide path, and readiness guidance; model-less enablement is blocked and RPC/runtime validation remains fail-closed. Entering Settings flushes pending writes and reloads durable global + project settings, so external edits appear; read, parse, or write failures fail loudly instead of showing stale settings. The global-only nested-context policy lists every explicit trusted root for audit and revoke, offers a simple add-by-path control, and includes a prominently warned expert trust-all toggle; the Files view remains the primary place to grant trust while browsing. Most defaults seed new sessions; context-trust changes are observed by active main/subagent processes for future lazy loads, but cannot remove already injected content. Dashboard-local preferences (always expand thinking, transcript image display mode, needs-attention notification permission) live in the browser, alongside an appearance section: a theme gallery of eight curated themes (entropist.ca, Dim, Solarized, Gruvbox, Caves of Qud, Van Gogh, and the colorblind-safe Okabe-Ito and Paul Tol) with live preview cards and a system/light/dark mode selector, saved per browser. Shows the current rotating pairing code on the host/local dashboard, plus the paired-devices list with unpair. | | **Pairing** | Remote first-login: identity echo, rotating-code entry, and the security copy explaining what pairing grants. | diff --git a/packages/coding-agent/src/index.ts b/packages/coding-agent/src/index.ts index 0ef160fc..f1b55f48 100644 --- a/packages/coding-agent/src/index.ts +++ b/packages/coding-agent/src/index.ts @@ -142,6 +142,7 @@ export { } from "./core/extensions/index.js"; // Footer data provider (git branch + extension statuses - data not otherwise available to extensions) export type { ReadonlyFooterDataProvider } from "./core/footer-data-provider.js"; +export { findGitRoot } from "./core/git-root.js"; export { convertToLlm } from "./core/messages.js"; export { ModelRegistry } from "./core/model-registry.js"; export type { diff --git a/packages/dashboard/README.md b/packages/dashboard/README.md index 2546ded4..29523dc3 100644 --- a/packages/dashboard/README.md +++ b/packages/dashboard/README.md @@ -59,6 +59,13 @@ Open `http://127.0.0.1:5343`. effective global nested-context trust for the viewed canonical folder. Trust the folder and descendants, or untrust the actual granting root (including its inherited descendants). +- **Memories** — dreb-only memory management for `~/.dreb/memory` and active + project `.dreb/memory` scopes. It edits existing `MEMORY.md` indexes and + direct child `.md` entries only (no create/rename and no Claude memory paths), + displays valid entry metadata or frontmatter errors, shows sanitized Markdown + preview, warns when the complete index is over 200 lines, preserves drafts on + exact-revision conflicts, and deletes entries only after synchronously cleaning + matching safe index links. - **Settings** — persistent defaults (provider-grouped model dropdown, thinking, queue modes, image handling, skill commands, transport, hide-thinking, compaction/retry), a scoped-models editor, per-agent model @@ -83,6 +90,14 @@ The Settings scoped-models editor manages the persistent model-cycling scope. Se The selected project context reads effective global + project settings, but saves always write the global setting and warn if `.dreb/settings.json` shadows it. Changes seed new sessions only and never modify a running session. Running `/scoped-models` in a dashboard session opens this editor with that session's cwd selected. For persisted-setting and RPC details, see [Model Cycling](../coding-agent/docs/settings.md#model-cycling) and [`get_settings` / `set_settings`](../coding-agent/docs/rpc.md#settings). +### Memories + +The Memories screen exposes only dreb memory scopes: global `~/.dreb/memory` and project `.dreb/memory` directories derived from currently active sessions plus on-disk session cwd inventory. Missing memory directories are shown as missing and are not created by listing. Documents are existing-only: `MEMORY.md` is the special index, and entries are direct child `.md` files (excluding hidden/internal/path-like names). + +Saves require the exact opaque SHA-256 revision of the UTF-8 content that was loaded. A stale revision returns a conflict and leaves the browser draft intact. Entry saves validate `name`, `description`, and `type` frontmatter (`user-preferences`, `good-practices`, `project`, or `navigation`); listing/reading malformed entries surfaces a metadata error instead of hiding them so they can be repaired. The index accepts Markdown, is shown complete, and warns when it exceeds the 200-line memory-index convention. + +Deleting an entry requires both the entry revision and the current index revision (or `null` when no index exists). The server removes only index lines containing a Markdown link whose local target is exactly the entry filename or `./filename`; unsafe mixed-content lines fail loudly instead of being rewritten broadly. The cleaned index is written atomically before the entry is unlinked, and the original index is restored if unlinking fails, so a successful delete never leaves a matching dangling index link. + ### Transcript images Image blocks returned by any tool and images uploaded with a user turn render diff --git a/packages/dashboard/src/client/api.ts b/packages/dashboard/src/client/api.ts index 09f4721f..5bc17b07 100644 --- a/packages/dashboard/src/client/api.ts +++ b/packages/dashboard/src/client/api.ts @@ -16,6 +16,10 @@ import type { EventEnvelope, FleetDto, ImageAttachmentDto, + MemoryDocumentDto, + MemoryListingDto, + MemoryMutationResultDto, + MemoryScopeDto, ModelInfoDto, PairedDeviceDto, PairingCodeDto, @@ -65,6 +69,10 @@ function json(body: unknown): RequestInit { return { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) }; } +function jsonWithMethod(method: "PUT" | "DELETE", body: unknown): RequestInit { + return { method, headers: { "content-type": "application/json" }, body: JSON.stringify(body) }; +} + function withCwd(path: string, cwd?: string): string { return cwd ? `${path}?cwd=${encodeURIComponent(cwd)}` : path; } @@ -191,6 +199,22 @@ export const api = { removeTrustedContextFolder: (path: string) => request("/api/settings/remove-trusted", json({ path })), places: () => request<{ places: Array<{ label: string; path: string }> }>("/api/files/places"), + + memoryScopes: () => request<{ scopes: MemoryScopeDto[] }>("/api/memories/scopes"), + memoryListing: (scopeId: string) => request(`/api/memories/${encodeURIComponent(scopeId)}`), + memoryDocument: (scopeId: string, file: string) => + request(`/api/memories/${encodeURIComponent(scopeId)}/documents/${encodeURIComponent(file)}`), + saveMemoryDocument: (scopeId: string, file: string, content: string, revision: string) => + request( + `/api/memories/${encodeURIComponent(scopeId)}/documents/${encodeURIComponent(file)}`, + jsonWithMethod("PUT", { content, revision }), + ), + deleteMemoryEntry: (scopeId: string, file: string, revision: string, indexRevision: string | null) => + request( + `/api/memories/${encodeURIComponent(scopeId)}/entries/${encodeURIComponent(file)}`, + jsonWithMethod("DELETE", { revision, indexRevision }), + ), + downloadUrl: (path: string) => `/api/files/download?path=${encodeURIComponent(path)}`, upload: async (dir: string, file: File, overwrite: boolean) => { const res = await fetch( diff --git a/packages/dashboard/src/client/app.tsx b/packages/dashboard/src/client/app.tsx index 3d577508..4422aa76 100644 --- a/packages/dashboard/src/client/app.tsx +++ b/packages/dashboard/src/client/app.tsx @@ -7,6 +7,7 @@ import { createEffect, type JSX, Match, onCleanup, onMount, Switch } from "solid import { ToastRegion } from "./components/common.js"; import { FilesScreen } from "./screens/files.js"; import { FleetScreen } from "./screens/fleet.js"; +import { MemoriesScreen } from "./screens/memories.js"; import { PairingScreen } from "./screens/pairing.js"; import { SessionScreen } from "./screens/session.js"; import { SettingsScreen } from "./screens/settings.js"; @@ -169,6 +170,9 @@ export function App(): JSX.Element { + + + Object.values(props.store.sessions).filter((s) => s.needsAttention).length + props.store.fleet().runtimes.filter((r) => r.needsAttention).length; @@ -51,6 +51,9 @@ export function Topbar(props: { store: AppStore; active: "fleet" | "files" | "se files + + memories + settings diff --git a/packages/dashboard/src/client/screens/memories.tsx b/packages/dashboard/src/client/screens/memories.tsx new file mode 100644 index 00000000..3ae5ec8c --- /dev/null +++ b/packages/dashboard/src/client/screens/memories.tsx @@ -0,0 +1,412 @@ +/** + * Memories tab — dreb global/project memory browser and editor. + * Existing documents only: MEMORY.md index plus direct child .md entries. + */ + +import { createEffect, createResource, createSignal, For, type JSX, Show } from "solid-js"; +import type { + MemoryDocumentDto, + MemoryEntrySummaryDto, + MemoryListingDto, + MemoryScopeDto, +} from "../../shared/protocol.js"; +import { api } from "../api.js"; +import { Modal, Topbar } from "../components/common.js"; +import { MarkdownBody } from "../components/transcript.js"; +import type { AppStore } from "../state/store.js"; + +const INDEX_FILE = "MEMORY.md"; + +type SelectedDocument = { scopeId: string; file: string }; + +function metadataLine(entry: MemoryEntrySummaryDto): string { + if (entry.metadata) return `${entry.metadata.name} · ${entry.metadata.type}`; + return entry.metadataError ? `metadata error: ${entry.metadataError}` : "metadata unavailable"; +} + +function documentTitle(document: MemoryDocumentDto | undefined, file: string | undefined): string { + if (!document) return file ?? "memory"; + if (document.kind === "index") return "MEMORY.md index"; + return document.metadata?.name ?? document.file; +} + +function selectDefaultFile(listing: MemoryListingDto | undefined): string | undefined { + if (!listing) return undefined; + if (listing.indexContent !== null) return INDEX_FILE; + return listing.entries[0]?.file; +} + +export function MemoriesScreen(props: { store: AppStore }): JSX.Element { + const [selectedScopeId, setSelectedScopeId] = createSignal(); + const [selectedFile, setSelectedFile] = createSignal(); + const [error, setError] = createSignal(); + const [status, setStatus] = createSignal(); + const [draft, setDraft] = createSignal(""); + const [dirty, setDirty] = createSignal(false); + const [saving, setSaving] = createSignal(false); + const [deleteTarget, setDeleteTarget] = createSignal(); + + const [scopes, { refetch: refetchScopes }] = createResource(async () => { + try { + const result = await api.memoryScopes(); + return result.scopes; + } catch (failure) { + setError(failure instanceof Error ? failure.message : String(failure)); + return []; + } + }); + + createEffect(() => { + const all = scopes(); + if (!all?.length) return; + const current = selectedScopeId(); + if (!current || !all.some((scope) => scope.id === current)) setSelectedScopeId(all[0].id); + }); + + const [listing, { mutate: mutateListing, refetch: refetchListing }] = createResource( + () => selectedScopeId(), + async (scopeId): Promise => { + setError(undefined); + setStatus(undefined); + try { + return await api.memoryListing(scopeId); + } catch (failure) { + setError(failure instanceof Error ? failure.message : String(failure)); + return undefined; + } + }, + ); + + createEffect(() => { + const current = listing(); + if (!current) return; + const file = selectedFile(); + const available = new Set([ + ...(current.indexContent !== null ? [INDEX_FILE] : []), + ...current.entries.map((e) => e.file), + ]); + if (!file || !available.has(file)) setSelectedFile(selectDefaultFile(current)); + }); + + const [document, { mutate: mutateDocument, refetch: refetchDocument }] = createResource( + (): SelectedDocument | undefined => { + const scopeId = selectedScopeId(); + const file = selectedFile(); + return scopeId && file ? { scopeId, file } : undefined; + }, + async (selection): Promise => { + setError(undefined); + setStatus(undefined); + try { + return await api.memoryDocument(selection.scopeId, selection.file); + } catch (failure) { + setError(failure instanceof Error ? failure.message : String(failure)); + return undefined; + } + }, + ); + + createEffect(() => { + const doc = document(); + if (!doc || dirty()) return; + setDraft(doc.content); + }); + + function chooseScope(scope: MemoryScopeDto) { + if (selectedScopeId() === scope.id) return; + setSelectedScopeId(scope.id); + setSelectedFile(undefined); + mutateDocument(undefined); + setDirty(false); + setDraft(""); + } + + function chooseFile(file: string) { + if (selectedFile() === file) return; + setSelectedFile(file); + mutateDocument(undefined); + setDirty(false); + setDraft(""); + } + + async function refreshAll() { + await refetchScopes(); + await refetchListing(); + if (selectedFile()) await refetchDocument(); + } + + async function save() { + const scopeId = selectedScopeId(); + const doc = document(); + if (!scopeId || !doc) return; + setSaving(true); + setError(undefined); + setStatus(undefined); + try { + const result = await api.saveMemoryDocument(scopeId, doc.file, draft(), doc.revision); + mutateListing(result.listing); + if (result.document) mutateDocument(result.document); + setDraft(result.document?.content ?? draft()); + setDirty(false); + setStatus("saved"); + } catch (err: any) { + setError( + err?.status === 409 + ? `${err.message} Your draft is still here.` + : err instanceof Error + ? err.message + : String(err), + ); + } finally { + setSaving(false); + } + } + + async function confirmDelete() { + const scopeId = selectedScopeId(); + const doc = deleteTarget(); + const currentListing = listing(); + if (!scopeId || !doc || doc.kind !== "entry" || !currentListing) return; + setSaving(true); + setError(undefined); + setStatus(undefined); + try { + const result = await api.deleteMemoryEntry(scopeId, doc.file, doc.revision, currentListing.indexRevision); + mutateListing(result.listing); + mutateDocument(undefined); + setDeleteTarget(undefined); + setSelectedFile(selectDefaultFile(result.listing)); + setDirty(false); + setDraft(""); + setStatus(`deleted ${doc.file}; index links cleaned up`); + } catch (err: any) { + setError(err instanceof Error ? err.message : String(err)); + } finally { + setSaving(false); + } + } + + return ( +
+ +
+
+
+

Memories

+

+ Edit dreb memory only: global ~/.dreb/memory plus known project .dreb/memory scopes. Claude memory + paths are never included. +

+
+ +
+ + +

{error()}

+
+ +

{status()}

+
+ +
+ + +
+ +
+ Complete index warning: MEMORY.md is over 200 lines. The dashboard shows the + full file for repair, while the agent prompt may only load the indexed prefix. +
+
+ Select an existing memory document.

}> + {(doc) => ( + <> +
+
+

{documentTitle(doc(), selectedFile())}

+

+ {doc().file} · revision {doc().revision.slice(0, 12)} +

+
+
+ + + + +
+
+ + + {(meta) => ( + + )} + + +

Metadata error: {doc().metadataError}

+
+ +
+ + + {dirty() ? "unsaved draft" : "saved revision loaded"} +
+ +