@xrn1997/dsh-session-delete 0.3.1 → 0.5.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 (83) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +141 -129
  3. package/cordis.patch.yml +3 -3
  4. package/lib/client.js +2811 -1264
  5. package/lib/index.js +2368 -1277
  6. package/lib/types/api/attachment-orphans.d.ts +11 -0
  7. package/lib/types/api/attachment.d.ts +23 -0
  8. package/lib/types/api/dispatch.d.ts +13 -29
  9. package/lib/types/api/request-trust.d.ts +45 -0
  10. package/lib/types/api/route-table.d.ts +52 -18
  11. package/lib/types/api/trash-rows.d.ts +3 -3
  12. package/lib/types/client/attachment-view.d.ts +27 -0
  13. package/lib/types/client/batch-bar.d.ts +1 -1
  14. package/lib/types/client/batch-confirm.d.ts +2 -2
  15. package/lib/types/client/batch-menu-item.d.ts +1 -1
  16. package/lib/types/client/batch-select.d.ts +1 -1
  17. package/lib/types/client/busy-gate.d.ts +6 -2
  18. package/lib/types/client/confirm-close.d.ts +14 -0
  19. package/lib/types/client/context.d.ts +3 -2
  20. package/lib/types/client/copy.d.ts +102 -42
  21. package/lib/types/client/delete-confirm.d.ts +1 -3
  22. package/lib/types/client/delete-menu-item.d.ts +1 -1
  23. package/lib/types/client/host-rows.d.ts +5 -1
  24. package/lib/types/client/main-view.d.ts +1 -1
  25. package/lib/types/client/maintenance-parts.d.ts +25 -0
  26. package/lib/types/client/maintenance-settings.d.ts +22 -0
  27. package/lib/types/client/markdown.d.ts +9 -2
  28. package/lib/types/client/orphan-maintenance.d.ts +16 -13
  29. package/lib/types/client/receipt.d.ts +44 -2
  30. package/lib/types/client/remote.d.ts +38 -50
  31. package/lib/types/client/store.d.ts +14 -3
  32. package/lib/types/client/trash-confirm.d.ts +2 -15
  33. package/lib/types/client/trash-detail.d.ts +48 -6
  34. package/lib/types/client/trash-entry.d.ts +1 -1
  35. package/lib/types/client/trash-nav.d.ts +12 -8
  36. package/lib/types/client/trash-panel.d.ts +4 -3
  37. package/lib/types/client/ui/Checkbox.d.ts +1 -1
  38. package/lib/types/client/ui/Modal.d.ts +2 -2
  39. package/lib/types/client/ui/Toast.d.ts +3 -1
  40. package/lib/types/client/ui/icons.d.ts +7 -12
  41. package/lib/types/client/ui/top-layer.d.ts +10 -0
  42. package/lib/types/client/undo-notices.d.ts +52 -0
  43. package/lib/types/client/undo-toast.d.ts +6 -59
  44. package/lib/types/client/use-column-resize.d.ts +9 -20
  45. package/lib/types/client/use-maintenance-section.d.ts +52 -0
  46. package/lib/types/client/use-maintenance.d.ts +17 -0
  47. package/lib/types/client/use-panel-width.d.ts +1 -1
  48. package/lib/types/client/use-trash-panel-controller.d.ts +54 -16
  49. package/lib/types/client/use-trash-query.d.ts +39 -34
  50. package/lib/types/client/use-trash-selection.d.ts +15 -12
  51. package/lib/types/client/write-seats.d.ts +36 -28
  52. package/lib/types/client/write-settlement.d.ts +11 -17
  53. package/lib/types/client/write.d.ts +21 -8
  54. package/lib/types/deps.d.ts +18 -7
  55. package/lib/types/domain/closure.d.ts +44 -0
  56. package/lib/types/index.d.ts +6 -2
  57. package/lib/types/log/session-log.d.ts +4 -3
  58. package/lib/types/log/zstd-frames.d.ts +13 -8
  59. package/lib/types/ports/agent-handles.d.ts +1 -1
  60. package/lib/types/ports/connection-port.d.ts +33 -0
  61. package/lib/types/ports/fs-port.d.ts +24 -9
  62. package/lib/types/ports/host-port.d.ts +21 -50
  63. package/lib/types/ports/presence-port.d.ts +31 -0
  64. package/lib/types/shared/wire.d.ts +175 -38
  65. package/lib/types/trash/attachments.d.ts +92 -0
  66. package/lib/types/trash/detail.d.ts +46 -16
  67. package/lib/types/trash/entry.d.ts +10 -4
  68. package/lib/types/trash/groups.d.ts +9 -11
  69. package/lib/types/trash/manifest.d.ts +4 -4
  70. package/lib/types/trash/storage-manifest.d.ts +1 -1
  71. package/lib/types/verbs/batch-carried.d.ts +1 -3
  72. package/lib/types/verbs/delete-session.d.ts +2 -2
  73. package/lib/types/verbs/errors.d.ts +2 -0
  74. package/lib/types/verbs/live-admission.d.ts +4 -7
  75. package/lib/types/verbs/member-journal.d.ts +3 -4
  76. package/lib/types/verbs/member-presence.d.ts +16 -37
  77. package/lib/types/verbs/move-group.d.ts +2 -1
  78. package/lib/types/verbs/orphan-subagents.d.ts +19 -12
  79. package/lib/types/verbs/permanent-removal.d.ts +60 -25
  80. package/lib/types/verbs/restore-session.d.ts +7 -13
  81. package/lib/types/verbs/trash-scope.d.ts +36 -0
  82. package/package.json +117 -106
  83. package/lib/types/verbs/purge-trash.d.ts +0 -30
