Adds a per-project Notes surface: a scratchpad that doubles as an agent prompt launcher. Every note carries a Send to agent action that injects its text into a running Claude Code session for that project.
A Notes tab on Project Home, last in the row after Browser.
A side dock that pushes the app content inward, drag-resizable, width and open-state persisted.
Notes are plain text — title, body, pin. No note "types"; the same note serves as a reminder to you and as a prompt to the agent.
Send to agent resolves to the project's own sessions, with a picker when there is more than one.
Storage: host-side, one JSON file per project
Notes live on the host, not in the container, so they are readable with the project stopped. notes_store.rs is a free-function module (the migration_store.rs shape, not the struct-with-Mutex one) and writes are atomic and durable: .tmp → sync_all() → rename() → fsync the directory. The on-disk form is a { version, notes } envelope; a bare array still parses, so nothing written by an earlier build is orphaned.
A file that fails to parse is not silently discarded — it is copied aside first, capped at 4 copies so a repeatedly-corrupt file cannot fill the disk. Removing a project drops its notes alongside the migration artifacts; a failure there is logged, not propagated, since the project is already gone.
The project id arrives over IPC, so it is sanitized before it reaches a path.
The dock takes space inward — deliberately, on evidence
The original design grew the OS window rightward when the dock opened. A throwaway AppImage spike disproved that, and §6.1 of the spec records both runs:
requested
got
XWayland
+420 width
+420 width, height unchanged
native Wayland
+420 width
+600 width, +276 height nobody asked for
Native Wayland compounded a decoration offset per call, ending at 5400×2900 on a 4800×2700 monitor, and outer_position() reported a plausible Ok(0, 0) rather than failing — so a None-fallback would never have fired. The split is by packaging, not platform: linuxdeploy-plugin-gtk forces GDK_BACKEND=x11 into every Tauri AppImage, so the AppImage passes and the .deb/.rpm corrupts.
There is therefore no window-geometry API anywhere on this branch — a grep confirms the only mention is the comment explaining why.
Sending to the agent
toClaudePayload maps every newline to \x1b\r — Claude Code's in-band soft-newline, the same sequence its own /terminal-setup installs. A multi-line note arrives as one unsubmitted prompt with the cursor left in it, rather than as N submitted lines.
Two details that are not obvious:
The transform matches \r\n|\r|\n, not \r?\n. A lone \r submits the prompt.
It applies only when sessionType === "claude". A bash readline would run each line, and would ring the bell at the escape.
Ordering: the cache is sequenced, not last-write-wins
The tab and the dock can be open on the same project at once, so they share one zustand notesByProject slice rather than each holding a useState copy — two private caches let an edit in one pane silently destroy a saved edit in the other.
Sharing the cache is not sufficient on its own. Every write claims a per-project monotonic sequence token at issue time, and commitNotes drops any result whose token has been superseded. That is what stops a slow panel-mount list_notes from landing on top of a fresher post-save refresh, and it closes the p1 → p2 → p1 hole that an identity-based guard leaves open. commitNotes is the only caller of setProjectNotes in the entire frontend — one unsequenced write would void the guarantee.
list_notes sorts pinned-first then updated_at descending, in the backend, so every caller agrees on order without re-sorting.
NotesPanel.shared.test.tsx mounts two panels on one project against the real hook — the cross-pane corruption is a regression test, not a code comment.
Two things the suite cannot reach
jsdom never synthesizes the follow-up keypress — TerminalView.tsx's own comment records this — so these stay manual, for the pre-release pass:
The terminal visibly reflows when the dock opens.
A multi-line note arrives as one unsubmitted prompt with the cursor left in it.
Known follow-up, not introduced here
An unavailable control that is disabled is removed from the accessibility tree, so any explanation of why it is unavailable is announced to nobody. This branch's send-to-agent button does that, but so do controls on main, so it is being fixed app-wide in #49 rather than here — including the aria-disabled click-guard that the swap requires. Once both land, SendToAgentButton should adopt the useUnavailable hook that #49 introduces.
Adds a per-project **Notes** surface: a scratchpad that doubles as an agent prompt launcher. Every note carries a **Send to agent** action that injects its text into a running Claude Code session for that project.
Design: `docs/superpowers/specs/2026-09-01-project-notes-design.md`. Plan: `docs/superpowers/plans/2026-09-01-project-notes.md`.
## What you get
- A **Notes** tab on Project Home, last in the row after Browser.
- A **side dock** that pushes the app content inward, drag-resizable, width and open-state persisted.
- Notes are plain text — title, body, pin. No note "types"; the same note serves as a reminder to you and as a prompt to the agent.
- **Send to agent** resolves to the project's own sessions, with a picker when there is more than one.
## Storage: host-side, one JSON file per project
Notes live on the host, not in the container, so they are readable with the project stopped. `notes_store.rs` is a free-function module (the `migration_store.rs` shape, not the struct-with-Mutex one) and writes are atomic and durable: `.tmp` → `sync_all()` → `rename()` → fsync the directory. The on-disk form is a `{ version, notes }` envelope; a bare array still parses, so nothing written by an earlier build is orphaned.
A file that fails to parse is not silently discarded — it is copied aside first, capped at 4 copies so a repeatedly-corrupt file cannot fill the disk. Removing a project drops its notes alongside the migration artifacts; a failure there is logged, not propagated, since the project is already gone.
The project id arrives over IPC, so it is sanitized before it reaches a path.
## The dock takes space inward — deliberately, on evidence
The original design grew the OS window rightward when the dock opened. **A throwaway AppImage spike disproved that**, and §6.1 of the spec records both runs:
| | requested | got |
|---|---|---|
| XWayland | +420 width | +420 width, height unchanged |
| native Wayland | +420 width | **+600 width, +276 height nobody asked for** |
Native Wayland compounded a decoration offset per call, ending at 5400×2900 on a 4800×2700 monitor, and `outer_position()` reported a plausible `Ok(0, 0)` rather than failing — so a `None`-fallback would never have fired. The split is by **packaging**, not platform: `linuxdeploy-plugin-gtk` forces `GDK_BACKEND=x11` into every Tauri AppImage, so the AppImage passes and the `.deb`/`.rpm` corrupts.
There is therefore **no window-geometry API anywhere on this branch** — a grep confirms the only mention is the comment explaining why.
## Sending to the agent
`toClaudePayload` maps every newline to `\x1b\r` — Claude Code's in-band soft-newline, the same sequence its own `/terminal-setup` installs. A multi-line note arrives as **one unsubmitted prompt with the cursor left in it**, rather than as N submitted lines.
Two details that are not obvious:
- The transform matches `\r\n|\r|\n`, not `\r?\n`. A lone `\r` submits the prompt.
- It applies only when `sessionType === "claude"`. A bash readline would *run* each line, and would ring the bell at the escape.
## Ordering: the cache is sequenced, not last-write-wins
The tab and the dock can be open on the same project at once, so they share one zustand `notesByProject` slice rather than each holding a `useState` copy — two private caches let an edit in one pane silently destroy a saved edit in the other.
Sharing the cache is not sufficient on its own. Every write claims a **per-project monotonic sequence token at issue time**, and `commitNotes` drops any result whose token has been superseded. That is what stops a slow panel-mount `list_notes` from landing on top of a fresher post-save refresh, and it closes the `p1 → p2 → p1` hole that an identity-based guard leaves open. `commitNotes` is the only caller of `setProjectNotes` in the entire frontend — one unsequenced write would void the guarantee.
`list_notes` sorts pinned-first then `updated_at` descending, in the **backend**, so every caller agrees on order without re-sorting.
## Testing
**711 frontend tests across 58 files; 541 Rust tests, 1 ignored, 0 failed. `tsc --noEmit` clean.**
`NotesPanel.shared.test.tsx` mounts two panels on one project against the real hook — the cross-pane corruption is a regression test, not a code comment.
## Two things the suite cannot reach
jsdom never synthesizes the follow-up keypress — `TerminalView.tsx`'s own comment records this — so these stay manual, for the pre-release pass:
1. The terminal **visibly reflows** when the dock opens.
2. A multi-line note arrives as **one unsubmitted prompt with the cursor left in it**.
## Known follow-up, not introduced here
An unavailable control that is `disabled` is removed from the accessibility tree, so any explanation of *why* it is unavailable is announced to nobody. This branch's send-to-agent button does that, but so do controls on `main`, so it is being fixed app-wide in **#49** rather than here — including the `aria-disabled` click-guard that the swap requires. Once both land, `SendToAgentButton` should adopt the `useUnavailable` hook that #49 introduces.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_011YPqHpjV4EL6RNEwrRKqQm
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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HjL1E2JFNctUqCYotUwqqb
Seven tasks, each ending in a testable deliverable: the store, the IPC
surface, the hook, the two shared helpers, the send button, the tab, and
the dock.
Two extractions are folded in rather than left for later, both because
this feature would otherwise duplicate knowledge that is already written
down. `\x1b\r` becomes `lib/claudeInput.ts` so the hard-won comment in
`TerminalView` stays the single source of truth for a sequence that must
never be "simplified" to `\n`. The session display-name rule becomes
`lib/sessionName.ts`, which is a fix rather than a precaution: the rule is
currently written twice inside `MainTabs.tsx`, both copies local and
non-exported, and the send-target picker would have made three.
The spec is also corrected in three places against what the code actually
does. `migration_store` is a free-function module with no struct, so the
notes store is too, and the "read-modify-write under the store's Mutex"
line described a shape that file does not have — the upsert takes an
explicit process-wide write lock instead, and the read path takes none.
`useProjectSave` has no debounce; its only timer is a 2500 ms reset of the
"Saved" label. And the storage section now specifies the durable write
`migration_store` uses — fsync the file, rename, fsync the directory —
rather than `projects_store`'s bare rename, because notes are prose
nothing else holds a copy of.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HjL1E2JFNctUqCYotUwqqb
- Finding 1 (saveNote): After a successful save, re-read the canonical list from the backend instead of patching in place. A successful save stamps a new updated_at, and the backend sorts by updated_at descending, so the record's position has changed and positional patching would disagree with what a reload would show. If the re-read fails, keep the save reported as successful and leave the existing list alone.
- Finding 2 (stale notes): Clear notes on projectId change (not only when empty) and on load failure. Previously, switching from project A to project B would leave A's notes on screen until B's fetch resolved, and if a user edited one, A's note would be written into B's notes file—cross-project data corruption. If a load fails, A's notes stay visible under B indefinitely.
- Added four new tests covering these scenarios: projectId change clears old notes, failed load leaves no stale notes, saving a new note ends with the backend's list, and saves re-read the list rather than patching.
When a save is in flight for project A and the user switches to project B before it resolves, the stale closure still has projectId=A. When A's save resolves, the post-save re-read of listNotes(projectId) runs with the stale closed-over projectId, and setNotes(reloaded) overwrites B's displayed notes with A's list—the same cross-project contamination class as Finding 2 but reintroduced through the fix itself.
Fix: Add a currentProjectId ref updated on every render, and guard both saveNote and deleteNote callbacks with a check before replacing/filtering the whole list. If the project changed while the async operation was in flight, bail out of the state update but still report success (the operation itself succeeded on the backend; only the stale list update is skipped).
Added test: a save in flight for one project, a switch to another, then the first save resolving—asserts the second project's notes are still displayed.
- notesDockWidth store initialization now clamps/defaults a bad
localStorage value on load, not just on write (verified this fails
without the clamp).
- The keyboard resize test asserts the exact widened/narrowed value
instead of just that the setter was called, so a swapped or
inverted arrow-key branch would be caught.
Two things the plan dropped from the design spec's §1.
`keep_corrupt_copy`'s only guard was "does this second's copy already
exist", so a persistently unparseable file minted a full copy of the
user's prose every time the clock ticked over — and `list_notes` runs on
*every* NotesPanel mount, i.e. every project switch, every
dock-follows-tab change, every sub-tab toggle. A minute of clicking
between two projects was ~60 copies. `MAX_CORRUPT_BACKUPS`,
`corrupt_backups_full()` and the three-outcome `Kept` enum come across
from `migration_store` whole, including the reason the cap is asked
*before* the copy (so it is not implemented by writing a file and
deleting it again, and so the surviving copies are the oldest ones) and
the reason the log line must not claim a backup that was never written.
The file itself is now `{ version, notes }` rather than a bare array.
It costs nothing today and gets permanently more expensive once files
exist in the field. No released build has written notes, so there is no
migration path — but a bare array is still *read*, because declaring a
perfectly readable file corrupt is the one outcome this store exists to
avoid, and a developer's own notes are prose nothing else has a copy of.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HjL1E2JFNctUqCYotUwqqb
Implements the design spec's §2 — "notes cached in zustand keyed by
project id" — which the plan substituted with a hook-local `useState`.
Sharing the `NotesPanel` *component* between the Project Home sub-tab and
the dock did not share the *cache*. Both resolve to the same project, so
two panels mount two `useNotes(P)`, each with its own list. Edit a note
in the dock and blur; the tab's copy is still pre-edit, and the tab's
next blur commits `{...staleRecord, title, body}` — the dock's edit gone
from disk with no error and no indicator. That is the feature's own
primary workflow: take notes in the dock while the agent runs, which is
the reason the dock exists, then go back to the tab.
`notesByProject` plus a per-project in-flight flag now hold the list.
Both surfaces render from one array; two panels mounting for one project
make one read; and because the write is keyed by project, a response
that lands after the user has moved on updates the project it belongs to
rather than whichever is on screen. This is also the boundary §8 says a
detached notes window needs.
Three more bugs in the same code, fixed with it:
- Delete-after-edit could resurrect the note. Clicking Delete with the
textarea focused fires blur first, so `save_note` and `delete_note` go
out back to back; Rust's `write_lock` stops them interleaving but does
not order them, and a delete that wins the lock is undone by the
upsert behind it. A project's mutations now go through one promise
chain, module-scoped for the reason `useTerminal`'s input queue is.
- An unsaved draft vanished when any other note was saved, because the
re-read replaced the list with the backend's. "New note" now persists,
so the backend owns the row from the start — chosen over merging local
drafts because a local-only row in a *shared* cache would exist in the
panel that made it and nowhere else.
- The save outcome was reported for the wrong project after a switch:
the guard covered only the list replacement, so the new project's
SaveIndicator flashed "Saved ✓" for the old project's write. The
indicator now resets on a project change and reports only its own.
`NotesPanel` also re-seeds its draft when the *stored* text of the note
it has selected changes, so an edit made in the other surface reaches
the editor and not only the list. It never overwrites something
half-typed; that still blurs into a last-writer-wins save, as any
blur-commit editor does.
NotesPanel.shared.test.tsx is the configuration none of the existing
tests had: two panels, one project, the real hook. Four of its six
assertions fail against the previous implementation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HjL1E2JFNctUqCYotUwqqb
`toClaudePayload` matched `/\r?\n/`, so a bare CR that is not part of a
CRLF went through verbatim — and a bare CR *submits* in a Claude prompt
and *runs* the line in a shell, which is the terminator the function's
own contract says it never appends. A `<textarea>` cannot produce one,
but `load_in` returns whatever a hand-edited or externally written notes
file holds, so the guarantee has to cover that rather than only what the
editor can type.
`Note.pinned` is persisted and sorted on, but nothing in the app sets
it: there is no pin control and no indicator. The spec stated the
ordering rule as though pinning existed and §8 did not list it, so the
spec is amended to say `pinned` is reserved and inert in v1, and pinning
is added to the out-of-scope list. No UI is added — a user-facing
affordance does not belong in a fix wave.
Also renamed NotesPanel.test.tsx's "deletes the selected note and falls
back to another": `useNotes` is mocked in that file and the mocked list
never changes, so the fallback was never exercised. A name that claims
coverage which is absent is worse than an absent test, because it makes
the gap invisible. The real assertion now lives against the real hook in
NotesPanel.shared.test.tsx.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HjL1E2JFNctUqCYotUwqqb
One gesture puts two requests in flight. With the tab already loaded, clicking
the dock toggle while the textarea has focus fires `blur` -> `saveNote` and the
dock's mount -> `list_notes` in the same tick. The save finishes and its re-read
writes the post-save list; the mount's read -- issued earlier, still out -- then
lands its pre-save snapshot on top, and both panels show stale text until
something else refreshes.
`mutationChains` could not have caught this: it orders a project's writes
against each other and the mount load is a read outside it. Putting the read on
the chain would work, but it buys correctness with latency the user feels -- a
panel mount waiting behind `save_note`'s double-fsync write -- and leaves a
"mutation chain" holding reads.
The two requests are not competing for a resource; the loser's result is simply
older. So every write into `notesByProject[p]` now claims a per-project sequence
when the request behind it is issued, and `commitNotes` drops one whose sequence
predates what is already cached. Reads take their sequence at issue time, since
being ordered by resolution is the bug. Local patches -- the filter behind a
confirmed delete, the prepend behind a failed re-read -- take a fresh one at
commit time, because they are authoritative then rather than derived from an
earlier read, and anything still in flight behind them is genuinely stale. A
failed read commits under its *own* sequence, not a fresh one, so its empty list
cannot beat a later read that has the real answer.
`isCurrent()` stays, and is not folded in. It guards `setSaveState`, not the
cache: it asks whether this *panel* is still showing the project a save was made
for, which a per-project counter cannot answer -- two panels on one project share
every sequence value. Ordering and panel identity are two questions.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HjL1E2JFNctUqCYotUwqqb
The panel splits master/detail unconditionally: a 192px title column beside
the editor. That fits the Project Home tab and does not fit the dock. At the
dock's 352px default the editor gets 157px, and its action row wants ~200px,
so the Delete button lands outside the dock's `overflow-hidden` with no
scrollbar to reach it, and the textarea collapses to a two-word column.
The two surfaces differ in width while sharing a viewport, so this is a
container query rather than a `md:` breakpoint — a viewport query reads the
window and hands both surfaces the same answer, which is wrong for one of
them. Tailwind v4 has these in core; verified as real
`@container (min-width: 32rem)` rules in the built CSS, since a variant that
silently compiles to nothing looks identical in review.
The threshold is arithmetic: side by side needs the 192px list, an editor
wide enough for its own buttons (~280px), and the divider. `@lg` (512px) is
the first stop clearing ~473px. Below it the titles become a capped strip
above the editor, so the note being written keeps the height.
The action row now wraps, which is the part that holds at *any* width rather
than on one side of a threshold: the buttons are a group that does not shrink,
the title field shrinks to 96px, and past that the title takes one row and the
buttons the next. Nothing can be pushed out of the panel.
Not covered by the suite — jsdom has no layout engine, so 711 tests pass
before and after. This needs eyes on the dock at its minimum, default and
maximum widths.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011YPqHpjV4EL6RNEwrRKqQm
The dock was showing `NotesPanel`, which is a master/detail layout: a column
of titles beside an editor. The previous commit made that survive dock width;
it did not make it right. At 352px the layout still spends roughly 356px of
height on chrome — dock header, panel header, title strip, a button row that
wraps, and a paragraph of help — before the body gets a pixel.
So the dock now shows one note. The title field names what is open and the
chevron beside it switches; New and Delete move into the overflow menu; the
help text goes. Chrome drops to about 112px and the body takes the rest.
The two surfaces are now different components, which contradicts a docstring
I wrote — "shared so the two cannot drift into different behaviour". That
claim was about behaviour, and behaviour was never in the layout: it is in
`useNotes` for the cache and its write ordering, and now in `useNoteDraft`,
extracted here so when a keystroke becomes a save is defined in exactly one
place. Only the layout diverges. `NotesPanel.shared.test.tsx` gets stronger
for it — it now mounts the dock panel and the tab panel together, which is
what the app actually does, instead of the same component twice.
`NoteSwitcher` is not `OverflowMenu` despite the shape being close: that keys
items by label, and notes are addressed by id, so two untitled notes — the
ordinary case — would collapse into one row. It is also not a `combobox`; an
input plus a listbox button is two honest controls, where the role would owe
active-descendant tracking and filtering that nothing here needs.
`SendToAgentButton` picks up `useUnavailable` from #49, which is what its
`disabled` plus explanatory `title` was already asking for. Four tests moved
from `toBeDisabled()` to the new contract, and one of them — "does nothing for
an empty note" — turned out never to have asserted that it does nothing. It
does now, for click and for Enter, which is the guard the swap needs.
It also gains `dropUp`, and that is load-bearing rather than cosmetic: the
dock clips its own overflow, so a session menu opening downward from a button
on the bottom edge is drawn outside the panel and never seen.
744 tests pass, 62 files. As before, jsdom has no layout engine: that the dock
now reads as compact is not something the suite can tell you.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011YPqHpjV4EL6RNEwrRKqQm
Sending already switched to the target terminal's tab, which looks like it
should be enough: `TerminalView` focuses xterm whenever a terminal becomes
active. But that effect keys off `active`, so it only fires on a *change* —
and the dock's ordinary case is sending to the terminal already on screen.
`setActiveTabKey` writes the key that is already set, nothing changes, no
effect re-runs, and focus stays on the Send button. The note is sitting in the
prompt and the user still has to click the terminal before pressing Enter.
So the send now asks for focus explicitly, through a one-shot request in the
store that `TerminalView` consumes and clears — the shape `pendingHomeTab`
already uses. Clearing is not tidiness: hold the id and the second send to the
same terminal writes a value that is already there, which is precisely the
no-op this exists to fix.
Focus is requested only on success. A failed send toasts and leaves the user
where they are, because there is nothing in the prompt to press Enter on.
The three `TerminalView` tests give focus away after mounting before making
any assertion, so what they observe is the request landing and never the focus
that `active` already grants on mount — which would pass with the feature
absent.
752 tests pass, 62 files.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011YPqHpjV4EL6RNEwrRKqQm
jknapp
merged commit dc9cdd1760 into main2026-09-02 20:42:07 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Adds a per-project Notes surface: a scratchpad that doubles as an agent prompt launcher. Every note carries a Send to agent action that injects its text into a running Claude Code session for that project.
Design:
docs/superpowers/specs/2026-09-01-project-notes-design.md. Plan:docs/superpowers/plans/2026-09-01-project-notes.md.What you get
Storage: host-side, one JSON file per project
Notes live on the host, not in the container, so they are readable with the project stopped.
notes_store.rsis a free-function module (themigration_store.rsshape, not the struct-with-Mutex one) and writes are atomic and durable:.tmp→sync_all()→rename()→ fsync the directory. The on-disk form is a{ version, notes }envelope; a bare array still parses, so nothing written by an earlier build is orphaned.A file that fails to parse is not silently discarded — it is copied aside first, capped at 4 copies so a repeatedly-corrupt file cannot fill the disk. Removing a project drops its notes alongside the migration artifacts; a failure there is logged, not propagated, since the project is already gone.
The project id arrives over IPC, so it is sanitized before it reaches a path.
The dock takes space inward — deliberately, on evidence
The original design grew the OS window rightward when the dock opened. A throwaway AppImage spike disproved that, and §6.1 of the spec records both runs:
Native Wayland compounded a decoration offset per call, ending at 5400×2900 on a 4800×2700 monitor, and
outer_position()reported a plausibleOk(0, 0)rather than failing — so aNone-fallback would never have fired. The split is by packaging, not platform:linuxdeploy-plugin-gtkforcesGDK_BACKEND=x11into every Tauri AppImage, so the AppImage passes and the.deb/.rpmcorrupts.There is therefore no window-geometry API anywhere on this branch — a grep confirms the only mention is the comment explaining why.
Sending to the agent
toClaudePayloadmaps every newline to\x1b\r— Claude Code's in-band soft-newline, the same sequence its own/terminal-setupinstalls. A multi-line note arrives as one unsubmitted prompt with the cursor left in it, rather than as N submitted lines.Two details that are not obvious:
\r\n|\r|\n, not\r?\n. A lone\rsubmits the prompt.sessionType === "claude". A bash readline would run each line, and would ring the bell at the escape.Ordering: the cache is sequenced, not last-write-wins
The tab and the dock can be open on the same project at once, so they share one zustand
notesByProjectslice rather than each holding auseStatecopy — two private caches let an edit in one pane silently destroy a saved edit in the other.Sharing the cache is not sufficient on its own. Every write claims a per-project monotonic sequence token at issue time, and
commitNotesdrops any result whose token has been superseded. That is what stops a slow panel-mountlist_notesfrom landing on top of a fresher post-save refresh, and it closes thep1 → p2 → p1hole that an identity-based guard leaves open.commitNotesis the only caller ofsetProjectNotesin the entire frontend — one unsequenced write would void the guarantee.list_notessorts pinned-first thenupdated_atdescending, in the backend, so every caller agrees on order without re-sorting.Testing
711 frontend tests across 58 files; 541 Rust tests, 1 ignored, 0 failed.
tsc --noEmitclean.NotesPanel.shared.test.tsxmounts two panels on one project against the real hook — the cross-pane corruption is a regression test, not a code comment.Two things the suite cannot reach
jsdom never synthesizes the follow-up keypress —
TerminalView.tsx's own comment records this — so these stay manual, for the pre-release pass:Known follow-up, not introduced here
An unavailable control that is
disabledis removed from the accessibility tree, so any explanation of why it is unavailable is announced to nobody. This branch's send-to-agent button does that, but so do controls onmain, so it is being fixed app-wide in #49 rather than here — including thearia-disabledclick-guard that the swap requires. Once both land,SendToAgentButtonshould adopt theuseUnavailablehook that #49 introduces.🤖 Generated with Claude Code
https://claude.ai/code/session_011YPqHpjV4EL6RNEwrRKqQm
Two things the plan dropped from the design spec's §1. `keep_corrupt_copy`'s only guard was "does this second's copy already exist", so a persistently unparseable file minted a full copy of the user's prose every time the clock ticked over — and `list_notes` runs on *every* NotesPanel mount, i.e. every project switch, every dock-follows-tab change, every sub-tab toggle. A minute of clicking between two projects was ~60 copies. `MAX_CORRUPT_BACKUPS`, `corrupt_backups_full()` and the three-outcome `Kept` enum come across from `migration_store` whole, including the reason the cap is asked *before* the copy (so it is not implemented by writing a file and deleting it again, and so the surviving copies are the oldest ones) and the reason the log line must not claim a backup that was never written. The file itself is now `{ version, notes }` rather than a bare array. It costs nothing today and gets permanently more expensive once files exist in the field. No released build has written notes, so there is no migration path — but a bare array is still *read*, because declaring a perfectly readable file corrupt is the one outcome this store exists to avoid, and a developer's own notes are prose nothing else has a copy of. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HjL1E2JFNctUqCYotUwqqbImplements the design spec's §2 — "notes cached in zustand keyed by project id" — which the plan substituted with a hook-local `useState`. Sharing the `NotesPanel` *component* between the Project Home sub-tab and the dock did not share the *cache*. Both resolve to the same project, so two panels mount two `useNotes(P)`, each with its own list. Edit a note in the dock and blur; the tab's copy is still pre-edit, and the tab's next blur commits `{...staleRecord, title, body}` — the dock's edit gone from disk with no error and no indicator. That is the feature's own primary workflow: take notes in the dock while the agent runs, which is the reason the dock exists, then go back to the tab. `notesByProject` plus a per-project in-flight flag now hold the list. Both surfaces render from one array; two panels mounting for one project make one read; and because the write is keyed by project, a response that lands after the user has moved on updates the project it belongs to rather than whichever is on screen. This is also the boundary §8 says a detached notes window needs. Three more bugs in the same code, fixed with it: - Delete-after-edit could resurrect the note. Clicking Delete with the textarea focused fires blur first, so `save_note` and `delete_note` go out back to back; Rust's `write_lock` stops them interleaving but does not order them, and a delete that wins the lock is undone by the upsert behind it. A project's mutations now go through one promise chain, module-scoped for the reason `useTerminal`'s input queue is. - An unsaved draft vanished when any other note was saved, because the re-read replaced the list with the backend's. "New note" now persists, so the backend owns the row from the start — chosen over merging local drafts because a local-only row in a *shared* cache would exist in the panel that made it and nowhere else. - The save outcome was reported for the wrong project after a switch: the guard covered only the list replacement, so the new project's SaveIndicator flashed "Saved ✓" for the old project's write. The indicator now resets on a project change and reports only its own. `NotesPanel` also re-seeds its draft when the *stored* text of the note it has selected changes, so an edit made in the other surface reaches the editor and not only the list. It never overwrites something half-typed; that still blurs into a last-writer-wins save, as any blur-commit editor does. NotesPanel.shared.test.tsx is the configuration none of the existing tests had: two panels, one project, the real hook. Four of its six assertions fail against the previous implementation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HjL1E2JFNctUqCYotUwqqbpinnedis 436b6dd470