@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
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `/dsh-session-delete-api` 前缀路由的**内部分发**(薄线的 Node 半)。
2
+ * `/dsh-session-delete-api` 前缀路由的**内部分发**(薄线的 Node 半)——**只管传输**。
3
3
  *
4
4
  * 为什么是这一条路(裁决,不是偏好):浏览器侧 `remote.<namespace>` 只能由**生成的 Typert 远程
5
5
  * 描述符贡献**经 `ctx.remote.$mount(...)` 挂上,而盒内那份装配清单是**写死的 25 条官方包**、
@@ -7,49 +7,29 @@
7
7
  * 只能自挂自(要生成物 + `zod` + 改包布局),代价与收益完全不成比例。改用宿主**自己的 web server**
8
8
  * 开一条插件专属前缀路由:真相/账本/判据全在宿主那边,这一层只搬运。
9
9
  *
10
- * 四条口径:
11
- * ① **不依赖 Cordis**:`createApiHandler(deps)` 是纯构造器,返回一个 `(req, res) => Promise<void>`
12
- * ——所以它能用真 `node:http` server 直接单测(`tests/api.test.ts`),与宿主怎么挂它无关。
13
- * ② **信封就是浏览器半消费的那个形状**:`{ok:true,value}` / `{ok:false,error:{code,message,details?}}`。
14
- * `error.code` 走 `src/remote/map-error.ts` 的**同一张表**(设计稿 §6 的五个码),原文进
15
- * `error.message`,结构化附带物进可选的 `error.details`——浏览器半的 `unwrapRemoteResult` /
16
- * `failureOf` 因此一行都不用改。
17
- * ③ **HTTP 状态不是第二套语义**:**操作失败**时状态一律 200(含 `ok:false`)——我们与浏览器半之间的
18
- * 契约是信封本身,判断成败只看 `ok`(同宿主网关"每次 unary 调用 resolve 成 RemoteResult、
19
- * 不为载体问题 reject"的那条规矩)。**请求本身不对**才用 4xx(400 请求体 / 405 方法 / 404
20
- * 未知路由),同源栅栏拒的是**请求**而不是操作,回 403;浏览器半先解信封再看状态。
21
- * ④ **同源栅栏**:本机回环上一条能删会话的写口不能被任意网页用 simple request CSRF 打——没有栅栏时
22
- * `content-type: text/plain` 的跨源 POST 不触发预检、直接生效。放行的形状见 {@link isTrustedRequest}:
23
- * 同源(浏览器直接打开宿主那行 URL)、桌面壳自己的页面(`dsh-app://app`,真机那条路,见
24
- * {@link isDesktopShellUrl})、或两者皆缺席(curl / 本机工具)。本机既有插件
25
- * (`dsh-novel` 的 `api/wire.ts`)只认前两条里的"同源",本文件多的是桌面壳那一条——**它必须有**:
26
- * 桌面版的请求被壳摘掉了 `Origin`,漏掉它等于真机上四个入口一个都不出现(而直连 http 的过门
27
- * 读数看不出来)。
28
- * ⑤ **`list` 是"对账 + 读",且它与写动作按到达序串行**({@link createApiHandler} 里那条进程内链):
29
- * 对账(`reconcileTrash`)与删除/恢复/彻底删除都是"读清单 + 动文件 + 写清单"的多步,交错跑会互相踩
30
- * ——删除刚把目录搬进回收站、行还没写上时,对账抢先把这行按目录"重建"出来,再被真实行覆盖成降级
31
- * 元数据。串行的代价只是排队(单进程单用户,毫秒级);纯读(`detail`、`ping`、`apply/count`)
32
- * 不占这条链。
33
- * ⑥ **一段 `POST /apply` 管所有写动作**(2026-10-17 批量重构):旧形状是「一条一段、一次一发」,
34
- * 批量只能由调用方循环拼出来——100 条 = 100 次往返 ×(一次全量清册读 + 每条一次宿主会话表)。
35
- * 现在一组 id 一次进、逐条 `ApplyOutcome` 出;单条就是 N=1,仓里不留第二条执行路径。
36
- * 清空是聚合回执(不按条回),所以单独一段 `POST /empty`。
10
+ * 本文件只剩三件事:**栅栏 → 查表 → 信封**。
11
+ * - "这个段名该用什么方法、纯不纯、要不要预读 body、怎么处理"全在 `api/route-table.ts` 那一张表里
12
+ * (ADR-0004);这里一个字都不再重复(此前方法判断与纯度判断分居两个函数、靠注释保持一致)。
13
+ * - 一行里放什么(清册行 → 线上行)归 `api/trash-rows.ts`。
14
+ * - 前面四条口径(不依赖 Cordis / 信封与浏览器半同形 / 状态语义 / 同源栅栏)与到达序链(口径 ⑤)
15
+ * 仍住这里,因为它们是**传输**的事。
16
+ *
17
+ * 信封(口径 ②):`{ok:true,value}` / `{ok:false,error:{code,message,details?}}`。
18
+ * `error.code` 走 `src/remote/map-error.ts` 的**同一张表**(src/shared/wire.ts 的五个码),原文进
19
+ * `error.message`,结构化附带物进可选的 `error.details`。
20
+ *
21
+ * HTTP 状态不是第二套语义(口径 ③):**操作失败**时状态一律 200(含 `ok:false`)——我们与浏览器半之间的
22
+ * 契约是信封本身,判断成败只看 `ok`。**请求本身不对**才用 4xx(400 请求体 / 405 方法 / 404 未知路由),
23
+ * 同源栅栏拒的是**请求**而不是操作,回 403;浏览器半先解信封再看状态。
24
+ *
25
+ * 同源栅栏(口径 ④):本机回环上一条能删会话的写口不能被任意网页用 simple request CSRF 打——没有栅栏时
26
+ * `content-type: text/plain` 的跨源 POST 不触发预检、直接生效。放行的形状见 {@link isTrustedRequest}。
27
+ *
28
+ * 到达序链(口径 ⑤):`list`(含对账)与写动作都是"读清单 + 动文件 + 写清单"的多步,交错跑会互相踩
29
+ * ——删除刚把目录搬进回收站、行还没写上时,对账抢先把这行按目录"重建"出来,再被真实行覆盖成降级
30
+ * 元数据。串行的代价只是排队(单进程单用户,毫秒级);纯读不占这条链(哪几个段名纯、哪个动作纯,
31
+ * 由路由表说,不在这里列)。
37
32
  */
