diff --git a/docs/superpowers/specs/2026-09-01-project-notes-design.md b/docs/superpowers/specs/2026-09-01-project-notes-design.md new file mode 100644 index 0000000..02027fc --- /dev/null +++ b/docs/superpowers/specs/2026-09-01-project-notes-design.md @@ -0,0 +1,323 @@ +# Project Notes — design + +**Date:** 2026-09-01 · **Baseline:** v0.4 · Companion to [ROADMAP.md](../../../ROADMAP.md) +and [DESIGN-REVIEW.md](../../../DESIGN-REVIEW.md). + +A per-project notes surface, with a per-note **Send to agent** action that puts the note +into a running Claude session's prompt. + +--- + +## Why this earns a slot + +DESIGN-REVIEW's coherence test says every screen answers exactly one question. Notes +answers *"what do I want to hand this agent, and what did I keep learning here?"* — and it +answers it **while the container is stopped**, which is the gap the stop/start container +model creates and the same reasoning that made Sessions/Resume the flagship. + +Two things already do part of this job, and the design is shaped to avoid both: + +- `Project.claude_instructions` (`models/project.rs:405`, editor at + `components/projects/ClaudeInstructionsEditor.tsx`) is per-project free text merged into + the container's `CLAUDE.md` on every start. It is **ambient** — always in context, never + addressed. Notes are **discrete and fired on demand**. If Notes drifts into a second + instructions box, it is redundant with a feature that already ships. +- A `NOTES.md` in the workspace is readable by the agent already, but unreadable by the + user when the container is stopped, and invisible to the fleet view. + +What Notes uniquely adds is *addressable items with a fire-at-the-session action*. + +## Decisions taken + +| Decision | Choice | Rationale | +|---|---|---| +| Audience | Human scratchpad **and** agent prompts, one surface | See "no note types" below | +| Storage | Own file per project, host-side | Keeps prose out of `projects.json`; works with the container stopped | +| Send target | Project's own sessions; picker when >1 | Never guesses; mirrors STT's target-pinning guard | +| Surface | Side dock that takes space **inward**; never resizes the OS window | Phase 0 spike: growing corrupts under native Wayland, §6.1 | +| Formatting | Plain text, no markdown | It is a scratchpad; see §3 | +| Tab position | Last, after Browser | A companion to the work, not a step in it | + +**No note *types*.** A note is a title plus a body. What makes one "for the agent" is that +you pressed the button, not a mode set at creation. The moment there is a "prompt note" vs +"scratch note" toggle, the pane is two features wearing one coat, and every note costs a +classification decision at the moment of writing — which is the moment the user is least +willing to make one. + +--- + +## 1. Storage + +New `app/src-tauri/src/storage/notes_store.rs`, modeled on `migration_store.rs` rather than +on `projects_store.rs`: + +``` +/triple-c/notes/{project_id}.json +``` + +- **`sanitize()` on the project id**, copied from `migration_store.rs:41-46`. The id arrives + over IPC; it must not be able to steer the write. +- **Atomic write** — `.tmp` then `rename()`, per `projects_store.rs:167-179` and the Key + Conventions rule in CLAUDE.md. +- **Corrupt file is quarantined to `.bak`, not discarded** (`projects_store.rs:24-61`). + +```rust +struct Note { + id: String, // uuid v4 + title: String, + body: String, + pinned: bool, + created_at: String, // RFC 3339 + updated_at: String, +} +struct ProjectNotes { version: u32, notes: Vec } +``` + +Order is pinned-first then `updated_at` descending. Manual reordering is deliberately out. + +### Why not a field on `Project` + +`projects.json` is written on **every blur** by the debounced `useProjectSave` path +(`hooks/useSaveState.ts`, threaded through `ProjectHome.tsx:79-81` into Overview and +Config). Long user prose on that record means (a) the whole project list is rewritten every +time a note changes, and (b) a note edit and a Config edit can race, with the loser's write +clobbering the winner's. `migration_store.rs:1-12` already documents this exact reasoning +for why *it* is not in `projects.json`. Notes inherit it. + +A per-project file also means a corrupt notes file loses notes for one project, not the +project list. + +### Lifecycle + +`remove_project` deletes the project's notes file. A failure there is logged, never fatal — +an orphaned notes file is harmless, a project that cannot be removed is not. + +## 2. Commands and frontend state + +Registered in `lib.rs` via `generate_handler!`. Per CLAUDE.md, application commands need +**no** entry in `capabilities/default.json`. + +- `list_notes(projectId) -> Vec` +- `save_note(projectId, note) -> Note` — upsert; stamps `updated_at` backend-side +- `delete_note(projectId, noteId)` + +There is deliberately **no whole-list setter**. Bulk writes are the clobbering mechanism the +storage choice above exists to avoid; a read-modify-write under the store's `Mutex` per note +is both correct and cheap. + +Frontend: wrappers in `lib/tauri-commands.ts`, a `hooks/useNotes.ts`, and notes cached in +zustand keyed by project id. Rust is the source of truth; the cache is a cache. + +The dock and the tab live in one webview, so zustand alone suffices. The store boundary is +drawn so that a future detached window (§8) only swaps the transport: Rust emits a +`notes-changed` event, both windows listen. + +## 3. Editor — plain text, deliberately + +A note is a title and a `