DeepSeek Harness 插件

dsh-ui-gitworkbench

Out-of-tree dsh web UI plugin: a session-header git workbench chip opening a drawer with the file tree, per-file diff, history, compare, staging, commit, and sync (fetch/pull/push).(英文原文)

跳到安装方式

来源信息

GitHub 仓库
young1lin/dsh-ui-gitworkbench
最近更新
2026年8月20日
分类
工具与能力
GitHub stars
1
载体类型
plugin
目录证据
上游声明已找到 dsh.bundle
证据路径
package.json#dsh.bundle
核对版本
0.1.0-rc.8
上游核对日期
2026-08-20

该证据由上游目录提供。本站没有安装、运行或安全审核这个插件。

安装

默认先复制一段 Prompt,让 Agent 读 GitHub 仓库和源码;需要自己装时再切到命令。

复制这段 Prompt,发给 DSH、Codex 或其他 Agent,让它先读 GitHub 仓库和源码。

请先不要安装或执行任何命令。阅读这个插件的 GitHub 仓库、README 和关键源码,然后用清楚、直接的方式回答以下问题,帮助我判断它是否适合我的需求:

1. 这个插件是什么,解决什么问题;
2. 适合哪些用户和典型使用场景;
3. 安装后如何使用,并给出一个最小使用示例;
4. 有哪些已知限制,以及隐私、安全、兼容性或维护风险;
5. 给出“推荐 / 有条件推荐 / 不推荐”的明确建议和理由。

请区分仓库明确说明、根据源码推断和未知信息。证据不足时请明确说明,不要猜测或照抄 README。

GitHub:https://github.com/young1lin/dsh-ui-gitworkbench
插件名:dsh-ui-gitworkbench
作者:young1lin

检查来源文件

安装前先看这个插件目录里的 README 和其他文件。

文件资源管理器4 个文件
README.md来源说明 · 只读预览
README 语言

@young1lin/dsh-ui-gitworkbench

🌏 中文 · English

dsh(DeepSeek Harness) 的树外 Web UI 插件:给 dsh 的 Web 界面装一个 Git 工作台,不改动 dsh 本体。

每个会话的头部都有一枚状态卡,显示当前分支、领先/落后和增删计数。点开它,右侧滑出一张工作台面板,当前 worktree 的改动一览无余:

  • 变更:可折叠的文件树,配逐文件 diff——双列行号、词级高亮、Shiki 语法着色;树顶可打关键字过滤文件列表(多词与关系、智能大小写),文件行悬浮可一键撤回到上次提交(IDEA 的 Rollback,弹窗先说清后果);
  • 历史:提交列表 / 文件树 / diff 三栏并排,滚动到底自动翻页;行内带作者、悬浮卡带精确时间;IDEA 式过滤(user: / path: / after: 输入语法,或作者 / 日期 / 路径分区漏斗弹层),条件编译进 git log、全历史匹配、车道图常驻,另有「全部分支」;
  • 对比:任选两个分支互相比较;
  • 提交与同步:树上勾选文件就是真实的 git add / git restore --staged,配合提交框和 fetch / pull / push 同步条,一次提交加推送全程不用离开面板;
  • 外观:七套主题族各带亮暗,默认跟随系统;支持虚化背景图和自定义 CSS,按「项目 / 全局」两个作用域保存,项目优先。

另带 worktree 仿真:模型在会话里调用 worktree_enter / worktree_exit / worktree_status 三个工具,即可在 .agents/worktrees/<name> 下建立或退出隔离 worktree,并把会话绑定过去。绑定后状态卡点亮绑定标记,面板头部出现 worktree 切换器(按分支列出仓库全部 worktree),统计随之切换。

<div align="center"> <video src="https://github.com/user-attachments/assets/c6a73c7b-bf69-4b97-80a2-9175bc293d7d" muted autoplay loop playsinline controls width="100%"></video> <sub>演示(2 分 24 秒):状态卡 → 变更页(勾选暂存 / 逐文件 diff / 提交)→ 外观(明暗 + 七套配色 + 背景图)→ 历史页(提交图 / worktree 切换)</sub> </div>

> 这份 README 同时是交接文档:插件是什么、怎么写的、踩过哪些坑、怎么继续改,全部记录在案。接手开发前请先读「§6 踩坑实录」——那里是真实调试换来的关键事实。

0. 安装

前置:DSH 已装好(dsh web 能正常运行),Node.js ≥ 20,pnpm ≥ 10。