38
33
  import type { IncomingMessage, ServerResponse } from 'node:http';
39
- import type { TrashEntry } from '../trash/entry.js';
40
- import type { DeleteDeps } from '../verbs/delete-session.js';
41
- /** 清单交回浏览器的那一行:清册那一行 + 组视图的只读数。 */
42
- export interface TrashRow extends TrashEntry {
43
- /** 跟着这条父会话一起进回收站的孩子数(来源:清册里 `parentEntryId` 指向它的行)。 */
44
- readonly subagentCount?: number;
45
- /** 那些孩子的字节合计(页脚的合计要算得上它们,不能只加父自己那一格)。 */
46
- readonly subagentBytes?: number;
47
- /** **盘上还留着、没被带走**的孩子数(来源:宿主 `sessionQuery`)。级联落地前删掉的那些存量才非零;
48
- * 与 `subagentCount` 是两件事,界面上必须各说各的(不许用一个冒充另一个)。 */
49
- readonly strandedSubagents?: number;
50
- /** 删除时记下的工作区**已经不在账本里**(宿主的「删除工作区」只删注册、保留日志)。恢复会照常
51
- * 回盘、归入「未分组」——界面上点之前就得知道,别等它从清单里消失才发现换了地方。
52
- * 缺席 = 工作区还在 / 没有工作区可判(与上面三个只读数同一条"不拿缺席冒充否"的规矩)。 */
53
- readonly workspaceMissing?: boolean;
54
- }
34
+ import type { DeleteDeps } from '../deps.js';
55
35
  export declare function createApiHandler(deps: DeleteDeps): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * **路由表**(ADR-0004):段名 → `{ 方法, 纯度, body, 处理 }`。一条记录,两个消费者。
