@xrn1997/dsh-session-delete 0.3.0 → 0.4.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 (74) hide show
  1. package/README.md +21 -15
  2. package/lib/client.js +3317 -2173
  3. package/lib/index.js +1691 -964
  4. package/lib/types/api/attachment-orphans.d.ts +11 -0
  5. package/lib/types/api/attachment.d.ts +23 -0
  6. package/lib/types/api/dispatch.d.ts +9 -29
  7. package/lib/types/api/request-trust.d.ts +45 -0
  8. package/lib/types/api/route-table.d.ts +28 -14
  9. package/lib/types/client/attachment-view.d.ts +27 -0
  10. package/lib/types/client/batch-bar.d.ts +3 -2
  11. package/lib/types/client/batch-confirm.d.ts +3 -3
  12. package/lib/types/client/batch-menu-item.d.ts +1 -1
  13. package/lib/types/client/batch-select.d.ts +1 -1
  14. package/lib/types/client/busy-gate.d.ts +6 -0
  15. package/lib/types/client/confirm-close.d.ts +11 -0
  16. package/lib/types/client/copy.d.ts +48 -38
  17. package/lib/types/client/delete-confirm.d.ts +0 -2
  18. package/lib/types/client/host-rows.d.ts +18 -3
  19. package/lib/types/client/main-view.d.ts +8 -3
  20. package/lib/types/client/maintenance-parts.d.ts +25 -0
  21. package/lib/types/client/maintenance-settings.d.ts +22 -0
  22. package/lib/types/client/markdown.d.ts +9 -1
  23. package/lib/types/client/orphan-maintenance.d.ts +45 -0
  24. package/lib/types/client/receipt.d.ts +75 -5
  25. package/lib/types/client/remote.d.ts +49 -69
  26. package/lib/types/client/store.d.ts +31 -0
  27. package/lib/types/client/trash-confirm.d.ts +2 -15
  28. package/lib/types/client/trash-nav.d.ts +30 -0
  29. package/lib/types/client/trash-panel.d.ts +4 -3
  30. package/lib/types/client/ui/Checkbox.d.ts +1 -1
  31. package/lib/types/client/ui/Modal.d.ts +2 -2
  32. package/lib/types/client/ui/icons.d.ts +6 -11
  33. package/lib/types/client/ui/top-layer.d.ts +10 -0
  34. package/lib/types/client/undo-notices.d.ts +52 -0
  35. package/lib/types/client/undo-toast.d.ts +6 -52
  36. package/lib/types/client/use-column-resize.d.ts +9 -20
  37. package/lib/types/client/use-maintenance.d.ts +45 -0
  38. package/lib/types/client/use-panel-width.d.ts +1 -1
  39. package/lib/types/client/use-trash-panel-controller.d.ts +27 -23
  40. package/lib/types/client/use-trash-query.d.ts +39 -25
  41. package/lib/types/client/write-seats.d.ts +44 -0
  42. package/lib/types/client/write-settlement.d.ts +66 -0
  43. package/lib/types/client/write.d.ts +54 -0
  44. package/lib/types/deps.d.ts +20 -9
  45. package/lib/types/index.d.ts +8 -3
  46. package/lib/types/log/session-log.d.ts +4 -3
  47. package/lib/types/log/zstd-frames.d.ts +14 -8
  48. package/lib/types/ports/agent-handles.d.ts +1 -1
  49. package/lib/types/ports/connection-port.d.ts +33 -0
  50. package/lib/types/ports/fs-port.d.ts +13 -1
  51. package/lib/types/ports/host-port.d.ts +21 -63
  52. package/lib/types/ports/presence-port.d.ts +22 -0
  53. package/lib/types/shared/wire.d.ts +77 -26
  54. package/lib/types/trash/attachments.d.ts +85 -0
  55. package/lib/types/trash/detail.d.ts +29 -6
  56. package/lib/types/trash/entry.d.ts +3 -3
  57. package/lib/types/trash/groups.d.ts +18 -14
  58. package/lib/types/trash/ledger.d.ts +20 -0
  59. package/lib/types/trash/manifest.d.ts +2 -1
  60. package/lib/types/trash/reconcile.d.ts +4 -3
  61. package/lib/types/trash/storage-manifest.d.ts +1 -1
  62. package/lib/types/verbs/batch-carried.d.ts +20 -0
  63. package/lib/types/verbs/delete-session.d.ts +6 -5
  64. package/lib/types/verbs/errors.d.ts +4 -10
  65. package/lib/types/verbs/live-admission.d.ts +10 -0
  66. package/lib/types/verbs/member-journal.d.ts +25 -7
  67. package/lib/types/verbs/member-presence.d.ts +33 -24
  68. package/lib/types/verbs/move-group.d.ts +20 -0
  69. package/lib/types/verbs/orphan-subagents.d.ts +11 -5
  70. package/lib/types/verbs/permanent-removal.d.ts +31 -24
  71. package/lib/types/verbs/purge-trash.d.ts +21 -8
  72. package/lib/types/verbs/restore-session.d.ts +12 -16
  73. package/package.json +4 -2
  74. package/lib/types/client/write-commit.d.ts +0 -10
