@xrn1997/dsh-session-delete 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +46 -124
  2. package/icon.svg +18 -18
  3. package/lib/client.js +1749 -962
  4. package/lib/index.js +1841 -985
  5. package/lib/types/api/dispatch.d.ts +24 -44
  6. package/lib/types/api/route-table.d.ts +35 -0
  7. package/lib/types/api/trash-rows.d.ts +31 -0
  8. package/lib/types/client/batch-bar.d.ts +6 -6
  9. package/lib/types/client/batch-confirm.d.ts +5 -3
  10. package/lib/types/client/batch-menu-item.d.ts +2 -2
  11. package/lib/types/client/batch-select.d.ts +7 -50
  12. package/lib/types/client/batch-selection-store.d.ts +30 -0
  13. package/lib/types/client/copy.d.ts +16 -1
  14. package/lib/types/client/delete-confirm.d.ts +2 -0
  15. package/lib/types/client/host-rows.d.ts +35 -0
  16. package/lib/types/client/main-view.d.ts +37 -0
  17. package/lib/types/client/receipt.d.ts +43 -0
  18. package/lib/types/client/remote.d.ts +62 -76
  19. package/lib/types/client/store.d.ts +17 -0
  20. package/lib/types/client/trash-confirm.d.ts +39 -0
  21. package/lib/types/client/trash-panel.d.ts +4 -2
  22. package/lib/types/client/trash-store.d.ts +2 -0
  23. package/lib/types/client/trash-view.d.ts +38 -0
  24. package/lib/types/client/ui/icons.d.ts +3 -3
  25. package/lib/types/client/undo-toast.d.ts +21 -5
  26. package/lib/types/client/use-column-resize.d.ts +2 -2
  27. package/lib/types/client/use-panel-width.d.ts +1 -1
  28. package/lib/types/client/use-trash-panel-controller.d.ts +55 -0
  29. package/lib/types/client/use-trash-query.d.ts +32 -0
  30. package/lib/types/client/use-trash-selection.d.ts +32 -0
  31. package/lib/types/client/write-commit.d.ts +10 -0
  32. package/lib/types/deps.d.ts +14 -0
  33. package/lib/types/index.d.ts +9 -15
  34. package/lib/types/log/zstd-frames.d.ts +1 -1
  35. package/lib/types/ports/agent-handles.d.ts +25 -0
  36. package/lib/types/ports/home.d.ts +19 -0
  37. package/lib/types/ports/host-port.d.ts +70 -20
  38. package/lib/types/remote/map-error.d.ts +3 -3
  39. package/lib/types/shared/wire.d.ts +107 -11
  40. package/lib/types/trash/detail.d.ts +6 -24
  41. package/lib/types/trash/entry.d.ts +29 -6
  42. package/lib/types/trash/groups.d.ts +45 -0
  43. package/lib/types/trash/manifest.d.ts +2 -1
  44. package/lib/types/trash/reconcile.d.ts +2 -2
  45. package/lib/types/trash/storage-manifest.d.ts +12 -0
  46. package/lib/types/verbs/{apply.d.ts → batch.d.ts} +1 -1
  47. package/lib/types/verbs/delete-session.d.ts +12 -18
  48. package/lib/types/verbs/errors.d.ts +17 -6
  49. package/lib/types/verbs/member-journal.d.ts +27 -0
  50. package/lib/types/verbs/member-presence.d.ts +35 -0
  51. package/lib/types/verbs/orphan-subagents.d.ts +58 -0
  52. package/lib/types/verbs/permanent-removal.d.ts +113 -0
  53. package/lib/types/verbs/purge-trash.d.ts +19 -3
  54. package/lib/types/verbs/restore-session.d.ts +15 -13
  55. package/locale/en.json +6 -6
  56. package/locale/zh.json +6 -6
  57. package/package.json +105 -105
@@ -3,7 +3,7 @@
3
3
  * 加上信封判别与注入面那一处「展开成裸值」的适配。
4
4
  *
5
5
  * 通道形状(裁决):真相/账本/判据全在宿主那边,Node 半用宿主**自己的 web server** 开一条
6
- * 插件专属前缀路由把五个段名(`list` / `ping` / `detail` / `apply` / `empty`)搬出来
6
+ * 插件专属前缀路由把六个段名(`list` / `ping` / `orphans` / `detail` / `apply` / `empty`)搬出来
7
7
  * (`src/api/dispatch.ts`),浏览器半同源调用。**不是重造后端**:
8
8
  * 路由层只做搬运与信封。为什么不用官方的 `remote.<namespace>`:那条路要**生成的 Typert 远程描述符**
9
9
  * 加一次 `$mount`,而盒内装配清单写死在 `dsh-api-remotes` 里、全宿主没有动态发现,第三方只能自挂自,
@@ -26,70 +26,30 @@
26
26
  * 所以到了组件面前只剩「裸值 / throw」两支,调用方只写 `if (!outcome.ok)`,正常流里没有 try/catch;
27
27
  * 非本信封形状的异常(网络层、TypeError…)也不丢原文(见 `failureOf`)。
28
28
  *
29
- * 面(`list` / `ping` / `detail` / `apply` / `empty`)与 Node 半路由的五个段名一一对应(段名本身的家是
30
- * `src/shared/wire.ts`);`list` 回一行行的条目,`ping` 只回一个探针值,`detail` 回单条正文,
29
+ * 面(`list` / `ping` / `orphans` / `detail` / `apply` / `empty`)与 Node 半路由的六个段名一一对应(段名本身的家是
30
+ * `src/shared/wire.ts`);`list` 回一行行的条目,`ping` 只回一个探针值,`orphans` 回一份**纯读**的
31
+ * 残留扫描报告,`detail` 回单条正文,
31
32
  * `apply` 回**一组逐条结果**,`empty` 回聚合回执。2026-10-17 之前是六个动作段(一条一段),
32
33
  * 批量只能由调用方循环拼——那正是「恢复 100 条很慢」的形状来源,收成一段之后这条通道不再有
33
- * "第二条执行路径"。
34
+ * "第二条执行路径"。**清理无主子会话也不另开一段**:它是 `apply` 的第五个动作,`orphans` 只负责
35
+ * "有多少、在哪、多大"。
34
36
  */