@@ -8,7 +8,7 @@
8
8
  * 清掉——**除非把常驻实例本身丢掉**。宿主自己的归档走的就是这条路:`clearArchivedCurrent()`
9
9
  * 发现当前会话进了归档集就 `clearMain()`(`dsh-client-ui-workspace/lib/client.js`)。
10
10
  *
11
- * 所以关闭/重开是**一段生命周期**(2026-10-21),不是一个 id:删除成功后 `afterDelete` 关掉当前
11
+ * 所以关闭/重开是**一段生命周期**,不是一个 id:删除成功后 `afterDelete` 关掉当前
12
12
  * 主视图并记下"这一条是我们关掉的";撤销成功后 `afterRestore` 只重开自己记过的那些。调用方不再
13
13
  * 各自判定该不该关、也不再把会话 id 塞进撤销提示传一圈——判据全在这一处。两次重开排队时只有最新
14
14
  * 那条会打开(最新票守卫),`resetMainView()` 清掉这份记忆并让在等的重开作废。
@@ -0,0 +1,25 @@
1
+ /**
2
+ * 维护卡两段共用的两块皮:警告条外壳与失败块。
3
+ *
4
+ * 两段(孤儿会话 / 孤儿附件)在结构上只差正文:图标、底色、动作按钮与失败块的「原文 + 重试」
5
+ * 都一样。收在这里,改布局或按钮材质时不必在同一条卡里改两遍;正文仍由各段自己给。
6
+ */
7
+ import type { ReactElement, ReactNode } from 'react';
8
+ import type { Translate } from './copy.js';
9
+ interface MaintenanceChromeProps {
10
+ /** 给测试与界面规格一个稳定的块名;缺省不写 `data-*`。 */
11
+ slot?: string;
12
+ busy: boolean;
13
+ t: Translate;
14
+ /** 旧面板块用过的下边距;设置页段内间距由父级 gap 给,默认不加。 */
15
+ gapBelow?: boolean;
16
+ }
17
+ export declare function MaintenanceBanner({ slot, busy, t, gapBelow, onOpen, children, }: MaintenanceChromeProps & {
18
+ onOpen: () => void;
19
+ children: ReactNode;
20
+ }): ReactElement;
21
+ export declare function MaintenanceFailure({ slot, message, busy, t, gapBelow, onRetry, }: MaintenanceChromeProps & {
22
+ message: string;
23
+ onRetry: () => void;
24
+ }): ReactElement;
25
+ export {};
@@ -0,0 +1,22 @@
1
+ /**
2
+ * 插件设置页的「维护」卡(`plugins.bundle.config` 的占用者)。
3
+ *
4
+ * 两张段都是**手动**的:检查 → 预览 → 清理,没有挂载即扫、没有定时器。卡片的职责只有呈现:
5
+ * 状态那一行、检查按钮、以及各段自己的横幅 / 失败 / 提示 / 确认框;读数与动作全归
6
+ * `src/client/use-maintenance.ts`。
7
+ *
8
+ * 与回收站面板的区别:这里没有面板高度、没有清单与详情两栏、没有全局闸。控件只用
9
+ * `CARD` / `Button` / `Modal` / `fileSizeText`,所以同一张卡放在插件详情页的任意宽度下都成立。
10
+ *
11
+ * 孤儿会话段直接复用 `OrphanMaintenance`(警告条 / 失败 / 中性提示 / 预览确认的既有呈现);
12
+ * 孤儿附件段是本文件里的 `AttachmentMaintenance`(横幅 + 失败 + 回执 + 缩略图预览与清理确认)。
13
+ */
14
+ import type { ReactElement } from 'react';
15
+ import { type Translate } from './copy.js';
16
+ import { type MaintenanceDeps } from './use-maintenance.js';
17
+ export interface MaintenanceSettingsProps {
18
+ deps: MaintenanceDeps;
19
+ /** 文案(缺省 = 内置 zh;宿主 locale 服务缺席时也是它,见 `copy.ts`)。 */
20
+ t?: Translate;
21
+ }
22
+ export declare function MaintenanceSettings({ deps, t }: MaintenanceSettingsProps): ReactElement;
@@ -1,11 +1,18 @@
1
+ import type { Translate } from './copy.js';
1
2
  import './ui/styles.js';
