Skip to content

feat: local version history (Movement I.3) - #22

Merged
evangress merged 8 commits into
mainfrom
feat/local-version-history
Jul 24, 2026
Merged

feat: local version history (Movement I.3)#22
evangress merged 8 commits into
mainfrom
feat/local-version-history

Conversation

@evangress

Copy link
Copy Markdown
Collaborator

Local version history (ROADMAP Movement I.3)

Every save now records a restorable version of the note, browsable in a new history panel with a line diff against the current text and one-click restore. This is the third branch of Movement I (the data-safety floor) — it exists to make the user's writing more recoverable, never less (CLAUDE.md §3).

Design spec: docs/superpowers/specs/2026-07-08-local-version-history-design.md.

Decisions (locked during brainstorming)

  • Location: app config dir, keyed by sha256(path)outside the vault (like recovery/session), so it never pollutes the plain-files vault or rides folder-sync.
  • Trigger: on every atomic save, content-deduped. Hooked into the Rust save_file/save_file_as commands, so "every save = a version" is enforced by the call graph, not frontend discipline.
  • Retention: time-decay thinning (keep-all <24h → hourly <7d → daily <30d → weekly beyond; always keep oldest + newest).
  • Store: hand-rolled content-addressed store, not gix (resolves ROADMAP §8) — sha2 + flate2/miniz_oxide + serde, all pure-Rust/C-free (§2). ~200 LOC.

Data-safety properties (§3)

  • Additive: snapshot failure never blocks or fails a save (best-effort, logged).
  • Undoable restore: the current on-disk state is snapshotted before the overwrite, which is atomic — restore can only add recoverability.
  • Crash-safe rename: rekey copies history to the new key before deleting the old (≥1 intact dir on power loss). The in-app rename that calls it is Movement II.12.

What's here

  • crates/snapshots — the store (snapshot/list/read/rekey + pure prune). 11 tests.
  • commands/snapshots.rslist_history / read_snapshot / restore_snapshot, rooted at <app-config>/history; save-path hook.
  • src/ui/history.ts + src/ui/linediff.ts — DI panel + pure line diff; toggle (menu/button, persisted, hidden by default); undoable restore flow in main.ts.
  • Docs: CLAUDE.md §5/§4, CHANGELOG, ROADMAP tick.

Gates (all green)

  • cargo test -p snapshots — 11/11 (round-trip byte-exact, dedup, thinning + orphan GC, resilience, rekey merge).
  • tests/history.test.ts — line diff + label helpers + panel render/restore in jsdom.
  • All logic crates pass; cargo fmt --all --check clean; clippy clean; pnpm typecheck + pnpm build clean; 129 frontend tests pass.

Not verified here (needs on-device)

The app crate can't link on this webview-free box (§0), so the GUI integration — menu toggle, click-to-diff, restore-and-reload, watcher suppression — is unverified. Logic underneath is fully gated. Verify with pnpm tauri dev on a webview-capable machine.

🤖 Generated with Claude Code

evangress and others added 8 commits July 8, 2026 01:53
The Toril-TUI MVP is parked by owner decision; capture its deferred
next steps (on-device verify, delete->trash, in-doc find, multi-file
tabs, RTF/HTML export) as a marked parallel track so they are not lost.
Records the byte-exact-source-edit invariant to preserve in follow-ups.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Content-addressed, app-config-dir snapshot store (hand-rolled CAS:
sha2 + flate2/miniz_oxide + serde); on-save deduped capture hooked in
the Rust save commands; time-decay thinning; crash-safe rekey
(copy-then-delete) so renames leave >=1 intact history dir; undoable
restore via the atomic path. Resolves ROADMAP §8's gix-vs-hand-rolled
decision toward hand-rolled.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New crates/snapshots (ROADMAP Movement I.3): hand-rolled CAS keyed by
sha256(path), gzip-compressed sha256(content) blobs + a JSON manifest.
snapshot() dedups against the latest version and thins via prune(); a
pure time-decay policy (keep-all <24h, hourly <7d, daily <30d, weekly
beyond, always keeping oldest+newest) with orphaned-blob GC. rekey()
carries history across renames copy-then-delete (>=1 intact dir on
power loss). Pure std+serde+sha2+flate2, no Tauri; 11 tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add commands/snapshots.rs (list_history, read_snapshot, restore_snapshot)
over the snapshots crate, rooted at <app-config>/history (outside the
vault, like the recovery journal). save_file/save_file_as now take a
snapshot after a successful atomic write — best-effort and additive, so
history can never block or fail a save (§3). restore_snapshot captures
the current on-disk content before overwriting, making restore undoable.

App-crate wiring is unverified on this webview-free box (§0); needs
on-device verification. Register the three commands in lib.rs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add src/ui/linediff.ts (pure LCS line diff) and src/ui/history.ts (a
dependency-injected panel: version list, read-only diff vs current,
restore). ipc.ts gains SnapshotMeta + list/read/restore wrappers and a
history_visible setting (mirrored in settings.rs). main.ts instantiates
and toggles the panel (menu/button, persisted, default hidden), refreshes
it on activate/save, and owns the save-if-dirty -> restore -> reload flow
with recordSelfWrite so the watcher ignores our own write. app.html +
styles.css add the fourth workspace column; menu.rs the toggle item.

Gate: tests/history.test.ts (line diff + label helpers + panel render/
restore in jsdom). GUI toggle/restore integration is on-device (§0).
129 frontend tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add the three history commands to the §5 contract + note save_file's
snapshot side-effect and the crates/snapshots store; CHANGELOG entry;
tick ROADMAP Movement I.3 done, advance the pickup pointer to branch 4
(feat/sync-coexistence), and resolve §8's gix-vs-hand-rolled decision.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
clippy::redundant_closure, surfaced now that the app crate can be
linted + linted on-device (webview deps installed). Pre-existing from
the Phase 4 menu work; makes cargo clippy -p toril-app first-party clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On-device testing surfaced three UX issues, all fixed:
- Versions saved seconds apart all read '2 min ago' — unorderable. Show
  absolute clock time (to the second) as the label; relative time moves
  to a hover tooltip (formatTimestamp; +tests).
- Clicking between versions STACKED diffs below the fold (renderDiff
  appended without cleanup). Now only one diff shows at a time; picking
  another replaces it, the selected row highlights, and re-clicking
  closes it (+tests asserting no stacking / toggle-close).
- The panel had no title, so it blurred into the adjacent outline panel
  (which shows the note's H1). Added a 'Version history' header.

133 frontend tests green; verified live via vite HMR on-device.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@evangress
evangress merged commit c826608 into main Jul 24, 2026
5 checks passed
@evangress
evangress deleted the feat/local-version-history branch July 24, 2026 22:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant