@xrn1997/dsh-session-delete 0.4.0 → 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 (54) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +141 -135
  3. package/cordis.patch.yml +3 -3
  4. package/lib/client.js +1477 -563
  5. package/lib/index.js +1230 -755
  6. package/lib/types/api/dispatch.d.ts +6 -2
  7. package/lib/types/api/route-table.d.ts +29 -9
  8. package/lib/types/api/trash-rows.d.ts +3 -3
  9. package/lib/types/client/busy-gate.d.ts +6 -2
  10. package/lib/types/client/confirm-close.d.ts +4 -1
  11. package/lib/types/client/context.d.ts +3 -2
  12. package/lib/types/client/copy.d.ts +54 -4
  13. package/lib/types/client/delete-confirm.d.ts +1 -1
  14. package/lib/types/client/delete-menu-item.d.ts +1 -1
  15. package/lib/types/client/host-rows.d.ts +5 -1
  16. package/lib/types/client/markdown.d.ts +6 -7
  17. package/lib/types/client/orphan-maintenance.d.ts +10 -25
  18. package/lib/types/client/receipt.d.ts +3 -1
  19. package/lib/types/client/remote.d.ts +7 -3
  20. package/lib/types/client/store.d.ts +14 -3
  21. package/lib/types/client/trash-detail.d.ts +48 -6
  22. package/lib/types/client/trash-entry.d.ts +1 -1
  23. package/lib/types/client/trash-nav.d.ts +12 -8
  24. package/lib/types/client/ui/Toast.d.ts +3 -1
  25. package/lib/types/client/ui/icons.d.ts +1 -1
  26. package/lib/types/client/undo-toast.d.ts +2 -1
  27. package/lib/types/client/use-maintenance-section.d.ts +52 -0
  28. package/lib/types/client/use-maintenance.d.ts +7 -35
  29. package/lib/types/client/use-trash-panel-controller.d.ts +35 -14
  30. package/lib/types/client/use-trash-query.d.ts +10 -8
  31. package/lib/types/client/use-trash-selection.d.ts +15 -12
  32. package/lib/types/client/write-seats.d.ts +32 -16
  33. package/lib/types/client/write-settlement.d.ts +1 -1
  34. package/lib/types/client/write.d.ts +13 -3
  35. package/lib/types/domain/closure.d.ts +44 -0
  36. package/lib/types/log/zstd-frames.d.ts +2 -3
  37. package/lib/types/ports/fs-port.d.ts +15 -12
  38. package/lib/types/ports/host-port.d.ts +4 -5
  39. package/lib/types/ports/presence-port.d.ts +9 -0
  40. package/lib/types/shared/wire.d.ts +119 -26
  41. package/lib/types/trash/attachments.d.ts +8 -1
  42. package/lib/types/trash/detail.d.ts +32 -22
  43. package/lib/types/trash/entry.d.ts +7 -1
  44. package/lib/types/trash/groups.d.ts +6 -1
  45. package/lib/types/trash/manifest.d.ts +4 -4
  46. package/lib/types/verbs/batch-carried.d.ts +1 -3
  47. package/lib/types/verbs/live-admission.d.ts +1 -1
  48. package/lib/types/verbs/member-presence.d.ts +3 -1
  49. package/lib/types/verbs/move-group.d.ts +2 -1
  50. package/lib/types/verbs/orphan-subagents.d.ts +11 -5
  51. package/lib/types/verbs/permanent-removal.d.ts +39 -10
  52. package/lib/types/verbs/trash-scope.d.ts +36 -0
  53. package/package.json +117 -107
  54. package/lib/types/verbs/purge-trash.d.ts +0 -35
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * /dsh-session-delete-api 的前缀路由分发:只负责传输。
3
3
  *
