dsh-sidebar-row-menu 1.4.1 → 1.4.3
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/CHANGELOG.md +40 -0
- package/README.en.md +40 -10
- package/README.md +31 -8
- package/lib/client.js +359 -31
- package/lib/host/route.js +67 -6
- package/lib/host/workspace-delete.js +101 -0
- package/lib/index.js +12 -8
- package/package.json +6 -5
- package/scripts/marketplace-preflight.mjs +7 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.4.3 — 2026-11-04
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- **Declared the supported DSH range**: `package.json` now carries
|
|
8
|
+
`engines.dsh: ">=0.2.1-rc.5 <0.3.0-0"`. DSH refuses to install or activate the
|
|
9
|
+
plugin on a host outside that range, and the plugin market lists the range on the
|
|
10
|
+
card instead of treating the plugin as undeclared. No runtime code changed.
|
|
11
|
+
|
|
12
|
+
### 中文摘要
|
|
13
|
+
|
|
14
|
+
- 在 `package.json` 中声明 `engines.dsh: ">=0.2.1-rc.5 <0.3.0-0"`:范围外的 DSH 会拒绝
|
|
15
|
+
安装/激活,插件市场会显示支持版本,不再当作“未声明”。运行代码无改动。
|
|
16
|
+
|
|
17
|
+
## 1.4.2 — 2026-11-03
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- **Delete workspace and all sessions**: a new workspace row entry removes every
|
|
22
|
+
session transcript under the workspace first, then drops the workspace
|
|
23
|
+
registration. The built-in "Delete workspace" is unchanged and still keeps
|
|
24
|
+
sessions under "Ungrouped". If any session fails to delete, the workspace is kept
|
|
25
|
+
so the user can retry. Backed by a new `POST /dsh-sidebar-row-menu/workspace/delete`
|
|
26
|
+
route.
|
|
27
|
+
- **Also show in the "…" menu**: every plugin right-click entry can now be added
|
|
28
|
+
individually to the row's native "…" menu (Settings → "Row menu" → "Also show in
|
|
29
|
+
the "…" menu"). Session entries mount through the existing session slot;
|
|
30
|
+
workspace entries make the plugin render the "…" menu with the official
|
|
31
|
+
Rename / Delete workspace items plus the enabled entries. All switches are off by
|
|
32
|
+
default, so the native menu is untouched until opted in.
|
|
33
|
+
|
|
34
|
+
### 中文摘要
|
|
35
|
+
|
|
36
|
+
- 工作区右键新增「删除工作区及所有会话」:先逐个永久删除会话日志,全部成功后再删除
|
|
37
|
+
工作区登记;任一会话失败则保留工作区便于重试。内置「删除工作区」行为不变(会话归入
|
|
38
|
+
「未分组」)。新增宿主路由 `POST /dsh-sidebar-row-menu/workspace/delete`。
|
|
39
|
+
- 设置 →「行菜单」新增「同时放进「…」菜单」:可逐项把插件右键条目加进行尾「…」菜单。
|
|
40
|
+
会话条目走原生 slot;工作区开启任意一项后由插件接管「…」按钮,保留官方「重命名 /
|
|
41
|
+
删除工作区」。全部默认关闭,未开启时「…」菜单与 DSH 原样一致。
|
|
42
|
+
|
|
3
43
|
## 1.4.1 — 2026-11-03
|
|
4
44
|
|
|
5
45
|
### Added
|
package/README.en.md
CHANGED
|
@@ -39,7 +39,8 @@ or hardcode any built-in action.
|
|
|
39
39
|
| Open with | A submenu of external apps (editors, terminals, file managers) sharing the session menu's catalog, pinning and toggles; hover expands it |
|
|
40
40
|
| Copy workspace path | Copies the directory path |
|
|
41
41
|
| Open in new tab | Only when `dsh-project-tabs` (exposing `openWorkspaceInTab`) is installed |
|
|
42
|
-
| Delete workspace | Opens the official delete confirmation |
|
|
42
|
+
| Delete workspace | Opens the official delete confirmation; removes only the workspace registration and leaves its sessions under "Ungrouped" |
|
|
43
|
+
| Delete workspace and all sessions | Permanently deletes every session and transcript under the workspace first, then the workspace registration. If any session fails, the workspace is kept so the user can retry. The directory itself is not deleted |
|
|
43
44
|
|
|
44
45
|
### Session row
|
|
45
46
|
|
|
@@ -67,9 +68,16 @@ Both delete entries are served by this plugin's own host route. They leave
|
|
|
67
68
|
unarchived sessions alone by default; the opt-in switch extends the single-session
|
|
68
69
|
entry to them. Deletion never cascades into forked child sessions.
|
|
69
70
|
|
|
70
|
-
These entries are **right-click only**: they do not mount when the "…"
|
|
71
|
-
the menu. A short-lived window flag is set before opening; outside that
|
|
72
|
-
entries render `null`.
|
|
71
|
+
These entries are **right-click only by default**: they do not mount when the "…"
|
|
72
|
+
button opens the menu. A short-lived window flag is set before opening; outside that
|
|
73
|
+
window the entries render `null`.
|
|
74
|
+
|
|
75
|
+
To use them from the row's "…" button, enable them one by one under Settings →
|
|
76
|
+
"Row menu" → "Also show in the "…" menu". Session entries mount into the native
|
|
77
|
+
"…" menu; enabling any workspace entry makes this plugin take over the row's "…"
|
|
78
|
+
button and render the official "Rename / Delete workspace" items alongside the
|
|
79
|
+
enabled plugin entries. With everything off, the "…" button behaves exactly like
|
|
80
|
+
DSH's own.
|
|
73
81
|
|
|
74
82
|
Blank draft sessions have no native "…" menu and are left alone. Rows that are
|
|
75
83
|
neither a workspace nor a session (e.g. "Show more") are not intercepted.
|
|
@@ -85,9 +93,15 @@ Settings → "Row menu" toggles every new entry:
|
|
|
85
93
|
| Delete session | Hides the single-session delete entry in session menus |
|
|
86
94
|
| Allow deleting unarchived sessions | Off by default; also shows "Delete session permanently" on unarchived rows. Requires the previous switch |
|
|
87
95
|
| Delete all archived sessions | Hides the bulk-delete entry |
|
|
96
|
+
| Also show in the "…" menu → workspace | Adds workspace right-click entries one by one to the row's "…" menu: New session, Open with, Copy workspace path, Open in new tab, Delete workspace and all sessions. Enabling any of them makes this plugin render the "…" menu |
|
|
97
|
+
| Also show in the "…" menu → session | Adds session right-click entries one by one to the native "…" menu: Open with, Delete session, Delete all archived sessions |
|
|
98
|
+
|
|
99
|
+
The "Also show in the "…" menu" switches are all off by default; until one is
|
|
100
|
+
enabled the "…" menu is exactly DSH's own.
|
|
88
101
|
|
|
89
102
|
Switches live in browser `localStorage` (`dsh.sidebar-row-menu.items.v1`,
|
|
90
|
-
`dsh.sidebar-row-menu.open-with.v1
|
|
103
|
+
`dsh.sidebar-row-menu.open-with.v1`, `dsh.sidebar-row-menu.open-with.pinned.v1` and
|
|
104
|
+
`dsh.sidebar-row-menu.ellipsis.v1`)
|
|
91
105
|
and apply immediately. Before the first explicit pin, the plugin derives one: the
|
|
92
106
|
first detected editor (usually VS Code), or the first catalog app when no editor
|
|
93
107
|
exists.
|
|
@@ -126,13 +140,13 @@ resolve it directly by name:
|
|
|
126
140
|
Pin a version by appending it to the name:
|
|
127
141
|
|
|
128
142
|
```
|
|
129
|
-
dsh-sidebar-row-menu@1.4.
|
|
143
|
+
dsh-sidebar-row-menu@1.4.2
|
|
130
144
|
```
|
|
131
145
|
|
|
132
146
|
Equivalent command line:
|
|
133
147
|
|
|
134
148
|
```powershell
|
|
135
|
-
dsh plugin --profile desktop add dsh-sidebar-row-menu@1.4.
|
|
149
|
+
dsh plugin --profile desktop add dsh-sidebar-row-menu@1.4.2
|
|
136
150
|
```
|
|
137
151
|
|
|
138
152
|
### Install from GitHub
|
|
@@ -152,13 +166,13 @@ repository is already runnable, so **no local build is required**.
|
|
|
152
166
|
That URL tracks the latest commit on `main`. Pin a tag for a stable version:
|
|
153
167
|
|
|
154
168
|
```
|
|
155
|
-
https://github.com/TLEWA/dsh-sidebar-row-menu#v1.4.
|
|
169
|
+
https://github.com/TLEWA/dsh-sidebar-row-menu#v1.4.2
|
|
156
170
|
```
|
|
157
171
|
|
|
158
172
|
Or install the tarball built by Actions directly, without Git:
|
|
159
173
|
|
|
160
174
|
```
|
|
161
|
-
https://github.com/TLEWA/dsh-sidebar-row-menu/releases/download/v1.4.
|
|
175
|
+
https://github.com/TLEWA/dsh-sidebar-row-menu/releases/download/v1.4.2/dsh-sidebar-row-menu-1.4.2.tgz
|
|
162
176
|
```
|
|
163
177
|
|
|
164
178
|
Equivalent command line:
|
|
@@ -290,8 +304,9 @@ lib/index.js host entry: name / inject / apply plus re-exported helpers
|
|
|
290
304
|
lib/host/security.js loopback request fence
|
|
291
305
|
lib/host/paths.js path validation, vscode://file encoding
|
|
292
306
|
lib/host/launch.js VS Code / Explorer launching
|
|
293
|
-
lib/host/route.js /open and /
|
|
307
|
+
lib/host/route.js /open, /session/delete and /workspace/delete routes, HTTP status codes
|
|
294
308
|
lib/host/session-delete.js transcript verification and deletion (detach / rm / re-check)
|
|
309
|
+
lib/host/workspace-delete.js workspace + all sessions deletion, workspace kept on failure
|
|
295
310
|
lib/client.js browser half: fiber lookup, slot registration, menu mount
|
|
296
311
|
cordis.patch.yml bundle patch inserting the plugin into the profile tree
|
|
297
312
|
scripts/install.ps1 pnpm-free install / uninstall
|
|
@@ -387,11 +402,26 @@ the row's existing menu.
|
|
|
387
402
|
- partial failures return `200` + `{ ok:false, deleted, failed, error }`; the dialog
|
|
388
403
|
stays open and reports the counts.
|
|
389
404
|
|
|
405
|
+
`POST /dsh-sidebar-row-menu/workspace/delete`, body `{ workspaceId: string }`.
|
|
406
|
+
|
|
407
|
+
- same loopback / POST / 64 KB body checks; `workspaceId` must be a non-empty string;
|
|
408
|
+
- reads `workspaceRegistry.get(workspaceId).sessionIds` and permanently deletes each
|
|
409
|
+
session with the flow above;
|
|
410
|
+
- only when every session was deleted (including sessions that were already gone)
|
|
411
|
+
does it call `workspaceRegistry.delete()`; any failure returns `200` + `ok:false`
|
|
412
|
+
and keeps the workspace, so no orphaned "Ungrouped" sessions are left behind;
|
|
413
|
+
- an unknown workspace returns `ok:true, missing:true` and is not an error.
|
|
414
|
+
|
|
390
415
|
"Open with" does not touch this plugin's host: it calls DSH's built-in
|
|
391
416
|
`open-in-app/apps`, `open-in-app/open` and `open-in-app/icon/<id>` directly.
|
|
392
417
|
|
|
393
418
|
## Compatibility
|
|
394
419
|
|
|
420
|
+
The package declares its supported DSH range in `package.json` as
|
|
421
|
+
`engines.dsh`: `>=0.2.1-rc.5 <0.3.0-0`. The DSH host uses it to refuse
|
|
422
|
+
incompatible installs, and the plugin market shows it as the supported version;
|
|
423
|
+
0.3 and later need re-verification before the upper bound is raised.
|
|
424
|
+
|
|
395
425
|
Built against DSH client `0.2.1-rc.5` (`ProjectRowItem`, `SessionNodeItem`,
|
|
396
426
|
`Menu`). If a DSH upgrade changes those row components or the menu positioning:
|
|
397
427
|
|
package/README.md
CHANGED
|
@@ -33,7 +33,8 @@
|
|
|
33
33
|
| 打开方式 | 二级菜单列出宿主提供的外部应用(编辑器、终端、文件管理器);与会话菜单共用同一套应用列表、置顶与开关设置,鼠标悬浮自动展开 |
|
|
34
34
|
| 复制工作区路径 | 复制目录路径到剪贴板 |
|
|
35
35
|
| 在新标签中打开 | 仅在装有 `dsh-project-tabs`(暴露 `openWorkspaceInTab`)时出现 |
|
|
36
|
-
| 删除工作区 |
|
|
36
|
+
| 删除工作区 | 打开官方删除确认对话框;只删除工作区登记,会话保留并归入「未分组」 |
|
|
37
|
+
| 删除工作区及所有会话 | 先永久删除该工作区下的每个会话及其日志,全部成功后再删除工作区登记;任一会话删除失败则保留工作区,便于重试。工作区目录本身不删除 |
|
|
37
38
|
|
|
38
39
|
### 会话行右键
|
|
39
40
|
|
|
@@ -59,9 +60,13 @@
|
|
|
59
60
|
一级菜单。两个删除入口由本插件自己的宿主路由完成,默认不会删除未归档会话;
|
|
60
61
|
开启「允许删除未归档会话」后才会对普通会话开放。删除不会级联删除 fork 子会话。
|
|
61
62
|
|
|
62
|
-
|
|
63
|
+
这些条目默认是**右键专属**:点行尾「…」按钮时它们不挂载。实现上使用一个只在右键展开瞬间
|
|
63
64
|
有效的窗口标记,窗口外的渲染直接返回 `null`。
|
|
64
65
|
|
|
66
|
+
如果想直接使用行尾「…」菜单,可以在设置 →「行菜单」→「同时放进「…」菜单」里逐个开启:
|
|
67
|
+
会话条目会挂进原生「…」菜单;工作区行开启任意一项后,本插件会接管行尾「…」按钮,用同一套
|
|
68
|
+
菜单展示官方「重命名 / 删除工作区」加上已开启的插件条目。全部关闭时「…」按钮保持 DSH 原样。
|
|
69
|
+
|
|
65
70
|
草稿会话(还没有任何消息的空白会话)没有内置「…」菜单,本插件同样不接管。
|
|
66
71
|
非工作区 / 非会话的侧边栏行(如“显示更多”)也不会被拦截。
|
|
67
72
|
|
|
@@ -76,9 +81,14 @@
|
|
|
76
81
|
| 永久删除会话 | 关闭后会话菜单里不再出现单条删除入口 |
|
|
77
82
|
| 允许删除未归档会话 | 默认关闭;开启后未归档会话的右键菜单也会出现「永久删除会话」,需先开启上一项 |
|
|
78
83
|
| 删除所有已归档会话 | 关闭后不再出现批量删除入口 |
|
|
84
|
+
| 同时放进「…」菜单 → 工作区 | 逐项把工作区行右键条目加进行尾「…」菜单:新会话、打开方式、复制工作区路径、在新标签中打开、删除工作区及所有会话。开启任意一项后「…」按钮改由本插件渲染 |
|
|
85
|
+
| 同时放进「…」菜单 → 会话 | 逐项把会话行右键条目加进原生「…」菜单:打开方式、永久删除会话、删除所有已归档会话 |
|
|
86
|
+
|
|
87
|
+
「同时放进「…」菜单」下的开关默认全部关闭,`…` 菜单在未开启时与 DSH 完全一致。
|
|
79
88
|
|
|
80
89
|
开关存在浏览器 `localStorage`(`dsh.sidebar-row-menu.items.v1`、
|
|
81
|
-
`dsh.sidebar-row-menu.open-with.v1
|
|
90
|
+
`dsh.sidebar-row-menu.open-with.v1`、`dsh.sidebar-row-menu.open-with.pinned.v1` 与
|
|
91
|
+
`dsh.sidebar-row-menu.ellipsis.v1`),
|
|
82
92
|
改完立即生效,不需要重启。置顶项在第一次配置前是自动推导的:优先置顶检测到的第一个
|
|
83
93
|
编辑器(通常是 VS Code),没有编辑器时置顶应用列表里的第一个。
|
|
84
94
|
|
|
@@ -113,13 +123,13 @@
|
|
|
113
123
|
要固定版本,在包名后加版本号:
|
|
114
124
|
|
|
115
125
|
```
|
|
116
|
-
dsh-sidebar-row-menu@1.4.
|
|
126
|
+
dsh-sidebar-row-menu@1.4.2
|
|
117
127
|
```
|
|
118
128
|
|
|
119
129
|
命令行等价写法:
|
|
120
130
|
|
|
121
131
|
```powershell
|
|
122
|
-
dsh plugin --profile desktop add dsh-sidebar-row-menu@1.4.
|
|
132
|
+
dsh plugin --profile desktop add dsh-sidebar-row-menu@1.4.2
|
|
123
133
|
```
|
|
124
134
|
|
|
125
135
|
### 从 GitHub 安装
|
|
@@ -139,13 +149,13 @@ DSH 桌面端通过 pnpm 安装 Git 依赖;本仓库的 `lib/` 已是可直接
|
|
|
139
149
|
该地址跟随 `main` 的最新提交。要固定版本,在地址后加标签:
|
|
140
150
|
|
|
141
151
|
```
|
|
142
|
-
https://github.com/TLEWA/dsh-sidebar-row-menu#v1.4.
|
|
152
|
+
https://github.com/TLEWA/dsh-sidebar-row-menu#v1.4.2
|
|
143
153
|
```
|
|
144
154
|
|
|
145
155
|
也可以直接安装 Actions 打出的压缩包(不经过 Git):
|
|
146
156
|
|
|
147
157
|
```
|
|
148
|
-
https://github.com/TLEWA/dsh-sidebar-row-menu/releases/download/v1.4.
|
|
158
|
+
https://github.com/TLEWA/dsh-sidebar-row-menu/releases/download/v1.4.2/dsh-sidebar-row-menu-1.4.2.tgz
|
|
149
159
|
```
|
|
150
160
|
|
|
151
161
|
命令行等价写法:
|
|
@@ -260,8 +270,9 @@ lib/index.js 宿主入口:name / inject / apply,再导出可测试
|
|
|
260
270
|
lib/host/security.js loopback 请求围栏
|
|
261
271
|
lib/host/paths.js 路径校验、vscode://file 编码
|
|
262
272
|
lib/host/launch.js VS Code / 资源管理器启动
|
|
263
|
-
lib/host/route.js /open 与 /
|
|
273
|
+
lib/host/route.js /open、/session/delete 与 /workspace/delete 路由、HTTP 状态码
|
|
264
274
|
lib/host/session-delete.js 会话日志目录的校验与删除(detach / rm / 复查)
|
|
275
|
+
lib/host/workspace-delete.js 删除工作区及其所有会话;会话失败时保留工作区
|
|
265
276
|
lib/client.js 客户端半边:fiber 解析、slot 注册、菜单挂载
|
|
266
277
|
cordis.patch.yml bundle patch:把插件插进 profile 插件树
|
|
267
278
|
scripts/install.ps1 无 pnpm 的安装 / 卸载脚本
|
|
@@ -341,11 +352,23 @@ test/*.test.mjs node:test
|
|
|
341
352
|
`api-session/removed`;
|
|
342
353
|
- 部分失败返回 `200` + `{ ok:false, deleted, failed, error }`,前端保留对话框并显示计数。
|
|
343
354
|
|
|
355
|
+
`POST /dsh-sidebar-row-menu/workspace/delete`,body `{ workspaceId: string }`。
|
|
356
|
+
|
|
357
|
+
- 同样的 loopback / POST / 64 KB body 校验;`workspaceId` 必须是非空字符串;
|
|
358
|
+
- 先读取 `workspaceRegistry.get(workspaceId).sessionIds`,用上面的会话删除流程逐个永久删除;
|
|
359
|
+
- 只有全部会话删除成功(包括本来就不存在的会话)才调用 `workspaceRegistry.delete()`;
|
|
360
|
+
任一会话失败返回 `200` + `ok:false` 并保留工作区,避免留下无人管理的「未分组」会话;
|
|
361
|
+
- 未知工作区返回 `ok:true, missing:true`,不视为错误。
|
|
362
|
+
|
|
344
363
|
「打开方式」不经过本插件宿主:直接调用 DSH 内置的 `open-in-app/apps`、
|
|
345
364
|
`open-in-app/open`、`open-in-app/icon/<id>`。
|
|
346
365
|
|
|
347
366
|
## 兼容性
|
|
348
367
|
|
|
368
|
+
本包在 `package.json` 的 `engines.dsh` 中声明支持范围:`>=0.2.1-rc.5 <0.3.0-0`。
|
|
369
|
+
DSH 宿主按该范围拒绝安装不兼容的宿主,插件市场也按它显示支持版本;0.3 起需要重新验证后
|
|
370
|
+
再放宽上限。
|
|
371
|
+
|
|
349
372
|
按 DSH 客户端 `0.2.1-rc.5` 的 `ProjectRowItem` / `SessionNodeItem` / `Menu` 实现。
|
|
350
373
|
DSH 升级若改动行组件或菜单定位实现:
|
|
351
374
|
|
package/lib/client.js
CHANGED
|
@@ -90,6 +90,8 @@ window.__ModuleLoader__.load({
|
|
|
90
90
|
}
|
|
91
91
|
/** 永久删除会话的宿主端点;是否对未归档会话开放由本地开关决定。 */
|
|
92
92
|
const DELETE_ENDPOINT = '/dsh-sidebar-row-menu/session/delete';
|
|
93
|
+
/** 删除「工作区 + 其下全部会话」的宿主端点。 */
|
|
94
|
+
const WORKSPACE_DELETE_ENDPOINT = '/dsh-sidebar-row-menu/workspace/delete';
|
|
93
95
|
/** 官方 open-in-app 的三条路由;用文档相对路径让浏览器自带认证。 */
|
|
94
96
|
const APPS_ROUTE = 'open-in-app/apps';
|
|
95
97
|
const OPEN_ROUTE = 'open-in-app/open';
|
|
@@ -115,6 +117,22 @@ window.__ModuleLoader__.load({
|
|
|
115
117
|
deleteUnarchived: false,
|
|
116
118
|
};
|
|
117
119
|
const ITEM_KEYS = Object.keys(ITEM_DEFAULTS);
|
|
120
|
+
/**
|
|
121
|
+
* 「右键专属条目是否也放进行尾…菜单」的逐项开关。
|
|
122
|
+
* 全部默认关闭:不勾选时点「…」看到的仍是 DSH 原生菜单。
|
|
123
|
+
*/
|
|
124
|
+
const ELLIPSIS_KEY = 'dsh.sidebar-row-menu.ellipsis.v1';
|
|
125
|
+
const ELLIPSIS_DEFAULTS = {
|
|
126
|
+
newSession: false,
|
|
127
|
+
openWith: false,
|
|
128
|
+
copyPath: false,
|
|
129
|
+
openInTab: false,
|
|
130
|
+
deleteSessions: false,
|
|
131
|
+
sessionOpenWith: false,
|
|
132
|
+
sessionDelete: false,
|
|
133
|
+
sessionDeleteArchived: false,
|
|
134
|
+
};
|
|
135
|
+
const ELLIPSIS_KEYS = Object.keys(ELLIPSIS_DEFAULTS);
|
|
118
136
|
/**
|
|
119
137
|
* dsh-session-menu 注册的同一能力条目 id。检测到对方存在时,本插件对应项
|
|
120
138
|
* 自动隐藏,避免同一份菜单里出现两条一模一样的动作。
|
|
@@ -135,6 +153,7 @@ window.__ModuleLoader__.load({
|
|
|
135
153
|
copyPath: '复制工作区路径',
|
|
136
154
|
openInTab: '在新标签中打开',
|
|
137
155
|
deleteWorkspace: '删除工作区',
|
|
156
|
+
deleteWorkspaceSessions: '删除工作区及所有会话',
|
|
138
157
|
copied: '已复制',
|
|
139
158
|
copyFailed: '复制失败,请重试',
|
|
140
159
|
noPath: '没有可用的目录路径',
|
|
@@ -166,6 +185,26 @@ window.__ModuleLoader__.load({
|
|
|
166
185
|
'settings.appsAllOff': '所有应用都被二级开关隐藏了',
|
|
167
186
|
'settings.expand': '展开应用列表',
|
|
168
187
|
'settings.collapse': '收起应用列表',
|
|
188
|
+
'settings.ellipsisTitle': '同时放进「…」菜单',
|
|
189
|
+
'settings.ellipsisHint': '勾选后,对应条目会额外出现在行尾「…」按钮的菜单里,并沿用上面的单项开关;不勾选则只在右键菜单出现。',
|
|
190
|
+
'settings.ellipsisWorkspace': '工作区行',
|
|
191
|
+
'settings.ellipsisSession': '会话行',
|
|
192
|
+
'settings.ellipsis.newSession.label': '新建会话',
|
|
193
|
+
'settings.ellipsis.newSession.hint': '在「…」菜单里直接新建该工作区的会话。',
|
|
194
|
+
'settings.ellipsis.openWith.label': '打开方式',
|
|
195
|
+
'settings.ellipsis.openWith.hint': '在「…」菜单里用外部应用打开工作区或会话目录。',
|
|
196
|
+
'settings.ellipsis.copyPath.label': '拷贝工作区路径',
|
|
197
|
+
'settings.ellipsis.copyPath.hint': '在「…」菜单里复制工作区目录路径。',
|
|
198
|
+
'settings.ellipsis.openInTab.label': '在新标签页打开',
|
|
199
|
+
'settings.ellipsis.openInTab.hint': '在「…」菜单里把工作区作为标签页打开。',
|
|
200
|
+
'settings.ellipsis.deleteSessions.label': '删除工作区及所有会话',
|
|
201
|
+
'settings.ellipsis.deleteSessions.hint': '在「…」菜单里同时清掉工作区记录和它下面的全部会话。',
|
|
202
|
+
'settings.ellipsis.sessionOpenWith.label': '打开方式',
|
|
203
|
+
'settings.ellipsis.sessionOpenWith.hint': '在「…」菜单里用外部应用打开会话目录。',
|
|
204
|
+
'settings.ellipsis.sessionDelete.label': '永久删除会话',
|
|
205
|
+
'settings.ellipsis.sessionDelete.hint': '在「…」菜单里永久删除这一个会话及其日志。',
|
|
206
|
+
'settings.ellipsis.sessionDeleteArchived.label': '删除所有已归档会话',
|
|
207
|
+
'settings.ellipsis.sessionDeleteArchived.hint': '在已归档会话的「…」菜单里一次清理全部归档会话。',
|
|
169
208
|
'delete.title': '永久删除会话?',
|
|
170
209
|
'delete.description': '会话「{name}」的日志将从磁盘永久删除,此操作无法撤销。',
|
|
171
210
|
'delete.archivedTitle': '删除所有已归档会话?',
|
|
@@ -180,6 +219,14 @@ window.__ModuleLoader__.load({
|
|
|
180
219
|
'delete.unavailable': '删除服务不可用',
|
|
181
220
|
'delete.done': '已删除会话',
|
|
182
221
|
'delete.doneMany': '已删除 {n} 个会话',
|
|
222
|
+
'delete.workspaceTitle': '删除工作区及所有会话?',
|
|
223
|
+
'delete.workspaceDescription': '工作区「{name}」及其下 {n} 个会话的日志将被永久删除,目录本身保留。此操作无法撤销。',
|
|
224
|
+
'delete.workspaceEmptyDescription': '工作区「{name}」下没有会话,将只删除工作区记录。',
|
|
225
|
+
'delete.workspaceConfirm': '全部删除',
|
|
226
|
+
'delete.workspacePending': '正在删除工作区及会话…',
|
|
227
|
+
'delete.workspacePartial': '已删除 {done} 个会话,失败 {failed} 个;工作区已保留。',
|
|
228
|
+
'delete.workspaceDone': '已删除工作区及 {n} 个会话',
|
|
229
|
+
'delete.workspaceEmptyDone': '已删除工作区',
|
|
183
230
|
};
|
|
184
231
|
const en = {
|
|
185
232
|
newSession: 'New session',
|
|
@@ -189,6 +236,7 @@ window.__ModuleLoader__.load({
|
|
|
189
236
|
copyPath: 'Copy workspace path',
|
|
190
237
|
openInTab: 'Open in new tab',
|
|
191
238
|
deleteWorkspace: 'Delete workspace',
|
|
239
|
+
deleteWorkspaceSessions: 'Delete workspace and all sessions',
|
|
192
240
|
copied: 'Copied',
|
|
193
241
|
copyFailed: 'Copy failed, please retry',
|
|
194
242
|
noPath: 'No usable directory path',
|
|
@@ -220,6 +268,26 @@ window.__ModuleLoader__.load({
|
|
|
220
268
|
'settings.appsAllOff': 'Every application is hidden by the switches above',
|
|
221
269
|
'settings.expand': 'Show applications',
|
|
222
270
|
'settings.collapse': 'Hide applications',
|
|
271
|
+
'settings.ellipsisTitle': 'Also show in the "…" menu',
|
|
272
|
+
'settings.ellipsisHint': 'Checked entries also appear in the row\'s "…" button menu, still subject to the switches above. Unchecked entries stay right-click only.',
|
|
273
|
+
'settings.ellipsisWorkspace': 'Workspace rows',
|
|
274
|
+
'settings.ellipsisSession': 'Session rows',
|
|
275
|
+
'settings.ellipsis.newSession.label': 'New session',
|
|
276
|
+
'settings.ellipsis.newSession.hint': 'Create a session for this workspace from the "…" menu.',
|
|
277
|
+
'settings.ellipsis.openWith.label': 'Open with',
|
|
278
|
+
'settings.ellipsis.openWith.hint': 'Open the workspace or session directory with an external app from the "…" menu.',
|
|
279
|
+
'settings.ellipsis.copyPath.label': 'Copy workspace path',
|
|
280
|
+
'settings.ellipsis.copyPath.hint': 'Copy the workspace directory path from the "…" menu.',
|
|
281
|
+
'settings.ellipsis.openInTab.label': 'Open in new tab',
|
|
282
|
+
'settings.ellipsis.openInTab.hint': 'Open the workspace as a tab from the "…" menu.',
|
|
283
|
+
'settings.ellipsis.deleteSessions.label': 'Delete workspace and all sessions',
|
|
284
|
+
'settings.ellipsis.deleteSessions.hint': 'Remove the workspace record and every session under it from the "…" menu.',
|
|
285
|
+
'settings.ellipsis.sessionOpenWith.label': 'Open with',
|
|
286
|
+
'settings.ellipsis.sessionOpenWith.hint': 'Open the session directory with an external app from the "…" menu.',
|
|
287
|
+
'settings.ellipsis.sessionDelete.label': 'Delete session permanently',
|
|
288
|
+
'settings.ellipsis.sessionDelete.hint': 'Permanently delete this session and its log from the "…" menu.',
|
|
289
|
+
'settings.ellipsis.sessionDeleteArchived.label': 'Delete all archived sessions',
|
|
290
|
+
'settings.ellipsis.sessionDeleteArchived.hint': 'Clean up every archived session from an archived row\'s "…" menu.',
|
|
223
291
|
'delete.title': 'Delete this session permanently?',
|
|
224
292
|
'delete.description': 'The log of "{name}" will be removed from disk. This cannot be undone.',
|
|
225
293
|
'delete.archivedTitle': 'Delete all archived sessions?',
|
|
@@ -234,6 +302,14 @@ window.__ModuleLoader__.load({
|
|
|
234
302
|
'delete.unavailable': 'The delete service is unavailable',
|
|
235
303
|
'delete.done': 'Session deleted',
|
|
236
304
|
'delete.doneMany': 'Deleted {n} sessions',
|
|
305
|
+
'delete.workspaceTitle': 'Delete the workspace and all its sessions?',
|
|
306
|
+
'delete.workspaceDescription': 'The workspace "{name}" and the logs of its {n} sessions will be permanently deleted; the directory itself is kept. This cannot be undone.',
|
|
307
|
+
'delete.workspaceEmptyDescription': 'The workspace "{name}" has no sessions; only the workspace record will be deleted.',
|
|
308
|
+
'delete.workspaceConfirm': 'Delete all',
|
|
309
|
+
'delete.workspacePending': 'Deleting the workspace and sessions…',
|
|
310
|
+
'delete.workspacePartial': 'Deleted {done} sessions, failed {failed}; the workspace was kept.',
|
|
311
|
+
'delete.workspaceDone': 'Deleted the workspace and {n} sessions',
|
|
312
|
+
'delete.workspaceEmptyDone': 'Workspace deleted',
|
|
237
313
|
};
|
|
238
314
|
|
|
239
315
|
/** 官方 open-in-app 的应用 id → 显示名。官方 locale 没有这张表。 */
|
|
@@ -355,6 +431,8 @@ window.__ModuleLoader__.load({
|
|
|
355
431
|
/** 当前挂着的菜单宿主与 React root。 */
|
|
356
432
|
let menuHost = null;
|
|
357
433
|
let menuRoot = null;
|
|
434
|
+
/** 当前「…」菜单锚定的按钮;用来判断再次点击是否应收起。 */
|
|
435
|
+
let ellipsisTrigger = null;
|
|
358
436
|
/** 客户端根上下文(apply 时记下,右键时用来取服务 / 文案)。 */
|
|
359
437
|
let rootCtx = null;
|
|
360
438
|
|
|
@@ -375,6 +453,7 @@ window.__ModuleLoader__.load({
|
|
|
375
453
|
const root = menuRoot;
|
|
376
454
|
menuHost = null;
|
|
377
455
|
menuRoot = null;
|
|
456
|
+
ellipsisTrigger = null;
|
|
378
457
|
unmountAt(host, root);
|
|
379
458
|
}
|
|
380
459
|
|
|
@@ -559,6 +638,76 @@ window.__ModuleLoader__.load({
|
|
|
559
638
|
return itemSettings[key] ?? ITEM_DEFAULTS[key] ?? false;
|
|
560
639
|
}
|
|
561
640
|
|
|
641
|
+
/* -------------------------------- 「…」菜单附加开关(localStorage) */
|
|
642
|
+
|
|
643
|
+
function readStoredEllipsis() {
|
|
644
|
+
const defaults = { ...ELLIPSIS_DEFAULTS };
|
|
645
|
+
try {
|
|
646
|
+
const raw = globalThis.localStorage?.getItem(ELLIPSIS_KEY);
|
|
647
|
+
if (typeof raw !== 'string' || raw === '') return defaults;
|
|
648
|
+
const parsed = JSON.parse(raw);
|
|
649
|
+
if (parsed === null || typeof parsed !== 'object') return defaults;
|
|
650
|
+
for (const key of ELLIPSIS_KEYS) {
|
|
651
|
+
if (typeof parsed[key] === 'boolean') defaults[key] = parsed[key];
|
|
652
|
+
}
|
|
653
|
+
} catch {
|
|
654
|
+
/* 坏数据回退默认 */
|
|
655
|
+
}
|
|
656
|
+
return defaults;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
let ellipsisSettings = readStoredEllipsis();
|
|
660
|
+
const ellipsisListeners = new Set();
|
|
661
|
+
|
|
662
|
+
function publishEllipsis() {
|
|
663
|
+
for (const listener of ellipsisListeners) {
|
|
664
|
+
try {
|
|
665
|
+
listener();
|
|
666
|
+
} catch {
|
|
667
|
+
/* ignore */
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function subscribeEllipsis(listener) {
|
|
673
|
+
ellipsisListeners.add(listener);
|
|
674
|
+
return () => {
|
|
675
|
+
ellipsisListeners.delete(listener);
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function ellipsisSnapshot() {
|
|
680
|
+
return ellipsisSettings;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function useEllipsis() {
|
|
684
|
+
return useSyncExternalStore(subscribeEllipsis, ellipsisSnapshot, ellipsisSnapshot);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function setEllipsis(key, enabled) {
|
|
688
|
+
if (!ELLIPSIS_KEYS.includes(key)) return;
|
|
689
|
+
ellipsisSettings = { ...ellipsisSettings, [key]: enabled === true };
|
|
690
|
+
try {
|
|
691
|
+
globalThis.localStorage?.setItem(ELLIPSIS_KEY, JSON.stringify(ellipsisSettings));
|
|
692
|
+
} catch {
|
|
693
|
+
/* 隐私模式写不进去也照样生效到本次会话 */
|
|
694
|
+
}
|
|
695
|
+
publishEllipsis();
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function ellipsisOn(key) {
|
|
699
|
+
return ellipsisSettings[key] === true;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/** 工作区行是否至少有一项要在「…」菜单里显示(否则完全不接管原生菜单)。 */
|
|
703
|
+
function workspaceEllipsisOn() {
|
|
704
|
+
return ellipsisSettings.newSession === true
|
|
705
|
+
|| ellipsisSettings.openWith === true
|
|
706
|
+
|| ellipsisSettings.copyPath === true
|
|
707
|
+
|| ellipsisSettings.openInTab === true
|
|
708
|
+
|| ellipsisSettings.deleteSessions === true;
|
|
709
|
+
}
|
|
710
|
+
|
|
562
711
|
/* ------------------------------------- 打开方式:应用列表与二级开关 */
|
|
563
712
|
|
|
564
713
|
function readStoredChildren() {
|
|
@@ -902,6 +1051,20 @@ window.__ModuleLoader__.load({
|
|
|
902
1051
|
.catch(() => ({ ok: false, deleted: 0, failed: ids.length, error: 'network' }));
|
|
903
1052
|
}
|
|
904
1053
|
|
|
1054
|
+
function postWorkspaceDelete(workspaceId) {
|
|
1055
|
+
if (typeof fetch !== 'function') return Promise.resolve({ ok: false, deleted: 0, failed: 0, error: 'network' });
|
|
1056
|
+
return fetch(WORKSPACE_DELETE_ENDPOINT, {
|
|
1057
|
+
method: 'POST',
|
|
1058
|
+
headers: { 'content-type': 'application/json' },
|
|
1059
|
+
body: JSON.stringify({ workspaceId }),
|
|
1060
|
+
})
|
|
1061
|
+
.then((response) => response.json().catch(() => null)
|
|
1062
|
+
.then((payload) => (payload !== null && typeof payload === 'object'
|
|
1063
|
+
? payload
|
|
1064
|
+
: { ok: false, deleted: 0, failed: 0, error: `HTTP ${response.status}` })))
|
|
1065
|
+
.catch(() => ({ ok: false, deleted: 0, failed: 0, error: 'network' }));
|
|
1066
|
+
}
|
|
1067
|
+
|
|
905
1068
|
/** shell.overlay 上的对话框宿主;没有请求时什么都不渲染。 */
|
|
906
1069
|
function DeleteDialog({ t }) {
|
|
907
1070
|
const request = useDeleteRequest();
|
|
@@ -913,8 +1076,11 @@ window.__ModuleLoader__.load({
|
|
|
913
1076
|
const [busy, setBusy] = useState(false);
|
|
914
1077
|
const [error, setError] = useState(null);
|
|
915
1078
|
const [partial, setPartial] = useState(null);
|
|
1079
|
+
const workspace = request.mode === 'workspace';
|
|
916
1080
|
const bulk = request.mode === 'archived';
|
|
917
|
-
const ids =
|
|
1081
|
+
const ids = workspace
|
|
1082
|
+
? (Array.isArray(request.sessionIds) ? request.sessionIds : [])
|
|
1083
|
+
: bulk ? request.sessionIds : [request.sessionId];
|
|
918
1084
|
const close = () => {
|
|
919
1085
|
if (!busy) closeDelete();
|
|
920
1086
|
};
|
|
@@ -926,11 +1092,29 @@ window.__ModuleLoader__.load({
|
|
|
926
1092
|
const wasMain = ids.some((id) => isMainSession(id));
|
|
927
1093
|
void (async () => {
|
|
928
1094
|
try {
|
|
929
|
-
const result =
|
|
1095
|
+
const result = workspace
|
|
1096
|
+
? await postWorkspaceDelete(request.workspaceId)
|
|
1097
|
+
: await postDelete(ids);
|
|
930
1098
|
if (result.ok !== true && (result.deleted ?? 0) === 0) {
|
|
931
1099
|
const reason = typeof result.error === 'string' && result.error !== '' ? result.error : t('delete.unavailable');
|
|
932
1100
|
throw new Error(reason);
|
|
933
1101
|
}
|
|
1102
|
+
if (workspace) {
|
|
1103
|
+
// 会话删除有失败时 host 会保留工作区,让用户重试;成功才关框。
|
|
1104
|
+
if (result.ok !== true) {
|
|
1105
|
+
setPartial({ done: result.deleted ?? 0, failed: result.failed ?? 0, workspace: true });
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
if (wasMain) serviceOf(rootCtx, 'uiWorkspace')?.clearMain?.();
|
|
1109
|
+
closeDelete();
|
|
1110
|
+
toast(
|
|
1111
|
+
(result.deleted ?? 0) > 0
|
|
1112
|
+
? t('delete.workspaceDone', { n: result.deleted ?? 0 })
|
|
1113
|
+
: t('delete.workspaceEmptyDone'),
|
|
1114
|
+
'success',
|
|
1115
|
+
);
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
934
1118
|
if (wasMain) serviceOf(rootCtx, 'uiWorkspace')?.clearMain?.();
|
|
935
1119
|
if ((result.failed ?? 0) > 0) {
|
|
936
1120
|
setPartial({ done: result.deleted ?? 0, failed: result.failed });
|
|
@@ -949,24 +1133,34 @@ window.__ModuleLoader__.load({
|
|
|
949
1133
|
open: true,
|
|
950
1134
|
onClose: close,
|
|
951
1135
|
closeLabel: t('delete.cancel'),
|
|
952
|
-
title: bulk ? t('delete.archivedTitle') : t('delete.title'),
|
|
953
|
-
description:
|
|
954
|
-
?
|
|
955
|
-
|
|
1136
|
+
title: workspace ? t('delete.workspaceTitle') : bulk ? t('delete.archivedTitle') : t('delete.title'),
|
|
1137
|
+
description: workspace
|
|
1138
|
+
? (ids.length > 0
|
|
1139
|
+
? t('delete.workspaceDescription', { name: request.displayTitle ?? '', n: ids.length })
|
|
1140
|
+
: t('delete.workspaceEmptyDescription', { name: request.displayTitle ?? '' }))
|
|
1141
|
+
: bulk
|
|
1142
|
+
? t('delete.archivedDescription', { n: ids.length })
|
|
1143
|
+
: t('delete.description', { name: request.displayTitle ?? '' }),
|
|
956
1144
|
footer: h(Fragment, null,
|
|
957
1145
|
jsx(Button, { variant: 'outline', disabled: busy, onClick: close, children: t('delete.cancel') }),
|
|
958
1146
|
jsx(Button, {
|
|
959
1147
|
variant: 'outline',
|
|
960
1148
|
disabled: busy,
|
|
961
1149
|
onClick: confirm,
|
|
962
|
-
children: h('span', { className: 'dsrm-danger-label' },
|
|
1150
|
+
children: h('span', { className: 'dsrm-danger-label' }, workspace
|
|
1151
|
+
? t('delete.workspaceConfirm')
|
|
1152
|
+
: bulk ? t('delete.archivedConfirm') : t('delete.confirm')),
|
|
963
1153
|
})),
|
|
964
1154
|
children: [
|
|
965
1155
|
busy
|
|
966
|
-
? h('div', { className: 'dsrm-status-text', role: 'status', key: 'busy' },
|
|
1156
|
+
? h('div', { className: 'dsrm-status-text', role: 'status', key: 'busy' }, workspace
|
|
1157
|
+
? t('delete.workspacePending')
|
|
1158
|
+
: bulk ? t('delete.archivedPending') : t('delete.pending'))
|
|
967
1159
|
: null,
|
|
968
1160
|
partial !== null
|
|
969
|
-
? h('div', { className: 'dsrm-status-text', role: 'status', key: 'partial' },
|
|
1161
|
+
? h('div', { className: 'dsrm-status-text', role: 'status', key: 'partial' }, partial.workspace
|
|
1162
|
+
? t('delete.workspacePartial', { done: partial.done, failed: partial.failed })
|
|
1163
|
+
: t('delete.archivedPartial', { done: partial.done, failed: partial.failed }))
|
|
970
1164
|
: null,
|
|
971
1165
|
error !== null
|
|
972
1166
|
? h('div', { className: 'dsrm-error-text', role: 'alert', key: 'error' }, t('delete.failed', { message: error }))
|
|
@@ -1165,7 +1359,9 @@ window.__ModuleLoader__.load({
|
|
|
1165
1359
|
* 且和 dsh-session-menu 的同名能力二选一。
|
|
1166
1360
|
*/
|
|
1167
1361
|
function SessionOpenWithMenuItem({ sessionId, useMenuOpenState, t }) {
|
|
1168
|
-
const
|
|
1362
|
+
const rightClick = useRightClickEntry();
|
|
1363
|
+
const ellipsis = useEllipsis();
|
|
1364
|
+
const visible = rightClick || ellipsis.sessionOpenWith === true;
|
|
1169
1365
|
const items = useItems();
|
|
1170
1366
|
const apps = useApps();
|
|
1171
1367
|
const setMenuOpen = menuCloser(useMenuOpenState);
|
|
@@ -1186,7 +1382,9 @@ window.__ModuleLoader__.load({
|
|
|
1186
1382
|
* 放进一级菜单,用户也可以在设置里改。置顶项不再重复出现在「打开方式」里。
|
|
1187
1383
|
*/
|
|
1188
1384
|
function SessionOpenWithPinnedMenuItem({ sessionId, useMenuOpenState, t }) {
|
|
1189
|
-
const
|
|
1385
|
+
const rightClick = useRightClickEntry();
|
|
1386
|
+
const ellipsis = useEllipsis();
|
|
1387
|
+
const visible = rightClick || ellipsis.sessionOpenWith === true;
|
|
1190
1388
|
const items = useItems();
|
|
1191
1389
|
useChildren();
|
|
1192
1390
|
const apps = useApps();
|
|
@@ -1223,7 +1421,9 @@ window.__ModuleLoader__.load({
|
|
|
1223
1421
|
* 打开「允许删除未归档会话」。和 dsh-session-menu 的同名功能二选一。
|
|
1224
1422
|
*/
|
|
1225
1423
|
function SessionDeleteMenuItem({ sessionId, displayTitle, useMenuOpenState, t }) {
|
|
1226
|
-
const
|
|
1424
|
+
const rightClick = useRightClickEntry();
|
|
1425
|
+
const ellipsis = useEllipsis();
|
|
1426
|
+
const visible = rightClick || ellipsis.sessionDelete === true;
|
|
1227
1427
|
const items = useItems();
|
|
1228
1428
|
const archived = useArchivedIds();
|
|
1229
1429
|
const setMenuOpen = menuCloser(useMenuOpenState);
|
|
@@ -1246,7 +1446,9 @@ window.__ModuleLoader__.load({
|
|
|
1246
1446
|
* 和 dsh-session-menu 的同名功能二选一。displayTitle 用作确认文案。
|
|
1247
1447
|
*/
|
|
1248
1448
|
function SessionDeleteArchivedMenuItem({ sessionId, displayTitle, useMenuOpenState, t }) {
|
|
1249
|
-
const
|
|
1449
|
+
const rightClick = useRightClickEntry();
|
|
1450
|
+
const ellipsis = useEllipsis();
|
|
1451
|
+
const visible = rightClick || ellipsis.sessionDeleteArchived === true;
|
|
1250
1452
|
const items = useItems();
|
|
1251
1453
|
const archived = useArchivedIds();
|
|
1252
1454
|
const setMenuOpen = menuCloser(useMenuOpenState);
|
|
@@ -1275,8 +1477,25 @@ window.__ModuleLoader__.load({
|
|
|
1275
1477
|
['deleteArchived', 'settings.deleteArchived.label', 'settings.deleteArchived.hint'],
|
|
1276
1478
|
];
|
|
1277
1479
|
|
|
1480
|
+
/** 「…」菜单附加开关;按行分组,避免用户分不清工作区项和会话项。 */
|
|
1481
|
+
const ELLIPSIS_ROWS = [
|
|
1482
|
+
['workspace', 'settings.ellipsisWorkspace', [
|
|
1483
|
+
['newSession', 'settings.ellipsis.newSession.label', 'settings.ellipsis.newSession.hint'],
|
|
1484
|
+
['openWith', 'settings.ellipsis.openWith.label', 'settings.ellipsis.openWith.hint'],
|
|
1485
|
+
['copyPath', 'settings.ellipsis.copyPath.label', 'settings.ellipsis.copyPath.hint'],
|
|
1486
|
+
['openInTab', 'settings.ellipsis.openInTab.label', 'settings.ellipsis.openInTab.hint'],
|
|
1487
|
+
['deleteSessions', 'settings.ellipsis.deleteSessions.label', 'settings.ellipsis.deleteSessions.hint'],
|
|
1488
|
+
]],
|
|
1489
|
+
['session', 'settings.ellipsisSession', [
|
|
1490
|
+
['sessionOpenWith', 'settings.ellipsis.sessionOpenWith.label', 'settings.ellipsis.sessionOpenWith.hint'],
|
|
1491
|
+
['sessionDelete', 'settings.ellipsis.sessionDelete.label', 'settings.ellipsis.sessionDelete.hint'],
|
|
1492
|
+
['sessionDeleteArchived', 'settings.ellipsis.sessionDeleteArchived.label', 'settings.ellipsis.sessionDeleteArchived.hint'],
|
|
1493
|
+
]],
|
|
1494
|
+
];
|
|
1495
|
+
|
|
1278
1496
|
function SettingsSection({ t }) {
|
|
1279
1497
|
const items = useItems();
|
|
1498
|
+
const ellipsis = useEllipsis();
|
|
1280
1499
|
const children = useChildren();
|
|
1281
1500
|
const apps = useApps();
|
|
1282
1501
|
usePinned();
|
|
@@ -1359,10 +1578,36 @@ window.__ModuleLoader__.load({
|
|
|
1359
1578
|
return key === 'openWith' && childRows ? [row, childRows] : [row];
|
|
1360
1579
|
});
|
|
1361
1580
|
|
|
1581
|
+
const ellipsisGroups = ELLIPSIS_ROWS.map(([groupKey, titleKey, rows]) => h('div', {
|
|
1582
|
+
className: 'dsrm-settings-group',
|
|
1583
|
+
key: groupKey,
|
|
1584
|
+
},
|
|
1585
|
+
h('div', { className: 'dsrm-settings-group-title' }, t(titleKey)),
|
|
1586
|
+
h('div', { className: 'dsrm-settings-list' }, rows.map(([key, labelKey, hintKey]) => h('div', {
|
|
1587
|
+
className: 'dsrm-settings-row',
|
|
1588
|
+
key,
|
|
1589
|
+
},
|
|
1590
|
+
h('div', { className: 'dsrm-settings-copy' },
|
|
1591
|
+
h('div', { className: 'dsrm-settings-label' }, t(labelKey)),
|
|
1592
|
+
h('div', { className: 'dsrm-settings-hint' }, t(hintKey)),
|
|
1593
|
+
),
|
|
1594
|
+
h('div', { className: 'dsrm-settings-controls' },
|
|
1595
|
+
jsx(Switch, {
|
|
1596
|
+
checked: ellipsis[key] === true,
|
|
1597
|
+
label: t(labelKey),
|
|
1598
|
+
onChange: (next) => setEllipsis(key, next),
|
|
1599
|
+
}),
|
|
1600
|
+
),
|
|
1601
|
+
))),
|
|
1602
|
+
));
|
|
1603
|
+
|
|
1362
1604
|
return h('div', { className: 'dsrm-settings' },
|
|
1363
1605
|
h('h2', { className: 'dsrm-settings-title' }, t('settings.title')),
|
|
1364
1606
|
anyConflict ? h('div', { className: 'dsrm-settings-note' }, t('settings.conflictNote')) : null,
|
|
1365
1607
|
h('div', { className: 'dsrm-settings-list' }, rows),
|
|
1608
|
+
h('h2', { className: 'dsrm-settings-title dsrm-settings-subtitle' }, t('settings.ellipsisTitle')),
|
|
1609
|
+
h('div', { className: 'dsrm-settings-note' }, t('settings.ellipsisHint')),
|
|
1610
|
+
h('div', { className: 'dsrm-settings-groups' }, ellipsisGroups),
|
|
1366
1611
|
);
|
|
1367
1612
|
}
|
|
1368
1613
|
|
|
@@ -1372,16 +1617,20 @@ window.__ModuleLoader__.load({
|
|
|
1372
1617
|
* 工作区行右键菜单。官方 Menu 的 items 只认单层条目,不支持子菜单,
|
|
1373
1618
|
* 所以这里自绘 panel;普通行仍用 MenuItemButton,分隔线与菜单外观和官方一致。
|
|
1374
1619
|
*/
|
|
1375
|
-
function WorkspaceRowMenu({ x, y, props, onClose }) {
|
|
1620
|
+
function WorkspaceRowMenu({ x, y, props, onClose, mode = 'context' }) {
|
|
1376
1621
|
const t = localeOf(rootCtx);
|
|
1377
1622
|
const items = useItems();
|
|
1623
|
+
const ellipsis = useEllipsis();
|
|
1378
1624
|
const apps = useApps();
|
|
1379
1625
|
usePinned();
|
|
1380
1626
|
useChildren();
|
|
1381
1627
|
const hostRef = useRef(null);
|
|
1382
1628
|
const [pos, setPos] = useState(null);
|
|
1383
1629
|
|
|
1384
|
-
const
|
|
1630
|
+
const ellipsisMode = mode === 'ellipsis';
|
|
1631
|
+
/** 右键菜单永远显示;「…」菜单只在对应开关打开时显示(内置项除外)。 */
|
|
1632
|
+
const show = (key) => !ellipsisMode || ellipsis[key] === true;
|
|
1633
|
+
const openWithOn = items.openWith !== false && show('openWith');
|
|
1385
1634
|
|
|
1386
1635
|
useEffect(() => {
|
|
1387
1636
|
if (openWithOn) ensureApps();
|
|
@@ -1431,6 +1680,26 @@ window.__ModuleLoader__.load({
|
|
|
1431
1680
|
});
|
|
1432
1681
|
};
|
|
1433
1682
|
|
|
1683
|
+
const requestWorkspaceDelete = () => {
|
|
1684
|
+
if (workspaceId === '') {
|
|
1685
|
+
toast(t('actionFailed'));
|
|
1686
|
+
return;
|
|
1687
|
+
}
|
|
1688
|
+
const snapshot = serviceOf(rootCtx, 'workspaces')?.list?.getSnapshot?.();
|
|
1689
|
+
const entry = Array.isArray(snapshot?.items)
|
|
1690
|
+
? snapshot.items.find((candidate) => candidate?.workspaceId === workspaceId)
|
|
1691
|
+
: undefined;
|
|
1692
|
+
const sessionIds = Array.isArray(entry?.sessionIds)
|
|
1693
|
+
? entry.sessionIds.filter((id) => typeof id === 'string' && id !== '')
|
|
1694
|
+
: [];
|
|
1695
|
+
const title = typeof group.label === 'string' && group.label !== ''
|
|
1696
|
+
? group.label
|
|
1697
|
+
: typeof group.title === 'string' && group.title !== ''
|
|
1698
|
+
? group.title
|
|
1699
|
+
: cwd;
|
|
1700
|
+
requestDelete({ mode: 'workspace', workspaceId, sessionIds, displayTitle: title });
|
|
1701
|
+
};
|
|
1702
|
+
|
|
1434
1703
|
const run = (id) => {
|
|
1435
1704
|
if (id === 'new-session') {
|
|
1436
1705
|
try {
|
|
@@ -1456,6 +1725,10 @@ window.__ModuleLoader__.load({
|
|
|
1456
1725
|
}
|
|
1457
1726
|
return;
|
|
1458
1727
|
}
|
|
1728
|
+
if (id === 'delete-sessions') {
|
|
1729
|
+
requestWorkspaceDelete();
|
|
1730
|
+
return;
|
|
1731
|
+
}
|
|
1459
1732
|
if (id === 'tab') {
|
|
1460
1733
|
Promise.resolve().then(() => tabs.openWorkspaceInTab(workspaceId)).catch((reason) => {
|
|
1461
1734
|
console.warn('[dsh-sidebar-row-menu] open in tab failed:', reason);
|
|
@@ -1473,7 +1746,7 @@ window.__ModuleLoader__.load({
|
|
|
1473
1746
|
};
|
|
1474
1747
|
|
|
1475
1748
|
const rows = [];
|
|
1476
|
-
if (typeof props.onCreate === 'function') {
|
|
1749
|
+
if (typeof props.onCreate === 'function' && show('newSession')) {
|
|
1477
1750
|
rows.push(jsx(MenuItemButton, {
|
|
1478
1751
|
key: 'new-session',
|
|
1479
1752
|
onSelect: () => {
|
|
@@ -1515,17 +1788,19 @@ window.__ModuleLoader__.load({
|
|
|
1515
1788
|
}));
|
|
1516
1789
|
}
|
|
1517
1790
|
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1791
|
+
if (show('copyPath')) {
|
|
1792
|
+
rows.push(jsx(MenuItemButton, {
|
|
1793
|
+
key: 'copy-path',
|
|
1794
|
+
separatorBefore: !openWithOn,
|
|
1795
|
+
onSelect: () => {
|
|
1796
|
+
onClose();
|
|
1797
|
+
run('copy-path');
|
|
1798
|
+
},
|
|
1799
|
+
icon: jsx(IconCopyOutlineRegular, { size: 14 }),
|
|
1800
|
+
children: t('copyPath'),
|
|
1801
|
+
}));
|
|
1802
|
+
}
|
|
1803
|
+
if (canOpenInTab && show('openInTab')) {
|
|
1529
1804
|
rows.push(jsx(MenuItemButton, {
|
|
1530
1805
|
key: 'tab',
|
|
1531
1806
|
onSelect: () => {
|
|
@@ -1547,6 +1822,18 @@ window.__ModuleLoader__.load({
|
|
|
1547
1822
|
icon: jsx(IconTrashOutlineRegular, {}),
|
|
1548
1823
|
children: t('deleteWorkspace'),
|
|
1549
1824
|
}));
|
|
1825
|
+
if (show('deleteSessions')) {
|
|
1826
|
+
rows.push(jsx(MenuItemButton, {
|
|
1827
|
+
key: 'delete-sessions',
|
|
1828
|
+
danger: true,
|
|
1829
|
+
onSelect: () => {
|
|
1830
|
+
onClose();
|
|
1831
|
+
run('delete-sessions');
|
|
1832
|
+
},
|
|
1833
|
+
icon: jsx(IconTrashOutlineRegular, {}),
|
|
1834
|
+
children: t('deleteWorkspaceSessions'),
|
|
1835
|
+
}));
|
|
1836
|
+
}
|
|
1550
1837
|
|
|
1551
1838
|
return h('div', {
|
|
1552
1839
|
className: 'dsrm-menu',
|
|
@@ -1559,8 +1846,8 @@ window.__ModuleLoader__.load({
|
|
|
1559
1846
|
}, h(MenuSurface, { role: 'group', className: 'dsrm-menu-surface' }, rows));
|
|
1560
1847
|
}
|
|
1561
1848
|
|
|
1562
|
-
/** 在 (x, y) 处挂出工作区菜单。 */
|
|
1563
|
-
function showWorkspaceMenu(props, x, y) {
|
|
1849
|
+
/** 在 (x, y) 处挂出工作区菜单。mode='ellipsis' 表示从行尾「…」按钮打开。 */
|
|
1850
|
+
function showWorkspaceMenu(props, x, y, mode = 'context') {
|
|
1564
1851
|
closeMenu();
|
|
1565
1852
|
menuHost = document.createElement('div');
|
|
1566
1853
|
menuHost.setAttribute(MENU_ATTR, '1');
|
|
@@ -1571,6 +1858,7 @@ window.__ModuleLoader__.load({
|
|
|
1571
1858
|
x,
|
|
1572
1859
|
y,
|
|
1573
1860
|
props,
|
|
1861
|
+
mode,
|
|
1574
1862
|
onClose: () => closeMenu(),
|
|
1575
1863
|
}));
|
|
1576
1864
|
}
|
|
@@ -1585,9 +1873,44 @@ window.__ModuleLoader__.load({
|
|
|
1585
1873
|
}
|
|
1586
1874
|
|
|
1587
1875
|
/** 工作区行右键菜单(没有 slot 可复用,只能自己组)。 */
|
|
1588
|
-
function openWorkspaceMenu(props, x, y) {
|
|
1876
|
+
function openWorkspaceMenu(props, x, y, mode = 'context') {
|
|
1589
1877
|
if (rootCtx === null) return;
|
|
1590
|
-
showWorkspaceMenu(props, x, y);
|
|
1878
|
+
showWorkspaceMenu(props, x, y, mode);
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
/**
|
|
1882
|
+
* 行尾「…」按钮已经展开内置菜单时,用同一个按钮位置改挂本插件菜单。
|
|
1883
|
+
* 只在用户为工作区行开启了至少一个「…」附加项时接管;否则原生菜单原样工作。
|
|
1884
|
+
*/
|
|
1885
|
+
function onWorkspaceEllipsisClick(event) {
|
|
1886
|
+
if (event.defaultPrevented || event.button !== 0) return;
|
|
1887
|
+
if (event.target === null || event.target === undefined) return;
|
|
1888
|
+
if (rootCtx === null || !workspaceEllipsisOn()) return;
|
|
1889
|
+
const target = event.target;
|
|
1890
|
+
if (typeof target.closest !== 'function') return;
|
|
1891
|
+
const row = target.closest('[data-row-key]');
|
|
1892
|
+
if (row === null) return;
|
|
1893
|
+
const rowKey = row.getAttribute('data-row-key') ?? '';
|
|
1894
|
+
if (!rowKey.startsWith('workspace:')) return;
|
|
1895
|
+
const button = target.closest('button');
|
|
1896
|
+
if (button === null || !row.contains(button)) return;
|
|
1897
|
+
// 只接管行内第一个按钮(「…」);新建会话按钮带快捷键标记,直接放行。
|
|
1898
|
+
const buttons = row.querySelectorAll('button');
|
|
1899
|
+
if (buttons.length === 0 || buttons[0] !== button) return;
|
|
1900
|
+
if (button.getAttribute('aria-keyshortcuts') !== null) return;
|
|
1901
|
+
const props = resolveWorkspaceRow(target);
|
|
1902
|
+
if (props === null) return;
|
|
1903
|
+
event.preventDefault();
|
|
1904
|
+
event.stopPropagation();
|
|
1905
|
+
event.stopImmediatePropagation?.();
|
|
1906
|
+
// 再点一次「…」视为收起当前菜单。
|
|
1907
|
+
if (menuHost !== null && ellipsisTrigger === button) {
|
|
1908
|
+
closeMenu();
|
|
1909
|
+
return;
|
|
1910
|
+
}
|
|
1911
|
+
const rect = button.getBoundingClientRect();
|
|
1912
|
+
openWorkspaceMenu(props, rect.left + rect.width, rect.bottom + 4, 'ellipsis');
|
|
1913
|
+
ellipsisTrigger = button;
|
|
1591
1914
|
}
|
|
1592
1915
|
|
|
1593
1916
|
/* ------------------------------------------------ 会话菜单的插件条目 */
|
|
@@ -1858,6 +2181,9 @@ window.__ModuleLoader__.load({
|
|
|
1858
2181
|
.dsrm-app-icon{width:14px;height:14px;border-radius:3px;display:block;object-fit:contain}
|
|
1859
2182
|
.dsrm-settings{max-width:820px;margin:0 auto;padding:2px 0 36px}
|
|
1860
2183
|
.dsrm-settings-title{margin:0;font-size:16px;line-height:24px;font-weight:600}
|
|
2184
|
+
.dsrm-settings-subtitle{margin-top:28px}
|
|
2185
|
+
.dsrm-settings-groups{margin-top:10px;display:flex;flex-direction:column;gap:18px}
|
|
2186
|
+
.dsrm-settings-group-title{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;font-weight:600;margin-bottom:2px}
|
|
1861
2187
|
.dsrm-settings-note{margin-top:8px;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}
|
|
1862
2188
|
.dsrm-settings-list{margin-top:10px;display:flex;flex-direction:column}
|
|
1863
2189
|
.dsrm-settings-row{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:14px 0;border-top:.5px solid var(--dsw-alias-border-l2)}
|
|
@@ -1932,9 +2258,11 @@ window.__ModuleLoader__.load({
|
|
|
1932
2258
|
|
|
1933
2259
|
function install() {
|
|
1934
2260
|
window.addEventListener('contextmenu', onContextMenu, true);
|
|
2261
|
+
document.addEventListener('click', onWorkspaceEllipsisClick, true);
|
|
1935
2262
|
console.info('[dsh-sidebar-row-menu] installed');
|
|
1936
2263
|
return () => {
|
|
1937
2264
|
window.removeEventListener('contextmenu', onContextMenu, true);
|
|
2265
|
+
document.removeEventListener('click', onWorkspaceEllipsisClick, true);
|
|
1938
2266
|
closeMenu();
|
|
1939
2267
|
closeToast();
|
|
1940
2268
|
};
|
package/lib/host/route.js
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* HTTP 边界:/open 与 /
|
|
2
|
+
* HTTP 边界:/open、/session/delete 与 /workspace/delete。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* POST /dsh-sidebar-row-menu/open
|
|
6
|
-
* POST /dsh-sidebar-row-menu/session/delete
|
|
4
|
+
* 浏览器端不能自己打开系统应用,也不该直接碰会话日志,所以这几件事都放在 host:
|
|
5
|
+
* POST /dsh-sidebar-row-menu/open 打开目录(VS Code / 资源管理器)
|
|
6
|
+
* POST /dsh-sidebar-row-menu/session/delete 永久删除会话(归档范围由 UI 把关)
|
|
7
|
+
* POST /dsh-sidebar-row-menu/workspace/delete 永久删除工作区及其全部会话
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
import { inspectOpenPath } from './paths.js';
|
|
10
11
|
import { isLoopbackRequest } from './security.js';
|
|
11
12
|
import { openInVscode, revealDirectory } from './launch.js';
|
|
12
13
|
import { DELETE_BATCH_LIMIT, removeSessions } from './session-delete.js';
|
|
14
|
+
import { removeWorkspaceWithSessions } from './workspace-delete.js';
|
|
13
15
|
|
|
14
16
|
/** 路由前缀;客户端半边硬编码同一个值。 */
|
|
15
17
|
export const ROUTE_PREFIX = '/dsh-sidebar-row-menu';
|
|
16
18
|
export const ROUTE_OPEN = `${ROUTE_PREFIX}/open`;
|
|
17
19
|
export const ROUTE_DELETE = `${ROUTE_PREFIX}/session/delete`;
|
|
20
|
+
export const ROUTE_WORKSPACE_DELETE = `${ROUTE_PREFIX}/workspace/delete`;
|
|
18
21
|
/** 请求体上限(只传路径或会话 id 列表,64KB 足够)。 */
|
|
19
22
|
const MAX_BODY_BYTES = 64 * 1024;
|
|
20
23
|
|
|
@@ -171,8 +174,65 @@ export async function handleSessionDeleteRequest(request, response, ctx) {
|
|
|
171
174
|
}
|
|
172
175
|
}
|
|
173
176
|
|
|
174
|
-
/**
|
|
175
|
-
|
|
177
|
+
/**
|
|
178
|
+
* 处理一次「删除工作区及其全部会话」请求。
|
|
179
|
+
* 请求体:{ workspaceId: string }。
|
|
180
|
+
* 200 返回 { ok, sessions, deleted, failed, workspaceDeleted, error?, code? }。
|
|
181
|
+
* 会话删除有失败时不会删工作区,返回 ok:false 让客户端保留工作区并提示重试。
|
|
182
|
+
*/
|
|
183
|
+
export async function handleWorkspaceDeleteRequest(request, response, ctx) {
|
|
184
|
+
if (!isLoopbackRequest(request)) {
|
|
185
|
+
writeJson(response, 403, { ok: false, error: 'forbidden' });
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (request.method !== 'POST') {
|
|
189
|
+
writeJson(response, 405, { ok: false, error: 'method-not-allowed' });
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (pathnameOf(request) !== ROUTE_WORKSPACE_DELETE) {
|
|
193
|
+
writeJson(response, 404, { ok: false, error: 'not-found' });
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
let payload;
|
|
198
|
+
try {
|
|
199
|
+
payload = await readJsonBody(request);
|
|
200
|
+
} catch {
|
|
201
|
+
writeJson(response, 400, { ok: false, error: 'bad-request' });
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const workspaceId = payload?.workspaceId;
|
|
205
|
+
if (typeof workspaceId !== 'string' || workspaceId.trim() === '') {
|
|
206
|
+
writeJson(response, 400, { ok: false, error: 'bad-request' });
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (ctx === undefined || ctx === null || typeof ctx.get !== 'function') {
|
|
211
|
+
writeJson(response, 503, { ok: false, error: 'service-unavailable' });
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
for (const name of ['sessions', 'sessionQuery', 'sessionPersistence', 'workspaceRegistry']) {
|
|
215
|
+
let available;
|
|
216
|
+
try {
|
|
217
|
+
available = ctx.get(name) !== undefined && ctx.get(name) !== null;
|
|
218
|
+
} catch {
|
|
219
|
+
available = false;
|
|
220
|
+
}
|
|
221
|
+
if (!available) {
|
|
222
|
+
writeJson(response, 503, { ok: false, error: 'service-unavailable' });
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
try {
|
|
227
|
+
const outcome = await removeWorkspaceWithSessions(ctx, workspaceId);
|
|
228
|
+
writeJson(response, 200, outcome);
|
|
229
|
+
} catch (error) {
|
|
230
|
+
ctx.logger?.warn?.('[dsh-sidebar-row-menu] workspace delete failed:', error);
|
|
231
|
+
writeJson(response, 500, { ok: false, error: 'delete-failed' });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** 构造 webServer 路由对象(kind=prefix)。 */export function createOpenRoute(ctx) {
|
|
176
236
|
return {
|
|
177
237
|
kind: 'prefix',
|
|
178
238
|
path: ROUTE_PREFIX,
|
|
@@ -180,6 +240,7 @@ export function createOpenRoute(ctx) {
|
|
|
180
240
|
const pathname = pathnameOf(request);
|
|
181
241
|
if (pathname === ROUTE_OPEN) return handleOpenRequest(request, response);
|
|
182
242
|
if (pathname === ROUTE_DELETE) return handleSessionDeleteRequest(request, response, ctx);
|
|
243
|
+
if (pathname === ROUTE_WORKSPACE_DELETE) return handleWorkspaceDeleteRequest(request, response, ctx);
|
|
183
244
|
if (!isLoopbackRequest(request)) {
|
|
184
245
|
writeJson(response, 403, { ok: false, error: 'forbidden' });
|
|
185
246
|
return undefined;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 删除工作区及其下所有会话。
|
|
3
|
+
*
|
|
4
|
+
* 内置 `workspaces.delete(id)`(registry.delete)只摘掉工作区登记,会话会被
|
|
5
|
+
* 重新归到「未分组」。这里先按工作区的 sessionIds 永久删除每个会话日志,
|
|
6
|
+
* 全部成功后再删工作区登记;任何一个会话删除失败就保留工作区,避免留下
|
|
7
|
+
* 一堆没人管的「未分组」会话。
|
|
8
|
+
*
|
|
9
|
+
* 工作区目录本身不删(和内置行为一致),只删会话日志与工作区记录。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { removeSessions } from './session-delete.js';
|
|
13
|
+
|
|
14
|
+
function service(ctx, name) {
|
|
15
|
+
try {
|
|
16
|
+
return ctx.get(name);
|
|
17
|
+
} catch {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 永久删除一个工作区及其全部会话。
|
|
24
|
+
* @param ctx - 宿主插件上下文。
|
|
25
|
+
* @param workspaceId - 工作区 id。
|
|
26
|
+
* @returns {Promise<{ok:boolean,sessions:number,deleted:number,failed:number,
|
|
27
|
+
* workspaceDeleted:boolean,missing?:boolean,code?:string,error:string}>}
|
|
28
|
+
*/
|
|
29
|
+
export async function removeWorkspaceWithSessions(ctx, workspaceId) {
|
|
30
|
+
const registry = service(ctx, 'workspaceRegistry');
|
|
31
|
+
if (
|
|
32
|
+
registry === null
|
|
33
|
+
|| registry === undefined
|
|
34
|
+
|| typeof registry.get !== 'function'
|
|
35
|
+
|| typeof registry.delete !== 'function'
|
|
36
|
+
) {
|
|
37
|
+
return {
|
|
38
|
+
ok: false,
|
|
39
|
+
sessions: 0,
|
|
40
|
+
deleted: 0,
|
|
41
|
+
failed: 0,
|
|
42
|
+
workspaceDeleted: false,
|
|
43
|
+
code: 'unavailable',
|
|
44
|
+
error: 'workspace-registry-unavailable',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let workspace;
|
|
49
|
+
try {
|
|
50
|
+
workspace = registry.get(workspaceId);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
return {
|
|
53
|
+
ok: false,
|
|
54
|
+
sessions: 0,
|
|
55
|
+
deleted: 0,
|
|
56
|
+
failed: 0,
|
|
57
|
+
workspaceDeleted: false,
|
|
58
|
+
code: 'lookup-failed',
|
|
59
|
+
error: error instanceof Error ? error.message : String(error),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (workspace === null || workspace === undefined) {
|
|
63
|
+
return { ok: true, sessions: 0, deleted: 0, failed: 0, workspaceDeleted: false, missing: true, error: '' };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const ids = Array.isArray(workspace.sessionIds)
|
|
67
|
+
? workspace.sessionIds.filter((id) => typeof id === 'string' && id.trim() !== '')
|
|
68
|
+
: [];
|
|
69
|
+
let deleted = 0;
|
|
70
|
+
if (ids.length > 0) {
|
|
71
|
+
const outcome = await removeSessions(ctx, ids);
|
|
72
|
+
deleted = outcome.deleted ?? 0;
|
|
73
|
+
if (outcome.ok !== true) {
|
|
74
|
+
return {
|
|
75
|
+
ok: false,
|
|
76
|
+
sessions: ids.length,
|
|
77
|
+
deleted,
|
|
78
|
+
failed: outcome.failed ?? Math.max(0, ids.length - deleted),
|
|
79
|
+
workspaceDeleted: false,
|
|
80
|
+
code: typeof outcome.code === 'string' ? outcome.code : 'session-delete-failed',
|
|
81
|
+
error: typeof outcome.error === 'string' ? outcome.error : '',
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let workspaceDeleted = false;
|
|
87
|
+
try {
|
|
88
|
+
workspaceDeleted = (await registry.delete(workspaceId)) !== false;
|
|
89
|
+
} catch (error) {
|
|
90
|
+
return {
|
|
91
|
+
ok: false,
|
|
92
|
+
sessions: ids.length,
|
|
93
|
+
deleted,
|
|
94
|
+
failed: 0,
|
|
95
|
+
workspaceDeleted: false,
|
|
96
|
+
code: 'workspace-delete-failed',
|
|
97
|
+
error: error instanceof Error ? error.message : String(error),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return { ok: true, sessions: ids.length, deleted, failed: 0, workspaceDeleted, error: '' };
|
|
101
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -4,18 +4,21 @@
|
|
|
4
4
|
* 两件事:
|
|
5
5
|
* 1. `/open` 让浏览器请宿主用系统命令打开目录;
|
|
6
6
|
* 2. `/session/delete` 永久删除会话日志(默认由 UI 对已归档会话开放;
|
|
7
|
+
* 3. `/workspace/delete` 永久删除工作区及其下全部会话。
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
+
* 具体实现拆成六个模块:
|
|
9
10
|
*
|
|
10
|
-
* lib/host/security.js
|
|
11
|
-
* lib/host/paths.js
|
|
12
|
-
* lib/host/launch.js
|
|
13
|
-
* lib/host/session-delete.js
|
|
14
|
-
* lib/host/
|
|
11
|
+
* lib/host/security.js loopback 请求围栏
|
|
12
|
+
* lib/host/paths.js 路径校验与 vscode:// URL 编码
|
|
13
|
+
* lib/host/launch.js VS Code / 资源管理器启动
|
|
14
|
+
* lib/host/session-delete.js 停跑、摘运行时、删日志、清归档记录
|
|
15
|
+
* lib/host/workspace-delete.js 先删会话再摘工作区登记
|
|
16
|
+
* lib/host/route.js HTTP 边界与状态码
|
|
15
17
|
*
|
|
16
18
|
* 浏览器自己不能打开资源管理器,也不该直接删会话日志,所以这些必须放在 host。
|
|
17
19
|
* 打开请求体:{ target: 'vscode' | 'explorer', path: '<绝对目录路径>' }
|
|
18
|
-
*
|
|
20
|
+
* 删除会话请求体:{ sessionIds: ['<会话 id>', ...] }
|
|
21
|
+
* 删除工作区请求体:{ workspaceId: '<工作区 id>' }
|
|
19
22
|
*
|
|
20
23
|
* 删除流程参考 dsh-session-menu v0.1.3(MIT, big-head-pigeon),不引入 Typert 依赖。
|
|
21
24
|
*/
|
|
@@ -29,8 +32,9 @@ export const inject = ['webServer', 'sessions', 'sessionPersistence', 'sessionQu
|
|
|
29
32
|
export { isLoopbackRequest } from './host/security.js';
|
|
30
33
|
export { inspectOpenPath, toVscodeFileUrl } from './host/paths.js';
|
|
31
34
|
export { resolveVscodeLauncher } from './host/launch.js';
|
|
32
|
-
export { createOpenRoute, handleSessionDeleteRequest } from './host/route.js';
|
|
35
|
+
export { createOpenRoute, handleSessionDeleteRequest, handleWorkspaceDeleteRequest } from './host/route.js';
|
|
33
36
|
export { DELETE_BATCH_LIMIT, removeSession, removeSessions } from './host/session-delete.js';
|
|
37
|
+
export { removeWorkspaceWithSessions } from './host/workspace-delete.js';
|
|
34
38
|
|
|
35
39
|
/**
|
|
36
40
|
* 注册路由。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-sidebar-row-menu",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "DSH sidebar right-click menus: a self-composed menu for workspace rows, and the native slot menu opened at the pointer for session rows, with open-with and archived-session cleanup.",
|
|
6
6
|
"keywords": [
|
|
@@ -55,12 +55,13 @@
|
|
|
55
55
|
}
|
|
56
56
|
},
|
|
57
57
|
"engines": {
|
|
58
|
-
"node": ">=20"
|
|
58
|
+
"node": ">=20",
|
|
59
|
+
"dsh": ">=0.2.1-rc.5 <0.3.0-0"
|
|
59
60
|
},
|
|
60
61
|
"scripts": {
|
|
61
|
-
"test": "node --test test/host.test.mjs test/client.test.mjs",
|
|
62
|
-
"test:local": "node --test --test-isolation=none test/host.test.mjs test/client.test.mjs",
|
|
63
|
-
"check": "node --check lib/index.js && node --check lib/client.js && node --check lib/host/paths.js && node --check lib/host/security.js && node --check lib/host/launch.js && node --check lib/host/route.js && node --check lib/host/session-delete.js",
|
|
62
|
+
"test": "node --test test/host.test.mjs test/manifest.test.mjs test/client.test.mjs",
|
|
63
|
+
"test:local": "node --test --test-isolation=none test/host.test.mjs test/manifest.test.mjs test/client.test.mjs",
|
|
64
|
+
"check": "node --check lib/index.js && node --check lib/client.js && node --check lib/host/paths.js && node --check lib/host/security.js && node --check lib/host/launch.js && node --check lib/host/route.js && node --check lib/host/session-delete.js && node --check lib/host/workspace-delete.js",
|
|
64
65
|
"build": "npm pack",
|
|
65
66
|
"marketplace:check": "node scripts/marketplace-preflight.mjs",
|
|
66
67
|
"prepack": "npm run check",
|
|
@@ -65,6 +65,13 @@ String(pkg.repository?.url ?? '').includes(repoSlug)
|
|
|
65
65
|
? pass('repository', pkg.repository.url)
|
|
66
66
|
: fail('repository', 'does not point at the GitHub repo');
|
|
67
67
|
|
|
68
|
+
// The market index and the DSH host both read the top-level `engines.dsh`; the
|
|
69
|
+
// nested `dsh.engines.dsh` the organizer-style manifests use is ignored by both.
|
|
70
|
+
const enginesDsh = pkg.engines?.dsh;
|
|
71
|
+
typeof enginesDsh === 'string' && enginesDsh.trim()
|
|
72
|
+
? pass('engines.dsh', enginesDsh)
|
|
73
|
+
: fail('engines.dsh', 'missing — the market would show no supported DSH version');
|
|
74
|
+
|
|
68
75
|
// --- storefront icon --------------------------------------------------------
|
|
69
76
|
|
|
70
77
|
if (!existsSync(iconPath)) {
|