@wjj-8283/dsh-temp-workspace 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 wjj-8283
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,263 @@
1
+ # @dsh-dev/dsh-temp-workspace
2
+
3
+ A DSH web **client plugin** that adds a **temporary workspace**.
4
+
5
+ > [中文说明](README.zh.md)
6
+
7
+ Click the small hourglass icon beside the sidebar's "Add workspace" (+) button
8
+ to create a throwaway workspace and open a fresh conversation in it. On the
9
+ next Harness restart this plugin **deletes the conversations** under that
10
+ workspace **and the workspace itself** — the temp workspace leaves no trace.
11
+
12
+ ## What it is
13
+
14
+ - **Host half** (`lib/index.js`) — a Cordis plugin that injects
15
+ `webServer` / `webRuntime` / `workspaceRegistry` / `sessionPersistence`,
16
+ `settings` and `loader`, registers a browser-trust-fenced
17
+ `/temp-workspace/api` route, and drives the cleanup at boot.
18
+ - **Browser half** (`src/client.js` → `lib/client.js`) — injects a small icon
19
+ button beside the sidebar "+ Add workspace" button; on click it calls the
20
+ host route and opens a new Conversation in the created workspace. It also
21
+ registers a **Settings → Plugins** card and, when the setting is enabled, a
22
+ confirm dialog at boot.
23
+
24
+ ## Installation
25
+
26
+ ### From npm (prebuilt — recommended)
27
+
28
+ The package is published to npm with `lib/` **prebuilt**, so installing it
29
+ needs no build and skips pnpm's `allowBuilds` approval — a single command:
30
+
31
+ ```sh
32
+ dsh plugin --profile web add @dsh-dev/dsh-temp-workspace
33
+ ```
34
+
35
+ `dsh plugin … add` forwards the spec to pnpm inside the profile dir, then
36
+ reconciles `dsh.profile.bundles` for you. Because the manifest declares
37
+ `dsh.bundle.patch`, the package auto-joins the Loader layer stack, so on the
38
+ next `dsh web` start the host composes the plugin as a Loader entry, registers
39
+ the `/temp-workspace/api` route, and the settings card enters `__DSH_BOOT__`.
40
+
41
+ To republish after a change (requires an npm account that owns the
42
+ `@dsh-dev` scope):
43
+
44
+ ```sh
45
+ npm login # one-time
46
+ npm publish # prepublishOnly rebuilds lib/client.js from src
47
+ ```
48
+
49
+ ### From GitHub
50
+
51
+ Install it straight from GitHub with the `dsh plugin` forwarder (pnpm clone +
52
+ `add` inside the profile dir, then the bundle layer is reconciled for you):
53
+
54
+ ```sh
55
+ # boot a profile, e.g. the web app
56
+ dsh plugin --profile web add github:wjj-8283/dsh-temp-workspace
57
+ ```
58
+
59
+ Because the manifest declares `dsh.bundle.patch`, the add step auto-appends
60
+ the package to `dsh.profile.bundles`, so on the next `dsh web` start the host
61
+ composes the plugin as a Loader entry, registers the `/temp-workspace/api`
62
+ route, and the settings card enters `__DSH_BOOT__`.
63
+
64
+ If you prefer to drive pnpm yourself, run the same spec from inside the profile
65
+ directory:
66
+
67
+ ```sh
68
+ cd "$DSH_HOME/profiles/web"
69
+ pnpm add github:wjj-8283/dsh-temp-workspace
70
+ ```
71
+
72
+ To pin a branch, tag or commit, append the usual pnpm git ref:
73
+
74
+ ```sh
75
+ dsh plugin --profile web add github:wjj-8283/dsh-temp-workspace#<ref>
76
+ ```
77
+
78
+ > The built client bundle (`lib/client.js`) is committed in the repo, so a git
79
+ > install runs no build step. To hack on it, clone the repo and use
80
+ > `node build.mjs --watch` (see [Dev loop](#dev-loop)) — or install from a local
81
+ > checkout instead (see [Wiring into the profile](#wiring-into-the-profile)).
82
+
83
+ ## Settings → Plugins
84
+
85
+ The plugin registers a card in **Settings → Plugins** (keyed by the settings
86
+ namespace `dsh-temp-workspace`, persisted in `~/.dsh/settings.yaml`):
87
+
88
+ | setting | values | default | meaning |
89
+ | --- | --- | --- | --- |
90
+ | `deleteMode` | `immediate` / `delayed` | `immediate` | when a temp workspace is removed on the next boot |
91
+ | `deleteDelay` | seconds | `3600` | grace period before removal when `delayed` |
92
+ | `confirmBeforeDelete` | on / off | `on` | ask the user before removing held workspaces |
93
+
94
+ - `confirmBeforeDelete` **on**: the host never auto-deletes at boot. It holds the
95
+ workspaces and the browser shows a **confirm dialog** with two choices:
96
+ - **Delete** — purge the held workspaces (sessions + registration + dir).
97
+ - **Keep temporarily** — leave the workspaces alone this boot but
98
+ keep them **temporary**: they stay in the marker and are subject to cleanup
99
+ (and re-confirmation) again on a later boot.
100
+ - An unanswered dialog leaves the workspaces intact for the next boot.
101
+
102
+ **Permanent keep** is not in the boot dialog — it has its own button.
103
+ Each temp workspace row in the sidebar (titled "临时工作区", i.e. "Temporary Workspace") shows a small **pin**
104
+ button next to its "+" (New Session). Clicking it picks a destination folder (the
105
+ OS folder picker via `ctx.workspaces.pickDirectory()`). The temp workspace's files
106
+ are moved **directly into that folder** (no extra "临时工作区" subfolder is
107
+ created), the folder itself becomes the permanent workspace, and its **folder name
108
+ is used as the workspace title**. The workspace's **conversations are migrated
109
+ too**: each session log's header `cwd` is rewritten to the folder path and its log
110
+ directory is moved into the new cwd's projectKey slot. After it succeeds, a
111
+ **non-dismissible** dialog appears with a **"Restart now"** button that
112
+ triggers a **real host-level restart** (a detached helper respawns the exact DSH
113
+ invocation — the same mechanism `dsh-market` uses for plugin updates) so the
114
+ registry re-indexes and the migrated conversations appear under the new workspace.
115
+ - `confirmBeforeDelete` **off**: deletion is automatic — immediately on boot, or
116
+ after `deleteDelay` seconds when `deleteMode` is `delayed`.
117
+
118
+ ## How cleanup works
119
+
120
+ There is **no public "delete session log" API** in DSH — the persistence seam is
121
+ append-only. So a temporary workspace is removed in four steps:
122
+
123
+ 0. **Live sessions first** — sessions the running host still holds in memory
124
+ (a confirm dialog, the delayed auto-delete, or a page reload can all trigger
125
+ the deletion in a host that never truly restarted) are torn down best-effort:
126
+ their agent is cancelled and awaited idle, the session's pending writes are
127
+ flushed to disk, and the live store entry is detached so `session.list` drops
128
+ it and the client closes the conversation immediately. Flushing **before**
129
+ detaching keeps multi-session workspaces clean: detaching emits
130
+ `session/disposed`, whose retirement flush would otherwise re-materialize a
131
+ deleted log for sessions that still had pending writes, leaving unreadable
132
+ Ungrouped leftovers.
133
+ 0b. **Archive cold sessions** — sessions that were never attached in this host
134
+ exist only as disk logs; deleting those logs sends no `session-removed`
135
+ event, so an already-open browser would keep them as unreadable Ungrouped
136
+ leftovers. `archiveColdSessions` archives each of them (the supported
137
+ "hidden from every grouping surface" mechanism) BEFORE the logs are deleted:
138
+ the archive-set change pushes `host/archived-sessions-changed` and the
139
+ sidebar drops them at once. The durable archived-id entries are reclaimed by
140
+ the boot-time orphan sweep.
141
+ 1. **Sessions on disk** — every session owned by the workspace (its header `cwd`
142
+ equals the workspace path) is located through `ctx.sessionPersistence.locate`
143
+ and its on-disk directory is removed with `fs.rm`.
144
+ 2. **Registration** — `ctx.workspaceRegistry.delete(id)` removes the workspace
145
+ record (which never touches logs or the directory by itself).
146
+ 3. **Directory** — the throwaway directory (under
147
+ `<dsh-home>/temp-workspaces/<uuid>`) is removed.
148
+
149
+ The plugin tracks temporary workspaces in `<dsh-home>/temp-workspaces/state.json`
150
+ (a durable marker of `{ workspaceId, path, createdAt }`). Deletion is driven by
151
+ that marker path, so a temp workspace whose **registry record was already removed
152
+ via the UI before a restart** still reaps its leftover directory (the fix for the
153
+ "leftover folder" bug) — the marker path, not the registry record, is the source of
154
+ truth for the directory.
155
+
156
+ **No trace**: deletion also prunes each detached live session's
157
+ **projection-cache row** (the `session_projcache` domain under
158
+ `~/.dsh/storages/session_projcache.json`) and the legacy per-session cache file
159
+ from an older storage layout. A boot-time **orphan sweep**
160
+ (`pruneOrphanTempResidue`) additionally reclaims every projection-cache row
161
+ whose `cwd` lives under the temp root and is no longer referenced by the
162
+ marker — together with its archived-id record (the ones `archiveColdSessions`
163
+ and the detach fallback leave behind) and legacy cache file. Those rows never
164
+ surface ghost conversations by themselves (identity-checked reads miss them),
165
+ but "no trace" means no trace; the user's intentional archives of real
166
+ conversations are never touched.
167
+
168
+ ## Layout
169
+
170
+ ```
171
+ temp-workspace-plugin/
172
+ package.json name, dsh.bundle.patch + dsh.client, exports
173
+ cordis.patch.yml inserts this plugin as a host Loader entry
174
+ lib/index.js node half — /temp-workspace/api route + settings + boot cleanup
175
+ src/client.js browser half SOURCE (icon + settings card + confirm dialog)
176
+ lib/client.js BUILT browser half (generated; do not edit)
177
+ build.mjs wrap src/client.js -> lib/client.js (HMR watch)
178
+ ```
179
+
180
+ ## Dev loop
181
+
182
+ ```sh
183
+ node build.mjs --watch
184
+ ```
185
+
186
+ `lib/client.js` is the single file client-modules serves and dsh-client-hmr
187
+ polls; editing `src/client.js` hot-reloads the UI without a page refresh.
188
+ **Host-half changes** (`lib/index.js`) need a `dsh web` restart.
189
+
190
+ ## Wiring into the profile
191
+
192
+ ```sh
193
+ # local checkout / clone (any absolute or relative path)
194
+ dsh plugin --profile web add /path/to/dsh-temp-workspace
195
+ ```
196
+
197
+ This adds it to `dsh.profile.bundles`. On the next `dsh web` start the host
198
+ composes the row, the `/temp-workspace/api` route is registered, the settings
199
+ card enters `__DSH_BOOT__`, and the boot cleanup / confirm flow runs.
200
+
201
+ ## API (`/temp-workspace/api/<method>`)
202
+
203
+ Reads accept `GET`, mutations `POST`.
204
+
205
+ | method | returns | notes |
206
+ | --- | --- | --- |
207
+ | `POST create` | `{ workspace, created: true }` | mkdir + `workspaceRegistry.create` + marker |
208
+ | `GET list` | `{ entries }` | `[{ workspaceId, path, createdAt }]` currently marked temporary |
209
+ | `POST delete` | `{ ok: true }` | remove one temp workspace now (sessions + registration + dir) |
210
+ | `GET/POST config` | `{ ok, value }` | read the settings, or `POST {…}` to write a patch |
211
+ | `POST configReset` | `{ ok, value }` | restore every setting to its default |
212
+ | `GET pending` | `{ ok, value }` | held workspaces + `confirmBeforeDelete` + `deleteAt` |
213
+ | `POST confirm` | `{ ok, deleted }` | user confirmed — delete the held set |
214
+ | `POST keep` | `{ ok, kept }` | temporary keep — leave the held set alone (still temp) |
215
+ | `POST permanentKeep` | `{ ok, moved }` | `{ target, workspaceId? }` — use `target` (picked folder) directly and de-temp that workspace (or all held when `workspaceId` omitted) |
216
+ | `POST restart` | `{ restarting, pid, helperPid }` | real host restart (same-origin loopback only) |
217
+
218
+ ## Notes / limits
219
+
220
+ - The sidebar workspace browser has **no slot** for a header action, so the
221
+ icon is injected into the DOM with a `MutationObserver` (same idiom as
222
+ `dsh-workspace-auto-approval`). If the sidebar layout changes upstream the
223
+ icon may need a matching placement update.
224
+ - The add-workspace button only renders when the directory-flow hole is
225
+ occupied, so the temp-workspace icon appears only then too.
226
+ - Cleaning removes session log directories directly. **Live** sessions owned by
227
+ a temp workspace are torn down best-effort on delete (agent cancel + archive +
228
+ live-store detach), so the conversations disappear even when the deletion runs
229
+ in a host that still holds them in memory; at boot there are no live sessions,
230
+ which is exactly when cleanup runs.
231
+ - A temp workspace's directory lives under the DSH home (`~/.dsh/temp-workspaces`),
232
+ not in your normal project folders.
233
+ - There is no generic "ask the human" API for a browser plugin, so the confirm
234
+ dialog is rendered by the plugin itself as a lightweight inline-styled overlay
235
+ through its own `react-dom/client` root (no dependency on the primitives'
236
+ CSS-module scope, so it always shows).
237
+ - **Permanent Keep** requires the `native` directory-picker capability
238
+ (`ctx.workspaces.pickDirectory()`); on a remote/non-native deployment that
239
+ picker is unavailable and the button is disabled.
240
+ - Conversation migration rewrites only the **header frame** of each session log
241
+ (a `.jsonl.zstd` file is a concatenation of independently-compressed
242
+ Zstandard frames — frame 0 is the header line carrying `cwd`, the rest are
243
+ the event batches). The header frame's `cwd` is rewritten and every event
244
+ frame is preserved byte-for-byte, so no conversation content is lost.
245
+ - A `cwd` is immutable in the DSH session header, hence the rewrite approach.
246
+ The DSH workspace registry's session→workspace **grouping (`bootstrap`) only
247
+ runs once on the first boot**; later boots only rebuild the id→cwd index and
248
+ never add a migrated session to the new workspace's `sessionIds`. To avoid the
249
+ migrated conversations ending up **ungrouped**, the plugin records a durable
250
+ **pending-attach** list during the move and, on the next boot, re-attaches the
251
+ migrated sessions to the new workspace — which is why the client prompts a
252
+ restart after a permanent keep.
253
+
254
+ ## Known bugs that we will not fix
255
+
256
+ - Due to Harness limits, after a permanent keep there is a chance the conversation
257
+ ends up `Ungrouped`. We have done what we can to handle it, but it can still
258
+ occur.
259
+ - After creating a temp workspace, the permanent-keep button may not appear
260
+ immediately; it will show up after a short while (2–3 s). Not planning to fix.
261
+ - Due to Harness limits, right after a migration the conversation name may take
262
+ the workspace's name; clicking it changes it back. It does not affect use; not
263
+ planning to fix — rename it yourself might help.
package/README.zh.md ADDED
@@ -0,0 +1,207 @@
1
+ # @dsh-dev/dsh-temp-workspace
2
+
3
+ 一个 DSH web **客户端插件**,提供 **临时工作区(临时工作区)** 功能。
4
+
5
+ 点击侧边栏「添加工作区」(+)右侧的小沙漏图标,即可创建一个一次性工作区并打开一个全新的对话。下次 Harness 重启时,这个插件会**删除该工作区下的全部对话以及工作区本身**——临时工作区不留任何痕迹。
6
+
7
+ ## 它是什么
8
+
9
+ - **宿主半边**(`lib/index.js`)—— 一个 Cordis 插件,注入
10
+ `webServer` / `webRuntime` / `workspaceRegistry` / `sessionPersistence`、
11
+ `settings` 和 `loader`,注册受浏览器信任围墙保护的 `/temp-workspace/api` 路由,
12
+ 并在启动时驱动清理逻辑。
13
+ - **浏览器半边**(`src/client.js` → `lib/client.js`)—— 在侧边栏「+ 添加工作区」
14
+ 按钮旁注入一个小图标;点击后调用宿主路由,并在新建的工作区里打开一个新对话。
15
+ 它还注册了一张 **设置 → 插件** 卡片,并在设置开启时于启动阶段弹确认对话框。
16
+
17
+ ## 安装方式
18
+
19
+ ### 从 npm 安装(预构建,推荐)
20
+
21
+ 该包已发布到 npm,`lib/` **为预构建产物**,安装无需构建、可跳过 pnpm 的 `allowBuilds` 授权——一条命令搞定:
22
+
23
+ ```sh
24
+ dsh plugin --profile web add @dsh-dev/dsh-temp-workspace
25
+ ```
26
+
27
+ `dsh plugin … add` 会把该 spec 转发给 profile 目录里的 pnpm,并代为合并 `dsh.profile.bundles`。因为清单声明了 `dsh.bundle.patch`,包会自动加入 Loader 层栈,下次启动 `dsh web` 时宿主把它组合为 Loader 条目、注册 `/temp-workspace/api` 路由、设置卡片进入 `__DSH_BOOT__`。
28
+
29
+ 改动后重新发布(需要拥有 `@dsh-dev` scope 的 npm 账号):
30
+
31
+ ```sh
32
+ npm login # 一次即可
33
+ npm publish # prepublishOnly 会从 src 重新生成 lib/client.js
34
+ ```
35
+
36
+ ### 从 GitHub 安装
37
+
38
+ 直接从 GitHub 安装,使用 `dsh plugin` 转发器(在 profile 目录里 pnpm clone + `add`,并自动合并 bundle 层):
39
+
40
+ ```sh
41
+ # 启动某个 profile,例如 web 应用
42
+ dsh plugin --profile web add github:wjj-8283/dsh-temp-workspace
43
+ ```
44
+
45
+ 因为清单声明了 `dsh.bundle.patch`,add 会把该包自动追加到 `dsh.profile.bundles`,下次启动 `dsh web` 时宿主就把它组合为 Loader 条目、注册 `/temp-workspace/api` 路由、设置卡片进入 `__DSH_BOOT__`。
46
+
47
+ 若想自己驱动 pnpm,可在 profile 目录里执行同一 spec:
48
+
49
+ ```sh
50
+ cd "$DSH_HOME/profiles/web"
51
+ pnpm add github:wjj-8283/dsh-temp-workspace
52
+ ```
53
+
54
+ 要固定到某个分支/标签/提交,按 pnpm 的 git ref 语法追加:
55
+
56
+ ```sh
57
+ dsh plugin --profile web add github:wjj-8283/dsh-temp-workspace#<ref>
58
+ ```
59
+
60
+ > 构建后的客户端 bundle(`lib/client.js`)已提交在仓库里,所以 git 安装不需要构建步骤。要改它,克隆仓库后用 `node build.mjs --watch`(见[开发循环](#开发循环)),或改为从本地 checkout 安装(见[接入 profile](#接入-profile))。
61
+
62
+ ## 设置 → 插件
63
+
64
+ 插件在 **设置 → 插件** 注册一张卡片(以设置命名空间 `dsh-temp-workspace` 为键,
65
+ 持久化在 `~/.dsh/settings.yaml`):
66
+
67
+ | 设置 | 取值 | 默认 | 含义 |
68
+ | --- | --- | --- | --- |
69
+ | `deleteMode` | `immediate` / `delayed` | `immediate` | 下次启动时何时删除临时工作区 |
70
+ | `deleteDelay` | 秒 | `3600` | `delayed` 模式下删除前的宽限期 |
71
+ | `confirmBeforeDelete` | 开 / 关 | `开` | 删除前是否先询问用户 |
72
+
73
+ - `confirmBeforeDelete` **开**:宿主启动时绝不自动删除,而是挂起这些工作区,
74
+ 浏览器弹出**确认对话框**,提供两个选项:
75
+ - **删除** —— 清空被挂起的工作区(会话 + 注册 + 目录)。
76
+ - **临时保留** —— 本次不动它们,但**仍然是临时**:它们还在标记里,会在之后的
77
+ 启动时再次(按设置)被确认/清理。
78
+ - 未作答时工作区原样保留到下次启动。
79
+
80
+ **永久保留**不在启动确认弹窗里——它有**自己的按钮**。侧边栏里每个临时工作区行
81
+ (标题「临时工作区」)在其「+」(New Session) 旁边有一个**图钉**按钮。点击后选取一个
82
+ 目标文件夹(通过 `ctx.workspaces.pickDirectory()` 的系统目录选择器)。临时工作区的文件会
83
+ **直接移进该文件夹**(不会额外创建「临时工作区」子文件夹),该文件夹本身成为永久工作区,
84
+ 并用**其文件夹名作为工作区标题**。同时**迁移对话**:把每个会话日志头行的 `cwd` 改写为该
85
+ 文件夹路径,并把日志目录移到新 `cwd` 的 projectKey 槽位。成功后弹出一个**不可关闭**的
86
+ 对话框,带 **「立即重启」** 按钮——它触发**真正的宿主级重启**(一个脱离的辅助进程按原
87
+ DSH 启动命令重新拉起,机制与 `dsh-market` 更新插件时一致),让注册表重新索引、迁移的对话
88
+ 出现在新工作区下。
89
+ - `confirmBeforeDelete` **关**:自动删除——启动时立即执行,或在 `deleteMode = 'delayed'`
90
+ 时等待 `deleteDelay` 秒后执行。
91
+
92
+ ## 清理是如何工作的
93
+
94
+ DSH **没有公开的「删除会话日志」API**——持久化层是 append-only。所以临时工作区的删除分为四步:
95
+
96
+ 0. **先拆在线会话** —— 如果宿主进程里还活着该工作区的会话(确认弹窗、延迟自动删除、或只刷新了页面而宿主并未真正重启时,删除都会在仍持有这些会话的宿主里运行),会尽力拆除它们:
97
+ 先取消 agent 并等它回到空闲(`whenIdle`,镜像 agent loop 自身的销毁顺序),再**先把待写内容落盘**
98
+ (`sessions.flush`),最后从会话存储中摘除该条目(发出 `session/disposed`,客户端立即关闭该对话)。
99
+ **先 flush 再摘除对多会话工作区至关重要**:摘除会触发 `session/disposed`,持久化协调器随之做一次
100
+ "退休刷新"(retire → flush → initFor),若会话还有未落盘的写入,这次刷新会在目录被删除后把日志
101
+ 重新物化出来——那些会话就会变成不可读取的 Ungrouped 残留。先 flush 后,退休刷新无内容可写,日志
102
+ 只会被删除一次。
103
+ 0b. **归档冷会话** —— 本轮宿主里从未被打开(未 attach)的会话只有磁盘日志,删日志后浏览器收不到
104
+ `session-removed` 事件,会在侧边栏残留成不可读取的 Ungrouped 条目。`archiveColdSessions` 在删日志
105
+ **之前**把它们逐一归档(`workspaceRegistry.archiveSession`):归档集变化会推送
106
+ `host/archived-sessions-changed`,侧边栏(分组、扁平列表、搜索)都会过滤归档 id,残留条目立即消失。
107
+ 归档留下的 archived-id 记录由启动清扫回收。
108
+ 1. **再删会话** —— 工作区所属的每个会话(其头行 `cwd` 等于工作区路径)通过
109
+ `ctx.sessionPersistence.locate` 定位,并用 `fs.rm` 删除其磁盘目录。
110
+ 2. **删注册** —— `ctx.workspaceRegistry.delete(id)` 删除工作区记录(本身不动日志/目录)。
111
+ 3. **删目录** —— 删除临时目录(位于 `<dsh-home>/temp-workspaces/<uuid>`)。
112
+
113
+ 插件用 `<dsh-home>/temp-workspaces/state.json` 记录临时工作区(持久化标记
114
+ `{ workspaceId, path, createdAt }`)。删除由该标记里的 `path` 驱动,所以重启前已通过
115
+ UI 删除注册记录的临时工作区**仍会清理残留目录**(修复了「残留文件夹」bug)——目录以
116
+ 标记路径(而非注册记录)为准。
117
+
118
+ **零残留**:删除时还会清理会话的**投影缓存行**(`session_projcache` 域,位于
119
+ `~/.dsh/storages/session_projcache.json`)与旧布局遗留的 per-session 缓存文件。
120
+ 启动时还有一次**孤儿残留清扫**(`pruneOrphanTempResidue`):凡 `cwd` 位于临时根目录下、
121
+ 且不再被标记引用的投影缓存行,连同其 archived-id 记录与遗留缓存文件一并删除。这些行
122
+ 永远不会把幽灵对话显示出来(身份校验后读不到),但"不留痕迹"就是不留痕迹;用户对真实
123
+ 会话的主动归档不会被触碰。
124
+
125
+ ## 目录结构
126
+
127
+ ```
128
+ temp-workspace-plugin/
129
+ package.json name, dsh.bundle.patch + dsh.client, exports
130
+ cordis.patch.yml 把该插件插入为宿主 Loader 条目
131
+ lib/index.js 节点半边 —— /temp-workspace/api 路由 + 设置 + 启动清理
132
+ src/client.js 浏览器半边源码(图标 + 设置卡片 + 确认弹窗)
133
+ lib/client.js 构建后的浏览器半边(生成文件,勿改)
134
+ build.mjs 把 src/client.js 包装 -> lib/client.js(HMR watch)
135
+ ```
136
+
137
+ ## 开发循环
138
+
139
+ ```sh
140
+ node build.mjs --watch
141
+ ```
142
+
143
+ `lib/client.js` 是 client-modules 提供、dsh-client-hmr 轮询的**唯一文件**;编辑
144
+ `src/client.js` 会热更新界面(无需刷新页面)。**宿主半边改动**(`lib/index.js`)需要重启
145
+ `dsh web`。
146
+
147
+ ## 接入 profile
148
+
149
+ ```sh
150
+ # 本地 checkout / clone(任意绝对或相对路径)
151
+ dsh plugin --profile web add /path/to/dsh-temp-workspace
152
+ ```
153
+
154
+ 这会把它加入 `dsh.profile.bundles`。下次启动 `dsh web` 时宿主组合该行、
155
+ 注册 `/temp-workspace/api` 路由、设置卡片进入 `__DSH_BOOT__`,并运行启动清理/确认流程。
156
+
157
+ ## API(`/temp-workspace/api/<method>`)
158
+
159
+ 读取用 `GET`,变更用 `POST`。
160
+
161
+ | 方法 | 返回 | 说明 |
162
+ | --- | --- | --- |
163
+ | `POST create` | `{ workspace, created: true }` | mkdir + `workspaceRegistry.create` + 标记 |
164
+ | `GET list` | `{ entries }` | 当前标记为临时的 `[{ workspaceId, path, createdAt }]` |
165
+ | `POST delete` | `{ ok: true }` | 立即删除一个临时工作区(会话 + 注册 + 目录) |
166
+ | `GET/POST config` | `{ ok, value }` | 读取设置,或 `POST {…}` 写入补丁 |
167
+ | `POST configReset` | `{ ok, value }` | 恢复所有设置为默认 |
168
+ | `GET pending` | `{ ok, value }` | 被挂起的工作区 + `confirmBeforeDelete` + `deleteAt` |
169
+ | `POST confirm` | `{ ok, deleted }` | 用户确认——删除被挂起的集合 |
170
+ | `POST keep` | `{ ok, kept }` | 临时保留——不动作(仍为临时) |
171
+ | `POST permanentKeep` | `{ ok, moved }` | `{ target, workspaceId? }` —— 把 `target`(所选文件夹)直接作为新工作区并去临时化;带 `workspaceId` 时只移动该工作区(省略则移动全部被挂起的) |
172
+ | `POST restart` | `{ restarting, pid, helperPid }` | 真正的宿主重启(仅限同源 loopback) |
173
+
174
+ ## 说明 / 局限
175
+
176
+ - 侧边栏工作区浏览器**没有**给头部动作用的 slot,所以图标是用 `MutationObserver`
177
+ 注入 DOM 的(与 `dsh-workspace-auto-approval` 同款写法)。若上游侧边栏布局改变,
178
+ 图标可能需要相应调整位置。
179
+ - 「添加工作区」按钮只在目录流槽位被占用时才渲染,因此临时工作区图标也只在此时出现。
180
+ - 清理时直接删除会话日志目录。**在线**会话会在删除时尽力拆除(取消 agent + 归档 + 从会话存储
181
+ 摘除),因此即使删除发生在仍持有这些会话的宿主进程里(确认弹窗、延迟删除、页面刷新),对话也会
182
+ 立即从界面消失;启动时没有在线会话,正是清理运行的时候。
183
+ - 临时工作区的目录位于 DSH home(`~/.dsh/temp-workspaces`),不在你的常规项目文件夹里。
184
+ - 浏览器插件没有通用的「询问用户」API,因此确认弹窗由插件自绘(轻量内联样式遮罩),
185
+ 通过自己的 `react-dom/client` root 渲染(不依赖 primitives 的 CSS-module 作用域,所以必定显示)。
186
+ - **永久保留**需要 `native` 目录选择器能力(`ctx.workspaces.pickDirectory()`);
187
+ 在远程/非 native 部署下该选择器不可用,按钮会被禁用。
188
+ - 对话迁移只改写每个会话日志的**头帧**(`.jsonl.zstd` 是一串独立压缩的 Zstandard 帧
189
+ 连接而成——帧 0 是带 `cwd` 的头行,其余是事件批次)。头帧的 `cwd` 被改写,每个事件帧
190
+ 逐字节保留,因此不会丢失任何对话内容。
191
+ - DSH 会话头中的 `cwd` 不可变,故采用改写方式。DSH 工作区注册表的「会话→工作区分组」
192
+ (`bootstrap`)**只在第一次启动时运行一次**;之后的启动只会重建 id→cwd 索引,不会把
193
+ 迁移后的会话加进新工作区的 `sessionIds`。为避免迁移的对话最终变成**未分组**,插件在
194
+ 移动时记录一条持久化的 **pending-attach** 列表,并在下次启动时把这些迁移会话重新挂载到
195
+ 新工作区——这也是永久保留后客户端提示重启的原因。
196
+
197
+ ## 不准备修复的Bug
198
+
199
+ - 受到Harness的限制,永久保留后对话有概率变成`Ungrouped`,已经尽力处理了,但是仍然有概率出问题
200
+ - 创建临时工作区后永久保留相关按钮不一定会立即出现,但是肯定会在一段时间后(2-3s后)出现的,不准备修复
201
+ - 受到Harness的限制,迁移过程完成后对话的名称有概率变成工作区的名称,点击后又变回原来的名称,不影响使用,不计划修复,自己改一下名应该就可以处理这个问题
202
+
203
+ ## 界面语言
204
+
205
+ 所有界面文案(图标按钮、行内“永久保留”按钮、设置卡片、确认/重启弹窗)都提供**中文与英文**
206
+ 两套字典,并跟随 DSH 的激活语言自动切换(`ctx.locale` + `locale.subscribe`),语言切换时
207
+ 已注入的按钮文案也会刷新。
@@ -0,0 +1,12 @@
1
+ # @wjj-8283/dsh-temp-workspace — bundle patch.
2
+ #
3
+ # Inserts this plugin as a host Loader entry. The host half injects webServer /
4
+ # webRuntime / workspaceRegistry / sessionPersistence and registers a fenced
5
+ # /temp-workspace/api route (create + delete + list) plus a boot cleanup pass
6
+ # that removes every workspace recorded as temporary (sessions first, then the
7
+ # registration, then the throwaway directory). The client half (dsh.client)
8
+ # injects a small icon beside the sidebar's "Add workspace" button; clicking it
9
+ # creates a temporary workspace and opens a new conversation in it.
10
+ - insert:
11
+ - id: dsh-temp-workspace
12
+ name: '@wjj-8283/dsh-temp-workspace'