35
- import { type ApplyAction, type ApplyOutcome, type RemoteFailure, type RemoteResult } from '../shared/wire.js';
36
- /** 回收站条目(镜像 `src/trash/entry.ts` 的 `TrashEntry`;只读面)。 */
37
- export interface TrashEntryLike {
38
- id: string;
39
- sessionId: string;
40
- projectDir: string;
41
- workspaceId: string;
42
- wasArchived: boolean;
43
- deletedAt: number;
44
- title: string;
45
- sizeBytes: number;
46
- /** 组视图的只读数,**都由 `list` 现算现给、不住在清册里**(清册只记 `parentEntryId` 那件事实)。
47
- * 缺席 = 不知道或没有 ⇒ 界面上半句都不出现,也不许拿 0 冒充"没有"。
48
- * `subagentCount`:跟着这条父会话**一起进了回收站**的孩子数(清册里指向它的孩子行)。
49
- * `subagentBytes`:那些孩子的字节合计——孩子不单独列行,头部合计只加父那一格就会少报。
50
- * `strandedSubagents`:**还留在盘上没被带走**的孩子数(来源是宿主 `sessionQuery`),
51
- * 级联落地之前删掉的那些存量才非零。与 `subagentCount` 各说各的,不许互相冒充。
52
- * `workspaceMissing`:删除时记下的工作区已被删掉(宿主的「删除工作区」只删注册、保留日志)——
53
- * 恢复照常回盘,但会落在「未分组」。行上先说一句,别等它从清单里消失才发现换了地方。 */
54
- subagentCount?: number;
55
- subagentBytes?: number;
56
- strandedSubagents?: number;
57
- workspaceMissing?: boolean;
58
- }
59
- /** 彻底删除的回执(镜像 `PurgeResult`)。 */
60
- export interface PurgeResultLike {
61
- removed: number;
62
- freedBytes: number;
63
- }
64
- /** 详情正文里的一条消息(镜像 `TrashDetailMessage`;只读面)。 */
65
- export interface TrashDetailMessageLike {
66
- role: 'user' | 'assistant';
67
- time: number;
68
- text: string;
69
- truncated: boolean;
70
- }
37
+ import { type ApplyAction, type ApplyOutcome, type OrphanReportWire, type PurgeStatsWire, type RemoteFailure, type RemoteResult, type TrashDetailWire, type TrashRowWire } from '../shared/wire.js';
38
+ /**
39
+ * 回收站条目(`/list` 那一行)。**形状的家在 `shared/wire.ts`**(ADR-0003):这里只是旧名的别名,
40
+ * 所以浏览器半不再自己抄一遍字段;下面几处"只读面"的镜像同理。
41
+ */
42
+ export type TrashEntryLike = TrashRowWire;
43
+ /** 彻底删除的回执(`PurgeStatsWire`)。 */
44
+ export type PurgeResultLike = PurgeStatsWire;
71
45
  /**
72
- * 回收站单条详情(镜像 `src/trash/detail.ts` 的 `TrashDetail`;只读面)。
46
+ * 回收站单条详情(`TrashDetailWire`)。
73
47
  *
74
48
  * **线上只有"日志里读出来的那几件"**:`deletedAt` / `sizeBytes` / `wasArchived` / 清册 `title` 这些
75
49
  * 清册已有的字段一律不过线——组件按 `entryId` 与自己手上那份清单配对,这样"清单里的大小"和
76
50
  * "详情里的大小"永远是同一个数,不会因为两次读数不同而各说各话。
77
51
  */
78
- export interface TrashDetailLike {
79
- entryId: string;
80
- cwd: string;
81
- createdAt: number;
82
- lastActivity: number;
83
- logTitle: string;
84
- models: string[];
85
- turnCount: number;
86
- userMessageCount: number;
87
- assistantMessageCount: number;
88
- toolCallCount: number;
89
- injectedCount: number;
90
- messages: TrashDetailMessageLike[];
91
- omittedByPayloadCap: number;
92
- }
52
+ export type TrashDetailLike = TrashDetailWire;
93
53
  /** 任意异常 → 失败面。非带标记的失败(网络层、TypeError…)也保留原文,绝不吞;带标记的失败把
94
54
  * `details` 第三件一起带出。 */
95
55
  export declare function failureOf(reason: unknown): RemoteFailure;
@@ -111,15 +71,19 @@ export type RemoteOutcome<T> = {
111
71
  * code,其余按原文兜底)。
112
72
  */
113
73
  export declare function callRemote<T>(run: () => Promise<T>): Promise<RemoteOutcome<T>>;