2
3
  /**
3
4
  * `memo` 是必需的而不是优化:`react-markdown@10.1.0` 里**一处 memoization 都没有**(读实
4
- * `node_modules/react-markdown/lib/index.js:175-178`——`Markdown()` 每次调用都 `createProcessor(options)`
5
+ * `node_modules/react-markdown/lib/index.js`——`Markdown()` 每次调用都 `createProcessor(options)`
5
6
  * 再 `runSync(parse(file))`,`MarkdownHooks` 那一支也一样),而面板任何一次重渲(悬停、↑↓、
6
7
  * 「刚刚删除」那句要读 `Date.now()`)都会走到这里 ⇒ 不按住就是每条助手回复每拍重解析一遍。
7
- * 按住的条件就只有 `text`:它是这里唯一的输入,样式挂在 `.sd-md` 上、不随渲染变。
8
+ * 按住的条件是 `text` 与**探针 `probe`**:一句按当前 locale 现算的译文,专给 comparator 当
9
+ * 「换语言了没」的读数。为什么必须渲染时现算存进 props、为什么宿主 `t` 只能当稳定身份而不是
10
+ * 语言读数、「借哪句」,这些都住 `copy.ts` 的 `localeProbe`——调用方与测试一律
11
+ * `probe={localeProbe(t)}`,「借哪句」的知识不散到各调用方。
12
+ * 探针没变 = 同语言同 `t` ⇒ 跳过重解析;变了(换语言或换 `t`)⇒ 重解析。
8
13
  */
9
14
  export declare const MarkdownBody: import("react").NamedExoticComponent<{
10
15
  text: string;
16
+ t: Translate;
17
+ probe: string;
11
18
  }>;
