converk/dsh-ux-kit--plugins-turn-file-changes ↗★ 0
dsh-plugin-turn-file-changes
在每个回合结尾展示本次修改的文件(含 +/− 行数),并支持单个/一键撤回
安装
此插件尚未提供可验证的 bundle,或兼容性检查未通过。请先阅读仓库说明。 阅读完整 README ↗
说明文档
阅读完整 README ↗dsh-plugin-turn-file-changes
在每个回合(一问一答)的结尾,把本回合模型改动的文件列成一块面板:每个文件带 +新增行 / -删除行、是否新建、以及单个撤回按钮;面板头部有全部撤回。只统计本回合,不是整个会话。
… 助手的最终回答 …
本次修改 3 个文件 +42 -7 [全部撤回]
panel.tsx +28 -3 [撤回]
index.ts +12 -4 [撤回]
locales.ts +2 -0 新建 [撤回]
官方产物行 + 消息操作条(本插件不动它们)
它为什么不是官方「产物」行
DSH 自带 @deepseek-ai/dsh-client-ui-deliverables,它渲染的正是
conversation.chat.turnTail 这个 chain 席位(「第一个接受 owner 的选择器渲染」)。
chain 上只有一个占位,官方已经在上面,本插件再挤进去就会被它的选择器挡掉。
所以本插件走增量路径(与官方 dsh-client-ui-workflow-run 同款做法):
| 层 | 做法 |
|---|---|
| 会话节点 | 注册自己的 ConversationNodeDefinition(kind turn-file-changes,target chat),把 turn/start → tool/call → tool/result → turn/end 折叠成一个独立节点 |
| 渲染 | 注册 conversation.chat.node 的 turn-file-changes key(新增 key,不替换任何官方渲染器) |
| 位置 | 节点锚在 turn/end 序号 −0.5:晚于本回合所有事件(含收尾回答),早于官方 turn-tail(产物行 + 复制/分支操作条) |
| 显示时机 | 只有收到 turn/end 才出节点——回合收尾后位置固定,不会随流式输出跳动 |
官方产物行、消息操作条、回合过程折叠都保持原样。
数据从哪来
统计与撤回只有宿主知道(要读旧内容、写回文件),所以是双面插件:
宿主半区(src/index.ts + src/host/*)
session/event的tool/call给出callId → (sessionId, turn)的权威映射;agent/pre-step作为兜底(PTC 子调用没有tool/call事件)。tools/pre-execute在文件工具执行前读出目标文件旧内容(ctx.fs.resolve/stat/readText)。tools/post-execute在执行后读出新内容,按行算+/-,写进「本会话第 N 回合」的记录。- 通过
ctx.connection.rpc.handle('/turn-file-changes', …)暴露list/revert/revertAll三个端点——它和/api共用同一套浏览器信任栅栏 + 登录 cookie,插件自己不写鉴权。
浏览器半区(src/client/*)
- 会话日志折叠出「本回合改了哪些文件」→ 决定节点是否存在、显示哪些文件名(刷新页面后依然在)。
- 挂载时向宿主 RPC 要行数与撤回能力 → 渲染
+/-、新建、已撤回等状态。 - 宿主重启过(记录已丢)时返回
tracked:false:只列文件名、显示提示、禁用撤回按钮。
语义细节
| 情况 | 行为 |
|---|---|
| 同回合多次改同一文件 | 保留第一次改动前的内容(撤回目标)、最后一次改动后的内容(统计口径 = 本回合净变化);面板只出现一行 |
| 回合内新建的文件 | 标 新建,撤回 = 删除该文件 |
| 工具调用失败 | 不记录(失败调用不会改文件) |
read / glob / grep / 命令类工具 | 不记录 |
| 文件 > 4 MiB 或二进制 | 记路径但不统计、不可撤回,行内显示原因 |
| 撤回后再点 | 幂等:直接返回成功 |
| 撤回时文件已被你手改过 | 仍然按「改动前内容」写回(撤回就是这个意思),返回值里带 unchangedSinceTurn 标记 |
| 删除新建文件 | 只在文件位于会话工作目录内时执行(工作目录外的删除直接拒绝) |
| 列表显示 | 默认最多 5 行,其余点 ▸ 展开其余 N 个文件(展开后可 ▾ 收起) |
| 路径显示 | 一律相对化,绝不显示 D:\... 绝对路径:优先相对会话工作目录;拿不到工作目录(或它不匹配这些文件)时用本回合文件的公共目录前缀;连公共前缀都没有(跨盘符)时每个文件取末尾若干段 |
统计口径是行级差异:先在首尾剥掉相同的行,再对中间段求 Myers 最短编辑脚本长度
D,由 LCS = (N + M − D) / 2 反推增删行数。差异过大(D > 4096 或单侧 > 20000 行)
退化为上界估计并在面板上标 ~,保证任何输入都不卡。
构建与检查
pnpm install --ignore-scripts
node build.mjs # lib/{shared,definition,index,client}.js(+client.js.map)
node node_modules/typescript/bin/tsc --noEmit # 0 错
node scripts/selftest.mjs # 纯逻辑自测 62 项(行差统计 / 工具词表 / 节点折叠 / 追踪器+撤回)
node scripts/clientsmoke.mjs # 浏览器半区冒烟 27 项(bundle 协议 + apply 注册 + SSR 渲染)
react-dom / @types/react-dom 只进 devDependencies,用于 clientsmoke.mjs 的服务端渲染;client bundle 运行时仍然只 require("react") / require("react/jsx-runtime")(冒烟测试会断言这一点)。
pnpm 10+ 默认拦截依赖构建脚本(
ERR_PNPM_IGNORED_BUILDS: esbuild);esbuild 的平台二进制经 optionalDependencies 分发,postinstall 并非必需——直接node build.mjs/node node_modules/typescript/bin/tsc --noEmit,不要走pnpm run。
产物协议(AGENTS.md 3.4):lib/client.js 是经典脚本(非 ES Module),自注册
window.__ModuleLoader__.load({ id: "dsh-plugin-turn-file-changes", factory }),
工厂体内只 require("react") / require("react/jsx-runtime"),并带合法 sourcemap trailer。
安装到 DSH
本包是独立 npm 包,不依赖仓库内任何其他插件:
dsh plugin --profile web add D:\env\Projects\dsh-plugins\plugins\turn-file-changes
然后在 profile 的 cordis.patch.yml 里用 insert 形制挂载(裸 - name: 会被
dsh-app-boot 的 applyEntryPatches 跳过并 warn patch: id is required for non-insert patches):
- insert:
- id: turn-file-changes
name: dsh-plugin-turn-file-changes
它必须挂在宿主组合(profile patch),不能挂进 agent preset:宿主半区注册的是
进程级 HTTP RPC 通道(按会话追踪工具事件),挂进 preset 会在第二个会话上因路由重复而冲突,
而且只会追踪挂载该 preset 的会话。patchReload: live 下保存 patch 即重新组合,
但浏览器要再刷新一次才会拿到新的 window.__DSH_BOOT__ 并加载本插件的 client bundle。
对接的真实契约(本机 DSH 0.1.2-rc.1,部署于 D:\env\node-global\dsh-stable)
| 用途 | 位置 |
|---|---|
会话节点定义(match/start/update/publication/buildViewNode、节点字段) | dsh-client-ui-conversation/lib/types/client/contract/conversation.d.ts;参考实现 dsh-client-ui-chat/lib/client.js 的 turnTailDefinition(closingAnchor、chatNode、CHAT_SYNTHETIC_SEQ_OFFSETS)与 dsh-client-ui-workflow-run/lib/client.js |
| 节点排序 | dsh-client-ui-chat/lib/client.js 的 orderedVisibleChatNodes(anchor → rank → originalAnchor → key)与 presentationPosition(TURN_PROCESS_INDEPENDENT_KINDS 之外、且 anchor ≥ answerAnchorSeq 的节点不会被折进「回合过程」) |
| 键控渲染器 | dsh-client-ui-chat/lib/types/client/contract/slots.d.ts(conversation.chat.node,keyed/session) |
| 工具事件 | dsh-tools/lib/types/index.d.ts(tools/pre-execute waterfall、tools/post-execute waterfall)、Inspect Event.listEvents(tools/result) |
| 会话事件 | dsh-session/lib/types/types.d.ts(tool/call{turn,step,callId,name,arguments}、tool/result{turn,step,message,meta}、turn/start、turn/end) |
| 会话工作目录 | dsh-session/lib/types/types.d.ts 的 SessionHeader.cwd(运行时经 exec.agent.session.header.cwd) |
| 文件服务 | dsh-fs/lib/types/index.d.ts(resolve/stat/readText/writeText/processPath/contains;没有删除原语,新建文件的撤回走 node:fs/promises.rm,并先用 contains 做工作区包含性检查) |
| 私有 RPC | dsh-client-connection/lib/types/rpc.d.ts(HostConnectionRpc.handle(channel, handler)、ClientConnectionRpc.call(channel, endpoint, payload);CHANNEL_PATTERN = /^\/[A-Za-z0-9._~-]+$/,/api 为保留通道) |
| 文件工具参数 | dsh-tool-fs/README.md(write/edit)、dsh-tool-str-replace-editor(str_replace_editor 的 create/str_replace/insert) |
边界与已知限制
- 不 import / 依赖任何其他插件,不共享 utils;删除本插件不影响 DSH 与其他插件(AGENTS.md 4.1 / 4.4)。
- 只追踪三个文件工具(
write/edit/str_replace_editor)的改动。用bash/pwsh命令改的文件不在统计内(宿主没有「文件变了」的通用事件;要覆盖它只能整工作区快照,代价不成比例)。 - PTC 模式下(
run_code子调用)宿主能追踪,但浏览器侧看不到:子调用写的是tool/code-dispatch事件,没有turn字段,无法归属回合。原生工具模式(默认)不受影响。 - 只保留进程内状态:DSH 重启后,旧回合面板只剩文件名,行数与撤回不可用(面板有提示)。 每个会话保留最近 64 个回合、单回合最多 500 个文件、单文件保留 4 MiB 内容。
- 改宿主半区必须重启 dsh:client bundle 走 HMR 轮询热替换,但宿主半区是 Node ESM,Loader
用同一条说明符
import(),Node 的模块缓存不会失效(本机实测:改了宿主代码后,运行中的进程 仍执行挂载时的版本)。重启后才生效。 - 撤回不做冲突检查:若该文件在回合结束后被你或后续回合改过,撤回仍会写回「本回合改动前」的内容。
- 撤回不回滚会话记录:只改磁盘文件,聊天记录与工具卡片保持原样(与 Codex 的 revert 一致)。
- 删除新建文件依赖后端能给出宿主路径(
ctx.fs.processPath):远端执行世界(如 e2b)会拒绝并提示。