3
+ *
4
+ * 此前"同一条请求该过哪几道判断"的知识散在两个函数里:`dispatch()` 里六个 `if` 各调一次
5
+ * `requireMethod`,`purityOf()` 里另一组 `if` 决定"要不要进到达序链",两处靠**注释**维持一致。
6
+ * 不一致的两种后果都不好在测试里撞见:
7
+ * - 该纯的判成写 ⇒ 一次探测/大正文读/扫描把删除按钮堵在串行链后面;
8
+ * - 该写的判成纯 ⇒ 写动作绕开到达序链,与对账交错,回收站出现"目录在、行是降级元数据"的世界;
9
+ * - body 该读没读 ⇒ 下游拿到 `null`;读了两遍 ⇒ 请求流已被消费(Node 的流只能消费一次)。
10
+ *
11
+ * 为什么这张表**不在** `shared/wire.ts`:处理函数会 import 动词与 `node:fs`,而浏览器半要 import
12
+ * `wire.ts`——值 import 会把 Node 半拖进客户端 bundle,正好撞上 `tsdown` 的纯度门。段名与动作仍住
13
+ * `wire.ts`(两半的共同契约),这张表只回答"怎么处理"。
14
+ */
15
+ import { ROUTES, type ApplyAction } from '../shared/wire.js';
16
+ import type { DeleteDeps } from '../deps.js';
17
+ /** 请求体:`json` 才读下来(GET 一律不读;读两遍会把流消费掉)。 */
18
+ export type BodyKind = 'none' | 'json';
19
+ /** 到达序:`pure` 不进链;`chained` 进链;函数形式给 `/apply` 用(`count` 是纯读)。 */
20
+ export type Purity = 'pure' | 'chained' | ((body: JsonBody) => 'pure' | 'chained');
21
+ type JsonBody = Record<string, unknown> | null;
22
+ export interface RouteSpec {
23
+ readonly method: 'GET' | 'POST';
24
+ readonly body: BodyKind;
25
+ readonly purity: Purity;
26
+ run(deps: DeleteDeps, body: JsonBody): Promise<unknown>;
27
+ }
28
+ export declare const APPLY_ACTIONS: readonly ApplyAction[];
29
+ /**
30
+ * 六个段名各一行。`Record<段名, RouteSpec>` 是闸:`wire.ts` 加了段名而这里没加,编译不过。
31
+ */
32
+ export declare const ROUTE_TABLE: Record<(typeof ROUTES)[keyof typeof ROUTES], RouteSpec>;
33
+ /** 段名 → 那一行;不认识的段名回 `undefined`(调用方抛 404,并带出原路径)。 */
34
+ export declare function routeOf(rest: string): RouteSpec | undefined;
35
+ export {};
@@ -0,0 +1,31 @@
1
+ /**
2
+ * `GET /list` 那一行的装配:清册行(`TrashEntry`)→ 线上那一行(`TrashRowWire`),外加组视图的几个只读数。
3
+ *
4
+ * 它从 `api/dispatch.ts` 里搬出来,因为路由层的事是**传输**(栅栏、方法、纯度、信封),而"一行里放什么"
5
+ * 是这一层的事。「折进谁、谁露面」那条判据读 `trash/groups.ts`(ADR-0001);一组会释放多少、盘上还剩谁
6
+ * 归 `verbs/permanent-removal.ts` 的只读计划 —— 这里只做装配,不再自己枚举盘上后代。
7
+ */
8
+ import type { CoreDeps } from '../deps.js';
9
+ import type { TrashEntry } from '../trash/entry.js';
10
+ import type { TrashRowWire } from '../shared/wire.js';
11
+ /** 组视图那几个只读数的容器(行投影的第二个入参)。`releaseBytes` 必填(计划里实测好的那一格);
12
+ * 其余缺席 = 这次没算,不是 0。 */
13
+ type TrashRowReadouts = {
14
+ readonly releaseBytes: number;
15
+ } & Partial<Pick<TrashRowWire, 'subagentCount' | 'subagentBytes' | 'strandedSubagents' | 'strandedBytes' | 'workspaceMissing'>>;
16
+ /**
17
+ * 清册那一行 → 线上那一行。**显式字面量、不用 `...row`**:线上形状多一个必填字段时,这里编译不过,
18
+ * 于是"线上多了一格却没人填"不可能悄悄发生(`...row` 会把它带过去,看上去一切正常)。
19
+ */
20
+ export declare function toWire(row: TrashEntry, readouts: TrashRowReadouts): TrashRowWire;
21
+ /**
22
+ * 把清册的全部行装配成「一行 = 一组」:孩子折进父那一行、不单独列(宿主的客户端本来就藏孩子,
23
+ * 我们若单列就会冒出一堆点不到的行);父行不在时孩子自己站出来占一行,否则它既看不见也清不掉。
24
+ * 这些派生值只加在这次回信上,**不落清册**——清册该记的是文件里真有的事实,而"还剩几个孩子"
25
+ * "工作区还在不在"是随盘上随时变的读数。取数失败照抛(响亮失败,不端一份少了这栏的清单当没事发生)。
26
+ *
27
+ * 一组要释放多少、盘上还剩谁**只读计划一处算**(`planVisibleTrash`,与 purge 同一条口径);
28
+ * 这里不再自己枚举盘上后代,也不碰 `isLive`(活体准入只属于写动词)。
29
+ */
30
+ export declare function assembleTrashRows(deps: CoreDeps, rows: readonly TrashEntry[]): Promise<TrashRowWire[]>;
31
+ export {};
@@ -1,17 +1,17 @@
1
1
  /**
2
- * 批量删除的浮条(第十三批①):**多选模式内常驻**(0 条也渲染,既显示模式又给退出入口),
2
+ * 批量删除的浮条(2026-10-17 `6f24fd4`):**多选模式内常驻**(0 条也渲染,既显示模式又给退出入口),
3
3
  * 汇总「已选 N 条」+(读得到时)「含 K 个子会话」;「查看已选」展开限高可滚动的标题清单
4
4
  * (逐条可移除),回答"到底选了哪些"——清单默认收起,不把底部输入区盖住。
5
- * 模式内的整行点击守卫也挂在这棵常驻宿主上(见 `batch-select.tsx` 的 `useSelectionRowClickGuard`)。
5
+ * 模式内的整行点击守卫也挂在这棵常驻宿主上(见 `host-rows.ts` 的 `useSelectionRowClickGuard`)。
6
6
  *
7
- * 为什么单独一个文件而不是并进 `batch-select.tsx`:那份是"选中集 + 行开关"(被确认框 import),
8
- * 而浮条要点开确认框(要 import 它的现场)——两个方向合到一个文件上就是一圈。浮条住在
7
+ * 为什么单独一个文件而不是并进 `batch-select.tsx`:那份是"行开关控件",而浮条要点开确认框
8
+ * (要 import 它的现场)——两个方向合到一个文件上就是一圈。浮条住在
9
9
  * `shell.overlay` 上,与单删的撤销提示/确认框同类(槽位原文:"a toast stack or a status pill
10
10
  * all belong here")。
11
11
  *
12
12
  * 那个 K 是**勾选变化后防抖 300ms 的一次批量 count**(`/apply` 的 `action:'count'`,一组 id 一次),
13
- * 并且带**序号守卫**:在飞的旧响应不许盖掉新名单的读数(与第十二批那条 `reloadSeq` 同一个道理)。
14
- * 读不到就整句不说——K 是合计,缺一条就是假数(10-15 稿 §10「不许拿 0 冒充没有」)。
13
+ * 并且带**序号守卫**:在飞的旧响应不许盖掉新名单的读数(与 2026-10-17 `c66f082` 那条 `reloadSeq` 同一个道理)。
14
+ * 读不到就整句不说——K 是合计,缺一条就是假数(「不许拿 0 冒充没有」)。
15
15
  */
