Plan the project Notes implementation

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
This commit is contained in:
2026-09-01 12:00:52 -07:00
co-authored by Claude Opus 5
parent e58e2cdaf7
commit 221e7566c3
2 changed files with 2391 additions and 7 deletions
File diff suppressed because it is too large Load Diff
@@ -57,9 +57,19 @@ on `projects_store.rs`:
- **`sanitize()` on the project id**, copied from `migration_store.rs:41-46`. The id arrives - **`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. 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 - **Atomic *and durable* write** — `.tmp`, `sync_all()`, `rename()`, then fsync the
Conventions rule in CLAUDE.md. directory, per `migration_store.rs:203-261` rather than `projects_store.rs:167-179`. That
- **Corrupt file is quarantined to `.bak`, not discarded** (`projects_store.rs:24-61`). file's comment is explicit that write-temp-then-rename alone is only half of it: `fs::write`
returns once the bytes are in the page cache, so losing power in the window leaves the
rename applied and the data not written — a truncated file produced by the very code meant
to prevent one. Notes are user prose; that is the data least worth losing to a half-write.
- **Corrupt file is copied aside and left in place**, per `migration_store.rs:49-125`
timestamped, capped, and never overwriting an earlier copy, because the first copy is the
one taken before anything rewrote the file.
- **Path resolution is split for testability.** `dirs::data_dir()` is resolved in thin public
wrappers; the real work takes an explicit `&Path`. `ProjectsStore::new()` hardcodes
`dirs::data_dir()` and is therefore not constructible against a temp dir, which is why its
own tests only exercise free functions. The notes store should not inherit that limit.
```rust ```rust
struct Note { struct Note {
@@ -102,8 +112,15 @@ Registered in `lib.rs` via `generate_handler!`. Per CLAUDE.md, application comma
- `delete_note(projectId, noteId)` - `delete_note(projectId, noteId)`
There is deliberately **no whole-list setter**. Bulk writes are the clobbering mechanism the 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 storage choice above exists to avoid.
is both correct and cheap.
`notes_store` is a **free-function module** keyed by project id, exactly like
`migration_store` — no struct, nothing held in `AppState`, no in-memory copy of the notes.
`ProjectsStore`'s `Mutex` exists because it caches the project list in memory; a notes store
that reads and writes the file per call has nothing to cache and nothing to guard. What it
does need is that each upsert's read-modify-write is not interleaved with another's, so the
module holds one process-wide write lock (`OnceLock<Mutex<()>>`, the idiom already in
`browser_view/popout.rs`) taken for the read-modify-write, not for the read path.
Frontend: wrappers in `lib/tauri-commands.ts`, a `hooks/useNotes.ts`, and notes cached in 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. zustand keyed by project id. Rust is the source of truth; the cache is a cache.
@@ -114,8 +131,11 @@ drawn so that a future detached window (§8) only swaps the transport: Rust emit
## 3. Editor — plain text, deliberately ## 3. Editor — plain text, deliberately
A note is a title and a `<textarea>`, with save-on-blur plus a debounce, following A note is a title and a `<textarea>`, saved on blur, following
`ClaudeInstructionsEditor.tsx`. **No rich editor, no markdown library, and no markdown `ClaudeInstructionsEditor.tsx` (which saves in `onBlur` and holds no timer) and reporting
the outcome through `ui/SaveIndicator`. There is no debounce anywhere in the existing save
path — `useSaveState.ts`'s only timer is a 2500 ms reset of the "Saved ✓" label — and notes
add none. **No rich editor, no markdown library, and no markdown
rendering** — it is a scratchpad for reminders, and it stays one. rendering** — it is a scratchpad for reminders, and it stays one.
The body is stored and displayed exactly as typed. There is no view/edit mode split, so The body is stored and displayed exactly as typed. There is no view/edit mode split, so
@@ -187,6 +207,12 @@ Two consequences follow from that same comment:
| 1 | Send | | 1 | Send |
| >1 | Menu of session display names (`Project.renamed_session_names` where set) | | >1 | Menu of session display names (`Project.renamed_session_names` where set) |
The display-name rule is currently written **twice**, both copies non-exported and local to
`MainTabs.tsx``tabLabel` (:192-203) and inline in `renderTab` (:362-367). The picker would
be a third copy of a rule that already disagrees with itself the moment one copy is edited,
so it is extracted once to a shared helper and both existing sites call it. That is a
targeted improvement to code this feature depends on, not unrelated refactoring.
- **The target is pinned at click time**, per the hazard `useSTT.ts:20,30` guards against - **The target is pinned at click time**, per the hazard `useSTT.ts:20,30` guards against
(it pins at record-start so text does not land in whatever tab is active at stop time). (it pins at record-start so text does not land in whatever tab is active at stop time).
- Transport is `useTerminal`'s module-scoped ordered queue (`hooks/useTerminal.ts:32-85`, - Transport is `useTerminal`'s module-scoped ordered queue (`hooks/useTerminal.ts:32-85`,