dsh-plugin-edit-files
给 DSH(DeepSeek Harness)Web GUI 加两个能力的插件:在右侧文件预览里直接编辑保存,以及在侧栏标签页里看清本地工作区与上游远程到底差在哪。
这个插件只做这两件事,不碰你的项目配置,也不是 IDE 的替代品。
能力
一、右侧预览直接编辑
- 右侧 Sidebar 的文档预览目前只能看;启用插件后,预览工具栏多出「编辑」入口,在同一个窗口里直接改文件,不用切回编辑器。
- ⌘S / Ctrl+S 保存,Esc 关闭;有未保存修改时先弹确认,不会手滑丢掉内容。
- 保存前核对文件是否被外部改过:带着上次读到的
mtime提交,磁盘上的文件已经变了就报冲突,由你选「仍然覆盖保存」还是「载入磁盘版本」,不会静默覆盖别人的修改。 - 写盘保留原文件的换行符(LF / CRLF)与 BOM;用「同目录临时文件 + rename」原子替换,尽量不出现写了一半的文件。
- 二进制文件、超过大小上限的文件、非普通文件会被明确拒绝并提示,不会把乱码塞进编辑器。
二、Git 改动(只列真正不同的文件)
- 只在 git 仓库里出现:从当前目录向上找仓库根;不是仓库时,客户端不渲染任何 git 入口。
- 五个入口打开同一个「Git 改动」右栏标签页:
- 工作区分组标题的「⋯」菜单:多出第一行「Git 改动」(行尾直接显示改动文件数)——就是侧栏里
dsh-plugin-edit-files ▾ ⋯那颗按钮的菜单; - 会话行右侧 hover 出来的 git 图标(有改动时带数字);
- 会话「⋯」菜单里的「Git 改动」;
- 会话头部(Session header)的 git 徽标(有改动时显示数字);
- 右侧文件树工具栏的 git 按钮;文档预览工具栏的 git 标识只在当前文件确实与远程不同时亮起,并显示 M/A/D 状态字母。
- 工作区分组标题的「⋯」菜单:多出第一行「Git 改动」(行尾直接显示改动文件数)——就是侧栏里
- 说明:DSH 这一版「工作区分组 ⋯ 菜单」的两项(重命名 / 删除工作区)是 ui-workspace 里写死的,没有给插件留 slot。工作区菜单里的那一行是插件在菜单打开时把一行克隆进它的 DOM 得到的(克隆原生行 → 样式与键盘走查跟原生一致;只在该工作区确是 git 仓库时才插)。实现包在 try/catch 里,DSH 改版导致结构对不上时最多是这个增强失效,不会影响其它入口。
- 对比基准是上游远程分支(例如
origin/main),不是简单的git status:本地已提交但还没推送的改动、未提交的暂存/工作区修改、未跟踪的新文件,全都统一拿来和上游比。改回与远程一致的文件会自动从列表消失——这正是需求。 - 列表只显示真正不一样的文件,带状态字母(A/M/D/R/C/T/U)和增删行数;点某个文件看 unified diff,未跟踪的新文件直接显示全文。
- 顶部顺带显示分支、相对上游的领先 / 落后提交数;没有上游时依次退回
origin/HEAD、HEAD(此时会写明「对比 HEAD」);空仓库把所有文件当未跟踪列出。 - 列表为空时显示「与远程一致,没有本地改动」。
三、文件面板(当前项目的所有文件)
会话顶部页签条多一个 「文件」页签(就在「对话 / 轨迹」旁边),点开是当前项目(会话所在工作区)的完整文件面板:
- 浏览:默认递归列出工作区里的文件与目录;点目录进入、面包屑回退;默认跳过
node_modules/dist/.git这类重目录,工具栏可切「显示忽略目录」(.git永远不列)。 - 搜索:模糊匹配(子序列),输入
mnts也能找到main.ts;大小写无关,基名命中的排前面。 - 排序:名称 / 修改时间 / 大小,升序或降序;目录始终排在文件前面。
- 操作:点文件直接在编辑器里打开(可改可存);「新建文件 / 新建文件夹」建在当前目录里;每行 hover 出「重命名 / 删除」,删除目录需要显式勾选递归确认。
- Git 同步:每行右侧显示该文件相对远程的状态字母(M/A/D/R…),顶部显示「N 个文件有改动」;面板在前台时每 10 秒静默刷新,增删改与保存后立刻重读文件树和 git 状态,所以「文件变了,git 也跟着变」。
- 多个工作区时,顶部是「文件夹图标 + 工作区名 + 下拉箭头」的选择器(和首页那个工作区选择器同一套样式):选中项在下拉里高亮打勾;目录已被移动/删除的工作区会标「目录不存在」。选择会记住(localStorage)。
- 选工作区 = 切工作区:选中一个正常的工作区后,会同时切到该工作区最近的一个会话 —— 左侧边栏会高亮并滚动到那个会话,主区切到对应的「对话」;文件面板的根目录也换成该工作区(下次打开文件面板还是它)。
- 选中目录已被移动/删除的工作区时不切会话:面板会把选中的那个工作区(图标 + 名称 + 完整路径)摆出来,说明「文件夹不存在,可能已被移动或删除」,并给「重新检测 / 换个工作区」两个出口,而不是只丢一句「文件不存在」。
- 页签只在选中时渲染;切回「对话」页签不影响会话,文件面板的滚动/搜索状态在切走时会重置(每次都从工作区根目录开始)。
- 首次安装 / 升级 Host 代码后需要重启一次 DSH(理由见「安装」一节);面板检测到旧 Host 时会直接显示「重启 DSH」按钮。
安装
前置条件:DSH Desktop 或 dsh web(带 Web GUI 的 profile);Node.js ≥ 22.13.0(package.json 的 engines 要求);要用 Git 改动功能的话,本机要有 git。插件没有 npm 依赖,不需要 npm install。
方式一:从本机目录安装(推荐)
在 DSH 里让 agent 用插件管理器安装,target 填本仓库的绝对路径:
plugin_manager({ action: "install_bundle", target: "/绝对路径/dsh-plugin-edit-files" })
安装会把包装进当前 profile 并登记 bundle,插件列表里出现 dsh-plugin-edit-files;如果显示为关闭,打开开关即可。
装完请重启一次 DSH Desktop(菜单里退出再打开即可,会话记录会保留):Host 侧插件模块在进程里是缓存的,升级/新增 Host 路由后,运行中的进程仍会跑旧模块;客户端代码会热更新,但 Host 新路由不会。重启一次性解决客户端与 Host 两边。
如果暂时不想重启:打开左栏「文件」面板时,如果检测到 Host 路由还是旧的(404),面板会直接给出「重启 DSH」按钮,点一下就会走 Desktop 自己的有序重启,会话记录同样保留。
方式二:从插件市场安装
如果本插件被收录进某个插件市场索引(DSH Desktop 的「设置 → 插件市场」里可选 dsh-market 或 dsh-community-market),在「插件 → 发现」里搜索 dsh-plugin-edit-files 即可安装。本仓库不附带市场索引;没被收录时请用方式一。
用法
在预览里编辑文件
- 在右侧 Sidebar 打开一个文本文件的预览(从工作区或会话里的文件引用点开都行)。
- 点预览工具栏上的「编辑」按钮(铅笔图标):会在预览上方打开编辑浮层,改完按 ⌘S(Windows / Linux 用 Ctrl+S)保存。
- 保存成功显示「已保存」;「还原」回到打开时的内容,「自动换行」切换折行。保存后 Git 徽标与「Git 改动」列表会立即刷新。
- 如果文件在你编辑期间被外部程序改动,保存时不会被静默覆盖:浮层底部提示「磁盘上的文件已被改动」,可选「仍然覆盖保存」或「载入磁盘版本」(丢弃本地草稿)。
- 打开二进制文件或超过上限的文件、以及文件已不存在时,会给出明确提示,不进入编辑。
查看 Git 改动
- 在 git 仓库的工作目录里开会话。目录不是仓库时,菜单项和徽标都不会出现。
- 从会话「⋯」菜单选「Git 改动」,或点会话头部的 git 徽标,右侧会打开「Git 改动」标签页。
- 顶部显示分支、领先 / 落后提交数和对比基准(如「对比 origin/main」);下面是只含真正不同文件的列表:状态字母 + 文件名 + 增删行数,二进制文件标
bin。 - 点一个文件看 unified diff;面板开着时每 6 秒自动刷新,也可以手动刷新。
- 把文件改回与远程一致,它会自动从列表里消失;全部一致时显示「与远程一致,没有本地改动」。如果会话还没有工作区路径,页面会提示「这个会话还没有工作区路径」。
在文件面板里管理文件
- 点会话顶部页签条里的「文件」页签(「对话 / 轨迹」旁边);面板默认打开当前会话所在工作区(多工作区时顶部可切换)。
- 搜索框输入片段做模糊匹配;右侧下拉切排序方式,箭头切升降序;「只看改动」只列相对远程有变化的文件。
- 点文件 → 编辑器浮层打开(⌘S 保存);点目录 → 进入;建文件 / 文件夹 / 重命名 / 删除都在当前目录里进行。
- 增删改与保存后文件树和 git 状态会自动重读;面板在前台时每 10 秒也会静默刷新一次。
- 升级插件后如果面板显示「需要重启一次 DSH Desktop」,点面板里的「重启 DSH」按钮即可(Host 新路由要重启进程才加载;正常安装包升级时插件管理器会处理)。
Host 路由(/api/edit-files/*)
所有响应都是 JSON(cache-control: no-store);失败统一返回 { ok: false, reason, error? } + 合适的 HTTP 状态码,不会把异常抛到路由之外。GET 从 query 取参数,POST 从 JSON body 取参数。
| 方法 | 路径 | 作用 |
|---|---|---|
| GET | /api/edit-files/health |
自检:插件名、版本、已注册路由清单与当前限额 |
| GET | /api/edit-files/git/info |
判断路径是否在 git 仓库里,回仓库根、分支、上游、ahead/behind(?path=) |
| GET | /api/edit-files/git/changes |
本地工作区 vs 远程基准,只列真正不同的文件与增删行数(?path=) |
| GET | /api/edit-files/git/diff |
单个文件的 unified diff;未跟踪文件回 newText 全文(?path=&file=) |
| GET | /api/edit-files/git/file |
单个文件是否与远程不同,供预览工具栏 / 会话徽标判断(?path=) |
| POST | /api/edit-files/file/read |
读 UTF-8 文本:内容、大小、mtime、换行符、BOM、权限位 |
| POST | /api/edit-files/file/write |
写文件:乐观并发检查、原子写、保留 EOL / BOM / 权限位 |
| GET | /api/edit-files/files/tree |
递归列出一个目录下的文件与目录(可含忽略目录),带大小与修改时间(?root=&includeIgnored=&limit=) |
| POST | /api/edit-files/files/create |
新建文件或目录({root, rel, kind, content?}) |
| POST | /api/edit-files/files/rename |
重命名 / 移动到工作区内的另一个路径({root, from, to}) |
| POST | /api/edit-files/files/delete |
删除文件;目录需 recursive:true({root, rel, recursive?}) |
参数、状态码、基准选择规则与返回字段以契约文档为准:docs/route-contract.md。
配置
三个可调项都在 bundle patch 的 config 里,全部可选:
| 字段 | 默认 | 说明 |
|---|---|---|
maxFileBytes |
2097152(2 MiB) |
单个文件读取 / 保存上限(字节),超过就拒绝读写 |
maxDiffLines |
4000 |
单个文件 diff 最多回传行数,超出截断并标 truncated |
maxChangedFiles |
3000 |
一次改动列表最多回传条数,超出截断并标 truncated |
maxTreeEntries |
20000 |
文件面板一次最多列多少条(目录 + 文件) |
treeIgnores |
见 lib/config.js |
文件面板默认跳过的目录名(可切「显示忽略目录」看到) |
改的是 profile 里这个 bundle 的 patch 配置(或安装前改仓库里的 cordis.patch.yml)。cordis.patch.example.yml 只是把可调项抄出来给人看的参考,改它不会生效。
已知限制
- 只支持 UTF-8 文本:二进制文件(前 8 KiB 出现
\0)直接拒绝,没有十六进制编辑。 - 单文件默认 ≤ 2 MiB:超过上限不读也不写;可以调大
maxFileBytes,但别指望用它看大日志。 - 不是 IDE:没有语法高亮、补全、LSP、多光标;改大文件请用真正的编辑器。
- 不自动合并冲突:保存时发现外部改动只会提示,由你选覆盖或载入磁盘版本。
- 依赖本机
git:git不可用时 Host 返回git-unavailable,Git 入口不可用,面板提示「这台机器上找不到 git 命令」。 - 大仓库会截断:文件数超过
maxChangedFiles、diff 超过maxDiffLines、文件面板超过maxTreeEntries时只回前 N 条 / 行,并在响应里标truncated。 - 删除不是回收站:文件面板的删除是直接删(目录要勾递归确认),删之前请确认 git 里能找回。
- 基准是远程:没有上游时退回
origin/HEAD或HEAD;此时只能看到相对该基准的差异,建议先git push -u设好上游。 - 二进制文件只在列表可见(标
binary: true),diff 不展开。
开发与自检
仓库结构:
index.js Host 入口(export name / inject / apply)
lib/ Host 实现(config / git / files / router)
client.js Web GUI 客户端(预览编辑浮层 + Git 改动标签页)
cordis.patch.yml bundle patch(插入插件行与默认配置)
cordis.patch.example.yml 可调配置的参考副本(不生效)
docs/route-contract.md Host 与 Client 的接口契约
locale/ zh.json / en.json(插件管理器里显示的标题与描述)
icon.svg 插件图标
test/ host-selftest.mjs / client-selftest.mjs
自检(不需要额外依赖,但需要本机 git):
node --check index.js
node --check client.js
node test/host-selftest.mjs
node test/client-selftest.mjs
host-selftest.mjs 在 os.tmpdir() 下建临时的 bare 远程 + clone 仓库,把契约语义跑一遍:干净仓库不列文件、改回一致自动消失、未跟踪 / 删除 / 重命名、diff、读写往返、冲突、CRLF / BOM 保留、二进制与超限拒绝等,结束后清理临时目录。client-selftest.mjs 离线加载 client.js,检查 slot 注册、locale 字典完整性和几个纯函数。仓库没有构建步骤,client.js 直接由 DSH 页面加载。
插件自带自检入口(排查「按钮没出现」)
Host 与 Client 各注册了一个只读的 cordis_inspect 提供者 EditFiles,agent 可以直接问:
cordis_inspect_query({ platform: "client", provider: "EditFiles", method: "probe" }) // Host 路由可达性 + git 结果 + 渲染异常
cordis_inspect_query({ platform: "client", provider: "EditFiles", method: "dom" }) // 页面上实际渲染出的控件/工作区索引/菜单增强日志
cordis_inspect_query({ platform: "client", provider: "EditFiles", method: "menuSelfTest" }) // 程序化点开工作区「⋯」菜单,确认 Git 行被插入
cordis_inspect_query({ platform: "client", provider: "EditFiles", method: "editorSelfTest", input: { path: "/abs/file.md" } })
cordis_inspect_query({ platform: "client", provider: "EditFiles", method: "openTabSelfTest", input: { path: "/abs/repo" } })
cordis_inspect_query({ platform: "client", provider: "EditFiles", method: "filesSelfTest", input: { root: "/tmp/某个可写目录" } })
cordis_inspect_query({ platform: "client", provider: "EditFiles", method: "panelSelfTest", input: { openFile: true } })
cordis_inspect_query({ platform: "host", provider: "EditFiles", method: "status" }) // 重启后可用:路由注册结果 + 浏览器调用计数
除 status 外都跑在页面里(Client 平台);menuSelfTest / openTabSelfTest / editorSelfTest / panelSelfTest 会短暂操作 UI 后自己恢复原状,filesSelfTest 只在传入的临时目录里建删自己的东西。
许可证
MIT。图标 icon.svg 为本仓库原创,随仓库以 MIT 许可发布。
No comments yet. Be the first to write one.