16
16
  import { type ReactElement } from 'react';
17
17
  import { type Translate } from './copy.js';
@@ -1,7 +1,7 @@
1
1
  /**
2
- * 批量删除的确认框(第十三批②)——挂在 `shell.overlay` 上的宿主,加它读的那个模块级现场。
2
+ * 批量删除的确认框(2026-10-17)——挂在 `shell.overlay` 上的宿主,加它读的那个模块级现场。
3
3
  *
4
- * 为什么又开一个文件而不是把 `delete-confirm.tsx` 撑大:单删那张框是**已交付的面**(设计稿 §7 状态 ②,
4
+ * 为什么又开一个文件而不是把 `delete-confirm.tsx` 撑大:单删那张框是**已交付的面**(docs/ui-spec.md 状态 ②,
5
5
  * 用例钉着),批量形与它只有三件不同——标题随条数、目标卡逐条列、执行是一次批量 `apply`。共用一个壳
6
6
  * 意味着单删的 `busy` / 失败面要跟着学批量形,那是拿已交付的行为去换省一个文件。卡片样式(`TARGET_CARD`)
7
7
  * 与 hint、子会话那两句**复用**单删的,一句不造第二个键。
@@ -10,9 +10,9 @@
10
10
  * `shell.overlay` 上——它必须活在行开关那棵子树之外(槽位行随宿主重排而卸载,见 `delete-confirm.tsx` 头注)。
11
11
  */
12
12
  import { type ReactElement } from 'react';
13
- import { type PickItem } from './batch-select.js';
14
13
  import { type Translate } from './copy.js';
15
14
  import { type SessionDeleteRemote } from './remote.js';
