dsh-better-summary
DSH Web 插件:把对话流末尾的「本次产出」chip 行,替换为 Codex 风格的改动汇总卡片 —— 已编辑 N 个文件、总计与每文件 +新增 −删除 行数、折叠展开、按文件内联 diff、「在文件夹中显示」。行数统计不依赖 git,非 git 目录同样精确可用。
状态:v1 已实现并通过全部验证(typecheck / 143 项单元与组件测试 / 5 条 E2E lane)。
兼容性:当前版本 0.2.0 要求 DSH ≥ 0.1.6-alpha.2 —— 装到更早的宿主上,客户端半边会在装载期直接报错,卡片不出现(详见兼容性,不是静默降级)。
设计与决策记录见 docs/design.md。
效果
产出卡片:文件数、增删行数、内联 diff、「在文件夹中显示」与「审查」
上图为真实运行截图(主题跟随 DSH,此处为深色)。数据来自 E2E 的 mock 场景:新建 a.txt 与 12 行的
b.txt,再把 a.txt 里的 beta 改成 BETA,故逐文件 +3 −1 / +12 −0、合计 +15 −1。图中两个文件
的 +N 位宽不同(2 位 vs 3 位)却与头部合计落在同一组列上;字号默认与 DSH 正文一致,可在设置里固定为具体 px。
核心特性
- 不依赖 git:统计来自每轮成功工具结果的 applied diff(
write/edit 写入 tool result meta.diffs 并随会话日志持久化),新建文件由工具参数补齐。非 git 目录、临时目录与 git 仓库表现一致。
- 接管而非入侵:以 DSH 自带产出行所在的单元格 id 注册进
conversation.chat.turnTail(list 槽,priority -2),顶替自带的 ui-deliverables 行(0)与 dsh-better-sidebar 的 chip 行(-1);关闭开关或卸载即恢复原样。不修改 DSH 任何源码。
- 与显示模式无关:自己注册
ConversationNodeDefinition 折叠原始事件(紧凑显示下工具节点会被折叠,读渲染节点会丢数据);不发布任何视图节点。
- 可扩展的数据推导:变更来源三层抽象 + 策略注册表 +
ctx.betterSummary 服务 —— 接入新工具只需注册一个来源,不改卡片。
- 设置即时生效:
collapsedRows、inlineDiffMaxLines、folderReveal、fontSize、priority、enabled 在 DSH 设置的独立页面(「任务产出」)中调整,保存即生效,无需刷新。
- 「在文件夹中显示」:由本插件自研(Host 侧 argv 数组原生打开,不经过 shell);带 WSL 分支(
wslpath + explorer.exe /select, / Invoke-Item),并且打开失败会说原因,不再静默。
兼容性
| 插件版本 | 适用 DSH | 该版本 DSH 上 conversation.chat.turnTail 的槽契约 |
|---|
0.2.0(当前) | ≥ 0.1.6-alpha.2 | list 槽:条目按 id 划分单元格,同格只渲染优先级最低的一个 |
0.1.1 | 0.1.5-alpha.1 | chain 槽:条目靠 select 座位路由 |
DSH 0.1.6-alpha.2 把这个槽从 chain 换成 list,注册形态随之改变(0.2.0 注册 id,不再注册 select)。
两边都装错的症状相同:客户端半边在装载期抛错,插件不生效、卡片不出现(DSH 自带产出行照常显示),
日志里是槽注册表给出的明确原因 —— 不是渲染成空卡片,也不会静默降级。下面两条都是真实挂载的报错原文:
# 0.2.0 装到 DSH 0.1.5 及更早:
chain slot "conversation.chat.turnTail" requires options.select
# 0.1.1 装到 DSH 0.1.6 及更新:
list slot "conversation.chat.turnTail" requires options.id
package.json 用 engines.dsh 声明了这条下限。DSH 自身不读取也不校验该字段,它只是把版本要求
随清单一起暴露出来;真正的依据是上表的对应关系。
安装
插件以 profile bundle 方式挂载。三种渠道:
官方 CLI(推荐)
# 在 DSH checkout 内(本机 dsh 不在 PATH 上时):
pnpm -C <deepseek-harness 路径> dsh plugin --profile web add file:<绝对路径>/dsh-better-summary-0.2.0.tgz
该命令会在 profile 目录里执行 pnpm add,并依据包的 dsh.bundle.patch 自动把 dsh-better-summary 追加进 dsh.profile.bundles。
从 GitHub 安装(git 依赖)
dsh plugin --profile web add github:yaodongH/dsh-better-summary
# 等价写法(github: 是 pnpm 的托管仓库简写):
# dsh plugin --profile web add git+https://github.com/yaodongH/dsh-better-summary
这条路要多一步放行。lib/ 不在仓库里(被 gitignore),靠包的 prepare 钩子在安装时现场构建;而
pnpm 10 默认不执行依赖的构建脚本,不放行会被明确拒绝安装(不会装出一个缺 lib/ 的坏包):
ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED
The git-hosted package "dsh-better-summary@0.2.0" needs to execute build scripts
but is not in the "onlyBuiltDependencies" allowlist.
在 profile 的 pnpm-workspace.yaml(即 ~/.dsh/profiles/web/pnpm-workspace.yaml)里放行:
allowBuilds:
dsh-better-summary: true
onlyBuiltDependencies: [dsh-better-summary] 是等价写法(pnpm 报错文案里给的就是这个键)。放行后
安装会从源码构建出 lib/index.js、lib/client.js 与类型声明;pnpm 会缓存构建结果,换成新 commit 才会重跑。
安装输出里可能出现 missing peer @deepseek-ai/cordis / missing peer react 的警告:profile 的
pnpm-workspace.yaml 设了 autoInstallPeers: false,而这两个 peer 由 DSH 应用本身提供,不需要在
profile 里再声明一份。判定安装成功的依据是 dsh.profile.bundles 里出现 dsh-better-summary。
手工编辑 profile
pnpm pack 产出 tarball(prepack 钩子会自动先构建);
- 编辑
~/.dsh/profiles/web/package.json:
dependencies 增加 "dsh-better-summary": "file:/<绝对路径>/dsh-better-summary-0.2.0.tgz"
dsh.profile.bundles 数组追加 "dsh-better-summary"
pnpm install(在 ~/.dsh/profiles/web 内);
- 重启
dsh web。
回滚
从 dsh.profile.bundles 移除该包(或停用插件行)后重启,原有产出行立即回归;DSH 源码零改动。
配置
| 配置项 | 位置 | 默认 | 说明 |
|---|
| 折叠行数 | DSH 设置 → 任务产出 | 3 | 折叠时显示的文件行数(1–20) |
| 内联差异行数上限 | 同上 | 200 | 单文件 diff 最多渲染行数(20–2000) |
| 文件夹按钮 | 同上 | 图标加文字 | 图标加文字 / 仅图标 / 隐藏 |
| 卡片字号 | 同上 | 跟随正文 | 跟随正文(跟随 DSH 正文字号设置)或固定 11–18px |
| 槽位优先级 | 同上(高级) | -2 | 同一单元格内越小越优先渲染;自带侧栏产出行用 -1 |
| 启用 | 同上 | 开 | 关闭后恢复 DSH 原有产出行 |
opener | cordis.patch.yml(部署级) | 平台默认 | 覆盖「在文件夹中显示」的命令,参数中 {path} 为打开目标 |
部署级 opener 覆盖示例(例如改用 nautilus,或用替身脚本记录 argv):
- id: better-summary
config:
opener:
command: /usr/bin/nautilus
args: ['--select', '{path}']
平台默认:Linux xdg-open <目录>;macOS open -R <路径>;Windows explorer.exe /select,<路径>。
工作原理
- 数据:本插件注册一个
ConversationNodeDefinition(kind better-summary),只折叠 turn/start、tool/call 与 append 语义的 tool/result,把成功结果的 {name, argsRaw, meta} 按日志顺序发布到 Turn 数据上;同时读取 DSH 自带的产出清单(Turn 数据 deliverables)作为文件顺序与兜底。
- 推导:卡片用「变更来源」选举把这些数据变成每文件
+a −d 与 diff 分块 —— 内置两个来源:
dsh.tool-result-diff-meta(优先级 100):读工具自报的 applied diff(含 3 行上下文的 hunk);
dsh.tool-call-args(优先级 50):新建文件时工具没有前像(meta.diffs 为空),由调用参数补齐。
- 渲染:卡片在轮次尾部渲染;「审查」展开全部文件的内联 diff;行点击走 DSH 既有的文件打开漏斗。
- 宿主:唯一的 Host 调用是「在文件夹中显示」——回环校验 + 路径解析 + argv 数组 spawn,不经过 shell。
「在文件夹中显示」的打开规则
| 平台 | 单文件(select) | 多文件 / 目录(folder) |
|---|
| macOS | open -R <path> | open <dir> |
| Windows | explorer.exe /select,<path> | explorer.exe <dir> |
| WSL | wslpath -w → explorer.exe /select,<winPath> | wslpath -w → powershell.exe Invoke-Item -LiteralPath '<winPath>' |
| 桌面 Linux | xdg-open <dir>(Linux 无通用 select 协议) | xdg-open <dir> |
- WSL 专门分支:WSL 的 Linux 侧通常没有桌面,
xdg-open 会「启动成功、随即以退出码 3 失败」,表现为点了没反应;因此 WSL 一律把路径交给 Windows 桌面(wslpath -w 翻译,目录用 Invoke-Item——直接 explorer.exe <dir> 不能可靠唤起窗口,与 DSH 自带 path opener 同款结论)。
- 成功判据:进程没启动、或在 2 秒窗口内以非零码退出都算失败(
explorer.exe 除外,它成功时也返回 1);失败时卡片显示原因(例如 "xdg-open" exited with 3: no method available),不再静默。
- 路径解析:绝对路径直接采纳(产出常在
/tmp 或兄弟仓库),相对路径相对会话工作目录解析并禁止 .. 逃逸;一个有效路径都没有时回落到工作目录本身。
扩展:接入新工具
新工具的 result 若携带可识别 meta,或可从未变更的调用参数推导,注册一个来源即可:
const betterSummary = ctx.get('betterSummary') // 客户端上下文
const dispose = betterSummary.registerNodeSource({
id: 'example.apply-patch',
priority: 60,
match: node => node.kind === 'tool-result' && node.call?.name === 'apply_patch',
extract: node => [{ path: 'x.ts', counts: { added: 3, removed: 1 } }],
})
ctx.betterSummary 提供 registerNodeSource / registerTurnSource / listSources / features / version;Host 侧另有 registerWorkspaceSource 注册位(留给后续 git 补漏等实现)。当前生效的来源可在设置页只读列表中查看。
已知限制(v1)
- 只覆盖
write / edit / str_replace_editor(与 DSH 自带产出清单口径一致);bash、代码生成器等非工具途径改动的文件不在清单内。
str_replace_editor 的 str_replace 为片段级计数,insert 只计新增行,view 不产出。
- 无撤销(计划:反向应用 hunk + staleness 校验 + 备份)。
- 外部直接编辑
settings.yaml 后,已打开的页面不会即时反映(设置页挂载或写入前会重新读取)。
- 打开文件夹依赖系统里有可用的文件管理器命令;命令不存在时卡片给出一次性提示,不抛异常。
开发
pnpm install
pnpm build # Host ESM + Client bundle + 类型声明
pnpm typecheck # tsc --noEmit
pnpm test # 单元与组件测试(vitest)
pnpm test:e2e # 构建 → pack → 真实挂载 + Playwright(L1–L4,无需 API key)
pnpm test:e2e:real # 追加 L5 真实模型 lane(只读复用本机 ~/.dsh 的模型配置与凭据)
pnpm pack # 产出可安装 tarball(prepack 钩子自动先构建,不会打出缺 lib/ 的空包)
E2E lane
| lane | 触发 | 验证内容 |
|---|
| L1 挂载冒烟 | test:e2e | 官方渠道真实挂载;外壳无 pageerror/console error/坏响应;设置导航出现「任务产出」且正文控件可见(同时证明 Host 与 Client 双侧生效) |
| L2 卡片与内联 diff | 同上 | mock 模型跑真实工具链;断言文件数、总计 +15 −1、逐文件数字、两段 hunk 的 6 行内联 diff、审查模式、行点击;另断言字号跟随正文字号、计数列跨行对齐、头部合计与行计数逐列同宽同右缘,以及两个计数共享同一条基线(注入行盒扰动模拟 − 的字体回退,扰动前后基线都必须重合) |
| L3 设置动态生效 | 同上 | 设置页改折叠行数、文件夹按钮与字号后,不刷新页面卡片即时变化;再改回并断言双向持久化 |
| L4 在文件夹中显示 | 同上 | profile 覆盖 opener 指向替身脚本,断言宿主把正确目录交给了操作系统;再把替身换成目录制造 spawn 失败,断言路由返回 5xx 且卡片给出提示 |
| L5 真实模型端到端 | test:e2e:real | 只读复制本机 ~/.dsh 的 settings.yaml/.credentials.yaml/.env 到 scratch,用真实 provider 与凭据在非 git 工作区跑一轮,断言卡片与真实统计 |
lane 编排(scripts/e2e-common.sh):解析 dsh 命令 → 建 scratch DSH_HOME → dsh plugin add 挂载 → 播种设置 → 启动 dsh web --port 0 并解析地址 → Playwright。全程只写 scratch 目录,退出即删。
共存验证:E2E_EXTRA_PLUGINS 可挂载额外插件 tarball(冒号分隔),用于在真实插件组合下验证同一单元格的遮蔽顺序:
E2E_EXTRA_PLUGINS=/path/to/dsh-better-sidebar-0.20.0.tgz pnpm test:e2e
# → 卡片仍以 priority -2 渲染,统计与内联 diff 全部正确
list 槽按 id 划分单元格、每格只渲染优先级最低的一个条目,因此本包与
dsh-better-sidebar(priority -1)不会并排出现两行:-2 恒胜出,后者只是被遮蔽
(既占着内存也不渲染)。两者优先级相等时 register() 会在装载期抛错
(同格同优先级被视为配置冲突),共存时请保持数值不同。
许可证
MIT