4
- * 三件事:请求栅栏 → 查路由表 → 回信封。方法、纯度、body 预读与读面的判据都在
5
- * src/api/route-table.ts(这里只按判据执行);行投影在 src/api/trash-rows.ts。
4
+ * 四件事:请求栅栏 → 查路由表 → 按段注入窄面 → 回信封。纯度、body 预读与读面的判据都在
5
+ * src/api/route-table.ts(方法也从那张表读,字面量家在 `shared/wire.ts` 的段名表;这里只按判据执行);
6
+ * 行投影在 src/api/trash-rows.ts。
6
7
  *
7
8
  * - 请求本身不对用 4xx(400 请求体 / 405 方法 / 404 未知路由 / 403 栅栏拒绝);
8
9
  * 操作失败回 200 + {ok:false,error}。浏览器半先解信封,再看状态。
@@ -12,4 +13,7 @@
12
13
  import type { IncomingMessage, ServerResponse } from 'node:http';
13
14
  import type { DeleteDeps } from '../deps.js';
14
15
  import { type RequestTrustFacts } from './request-trust.js';
16
+ import { type SliceDeps } from './route-table.js';
17
+ /** 三格只建一次:收窄对象在 handler 构造期固定,请求路径只做引用选择。 */
18
+ export declare function slicesOf(full: DeleteDeps): SliceDeps;
15
19
  export declare function createApiHandler(deps: DeleteDeps, trust: RequestTrustFacts): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
@@ -1,16 +1,18 @@
1
1
  /**
2
- * 路由表(ADR-0004):段名 → { 方法, 纯度, 读面, body, 处理 }。一条记录,两个消费者。
2
+ * 路由表(ADR-0004;deps 切片见 ADR-0032):段名 → { 方法, 纯度, 读面, body, deps 切片, 处理 }。一条记录,两个消费者。
3
3
  *
4
4
  * 一致性要求:
5
5
  * - 该纯的判成写 ⇒ 探测/正文读/扫描被堵在串行链后;
6
6
  * - 该写的判成纯 ⇒ 写动作绕开到达序链,与对账交错;
7
- * - body 该读没读 ⇒ 下游拿到 null;读两遍 ⇒ 流已被消费。
7
+ * - body 该读没读 ⇒ 下游拿到 null;读两遍 ⇒ 流已被消费;
8
+ * - 切片声明宽了 ⇒ 危险能力(删目录 / 时钟)漏进读段的 run;窄了 ⇒ 写段的处理编译不过。
8
9
  *
9
10
  * 这张表不放 shared/wire.ts:处理函数会 import 动词与 node:fs,而浏览器半要 import wire.ts。
10
- * 段名与动作仍住 wire.ts;这张表只回答“怎么处理”。
11
+ * 段名、方法与动作仍住 wire.ts(`ROUTES` 每项 `{ path, method }`,本表的 method 只投影、不写第二份字面量);
12
+ * 这张表只回答“怎么处理”。
11
13
  */
12
- import { ROUTES, type ApplyAction } from '../shared/wire.js';
13
- import type { DeleteDeps } from '../deps.js';
14
+ import { type ApplyAction, type RouteName } from '../shared/wire.js';
15
+ import type { CoreDeps, DeleteDeps, RemovalDeps } from '../deps.js';
14
16
  import type { TrashEntry } from '../trash/entry.js';
15
17
  /** 请求体:`json` 才读下来(GET 一律不读;读两遍会把流消费掉)。 */
16
18
  export type BodyKind = 'none' | 'json';
