feat: in-app frontmatter editing — the drawer becomes a guarded write site - #82
Conversation
… site (#79) The frontmatter drawer grows an Edit affordance: the raw YAML in a plain textarea with explicit Save/Cancel, backed by one new façade op — `Vault::write_frontmatter(note_ref, fm_yaml, base_revision)`, `Vault::write`'s frontmatter sibling. Raw-text-first by design: a form round-trip would be a silent rewrite of the human's YAML (W4/W5), so B2 splices the buffer verbatim between the fences and judges nothing. Core (`b2-core`): - `ParsedNote::replace_frontmatter` — the byte-honest splice mirroring `replace_body`: only the bytes between the fences change; every body byte is preserved by construction. The one mechanical touch is a missing final newline (so the closing fence stays on its own line). - `Vault::write_frontmatter` — same shape as `write`: whole-file content-hash revision guard (so body saves and frontmatter saves guard each other with no new concurrency story), splice, model-free re-projection. An unchanged body keeps its chunks and vectors — a frontmatter save never re-embeds. - Two refusals, validated on the re-parsed spliced candidate before any byte reaches disk: a changed/removed/duplicated `b2id` (new `Error::FrontmatterIdentity` — L1, the one line B2 protects) and a top-level `---` line (it would close the block early and leak into the body). Everything else saves — including malformed YAML, which is the human's to fix, exactly as the same edit in an external editor would be. - `ParsedNote::frontmatter_readable` + `NoteView.frontmatter_readable` — the warn-don't-block half: a block that fails to read as YAML metadata surfaces as a non-blocking notice. Computed at read time, so the fs-watch reconcile path raises the identical warning for an external hand-edit — the case #79 is really hardening against (the #75 self-healing stance extended). Adapters (thin, per E3): - desktop: `write_frontmatter` command (model-free, `write_note`'s posture), actionable error mappings; CLI: matching `user_message` arms. - ui: the drawer's Edit → mini-editor with Save (⌘⏎/⌘S) / Cancel (Esc), inline refusal + conflict (Reload / Keep mine) strips, the unreadable-YAML flag on the toggle, and the render carve-out + edit guards so the live buffer is never rebuilt or silently discarded (K1-conforming keyboard paths). Docs: W3's on-command write enumeration, the root CLAUDE.md write list, and the desktop CLAUDE.md model-free-ops list gain the new write site. Tests: 8 new core integration tests (byte-honesty, identity guard, conflict chain, malformed-YAML round-trip, edges-without-reembed, model-free), 5 new note.rs unit tests, 2 new desktop command tests. Whole core + desktop suites green; ui `tsc`/vite build and pure-logic suite green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013nEUZMKhLH5DRUT2z3g2Nr
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughAdds guarded frontmatter persistence with YAML readability tracking, identity and revision validation, desktop IPC wiring, and a frontend mini-editor with save, conflict handling, navigation guards, and unreadable-frontmatter warnings. ChangesFrontmatter editing
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@ui/src/main.ts`:
- Around line 1470-1472: Update the analogous post-embed reread in
autoIndexOnOpen to require both !state.editing and !state.fmEditing before
calling api.readNote, preserving the existing reread behavior when neither
editor is active.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 9e6666bb-322e-4bc9-9575-cefbe841ecbb
📒 Files selected for processing (17)
CLAUDE.mdcrates/b2-cli/src/main.rscrates/b2-core/src/error.rscrates/b2-core/src/note.rscrates/b2-core/src/vault.rscrates/b2-core/tests/write_frontmatter.rscrates/b2-desktop/CLAUDE.mdcrates/b2-desktop/src/commands.rscrates/b2-desktop/src/error.rscrates/b2-desktop/src/main.rsdocs/design/invariants.mdui/src/api.tsui/src/main.tsui/src/render.tsui/src/state.tsui/src/types.tsui/style.css
…ontmatter edit The auto-index pass runs unbidden on vault open, and its post-embed re-read of the open note adopted a fresh revision under whatever editor was live. The body editor was already excluded; the frontmatter mini-editor (#79) was not — so a frontmatter save begun before auto-indexing finished could chain the adopted revision and silently overwrite a concurrent disk change instead of conflicting. Same carve-out as reconcileExternalChange and doReindex. (CodeRabbit, PR #82.) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013nEUZMKhLH5DRUT2z3g2Nr
Closes #79.
The frontmatter drawer grows an Edit affordance: the raw YAML in a plain textarea with explicit Save (⌘⏎/⌘S) / Cancel (Esc), backed by one new façade op —
Vault::write_frontmatter(note_ref, fm_yaml, base_revision),Vault::write's frontmatter sibling. Raw-text-first by design: a form round-trip would be a silent rewrite of the human's YAML (W4/W5), so B2 splices the buffer verbatim between the fences and judges nothing.Core (
b2-core)ParsedNote::replace_frontmatter— the byte-honest splice mirroringreplace_body: only the bytes between the fences change; every body byte is preserved by construction. The one mechanical touch is a missing final newline (so the closing fence stays on its own line).Vault::write_frontmatter— same shape aswrite: whole-file content-hash revision guard (body saves and frontmatter saves guard each other with no new concurrency story), splice, model-free re-projection. An unchanged body keeps its chunks and vectors — a frontmatter save never re-embeds; the notes row and typed edges re-derive from the new block.Two refusals, validated on the re-parsed spliced candidate before any byte reaches disk:
b2id(newError::FrontmatterIdentity) — L1, the one line B2 protects. Re-quoting the same id passes (identity, not bytes);---line (Error::Frontmatter) — it would close the block early and leak the rest into the body, and the body is not this op's to change.Everything else saves — malformed YAML included: it round-trips verbatim and projects best-effort, exactly as the same edit made in vim would.
Warn-don't-block surfacing:
ParsedNote::frontmatter_readable+NoteView.frontmatter_readableflag a block that doesn't read as YAML metadata. Computed at read time, so the fs-watch reconcile path raises the identical warning for an external hand-edit — the case In-app frontmatter editing — a self-healing FM write site #79 is really hardening against (the Invalid YAML frontmatter triggers a b2id re-stamp loop (file grows every reindex; desktop can loop on it) #75 self-healing stance, extended).Adapters (thin, per E3)
write_frontmattercommand (model-free,write_note's posture, outside the embed slot) + actionable error mappings; CLI: matchinguser_messagearms.Docs
W3's on-command write enumeration (
invariants.md), the rootCLAUDE.mdwrite list, and the desktopCLAUDE.mdmodel-free-ops list gain the new write site — deliberate amendments, sanctioned by #79.Tests
8 new core integration tests (
tests/write_frontmatter.rs: byte-honesty, identity guard, fence refusal, conflict chain, malformed-YAML round-trip + heal, edges-without-reembed, model-free posture, mixed body/FM revision chaining), 5 newnote.rsunit tests, 2 new desktop command tests. Full core suite (31 binaries), desktop suite (44 tests), clippy, fmt,tsc+ vite build, and the frontend pure-logic suite all green.Extracted follow-ups (so this closes #79 cleanly)
type:, keepcreated:, namespace neither #80 — new-note template keys: droptype:, keepcreated:, namespace neither (the question from In-app frontmatter editing — a self-healing FM write site #79's comment thread; needs a deliberatedata-model.md§1 amendment).b2idcollisions (and identity restamps) instead of silently shadowing #81 — reindex-time diagnostic for cross-noteb2idcollisions and identity restamps (the one In-app frontmatter editing — a self-healing FM write site #79 pathology this PR doesn't cover: it needs index-wide knowledge, and the in-app path is now closed by the guard).🤖 Generated with Claude Code
https://claude.ai/code/session_013nEUZMKhLH5DRUT2z3g2Nr
Generated by Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
b2id.---handling) and revision chaining behavior.Tests