@xrn1997/dsh-session-delete 0.2.0 → 0.2.1
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 +124 -56
- package/icon.svg +18 -18
- package/lib/client.js +38 -16
- package/lib/index.js +61 -17
- package/lib/types/api/dispatch.d.ts +5 -1
- package/lib/types/client/copy.d.ts +1 -0
- package/lib/types/client/remote.d.ts +5 -2
- package/lib/types/ports/host-port.d.ts +12 -0
- package/lib/types/verbs/restore-session.d.ts +18 -0
- package/locale/en.json +6 -6
- package/locale/zh.json +6 -6
- package/package.json +105 -100
package/README.md
CHANGED
|
@@ -1,90 +1,150 @@
|
|
|
1
1
|
# @xrn1997/dsh-session-delete
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@xrn1997/dsh-session-delete)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+

|
|
6
|
+
|
|
3
7
|
给 DeepSeek Harness(DSH)加一个**删除会话**的能力。DSH 官方有归档、没有删除——归档只是把会话从列表里挪走,文件还在。本插件补上删除,并且让删除可恢复:删除 = 移入回收站,另有一个显式的「彻底删除」。
|
|
4
8
|
|
|
5
9
|

|
|
6
|
-
<!-- 首图待换:这张还是 2026-10-01 的"一列卡片 + 行内两颗按钮"。改版后的形态要一张真机截图(本环境没有可视浏览器,画不出可信的图);alt 已按现状写,图换掉之前请先看文字。 -->
|
|
7
10
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
+
## 快速开始
|
|
12
|
+
|
|
13
|
+
1. 安装:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
dsh plugin --profile <profile> add @xrn1997/dsh-session-delete
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
2. 重启宿主(Node 半不走 HMR)。
|
|
20
|
+
3. 用起来:
|
|
21
|
+
- **删一条**:会话行「⋯」→「删除」。
|
|
22
|
+
- **删一批**:会话行「⋯」→「多选」,勾选后点浮条里的「移入回收站 N 条」。
|
|
23
|
+
- **管理回收站**:侧栏的「回收站」。
|
|
24
|
+
|
|
25
|
+
兼容性与离线安装见[安装与兼容性](#安装与兼容性)。
|
|
11
26
|
|
|
12
27
|
## 目录
|
|
13
28
|
|
|
14
|
-
- [
|
|
15
|
-
- [
|
|
16
|
-
- [
|
|
17
|
-
- [
|
|
29
|
+
- [快速开始](#快速开始)
|
|
30
|
+
- [功能一览](#功能一览)
|
|
31
|
+
- [删除会话](#删除会话)
|
|
32
|
+
- [回收站](#回收站)
|
|
33
|
+
- [安装与兼容性](#安装与兼容性)
|
|
34
|
+
- [数据与安全](#数据与安全)
|
|
18
35
|
- [界面语言](#界面语言)
|
|
19
36
|
- [常见问题](#常见问题)
|
|
20
37
|
- [开发](#开发)
|
|
21
38
|
- [贡献](#贡献)
|
|
22
39
|
- [许可](#许可)
|
|
23
40
|
|
|
24
|
-
##
|
|
41
|
+
## 功能一览
|
|
25
42
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
- **文案随宿主语言**:中文 / English 挂在宿主 locale 服务上,跟随设置的界面语言
|
|
43
|
+
| 想做什么 | 从哪进 | 结果 |
|
|
44
|
+
| --- | --- | --- |
|
|
45
|
+
| 删一条会话 | 会话行「⋯」→「删除」 | 确认后移入回收站,连同盘上的子会话与更深后代;6 秒内可撤销 |
|
|
46
|
+
| 删一批会话 | 会话行「⋯」→「多选」→ 勾选 → 浮条「移入回收站 N 条」 | 一次确认、一次批量执行;失败的留在框里可重试,成功的 6 秒内可整批撤销 |
|
|
47
|
+
| 看回收站、判断该不该恢复 | 侧栏「回收站」 | 左清单、右详情;可折叠分组、拖分栏 |
|
|
48
|
+
| 恢复一条 | 回收站行右侧「恢复」 | 直接放回原项目目录,恢复删除前的归档状态 |
|
|
49
|
+
| 批量恢复 | 回收站「多选」→ 勾选 → 页脚「恢复 N 条」 | 确认框逐条列出目标;回到各自的原项目目录 |
|
|
50
|
+
| 彻底删一条 / 清空 | 回收站行右侧「彻底删除…」/ 页脚「清空回收站…」 | 二次确认后真的移除文件,不可恢复 |
|
|
51
|
+
| 批量彻底删除 | 回收站「多选」→ 勾选 → 页脚「彻底删除 N 条」 | 二次确认后执行,失败原因留在该行 |
|
|
36
52
|
|
|
37
|
-
##
|
|
53
|
+
## 删除会话
|
|
38
54
|
|
|
39
|
-
|
|
40
|
-
dsh plugin --profile <profile> add @xrn1997/dsh-session-delete
|
|
41
|
-
```
|
|
55
|
+
### 单条删除
|
|
42
56
|
|
|
43
|
-
|
|
57
|
+
会话行「⋯」→「删除」→ 确认框「移入回收站」。确认框在点之前就说明**这一组**的规模:
|
|
44
58
|
|
|
45
|
-
|
|
46
|
-
dsh plugin --profile <profile> remove @xrn1997/dsh-session-delete
|
|
47
|
-
```
|
|
59
|
+

|
|
48
60
|
|
|
49
|
-
|
|
61
|
+
- 「含 N 个子会话,将一起移入回收站。」——只在 N>0 时出现;
|
|
62
|
+
- 删完有一条停留 6 秒的撤销提示,点「撤销」即恢复。
|
|
50
63
|
|
|
51
|
-
|
|
64
|
+
删除按**整组**走:父会话在盘上的子会话与更深后代一起移走(那些行宿主本来就不显示)。任一组员正在运行 ⇒ 整组拒绝、零副作用,报的是那个运行中的 id。
|
|
52
65
|
|
|
53
|
-
|
|
66
|
+
### 批量删除
|
|
67
|
+
|
|
68
|
+
宿主会话列表上的批量删除是**显式模式**:
|
|
69
|
+
|
|
70
|
+

|
|
71
|
+
|
|
72
|
+
- 入口:会话行「⋯」→「多选」;模式内再点同一条(变成「退出多选」)退出。模式外不显示勾选框。
|
|
73
|
+
- 模式内:点整行勾选/取消,且**不会切换会话**;行首出勾选框(有状态点或归档的行没有这一格,用行尾那颗兜底);在跑的会话勾不动,悬停说明原因。
|
|
74
|
+
- 浮条在多选模式内常驻(0 条也显示,给退出入口):已选条数、读得到时合计「含 K 个子会话,跟着整组走」;「查看已选」展开限高可滚的清单(可逐条移除);同一个位置按当前可见可选行在「全选 / 取消全选」之间切换(折叠组、归档过滤外的行不算);「移入回收站 N 条」;「退出多选」。
|
|
75
|
+
- 确认框逐条列出这批会话(有子会话的那条标「含 M 个子会话」),汇总再说一次这一批会带走多少;卡片限高 280px 滚动,计数在开框时重新问。
|
|
76
|
+
- 执行是一次批量请求。部分失败不关框,原文落在失败那一行,按钮变成「重试这 N 条」;成功的条目 6 秒内可整批撤销(撤销 = 一次批量恢复)。
|
|
77
|
+
- 计数读不到就整句不说,也不因此锁死确认按钮——不拿 0 冒充「没有」。
|
|
78
|
+
|
|
79
|
+
### 级联与运行中拒绝
|
|
80
|
+
|
|
81
|
+
- 删除按**传递闭包**取子会话:子会话的子会话也跟着走,一层层取到底。
|
|
82
|
+
- 任一成员在跑(包括宿主的列表里看不见的子会话)⇒ 整组拒绝、零副作用。本插件不替用户终止正在进行的活。
|
|
83
|
+
- 运行中的父会话在菜单里置灰,提示「会话正在运行,先停止再删除」。
|
|
84
|
+
|
|
85
|
+
## 回收站
|
|
86
|
+
|
|
87
|
+
### 清单与详情
|
|
54
88
|
|
|
55
|
-
|
|
56
|
-
- **真跑过**由 `dsh.compatibility.dshReleases` 记录,只列实测过的宿主:**DSH Desktop 0.2.0-rc.2**(`compatible`)。别的 rc 装得上,但没验过。
|
|
89
|
+
侧栏面板列表里的「回收站」一行(与「插件」同层)。面板用满中央列:
|
|
57
90
|
|
|
58
|
-
|
|
91
|
+
- 盒子宽 ≥900px 时左右两栏:左清单(默认 420px)、右详情。中央列更窄时改成下钻——点一条看详情,「← 返回清单」回来。
|
|
92
|
+
- 清单按项目分组,每条两行:标题 / 「刚刚删除 · 24KB」或「删除于 7分钟前 · 350KB」。带子会话的行会说「含 N 个子会话」;级联落地前删掉的存量说「盘上还剩 M 个子会话」——两个数各说各的。
|
|
93
|
+
- 点行选中并在右边看详情;行右侧是「恢复」(描边)与「彻底删除…」(安静的红)。
|
|
94
|
+
- 头部是条数与合计占用(多选态换成已选条数);页脚是回收站位置说明与「清空回收站…」(多选态换成两颗动作按钮)。
|
|
95
|
+
- 组头可单独折叠,也可一键全部折叠/展开;折叠起来的组条目直接收掉,计数还留在组头上。两栏之间那根线可拖(键盘 ←→ / Home / End 也行),最窄 330px,Home 回默认 420px。
|
|
96
|
+
- 详情有四组元信息(时间/体量/计数/位置)和正文:只列你真实发的提问与助手的文字,宿主注入的上下文与工具调用不进正文。助手文字按 Markdown 排版,你的提问保持原话。
|
|
97
|
+
- 面板有读数中 / 空 / 失败 / 有条目;详情有未选中 / 读数中 / 失败 / 有内容。失败时把宿主原文上屏并给「重试」;详情坏掉只坏详情那一块,清单照常可用。空态时底部不出现「清空回收站…」。
|
|
59
98
|
|
|
60
|
-
|
|
99
|
+
### 恢复
|
|
61
100
|
|
|
62
|
-
|
|
101
|
+
- 单条恢复**不需要确认框**:按记录的原目录名放回原项目目录,并还原删除前的可见性——原来未归档的仍未归档,原来归档的仍在归档态。
|
|
102
|
+
- 原工作区已经被删掉也照常恢复:跳过账本挂接,会话归入「未分组」。清单行在点之前就说「原工作区已不存在,恢复后归入未分组」,批量恢复的确认框逐条重复同一句。
|
|
63
103
|
|
|
64
|
-
|
|
104
|
+
### 多选:批量恢复与批量彻底删除
|
|
65
105
|
|
|
66
|
-
|
|
67
|
-
- 头部是条数与合计占用(「2 个会话 · 共 49KB」);页脚是「清空回收站…」——**整页唯一一块实心红**
|
|
68
|
-
- 头部下面那一排(不随清单滚走):**全部折叠 / 全部展开**、**多选**(「刷新」已删——每次读清单都会先与回收站目录对账,不再需要手动刷)
|
|
69
|
-
- 组头可以单独折叠(也可以一键全部折叠),折叠起来的组条目直接收掉、计数还留在组头上;多选态下组头多出一颗**三态**勾选框——点它就是把这个项目的**全部**会话选上(包括折叠起来看不见的),只勾了几条时显示半选
|
|
70
|
-
- 两栏之间那根线可以左右拖(也能聚焦后用 ←→ 调,Home 回默认 420):最窄 330(再窄「删除于 … · 大小」那一行就被截断了),最右拖到「详情还读得开」为止
|
|
71
|
-
- 多选态下:行右侧两颗按钮退场、标题左侧出勾选框,点行就是切换勾选;页脚那颗换成「彻底删除 N 条」(一条没勾时置灰并说明原因),确认后按清单顺序逐条执行,失败的那条把原文留在自己身上。退出多选即清空勾选,不留看不见的勾选
|
|
72
|
-
- 面板四态:读数中 / 空(「回收站是空的」,此时底部**不出现**「清空回收站…」)/ 读数失败(宿主原文 + 「重试」)/ 有条目
|
|
73
|
-
- 详情四态:未选中(一句引导)/ 读数中 / 读数失败(原文 + 重试,**只坏详情这一块**,清单照常选、动作照常点)/ 有内容。正文读的是回收站目录里那条会话自己的日志:只列你真实发的提问与助手的文字,宿主注入的上下文与工具调用不进正文(条数会在页面上说明)。**助手的文字按 Markdown 排版**(与宿主会话区同一套字号与 token),**你的提问保持原话**(不解析,换行与反引号照原样)
|
|
74
|
-
- 「恢复」按记录的原目录名放回原项目目录,并还原它原本的可见性:删除前未归档的,恢复后仍未归档;删除前已归档的,恢复后仍在归档态
|
|
106
|
+
回收站工具栏的「多选」进入:
|
|
75
107
|
|
|
76
|
-
|
|
108
|
+
- 模式内行右侧两颗按钮退场、标题左侧出勾选框,点行就是切换勾选;组头多出一颗**三态**勾选框,点它把这个项目的**全部**会话选上(包括折叠起来看不见的),只勾了几条时显示半选。退出多选即清空勾选,不留看不见的勾选。
|
|
109
|
+
- 页脚换成两颗:「恢复 N 条」描边 + 「彻底删除 N 条」实心红;一条没勾时两颗都置灰并说明原因。
|
|
110
|
+
- **批量恢复带确认框**(单行恢复仍直接执行):逐条列出标题与项目名,正文说明「这一批会回到它们各自的原项目目录」。
|
|
111
|
+
- **批量彻底删除**同样先确认;确认框说清这一批不可恢复。
|
|
112
|
+
- 两者都是一次批量请求;失败的原文留在它自己那行,成功的随清单重读消失、已选计数下降。批量恢复失败后多选态保持,方便直接处理剩下的。
|
|
113
|
+
|
|
114
|
+
### 彻底删除与清空
|
|
115
|
+
|
|
116
|
+
- 单条「彻底删除…」与页脚「清空回收站…」共用同一个二次确认;单条那张卡片报的是**这一次会释放多少**(父加跟着进来的孩子,与页脚合计同一个数)。
|
|
117
|
+
- 没有自动过期,只有显式彻底删除或清空才会真的丢字节。
|
|
118
|
+
|
|
119
|
+
## 安装与兼容性
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
dsh plugin --profile <profile> add @xrn1997/dsh-session-delete
|
|
123
|
+
dsh plugin --profile <profile> remove @xrn1997/dsh-session-delete
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
装完**重启宿主**(Node 半不走 HMR)。卸载只移除插件,`~/.dsh/session-trash/` 下的已有目录不会被自动清理。
|
|
127
|
+
|
|
128
|
+
也可以离线分发:`pnpm pack` 出 tarball 后 `dsh plugin add ./dsh-session-delete-<版本>.tgz`。包里带预构建产物(`prepack` 会跑 `build`),这条路不需要为构建脚本授权。
|
|
129
|
+
|
|
130
|
+
**兼容性分两层,别混**:
|
|
131
|
+
|
|
132
|
+
- **装得上**由 `package.json` 的 `engines.dsh` 决定:宿主的 `dsh plugin add` 会拿运行中的宿主版本比这个区间,不满足即拒绝并回滚;清单里声明了 `engines.dsh` 时,`@deepseek-ai/dsh*` 的 peer 区间不再参与判断(本插件仍保持零 `dsh-*` peer)。当前窗口是 **`>=0.1.7-rc.2 <0.3.0-0`**(预发布版本参与匹配)。地板是**完整功能**的最低代次:`0.1.7-rc.2` 在 CLI 台架上验过四个槽位齐全、激活零错误;更低的 `0.1.7-rc.1` 缺行首勾选槽位,`0.1.5` 起连会话行入口都没有。
|
|
133
|
+
- **真跑过**由 `dsh.compatibility.dshReleases` 记录,只列桌面真机实测过的宿主:**DSH Desktop 0.2.0-rc.2、0.2.1-rc.1、0.2.1-rc.2**(均 `compatible`)。窗口内的其他代次装得上但没在桌面真机上验过;窗口外的一律拒装。
|
|
134
|
+
|
|
135
|
+
## 数据与安全
|
|
77
136
|
|
|
78
137
|
| 东西 | 位置 |
|
|
79
138
|
| --- | --- |
|
|
80
139
|
| 回收站本体 | `~/.dsh/session-trash/<时间戳>-<原目录名>/` |
|
|
81
140
|
| 回收站清单 | 本插件自己的 storage domain(介质在 `~/.dsh/storages/`) |
|
|
82
141
|
|
|
83
|
-
- 回收站与 `~/.dsh/sessions/` **同卷**:搬进搬出靠整目录 `rename
|
|
84
|
-
- **一格目录 =
|
|
85
|
-
- **清单丢了不致命**:会话文件自描述,解压后第一行即 header,含 `id` 与 `cwd
|
|
86
|
-
-
|
|
87
|
-
-
|
|
142
|
+
- 回收站与 `~/.dsh/sessions/` **同卷**:搬进搬出靠整目录 `rename`,原子、瞬时。
|
|
143
|
+
- **一格目录 = 一个会话**:父会话与跟着它进来的子会话各占一格,面板上折成一行。手工翻回收站目录时看到比清单多的目录,那是子会话。
|
|
144
|
+
- **清单丢了不致命**:会话文件自描述,解压后第一行即 header,含 `id` 与 `cwd`;每次读清单都会与回收站目录对账。
|
|
145
|
+
- **不自动过期**:不设天数上限,清空是显式动作;界面显示条数与占用。
|
|
146
|
+
- **附件不连坐**:附件是全局内容寻址的,与会话不是父子关系——删除不连带,也不会回收;被删会话独享的附件会留成垃圾(要回收得先有引用计数,属另一件事)。
|
|
147
|
+
- 回收站本体留在本机、不联网、不上传。
|
|
88
148
|
|
|
89
149
|
## 界面语言
|
|
90
150
|
|
|
@@ -94,19 +154,27 @@ dsh plugin --profile <profile> remove @xrn1997/dsh-session-delete
|
|
|
94
154
|
|
|
95
155
|
**装完侧栏没有「回收站」,会话菜单里也没有「删除」?**
|
|
96
156
|
|
|
97
|
-
|
|
157
|
+
先重启宿主。若仍没有,确认这一代宿主上本插件激活过——取数通道探测不过时,本插件一个入口都不注册:页面上看不出它的存在,但也不会因此把宿主拖死。这是有意用「入口不出现」表达不可用,不是禁用态。
|
|
98
158
|
|
|
99
159
|
**删完之后侧栏那一行怎么还在?**
|
|
100
160
|
|
|
101
|
-
|
|
161
|
+
本插件只在**宿主确实不再列它**时才通告客户端把那一行去掉;拿不准时宁可沉默(那时说「已移除」是假话)。刚巧遇上就刷新页面,那一行会自己消失。本机运行期没打开过的**冷会话**走正常路径,删完即走。
|
|
102
162
|
|
|
103
163
|
**正在运行的会话为什么删不掉?**
|
|
104
164
|
|
|
105
|
-
|
|
165
|
+
判据用的是宿主自己的活跃检查(与官方归档的准入同源)。一律拒绝,不替用户终止正在进行的活——先停下这个会话再删。**它正在跑的子会话也算**:一条父会话的子会话在跑时,整条删除会被拒绝,报的是那个子会话的 id(那一行宿主的列表里不显示,所以别看列表没在跑就以为删得掉)。
|
|
166
|
+
|
|
167
|
+
**批量删除/恢复为什么只成功了一部分?**
|
|
168
|
+
|
|
169
|
+
执行是逐条的,失败不会连坐。批量删除的确认框不关,原文留在失败那一行,按钮变成「重试这 N 条」;批量恢复失败的条目留在清单自己那行,多选态保持,成功的已经消失。重试只处理失败名单。
|
|
170
|
+
|
|
171
|
+
**把工作区删了,回收站里的会话还能恢复吗?**
|
|
172
|
+
|
|
173
|
+
能。宿主的「删除工作区」只删注册、保留目录和每个会话日志;恢复照常回盘,只是跳过账本挂接,会话落在「未分组」。清单行和批量恢复确认框都会在点之前说明。
|
|
106
174
|
|
|
107
175
|
**能恢复多久?**
|
|
108
176
|
|
|
109
|
-
|
|
177
|
+
不存在时间上限。不自动过期,只有显式清空或彻底删除才会真的丢字节。
|
|
110
178
|
|
|
111
179
|
## 开发
|
|
112
180
|
|
|
@@ -128,7 +196,7 @@ dsh plugin --profile <profile> add "link:<本仓绝对路径>"
|
|
|
128
196
|
|
|
129
197
|
改动生效分两半:**浏览器半**(`src/client/`)由宿主 HMR 推到浏览器,无需重启;**Node 半**不在热重载链路内,必须重启宿主。
|
|
130
198
|
|
|
131
|
-
|
|
199
|
+
设计住在 [`docs/design/2026-09-30-session-delete-design.md`](docs/design/2026-09-30-session-delete-design.md);回收站两栏、详情与批量动作另见 [`docs/design/2026-10-15-trash-detail-design.md`](docs/design/2026-10-15-trash-detail-design.md)。README 不复述。
|
|
132
200
|
|
|
133
201
|
## 贡献
|
|
134
202
|
|
package/icon.svg
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
<!--
|
|
2
|
-
插件卡片/组件行的图标(`package.json` 顶层 `icon`,宿主按**图片**读它、不内联 HTML)。
|
|
3
|
-
|
|
4
|
-
形状逐字取自 `@deepseek-ai/dsh-client-ui-primitives@0.2.0-rc.2` 的 `IconTrashOutlineRegular`
|
|
5
|
-
(与插件界面里那枚垃圾桶图标同源)。
|
|
6
|
-
|
|
7
|
-
两处与组件版不同:
|
|
8
|
-
1. 颜色写死。图片形式的 SVG **不继承 `currentColor`**(它是一份独立文档),所以不能像组件里那样
|
|
9
|
-
吃宿主的字色 ⇒ 取一个浅色/深色底上都能读的中灰。
|
|
10
|
-
2. 不带 `aria-hidden`。这不是行内构件,可及名由承载它的宿主卡片提供。
|
|
11
|
-
-->
|
|
12
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" stroke="#8a9099" stroke-width="1">
|
|
13
|
-
<path d="M1.28149 3.88831H14.7187" />
|
|
14
|
-
<path d="M5.41602 3.88833V2.47962C5.41602 2.29282 5.52492 2.11366 5.71876 1.98157C5.9126 1.84948 6.17551 1.77527 6.44964 1.77527H9.55053C9.82466 1.77527 10.0876 1.84948 10.2814 1.98157C10.4753 2.11366 10.5842 2.29282 10.5842 2.47962V3.88833" />
|
|
15
|
-
<path d="M2.57349 3.88831L3.19366 13.2943C3.21937 13.5502 3.33952 13.7872 3.53065 13.9593C3.72178 14.1313 3.97016 14.2259 4.22729 14.2246H11.7728C12.0299 14.2259 12.2783 14.1313 12.4694 13.9593C12.6605 13.7872 12.7807 13.5502 12.8064 13.2943L13.4266 3.88831" />
|
|
16
|
-
<path d="M6.44946 6.98926V11.1238" />
|
|
17
|
-
<path d="M9.55054 6.98926V11.1238" />
|
|
18
|
-
</svg>
|
|
1
|
+
<!--
|
|
2
|
+
插件卡片/组件行的图标(`package.json` 顶层 `icon`,宿主按**图片**读它、不内联 HTML)。
|
|
3
|
+
|
|
4
|
+
形状逐字取自 `@deepseek-ai/dsh-client-ui-primitives@0.2.0-rc.2` 的 `IconTrashOutlineRegular`
|
|
5
|
+
(与插件界面里那枚垃圾桶图标同源)。
|
|
6
|
+
|
|
7
|
+
两处与组件版不同:
|
|
8
|
+
1. 颜色写死。图片形式的 SVG **不继承 `currentColor`**(它是一份独立文档),所以不能像组件里那样
|
|
9
|
+
吃宿主的字色 ⇒ 取一个浅色/深色底上都能读的中灰。
|
|
10
|
+
2. 不带 `aria-hidden`。这不是行内构件,可及名由承载它的宿主卡片提供。
|
|
11
|
+
-->
|
|
12
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" stroke="#8a9099" stroke-width="1">
|
|
13
|
+
<path d="M1.28149 3.88831H14.7187" />
|
|
14
|
+
<path d="M5.41602 3.88833V2.47962C5.41602 2.29282 5.52492 2.11366 5.71876 1.98157C5.9126 1.84948 6.17551 1.77527 6.44964 1.77527H9.55053C9.82466 1.77527 10.0876 1.84948 10.2814 1.98157C10.4753 2.11366 10.5842 2.29282 10.5842 2.47962V3.88833" />
|
|
15
|
+
<path d="M2.57349 3.88831L3.19366 13.2943C3.21937 13.5502 3.33952 13.7872 3.53065 13.9593C3.72178 14.1313 3.97016 14.2259 4.22729 14.2246H11.7728C12.0299 14.2259 12.2783 14.1313 12.4694 13.9593C12.6605 13.7872 12.7807 13.5502 12.8064 13.2943L13.4266 3.88831" />
|
|
16
|
+
<path d="M6.44946 6.98926V11.1238" />
|
|
17
|
+
<path d="M9.55054 6.98926V11.1238" />
|
|
18
|
+
</svg>
|
package/lib/client.js
CHANGED
|
@@ -148,6 +148,7 @@ window.__ModuleLoader__.load({
|
|
|
148
148
|
"trash.row.deletedAgo": "删除于 {n}{unit}前",
|
|
149
149
|
"trash.row.subagents": "含 {n} 个子会话",
|
|
150
150
|
"trash.row.strandedSubagents": "盘上还剩 {n} 个子会话",
|
|
151
|
+
"trash.row.workspaceGone": "原工作区已不存在,恢复后归入未分组",
|
|
151
152
|
"trash.unit.minutes": "分钟",
|
|
152
153
|
"trash.unit.hours": "小时",
|
|
153
154
|
"trash.unit.days": "天",
|
|
@@ -250,6 +251,7 @@ window.__ModuleLoader__.load({
|
|
|
250
251
|
"trash.row.deletedAgo": "Deleted {n}{unit} ago",
|
|
251
252
|
"trash.row.subagents": "has {n} subagent sessions",
|
|
252
253
|
"trash.row.strandedSubagents": "{n} subagent sessions still on disk",
|
|
254
|
+
"trash.row.workspaceGone": "Its workspace no longer exists; it will be restored under “Ungrouped”",
|
|
253
255
|
"trash.unit.minutes": "min",
|
|
254
256
|
"trash.unit.hours": "h",
|
|
255
257
|
"trash.unit.days": "d",
|
|
@@ -25531,6 +25533,15 @@ window.__ModuleLoader__.load({
|
|
|
25531
25533
|
stranded > 0 ? t("trash.row.strandedSubagents", { n: stranded }) : ""
|
|
25532
25534
|
].filter(Boolean).join(" · ")
|
|
25533
25535
|
}),
|
|
25536
|
+
entry.workspaceMissing === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
25537
|
+
"data-dsh-session-delete": "trash-row-workspace-gone",
|
|
25538
|
+
style: {
|
|
25539
|
+
fontSize: 12,
|
|
25540
|
+
lineHeight: "16px",
|
|
25541
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
25542
|
+
},
|
|
25543
|
+
children: t("trash.row.workspaceGone")
|
|
25544
|
+
}) : null,
|
|
25534
25545
|
actionFailure[entry.id] === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
25535
25546
|
role: "status",
|
|
25536
25547
|
style: {
|
|
@@ -25827,22 +25838,33 @@ window.__ModuleLoader__.load({
|
|
|
25827
25838
|
gap: 2,
|
|
25828
25839
|
padding: "9px 12px"
|
|
25829
25840
|
},
|
|
25830
|
-
children: [
|
|
25831
|
-
|
|
25832
|
-
|
|
25833
|
-
|
|
25834
|
-
|
|
25835
|
-
|
|
25836
|
-
|
|
25837
|
-
|
|
25838
|
-
|
|
25839
|
-
|
|
25840
|
-
|
|
25841
|
-
|
|
25842
|
-
|
|
25843
|
-
|
|
25844
|
-
|
|
25845
|
-
|
|
25841
|
+
children: [
|
|
25842
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
25843
|
+
style: {
|
|
25844
|
+
...ELLIPSIS,
|
|
25845
|
+
fontSize: 13.5,
|
|
25846
|
+
lineHeight: "19px"
|
|
25847
|
+
},
|
|
25848
|
+
children: entry.title === "" ? t("common.untitled") : entry.title
|
|
25849
|
+
}),
|
|
25850
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
25851
|
+
style: {
|
|
25852
|
+
...ELLIPSIS,
|
|
25853
|
+
fontSize: 12,
|
|
25854
|
+
lineHeight: "16px",
|
|
25855
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
25856
|
+
},
|
|
25857
|
+
children: projectLabel(entry.projectDir, t)
|
|
25858
|
+
}),
|
|
25859
|
+
entry.workspaceMissing === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
25860
|
+
style: {
|
|
25861
|
+
fontSize: 12,
|
|
25862
|
+
lineHeight: "16px",
|
|
25863
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
25864
|
+
},
|
|
25865
|
+
children: t("trash.row.workspaceGone")
|
|
25866
|
+
}) : null
|
|
25867
|
+
]
|
|
25846
25868
|
}, entry.id))
|
|
25847
25869
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("trash.confirm.restore.body") })]
|
|
25848
25870
|
});
|
package/lib/index.js
CHANGED
|
@@ -721,6 +721,14 @@ async function controllerItems(host) {
|
|
|
721
721
|
return [];
|
|
722
722
|
}
|
|
723
723
|
}
|
|
724
|
+
/** 账本里没有那个工作区时 {@link HostPort.attach} / {@link HostPort.detach} 抛的原话开头。
|
|
725
|
+
* **原话与判据同住这里**:抛它的是这个端口,认它的也是端口给的那句,调用方不许另抄一份字符串。 */
|
|
726
|
+
const WORKSPACE_MISSING_MESSAGE = "workspace not found";
|
|
727
|
+
/** 这次失败是不是"账本里没那个工作区"(而不是另有故障)。恢复侧靠它把两种失败分开:
|
|
728
|
+
* 前者按「未分组」收,后者照旧回滚报 `io`——不给真故障静默改归属的机会。 */
|
|
729
|
+
function isWorkspaceMissingError(error) {
|
|
730
|
+
return error instanceof Error && error.message.startsWith(WORKSPACE_MISSING_MESSAGE);
|
|
731
|
+
}
|
|
724
732
|
/**
|
|
725
733
|
* 宿主服务层的**真实绑定**:各方法把动词层的问题翻成宿主官方服务的调用。
|
|
726
734
|
*
|
|
@@ -736,6 +744,11 @@ function createHostPort(ctx) {
|
|
|
736
744
|
async workspaceOf(sessionId) {
|
|
737
745
|
return registry.list().find((workspace) => workspace.sessionIds.includes(sessionId))?.id;
|
|
738
746
|
},
|
|
747
|
+
/** 账本里还有这个工作区吗:`registry.get(id)` 是 `this.entities.get(id)` 的直通(同步 Map 查找),
|
|
748
|
+
* 未知 id 回 `undefined` ⇒ 这里只做一次判别,不抛。 */
|
|
749
|
+
async workspaceExists(workspaceId) {
|
|
750
|
+
return registry.get(workspaceId) !== void 0;
|
|
751
|
+
},
|
|
739
752
|
/**
|
|
740
753
|
* 原生 cwd → 宿主项目目录名(`projectKey`);没有 cwd 时按宿主约定落到 `_no-cwd`。
|
|
741
754
|
*
|
|
@@ -885,13 +898,13 @@ function createHostPort(ctx) {
|
|
|
885
898
|
/** 摘账本:实体方法 `detachSession`(顶层没有);未知工作区**响亮失败**,不静默当成功。 */
|
|
886
899
|
async detach(sessionId, workspaceId) {
|
|
887
900
|
const workspace = registry.get(workspaceId);
|
|
888
|
-
if (workspace === void 0) throw new Error(
|
|
901
|
+
if (workspace === void 0) throw new Error(`${WORKSPACE_MISSING_MESSAGE} (detach): ${workspaceId}`);
|
|
889
902
|
await workspace.detachSession(sessionId);
|
|
890
903
|
},
|
|
891
904
|
/** 挂账本:实体方法 `attachSession`(顶层没有)。 */
|
|
892
905
|
async attach(sessionId, workspaceId) {
|
|
893
906
|
const workspace = registry.get(workspaceId);
|
|
894
|
-
if (workspace === void 0) throw new Error(
|
|
907
|
+
if (workspace === void 0) throw new Error(`${WORKSPACE_MISSING_MESSAGE} (attach): ${workspaceId}`);
|
|
895
908
|
await workspace.attachSession(sessionId);
|
|
896
909
|
},
|
|
897
910
|
/** 确保归档:官方 `archiveSession(sessionId)`。归档是 registry **全局**的,与工作区账目无关
|
|
@@ -1085,6 +1098,8 @@ function purgeTrash(deps) {
|
|
|
1085
1098
|
* ① **预检整组**(一个字节都不写):每个成员的回收站目录与项目目录都得在。缺席的判据与原来一致
|
|
1086
1099
|
* (回收站目录不在 = `trash-empty`;原项目目录不在 = `project-missing`,且绝不静默建目录)。
|
|
1087
1100
|
* 预检在写之前,是为了不让"父已经回到 `sessions/`、孩子因为项目目录没了而回不去"这种半组留在外面。
|
|
1101
|
+
* **工作区账本不在这条硬前提里**:宿主的「删除工作区」只删注册、保留日志,记下的工作区没了就按
|
|
1102
|
+
* 「未分组」收(判据在 {@link attachableWorkspaceId}),照旧回盘——恢复是加法,不该因为一个可选的账目而失败。
|
|
1088
1103
|
* ② **逐个恢复**,顺序与删除相反:文件先回来 → 挂账本 → 还原可见性 → 摘清册行 → 通告回来。
|
|
1089
1104
|
* ③ 中途失败把这个成员自己退回回收站(就地回滚),并把**已经恢复完的**兄弟按相反顺序一起退回,失败照抛。
|
|
1090
1105
|
*/
|
|
@@ -1120,39 +1135,65 @@ function restoreSession(deps) {
|
|
|
1120
1135
|
function restoreTargetDir(deps, entry) {
|
|
1121
1136
|
return sessionDirOf(entry.projectDir, parseEntryId(entry.id).sessionDirName, deps.home);
|
|
1122
1137
|
}
|
|
1138
|
+
/**
|
|
1139
|
+
* 这一次恢复**到底挂不挂账本**:交出能挂的那个 `workspaceId`,挂不了就 `undefined`。
|
|
1140
|
+
*
|
|
1141
|
+
* 两种缺席是同一件事:空串 = 删除时它就没有账目(「未分组」的会话);非空但账本里已经没它 =
|
|
1142
|
+
* 宿主的「删除工作区」只删注册、保留目录与每一份日志,那些会话之后本来就在「未分组」下。
|
|
1143
|
+
* 恢复不该为了挂一个不存在的账本把整条退回回收站。
|
|
1144
|
+
*
|
|
1145
|
+
* 判据**只有这一处**:`restoreOne` 拿它决定挂不挂,`GET /list` 拿它决定行上那句「原工作区已不存在,
|
|
1146
|
+
* 恢复后归入未分组」说不说。两边各问一遍就会有一天一个说、一个不挂。
|
|
1147
|
+
*/
|
|
1148
|
+
async function attachableWorkspaceId(host, entry) {
|
|
1149
|
+
if (entry.workspaceId === "") return void 0;
|
|
1150
|
+
return await host.workspaceExists(entry.workspaceId) ? entry.workspaceId : void 0;
|
|
1151
|
+
}
|
|
1152
|
+
/** 这一条记着的工作区**是不是已经没了**(区别于"删除时它就没有账目"的空串)——清单行据此决定
|
|
1153
|
+
* 要不要在点之前说那句「原工作区已不存在,恢复后归入未分组」。由 {@link attachableWorkspaceId}
|
|
1154
|
+
* 派生,不另起一份判据:那样会有一天一个说、一个不挂。 */
|
|
1155
|
+
async function isWorkspaceMissing(host, entry) {
|
|
1156
|
+
return entry.workspaceId !== "" && await attachableWorkspaceId(host, entry) === void 0;
|
|
1157
|
+
}
|
|
1123
1158
|
/** 恢复一个成员,交回它的反向动作。 */
|
|
1124
1159
|
async function restoreOne(deps, entry) {
|
|
1125
1160
|
const trashDir = trashDirOf(entry.id, deps.home);
|
|
1126
1161
|
const sessionDir = restoreTargetDir(deps, entry);
|
|
1162
|
+
const workspaceId = await attachableWorkspaceId(deps.host, entry);
|
|
1163
|
+
/** 真挂上了账本才记下那个 id——回滚里"摘账本"那一步只对挂过的成员存在。 */
|
|
1164
|
+
let attachedTo;
|
|
1127
1165
|
await deps.fs.rename(trashDir, sessionDir).catch((error) => {
|
|
1128
1166
|
throw verbError("io", `放回会话目录失败:${String(error)}`, error);
|
|
1129
1167
|
});
|
|
1130
|
-
if (
|
|
1131
|
-
await deps.host.attach(entry.sessionId,
|
|
1168
|
+
if (workspaceId !== void 0) try {
|
|
1169
|
+
await deps.host.attach(entry.sessionId, workspaceId);
|
|
1170
|
+
attachedTo = workspaceId;
|
|
1132
1171
|
} catch (error) {
|
|
1133
|
-
await deps.
|
|
1134
|
-
|
|
1172
|
+
if (!isWorkspaceMissingError(error) || await deps.host.workspaceExists(workspaceId)) {
|
|
1173
|
+
await deps.fs.rename(sessionDir, trashDir).catch(() => {});
|
|
1174
|
+
throw verbError("io", `挂回账本失败:${String(error)}`, error);
|
|
1175
|
+
}
|
|
1135
1176
|
}
|
|
1136
1177
|
if (!entry.wasArchived) try {
|
|
1137
1178
|
await deps.host.unarchive(entry.sessionId);
|
|
1138
1179
|
} catch (error) {
|
|
1139
|
-
await deps.host.archive(entry.sessionId,
|
|
1140
|
-
if (
|
|
1180
|
+
await deps.host.archive(entry.sessionId, workspaceId ?? void 0).catch(() => {});
|
|
1181
|
+
if (attachedTo !== void 0) await deps.host.detach(entry.sessionId, attachedTo).catch(() => {});
|
|
1141
1182
|
await deps.fs.rename(sessionDir, trashDir).catch(() => {});
|
|
1142
1183
|
throw verbError("io", `取消归档失败:${String(error)}`, error);
|
|
1143
1184
|
}
|
|
1144
1185
|
try {
|
|
1145
1186
|
await deps.manifest.remove(entry.id);
|
|
1146
1187
|
} catch (error) {
|
|
1147
|
-
if (!entry.wasArchived) await deps.host.archive(entry.sessionId,
|
|
1148
|
-
if (
|
|
1188
|
+
if (!entry.wasArchived) await deps.host.archive(entry.sessionId, workspaceId ?? void 0).catch(() => {});
|
|
1189
|
+
if (attachedTo !== void 0) await deps.host.detach(entry.sessionId, attachedTo).catch(() => {});
|
|
1149
1190
|
await deps.fs.rename(sessionDir, trashDir).catch(() => {});
|
|
1150
1191
|
throw verbError("io", `摘回收站条目失败:${String(error)}`, error);
|
|
1151
1192
|
}
|
|
1152
1193
|
return async () => {
|
|
1153
1194
|
await deps.manifest.add(entry).catch(() => {});
|
|
1154
|
-
if (!entry.wasArchived) await deps.host.archive(entry.sessionId,
|
|
1155
|
-
if (
|
|
1195
|
+
if (!entry.wasArchived) await deps.host.archive(entry.sessionId, workspaceId ?? void 0).catch(() => {});
|
|
1196
|
+
if (attachedTo !== void 0) await deps.host.detach(entry.sessionId, attachedTo).catch(() => {});
|
|
1156
1197
|
await deps.fs.rename(sessionDir, trashDir).catch(() => {});
|
|
1157
1198
|
if (await deps.host.evictLive(entry.sessionId).catch(() => "unknown") !== "unknown") await deps.host.announceRemoved(entry.sessionId).catch(() => {});
|
|
1158
1199
|
};
|
|
@@ -1578,8 +1619,8 @@ function requireMethod(method, want, route) {
|
|
|
1578
1619
|
/**
|
|
1579
1620
|
* 把清册的全部行装配成「一行 = 一组」:孩子折进父那一行、不单独列(宿主的客户端本来就藏孩子,
|
|
1580
1621
|
* 我们若单列就会冒出一堆点不到的行);父行不在时孩子自己站出来占一行,否则它既看不见也清不掉。
|
|
1581
|
-
*
|
|
1582
|
-
* 文件里真有的事实,而"还剩几个孩子"
|
|
1622
|
+
* 这些派生值只加在这次回信上,**不落清册**——清册该记的是
|
|
1623
|
+
* 文件里真有的事实,而"还剩几个孩子""工作区还在不在"是随盘上随时变的读数。取数失败照抛(响亮失败,不端一份
|
|
1583
1624
|
* 少了这栏的清单当没事发生)。
|
|
1584
1625
|
*/
|
|
1585
1626
|
async function assembleTrashRows(deps, rows) {
|
|
@@ -1596,18 +1637,21 @@ async function assembleTrashRows(deps, rows) {
|
|
|
1596
1637
|
else siblings.push(row);
|
|
1597
1638
|
}
|
|
1598
1639
|
const visible = rows.filter((row) => !foldedInto.has(row.id));
|
|
1599
|
-
const stranded = await deps.host.subagentCounts(visible.map((row) => row.sessionId));
|
|
1640
|
+
const [stranded, missingByRowId] = await Promise.all([deps.host.subagentCounts(visible.map((row) => row.sessionId)), Promise.all(visible.map(async (row) => [row.id, await isWorkspaceMissing(deps.host, row)]))]);
|
|
1641
|
+
const gone = new Map(missingByRowId);
|
|
1600
1642
|
return visible.map((row) => {
|
|
1601
1643
|
const kids = kidsByParent.get(row.id);
|
|
1602
1644
|
const left = stranded[row.sessionId];
|
|
1603
|
-
|
|
1645
|
+
const missing = gone.get(row.id) === true;
|
|
1646
|
+
if (kids === void 0 && left === void 0 && !missing) return row;
|
|
1604
1647
|
return {
|
|
1605
1648
|
...row,
|
|
1606
1649
|
...kids === void 0 ? {} : {
|
|
1607
1650
|
subagentCount: kids.length,
|
|
1608
1651
|
subagentBytes: kids.reduce((total, kid) => total + kid.sizeBytes, 0)
|
|
1609
1652
|
},
|
|
1610
|
-
...left === void 0 ? {} : { strandedSubagents: left }
|
|
1653
|
+
...left === void 0 ? {} : { strandedSubagents: left },
|
|
1654
|
+
...missing ? { workspaceMissing: true } : {}
|
|
1611
1655
|
};
|
|
1612
1656
|
});
|
|
1613
1657
|
}
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
39
39
|
import type { TrashEntry } from '../trash/entry.js';
|
|
40
40
|
import type { DeleteDeps } from '../verbs/delete-session.js';
|
|
41
|
-
/** 清单交回浏览器的那一行:清册那一行 +
|
|
41
|
+
/** 清单交回浏览器的那一行:清册那一行 + 组视图的只读数。 */
|
|
42
42
|
export interface TrashRow extends TrashEntry {
|
|
43
43
|
/** 跟着这条父会话一起进回收站的孩子数(来源:清册里 `parentEntryId` 指向它的行)。 */
|
|
44
44
|
readonly subagentCount?: number;
|
|
@@ -47,5 +47,9 @@ export interface TrashRow extends TrashEntry {
|
|
|
47
47
|
/** **盘上还留着、没被带走**的孩子数(来源:宿主 `sessionQuery`)。级联落地前删掉的那些存量才非零;
|
|
48
48
|
* 与 `subagentCount` 是两件事,界面上必须各说各的(不许用一个冒充另一个)。 */
|
|
49
49
|
readonly strandedSubagents?: number;
|
|
50
|
+
/** 删除时记下的工作区**已经不在账本里**(宿主的「删除工作区」只删注册、保留日志)。恢复会照常
|
|
51
|
+
* 回盘、归入「未分组」——界面上点之前就得知道,别等它从清单里消失才发现换了地方。
|
|
52
|
+
* 缺席 = 工作区还在 / 没有工作区可判(与上面三个只读数同一条"不拿缺席冒充否"的规矩)。 */
|
|
53
|
+
readonly workspaceMissing?: boolean;
|
|
50
54
|
}
|
|
51
55
|
export declare function createApiHandler(deps: DeleteDeps): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
@@ -54,6 +54,7 @@ declare const ZH: {
|
|
|
54
54
|
readonly 'trash.row.deletedAgo': "删除于 {n}{unit}前";
|
|
55
55
|
readonly 'trash.row.subagents': "含 {n} 个子会话";
|
|
56
56
|
readonly 'trash.row.strandedSubagents': "盘上还剩 {n} 个子会话";
|
|
57
|
+
readonly 'trash.row.workspaceGone': "原工作区已不存在,恢复后归入未分组";
|
|
57
58
|
readonly 'trash.unit.minutes': "分钟";
|
|
58
59
|
readonly 'trash.unit.hours': "小时";
|
|
59
60
|
readonly 'trash.unit.days': "天";
|
|
@@ -43,15 +43,18 @@ export interface TrashEntryLike {
|
|
|
43
43
|
deletedAt: number;
|
|
44
44
|
title: string;
|
|
45
45
|
sizeBytes: number;
|
|
46
|
-
/**
|
|
46
|
+
/** 组视图的只读数,**都由 `list` 现算现给、不住在清册里**(清册只记 `parentEntryId` 那件事实)。
|
|
47
47
|
* 缺席 = 不知道或没有 ⇒ 界面上半句都不出现,也不许拿 0 冒充"没有"。
|
|
48
48
|
* `subagentCount`:跟着这条父会话**一起进了回收站**的孩子数(清册里指向它的孩子行)。
|
|
49
49
|
* `subagentBytes`:那些孩子的字节合计——孩子不单独列行,头部合计只加父那一格就会少报。
|
|
50
50
|
* `strandedSubagents`:**还留在盘上没被带走**的孩子数(来源是宿主 `sessionQuery`),
|
|
51
|
-
* 级联落地之前删掉的那些存量才非零。与 `subagentCount` 各说各的,不许互相冒充。
|
|
51
|
+
* 级联落地之前删掉的那些存量才非零。与 `subagentCount` 各说各的,不许互相冒充。
|
|
52
|
+
* `workspaceMissing`:删除时记下的工作区已被删掉(宿主的「删除工作区」只删注册、保留日志)——
|
|
53
|
+
* 恢复照常回盘,但会落在「未分组」。行上先说一句,别等它从清单里消失才发现换了地方。 */
|
|
52
54
|
subagentCount?: number;
|
|
53
55
|
subagentBytes?: number;
|
|
54
56
|
strandedSubagents?: number;
|
|
57
|
+
workspaceMissing?: boolean;
|
|
55
58
|
}
|
|
56
59
|
/** 彻底删除的回执(镜像 `PurgeResult`)。 */
|
|
57
60
|
export interface PurgeResultLike {
|
|
@@ -3,6 +3,15 @@
|
|
|
3
3
|
export type EvictionOutcome = 'evicted' | 'not-live' | 'unknown';
|
|
4
4
|
export interface HostPort {
|
|
5
5
|
workspaceOf(sessionId: string): Promise<string | undefined>;
|
|
6
|
+
/** 账本里还有这个工作区吗。**只读的判别**(`registry.get(id) !== undefined`),不抛。
|
|
7
|
+
*
|
|
8
|
+
* **为什么恢复需要它**:宿主的「删除工作区」只删**注册**、保留目录与每一份会话日志
|
|
9
|
+
* (asar `WorkspaceRegistry.delete` 的 JSDoc 原文 "Delete one workspace registration while
|
|
10
|
+
* retaining its directory and every session log"),那些会话之后显示在「未分组」下。回收站里
|
|
11
|
+
* **删在工作区之前**的条目记着那个已被删掉的 `workspaceId`,恢复时若照旧 `attach` 就会
|
|
12
|
+
* `workspace not found` 整条失败(会话留在回收站里,用户看起来就是"恢复报错")。所以恢复
|
|
13
|
+
* 先问这一句:不在 ⇒ 不挂账本,照常回盘、归入「未分组」(与宿主自己处理这类会话一致)。 */
|
|
14
|
+
workspaceExists(workspaceId: string): Promise<boolean>;
|
|
6
15
|
/** 会话落盘的**项目目录名**(`<sessions>/<projectKey(cwd)>`);账本里没有这个会话 ⇒ `undefined`。
|
|
7
16
|
* 与 `workspaceOf` 同一口径:**未命中是正常的 no-op,不是故障**(存储真故障照抛)。 */
|
|
8
17
|
projectDirOf(sessionId: string): Promise<string | undefined>;
|
|
@@ -75,6 +84,9 @@ export declare const NO_CWD_PROJECT_DIR = "_no-cwd";
|
|
|
75
84
|
* (手工放进回收站的目录,从日志 header 的 `cwd` 反推它原本属于哪个项目目录)——算法只有这一处家。
|
|
76
85
|
*/
|
|
77
86
|
export declare function projectKey(cwd: string): string;
|
|
87
|
+
/** 这次失败是不是"账本里没那个工作区"(而不是另有故障)。恢复侧靠它把两种失败分开:
|
|
88
|
+
* 前者按「未分组」收,后者照旧回滚报 `io`——不给真故障静默改归属的机会。 */
|
|
89
|
+
export declare function isWorkspaceMissingError(error: unknown): boolean;
|
|
78
90
|
/**
|
|
79
91
|
* 宿主服务层的**真实绑定**:各方法把动词层的问题翻成宿主官方服务的调用。
|
|
80
92
|
*
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type TrashEntry } from '../trash/entry.js';
|
|
2
|
+
import { type HostPort } from '../ports/host-port.js';
|
|
2
3
|
import type { DeleteDeps } from './delete-session.js';
|
|
3
4
|
/**
|
|
4
5
|
* 恢复 = 把删除那次**按原路退回**,而且按组走:当年是按组一起搬进来的(父 + 它在盘上的孩子),
|
|
@@ -8,6 +9,8 @@ import type { DeleteDeps } from './delete-session.js';
|
|
|
8
9
|
* ① **预检整组**(一个字节都不写):每个成员的回收站目录与项目目录都得在。缺席的判据与原来一致
|
|
9
10
|
* (回收站目录不在 = `trash-empty`;原项目目录不在 = `project-missing`,且绝不静默建目录)。
|
|
10
11
|
* 预检在写之前,是为了不让"父已经回到 `sessions/`、孩子因为项目目录没了而回不去"这种半组留在外面。
|
|
12
|
+
* **工作区账本不在这条硬前提里**:宿主的「删除工作区」只删注册、保留日志,记下的工作区没了就按
|
|
13
|
+
* 「未分组」收(判据在 {@link attachableWorkspaceId}),照旧回盘——恢复是加法,不该因为一个可选的账目而失败。
|
|
11
14
|
* ② **逐个恢复**,顺序与删除相反:文件先回来 → 挂账本 → 还原可见性 → 摘清册行 → 通告回来。
|
|
12
15
|
* ③ 中途失败把这个成员自己退回回收站(就地回滚),并把**已经恢复完的**兄弟按相反顺序一起退回,失败照抛。
|
|
13
16
|
*/
|
|
@@ -17,3 +20,18 @@ export declare function restoreSession(deps: DeleteDeps): (entryId: string, opti
|
|
|
17
20
|
* 单条一份摘要 = 一条一次宿主全表扫,那是 2026-10-17 "恢复很慢"的头号来源。 */
|
|
18
21
|
readonly suppressAnnounce?: boolean;
|
|
19
22
|
}) => Promise<TrashEntry>;
|
|
23
|
+
/**
|
|
24
|
+
* 这一次恢复**到底挂不挂账本**:交出能挂的那个 `workspaceId`,挂不了就 `undefined`。
|
|
25
|
+
*
|
|
26
|
+
* 两种缺席是同一件事:空串 = 删除时它就没有账目(「未分组」的会话);非空但账本里已经没它 =
|
|
27
|
+
* 宿主的「删除工作区」只删注册、保留目录与每一份日志,那些会话之后本来就在「未分组」下。
|
|
28
|
+
* 恢复不该为了挂一个不存在的账本把整条退回回收站。
|
|
29
|
+
*
|
|
30
|
+
* 判据**只有这一处**:`restoreOne` 拿它决定挂不挂,`GET /list` 拿它决定行上那句「原工作区已不存在,
|
|
31
|
+
* 恢复后归入未分组」说不说。两边各问一遍就会有一天一个说、一个不挂。
|
|
32
|
+
*/
|
|
33
|
+
export declare function attachableWorkspaceId(host: Pick<HostPort, 'workspaceExists'>, entry: TrashEntry): Promise<string | undefined>;
|
|
34
|
+
/** 这一条记着的工作区**是不是已经没了**(区别于"删除时它就没有账目"的空串)——清单行据此决定
|
|
35
|
+
* 要不要在点之前说那句「原工作区已不存在,恢复后归入未分组」。由 {@link attachableWorkspaceId}
|
|
36
|
+
* 派生,不另起一份判据:那样会有一天一个说、一个不挂。 */
|
|
37
|
+
export declare function isWorkspaceMissing(host: Pick<HostPort, 'workspaceExists'>, entry: TrashEntry): Promise<boolean>;
|
package/locale/en.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
{
|
|
2
|
-
"meta": {
|
|
3
|
-
"title": "Session Delete",
|
|
4
|
-
"description": "Delete sessions: delete moves them to a recoverable trash, with an explicit permanent delete."
|
|
5
|
-
}
|
|
6
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"meta": {
|
|
3
|
+
"title": "Session Delete",
|
|
4
|
+
"description": "Delete sessions: delete moves them to a recoverable trash, with an explicit permanent delete."
|
|
5
|
+
}
|
|
6
|
+
}
|
package/locale/zh.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
{
|
|
2
|
-
"meta": {
|
|
3
|
-
"title": "会话删除",
|
|
4
|
-
"description": "删除会话:删除 = 移入回收站(可恢复),另设显式的彻底删除。"
|
|
5
|
-
}
|
|
6
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"meta": {
|
|
3
|
+
"title": "会话删除",
|
|
4
|
+
"description": "删除会话:删除 = 移入回收站(可恢复),另设显式的彻底删除。"
|
|
5
|
+
}
|
|
6
|
+
}
|
package/package.json
CHANGED
|
@@ -1,100 +1,105 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@xrn1997/dsh-session-delete",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "在 DeepSeek Harness 里删除会话:删除=移入回收站(可恢复),另设显式彻底删除",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "lib/index.js",
|
|
7
|
-
"types": "lib/types/index.d.ts",
|
|
8
|
-
"exports": {
|
|
9
|
-
".": {
|
|
10
|
-
"types": "./lib/types/index.d.ts",
|
|
11
|
-
"default": "./lib/index.js"
|
|
12
|
-
},
|
|
13
|
-
"./client": {
|
|
14
|
-
"types": "./lib/types/client/index.d.ts",
|
|
15
|
-
"default": "./lib/client.js"
|
|
16
|
-
},
|
|
17
|
-
"./locale/*.json": "./locale/*.json",
|
|
18
|
-
"./package.json": "./package.json"
|
|
19
|
-
},
|
|
20
|
-
"icon": "./icon.svg",
|
|
21
|
-
"files": [
|
|
22
|
-
"lib/index.js",
|
|
23
|
-
"lib/client.js",
|
|
24
|
-
"lib/types/**/*.d.ts",
|
|
25
|
-
"locale",
|
|
26
|
-
"icon.svg",
|
|
27
|
-
"cordis.patch.yml",
|
|
28
|
-
"README.md",
|
|
29
|
-
"LICENSE"
|
|
30
|
-
],
|
|
31
|
-
"engines": {
|
|
32
|
-
"node": ">=24"
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
"@
|
|
47
|
-
"@testing-library/
|
|
48
|
-
"@testing-library/
|
|
49
|
-
"@
|
|
50
|
-
"@types/
|
|
51
|
-
"@types/react
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
"
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
"
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
"
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
"
|
|
84
|
-
"
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
"
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
"
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
"
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@xrn1997/dsh-session-delete",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "在 DeepSeek Harness 里删除会话:删除=移入回收站(可恢复),另设显式彻底删除",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"types": "lib/types/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./lib/types/index.d.ts",
|
|
11
|
+
"default": "./lib/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./client": {
|
|
14
|
+
"types": "./lib/types/client/index.d.ts",
|
|
15
|
+
"default": "./lib/client.js"
|
|
16
|
+
},
|
|
17
|
+
"./locale/*.json": "./locale/*.json",
|
|
18
|
+
"./package.json": "./package.json"
|
|
19
|
+
},
|
|
20
|
+
"icon": "./icon.svg",
|
|
21
|
+
"files": [
|
|
22
|
+
"lib/index.js",
|
|
23
|
+
"lib/client.js",
|
|
24
|
+
"lib/types/**/*.d.ts",
|
|
25
|
+
"locale",
|
|
26
|
+
"icon.svg",
|
|
27
|
+
"cordis.patch.yml",
|
|
28
|
+
"README.md",
|
|
29
|
+
"LICENSE"
|
|
30
|
+
],
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=24",
|
|
33
|
+
"dsh": ">=0.1.7-rc.2 <0.3.0-0"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsdown && tsc -p tsconfig.build.json",
|
|
37
|
+
"typecheck": "tsc --noEmit",
|
|
38
|
+
"test": "vitest run",
|
|
39
|
+
"prepack": "npm run build"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"@deepseek-ai/cordis": "^4.0.4",
|
|
43
|
+
"react": "^18.3.1"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@deepseek-ai/cordis": "^4.0.4",
|
|
47
|
+
"@testing-library/dom": "^10.4.2",
|
|
48
|
+
"@testing-library/jest-dom": "^6.10.0",
|
|
49
|
+
"@testing-library/react": "^16.3.3",
|
|
50
|
+
"@types/node": "24.19.0",
|
|
51
|
+
"@types/react": "18.3.31",
|
|
52
|
+
"@types/react-dom": "18.3.5",
|
|
53
|
+
"@types/semver": "^7.8.0",
|
|
54
|
+
"jsdom": "^26.1.0",
|
|
55
|
+
"react": "^18.3.1",
|
|
56
|
+
"react-dom": "18.3.1",
|
|
57
|
+
"semver": "^7.8.5",
|
|
58
|
+
"tsdown": "0.23.0",
|
|
59
|
+
"typescript": "5.9.3",
|
|
60
|
+
"vitest": "3.2.7"
|
|
61
|
+
},
|
|
62
|
+
"dsh": {
|
|
63
|
+
"bundle": {
|
|
64
|
+
"patch": "./cordis.patch.yml"
|
|
65
|
+
},
|
|
66
|
+
"client": {
|
|
67
|
+
"inject": [
|
|
68
|
+
"@deepseek-ai/dsh-client-locale"
|
|
69
|
+
],
|
|
70
|
+
"platform": "web"
|
|
71
|
+
},
|
|
72
|
+
"compatibility": {
|
|
73
|
+
"dshReleases": {
|
|
74
|
+
"0.2.0-rc.2": "compatible",
|
|
75
|
+
"0.2.1-rc.1": "compatible",
|
|
76
|
+
"0.2.1-rc.2": "compatible"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
"publishConfig": {
|
|
81
|
+
"access": "public"
|
|
82
|
+
},
|
|
83
|
+
"repository": {
|
|
84
|
+
"type": "git",
|
|
85
|
+
"url": "git+https://github.com/xrn1997/dsh-session-delete.git"
|
|
86
|
+
},
|
|
87
|
+
"homepage": "https://github.com/xrn1997/dsh-session-delete#readme",
|
|
88
|
+
"bugs": {
|
|
89
|
+
"url": "https://github.com/xrn1997/dsh-session-delete/issues"
|
|
90
|
+
},
|
|
91
|
+
"keywords": [
|
|
92
|
+
"dsh",
|
|
93
|
+
"deepseek-harness",
|
|
94
|
+
"plugin",
|
|
95
|
+
"session",
|
|
96
|
+
"trash",
|
|
97
|
+
"delete"
|
|
98
|
+
],
|
|
99
|
+
"author": "xrn1997",
|
|
100
|
+
"license": "Apache-2.0",
|
|
101
|
+
"dependencies": {
|
|
102
|
+
"react-markdown": "^10.1.0",
|
|
103
|
+
"remark-gfm": "^4.0.1"
|
|
104
|
+
}
|
|
105
|
+
}
|