@@ -31,19 +33,37 @@ export interface BinaryResponse {
31
33
  export interface RouteRead {
32
34
  rows(): Promise<readonly TrashEntry[]>;
33
35
  }
34
- export interface RouteSpec {
36
+ /** deps 切片(ADR-0032):每一段只声明自己要的那一面。读与探测六段拿基础面,两个删盘段多删除能力,
37
+ * `/apply` 是唯一拿全量面的行——段的 run 里长出超面调用就是编译错,dispatch 也只按这一格注入
38
+ * (运行期收窄住 `src/api/dispatch.ts` 的 `slicesOf`)。 */
39
+ export type RouteSlice = 'core' | 'removal' | 'full';
40
+ /** 切片名 → 依赖面:{@link RouteSpec.run} 的 deps 参数类型与 `depsFor` 的返回类型都从这一格取。 */
41
+ export type SliceDeps = {
42
+ core: CoreDeps;
43
+ removal: RemovalDeps;
44
+ full: DeleteDeps;
45
+ };
46
+ export interface RouteSpec<S extends RouteSlice = RouteSlice> {
47
+ /** 这一行吃哪一格 deps:dispatch 按它注入,`run` 的 deps 参数类型也跟着它走。 */
48
+ readonly slice: S;
35
49
  readonly method: 'GET' | 'POST';
36
50
  readonly body: BodyKind;
37
51
  readonly purity: Purity;
38
52
  readonly read: ReadKind;
39
53
  /** 响应形态:`json` 走信封(`{ok:true,value}`);`binary` 直接把处理函数的 `BinaryResponse` 写出去。 */
40
54
  readonly response: 'json' | 'binary';
41
- run(deps: DeleteDeps, body: JsonBody, read: RouteRead, query: URLSearchParams): Promise<unknown>;
55
+ /** 声明成函数属性而不是方法:方法签名是双变的,显式标注更宽的 deps 能绕过切片闸;属性按逆变校验。 */
56
+ readonly run: (deps: SliceDeps[S], body: JsonBody, read: RouteRead, query: URLSearchParams) => Promise<unknown>;
42
57
  }
58
+ /** 九行的并集(表的值类型):每行由自己的 `slice` 字面量定型,`run` 的 deps 面随之收窄。 */
59
+ export type AnyRouteSpec = {
60
+ [S in RouteSlice]: RouteSpec<S>;
61
+ }[RouteSlice];
43
62
  export declare const APPLY_ACTIONS: readonly ApplyAction[];
44
63
  /**
45
- * 九个段名各一行。`Record<段名, RouteSpec>` 是闸:`wire.ts` 加了段名而这里没加,编译不过。
64
+ * 九个段名各一行。`Record<段名, AnyRouteSpec>` 是闸:`wire.ts` 加了段名而这里没加,编译不过;
65
+ * 每行的 `slice` 又把该行的 `run` 钉在自己那一格 deps 上(ADR-0032)。
46
66
  */
47
- export declare const ROUTE_TABLE: Record<(typeof ROUTES)[keyof typeof ROUTES], RouteSpec>;
67
+ export declare const ROUTE_TABLE: Record<RouteName, AnyRouteSpec>;
48
68
  /** 段名 → 那一行;不认识的段名回 `undefined`(调用方抛 404,并带出原路径)。 */
49
69
  export declare function routeOf(rest: string): RouteSpec | undefined;
@@ -6,7 +6,7 @@
6
6
  * 归 `verbs/permanent-removal.ts` 的只读计划 —— 这里只做装配,不再自己枚举盘上后代。
7
7
  */
8
8
  import type { CoreDeps } from '../deps.js';
9
- import type { TrashEntry } from '../trash/entry.js';
9
+ import { type TrashEntry } from '../trash/entry.js';
10
10
  import type { TrashRowWire } from '../shared/wire.js';
11
11
  /** 组视图那几个只读数的容器(行投影的第二个入参)。`releaseBytes` 必填(计划里实测好的那一格);
12
12
  * 其余缺席 = 这次没算,不是 0。 */
@@ -14,8 +14,8 @@ type TrashRowReadouts = {
14
14
  readonly releaseBytes: number;
15
15
  } & Partial<Pick<TrashRowWire, 'subagentCount' | 'subagentBytes' | 'strandedSubagents' | 'strandedBytes' | 'workspaceMissing'>>;
16
16
  /**
17
- * 清册那一行 → 线上那一行。**显式字面量、不用 `...row`**:线上形状多一个必填字段时,这里编译不过,
18
- * 于是"线上多了一格却没人填"不可能悄悄发生(`...row` 会把它带过去,看上去一切正常)。
17
+ * 清册那一行 → 线上那一行。清册字段走 `entryWire` 那份显式字面量(线上形状多一个必填字段时编译不过,
18
+ * `...row` 会把它带过去),这里只补 `/list` 的只读数。
19
19
  */
20
20
  export declare function toWire(row: TrashEntry, readouts: TrashRowReadouts): TrashRowWire;
21
21
  /**
@@ -1,6 +1,10 @@
1
1
  export interface BusyGate {
2
2
  readonly busy: boolean;
3
- /** 把一次任务整个包进「这一趟还算忙」;落地与抛错都放回 false。 */
4
- around<T>(task: () => Promise<T>): Promise<T>;
3
+ /** 把一次任务整个包进「这一趟还算忙」;落地与抛错都放回 false。
4
+ * 同一枚闸已有任务在飞时这一次**不执行**(同步判重),立即回 `undefined`。类型里带上 `undefined`
5
+ * 不是将就,是判据:挡回的那一趟什么都没跑,把它伪装成一枚 `T` 就是把「没跑」当成「跑完且结果是
6
+ * 那个值」——消费返回值的调用方(写入座位、清空)会照着编出失败原文或宣布成功。怎么收尾由调用方
7
+ * 定:这一趟的界面后果归在飞的那一趟,挡回来的那一份什么都不该改。 */
8
+ around: <T>(task: () => Promise<T>) => Promise<T | undefined>;
5
9
  }
6
10
  export declare function useBusyGate(): BusyGate;
@@ -5,7 +5,10 @@
5
5
  * 返回的 `closeDisabled` 与 `close` 同源:取消键的置灰读数也从这里取,各组件不再各写一遍
6
6
  * `disabled={busy}`——否则"拦住"这件事会有两个判据。 */
7
7
  export interface IdleClose {
8
- close(): void;
8
+ /** 属性声明而不是方法简写:实现体不读 `this`,调用方会把 `close` 摘下来直接交给 `Modal`/
9
+ * `Button`(`onClose={close}`);方法简写会给它隐式 `this: IdleClose`,`unbound-method`
10
+ * 就会把这种正常交接报成可疑。形状仍是同一个函数,零运行时改动。 */
11
+ close: () => void;
9
12
  readonly closeDisabled: boolean;
10
13
  }
11
14
  export declare function useIdleClose(busy: boolean, onClose: () => void): IdleClose;
@@ -26,6 +26,7 @@ export interface ClientContextLike {
26
26
  slots: SlotsLike;
27
27
  /** 可选服务的读面(宿主自己的写法:`ctx.get('…')?.…`)。**缺省调用**:老宿主上没有这个方法。 */
28
28
  get?(name: string): unknown;
29
- /** 挂一个随本插件卸载而撤销的副作用(返回的清理函数由框架接管)。 */
30
- effect(execute: () => unknown, label?: string): unknown;
29
+ /** 挂一个随本插件卸载而撤销的副作用(返回的清理函数由框架接管)。
30
+ * 属性声明:测试会把它当成员读(`expect(ctx.effect).not.toHaveBeenCalled()`),实现不读 `this`。 */
31
+ effect: (execute: () => unknown, label?: string) => unknown;
31
32
  }
@@ -102,7 +102,7 @@ declare const ZH: {
102
102
  readonly 'detail.notSelected': "选中一条会话,看它讲了什么";
103
103
  readonly 'detail.loading': "正在读这条会话…";
104
104
  readonly 'detail.readFailed': "这条会话的正文读不出来";
105
- readonly 'detail.noBody': "这条会话没有可显示的文字";
105
+ readonly 'detail.noBody': "这条会话没有可显示的内容";
106
106
  readonly 'detail.field.created': "创建";
107
107
  readonly 'detail.field.lastActivity': "最后活动";
108
108
  readonly 'detail.field.deletedAt': "删除于";
@@ -111,8 +111,9 @@ declare const ZH: {
111
111
  readonly 'detail.field.size': "大小";
112
112
  readonly 'detail.field.turns': "轮数";
113
113
  readonly 'detail.field.userMessages': "提问";
114
- readonly 'detail.field.assistantMessages': "回复";
114
+ readonly 'detail.field.assistantMessages': "助手消息";
115
115
  readonly 'detail.field.toolCalls': "工具调用";
116
+ readonly 'detail.field.injections': "注入上下文";
116
117
  readonly 'detail.field.models': "模型";
117
118
  readonly 'detail.field.project': "项目";
118
119
  readonly 'detail.field.sessionId': "会话 id";
@@ -120,10 +121,36 @@ declare const ZH: {
120
121
  readonly 'detail.field.wasArchived': "原本归档";
121
122
  readonly 'detail.value.yes': "是";
122
123
  readonly 'detail.value.no': "否";
124
+ readonly 'detail.value.spanLessThanHour': "不足 1 小时";
123
125
  readonly 'detail.value.spanHours': "{n} 小时";
124
126
  readonly 'detail.value.spanDays': "{n} 天";
125
- readonly 'detail.messages.hidden': "另有 {count} 条宿主注入的上下文未显示";
126
- readonly 'detail.messages.omitted': "还有 {count} 条未显示";
127
+ readonly 'detail.block.reasoning': "思考";
128
+ readonly 'detail.block.tool': "工具调用";
129
+ readonly 'detail.block.injection': "上下文注入";
130
+ readonly 'detail.block.system': "系统消息";
131
+ readonly 'detail.block.compaction': "上下文已压缩";
132
+ readonly 'detail.block.args': "参数";
133
+ readonly 'detail.block.result': "结果";
134
+ readonly 'detail.block.noResult': "日志在调用结束前结束,没有结果";
135
+ readonly 'detail.block.status.ok': "成功";
136
+ readonly 'detail.block.status.error': "失败";
137
+ readonly 'detail.block.status.unfinished': "未完成";
138
+ readonly 'detail.block.truncated': "已截断";
139
+ readonly 'detail.loadOlder': "加载更早";
140
+ readonly 'detail.loadOlder.loading': "正在加载更早…";
141
+ readonly 'detail.expandAll': "全部展开";
142
+ readonly 'detail.collapseAll': "全部折叠";
143
+ readonly 'detail.injection.runtimeContext': "运行上下文";
144
+ readonly 'detail.injection.timeContext': "时间上下文";
145
+ readonly 'detail.injection.agentInstructions': "Agent 指令";
146
+ readonly 'detail.injection.skillCatalog': "技能目录";
147
+ readonly 'detail.injection.agentMessage': "子会话消息";
148
+ readonly 'detail.injection.subagentSettled': "子会话结束";
149
+ readonly 'detail.injection.toolJobs': "后台任务";
150
+ readonly 'detail.injection.skillInvocation': "技能调用";
151
+ readonly 'detail.injection.userApproval': "用户批准";
152
+ readonly 'detail.injection.goal': "目标";
153
+ readonly 'detail.injection.other': "其他上下文";
127
154
  readonly 'detail.image': "[图片]";
128
155
  readonly 'detail.image.alt': "[图片 · {alt}]";
129
156
  readonly 'detail.attachment.file': "文件";
@@ -135,6 +162,9 @@ declare const ZH: {
135
162
  readonly 'detail.attachment.saving': "保存中…";
136
163
  readonly 'detail.attachment.saveFailed': "保存失败:{message}";
137
164
  readonly 'detail.back': "← 返回清单";
165
+ readonly 'detail.subagents.header': "子会话({n}) · {size}";
166
+ readonly 'detail.subagents.truncated': "仅列出前 {shown} 条,共 {total} 条";
167
+ readonly 'detail.backToParent': "← 返回父会话";
138
168
  readonly 'detail.list.label': "回收站清单";
139
169
  readonly 'detail.role.user': "你";
140
170
  readonly 'detail.role.assistant': "助手";
@@ -160,6 +190,7 @@ declare const ZH: {
160
190
  readonly 'undo.moved': "{title} 已移入回收站";
161
191
  readonly 'undo.session': "会话";
162
192
  readonly 'undo.undo': "撤销";
193
+ readonly 'undo.failedBatch': "{n} 条会话未能撤销:{reasons}";
163
194
  };
164
195
  export type CopyKey = keyof typeof ZH;
165
196
  export type CopyParams = Readonly<Record<string, string | number>>;
@@ -174,6 +205,25 @@ export declare const zhTranslate: Translate;
174
205
  * 而且同一命名空间的同一语言**只能注册一次**——effect 没挂,HMR 重挂就撞 "already has locale"。
175
206
  */
176
207
  export declare function createTranslate(ctx: ClientContextLike): Translate;
208
+ /**
209
+ * `MarkdownBody`(`markdown.tsx`)memo comparator 的**语言探针**:借一句固定键的译文,当
210
+ * 「当前是哪种语言」的读数。为什么借译文而不问 locale 服务:宿主 locale 服务的窄镜像不暴露
211
+ * 当前语言(受窄镜像形状约束,不做 `locale()` 探针),当前语言只能从 `t` 的输出里读出来。
212
+ *
213
+ * 为什么必须在**渲染时**现算、由调用方存进 props:宿主 `t` 读的是**当前** locale,若在
214
+ * comparator 里现调,prev/next 那一拍都取到新值、恒等,换语言就漏了——只有渲染那一刻算好的
215
+ * 那份才是换语言那一拍的读数。
216
+ *
217
+ * 为什么拿 `t` 的输出当读数、而不是拿 `t` 的身份:宿主 `t` 是 `bind` 出来的**稳定身份**
218
+ * (`createTranslate` 一次客户端激活只造一个),换语言后同一个 `t` 吐另一套词——身份不变、
219
+ * 词变了,所以判据必须是这个词(探针值)而不是身份本身。
220
+ *
221
+ * 「借哪句」的知识只住这里:调用方与测试一律传 `probe={localeProbe(t)}`,不各自记得借哪句。
222
+ *
223
+ * **失效模式**:借的那句若在两种语言下译成同一个字面,探针就分不出换语言——memo 恒等、旧语言滞留。
224
+ * 这条由 `tests/client/markdown.test.tsx` 的换语言用例兜住;换借句或改那句译文时先看它红不红。
225
+ */
226
+ export declare function localeProbe(t: Translate): string;
177
227
  /**
178
228
  * 把一句话里内嵌的那一段拆出来:`'…文件,之后没有任何副本可以恢复。'` 交给调用面用 `<b>` 包中间那段。
179
229
  *
@@ -16,7 +16,7 @@ export declare const TARGET_CARD: {
16
16
  export interface DeleteRequest {
17
17
  /** 每次发布递增:换一个会话就重挂载确认框,busy / failure 不带着上一条请求的记忆。 */
18
18
  seq: number;
19
- /** 要删的会话 id——原样透给 `sessiondelete/delete`。 */
19
+ /** 要删的会话 id——原样透给删除写入(`apply` 的 `delete` 动作;段名与动作住 `shared/wire.ts`)。 */
20
20
  sessionId: string;
21
21
  /** 删除那一刻的会话名(槽位投影的 `displayTitle`):只用于确认框文案与撤销提示。 */
22
22
  title: string;
@@ -1,6 +1,6 @@
1
1
  import { type Translate } from './copy.js';
2
2
  export interface DeleteMenuItemProps {
3
- /** 宿主菜单槽投影的会话 id,原样透给 `sessiondelete/delete`(浏览器半不自己拼 id)。 */
3
+ /** 宿主菜单槽投影的会话 id,原样透给删除写入(`apply` 的 `delete` 动作;段名与动作住 `shared/wire.ts`,浏览器半不自己拼 id)。 */
4
4
  sessionId: string;
5
5
  /** 宿主菜单槽投影的 `displayTitle`:只用于确认框与提示文案,不参与任何请求。 */
6
6
  title: string;
@@ -7,9 +7,13 @@ export interface HostRowInfo {
7
7
  }
8
8
  /**
9
9
  * 行尾兜底勾选开关登记一行:模式内与模式外都要登记(守卫进模式的那一帧就得有标题)。
10
- * 只有标题/运行态**真的变了**才标脏:重渲染时的同值登记不该把快照翻新一遍。
10
+ * 每次登记算**多一个挂载实例**;只有标题/运行态**真的变了**才标脏:重渲染时的同值登记
11
+ * 不该把快照翻新一遍。
11
12
  */
12
13
  export declare function registerHostRowInfo(sessionId: string, info: HostRowInfo): void;
14
+ /** 行尾开关卸载时调用:那一行已经离开渲染树,登记跟着走。
15
+ * 还有别的挂载实例在用同一行(重复行)就只减计数,不摘登记。 */
16
+ export declare function unregisterHostRowInfo(sessionId: string): void;
13
17
  /**
14
18
  * 「当前可见」的可选行:侧栏里**已渲染**的会话行(`data-row-key="session:…"`)——折叠组、
15
19
  * 归档过滤外的行不在 DOM 里,宿主隐藏的 subagent 也不在;在跑的行跳过(与置灰勾选框同口径)。
@@ -2,18 +2,17 @@ import type { Translate } from './copy.js';
2
2
  import './ui/styles.js';
3
3
  /**
4
4
  * `memo` 是必需的而不是优化:`react-markdown@10.1.0` 里**一处 memoization 都没有**(读实
5
- * `node_modules/react-markdown/lib/index.js:175-178`——`Markdown()` 每次调用都 `createProcessor(options)`
5
+ * `node_modules/react-markdown/lib/index.js`——`Markdown()` 每次调用都 `createProcessor(options)`
6
6
  * 再 `runSync(parse(file))`,`MarkdownHooks` 那一支也一样),而面板任何一次重渲(悬停、↑↓、
7
7
  * 「刚刚删除」那句要读 `Date.now()`)都会走到这里 ⇒ 不按住就是每条助手回复每拍重解析一遍。
8
- * 按住的条件是 `text` 与**探针 `localeKey`**(调用方在渲染时算好的一句固定键译文 `t('common.close')`)。
9
- * `t` 本身是宿主 `bind` 出来的**稳定身份**(`copy.ts` 头注),一次客户端激活只造一个——拿身份当判据,
10
- * 换语言就漏了:同一个 `t` 换 locale 后吐另一套词,烘进正文树的那句图片占位会滞留旧语言,而周围的
11
- * 标签那一拍已经改口。探针也不能在 comparator 里现调 `t` 取——宿主 `t` 读的是**当前** locale,
12
- * prev/next 那一拍都取到新值、恒等;只有渲染时算好存进 props 的那份才是换语言那一刻的读数。
8
+ * 按住的条件是 `text` 与**探针 `probe`**:一句按当前 locale 现算的译文,专给 comparator 当
9
+ * 「换语言了没」的读数。为什么必须渲染时现算存进 props、为什么宿主 `t` 只能当稳定身份而不是
10
+ * 语言读数、「借哪句」,这些都住 `copy.ts` 的 `localeProbe`——调用方与测试一律
11
+ * `probe={localeProbe(t)}`,「借哪句」的知识不散到各调用方。
13
12
  * 探针没变 = 同语言同 `t` ⇒ 跳过重解析;变了(换语言或换 `t`)⇒ 重解析。
14
13
  */
15
14
  export declare const MarkdownBody: import("react").NamedExoticComponent<{
16
15
  text: string;
17
16
  t: Translate;
18
- localeKey: string;
17
+ probe: string;
19
18
  }>;
@@ -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;
@@ -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;
@@ -3,7 +3,9 @@
3
3
  *
4
4
  * - 路由是宿主 web server 上的插件前缀;官方 remote.<namespace> 需要生成的 Typert 描述符,
5
5
  * 第三方无法动态挂载,因此走这条薄线(ADR-0009)。
6
- * - 九段:list / ping / orphans / detail / apply / empty / attachment / attachment-orphans / attachment-clean;
6
+ * - 九段的**段名与方法**住 `src/shared/wire.ts` 的结构化段名表(`ROUTES` 每项 `{ path, method }`),
7
+ * 本文件只投影路径(`prefix + ROUTES.x.path`)与方法(GET 段直接取 `ROUTES.x.method`,写段把它
8
+ * 交给 `post()`)——方法字面量不在本文件出现第二份;
7
9
  * 写动作统一走 apply;`attachment` 是唯一二进制段,由 `src/client/attachment-view.tsx` 直接拼地址,
8
10
  * 不经过本文件的信封通道;`attachment-orphans`(纯读)与 `attachment-clean`(聚合写)是孤儿附件的两段。
9
11
  * - 信封与官方 unary 面同形:{ok:true,value} / {ok:false,error}。HTTP 往返本身失败才 reject;
@@ -25,6 +27,8 @@ export type PurgeResultLike = PurgeStatsWire;
25
27
  * **线上只有"日志里读出来的那几件"**:`deletedAt` / `sizeBytes` / `wasArchived` / 清册 `title` 这些
26
28
  * 清册已有的字段一律不过线——组件按 `entryId` 与自己手上那份清单配对,这样"清单里的大小"和
27
29
  * "详情里的大小"永远是同一个数,不会因为两次读数不同而各说各话。
30
+ * 例外只有一格:`subagents`(孩子摘要)。孩子不在客户端清单里、无从配对,
31
+ * 它们的清册事实只能随详情过线(issue #7,ADR-0029)。
28
32
  */
29
33
  export type TrashDetailLike = TrashDetailWire;
30
34
  /** 任意异常 → 失败面。非带标记的失败(网络层、TypeError…)也保留原文,绝不吞;带标记的失败把
@@ -63,7 +67,7 @@ export interface RawSessionDeleteRemote {
63
67
  attachmentOrphans(): Promise<RemoteResult<AttachmentOrphanReportWire>>;
64
68
  /** 聚合写:只删上一条报告那一刻无引用的图片对象,回 `AttachmentCleanupWire`。 */
65
69
  attachmentClean(): Promise<RemoteResult<AttachmentCleanupWire>>;
66
- detail(entryId: string): Promise<RemoteResult<TrashDetailLike>>;
70
+ detail(entryId: string, before?: number): Promise<RemoteResult<TrashDetailLike>>;
67
71
  /** 一组 id 上的同一个动作。顶层信封的 `ok:false` 只表示**整次调用**没办成(路由自检、栅栏之外);
68
72
  * 单条动作的成败住在 `ApplyOutcome` 里——这是批量接口的判据,别在信封层找它。 */
69
73
  apply(action: ApplyAction, ids: readonly string[]): Promise<RemoteResult<ApplyOutcome[]>>;
@@ -117,7 +121,7 @@ export interface SessionDeleteRemote {
117
121
  scanAttachmentOrphans(): Promise<AttachmentOrphanReportWire>;
118
122
  /** 清掉报告那一刻无引用的图片副本(聚合回执)。 */
119
123
  cleanAttachmentOrphans(): Promise<AttachmentCleanupWire>;
120
- detail(entryId: string): Promise<TrashDetailLike>;
124
+ detail(entryId: string, before?: number): Promise<TrashDetailLike>;
121
125
  /** 一段管所有写动作:一次调用把整批办完,逐条结果原样交回组件。 */
122
126
  applyOutcomes(action: ApplyAction, ids: readonly string[]): Promise<ApplyOutcome[]>;
123
127
  /** 清空回收站(聚合回执)。 */
@@ -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,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` 读它。