15
+ import { type PickItem } from './batch-selection-store.js';
16
16
  export interface BatchDeleteRequest {
17
17
  /** 每次发布递增:换一批就重挂载,busy / 失败面不带着上一批的记忆。 */
18
18
  seq: number;
@@ -23,6 +23,8 @@ export declare function requestDeleteBatch(items?: readonly PickItem[]): void;
23
23
  export declare function clearDeleteBatchRequest(): void;
24
24
  /** 供浮条与装配面订阅。 */
25
25
  export declare function useDeleteBatchRequest(): BatchDeleteRequest | null;
26
+ /** 测试专用:现场与 seq 归零,用例之间互不串味。 */
27
+ export declare function resetBatchConfirm(): void;
26
28
  export type BatchConfirmDeps = Pick<SessionDeleteRemote, 'applyOutcomes'>;
27
29
  /** 常驻的批量确认框宿主。没有待确认的批量删除时什么都不渲染。 */
28
30
  export declare function BatchConfirmHost({ deps, t, }: {
@@ -1,8 +1,8 @@
1
1
  /**
2
- * 会话「⋯」菜单里的「多选」入口(第十三批①改版)。
2
+ * 会话「⋯」菜单里的「多选」入口(2026-10-17)。
3
3
  *
4
4
  * 常驻勾选框被用户打回后,入口改成显式模式:按下这一行才进入多选态(再按一次退出),
5
- * 模式内才渲染勾选框、才把整行点击变成"勾/取消"(守卫在 `batch-select.tsx`)。
5
+ * 模式内才渲染勾选框、才把整行点击变成"勾/取消"(守卫在 `host-rows.ts`)。
6
6
  * 菜单的开关归 owner(与 `delete-menu-item.tsx` 同一条契约):按下后先切模式、再收菜单。
7
7
  *
8
8
  * 文案复用回收站工具栏那两句(`trash.toolbar.select` / `selectExit`)——同一个"多选/退出多选"
@@ -1,69 +1,26 @@
1
1
  /**
2
- * 宿主列表批量删除的选择面(第十三批①):多选模式 + 选中集 + 行首勾选 + 行尾兜底 + 整行点击守卫。
2
+ * 宿主列表批量删除的**控件面**(2026-10-17,2026-10-20 拆出状态与行适配):行首勾选框、行尾兜底开关,
3
+ * 以及确认框/浮条读的那一份计数问句。
3
4
  *
4
5
  * 入口在每行的「⋯」菜单里(`batch-menu-item.tsx`):**按下才进入多选模式**,模式内才渲染勾选框、
5
- * 才把整行点击变成"勾/取消";退出模式清空名单。选中集是**模块级现场**(本仓既有的客户端模式,见
6
- * `delete-confirm.tsx` / `undo-toast.tsx`):主勾选框住在每行的 `sidebar.session.row.leading` 槽位里,
7
- * 行尾兜底住在 `sidebar.workspaces.session.row.action`,浮条与批量确认框住在 `shell.overlay` 上——
8
- * 四处隔着槽位边界,只有模块级 store 能让他们看见同一份名单。状态源用 React 自带的
9
- * `useSyncExternalStore`,不为此引新的状态库。
6
+ * 才把整行点击变成"勾/取消";退出模式清空名单。选中集只住 `batch-selection-store.ts`(模式 + 名单一份
7
+ * 合并快照);"哪些行现在可见、点行算不算选中"只住 `host-rows.ts`。四处隔着槽位边界,只有模块级
8
+ * store 能让他们看见同一份名单;本文件不再持有任何现场。
10
9
  *
11
10
  * **为什么主勾选在行首**:宿主对行尾 `.rowActions` 是 `display:none`,只有 `row:hover` / 菜单打开
12
11
  * 才出现(app.asar 0.2.0-rc.2 的 Rows 渲染);行首 16px 格在无状态点时是空的,模式内可见。
13
12
  * 宿主在「有状态点(运行中/待确认/未读完成)」与归档行不渲染行首格 ⇒ 这些行走行尾悬停兜底,
14
- * 但模式内**点整行**也能选(`useSelectionRowClickGuard`),所以不依赖那颗小框。
13
+ * 但模式内**点整行**也能选(`host-rows.ts` 的 `useSelectionRowClickGuard`),所以不依赖那颗小框。
15
14
  * 两条勾选不会同时出现(`ui.css` 的 `:has` 兜底隐藏规则,靠宿主行的 `data-row-key`)。
16
- *
17
- * 名单只收槽位投影原样给的 `sessionId` / `displayTitle`(浏览器半不自己拼 id),**顺序 = 勾选顺序**:
18
- * 宿主列表的 DOM 序不归我们读,`/apply` 那条「保持输入顺序」吃到的因此就是勾选序。
19
15
  */
20
16
  import { type ReactElement } from 'react';
21
17
  import { type Translate } from './copy.js';
22
18
  import { type SessionDeleteRemote } from './remote.js';
23
- export interface PickItem {
24
- /** 宿主槽位投影的会话 id,原样透给 `applyOutcomes('delete', …)`。 */
25
- sessionId: string;
26
- /** 删除那一刻的会话名(投影的 `displayTitle`):只用于文案与确认框,不参与请求。 */
27
- title: string;
28
- }
29
- export declare function getPicks(): readonly PickItem[];
30
- export declare function isSelecting(): boolean;
31
- /** 「⋯ → 多选」按下时进入;重复进入是幂等的。 */
32
- export declare function enterSelection(): void;
33
- /** 退出模式并清空名单:多选态和选中集是同一次交互的两半,退出后不该留下看不见的名单。 */
34
- export declare function exitSelection(): void;
35
- export declare function useSelecting(): boolean;
36
- /** 翻转一条:没选过就排队尾,选过就摘掉(列表里永远不会有重复 id)。 */
37
- export declare function togglePick(item: PickItem): void;
38
- export declare function clearPicks(): void;
39
- /** 全选:把当前可见可选行里还没选的按 DOM 序追加;原有选中保持在前。 */
40
- export declare function selectAllVisible(): void;
41
- /**
42
- * 同一个切换口该显示「全选」还是「取消全选」:
43
- * - 有选中、且当前可见可选行全都选中 ⇒ 取消全选;
44
- * - 有选中、但当前没有可见可选行(全折叠/过滤掉)⇒ 只能清空,也显示取消全选;
45
- * - 其余(含 0 条)⇒ 全选。
46
- */
47
- export declare function allVisibleSelected(): boolean;
48
- /** 执行后收缩:只留失败名单(空数组 = 全清)。成功的随通告从侧栏消失,不该留在名单里。 */
49
- export declare function shrinkPicksToFailures(failedSessionIds: readonly string[]): void;
50
- export declare function usePicks(): readonly PickItem[];
51
- /**
52
- * 多选态内的整行点击守卫(document 捕获阶段)。
53
- *
54
- * 为什么必须在这里拦:宿主的会话行自己带 `onClick = open(sessionId)`,第三方没有官方的
55
- * "别切会话"开关;模式内点行要变成"勾/取消",只能抢在 React 根监听之前把事件吃掉。
56
- * 边界:行里的按钮 / 链接 / 输入 / 菜单项一律放行(否则 ⋯ 菜单、归档、置顶、单删都点不动);
57
- * 双击的第二下与 dblclick 不翻转(第一下已经选中,第二下不能又反掉,也不能打开宿主的重命名)。
58
- * 认行靠 `data-row-key="session:…"`——宿主将来改掉这个属性,最差是模式内点行不生效,
59
- * 勾选框本身仍在。
60
- */
61
- export declare function useSelectionRowClickGuard(): void;
62
19
  /**
63
20
  * 一组会话的子会话计数(`/apply` 的 `action:'count'`,纯读、不进串行链)。
64
21
  *
65
22
  * **一条读不到就整份返回 null** ——界面上那两个数(浮条的 K、确认框的 K 与逐条 M)都是合计,
66
- * 缺一条就是假数;「不许拿 0 冒充没有」(10-15 稿 §10)。
23
+ * 缺一条就是假数;「不许拿 0 冒充没有」。
67
24
  */
68
25
  export declare function fetchCounts(deps: Pick<SessionDeleteRemote, 'applyOutcomes'>, ids: readonly string[]): Promise<Map<string, number> | null>;
69
26
  export interface BatchPickCheckboxProps {
@@ -0,0 +1,30 @@
1
+ export interface PickItem {
2
+ /** 宿主槽位投影的会话 id,原样透给 `applyOutcomes('delete', …)`。 */
3
+ readonly sessionId: string;
4
+ /** 删除那一刻的会话名(投影的 `displayTitle`):只用于文案与确认框,不参与请求。 */
5
+ readonly title: string;
6
+ }
7
+ export interface SelectionState {
8
+ /** 多选模式:入口在每一行的「⋯」菜单里;只有模式内才渲染勾选框、才吃整行点击。 */
9
+ readonly selecting: boolean;
10
+ readonly picks: readonly PickItem[];
11
+ }
12
+ export declare function getSelection(): SelectionState;
13
+ export declare function getPicks(): readonly PickItem[];
14
+ export declare function isSelecting(): boolean;
15
+ /** 供组件订阅:模式与名单是同一次交互的两半,读它们的组件订同一份快照。 */
16
+ export declare function useSelection(): SelectionState;
17
+ /** 「⋯ → 多选」按下时进入;重复进入是幂等的(同一份快照,不发通告)。 */
18
+ export declare function enterSelection(): void;
19
+ /** 退出模式并清空名单:多选态和选中集是同一次交互的两半,退出后不该留下看不见的名单。 */
20
+ export declare function exitSelection(): void;
21
+ /** 翻转一条:没选过就排队尾,选过就摘掉(列表里永远不会有重复 id)。 */
22
+ export declare function togglePick(item: PickItem): void;
23
+ /** 清空名单但**留在多选态**(「取消全选」那颗);已经空了就不发通告。 */
24
+ export declare function clearPicks(): void;
25
+ /** 只追加还没选过的条目;名单序 = 原有选中在前、新条目按传入序接在后面。没有新条目就不发通告。 */
26
+ export declare function addPicks(items: readonly PickItem[]): void;
27
+ /** 执行后收缩:只留失败名单(空数组 = 全清)。成功的随通告从侧栏消失,不该留在名单里。 */
28
+ export declare function shrinkPicksToFailures(failedSessionIds: readonly string[]): void;
29
+ /** 测试专用:模式与名单一起回初值(订阅者随组件卸载自行退订)。 */
30
+ export declare function resetSelection(): void;
@@ -1,5 +1,8 @@
1
1
  /**
2
- * 可见文案(**全部**住在这里)+接上宿主 Client locale 服务的那一步。
2
+ * 可见文案(界面标签与提示)+接上宿主 Client locale 服务的那一步。
3
+ *
4
+ * **例外:渠道错误原文。** `src/client/receipt.ts` 的 missing-id、`src/client/remote.ts` 的
5
+ * 幂等空值与取数通道错误是协议诊断原文,固定中文、不随 locale;它们不进本字典。
3
6
  *
4
7
  * ## 为什么不是散在各组件里的中文字面量
5
8
  *
@@ -85,6 +88,18 @@ declare const ZH: {
85
88
  readonly 'trash.confirm.restore': "恢复 {count} 个会话?";
86
89
  readonly 'trash.confirm.restore.body': "这一批会回到它们各自的原项目目录,恢复到会话列表里。";
87
90
  readonly 'trash.confirm.restoreNow': "恢复";
91
+ readonly 'trash.orphans.banner': "回收站之外发现 {count} 个无主子会话";
92
+ readonly 'trash.orphans.meta': "共 {size},来自 {parents} 个失踪的父会话";
93
+ readonly 'trash.orphans.open': "查看并清理";
94
+ readonly 'trash.orphans.confirm': "清理 {count} 个无主子会话?";
95
+ readonly 'trash.orphans.body': "这些子会话的父会话已经不在会话列表或回收站里,清理后不可恢复。正在运行的子会话会被拒绝,不会被删除。";
96
+ readonly 'trash.orphans.group': "{project} · {count} 个 · {size}";
97
+ readonly 'trash.orphans.cacheNote': "本次只清理会话目录,不包含宿主投影缓存和附件。";
98
+ readonly 'trash.orphans.clean': "清理";
99
+ readonly 'trash.orphans.failure': "残留检查/清理失败:{message}";
100
+ readonly 'trash.orphans.cleanFailed': "{count} 组未清理:{message}";
101
+ readonly 'trash.orphans.cleanPartial': "已先删掉 {removed} 个会话目录({size})";
102
+ readonly 'trash.orphans.cleanGone': "有 {count} 组已不在了(可能已被别处清理,或父会话已恢复)";
88
103
  readonly 'detail.notSelected': "选中一条会话,看它讲了什么";
89
104
  readonly 'detail.loading': "正在读这条会话…";
90
105
  readonly 'detail.readFailed': "这条会话的正文读不出来";
@@ -30,6 +30,8 @@ export declare function requestDelete(input: {
30
30
  export declare function clearDeleteRequest(): void;
31
31
  /** 供宿主组件订阅。 */
32
32
  export declare function useDeleteRequest(): DeleteRequest | null;
33
+ /** 测试专用:现场与 seq 归零,用例之间互不串味。 */
34
+ export declare function resetDeleteRequest(): void;
33
35
  /**
34
36
  * 常驻的确认框宿主。挂 `shell.overlay`(框架级浮层;槽位说明原文 "a toast stack or a status
35
37
  * pill all belong here"):没有待确认的删除时什么都不渲染。
@@ -0,0 +1,35 @@
1
+ import { type PickItem } from './batch-selection-store.js';
2
+ export interface HostRowInfo {
3
+ /** 槽位投影的 `displayTitle`(行首槽只投 id,所以守卫靠行尾那颗常驻登记)。 */
4
+ readonly title: string;
5
+ /** 在跑的行点整行也不选:与置灰的勾选框同口径(级联遇在跑成员整组拒绝)。 */
6
+ readonly live: boolean;
7
+ }
8
+ /** 行尾兜底勾选开关登记一行:模式内与模式外都要登记(守卫进模式的那一帧就得有标题)。 */
9
+ export declare function registerHostRowInfo(sessionId: string, info: HostRowInfo): void;
10
+ /**
11
+ * 「当前可见」的可选行:侧栏里**已渲染**的会话行(`data-row-key="session:…"`)——折叠组、
12
+ * 归档过滤外的行不在 DOM 里,宿主隐藏的 subagent 也不在;在跑的行跳过(与置灰勾选框同口径)。
13
+ * 标题/运行态来自行尾兜底登记的 `rowInfo`(模式内它对每个非空行都渲染);DOM 序就是侧栏顺序。
14
+ */
15
+ export declare function readVisibleSelectableRows(): PickItem[];
16
+ /** 全选:把当前可见可选行按 DOM 序追加;已在名单里的由 `addPicks` 自己跳过,原有选中保持在前。 */
17
+ export declare function selectAllVisible(): void;
18
+ /**
19
+ * 同一个切换口该显示「全选」还是「取消全选」:
20
+ * - 有选中、且当前可见可选行全都选中 ⇒ 取消全选;
21
+ * - 有选中、但当前没有可见可选行(全折叠/过滤掉)⇒ 只能清空,也显示取消全选;
22
+ * - 其余(含 0 条)⇒ 全选。
23
+ */
24
+ export declare function allVisibleSelected(): boolean;
25
+ /**
26
+ * 多选态内的整行点击守卫(document 捕获阶段)。
27
+ *
28
+ * 边界:行里的按钮 / 链接 / 输入 / 菜单项一律放行(否则 ⋯ 菜单、归档、置顶、单删都点不动);
29
+ * 双击的第二下与 dblclick 不翻转(第一下已经选中,第二下不能又反掉,也不能打开宿主的重命名)。
30
+ * 认行靠 `data-row-key="session:…"`——宿主将来改掉这个属性,最差是模式内点行不生效,
31
+ * 勾选框本身仍在。模式外不挂监听:这时点行必须还是宿主的"打开会话"。
32
+ */
33
+ export declare function useSelectionRowClickGuard(): void;
34
+ /** 测试专用:登记表清空(会话 id 不跨用例复用,名字/运行态别串味)。 */
35
+ export declare function resetHostRows(): void;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * 宿主**主视图**的一处窄面:删掉当前打开的会话时把它关掉,撤销恢复后把它重开。
3
+ *
4
+ * 为什么需要:宿主客户端的常驻 `Session` 实例把 `removed` 记成**粘性**状态
5
+ * (`dsh-api-session-controller/lib/client.js` 的 `handleRemoved()` 只置位;`handleSessionAdded()`
6
+ * 只 merge 摘要,从不清它)。我们发 `api-session/removed` 之后,正在看的那个会话会一直停在
7
+ * `removed=true`:输入框禁用、占位文案是「会话不可用」。恢复再发 `api-session/added` 也不会把它
8
+ * 清掉——**除非把常驻实例本身丢掉**。宿主自己的归档走的就是这条路:`clearArchivedCurrent()`
9
+ * 发现当前会话进了归档集就 `clearMain()`(`dsh-client-ui-workspace/lib/client.js`)。
10
+ *
11
+ * 所以关闭/重开是**一段生命周期**(2026-10-21),不是一个 id:删除成功后 `afterDelete` 关掉当前
12
+ * 主视图并记下"这一条是我们关掉的";撤销成功后 `afterRestore` 只重开自己记过的那些。调用方不再
13
+ * 各自判定该不该关、也不再把会话 id 塞进撤销提示传一圈——判据全在这一处。两次重开排队时只有最新
14
+ * 那条会打开(token 守卫),`resetMainView()` 清掉这份记忆并让在等的重开作废。
15
+ *
16
+ * **不 import 宿主客户端包**(AGENTS.md 红线):只通过 `ctx.get('uiWorkspace')` 可选读服务,
17
+ * 形状不符/服务缺席都安静退回,绝不影响插件激活。字段与方法名来自上面两个包的产物原文;
18
+ * 宿主换代时先复核 `UiWorkspaceService` 的 `selection` / `sessions` / `clearMain` / `openSession`。
19
+ */
20
+ import type { ClientContextLike } from './context.js';
21
+ /** 入口激活时绑一次;之后每次调用再读服务(客户端插件激活顺序早于 `uiWorkspace` 也没关系)。 */
22
+ export declare function bindMainView(ctx: ClientContextLike): void;
23
+ /** 测试专用:模块级现场归零(记忆清空、在等的重开作废),用例之间互不串味。 */
24
+ export declare function resetMainView(): void;
25
+ /**
26
+ * 删除成功后调用:当前主视图若在这批里,把它关掉并记下"这一条是我们关掉的"(供撤销重开)。
27
+ * 关不掉/不是当前会话都什么都不做——这只是界面收尾,绝不让删除结果变红。
28
+ */
29
+ export declare function afterDelete(sessionIds: readonly string[]): void;
30
+ /**
31
+ * 撤销恢复成功后调用:把**我们自己关掉的**那几个会话重新打开(只认 `afterDelete` 记过的)。
32
+ *
33
+ * 一批里只有最后一条会开(`targets[targets.length - 1]`):主视图一次只装一个会话,而撤销恢复
34
+ * 的成功条数可能多于一条;`openWhenListed` 的 token 守卫保证前一条的等待不会后落地抢焦点。
35
+ * 任何更新的删除/恢复都会先作废旧等待;这里只负责「这一次要开哪一条」。
36
+ */
37
+ export declare function afterRestore(sessionIds: readonly string[]): void;
@@ -0,0 +1,43 @@
1
+ import { type ApplyAction, type ApplyOutcome, type ApplyValueByAction, type RemoteFailure } from '../shared/wire.js';
2
+ /**
3
+ * 一条 `/apply` 结果对一个请求 id 的含义。各写入面不许再各自推导:
4
+ * 缺 id 是通道错误,`null` 是幂等成功,orphan-clean 的 `not-found` 是中性提示而非失败。
5
+ *
6
+ * 各 action 可能出现的 kind(类型面故意保持同一个联合,消费方按 action 自己收窄):
7
+ * - delete / restore:`ok` / `already-gone` / `failed` / `missing`
8
+ * - count / purge:`ok` / `failed` / `missing`
9
+ * - orphan-clean:`ok` / `neutral` / `failed` / `missing`
10
+ */
11
+ export type ReceiptVerdict<A extends ApplyAction> = {
12
+ readonly kind: 'ok';
13
+ readonly id: string;
14
+ readonly value: ApplyValueByAction[A];
15
+ } | {
16
+ readonly kind: 'already-gone';
17
+ readonly id: string;
18
+ } | {
19
+ readonly kind: 'failed';
20
+ readonly id: string;
21
+ readonly error: RemoteFailure;
22
+ } | {
23
+ readonly kind: 'neutral';
24
+ readonly id: string;
25
+ readonly error: RemoteFailure;
26
+ } | {
27
+ readonly kind: 'missing';
28
+ readonly id: string;
29
+ readonly message: string;
30
+ };
31
+ export interface Receipt<A extends ApplyAction> {
32
+ readonly verdicts: readonly ReceiptVerdict<A>[];
33
+ readonly byId: ReadonlyMap<string, ReceiptVerdict<A>>;
34
+ }
35
+ export declare function missingOutcomeMessage(action: ApplyAction, id: string): string;
36
+ /** 三种“这一条没成”的 verdict;`failureMessage` 是它们原文取值的唯一入口。 */
37
+ export type FailureVerdict<A extends ApplyAction> = Extract<ReceiptVerdict<A>, {
38
+ kind: 'failed' | 'neutral' | 'missing';
39
+ }>;
40
+ export declare function isFailureVerdict<A extends ApplyAction>(verdict: ReceiptVerdict<A>): verdict is FailureVerdict<A>;
41
+ /** failed / neutral 取服务端原文 `error.message`;missing 取通道自己写的那句。 */
42
+ export declare function failureMessage<A extends ApplyAction>(verdict: FailureVerdict<A>): string;
43
+ export declare function readReceipt<A extends ApplyAction>(action: A, requestedIds: readonly string[], outcomes: readonly ApplyOutcome[]): Receipt<A>;