Install
Inside DeepSeek Harness, with dsh-market
dsh plugin --profile web add dshmarket
Or from the command line
dsh plugin --profile web add dsh-checkpoint-diff
Installing runs third-party code with your own permissions — it can read your files, use your credentials and reach the network. Review the source first, and pin a commit (github:owner/repo#sha) when you can.
README
File-diff visualization between checkpoint time nodes for DeepSeek Harness.
Every time a mutating tool runs,
dsh-checkpoint-rewind
captures a snapshot of your workspace — a checkpoint time node. This plugin
turns those nodes into a browsable timeline and shows you exactly what changed
between any two of them: a GUI panel with a per-file line diff, a /diff
command for headless use, and a small JSON API.
Read-only by design, with one explicit exception: rollback. You can restore
workspace files from any time node — the whole node or a single file — through
the panel, /rollback, or the API. Rollback is overwrite only: it never
deletes files created after the node, never touches .git/.dsh, and never
writes to snapshot storage, git, or sessions. The single exception to "never
deletes" is rollback's single-shot undo (/rollback --undo, panel button):
it may remove a file only when the restore itself created it — and nothing
else (in-process, lost on restart).
中文版(默认):README.md
Why it exists: the idea
The situation. AI agents write code and modify files for you. Their thinking is a black box, but their actions land in your workspace — and with that come the questions: what did it change? When? And what if it broke something? These questions must never be unanswerable.
The stance. Tools like this usually go one of two ways: save you the effort — summarize, judge, filter on your behalf; or let you rest easy — hand you the raw truth and guarantee you can always inspect it, trace it, and roll it back. This plugin is unambiguously the second kind: no "read it for you" summaries or filtering — it hands you the full power to see clearly and step back.
- You don't have to look — but you must be able to. Ignore it day to day; but whenever you want, every mutating action maps to a fine-grained snapshot time node, and any two nodes can be diffed file by file, line by line.
- When something goes wrong, you can trace it to the bottom. Which step, which tool, which files — precisely reconstructed; and if it was a mistake, restore the workspace from any time node (overwrite-only, never deletes, undoable).
- Peace of mind comes not from infallibility, but from traceability and reversibility.
One line: you may not look, but it must never be un-inspectable. People may choose not to look and not to investigate; what they must never lack is the means to do both. That is why this plugin exists.
Demo
GUI panel
Real captures from a live harness session. Panel overview — scope switch,
from/to node selects (intent labels, (HEAD) prefix), tree file list
(A/M/D), per-line diff (red/green + ↑/↓ change-block jumping), and the
Restore workspace card:

Trace (session log) scope — session-log replay with the three-lane drag-select timeline (Input / Model / Tools spans, turn boundaries; a drag range maps to the nearest tool-call boundaries in from/to):

This session scope — the current session's checkpoint timeline.Select the version to roll back and click Preview Restore to preview the changes:

This project scope — cross-session merge with the fork-lineage branch dropdown:

Re-capture and update the README after any significant UI change — capture rules in docs/screenshots/README.md.
Command line (verbatim output format)
/diff lists this session's timeline — rewind leaves a snapshot time node
before every mutating tool run (format is the real output, data is
illustrative):
diff: 3 checkpoint(s) for this session
#a1b2c3d4 20m ago turn 2 step 1 copy 3 file(s) 18 KiB edit lib/engine.mjs
#b2c3d4e5 10m ago turn 3 step 1 copy 2 file(s) 12 KiB edit README.md
#c3d4e5f6 5m ago turn 3 step 2 copy 4 file(s) 25 KiB bash pnpm test
usage: /diff <from> <to> (id prefix or "latest" for either side)
/diff <from> <to> between any two nodes — file summary + line diff (the GUI
panel shows the same red/green diff side by side):
diff: #b2c3d4e5 (14:02 · edit README.md) → #c3d4e5f6 (14:07 · bash pnpm test)
4 file(s) changed: +1 added, 2 modified, 1 deleted
M lib/engine.mjs
- function lcs(a, b) {
+ function lcs(a, b, opts) {
M README.md
- Read-only by design, one exception: rollback
+ Read-only by design, one explicit exception: rollback
A test/engine.test.mjs
D lib/legacy.mjs
At a glance
- What it is — a DeepSeek Harness plugin that turns
dsh-checkpoint-rewind snapshots into a browsable timeline with per-file,
line-level diffs between any two time nodes (GUI panel /
/diff/ HTTP API), plus preview-first rollback (overwrite-only, never deletes, undoable). - What it solves — after an AI agent mutates your workspace: what changed, when, and how to trace and recover when things go wrong.
- Who it's for — developers using DeepSeek Harness with dsh-checkpoint-rewind installed.
- Compatibility — DSH 0.1.0-rc.5 / rc.6 · Node ^22.19 || >=24 · Apache-2.0.
- Entry points — Diff button in the session header ·
/diff·/rollback·/checkpoint-diff/api.
Features
- Timeline — every checkpoint as a selectable node (
#short-id, time, turn/step, provider, trigger tool;/rewindguard checkpoints are marked). - Cross-session / same-project timelines — switch to This project to
merge checkpoints from every session sharing the workspace key, organized
by
/rewindfork lineage: branch dropdown (root + side branches, session titles), per-session tags, and fork / root-missing markers. Falls back to a flat merge when the optionaldsh-session-queryservice is absent. - Intent labels — each node is named from the session log: the
tool/callevents of its(turn, step)are looked up to produce labels likeedit README.mdorbash pnpm test(in the GUI,/diffoutput and the JSON API), falling back to the raw trigger tool when the log is unavailable. Cold sessions are resolved read-only throughsessionQuery.readSession. - File summary — changed files between two nodes with
A(added) /M(modified) /D(deleted) badges. - Tree file view — the changed-file list renders as a collapsible directory tree with per-directory A/M/D counts; click a file for its line diff.
- Line diff — a self-contained LCS engine (no git required for
copysnapshots) rendersctx/del/addlines with aligned line numbers. - Trace replay (works without rewind) — switch the panel scope to
Trace (session log) or run
/diff --trace: everytool/callboundary in the session log (session.jsonl.zstd) becomes a time node (trace:<seq>), and selecting any two nodes diffs that interval by replayingwrite/edit/str_replace_editorarguments (full file contents are in the tool parameters). No snapshot producer is needed — past sessions work out of the box, and rewind absence, quota eviction orgit gcno longer silence the "what changed" question. Shell-command edits are invisible to the trace; any replay divergence is reported innotes(never silent). Data comes fromsessionQuery.readSessionfirst, with a direct zstd read (Node ≥ 23.5) as the last-resort fallback. - Two snapshot providers —
git(unreferenced stash/commit-tree objects, read viagit diff-tree/git show) andcopy(snapshot directories + manifests), dispatched per record; mixed-provider pairs are rejected loudly. - Rollback — restore the workspace (or a single file) from any time node:
preview first, then apply. Overwrite only — files created after the node are
reported and left in place, never deleted. Works in session and project
scope (cross-session nodes address the same workspace). Paths are validated
(no traversal, no absolute paths,
.git/.dshnever touched); the git provider only uses read-only git primitives and refuses to run unless the session cwd is the repository root. - Restore preview diff — while previewing a restore, click any file in the plan to see the current workspace → target snapshot line diff right in the diff pane ("current → #target"), so you can see exactly what the restore would change before applying it.
- Single-shot undo — after applying a restore, one
↩ Undo this restoreaction (panel button,/rollback --undo,POST /api/rollback-undo) reverts it: overwritten files get their pre-restore content back, and files the restore created are removed (the one "never deletes" exception). In-process only (lost on restart), no redo; files changed since the restore are skipped untouched. - Diff view niceties — ↑/↓ buttons jump between change blocks of the current diff (a block is a maximal run of changed lines — a modification's red and green regions count as one block, a run of added lines as one); each jump centers the block's middle line in the viewport; at the last block ↓ shows a "Last change block (click again to wrap)" toast and only wraps on the next press (↑ symmetric at the first block); opening a file auto-scrolls to its first change block; a small "Last view" line under the diff pane jumps back to the previously viewed node pair (persisted in localStorage).
- Position labels — the newest node is tagged
(HEAD)(uppercase, prefixed to the id:(HEAD) #bbbbbbbb 20:26 · edit b.txt) in the from/to/restore-target selects, in the summary range line and in the restore-preview tag. HEAD always means the current snapshot — the global newest node — so it never moves when you pick other versions or filter branches; refreshing the timeline moves it to a newer snapshot. - Separate restore section — rollback lives in its own collapsible
"Restore workspace" card below the toolbar, clearly separated from the
node-diff area; its plan/result/undo status renders inside the card, and
the restore preview (tagged "Restore preview: current workspace → …")
is independent of the from/to diff: picking other versions never
clears it, and changing the restore target reloads it against the new
target. Preview-plan file rows are obvious links (
🔍 file). - Graceful degradation — git checkpoints whose snapshot object was
reclaimed by
git gc(or lost on re-clone) are marked⚠ degradedin the selects and excluded from the default selection; the timeline shows a "N checkpoint(s) degraded" banner, and any diff/rollback error names the exact dead node (checkpoint #9312717a (to side) is missing from this repository (bad object …)). No data is ever deleted; pick newer nodes or run rewind's/rewind clearto reset the timeline. Pruned checkpoints, missing records and missing files keep their existing clear errors.
Alternatives compared
Through the lens of "an AI agent just modified my workspace", against the common options (facts per each project's official docs):
| Tool | Snapshot granularity | Diff between any two nodes | Restore / rollback | Session awareness |
|---|---|---|---|---|
| dsh-checkpoint-diff (this plugin) | before every mutating tool run | ✅ yes, per file and per line (GUI / /diff / API; intent labels, tree view, change-block jumping) |
preview-first → apply; never deletes; single-shot undo; whole node or single file | session + cross-session project scope (fork lineage, branches) |
| dsh-checkpoint-rewind (upstream producer) | before every mutating tool run (0.5.0+: + per-turn / scheduled / manual) | ✅ settings-page pairwise compare (0.5.0+: file change-set + config line diff; no per-file workspace line diff) | /rewind preview → restore → fork (guard checkpoint; 0.5.0+: seed-replay session rewind) |
single session |
| dsh-turn-rewind | per user message (coarser) | ❌ no (changeLedger service, no visual diff) | conversation + workspace rewind; web restore dialog | single session |
| dsh-snapshot | before each write/edit/delete (target file content) | ❌ no line-level diff | roll back a whole "conversation" (re-rollbackable) | single session (project-level quota) |
Claude Code native checkpointing + /rewind |
per conversation turn (automatic) | no node-to-node diff view in the docs (see official docs) | /rewind restores files (git-tracked) |
inside Claude Code sessions |
git baseline (git diff/restore, GitLens etc.) |
per manual commit | ✅ any two commits, line-level (mature ecosystem) | git restore/checkout; no preview planning, no undo guard |
none (bare repo) |
| IDE local history (JetBrains Local History / VS Code Timeline) | per edit/save (per file) | single-file revision level | restore any single-file revision | none |
| OS-level snapshots (Time Machine / Windows File History) | hourly/daily | ❌ no line-level diff | whole file/directory restore | none |
Note: the DSH ecosystem also has timeline-style plugins such as
dsh-message-timeline, dsh-session-timeline, dsh-chat-timeline and
dsh-undo-savepoint (DSH-config domain); this table focuses on the
diff-visualization + rollback dimension.
Ecological position (verified 2026-08-17): upstream rewind 0.5.0 now ships
its own settings-page timeline with pairwise compare (file change-set + config
diff; source-verified: no per-file workspace line diff). This plugin
does not compete with the producer over "having a diff"; it stays a read-only
consumer + analysis layer — intent labels, cross-session fork lineage, non-git
workspaces (copy), preview-first rollback with single-shot undo, /diff and
the HTTP API — and aims to degrade gracefully even when rewind is absent or
snapshots are gone. Ecosystem assessment, compatibility facts (rewind 0.5.0
domain v2) and the differentiation roadmap:
docs/competitive-analysis.md.
Install
dsh plugin --profile web add dsh-checkpoint-rewind # checkpoint producer (first)
dsh plugin --profile web add dsh-checkpoint-diff # this plugin
Both are bundle plugins: dsh plugin add appends them to the profile's
dsh.profile.bundles, and each package's cordis.patch.yml inserts its row.
Restart the harness for host-side changes; the browser bundle is served
with cache-control: no-cache, so a page refresh picks up client fixes.
The bundle layers must mount rewind before diff (the diff plugin reuses rewind's
checkpointsstorage domain; the bundle order indsh.profile.bundlesdecides this — verify it afterremove/addcycles, which re-sort dependencies).
Usage
GUI
A Diff button appears in the session header action row. It opens a panel:
- the toolbar has a scope switch — This session (current session only,
default) or This project (every session sharing the workspace, with a
branch dropdown when fork lineage is available: root branch + side
branches, titles from
readTitle); - pick two time nodes with the from / to selects (defaults: first →
latest); each option shows the derived intent label when one is available
(e.g.
#a1b2c3d4 14:02 · edit README.md), prefixed with the owning session's short id in project scope; - the left pane lists changed files as a collapsible directory tree — directory rows show their A/M/D counts and toggle their subtree; file rows keep the A/M/D badge;
- click a file for its line diff (red = deleted, green = added); the diff head carries ↑/↓ buttons that jump between the change blocks (block center aligned; at the last block ↓ first shows a toast and only wraps on the next press — ↑ symmetric), and the view opens at the first change block of the file.
Rollback. A collapsible Restore workspace card sits below the toolbar,
separate from the node-diff area: pick the node you want to restore to
(defaults to the latest node; every file row also carries a ↩ button to
restore just that file), press Preview restore to see the plan
(would-restore / unchanged / skipped files, plus files created after the node
that will be left in place), then Apply restore. The panel refreshes the
timeline afterwards.
While a restore plan is showing, click any would-restore file (shown as an
obvious 🔍 file link) to see the current workspace → target snapshot
diff in the right pane, tagged "Restore preview: …" — the toolbar from/to
selects switch to a read-only "current workspace → target" pair so the
comparison direction is always obvious (click a file in the tree to exit
back to the normal from/to diff). The preview belongs to the restore section:
picking other from/to versions never clears it, and changing the restore
target reloads it against the new target. After an apply, the status area
lists what was restored without offering stale preview rows; an ↩ Undo this
restore button reverts the restore in one shot (overwritten files get their
previous content back; files the restore created are removed; anything
changed since is left alone). The diff pane also remembers your last viewed
node pair: a small Last view line below it jumps back to it (even across
session/project scope).
Fork junctions crossed by the selected range are shown as ⤷ fork markers;
an incomplete lineage renders an "earlier history unavailable" hint.
Click anywhere outside the panel to close it.
Command
/diff list this session's checkpoint timeline
/diff <from> <to> file summary + per-file line diffs between two nodes
/diff --project list the project timeline (all sessions, branch header)
/diff --project <from> <to> diff between two nodes across sessions
/rollback [--project] [--dry-run] <node> [<path>...]
restore workspace files from a time node
/rollback --undo revert the most recent restore (single-shot)
<from> / <to> accept an id prefix or latest, e.g. /diff a1b2c3d4 latest.
In project scope, ambiguous prefixes prefer the current session's records.
/rollback restores every file of the node, or only the given paths; latest
is a valid node. --dry-run prints the plan without writing anything
(would restore N file(s), M unchanged, K skipped + the files that will be
left in place); --project addresses nodes from any session sharing this
workspace. Rollback never deletes files — contents are overwritten in place.
/rollback --undo reverts the most recent restore (in this process): files
return to their pre-restore content, and files the restore created are
removed; files changed since the restore are skipped (all skipped → error).
HTTP API
Served by the harness webServer at the /checkpoint-diff/api prefix
(same-origin JSON). Read endpoints are GET-only and accept an optional
scope=project parameter to address records across sessions; rollback and
rollback-undo are POST endpoints (the only endpoints that write, and they
only write into the session workspace):
| Endpoint | Method | Params / Body | Returns |
|---|---|---|---|
/api/timeline |
GET | session, scope? |
{ok, records[]} — newest-first timeline views; scope=project adds branches[] and markers[] |
/api/summary |
GET | session, from, to, scope? |
{ok, from, to, files[{path,status}], totalFiles, truncated} |
/api/file-diff |
GET | session, from, to, path, scope? |
{ok, ops[{type:'ctx'|'del'|'add', text, a?, b?}], truncated, binary} |
/api/preview-diff |
GET | session, target, path, scope? |
{ok, path, ops[], truncated, binary, present} — current workspace → target snapshot diff (read-only) |
/api/rollback |
POST | JSON {session, target, scope?, paths?, dryRun?} |
{ok, dryRun, scope, target, files[{rel,action,reason?,mode?}], restored, unchanged, skipped, leftovers, notes} |
/api/rollback-undo |
POST | JSON {session} |
{ok, target, time, restored, removed, skipped[]} — reverts the most recent restore |
For /api/rollback: target is an id prefix or latest; paths limits the
restore to those files (omit = whole node); dryRun: true plans without
writing (restored then means would restore). files[].action is
restore | unchanged | skip; leftovers lists files created after the
node that were left in place (never deleted).
For /api/preview-diff: the diff direction is current → snapshot (del =
lines the restore would remove, add = lines it would bring back); present
is both or workspace-missing (file does not exist in the workspace yet).
For /api/rollback-undo: restored = files rewritten to their pre-restore
content, removed = files the restore created that were deleted (the one
"never deletes" exception), skipped = files left alone because they changed
since the restore (all skipped → 409). Undo state lives in process memory
only — a harness restart clears it (then 404 nothing to undo).
Errors: {ok:false, error} (400 bad params / mixed providers / bad scope /
unsafe paths, 404 unknown session, file absent from the node, or endpoint,
405 wrong method, 413 oversized body, 500 storage or git failures).
Data contract
The public contract of this plugin's guarantees — consumption contract + rollback safety contract: docs/contract.md.
- Reads the
checkpointsstorage domain (schema re-declared inlib/domain.mjs; the spec is not exported by rewind). Reuses the domain already opened by rewind (storageDomain.get()first,open()fallback with analready-openretry window). - Records belong to a
(sessionId, cwd)pair.scope=session(default) filters to the current session;scope=projectmerges every session sharing the workspace key and organizes them by/rewindfork lineage (sessionQuery.traceSession, optional service — absent = flat merge with no branch markers). - Intent labels are derived read-only from the session log:
tool/callevents whose(turn, step)matches the checkpoint are indexed per request; a name matching the record'striggerToolwins,fs/*-intenttriggers prefer the step's first mutating tool, otherwise the step's first call, and a missing log falls back to the raw trigger tool (lib/labels.mjs). Live sessions are read viasession.events; cold sessions viasessionQuery.readSession(both fail soft). - Snapshots are pre-mutation states:
/diff <from> <to>compares thefromsnapshot against thetosnapshot —todoes not include changes made after it. - Rollback restores a node's snapshot file set into the current workspace:
for
gitnodes that is the tracked file tree of the snapshot object, forcopynodes the manifest file list. Anything in the workspace that is not in the node's set is left in place and reported (leftovers). The git provider refuses to roll back when the session cwd is not the repository root (snapshot tree paths are root-relative). The most recent apply can be reverted once with/rollback --undo(in-process only). - Unreferenced git objects are reaped by
git gc(default ~2 weeks) andmaxSnapshots/maxSnapshotBytesquotas prune old records — missing nodes degrade gracefully.
Development
pnpm install
pnpm test # unit tests (diff engine, timeline, labels, tree, jsdom panel smoke tests)
pnpm test:integration # assembled headless integration vs. real dsh-checkpoint-rewind
pnpm build:client # rebuild the browser bundle -> lib/client.js
See CONTRIBUTING.md for conventions and the
ARCHITECTURE.md for the design. The integration suite uses
the local harness deployment (rc.5 packages are not on the npm registry; see
scripts/link-profile-deps.mjs).
Roadmap
- Projection-based queries — the
scope=projecttimeline currently scans the whole checkpoints table per request; asessionProjectionsunit could index by workspace key once the host event vocabulary coverscheckpoint/*events. - Branch-aware diffing UI — visual branch lines between time nodes (the panel currently shows fork markers in the summary area only).
Acknowledgements
This plugin is built on top of
dsh-checkpoint-rewind
by PerryLink — the checkpoint producer
whose read-only storage domain (checkpoints) and snapshot layout this
project consumes. It is not a fork and shares no code with it: the two
integrate through the storage contract only. Third-party notices:
THIRD_PARTY_NOTICES.md.
License
Apache-2.0 — see LICENSE. Third-party notices in THIRD_PARTY_NOTICES.md.