From e58e2cdaf72c9568cdc936d3e6e059f19b45ccdd Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Tue, 1 Sep 2026 11:48:31 -0700 Subject: [PATCH] Design a per-project Notes tab with a send-to-agent action MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Notes are discrete, addressable items with a button that puts one into a running Claude session's prompt. That is deliberately not what `claude_instructions` does — that field is *ambient*, merged into the container's CLAUDE.md on every start and always in context. Nor is it a `NOTES.md` in the workspace, which the agent can read but the user cannot, once the container is stopped. Discrete items, fired on demand, readable with the container down, is the gap neither of those covers. Storage is one file per project under the app data dir, following `migration_store.rs` rather than living on the `Project` record: that record is rewritten on every blur by the debounced save path, so notes there would mean the whole project list is rewritten per keystroke-batch and a note edit could clobber a Config edit. `migration_store.rs` already documents that reasoning for itself. Two findings are worth more than the design they support. **Newlines already have a verified answer.** A note body has newlines; typed as raw keystrokes each one submits a separate prompt, so a note would arrive as N truncated messages. `TerminalView.tsx` already sends `\x1b\r` for Shift+Enter and its comment states those are the in-band bytes, not a guess, with an explicit warning against simplifying to `\n` because a shell would run the line. Send-to-agent reuses that sequence through one shared helper, and — from the same comment — only offers `claude` sessions as targets, since bash's readline has no binding for it and merely bells. **The dock cannot widen the OS window.** A throwaway Tauri app was built and run on KDE Plasma to find out, because the app has no window-geometry code to reason from. Under XWayland every test passed exactly. Under native Wayland the same binary asked +420 and got +600, moved the height +276 without being asked, compounded that offset on every call, and ended reporting 5400x2900 on a 4800x2700 monitor. Worse, `outer_position()` did not fail — it returned `Ok(0,0)` for a window that was not at 0,0, so a "cannot determine position, do not grow" fallback never fires. A clean failure could have been handled; a plausible wrong answer cannot be detected from the value itself. AppImages get XWayland because linuxdeploy-plugin-gtk forces GDK_BACKEND=x11; the .deb and .rpm do not. The split is therefore by *packaging*, not platform — two users on identical hardware would see different behavior. So the dock takes space inward on every backend, which also costs nothing: the ResizeObserver in `TerminalView.tsx` already reflows xterm and resizes the container PTY on width change. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01HjL1E2JFNctUqCYotUwqqb --- .../specs/2026-09-01-project-notes-design.md | 323 ++++++++++++++++++ 1 file changed, 323 insertions(+) create mode 100644 docs/superpowers/specs/2026-09-01-project-notes-design.md 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 `