@@ -1,27 +1,30 @@
1
1
  /**
2
- * 无主子会话的**维护块**——警告条、失败块、中性提示与清理确认框,回收站面板上唯一的维护块 seam。
2
+ * 无主子会话的**维护块**——警告条、失败块、中性提示与清理确认框。
3
3
  *
4
- * 为什么收回一个 module:这三段与面板的清单渲染、选择、键盘、拖栏毫无关系,却同住回收站最大的那个文件;
5
- * 改维护块的文案或确认框时,diff 会与布局改动互相踩。搬出来之后面板只留一个插入点
6
- * (`<OrphanMaintenance />`,在 header 之后、toolbar 之前),本 module 的 interface 就是那四个读数与四个动作。
4
+ * 它先是回收站面板上的一块,2026-10-27 起搬进插件设置页的「维护」卡(面板不再自动扫孤儿,ADR-0021)。
5
+ * 之所以仍收成一个 module:这三段与任何一面的清单渲染、选择、键盘、拖栏都毫无关系;改维护块的文案或
6
+ * 确认框时,diff 不该与布局改动互相踩。它只吃泛型段 module 的一张视图 `MaintenanceSectionView`——
7
+ * 报告、三条提示、确认框与四个动作;实现方是 `use-maintenance-section.ts` 的状态机
8
+ * (设置页的 `use-maintenance.ts` 为孤儿会话段实例化它)。
7
9
  *
8
- * 搬字时一处未改的三条界面规则(现状住 `docs/ui-spec.md`):
10
+ * 三条界面规则(现状住 `docs/ui-spec.md`):
9
11
  * - **报告非空才出警告条**:空报告与 `null` 都不出现——"检查过了,什么都没有"不是要给用户看的东西,
10
12
  * 而"还没扫到"更不是"零条"。
11
13
  * - **读不到就说读不到**:检查/清理失败在原位摆原文 + 重试,绝不静默折成一条都不出现
12
14
  * ("没能回答有多少残留"与"没有残留"是两件事,不许折成空报告)。
13
- * - **「已经不在」不是失败**:别处清掉或父已回到清册时单独一条中性提示,不计入失败,下次扫描/刷新时自然消失。
15
+ * - **「已经不在」不是失败**:别处清掉或父已回到清册时单独一条中性提示,不计入失败,下次检查时自然消失。
14
16
  *
15
- * 写动作仍走 `use-trash-query.ts` 那一条:警告条开预览(`openOrphanPreview`)、失败块重试(`refreshAll`)、
16
- * 确认框清理(`cleanOrphans`)。`busy` 不归本 module 管——它是面板那枚全局闸的读数,通过 props 传进来。
17
+ * 四个动作都从段上取:警告条开预览(`openPreview`,唯一开门、先清旧提示)、失败块重试(`scan`)、
18
+ * 确认框清理(`clean`)、取消(`cancel`,只收框)。`busy` 也不再是 prop——段自己给(在飞即忙),
19
+ * 本块只读它按住按钮。
17
20
  */
18
21
  import type { ReactElement } from 'react';
19
22
  import type { Translate } from './copy.js';
20
- import type { TrashQuery } from './use-trash-query.js';
23
+ import type { OrphanReportWire } from './remote.js';
24
+ import type { MaintenanceSectionView } from './use-maintenance-section.js';
21
25
  export interface OrphanMaintenanceProps {
22
- query: Pick<TrashQuery, 'orphanReport' | 'orphanFailure' | 'orphanNotice' | 'orphanConfirm' | 'setOrphanConfirm' | 'openOrphanPreview' | 'cleanOrphans' | 'refreshAll'>;
23
- /** 面板那枚全局闸的读数:写动作在飞时按住本块里的每一颗按钮。 */
24
- busy: boolean;
26
+ /** 段视图:读数、提示、确认框、忙碌与四个动作都在它上面,本块不多要也不少读。 */
27
+ section: MaintenanceSectionView<OrphanReportWire>;
25
28
  t: Translate;
26
29
  }
27
- export declare function OrphanMaintenance({ query, busy, t }: OrphanMaintenanceProps): ReactElement | null;
30
+ export declare function OrphanMaintenance({ section, t }: OrphanMaintenanceProps): ReactElement | null;
@@ -1,4 +1,5 @@
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
5
  * 缺 id 是通道错误,`null` 是幂等成功,orphan-clean 的 `not-found` 是中性提示而非失败,
