@xrn1997/dsh-session-delete 0.4.0 → 0.6.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 (70) hide show
  1. package/README.md +39 -10
  2. package/cordis.patch.yml +4 -0
  3. package/icon.svg +18 -18
  4. package/lib/client.js +1897 -859
  5. package/lib/index.js +1803 -1058
  6. package/lib/types/api/attachment-orphans.d.ts +9 -3
  7. package/lib/types/api/attachment.d.ts +17 -7
  8. package/lib/types/api/connection-route.d.ts +23 -0
  9. package/lib/types/api/dispatch.d.ts +25 -14
  10. package/lib/types/api/route-table.d.ts +47 -11
  11. package/lib/types/api/trash-rows.d.ts +3 -3
  12. package/lib/types/client/batch-bar.d.ts +1 -1
  13. package/lib/types/client/batch-pick.d.ts +19 -0
  14. package/lib/types/client/batch-select.d.ts +7 -5
  15. package/lib/types/client/busy-gate.d.ts +6 -2
  16. package/lib/types/client/confirm-close.d.ts +4 -1
  17. package/lib/types/client/context.d.ts +3 -2
  18. package/lib/types/client/copy.d.ts +54 -4
  19. package/lib/types/client/delete-confirm.d.ts +1 -1
  20. package/lib/types/client/delete-menu-item.d.ts +1 -1
  21. package/lib/types/client/host-rows.d.ts +29 -30
  22. package/lib/types/client/main-view.d.ts +2 -1
  23. package/lib/types/client/markdown.d.ts +6 -7
  24. package/lib/types/client/orphan-maintenance.d.ts +10 -25
  25. package/lib/types/client/pending-open.d.ts +17 -0
  26. package/lib/types/client/receipt.d.ts +3 -1
  27. package/lib/types/client/remote.d.ts +21 -18
  28. package/lib/types/client/store.d.ts +15 -10
  29. package/lib/types/client/trash-detail.d.ts +48 -6
  30. package/lib/types/client/trash-entry.d.ts +1 -1
  31. package/lib/types/client/trash-nav.d.ts +12 -8
  32. package/lib/types/client/ui/Toast.d.ts +3 -1
  33. package/lib/types/client/ui/icons.d.ts +1 -1
  34. package/lib/types/client/undo-toast.d.ts +2 -1
  35. package/lib/types/client/use-maintenance-section.d.ts +52 -0
  36. package/lib/types/client/use-maintenance.d.ts +7 -35
  37. package/lib/types/client/use-trash-panel-controller.d.ts +35 -14
  38. package/lib/types/client/use-trash-query.d.ts +10 -8
  39. package/lib/types/client/use-trash-selection.d.ts +15 -12
  40. package/lib/types/client/write-seats.d.ts +32 -16
  41. package/lib/types/client/write-settlement.d.ts +1 -1
  42. package/lib/types/client/write.d.ts +13 -3
  43. package/lib/types/config.d.ts +45 -0
  44. package/lib/types/deps.d.ts +8 -3
  45. package/lib/types/domain/closure.d.ts +47 -0
  46. package/lib/types/index.d.ts +13 -12
  47. package/lib/types/log/zstd-frames.d.ts +2 -3
  48. package/lib/types/ports/attachments-port.d.ts +40 -0
  49. package/lib/types/ports/fs-port.d.ts +15 -12
  50. package/lib/types/ports/host-port.d.ts +4 -5
  51. package/lib/types/ports/presence-port.d.ts +9 -0
  52. package/lib/types/shared/wire.d.ts +137 -31
  53. package/lib/types/trash/attachments.d.ts +10 -1
  54. package/lib/types/trash/detail.d.ts +32 -22
  55. package/lib/types/trash/entry.d.ts +7 -1
  56. package/lib/types/trash/groups.d.ts +6 -1
  57. package/lib/types/trash/manifest.d.ts +4 -4
  58. package/lib/types/verbs/batch-carried.d.ts +1 -3
  59. package/lib/types/verbs/live-admission.d.ts +1 -1
  60. package/lib/types/verbs/member-presence.d.ts +3 -1
  61. package/lib/types/verbs/move-group.d.ts +2 -1
  62. package/lib/types/verbs/orphan-subagents.d.ts +12 -5
  63. package/lib/types/verbs/permanent-removal.d.ts +39 -10
  64. package/lib/types/verbs/trash-scope.d.ts +36 -0
  65. package/locale/en.json +6 -6
  66. package/locale/zh.json +6 -6
  67. package/package.json +13 -3
  68. package/lib/types/api/request-trust.d.ts +0 -45
  69. package/lib/types/ports/connection-port.d.ts +0 -33
  70. package/lib/types/verbs/purge-trash.d.ts +0 -35
@@ -3,8 +3,9 @@
3
3
  *
4
4
  * 它先是回收站面板上的一块,2026-10-27 起搬进插件设置页的「维护」卡(面板不再自动扫孤儿,ADR-0021)。
5
5
  * 之所以仍收成一个 module:这三段与任何一面的清单渲染、选择、键盘、拖栏都毫无关系;改维护块的文案或
6
- * 确认框时,diff 不该与布局改动互相踩。它只吃一个窄接口 `OrphanMaintenanceQuery`——报告、三条提示与
7
- * 四个动作,实现方是设置页的 `use-maintenance.ts`。
6
+ * 确认框时,diff 不该与布局改动互相踩。它只吃泛型段 module 的一张视图 `MaintenanceSectionView`——
7
+ * 报告、三条提示、确认框与四个动作;实现方是 `use-maintenance-section.ts` 的状态机
8
+ * (设置页的 `use-maintenance.ts` 为孤儿会话段实例化它)。
8
9
  *
9
10
  * 三条界面规则(现状住 `docs/ui-spec.md`):
10
11
  * - **报告非空才出警告条**:空报告与 `null` 都不出现——"检查过了,什么都没有"不是要给用户看的东西,
@@ -13,33 +14,17 @@
13
14
  * ("没能回答有多少残留"与"没有残留"是两件事,不许折成空报告)。
14
15
  * - **「已经不在」不是失败**:别处清掉或父已回到清册时单独一条中性提示,不计入失败,下次检查时自然消失。