推荐:官方插件通道,一条命令。

dsh plugin --profile web add @young1lin/dsh-ui-gitworkbench

装完重启 DSH,再硬刷新浏览器(Ctrl/Cmd + Shift + R)。包内声明了 dsh.bundle.patch,CLI 会自动把宿主半注册进 profile 的 dsh.profile.bundles,下次启动即挂载,不需要手写任何 cordis.patch.yml 挂载行。机器上没有 dsh 命令时,用 npx 直接跑:

npx -y --package @deepseek-ai/dsh dsh plugin --profile web add @young1lin/dsh-ui-gitworkbench

<details> <summary><b>备选:一键脚本</b>(同样走官方通道,多处理两件小事)</summary>

# macOS / Linux(Windows 装了 Git Bash 或 WSL 也可)
curl -fsSL https://raw.githubusercontent.com/young1lin/dsh-ui-gitworkbench/main/scripts/install.sh | bash
# Windows(PowerShell 5.1+ / pwsh)
irm https://raw.githubusercontent.com/young1lin/dsh-ui-gitworkbench/main/scripts/install.ps1 | iex

脚本在安装命令之外多做两件事:预写 pnpm 11 的 minimumReleaseAgeExclude,让刚发布不足 24 小时的版本也能立即安装;幂等清理旧版手动挂载行,避免宿主半挂载两次(页面上出现两个状态卡)。支持指定版本、装完 pm2 restart dsh-web--dry-run 试跑等参数,见脚本头部注释。

</details>

<details> <summary><b>从源码开发</b></summary>

dsh plugin --profile web add <本仓库路径> 把源码装进 profile;改完客户端半跑 npx tsdown 再刷新浏览器即可生效(宿主半改动需重启 dsh web)。详见 §5。从 link: 源码依赖切回 npm 版时,记得移除 cordis.patch.yml 里的手动挂载行(安装脚本会自动处理)。

</details>

发布(维护者)

