dsh-worktree-switch 0.0.0-stage → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +178 -2
- package/cordis.patch.yml +19 -0
- package/icon.svg +8 -0
- package/lib/client.js +1137 -0
- package/lib/index.d.ts +27 -0
- package/lib/index.js +1091 -0
- package/lib/index.js.map +1 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +135 -4
package/README.md
CHANGED
|
@@ -1,3 +1,179 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `dsh-worktree-switch`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
在新会话屏幕的输入框工具行里放一个 **Worktree 开关**。默认打开(按项目记忆):
|
|
4
|
+
打开时立刻在 `<项目根>/.dsh/worktrees/<名字>` 建一份仓库副本,并把这条会话的工作
|
|
5
|
+
目录切进去;在发出第一条消息之前再关掉,会话会被切回项目目录,开关自己刚建的干净
|
|
6
|
+
副本也一并删掉——关掉就是撤销这次隔离。副本不会自动消失、也不会自动合并,用户可以
|
|
7
|
+
在 **Worktrees 面板**里把它丢弃,或者自己把分支合回源仓库。会话在副本里时,宿主半还会
|
|
8
|
+
给模型一条工作区说明,并拒绝把 git 指向主检出的 shell 调用(可关)。
|
|
9
|
+
|
|
10
|
+
## 它做什么
|
|
11
|
+
|
|
12
|
+
- **开关**(`conversation.input.left` 槽,只在空白新会话的 hero 上出现):显示本会话
|
|
13
|
+
当前的副本状态——检查中 / 关闭 / 创建中 / 离开中 / 就绪(显示名字)/ 失败(显示原因 +
|
|
14
|
+
重试)/ 不可用(项目不是 Git 仓库)。开关位置本身记的是"这个项目的新会话要不要隔离"。
|
|
15
|
+
- **创建**发生在开关打开的那一刻,早于第一条消息;**关闭**发生在第一条消息之前时,
|
|
16
|
+
会话被切回项目目录,开关在这次打开中建的那份干净副本被删除(脏副本或开关之前就存在
|
|
17
|
+
的副本留在池里,交给面板处理)。副本名 = 分支名 = 池内目录名,形如
|
|
18
|
+
`quiet-harbor-3fa9`,由插件生成,短且唯一。
|
|
19
|
+
- **创建失败不回落**:提示词不发送,错误就地显示。从检查开始、到副本就绪之前、离开副本
|
|
20
|
+
的过程中(或失败之后),开关通过 `ctx.conversation.blocks` 把该会话的输入框置为"暂停
|
|
21
|
+
发送";重试成功、关掉开关(确认不要隔离)或副本就绪时才解除。关掉开关是明确的退出
|
|
22
|
+
方式——它表示"这条会话就在原目录里跑",而不是假装隔离成功。
|
|
23
|
+
- **会话范围说明**:会话在副本里时,宿主半跟着每轮请求给模型补一条上下文(紧跟工作目录那
|
|
24
|
+
行):当前目录是 `<项目>` 的 linked worktree 副本,git 与文件操作在副本里做,主检出是源。
|
|
25
|
+
它会随恢复会话、每次进/出副本重新推导,回到项目目录后自动消失。这是**提示**,不是权限
|
|
26
|
+
边界——真正的写隔离仍取决于沙箱根(见「已知边界」)。
|
|
27
|
+
- **主检出守卫**:会话在副本里时,`tools/pre-execute` 会拦下“把 git 指向主检出”的 bash
|
|
28
|
+
调用(命令里出现主检出的绝对路径,或 `workdir` 落在主检出里),拒绝原因直接告诉模型
|
|
29
|
+
在副本里该怎么做(`git fetch origin main` + `merge`/`rebase`)。这是启发式拦截,不是
|
|
30
|
+
沙箱边界:`cd ..` 之类的相对逃逸、shell 变量展开不在它的覆盖范围内;配置
|
|
31
|
+
`guardSourceCheckout: false` 可关掉。拦下的是 git,主检出里的普通写入仍然由沙箱放行。
|
|
32
|
+
- **面板**(侧边栏条目 + 自己的页面):按项目列出该仓库的所有 linked worktree(包括
|
|
33
|
+
手工 `git worktree add` 出来的),含名字、分支、路径、创建时间和未提交改动数;丢弃
|
|
34
|
+
需要第二次确认,且**有未提交改动时拒绝丢弃**并说明原因。丢弃会删掉检出目录和分支。
|
|
35
|
+
- **会话身份不变**:会话仍然建在源项目的工作区里,起始目录不变,插件只改会话的工作
|
|
36
|
+
目录(一条持久化事件,重开会话仍然生效)。
|
|
37
|
+
|
|
38
|
+
## 安装
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
# 本地 checkout
|
|
42
|
+
dsh plugin --profile web add /home/ubuntu/code/deepseek-plugins/packages/dsh-worktree-switch
|
|
43
|
+
dsh --profile web --dump-config # 应能看到 "# == dsh-worktree-switch" 和 "# == worktree" 两层
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`cordis.patch.yml` 会插入两行:`@deepseek-ai/dsh-experimental-worktree`(提供
|
|
47
|
+
`ctx.worktrees`,行 id 特意用 `worktree`,与官方 tool-worktree bundle 的行 id 相同,避免
|
|
48
|
+
重复注册服务)和本插件自己。装完需要重启正在运行的 Web profile。
|
|
49
|
+
|
|
50
|
+
构建产物由 `prepare` 脚本(tsdown)产出;改完源码后重新 `pnpm --filter dsh-worktree-switch build`
|
|
51
|
+
即可。外部安装的插件不在 `pnpm run dev:web` 的监视范围内,client-hmr 每 500ms 轮询
|
|
52
|
+
`lib/client.js`,重新构建后页面会自动换上新的浏览器半。
|
|
53
|
+
|
|
54
|
+
## 在 GUI 里走一遍
|
|
55
|
+
|
|
56
|
+
1. 重启正在跑的 Web profile。
|
|
57
|
+
2. 新建会话(hero 屏幕):输入框工具行最左侧出现 Worktree 开关;默认打开的项会先显示
|
|
58
|
+
"检查中",随即"正在创建",然后显示这条会话的副本名。
|
|
59
|
+
3. 按回车发消息:助手的工作目录已经是 `<项目根>/.dsh/worktrees/<名字>`,源仓库的
|
|
60
|
+
未提交内容留在原地。
|
|
61
|
+
4. 侧边栏出现 **Worktree** 入口:打开后按项目列出磁盘上的副本,含创建时间与未提交改动。
|
|
62
|
+
5. 想丢弃一份副本:点两次 Discard(第二次是确认);有未提交改动时按钮禁用并说明原因。
|
|
63
|
+
6. 开关只在空白会话上出现:发第一条消息之前关掉,会把会话切回项目目录并删掉刚建的空
|
|
64
|
+
副本;发过消息之后开关消失,副本留在池里,由面板丢弃。拨开关同时记住这个项目下次
|
|
65
|
+
的默认位置。
|
|
66
|
+
|
|
67
|
+
## 配置
|
|
68
|
+
|
|
69
|
+
### 副本目录
|
|
70
|
+
|
|
71
|
+
副本目录(池)由 **`cordis.patch.yml` 里那一行 `worktree`** 决定,也就是 harness
|
|
72
|
+
worktree 服务自己的 `directory` 选项——本插件不另存一份设置,`status` / `list` /
|
|
73
|
+
`discard` 都按“仓库里有哪些 linked worktree”去认副本,所以服务把池挪到哪里,插件就
|
|
74
|
+
跟到哪里。本包默认是 `.dsh/worktrees`。
|
|
75
|
+
|
|
76
|
+
要改成别的目录,在 **profile 自己的 patch 层**覆盖同一行即可(后层按行 id 覆盖,整块
|
|
77
|
+
`config` 替换):
|
|
78
|
+
|
|
79
|
+
```yaml
|
|
80
|
+
# ~/.dsh/profiles/web/cordis.patch.yml
|
|
81
|
+
- id: worktree
|
|
82
|
+
name: '@deepseek-ai/dsh-experimental-worktree'
|
|
83
|
+
config:
|
|
84
|
+
directory: worktrees # 相对项目根;这里就会落在 <项目根>/worktrees/<名字>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
两条硬约束:
|
|
88
|
+
|
|
89
|
+
- **必须留在项目根内部**。会话的沙箱写入边界是不可变的会话起始目录,池在项目根外时
|
|
90
|
+
创建副本会被直接拒绝。
|
|
91
|
+
- 目录对每个项目都生效(是相对路径,不是在某个绝对位置开池)。若想按项目不同,只能
|
|
92
|
+
在项目里放不同的 profile——目前没做这种粒度。
|
|
93
|
+
|
|
94
|
+
改完重启 profile 生效。已经在旧目录里的副本不会消失:面板列的是仓库的所有 linked
|
|
95
|
+
worktree,所以新旧目录的副本都会出现,可以照常丢弃。
|
|
96
|
+
|
|
97
|
+
### 开关默认
|
|
98
|
+
|
|
99
|
+
| 字段 | 默认 | 含义 |
|
|
100
|
+
|---|---|---|
|
|
101
|
+
| `defaultEnabled` | `true` | 用户从未在这个项目上拨过开关时,新会话是否默认隔离。用户拨过一次后,按项目记在浏览器 localStorage 里,优于此项。 |
|
|
102
|
+
| `guardSourceCheckout` | `true` | 会话在副本里时,用 `tools/pre-execute` 拒绝把 git 指向主检出的 bash 调用。关掉就只剩提示。 |
|
|
103
|
+
|
|
104
|
+
## 目录与命名
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
<项目根>/.dsh/worktrees/
|
|
108
|
+
├─ .gitignore # worktree 服务建池时写入(内容 "*"),池本身不进版本库
|
|
109
|
+
└─ quiet-harbor-3fa9/ # 一份副本;同名分支 quiet-harbor-3fa9
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
名字不带 `wt/` 前缀:池路径已经把“这是什么”说清楚了。名字、分支、目录三者是同一个
|
|
113
|
+
字符串,`git branch` 里看到的名字就是磁盘上的目录名。
|
|
114
|
+
|
|
115
|
+
## 已知边界(接受,不修)
|
|
116
|
+
|
|
117
|
+
- 副本里**看不到**未跟踪的项目本地资产:`.dsh/skills`、`.agents/skills`、
|
|
118
|
+
`AGENTS.local.md` / `CLAUDE.local.md`。被 git 跟踪的 `AGENTS.md` 照常可见。
|
|
119
|
+
- 会话记录里的起始目录仍指向源项目根;任何信任该字段的功能("在终端打开"、按目录
|
|
120
|
+
定位文件)显示的是源项目根,而不是副本。这是刻意的:沙箱边界与侧边栏归属都依赖它。
|
|
121
|
+
- 沙箱边界仍是源项目根,因此副本不是"写隔离",它隔离的是**提交与未提交改动**:
|
|
122
|
+
助手在副本里改的文件不会落到源检出的工作树里,但同一会话仍可写源项目根下的文件。
|
|
123
|
+
主检出守卫只覆盖 bash 里明显指向主检出的 git 调用,是提示之外的兜底,不是权限。
|
|
124
|
+
- 不自动合并、不自动回收、不支持多仓库、不识别旧插件的 `.worktree-space/` 目录。
|
|
125
|
+
|
|
126
|
+
## 实现
|
|
127
|
+
|
|
128
|
+
宿主半(`src/index.ts`)做三件事:拿 `cordis.patch.yml` 挂上的 `ctx.worktrees` 和
|
|
129
|
+
`ctx.sessionController`,把五个动词注册成 `/api/dsh-worktree-switch/<verb>` 上的精确
|
|
130
|
+
Fetch 路由(`src/host/routes.ts`),说 `client-request` / `server-response` 信封;另注册一条
|
|
131
|
+
`systemPrompt.context`(`worktree-switch:scope`),内容来自 `SessionScopeRegistry`,由五个
|
|
132
|
+
动词、`agent/created` 和每次组装时的目录校验共同维护。
|
|
133
|
+
|
|
134
|
+
| 动词 | 入参 | 行为 |
|
|
135
|
+
|---|---|---|
|
|
136
|
+
| `status` | `{ sessionId }` | 解析仓库根、本会话是否已在某个 linked worktree 里、源仓库未提交改动数 |
|
|
137
|
+
| `prepare` | `{ sessionId }` | 幂等;必要时经 `ctx.worktrees.create` 建副本并切目录,同名冲突自动换名重试 |
|
|
138
|
+
| `release` | `{ sessionId }` | 把会话切回项目目录;删掉开关自己建的那份干净副本,脏副本与非开关建的副本留在池里 |
|
|
139
|
+
| `list` | `{ path }` | 列出该仓库除主检出外的所有 linked worktree,返回名字/分支/路径/创建时间/改动数 |
|
|
140
|
+
| `discard` | `{ path, name }` | 干净才删:`git worktree remove` + `git branch -D`;改动非空则拒绝 |
|
|
141
|
+
|
|
142
|
+
创建走 harness 的 worktree 服务(沿用它的沙箱、filter 覆盖和失败语义);`status`/`list`/
|
|
143
|
+
`discard` 是人在界面上的显式动作,用 `ctx.subprocess` 直接跑 Git,与会安装的
|
|
144
|
+
`dsh-worktree-space` 一致。浏览器半(`src/client/index.tsx`)只注册三个槽:开关、keyed
|
|
145
|
+
`main` 面板页、`sidebar.panellist` 入口;状态机在 `src/client/state.ts`,不依赖 React。
|
|
146
|
+
|
|
147
|
+
## 校验
|
|
148
|
+
|
|
149
|
+
```sh
|
|
150
|
+
pnpm --filter dsh-worktree-switch typecheck
|
|
151
|
+
pnpm --filter dsh-worktree-switch test
|
|
152
|
+
pnpm build # 构建全部包并校验 exports / bundle patch 文件存在
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
测试只有两条接缝:宿主半的五个动词(真实临时 Git 仓库 + 真实服务,`tests/host.spec.ts`)
|
|
156
|
+
和浏览器半的槽注册与渲染(真实 `SlotRegistry` + 假 `connection`/`conversation`,
|
|
157
|
+
`tests/*.client.spec.tsx`)。不断言内部 Git 调用次数,也不测上游 worktree 服务自己的
|
|
158
|
+
Git 细节。
|
|
159
|
+
|
|
160
|
+
浏览器半为什么自带一个 `tests/client-harness.ts` 而不是用官方的
|
|
161
|
+
`@deepseek-ai/dsh-client-test-runtime`:该包 0.2.1-rc.5 发布版会 import
|
|
162
|
+
`@deepseek-ai/dsh-client-ui-renderer/src/...`,而发布产物里没有 `src/`,装上也跑不起来。
|
|
163
|
+
为此测试侧另有两处上游缺口的补丁,都只影响开发依赖:`dsh-client-store` 的运行依赖
|
|
164
|
+
`zustand`/`immer` 在发布清单里只写在 devDependencies(所以本包自己也装了这两个);
|
|
165
|
+
`dsh-client-ui-primitives` 同样漏声明了整条工具链,测试里把它 mock 到实际渲染到的
|
|
166
|
+
Tooltip。
|
|
167
|
+
|
|
168
|
+
## 发布
|
|
169
|
+
|
|
170
|
+
`prepare` 会在 npm 打包前跑 tsdown,所以发布出去的是预构建产物,用户
|
|
171
|
+
`dsh plugin add dsh-worktree-switch` 无需构建授权:
|
|
172
|
+
|
|
173
|
+
```sh
|
|
174
|
+
pnpm --filter dsh-worktree-switch publish --access public
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
包元数据里的 `repository.directory` 指向本仓库的这个子目录,收录到社区目录
|
|
178
|
+
(awesome-dsh-plugin / dsh-market)后 npm 包会自动关联回仓库;收录步骤见仓库根
|
|
179
|
+
README 的「社区目录」一节。
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# The configuration layers this bundle contributes to a dsh profile.
|
|
2
|
+
#
|
|
3
|
+
# Row one mounts the harness worktree service this plugin drives. Its id is
|
|
4
|
+
# deliberately the same `worktree` id the in-box @deepseek-ai/dsh-experimental-tool-worktree
|
|
5
|
+
# bundle uses: a profile that already mounts the model-facing worktree tool
|
|
6
|
+
# overrides this row instead of registering the `worktrees` service twice
|
|
7
|
+
# (Cordis throws on a duplicate service name).
|
|
8
|
+
- insert:
|
|
9
|
+
- id: worktree
|
|
10
|
+
name: '@deepseek-ai/dsh-experimental-worktree'
|
|
11
|
+
config:
|
|
12
|
+
# The pool must stay inside the project root: a session's sandbox grant
|
|
13
|
+
# is its immutable start directory, so a pool outside it could not be
|
|
14
|
+
# written. This row is the one place the directory is configured —
|
|
15
|
+
# override `directory` in your profile patch to move it (the plugin
|
|
16
|
+
# itself follows whatever this service uses).
|
|
17
|
+
directory: '.dsh/worktrees'
|
|
18
|
+
- id: dsh-worktree-switch
|
|
19
|
+
name: 'dsh-worktree-switch'
|
package/icon.svg
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" role="img" aria-label="dsh worktree toggle">
|
|
2
|
+
<rect width="64" height="64" rx="14" fill="#1f6feb"/>
|
|
3
|
+
<path d="M20 17v20a6 6 0 0 0 6 6h13" fill="none" stroke="#ffffff" stroke-width="4" stroke-linecap="round"/>
|
|
4
|
+
<circle cx="20" cy="14" r="4" fill="#ffffff"/>
|
|
5
|
+
<circle cx="20" cy="50" r="4" fill="#ffffff"/>
|
|
6
|
+
<circle cx="44" cy="43" r="4" fill="#ffffff"/>
|
|
7
|
+
<path d="M34 24h10M34 24v14" fill="none" stroke="#ffffff" stroke-width="4" stroke-linecap="round" stroke-dasharray="3 4"/>
|
|
8
|
+
</svg>
|