# dsh-follow-edits [English](README.md) · **中文** [![test](https://github.com/zcr-133/dsh-follow-edits/actions/workflows/ci.yml/badge.svg)](https://github.com/zcr-133/dsh-follow-edits/actions/workflows/ci.yml) 给 **DeepSeek Harness (dsh)** 做的「跟随式编辑」插件:agent 每成功改一个文件,就在**右侧 Sidebar 的文档预览**里打开那个文件、跳到改动行(Cline / VS Code 里那种"看着它改"的体验,搬进 dsh 自己的网页界面)。 > **状态:已实现 v0.3(打开 + 跳行 + 黄/红标亮,改行/删除都精确定位),host/client 逻辑均通过单元测试,并在 desktop profile 真机验证。** API 全部对照桌面端 0.2.0-rc.2 的内置包逐一核实过,见 `docs/research.md`。 ```text pnpm test # 跑 host.test.mjs + client.test.mjs(mock ctx 喂合成事件验证投影折叠与打开/跳行) ``` --- ## 它做了什么 / 没做什么 | 能力 | 状态 | |---|---| | 在侧边栏预览文件 | 平台内置 | | 编辑后自动刷新预览 | 平台内置(`workspace-files` 的 `changes()` 流) | | 打开 + 跳到指定行 | 平台内置(`ctx.sidebarRight.openResource(address, { params: { line } })`) | | 每轮结束的 diff 红绿对比 | 平台内置(`workspace-changes` + `deliverables` review tab) | | **编辑发生的「那一刻」自动打开 + 跳行** | ✅ **本插件** | | 预览里把"刚改的行"标黄、被删内容标红 | ✅ **本插件**(自定义 `documentPreviews` 渲染器) | | **终端 / shell 命令改的文件也自动打开** | ⚠️ **本插件**(降级:只打开,不跳行不标亮) | 一句话:**本插件不是"加跳转",而是把一堆已经内置的原语,用一个"实时跟随 + 实时高亮"的触发逻辑串起来**——dsh 目前只有"回合末出改动卡片",没有"编辑当下就跟着看"。 两类改动,两条路径: - **编辑工具**(`str_replace_editor` / `write` / `edit`):精确跟随 —— 打开 + 跳行 + 黄/红标亮。 - **终端 / shell 命令**(`pwsh` / `bash` 里写文件):降级跟随 —— **只打开文件**(预览会自己刷新出新内容),不跳行不标亮。因为 shell 绕过 `ctx.fs`,没有前后内容可 diff。 行号支持情况(`line`): - `create` / `write` → 第 1 行(参数直接可推) - `str_replace_editor` `insert` → `insert_line + 1`(参数直接可推) - `str_replace` / `edit`(改行 **和删除**)→ **host 在 `tools/pre-execute`(编辑前)读旧文件、`indexOf(oldStr)` 定位起始行**,精确 - 删除的内容编辑后就不在文件里了、事后搜不到,所以必须在编辑【前】捕获 - 冷读/重放时该行号映射为空 → 回退到 client 侧按 `newStr` 搜索(改行仍准,删除退到文件顶部) --- ## 架构(对照官方实现验证过) ``` 路径 A:编辑工具(精确) agent 调用 str_replace_editor / write / edit ▼ tool/call 提交 ▼ tools/pre-execute(★ 编辑前)→ ctx.fs 读旧文件 → indexOf(oldStr) → 行号 ▼ 工具执行 ▼ tool/result 提交 host 面:sessionProjections.register({ key: 'followEdits', ... }) │ 折叠 tool/call + tool/result → last = { path, line, oldStr, newStr, ... } │ wire.view 把该状态自动推给 client(按 key,无需白名单) ▼ client:openResource(fileAddress, { params: { line } }) → 打开 + 跳行 + 黄/红标亮 路径 B:终端 / shell(降级) shell 命令在子进程里直接写盘(绕过 ctx.fs,没有 tool/call 也没有 fs/observed) ▼ host:node:fs.watch(session.header.cwd, { recursive: true }) ▼ 命中文本后缀 → shellBySession.set(sessionId, { path, ts }) ▼ 下一次会话事件驱动投影时合并进 last(command = 'shell') ▼ client:openResource(fileAddress) → 只打开文件,不跳行不标亮 ``` - **host 半**(`index.js`):`sessionProjections.register` 一个投影单元 + `tools/pre-execute` 监听器(编辑前捕获 `oldStr` 行号)+ `session/created` 上挂工作区根递归 watcher,`inject: ['sessionProjections', 'fs']`。 - **client 半**(`client.js`):订阅 `sessions` 列表 + `faceOf('followEdits')`,收到就 `openResource`;并注册一个 `priority: extension` 的 `documentPreviews` 渲染器,把改动行标黄、被删内容标红。 - 关键点:第三方投影的 wire 值会**按 key 自动**出现在 client 的 `faceOf(key)`,不需要 `dsh-api-remotes` 那张编译期白名单。 --- ## 目录结构 ``` dsh-follow-edits/ ├── package.json # bundle 清单:dsh.bundle.patch + dsh.client ├── cordis.patch.yml # patch 层:insert 一行 dsh-follow-edits ├── index.js # host 半:sessionProjections 投影 + tools/pre-execute 行号捕获 ├── client.js # client 半:订阅投影 → openResource 跳行 + 自定义高亮渲染器 ├── host.test.mjs # host 单测(投影折叠 / 行号捕获) ├── client.test.mjs # client 单测(跟随接线 / 地址拼接 / 高亮元素树) ├── scripts/verify-install.mjs # 装载解析自检(改名漏改、目标缺失) ├── .github/workflows/ci.yml # CI:Linux + Windows,Node 20/22 ├── docs/research.md # 调研笔记(每个 API 的取证位置) └── LICENSE ``` --- ## 安装 用官方 Plugin Manager 安装(会做 pnpm install + 双面装载): ```text plugin_manager action: install_bundle target: <本目录绝对路径> ``` 也可以用 `dsh` CLI(web 版先验证): ```text dsh web --patch ./cordis.patch.yml ``` 桌面端:把 `cordis.patch.yml` 里的 `insert` 段合并进 `~/.dsh/profiles/desktop/cordis.patch.yml`,重启桌面端。 > 卸载 / 停用:`plugin_manager` 的 `set_bundle` / `remove_bundle`。 --- ## 验证 **单元级(本仓库内可复现)**:`pnpm test` - `host.test.mjs`:投影折叠(create/insert/write/view/失败/无关工具/引用闸门)、`tools/pre-execute` 行号捕获(删除→第 2 行、改行→第 3 行、捕获失败回退、映射读一次即删)、**shell 递归 watcher**(真跑 `node:fs.watch`:写文件 → last.command === 'shell'、line 为 null、oldStr/newStr 为空、二次改动跟随最新文件)。 - `client.test.mjs`:跟随接线(订阅 → 打开 + 跳行、ts 去重、90s 过期)、地址拼接(相对/绝对/工作区外)、高亮元素树(黄底行 + 红 `⊖` 行 + 别的文件不高亮)。 **真机级(desktop profile,已通过)**:装进 profile 重启后, 1. 让 agent **改一行** → 右侧 Sidebar 自动打开该文件,新内容**黄底**、旧内容**红字 ⊖**; 2. 让 agent **删一行** → 被删内容在它原来的位置**红字 ⊖**。 验证手段:截屏 + 像素分析(黄底预期混合色 `(103,96,36)`、红字 `#e5484d`=`(229,72,77)` 均精确命中)+ 人眼确认。 **安装期自检(可复现,不需要 dsh)**:`node scripts/verify-install.mjs --profile ~/.dsh/profiles/<名字>` 按 dsh 装载器的解析步骤逐条走一遍:`exports` 目标存在、patch 条目名与 client 模块 id 都与 `package.json` 一致、依赖可解析、profile 软链指回本目录。存在的理由:改名只改漏一处,界面上不报错,重启后就是"不跟随了"。 **干净环境装载验证(已通过)**——全新的 `$DSH_HOME` + 全新 profile,非官方条目只有本插件: ```bash export DSH_HOME=/tmp/dsh-home-clean # profiles/verify-web/package.json → dependencies: { "dsh-follow-edits": "link:<本仓库>" } # → dsh.profile.bundles: [dsh-base, dsh-web-app, dsh-follow-edits] dsh --profile verify-web --dump-config | grep -A2 '== dsh-follow-edits' ``` ```text # == dsh-follow-edits - id: dsh-follow-edits name: dsh-follow-edits ``` 退出码 0、组合后配置树 1262 行、零错误 —— 它和官方 bundle 的成层方式完全一致。 --- ## 已知限制 - **高亮渲染器替换了内置代码预览**:为叠加 diff 标色,注册了 `priority: extension` 的渲染器,默认不再用内置 Shiki 语法高亮(可在预览的渲染器下拉里切回「代码」)。后续可考虑内置高亮 + 叠加 diff。 - **精确行号依赖一个非会话状态映射**:host 在 `tools/pre-execute` 里把「callId → oldStr 起始行」存进内存映射,投影在 `tool/result` 折叠时读取它。这属于 `practices.md` 点名的「读取撕裂的非会话状态的 apply」(评审级、非机械可查),代价是冷读/重放时该映射为空,行号只能回退到 client 侧搜索。 - **终端跟随是降级且依赖递归监听**:shell 改动走 `node:fs.watch({recursive:true})`(Windows/macOS 支持,**Linux 上静默不生效**);没有前后内容所以只能「打开文件」;只跟随文本/代码后缀,跳过 `node_modules`/`.git`/`dist` 等目录。 - **shell 改动的投递要等下一次会话事件**:文件系统事件是异步的,投影只在会话事件驱动时重新计算 wire 值,所以 shell 改动通常在紧随其后的那次事件(往往是 shell 自己的 `tool/result`)到达;若事件先到,则顺延到下一跳。 - 跟随所有会话(含后台);如只想跟随当前会话,在 client.js 里加 `retainedBy.mainView` 过滤即可。 - dsh 是 developer preview,API 会随 rc 版本漂移。 ## License MIT