114
- /** 本插件前缀路由那套**响应信封**的窄镜像(不是宿主服务 `ctx.remote.*`——那条 typert 通道见 §10):
74
+ /** 本插件前缀路由那套**响应信封**的窄镜像(不是宿主服务 `ctx.remote.*`——那条 typert 通道为什么用不了,见 `AGENTS.md` 的 `remote.<namespace>` 那条):
115
75
  * 每个方法 resolve 成 `RemoteResult<T>`。
116
76
  *
117
77
  * 2026-10-17 批量重构:写动词收成**一个 `apply`**(一段管所有动作、一组 id 进、逐条结果出),
118
- * 再加一段**纯探测** `ping` ⇒ 面是五段:`list` / `ping` / `detail` / `apply` / `empty`(原先六个动作段)。 */
78
+ * 再加一段**纯探测** `ping`;2026-10-19 又加一段**纯读扫描** `orphans` ⇒ 面是六段:
79
+ * `list` / `ping` / `orphans` / `detail` / `apply` / `empty`(原先六个动作段)。 */
119
80
  export interface RawSessionDeleteRemote {
120
81
  list(): Promise<RemoteResult<TrashEntryLike[]>>;
121
82
  /** 通道的"在不在":一次零成本的 GET(探测专用,见 `src/api/dispatch.ts` 的 `ping` 支)。 */
122
83
  ping(): Promise<RemoteResult<string>>;
84
+ /** 纯读:回收站之外的无主子会话扫描。它**不清理**——清理是 `apply` 的 `orphan-clean` 动作,
85
+ * 这一段只交回"有多少、在哪、多大"(读不到语料时报 `io`,调用方不许把它折成"零条残留")。 */
86
+ orphans(): Promise<RemoteResult<OrphanReportWire>>;
123
87
  detail(entryId: string): Promise<RemoteResult<TrashDetailLike>>;
124
88
  /** 一组 id 上的同一个动作。顶层信封的 `ok:false` 只表示**整次调用**没办成(路由自检、栅栏之外);
125
89
  * 单条动作的成败住在 `ApplyOutcome` 里——这是批量接口的判据,别在信封层找它。 */
@@ -138,44 +102,66 @@ export declare function createHttpRemote(prefix?: string): RawSessionDeleteRemot
138
102
  *
139
103
  * 判据是**「拿到信封」而不是「操作成功」**:`{ok:false, error}` 也是一次成功的往返(路由在、依赖接上了、
140
104
  * 错误映射也生效了),那时该做的是把入口显示出来、让原文上屏;只有往返本身失败才算通道缺席 ⇒
141
- * 一个槽位贡献都不注册(设计稿 §7「未接通态」)。
105
+ * 一个槽位贡献都不注册(docs/architecture.md §3「通道探测决定入口出现与否」)。
142
106
  *
143
- * 2026-10-17 第四批:探测从"读一遍清单"改成这一问——**每次页面加载都跑全表对账**是白付的钱,
107
+ * 2026-10-17:探测从"读一遍清单"改成这一问——**每次页面加载都跑全表对账**是白付的钱,
144
108
  * 而"信封到不到"这件事一次零成本的 GET 就答得出来。
145
109
  */
146
110
  export declare function probeChannel(raw: RawSessionDeleteRemote): Promise<boolean>;
147
- /** `shared/wire.ts` 的 `ApplyOutcome` 从这里转出去——浏览器半只认 `remote.js` 这一扇门。 */
148
- export type { ApplyAction, ApplyOutcome } from '../shared/wire.js';
111
+ /** `shared/wire.ts` 的 `ApplyOutcome` 与那份扫描报告从这里转出去——浏览器半只认 `remote.js` 这一扇门。 */
112
+ export type { ApplyAction, ApplyOutcome, OrphanReportWire } from '../shared/wire.js';
149
113
  /** 一段管所有写动作的返回值(逐条)。`ApplySuccess<A>` = 某动作成功支(`value` 按动作收窄)。 */
150
114
  export type ApplySuccess<A extends ApplyAction> = Extract<ApplyOutcome, {
151
115
  action: A;
152
116
  ok: true;
153
117
  }>;
118
+ /**
119
+ * N=1 的成功支:`delete` / `restore` 的幂等 `null` 已被 `applyOne` 判成通道错,
120
+ * 所以它的 `value` 必然非空;其余动作就是 `ApplySuccess`。
121
+ */
122
+ export type OneShotSuccess<A extends ApplyAction> = A extends 'delete' | 'restore' ? Omit<ApplySuccess<A>, 'value'> & {
123
+ readonly value: NonNullable<ApplySuccess<A>['value']>;
124
+ } : ApplySuccess<A>;
125
+ /**
126
+ * 注入面(`deps`)的 interface:**与六个段名一一对应的五个方法**(`ping` 只给探测用,留在 raw 层)。
127
+ *
128
+ * **不再有 N=1 的便捷方法**:`delete` / `restore` / `countSubagents` 曾是同一段
129
+ * `/apply` 的三种写法,于是一个 `module` 里同一个动作有两条路(回收站面板的 deps 同时躺着
130
+ * `applyOutcomes` 与 `restore`)。现在它们收成一个自由函数 {@link applyOne}——要旧形状就显式调它,
131
+ * 而不是让 interface 多背三个方法。
132
+ */
154
133
  export interface SessionDeleteRemote {
155
134
  list(): Promise<TrashEntryLike[]>;
135
+ /** 回收站之外的无主子会话报告(纯读)。读不到就是 throw —— 界面上那是**一次失败的检查**,
136
+ * 不是"零条残留"(把 `io` 折成空报告等于把"不知道"说成"没有")。 */
137
+ scanOrphans(): Promise<OrphanReportWire>;
156
138
  detail(entryId: string): Promise<TrashDetailLike>;
157
139
  /** 一段管所有写动作(2026-10-17 批量重构):一次调用把整批办完,逐条结果原样交回组件。 */
158
140
  applyOutcomes(action: ApplyAction, ids: readonly string[]): Promise<ApplyOutcome[]>;
159
141
  /** 清空回收站(聚合回执)。 */
160
142
  emptyTrash(): Promise<PurgeResultLike>;
161
- /** 单条便捷包装 = `applyOutcomes` 的 N=1 支。`null` 是批量里"已被同批前一条带走"的幂等值;
162
- * 单条删除(N=1)永远不会命中它。 */
163
- delete(sessionId: string): Promise<TrashEntryLike | null>;
164
- restore(entryId: string): Promise<TrashEntryLike | null>;
165
- /** 这条会话在盘上有几个后代(删除确认框那句「含 N 个子会话」)。拿不到 ⇒ 0,那一栏整句不出现。 */
166
- countSubagents(sessionId: string): Promise<number>;
167
143
  }
144
+ /** 需要"单条便捷支"的组件只要这一段 deps(同一段 `/apply` 的 N=1 写法,见 {@link applyOne})。 */
145
+ export type OneShotDeps = Pick<SessionDeleteRemote, 'applyOutcomes'>;
146
+ /**
147
+ * **N=1 的 `apply`**:一个动作 + 一个 id → 那一条的成功支(`value` 按动作收窄)。
148
+ *
149
+ * 三条判据都在这一处:
150
+ * - **按发出去的那个 id** 取结果(取"第一条"会把别条的成败安在这条头上);
151
+ * - 一条没回是**通道的错**,不是"没做成"(后者住在 outcome 的 `ok:false` 里);
152
+ * - `null` 是批量里"已被同批前一条带走"的幂等值——N=1 永远不会命中它,回 `null` 只能是通道答错了,
153
+ * 所以同样按通道错抛,不冒充成功。
154
+ *
155
+ * 成/败/幂等/缺失的判读只走 {@link readReceipt}(一个家)。
156
+ */
157
+ export declare function applyOne<A extends ApplyAction>(deps: OneShotDeps, action: A, id: string): Promise<OneShotSuccess<A>>;
168
158
  /**
169
- * 注入面的**唯一一处适配**:`RemoteResult` → 裸值,外加「一段批量」→「单条便捷面」的展开。
159
+ * 注入面的**唯一一处适配**:`RemoteResult` → 裸值。
170
160
  *
171
161
  * **为什么必须有这一步**:通道那头的调用 resolve 成 `RemoteResult<T>`(`{ ok: true, value }` /
172
162
  * `{ ok: false, error }`),失败是**返回**而不是 reject。组件面要的是「拿到值 / 抛异常」,
173
- * 所以在这里展开一次:`ok ? value : throw`。放在这一处而不是组件里,是为了让三个组件的 props 契约
174
- * (以及钉住它们的用例)保持裸值形状不变。
175
- *
176
- * **单条支里只有一条落地**:没有它(幽灵 id、或路由整次没办成)就 throw——"批量只回了一条别的"
177
- * 不是成功,也不是 0。
163
+ * 所以在这里展开一次:`ok ? value : throw`。
178
164
  */
179
165
  export declare function buildDeps(raw: RawSessionDeleteRemote): SessionDeleteRemote;
180
- /** 「删除」确认框的宿主需要的两件:提交删除的那条便捷包装,与删除前那一次计数(那句「含 N 个子会话」)。 */
181
- export type DeleteConfirmDeps = Pick<SessionDeleteRemote, 'delete' | 'countSubagents'>;
166
+ /** 「删除」确认框的宿主只需要那一段 `/apply`:提交删除与删除前那一次计数都走 {@link applyOne}。 */
167
+ export type DeleteConfirmDeps = OneShotDeps;
@@ -0,0 +1,17 @@
1
+ export interface ClientStore<T> {
2
+ get(): T;
3
+ set(next: T): void;
4
+ /** 回调回同一引用 ⇒ `false`(不发通告);新值 ⇒ `true`。 */
5
+ update(next: (current: T) => T): boolean;
6
+ /**
7
+ * 以本次递增的 seq(从 1 起)造新值并通告,值没变也通告。
8
+ * **`make` 回的值必须把 seq 带上**(放进快照字段,如 `{ seq, items }`):订阅方拿它当 React key
9
+ * 才会在"同一段文案连续出现两次"时重挂载;丢了 seq,重挂载语义会静默消失。
10
+ */
11
+ publish(make: (seq: number) => T): void;
12
+ subscribe(listener: () => void): () => void;
13
+ use(): T;
14
+ /** 测试专用:回初值并清零 seq。 */
15
+ reset(): void;
16
+ }
17
+ export declare function createStore<T>(initial: T): ClientStore<T>;
@@ -0,0 +1,39 @@
1
+ import { type Translate } from './copy.js';
2
+ import type { OrphanReportWire, TrashEntryLike } from './remote.js';
3
+ /** 待确认的动作:一条记录、整个回收站、或多选挑出来的一批(`ids` 已按清单顺序排好)。
4
+ * `purge` 与 `restore` 的确认框共用这一个壳——两者都是"点之前先说清这一批有哪些、要去哪"。 */
5
+ export type PendingAction = {
6
+ kind: 'entry';
7
+ entry: TrashEntryLike;
8
+ } | {
9
+ kind: 'all';
10
+ } | {
11
+ kind: 'entries';
12
+ ids: readonly string[];
13
+ } | {
14
+ kind: 'restore';
15
+ ids: readonly string[];
16
+ };
17
+ /**
18
+ * 两种批量动作共用这一个确认框壳:`purge`(不可恢复)与 `restore`(加法,但一批条目多、去向是
19
+ * 各自的旧项目目录,点之前也要过一道)。标题、正文、确认键的措辞各处一套;批量恢复把选中条目
20
+ * 列成卡片(标题 + 项目名——恢复的去向是**每条各自的**旧项目目录,逐条列出来是这句话的正文);
21
+ * 批量彻底删除只给条数——那批的"去哪"没有可说的,而"不可恢复"那句警告才是要读的。
22
+ */
23
+ export declare function TrashConfirm({ pending, restoreEntries, busy, t, onCancel, onConfirmPurge, onConfirmRestore, }: {
24
+ pending: PendingAction;
25
+ restoreEntries: readonly TrashEntryLike[];
26
+ busy: boolean;
27
+ t: Translate;
28
+ onCancel: () => void;
29
+ onConfirmPurge: () => void;
30
+ onConfirmRestore: (ids: readonly string[]) => void;
31
+ }): import("react").JSX.Element;
32
+ /** 无主子会话的清理确认框(2026-10-19)。载荷是刚扫出来的那份报告,界面不重算。 */
33
+ export declare function OrphanConfirm({ report, busy, t, onCancel, onConfirm, }: {
34
+ report: OrphanReportWire;
35
+ busy: boolean;
36
+ t: Translate;
37
+ onCancel: () => void;
38
+ onConfirm: () => void;
39
+ }): import("react").JSX.Element;
@@ -1,7 +1,9 @@
1
1
  import { type Translate } from './copy.js';
2
2
  import { type SessionDeleteRemote } from './remote.js';
3
- /** 批量重构(2026-10-17)后这份依赖面:两个读 + 一个批量口 + 清空 + 单条恢复的便捷包装。 */
4
- export type TrashPanelDeps = Pick<SessionDeleteRemote, 'list' | 'detail' | 'applyOutcomes' | 'emptyTrash' | 'restore'>;
3
+ /** 面板的依赖面:三个读(`list` / `detail` / `scanOrphans`)+ **一段 `apply`** + 清空。
4
+ * **没有第二条路**:「恢复一条」也走 `apply`(N=1),面板不再同时吃
5
+ * `applyOutcomes` 与一个便捷的 `restore`。 `scanOrphans` 是回收站之外那批无主子会话的纯读报告。 */
6
+ export type TrashPanelDeps = Pick<SessionDeleteRemote, 'list' | 'detail' | 'applyOutcomes' | 'emptyTrash' | 'scanOrphans'>;
5
7
  export interface TrashPanelProps {
6
8
  deps: TrashPanelDeps;
7
9
  /** 文案(缺省 = 内置 zh,见 `copy.ts`:这不是"忘了传",而是"这次没有 locale 服务")。 */
@@ -2,3 +2,5 @@
2
2
  export declare function invalidateTrash(): void;
3
3
  /** 供面板订阅。 */
4
4
  export declare function useTrashRevision(): number;
5
+ /** 测试专用:修订号归零(订阅者随组件卸载自行退订)。 */
6
+ export declare function resetTrashStore(): void;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * 回收站界面共用的**视图词汇**:卡片配方、危险红、项目名的念法。
3
+ *
4
+ * 它从 `trash-panel.tsx` 拆出来,因为清单面板与两个确认框(`trash-confirm.tsx`)都要用这几样——
5
+ * 拆之前它们同住一个文件、只能靠"同一个文件里的常量"共享。拆之后两处读的是同一份,不是两份抄写。
6
+ *
7
+ * **配色与圆角一律走 token,字号是字面量 px**(宿主同类面也这么写):卡片用宿主设置面的底层 token
8
+ * (`bg-layer-2` + `border-l4` + `radius-lg`),选中态换宿主"选中"那一档(见 `trash-panel.tsx` 头注)。
9
+ */
10
+ import type { CSSProperties } from 'react';
11
+ import type { Translate } from './copy.js';
12
+ import type { TrashEntryLike } from './remote.js';
13
+ /** 卡片配方:宿主设置面的 fill/stroke 底层 token + 官方 `lg` 圆角。 */
14
+ export declare const CARD: CSSProperties;
15
+ /** 选中行:底色与描边都换成宿主"选中"那一档(`trash-panel.tsx` 头注里有这两枚 token 的实测取值)。 */
16
+ export declare const CARD_SELECTED: CSSProperties;
17
+ /**
18
+ * **整页唯一那块实心红**的写法:`variant="primary"` + 在元素上覆盖 primary 那对填充 token。
19
+ * 宿主插件管理页的 danger 按钮就是这个写法(asar @21658274),所以 hover 仍走 `.primary:hover` 类。
20
+ */
21
+ export declare const BULK_DANGER: CSSProperties;
22
+ /**
23
+ * 项目名怎么念:宿主的 `projectKey` 把 cwd 里的 `/`、`\`、`:` 全折成**一个** `-` ⇒ 分隔符那部分有损、
24
+ * 拆不回真实路径;但**非分隔符那部分可逆**(`~XXXX` 十六进制码元,字面 `~` 编成 `~007E`)。
25
+ * 摘掉 `--…--` 裹边、还原这些转义,剩下的原样摆出来,完整串交给组头 `title`。
26
+ * 字母表只有一份:还原走 `shared/wire.ts`。
27
+ */
28
+ export declare function projectLabel(projectDir: string, t: Translate): string;
29
+ /** 清单上的一个项目组(组头 + 组内的条目,组内按删除时间倒序)。 */
30
+ export interface ProjectGroup {
31
+ projectDir: string;
32
+ label: string;
33
+ /** 组内最新一条的删除时间——组间排序的键。 */
34
+ latest: number;
35
+ entries: readonly TrashEntryLike[];
36
+ }
37
+ /** 按项目分组:组内按删除时间倒序,组间按各组最新一条倒序(刚删掉的那个项目排在最上面)。 */
38
+ export declare function groupByProject(entries: readonly TrashEntryLike[], t: Translate): ProjectGroup[];
@@ -3,8 +3,8 @@
3
3
  * 随包发布的明文产物 `lib/index.js`;出处与改写规则见 `ui.css` 头注)。
4
4
  *
5
5
  * 只留本插件用到的:垃圾桶(删除动作 / 回收站)、警示(失败面)、打开的项目文件夹、
6
- * 关闭叉(对话框)、圆圈对勾(撤销提示的 success 档)、对勾(行尾兜底勾选开关,2026-10-17
7
- * 第十三批加;行首主勾选走原生 Checkbox)、下折与右折 chevron(清单的分组折叠头,
6
+ * 关闭叉(对话框)、圆圈对勾(撤销提示的 success 档)、对勾(行尾兜底勾选开关,2026-10-17 加;
7
+ * 行首主勾选走原生 Checkbox)、下折与右折 chevron(清单的分组折叠头,
8
8
  * 2026-10-15 加:`DisclosureRow` 那套折叠行的两态 glyph)。「刷新」那枚随按钮一起删了
9
9
  * (2026-10-16,见 `trash-panel.tsx` 工具排的注释)——本文件不留无人消费的几何。
10
10
  * **几何、`viewBox`、`strokeWidth` 逐字不动**——它们是官方图标集的形状,改了就不再与宿主同屏同形。
@@ -25,7 +25,7 @@ export declare function IconFolderOpenOutlineRegular({ size }: IconProps): React
25
25
  export declare function IconCloseOutlineRegular({ size }: IconProps): ReactElement;
26
26
  /** 撤销提示 success 档的前导 glyph(官方 `Toast` 在 `tone="success"` 时自带的那一枚)。 */
27
27
  export declare function IconCheckCircleOutlineRegular({ size }: IconProps): ReactElement;
28
- /** 行尾兜底「勾选开关」那枚对勾(状态点/归档行;官方 `IconCheckOutlineRegular`,`viewBox`/`d` 逐字;2026-10-17 第十三批加)。 */
28
+ /** 行尾兜底「勾选开关」那枚对勾(状态点/归档行;官方 `IconCheckOutlineRegular`,`viewBox`/`d` 逐字;2026-10-17 加)。 */
29
29
  export declare function IconCheckOutlineRegular({ size }: IconProps): ReactElement;
30
30
  /** 分组头「展开中」那一枚(官方 `IconChevronDownOutlineRegular`,`viewBox`/`d` 逐字)。 */
31
31
  export declare function IconChevronDownOutlineRegular({ size }: IconProps): ReactElement;
@@ -1,8 +1,22 @@
1
+ /**
2
+ * 撤销提示(docs/ui-spec.md 状态 ③):删除成功后一条可撤销的横幅。
3
+ *
4
+ * 为什么是**独立组件 + 模块级通知台**,而不是死在菜单行里:宿主的菜单行随菜单**一起卸载**
5
+ * (真产物 Menu 的注释原文 "the rows unmount with the list"),而提示要在菜单关掉之后还活着。
6
+ * 宿主的官方配方同形——会话行的动作把请求写进一个模块级 store,`shell.overlay` 里的
7
+ * `workspace.row-toast` 读它(asar 原文:`inject("shell.overlay", … id: "workspace.row-toast" …
8
+ * inject: rowToastInjected … }, RowActionToast)`,而
9
+ * `rowToastInjected = () => ({ hooks: { toast: rowToast }, dismissToast, undoArchive: unarchiveSession })`)。
10
+ * 本文件就是那份 store + 那个常驻组件。
11
+ *
12
+ * 状态源走 `store.ts` 的 `createStore`(内部用 React 自带的 `useSyncExternalStore`)——
13
+ * 不引 `@deepseek-ai/dsh-client-store`(它在平台表内,但这里用不着;React 就够,少一条平台依赖)。
14
+ */
1
15
  import { type Translate } from './copy.js';
2
- import { type SessionDeleteRemote } from './remote.js';
3
- export type UndoToastDeps = Pick<SessionDeleteRemote, 'restore' | 'applyOutcomes'>;
16
+ import { type OneShotDeps } from './remote.js';
17
+ export type UndoToastDeps = OneShotDeps;
4
18
  /**
5
- * 一次待撤销的事。两形(第十三批②起):单条删除(主语是会话名)/批量删除(主语是条数)。
19
+ * 一次待撤销的事。两形(2026-10-17 起):单条删除(主语是会话名)/批量删除(主语是条数)。
6
20
  * 同一个 Toast 宿主、同一停留时长;撤销动作按形走——单条 `restore`,批量**一次** `applyOutcomes`。
7
21
  */
8
22
  export type UndoNotice = {
@@ -28,7 +42,7 @@ export declare function publishUndoNotice(input: {
28
42
  entryId: string;
29
43
  title: string;
30
44
  }): void;
31
- /** 批量删除成功后发布一条撤销提示(第十三批②):覆盖上一条,只报这一次移走的条数。 */
45
+ /** 批量删除成功后发布一条撤销提示(2026-10-17):覆盖上一条,只报这一次移走的条数。 */
32
46
  export declare function publishUndoBatchNotice(input: {
33
47
  entryIds: readonly string[];
34
48
  n: number;
@@ -37,7 +51,9 @@ export declare function publishUndoBatchNotice(input: {
37
51
  export declare function clearUndoNotice(): void;
38
52
  /** 供组件订阅。 */
39
53
  export declare function useUndoNotice(): UndoNotice | null;
40
- /** 提示停留时长。设计稿 §7 定死:**6 秒**(Toast 自己的默认是 3 秒)。 */
54
+ /** 测试专用:现场与 seq 归零,用例之间互不串味。 */
55
+ export declare function resetUndoNotice(): void;
56
+ /** 提示停留时长。**6 秒** 定死在本行(比 Toast 默认的 3 秒长)——`docs/ui-spec.md` §2 状态 ③ 只指到这里,不抄这个数。 */
41
57
  export declare const UNDO_HOLD_MS = 6000;
42
58
  /**
43
59
  * 常驻的撤销提示宿主。挂 `shell.overlay`(框架级浮层;槽位说明原文 "a toast stack or a status
@@ -1,11 +1,11 @@
1
1
  /**
2
- * 清单列与详情列之间那根**可拖的宽度把手**(2026-10-15 加,设计稿 §7 状态④)。
2
+ * 清单列与详情列之间那根**可拖的宽度把手**(2026-10-15 加,`docs/ui-spec.md` 的「面板与清单」)。
3
3
  *
4
4
  * 三件事住在这里,因为它们互相咬合、分开测各自都不成立:
5
5
  * ① **宽度夹在两个界之间**。下限由调用方给(回收站那条的来历写在 `trash-panel.tsx`:真 Chromium
6
6
  * 上扫出来的,约束不是"两颗按钮放得下"——它们 `flex:none`,再窄也是文字列缩;而是「删除于 … ·
7
7
  * 大小」那一行不再被省略号截断),上限 = 面板宽 − `reserve`。**`reserve` 的数值与来历都住在调用方**
8
- * (`DETAIL_RESERVE`,那三件东西怎么加出来的见设计稿 2026-10-15 §7.1 与现状稿 §8 的几何读数):
8
+ * (`DETAIL_RESERVE`,那三件东西怎么加出来的见 `trash-panel.tsx` 该常量的注释与几何用例):
9
9
  * 这一层不复制那份算式——上一版在这里钉过一个"480 ⇒ 900 那档正好回 420",调用方改到 489 之后
10
10
  * 它就成了假数,而 900 那档的真实读数是**清单被夹到 411**(比默认窄 9px,还能往里拖)。
11
11
  * ② **夹在"读"的时候也做一次**。窗口变窄不触发任何写入,若只在写入时夹,之前拖出来的超宽
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 观测**面板自己那一块**的宽度(回收站的断点全靠它,设计稿 2026-10-15 §2.1)。
2
+ * 观测**面板自己那一块**的宽度(回收站的断点全靠它,docs/ui-spec.md)。
3
3
  *
4
4
  * 为什么不是 `window.innerWidth`:这块住在宿主的中央列里,**侧栏折叠/展开、拖动分栏**都会改它的宽度
5
5
  * 而 window 一点不动——只听 window 的 resize 会在侧栏折叠后拿着旧宽度画错形态。
@@ -0,0 +1,55 @@
1
+ import { type SessionDeleteRemote, type TrashEntryLike } from './remote.js';
2
+ import type { PendingAction } from './trash-confirm.js';
3
+ import type { TrashDetailState } from './trash-detail.js';
4
+ import type { TrashQuery } from './use-trash-query.js';
5
+ /** 面板的一枚全局闸:写动作在飞时 `busy` 为真,`around` 把一次任务整个包在"这一趟还算忙"里。 */
6
+ export interface TrashPanelGate {
7
+ readonly busy: boolean;
8
+ around<T>(task: () => Promise<T>): Promise<T>;
9
+ }
10
+ /**
11
+ * 闸本体(从 `trash-panel.tsx` 的函数体原样收进来):任务落地或抛错都把 `busy` 放回 false。
12
+ * 不做引用计数:面板同一时刻只允许一趟写动作(按钮在 `busy` 期间全被按住),多一层计数只会
13
+ * 把"这一趟什么时候算完"变成第二个判据。
14
+ *
15
+ * 返回值经 `useMemo` 稳定:`gate` 进 `restore` / `confirmApply` 的 `useCallback` 依赖,每次渲染
16
+ * 换个新对象会让那两个回调跟着失效,进而把引用它们的 effect 也带着重跑。
17
+ */
18
+ export declare function useTrashPanelGate(): TrashPanelGate;
19
+ /**
20
+ * 控制器要的面。`query` 只取三样:清单(选中从它里面找)、重读(失效通告与写动作之后用)、
21
+ * 清空(面板的写动作那一半)。`ordered` 是键盘/自动选中用的"屏幕上的顺序"(只含展开的组),
22
+ * 由面板算好传进来——布局常量不搬进控制器。
23
+ */
24
+ export interface TrashPanelControllerInput {
25
+ deps: Pick<SessionDeleteRemote, 'detail' | 'applyOutcomes'>;
26
+ gate: TrashPanelGate;
27
+ query: Pick<TrashQuery, 'entries' | 'refresh' | 'emptyTrash'>;
28
+ ordered: readonly TrashEntryLike[];
29
+ canAutoSelect: boolean;
30
+ focusRow(entryId: string): void;
31
+ }
32
+ export interface TrashPanelController {
33
+ readonly busy: boolean;
34
+ readonly selected: TrashEntryLike | null;
35
+ readonly selectedId: string | null;
36
+ readonly drilled: boolean;
37
+ readonly detail: TrashDetailState;
38
+ /** 待确认的那一批(确认框的开合);确认落地后由控制器收起。 */
39
+ readonly pending: PendingAction | null;
40
+ /** 恢复/彻底删除/批量 的失败原文,按条目 id 存:切走再回来还在。 */
41
+ readonly actionFailure: Readonly<Record<string, string>>;
42
+ select(entry: TrashEntryLike, options?: {
43
+ drill?: boolean;
44
+ }): Promise<void>;
45
+ retry(): void;
46
+ back(): void;
47
+ setPending(pending: PendingAction | null): void;
48
+ /** 单条恢复(`/apply` 的 N=1):成功后丢缓存并重读清单,失败原文落回这一行。 */
49
+ restore(entry: TrashEntryLike): Promise<void>;
50
+ /** 确认框上的「确认」:按 `pending.kind` 分派到空清或 `confirmApply`。 */
51
+ confirmPurge(): Promise<void>;
52
+ /** 一组 id 上的同一个动作:逐条成败落成行上原文/缓存失效,整批只重读一次清单。 */
53
+ confirmApply(action: 'purge' | 'restore', ids: readonly string[]): Promise<void>;
54
+ }
55
+ export declare function useTrashPanelController(input: TrashPanelControllerInput): TrashPanelController;
@@ -0,0 +1,32 @@
1
+ import { type OrphanReportWire, type SessionDeleteRemote, type TrashEntryLike } from './remote.js';
2
+ import type { Translate } from './copy.js';
3
+ export type TrashQueryDeps = Pick<SessionDeleteRemote, 'list' | 'scanOrphans' | 'applyOutcomes' | 'emptyTrash'>;
4
+ export interface TrashQuery {
5
+ /** `null` = 还没读到(第一次读或读失败);与"空的"是两件事。 */
6
+ readonly entries: readonly TrashEntryLike[] | null;
7
+ readonly listFailure: string | null;
8
+ /** `null` = 还没扫到;失败时也置回 `null`(不拿空报告冒充干净)。 */
9
+ readonly orphanReport: OrphanReportWire | null;
10
+ readonly orphanFailure: string | null;
11
+ /** 「这一组已经不在」不是失败:单独一条中性提示。 */
12
+ readonly orphanNotice: string | null;
13
+ readonly orphanConfirm: boolean;
14
+ setOrphanConfirm(open: boolean): void;
15
+ /** 「查看并清理」:把上一条失败/中性提示清掉再开预览框(旧提示不该跟新一次预览并排出现)。 */
16
+ openOrphanPreview(): void;
17
+ /** 交回**这一次读数是否真的落地**:孤儿扫描吃的是对账后的清册,清单没读成就不能扫。 */
18
+ reload(): Promise<boolean>;
19
+ scanOrphans(): Promise<void>;
20
+ /** "重读一遍" = 先对账并读清单,再扫残留;清单没读成就不扫。 */
21
+ refresh(): Promise<void>;
22
+ /** 清理那批无主子会话(一段 `/apply` 的第五个动作),逐条结果落成"已不在了 / N 组未清理"。 */
23
+ cleanOrphans(): Promise<void>;
24
+ /** 清空回收站(聚合回执):成功就重读,失败就把原文摆到清单那块。交回是否成功。 */
25
+ emptyTrash(): Promise<boolean>;
26
+ }
27
+ export declare function useTrashQuery({ deps, t, around, }: {
28
+ deps: TrashQueryDeps;
29
+ t: Translate;
30
+ /** 把"这一趟还算忙"的一枚全局闸套在一次动作外(面板的 `busy`)。 */
31
+ around: <T>(task: () => Promise<T>) => Promise<T>;
32
+ }): TrashQuery;
@@ -0,0 +1,32 @@
1
+ import type { TrashEntryLike } from './remote.js';
2
+ import type { ProjectGroup } from './trash-view.js';
3
+ export interface TrashSelection {
4
+ /** 多选模式:入口在工具排(「多选」/「退出多选」),模式内点行说的是"勾/取消"。 */
5
+ readonly selectMode: boolean;
6
+ readonly checked: ReadonlySet<string>;
7
+ /** 按**清单从上往下**排好的勾选 id(页脚两颗与批量执行都读这一份)。 */
8
+ readonly checkedIds: readonly string[];
9
+ /** 键盘 ↑↓ 与下钻走这个顺序:**只含展开的组**(折叠的行不在 DOM 里,方向键别停过去)。 */
10
+ readonly ordered: readonly TrashEntryLike[];
11
+ readonly allCollapsed: boolean;
12
+ isCollapsed(projectDir: string): boolean;
13
+ enterSelect(): void;
14
+ exitSelect(): void;
15
+ toggleGroup(projectDir: string): void;
16
+ toggleAllGroups(): void;
17
+ toggleCheck(entryId: string): void;
18
+ /** 组头那颗:说的是"这个项目的全部会话",折叠起来的条目也算。勾满再点 = 整组取消。 */
19
+ toggleGroupCheck(group: {
20
+ entries: readonly TrashEntryLike[];
21
+ }): void;
22
+ groupFullyChecked(group: {
23
+ entries: readonly TrashEntryLike[];
24
+ }): boolean;
25
+ groupPartiallyChecked(group: {
26
+ entries: readonly TrashEntryLike[];
27
+ }): boolean;
28
+ }
29
+ export declare function useTrashSelection({ entries, groups, }: {
30
+ entries: readonly TrashEntryLike[] | null;
31
+ groups: readonly ProjectGroup[];
32
+ }): TrashSelection;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * 回收站面板之外发生的写入(菜单删除、批量删除、撤销恢复)的**跨槽位收尾顺序**:
3
+ * 主视图收尾 → 失效回收站 → 撤销通告。面板自己的写入,本地 refresh / 缓存的
4
+ * 生命周期仍住在 controller 与 query 里。
5
+ */
6
+ export declare function commitExternalDelete(input: {
7
+ sessionIds: readonly string[];
8
+ publishUndo(): void;
9
+ }): void;
10
+ export declare function commitExternalRestore(sessionIds: readonly string[]): void;
@@ -0,0 +1,14 @@
1
+ import type { FsPort } from './ports/fs-port.js';
2
+ import type { HostPort } from './ports/host-port.js';
3
+ import type { ManifestPort } from './trash/manifest.js';
4
+ /** Node 半的基础依赖面:组合根只装配这一颗;动词之间不互相 import 依赖面 interface。 */
5
+ export interface CoreDeps {
6
+ fs: FsPort;
7
+ host: HostPort;
8
+ manifest: ManifestPort;
9
+ home: string;
10
+ }
11
+ /** 只有删除读时钟;其余读 / 计划 / 恢复 / 永久删除只用 CoreDeps。 */
12
+ export type DeleteDeps = CoreDeps & {
13
+ now(): number;
14
+ };
@@ -1,33 +1,27 @@
1
1
  import type { Context } from '@deepseek-ai/cordis';
2
- import { type ManifestPort } from './trash/manifest.js';
3
2
  export declare const name = "@xrn1997/dsh-session-delete";
4
3
  /**
5
4
  * 插件依赖:**只写字符串数组**(隔离数据根上实测:写成 `{ required, optional }` 会让插件停在
6
5
  * `pending (waiting for services: required, optional)`)。
7
6
  *
8
- * 三项都是在隔离数据根上实测注入成功、且本插件真正用到的宿主服务。三个**故意不写**的:
7
+ * 三项都是在隔离数据根上实测注入成功、且本插件真正用到的宿主服务。**不进 inject 的**:
9
8
  * - `remote`:那是**浏览器半**的服务(asar `dsh-api-gateway/lib/client.js`:`super(ctx, "remote")`),
10
9
  * Node 半挂的是 `ctx.typertGateway`。把它写进 Node 半的 inject 会让插件永远 pending(同 `slots` 那次的形状)。
11
- * - `sessions` / `sessionProjectionCache`:前者本实现不用(活跃判据走 waterfall,见设计稿 §5);后者是**可选**服务,
12
- * 官方消费方也按可选读(`ctx.get('sessionProjectionCache')?.…`),inject 一个可能没挂的服务 = 整个插件 pending。
10
+ * - `sessions` / `sessionProjectionCache`:两者都按 `ctx.get(...)` **可选读**——`sessions` 只给 `evictLive`
11
+ * 用(活跃判据走 waterfall,见 src/ports/host-port.ts;服务缺席回 `unknown`),`sessionProjectionCache` 是标题的尽力读面。
12
+ * inject 一个可能没挂的服务 = 整个插件 pending。
13
13
  * 它取不到时 `titleOf` 返回空串,正是契约允许的降级。
14
+ * - `agents`:只给 handle 捕获用(`ctx.get('agents')`;缺席时 `ctx.inject(['agents'])` 等服务就绪再装)。
15
+ * 它同样可能晚于本插件激活,写进插件 inject 只会让整个插件 pending。
14
16
  *
15
17
  * `webServer` 是**浏览器半取数通道的承载者**(那条薄线):插件用它注册 `/dsh-session-delete-api`
16
- * 前缀路由,浏览器半同源 fetch 那五个段名。它是宿主 web 组成里的硬依赖(`dsh-host-webserver` 的
18
+ * 前缀路由,浏览器半同源 fetch 那六个段名。它是宿主 web 组成里的硬依赖(`dsh-host-webserver` 的
17
19
  * `super(ctx, "webServer")`),服务名与 `register({kind:'prefix', path, handler})` 的形状按
18
20
  * `dsh-host-webserver/lib/index.js` 与 `README.md` 原文读实,不是猜的。
19
- */
20
- export declare const inject: string[];
21
- /**
22
- * 清单的真实绑定:官方 storage domain → `ManifestPort`(`StorageDomainLike` 在动词层保持一种读法)。
23
- * 域**懒开**且成功句柄只开一次(`apply` 是同步的,而 `open()` 是异步的),并挂一条 `ctx.effect` 清理:
24
- * 插件卸载时关掉句柄并把 `close()` 的落地交回框架(句柄归调用方所有,这是 `open()` 的 JSDoc 明写的契约;
25
- * 2026-10-17 起不再 `void` 走掉——没关严的句柄会让下一份同名域撞 `already-open`)。
26
21
  *
27
- * **失败不缓存**:`open()` 被拒是瞬态(域被别的持有者占着、装载竞态),把第一次的 rejected promise
28
- * 缓存起来等于"这个会话永远删不掉"——下一次读会重新走 `open()`,成功才落下活句柄。
22
+ * 清单(storage domain 绑定)与数据根各有一个家:`src/trash/storage-manifest.ts`、`src/ports/home.ts`。
29
23
  */
30
- export declare function createStorageManifest(ctx: Context): ManifestPort;
24
+ export declare const inject: string[];
31
25
  /**
32
26
  * 装配顺序:**清单 → 端口 → 动词依赖 → 前缀路由**(`DeleteDeps` 就是前两者的合一)。
33
27
  *
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 多帧 zstd 流的**帧边界**(设计稿 2026-10-15 §1.1)。
2
+ * 多帧 zstd 流的**帧边界**(2026-10-15 实测)。
3
3
  *
4
4
  * 为什么要有这个文件:会话日志是一次次 append 出来的**帧序列**,而 Node 的 `zstdDecompressSync`
5
5
  * **与** `createZstdDecompress()` 流式**都只给第一帧**(实测:本机一个 3.8 MB 的文件,两个 API 都只回