15
16
  *
16
- * 四个动作都由实现方给:警告条开预览(`openOrphanPreview`)、失败块重试(`scanOrphans`)、
17
- * 确认框清理(`cleanOrphans`)、取消(`setOrphanConfirm`)。`busy` 不归本 module 管——它是实现方
18
- * 那一段自己的忙碌读数,通过 props 传进来。
17
+ * 四个动作都从段上取:警告条开预览(`openPreview`,唯一开门、先清旧提示)、失败块重试(`scan`)、
18
+ * 确认框清理(`clean`)、取消(`cancel`,只收框)。`busy` 也不再是 prop——段自己给(在飞即忙),
19
+ * 本块只读它按住按钮。
19
20
  */
20
21
  import type { ReactElement } from 'react';
21
22
  import type { Translate } from './copy.js';
22
23
  import type { OrphanReportWire } from './remote.js';
23
- /**
24
- * 本块消费的那一面:一份报告、三条提示、四个动作。窄到只列它真读的成员——实现方多给(阶段、忙碌、
25
- * 附件回执…)都与本块无关,也不许本块去读。设置页的 `Maintenance` 结构上满足它。
26
- */
27
- export interface OrphanMaintenanceQuery {
28
- /** `null` = 还没检查/读不到;本块只认"非空报告"那一档。 */
29
- readonly orphanReport: OrphanReportWire | null;
30
- readonly orphanFailure: string | null;
31
- readonly orphanNotice: string | null;
32
- readonly orphanConfirm: boolean;
33
- setOrphanConfirm(open: boolean): void;
34
- openOrphanPreview(): void;
35
- cleanOrphans(): Promise<void>;
36
- /** 检查(也是失败块那颗「重试」):维护面上没有第二份扫描入口。 */
37
- scanOrphans(): Promise<void>;
38
- }
24
+ import type { MaintenanceSectionView } from './use-maintenance-section.js';
39
25
  export interface OrphanMaintenanceProps {
40
- query: OrphanMaintenanceQuery;
41
- /** 这一段自己的忙碌读数:在飞时按住本块里的每一颗按钮。 */
42
- busy: boolean;
26
+ /** 段视图:读数、提示、确认框、忙碌与四个动作都在它上面,本块不多要也不少读。 */
27
+ section: MaintenanceSectionView<OrphanReportWire>;
43
28
  t: Translate;
44
29
  }
