converk/dsh-ux-kit--plugins-turn-file-changes0

dsh-plugin-turn-file-changes

在每个回合结尾展示本次修改的文件(含 +/− 行数),并支持单个/一键撤回

包名
dsh-plugin-turn-file-changes
版本
0.1.0
最近更新
2026年9月10日

安装

此插件尚未提供可验证的 bundle,或兼容性检查未通过。请先阅读仓库说明。 阅读完整 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.nodeturn-file-changes key(新增 key,不替换任何官方渲染器)
位置节点锚在 turn/end 序号 −0.5:晚于本回合所有事件(含收尾回答),早于官方 turn-tail(产物行 + 复制/分支操作条)
显示时机只有收到 turn/end 才出节点——回合收尾后位置固定,不会随流式输出跳动

官方产物行、消息操作条、回合过程折叠都保持原样。

数据从哪来

统计与撤回只有宿主知道(要读旧内容、写回文件),所以是双面插件:

宿主半区src/index.ts + src/host/*

  1. session/eventtool/call 给出 callId → (sessionId, turn) 的权威映射; agent/pre-step 作为兜底(PTC 子调用没有 tool/call 事件)。
  2. tools/pre-execute 在文件工具执行前读出目标文件旧内容(ctx.fs.resolve/stat/readText)。
  3. tools/post-execute执行后读出新内容,按行算 +/-,写进「本会话第 N 回合」的记录。
  4. 通过 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-bootapplyEntryPatches 跳过并 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.jsturnTailDefinitionclosingAnchorchatNodeCHAT_SYNTHETIC_SEQ_OFFSETS)与 dsh-client-ui-workflow-run/lib/client.js
节点排序dsh-client-ui-chat/lib/client.jsorderedVisibleChatNodes(anchor → rank → originalAnchor → key)与 presentationPositionTURN_PROCESS_INDEPENDENT_KINDS 之外、且 anchor ≥ answerAnchorSeq 的节点不会被折进「回合过程」)
键控渲染器dsh-client-ui-chat/lib/types/client/contract/slots.d.tsconversation.chat.node,keyed/session)
工具事件dsh-tools/lib/types/index.d.tstools/pre-execute waterfall、tools/post-execute waterfall)、Inspect Event.listEventstools/result
会话事件dsh-session/lib/types/types.d.tstool/call{turn,step,callId,name,arguments}tool/result{turn,step,message,meta}turn/startturn/end
会话工作目录dsh-session/lib/types/types.d.tsSessionHeader.cwd(运行时经 exec.agent.session.header.cwd
文件服务dsh-fs/lib/types/index.d.tsresolve/stat/readText/writeText/processPath/contains没有删除原语,新建文件的撤回走 node:fs/promises.rm,并先用 contains 做工作区包含性检查)
私有 RPCdsh-client-connection/lib/types/rpc.d.tsHostConnectionRpc.handle(channel, handler)ClientConnectionRpc.call(channel, endpoint, payload)CHANNEL_PATTERN = /^\/[A-Za-z0-9._~-]+$//api 为保留通道)
文件工具参数dsh-tool-fs/README.mdwrite/edit)、dsh-tool-str-replace-editorstr_replace_editorcreate/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)会拒绝并提示。