Skip to content

v1.6: diff-on-reload (track-changes review surface)#7

Merged
nonatofabio merged 5 commits intomainfrom
feat/diff-on-reload
Apr 29, 2026
Merged

v1.6: diff-on-reload (track-changes review surface)#7
nonatofabio merged 5 commits intomainfrom
feat/diff-on-reload

Conversation

@nonatofabio
Copy link
Copy Markdown
Owner

Summary

v1.6 lands the headline UX leap from the v2 roadmap: when an external edit hits the active file (vim, an agent, anything), Mindle renders the change as a Word-style track-changes overlay you can accept or reject — per chunk, or whole-document.

This is the foundation the v2.0 MCP loop sits on. Today it's already useful on its own — anyone editing the file in vim, Cursor, or Claude Code outside Mindle gets visible change tracking.

What changed

Snapshot model (Swift)DocumentStore now carries a per-tab lastSyncedText alongside rawText. When the file watcher fires after an external edit, rawText updates but lastSyncedText holds the user's last reviewed version, so the JS layer can compute and render the diff. The sidecar persists lastSyncedText only when there's an in-flight review (so v1.5 sidecars round-trip clean), letting an unfinished review survive close and reopen.

Diff render (JS + CSS)mindleLoad takes an optional third argument: the baseline. When it differs from the current text, the pipeline:

  1. Walks Diff.diffLines(baseline, current) (jsdiff vendored at Resources/web/vendor/diff.min.js) and groups consecutive +/- segments into chunks with start/end positions in both texts.
  2. Builds a markdown source that interleaves unchanged regions of the new text with raw-HTML wrappers around each chunk.
  3. Pre-renders each chunk's before/after through markdown-it (so lists, headings, code fences inside a chunk look right) and surfaces them as styled blocks with ✓ Keep / ✗ Revert chips.

The print pipeline strips the chunk chrome, so PDF export reflects accepted text only.

Round-trip (Swift ↔ JS) — clicking ✓ Keep posts a new baseline back to Swift via diffSetLastSynced; clicking ✗ Revert posts a new current text via diffSetCurrent, which Swift writes through to disk. Each click triggers a re-render of the now-shrunk diff. No partial state in JS — Swift owns all text.

Whole-doc shortcuts — Edit menu gains Keep All Changes (⌘⌥⏎) and Revert All Changes (⌘⌥⌫). Disabled when there's no in-flight diff. Useful when the agent's revision is bulk-correct or bulk-wrong.

Test plan

  • Open a .md file, edit it externally (echo "..." >> file.md or save in vim) — diff chunks render with green/red wash and chips.
  • Click ✓ Keep on a single chunk — that chunk's content joins the baseline; remaining chunks still rendered.
  • Click ✗ Revert on a single chunk — file on disk reverts that section to the baseline; the diff shrinks correspondingly.
  • Click ⌘⌥⏎ (Keep All) — baseline catches up to current; document renders normally without chunk chrome.
  • Click ⌘⌥⌫ (Revert All) — file on disk reverts to baseline.
  • Close the window mid-review, reopen — sidecar restores the in-flight diff (chunks still show).
  • Tab switching between two files mid-review — each tab keeps its own diff state.
  • PDF export with diffs in flight — PDF contains only accepted (current) text, no chrome.

Out of scope (v1.6.x or later)

  • Word-level highlights inside chunks (currently paragraph-block granularity only).
  • Diff statistics / chunk count badge in the UI.
  • Keyboard shortcuts to navigate between chunks.
  • Inline annotations on chunks (reusing the highlight system to mark chunks for the agent).

Part of v2.0 roadmap.

Foundation for v1.6 diff-on-reload. DocumentStore now carries a per-tab
lastSyncedText alongside rawText: when external edits land via the file
watcher, rawText updates but lastSyncedText holds the user's last
reviewed version, so a diff can be computed.

New surface:
  hasInFlightDiff       — true when lastSyncedText != rawText
  acceptAllChanges()    — promotes rawText to baseline, no file change
  rejectAllChanges()    — writes baseline back to disk, the in-place
                          watcher rewrite is a no-op (rawText already
                          matches), so we don't loop
  setLastSyncedText(_:) — JS-side per-chunk accept; new baseline
  setRawText(_:)        — JS-side per-chunk reject; writes through

Sidecar gains an optional lastSyncedText (omitted when there's no
in-flight diff, so v1.5 sidecars round-trip cleanly). Closing a tab
or app mid-review restores the review state on reopen.
Loaded after the existing markdown-it / highlight.js / katex / mermaid
chain in reader.html. Exposes window.Diff with diffLines / diffWords,
which the diff render layer uses to walk lastSyncedText vs rawText.
mindleLoad now takes an optional third argument — the diff baseline.
When the baseline differs from the current text, the JS pipeline:

  1. Walks Diff.diffLines(baseline, current) and groups consecutive
     +/- segments into chunks (each with start/end positions in both
     baseline and current).
  2. Builds a markdown source that interleaves unchanged regions of
     'current' with raw-HTML chunk wrappers around each change.
  3. Pre-renders each chunk's before/after through markdown-it (so
     lists, headings, code fences inside a chunk look right) and
     wraps them in styled <div> blocks with ✓ Keep / ✗ Revert chips.
  4. Attaches click handlers that compute a new baseline (accept) or
     a new current text (reject) and post to Swift via the
     diffSetLastSynced / diffSetCurrent message channels.

CSS adds the chunk chrome — accent-bordered card, soft red strikethrough
for the deleted block, soft green wash for the added block, small
serif-friendly buttons. The print pipeline (PDF export) hides the
deleted blocks and chrome entirely so exported docs reflect the
accepted text only.
WebReaderView:
- Watches lastSyncedText alongside rawText so a baseline-only change
  triggers a re-render. Live-reload preservation still applies.
- Passes the baseline as mindleLoad's third arg (or null when there
  is no in-flight diff, so the JS hot path stays cheap).
- Adds diffSetLastSynced and diffSetCurrent message handlers; per-chunk
  accept/reject in JS lands in DocumentStore.setLastSyncedText /
  setRawText.

MindleCommands:
- Edit menu gains 'Keep All Changes' (⌘⌥⏎) and 'Revert All Changes'
  (⌘⌥⌫). Both disabled when there's no in-flight diff. Useful when
  the agent's revision is bulk-correct or bulk-wrong and the user
  doesn't want to chip through every chunk.
After a Keep / Revert, snap the next-render scroll position to where
the chunk used to live so the user gets visible confirmation that the
action landed. Without this, removing the chunk's chrome collapses
the doc and preserveScroll lands you in the wrong place — the change
landed but felt invisible.

Also removes the v1.6 instrumentation that helped diagnose the issue
(NSLog calls on accept/reject paths, a JS dlog channel + the matching
WKScriptMessageHandler). Pure cleanup — no behavior change beyond
the scroll.
@nonatofabio nonatofabio merged commit 5579c1a into main Apr 29, 2026
1 check passed
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