dsh-follow-edits
English · 中文
Watch the agent edit, live. A plugin for DeepSeek Harness (dsh) that opens the file the agent just changed in the right-hand Sidebar preview, jumps to the changed line, and highlights the diff — the Cline / VS Code follow-along experience, inside dsh's own web UI.
Status: v0.3, working. Open + jump + yellow/red highlighting, exact positioning for both edits and deletions. Host/client logic covered by unit tests and verified on a real desktop profile. Every API used here was checked against the packages bundled in dsh desktop
0.2.0-rc.2— seedocs/research.mdfor the evidence trail.
pnpm install && pnpm test # host.test.mjs + client.test.mjs (mock ctx, synthetic events)
What dsh already does, and what this adds
| Capability | Where it comes from |
|---|---|
| Preview files in the Sidebar | built in |
| Refresh the preview after an edit | built in (workspace-files changes() stream) |
| Open a file at a given line | built in (ctx.sidebarRight.openResource(address, { params: { line } })) |
| End-of-turn diff review (red/green) | built in (workspace-changes + the deliverables review tab) |
| Open + jump at the moment the edit happens | ✅ this plugin |
| Yellow = changed line, red = removed content, in the preview | ✅ this plugin (custom documentPreviews renderer) |
| Files written by terminal / shell commands also open | ⚠️ this plugin (degraded: opens the file, no jump, no highlight) |
In one line: this is not "add a jump button" — it chains a set of primitives dsh already ships behind a live follow trigger, because dsh currently only tells you about edits at the end of a turn.
Two kinds of change, two paths:
- Edit tools (
str_replace_editor/write/edit) → precise follow: open + jump + yellow/red highlighting. - Terminal / shell (a command writing a file via
pwsh/bash) → degraded follow: open the file only (the preview refreshes itself). Shell writes bypassctx.fs, so there is no before/after content to diff.
How the line number is obtained:
create/write→ line 1str_replace_editorinsert→insert_line + 1str_replace/edit(both edits and deletions) → the host reads the old file intools/pre-execute(i.e. before the edit) and locates the start line withindexOf(oldStr)— exact- Removed content no longer exists in the file afterwards, so it must be captured before the edit
- On a cold read/replay that mapping is empty → the client falls back to searching for
newStr(still exact for edits; deletions fall back to the top of the file)
Architecture (checked against the official packages)
Path A: edit tools (precise)
agent calls str_replace_editor / write / edit
▼ tool/call committed
▼ tools/pre-execute (★ BEFORE the edit) → ctx.fs reads the old file → indexOf(oldStr) → line
▼ tool executes
▼ tool/result committed
host side: sessionProjections.register({ key: 'followEdits', ... })
│ folds tool/call + tool/result → last = { path, line, oldStr, newStr, ... }
│ wire.view pushes that state to the client automatically (keyed, no allow-list)
▼
client: openResource(fileAddress, { params: { line } }) → open + jump + yellow/red highlight
Path B: terminal / shell (degraded)
a shell command writes to disk from a child process (bypasses ctx.fs; no tool/call, no fs/observed)
▼ host: node:fs.watch(session.header.cwd, { recursive: true })
▼ text/code suffix hit → shellBySession.set(sessionId, { path, ts })
▼ the next session event recomputes the projection and merges it into last (command = 'shell')
▼
client: openResource(fileAddress) → opens the file only, no jump, no highlight
- Host half (
index.js): onesessionProjections.registerunit + atools/pre-executelistener (captures the pre-edit line) + a recursive workspace-root watcher attached onsession/created;inject: ['sessionProjections', 'fs']. - Client half (
client.js): subscribes to thesessionslist +faceOf('followEdits'), callsopenResourceon change, and registers apriority: extensiondocumentPreviewsrenderer that paints changed lines yellow and removed content red. - Key detail: a third-party projection's wire value shows up in the client's
faceOf(key)by key — no compile-time allow-list fromdsh-api-remotesis needed.
Repository layout
dsh-follow-edits/
├── package.json # bundle manifest: dsh.bundle.patch + dsh.client
├── cordis.patch.yml # patch layer: inserts dsh-follow-edits
├── index.js # host half: projection + tools/pre-execute line capture
├── client.js # client half: projection subscription → openResource; highlight renderer
├── host.test.mjs # host unit tests (projection folding / line capture)
├── client.test.mjs # client unit tests (follow wiring / address building / highlight tree)
├── scripts/verify-install.mjs # loader-resolution self-check (stale names, missing targets)
├── .github/workflows/ci.yml # pnpm test on Linux + Windows, Node 20/22
├── docs/research.md # research notes — where every API was verified
└── LICENSE
Install
Through the official Plugin Manager (runs pnpm install and loads both faces):
plugin_manager action: install_bundle target: <absolute path to this directory>
Or with the dsh CLI first, to check it on the web build:
dsh web --patch ./cordis.patch.yml
Desktop: merge the insert entry from cordis.patch.yml into ~/.dsh/profiles/desktop/cordis.patch.yml and restart the app.
Uninstall / disable: the Plugin Manager's
set_bundle/remove_bundle.
Verification
Unit level (reproducible in this repo): pnpm test
host.test.mjs— projection folding (create/insert/write/view/failure/unrelated tools/gating),tools/pre-executeline capture (deletion → line 2, edit → line 3, capture failure → fallback, mapping read-once-then-deleted), and the recursive shell watcher (realnode:fs.watch: write →last.command === 'shell',linenull, no before/after text, a second write follows the newest file).client.test.mjs— follow wiring (subscribe → open + jump, ts dedupe, 90 s expiry), address building (relative / absolute / outside the workspace), highlight tree (yellow line + red⊖row, other files untouched).
Real machine (desktop profile, verified) — after installing into the profile and restarting:
- Have the agent change a line → the right Sidebar opens the file automatically, the new content is yellow, the old content shows as red
⊖; - Have the agent delete a line → the removed content appears as red
⊖where it used to be.
Evidence: screenshot + pixel analysis (the yellow blend lands on (103,96,36) and the red on #e5484d = (229,72,77), both exact) plus eyeball confirmation.
Install-time self-check (reproducible, no dsh needed): node scripts/verify-install.mjs --profile ~/.dsh/profiles/<name>
Walks the same resolution steps the dsh loader does — exports targets exist, the patch entry name and the client module id both match package.json, dependencies resolve, and the profile symlink points back at this directory. It exists because a rename that misses one of those references fails silently: nothing in the UI tells you, it just stops following after the next restart.
Clean-room load check (verified) — a brand-new $DSH_HOME with a brand-new profile whose only non-official entry is this bundle:
export DSH_HOME=/tmp/dsh-home-clean
# profiles/verify-web/package.json → dependencies: { "dsh-follow-edits": "link:<repo>" }
# → dsh.profile.bundles: [dsh-base, dsh-web-app, dsh-follow-edits]
dsh --profile verify-web --dump-config | grep -A2 '== dsh-follow-edits'
# == dsh-follow-edits
- id: dsh-follow-edits
name: dsh-follow-edits
Exit code 0, 1262-line composed tree, no errors — the bundle layers exactly like an official one.
Known limitations
- The highlight renderer replaces the built-in code preview. To overlay diff colours it registers a
priority: extensionrenderer, so you lose the built-in Shiki syntax highlighting by default (switch back to "Code" in the preview's renderer dropdown). Folding diff colours onto the built-in highlighter is future work. - Exact line numbers depend on a non-session map. The host stores
callId → oldStr start linein an in-memory map duringtools/pre-execute, and the projection reads it when foldingtool/result. This is the "apply that reads torn non-session state" pattern called out inpractices.md(review-tier, not mechanically detectable). The cost: on a cold read/replay the map is empty and the line falls back to a client-side search. - Shell following is degraded and depends on a recursive watcher. Shell changes come from
node:fs.watch({recursive: true})(Windows/macOS support it; silently unavailable on Linux); with no before/after content it can only open the file; it follows text/code suffixes only and skipsnode_modules/.git/distand friends. - A shell change is delivered on the next session event. Filesystem events are async and the projection is only recomputed when a session event drives it, so a shell write usually arrives with the event right after it (often the shell's own
tool/result), or one hop later. - Follows all sessions (including background ones). To restrict it to the session in view, filter with
retainedBy.mainViewinclient.js. - dsh is a developer preview; APIs drift between rc releases.
License
MIT
No comments yet. Be the first to write one.