feat: local version history (Movement I.3) - #22
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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)
sha256(path)— outside the vault (like recovery/session), so it never pollutes the plain-files vault or rides folder-sync.save_file/save_file_ascommands, so "every save = a version" is enforced by the call graph, not frontend discipline.gix(resolves ROADMAP §8) — sha2 + flate2/miniz_oxide + serde, all pure-Rust/C-free (§2). ~200 LOC.Data-safety properties (§3)
rekeycopies 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 + pureprune). 11 tests.commands/snapshots.rs—list_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 inmain.ts.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.cargo fmt --all --checkclean; clippy clean;pnpm typecheck+pnpm buildclean; 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 devon a webview-capable machine.🤖 Generated with Claude Code