@@ -31,6 +32,8 @@ export type ReceiptVerdict<A extends ApplyAction> = {
31
32
  readonly message: string;
32
33
  };
33
34
  export interface Receipt<A extends ApplyAction> {
35
+ /** 产生这份回执的动作:摘要据此只派发该动作真的有的那几种落地 id。 */
36
+ readonly action: A;
34
37
  readonly verdicts: readonly ReceiptVerdict<A>[];
35
38
  readonly byId: ReadonlyMap<string, ReceiptVerdict<A>>;
36
39
  }
@@ -52,7 +55,6 @@ export declare function missingOutcomeMessage(action: ApplyAction, id: string):
52
55
  export type FailureVerdict<A extends ApplyAction> = Extract<ReceiptVerdict<A>, {
53
56
  kind: 'failed' | 'neutral' | 'missing';
54
57
  }>;
55
- export declare function isFailureVerdict<A extends ApplyAction>(verdict: ReceiptVerdict<A>): verdict is FailureVerdict<A>;
56
58
  /** failed / neutral 取服务端原文 `error.message`;missing 取通道自己写的那句。 */
57
59
  export declare function failureMessage<A extends ApplyAction>(verdict: FailureVerdict<A>): string;
58
60
  export declare function readReceipt<A extends ApplyAction>(action: A, requestedIds: readonly string[], outcomes: readonly ApplyOutcome[], options?: ReceiptOptions): Receipt<A>;
@@ -61,6 +63,46 @@ export declare function readReceipt<A extends ApplyAction>(action: A, requestedI
61
63
  * single 读法运行期不会产出它)。`applyOne` 用它之后只剩「按 id 取一条 + 抛失败」。
62
64
  */
