Bundle
dsh-better-summary
DSH Web 插件:把对话末尾的「本次产出」chip 行替换为 Codex 风格的改动汇总卡片(文件数、增删行数、内联 diff、在文件夹中显示)
- Source
- yaodongH
- License
- MIT
- Updated
- Updated 2 days ago
Readme
# 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](docs/design.md)。
---
## 效果
<img src="docs/images/card.png" alt="产出卡片:文件数、增删行数、内联 diff、「在文件夹中显示」与「审查」" width="744">
上图为真实运行截图(主题跟随 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(推荐)
```sh
# 在 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 依赖)
```sh
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`)里放行:
```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
1. `pnpm pack` 产出 tarball(`prepack` 钩子会自动先构建);
2. 编辑 `~/.dsh/profiles/web/package.json`:
- `dependencies` 增加 `"dsh-better-summary": "file:/<绝对路径>/dsh-better-summary-0.2.0.tgz"`
- `dsh.profile.bundles` 数组追加 `"dsh-better-summary"`
3. `pnpm install`(在 `~/.dsh/profiles/web` 内);
4. 重启 `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):
```yaml
- id: better-summary
config:
opener:
command: /usr/bin/nautilus
args: ['--select', '{path}']
```
平台默认:Linux `xdg-open <目录>`;macOS `open -R <路径>`;Windows `explorer.exe /select,<路径>`。
## 工作原理
1. **数据**:本插件注册一个 `ConversationNodeDefinition`(kind `better-summary`),只折叠 `turn/start`、`tool/call` 与 append 语义的 `tool/result`,把成功结果的 `{name, argsRaw, meta}` 按日志顺序发布到 Turn 数据上;同时读取 DSH 自带的产出清单(Turn 数据 `deliverables`)作为文件顺序与兜底。
2. **推导**:卡片用「变更来源」选举把这些数据变成每文件 `+a −d` 与 diff 分块 —— 内置两个来源:
- `dsh.tool-result-diff-meta`(优先级 100):读工具自报的 applied diff(含 3 行上下文的 hunk);
- `dsh.tool-call-args`(优先级 50):新建文件时工具没有前像(`meta.diffs` 为空),由调用参数补齐。
3. **渲染**:卡片在轮次尾部渲染;「审查」展开全部文件的内联 diff;行点击走 DSH 既有的文件打开漏斗。
4. **宿主**:唯一的 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,或可从未变更的调用参数推导,注册一个来源即可:
```ts
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` 后,已打开的页面不会即时反映(设置页挂载或写入前会重新读取)。
- 打开文件夹依赖系统里有可用的文件管理器命令;命令不存在时卡片给出一次性提示,不抛异常。
## 开发
```sh
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(冒号分隔),用于在真实插件组合下验证同一单元格的遮蔽顺序:
```sh
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
Install
dsh plugin --profile web add github:yaodongH/dsh-better-summary
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-better-summary from the hub
- This package builds from source on install. pnpm will ask you to allow its build script — that is permission to run the package’s code on your machine, outside the agent sandbox. Only allow sources you trust.
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.