diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9ea8253..0517d46 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,7 +1,7 @@ name: CI # 每次 push 到主分支、以及任何 PR 包括来自 fork 的 PR 都自动跑完整检查。 -# 本插件是纯 JS(无 TypeScript 源码、无构建步骤),lib/*.js 即发布产物。 +# 本插件是纯 JS,无 TypeScript 源码、无构建步骤,lib/*.js 即发布产物。 # 因此 CI 检查 = 语法检查 + 产物齐全校验,全绿才允许合并。 on: push: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4216ab6..746b5a8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,10 +1,10 @@ name: Release -# 推送形如 vX.Y.Z(如 v0.1.0)的 tag 时自动发版: +# 推送形如 vX.Y.Z,如 v0.1.0,的 tag 时自动发版: # 凭 tag 号更新 package.json 版本并提交回 main → 语法/产物校验 # → 发布 npm → 生成 GitHub Release draft。人只需打一个 tag,其余自动衔接。 # 注意:npm 发布依赖 npmjs 侧开启的 Trusted Publishing -# (npmjs.com → 本包 → Enable trusted publishing → 绑本仓库 GitHub Actions), +# npmjs.com → 本包 → Enable trusted publishing → 绑本仓库 GitHub Actions, # 无需配置 NPM_TOKEN;未开启时发布步骤会失败。 on: push: diff --git a/README.md b/README.md index 4ef474d..36b475d 100644 --- a/README.md +++ b/README.md @@ -16,14 +16,15 @@ DeepSeek Harness 内置的 DiffBlock 展示工具改动时有个毛病:它拿 ## 功能 -- **仅差异行**:完全相同行直接不渲染,只显示真实变化的删除行(红 `-`)与新增行(绿 `+`),消除重复。 +- **仅差异行**:完全相同行直接不渲染,只显示真实变化的删除行红 `-` 与新增行绿 `+`,消除重复。 - **行内字符高亮**:删/增行数相等的替换对,再做字符级 diff,用下划线在行内标出真正改动的字符,而不是整行糊在一起。 -- **近线性算法**:先做公共前缀/后缀收缩(线性),仅对中间小差异带运行 Myers diff,复杂度与差异量成正比而非平方,超大文件也流畅。 +- **近线性算法**:先做公共前缀/后缀收缩线性,仅对中间小差异带运行 Myers diff,复杂度与差异量成正比而非平方,超大文件也流畅。 - **默认收起**:卡片默认一行摘要,点开才显示 diff 体,不刷屏。 - **长 diff 二次折叠**:展开后若差异行超 14 行,折成头 +「⋯ 展开其余 N 行差异」+ 尾,点开才全量。 - **对齐原生观感**:字号、圆角、配色复用内置 DiffBlock/ToolRow 同一批 `--dsw-*` 主题变量,视觉上就是原生卡片。 -- **复制与统计**:复制(只含差异行,`- `/`+ ` 前缀);底部 `└ +A -R · N file(s)` 变更统计。 -- **单文件与多文件**:`edit` / `write` 双双接管,覆盖新建(只显示绿色新增)、纯删、多 hunk 等场景。 +- **复制与统计**:复制只含差异行,`- `/`+ ` 前缀;底部 `└ +A -R · N file(s)` 变更统计。 +- **单文件与多文件**:`edit` / `write` 双双接管,覆盖新建只显示绿色新增,纯删、多 hunk 等场景。 +- **PTC 模式兼容**:`run_code` 子调用中的 `edit` / `write` 同样显示去重 diff,通过 `callId` 中的 `:code:` 标记识别子调用。注意 `write` 是全文件覆写,无旧内容,只能显示全新增;想看精确 diff 应使用 `edit`。 ## 它长什么样 @@ -32,7 +33,7 @@ DeepSeek Harness 内置的 DiffBlock 展示工具改动时有个毛病:它拿 ![去重复 diff 效果](docs/去重复diff.webp) - **顶部行**:`✎ Edit · dsh-cache-billing\src\client.ts`,右侧 chevron 可点开收起。 -- **diff 体**:只显示真实变化的删除行(红色 `-`)与新增行(绿色 `+`),完全相同行不重复渲染。 +- **diff 体**:只显示真实变化的删除行红色 `-` 与新增行绿色 `+`,完全相同行不重复渲染。 - **行内高亮**:把 `峰谷价标注` 改成 `峰谷价标注附模型名`,改动到的字符 `附模型名` 以下划线加粗标出;右下角为复制按钮。 - **底部统计**:`└ +1 -1 · 1 file`,列明净增删行数与文件数。 @@ -58,11 +59,12 @@ dsh plugin --profile web remove dsh-edit-diff - 是**标准形态的 dsh client 插件**,声明 `dsh.client`,导出 `./client`。 - 同时声明了 `dsh.bundle`,因此也是一个**自挂载的 bundle 层插件**:用 `dsh plugin --profile add` 从 GitHub 安装后,会被自动识别为 profile layer 并挂载,无需手工写组合 entry。 -- 纯浏览器半身,无 host 行为;`lib/index.js` 是无操作的 no-op 主机插件(标准双面包约定)。 +- 纯浏览器半身,无 host 行为;`lib/index.js` 是无操作的 no-op 主机插件标准双面包约定。 - **UI 挂载点**:接管 keyed 槽位 `tool.call.toolview` 的 `edit` 与 `write` 两个 key。 -- **遮蔽而非冲突**:注册时显式传 `priority: -1`(低于内置 `file-mutation-toolview` 的默认 0),用更低优先级遮蔽默认渲染,而不是在同一优先级上 clash。 +- **遮蔽而非冲突**:注册时显式传 `priority: -1`低于内置 `file-mutation-toolview` 的默认 0,用更低优先级遮蔽默认渲染,而不是在同一优先级上 clash。 +- **PTC 模式**:通过 `callId` 包含 `:code:` 判断是否为 `run_code` 子调用,从 `argsRaw` 中提取 `old_string`/`new_string` 即 edit 或 `content` 即 write 动态构建 diff。 - **无构建**:`lib/client.js` 是按 DSH client bundle 产出的注册式模块,源码即产物,改完即用。 -- 纯 JavaScript 单文件,不引入任何额外 npm 依赖(diff 算法就地内联)。 +- 纯 JavaScript 单文件,不引入任何额外 npm 依赖diff 算法就地内联。 ## License diff --git a/lib/client.js b/lib/client.js index cafb0bc..d38147c 100644 --- a/lib/client.js +++ b/lib/client.js @@ -1,23 +1,14 @@ -// Browser half of dsh-edit-diff, built as a standard dsh client bundle. +// dsh-edit-diff 浏览器端,标准 dsh client bundle 形态。 // -// Artifact shape follows the deployment's own client bundles: register a module -// table entry through window.__ModuleLoader__.load({ id, factory }). The factory -// resolves its externals through the loader's injected require, which for -// `react` returns the platform module-table instance kept alive by the shell -// seed. The default export is the Cordis plugin object the browser kernel -// adopts and mounts. +// 通过 window.__ModuleLoader__.load 注册模块表条目,factory 从注入的 require +// 解析 react 等外部依赖。默认导出 Cordis 插件对象,由浏览器内核挂载。 // -// Feature: replace the shipped `edit`/`write` tool-call cards. The built-in -// DiffBlock renders the old file's every line as a removed row and the new -// file's every line as an added row with NO line matching, so unchanged lines -// are duplicated across the `-` and `+` blocks. This plugin takes over the -// keyed `tool.call.toolview` slot for both tool names and renders a near-linear -// line-level diff (common-prefix/suffix shrink + Myers on the trimmed core), -// skipping identical lines entirely and highlighting real intra-line character -// changes. Visuals mirror the shipped DiffBlock/ToolRow so the card reads as -// native. Cards collapse to a single summary row by default; opening shows the -// body, and a very long diff folds its middle behind a "⋯ 展开其余 N 行差异" -// button instead of flooding the flow. +// 接管 edit/write 工具卡片:内置 DiffBlock 对新旧全文逐行染色,不做行级匹配, +// 相同行在红绿两区各渲染一遍。本插件接管 tool.call.toolview 的 edit/write key, +// 用近线性行级 diff 公共前缀/后缀收缩 + Myers 跳过相同行,替换行做字符级 +// 下划线高亮。视觉对齐内置 DiffBlock/ToolRow。卡片默认收起,长 diff 二次折叠。 +// PTC 模式下 run_code 子调用中的 edit/write 同样显示去重 diff, +// 通过 callId 中的 :code: 标记识别子调用,从 argsRaw 动态构建 diff。 window.__ModuleLoader__.load({ id: 'dsh-edit-diff', @@ -96,7 +87,7 @@ window.__ModuleLoader__.load({ return ops } - /** 把字符级 op 汇总成高亮区间([start, end))。 */ + /** 把字符级 op 汇总成高亮区间 [start, end)。 */ function charRanges(ops) { const delR = [], addR = [] for (const op of ops) { @@ -125,7 +116,7 @@ window.__ModuleLoader__.load({ return (text.endsWith('\n') ? text.slice(0, -1) : text).split('\n') } - /** 从 ops 构建渲染行,仅含真实差异;替换对(删/增行数相等)做行内字符高亮。 */ + /** 从 ops 构建渲染行,仅含真实差异;替换对删/增行数相等做行内字符高亮。 */ function buildDiffLines(oldLines, newLines) { const ops = lineDiff(oldLines, newLines) const out = [] @@ -169,6 +160,31 @@ window.__ModuleLoader__.load({ } function extractDiffs(block) { + // PTC 模式:从 argsRaw 动态构建 diffs + // callId 包含 ':code:' 表示这是 run_code 的子调用 + if (typeof block.callId === 'string' && block.callId.includes(':code:')) { + // RunningToolCall 的 argsRaw 在 block 上,ToolResultNode 的 argsRaw 在 block.call 上 + const argsRaw = block.call ? block.call.argsRaw : block.argsRaw + const name = block.call ? block.call.name : block.name + if (name === 'edit' || name === 'write') { + try { + const args = JSON.parse(argsRaw) + if (args && typeof args === 'object') { + const filePath = args.file_path + if (typeof filePath === 'string' && filePath !== '') { + if (name === 'write' && typeof args.content === 'string') { + return narrowDiffs([{ path: filePath, oldText: null, newText: args.content }]) + } + if (name === 'edit' && typeof args.old_string === 'string' && typeof args.new_string === 'string') { + return narrowDiffs([{ path: filePath, oldText: args.old_string || null, newText: args.new_string }]) + } + } + } + } catch {} + } + } + + // 标准模式:从 callView 或 resultView 提取 diffs if (!('kind' in block)) { const call = block.callView && block.callView.card === 'diff' ? block.callView : null return call ? narrowDiffs(call.diffs) : null @@ -405,7 +421,7 @@ window.__ModuleLoader__.load({ }) slots.inject('tool.call.toolview', () => { - // priority 要低于默认 0,遮蔽 file-mutation-toolview 的 edit/write(最低者渲染); + // priority 要低于默认 0,遮蔽 file-mutation-toolview 的 edit/write,最低者渲染; // 若也传 0 会在同一 key 上 clash,而非替换。 slots.register({ name: 'tool.call.toolview', key: 'edit', priority: -1 }, DiffCard) slots.register({ name: 'tool.call.toolview', key: 'write', priority: -1 }, DiffCard) diff --git a/lib/index.js b/lib/index.js index 866ae0c..b08f0f9 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,15 +1,14 @@ /** - * Node half: a no-op Cordis plugin. All behavior lives in the browser half - * (lib/client.js), which the web client module system discovers through the - * package.json `dsh.client` declaration. This half exists so the plugin root - * is a complete dual-face package and shows up as one entry in the host - * Loader — and, via `dsh.bundle`, as a self-mounting bundle layer so that a - * single `dsh plugin --profile web add …` fully activates it. + * 主机端:空操作 Cordis 插件。所有行为在浏览器端 lib/client.js, + * 通过 package.json dsh.client 声明被 web 客户端模块系统发现。 + * 本半身存在是为了让插件根目录成为完整的双面包,在宿主 Loader 中 + * 显示为一个条目;通过 dsh.bundle 成为自挂载 bundle 层, + * 一条 dsh plugin --profile web add 即可完整激活。 */ -/** Plugin name (= the config entry id). */ +/** 插件名,即配置条目 id。 */ const name = "dsh-edit-diff"; -/** No host-side services are used. */ +/** 不使用任何主机端服务。 */ const inject = []; -/** No host-side behavior for this browser-surface plugin. */ +/** 本浏览器端插件无主机端行为。 */ function apply() {} export { apply, inject, name }; \ No newline at end of file diff --git a/package.json b/package.json index 3728ed6..7f1f53f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "dsh-edit-diff", - "version": "0.1.1", - "description": "质量 diff:接管 edit/write 工具卡片,行级去重与行内高亮,消除相同内容在删除与新增两区的重复,标准可安装的 dsh 客户端插件", + "version": "0.2.0", + "description": "质量 diff:接管 edit/write 工具卡片,行级去重与行内高亮,消除相同内容在删除与新增两区的重复;兼容 PTC 模式,标准可安装的 dsh 客户端插件", "keywords": [ "deepseek-harness", "dsh",