63
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`。 */
72
+ readonly failed: readonly {
73
+ readonly id: string;
74
+ readonly error: RemoteFailure;
75
+ }[];
76
+ /** 部分计数的累加语义只住这里:只从 failed 的 `details` 取 `removed`/`freedBytes` 并按批相加
77
+ * ("删了一半才炸"的那部分释放量);neutral / missing 不带 details,不参与累加。 */
78
+ readonly partial: {
79
+ readonly removed: number;
80
+ readonly freedBytes: number;
81
+ };
82
+ /** 这次动作真正落地的 id。**域由 `action` 定**:delete 是条目 id(撤销名单),restore 是会话 id
83
+ * (回盘名单);purge / count / orphan-clean 没有可撤销的落地 id,恒为空。
84
+ * 两个域不混在一个数组里靠调用方猜,也不各留一个永远为空的字段。 */
85
+ readonly landedIds: readonly string[];
86
+ readonly anySuccess: boolean;
87
+ /** purge / orphan-clean 的 `ok` 回执里那半份附件读数,**按批相加**(一段 `/apply` 可能带多条 ok)。
88
+ * `skipped` 取第一条有的:它说的是"这一轮为什么没敢清",不是可累加的计数。
89
+ * 两种动作都没有读数(或这一批一条都没落地)时缺席——不拿零冒充"清过"。 */
90
+ readonly attachmentCleanup?: AttachmentCleanupWire;
91
+ }
92
+ /** 一次摘要派生的两样产物:聚合摘要与逐条失败原文。
93
+ * 失败表只挂在 `WriteSuccess` 上(成败两支的统一读法),摘要自己不再留第二份。 */
94
+ export interface ReceiptRead {
95
+ readonly summary: ReceiptSummary;
96
+ readonly failures: ReadonlyMap<string, string>;
97
+ }
98
+ /**
99
+ * 附件清理回执的「跳过半句」:`skipped` 说的是"这一轮为什么没敢清",不是失败——原因上屏,
100
+ * `removed > 0` 时说「部分」,`failed` 件数追加在句尾(不被跳过原因吞掉)。
101
+ * 面板(`use-trash-query.ts`)与维护卡(`use-maintenance.ts`)共用这一判据,不许各拼一份。
102
+ * 没 `skipped` 时回 `null`:后半句(全清/失败怎么说)两个面各有各的口径,由调用方自己接。
103
+ */
104
+ export declare function attachmentSkippedSentence(a: AttachmentCleanupWire, t: Translate): string | null;
105
+ export declare function summarizeReceipt<A extends ApplyAction>(receipt: Receipt<A>, sessionIdOfEntry?: (entryId: string) => string | undefined): ReceiptRead;
64
106
  /**
65
107
  * `/apply` 成功支回执里的条目 id(delete / restore 的 `value.id`);purge 的聚合值与 count 的数字值跳过。
66
108
  * 撤销名单只收 `ok` 支的条目 id:already-gone 是「已被同批前一条带走」的幂等条,跟着组走,不单列。
@@ -1,42 +1,21 @@
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
+ * - 九段的**段名与方法**住 `src/shared/wire.ts` 的结构化段名表(`ROUTES` 每项 `{ path, method }`),
7
+ * 本文件只投影路径(`prefix + ROUTES.x.path`)与方法(GET 段直接取 `ROUTES.x.method`,写段把它
8
+ * 交给 `post()`)——方法字面量不在本文件出现第二份;
9
+ * 写动作统一走 apply;`attachment` 是唯一二进制段,由 `src/client/attachment-view.tsx` 直接拼地址,
10
+ * 不经过本文件的信封通道;`attachment-orphans`(纯读)与 `attachment-clean`(聚合写)是孤儿附件的两段。
11
+ * - 信封与官方 unary 面同形:{ok:true,value} / {ok:false,error}。HTTP 往返本身失败才 reject;
12
+ * 业务失败以信封返回,由 unwrapRemoteResult 在注入面抛出。
13
+ * - error 在线上是 {code,message,details?} 普通对象;判别依据 isDSHRemoteError 住在
14
+ * src/shared/wire.ts,这里不 import 任何宿主客户端包。
36
15
  */
37
- import { type ApplyAction, type ApplyOutcome, type ApplyValueByAction, type OrphanReportWire, type PurgeStatsWire, type RemoteFailure, type RemoteResult, type TrashDetailWire, type TrashRowWire } from '../shared/wire.js';
16
+ 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
17
  /**
39
- * 回收站条目(`/list` 那一行)。**形状的家在 `shared/wire.ts`**(ADR-0003):这里只是旧名的别名,
18
+ * 回收站条目(`/list` 那一行)。**形状的家在 `shared/wire.ts`**(ADR-0003):这里只是别名,
40
19
  * 所以浏览器半不再自己抄一遍字段;下面几处"只读面"的镜像同理。
41
20
  */
42
21
  export type TrashEntryLike = TrashRowWire;
@@ -48,6 +27,8 @@ export type PurgeResultLike = PurgeStatsWire;
48
27
  * **线上只有"日志里读出来的那几件"**:`deletedAt` / `sizeBytes` / `wasArchived` / 清册 `title` 这些
49
28
  * 清册已有的字段一律不过线——组件按 `entryId` 与自己手上那份清单配对,这样"清单里的大小"和
50
29
  * "详情里的大小"永远是同一个数,不会因为两次读数不同而各说各话。
30
+ * 例外只有一格:`subagents`(孩子摘要)。孩子不在客户端清单里、无从配对,
31
+ * 它们的清册事实只能随详情过线(issue #7,ADR-0029)。
51
32
  */
52
33
  export type TrashDetailLike = TrashDetailWire;
53
34
  /** 任意异常 → 失败面。非带标记的失败(网络层、TypeError…)也保留原文,绝不吞;带标记的失败把
@@ -72,11 +53,9 @@ export type RemoteOutcome<T> = {
72
53
  */
73
54
  export declare function callRemote<T>(run: () => Promise<T>): Promise<RemoteOutcome<T>>;
74
55
  /** 本插件前缀路由那套**响应信封**的窄镜像(不是宿主服务 `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`(原先六个动作段)。 */
56
+ * 每个方法 resolve 成 `RemoteResult<T>`。九个段名里这层走八个 JSON 段:`list` / `ping` / `orphans` / `detail` / `apply` / `empty` /
57
+ * `attachment-orphans` / `attachment-clean`;写动作统一走 `apply`(一组 id 进、逐条结果出);
58
+ * `/attachment` 是二进制段,由 `src/client/attachment-view.tsx` 直接拼地址。 */
80
59
  export interface RawSessionDeleteRemote {
81
60
  list(): Promise<RemoteResult<TrashEntryLike[]>>;
82
61
  /** 通道的"在不在":一次零成本的 GET(探测专用,见 `src/api/dispatch.ts` 的 `ping` 支)。 */
@@ -84,7 +63,11 @@ export interface RawSessionDeleteRemote {
84
63
  /** 纯读:回收站之外的无主子会话扫描。它**不清理**——清理是 `apply` 的 `orphan-clean` 动作,
85
64
  * 这一段只交回"有多少、在哪、多大"(读不到语料时报 `io`,调用方不许把它折成"零条残留")。 */
86
65
  orphans(): Promise<RemoteResult<OrphanReportWire>>;
87
- detail(entryId: string): Promise<RemoteResult<TrashDetailLike>>;
66
+ /** 纯读:`.dsh` 内无引用图片对象的数量、字节与预览清单(引用源读不出时报 `io`,不折成 0)。 */
67
+ attachmentOrphans(): Promise<RemoteResult<AttachmentOrphanReportWire>>;
68
+ /** 聚合写:只删上一条报告那一刻无引用的图片对象,回 `AttachmentCleanupWire`。 */
69
+ attachmentClean(): Promise<RemoteResult<AttachmentCleanupWire>>;
70
+ detail(entryId: string, before?: number): Promise<RemoteResult<TrashDetailLike>>;
88
71
  /** 一组 id 上的同一个动作。顶层信封的 `ok:false` 只表示**整次调用**没办成(路由自检、栅栏之外);
89
72
  * 单条动作的成败住在 `ApplyOutcome` 里——这是批量接口的判据,别在信封层找它。 */
90
73
  apply(action: ApplyAction, ids: readonly string[]): Promise<RemoteResult<ApplyOutcome[]>>;
@@ -93,8 +76,9 @@ export interface RawSessionDeleteRemote {
93
76
  }
94
77
  /**
95
78
  * 前缀路由的 fetch 实现。**相对路径**:不拼 base、不认 origin——页面本身就是从宿主 web server
96
- * 加载的,同源是这条薄线的全部前提(也是它敢不带凭据的原因:宿主那行 URL 上的 browser-trust token
97
- * 不是这条通道的凭据,栅栏在 Node 半那一侧按 Origin/Referer 判)。
79
+ * 加载的,同源是这条薄线的全部前提。本机(含转发落在回环的桌面壳)靠回环 + Origin/Referer 过栅栏;
80
+ * 远程(LAN / 反代)客户端另由宿主 `connection` 判「已认证的 Web 身份」,所以这条通道**只对**
81
+ * `dsh web` 打印的 URL 打开过的页面成立(凭据是那行 URL 换来的 cookie,不是插件自己的秘密)。
98
82
  */
99
83
  export declare function createHttpRemote(prefix?: string): RawSessionDeleteRemote;
100
84
  /**
@@ -104,12 +88,11 @@ export declare function createHttpRemote(prefix?: string): RawSessionDeleteRemot
104
88
  * 错误映射也生效了),那时该做的是把入口显示出来、让原文上屏;只有往返本身失败才算通道缺席 ⇒
105
89
  * 一个槽位贡献都不注册(docs/architecture.md §3「通道探测决定入口出现与否」)。
106
90
  *
107
- * 2026-10-17:探测从"读一遍清单"改成这一问——**每次页面加载都跑全表对账**是白付的钱,
108
- * 而"信封到不到"这件事一次零成本的 GET 就答得出来。
91
+ * 探测用一次零成本的 `GET /ping`,不读清单,避免每次页面加载都跑全表对账。
109
92
  */
110
93
  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';
94
+ /** `shared/wire.ts` 的 `ApplyOutcome`、扫描报告与附件回执从这里转出去——浏览器半只认 `remote.js` 这一扇门。 */
95
+ export type { ApplyAction, ApplyOutcome, AttachmentCleanupWire, AttachmentOrphanReportWire, OrphanReportWire, } from '../shared/wire.js';
113
96
  /**
114
97
  * N=1 的成功支:`null` 已被 `receipt.ts` 的单条读法折成协议错,所以 `value` 必然非空。
115
98
  * 不再需要条件类型把 `delete` / `restore` 的 null 抠掉——那份判据只住回执读法一处。
@@ -121,7 +104,8 @@ export type OneShotSuccess<A extends ApplyAction> = {
121
104
  readonly value: Exclude<ApplyValueByAction[A], null>;
122
105
  };
123
106
  /**
124
- * 注入面(`deps`)的 interface:**与六个段名一一对应的五个方法**(`ping` 只给探测用,留在 raw 层)。
107
+ * 注入面(`deps`)的 interface:**七个方法**(raw 层另有 `ping` 供探测;九段里的 `/attachment` 由
108
+ * `src/client/attachment-view.tsx` 直接拼地址,也不在这层)。
125
109
  *
126
110
  * **不再有 N=1 的便捷方法**:`delete` / `restore` / `countSubagents` 曾是同一段
127
111
  * `/apply` 的三种写法,于是一个 `module` 里同一个动作有两条路(回收站面板的 deps 同时躺着
@@ -133,8 +117,12 @@ export interface SessionDeleteRemote {
133
117
  /** 回收站之外的无主子会话报告(纯读)。读不到就是 throw —— 界面上那是**一次失败的检查**,
134
118
  * 不是"零条残留"(把 `io` 折成空报告等于把"不知道"说成"没有")。 */
135
119
  scanOrphans(): Promise<OrphanReportWire>;
136
- detail(entryId: string): Promise<TrashDetailLike>;
137
- /** 一段管所有写动作(2026-10-17 批量重构):一次调用把整批办完,逐条结果原样交回组件。 */
120
+ /** `.dsh` 内无引用图片对象的报告(纯读;含最多 200 条预览清单)。读不到就是 throw,同样不许折成 0。 */
121
+ scanAttachmentOrphans(): Promise<AttachmentOrphanReportWire>;
122
+ /** 清掉报告那一刻无引用的图片副本(聚合回执)。 */
123
+ cleanAttachmentOrphans(): Promise<AttachmentCleanupWire>;
124
+ detail(entryId: string, before?: number): Promise<TrashDetailLike>;
125
+ /** 一段管所有写动作:一次调用把整批办完,逐条结果原样交回组件。 */
138
126
  applyOutcomes(action: ApplyAction, ids: readonly string[]): Promise<ApplyOutcome[]>;
139
127
  /** 清空回收站(聚合回执)。 */
140
128
  emptyTrash(): Promise<PurgeResultLike>;
@@ -17,10 +17,12 @@ 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
27
  export interface LazyStoreLifecycle {
26
28
  /** 第一个订阅者到来时跑(观察者在这里接上)。 */
@@ -46,3 +48,12 @@ export interface LatestWins {
46
48
  invalidate(): void;
47
49
  }
48
50
  export declare function createLatestWins(): LatestWins;
51
+ /**
52
+ * 引用归一:输入允许调用方逐渲染换新对象,回调只经这份 ref 读**最新**那一份,于是依赖它的回调
53
+ * 身份恒稳、调用方的 effect 可以放心把它们当依赖——「输入必须是稳定引用」的隐式契约就此不存在
54
+ * (issue #8 第 3 条)。写回在 effect 里(提交后),与 React 的渲染期只读约定一致。
55
+ * 这一形状的唯一实现:别在调用方再手写一份「useRef + 每渲染写回」。
56
+ */
57
+ export declare function useLatest<T>(value: T): {
58
+ readonly current: T;
59
+ };
@@ -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;
@@ -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;
@@ -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';
@@ -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
  }