@@ -0,0 +1,45 @@
1
+ /**
2
+ * 无主子会话的**维护块**——警告条、失败块、中性提示与清理确认框。
3
+ *
4
+ * 它先是回收站面板上的一块,2026-10-27 起搬进插件设置页的「维护」卡(面板不再自动扫孤儿,ADR-0021)。
5
+ * 之所以仍收成一个 module:这三段与任何一面的清单渲染、选择、键盘、拖栏都毫无关系;改维护块的文案或
6
+ * 确认框时,diff 不该与布局改动互相踩。它只吃一个窄接口 `OrphanMaintenanceQuery`——报告、三条提示与
7
+ * 四个动作,实现方是设置页的 `use-maintenance.ts`。
8
+ *
9
+ * 三条界面规则(现状住 `docs/ui-spec.md`):
10
+ * - **报告非空才出警告条**:空报告与 `null` 都不出现——"检查过了,什么都没有"不是要给用户看的东西,
11
+ * 而"还没扫到"更不是"零条"。
12
+ * - **读不到就说读不到**:检查/清理失败在原位摆原文 + 重试,绝不静默折成一条都不出现
13
+ * ("没能回答有多少残留"与"没有残留"是两件事,不许折成空报告)。
14
+ * - **「已经不在」不是失败**:别处清掉或父已回到清册时单独一条中性提示,不计入失败,下次检查时自然消失。
15
+ *
16
+ * 四个动作都由实现方给:警告条开预览(`openOrphanPreview`)、失败块重试(`scanOrphans`)、
17
+ * 确认框清理(`cleanOrphans`)、取消(`setOrphanConfirm`)。`busy` 不归本 module 管——它是实现方
18
+ * 那一段自己的忙碌读数,通过 props 传进来。
19
+ */
20
+ import type { ReactElement } from 'react';
21
+ import type { Translate } from './copy.js';
22
+ 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
+ }
39
+ export interface OrphanMaintenanceProps {
40
+ query: OrphanMaintenanceQuery;
41
+ /** 这一段自己的忙碌读数:在飞时按住本块里的每一颗按钮。 */
42
+ busy: boolean;
43
+ t: Translate;
44
+ }
45
+ export declare function OrphanMaintenance({ query, busy, t }: OrphanMaintenanceProps): ReactElement | null;
@@ -1,17 +1,20 @@
1
- import { type ApplyAction, type ApplyOutcome, type ApplyValueByAction, type RemoteFailure } from '../shared/wire.js';
1
+ import type { Translate } from './copy.js';
2
+ import { type ApplyAction, type ApplyOutcome, type ApplyValueByAction, type AttachmentCleanupWire, type RemoteFailure } from '../shared/wire.js';
2
3
  /**
3
4
  * 一条 `/apply` 结果对一个请求 id 的含义。各写入面不许再各自推导:
4
- * 缺 id 是通道错误,`null` 是幂等成功,orphan-clean 的 `not-found` 是中性提示而非失败。
5
+ * 缺 id 是通道错误,`null` 是幂等成功,orphan-clean 的 `not-found` 是中性提示而非失败,
6
+ * **N=1 的 `null` 是协议错**——最后这条也在这里判,`applyOne` 与 `runWrite` 只消费同一份 verdict。
5
7
  *
6
8
  * 各 action 可能出现的 kind(类型面故意保持同一个联合,消费方按 action 自己收窄):
7
9
  * - delete / restore:`ok` / `already-gone` / `failed` / `missing`
8
10
  * - count / purge:`ok` / `failed` / `missing`
9
11
  * - orphan-clean:`ok` / `neutral` / `failed` / `missing`
12
+ * - 单条读法(`readSingleReceipt`):不含 `already-gone`;null 折成 `failed`
10
13
  */
