dsh-chat-diff-summary-legacy
输入框正上方的一条「本轮改动」统计条:这一轮对话动了哪些文件、增删多少行,不用切终端一眼就能看到。
9 个文件已更改 +95 -13

适用内核:>=0.1.5-rc.1 <0.1.8 · 实测 0.1.5-rc.2(DSH Desktop 2.0.13)与 0.1.7-rc.2(当前桌面版内核)
起点是 DSH Desktop 2.0.13 / DeepSeek Harness 0.1.5-rc.2 这条 legacy 线:不使用、也不需要 0.1.6 及之后引入的任何东西,所以 0.1.5 与 0.1.7 两条线共用同一份构建。版本对应关系、自查方法与上界理由见兼容的 DSH 内核版本。
效果
四张图都取自真实运行的 DSH Desktop 2.0.13(隔离的 DSH_HOME),一轮真实对话里由脚本改动了 9 个文件。不是画出来的示意图。
| 收起 · 深色 | 展开 · 深色 |
|---|---|
![]() |
![]() |
| 收起 · 浅色 | 展开 · 浅色 |
|---|---|
![]() |
![]() |
展开那两张里能看清最关键的一点:面板是浮在对话之上的。它背后压着的正是上一条回复的文字("已经改好了:新增 useAutoScroll…"),面板把它们盖住了,而不是把上面的内容顶开、把输入框上方挤小。
深浅两套配色全部来自宿主自己的主题 token,插件不带任何自己的颜色,所以宿主换肤它跟着换。
它统计的是什么
+N/-M是行数,不是编辑次数。 改写一行、行数没变,显示的就是+1 -1;删掉一行是+0 -1。新建文件的全部内容算作新增。- 一轮一测。 基线在
turn/start时拍下,turn/end时再拍一次,两次工作树快照对比。所以一轮里同一个文件改十次,最终只按净变化算一次。 - 一轮什么都没改,就什么都不显示 —— 不占位、不留空条。
- 范围是整个仓库:相对仓库根目录统计,路径只显示文件名。
- 预先存在的改动不算。 你在对话开始前就已经改脏、甚至已经
git add过的东西,在两份快照里都存在,差值为零,不会算到这一轮头上。 - shell 改动照样算。 统计读的是磁盘上的工作树,不是工具日志 ——
sed、脚本、构建产物、Python 一行流,和你手动改文件一视同仁。 - 每个 Session 各算各的,两个 Session 的数字不会串。
一轮运行中就在涨,不等跑完
统计条跟随正在跑的回合,不是等回合结束才出现:
t+1…6s 不显示 (还没有改动)
t+7s 1 个文件已更改 +2 (回合仍在运行)
t+14s 2 个文件已更改 +3
t+20s 3 个文件已更改 +6
t+24s 回合结束,数字定格
- 每次工具调用落地(
tool/result)后重新测一次。 - 测量做了合并:同一时刻只有一次测量在跑,同一 Session 两次测量至少间隔 400ms。一串连续的 bash 调用只走一遍树,不会每个调用走一遍。
- 回合结束的那次永不节流 —— 被间隔挡掉的改动最多是"晚一点显示",不会丢。
- 浏览器侧在每个持久事件后重读一次,同一时刻只允许一次读取在途;只为客户端渲染的流式分片被刻意排除,否则回复每吐一个字它就要重读一遍。
- 重读如果发现数字没变,快照保持同一个身份 —— 正在跑的回合不会把用户刚展开的文件清单晃回去。
布局与尺寸
- 统计条只有自身内容那么宽(
width: max-content),是一枚居中的胶囊,不是和输入框等宽的长条。文件名太长会省略号,数字永远完整。 - 文字和数字之间只隔一个字符(
gap: 1em),没有把它们推到两端的弹性占位。 - 展开的清单占输入框宽度的三分之一并居中,长文件名有地方可省。
- 清单一屏 7 行,高度精确等于
7 × 38px + 2 × 6px,所以折行处永远不会出现半截行;其余的靠面板内滚动。 - 滚轮被限制在面板内(
overscroll-behavior: contain),滚到最后一个文件不会把后面的对话带着滚。 - 窄了就变宽:输入框低于 760px 时清单占 50%,低于 520px 时占满。
在宿主自己的 Chromium 里实测(1000px 宽的对话区):
统计条宽 189px (= 自身内容:文字 + 1em + 数字 + 内边距)
统计条高 38px
清单宽 330px (= 对话区 1/3)
清单高 278px (= 7 × 38 + 2 × 6)
一屏完整行数 7 可滚动:是(本轮 9 个文件)
展开前后条高 38px 顶边不动 —— 什么都没被顶开
面板覆盖 280px 在条上方,盖住对话内容
这一轮的 9 个文件已更改 +95 -13,与用 git diff --numstat 独立算出来的结果逐文件完全一致(architecture.md +6 -1、Composer.tsx +17 -3、useAutoScroll.ts +27 -0……合计 9 个文件 +95 -13)。
Git 语义:绝不动你的仓库
数字来自工作树对工作树的比较,不是 git diff HEAD。
所有写入都落在一个 mkdtemp 出来的私有临时目录里:
GIT_OBJECT_DIRECTORY指向临时对象库,新产生的 blob / tree 都写在那儿;仓库自己的对象库通过GIT_ALTERNATE_OBJECT_DIRECTORIES只读挂进来。- 每次快照用自己独立的
GIT_INDEX_FILE。 - 私有索引用
git read-tree HEAD初始化。 - 插件从不针对项目索引执行
git add,从不 commit,从不碰 HEAD 和 refs,从不 stash / checkout / reset。
tests/git.spec.ts 会在一轮"修改、新增、删除、重命名、二进制化"的操作前后,对仓库的 .git/index、HEAD、for-each-ref 输出和 .git/objects 下每个文件取哈希,断言四者逐字节不变。临时目录在 Session 销毁和插件销毁时清理。
一个值得写下来的真实 bug
私有索引用复制仓库索引的方式初始化是不成立的 —— 早期版本就是这么做的。复制来的索引带着仓库的 stat 缓存,而 git add 允许信任缓存的 stat:当一次编辑保持文件字节长度不变、而副本自身的 mtime 又晚于该文件时,git 会跳过重新哈希,写出一个装旧内容的 tree。
在 macOS + APFS 上实测,40 次等长编辑里有 2 次静默产生了过期快照 —— 也就是那一轮改动没被报出来。改用 git read-tree HEAD(条目不带 stat 信息,git add 必须重新哈希每个路径)后,同一个探针是 0/40。这个探针作为回归测试留在 tests/git.spec.ts 里。
安装
标准 DSH profile bundle:node 半边在 exports["."],浏览器半边在 exports["./client"](通过 dsh.client 声明发现),cordis.patch.yml 只插入本插件一行,不禁用任何东西。
link 安装(profile 跟随工作树,改完 npm run build 即可):
dsh plugin --profile <profile> add link:/path/to/dsh-chat-diff-summary-legacy
打包安装(自包含快照,每次改动后重新打包):
npm run verify # typecheck + build + 测试
npm pack --pack-destination release # → release/<name>-<version>.tgz
dsh plugin --profile <profile> add "file:$PWD/release/<name>-<version>.tgz"
两种方式都需要 宿主进程重新加载才生效,所以装完请重启 DSH Desktop。只重载页面只能带回浏览器半边,node 半边不行。
兼容的 DSH 内核版本
| 声明支持 | >=0.1.5-rc.1 <0.1.8 —— package.json 里的 dsh.engines.dsh |
| 实测通过 | 0.1.5-rc.2(DSH Desktop 2.0.13 搭载的内核)与 0.1.7-rc.2(当前桌面版内核) |
| 未实测 | 0.1.5-rc.1、0.1.6 线,以及两条线里的其它补丁版 |
| 不在支持范围 | 0.1.8 及之后 |
桌面版号 ≠ 内核版号,别混。 DSH Desktop 走的是 2.0.x,DeepSeek Harness 内核走的是 0.1.x。本插件实际验证过两个组合:DSH Desktop 2.0.13 + 内核 0.1.5-rc.2,以及当前桌面版 + 内核 0.1.7-rc.2。
查自己装的是哪个内核版本:
# @deepseek-ai/dsh-base 的版本就是内核版本
node -p "require(process.env.HOME + '/.dsh/profiles/node_modules/@deepseek-ai/dsh-base/package.json').version"
# → 0.1.5-rc.2
上界为什么是 0.1.8:本插件用到的宿主接口全部是 0.1.5 线就有的 —— conversation.input.dock 槽位、ctx.connection.fetch 路由注册、session/event 与 tools/pre-execute 事件。这些接口在 0.1.7-rc.2 上逐一核对仍然存在(ctx.sessions.binding(id) 返回的仍是 { sessionId, session, eventSource, ctx }),所以上界抬到覆盖当前桌面版内核为止;0.1.6 线没有实测过,0.1.8 及之后不在声明范围内。
这个字段是声明,不是闸门。 dsh.engines.dsh 和 @linxin666/* 等第三方插件用的是同一个字段(都在 dsh 对象里,与 bundle、client 并列);当前 DSH 的加载路径并不读取它,所以它挡不住加载 —— 它表达的是"作者支持并实测过的范围",而不是运行时的版本校验。
为什么叫 legacy:0.1.6+ / 0.1.7+ 的接口已经存在,本插件刻意只使用 0.1.5-rc.2 就有的那一套,方便仍在 2.0.13 上的用户直接用。node 半边与浏览器半边在运行时都不 import 任何 @deepseek-ai/* 包 —— 插件消费的每个宿主服务都在 src/host.ts 和 src/client/contract.ts 里做了结构化声明,旁边逐条引用了 0.1.5-rc.2 里的调用点。
刻意没做的:
- diff 审阅。0.1.5-rc.2 没有
changes-review能力,所以点开统计条只是展开/收起一份紧凑的文件清单,文件名是纯文本而不是链接。 - 持久化。统计活在宿主进程内存里,重启宿主就没了,统计条会诚实地藏起来直到下一轮结束。
已知边界:
- 仓库整体统计:Session 的工作目录是仓库子目录时,报出来的路径相对仓库根。
- 两个 Session 同时在同一仓库工作时,各自测量自己的回合边界,但共享同一份工作树 —— 并发改动可能同时出现在两个 Session 的统计里。
- 行数遵循仓库自己的 git 配置,包括
.gitattributes过滤器和core.autocrlf。 - 文件清单最多 200 条;计数保持完整,清单会说明省略了多少条。
- 宿主进程被
SIGKILL时没有任何 disposer 会跑,会在系统临时目录留下每个 Session 一个dsh-chat-diff-legacy-*小目录(只含该仓库的快照对象和一份私有索引),随时删掉都是安全的。
非 Git 工作区
会话的工作目录不在任何仓库里时,插件在临时目录里自建一个私有仓库来测量这份目录:git init 只发生在该 Session 自己的 scratch 目录里,工作目录不会被写入 .git、索引或对象库(tests/git.spec.ts 里有断言)。测量仍然完全交给 git —— add --all、write-tree、diff-tree --numstat —— 插件自己不遍历、不哈希任何文件。
第一遍只预热,不出数字。首次遇到该目录时要读一遍被接受的全部文件:实测 1.7 GB / 28k 文件 + 默认忽略集 = 36.7 s,对象库约 810 MB(都在临时目录里)。这一遍在后台跑,不会卡住工具调用;它落地的那个回合不报数字,从下一个回合起正常测量,此后每轮只是一次 stat 扫描(同一目录实测 0.13 s)。
默认忽略集写在私有仓库的
info/exclude,目录里已有的.gitignore照常生效:.git/ .hg/ .svn/ node_modules/ dist/ build/ out/ target/ coverage/ __pycache__/ .venv/ venv/ *.apk *.zip *.7z *.rar *.exe *.dll *.so *.dylib *.iso *.dmg *.msi不做行尾转换:私有仓库里写死
core.autocrlf=false、core.safecrlf=false,统计的是磁盘上的字节,而不是某个仓库的检出策略。这一模式信任 git 的 stat 缓存。仓库模式为了保证正确性每轮强制重哈希每个路径(见
snapshotTree的注释),非仓库模式为了成本只重读变过的文件;在时间戳精度很粗的文件系统上(FAT/exFAT、部分网络盘),"改成同样长度、时间戳又恰好相同"的极端情况可能漏报。要强制重读,把目录放进一个仓库里即可(那样走的是仓库模式)。嵌在目录里的 git 仓库按 git 自己的 embedded repository / gitlink 语义处理:它内部改了什么都不会逐文件报出来;只有它自己的提交前进时,统计条列出该目录一行且不计行数(
diffSyntheticTrees把 gitlink 从 git 报的+1 -1改写成不可计数,避免编造行数)。要它内部的逐文件统计,把会话 cwd 设到那个仓库里。Windows 的目录联接(junction)会被跟随,从工作目录走出去的联接内容也会进入快照。默认忽略集覆盖依赖目录与构建产物,但不覆盖任意联接。
用户级
core.excludesFile只在 POSIX 上生效:Windows 下插件交给 git 的环境不含用户 profile 路径,git 读不到~/.gitconfig,所以只有目录内的.gitignore和上面的默认忽略集在起作用。
开发
npm install
npm run typecheck
npm run build # lib/index.js(宿主)+ lib/client.js(浏览器)
npm run test # 156 个测试
npm run verify # 上面三步依次跑
测试 156 个,覆盖:统计路由与文案、真实 git 仓库上的 snapshots / numstat 解析 / 重命名 / 二进制 / 等长编辑回归探针、回合跟踪与节流、跨回合集成、浏览器侧取值与订阅、槽位注册与渲染。
浏览器半边通过 window.__ModuleLoader__ 加载,因此 react 和 react/jsx-runtime 必须声明为 external —— tsdown 的 external 选项和 peerDependencies 两处都要写,因为 tsdown 0.15 没有 deps.neverBundle 这个键,而无法识别的选项是静默失效的,会打进第二份 React(tests/bundle.spec.ts 断言产物里只有两个 require)。
lib/是构建产物,随仓库一起提交,这样 clone 下来即可被dsh plugin add link:加载;改动源码后请重新npm run build。
更完整的英文说明(含逐项验证记录)见 README.en.md。



No comments yet. Be the first to write one.