45
- export declare function OrphanMaintenance({ query, busy, t }: OrphanMaintenanceProps): ReactElement | null;
30
+ export declare function OrphanMaintenance({ section, t }: OrphanMaintenanceProps): ReactElement | null;
@@ -0,0 +1,17 @@
1
+ export interface PendingOpenInput {
2
+ /** 订阅「清单变了」;返回退订。旧 store 可能没有——那时只有"已经出现"这一支能开。 */
3
+ readonly subscribe?: ((listener: () => void) => () => void) | undefined;
4
+ /** 目标是否已经出现在清单里。 */
5
+ readonly isReady: () => boolean;
6
+ /** 出现的这一拍还该不该抢焦点(用户可能已经选了别的会话)。 */
7
+ readonly canOpen: () => boolean;
8
+ /** 打开目标;调用方自己收宿主抛错。 */
9
+ readonly open: () => void;
10
+ }
11
+ export interface PendingOpen {
12
+ /** 取消在等的等待(没有也安全);后一次 `schedule` 会先取消前一次。 */
13
+ cancel(): void;
14
+ /** 等目标出现再打开;已经在清单里就立即打开。 */
15
+ schedule(input: PendingOpenInput): void;
16
+ }
17
+ export declare function createPendingOpen(giveUpMs?: number): PendingOpen;
@@ -68,11 +68,13 @@ export declare function readSingleReceipt<A extends ApplyAction>(action: A, id:
68
68
  export interface ReceiptSummary {
69
69
  readonly successIds: readonly string[];
70
70
  readonly neutralIds: readonly string[];
71
- /** failed + missing(通道没回);neutral 单独在 `neutralIds`,部分计数只从 failed 的 details 取。 */
71
+ /** failed + missing(通道没回);neutral 单独在 `neutralIds`。 */
72
72
  readonly failed: readonly {
73
73
  readonly id: string;
74
74
  readonly error: RemoteFailure;
75
75
  }[];
76
+ /** 部分计数的累加语义只住这里:只从 failed 的 `details` 取 `removed`/`freedBytes` 并按批相加
77
+ * ("删了一半才炸"的那部分释放量);neutral / missing 不带 details,不参与累加。 */
76
78
  readonly partial: {
77
79
  readonly removed: number;
78
80
  readonly freedBytes: number;
@@ -1,9 +1,13 @@
1
1
  /**
2
- * 浏览器半的取数通道:同源 fetch /dsh-session-delete-api,并处理信封与失败原文。
2
+ * 浏览器半的取数通道:同源 fetch 宿主 `connection.fetch` 的 exact Fetch 路由(`/api` + 插件前缀)。
3
+ * 信封与失败原文在这一层收口。
3
4
  *
4
- * - 路由是宿主 web server 上的插件前缀;官方 remote.<namespace> 需要生成的 Typert 描述符,
5
- * 第三方无法动态挂载,因此走这条薄线(ADR-0009)。
6
- * - 九段:list / ping / orphans / detail / apply / empty / attachment / attachment-orphans / attachment-clean;
5
+ * - 九段注册在宿主 `connection.fetch.register` 上,鉴权归宿主 `/api` bridge(ADR-0033);
6
+ * 没有旧前缀 fallback——支持面是 web UI 与桌面端,二者的标准组合都装 `client-connection`。
7
+ * 官方 remote.<namespace> 仍不可用(第三方拿不到 Typert 描述符的动态发现挂载点)。
8
+ * - 九段的**段名与方法**住 `src/shared/wire.ts` 的结构化段名表(`ROUTES` 每项 `{ path, method }`),
9
+ * 本文件只投影路径(`prefix + ROUTES.x.path`)与方法(GET 段直接取 `ROUTES.x.method`,写段把它
10
+ * 交给 `post()`)——方法字面量不在本文件出现第二份;
7
11
  * 写动作统一走 apply;`attachment` 是唯一二进制段,由 `src/client/attachment-view.tsx` 直接拼地址,
8
12
  * 不经过本文件的信封通道;`attachment-orphans`(纯读)与 `attachment-clean`(聚合写)是孤儿附件的两段。
9
13
  * - 信封与官方 unary 面同形:{ok:true,value} / {ok:false,error}。HTTP 往返本身失败才 reject;
@@ -25,6 +29,8 @@ export type PurgeResultLike = PurgeStatsWire;
25
29
  * **线上只有"日志里读出来的那几件"**:`deletedAt` / `sizeBytes` / `wasArchived` / 清册 `title` 这些
26
30
  * 清册已有的字段一律不过线——组件按 `entryId` 与自己手上那份清单配对,这样"清单里的大小"和
27
31
  * "详情里的大小"永远是同一个数,不会因为两次读数不同而各说各话。
32
+ * 例外只有一格:`subagents`(孩子摘要)。孩子不在客户端清单里、无从配对,
33
+ * 它们的清册事实只能随详情过线(issue #7,ADR-0029)。
28
34
  */
29
35
  export type TrashDetailLike = TrashDetailWire;
30
36
  /** 任意异常 → 失败面。非带标记的失败(网络层、TypeError…)也保留原文,绝不吞;带标记的失败把
@@ -63,7 +69,7 @@ export interface RawSessionDeleteRemote {
63
69
  attachmentOrphans(): Promise<RemoteResult<AttachmentOrphanReportWire>>;
64
70
  /** 聚合写:只删上一条报告那一刻无引用的图片对象,回 `AttachmentCleanupWire`。 */
65
71
  attachmentClean(): Promise<RemoteResult<AttachmentCleanupWire>>;
66
- detail(entryId: string): Promise<RemoteResult<TrashDetailLike>>;
72
+ detail(entryId: string, before?: number): Promise<RemoteResult<TrashDetailLike>>;
67
73
  /** 一组 id 上的同一个动作。顶层信封的 `ok:false` 只表示**整次调用**没办成(路由自检、栅栏之外);
68
74
  * 单条动作的成败住在 `ApplyOutcome` 里——这是批量接口的判据,别在信封层找它。 */
69
75
  apply(action: ApplyAction, ids: readonly string[]): Promise<RemoteResult<ApplyOutcome[]>>;
@@ -71,20 +77,17 @@ export interface RawSessionDeleteRemote {
71
77
  empty(): Promise<RemoteResult<PurgeResultLike>>;
72
78
  }
73
79
  /**
74
- * 前缀路由的 fetch 实现。**相对路径**:不拼 base、不认 origin——页面本身就是从宿主 web server
75
- * 加载的,同源是这条薄线的全部前提。本机(含转发落在回环的桌面壳)靠回环 + Origin/Referer 过栅栏;
76
- * 远程(LAN / 反代)客户端另由宿主 `connection` 判「已认证的 Web 身份」,所以这条通道**只对**
77
- * `dsh web` 打印的 URL 打开过的页面成立(凭据是那行 URL 换来的 cookie,不是插件自己的秘密)。
80
+ * exact Fetch 路由的 fetch 实现。**相对路径**:不拼 host、不认 origin——页面本身就是从宿主页面
81
+ * 加载的,同源是这条通道的全部前提;鉴权归宿主 `/api` bridge(ADR-0033)。所以这条通道**只对**
82
+ * 宿主页面打开过、带宿主身份的客户端成立(凭据是那次访问换来的 cookie,不是插件自己的秘密)。
78
83
  */
79
- export declare function createHttpRemote(prefix?: string): RawSessionDeleteRemote;
84
+ export declare function createHttpRemote(): RawSessionDeleteRemote;
80
85
  /**
81
- * 取数通道探测:一次 `GET /ping` 走通(或走到)就是"通道在"。
86
+ * 探测唯一通道:一次 `GET /ping` 回成功信封(`pong`)才注册入口。
82
87
  *
83
- * 判据是**「拿到信封」而不是「操作成功」**:`{ok:false, error}` 也是一次成功的往返(路由在、依赖接上了、
84
- * 错误映射也生效了),那时该做的是把入口显示出来、让原文上屏;只有往返本身失败才算通道缺席 ⇒
85
- * 一个槽位贡献都不注册(docs/architecture.md §3「通道探测决定入口出现与否」)。
86
- *
87
- * 探测用一次零成本的 `GET /ping`,不读清单,避免每次页面加载都跑全表对账。
88
+ * - 401/403 是宿主 `/api` 的纯文本身份/归属失败:入口不出现,不装禁用态。
89
+ * - 失败信封 / 404 / 非信封同样按不可用处理:`/api` 网关对未知路径也可能回错误形状,
90
+ * 不能据此把入口摆出来让每个动作都失败。
88
91
  */
89
92
  export declare function probeChannel(raw: RawSessionDeleteRemote): Promise<boolean>;
90
93
  /** `shared/wire.ts` 的 `ApplyOutcome`、扫描报告与附件回执从这里转出去——浏览器半只认 `remote.js` 这一扇门。 */
@@ -113,11 +116,11 @@ export interface SessionDeleteRemote {
113
116
  /** 回收站之外的无主子会话报告(纯读)。读不到就是 throw —— 界面上那是**一次失败的检查**,
114
117
  * 不是"零条残留"(把 `io` 折成空报告等于把"不知道"说成"没有")。 */
115
118
  scanOrphans(): Promise<OrphanReportWire>;
116
- /** `.dsh` 内无引用图片对象的报告(纯读;含最多 200 条预览清单)。读不到就是 throw,同样不许折成 0。 */
119
+ /** `.dsh` 内无引用图片对象的报告(纯读;预览清单条数由 Node 半配置,默认 200)。读不到就是 throw,同样不许折成 0。 */
117
120
  scanAttachmentOrphans(): Promise<AttachmentOrphanReportWire>;
118
121
  /** 清掉报告那一刻无引用的图片副本(聚合回执)。 */
119
122
  cleanAttachmentOrphans(): Promise<AttachmentCleanupWire>;
120
- detail(entryId: string): Promise<TrashDetailLike>;
123
+ detail(entryId: string, before?: number): Promise<TrashDetailLike>;
121
124
  /** 一段管所有写动作:一次调用把整批办完,逐条结果原样交回组件。 */
122
125
  applyOutcomes(action: ApplyAction, ids: readonly string[]): Promise<ApplyOutcome[]>;
123
126
  /** 清空回收站(聚合回执)。 */
@@ -17,18 +17,14 @@ export interface ClientStore<T> {
17
17
  export declare function createStore<T>(initial: T): ClientStore<T>;
18
18
  /** 懒读快照:`get()` 只在脏了时调用 `read()`,干净时回同一个引用(React snapshot 契约)。 */
19
19
  export interface LazyStore<T> {
20
- get(): T;
20
+ /** 属性声明:`get` / `subscribe` 会被摘下来直接交给 `useSyncExternalStore`(宿主行快照),
21
+ * 实现不读 `this`。方法简写会给它隐式 `this: LazyStore<T>`,触发 `unbound-method` 误报。 */
22
+ get: () => T;
21
23
  /** 标脏并通告;下一次 `get()` 重读。 */
22
- invalidate(): void;
23
- subscribe(listener: () => void): () => void;
24
+ invalidate: () => void;
25
+ subscribe: (listener: () => void) => () => void;
24
26
  }
25
- export interface LazyStoreLifecycle {
26
- /** 第一个订阅者到来时跑(观察者在这里接上)。 */
27
- onFirstSubscribe?(): void;
28
- /** 最后一个订阅者退订时跑(观察者在这里断开)。 */
29
- onLastUnsubscribe?(): void;
30
- }
31
- export declare function createLazyStore<T>(initial: T, read: () => T, lifecycle?: LazyStoreLifecycle): LazyStore<T>;
27
+ export declare function createLazyStore<T>(initial: T, read: () => T): LazyStore<T>;
32
28
  /**
33
29
  * 最新票句柄:每张票记着签发时的序号,只有序号仍等于当前的那张才 `current`。
34
30
  * 调用方在发起异步读时 `issue()` 一张票,落地前问 `ticket.current`——旧读因此不会盖掉新状态。
@@ -46,3 +42,12 @@ export interface LatestWins {
46
42
  invalidate(): void;
47
43
  }
48
44
  export declare function createLatestWins(): LatestWins;
45
+ /**
46
+ * 引用归一:输入允许调用方逐渲染换新对象,回调只经这份 ref 读**最新**那一份,于是依赖它的回调
47
+ * 身份恒稳、调用方的 effect 可以放心把它们当依赖——「输入必须是稳定引用」的隐式契约就此不存在
48
+ * (issue #8 第 3 条)。写回在 effect 里(提交后),与 React 的渲染期只读约定一致。
49
+ * 这一形状的唯一实现:别在调用方再手写一份「useRef + 每渲染写回」。
50
+ */
51
+ export declare function useLatest<T>(value: T): {
52
+ readonly current: T;
53
+ };
@@ -1,27 +1,69 @@
1
+ /**
2
+ * 回收站详情块:只画状态,不取数。选中、缓存、失败与重读都在面板控制器里。
3
+ *
4
+ * 三条界面约束:
5
+ * - 动作(恢复/彻底删除)留在清单行右侧;详情只负责看。
6
+ * - 标题优先用清册标题,空串才回落到日志的 session/title。
7
+ * - 正文是**块**:用户/助手文字常显;思考、工具、注入、系统消息、压缩摘要默认折叠,折叠头给
8
+ * 标签 + 首行摘要(工具另给状态)。注入也铺进正文——这是对宿主 Chat 的有意偏离(它隐藏普通注入,
9
+ * 而这里的目标是「删之前看全」);条数另进元信息表。
10
+ *
11
+ * 助手正文与思考走 Markdown,用户提问保持原话;工具参数/结果走等宽纯文本(限高可滚)。
12
+ * 附件随块位置渲染:用户气泡之上、助手正文之后、工具展开体末尾。
13
+ */
14
+ import { type RefObject } from 'react';
1
15
  import { type Translate } from './copy.js';
2
- import type { TrashDetailLike, TrashEntryLike } from './remote.js';
16
+ import type { TrashEntryWire } from '../shared/wire.js';
17
+ import type { TrashDetailLike } from './remote.js';
18
+ /** 详情主体只承诺清册那一行:孩子视图看的那条不在客户端清单里,没有只读数可配对
19
+ * (孩子摘要随父详情过线,那一格的判据住 `src/client/remote.ts` 的 `TrashDetailLike`)。 */
3
20
  export type TrashDetailState = {
4
21
  kind: 'idle';
5
22
  } | {
6
23
  kind: 'loading';
7
- entry: TrashEntryLike;
24
+ entry: TrashEntryWire;
8
25
  } | {
9
26
  kind: 'failed';
10
- entry: TrashEntryLike;
27
+ entry: TrashEntryWire;
11
28
  message: string;
12
29
  } | {
13
30
  kind: 'ready';
14
- entry: TrashEntryLike;
31
+ entry: TrashEntryWire;
15
32
  detail: TrashDetailLike;
16
33
  };
34
+ /** 「加载更早」这一块(在飞/失败 + 滚动容器)整体过手:`TrashDetail` 只转交,谁画正文谁拆开用。
35
+ * 四样东西是一个概念(分页读面)的四个格子,打包走一个名字,不再逐样穿透组件边界。 */
36
+ export interface DetailPager {
37
+ /** 更早一页正在读。 */
38
+ loadingOlder: boolean;
39
+ /** 更早一页读失败的原文;正文本身仍是 ready。 */
40
+ olderFailure: string | null;
41
+ onLoadOlder: () => void;
42
+ /** 详情栏的滚动容器;prepend 更早一页后按高度差补偿滚动位置。 */
43
+ scrollRef?: RefObject<HTMLDivElement | null>;
44
+ }
45
+ /** 详情的导航面(孩子视图的开与回):类型定义住在这里(视图定 prop 形状),
46
+ * 控制器 `import type` 它避免环。判据(谁在场、title 给什么)住控制器的 `detailNav` 派生。 */
47
+ export interface DetailNav {
48
+ /** 只在主体是清单选中那条时交出(ADR-0029 单级判据的唯一实现)。 */
49
+ readonly onOpenChild?: (child: TrashEntryWire) => void;
50
+ /** 孩子视图的返回目标;title 是原文(未命名兜底由视图按 t 处理)。 */
51
+ readonly parentContext?: {
52
+ readonly title: string;
53
+ readonly onBack: () => void;
54
+ };
55
+ }
17
56
  export interface TrashDetailProps {
18
57
  state: TrashDetailState;
19
58
  /** 有写操作在飞(动作按钮住在清单行里,这里只用来把「重试」按住)。 */
20
59
  busy: boolean;
21
- /** 宽档不画返回——同屏就有清单,返回是多余的一步。 */
60
+ /** 宽档不画「返回清单」——同屏就有清单,那一步多余。孩子视图另有一颗返回,见 `detailNav.parentContext`。 */
22
61
  wide: boolean;
23
62
  onRetry: () => void;
24
63
  onBack: () => void;
64
+ /** 孩子视图的开与回(控制器派生,判据见 `DetailNav`):缺席 = 不画入口/返回,组件只画状态,不自己造动作。 */
65
+ detailNav: DetailNav;
66
+ pager: DetailPager;
25
67
  t: Translate;
26
68
  }
27
- export declare function TrashDetail({ state, busy, wide, onRetry, onBack, t }: TrashDetailProps): import("react").JSX.Element;
69
+ export declare function TrashDetail({ state, busy, wide, onRetry, onBack, detailNav, pager, t, }: TrashDetailProps): import("react").JSX.Element;
@@ -1,6 +1,6 @@
1
1
  /** 宿主 `SidebarPanelIconOwnerProps`:请求的方形边长;`active` 收了但不用(见头注)。 */
2
2
  export interface TrashPanelIconProps {
3
- size?: number;
3
+ size?: number | undefined;
4
4
  active?: boolean;
5
5
  }
6
6
  export declare function TrashPanelIcon({ size }: TrashPanelIconProps): import("react").JSX.Element;
@@ -4,8 +4,8 @@ import type { TrashEntryLike } from './remote.js';
4
4
  * 导航要的面。`ordered` 是**屏幕上的顺序**(只含展开的组),由面板/选择面算好传进来——
5
5
  * 布局常量与本 module 无关。`drillEnabled` 是这一档支不支持整行下钻(窄档为真;宽档 Enter/Space
6
6
  * 只移动选中,与整行点击的 `{ drill: !split }` 同一条判据)。四个动作都由调用方注入:
7
- * `select` 只移动选中(带 `drill` 才下钻),`exitDrill` 是控制器那条「← 返回清单」的同一动作,
8
- * `focusRow` 把焦点交给某一行。
7
+ * `select` 只移动选中(带 `drill` 才下钻),`back` 是控制器的回退动作(两程:先收孩子视图、
8
+ * 再退下钻),`focusRow` 把焦点交给某一行。
9
9
  */
10
10
  export interface TrashNavInput {
11
11
  ordered: readonly TrashEntryLike[];
@@ -13,18 +13,22 @@ export interface TrashNavInput {
13
13
  selectMode: boolean;
14
14
  drillEnabled: boolean;
15
15
  drilled: boolean;
16
- select(entry: TrashEntryLike, options?: {
16
+ /** 四个动作都是**属性声明**:它们被摘下来放进 `useTrashNav` 的输入或直接挂成事件处理器,
17
+ * 实现体一个都不读 `this`。方法简写会带上隐式 `this: TrashNavInput`,`unbound-method`
18
+ * 会把这种交接报成可疑;改属性只是让类型面说真话。 */
19
+ select: (entry: TrashEntryLike, options?: {
17
20
  drill?: boolean;
18
- }): Promise<void>;
19
- toggleCheck(entryId: string): void;
20
- exitDrill(): void;
21
- focusRow(entryId: string): void;
21
+ }) => Promise<void>;
22
+ toggleCheck: (entryId: string) => void;
23
+ back: () => void;
24
+ focusRow: (entryId: string) => void;
22
25
  }
23
26
  export interface TrashNav {
24
27
  /** `aria-activedescendant` 的那条 id:选中行不在 DOM 里时是 `null`。 */
25
28
  readonly activeDescendantId: string | null;
26
29
  /** 清单里唯一拿着 `tabIndex=0` 的那一行;清单空时是 `null`。 */
27
30
  readonly tabStopId: string | null;
28
- onListKeyDown(event: ReactKeyboardEvent<HTMLElement>): void;
31
+ /** 直接挂到清单容器的 `onKeyDown`:属性声明(见上)让「摘下来挂事件」不被误报。 */
32
+ onListKeyDown: (event: ReactKeyboardEvent<HTMLElement>) => void;
29
33
  }
30
34
  export declare function useTrashNav(input: TrashNavInput): TrashNav;
@@ -19,6 +19,8 @@ import './styles.js';
19
19
  export interface ToastAction {
20
20
  label: string;
21
21
  onClick: () => void;
22
+ /** 在飞/不可用时按灰并挡住点击(Toast 是本地配方,动作面按调用点的需要长)。 */
23
+ disabled?: boolean;
22
24
  }
23
25
  export interface ToastProps {
24
26
  /** 已解析好的横幅文案(本地化由调用面负责)。 */
@@ -29,7 +31,7 @@ export interface ToastProps {
29
31
  /** 全不透明停留时长;缺省 3000。 */
30
32
  holdMs?: number;
31
33
  /** 句尾的行内动作(照着句子的顺序排)。 */
32
- actions?: readonly ToastAction[];
34
+ actions?: readonly ToastAction[] | undefined;
33
35
  /** 淡出完成时调用一次:在这里卸载。 */
34
36
  onDone: () => void;
35
37
  }
@@ -12,7 +12,7 @@
12
12
  import type { ReactElement } from 'react';
13
13
  /** 宿主图标组件的实际使用面:一个方形边长,色走 `currentColor`。 */
14
14
  export interface IconProps {
15
- size?: number;
15
+ size?: number | undefined;
16
16
  }
17
17
  export declare function IconTrashOutlineRegular({ size }: IconProps): ReactElement;
18
18
  export declare function IconWarningOutlineRegular({ size }: IconProps): ReactElement;
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * 撤销提示(docs/ui-spec.md 状态 ③):删除成功后一条可撤销的横幅。
3
3
  *
4
- * 现场与发布住在 `undo-notices.ts`;本文件只做组件,并把通知台与外部恢复座位组合起来。
4
+ * 现场与发布住在 `undo-notices.ts`;本文件只做组件:画通知台已有的事实,「撤销」整趟按下交给
5
+ * `write-seats.ts` 的 `restoreFromUndo`(编排归恢复座位,见 ADR-0018 的边界)。
5
6
  * 为什么组件要独立:宿主的菜单行随菜单**一起卸载**(真产物 Menu 的注释原文
6
7
  * "the rows unmount with the list"),而提示要在菜单关掉之后还活着。宿主的官方配方同形——
7
8
  * 会话行的动作把请求写进一个模块级 store,`shell.overlay` 里的 `workspace.row-toast` 读它。
@@ -0,0 +1,52 @@
1
+ import type { RemoteOutcome } from './remote.js';
2
+ /** 一段在飞的动作:检查只读,清理会写。状态行靠它分开说「检查中…」与「清理中…」;`null` = 空闲。 */
3
+ export type MaintenancePhase = 'scanning' | 'cleaning';
4
+ /** 一段维护交给界面的全部:三份读数(报告 / 失败 / 中性提示)、一个确认框、一档忙碌,加四个动作。 */
5
+ export interface MaintenanceSectionView<R> {
6
+ /** `null` = 还没检查(或上一次检查失败——失败在 `failure` 单独一格,不折成零条)。 */
7
+ readonly report: R | null;
8
+ readonly failure: string | null;
9
+ /** 「这一组已经不在」/ 上一趟清理的回执——不是失败:单独一条中性提示。 */
10
+ readonly notice: string | null;
11
+ readonly confirm: boolean;
12
+ readonly phase: MaintenancePhase | null;
13
+ /** 段自己给的忙碌(在飞即忙),不再作为 prop 传递。 */
14
+ readonly busy: boolean;
15
+ /** 手动检查(也是失败块的重试)。 */
16
+ scan: () => Promise<void>;
17
+ /** 唯一开门:先清旧失败与旧提示再开确认框。 */
18
+ openPreview: () => void;
19
+ /** 清完必重查;回执句子由 config 拼。 */
20
+ clean: () => Promise<void>;
21
+ /** 只收确认框,不碰提示 / 失败。 */
22
+ cancel: () => void;
23
+ }
24
+ /** `settle` 拼出的这一趟要摆的句子;**缺席的格子表示"不碰"**(重查刚摆上的状态保持原样)。 */
25
+ export interface MaintenanceSettlePlan {
26
+ readonly notice?: string | null;
27
+ readonly failure?: string | null;
28
+ }
29
+ export interface MaintenanceSectionConfig<R, C> {
30
+ /** 手动检查(也是失败块的重试)。交回的已是落地面——glue 用 `callRemote` 包装、不会抛,module 不再包一层。 */
31
+ scan(): Promise<RemoteOutcome<R>>;
32
+ /** 清理写动作;入参是点击那一刻的报告(清理输入由各段自己从里面点名)。报告没活可干时
33
+ * 同步回 `null`:不发请求、只收确认框——"没活"的判据各段不同,是各段的业务。
34
+ * 同样交回落地面、不抛。`C` 是这一段的回执形状:状态机不认识它,只把它原样递给
35
+ * `beforeRescan` / `settle`,段那边就不必把 `outcome.value` 擦回 `unknown` 再 `as` 一次。 */
36
+ clean(report: R): Promise<RemoteOutcome<C>> | null;
37
+ /** 清理落地后、必发的重查起飞前先摆的读数;`null` = 不动。附件段用它把"已清空"先收成 0:
38
+ * 旧报告不再可信,别让旧横幅在重查飞着时冒充"没清"。 */
39
+ beforeRescan?(outcome: RemoteOutcome<C>): R | null;
40
+ /** 检查读不到时的上屏句;本地化前缀各段自己给。 */
41
+ scanFailureText(message: string): string;
42
+ /** 这一趟检查**赢了**(没被更晚的检查顶掉)之后顺手收掉的东西;输家不调用。它在赢家的读数
43
+ * 落地**之前**触发,故只许写自己那份不属于读数的状态。孤儿会话段用它清上一趟清理的附件回执
44
+ * ——「收掉旧回执」属于最新读数成立的一部分,放在发请求之前会让被顶掉的输家也清掉赢家的回执。 */
45
+ onScanWon?: () => void;
46
+ /** 清理回执(含顶层失败)→ 这一趟的句子。重查赢了才调用;段独有的第二回执(如孤儿会话段顺带
47
+ * 清掉的附件读数)由它顺手写自己的状态——那不是这台状态机的格子。
48
+ * `report` 是**这一趟**点击那一刻的报告:回执要按请求名单逐条判读,名单只能随这一趟交下来;
49
+ * 段自己存一份"最近一次清理的名单"会在两趟交错时把上一趟的回执按后一趟的名单判读。 */
50
+ settle(outcome: RemoteOutcome<C>, report: R): MaintenanceSettlePlan;
51
+ }
52
+ export declare function useMaintenanceSection<R, C>(config: MaintenanceSectionConfig<R, C>): MaintenanceSectionView<R>;
@@ -1,43 +1,15 @@
1
1
  import type { Translate } from './copy.js';
2
2
  import { type AttachmentOrphanReportWire, type OrphanReportWire, type SessionDeleteRemote } from './remote.js';
3
+ import { type MaintenanceSectionView } from './use-maintenance-section.js';
3
4
  /** 维护卡只需要这四段:两份报告 + 孤儿会话的批量写 + 孤儿附件的聚合写。 */
4
5
  export type MaintenanceDeps = Pick<SessionDeleteRemote, 'scanOrphans' | 'applyOutcomes' | 'scanAttachmentOrphans' | 'cleanAttachmentOrphans'>;
5
- /** 一段在飞的动作:检查只读,清理会写。状态行靠它分开说「检查中…」与「清理中…」;`null` = 空闲。 */
6
- export type MaintenancePhase = 'scanning' | 'cleaning';
7
6
  export interface Maintenance {
8
- readonly orphanReport: OrphanReportWire | null;
9
- readonly orphanFailure: string | null;
10
- /** 「这一组已经不在」不是失败:单独一条中性提示。 */
11
- readonly orphanNotice: string | null;
12
- /** 上一趟孤儿会话清理顺带清掉的附件读数(`.dsh` 内独占图片副本)。重查读不出来,和
13
- * `orphanNotice` 一样是过去那一趟的回执;下一次检查 / 清理会被收掉。 */
14
- readonly orphanAttachmentNotice: string | null;
15
- readonly orphanConfirm: boolean;
16
- readonly orphanBusy: boolean;
17
- /** 在飞的是检查还是清理;`null` = 空闲。写动作在飞时状态行据此说「清理中…」。 */
18
- readonly orphanPhase: MaintenancePhase | null;
19
- /** 手动检查(`GET /orphans`)。失败回 `null` + 原文,不折成零条。 */
20
- scanOrphans(): Promise<void>;
21
- /** 「查看并清理」:先收掉旧提示再开确认框。 */
22
- openOrphanPreview(): void;
23
- /** `/apply` 的 `orphan-clean`:清完必重查;中性 / 失败 / 部分计数按回执说。 */
24
- cleanOrphans(): Promise<void>;
25
- setOrphanConfirm(open: boolean): void;
26
- readonly attachmentReport: AttachmentOrphanReportWire | null;
27
- readonly attachmentFailure: string | null;
28
- /** 上一趟清理的回执(已清 / 跳过 / 逐文件失败),重查读不出来。 */
29
- readonly attachmentNotice: string | null;
30
- readonly attachmentConfirm: boolean;
31
- readonly attachmentBusy: boolean;
32
- /** 在飞的是检查还是清理;`null` = 空闲。 */
33
- readonly attachmentPhase: MaintenancePhase | null;
34
- /** 手动检查(`GET /attachment-orphans`)。 */
35
- scanAttachmentOrphans(): Promise<void>;
36
- /** 「查看并清理」:先收掉旧提示再开确认框。 */
37
- openAttachmentPreview(): void;
38
- /** `POST /attachment-clean`:清完必重查;`removed` / `failed` / `skipped` 按回执说。 */
39
- cleanAttachmentOrphans(): Promise<void>;
40
- setAttachmentConfirm(open: boolean): void;
7
+ /** 孤儿会话段 + 它独有的第二回执:这一趟清理顺带清掉的附件读数(`.dsh` 内独占图片副本)。
8
+ * 重查读不出来,和段的 `notice` 一样是过去那一趟的回执;下一次检查 / 清理会被收掉。 */
9
+ readonly orphans: MaintenanceSectionView<OrphanReportWire> & {
10
+ readonly attachmentNotice: string | null;
11
+ };
12
+ readonly attachments: MaintenanceSectionView<AttachmentOrphanReportWire>;
41
13
  }
42
14
  export declare function useMaintenance({ deps, t }: {
43
15
  deps: MaintenanceDeps;
@@ -1,6 +1,7 @@
1
1
  import type { BusyGate } from './busy-gate.js';
2
2
  import { type SessionDeleteRemote, type TrashEntryLike } from './remote.js';
3
- import type { TrashDetailState } from './trash-detail.js';
3
+ import type { TrashEntryWire } from '../shared/wire.js';
4
+ import type { DetailNav, TrashDetailState } from './trash-detail.js';
4
5
  import { type TrashQuery } from './use-trash-query.js';
5
6
  /** 待确认的动作:一条记录、整个回收站、或多选挑出来的一批(`ids` 已按清单顺序排好)。
6
7
  * 类型归状态机,视图只 import 它来渲染。 */
@@ -17,15 +18,16 @@ export type PendingAction = {
17
18
  ids: readonly string[];
18
19
  };
19
20
  /**
20
- * 控制器要的面。`query` 取五样:清单(选中从它里面找)、`refreshAll`(挂载与显式重试)、
21
- * `refreshAfterWrite`(失效与写后)、清空(面板的写动作那一半)、`noteAttachmentCleanup`(purge 回执里
21
+ * 控制器要的面。`query` 取四样:清单(选中从它里面找)、`reload`(挂载、显式重试、失效与写后
22
+ * 都是同一条读)、清空(面板的写动作那一半)、`noteAttachmentCleanup`(purge 回执里
22
23
  * 附件那半句交给查询侧拼句子上屏)。`ordered` 是键盘/自动选中用的"屏幕上的顺序"(只含展开的组),
23
- * 由面板算好传进来——布局常量不搬进控制器。
24
+ * 由面板算好传进来——布局常量不搬进控制器。输入的引用**不需稳定**:失效 effect 的判据只有
25
+ * `revision`,reload 回调经 `useLatest` 读最新那一份(判据住 `store.ts`)。
24
26
  */
25
27
  export interface TrashPanelControllerInput {
26
28
  deps: Pick<SessionDeleteRemote, 'detail' | 'applyOutcomes'>;
27
29
  gate: BusyGate;
28
- query: Pick<TrashQuery, 'list' | 'refreshAll' | 'refreshAfterWrite' | 'emptyTrash' | 'noteAttachmentCleanup'>;
30
+ query: Pick<TrashQuery, 'list' | 'reload' | 'emptyTrash' | 'noteAttachmentCleanup'>;
29
31
  ordered: readonly TrashEntryLike[];
30
32
  canAutoSelect: boolean;
31
33
  }
@@ -33,27 +35,46 @@ export interface TrashPanelController {
33
35
  readonly busy: boolean;
34
36
  readonly selected: TrashEntryLike | null;
35
37
  readonly selectedId: string | null;
38
+ /** 正在看哪个孩子(离清单的详情导航态;null = 看清单选中那条)。单级是设计(ADR-0029):
39
+ * 只有一个槽,`openChild` 换槽不叠栈,返回交还清单选中那条。 */
40
+ readonly childView: TrashEntryWire | null;
41
+ /** 详情的导航面(开孩子 / 回父)。ADR-0029 单级判据的**唯一实现**:面板只转交它,
42
+ * 不再自己拼 openChild / parentContext 两个派生块(判据有第二个家,视图与测试就得各自复述语义)。 */
43
+ readonly detailNav: DetailNav;
44
+ /** 打开一个孩子(父详情子会话段点进来):读它的详情,但清单选中不动。 */
45
+ openChild(child: TrashEntryWire): Promise<void>;
46
+ /** 回到父详情(详情顶部那颗「返回父会话」;窄档 back/Esc 的第一程也走它)。 */
47
+ closeChild(): void;
36
48
  readonly drilled: boolean;
37
49
  readonly detail: TrashDetailState;
50
+ /** 更早一页正在读(只对当前详情主体有意义:孩子视图开着就是孩子)。 */
51
+ readonly loadingOlder: boolean;
52
+ /** 更早一页读失败的原文(只对当前详情主体有意义;正文本身仍是 ready)。 */
53
+ readonly olderFailure: string | null;
38
54
  /** 待确认的那一批(确认框的开合);确认落地后由控制器收起。 */
39
55
  readonly pending: PendingAction | null;
40
56
  /** 恢复确认框要摆的这一批(按勾选顺序;清单里已找不到的 id 滤掉)。 */
41
57
  readonly pendingEntries: readonly TrashEntryLike[];
42
58
  /** 恢复/彻底删除/批量 的失败原文,按条目 id 存:切走再回来还在。 */
43
59
  readonly actionFailure: Readonly<Record<string, string>>;
44
- select(entry: TrashEntryLike, options?: {
60
+ /** 下面是面板会**摘下来转交**的动作(导航的 `select`/`back`、确认框的 `cancelPending`):
61
+ * 属性声明而不是方法简写,实现体都不读 `this`。方法简写的隐式 `this` 会让 `unbound-method`
62
+ * 把转交报成可疑;改形状是纯类型面,零运行时改动。 */
63
+ select: (entry: TrashEntryLike, options?: {
45
64
  drill?: boolean;
46
- }): Promise<void>;
47
- retry(): void;
48
- back(): void;
65
+ }) => Promise<void>;
66
+ /** 按当前详情的 `olderCursor` 读更早一页,拼到现有块前面。 */
67
+ loadOlder: () => Promise<void>;
68
+ retry: () => void;
69
+ back: () => void;
49
70
  /** 构造待确认动作(唯一入口:新增一种确认只改 `PendingAction` 与确认框,不再加一串转调)。 */
50
- request(action: PendingAction): void;
51
- cancelPending(): void;
71
+ request: (action: PendingAction) => void;
72
+ cancelPending: () => void;
52
73
  /** 单条恢复(`/apply` 的 N=1):成功后丢缓存,写完无论成败都重读清单,失败原文落回这一行。 */
53
- restore(entry: TrashEntryLike): Promise<void>;
74
+ restore: (entry: TrashEntryLike) => Promise<void>;
54
75
  /** 确认框上的「确认」:按 `pending.kind` 分派到空清或 `confirmApply`。 */
55
- confirmPurge(): Promise<void>;
76
+ confirmPurge: () => Promise<void>;
56
77
  /** 一组 id 上的同一个动作:逐条成败落成行上原文/缓存失效,整批只重读一次清单。 */
57
- confirmApply(action: 'purge' | 'restore', ids: readonly string[]): Promise<void>;
78
+ confirmApply: (action: 'purge' | 'restore', ids: readonly string[]) => Promise<void>;
58
79
  }
59
80
  export declare function useTrashPanelController(input: TrashPanelControllerInput): TrashPanelController;
@@ -29,14 +29,16 @@ export interface TrashQuery {
29
29
  * 读数冒充这一趟;删除与恢复不碰它。它是**过去那一趟的回执**、不是清单状态:清单重读读不出来。 */
30
30
  readonly attachmentNotice: string | null;
31
31
  /** 把一份附件读数摆上屏(`undefined` = 这一趟没有读数,旧提示收掉);句子怎么拼归这里,调用方只管转交。 */
32
- noteAttachmentCleanup(a: AttachmentCleanupWire | undefined): void;
33
- /** 重读清单;挂载与显式「重试」用它。面板没有第二条扫描:孤儿检查是设置页维护卡的手动动作。 */
34
- refreshAll(): Promise<void>;
35
- /** 写后/失效通告:与 `refreshAll` 同一条读。两个名字留着是因为调用点问的是两件事
36
- * ("用户要看现在的"与"别处动过了,跟上"),不是两条路径。清单读失败时旧行进 `unknown`。 */
37
- refreshAfterWrite(): Promise<void>;
38
- /** 清空回收站(聚合回执):成功就熄掉撤销横幅并重读,失败就把清单置为未知并摆上原文。交回是否成功。 */
39
- emptyTrash(): Promise<boolean>;
32
+ noteAttachmentCleanup: (a: AttachmentCleanupWire | undefined) => void;
33
+ /** 重读清单:挂载、显式「重试」、写后与失效通告都走这一条读——问的是同一件事("清单现在长什么样")。
34
+ * 曾经分作 `refreshAll` / `refreshAfterWrite` 两个名字,那是调用点问的两件事("用户要看现在的"与
35
+ * "别处动过了,跟上"),不是两条路径,合一为 `reload`。面板没有第二条扫描:孤儿检查是设置页
36
+ * 维护卡的手动动作(ADR-0021)。清单读失败时旧行进 `unknown`。 */
37
+ reload: () => Promise<void>;
38
+ /** 清空回收站(聚合回执):成功就熄掉撤销横幅并重读,失败就把清单置为未知并摆上原文。
39
+ * 三种形状各有意思:`true` = 清空落地了;`false` = 这一趟跑了但没清成(原文已在清单那块上屏);
40
+ * `undefined` = 闸把这一趟挡回来了、什么都没跑(调用方不收框,等在飞那一趟的结论)。 */
41
+ emptyTrash: () => Promise<boolean | undefined>;
40
42
  }
41
43
  export declare function useTrashQuery({ deps, t, gate, }: {
42
44
  deps: TrashQueryDeps;