Skip to content
dsh-market Browse plugins GitHub 中文

Yinxe/deepseek-harness-plugins#file-change-viewer

Takes over the edit/write rows in the chat stream with the official native row shell and collapse behavior; expanding shows per-file code cards with file-type icon, add/delete stats, per-block collapse, highlight/plus-minus toggle, syntax-highlighted unified diff with real file line numbers, and a configurable context margin. Also ships a patch tool (beta, off by default) that applies multi-spot multi-file patches atomically via the *** Begin Patch envelope.

Stars ★ 6 Category UI Enhancements Listed 2026-09-19

Install

Inside DeepSeek Harness, with dsh-market

dsh plugin --profile web add dshmarket

Or from the command line

dsh plugin --profile web add github:Yinxe/deepseek-harness-plugins#path:/plugins/file-change-viewer

Installing runs third-party code with your own permissions — it can read your files, use your credentials and reach the network. Review the source first, and pin a commit (github:owner/repo#sha) when you can.

Screenshots

README

This plugin publishes its README in Chinese only.

DeepSeek Harness(DSH)文件修改查看器:接管对话流里的 edit / write 行——行的外壳与折叠行为完全沿用官方原生行(和「思考 / 读取」行同一套样式,默认同样折叠,也可在设置里改成默认展开),展开后每个文件块是一张代码卡片:卡头是「文件类型图标 + 文件名 + 增删统计(图标 + 绿增红删)」并带该块自己的折叠箭头与「高亮 / ± 差异」切换(各带图标),卡内是带语法高亮的统一 diff(新增行绿底、删除行红底、行号是文件里的真实行号,删除行不占号)。每张卡片还会在改动两侧多显示几行没受影响的上下文(默认 3 行,设置里可调 0 / 3 / 5 / 8)——这几行取自文件当前内容,模型只圈 1 行上下文时也看得清改动落在哪。另外它还带一个 patch 工具(测试版,默认关,设置里可开):一次调用把多处零散修改 / 多个文件的补丁应用下去(*** Begin Patch 信封,全有或全无),批量改文件从此不必退化成 bash + sed/python——那种改法在界面上只留一行 Bash,看不见任何 diff,还得整份重写或逐个替换。

全局默认值在设置 → 左侧导航「File Change View」(本插件自己的设置节)里配置——展示方式是两张并排的单选卡,卡里直接画出两种视图的真实效果,另有上下文行数与默认展开开关;这些值持久化在 profile 条目 dshp-file-change-viewer 的 config:(0.1.7 契约:条目导出 Config schema + volatile 字段,设置页经 settings.update 写入即热更新)。会话页头右侧还多了两个只作用于当前会话的快捷开关(一键展开 / 收起本会话的所有文件改动、切换差异视图),它们不改上面的全局默认,也不写持久化配置。

内置的文件修改行是默认折叠且只有 -/+ 行色的:DSH 用 DisclosureRow 承载折叠语义,折叠态下 DiffBlock 根本不挂载,所以「用 CSS 把行撑开」走不通。本插件按官方扩展点注册 tool.call.toolview 的同名 key(官方文档:a key the shipped composition already covers is replaced, not shared),用同一批官方 primitives 重新组装这一行。

设计原则:只改渲染,不碰数据;能不改的就不改。不注册工具、不改任何文件——内容全部来自官方 dsh-tool-fs 已给出的 meta.diffs 与调用参数,行的几何全部来自官方 DisclosureRow 默认样式,配色全部来自官方 token。Host 半只多做两件事:把自己的 settings 命名空间注册进设置系统,并提供两条同源 /ext 路由供设置节读写这两项偏好。

长什么样

▸ 编辑 · src/client/index.ts            +58 -43          ← 原生行:默认折叠(可配成默认展开),与读取/思考行一致
   ╭──────────────────────────────────────────────────╮
   │ ▾ 📄 src/client/index.ts          ⊕37 ⊖26  [⌘高亮][⑂± 差异] │  ← 卡头:类型图标 + 文件名 + 图标化统计 + 两块控件
   │    1  const RAW_MAX_CHARS = 4000;                 │
   │    2                                              │
   │    3  /** 生成「按行上底色」的 CSS 规则。 */        │
   │       -  * 官方 CodeBlock 没有逐行样式的 API,      │  ← 红底 = 删除
   │    4  +  * 官方 CodeBlock 在行号模式下…            │  ← 绿底 = 新增
   ╰──────────────────────────────────────────────────╯
  • 行是原生行:DisclosureRow 默认样式(24px 行高、16px 图标位、13px 标色、hover 图标↔箭头交叉淡出),本插件只把官方 ToolRow 里那几条工具专属样式(分隔点、路径链接、摘要、统计、Inspect 药丸)镜像过来——不自造行外壳、不覆盖背景。
  • 统计绿增红删:+58 用 --dsw-alias-state-success-primary,-43 用 --dsw-alias-state-error-primary(官方行内是单一灰色,这里按要求分色)。
  • 卡片头:官方 FileTypeIcon(按扩展名判类型)+ 文件名 + 图标化增删统计(IconPlusOutline16 / 一图元减号 SVG,绿增红删)+ 该块自己的折叠箭头 + 该块自己的「高亮 / ± 差异」(IconCodeOutline16 / IconBranchOutline16)。官方代码块自带的语言标签栏与复制按钮继续隐藏(不需要)。
  • 编辑行的 leading 图标沿用官方 IconEditOutline16(与内置文件修改行同一个)。
  • 失败时 summary 位只显示首行错误(红字,官方 ToolRow 同做法),不显示路径与统计。

patch 工具(批量 / 零散修改,测试版 · 默认关)

它是测试版能力,默认不注册。 到 设置 → File Change View → 工具 打开「启用 patch 工具(测试版)」, 模型才会看到这个工具;关掉即刻消失(正在进行的那次调用不受影响)。开关值是条目 config: 里的 patchTool 字段(volatile),也可以直接用 composition 的 config: { patchTool: true } 在部署层打开—— 设置页一开一关走 settings.update → loader/volatile-update,不需要重启 dsh web;手工编辑条目 config 则重启生效。

官方的文件工具是 read / write / edit:write 要重发整份文件,edit 一次只能替换一处。所以「一个文件里改 8 处、顺带再改 3 个文件」这种活儿,agent 往往会退化成 bash + sed/python 一把梭——改动藏在命令里:界面上只留一行 Bash,没有 diff、也没法整体回滚。本插件补上这个缺口:一次调用改多处 / 多文件,而不用重写任何一份文件(省 token、不会误覆盖整份文件、每一段改动都进差异卡片可审查)。

补丁格式是 Codex / opencode apply_patch 的 *** Begin Patch 信封,不是 patch(1) 的 unified diff:

patch(patch: "*** Begin Patch
              *** Update File: src/a.ts
              @@
              -const a = 1;
              +const a = 11;
              *** Add File: notes/t.md
              +| a | b |
              *** End Patch")
性质 说明
格式 *** Begin Patch / *** End Patch 信封,段落是 *** Add File: 与 *** Update File:;一次可带多个文件、每个文件多个 @@ 片段
定位 不写行号:片段靠自己的上下文内容定位,五级回退(精确 → 去尾空格 → 两端 trim → Unicode 标点归一 → 忽略一切空白但要求全文件唯一);@@ 锚点 用于消歧——锚点写成本段第一行也认;*** End of File 钉在文件尾
追加 只有 + 行的片段(没有上下文、没有删除)追加到文件末尾——这是上游行为,照抄
新建文件 *** Add File: <路径> + 整段 + 行,父目录自动创建
原子性 全有或全无:先把每个文件读进来、在内存里全部应用成功才落盘;任一片段对不上就整体失败,绝不写一半
后端 走 ctx.fs:同样吃沙箱模式、fs/write-intent 观察策略、fs/observed 记账;不绕过 seam
行尾/BOM CRLF 文件归一后应用、写回时恢复整份 CRLF;带 UTF-8 BOM 的文件改完 BOM 还在
范围 只做「新建 / 修改」:*** Delete File: 与 *** Move to: 解析层认识、执行层明确拒绝(见下)
渲染 结果元数据带 diffs,于是这张调用也用它自己的差异卡片显示(按片段各一张卡、统计只算真正变化的行、行号是落盘时算出的真实行号)

一次调用就成功的几条设计

模型第一次调用 patch 失败、第二次才改对,是最常见的浪费。真实会话里抓到的两类根因,以及对应的处理:

现象 根因 现在怎么处理
写了 @@ Line 1: Hello DSH,下一行又是 -Line 1: Hello DSH,报「找不到这一段的上下文」 模型把 @@ 当小节标题用(像 unified diff 的 hunk 头),而它是锚点;旧实现匹配到锚点后从下一行开始找,于是要改的那一行被锚点本身"吃掉" 锚点命中后从锚点自身开始找:锚点是上方一行 → 继续往下找;锚点就是改动行 → 当场命中。工具描述也把「锚点是文件里真实存在的一行、不是标题」写明白了
凭记忆写的那一行与文件只差几个空格(.dashing{ vs .dashing {),报「找不到」 前四级回退吃缩进与行尾空白,**吃不了「有没有空格 / 空格在哪」**的差异 加第五级:忽略一切空白比较,但配唯一性安全阀——全文件只有一处能对上才采用,两处以上就不猜(宁可失败,也不改错地方)
一个片段里堆了多处删除,但它们不相邻 片段语义要求旧内容在文件里连续;模型把它当成「一次批量」 报错时点破这一点,并教它「每处单独写一个 @@ 片段 / 把中间那些没改的行也写成上下文行」(判定是精确的:按顺序都能找到、但彼此不连续)
只发了 *** Begin Patch,里面是空的 生成被截断 / 模型先写信封再想内容 报错分开说:空补丁(教它段落怎么写)、缺 *** End Patch(提示可能被截断)、整段写成 unified diff(给出可照抄的正确格式,并提醒小改用 edit 更省事)
重试时又发了一遍同样的改动 上一次其实已经改好了 检测「新内容已经在文件里、旧内容不在」,提示「这段改动看起来已经应用过,确认后把这一段从补丁里去掉」(只是提示,不动磁盘)

报错不再只说「找不到」:applyChunksToText 失败时会在文件里找与模型写的那一行最像的真实行,把行号、原文与「差在哪」一起回给模型:

patch 无法应用:在 /path/to/s.css 里找不到这一段的上下文:
  .dashing{animation:dashFlash .18s ease-out;}
文件第 64 行最像(相似度 94%),实际是:
    .dashing { animation: dashFlash .18s ease-out; }
(只有空格 / 制表符的差别(有没有、在哪里))
把这一行**原样**(连同缩进)抄进 - 行再试一次;改动上下各留一行上下文会更稳。
文件没有被修改。

三段信息各有用途:原文回显让它能对照;文件里的真实行让它照抄就能过;差异原因(缩进 / 行内空白 / 标点 / 第几个字符起不同)让它知道下次别再凭记忆写。相似度不够高时改成明确提示「这段文本在文件里不存在,先 read 再写」。诊断只在失败路径上跑,并且有行数上限(2 万行以上不扫)。

为什么不做删除与改名:这两件事本来就有官方出口——bash 的 rm / mv(git 跟踪的用 git rm / git mv),而且与 patch 吃同一份沙箱策略,越界一样会被拒。反过来,把它们塞进 patch 的收益只是省一次调用,代价却很实:

  • 删除不可逆,而 patch 的「全有或全无」只覆盖验证阶段——落盘仍是逐个执行的,第 3 个文件失败时前两个已经改了、删掉的也回不来;
  • 改名同样会不可逆地动到另一个路径;
  • 跨平台另有一套命令转义要维护(POSIX rm 与 Windows 的 pwsh 语法不同),等于把一段在 CI 上跑不到的代码交付出去。

所以模型真写出这两段时,patch 会在读任何文件、问任何审批之前整体拒绝,并给出替代做法:

patch 只做「新建 / 修改」,不执行删除与改名,这份补丁里有:
  *** Delete File: obsolete.txt
删除请用 bash 的 `rm`(git 跟踪的用 `git rm`)、改名请用 `mv`(跟踪的用 `git mv`);两者与 patch 吃同一份沙箱策略,越界一样会被拒。把这几段从补丁里去掉、只留增改,patch 就能继续。

客户端仍然把「它本来打算做的改名」画出来(卡头 旧 → 新)并打 rejected 角标——用户要看到的是模型想干什么。

什么时候用哪个:一处小改动 → edit;新建 / 整份重写 → write;多处零散或跨文件 → patch;删除 / 改名 / 构建 / 测试 / git → bash。

两种展示方式(设置 → File Change View)

方式 组件 形态
高亮(默认) 官方 CodeBlock 单个代码块里放完整统一 diff(LCS 结果):未变行只出现一次,删除行红底、新增行绿底,删行不占行号;代码原样(不加 - / + 标记)
± 差异 官方 DiffBlock + 中性上下文行 逐行 -/+ 红绿文字(紧凑),超长中部折叠;自带复制按钮与 └ +A -B 页脚。喂给它的是语义变更(只含真正变化的行),所以它的 +A -B 与高亮视图、行头统计永远一致

设置页里这两种方式是两张并排的单选卡,每张卡下面用同一份渲染代码(diffView.ts,与工具行走的就是这一套)画一段真实样张(src/greet.ts 的一次 +2 -1 小改),选中即生效——挑的时候看到什么,会话里就是什么。卡片整块可点(样式里给样张加了 pointer-events:none,官方组件自带的复制按钮在样张里不会抢走点击)。

两个视图都会在改动两侧多显示几行没受影响的上下文(contextLines,默认 3 行)。这几行取自文件当前内容——Host 的 /locate 路由拿文件定位这段 hunk 时,顺手把它两侧各最多 8 行原文回给客户端(响应里只有行号与这十几行,整份文件不回传),客户端按偏好截。

为什么 ± 差异视图的上下文是「另外画的一行行中性文字」:官方 DiffBlock 的行 kind 只有 path | del | add | gap(别的 kind 直接 throw),画不了中性的上下文行——上下文一旦塞进它的 diffs,就会变成绿色的 + 或红色的 -,把没改的行说成改了。所以本插件把上下文单独渲染成中性行贴在它上下:上文 → DiffBlock(红绿改动) → 下文,拼起来正好是一份标准的 unified diff。

官方 DiffBlock 进卡后还会自带三样冗余,被结构性清掉(选择器全走它自带的 data-diff 根 + 子元素位置):body 首行那条完整路径(官方 buildRows 无条件插入,600 字重 + 56px 右留白,读起来像正文里横插的标题)、右上角复制按钮(卡头视图切换旁再浮一个)、块底 └ +A -B · N 个文件 footer(与卡头统计同口径重复报数)。卡头已有文件名与统计,这三样只会在上下文与改动之间制造噪声。

上下文行虽然不是官方组件画的,但读起来必须像同一块代码,所以做了三件事对齐:① 排版逐像素镜像官方——同一字体 token(--dsw-font-markdown-code-block,实测 11px/19px + code 字族)、行盒与官方 .line 同为 22px 最小盒高;② 衔接处零缝隙——有上文时把 DiffBlock 改动区自己的顶部内边距归零(+ .ctx + .diff > div:first-of-type),上下文行与 ± 行逐像素相接;③ 横向滚动收敛到一处——外层 diffWrap 是整份 diff(上文 + 改动 + 下文)唯一的滚动容器:官方 .body 的滚动被结构性覆盖关掉(data-diff 根下第一个 div,注意不能写 :first-child——body 前面还有个复制按钮 <button>),上下文行不自己滚。超长行因此只有一根滚动条、一条轴,不会出现「上下文一根条、改动另一根条、各滚各的」。

高亮视图的行底色铺满整条滚动宽度:官方 .line 是普通块盒,宽度只到 code 可视宽度;white-space:pre 下超长文字以行内溢出延伸到盒外——直接在 .line 上叠底色的话,往右滚,红绿条会在行中间断开(文字还在、底色没了)。所以行盒被显式撑宽:code > :global(.line) { box-sizing:border-box; width:max-content; min-width:100% }——短行底色照旧铺满整卡,长行底色跟着文字走到行尾。border-box 必须显式写:行号列是 .line 自己的 padding-inline-start,默认 content-box 下 min-width:100% 只约束内容宽,行号内边距叠加在外,背景会凭空多出约一个行号列宽。.line 是官方全局类名(官方自己的 CSS Module 里就是 :global(.line)),所以这里必须写 :global 形态,构建产物里它是合法的裸类名(check-client.mjs 的哈希护卫已为它豁免)。

行号列从卡片左边框让出一档 14px:官方把行号画在行盒的最左缘(绝对定位的 ::before 落在 inset-inline-start:0),而上面为了让底色铺满整卡把 pre 的横向内边距归零了——官方默认那 16px 一拿掉,行号列就紧贴卡片边框。行号列宽是 max(2, 位数)ch(官方按代码行数内联写在块根上),位数一多(3–4 位)数字会把整列填满,等于压在边框上。所以行盒补一条 padding-inline-start: calc(var(--dsl-code-block-line-number-width) + 26px)(26px = 新的 14px 档 + 原有的 12px 行号/正文间隙),行号本身用 .line::before { inset-inline-start: 14px } 同步右移。14px 不是随手取的数:它正是本插件另一套版式那一档(.ctx 的 padding:0 14px、官方 DiffBlock 的 .body{padding:12px 14px}),于是高亮与 ± 两个视图的行号列、正文列落在同一个 x 上(无头 Chrome 像素实测:两视图的行号 ink 都自卡片左缘 +15px 起、正文 ink 同一 x)。设置页样张的 .pvCode 复刻同两条规则,样张与真用起来一致。

± 差异视图:上下文行带真实行号,改动行的行号列放 + / -。官方 DiffBlock 的行没有编号机制,上下文行的行号由本插件用 CSS 计数器(dshp-fcv-num)补上——上文从「上文首行的真实行号」起数,下文的起点 = 上文首行 + 上文行数 + 语义新增行数(删除行不占号,与高亮视图同口径);起点由 JSX 内联成 .ctx 容器上的 counter-reset,定位不到(locate 没回结果)时不带号、绝不编数。行号列的宽窄(3ch + 12px 间隙)、颜色与淡化与高亮视图的官方行号一致。

改动行不编号,而是把 + / - 画在同一列里:diffSignRules 生成的动态 CSS 给 del / add 行让出与上下文行号同一列(padding-inline-start: calc(3ch + 12px)),并把官方行内的 - /+ 前缀压掉(::before{content:none}),改在自己的 ::after 里画符号(del 红 / add 绿)——于是「上下文行号 / 改动符号 / 正文」三列严格对齐(无头 Chrome 实测:三行正文左缘都是同一个 x)。这里还有一条必需的配套规则:改动行自己得是那个 ::after 的包含块(.diff[data-diff] > div > div { position:relative })——官方行是 static,不定位的话 inset-inline-start:0 会向上找到官方块根(整张卡)的左缘,+ / - 就比上下文行号左出一档、还紧贴卡片边框(行号列与符号列错开 14px)。不这样做的话,官方的行内前缀会把改动行的正文多推两个字符,上下文行与改动行的正文就错开了。区间用 div:nth-of-type 而非 nth-child:官方块超长折叠时行间会插一个 <button>(展开其余行),:nth-of-type 按元素类型计数、button 不占 div 的位,索引与行序严格一致;已隐藏的 path 行仍占 div 索引(display 不影响选择器),所以区间统一 +1 偏移。设置页的样张不传 hunk 类名,所以样张里的改动行保持官方原样(行内前缀、不与上下文对齐)。

三层优先级:单块点击 > 会话级覆盖 > 全局偏好

四项设置(展示方式、上下文行数、编辑 / 写入是否默认展开、patch 工具开关)里,前三项都有两个入口,第四项只有一个:

层 入口 落盘 作用范围
单块自己的临时点击 卡片头上的折叠箭头 / 「高亮 · ± 差异」两个药丸 不落盘 那一个文件块
会话级覆盖 会话页头右侧的两个快捷开关 不落盘 当前会话的所有文件改动
全局偏好 设置 → File Change View(单选卡 / 下拉 / 开关) 条目 config: 所有会话的默认值
  • 「patch 工具开关」不参与这套优先级:它不是渲染偏好,而是 Host 半注不注册那个工具的开关,改完立即生效。
  • 全局偏好改动立即经 /ext/dshp-file-change-viewer/config 写进 profile 条目 dshp-file-change-viewer 的 config:(Host 调 settings.update),保存成功 / 失败都在设置节里如实反馈。

会话页头的两个快捷开关(只影响当前会话)

会话标题栏右侧(官方「在应用中打开」、会话日志下载的同一排)多出一组胶囊按钮:

[ ⌄ 收起改动 ] | [ </> 高亮 ]  ↺      ← 当前会话已被临时改动(↺ = 恢复跟随偏好)
[ › 展开改动 ] | [ ⑂ ± 差异 ]         ← 跟随设置页里的全局默认
  • 左边那个一键展开 / 收起本会话的所有文件改动(行与行内文件块一起),右边那个切换本会话的差异视图;
  • 它们改的是内存里的会话级覆盖(src/client/session.ts):一个 /config 请求都不发,持久化配置纹丝不动,换一个会话立刻回到全局默认;
  • 覆盖生效时胶囊边框变品牌色并多出一个「恢复跟随偏好」的小按钮,点它即回到默认;
  • 点页头那一下是真正的一键全改:连你之前手动折叠过的行也会翻过去(覆盖带一个版本号 rev,行组件发现覆盖换版就作废自己那份临时状态)。改完之后你再单独点某一行 / 某一块,仍然由你说了算。

「全局偏好里的默认展开」只影响之后新渲染的编辑 / 写入操作(已经在会话里的行保持原样,不会被开关抽走或撑开);想对当前这个会话立刻生效,用页头那两个开关。行内不再放全局切换按钮——逐行记忆只会让界面自相矛盾。

安装

方式一(推荐):克隆 monorepo 安装 —— 更新只需 git pull,旧版 DSH 可 checkout tag

# 1. 克隆 monorepo(lib/ 构建产物已提交,clone 下来就能用,无需 build)
git clone git@github.com:Yinxe/deepseek-harness-plugins.git
cd deepseek-harness-plugins
pnpm install

# 2. 本地安装到 profile(路径按你执行命令时的 cwd 解析)
dsh plugin --profile web add ./plugins/file-change-viewer

# 3. 重启生效
dsh web
  • 更新:仓库内 git pull + dsh web(最快——lib 已提交,未改 src 无需 build);兼容旧版 DSH:按 compat.json 的 tag git checkout <tag> 后重跑上面的 add。

dsh plugin 会把包写进 profile 的 dsh.profile.bundles —— 无需手动改配置文件。

改源码后:src/client 改完跑 pnpm --filter @dshp/file-change-viewer build 重新打出 lib/host.js + lib/client.js,然后 dsh web 重启(client 半强刷页面即可)。

验证:pnpm --filter @dshp/file-change-viewer test 会跑 node --check 两份产物 + Host 冒烟(scripts/check-host.mjs,152 项:命名空间与 Config 导出契约、configure({ auto: false })、补丁消毒、/ext 三条路由、非法补丁不落库、patch 工具注册契约与动态开关(默认关 / 开了就注册 / 关了立刻反注册,由 loader/volatile-update 驱动)、全有或全无、写前预检、删除 / 改名拒绝、BOM 与沙箱提权、locate 回行号 + 两侧上下文且同一文件只读一次)+ patch 纯函数(scripts/check-patch.mjs,74 项:多文件 / 多片段 / 五级模糊定位(含唯一性安全阀)/ End of File 锚点 / 行尾与 BOM / 现场回归(@@ 当标签用、行内空白差异、多处删除不相邻)/ 信封诊断(空补丁 / 缺收尾 / 写成 unified diff)/ 失败诊断(最近的真实行 + 差异原因 + 已应用提示))+ Client 无头渲染断言(scripts/check-client.mjs,207 项,含一层迷你 React,能验证「偏好实时作用于行 / 换调用复用实例 / 会话页头开关只改当前会话」这类带生命周期的行为);界面上让 AI 改任意一个文件,展开「编辑」行后每个文件块应是带语法高亮的卡片、+A -B 与路径链接正常,且统计与 ± 视图只报真正变化的行数;在设置 → 「File Change View」里改一次偏好,profile 条目 dshp-file-change-viewer 的 config: 应立即出现对应字段,页头那两个开关则不该在配置里留下任何东西。

一键 AI 安装:把下面这段发给你的 DSH AI 即可:

帮我安装文件修改查看器插件(monorepo Yinxe/deepseek-harness-plugins,子目录 plugins/file-change-viewer,包名 @dshp/file-change-viewer):
1. git clone monorepo 并 pnpm install
2. dsh plugin --profile web add ./plugins/file-change-viewer
3. dsh web 重启,然后改一个文件,确认 edit / write 卡片默认就是展开的、代码有语法高亮
4. 如果这个会话里还有动态注册的同名插件(cordis_define 定义的,@ 引用能列出),用 cordis_undefine 卸掉,避免两份注册互相顶替

方式二:从 Release 安装(无需 clone;更新需手动重跑命令)

latest 滚动版(跟随 main 最新构建;滚动更新 = 重跑同一条 add 命令):

dsh plugin --profile web add \
  https://github.com/Yinxe/deepseek-harness-plugins/releases/download/latest/dshp-file-change-viewer-latest.tgz

dsh web   # 重启生效

历史版本(兼容旧版 DSH)

main 永远跟随最新 DSH。老版本 DSH 用户装静态历史版本:按 compat.json 记录的兼容 tag,到 Releases 找对应版本 Release,资产名 = dshp-file-change-viewer-<tag>.tgz:

dsh plugin --profile web add \
  https://github.com/Yinxe/deepseek-harness-plugins/releases/download/<tag>/dshp-file-change-viewer-<tag>.tgz

dsh web   # 重启生效

更新

三种安装方式对应的更新方式

安装方式 更新命令 说明
方式一 clone(推荐) 仓库内 git pull + dsh web 最快;未改 src 免 build;旧版 DSH 用 git checkout <tag>
方式二 Release 重跑同一条 add 命令 + dsh web URL 直装对 update 免疫
方式三 git 依赖 dsh plugin --profile web update @dshp/file-change-viewer + dsh web 一条命令;git 解析约 35s/插件(实测)

功能

部分 内容
Host(src/host/ → lib/host.js) name / inject: ['webServer'] / Config 导出 / apply:① 上交条目 Config schema(三项偏好全是 volatile 字段,权威存储在 profile 条目 config:)+ settings.configure({ auto: false }) 关掉自动设置页;② 三条同源路由 /ext/dshp-file-change-viewer/{state,config,locate} 供设置节读写与行号定位;③ patch 工具(测试版,默认关:patchTool 一开一关就注册 / 反注册,热更新走 loader/volatile-update,注册面在 ctx.inject(['tools','fs']) 的注入作用域里)。没有事件;这一行同时是 DSH 从 Host Loader 的条目里扫描 dsh.client 声明来发现并下发客户端 bundle 的入口——没有它,lib/client.js 不会被加载(详见 src/host/index.ts 头注释)。
Client(src/client/ → lib/client.js) 用 priority: -1 注册 tool.call.toolview 的 edit / write / str_replace_editor 三个 key(影子化官方内置行;同 key 同 priority 会抛错,所以必须比内置的 0 更小),带 locale: 'conversation' 取文案;行外壳用官方 DisclosureRow 默认样式(不自造卡片),展开后每个 hunk 一个自己的折叠块,块内是纯高亮代码(官方 CodeBlock 去掉语言栏/复制/外壳);diff.ts 推导模型与 LCS 统一 diff,lang.ts 映射 26 个官方语法 id,行内没内容时给等待 / 截断兜底文案;diff.ts 把每个 hunk 跑一次 LCS(raw / rows / changed 三种口径),统计与 ± 视图都用语义变更;prefs.ts 管全局偏好的读写往返与订阅(设置节与工具行共用同一个 store),FileChangeViewerSection.ts 是设置 → 「File Change View」这一节的内容(展示方式两张样张单选卡走 viewCards.ts),api.ts 封装两条 /ext 路由;diffView.ts 把「高亮 / ± 差异」两种视图的渲染收口(工具行与设置页样张共用同一份代码),SessionControls.ts + session.ts 是会话页头那两个快捷开关与它们的内存级会话覆盖。
同源路由 GET /ext/dshp-file-change-viewer/state(当前生效偏好)/ POST …/config(保存补丁 → settings.update)/ POST …/locate(真实行号定位,只回整数、不回文件内容)。业务错误一律 200 + ok:false,跨站 / 方法错误 4xx。
工具 注册一个 patch 工具(*** Begin Patch 信封,一次可改多处 / 多文件,全有或全无):经 ctx.fs 读写,吃沙箱模式与观察策略,结果元数据带 diffs 交给卡片渲染;删除 / 改名明确拒绝并指路 bash。

行与文件块行为

场景 表现
行的默认状态 渲染那一刻的会话级覆盖 → 全局偏好:默认折叠(与思考 / 读取行一致);打开「编辑 / 写入默认展开」后新渲染的行直接展开,或点会话页头的「展开改动」立刻摊开当前会话的全部改动;点行任意处或 Enter / Space 可随时单独切换(临时,不写回)
write 新建文件 整份内容绿底 + 高亮,行头统计显示 +N -0
write 覆盖已有文件 用结果元数据的真实 hunk,再按偏好补上文件里前后各 N 行上下文
edit 改文件 一个代码块内交错:红底旧行(不占行号)、绿底新行、上下文行不染色,全部带语法高亮;replace_all 在行头提示;行号是文件里的真实行号(上下文行一并占号)
上下文行(两个视图) 改动的上下各 N 行(contextLines,默认 3)取自文件当前内容;高亮视图并进同一个代码块(行号相应前移),± 差异视图渲染成中性行贴在官方 DiffBlock 上下
调用进行中(参数已完整) 行头先按参数给出预览
调用进行中(参数还在流式生成) 宽容解析半截 JSON,逐帧显示正在写入的内容
调用失败(未落盘) summary 位只显示首行错误(红字),不显示路径与统计
窗口截断(读不到调用参数) summary 位显示「参数不可用」;参数能解析时交给官方 JsonBlock(自带截断),否则原样打印并截断 4000 字符
点击文件路径 打开该文件(openFile);路径按工作区相对化,POSIX home 显示为 ~
每个文件块 自己一条原生折叠行(路径 + 该块统计),默认开合跟随会话级覆盖与用户偏好(单独点过的块由用户说了算),可临时点开 / 收起
patch 调用 与 edit / write 完全同一张卡片:标题「补丁」,多文件各占一张卡;流式生成时先用参数里的 patch 文本预览
高亮视图超过 400 行 只渲染前 400 行,下面一行提示剩余行数并指引切到 ± 差异视图(CodeBlock 没有 maxLines)
± 差异视图超过 200 行 由 DiffBlock 中部折叠接管,给出「… 其余 N 行」就地展开按钮

与内置行的差异(刻意的)

维度 内置 file-mutation-toolview 本插件
展开态 默认收起 默认收起;设置里可把编辑 / 写入改成默认展开(只影响之后新渲染的行),会话页头的「展开 / 收起改动」可立刻摊开当前会话的全部改动
展开后的内容 摘要 + -/+ 文字色的 DiffBlock 语法高亮 + 整行红绿 + 行号,每个文件块可独立折叠
代码块外壳 无(裸 DiffBlock) 一张自制代码卡片:卡头是「类型图标 + 文件名 + 统计 + 逐块控件」,官方那个语言栏(含复制)隐藏,卡内是插件自己的盒子(不套两层);代价是高亮视图没有复制入口,要复制补丁就切到 ± 差异
统计 灰色 +A -B,按工具原文行数 绿增红删,按 LCS 后的语义变更行数(一次单行替换就是 +1 -1,不是原文的 +7 -7)
进行中的调用 只显示一行摘要 实时显示正在写入 / 将要写入的内容
行外壳 官方 ToolRow(含 output 等分区) 官方 DisclosureRow 默认样式 + 那几条工具专属样式

配置项

三项显示偏好 + 一个工具开关,落 profile 条目 dshp-file-change-viewer 的 config:,界面入口是 设置 → 左侧导航「File Change View」(settings.section,id = 该命名空间):

偏好 控件 默认 说明
view 两张预览单选卡 highlight highlight = 单代码块统一 diff(整行红绿 + 行号 + shiki 高亮);diff = 逐行 ± 视图。卡里直接画出两种效果
contextLines 下拉选择框 3 改动两侧各多显示几行没受影响的上下文(0 / 3 / 5 / 8)。这几行取自文件当前内容(Host 的 /locate 路由定位后一并回传),不是模型在 old_string / 补丁片段里带的那几行——模型只圈 1 行上下文时也能看清改动落在哪里;读不到文件、或这段改动之后又被改过时不补上下文(不编内容)
sectionsOpen 开关 false 新渲染的「编辑 / 写入」操作是否默认展开(开 = 直接看到改动,行内文件块也默认展开;关 = 与思考 / 读取行一致,点一下才展开)。键名是历史遗留,含义已收敛为「这一行要不要默认展开」;只作用于新渲染的行,且只作用于本插件接管的编辑 / 写入行,bash / read 等其它行不受影响。会话页头的「展开 / 收起改动」是另一个入口(只压当前会话、不落盘),两者互不影响
patchTool 开关 false 是否注册 patch 工具(测试版)。关 = 模型只用官方 read / write / edit;开 = 多出本插件的批量补丁工具。动态生效:Host 在 settings 的 onChange 里重新判定,注册 / 反注册即时完成,不必重启 dsh web
# profile cordis.patch.yml → dshp-file-change-viewer 条目 config:(只认这一个 NS;设置页写入即热更,手工编辑重启生效)
dshp-file-change-viewer:
  view: highlight # highlight | diff
  contextLines: 3 # 改动两侧多显示几行上下文:0 | 3 | 5 | 8
  sectionsOpen: false # 新渲染的编辑 / 写入行是否默认展开
  patchTool: false # 是否注册 patch 工具(测试版,默认关)

动态开关改的是「注册」而不是「配置」:patchTool 一开一关,Host 会真的注册 / 反注册这个工具。 判定「现在该不该注册」时问的是 settings 服务的权威值(settings.get(NS),schema 默认 + base 层 + 用户层的合并结果), 而不是 setSource 交付的那个 thunk——真实 settings 在 attach / detach 时也会用「只有 base 层」的源回调一次, 只看 thunk 会在那一刻把工具误反注册(真实会话里表现为模型收到 unknown tool "patch")。 scripts/check-host.mjs 有断言守着这条,并且反向验证过:把逻辑改回旧样子,该断言就会挂。

设置节的读写为什么不走客户端 settingsScope:官方 settingsScope.bind({ namespace }) 也能读写同一分节, 但它要求 Host 已注册命名空间、浏览器侧服务已挂载、写操作按 revision 设栅;任一环时序不对(服务晚挂载、 页面非 loopback)就会静默退化成只读默认值——症状正是「下拉和开关点了没反应、profile 条目 config: 里也没有键值」。 本仓既有插件(mcwiki-search / vision-bridge / token-meter)统一走「自有 /ext 路由 → Host settings.update」: 写的是同一份文件,但少一层时序依赖。设置节与工具行共用同一个客户端 store(src/client/prefs.ts), 读走 state、写走 config;写是乐观的,失败一定回滚并报错,绝不假装保存成功。 scripts/check-host.mjs 守着「NS = cordis 行 id + Config 导出与 configure({ auto: false }) 契约 + 非法补丁不落库」, scripts/check-client.mjs 守着「注册了 settings.section 且 id / order 正确 + 点控件真的 POST 出去 + 失败会回滚」。

几个刻意的内置常量(UI 几何常量,不是部署配置,改完需要重新 build):

常量 位置 值 / 来源 含义
DEFAULT_CONFIG src/host/config.ts 与 schema 默认同值 出厂默认(highlight / contextLines: 3 / sectionsOpen: false / patchTool: false)
HIGHLIGHT_MAX_LINES src/client/FileChangeRow.ts 400 高亮视图单块渲染上限(CodeBlock 没有 maxLines)
DIFF_MAX_LINES src/client/FileChangeRow.ts 200 ± 差异视图交给 DiffBlock 的展开上限
RAW_MAX_CHARS src/client/FileChangeRow.ts 4000 原始参数兜底展示的截断长度
LANG_BY_EXTENSION src/client/lang.ts 26 个官方语法 id 扩展名 → 语法 id;不在表里的扩展名退化成纯文本
--fcv-del-bg / --fcv-add-bg src/client/styles.ts 官方 danger / success 填充 删 / 增行的整行底色(动态规则只引用这两个变量)
图标 primitives FileTypeIcon / IconPlusOutline16 … 文件类型、增删、视图切换各用官方图标;官方图标集没有减号,减号用一图元内联 SVG
TOOL_KEYS / SHADOW_PRIORITY src/client/index.ts edit / write / str_replace_editor,-1 接管哪些工具行、影子化优先级

不建自有配置文件、不碰任何用户数据;唯一的写入是 profile 条目 dshp-file-change-viewer 的 config:(经官方 settings.update,docs/settings.md 与 docs/security.md)。

代码结构

plugins/file-change-viewer/
├── cordis.patch.yml             # bundle 层 patch:只 insert 一行挂载(id = dshp-file-change-viewer)
├── package.json                 # 包描述 + DSH bundle 声明(dsh.bundle.patch / dsh.client.platform)
├── src/host/index.ts            # Host 半:name / inject ['webServer'] / apply(settings 命名空间 + 两条路由 + bundle 入口)
├── src/host/config.ts           # Host 配置:NS / ConfigSchema / 默认值 / 补丁消毒
├── src/host/routes.ts           # /ext 三条路由:state / config(偏好)+ locate(真实行号)
├── src/host/http.ts             # 路由小工具:json / sameOrigin / readBody
├── src/host/patch-tool.ts       # patch 工具:*** Begin Patch → ctx.fs(全有或全无 + 差异元数据 + 删/改名拒绝)
├── src/host/types.ts            # Host 半类型(PluginConfig / PluginConfigPatch / AnyCtx)
├── src/shared/apply-patch.ts    # *** Begin Patch 解析 / 应用 / 行尾(纯函数,Host 与 Client 两份产物共用)
├── src/client/index.ts          # Client 入口:__ModuleLoader__ + 样式注入 + 槽位注册(设置节 + 页头开关 + 4 个工具 key)
├── src/client/FileChangeRow.ts  # 工具行:原生 DisclosureRow + 每文件块折叠 + 三层优先级(块 > 会话 > 偏好)
├── src/client/FileChangeViewerSection.ts # 设置 → 「File Change View」一节(单选卡 + 下拉 + 开关)
├── src/client/viewCards.ts      # 展示方式两张单选卡 + 卡里直接画出来的真实样张
├── src/client/SessionControls.ts # 会话页头:「展开 / 收起改动」+「差异视图」两个快捷开关
├── src/client/session.ts        # 会话级覆盖(内存、不落盘、换会话即失效)
├── src/client/diffView.ts       # 两种差异视图的渲染(工具行与设置页样张共用同一份代码)
├── src/client/prefs.ts          # 偏好:客户端 store(订阅 + 乐观写 + 失败回滚)↔ /ext 路由
├── src/client/api.ts            # /ext 路由封装(fetchState / saveConfig)
├── src/client/locate.ts         # 真实行号 + 上下文:批量问 Host + 缓存 + 订阅(定位不到就回 1 起、不补上下文)
├── src/client/diff.ts           # 变更推导(纯函数:meta.diffs / 半截参数 JSON / LCS 统一 diff / 路径显示)
├── src/client/lang.ts           # 扩展名 → 官方语法 id(镜像 DSH 高亮别名表,26 个语言)
├── src/client/styles.ts         # 样式(前缀 fcv-:官方行内样式 + 差异底色 + 设置节版式 + 页头开关)
├── src/client/types.ts          # Client 半类型(与官方 RunningToolCall / ToolResultNode / CodeBlock 契约对齐)
├── scripts/check-host.mjs       # Host 冒烟测试:Config/configure 契约 + /ext 三条路由 + patch 工具落盘
├── scripts/check-patch.mjs      # patch 纯函数测试:解析 / 应用 / 各种失败姿势
├── scripts/check-client.mjs     # Client 冒烟测试:加载 lib/client.js,断言注册结果与渲染树
└── lib/{host,client}.js         # 单文件构建产物(已提交,DSH git 安装必需)

常见问题

  • 装完了但行没变化:先硬刷新浏览器(client 半是页面脚本);再确认 profile 的 dsh.profile.bundles 里有 @dshp/file-change-viewer;最后看 dsh web 日志有没有 [dshp-file-change-viewer] 的报错行。
  • 编辑行默认是折叠的,我要一眼看到:只想现在这个会话立刻摊开,点会话页头右侧的「展开改动」(不改任何设置,换会话就回到默认);想以后都默认展开,到 设置 → File Change View 打开「编辑 / 写入默认展开」——全局开关只作用于之后新渲染的编辑 / 写入行。
  • 偏好设置在哪:设置 → 左侧导航「File Change View」(本插件自己的一节,分「显示」与「工具」两组:展示方式是两张并排的单选卡、卡里直接画出两种效果,上下文行数是下拉,编辑 / 写入默认展开与启用 patch 工具是开关;改完立即写入 profile 条目 dshp-file-change-viewer 的 config:)。
  • 模型看不到 patch 工具:它是测试版,默认关。到 设置 → File Change View → 工具打开「启用 patch 工具(测试版)」即可;也可以在 composition 的 config: 里写 patchTool: true 让整个部署默认打开。改完不需要重启 dsh web(Host 在 settings 的 onChange 里重新判定并即时注册 / 反注册)。反过来,如果开关是开着的、模型却还是说没有这个工具,先确认 dsh web 起得来(lib/host.js 有没有 patchTool 这个字段:curl -s http://127.0.0.1:3080/ext/dshp-file-change-viewer/state)。
  • 设置里看不到「File Change View」这一节 / 会话页头看不到那两个开关:两者都由客户端半注册(settings.section 与 conversation.session.header.utilities),先硬刷新页面(client 半是页面脚本)。若节在、但一改就显示「未保存」+ 红字,那是 Host 半没挂载:dsh web 没重启时 lib/host.js 还是旧的、没有 /ext 路由(探一下就知道:curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3080/ext/dshp-file-change-viewer/state,404 = 没重启)。改完 Host 半必须重启 dsh web(只改 client 半强刷即可)。三条护栏:scripts/check-host.mjs(NS = 行 id + Config/configure 契约 + 路由真的写库 + 非法补丁不落库)、scripts/check-client.mjs(设置节与会话页头的注册契约 / 点控件真的 POST / 保存失败会回滚 / 页头开关一个 /config 都不发)。
  • 点页头的「展开改动 / 收起改动」之后,我在某一行上手动点的折叠还在吗:页头那一下会先把该会话里所有行与文件块的临时状态作废(这样它才是真正的「一键全改」),之后你在单行 / 单块上的点击照常由你说了算;点右侧的「↺ 恢复跟随偏好」则整个会话回到设置页里的全局默认。
  • 为什么统计不是内置行那种 +7 -7(只改了一行却报 7 行):edit 写进 meta.diffs 的 oldText / newText 是模型这次给的 old_string / new_string 原文——为了让 old_string 在文件里唯一,模型通常会把上下几行一起圈进来。官方 diffTotals 与官方 DiffBlock 都按这份原文算,于是「只改一行」被报成 +7 -7,± 视图里看起来像整段删、整段加。本插件对每个 hunk 跑一次 LCS,然后三个地方统一用它的结果:行头 / 卡头统计、高亮视图、± 差异视图(喂给 DiffBlock 的也是只含变化行的 hunk),所以三处永远说同一件事。想要原文口径的话,切到内置行即可(dsh plugin --profile web remove)。
  • 打开「编辑 / 写入默认展开」后,老的行还是折叠的?:这是有意的——这项偏好只决定「新渲染的编辑 / 写入操作长什么样」,不会回头去改已经渲染出来的行(否则你正在读的 diff 会被开关抽走)。新调用、或列表滚动重挂后采用新值。
  • 同一个会话里为什么有的行展开、有的折叠:行的开合状态跟它「渲染那一刻的偏好 + 之后用户自己的临时点开 / 收起」走,所以改过偏好之后新出现的行与之前的行看起来会不一样。另外,临时覆盖绑定在 callId 上——宿主用 react.memo(ToolCall) 且列表没有显式 key,会在同一位置复用组件实例渲染另一次调用,早期版本因此把上一行「我手动折叠过第 0 块」漏给了新行。
  • 卡片头为什么只有文件名和增删,没有复制:卡头是插件按官方 banner 版式自绘的一行(文件名 + 统计 + 该块控件),官方代码块自带的语言标签栏与复制按钮用 [data-code-block-banner] 隐藏。代价是高亮方式没有复制入口——要复制带 -/+ 的 patch 请把该块切到 ± 差异(DiffBlock 自带复制)。
  • 单块能不能不听全局设置:能。卡头的「高亮 / ± 差异」与折叠箭头都是该块自己的临时状态(不写回偏好,切走即回全局默认);想让某一块默认就是另一种方式,请改全局默认(设置 → 插件)。
  • 某个文件没有高亮:DSH 的高亮器(shiki)只打包了 26 个语言的语法;官方没有 .vue / .svelte / .diff / .txt 这些,本插件对它们传「无语言」,CodeBlock 会渲染成正常颜色的纯文本(不会硬塞一个近似的语言,那只会给你看错误的着色)。语言清单见 src/client/lang.ts。
  • 高亮视图的增删为什么是「一个代码块里交错」:先用 LCS 把每个 hunk 的旧/新文本算成真正的统一 diff(未变行只出现一次),再把这些行原样喂给官方 CodeBlock,并在自己的类作用域内按行区间给删/增行上整行底色。代码一个字符都没改,所以高亮与缩进就是原文的样子。
  • 整行红绿底色是怎么来的:官方 CodeBlock 没有「按行上色」接口,但它在行号模式下把每行渲染成块级 code > .line。本插件按行号区间生成规则(连续同类行并成一个区间选择器,一个 hunk 通常 2–4 条),颜色引用 --fcv-del-bg / --fcv-add-bg(官方 danger / success 填充)。所以行号模式必须开——关掉后 .line 变回行内元素,底色盖不满整行。
  • 卡片上改动前后那几行灰字是什么:是上下文——从文件当前内容里取出的、没受影响的上下各 N 行(默认 3,设置 → File Change View → 「上下文行数」可改 0 / 3 / 5 / 8)。模型给 edit 的 old_string 常常只圈 1 行(它只要保证唯一),补丁片段也只带自己那点上下文,所以这几行是本插件补的,不是模型写的;它们不参与 +A -B 统计,也不是变更内容。文件读不到(比如刚被删了、路径在窗口之外)或这段改动之后又被改过时就不补(宁可没有,也不编内容)。
  • 行号是文件里的真实行号吗:是。官方代码块的行号由 CSS 计数器给出、默认从 1 数起(counter-reset:source-line 挂在 code 上),本插件在这一块自己的类名下标一次(特异性更高):counter-reset:source-line <起始行号 - 1>,于是首行编号就是它在文件里的真实行号;删除行照旧不占号、不推进计数器。起始行号有三个来源,按可靠度取:① 本插件 patch 工具落盘时算出的片段落点(确切:片段自己的位置 + 前面片段造成的行数漂移);② edit / write 的元数据里没有偏移(官方 computeHunkDiffs 只给 {path, oldText, newText}),于是向 Host 的 /ext/dshp-file-change-viewer/locate 问一次——Host 拿文件当前内容定位这段 hunk 的首行,响应里只有整数、不回文件内容,结果按 path + newText 缓存;③ 定位不到(文件之后又被改过)就老老实实从 1 开始,不编一个错的数字。行头的路径链接也会带上这个行号,点开直接落在改动处。
  • 上游改版会不会把样式改坏:两处依赖官方的 DOM 约定——行底色用 code > .line(未哈希固定类名)、去外壳用 [data-code-block-banner] 与 pre。任一处改名/改结构,后果都只是装饰性降级(底色或"无外壳"失效,代码、行号、语法高亮照常),± 差异 视图完全不依赖它们。行内样式里那几条 ToolRow 专属样式(分隔点、路径链接、统计、Inspect 药丸)是逐条镜像的,升级后对照上游同名文件同步即可。
  • 同一行出现两份 / 互相顶替:这个会话里可能还有一份动态注册的同名插件(cordis_define 定义的进程内扩展,@ 引用能列出)。两者都用 priority: -1 抢同一个 key,同 key 同优先级会直接抛错,日志里会出现 keyed slot "tool.call.toolview" already has an entry for key "edit" … register at a different priority to shadow it,其中一份注册被跳过(页面不会坏)。装标准包后请把动态版 cordis_undefine 掉。
  • 改了设置节的样式,结果「编辑 / 写入」行也被改了(改代码的人必读):styles.ts 里的规则是全局作用域,而工具行元素上的类名是 FileChangeRow.ts 用字符串传下去的(className: 'fcv-rowRoot' / rowClassName: 'fcv-toolRow' / 卡头 'fcv-cardRow')。两套版式一旦类名重名,设置节的布局就会打到工具行上——曾经设置节的 .fcv-row{padding:16px 0} 把编辑 / 写入行撑高 32px 还加了一条底边线。约定:工具行用 fcv-toolRow / fcv-rowRoot / fcv-card*,设置节用 fcv-setting* / fcv-section* / fcv-page;scripts/check-client.mjs 有一条断言扫「工具行整棵子树的类名 ∩ 设置节版式类名 = ∅」。
  • 为什么要 priority: -1(改代码的人必读):keyed 槽位派发是「按 priority 升序取每个 key 的第一条」,官方内置行占着 0,所以接管必须用比 0 更小的值;同 key 同 priority 不是覆盖而是抛错。这条有断言守着。
  • 只有 edit / write 变了,bash 里的文件改动没变:本插件只接管这两个(以及兼容项 str_replace_editor)。用 shell 重定向改文件属于 bash 行,不在范围内。读取(read)/ read_image / 其它工具行完全没被注册、也没被改样式(可用 Slots.listSubTree 查 tool.call.toolview 的占用者核对)。
  • 某个工具的行突然显示「原始参数」/「参数不可用」:说明这次调用的 meta.diffs 为空且参数推导不出变更(例如窗口截断把调用头切掉了)。这是兜底路径:能解析成对象就交给官方 JsonBlock,否则原样打印并截断。
  • 想回到内置行:dsh plugin --profile web remove "@dshp/file-change-viewer" + dsh web;偏好存在 profile 条目 dshp-file-change-viewer 的 config:,删掉该分节即回默认值。

卸载

dsh plugin --profile web remove "@dshp/file-change-viewer"
dsh web

remove 会自动从 dsh.profile.bundles 撤下挂载(monorepo 本体不用删)。本插件不建自有文件、不缓存任何数据;唯一的落盘是 profile 条目 dshp-file-change-viewer 的 config:,卸载后该分节可自行删除。

移植说明

动态版 → Monorepo + TS 标准包:本目录是 deepseek-harness-plugins monorepo 的标准子项目(plugins/file-change-viewer),由一次动态会话里用 cordis_define 创建的 client-only package(filech-1/pkg-1)等价重写而来。

  • Host:动态版没有 Host 半(只有 client);标准包必须有一条 Host Loader 条目(cordis.patch.yml 的 insert + lib/host.js),因为 DSH 是从 Host Loader 的条目里扫描 dsh.client 声明来发现客户端 bundle 的。因此 src/host/index.ts 是有意的极简:name / inject: [] / 一行就绪日志。
  • Client:动态版的单文件 JS → src/client/{types,diff,styles,FileChangeRow,index}.ts,tsup 打包为单文件 lib/client.js(IIFE,内含 __ModuleLoader__.load,react/primitives 运行时注入不打包)。
  • 五处实质性升级:
    1. diff 渲染从「手写 LCS 行级 diff + 自绘 DOM」换成官方组件:高亮方式把 LCS 统一 diff(未变行只出现一次、增删交错、代码原样)交给官方 CodeBlock,再借行号模式的块级 .line 按行区间叠整行红/绿底色并让删除行不占行号;可切到 DiffBlock 的逐行 ± 视图;
    2. 颜色从写死的 rgba(248,81,73,.10) 换成官方 token(--dsw-alias-* / --shiki-token-*);
    3. 必须显式传 priority: -1:动态注册走 Guard 的 allocatePriority(),静态插件没有这层保护,用默认优先级注册会直接抛错(症状是「装上了但界面毫无变化」);
    4. 行从「自造卡片」收敛成原生行:外壳改用官方 DisclosureRow 默认样式,工具专属样式逐条镜像;只在展开体里放差异,并把代码块的语言栏/复制/外壳也去掉,只留纯高亮代码;
    5. 新增全局偏好(展示方式下拉 + 编辑 / 写入是否默认展开),注册成设置里独立的一节(settings.section),读写走自有 /ext 路由 → Host settings.update,落 settings.yaml 的 NS 分节;不再有行内切换按钮,也不占用「插件」tab。默认展开只影响新渲染的行。
  • 新增验证:动态版只能靠肉眼看;标准包把它变成可执行断言——scripts/check-client.mjs 直接加载构建产物,用 stub React/primitives(useState 带状态,所以能「点一下再渲染」)跑组件函数并遍历元素树,覆盖 loader 契约、三个工具 key 与设置节的注册(id / order)、影子优先级、设置节控件的读写往返(含读回 Host 权威值、保存失败回滚 + 报错)、「偏好只影响新渲染的行」、换 callId 后临时覆盖不泄漏、类名撞车护卫(工具行整棵子树的类名 ∩ 设置节版式 = ∅)、行底色的语义统计(真实案例:一次单行替换必须是 +1 -1 而不是原文的 +7 -7)、区间选择器(含删行不占号)、去代码块外壳的样式、以及 meta.diffs / 半截 JSON / 失败 / 截断四条推导路径。

构建:pnpm --filter @dshp/file-change-viewer build(tsup)→ lib/host.js + lib/client.js;包入口 lib/host.js,./client → lib/client.js。lib/ 已提交(DSH add github: 直接从 git 安装,不跑 build,必须带构建产物)。

免责声明

  • 本插件只改变工具调用在界面上的渲染方式,不修改任何文件内容、不干预模型行为;文件改动的正确性仍由官方 edit / write 工具与你的沙箱策略负责。
  • 不联网、不上传;唯一的落盘是 profile 条目 dshp-file-change-viewer 的 config:(经官方 settings 服务写入)。
  • 它接管了官方内置卡片的渲染。如果 DSH 上游改了 DiffBlock / DisclosureRow / CodeBlock 的 props 契约、或高亮别名表(lang.ts 镜像的那张),卡片可能渲染异常或高亮失效;如果改了代码块 code > .line 的结构,删/增行的整行底色会消失(其余照常)。以上情况卸载本插件即恢复内置卡片。

Content from the project README on GitHub ↗

Comments

Comments live in GitHub Discussions. Sign in with GitHub to post or react.