首次发布与后续发布走不同链路:

  • 首次(包还不存在于 npm,Trusted Publishing 尚无处配置):本机 npm loginnpm publish(scope 包的 publishConfig.access 已设 public)。发布后到 npmjs.com → 包 Settings → Trusted publishing 添加 GitHub Actions 发布器:user young1lin、repository dsh-ui-gitworkbench、workflow 填 publish.yml(不带路径前缀)、Environment 留空、勾选允许 npm publish。手工发布不经 CI 里那道机器路径门禁(见 publish.yml 的 grep 步骤),发布前可自行扫一眼 lib/*.js 确认没有本机绝对路径混入。
  • 后续npm version patch(或 minor/major)→ git pushgit push --tags。tag vX.Y.Z 触发 .github/workflows/publish.yml:CI 全量检查 → tag 与 package.json 版本一致性校验 → OIDC Trusted Publishing 自动 npm publish(provenance 自动生成,全程无 npm token)。不要手动补推已由人工发布过的版本的 tag(如首次的 v0.1.0),registry 会拒绝同版本重发。

发布产物不带 sourcemap。 lib/client.js.map 解包 3.1MB、gzip 416kB,占了整包下载的 46%;排掉后 tarball 从 914.6kB 降到 498.0kB。两处配合才干净:prepackbundle:publishtsdown --no-sourcemap,连 //# sourceMappingURL 注释一并不产出——只删文件不删注释的话,dsh 的 /plugins/<id>/client.js.map 路由会给每个使用者一个 404),files 里的 !lib/*.map 再兜一道,防止上一次 dev 构建遗留的 map 被 clean: false 留在 lib/ 里蹭进包。

副作用记一笔npm publishnpm pack(含 --dry-run)都会触发 prepack,所以跑完之后本机 lib/client.js 是不带 sourcemap 注释的那份,浏览器里断点看到的是打包后的代码。继续开发前跑一次 pnpm exec tsdown 就回来了。

---

1. 当前状态(已验证)

能力状态验证方式
宿主 gitWorkbench/stats RPC 返回真实统计curl -X POST /api/gitWorkbench/stats 返回 {ok:true, value:{branch, files[], diff}}
客户端 bundle 被 shell 加载(boot 清单)window.__DSH_BOOT__.entries@young1lin/dsh-ui-gitworkbench
浏览器→宿主 RPC 通页面内 fetch('/api/gitWorkbench/stats', ...) 返回 200
面板 diff 完整(不丢文件)换用 subprocess pipe 后,diff --git 计数 = 文件数
状态卡在 git 仓库会话常驻显示(分支/↑↓/计数),仅非 git 目录或 git 失败时隐藏干净树也显示分支名(状态卡即会话的环境信息位);绑定徽标见 §9
agent 工具 worktree_enter/exit/status(模型可调)真实会话冒烟 scripts/llm_smoke.py:模型调 enter → .agents/worktrees/llm-smoke 出现;exit(remove) → 消失
宿主 worktree RPC(enter/exit/status/sessionWorktree)+ 绑定文件python scripts/probe_worktree.py:scratch 仓库断言 + 真仓库冒烟 + 再进入分支复用,ALL PASS
状态卡绑定标记(树形图标;徽标文字与分支重名时省略)+ 头部 worktree 选择器python scripts/verify_worktree_ui.py:6 步 UI 探针(绑定标记、头部路径、选择器切换、折叠/选中回归)
历史过滤(作者 / 日期 / 路径下推 git log、「全部分支」、日历与三态路径树)python scripts/verify_history_feature.py:11 步 UI + host 探针全过(中文作者、All-branches、日历选界、目录吸收文件勾选、诚实空态)
单文件撤回(Rollback)与文件列表关键字过滤对 live app 实测:撤回弹窗措辞随 host 实时推导的后果变化、取消不动手、执行后 fixture 回静息态;过滤框多词 AND、忽略折叠、根勾选只动可见行
客户端半被类型检查tsconfig.client.json 进了 bundle/typecheck;曾故意写坏一处,确认报 TS2322
主题 7 族 × 亮暗 + 跟随系统明暗tests/theme-palettes.test.tsthemes.ts.module.css 互扣(两个方向都验过会红);lib/client.js 含全部 14 套调色板
背景图 / 自定义 CSS 的项目+全局存储构建产物 lib/index.js 跑 styleGet/styleSet 全流程(临时 HOME,18/18 PASS):读写、项目优先、越界钳制、恶意 image 拒绝、清空删记录、非仓库拒绝、两作用域并发写不互相覆盖

已知边界:状态卡挂在 conversation.session.header.actions 插槽,只有打开了会话(会话头渲染)时才挂载。无头自动化里若没真正打开会话,状态卡不会出现——这是预期行为,手动在 UI 里开一个会话即可看到。

---

2. 架构(一句话 + 详情)

> 宿主半:一个 TypertRemoteService,跑 git 算统计 + worktree 增删与「会话→worktree」绑定,经 Typert gateway 自动发现;同一服务再以 defineTool 注册三个 agent 工具。客户端半:一个 React 面板,注册进会话头插槽,通过 connection.rpc 向宿主要数据(统计 + 会话绑定)。

2.1 宿主半(src/index.ts

class GitWorkbenchService extends TypertRemoteService {
  static inject = ['subprocess']          // 等 subprocess 服务就绪才激活
  constructor(ctx) { super(ctx, 'gitWorkbench') }   // 注册为 ctx.gitWorkbench,命名空间 = 'gitWorkbench'
  @Remote('stats')                        // endpoint = gitWorkbench/stats
  async stats(worktreePath, signal) { ... 用 ctx.subprocess.spawn 跑 git ... }
}
export default GitWorkbenchService
  • Typert gateway 通过"源码标记反射"自动发现这个方法(读 @Remote 装饰器在原型上打的 marker)——不需要生成 descriptor、不需要改 monorepo 任何文件。这是树外插件最干净的 RPC 暴露方式。
  • 浏览器侧调用:ctx.connection.rpc.call('/api', 'gitWorkbench/stats', { args: { worktreePath } }, signal) → 返回 {ok, value} | {ok:false, error}
  • 取数用 ctx.subprocess.spawn({argv:['git',...], cwd, stdio:{stdout:'pipe'}}),自己累加 stdout 流。见踩坑 §6.3。

2.2 客户端半(src/client/

// src/client/index.ts
export const inject = ['sessions', 'slots', 'connection']
export function apply(ctx) {
  const connection = ctx.connection
  ctx.slots.inject('conversation.session.header.actions', () => ctx.slots.register(
    { name: 'conversation.session.header.actions', id: 'git-workbench', order: 30,
      inject: () => ({ fetchStats: async (worktreePath, signal) => {
        const r = await connection.rpc.call('/api', 'gitWorkbench/stats', worktreePath ? {args:{worktreePath}} : {args:{}}, signal)
        return r.ok ? r.value : null
      }}) },
    GitWorkbenchPanel,
  ))
}
  • 插槽系统ctx.slots.inject(key, cb) 会在 key 插槽被声明后执行 cbcbctx.slots.register({name,id,order,inject}, Component) 注册组件。可复用已有插槽(如本插件的 conversation.session.header.actions),也可用 declare module '@deepseek-ai/dsh-client-ui-slots' 声明合并新增插槽。
  • 组件 propsPropsRuntime<'conversation.session.header.actions'> 提供 sessionIduseSessions 等;inject 工厂返回的对象(如 fetchStats)会作为 props 注入组件。业务回调从 apply 作用域经 inject 工厂过到组件,绝不用全局 ctx
  • 拿 worktree 路径useSessions(state => state.byId[sessionId]?.cwd)——会话摘要自带 cwd
  • 数据刷新:挂载时拉一次 + 面板打开时轮询(空闲 15s、agent 运行中加密到 3s——运行中的会话正在改文件,等满 15s 看到的就是旧闻)+ 手动刷新按钮。面板关着时另有一条便宜的绑定探针(见 §6.0d):只在 agent 运行中开表,走不 spawn git 的 sessionWorktree,发现绑定变了才补一次 worktreeStatus

2.3 组件与样式(src/client/GitWorkbenchPanel.tsx + .module.css

  • 外壳:面板是一张四边留白的卡片(--gs-inset,14px 圆角、投影),最大化按钮切到满屏。三条边可拖:卡片左缘(MIN_DRAWER_WIDTH)、提交列表与文件树之间、文件树与 diff 之间。三处共用 useHorizontalDrag(pointer capture + pointercancel)。窗格上界由 applyPane 现场量出来算:面板宽 - 邻窗格宽 - MIN_DIFF_WIDTH,diff 是唯一不能折行的窗格,所以它的下限是硬的。宽度与主题存 localStorage。
  • 布局:变更页 = 文件树 + 逐文件 diff 两栏;历史页 = 提交列表 + 文件树 + diff 三栏并列(GitHub Desktop / JetBrains git log 的做法),各自独立滚动,因此没有可折叠的东西要解释。翻页是滚动哨兵(IntersectionObserver),不是按钮。
  • diff 渲染:renderDiff(segment) 把统一 diff 逐行分类,渲染成 [老行号][新行号][+/-槽][代码] 的 flex 行;行号从 @@ -a,b +c,d @@ 解析并随行递增。
  • 配色:面板自带调色板,不走 dsh 主题 token——diff 需要 增/删/词级/语法 四组颜色,dsh 没有定义。**所有颜色都过 --gs-* token,字面色只出现在 .overlay[data-gs-theme='<family>-<mode>'] 的调色板块里;换主题=换一组 token,别的什么都不动。只有状态卡(在 dsh 原生 chrome 里)保留 --dsw-* token**。

- 主题族与解析逻辑在 src/client/themes.ts(不 import CSS/React,因此可被测试直接加载):GitHub / IntelliJ IDEA / VS Code / One / Solarized / Nord / Cyberpunk,各带亮暗两套。 - 明暗默认 system = 跟随操作系统(matchMedia('(prefers-color-scheme: dark)'),挂载期间持续跟随);显式选亮/暗则完全覆盖。 - 明暗三个按钮的色块是写死的白 / 近黑 / 对角各半,不取调色板:那三个按钮命名的就是颜色本身,暗色主题下把「亮色」画成深灰等于告诉用户反话。这是全文件唯一允许出现字面色的第二处。 - tests/theme-palettes.test.tsthemes.ts 的族列表和 .module.css 的调色板选择器互相扣死:少一套调色板会让面板一个 --gs-* 都没有、整块退回浏览器默认色而不报错,所以这个不变量必须由测试守。

2.3b 自定义样式:背景图 + 自定义 CSS(src/style-store.ts + 宿主 styleGet/styleSet

  • 两个作用域project(按仓库根 key)与 global背景图整条取项目的——虚化度/遮罩是为某一张图调的,换一张图就不成立,所以不做逐字段合并。自定义 CSS 两边都生效,global 在前、project 在后,靠 CSS 层叠顺序让项目覆盖全局;这比"整块覆盖"有用:全局定字号、项目改强调色。解析逻辑在 themes.tseffectiveBackground / effectiveCsstests/style-resolve.test.ts 守着。
  • 存在宿主而不是 localStorage:项目设置该跟着项目走(换浏览器、清 origin 都不该丢),而且一张背景图远超 origin 配额。文件是 ~/.dsh/gitworkbench-style.json,原子写复用 src/atomic-json.ts(tmp+rename + Windows EPERM 退避),两个作用域并发写经 withStyle promise 队列串行化。
  • 图片先在浏览器里降采样createImageBitmap → canvas → JPEG,长边 ≤2560,q0.82)再存。手机照片 4-6MB,虚化之后那些细节一点都留不下,没必要每次开面板都拖着走。
  • image 只接受 base64 data: URLstyle-store.tsIMAGE_PATTERN)。客户端要把它插进 url("…"),而 base64 字母表里没有引号、括号、反斜杠、分号,所以存进去的值不可能闭合函数再追加规则。https://data:image/svg+xmldata:text/html 一律拒绝,tests/style-store.test.ts 逐条验过。
  • 背景怎么画.drawer[data-gs-bg]::before 铺图 + filter: blur()transform: scale(1.12) 是因为模糊会采样到盒子外,不放大边缘会透明)。同时 --gs-surface / --gs-surface-2 从实色切成 color-mix(… var(--gs-veil), transparent),各窗格因此透出底图;弹出层(主题菜单、分支选择器)故意保持实色,压在虚化照片上的菜单没法读。没设背景图时这两个 token 就等于 --gs-bg / --gs-panel,即与之前逐像素一致。
  • 用户 CSS 的抓手是 data-gs-partoverlay / card / header / tabs / commits / tree / diff。CSS Modules 的类名每次构建都换 hash,从外面根本选不中,所以必须有一组稳定属性。常见写法就是覆盖 token:[data-gs-part="card"] { --gs-accent: #ff0066; }

2.4 worktree 仿真(src/worktree.ts 纯逻辑 + src/index.ts 里的 RPC/工具)

  • 宿主 RPC(同一 GitWorkbenchService 上多挂 4 个 @Remote,参数照 §6.8 裸标识符、signal 最后):

- worktreeEnter(sessionId, repoPath, name, signal)——repoRootOf 解析仓库根;在 <repoRoot>/.agents/worktrees/<name> 创建(或复用)worktree、分支 = 名字本身(不加强制前缀),写绑定;返回 {ok, worktreePath, branch, hint},hint 教模型怎么用相对路径(会话 cwd 不可变)。复用判定走 realpath:目标目录已是注册 worktree(别的工具建的、或经 Junction 映射进来的,git 登记的是另一种拼写)→ 直接绑定并保留它自己的分支,不再 worktree add。 - worktreeExit(sessionId, remove, signal)——解绑;remove:true 且树干净才 git worktree remove,脏树拒绝。 - worktreeStatus(sessionId, signal)——绑定 + 仓库全部 worktree 列表。 - sessionWorktree(sessionId, signal)——{worktreePath, name},未绑定为双 null;客户端轮询已改用 worktreeStatus(绑定+列表一次拿全),这个 RPC 保留作轻量单查。

  • 绑定持久化 ~/.dsh/gitworkbench-worktree-bindings.json{v:1, bindings:{<sessionId>:{repoRoot,worktreePath,name,enteredAt}}})。写法是先写 .tmp 再 rename(崩溃不留半截文件);Windows 上 rename 可能 EPERM → 25/50/100/200/400ms 退避重试;所有 load→save 段落经 promise 队列互斥(withBindings),并发 enter/exit 不会互相覆盖。
  • agent 工具:同一份逻辑用 ctx.tools.register(defineTool({...})) 注册成 worktree_enter/exit/status,sessionId/cwd 取自 exec.agent?.session不接受模型传参)——注册要点见 §6.10,schema 限制见 §6.11。
  • 客户端跟随GitWorkbenchPanel 每轮拉 stats 的同时拉 worktreeStatus(sessionId, cwd)(绑定 + 仓库全部 worktree 一次拿到,agent 在 dsh 外面建的 worktree 也会跟进列表);有绑定 → 状态卡亮出绑定标记(树形图标;分支与徽标文字重名时省略后者)、stats 改传绑定的 worktree 绝对路径;面板头部的 worktree 选择器按分支列出所有源,只切显示对象、不动绑定。树的展开状态跨切换、跨轮询保留;选中在切换源时有意重置——旧 worktree 的路径不能漏进新树的选中(§6.0c)。

2.5 写操作:暂存 / 提交 / 同步 / 撤回(src/git-ops.ts + src/discard-ops.ts + 宿主 9 个 @Remote

  • 勾选就是 git 调用:勾一个文件=git add -- <path>,取消=git restore --staged -- <path>,立即生效。argv 全部数组构造(无 shell,引号不是攻击面),路径一律放 -- 之后并拒绝前导 -(文件可以合法叫 -f,位置参数传进去就成了选项);全库没有 --force/reset --hard/clean 任何拼写——丢提交类操作需要的是专门的确认设计,不是碰巧排在旁边的按钮。
  • 点击即显、不丢点击:勾选走乐观更新 + 队列(stage-tree.tsnextBatch 按动作聚批,一次 drain 只发一个 git 调用——宿主一次调用 ~300ms,等它返回再画勾就是用户投诉的「超级卡」),120ms 内连点两下都会入队生效;轮询回包经 settledTicks 对账后落定。
  • 提交commit(worktreePath, message, amend, signal)——消息整段作一个 argv 元素传 -m(多行 body 是常态,拆分才是风险),绝不 -a:面板有自己的暂存区,全量扫进去等于让分区变成摆设。
  • 同步syncStatus(branch/upstream/ahead/behind + hasRemote,读 git status 而非 rev-list --count——「没配 upstream」和「与 upstream 齐平」的计数都是 0,只有前者决定 push 要不要 --set-upstream)、fetch --prune(远端删掉的分支别再算作待拉取)、pull --ff-only/--rebase/--no-rebase(模式永远显式:按钮写什么就跑什么,不读用户的 pull.rebase 配置)、push(绝不 force;无 upstream 时 --set-upstream origin <branch>;被拒归类为 diverged,答案是先 pull 而不是覆盖别人的工作)。
  • 单文件撤回(IDEA 的 Rollback)discardPlan / discardFile,计划推导在 src/discard-ops.ts(纯函数)。语义与 IDEA 一致:不问暂存与否,索引与工作区一起回退——改过的还原、未提交过的删除、误删的找回、改名撤销;目录不提供这个手势。计划由 host 用全树 git status 现场推导(git 靠「一删一增」配对才认出改名,带单文件 pathspec 的 status 只看到一半,会把「撤销改名」错读成「还原一个 + 删掉另一个」,见 §6.16);弹窗措辞来自推导出的后果(找回已删文件是纯收益,不弹窗);执行前重推一遍并核对后果一致,文件变了就什么都不做。危险拼法禁令同上且更严:只有 git restore 加单个 pathspec(-- 之后),删除走文件系统但拒绝绝对路径 / 盘符 / UNC / .. 并按解析后路径复查在工作区内——tests/discard-ops.test.ts 扫描本模块可产出的每条计划守这条线。
  • 失败要说人话classifyFailure 把 stderr/exit 归类为 auth / no-upstream / diverged / conflict / nothing-to-commit / dirty,原始文本随行返回——归类是提示,不替代证据。子进程环境关掉全部凭据提示(GIT_TERMINAL_PROMPT=0GCM_INTERACTIVE=never、askpass 置空):stdin:'ignore' 不会把交互提示变成错误,只会变成没人能回答的等待,而那等待挂在宿主进程里——一个过期的 token 就能挂死整个插件 30s。

2.6 历史过滤(宿主 src/log-filter.ts + src/shortlog.ts;客户端 log-filter-query.ts / calendar.ts / dir-tree.ts / path-select.ts

  • 条件编译成 git log 参数log-filter.ts:统一 -i -E 方言、字面量转义、--author 逐人、approxidate --since/--until、pathspec 放 -- 之后),在全部历史上匹配后再分页——不是只筛已加载的页;过滤后的翻页仍是单次连续游走,车道图不断。裸 yyyy-mm-dd 由 host 展开为全天(§6.15);git log 失败原样透出 stderr(exit + 尾部),不静默成「无匹配」。
  • 两个入口写同一个过滤器:输入框语法(user: / path: / after: / before: + 可删除 chips,log-filter-query.ts)与漏斗弹层(作者来自 git shortlog跟随当前 ref——名单里的人必然搜得到;自绘日历 calendar.ts 纯函数月格;路径树 dir-tree.ts 聚合 + path-select.ts 三态勾选:勾目录覆盖并吸收子文件,目录有半选态)。防抖 300ms、在飞请求取消、条件变化回第 0 页。
  • 「全部分支」 = --all 哨兵(ref 不能以 - 开头,无歧义),ref 选择器与作者名单同步。按人搜索只匹配作者(git 没有「作者或提交者」并集下推,IDEA 同款),提交者完整显示在悬浮卡。

---

3. 文件布局

harness-worktree/
  package.json              dsh.client(web) 声明 + exports + peerDeps(@deepseek-ai:* 用 "*",运行时由 profile 提供)
  .npmrc                    auto-install-peers=false(关键!见 §6.5)
  tsconfig.json             tsc 构建【宿主半】用(含 stage-3 装饰器 + ambient shim)
  tsconfig.client.json      仅类型检查【客户端半】(tsdown 用 dts:false、rolldown 不检查,没有这个 config 就完全没人查 src/client)
  tsdown.config.ts          tsdown 构建【客户端半】用(closure-factory bundle + CSS Modules 插件)
  vitest.config.ts          排除 .agents/**(worktree 是整仓副本,否则同一套测试被收集多遍)
  src/
    index.ts                宿主半:GitWorkbenchService(TypertRemoteService + @Remote:stats/fileDiff/commits/authors/repoTree/compareRefs/commitStats/worktree*/style*/syncStatus/stage/unstage/discardPlan/discardFile/commit/fetch/pull/push)+ defineTool 三工具
    atomic-json.ts          崩溃安全的 JSON 写入(tmp+rename + Windows EPERM 退避),绑定文件与样式文件共用
    commit-cache.ts         内容寻址缓存:commit hash 指向不可变内容,只需容量上限、不需失效
    git-ops.ts              写操作的 argv 构造 + stderr 归类(纯函数、不 spawn)——见 §2.5
    git-log.ts              `--pretty` 日志与 porcelain 状态头的解析(纯函数)
    log-filter.ts           历史过滤条件 → `git log` 参数的编译 + 裸日期展开为全天(纯函数,§2.6)
    shortlog.ts             作者名单:`git shortlog -sne` 输出解析、按活跃度排序截断(纯函数)
    discard-ops.ts          单文件撤回的计划推导(全树 status → restore/delete 计划 + 路径防线,纯函数,§2.5)
    style-store.ts          背景图 + 自定义 CSS 的两作用域存储与校验(~/.dsh/gitworkbench-style.json)
    worktree.ts             worktree 纯逻辑:绑定文件读写(tmp+rename 原子、EPERM 重试)、名称/分支/目录推导、porcelain 解析、`isRefName`
    types/dsh-shim.d.ts     ambient 声明:让 tsc 在没装 @deepseek-ai/* 时也能编译(cordis/subprocess 的宽松类型)
    types/dsh-client-shim.d.ts  同上,客户端侧(CSS Modules + client-runtime/ui-slots);**故意宽松**,只查本插件自己的代码,不复述 harness 的类型
    client/
      index.ts              客户端半:注册会话头插槽(fetchStats/fetchFileDiff/fetchWorktreeStatus 等 RPC 回调)
      GitWorkbenchPanel.tsx     状态卡(树形图标绑定标记)+ 面板(三栏历史 + diff + 头部 worktree 选择器 + 齿轮挂设置弹层 + 拖拽改宽 + 勾选/提交/同步条)
      GitWorkbenchPanel.module.css  调色板(7 族 × 亮/暗)+ 全部布局
      stage-tree.ts         勾选状态推导 + 乐观勾选的 overlay/对账/聚批(§2.5,纯函数)
      diff-model.ts         统一 diff 行解析 + 词级变更区间(纯函数)
      commit-graph.ts       提交图的泳道分配(纯函数)
      worktree-view.ts      worktree/分支列表的展示推导(纯函数)
      log-filter-query.ts   过滤输入框语法(user:/path:/after:/before: → 条件对象 + chips,纯函数)
      commit-filter.ts      悬浮卡的精确时间渲染(`%cI` → 查看者时区,纯函数)
      dir-tree.ts           路径选择器的目录树聚合(ls-tree 平铺 → 目录 + 文件叶子,纯函数)
      path-select.ts        三态勾选树语义(勾目录吸收子文件、拆分级联、半选态,纯函数)
      calendar.ts           自绘日历的月格推导(纯函数,全主题 token 化)
      active-file.ts        过滤后点开提交的默认选中文件排序(已有选中 > 精确点名 > 目录之下 > 首个,纯函数)
      file-filter.ts        文件列表关键字过滤(多词 AND、逐词智能大小写,纯函数)
      highlight.ts          Shiki 封装:扩展名→语法、主题映射、语法包按需加载
      op-feedback.ts        写操作按钮反馈的时序常量(忙碌提示、过短操作不禁用)
      themes.ts             主题模型:族列表、明暗解析、两作用域样式解析、localStorage 值校验(无 CSS/React 依赖,便于测试)
      locales.ts            zh / en 两份词典
  tests/                    vitest:worktree-bindings(存储/原子写/重试)、worktree-derive(名称/分支/porcelain)、ref-name、git-ops(argv/归类)、git-log、commit-cache、style-store、style-resolve、theme-palettes(族×调色板互扣)、stage-tree(勾选模型)、worktree-view、commit-graph、diff-regression(diff 模型/词级区间/Shiki/CSS 不变量)、drawer-chrome(状态卡与面板的结构性扫描)、op-feedback、status-parse(porcelain/numstat 解析、二进制嗅探、截断与上限——fixture 场景目录 TESTS.md 的单测化)、log-filter(条件编译/裸日期展开)、log-filter-query(输入语法/chips)、shortlog、dir-tree、path-select(三态勾选)、calendar、commit-filter(精确时间渲染)、active-file(默认选中排序)、file-filter(关键字过滤)、discard-ops(撤回计划推导 + 危险拼法扫描)
  scripts/
    probe_worktree.py       宿主 RPC 探针:scratch 仓库全流程 + 真仓库冒烟 + 再进入分支复用
    verify_worktree_ui.py   UI 探针:状态卡标记/头部路径/选择器切换/折叠回归 6 步(经真实页面 RPC 回放)
    llm_smoke.py            真实 LLM 冒烟:让模型在会话里调 worktree_enter/exit,看目录出现/消失
    verify_turn_refresh.py  UI 探针:抽屉关着时回合结束自动刷新状态卡(0.1.2 的刷新链路)
    verify_history_feature.py  UI+host 探针:历史过滤两半(条件下推 + 输入框/漏斗/诚实空态)11 步
    (五个 .py 探针为本地集成脚本:需连真实 dsh 实例与 scratch 仓库,内嵌本机路径——已 gitignore,不入库、不随包发布)
  cordis.patch.yml          一行 insert,把宿主 entry 挂进 web profile
  README.md                 本文件
  README_EN.md              英文版(面向使用者与维护者;深度细节仍以本文为准)

---

4. 怎么构建

cd <仓库根目录>
pnpm install      # 装 tsdown/typescript/react/lightningcss/@types/node;.npmrc 关掉了 peer 自动安装
pnpm bundle       # = tsc -p tsconfig.json && tsc -p tsconfig.client.json && tsdown
pnpm typecheck    # 同样两个 tsc,不产出
pnpm test         # vitest

产物:

  • lib/index.js —— 宿主半(ESM,tsc 产出,装饰器已转译)
  • lib/client.js —— 客户端半(CJS closure-factory,tsdown 产出,CSS 已内联为 <style> 注入)

只改了客户端时,pnpm bundle 重建后刷新浏览器即可——web server 每次请求都从磁盘读 lib/client.js,不用重启。(别只跑 pnpm exec tsdown:rolldown 不做类型检查,会漏掉 tsconfig.client.json 才能发现的错误。)改了宿主半必须 pnpm bundle + 重启 dsh web(宿主代码在内存里,不重启不生效)。

---

5. 怎么加载 / 迭代

前提:deepseek-harness 仓库已 pnpm install + pnpm run build,且 DEEPSEEK_API_KEY 已设。

# 一次性:把插件装进 web profile(= 在 ~/.dsh/profiles/web 里 pnpm add 本目录)
dsh plugin --profile web add <仓库根目录>

# 启动(--patch 手工挂载宿主 entry;package.json 的 dsh.bundle.patch 声明已让
# `dsh plugin add` 自动带上补丁,--patch 仅在 profile 于该声明存在之前加入时需要)
dsh web --patch <仓库根目录>/cordis.patch.yml

# 便携交付:tarball 自足——prepack 现场构建 lib/,files 白名单只带 lib/src/文档/补丁
npm pack
dsh plugin --profi

…