11
14
  export type ReceiptVerdict<A extends ApplyAction> = {
12
15
  readonly kind: 'ok';
13
16
  readonly id: string;
14
- readonly value: ApplyValueByAction[A];
17
+ readonly value: Exclude<ApplyValueByAction[A], null>;
15
18
  } | {
16
19
  readonly kind: 'already-gone';
17
20
  readonly id: string;
@@ -29,15 +32,82 @@ export type ReceiptVerdict<A extends ApplyAction> = {
29
32
  readonly message: string;
30
33
  };
31
34
  export interface Receipt<A extends ApplyAction> {
35
+ /** 产生这份回执的动作:摘要据此只派发该动作真的有的那几种落地 id。 */
36
+ readonly action: A;
32
37
  readonly verdicts: readonly ReceiptVerdict<A>[];
33
38
  readonly byId: ReadonlyMap<string, ReceiptVerdict<A>>;
34
39
  }
40
+ /** 浏览器半自己的未分类诊断码(通道打不通 / 非信封 / 单条协议错)。它不过线,线上失败码仍归 `shared/wire.ts`。 */
41
+ export declare const CLIENT_UNKNOWN_CODE = "client/unknown";
42
+ export interface ReceiptOptions {
43
+ /** N=1:null 只能是通道答错了,不冒充幂等成功。 */
44
+ readonly single?: boolean;
45
+ }
46
+ export type SingleReceiptVerdict<A extends ApplyAction> = Exclude<ReceiptVerdict<A>, {
47
+ kind: 'already-gone';
48
+ }>;
49
+ export interface SingleReceipt<A extends ApplyAction> {
50
+ readonly verdicts: readonly SingleReceiptVerdict<A>[];
51
+ readonly byId: ReadonlyMap<string, SingleReceiptVerdict<A>>;
52
+ }
35
53
  export declare function missingOutcomeMessage(action: ApplyAction, id: string): string;
36
54
  /** 三种“这一条没成”的 verdict;`failureMessage` 是它们原文取值的唯一入口。 */
37
55
  export type FailureVerdict<A extends ApplyAction> = Extract<ReceiptVerdict<A>, {
38
56
  kind: 'failed' | 'neutral' | 'missing';
39
57
  }>;
40
- export declare function isFailureVerdict<A extends ApplyAction>(verdict: ReceiptVerdict<A>): verdict is FailureVerdict<A>;
41
58
  /** failed / neutral 取服务端原文 `error.message`;missing 取通道自己写的那句。 */
42
59
  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>;
60
+ export declare function readReceipt<A extends ApplyAction>(action: A, requestedIds: readonly string[], outcomes: readonly ApplyOutcome[], options?: ReceiptOptions): Receipt<A>;
61
+ /**
62
+ * N=1 的读法:null 折成协议错,类型面因此不含 `already-gone`(下面那次过滤只服务类型收窄,
63
+ * single 读法运行期不会产出它)。`applyOne` 用它之后只剩「按 id 取一条 + 抛失败」。
64
+ */
65
+ export declare function readSingleReceipt<A extends ApplyAction>(action: A, id: string, outcomes: readonly ApplyOutcome[]): SingleReceipt<A>;
66
+ /** 一次写入回执的**一个读法**:成功、失败、中性、部分计数与落地 id 都从这里出。
67
+ * 座位与收尾不再自己 walk `verdicts`——那会把同一套判据在调用点再组装一遍。 */
68
+ export interface ReceiptSummary {
69
+ readonly successIds: readonly string[];
70
+ readonly neutralIds: readonly string[];
71
+ /** failed + missing(通道没回);neutral 单独在 `neutralIds`,部分计数只从 failed 的 details 取。 */
72
+ readonly failed: readonly {
73
+ readonly id: string;
74
+ readonly error: RemoteFailure;
75
+ }[];
76
+ readonly partial: {
77
+ readonly removed: number;
78
+ readonly freedBytes: number;
79
+ };
80
+ /** 这次动作真正落地的 id。**域由 `action` 定**:delete 是条目 id(撤销名单),restore 是会话 id
81
+ * (回盘名单);purge / count / orphan-clean 没有可撤销的落地 id,恒为空。
82
+ * 两个域不混在一个数组里靠调用方猜,也不各留一个永远为空的字段。 */
83
+ readonly landedIds: readonly string[];
84
+ readonly anySuccess: boolean;
85
+ /** purge / orphan-clean 的 `ok` 回执里那半份附件读数,**按批相加**(一段 `/apply` 可能带多条 ok)。
86
+ * `skipped` 取第一条有的:它说的是"这一轮为什么没敢清",不是可累加的计数。
87
+ * 两种动作都没有读数(或这一批一条都没落地)时缺席——不拿零冒充"清过"。 */
88
+ readonly attachmentCleanup?: AttachmentCleanupWire;
89
+ }
90
+ /** 一次摘要派生的两样产物:聚合摘要与逐条失败原文。
91
+ * 失败表只挂在 `WriteSuccess` 上(成败两支的统一读法),摘要自己不再留第二份。 */
92
+ export interface ReceiptRead {
93
+ readonly summary: ReceiptSummary;
94
+ readonly failures: ReadonlyMap<string, string>;
95
+ }
96
+ /**
97
+ * 附件清理回执的「跳过半句」:`skipped` 说的是"这一轮为什么没敢清",不是失败——原因上屏,
98
+ * `removed > 0` 时说「部分」,`failed` 件数追加在句尾(不被跳过原因吞掉)。
99
+ * 面板(`use-trash-query.ts`)与维护卡(`use-maintenance.ts`)共用这一判据,不许各拼一份。
100
+ * 没 `skipped` 时回 `null`:后半句(全清/失败怎么说)两个面各有各的口径,由调用方自己接。
101
+ */
102
+ export declare function attachmentSkippedSentence(a: AttachmentCleanupWire, t: Translate): string | null;
103
+ export declare function summarizeReceipt<A extends ApplyAction>(receipt: Receipt<A>, sessionIdOfEntry?: (entryId: string) => string | undefined): ReceiptRead;
104
+ /**
105
+ * `/apply` 成功支回执里的条目 id(delete / restore 的 `value.id`);purge 的聚合值与 count 的数字值跳过。
106
+ * 撤销名单只收 `ok` 支的条目 id:already-gone 是「已被同批前一条带走」的幂等条,跟着组走,不单列。
107
+ */
108
+ export declare function landedEntryIds<A extends ApplyAction>(receipt: Receipt<A>): string[];
109
+ /**
110
+ * restore 回执里真正回盘的会话 id:`ok` 支取条目上的 `sessionId`;`already-gone` 没有行,
111
+ * 由调用方给条目查找兜底。purge / count 的 `ok` 值没有 `sessionId`,会被跳过。
112
+ */
113
+ export declare function restoredSessionIds<A extends ApplyAction>(receipt: Receipt<A>, sessionIdOfEntry?: (entryId: string) => string | undefined): string[];
@@ -1,42 +1,19 @@
1
1
  /**
2
- * 浏览器半的**取数通道**:同源 fetch 宿主 web server 上的 `/dsh-session-delete-api`,
3
- * 加上信封判别与注入面那一处「展开成裸值」的适配。
2
+ * 浏览器半的取数通道:同源 fetch /dsh-session-delete-api,并处理信封与失败原文。
4
3
  *
5
- * 通道形状(裁决):真相/账本/判据全在宿主那边,Node 半用宿主**自己的 web server** 开一条
6
- * 插件专属前缀路由把六个段名(`list` / `ping` / `orphans` / `detail` / `apply` / `empty`)搬出来
7
- * (`src/api/dispatch.ts`),浏览器半同源调用。**不是重造后端**:
8
- * 路由层只做搬运与信封。为什么不用官方的 `remote.<namespace>`:那条路要**生成的 Typert 远程描述符**
9
- * 加一次 `$mount`,而盒内装配清单写死在 `dsh-api-remotes` 里、全宿主没有动态发现,第三方只能自挂自,
10
- * 对一个薄插件不划算。
11
- *
12
- * 三条约束决定了这个文件长这样:
13
- *
14
- * 1. **信封刻意与官方 unary 面同形**:`{ ok: true, value }` / `{ ok: false, error }`。这不是"还留着
15
- * 官方通道"——它现在是**我们两半之间**的 wire 契约(Node 半按它产出,见 `src/api/dispatch.ts`),
16
- * 选它是为了让组件面、`unwrapRemoteResult` 与既有用例一行都不用改。失败是**返回**而不是
17
- * reject(同宿主网关原文:"Every unary call resolves to `RemoteResult<T>` … and never rejects for a
18
- * carrier problem");只有往返本身失败(打不通、回的不是信封)才 reject,那是**通道缺席**。
19
- * 2. **不许值 import 跨插件包**。`error` 在线上就是一段 JSON(`{code, message, details?}`),不是
20
- * `RemoteError` 实例——所以判别**所依据的那一个标记位**只镜一份,家在两半共用的
21
- * `src/shared/wire.ts`(`isDSHRemoteError`)。浏览器半落到本地时用 `throwRemoteFailure` 就地补上
22
- * Error 本体与标记位;Node 半(`src/remote/map-error.ts`)只用它做「这已经是一次远程失败」的判别,
23
- * 产出的是线上那三件(`details` 可缺席)、不造任何 Error,更不 import 宿主那个类。
24
- * 3. **失败原文必须能上屏**。调用方(`callRemote`)只把**抛出来的失败**收成判别式状态——
25
- * `{ ok: false, error }` 不是第三种落地:它在注入面那一处已经被 `unwrapRemoteResult` 抛成了异常,
26
- * 所以到了组件面前只剩「裸值 / throw」两支,调用方只写 `if (!outcome.ok)`,正常流里没有 try/catch;
27
- * 非本信封形状的异常(网络层、TypeError…)也不丢原文(见 `failureOf`)。
28
- *
29
- * 面(`list` / `ping` / `orphans` / `detail` / `apply` / `empty`)与 Node 半路由的六个段名一一对应(段名本身的家是
30
- * `src/shared/wire.ts`);`list` 回一行行的条目,`ping` 只回一个探针值,`orphans` 回一份**纯读**的
31
- * 残留扫描报告,`detail` 回单条正文,
32
- * `apply` 回**一组逐条结果**,`empty` 回聚合回执。2026-10-17 之前是六个动作段(一条一段),
33
- * 批量只能由调用方循环拼——那正是「恢复 100 条很慢」的形状来源,收成一段之后这条通道不再有
34
- * "第二条执行路径"。**清理无主子会话也不另开一段**:它是 `apply` 的第五个动作,`orphans` 只负责
35
- * "有多少、在哪、多大"。
4
+ * - 路由是宿主 web server 上的插件前缀;官方 remote.<namespace> 需要生成的 Typert 描述符,
5
+ * 第三方无法动态挂载,因此走这条薄线(ADR-0009)。
6
+ * - 九段:list / ping / orphans / detail / apply / empty / attachment / attachment-orphans / attachment-clean;
7
+ * 写动作统一走 apply;`attachment` 是唯一二进制段,由 `src/client/attachment-view.tsx` 直接拼地址,
8
+ * 不经过本文件的信封通道;`attachment-orphans`(纯读)与 `attachment-clean`(聚合写)是孤儿附件的两段。
9
+ * - 信封与官方 unary 面同形:{ok:true,value} / {ok:false,error}。HTTP 往返本身失败才 reject;
10
+ * 业务失败以信封返回,由 unwrapRemoteResult 在注入面抛出。
11
+ * - error 在线上是 {code,message,details?} 普通对象;判别依据 isDSHRemoteError 住在
12
+ * src/shared/wire.ts,这里不 import 任何宿主客户端包。
36
13
  */
37
- import { type ApplyAction, type ApplyOutcome, type OrphanReportWire, type PurgeStatsWire, type RemoteFailure, type RemoteResult, type TrashDetailWire, type TrashRowWire } from '../shared/wire.js';
14
+ import { type ApplyAction, type ApplyOutcome, type ApplyValueByAction, type AttachmentCleanupWire, type AttachmentOrphanReportWire, type OrphanReportWire, type PurgeStatsWire, type RemoteFailure, type RemoteResult, type TrashDetailWire, type TrashRowWire } from '../shared/wire.js';
38
15
  /**
39
- * 回收站条目(`/list` 那一行)。**形状的家在 `shared/wire.ts`**(ADR-0003):这里只是旧名的别名,
16
+ * 回收站条目(`/list` 那一行)。**形状的家在 `shared/wire.ts`**(ADR-0003):这里只是别名,
40
17
  * 所以浏览器半不再自己抄一遍字段;下面几处"只读面"的镜像同理。
41
18
  */
42
19
  export type TrashEntryLike = TrashRowWire;
@@ -72,11 +49,9 @@ export type RemoteOutcome<T> = {
72
49
  */
73
50
  export declare function callRemote<T>(run: () => Promise<T>): Promise<RemoteOutcome<T>>;
74
51
  /** 本插件前缀路由那套**响应信封**的窄镜像(不是宿主服务 `ctx.remote.*`——那条 typert 通道为什么用不了,见 `AGENTS.md` 的 `remote.<namespace>` 那条):
75
- * 每个方法 resolve 成 `RemoteResult<T>`。
76
- *
77
- * 2026-10-17 批量重构:写动词收成**一个 `apply`**(一段管所有动作、一组 id 进、逐条结果出),
78
- * 再加一段**纯探测** `ping`;2026-10-19 又加一段**纯读扫描** `orphans` ⇒ 面是六段:
79
- * `list` / `ping` / `orphans` / `detail` / `apply` / `empty`(原先六个动作段)。 */
52
+ * 每个方法 resolve 成 `RemoteResult<T>`。九个段名里这层走八个 JSON 段:`list` / `ping` / `orphans` / `detail` / `apply` / `empty` /
53
+ * `attachment-orphans` / `attachment-clean`;写动作统一走 `apply`(一组 id 进、逐条结果出);
54
+ * `/attachment` 是二进制段,由 `src/client/attachment-view.tsx` 直接拼地址。 */
80
55
  export interface RawSessionDeleteRemote {
81
56
  list(): Promise<RemoteResult<TrashEntryLike[]>>;
82
57
  /** 通道的"在不在":一次零成本的 GET(探测专用,见 `src/api/dispatch.ts` 的 `ping` 支)。 */
@@ -84,6 +59,10 @@ export interface RawSessionDeleteRemote {
84
59
  /** 纯读:回收站之外的无主子会话扫描。它**不清理**——清理是 `apply` 的 `orphan-clean` 动作,
85
60
  * 这一段只交回"有多少、在哪、多大"(读不到语料时报 `io`,调用方不许把它折成"零条残留")。 */
86
61
  orphans(): Promise<RemoteResult<OrphanReportWire>>;
62
+ /** 纯读:`.dsh` 内无引用图片对象的数量、字节与预览清单(引用源读不出时报 `io`,不折成 0)。 */
63
+ attachmentOrphans(): Promise<RemoteResult<AttachmentOrphanReportWire>>;
64
+ /** 聚合写:只删上一条报告那一刻无引用的图片对象,回 `AttachmentCleanupWire`。 */
65
+ attachmentClean(): Promise<RemoteResult<AttachmentCleanupWire>>;
87
66
  detail(entryId: string): Promise<RemoteResult<TrashDetailLike>>;
88
67
  /** 一组 id 上的同一个动作。顶层信封的 `ok:false` 只表示**整次调用**没办成(路由自检、栅栏之外);
89
68
  * 单条动作的成败住在 `ApplyOutcome` 里——这是批量接口的判据,别在信封层找它。 */
@@ -93,8 +72,9 @@ export interface RawSessionDeleteRemote {
93
72
  }
94
73
  /**
95
74
  * 前缀路由的 fetch 实现。**相对路径**:不拼 base、不认 origin——页面本身就是从宿主 web server
96
- * 加载的,同源是这条薄线的全部前提(也是它敢不带凭据的原因:宿主那行 URL 上的 browser-trust token
97
- * 不是这条通道的凭据,栅栏在 Node 半那一侧按 Origin/Referer 判)。
75
+ * 加载的,同源是这条薄线的全部前提。本机(含转发落在回环的桌面壳)靠回环 + Origin/Referer 过栅栏;
76
+ * 远程(LAN / 反代)客户端另由宿主 `connection` 判「已认证的 Web 身份」,所以这条通道**只对**
77
+ * `dsh web` 打印的 URL 打开过的页面成立(凭据是那行 URL 换来的 cookie,不是插件自己的秘密)。
98
78
  */
99
79
  export declare function createHttpRemote(prefix?: string): RawSessionDeleteRemote;
100
80
  /**
@@ -104,55 +84,54 @@ export declare function createHttpRemote(prefix?: string): RawSessionDeleteRemot
104
84
  * 错误映射也生效了),那时该做的是把入口显示出来、让原文上屏;只有往返本身失败才算通道缺席 ⇒
105
85
  * 一个槽位贡献都不注册(docs/architecture.md §3「通道探测决定入口出现与否」)。
106
86
  *
107
- * 2026-10-17:探测从"读一遍清单"改成这一问——**每次页面加载都跑全表对账**是白付的钱,
108
- * 而"信封到不到"这件事一次零成本的 GET 就答得出来。
87
+ * 探测用一次零成本的 `GET /ping`,不读清单,避免每次页面加载都跑全表对账。
109
88
  */
110
89
  export declare function probeChannel(raw: RawSessionDeleteRemote): Promise<boolean>;
111
- /** `shared/wire.ts` 的 `ApplyOutcome` 与那份扫描报告从这里转出去——浏览器半只认 `remote.js` 这一扇门。 */
112
- export type { ApplyAction, ApplyOutcome, OrphanReportWire } from '../shared/wire.js';
113
- /** 一段管所有写动作的返回值(逐条)。`ApplySuccess<A>` = 某动作成功支(`value` 按动作收窄)。 */
114
- export type ApplySuccess<A extends ApplyAction> = Extract<ApplyOutcome, {
115
- action: A;
116
- ok: true;
117
- }>;
90
+ /** `shared/wire.ts` 的 `ApplyOutcome`、扫描报告与附件回执从这里转出去——浏览器半只认 `remote.js` 这一扇门。 */
91
+ export type { ApplyAction, ApplyOutcome, AttachmentCleanupWire, AttachmentOrphanReportWire, OrphanReportWire, } from '../shared/wire.js';
118
92
  /**
119
- * N=1 的成功支:`delete` / `restore` 的幂等 `null` 已被 `applyOne` 判成通道错,
120
- * 所以它的 `value` 必然非空;其余动作就是 `ApplySuccess`。
93
+ * N=1 的成功支:`null` 已被 `receipt.ts` 的单条读法折成协议错,所以 `value` 必然非空。
94
+ * 不再需要条件类型把 `delete` / `restore` 的 null 抠掉——那份判据只住回执读法一处。
121
95
  */
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>;
96
+ export type OneShotSuccess<A extends ApplyAction> = {
97
+ readonly id: string;
98
+ readonly action: A;
99
+ readonly ok: true;
100
+ readonly value: Exclude<ApplyValueByAction[A], null>;
101
+ };
125
102
  /**
126
- * 注入面(`deps`)的 interface:**与六个段名一一对应的五个方法**(`ping` 只给探测用,留在 raw 层)。
103
+ * 注入面(`deps`)的 interface:**七个方法**(raw 层另有 `ping` 供探测;九段里的 `/attachment` 由
104
+ * `src/client/attachment-view.tsx` 直接拼地址,也不在这层)。
127
105
  *
128
106
  * **不再有 N=1 的便捷方法**:`delete` / `restore` / `countSubagents` 曾是同一段
129
107
  * `/apply` 的三种写法,于是一个 `module` 里同一个动作有两条路(回收站面板的 deps 同时躺着
130
- * `applyOutcomes` 与 `restore`)。现在它们收成一个自由函数 {@link applyOne}——要旧形状就显式调它,
131
- * 而不是让 interface 多背三个方法。
108
+ * `applyOutcomes` 与 `restore`)。现在单条非写调用(如 `count`)走自由函数 {@link applyOne};
109
+ * 写动作走 `src/client/write.ts` 的 `runWrite`,而不是让 interface 多背三个方法。
132
110
  */
133
111
  export interface SessionDeleteRemote {
134
112
  list(): Promise<TrashEntryLike[]>;
135
113
  /** 回收站之外的无主子会话报告(纯读)。读不到就是 throw —— 界面上那是**一次失败的检查**,
136
114
  * 不是"零条残留"(把 `io` 折成空报告等于把"不知道"说成"没有")。 */
137
115
  scanOrphans(): Promise<OrphanReportWire>;
116
+ /** `.dsh` 内无引用图片对象的报告(纯读;含最多 200 条预览清单)。读不到就是 throw,同样不许折成 0。 */
117
+ scanAttachmentOrphans(): Promise<AttachmentOrphanReportWire>;
118
+ /** 清掉报告那一刻无引用的图片副本(聚合回执)。 */
119
+ cleanAttachmentOrphans(): Promise<AttachmentCleanupWire>;
138
120
  detail(entryId: string): Promise<TrashDetailLike>;
139
- /** 一段管所有写动作(2026-10-17 批量重构):一次调用把整批办完,逐条结果原样交回组件。 */
121
+ /** 一段管所有写动作:一次调用把整批办完,逐条结果原样交回组件。 */
140
122
  applyOutcomes(action: ApplyAction, ids: readonly string[]): Promise<ApplyOutcome[]>;
141
123
  /** 清空回收站(聚合回执)。 */
142
124
  emptyTrash(): Promise<PurgeResultLike>;
143
125
  }
144
- /** 需要"单条便捷支"的组件只要这一段 deps(同一段 `/apply` 的 N=1 写法,见 {@link applyOne})。 */
126
+ /** 需要那一段 `/apply` 的组件只要这一段 deps(单条非写调用见 {@link applyOne},写动作走 `./write.js` 的 `runWrite`)。 */
145
127
  export type OneShotDeps = Pick<SessionDeleteRemote, 'applyOutcomes'>;
146
128
  /**
147
- * **N=1 的 `apply`**:一个动作 + 一个 id → 那一条的成功支(`value` 按动作收窄)。
129
+ * **N=1 的 `apply`**:一个动作 + 一个 id → 那一条的成功支(`value` 必然非空)。
148
130
  *
149
- * 三条判据都在这一处:
131
+ * 三件事各归一个家:
150
132
  * - **按发出去的那个 id** 取结果(取"第一条"会把别条的成败安在这条头上);
151
- * - 一条没回是**通道的错**,不是"没做成"(后者住在 outcome 的 `ok:false` 里);
152
- * - `null` 是批量里"已被同批前一条带走"的幂等值——N=1 永远不会命中它,回 `null` 只能是通道答错了,
153
- * 所以同样按通道错抛,不冒充成功。
154
- *
155
- * 成/败/幂等/缺失的判读只走 {@link readReceipt}(一个家)。
133
+ * - 成/败/幂等/缺失与「单条 null 是协议错」的判读只走 {@link readSingleReceipt};
134
+ * - 这里只把 verdict 折成「拿到值 / 抛失败」。
156
135
  */
157
136
  export declare function applyOne<A extends ApplyAction>(deps: OneShotDeps, action: A, id: string): Promise<OneShotSuccess<A>>;
158
137
  /**
@@ -163,5 +142,6 @@ export declare function applyOne<A extends ApplyAction>(deps: OneShotDeps, actio
163
142
  * 所以在这里展开一次:`ok ? value : throw`。
164
143
  */
165
144
  export declare function buildDeps(raw: RawSessionDeleteRemote): SessionDeleteRemote;
166
- /** 「删除」确认框的宿主只需要那一段 `/apply`:提交删除与删除前那一次计数都走 {@link applyOne}。 */
145
+ /** 「删除」确认框的宿主只需要那一段 `/apply`:删除前那一次计数走 {@link applyOne};
146
+ * 提交删除走 `src/client/write.ts` 的 `runWrite`。 */
167
147
  export type DeleteConfirmDeps = OneShotDeps;
@@ -15,3 +15,34 @@ export interface ClientStore<T> {
15
15
  reset(): void;
16
16
  }
17
17
  export declare function createStore<T>(initial: T): ClientStore<T>;
18
+ /** 懒读快照:`get()` 只在脏了时调用 `read()`,干净时回同一个引用(React snapshot 契约)。 */
19
+ export interface LazyStore<T> {
20
+ get(): T;
21
+ /** 标脏并通告;下一次 `get()` 重读。 */
22
+ invalidate(): void;
23
+ subscribe(listener: () => void): () => void;
24
+ }
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>;
32
+ /**
33
+ * 最新票句柄:每张票记着签发时的序号,只有序号仍等于当前的那张才 `current`。
34
+ * 调用方在发起异步读时 `issue()` 一张票,落地前问 `ticket.current`——旧读因此不会盖掉新状态。
35
+ *
36
+ * 组件里要拿到**同一枚**句柄时用 `useState(createLatestWins)`,不要用 `useMemo`:React 允许丢 memo
37
+ * 缓存,丢了就是两张各自记数的票台,守卫静默失效。模块级现场(`main-view.ts`)直接建一颗,没这个问题。
38
+ */
39
+ export interface LatestWinsTicket {
40
+ readonly current: boolean;
41
+ }
42
+ export interface LatestWins {
43
+ /** 签发一张新票;之前的票立刻作废。 */
44
+ issue(): LatestWinsTicket;
45
+ /** 只作废当前票,不签发新票(用于"这一拍整体作废")。 */
46
+ invalidate(): void;
47
+ }
48
+ export declare function createLatestWins(): LatestWins;
@@ -1,19 +1,6 @@
1
1
  import { type Translate } from './copy.js';
2
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
- };
3
+ import type { PendingAction } from './use-trash-panel-controller.js';
17
4
  /**
18
5
  * 两种批量动作共用这一个确认框壳:`purge`(不可恢复)与 `restore`(加法,但一批条目多、去向是
19
6
  * 各自的旧项目目录,点之前也要过一道)。标题、正文、确认键的措辞各处一套;批量恢复把选中条目
@@ -29,7 +16,7 @@ export declare function TrashConfirm({ pending, restoreEntries, busy, t, onCance
29
16
  onConfirmPurge: () => void;
30
17
  onConfirmRestore: (ids: readonly string[]) => void;
31
18
  }): import("react").JSX.Element;
32
- /** 无主子会话的清理确认框(2026-10-19)。载荷是刚扫出来的那份报告,界面不重算。 */
19
+ /** 无主子会话的清理确认框。载荷是刚扫出来的那份报告,界面不重算。 */
33
20
  export declare function OrphanConfirm({ report, busy, t, onCancel, onConfirm, }: {
34
21
  report: OrphanReportWire;
35
22
  busy: boolean;
@@ -0,0 +1,30 @@
1
+ import type { KeyboardEvent as ReactKeyboardEvent } from 'react';
2
+ import type { TrashEntryLike } from './remote.js';
3
+ /**
4
+ * 导航要的面。`ordered` 是**屏幕上的顺序**(只含展开的组),由面板/选择面算好传进来——
5
+ * 布局常量与本 module 无关。`drillEnabled` 是这一档支不支持整行下钻(窄档为真;宽档 Enter/Space
6
+ * 只移动选中,与整行点击的 `{ drill: !split }` 同一条判据)。四个动作都由调用方注入:
7
+ * `select` 只移动选中(带 `drill` 才下钻),`exitDrill` 是控制器那条「← 返回清单」的同一动作,
8
+ * `focusRow` 把焦点交给某一行。
9
+ */
10
+ export interface TrashNavInput {
11
+ ordered: readonly TrashEntryLike[];
12
+ selectedId: string | null;
13
+ selectMode: boolean;
14
+ drillEnabled: boolean;
15
+ drilled: boolean;
16
+ select(entry: TrashEntryLike, options?: {
17
+ drill?: boolean;
18
+ }): Promise<void>;
19
+ toggleCheck(entryId: string): void;
20
+ exitDrill(): void;
21
+ focusRow(entryId: string): void;
22
+ }
23
+ export interface TrashNav {
24
+ /** `aria-activedescendant` 的那条 id:选中行不在 DOM 里时是 `null`。 */
25
+ readonly activeDescendantId: string | null;
26
+ /** 清单里唯一拿着 `tabIndex=0` 的那一行;清单空时是 `null`。 */
27
+ readonly tabStopId: string | null;
28
+ onListKeyDown(event: ReactKeyboardEvent<HTMLElement>): void;
29
+ }
30
+ export declare function useTrashNav(input: TrashNavInput): TrashNav;
@@ -1,9 +1,10 @@
1
1
  import { type Translate } from './copy.js';
2
2
  import { type SessionDeleteRemote } from './remote.js';
3
- /** 面板的依赖面:三个读(`list` / `detail` / `scanOrphans`)+ **一段 `apply`** + 清空。
3
+ /** 面板的依赖面:两个读(`list` / `detail`)+ **一段 `apply`** + 清空。
4
4
  * **没有第二条路**:「恢复一条」也走 `apply`(N=1),面板不再同时吃
5
- * `applyOutcomes` 与一个便捷的 `restore`。 `scanOrphans` 是回收站之外那批无主子会话的纯读报告。 */
6
- export type TrashPanelDeps = Pick<SessionDeleteRemote, 'list' | 'detail' | 'applyOutcomes' | 'emptyTrash' | 'scanOrphans'>;
5
+ * `applyOutcomes` 与一个便捷的 `restore`。无主子会话的检查/清理不在这里——它是插件设置页
6
+ * 维护卡的手动动作(`use-maintenance.ts`),面板一次都不自动扫(ADR-0021)。 */
7
+ export type TrashPanelDeps = Pick<SessionDeleteRemote, 'list' | 'detail' | 'applyOutcomes' | 'emptyTrash'>;
7
8
  export interface TrashPanelProps {
8
9
  deps: TrashPanelDeps;
9
10
  /** 文案(缺省 = 内置 zh,见 `copy.ts`:这不是"忘了传",而是"这次没有 locale 服务")。 */
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 勾选框(2026-10-15 加,回收站多选态用)。
2
+ * 勾选框,回收站多选态用。
3
3
  *
4
4
  * 样式与行为抄自 `@deepseek-ai/dsh-client-ui-primitives` 的 `lib/Checkbox.module.css`
5
5
  * (那四条声明逐字进了 `ui.css` 的 `.sd-checkbox`):**本体是原生 `input[type=checkbox]` +
@@ -14,8 +14,8 @@
14
14
  * - **官方的多层栈**(`layers` WeakMap + `closeTopModal` / `isBehindModal`):那是给"对话框里还能再开
15
15
  * 对话框、且宿主菜单也在抢 Escape"的场景准备的。本插件同一时刻只会有一个确认框,所以这里只保留它
16
16
  * 的**判据本身**——Escape 只在"自己就是文档里最后一个 `[role="dialog"][aria-modal="true"]`
17
- * 或 `[role="menu"]`"时生效(`isTopLayer`)。这一条不能省:菜单/对话框先后打开时,
18
- * "谁在最上面谁收 Escape"是用户能看见的规矩。
17
+ * 或 `[role="menu"]`"时生效(`isTopLayer`,名单与判据住 `ui/top-layer.ts`)。这一条不能省:
18
+ * 菜单/对话框先后打开时,"谁在最上面谁收 Escape"是用户能看见的规矩。
19
19
  * - **输入法合成守卫**(`observeComposition`):本插件的对话框里没有文本输入框,不可能处在合成态。
20
20
  */
21
21
  import { type ReactElement, type ReactNode } from 'react';
@@ -1,18 +1,13 @@
1
1
  /**
2
- * 本插件 UI 层要用的图标(**逐条抄自** `@deepseek-ai/dsh-client-ui-primitives@0.2.0-rc.2`
3
- * 随包发布的明文产物 `lib/index.js`;出处与改写规则见 `ui.css` 头注)。
2
+ * 本插件 UI 层要用的图标(逐条抄自 `@deepseek-ai/dsh-client-ui-primitives` 的明文产物;
3
+ * 出处与改写规则见 `ui.css` 头注)。
4
4
  *
5
- * 只留本插件用到的:垃圾桶(删除动作 / 回收站)、警示(失败面)、打开的项目文件夹、
6
- * 关闭叉(对话框)、圆圈对勾(撤销提示的 success 档)、对勾(行尾兜底勾选开关,2026-10-17 加;
7
- * 行首主勾选走原生 Checkbox)、下折与右折 chevron(清单的分组折叠头,
8
- * 2026-10-15 加:`DisclosureRow` 那套折叠行的两态 glyph)。「刷新」那枚随按钮一起删了
9
- * (2026-10-16,见 `trash-panel.tsx` 工具排的注释)——本文件不留无人消费的几何。
5
+ * 只留本插件用到的:垃圾桶、警示、打开的项目文件夹、关闭叉、圆圈对勾、对勾、下折/右折 chevron。
10
6
  * **几何、`viewBox`、`strokeWidth` 逐字不动**——它们是官方图标集的形状,改了就不再与宿主同屏同形。
11
7
  *
12
8
  * 两条与官方实现的差异(都是减法,不是改写):
13
- * - 只暴露 `size`(官方是 `{size, className, strokeWidth}` 三件套 + `Medium` 1.3px 那一档):
14
- * 本插件调用处只传 `size`,加别的等于摊开官方整个图标 API。
15
- * - 不做 `ICON_REGULAR_STROKE` / `ICON_MEDIUM_STROKE` 常量:只有一个档位,直接写 1。
9
+ * - 只暴露 `size`:调用处只传 `size`,不摊开官方整个图标 API。
10
+ * - 不做 stroke 常量:只有一个档位,直接写 1。
16
11
  */
17
12
  import type { ReactElement } from 'react';
18
13
  /** 宿主图标组件的实际使用面:一个方形边长,色走 `currentColor`。 */
@@ -25,7 +20,7 @@ export declare function IconFolderOpenOutlineRegular({ size }: IconProps): React
25
20
  export declare function IconCloseOutlineRegular({ size }: IconProps): ReactElement;
26
21
  /** 撤销提示 success 档的前导 glyph(官方 `Toast` 在 `tone="success"` 时自带的那一枚)。 */
27
22
  export declare function IconCheckCircleOutlineRegular({ size }: IconProps): ReactElement;
28
- /** 行尾兜底「勾选开关」那枚对勾(状态点/归档行;官方 `IconCheckOutlineRegular`,`viewBox`/`d` 逐字;2026-10-17 加)。 */
23
+ /** 行尾兜底勾选开关那枚对勾(状态点/归档行;官方几何逐字)。 */
29
24
  export declare function IconCheckOutlineRegular({ size }: IconProps): ReactElement;
30
25
  /** 分组头「展开中」那一枚(官方 `IconChevronDownOutlineRegular`,`viewBox`/`d` 逐字)。 */
31
26
  export declare function IconChevronDownOutlineRegular({ size }: IconProps): ReactElement;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * 「谁在最上层收 Escape」的名单与判据。`Modal` 的关闭键和清单的 Esc 退下钻共用这一份:
3
+ * 两份各写一次选择器,菜单/对话框换代时必然只剩一处跟上。
4
+ *
5
+ * 名单与官方 `modalSelector` 同形:模态对话框 + 菜单。文档序里最后一个是「最上层」。
6
+ */
7
+ /** 文档里是否还有打开的顶层浮层。清单只问「有没有」,它自己不是其中一员。 */
8
+ export declare function hasTopLayer(doc: Document): boolean;
9
+ /** 自己是不是文档里最上面的那一层。Modal 只在这时收 Escape。 */
10
+ export declare function isTopLayer(element: HTMLElement): boolean;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * 一次待撤销的事。两形:单条删除(主语是会话名)/批量删除(主语是条数)。
3
+ * 同一个 Toast 宿主、同一停留时长;撤销动作按形分派(单条 / 批量),两条都走同一个外部恢复座位
4
+ * (`write-seats.ts` 的 `restoreFromUndo`),单条带 `single: true` 的读法(null 是协议错,不冒充幂等成功)。
5
+ */
6
+ export type UndoNotice = {
7
+ /** 每次发布递增:Toast 靠它重挂载,同一段文案连续出现两次也能重新计一次停留。 */
8
+ seq: number;
9
+ /** 撤销失败时把原因顶上来:这时不再有「撤销」动作,只剩原文。 */
10
+ failure?: string;
11
+ } & ({
12
+ kind: 'one';
13
+ /** 回收站条目 id——撤销就是拿它去 `restore`。 */
14
+ entryId: string;
15
+ /** 删除那一刻的会话名(菜单槽投影的 `displayTitle`;取不到时是空串)。 */
16
+ title: string;
17
+ } | {
18
+ kind: 'batch';
19
+ /** 撤销名单:只收成功支里非 null 的条目 id(「被同批带走」的成员跟着组走,不单列)。 */
20
+ entryIds: readonly string[];
21
+ /** 横幅那句的条数 = 那一次成功支的条数(含幂等带走的)——与撤销名单是两个数,各说各的。 */
22
+ n: number;
23
+ });
24
+ /** 删除成功后发布一条撤销提示(单条)。 */
25
+ export declare function publishUndoNotice(input: {
26
+ entryId: string;
27
+ title: string;
28
+ }): void;
29
+ /** 批量删除成功后发布一条撤销提示:覆盖上一条,只报这一次移走的条数。 */
30
+ export declare function publishUndoBatchNotice(input: {
31
+ entryIds: readonly string[];
32
+ n: number;
33
+ }): void;
34
+ /** 整条横幅不再留:撤销成了、停留时间走完,或清空回收站把所有落点一起清了。
35
+ * 与 `dismissUndoNotice` 的分别是「整条不再留」与「按条目匹配」——清空之后没有条目还能撤销,
36
+ * 所以那一路不需要名单。 */
37
+ export declare function clearUndoNotice(): void;
38
+ /**
39
+ * 某个条目已经在别处恢复:把指向它的横幅熄掉。
40
+ * 批量里任一命中就清整条——名单少了、计数没变,留着就是一条会骗人的「撤销」动作。
41
+ */
42
+ export declare function dismissUndoNotice(entryIds: readonly string[]): void;
43
+ /** 供组件订阅。 */
44
+ export declare function useUndoNotice(): UndoNotice | null;
45
+ /** 测试专用:现场与 seq 归零,用例之间互不串味。 */
46
+ export declare function resetUndoNotice(): void;
47
+ /** 提示停留时长。**6 秒** 定死在本行(比 Toast 默认的 3 秒长)——`docs/ui-spec.md` §2 状态 ③ 只指到这里,不抄这个数。 */
48
+ export declare const UNDO_HOLD_MS = 6000;
49
+ /** 这次撤销开始时的那条横幅还在吗——期间被新删除覆盖、或由面板恢复熄灭时,旧请求不许改写它。 */
50
+ export declare function ownsNotice(current: UndoNotice): boolean;
51
+ /** 撤销失败:把原因换到同一条横幅上(seq 递增 ⇒ Toast 重挂载、重新计停留)。 */
52
+ export declare function failNotice(current: UndoNotice, message: string): void;