# Note visual diff and time-machine slider (agy gap 21, roadmap 27) Part of `specs/plugins-parity/`. Extends the existing versions tab (`crates/ui-egui/src/views/versions_view/`) and `history.*` engine commands. One new small pure crate `notescraft-textdiff`. ## Research (2026-10-21; agy claims checked) - Obsidian has core File recovery (snapshots every 5 min by default, kept ~7 days) but NO diff UI in core; the community fills it: Snapshot Diff (word-level side by side over snapshots, File Recovery versions and git commits), Review Edit (inline diff, keep/revert per hunk), Time Machine (timeline merging File Recovery and git, diff preview, restore), Version History Diff (Sync, File Recovery, Git; uses private APIs). https://community.obsidian.md/plugins/snapshot-diff , https://community.obsidian.md/plugins/time-machine , https://community.obsidian.md/plugins/review-edit , https://community.obsidian.md/plugins/obsidian-version-history-diff - Demand: forum request https://forum.obsidian.md/t/version-history-diff-comparison-view/4988 (agy's source; not fetched, the title matches). - agy listed Bear and Apple Notes as having it: NOT verified (searches found nothing on Bear history); Google Docs version history with highlighted changes is the reference UX and is well known. Treat the "who has it" column as Google Docs, Obsidian Sync, Notion (page history), plus the plugins above. ## Problem The versions tab lists snapshots with Compare and Restore; Compare opens a unified LINE diff (`views/diff_view/compare`), which is noisy for prose (one changed word flags a whole paragraph) and needs one click per snapshot. ## Goal In the versions tab, a horizontal slider (time machine) over "oldest snapshot ... now". Dragging selects a snapshot; the pane below shows the WORD-level diff of that snapshot against the next newer one (default) or against now (toggle), additions green, deletions red strikethrough, reading as prose. One button restores the selected snapshot (existing undoable `Restore`). ## Non-goals (v1) Per-hunk revert; git commits and server revisions as slider stops (Feature ideas 1, 2); changing snapshot retention (Q1); diff of non-text files. ## Design decisions ### D1 Pure crate `notescraft-textdiff` (L0, `crates/textdiff`) ```rust pub enum Op { Same, Add, Del } pub struct Span { pub op: Op, pub text: String } pub struct Limits { pub max_bytes: usize /*2 MiB*/, pub max_tokens: usize /*40_000*/, pub max_cells: usize /*16_000_000*/ } pub fn word_diff(old: &str, new: &str, limits: &Limits) -> Result>; // Err(TooLarge) beyond limits, never truncates silently pub fn stats(spans: &[Span]) -> (usize, usize); // added words, removed words ``` Tokens: runs of word chars, whitespace runs, single punctuation. Algorithm: strip common prefix and suffix tokens, then Myers O(ND) with a cell cap; adjacent same-op spans merged. Concatenating Same+Del reproduces `old`, Same+Add reproduces `new` (property test). Own code (about 200 lines) because the `similar` crate is not approved: Q2. ### D2 Engine Add `history.diff` to `crates/engine/src/commands.rs` next to `history.list`: | id | params | result | |---|---|---| | `history.diff` | `{path, id, against?: or "current"}` (default `"current"`) | `{path, id, against, adds, dels, spans:[{op:"same"\|"add"\|"del", text}]}`, spans capped at 5 000 (field `truncated`) | `history.list` stays; snapshot `id` already is the Unix-ms timestamp, so no new field. Add `history.diff` to READ in `ai/assistant_gate.rs` and to `CORE_IDS`. Depends on `notescraft-textdiff` in `crates/engine/Cargo.toml`. ### D3 UI `views/versions_view/slider.rs`: draws a track with one tick per snapshot (max 20 + "Now"), spacing proportional to time, hover tooltip with the local time, keyboard left/right. `views/versions_view/word_diff.rs`: paints spans as a wrapped rich text (green background `tokens.theme.colors` success tone at low alpha; deletions danger + strikethrough). Diff computed on a worker thread per selection change (pattern of `share_dialog/mod.rs`), debounced 120 ms while dragging, result cached by (id, against). The existing rows list stays below as the accessible alternative. Strings: `[ui.versions]` additions appended in the existing versions table file `crates/ui-egui/src/text/versions_view.rs` (use its pattern; new keys at the END of that table). ### D4 Safety Snapshots are local files written by the app: still bounded by `Limits`. A too-large pair shows "Too large to compare, use the list" and keeps Compare (line diff) working. ## Acceptance criteria 1. Slider shows N snapshots + Now with ticks ordered by time; dragging to a tick updates the diff within one frame of the worker result. 2. A one-word change in a 2 000-word paragraph shows exactly one red and one green word, not the paragraph. 3. Concatenation property: Same+Del == old and Same+Add == new for 1 000 random pairs. 4. `word_diff` of two 2 MiB texts returns within 500 ms or `TooLarge`; no panics on empty input, only whitespace, emoji, CRLF. 5. Restore from the slider is ONE undo step; body of other notes untouched. 6. Dragging makes zero engine calls on the UI thread (worker only), and at most one call per 120 ms. ## Open questions - Q1: snapshots keep only 20, at least 5 minutes apart (`vault/snapshots/mod.rs` `KEEP`, `MIN_GAP_MS`), so the slider spans about 1.7 h of active editing at the densest. Raise `KEEP` (setting) or leave? Proposal: setting `files.history_keep` default 50. - Q2: own diff code (proposed) or add the `similar` crate (MIT, widely used)? New dependency needs owner approval. ## Feature ideas (researched, ranked) 1. Git commits as extra slider stops when the vault is a repo: `crates/git` already has `log` and `diff`; Time Machine plugin merges both sources. 2. Server revisions (`share/history.rs`) as stops with another colour. 3. Per-hunk Keep/Revert (Review Edit): turn spans into hunks, apply one hunk to the buffer. 4. Side by side layout reusing `diff_view/split.rs`; 5. "Who changed" author colours from live/collab sessions; 6. Export diff as markdown with `{++add++}{--del--}` CriticMarkup to share.