@xrn1997/dsh-session-delete 0.1.1 → 0.2.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.
- package/README.md +5 -4
- package/icon.svg +18 -18
- package/lib/client.js +1407 -427
- package/lib/index.js +785 -182
- package/lib/types/api/dispatch.d.ts +51 -0
- package/lib/types/client/batch-bar.d.ts +24 -0
- package/lib/types/client/batch-confirm.d.ts +31 -0
- package/lib/types/client/batch-menu-item.d.ts +19 -0
- package/lib/types/client/batch-select.d.ts +97 -0
- package/lib/types/client/context.d.ts +31 -0
- package/lib/types/client/copy.d.ts +167 -0
- package/lib/types/client/delete-confirm.d.ts +40 -0
- package/lib/types/client/delete-menu-item.d.ts +20 -0
- package/lib/types/client/index.d.ts +19 -0
- package/lib/types/client/markdown.d.ts +11 -0
- package/lib/types/client/remote.d.ts +178 -0
- package/lib/types/client/trash-detail.d.ts +27 -0
- package/lib/types/client/trash-entry.d.ts +6 -0
- package/lib/types/client/trash-panel.d.ts +10 -0
- package/lib/types/client/trash-store.d.ts +4 -0
- package/lib/types/client/ui/Button.d.ts +21 -0
- package/lib/types/client/ui/Checkbox.d.ts +29 -0
- package/lib/types/client/ui/MenuItemButton.d.ts +32 -0
- package/lib/types/client/ui/Modal.d.ts +35 -0
- package/lib/types/client/ui/Tag.d.ts +16 -0
- package/lib/types/client/ui/Toast.d.ts +36 -0
- package/lib/types/client/ui/Tooltip.d.ts +37 -0
- package/lib/types/client/ui/format.d.ts +18 -0
- package/lib/types/client/ui/icons.d.ts +33 -0
- package/lib/types/client/ui/recipe.d.ts +11 -0
- package/lib/types/client/ui/styles.d.ts +2 -0
- package/lib/types/client/undo-toast.d.ts +49 -0
- package/lib/types/client/use-column-resize.d.ts +62 -0
- package/lib/types/client/use-panel-width.d.ts +15 -0
- package/lib/{index.d.ts → types/index.d.ts} +9 -36
- package/lib/types/log/session-log.d.ts +40 -0
- package/lib/types/log/zstd-frames.d.ts +30 -0
- package/lib/types/ports/fs-port.d.ts +16 -0
- package/lib/types/ports/host-port.d.ts +85 -0
- package/lib/types/remote/map-error.d.ts +19 -0
- package/lib/types/shared/wire.d.ts +134 -0
- package/lib/types/trash/detail.d.ts +40 -0
- package/lib/types/trash/entry.d.ts +35 -0
- package/lib/types/trash/manifest.d.ts +18 -0
- package/lib/types/trash/reconcile.d.ts +35 -0
- package/lib/types/verbs/apply.d.ts +3 -0
- package/lib/types/verbs/delete-session.d.ts +30 -0
- package/lib/types/verbs/errors.d.ts +20 -0
- package/lib/types/verbs/purge-trash.d.ts +6 -0
- package/lib/types/verbs/restore-session.d.ts +19 -0
- package/locale/en.json +6 -6
- package/locale/zh.json +6 -6
- package/package.json +14 -9
- package/lib/client.js.map +0 -1
package/lib/client.js
CHANGED
|
@@ -146,6 +146,8 @@ window.__ModuleLoader__.load({
|
|
|
146
146
|
"trash.project.unknown": "未记录项目",
|
|
147
147
|
"trash.row.deletedNow": "刚刚删除",
|
|
148
148
|
"trash.row.deletedAgo": "删除于 {n}{unit}前",
|
|
149
|
+
"trash.row.subagents": "含 {n} 个子会话",
|
|
150
|
+
"trash.row.strandedSubagents": "盘上还剩 {n} 个子会话",
|
|
149
151
|
"trash.unit.minutes": "分钟",
|
|
150
152
|
"trash.unit.hours": "小时",
|
|
151
153
|
"trash.unit.days": "天",
|
|
@@ -169,10 +171,13 @@ window.__ModuleLoader__.load({
|
|
|
169
171
|
"trash.toolbar.selectExit": "退出多选",
|
|
170
172
|
"trash.select.count": "已选 {count} 条",
|
|
171
173
|
"trash.select.purge": "彻底删除 {count} 条",
|
|
172
|
-
"trash.select.
|
|
174
|
+
"trash.select.restore": "恢复 {count} 条",
|
|
175
|
+
"trash.select.needOne": "先勾选要处理的会话",
|
|
173
176
|
"trash.select.row": "勾选「{title}」",
|
|
174
177
|
"trash.confirm.bulk": "彻底删除 {count} 个会话?",
|
|
175
|
-
"trash.
|
|
178
|
+
"trash.confirm.restore": "恢复 {count} 个会话?",
|
|
179
|
+
"trash.confirm.restore.body": "这一批会回到它们各自的原项目目录,恢复到会话列表里。",
|
|
180
|
+
"trash.confirm.restoreNow": "恢复",
|
|
176
181
|
"detail.notSelected": "选中一条会话,看它讲了什么",
|
|
177
182
|
"detail.loading": "正在读这条会话…",
|
|
178
183
|
"detail.readFailed": "这条会话的正文读不出来",
|
|
@@ -204,8 +209,22 @@ window.__ModuleLoader__.load({
|
|
|
204
209
|
"menu.delete": "删除",
|
|
205
210
|
"menu.live": "会话正在运行,先停止再删除",
|
|
206
211
|
"dialog.title": "删除这个会话?",
|
|
212
|
+
"dialog.subagents": "含 {n} 个子会话,将一起移入回收站。",
|
|
207
213
|
"dialog.hint": "会话会移入回收站,之后可以随时恢复。彻底删除是回收站里的另一个动作。",
|
|
208
214
|
"dialog.confirm": "移入回收站",
|
|
215
|
+
"batch.row.pick": "勾选「{title}」以批量删除",
|
|
216
|
+
"batch.bar.count": "已选 {n} 条",
|
|
217
|
+
"batch.bar.pickHint": "点击会话行选择",
|
|
218
|
+
"batch.bar.kids": "含 {k} 个子会话,跟着整组走",
|
|
219
|
+
"batch.bar.review": "查看已选",
|
|
220
|
+
"batch.bar.reviewClose": "收起已选",
|
|
221
|
+
"batch.bar.remove": "取消勾选「{title}」",
|
|
222
|
+
"batch.bar.selectAll": "全选",
|
|
223
|
+
"batch.bar.selectNone": "取消全选",
|
|
224
|
+
"batch.run": "移入回收站 {n} 条",
|
|
225
|
+
"dialog.batch.title": "删除这些 {n} 个会话?",
|
|
226
|
+
"dialog.batch.retry": "重试这 {n} 条",
|
|
227
|
+
"undo.movedBatch": "{n} 条会话已移入回收站",
|
|
209
228
|
"undo.moved": "{title} 已移入回收站",
|
|
210
229
|
"undo.session": "会话",
|
|
211
230
|
"undo.undo": "撤销"
|
|
@@ -229,6 +248,8 @@ window.__ModuleLoader__.load({
|
|
|
229
248
|
"trash.project.unknown": "Unrecorded project",
|
|
230
249
|
"trash.row.deletedNow": "Deleted just now",
|
|
231
250
|
"trash.row.deletedAgo": "Deleted {n}{unit} ago",
|
|
251
|
+
"trash.row.subagents": "has {n} subagent sessions",
|
|
252
|
+
"trash.row.strandedSubagents": "{n} subagent sessions still on disk",
|
|
232
253
|
"trash.unit.minutes": "min",
|
|
233
254
|
"trash.unit.hours": "h",
|
|
234
255
|
"trash.unit.days": "d",
|
|
@@ -252,10 +273,13 @@ window.__ModuleLoader__.load({
|
|
|
252
273
|
"trash.toolbar.selectExit": "Done selecting",
|
|
253
274
|
"trash.select.count": "{count} selected",
|
|
254
275
|
"trash.select.purge": "Delete {count} permanently",
|
|
255
|
-
"trash.select.
|
|
276
|
+
"trash.select.restore": "Restore {count}",
|
|
277
|
+
"trash.select.needOne": "Pick the sessions to work on first",
|
|
256
278
|
"trash.select.row": "Select “{title}”",
|
|
257
279
|
"trash.confirm.bulk": "Delete these {count} sessions permanently?",
|
|
258
|
-
"trash.
|
|
280
|
+
"trash.confirm.restore": "Restore these {count} sessions?",
|
|
281
|
+
"trash.confirm.restore.body": "They go back to their original project folders and reappear in the session list.",
|
|
282
|
+
"trash.confirm.restoreNow": "Restore",
|
|
259
283
|
"detail.notSelected": "Select a session to see what it was about",
|
|
260
284
|
"detail.loading": "Reading this session…",
|
|
261
285
|
"detail.readFailed": "Could not read this session’s log",
|
|
@@ -287,8 +311,22 @@ window.__ModuleLoader__.load({
|
|
|
287
311
|
"menu.delete": "Delete",
|
|
288
312
|
"menu.live": "This session is running. Stop it first.",
|
|
289
313
|
"dialog.title": "Delete this session?",
|
|
314
|
+
"dialog.subagents": "It also moves {n} subagent sessions to the trash.",
|
|
290
315
|
"dialog.hint": "The session moves to the trash and can be restored at any time. Permanent deletion is a separate action in the trash.",
|
|
291
316
|
"dialog.confirm": "Move to trash",
|
|
317
|
+
"batch.row.pick": "Select “{title}” for batch delete",
|
|
318
|
+
"batch.bar.count": "{n} selected",
|
|
319
|
+
"batch.bar.pickHint": "Click a session row to select",
|
|
320
|
+
"batch.bar.kids": "{k} subagent sessions come along",
|
|
321
|
+
"batch.bar.review": "Review selected",
|
|
322
|
+
"batch.bar.reviewClose": "Hide list",
|
|
323
|
+
"batch.bar.remove": "Unselect “{title}”",
|
|
324
|
+
"batch.bar.selectAll": "Select all",
|
|
325
|
+
"batch.bar.selectNone": "Deselect all",
|
|
326
|
+
"batch.run": "Move {n} to trash",
|
|
327
|
+
"dialog.batch.title": "Delete these {n} sessions?",
|
|
328
|
+
"dialog.batch.retry": "Retry these {n}",
|
|
329
|
+
"undo.movedBatch": "{n} sessions were moved to the trash",
|
|
292
330
|
"undo.moved": "{title} was moved to the trash",
|
|
293
331
|
"undo.session": "the session",
|
|
294
332
|
"undo.undo": "Undo"
|
|
@@ -379,7 +417,7 @@ window.__ModuleLoader__.load({
|
|
|
379
417
|
* ② 跨插件一律 `import type`——本文件干脆没有 import。
|
|
380
418
|
*
|
|
381
419
|
* 信封(`RemoteResult`)与失败形状(`RemoteFailure`)也在这里——**因为它们本来就是两半的共同契约**:
|
|
382
|
-
* Node 半按它产出(`src/api/dispatch.ts` 的失败支就是 `RemoteFailure`
|
|
420
|
+
* Node 半按它产出(`src/api/dispatch.ts` 的失败支就是 `RemoteFailure` 那三件),浏览器半按它判别
|
|
383
421
|
* (`src/client/remote.ts` 的 `unwrapRemoteResult` / `failureOf`)。形状取自宿主网关客户端面的原文
|
|
384
422
|
* 「每次 unary 调用 resolve 成 RemoteResult、不为载体问题 reject」,但**它是本仓自己的 wire 契约**,
|
|
385
423
|
* 不是对宿主模块的 import。
|
|
@@ -392,20 +430,29 @@ window.__ModuleLoader__.load({
|
|
|
392
430
|
* 撞了就是整棵插件树 boot 失败——所以这条路径是与生态的硬边界,不是显示好不好看的问题。
|
|
393
431
|
*/
|
|
394
432
|
const SESSION_DELETE_API_PREFIX = "/dsh-session-delete-api";
|
|
395
|
-
/**
|
|
396
|
-
*
|
|
433
|
+
/** 五个段名(相对前缀)。2026-10-17 批量重构:写动词从「一条一段」收成**一段 `/apply`**——
|
|
434
|
+
* 一次 HTTP 带一组 id、逐条回结果;单条就是长度为 1 的批量,仓里不再留第二条执行路径。
|
|
435
|
+
* - `GET /list`:**对账并读**清单(`session-trash/` 里的目录是准)。
|
|
436
|
+
* - `GET /ping`:最便宜的在/不在(不碰清册、不碰盘、不进链)——客户端探测用它,不用一次全表读。
|
|
437
|
+
* - `POST /detail`:单条详情(用 POST 表达一个读:见设计稿 2026-10-15 §3)。
|
|
438
|
+
* - `POST /apply`:批量执行一个动作(`delete` / `restore` / `purge` / `count`),逐条回 `ApplyOutcome`。
|
|
439
|
+
* - `POST /empty`:清空回收站(聚合回执,**不**按条回)。 */
|
|
397
440
|
const ROUTES = {
|
|
398
441
|
list: "/list",
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
442
|
+
/** 通道的"在不在"专用:没有入参、没有副作用、没有磁盘读。 */
|
|
443
|
+
ping: "/ping",
|
|
444
|
+
/** 单条详情(读回收站目录里那条会话自己的日志)。 */
|
|
445
|
+
detail: "/detail",
|
|
446
|
+
/** 一组 id 上的同一个动作;删除确认框那句「含 N 个子会话」也走这里(`action: 'count'`,纯读语义)。 */
|
|
447
|
+
apply: "/apply",
|
|
448
|
+
/** 清空回收站。 */
|
|
449
|
+
empty: "/empty"
|
|
404
450
|
};
|
|
405
|
-
/** 请求体字段名:浏览器半发、Node 半读,同名同义(`
|
|
451
|
+
/** 请求体字段名:浏览器半发、Node 半读,同名同义(`empty` 没有入参)。 */
|
|
406
452
|
const PARAMS = {
|
|
407
|
-
|
|
408
|
-
|
|
453
|
+
entryId: "entryId",
|
|
454
|
+
action: "action",
|
|
455
|
+
ids: "ids"
|
|
409
456
|
};
|
|
410
457
|
/**
|
|
411
458
|
* 进程内判别「这已经是一次失败」——**按标记位认,不按 `instanceof`**(跨 realm / 跨 bundle 都不成立)。
|
|
@@ -441,7 +488,8 @@ window.__ModuleLoader__.load({
|
|
|
441
488
|
* 加上信封判别与注入面那一处「展开成裸值」的适配。
|
|
442
489
|
*
|
|
443
490
|
* 通道形状(裁决):真相/账本/判据全在宿主那边,Node 半用宿主**自己的 web server** 开一条
|
|
444
|
-
*
|
|
491
|
+
* 插件专属前缀路由把五个段名(`list` / `ping` / `detail` / `apply` / `empty`)搬出来
|
|
492
|
+
* (`src/api/dispatch.ts`),浏览器半同源调用。**不是重造后端**:
|
|
445
493
|
* 路由层只做搬运与信封。为什么不用官方的 `remote.<namespace>`:那条路要**生成的 Typert 远程描述符**
|
|
446
494
|
* 加一次 `$mount`,而盒内装配清单写死在 `dsh-api-remotes` 里、全宿主没有动态发现,第三方只能自挂自,
|
|
447
495
|
* 对一个薄插件不划算。
|
|
@@ -453,24 +501,29 @@ window.__ModuleLoader__.load({
|
|
|
453
501
|
* 选它是为了让组件面、`unwrapRemoteResult` 与既有用例一行都不用改。失败是**返回**而不是
|
|
454
502
|
* reject(同宿主网关原文:"Every unary call resolves to `RemoteResult<T>` … and never rejects for a
|
|
455
503
|
* carrier problem");只有往返本身失败(打不通、回的不是信封)才 reject,那是**通道缺席**。
|
|
456
|
-
* 2. **不许值 import 跨插件包**。`error` 在线上就是一段 JSON(`{code, message}`),不是
|
|
457
|
-
*
|
|
458
|
-
*
|
|
459
|
-
* Node 半(`src/remote/map-error.ts
|
|
460
|
-
*
|
|
504
|
+
* 2. **不许值 import 跨插件包**。`error` 在线上就是一段 JSON(`{code, message, details?}`),不是
|
|
505
|
+
* `RemoteError` 实例——所以判别**所依据的那一个标记位**只镜一份,家在两半共用的
|
|
506
|
+
* `src/shared/wire.ts`(`isDSHRemoteError`)。浏览器半落到本地时用 `throwRemoteFailure` 就地补上
|
|
507
|
+
* Error 本体与标记位;Node 半(`src/remote/map-error.ts`)只用它做「这已经是一次远程失败」的判别,
|
|
508
|
+
* 产出的是线上那三件(`details` 可缺席)、不造任何 Error,更不 import 宿主那个类。
|
|
461
509
|
* 3. **失败原文必须能上屏**。调用方(`callRemote`)只把**抛出来的失败**收成判别式状态——
|
|
462
510
|
* `{ ok: false, error }` 不是第三种落地:它在注入面那一处已经被 `unwrapRemoteResult` 抛成了异常,
|
|
463
511
|
* 所以到了组件面前只剩「裸值 / throw」两支,调用方只写 `if (!outcome.ok)`,正常流里没有 try/catch;
|
|
464
512
|
* 非本信封形状的异常(网络层、TypeError…)也不丢原文(见 `failureOf`)。
|
|
465
513
|
*
|
|
466
|
-
* 面(`list
|
|
467
|
-
* `src/shared/wire.ts`);`list
|
|
514
|
+
* 面(`list` / `ping` / `detail` / `apply` / `empty`)与 Node 半路由的五个段名一一对应(段名本身的家是
|
|
515
|
+
* `src/shared/wire.ts`);`list` 回一行行的条目,`ping` 只回一个探针值,`detail` 回单条正文,
|
|
516
|
+
* `apply` 回**一组逐条结果**,`empty` 回聚合回执。2026-10-17 之前是六个动作段(一条一段),
|
|
517
|
+
* 批量只能由调用方循环拼——那正是「恢复 100 条很慢」的形状来源,收成一段之后这条通道不再有
|
|
518
|
+
* "第二条执行路径"。
|
|
468
519
|
*/
|
|
469
|
-
/** 任意异常 → 失败面。非带标记的失败(网络层、TypeError
|
|
520
|
+
/** 任意异常 → 失败面。非带标记的失败(网络层、TypeError…)也保留原文,绝不吞;带标记的失败把
|
|
521
|
+
* `details` 第三件一起带出。 */
|
|
470
522
|
function failureOf(reason) {
|
|
471
523
|
if (isDSHRemoteError(reason)) return {
|
|
472
524
|
code: reason.code,
|
|
473
|
-
message: String(reason.message)
|
|
525
|
+
message: String(reason.message),
|
|
526
|
+
...reason.details === void 0 ? {} : { details: reason.details }
|
|
474
527
|
};
|
|
475
528
|
if (reason instanceof Error) return {
|
|
476
529
|
code: "client/unknown",
|
|
@@ -487,14 +540,15 @@ window.__ModuleLoader__.load({
|
|
|
487
540
|
* 后半句与网关客户端面同形:那边 `error` 是活的 `RemoteError`、`throw result.error` 保持 throw 语义,
|
|
488
541
|
* 网关自己的重建函数 `rebuiltFailure(error) { return new RemoteError(error.code, error.message,
|
|
489
542
|
* error.details) }` 就干这件事。这里多一层:线上递过来的失败必然是**普通对象**(JSON)而不是本 bundle
|
|
490
|
-
* 的 Error 实例,所以就地造一个并打上同一个标记位——**code 与 message
|
|
491
|
-
*
|
|
543
|
+
* 的 Error 实例,所以就地造一个并打上同一个标记位——**code 与 message 都不丢**,`details` 可选地
|
|
544
|
+
* 跟着走(`code` 是判别依据,`message` 是上屏的原文,`details` 是结构化附带物,见 `shared/wire.ts`)。
|
|
492
545
|
*/
|
|
493
546
|
function throwRemoteFailure(error) {
|
|
494
547
|
const candidate = error;
|
|
495
548
|
if (isDSHRemoteError(candidate)) throw candidate;
|
|
496
549
|
throw Object.assign(new Error(String(error.message)), {
|
|
497
550
|
code: error.code,
|
|
551
|
+
...error.details === void 0 ? {} : { details: error.details },
|
|
498
552
|
isDSHRemoteError: true
|
|
499
553
|
});
|
|
500
554
|
}
|
|
@@ -554,175 +608,108 @@ window.__ModuleLoader__.load({
|
|
|
554
608
|
});
|
|
555
609
|
return {
|
|
556
610
|
list: () => requestEnvelope(prefix + ROUTES.list, { method: "GET" }),
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
611
|
+
ping: () => requestEnvelope(prefix + ROUTES.ping, { method: "GET" }),
|
|
612
|
+
detail: (entryId) => requestEnvelope(prefix + ROUTES.detail, post({ [PARAMS.entryId]: entryId })),
|
|
613
|
+
apply: (action, ids) => requestEnvelope(prefix + ROUTES.apply, post({
|
|
614
|
+
[PARAMS.action]: action,
|
|
615
|
+
[PARAMS.ids]: [...ids]
|
|
616
|
+
})),
|
|
617
|
+
empty: () => requestEnvelope(prefix + ROUTES.empty, post({}))
|
|
561
618
|
};
|
|
562
619
|
}
|
|
563
620
|
/**
|
|
564
|
-
* 取数通道探测:一次 `
|
|
621
|
+
* 取数通道探测:一次 `GET /ping` 走通(或走到)就是"通道在"。
|
|
565
622
|
*
|
|
566
623
|
* 判据是**「拿到信封」而不是「操作成功」**:`{ok:false, error}` 也是一次成功的往返(路由在、依赖接上了、
|
|
567
624
|
* 错误映射也生效了),那时该做的是把入口显示出来、让原文上屏;只有往返本身失败才算通道缺席 ⇒
|
|
568
625
|
* 一个槽位贡献都不注册(设计稿 §7「未接通态」)。
|
|
626
|
+
*
|
|
627
|
+
* 2026-10-17 第四批:探测从"读一遍清单"改成这一问——**每次页面加载都跑全表对账**是白付的钱,
|
|
628
|
+
* 而"信封到不到"这件事一次零成本的 GET 就答得出来。
|
|
569
629
|
*/
|
|
570
630
|
async function probeChannel(raw) {
|
|
571
631
|
try {
|
|
572
|
-
await raw.
|
|
632
|
+
await raw.ping();
|
|
573
633
|
return true;
|
|
574
634
|
} catch {
|
|
575
635
|
return false;
|
|
576
636
|
}
|
|
577
637
|
}
|
|
578
638
|
/**
|
|
579
|
-
* 注入面的**唯一一处适配**:`RemoteResult` →
|
|
639
|
+
* 注入面的**唯一一处适配**:`RemoteResult` → 裸值,外加「一段批量」→「单条便捷面」的展开。
|
|
580
640
|
*
|
|
581
641
|
* **为什么必须有这一步**:通道那头的调用 resolve 成 `RemoteResult<T>`(`{ ok: true, value }` /
|
|
582
642
|
* `{ ok: false, error }`),失败是**返回**而不是 reject。组件面要的是「拿到值 / 抛异常」,
|
|
583
643
|
* 所以在这里展开一次:`ok ? value : throw`。放在这一处而不是组件里,是为了让三个组件的 props 契约
|
|
584
644
|
* (以及钉住它们的用例)保持裸值形状不变。
|
|
645
|
+
*
|
|
646
|
+
* **单条支里只有一条落地**:没有它(幽灵 id、或路由整次没办成)就 throw——"批量只回了一条别的"
|
|
647
|
+
* 不是成功,也不是 0。
|
|
585
648
|
*/
|
|
586
649
|
function buildDeps(raw) {
|
|
650
|
+
/** 一次 `apply`:顶层信封先展开(那是"这次调用办没办成"),逐条结果原样交给调用方。 */
|
|
651
|
+
const applyOnce = async (action, ids) => {
|
|
652
|
+
const result = await raw.apply(action, ids);
|
|
653
|
+
if (!result.ok) throwRemoteFailure(result.error);
|
|
654
|
+
return result.value;
|
|
655
|
+
};
|
|
656
|
+
/** N=1 的便捷支:**按发出去的那个 id** 取结果(取"第一条"会把别条的成败安在这条头上)。
|
|
657
|
+
* 一条没回是**通道的错**,不是"没做成"(后者住在 outcome 的 `ok:false` 里)。 */
|
|
658
|
+
const applyOne = async (action, id) => {
|
|
659
|
+
const outcome = (await applyOnce(action, [id])).find((candidate) => candidate.id === id);
|
|
660
|
+
if (outcome === void 0) throw new Error(`批量通道没回这一条的结果:${action} ${id}`);
|
|
661
|
+
if (!outcome.ok) throwRemoteFailure(outcome.error);
|
|
662
|
+
return outcome;
|
|
663
|
+
};
|
|
587
664
|
return {
|
|
588
665
|
list: () => raw.list().then((result) => unwrapRemoteResult(result)),
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
//#endregion
|
|
596
|
-
//#region src/client/trash-store.ts
|
|
597
|
-
/**
|
|
598
|
-
* 回收站清单的**失效通告台**:动过回收站的动作戳一下,清单面板据此重新取数。
|
|
599
|
-
*
|
|
600
|
-
* 为什么需要它(真机反馈):回收站面板是「挂载时取一次数」(`useEffect(reload)`),而删除发生在
|
|
601
|
-
* **侧栏菜单**那边(`delete-confirm.tsx` 的常驻宿主),两者没有共同的父组件——删完之后中央那个
|
|
602
|
-
* 面板(如果正开着)不会自己重读,用户得切走再切回来(= 重新挂载)才看得见新条目。撤销(`undo-toast.tsx`)
|
|
603
|
-
* 同理。
|
|
604
|
-
*
|
|
605
|
-
* 形状与两个邻居同款(`undo-toast.tsx` 的通知台、`delete-confirm.tsx` 的现场):**模块级现场 +
|
|
606
|
-
* `useSyncExternalStore`**,不为此引状态库、也不引入上下文——三样东西都挂在 `shell.overlay`/`main`
|
|
607
|
-
* 这些各不相邻的座位上,模块级是它们唯一的共同点。
|
|
608
|
-
*
|
|
609
|
-
* 语义只有"变过"这一件事:一个单调递增的修订号。面板把它写进 effect 的依赖里,戳一次重读一次;
|
|
610
|
-
* 不需要知道是谁动的、动了什么(清单本体永远是问宿主要)。
|
|
611
|
-
*/
|
|
612
|
-
let revision = 0;
|
|
613
|
-
const listeners$2 = /* @__PURE__ */ new Set();
|
|
614
|
-
/** 通告一次"回收站变过了"(删除成功、撤销成功;面板自己的恢复/彻底删除则就地重读)。 */
|
|
615
|
-
function invalidateTrash() {
|
|
616
|
-
revision += 1;
|
|
617
|
-
for (const listener of listeners$2) listener();
|
|
618
|
-
}
|
|
619
|
-
function getTrashRevision() {
|
|
620
|
-
return revision;
|
|
621
|
-
}
|
|
622
|
-
function subscribeTrash(listener) {
|
|
623
|
-
listeners$2.add(listener);
|
|
624
|
-
return () => {
|
|
625
|
-
listeners$2.delete(listener);
|
|
666
|
+
detail: (entryId) => raw.detail(entryId).then((result) => unwrapRemoteResult(result)),
|
|
667
|
+
applyOutcomes: applyOnce,
|
|
668
|
+
emptyTrash: () => raw.empty().then((result) => unwrapRemoteResult(result)),
|
|
669
|
+
delete: async (sessionId) => (await applyOne("delete", sessionId)).value,
|
|
670
|
+
restore: async (entryId) => (await applyOne("restore", entryId)).value,
|
|
671
|
+
countSubagents: async (sessionId) => (await applyOne("count", sessionId)).value
|
|
626
672
|
};
|
|
627
673
|
}
|
|
628
|
-
/** 供面板订阅。 */
|
|
629
|
-
function useTrashRevision() {
|
|
630
|
-
return (0, react.useSyncExternalStore)(subscribeTrash, getTrashRevision, getTrashRevision);
|
|
631
|
-
}
|
|
632
|
-
//#endregion
|
|
633
|
-
//#region src/client/ui/ui.css?dsh-css-text
|
|
634
|
-
var ui_default = "/*\r\n * 本插件自己的控件样式(**不再 require 宿主的 `@deepseek-ai/dsh-client-ui-primitives`**)。\r\n *\r\n * 出处:逐条抄自 `@deepseek-ai/dsh-client-ui-primitives@0.2.0-rc.2` 随包发布的明文 CSS\r\n * (`lib/Button.module.css`、`Tag.module.css`、`Modal.module.css`、`Tooltip.module.css`、\r\n * `Toast.module.css`、`Menu.module.css`;2026-10-15 又加两张:`lib/Checkbox.module.css`、\r\n * `lib/DisclosureRow.module.css`,以及 `dsh-client-ui-conversation` 里的\r\n * `ConversationRoot.module.css` 的 `.widthHandle` 那一族——宿主自己那根可拖的宽度把手)。\r\n * 这是官方给第三方 UI 插件的处方:\r\n * 「copy markup, CSS, and behavior from the primitive into the plugin … Rename copied classes under\r\n * your plugin's prefix, keep only `--dsw-alias-*` token references」(宿主技能\r\n * `cordis-plugin-development/references/practices.md` 原文)。理由是那个包**随宿主换代而变、且不经类型检查**,\r\n * 一个抛错的组件会把整个槽位条目打空(控制台 `slot entry crashed in '<slot>'`)。\r\n *\r\n * 三条改写规则:\r\n * 1. **类名全部加本插件前缀 `sd-`**(原来是 CSS module 的哈希名,全局无冲突由前缀承担)。\r\n * 2. **配色一律走 token**,一个颜色的字面量都没有——所以宿主换主题、改名 token 时最差是难看,\r\n * 不会崩。几何量(圆角/阴影/层级)同样取 token;**只有字号/行高/尺寸是字面量 px**,与官方 CSS 逐字一致。\r\n * 3. **`@keyframes` 全部改名**(`sd-` 前缀):它们是全局命名空间,撞名会互相覆盖。\r\n *\r\n * 只收本插件真正用到的部分:Button 的 `primary`/`ghost`/`outline` 三档与 `md`/`sm` 两号\r\n * (没有 `toolbar` 变体、没有 `lg`)、Tag 的 `danger` 一档、Modal 的全部骨架、Tooltip 的三个方位、\r\n * Toast 全部、Menu 行的全部(含 danger 态与 separator)、Checkbox 的全部、DisclosureRow 的\r\n * 折叠头那一半(没有 `data-state` 那档状态点)、宽度把手的视觉与光标。**没抄进来的就是本插件不用的**,\r\n * 不预先铺开——宿主那套里还有几十个变体与控件,抄全了等于把它的表面积整个搬进来。\r\n *\r\n * **两处不是抄本**:① `.sd-resizing`(拖动期间挂在面板根上、关掉文字选中)——宿主那份没有对应的类,\r\n * 它的把手由 JS 侧的 pointer capture 承担;② `[data-dsh-session-delete=\"trash-row\"]:focus-visible`\r\n * (清单那一行的键盘焦点描边,复用选中态那枚 `ghost-active-border`)——宿主那套表里行不是可聚焦\r\n * div,我们这一行是 `role=\"option\"` 且要吃 ↑↓,而这一代 `--dsw-focus-ring-color` 是 `transparent`。\r\n * 整张表里就这两条是我们自造的,标在这里,\r\n * 下次做「与官方逐条比对」时它属于**已知差异**,不是漂移。\r\n */\r\n\r\n/* ────────────────────────── Button ────────────────────────── */\r\n\r\n.sd-button {\r\n box-sizing: border-box;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n gap: 4px;\r\n border: none;\r\n border-radius: var(--dsw-radius-md);\r\n cursor: pointer;\r\n font-size: 14px;\r\n line-height: 22px;\r\n color: var(--dsw-alias-label-primary);\r\n background: transparent;\r\n padding: 0 14px;\r\n}\r\n\r\n.sd-button:disabled {\r\n cursor: not-allowed;\r\n opacity: 0.4;\r\n}\r\n\r\n.sd-button-md {\r\n height: 36px;\r\n}\r\n\r\n/* 紧凑档:尺寸不随填充/描边换档(官方原文的注释即此意)。 */\r\n.sd-button-sm {\r\n height: 28px;\r\n font-size: 12px;\r\n line-height: 18px;\r\n padding: 0 10px;\r\n border-radius: var(--dsw-radius-sm);\r\n}\r\n\r\n.sd-button-primary {\r\n background: var(--dsw-alias-button-primary-fill);\r\n color: var(--dsw-alias-label-primary-foreground);\r\n}\r\n\r\n.sd-button-primary:hover:not(:disabled) {\r\n background: var(--dsw-alias-button-primary-hover);\r\n}\r\n\r\n.sd-button-ghost:hover:not(:disabled) {\r\n background: var(--dsw-alias-interactive-bg-hover);\r\n}\r\n\r\n.sd-button-ghost:active:not(:disabled) {\r\n background: var(--dsw-alias-interactive-bg-active);\r\n}\r\n\r\n.sd-button-outline {\r\n border: 0.5px solid var(--dsw-alias-border-l3);\r\n background: transparent;\r\n}\r\n\r\n.sd-button-outline:hover:not(:disabled) {\r\n background: var(--dsw-alias-interactive-bg-hover);\r\n}\r\n\r\n.sd-button-icon {\r\n display: inline-flex;\r\n width: 16px;\r\n height: 16px;\r\n align-items: center;\r\n justify-content: center;\r\n}\r\n\r\n/* ────────────────────────── Tag ────────────────────────── */\r\n\r\n/* 胶囊几何固定:一个 tag 在哪里都是同一个尺寸,只有色板随 tone 走。 */\r\n.sd-tag {\r\n display: inline-flex;\r\n align-items: center;\r\n border-radius: 999px;\r\n corner-shape: round;\r\n padding: 1px 8px;\r\n font-size: 11px;\r\n line-height: 17px;\r\n font-weight: 500;\r\n white-space: nowrap;\r\n}\r\n\r\n/* 状态色自带填充:底色由自身颜色 10% 混出,换色板时底与字一起走,不需要第二个 token。 */\r\n.sd-tag-danger {\r\n background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);\r\n color: var(--dsw-alias-state-error-primary);\r\n}\r\n\r\n/* ────────────────────────── Modal ────────────────────────── */\r\n\r\n/* 整视口层(figma Mask + Dialog):蒙层 + 居中卡片。四边留 24px,上下按外壳公布的浮层内缩加宽,\r\n 高卡片才不会顶到外壳。卡片按这个内边距盒取尺寸:消费方用 `max-height:100%` 收口,不自己算视口。 */\r\n.sd-modal-root {\r\n pointer-events: auto;\r\n position: fixed;\r\n inset: 0;\r\n z-index: 1000;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n padding: max(24px, var(--dsh-frame-overlay-top, 24px)) 24px;\r\n}\r\n\r\n/* 只有绘制避让原生标题栏;根仍然挡住背景点击。 */\r\n.sd-modal-mask {\r\n position: absolute;\r\n inset: var(--dsh-frame-chrome-top, 0px) 0 0;\r\n backdrop-filter: var(--dsw-mask-blur);\r\n}\r\n\r\n.sd-modal-mask::after {\r\n content: '';\r\n position: absolute;\r\n inset: 0;\r\n background: var(--dsw-alias-bg-mask-1);\r\n animation: sd-modal-enter var(--ds-transition-duration) var(--ds-ease-in-out);\r\n}\r\n\r\n.sd-modal-dialog {\r\n box-sizing: border-box;\r\n position: relative;\r\n z-index: 1;\r\n display: flex;\r\n flex-direction: column;\r\n gap: 20px;\r\n width: min(380px, 100%);\r\n padding: 0 0 24px;\r\n overflow: hidden;\r\n border: 0;\r\n border-radius: var(--dsw-radius-panel);\r\n background: var(--dsw-alias-bg-layer-2);\r\n box-shadow: var(--dsw-elevation-prominent);\r\n animation: sd-modal-enter var(--ds-transition-duration) var(--ds-ease-in-out);\r\n}\r\n\r\n@keyframes sd-modal-enter {\r\n from { opacity: 0; }\r\n to { opacity: 1; }\r\n}\r\n\r\n@media (prefers-reduced-motion: reduce) {\r\n .sd-modal-mask::after,\r\n .sd-modal-dialog { animation: none; }\r\n}\r\n\r\n.sd-modal-dialog:focus {\r\n outline: none;\r\n}\r\n\r\n.sd-modal-content {\r\n display: flex;\r\n flex-direction: column;\r\n width: 100%;\r\n}\r\n\r\n/* 头行:标题左、关闭钮右。 */\r\n.sd-modal-header {\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n gap: 8px;\r\n padding: 22px 14px 12px 24px;\r\n}\r\n\r\n.sd-modal-title {\r\n margin: 0;\r\n font-size: 16px;\r\n line-height: 24px;\r\n font-weight: 500;\r\n color: var(--dsw-alias-label-primary);\r\n}\r\n\r\n.sd-modal-close {\r\n flex: none;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n width: 28px;\r\n height: 28px;\r\n border: none;\r\n border-radius: var(--dsw-radius-sm);\r\n background: transparent;\r\n cursor: pointer;\r\n color: var(--dsw-alias-label-secondary);\r\n}\r\n\r\n.sd-modal-close:hover {\r\n background: var(--dsw-alias-interactive-bg-hover);\r\n}\r\n\r\n.sd-modal-body {\r\n display: flex;\r\n flex-direction: column;\r\n min-width: 0;\r\n margin-top: 20px;\r\n padding: 0 24px;\r\n}\r\n\r\n.sd-modal-footer {\r\n display: flex;\r\n align-items: center;\r\n justify-content: flex-end;\r\n gap: 8px;\r\n padding: 0 24px;\r\n}\r\n\r\n/* ────────────────────────── Tooltip ────────────────────────── */\r\n\r\n.sd-tooltip-bubble {\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 8px;\r\n position: fixed;\r\n z-index: 100;\r\n /* 定宽收缩只按 `left` 到视口右缘量,靠右的锚点会提前折行;\r\n `max-content` 只按文案定宽,再用半个视口收口。 */\r\n width: max-content;\r\n max-width: 50vw;\r\n padding: 3px 7px;\r\n border-radius: var(--dsw-radius-sm);\r\n background: var(--dsw-alias-tooltip-bg);\r\n color: var(--dsw-static-neutral-bluish-00);\r\n font-size: 13px;\r\n line-height: 20px;\r\n white-space: pre-line;\r\n /* 不可断的长 token(URL、路径)不许顶破 max-width。 */\r\n overflow-wrap: break-word;\r\n pointer-events: none;\r\n animation: sd-tooltip-in 150ms var(--ds-ease-in-out);\r\n}\r\n\r\n.sd-tooltip-bubble[data-portal] { z-index: 1100; }\r\n.sd-tooltip-label { min-width: 0; }\r\n\r\n.sd-tooltip-bubble[data-side='right'] {\r\n transform: translateY(-50%);\r\n}\r\n\r\n.sd-tooltip-bubble[data-side='bottom'] {\r\n transform: translateX(-50%);\r\n}\r\n\r\n.sd-tooltip-bubble[data-side='top'] {\r\n transform: translate(-50%, -100%);\r\n}\r\n\r\n@keyframes sd-tooltip-in {\r\n from { opacity: 0; }\r\n}\r\n\r\n@media (prefers-reduced-motion: reduce) {\r\n .sd-tooltip-bubble {\r\n animation: none;\r\n }\r\n}\r\n\r\n/* ────────────────────────── Toast ────────────────────────── */\r\n\r\n/* 淡出延迟由组件以 `--dsh-toast-hold` 给出 ⇒ 同一个值同时驱动卸载计时器与这段动画,\r\n 两者不可能再对不上。淡出**时长**仍需与组件里的 FADE_MS 一致(没有 owner 会改它)。 */\r\n.sd-toast {\r\n position: fixed;\r\n top: 40px;\r\n left: 50%;\r\n /* 高过图片灯箱底层的 1000:预览开着时报错也必须读得到。 */\r\n z-index: 1100;\r\n /* 通告不截点击。 */\r\n pointer-events: none;\r\n display: flex;\r\n align-items: center;\r\n gap: 10px;\r\n /* 定宽盒设了 `left` 只按右侧空间收缩(-50% 的位移发生在定尺之后)⇒ 靠窗边的横幅会提前折行。\r\n `max-content` 只按文案定宽,再由下面的 max-width 收口。 */\r\n width: max-content;\r\n max-width: min(640px, calc(100vw - 48px));\r\n padding: 12px 16px;\r\n border-radius: var(--dsw-radius-lg);\r\n /* 设计平台的 toast 表面在**两套主题下都是深的**(浅色 bluish-800 / 深色 bluish-750),\r\n 所以这里的字色与动作色是固定值,不随主题翻转。 */\r\n background: var(--dsw-alias-toast-bg);\r\n color: var(--dsw-alias-toast-label);\r\n font-size: 14px;\r\n line-height: 22px;\r\n box-shadow: var(--dsw-shadow-lv3);\r\n transform: translateX(-50%);\r\n animation:\r\n sd-toast-in 160ms ease-out,\r\n sd-toast-fade 1000ms ease var(--dsh-toast-hold, 3000ms) forwards;\r\n}\r\n\r\n.sd-toast-icon {\r\n display: grid;\r\n place-items: center;\r\n flex: none;\r\n color: var(--dsw-alias-state-warn-label);\r\n}\r\n\r\n.sd-toast-icon-success {\r\n color: var(--dsw-alias-state-success-primary);\r\n}\r\n\r\n.sd-toast-text {\r\n min-width: 0;\r\n}\r\n\r\n.sd-toast-action {\r\n display: inline;\r\n padding: 0;\r\n border: 0;\r\n background: none;\r\n color: var(--dsw-static-deepseek-400);\r\n font: inherit;\r\n cursor: pointer;\r\n /* 动作文字与周围文案之间的呼吸位。 */\r\n margin-inline: 3px;\r\n /* 横幅表面整体是穿透点击的,只有动作文字接指针。 */\r\n pointer-events: auto;\r\n}\r\n\r\n.sd-toast-action:hover {\r\n opacity: 0.8;\r\n}\r\n\r\n@keyframes sd-toast-in {\r\n from {\r\n opacity: 0;\r\n transform: translate(-50%, -6px);\r\n }\r\n\r\n to {\r\n opacity: 1;\r\n transform: translate(-50%, 0);\r\n }\r\n}\r\n\r\n@keyframes sd-toast-fade {\r\n to {\r\n opacity: 0;\r\n visibility: hidden;\r\n }\r\n}\r\n\r\n/* 减动效去掉滑入;延迟淡出(透明度变化、不是位移)仍然在定时卸载前收掉横幅。 */\r\n@media (prefers-reduced-motion: reduce) {\r\n .sd-toast {\r\n animation: sd-toast-fade 1000ms ease var(--dsh-toast-hold, 3000ms) forwards;\r\n }\r\n}\r\n\r\n/* ────────────────────────── 菜单行 ────────────────────────── */\r\n\r\n.sd-menu-item-wrap {\r\n position: relative;\r\n}\r\n\r\n/* 菜单格:紧凑文字、图标与选中间距。 */\r\n.sd-menu-item {\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n width: 100%;\r\n min-height: 34px;\r\n padding: 6px 8px;\r\n border: none;\r\n border-radius: var(--dsw-radius-md);\r\n background: transparent;\r\n cursor: pointer;\r\n font-size: 13px;\r\n line-height: 20px;\r\n color: var(--dsw-alias-label-primary);\r\n text-align: left;\r\n}\r\n\r\n.sd-menu-item:hover:not(:disabled) {\r\n background: var(--dsw-alias-interactive-bg-hover);\r\n}\r\n\r\n/* 方向键移动的是真焦点 ⇒ 键盘所在的那一行拿指针同款填充;填充就是这一行的焦点呈现,\r\n 浏览器默认的 ring 再叠一层就重了。 */\r\n.sd-menu-item:focus-visible:not(:disabled) {\r\n background: var(--dsw-alias-interactive-bg-hover);\r\n outline: none;\r\n}\r\n\r\n.sd-menu-item:disabled {\r\n opacity: 0.4;\r\n cursor: not-allowed;\r\n}\r\n\r\n.sd-menu-item-icon {\r\n display: inline-flex;\r\n flex: none;\r\n width: 14px;\r\n height: 14px;\r\n align-items: center;\r\n justify-content: center;\r\n color: var(--dsw-alias-menu-icon);\r\n}\r\n\r\n.sd-menu-item-icon svg {\r\n width: 14px;\r\n height: 14px;\r\n}\r\n\r\n.sd-menu-item-label {\r\n flex: 1;\r\n min-width: 0;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n}\r\n\r\n/* 破坏性行:error 字/图标 + danger 悬停底。 */\r\n.sd-menu-item-danger {\r\n color: var(--dsw-alias-state-error-primary);\r\n}\r\n\r\n.sd-menu-item-danger .sd-menu-item-icon {\r\n color: var(--dsw-alias-state-error-primary);\r\n}\r\n\r\n.sd-menu-item-danger:hover:not(:disabled) {\r\n background: var(--dsw-alias-interactive-bg-hover-danger);\r\n}\r\n\r\n.sd-menu-item-danger:focus-visible:not(:disabled) {\r\n background: var(--dsw-alias-interactive-bg-hover-danger);\r\n outline: none;\r\n}\r\n\r\n/* 分隔格:发丝线上下的间距。取 l2 而不是 l1——l1 在这个表面上几乎看不见。 */\r\n.sd-menu-separator {\r\n height: 0.5px;\r\n margin: 3px 2px;\r\n background: var(--dsw-alias-border-l2);\r\n}\r\n\r\n/* ────────────────────────── Checkbox ────────────────────────── */\r\n/* 抄 `lib/Checkbox.module.css` 全部四条声明,逐字。勾选框本体是**原生 input** + `accent-color`,\r\n 不是自绘的圆框——回收站多选态用它(清单行左侧那一格)。 */\r\n\r\n.sd-checkbox {\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 6px;\r\n font-size: 14px;\r\n line-height: 20px;\r\n color: var(--dsw-alias-label-primary);\r\n cursor: pointer;\r\n}\r\n\r\n.sd-checkbox input {\r\n flex: 0 0 auto;\r\n width: 16px;\r\n height: 16px;\r\n margin: 0;\r\n accent-color: var(--dsw-alias-brand-primary);\r\n cursor: inherit;\r\n}\r\n\r\n.sd-checkbox input:focus-visible {\r\n outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));\r\n outline-offset: 2px;\r\n}\r\n\r\n.sd-checkbox:has(input:disabled) {\r\n cursor: default;\r\n opacity: 0.5;\r\n}\r\n\r\n/* ────────────────────────── DisclosureRow(分组折叠头) ────────────────────────── */\r\n/* 抄 `lib/DisclosureRow.module.css` 的 `.root`/`.row`/`.leading`/`.title` 与那两枚 glyph 的\r\n 显隐。没抄进来的是 `StateDot`(`svg[data-state]`)那一档——它说\"这是状态标记不是文字装饰\",\r\n 我们的组头没有状态点。行高与 leading 盒走 `--dsh-content-font-delta` 那根轴,\r\n 标题字号走 secondary 档:宿主的\"设置里调正文字号\"会一起推着它走。 */\r\n\r\n.sd-disclosure {\r\n display: flex;\r\n flex-direction: column;\r\n width: 100%;\r\n min-width: 0;\r\n}\r\n\r\n.sd-disclosure-row {\r\n position: relative;\r\n overflow: hidden;\r\n display: flex;\r\n align-items: center;\r\n height: calc(24px + var(--dsh-content-font-delta, 0px));\r\n min-width: 0;\r\n color: var(--dsw-alias-label-tertiary);\r\n transition: color 100ms ease;\r\n}\r\n\r\n.sd-disclosure-row[data-expandable] {\r\n cursor: pointer;\r\n}\r\n\r\n.sd-disclosure-row:hover {\r\n color: var(--dsw-alias-label-secondary);\r\n}\r\n\r\n.sd-disclosure-leading {\r\n position: relative;\r\n flex: none;\r\n width: calc(16px + var(--dsh-content-font-delta, 0px));\r\n height: calc(16px + var(--dsh-content-font-delta, 0px));\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n margin-right: 6px;\r\n padding: 0;\r\n border: none;\r\n background: none;\r\n color: inherit;\r\n}\r\n\r\n.sd-disclosure-leading svg:not([data-state]) {\r\n width: calc(14px + var(--dsh-content-font-delta, 0px));\r\n height: calc(14px + var(--dsh-content-font-delta, 0px));\r\n}\r\n\r\nbutton.sd-disclosure-leading {\r\n cursor: pointer;\r\n}\r\n\r\n.sd-disclosure-title {\r\n flex: none;\r\n font-size: var(--dsh-content-font-size-secondary, 13px);\r\n line-height: calc(24px + var(--dsh-content-font-delta, 0px));\r\n color: inherit;\r\n}\r\n\r\n@media (prefers-reduced-motion: reduce) {\r\n .sd-disclosure-row {\r\n transition: none;\r\n }\r\n}\r\n\r\n/* ────────────────────────── 宽度把手(可拖的分栏线) ────────────────────────── */\r\n/* 抄 `ConversationRoot.module.css` 的 `.widthHandle` 一族:10px 的命中区、`col-resize` 光标、\r\n 平时不画、悬停或拖动时才浮出一条 2px 的 `--dsw-alias-scrollbar-bg-l1` 短标(那条渐变里\r\n `--dsh-width-handle-pointer-y` 的兜底是 50%,宿主靠 JS 跟着指针走;我们不跟,\r\n 停在中间——少一条 pointermove 上的写字,视觉上是同一族东西)。\r\n 与宿主的差别只有位置:它绝对定位在会话栏两侧,我们这根住在两栏之间的那一格 flex 位里,\r\n 所以 `top/bottom/position` 那三样交给内联布局,这里只留视觉与光标。 */\r\n\r\n.sd-width-handle {\r\n position: relative;\r\n z-index: 0;\r\n cursor: col-resize;\r\n align-self: stretch;\r\n flex: 0 0 10px;\r\n width: 10px;\r\n padding: 0;\r\n border: none;\r\n background: none;\r\n}\r\n\r\n.sd-width-handle::after {\r\n content: \"\";\r\n background: linear-gradient(\r\n to bottom,\r\n transparent calc(var(--dsh-width-handle-pointer-y, 50%) - 36px),\r\n var(--dsw-alias-scrollbar-bg-l1) calc(var(--dsh-width-handle-pointer-y, 50%) - 8px),\r\n var(--dsw-alias-scrollbar-bg-l1) calc(var(--dsh-width-handle-pointer-y, 50%) + 8px),\r\n transparent calc(var(--dsh-width-handle-pointer-y, 50%) + 36px)\r\n );\r\n opacity: 0;\r\n pointer-events: none;\r\n border-radius: 2px;\r\n width: 2px;\r\n position: absolute;\r\n top: 0;\r\n bottom: 0;\r\n left: 4px;\r\n}\r\n\r\n.sd-width-handle:hover::after,\r\n.sd-width-handle[data-dragging]::after {\r\n opacity: 1;\r\n}\r\n\r\n.sd-width-handle:focus-visible {\r\n outline: none;\r\n}\r\n\r\n.sd-width-handle:focus-visible::after {\r\n opacity: 1;\r\n}\r\n\r\n/* 本插件自造的第一条(不是抄本,见文件头点名):拖动期间整块面板别把文字选上了。 */\r\n.sd-resizing {\r\n user-select: none;\r\n cursor: col-resize;\r\n}\r\n\r\n/* 本插件自造的第二条(不是抄本,见文件头点名):清单行是 `role=\"option\"` 的可聚焦 div,而这一代\r\n `--dsw-focus-ring-color` 逐字是 `transparent`(设计稿 2026-10-15 §1.3)——不自己描一条,键盘走\r\n ↑↓ 选会话时就没有任何焦点可见性。描边**复用选中态那枚** `ghost-active-border`,不新造颜色;\r\n 0.5px 与 `outline-offset: 1px` 是行的描边宽度与一档外移,不动布局(outline 不进滚动区)。 */\r\n[data-dsh-session-delete=\"trash-row\"]:focus-visible {\r\n outline: 0.5px solid var(--dsw-alias-button-ghost-active-border);\r\n outline-offset: 1px;\r\n}\r\n";
|
|
635
|
-
//#endregion
|
|
636
|
-
//#region src/client/ui/markdown.css?dsh-css-text
|
|
637
|
-
var markdown_default = "/**\r\n * 回收站详情里**会话正文**的排版(2026-10-15 真机反馈后新增)。\r\n *\r\n * 为什么单独一个文件而不并进 `ui.css`:`ui.css` 是\"照官方 primitives 逐字抄来的那一份表\",\r\n * 那条 fidelity 核对(§8 门 4)要的是它与官方明文**逐属性零差异**,只许文件头点名的几类例外\r\n * (类名前缀、`@keyframes` 改名,外加它点名的两处自造);条数不钉在这里,钉了就会漂成假基线。这一份抄的是\r\n * **另一个来源**(宿主 markdown 那一张表),混进去就把那份核对弄脏了。\r\n *\r\n * **来源(逐字抄,只改选择器与下面点名的三处)**:\r\n * `app.asar` 里 `@deepseek-ai/dsh-client-ui-primitives/lib/markdown/MarkdownText.module.css`\r\n * (`.markdown` 那一支;`.compact` 一支我们不用)+ `lib/markdown/CodeBlock.module.css` 的 `pre`。\r\n * token 名一个没换:`--dsw-font-markdown-*` / `--dsw-alias-markdown-*` / `--dsh-content-font-*`\r\n * 全是宿主会话区自己吃的那组,所以宿主调内容字号时,这里跟着一起动。\r\n *\r\n * 三处**有意**的差异,每条都是因为我们的 DOM 长不出宿主那个节点:\r\n * ① 宿主给表包一层 `.tableScroll`(div)再横向滚动;react-markdown 不产这层 ⇒ 滚动挂在 `table`\r\n * 自己身上(`display:block` + 匿名表盒)。这条得真机量过才算成立。\r\n * ② 宿主的代码卡有 banner 行(语言名 + 复制),四个圆角被拆成\"上圆角在 banner、下圆角在 pre\";\r\n * 我们没有 banner ⇒ 圆角整块给 `pre`。\r\n * ③ 宿主的链接可点(点开外链/文件),我们不碰宿主导航,链接渲成不可点的文本 ⇒ 保留链接色与字重,\r\n * **去掉** hover 的下划线(那是\"点我\"的邀请)。\r\n *\r\n * 抄过来但用不上的选择器已删:`.fileLink` / `.fileMention` / `.linkIcon` / `.image` / `.imageAlt`\r\n * (图片渲成 `[图片 · alt]`)/ `.katex-display` / `:global(.md-code-block)` / `.shiki`(高亮由宿主\r\n * 的 highlighter 提供,我们没有)。\r\n */\r\n.sd-md {\r\n min-width: 0;\r\n overflow-wrap: anywhere;\r\n font: var(--dsw-font-markdown-base);\r\n color: var(--dsw-alias-label-primary);\r\n}\r\n\r\n.sd-md strong {\r\n font-weight: 600;\r\n}\r\n\r\n.sd-md h1 {\r\n font: var(--dsw-font-markdown-h1);\r\n margin: 32px 0 16px;\r\n}\r\n\r\n.sd-md h2 {\r\n font: var(--dsw-font-markdown-h2);\r\n margin: 32px 0 16px;\r\n}\r\n\r\n.sd-md h3 {\r\n font: var(--dsw-font-markdown-h3);\r\n margin: 32px 0 16px;\r\n}\r\n\r\n.sd-md h4 {\r\n font: var(--dsw-font-markdown-h4);\r\n margin: 16px 0;\r\n}\r\n\r\n.sd-md :where(h5, h6) {\r\n font: var(--dsw-font-markdown-base-strong);\r\n margin: 16px 0;\r\n}\r\n\r\n.sd-md :where(h1, h2, h3, h4, h5, h6) strong {\r\n font-weight: inherit;\r\n}\r\n\r\n.sd-md p {\r\n margin: 16px 0;\r\n}\r\n\r\n/* Tighten h4–h6 against a following list (design: 8px gap). */\r\n.sd-md :where(h4, h5, h6) + :where(ul, ol) {\r\n margin-top: 8px;\r\n}\r\n\r\n.sd-md :where(h4, h5, h6):has(+ :where(ul, ol)) {\r\n margin-bottom: 8px;\r\n}\r\n\r\n/* 见文件头差异③:颜色与字重照抄,hover 的 dotted 下划线不抄——这里没有可点的东西。 */\r\n.sd-md-link {\r\n color: var(--dsw-alias-link);\r\n font-weight: 500;\r\n}\r\n\r\n.sd-md :where(ul, ol) {\r\n margin: 16px 0;\r\n padding-left: 18px;\r\n}\r\n\r\n.sd-md li:not(:first-child) {\r\n margin-top: 6px;\r\n}\r\n\r\n.sd-md li > :where(ul, ol) {\r\n margin-top: 4px;\r\n}\r\n\r\n.sd-md li::marker {\r\n line-height: 24px;\r\n color: var(--dsw-alias-label-secondary);\r\n}\r\n\r\n/* Nested ol under ul/ol: markers inside (models sometimes emit this shape). */\r\n.sd-md :where(ul, ol) ol {\r\n list-style-position: inside;\r\n padding-left: 0;\r\n}\r\n\r\n.sd-md :where(ul, ol) ol li p {\r\n display: inline;\r\n}\r\n\r\n.sd-md li > p {\r\n margin: 8px 0;\r\n}\r\n\r\n.sd-md li > *:first-child {\r\n margin-top: 0;\r\n}\r\n\r\n.sd-md li > *:last-child {\r\n margin-bottom: 0;\r\n}\r\n\r\n.sd-md hr {\r\n display: block;\r\n border: none;\r\n height: 0.5px;\r\n margin: 32px 0;\r\n background: var(--dsw-alias-border-l2);\r\n}\r\n\r\n.sd-md blockquote {\r\n border-left: 2px solid var(--dsw-alias-label-caption);\r\n margin: 16px 0 0;\r\n padding-left: 14px;\r\n}\r\n\r\n/* 代码卡:宿主的卡片由 `.block`(底色+圆角)与其内 `pre`(字号+内边距)两层拼成。\r\n 我们只有 `pre` 这一层,所以两层的声明合成在这里——见文件头差异②。 */\r\n.sd-md pre {\r\n font: var(--dsw-font-markdown-code-block);\r\n margin: 16px 0;\r\n padding: 16px;\r\n color: var(--dsw-alias-label-primary);\r\n background: var(--dsw-alias-markdown-code-block);\r\n border-radius: var(--dsw-radius-lg);\r\n overflow-x: auto;\r\n white-space: pre-wrap;\r\n word-break: break-all;\r\n}\r\n\r\n.sd-md pre code {\r\n font: inherit;\r\n background: none;\r\n padding: 0;\r\n}\r\n\r\n.sd-md :not(pre) > code {\r\n display: inline-flex;\r\n align-items: center;\r\n box-sizing: border-box;\r\n font: var(--dsw-font-markdown-code);\r\n font-family: var(--ds-font-family-code);\r\n font-size: 0.875em !important;\r\n background-color: var(--dsw-alias-markdown-inline-code);\r\n border: 0.5px solid var(--dsw-alias-border-l1);\r\n border-radius: var(--dsw-radius-sm);\r\n padding: 0 5px;\r\n}\r\n\r\n.sd-md :where(h1, h2, h3, h4, h5, h6) code {\r\n font: inherit;\r\n font-family: var(--ds-font-family-code);\r\n}\r\n\r\n.sd-md input[type='checkbox'] {\r\n margin: 0 8px 0 0;\r\n accent-color: var(--dsw-alias-label-secondary);\r\n}\r\n\r\n/* 见文件头差异①:宿主是 `.tableScroll > table`,我们没有那层 div。 */\r\n.sd-md table {\r\n display: block;\r\n box-sizing: border-box;\r\n max-width: 100%;\r\n overflow-x: auto;\r\n overscroll-behavior-x: contain;\r\n border-collapse: collapse;\r\n width: max-content;\r\n}\r\n\r\n.sd-md th {\r\n text-align: start;\r\n padding: 10px 16px;\r\n border-bottom: 0.5px solid var(--dsw-alias-border-l3);\r\n border-top: none;\r\n font: var(--dsw-font-markdown-table-head);\r\n max-width: 320px;\r\n max-width: min(30vw, 320px);\r\n min-width: 100px;\r\n}\r\n\r\n.sd-md td {\r\n padding: 10px 16px;\r\n border-bottom: 0.5px solid var(--dsw-alias-border-l2);\r\n font: var(--dsw-font-markdown-table);\r\n max-width: 320px;\r\n max-width: min(30vw, 320px);\r\n min-width: 100px;\r\n}\r\n\r\n.sd-md th:first-child,\r\n.sd-md td:first-child {\r\n padding-left: 0;\r\n}\r\n\r\n.sd-md td:last-child {\r\n padding-right: 0;\r\n}\r\n\r\n.sd-md table code {\r\n font-size: 11px;\r\n}\r\n\r\n.sd-md > *:first-child,\r\n.sd-md p:first-child {\r\n margin-top: 0 !important;\r\n}\r\n\r\n.sd-md > *:last-child,\r\n.sd-md p:last-child {\r\n margin-bottom: 0 !important;\r\n}\r\n\r\n/* 图片在回收站里读不到(路径属于会话当时的 cwd),渲成一句说明,字色用宿主的 tertiary。 */\r\n.sd-md-image {\r\n color: var(--dsw-alias-label-tertiary);\r\n font-style: italic;\r\n}\r\n";
|
|
638
674
|
//#endregion
|
|
639
|
-
//#region src/client/ui/
|
|
640
|
-
/**
|
|
641
|
-
* 本插件控件的样式装载:把 `ui.css` 的文本挂成一个 `<style>`,**每个 ui 组件 import 本模块**
|
|
642
|
-
* (副作用导入),所以「组件在屏幕上」必然等于「样式在场」。
|
|
643
|
-
*
|
|
644
|
-
* 为什么是往 DOM 里注 `<style>`、而不是构建期抽成 `.css` 资产:
|
|
645
|
-
* 宿主对客户端插件的样式**本来就有这条一等公民通道**——浏览器半的模块表在**工厂物化**
|
|
646
|
-
* (factory materialization)时会把插件自己注入的 `<style>` 认领归档(真产物原文:*"Claim and
|
|
647
|
-
* inventory the `<style>` tags a factory injected during materialization: preset-emitted tags arrive
|
|
648
|
-
* pre-tagged with `data-plugin`; any untagged tag is claimed for the materializing plugin (HMR
|
|
649
|
-
* bookkeeping)"*),卸载与换代时由它统一摘掉。这就是官方给插件 UI 的处方
|
|
650
|
-
* (`references/ui-plugin.md`:「Component-local styles can render as React elements so unmounting
|
|
651
|
-
* removes them」)。
|
|
652
|
-
*
|
|
653
|
-
* 因此装载点选在**模块作用域**而不是 `apply()` 里:模块作用域正好落在物化那一刻,能进宿主的认领表;
|
|
654
|
-
* 挂在 `apply` 里就晚了一拍。反复装载由 `data-dsh-session-delete` 标记挡住(同一插件在同一文档里
|
|
655
|
-
* 是单例,模块也只物化一次,这一层是给 HMR 换代留下的保险)。
|
|
656
|
-
*
|
|
657
|
-
* **不发布任何全局状态**:官方的 `pointerModality` 会往 `document.documentElement` 写
|
|
658
|
-
* `data-input-modality` 属性,那是宿主自己那份 primitives 的职责(它已经在跑),我们只在本文件里
|
|
659
|
-
* 记一个本地标志位(见 `Tooltip.tsx`)。
|
|
660
|
-
*/
|
|
661
|
-
/** 认领标记:既是宿主 HMR 的归档线索,也是「已经装过」的幂等判据。 */
|
|
662
|
-
const STYLE_MARKER = "dsh-session-delete";
|
|
663
|
-
function install() {
|
|
664
|
-
if (typeof document === "undefined") return;
|
|
665
|
-
const head = document.head;
|
|
666
|
-
if (head === null) return;
|
|
667
|
-
if (head.querySelector(`style[data-dsh-session-delete="${STYLE_MARKER}"]`) !== null) return;
|
|
668
|
-
const element = document.createElement("style");
|
|
669
|
-
element.setAttribute("data-dsh-session-delete", STYLE_MARKER);
|
|
670
|
-
element.textContent = `${ui_default}\n${markdown_default}`;
|
|
671
|
-
head.appendChild(element);
|
|
672
|
-
}
|
|
673
|
-
install();
|
|
675
|
+
//#region src/client/ui/Checkbox.tsx
|
|
674
676
|
/**
|
|
675
|
-
*
|
|
677
|
+
* 勾选框(2026-10-15 加,回收站多选态用)。
|
|
676
678
|
*
|
|
677
|
-
* `
|
|
678
|
-
*
|
|
679
|
-
* `
|
|
680
|
-
*
|
|
681
|
-
* 两个写入者抢同一个属性。
|
|
682
|
-
*/
|
|
683
|
-
let pointerModality = false;
|
|
684
|
-
if (typeof window !== "undefined") {
|
|
685
|
-
window.addEventListener("pointerdown", () => {
|
|
686
|
-
pointerModality = true;
|
|
687
|
-
}, true);
|
|
688
|
-
window.addEventListener("keydown", () => {
|
|
689
|
-
pointerModality = false;
|
|
690
|
-
}, true);
|
|
691
|
-
}
|
|
692
|
-
/** 最近一次输入是否来自指针(仅本插件自用)。 */
|
|
693
|
-
function isPointerModality() {
|
|
694
|
-
return pointerModality;
|
|
695
|
-
}
|
|
696
|
-
//#endregion
|
|
697
|
-
//#region src/client/ui/Button.tsx
|
|
698
|
-
/**
|
|
699
|
-
* `Button`(**照抄** `@deepseek-ai/dsh-client-ui-primitives@0.2.0-rc.2` 的 Button,出处与三条改写规则
|
|
700
|
-
* 见 `ui.css` 头注):`<button type="button">`,`variant` × `size` 两个正交档,可带前导图标。
|
|
679
|
+
* 样式与行为抄自 `@deepseek-ai/dsh-client-ui-primitives` 的 `lib/Checkbox.module.css`
|
|
680
|
+
* (那四条声明逐字进了 `ui.css` 的 `.sd-checkbox`):**本体是原生 `input[type=checkbox]` +
|
|
681
|
+
* `accent-color`,不是自绘的圆框**。参考稿上那种圆形勾选框是别家 App 的形状,抄进来就成了
|
|
682
|
+
* "宿主界面里长出一个外来控件";原生这一款在深浅两套主题下都跟宿主同形,键盘与读屏也白送。
|
|
701
683
|
*
|
|
702
|
-
*
|
|
703
|
-
*
|
|
684
|
+
* 一处与官方 `Checkbox` 的差异是**结构上的**:官方那版是 `<label>` 包 input + 可见文字,
|
|
685
|
+
* 我们这一格只有框、名字走 `aria-label`(行本身就是可点的整块,再挂一份可见文字是重复)。
|
|
704
686
|
*
|
|
705
|
-
*
|
|
706
|
-
*
|
|
687
|
+
* **`stopPropagation` 不是可有可无的**:清单整行是个点击目标(多选态下"点行=切换勾选"),
|
|
688
|
+
* 勾选框住在行里。不拦住冒泡,点框这一下会被行再处理一次 ⇒ 勾上又立刻取消,看起来像"点不动"。
|
|
707
689
|
*/
|
|
708
|
-
|
|
709
|
-
const
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
690
|
+
function Checkbox({ checked, indeterminate = false, onChange, label, disabled = false }) {
|
|
691
|
+
const input = (0, react.useRef)(null);
|
|
692
|
+
(0, react.useEffect)(() => {
|
|
693
|
+
if (input.current !== null) input.current.indeterminate = indeterminate;
|
|
694
|
+
}, [indeterminate]);
|
|
695
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
696
|
+
className: "sd-checkbox",
|
|
697
|
+
onClick: (event) => {
|
|
698
|
+
event.stopPropagation();
|
|
699
|
+
},
|
|
700
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
701
|
+
ref: input,
|
|
702
|
+
type: "checkbox",
|
|
703
|
+
checked,
|
|
704
|
+
disabled,
|
|
705
|
+
"aria-label": label,
|
|
706
|
+
"aria-checked": indeterminate ? "mixed" : checked,
|
|
707
|
+
onChange: (event) => {
|
|
708
|
+
onChange(event.target.checked);
|
|
709
|
+
}
|
|
710
|
+
})
|
|
724
711
|
});
|
|
725
|
-
}
|
|
712
|
+
}
|
|
726
713
|
//#endregion
|
|
727
714
|
//#region src/client/ui/icons.tsx
|
|
728
715
|
/** 各图标共用的外框(官方每个 artwork 函数里逐字重复的那一段)。 */
|
|
@@ -821,6 +808,16 @@ window.__ModuleLoader__.load({
|
|
|
821
808
|
})]
|
|
822
809
|
});
|
|
823
810
|
}
|
|
811
|
+
/** 行尾兜底「勾选开关」那枚对勾(状态点/归档行;官方 `IconCheckOutlineRegular`,`viewBox`/`d` 逐字;2026-10-17 第十三批加)。 */
|
|
812
|
+
function IconCheckOutlineRegular({ size }) {
|
|
813
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconFrame, {
|
|
814
|
+
size,
|
|
815
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
816
|
+
d: "M2.25 8.5L5.49732 11.7473C5.90519 12.1552 6.57263 12.1344 6.95426 11.7018L13.75 4",
|
|
817
|
+
stroke: "currentColor"
|
|
818
|
+
})
|
|
819
|
+
});
|
|
820
|
+
}
|
|
824
821
|
/** 分组头「展开中」那一枚(官方 `IconChevronDownOutlineRegular`,`viewBox`/`d` 逐字)。 */
|
|
825
822
|
function IconChevronDownOutlineRegular({ size }) {
|
|
826
823
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconFrame, {
|
|
@@ -840,7 +837,522 @@ window.__ModuleLoader__.load({
|
|
|
840
837
|
stroke: "currentColor"
|
|
841
838
|
})
|
|
842
839
|
});
|
|
843
|
-
}
|
|
840
|
+
}
|
|
841
|
+
//#endregion
|
|
842
|
+
//#region src/client/ui/ui.css?dsh-css-text
|
|
843
|
+
var ui_default = "/*\r\n * 本插件自己的控件样式(**不再 require 宿主的 `@deepseek-ai/dsh-client-ui-primitives`**)。\r\n *\r\n * 出处:逐条抄自 `@deepseek-ai/dsh-client-ui-primitives@0.2.0-rc.2` 随包发布的明文 CSS\r\n * (`lib/Button.module.css`、`Tag.module.css`、`Modal.module.css`、`Tooltip.module.css`、\r\n * `Toast.module.css`、`Menu.module.css`;2026-10-15 又加两张:`lib/Checkbox.module.css`、\r\n * `lib/DisclosureRow.module.css`,以及 `dsh-client-ui-conversation` 里的\r\n * `ConversationRoot.module.css` 的 `.widthHandle` 那一族——宿主自己那根可拖的宽度把手;\r\n * 2026-10-17 又加一张,也是首张来自 ui-primitives 之外的包:`dsh-client-ui-workspace` 的\r\n * `Rows.module.css` 的 `.iconButton` 那一族——会话行尾悬停排那颗按钮的基底(行尾兜底勾选开关\r\n * 要跟宿主自己的归档/置顶同形)。\r\n * 这是官方给第三方 UI 插件的处方:\r\n * 「copy markup, CSS, and behavior from the primitive into the plugin … Rename copied classes under\r\n * your plugin's prefix, keep only `--dsw-alias-*` token references」(宿主技能\r\n * `cordis-plugin-development/references/practices.md` 原文)。理由是那个包**随宿主换代而变、且不经类型检查**,\r\n * 一个抛错的组件会把整个槽位条目打空(控制台 `slot entry crashed in '<slot>'`)。\r\n *\r\n * 三条改写规则:\r\n * 1. **类名全部加本插件前缀 `sd-`**(原来是 CSS module 的哈希名,全局无冲突由前缀承担)。\r\n * 2. **配色一律走 token**,一个颜色的字面量都没有——所以宿主换主题、改名 token 时最差是难看,\r\n * 不会崩。几何量(圆角/阴影/层级)同样取 token;**只有字号/行高/尺寸是字面量 px**,与官方 CSS 逐字一致。\r\n * 3. **`@keyframes` 全部改名**(`sd-` 前缀):它们是全局命名空间,撞名会互相覆盖。\r\n *\r\n * 只收本插件真正用到的部分:Button 的 `primary`/`ghost`/`outline` 三档与 `md`/`sm` 两号\r\n * (没有 `toolbar` 变体、没有 `lg`)、Tag 的 `danger` 一档、Modal 的全部骨架、Tooltip 的三个方位、\r\n * Toast 全部、Menu 行的全部(含 danger 态与 separator)、Checkbox 的全部、DisclosureRow 的\r\n * 折叠头那一半(没有 `data-state` 那档状态点)、宽度把手的视觉与光标。**没抄进来的就是本插件不用的**,\r\n * 不预先铺开——宿主那套里还有几十个变体与控件,抄全了等于把它的表面积整个搬进来。\r\n *\r\n * **五处不是抄本**:① `.sd-resizing`(拖动期间挂在面板根上、关掉文字选中)——宿主那份没有对应的类,\r\n * 它的把手由 JS 侧的 pointer capture 承担;② `[data-dsh-session-delete=\"trash-row\"]:focus-visible`\r\n * (清单那一行的键盘焦点描边,复用选中态那枚 `ghost-active-border`)——宿主那套表里行不是可聚焦\r\n * div,我们这一行是 `role=\"option\"` 且要吃 ↑↓,而这一代 `--dsw-focus-ring-color` 是 `transparent`;\r\n * ③ `.sd-rowaction-button` 的 `[aria-pressed='true']` 与 `:disabled` 两档(2026-10-17 第十三批)——\r\n * 官方那颗 `iconButton` 只有常态与 hover,勾选开关要有\"按下\"和\"这条删不动\"两档,token 与回收站\r\n * 选中行同源(`--dsw-alias-button-ghost-active-*`);④ `.sd-batch-bar`(同日同批)——宿主没有\"浮条\"\r\n * 这件控件,四件 token 自拼;⑤ 宿主行的批量选中底色与兜底隐藏(同日改版)——宿主的行选中态不归\r\n * 第三方,整行可见性靠 `data-row-key` + `:has`,token 与回收站选中行同源;行首格在场时行尾兜底退场。\r\n * 整张表里就这五条是我们自造的,标在这里,\r\n * 下次做「与官方逐条比对」时它属于**已知差异**,不是漂移。\r\n */\r\n\r\n/* ────────────────────────── Button ────────────────────────── */\r\n\r\n.sd-button {\r\n box-sizing: border-box;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n gap: 4px;\r\n border: none;\r\n border-radius: var(--dsw-radius-md);\r\n cursor: pointer;\r\n font-size: 14px;\r\n line-height: 22px;\r\n color: var(--dsw-alias-label-primary);\r\n background: transparent;\r\n padding: 0 14px;\r\n}\r\n\r\n.sd-button:disabled {\r\n cursor: not-allowed;\r\n opacity: 0.4;\r\n}\r\n\r\n.sd-button-md {\r\n height: 36px;\r\n}\r\n\r\n/* 紧凑档:尺寸不随填充/描边换档(官方原文的注释即此意)。 */\r\n.sd-button-sm {\r\n height: 28px;\r\n font-size: 12px;\r\n line-height: 18px;\r\n padding: 0 10px;\r\n border-radius: var(--dsw-radius-sm);\r\n}\r\n\r\n.sd-button-primary {\r\n background: var(--dsw-alias-button-primary-fill);\r\n color: var(--dsw-alias-label-primary-foreground);\r\n}\r\n\r\n.sd-button-primary:hover:not(:disabled) {\r\n background: var(--dsw-alias-button-primary-hover);\r\n}\r\n\r\n.sd-button-ghost:hover:not(:disabled) {\r\n background: var(--dsw-alias-interactive-bg-hover);\r\n}\r\n\r\n.sd-button-ghost:active:not(:disabled) {\r\n background: var(--dsw-alias-interactive-bg-active);\r\n}\r\n\r\n.sd-button-outline {\r\n border: 0.5px solid var(--dsw-alias-border-l3);\r\n background: transparent;\r\n}\r\n\r\n.sd-button-outline:hover:not(:disabled) {\r\n background: var(--dsw-alias-interactive-bg-hover);\r\n}\r\n\r\n.sd-button-icon {\r\n display: inline-flex;\r\n width: 16px;\r\n height: 16px;\r\n align-items: center;\r\n justify-content: center;\r\n}\r\n\r\n/* ────────────────────────── Tag ────────────────────────── */\r\n\r\n/* 胶囊几何固定:一个 tag 在哪里都是同一个尺寸,只有色板随 tone 走。 */\r\n.sd-tag {\r\n display: inline-flex;\r\n align-items: center;\r\n border-radius: 999px;\r\n corner-shape: round;\r\n padding: 1px 8px;\r\n font-size: 11px;\r\n line-height: 17px;\r\n font-weight: 500;\r\n white-space: nowrap;\r\n}\r\n\r\n/* 状态色自带填充:底色由自身颜色 10% 混出,换色板时底与字一起走,不需要第二个 token。 */\r\n.sd-tag-danger {\r\n background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);\r\n color: var(--dsw-alias-state-error-primary);\r\n}\r\n\r\n/* ────────────────────────── Modal ────────────────────────── */\r\n\r\n/* 整视口层(figma Mask + Dialog):蒙层 + 居中卡片。四边留 24px,上下按外壳公布的浮层内缩加宽,\r\n 高卡片才不会顶到外壳。卡片按这个内边距盒取尺寸:消费方用 `max-height:100%` 收口,不自己算视口。 */\r\n.sd-modal-root {\r\n pointer-events: auto;\r\n position: fixed;\r\n inset: 0;\r\n z-index: 1000;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n padding: max(24px, var(--dsh-frame-overlay-top, 24px)) 24px;\r\n}\r\n\r\n/* 只有绘制避让原生标题栏;根仍然挡住背景点击。 */\r\n.sd-modal-mask {\r\n position: absolute;\r\n inset: var(--dsh-frame-chrome-top, 0px) 0 0;\r\n backdrop-filter: var(--dsw-mask-blur);\r\n}\r\n\r\n.sd-modal-mask::after {\r\n content: '';\r\n position: absolute;\r\n inset: 0;\r\n background: var(--dsw-alias-bg-mask-1);\r\n animation: sd-modal-enter var(--ds-transition-duration) var(--ds-ease-in-out);\r\n}\r\n\r\n.sd-modal-dialog {\r\n box-sizing: border-box;\r\n position: relative;\r\n z-index: 1;\r\n display: flex;\r\n flex-direction: column;\r\n gap: 20px;\r\n width: min(380px, 100%);\r\n padding: 0 0 24px;\r\n overflow: hidden;\r\n border: 0;\r\n border-radius: var(--dsw-radius-panel);\r\n background: var(--dsw-alias-bg-layer-2);\r\n box-shadow: var(--dsw-elevation-prominent);\r\n animation: sd-modal-enter var(--ds-transition-duration) var(--ds-ease-in-out);\r\n}\r\n\r\n@keyframes sd-modal-enter {\r\n from { opacity: 0; }\r\n to { opacity: 1; }\r\n}\r\n\r\n@media (prefers-reduced-motion: reduce) {\r\n .sd-modal-mask::after,\r\n .sd-modal-dialog { animation: none; }\r\n}\r\n\r\n.sd-modal-dialog:focus {\r\n outline: none;\r\n}\r\n\r\n.sd-modal-content {\r\n display: flex;\r\n flex-direction: column;\r\n width: 100%;\r\n}\r\n\r\n/* 头行:标题左、关闭钮右。 */\r\n.sd-modal-header {\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n gap: 8px;\r\n padding: 22px 14px 12px 24px;\r\n}\r\n\r\n.sd-modal-title {\r\n margin: 0;\r\n font-size: 16px;\r\n line-height: 24px;\r\n font-weight: 500;\r\n color: var(--dsw-alias-label-primary);\r\n}\r\n\r\n.sd-modal-close {\r\n flex: none;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n width: 28px;\r\n height: 28px;\r\n border: none;\r\n border-radius: var(--dsw-radius-sm);\r\n background: transparent;\r\n cursor: pointer;\r\n color: var(--dsw-alias-label-secondary);\r\n}\r\n\r\n.sd-modal-close:hover {\r\n background: var(--dsw-alias-interactive-bg-hover);\r\n}\r\n\r\n.sd-modal-body {\r\n display: flex;\r\n flex-direction: column;\r\n min-width: 0;\r\n margin-top: 20px;\r\n padding: 0 24px;\r\n}\r\n\r\n.sd-modal-footer {\r\n display: flex;\r\n align-items: center;\r\n justify-content: flex-end;\r\n gap: 8px;\r\n padding: 0 24px;\r\n}\r\n\r\n/* ────────────────────────── Tooltip ────────────────────────── */\r\n\r\n.sd-tooltip-bubble {\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 8px;\r\n position: fixed;\r\n z-index: 100;\r\n /* 定宽收缩只按 `left` 到视口右缘量,靠右的锚点会提前折行;\r\n `max-content` 只按文案定宽,再用半个视口收口。 */\r\n width: max-content;\r\n max-width: 50vw;\r\n padding: 3px 7px;\r\n border-radius: var(--dsw-radius-sm);\r\n background: var(--dsw-alias-tooltip-bg);\r\n color: var(--dsw-static-neutral-bluish-00);\r\n font-size: 13px;\r\n line-height: 20px;\r\n white-space: pre-line;\r\n /* 不可断的长 token(URL、路径)不许顶破 max-width。 */\r\n overflow-wrap: break-word;\r\n pointer-events: none;\r\n animation: sd-tooltip-in 150ms var(--ds-ease-in-out);\r\n}\r\n\r\n.sd-tooltip-bubble[data-portal] { z-index: 1100; }\r\n.sd-tooltip-label { min-width: 0; }\r\n\r\n.sd-tooltip-bubble[data-side='right'] {\r\n transform: translateY(-50%);\r\n}\r\n\r\n.sd-tooltip-bubble[data-side='bottom'] {\r\n transform: translateX(-50%);\r\n}\r\n\r\n.sd-tooltip-bubble[data-side='top'] {\r\n transform: translate(-50%, -100%);\r\n}\r\n\r\n@keyframes sd-tooltip-in {\r\n from { opacity: 0; }\r\n}\r\n\r\n@media (prefers-reduced-motion: reduce) {\r\n .sd-tooltip-bubble {\r\n animation: none;\r\n }\r\n}\r\n\r\n/* ────────────────────────── Toast ────────────────────────── */\r\n\r\n/* 淡出延迟由组件以 `--dsh-toast-hold` 给出 ⇒ 同一个值同时驱动卸载计时器与这段动画,\r\n 两者不可能再对不上。淡出**时长**仍需与组件里的 FADE_MS 一致(没有 owner 会改它)。 */\r\n.sd-toast {\r\n position: fixed;\r\n top: 40px;\r\n left: 50%;\r\n /* 高过图片灯箱底层的 1000:预览开着时报错也必须读得到。 */\r\n z-index: 1100;\r\n /* 通告不截点击。 */\r\n pointer-events: none;\r\n display: flex;\r\n align-items: center;\r\n gap: 10px;\r\n /* 定宽盒设了 `left` 只按右侧空间收缩(-50% 的位移发生在定尺之后)⇒ 靠窗边的横幅会提前折行。\r\n `max-content` 只按文案定宽,再由下面的 max-width 收口。 */\r\n width: max-content;\r\n max-width: min(640px, calc(100vw - 48px));\r\n padding: 12px 16px;\r\n border-radius: var(--dsw-radius-lg);\r\n /* 设计平台的 toast 表面在**两套主题下都是深的**(浅色 bluish-800 / 深色 bluish-750),\r\n 所以这里的字色与动作色是固定值,不随主题翻转。 */\r\n background: var(--dsw-alias-toast-bg);\r\n color: var(--dsw-alias-toast-label);\r\n font-size: 14px;\r\n line-height: 22px;\r\n box-shadow: var(--dsw-shadow-lv3);\r\n transform: translateX(-50%);\r\n animation:\r\n sd-toast-in 160ms ease-out,\r\n sd-toast-fade 1000ms ease var(--dsh-toast-hold, 3000ms) forwards;\r\n}\r\n\r\n.sd-toast-icon {\r\n display: grid;\r\n place-items: center;\r\n flex: none;\r\n color: var(--dsw-alias-state-warn-label);\r\n}\r\n\r\n.sd-toast-icon-success {\r\n color: var(--dsw-alias-state-success-primary);\r\n}\r\n\r\n.sd-toast-text {\r\n min-width: 0;\r\n}\r\n\r\n.sd-toast-action {\r\n display: inline;\r\n padding: 0;\r\n border: 0;\r\n background: none;\r\n color: var(--dsw-static-deepseek-400);\r\n font: inherit;\r\n cursor: pointer;\r\n /* 动作文字与周围文案之间的呼吸位。 */\r\n margin-inline: 3px;\r\n /* 横幅表面整体是穿透点击的,只有动作文字接指针。 */\r\n pointer-events: auto;\r\n}\r\n\r\n.sd-toast-action:hover {\r\n opacity: 0.8;\r\n}\r\n\r\n@keyframes sd-toast-in {\r\n from {\r\n opacity: 0;\r\n transform: translate(-50%, -6px);\r\n }\r\n\r\n to {\r\n opacity: 1;\r\n transform: translate(-50%, 0);\r\n }\r\n}\r\n\r\n@keyframes sd-toast-fade {\r\n to {\r\n opacity: 0;\r\n visibility: hidden;\r\n }\r\n}\r\n\r\n/* 减动效去掉滑入;延迟淡出(透明度变化、不是位移)仍然在定时卸载前收掉横幅。 */\r\n@media (prefers-reduced-motion: reduce) {\r\n .sd-toast {\r\n animation: sd-toast-fade 1000ms ease var(--dsh-toast-hold, 3000ms) forwards;\r\n }\r\n}\r\n\r\n/* ────────────────────────── 菜单行 ────────────────────────── */\r\n\r\n.sd-menu-item-wrap {\r\n position: relative;\r\n}\r\n\r\n/* 菜单格:紧凑文字、图标与选中间距。 */\r\n.sd-menu-item {\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n width: 100%;\r\n min-height: 34px;\r\n padding: 6px 8px;\r\n border: none;\r\n border-radius: var(--dsw-radius-md);\r\n background: transparent;\r\n cursor: pointer;\r\n font-size: 13px;\r\n line-height: 20px;\r\n color: var(--dsw-alias-label-primary);\r\n text-align: left;\r\n}\r\n\r\n.sd-menu-item:hover:not(:disabled) {\r\n background: var(--dsw-alias-interactive-bg-hover);\r\n}\r\n\r\n/* 方向键移动的是真焦点 ⇒ 键盘所在的那一行拿指针同款填充;填充就是这一行的焦点呈现,\r\n 浏览器默认的 ring 再叠一层就重了。 */\r\n.sd-menu-item:focus-visible:not(:disabled) {\r\n background: var(--dsw-alias-interactive-bg-hover);\r\n outline: none;\r\n}\r\n\r\n.sd-menu-item:disabled {\r\n opacity: 0.4;\r\n cursor: not-allowed;\r\n}\r\n\r\n.sd-menu-item-icon {\r\n display: inline-flex;\r\n flex: none;\r\n width: 14px;\r\n height: 14px;\r\n align-items: center;\r\n justify-content: center;\r\n color: var(--dsw-alias-menu-icon);\r\n}\r\n\r\n.sd-menu-item-icon svg {\r\n width: 14px;\r\n height: 14px;\r\n}\r\n\r\n.sd-menu-item-label {\r\n flex: 1;\r\n min-width: 0;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n}\r\n\r\n/* 破坏性行:error 字/图标 + danger 悬停底。 */\r\n.sd-menu-item-danger {\r\n color: var(--dsw-alias-state-error-primary);\r\n}\r\n\r\n.sd-menu-item-danger .sd-menu-item-icon {\r\n color: var(--dsw-alias-state-error-primary);\r\n}\r\n\r\n.sd-menu-item-danger:hover:not(:disabled) {\r\n background: var(--dsw-alias-interactive-bg-hover-danger);\r\n}\r\n\r\n.sd-menu-item-danger:focus-visible:not(:disabled) {\r\n background: var(--dsw-alias-interactive-bg-hover-danger);\r\n outline: none;\r\n}\r\n\r\n/* 分隔格:发丝线上下的间距。取 l2 而不是 l1——l1 在这个表面上几乎看不见。 */\r\n.sd-menu-separator {\r\n height: 0.5px;\r\n margin: 3px 2px;\r\n background: var(--dsw-alias-border-l2);\r\n}\r\n\r\n/* ────────────────────────── Checkbox ────────────────────────── */\r\n/* 抄 `lib/Checkbox.module.css` 全部四条声明,逐字。勾选框本体是**原生 input** + `accent-color`,\r\n 不是自绘的圆框——回收站多选态用它(清单行左侧那一格)。 */\r\n\r\n.sd-checkbox {\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 6px;\r\n font-size: 14px;\r\n line-height: 20px;\r\n color: var(--dsw-alias-label-primary);\r\n cursor: pointer;\r\n}\r\n\r\n.sd-checkbox input {\r\n flex: 0 0 auto;\r\n width: 16px;\r\n height: 16px;\r\n margin: 0;\r\n accent-color: var(--dsw-alias-brand-primary);\r\n cursor: inherit;\r\n}\r\n\r\n.sd-checkbox input:focus-visible {\r\n outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));\r\n outline-offset: 2px;\r\n}\r\n\r\n.sd-checkbox:has(input:disabled) {\r\n cursor: default;\r\n opacity: 0.5;\r\n}\r\n\r\n/* ────────────────────────── DisclosureRow(分组折叠头) ────────────────────────── */\r\n/* 抄 `lib/DisclosureRow.module.css` 的 `.root`/`.row`/`.leading`/`.title` 与那两枚 glyph 的\r\n 显隐。没抄进来的是 `StateDot`(`svg[data-state]`)那一档——它说\"这是状态标记不是文字装饰\",\r\n 我们的组头没有状态点。行高与 leading 盒走 `--dsh-content-font-delta` 那根轴,\r\n 标题字号走 secondary 档:宿主的\"设置里调正文字号\"会一起推着它走。 */\r\n\r\n.sd-disclosure {\r\n display: flex;\r\n flex-direction: column;\r\n width: 100%;\r\n min-width: 0;\r\n}\r\n\r\n.sd-disclosure-row {\r\n position: relative;\r\n overflow: hidden;\r\n display: flex;\r\n align-items: center;\r\n height: calc(24px + var(--dsh-content-font-delta, 0px));\r\n min-width: 0;\r\n color: var(--dsw-alias-label-tertiary);\r\n transition: color 100ms ease;\r\n}\r\n\r\n.sd-disclosure-row[data-expandable] {\r\n cursor: pointer;\r\n}\r\n\r\n.sd-disclosure-row:hover {\r\n color: var(--dsw-alias-label-secondary);\r\n}\r\n\r\n.sd-disclosure-leading {\r\n position: relative;\r\n flex: none;\r\n width: calc(16px + var(--dsh-content-font-delta, 0px));\r\n height: calc(16px + var(--dsh-content-font-delta, 0px));\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n margin-right: 6px;\r\n padding: 0;\r\n border: none;\r\n background: none;\r\n color: inherit;\r\n}\r\n\r\n.sd-disclosure-leading svg:not([data-state]) {\r\n width: calc(14px + var(--dsh-content-font-delta, 0px));\r\n height: calc(14px + var(--dsh-content-font-delta, 0px));\r\n}\r\n\r\nbutton.sd-disclosure-leading {\r\n cursor: pointer;\r\n}\r\n\r\n.sd-disclosure-title {\r\n flex: none;\r\n font-size: var(--dsh-content-font-size-secondary, 13px);\r\n line-height: calc(24px + var(--dsh-content-font-delta, 0px));\r\n color: inherit;\r\n}\r\n\r\n@media (prefers-reduced-motion: reduce) {\r\n .sd-disclosure-row {\r\n transition: none;\r\n }\r\n}\r\n\r\n/* ────────────────────────── 宽度把手(可拖的分栏线) ────────────────────────── */\r\n/* 抄 `ConversationRoot.module.css` 的 `.widthHandle` 一族:10px 的命中区、`col-resize` 光标、\r\n 平时不画、悬停或拖动时才浮出一条 2px 的 `--dsw-alias-scrollbar-bg-l1` 短标(那条渐变里\r\n `--dsh-width-handle-pointer-y` 的兜底是 50%,宿主靠 JS 跟着指针走;我们不跟,\r\n 停在中间——少一条 pointermove 上的写字,视觉上是同一族东西)。\r\n 与宿主的差别只有位置:它绝对定位在会话栏两侧,我们这根住在两栏之间的那一格 flex 位里,\r\n 所以 `top/bottom/position` 那三样交给内联布局,这里只留视觉与光标。 */\r\n\r\n.sd-width-handle {\r\n position: relative;\r\n z-index: 0;\r\n cursor: col-resize;\r\n align-self: stretch;\r\n flex: 0 0 10px;\r\n width: 10px;\r\n padding: 0;\r\n border: none;\r\n background: none;\r\n}\r\n\r\n.sd-width-handle::after {\r\n content: \"\";\r\n background: linear-gradient(\r\n to bottom,\r\n transparent calc(var(--dsh-width-handle-pointer-y, 50%) - 36px),\r\n var(--dsw-alias-scrollbar-bg-l1) calc(var(--dsh-width-handle-pointer-y, 50%) - 8px),\r\n var(--dsw-alias-scrollbar-bg-l1) calc(var(--dsh-width-handle-pointer-y, 50%) + 8px),\r\n transparent calc(var(--dsh-width-handle-pointer-y, 50%) + 36px)\r\n );\r\n opacity: 0;\r\n pointer-events: none;\r\n border-radius: 2px;\r\n width: 2px;\r\n position: absolute;\r\n top: 0;\r\n bottom: 0;\r\n left: 4px;\r\n}\r\n\r\n.sd-width-handle:hover::after,\r\n.sd-width-handle[data-dragging]::after {\r\n opacity: 1;\r\n}\r\n\r\n.sd-width-handle:focus-visible {\r\n outline: none;\r\n}\r\n\r\n.sd-width-handle:focus-visible::after {\r\n opacity: 1;\r\n}\r\n\r\n/* 本插件自造的第一条(不是抄本,见文件头点名):拖动期间整块面板别把文字选上了。 */\r\n.sd-resizing {\r\n user-select: none;\r\n cursor: col-resize;\r\n}\r\n\r\n/* 本插件自造的第二条(不是抄本,见文件头点名):清单行是 `role=\"option\"` 的可聚焦 div,而这一代\r\n `--dsw-focus-ring-color` 逐字是 `transparent`(设计稿 2026-10-15 §1.3)——不自己描一条,键盘走\r\n ↑↓ 选会话时就没有任何焦点可见性。描边**复用选中态那枚** `ghost-active-border`,不新造颜色;\r\n 0.5px 与 `outline-offset: 1px` 是行的描边宽度与一档外移,不动布局(outline 不进滚动区)。 */\r\n[data-dsh-session-delete=\"trash-row\"]:focus-visible {\r\n outline: 0.5px solid var(--dsw-alias-button-ghost-active-border);\r\n outline-offset: 1px;\r\n}\r\n\r\n/* 会话行尾悬停排那颗**兜底**勾选开关(状态点行 / 归档行;行首主勾选是原生 Checkbox)。\r\n 基底**逐字抄** `dsh-client-ui-workspace` 的 `Rows.module.css` 的 `.iconButton` / `.iconButton:hover`,\r\n 只改类名——兜底开关要跟同排宿主自己的归档/置顶同形,尺寸与色档都不自己发明。 */\r\n.sd-rowaction-button {\r\n border-radius: var(--dsw-radius-xs);\r\n cursor: pointer;\r\n width: 16px;\r\n height: 16px;\r\n color: var(--dsw-alias-label-tertiary);\r\n background: 0 0;\r\n border: none;\r\n flex: none;\r\n justify-content: center;\r\n align-items: center;\r\n padding: 0;\r\n display: inline-flex;\r\n}\r\n.sd-rowaction-button:hover {\r\n color: var(--dsw-alias-label-primary);\r\n}\r\n\r\n/* 本插件自造的第三条(不是抄本,见文件头点名):官方那颗只有常态与 hover,勾选开关还要有\r\n 「已按下」与「这条删不动」两档。按下档**复用回收站选中行那两枚** `ghost-active-*`,不新造颜色;\r\n 禁用档走 opacity。 */\r\n.sd-rowaction-button[aria-pressed='true'] {\r\n background: var(--dsw-alias-button-ghost-active-fill);\r\n box-shadow: inset 0 0 0 1px var(--dsw-alias-button-ghost-active-border);\r\n color: var(--dsw-alias-label-primary);\r\n}\r\n.sd-rowaction-button:disabled {\r\n opacity: 0.4;\r\n cursor: not-allowed;\r\n}\r\n\r\n/* 行首 16px 格里的勾选框(第十三批①改版,**只在多选模式内渲染**)。格本身由宿主居中(16×20),\r\n 这里只把包装层压成同样大小;勾选框本体是回收站多选同款原生 input(`.sd-checkbox`,见上)。 */\r\n.sd-batch-pick-leading {\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n}\r\n\r\n/* 本插件自造的第⑤条(不是抄本,见文件头点名):宿主行自己的选中态只归它自己,批量选择要有整行\r\n 底色;行首格在场时行尾兜底要退场(两条入口不许同屏)。两条都只认宿主行已有的\r\n `data-row-key`;将来这个属性改名,最差是底色不亮 / 兜底多露一颗,勾选本身仍在。 */\r\n[data-row-key^=\"session:\"]:has([data-dsh-session-delete=\"batch-pick\"][data-checked=\"true\"]) {\r\n background: var(--dsw-alias-button-ghost-active-fill);\r\n box-shadow: inset 0 0 0 0.5px var(--dsw-alias-button-ghost-active-border);\r\n}\r\n[data-row-key^=\"session:\"]:has([data-dsh-session-delete=\"batch-pick\"][data-seat=\"leading\"]) .sd-batch-pick-action {\r\n display: none;\r\n}\r\n\r\n/* 本插件自造的第四条(不是抄本,见文件头点名):批量删除的浮条。宿主没有\"浮条\"这件控件,形状照\r\n 本插件自己的浮层口径来——固定底部居中,表面/描边/圆角/高程四件全走 token(与单删那张目标卡同源)。\r\n **层级必须低于确认框(`.sd-modal-root` 1000)**:2026-10-17 真机发现浮条在 1100 时会盖住确认框、\r\n 还能抢它的按钮;撤销横幅与 Tooltip 是瞬时通告、保持 1100。列方向:清单在上、操作行在下;\r\n 清单限高可滚,默认收起(见 `batch-bar.tsx`)。**宽度按内容展开**(`max-content`,最多视口减 48):\r\n 只写 `left:50% + width:auto` 时 shrink-to-fit 只拿左半屏,新增「全选/取消全选」后会把「退出多选」\r\n 挤到第二行;窄到 max-width 还放不下才由 `.sd-batch-bar-main` 的 `flex-wrap` 兜底。 */\r\n.sd-batch-bar {\r\n position: fixed;\r\n bottom: 24px;\r\n left: 50%;\r\n transform: translateX(-50%);\r\n z-index: 900;\r\n width: max-content;\r\n max-width: calc(100vw - 48px);\r\n display: flex;\r\n flex-direction: column;\r\n align-items: stretch;\r\n gap: 8px;\r\n padding: 8px 12px;\r\n background: var(--dsw-alias-bg-layer-2);\r\n border: 0.5px solid var(--dsw-alias-border-l4);\r\n border-radius: var(--dsw-radius-lg);\r\n box-shadow: var(--dsw-elevation-prominent);\r\n font-size: 12px;\r\n line-height: 18px;\r\n}\r\n\r\n.sd-batch-bar-main {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n flex-wrap: wrap;\r\n}\r\n\r\n.sd-batch-bar-hint {\r\n color: var(--dsw-alias-label-tertiary);\r\n}\r\n\r\n.sd-batch-bar-list-head {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n flex-wrap: wrap;\r\n}\r\n\r\n/* 汇总的第二句(含 K 个子会话)比条数轻一档,和列表行里的 chip 同级。 */\r\n.sd-batch-bar-list-head span + span {\r\n color: var(--dsw-alias-label-tertiary);\r\n}\r\n\r\n.sd-batch-bar-list {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 2px;\r\n max-height: 240px;\r\n overflow: auto;\r\n margin: 0;\r\n padding: 0 0 8px;\r\n border-bottom: 0.5px solid var(--dsw-alias-border-l4);\r\n list-style: none;\r\n}\r\n\r\n.sd-batch-bar-item {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n min-width: 0;\r\n}\r\n\r\n.sd-batch-bar-title {\r\n flex: 1;\r\n min-width: 0;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n color: var(--dsw-alias-label-secondary);\r\n}\r\n\r\n.sd-batch-bar-meta {\r\n flex: none;\r\n color: var(--dsw-alias-label-tertiary);\r\n}\r\n\r\n.sd-batch-bar-remove {\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n flex: none;\r\n width: 16px;\r\n height: 16px;\r\n padding: 0;\r\n border: none;\r\n border-radius: var(--dsw-radius-xs);\r\n background: 0 0;\r\n color: var(--dsw-alias-label-tertiary);\r\n cursor: pointer;\r\n}\r\n\r\n.sd-batch-bar-remove:hover {\r\n color: var(--dsw-alias-label-primary);\r\n background: var(--dsw-alias-interactive-bg-hover);\r\n}\r\n";
|
|
844
|
+
//#endregion
|
|
845
|
+
//#region src/client/ui/markdown.css?dsh-css-text
|
|
846
|
+
var markdown_default = "/**\r\n * 回收站详情里**会话正文**的排版(2026-10-15 真机反馈后新增)。\r\n *\r\n * 为什么单独一个文件而不并进 `ui.css`:`ui.css` 是\"照官方 primitives 逐字抄来的那一份表\",\r\n * 那条 fidelity 核对(§8 门 4)要的是它与官方明文**逐属性零差异**,只许文件头点名的几类例外\r\n * (类名前缀、`@keyframes` 改名,外加它点名的两处自造);条数不钉在这里,钉了就会漂成假基线。这一份抄的是\r\n * **另一个来源**(宿主 markdown 那一张表),混进去就把那份核对弄脏了。\r\n *\r\n * **来源(逐字抄,只改选择器与下面点名的三处)**:\r\n * `app.asar` 里 `@deepseek-ai/dsh-client-ui-primitives/lib/markdown/MarkdownText.module.css`\r\n * (`.markdown` 那一支;`.compact` 一支我们不用)+ `lib/markdown/CodeBlock.module.css` 的 `pre`。\r\n * token 名一个没换:`--dsw-font-markdown-*` / `--dsw-alias-markdown-*` / `--dsh-content-font-*`\r\n * 全是宿主会话区自己吃的那组,所以宿主调内容字号时,这里跟着一起动。\r\n *\r\n * 三处**有意**的差异,每条都是因为我们的 DOM 长不出宿主那个节点:\r\n * ① 宿主给表包一层 `.tableScroll`(div)再横向滚动;react-markdown 不产这层 ⇒ 滚动挂在 `table`\r\n * 自己身上(`display:block` + 匿名表盒)。这条得真机量过才算成立。\r\n * ② 宿主的代码卡有 banner 行(语言名 + 复制),四个圆角被拆成\"上圆角在 banner、下圆角在 pre\";\r\n * 我们没有 banner ⇒ 圆角整块给 `pre`。\r\n * ③ 宿主的链接可点(点开外链/文件),我们不碰宿主导航,链接渲成不可点的文本 ⇒ 保留链接色与字重,\r\n * **去掉** hover 的下划线(那是\"点我\"的邀请)。\r\n *\r\n * 抄过来但用不上的选择器已删:`.fileLink` / `.fileMention` / `.linkIcon` / `.image` / `.imageAlt`\r\n * (图片渲成 `[图片 · alt]`)/ `.katex-display` / `:global(.md-code-block)` / `.shiki`(高亮由宿主\r\n * 的 highlighter 提供,我们没有)。\r\n */\r\n.sd-md {\r\n min-width: 0;\r\n overflow-wrap: anywhere;\r\n font: var(--dsw-font-markdown-base);\r\n color: var(--dsw-alias-label-primary);\r\n}\r\n\r\n.sd-md strong {\r\n font-weight: 600;\r\n}\r\n\r\n.sd-md h1 {\r\n font: var(--dsw-font-markdown-h1);\r\n margin: 32px 0 16px;\r\n}\r\n\r\n.sd-md h2 {\r\n font: var(--dsw-font-markdown-h2);\r\n margin: 32px 0 16px;\r\n}\r\n\r\n.sd-md h3 {\r\n font: var(--dsw-font-markdown-h3);\r\n margin: 32px 0 16px;\r\n}\r\n\r\n.sd-md h4 {\r\n font: var(--dsw-font-markdown-h4);\r\n margin: 16px 0;\r\n}\r\n\r\n.sd-md :where(h5, h6) {\r\n font: var(--dsw-font-markdown-base-strong);\r\n margin: 16px 0;\r\n}\r\n\r\n.sd-md :where(h1, h2, h3, h4, h5, h6) strong {\r\n font-weight: inherit;\r\n}\r\n\r\n.sd-md p {\r\n margin: 16px 0;\r\n}\r\n\r\n/* Tighten h4–h6 against a following list (design: 8px gap). */\r\n.sd-md :where(h4, h5, h6) + :where(ul, ol) {\r\n margin-top: 8px;\r\n}\r\n\r\n.sd-md :where(h4, h5, h6):has(+ :where(ul, ol)) {\r\n margin-bottom: 8px;\r\n}\r\n\r\n/* 见文件头差异③:颜色与字重照抄,hover 的 dotted 下划线不抄——这里没有可点的东西。 */\r\n.sd-md-link {\r\n color: var(--dsw-alias-link);\r\n font-weight: 500;\r\n}\r\n\r\n.sd-md :where(ul, ol) {\r\n margin: 16px 0;\r\n padding-left: 18px;\r\n}\r\n\r\n.sd-md li:not(:first-child) {\r\n margin-top: 6px;\r\n}\r\n\r\n.sd-md li > :where(ul, ol) {\r\n margin-top: 4px;\r\n}\r\n\r\n.sd-md li::marker {\r\n line-height: 24px;\r\n color: var(--dsw-alias-label-secondary);\r\n}\r\n\r\n/* Nested ol under ul/ol: markers inside (models sometimes emit this shape). */\r\n.sd-md :where(ul, ol) ol {\r\n list-style-position: inside;\r\n padding-left: 0;\r\n}\r\n\r\n.sd-md :where(ul, ol) ol li p {\r\n display: inline;\r\n}\r\n\r\n.sd-md li > p {\r\n margin: 8px 0;\r\n}\r\n\r\n.sd-md li > *:first-child {\r\n margin-top: 0;\r\n}\r\n\r\n.sd-md li > *:last-child {\r\n margin-bottom: 0;\r\n}\r\n\r\n.sd-md hr {\r\n display: block;\r\n border: none;\r\n height: 0.5px;\r\n margin: 32px 0;\r\n background: var(--dsw-alias-border-l2);\r\n}\r\n\r\n.sd-md blockquote {\r\n border-left: 2px solid var(--dsw-alias-label-caption);\r\n margin: 16px 0 0;\r\n padding-left: 14px;\r\n}\r\n\r\n/* 代码卡:宿主的卡片由 `.block`(底色+圆角)与其内 `pre`(字号+内边距)两层拼成。\r\n 我们只有 `pre` 这一层,所以两层的声明合成在这里——见文件头差异②。 */\r\n.sd-md pre {\r\n font: var(--dsw-font-markdown-code-block);\r\n margin: 16px 0;\r\n padding: 16px;\r\n color: var(--dsw-alias-label-primary);\r\n background: var(--dsw-alias-markdown-code-block);\r\n border-radius: var(--dsw-radius-lg);\r\n overflow-x: auto;\r\n white-space: pre-wrap;\r\n word-break: break-all;\r\n}\r\n\r\n.sd-md pre code {\r\n font: inherit;\r\n background: none;\r\n padding: 0;\r\n}\r\n\r\n.sd-md :not(pre) > code {\r\n display: inline-flex;\r\n align-items: center;\r\n box-sizing: border-box;\r\n font: var(--dsw-font-markdown-code);\r\n font-family: var(--ds-font-family-code);\r\n font-size: 0.875em !important;\r\n background-color: var(--dsw-alias-markdown-inline-code);\r\n border: 0.5px solid var(--dsw-alias-border-l1);\r\n border-radius: var(--dsw-radius-sm);\r\n padding: 0 5px;\r\n}\r\n\r\n.sd-md :where(h1, h2, h3, h4, h5, h6) code {\r\n font: inherit;\r\n font-family: var(--ds-font-family-code);\r\n}\r\n\r\n.sd-md input[type='checkbox'] {\r\n margin: 0 8px 0 0;\r\n accent-color: var(--dsw-alias-label-secondary);\r\n}\r\n\r\n/* 见文件头差异①:宿主是 `.tableScroll > table`,我们没有那层 div。 */\r\n.sd-md table {\r\n display: block;\r\n box-sizing: border-box;\r\n max-width: 100%;\r\n overflow-x: auto;\r\n overscroll-behavior-x: contain;\r\n border-collapse: collapse;\r\n width: max-content;\r\n}\r\n\r\n.sd-md th {\r\n text-align: start;\r\n padding: 10px 16px;\r\n border-bottom: 0.5px solid var(--dsw-alias-border-l3);\r\n border-top: none;\r\n font: var(--dsw-font-markdown-table-head);\r\n max-width: 320px;\r\n max-width: min(30vw, 320px);\r\n min-width: 100px;\r\n}\r\n\r\n.sd-md td {\r\n padding: 10px 16px;\r\n border-bottom: 0.5px solid var(--dsw-alias-border-l2);\r\n font: var(--dsw-font-markdown-table);\r\n max-width: 320px;\r\n max-width: min(30vw, 320px);\r\n min-width: 100px;\r\n}\r\n\r\n.sd-md th:first-child,\r\n.sd-md td:first-child {\r\n padding-left: 0;\r\n}\r\n\r\n.sd-md td:last-child {\r\n padding-right: 0;\r\n}\r\n\r\n.sd-md table code {\r\n font-size: 11px;\r\n}\r\n\r\n.sd-md > *:first-child,\r\n.sd-md p:first-child {\r\n margin-top: 0 !important;\r\n}\r\n\r\n.sd-md > *:last-child,\r\n.sd-md p:last-child {\r\n margin-bottom: 0 !important;\r\n}\r\n\r\n/* 图片在回收站里读不到(路径属于会话当时的 cwd),渲成一句说明,字色用宿主的 tertiary。 */\r\n.sd-md-image {\r\n color: var(--dsw-alias-label-tertiary);\r\n font-style: italic;\r\n}\r\n";
|
|
847
|
+
//#endregion
|
|
848
|
+
//#region src/client/ui/styles.ts
|
|
849
|
+
/**
|
|
850
|
+
* 本插件控件的样式装载:把 `ui.css` 的文本挂成一个 `<style>`,**每个 ui 组件 import 本模块**
|
|
851
|
+
* (副作用导入),所以「组件在屏幕上」必然等于「样式在场」。
|
|
852
|
+
*
|
|
853
|
+
* 为什么是往 DOM 里注 `<style>`、而不是构建期抽成 `.css` 资产:
|
|
854
|
+
* 宿主对客户端插件的样式**本来就有这条一等公民通道**——浏览器半的模块表在**工厂物化**
|
|
855
|
+
* (factory materialization)时会把插件自己注入的 `<style>` 认领归档(真产物原文:*"Claim and
|
|
856
|
+
* inventory the `<style>` tags a factory injected during materialization: preset-emitted tags arrive
|
|
857
|
+
* pre-tagged with `data-plugin`; any untagged tag is claimed for the materializing plugin (HMR
|
|
858
|
+
* bookkeeping)"*),卸载与换代时由它统一摘掉。这就是官方给插件 UI 的处方
|
|
859
|
+
* (`references/ui-plugin.md`:「Component-local styles can render as React elements so unmounting
|
|
860
|
+
* removes them」)。
|
|
861
|
+
*
|
|
862
|
+
* 因此装载点选在**模块作用域**而不是 `apply()` 里:模块作用域正好落在物化那一刻,能进宿主的认领表;
|
|
863
|
+
* 挂在 `apply` 里就晚了一拍。反复装载由 `data-dsh-session-delete` 标记挡住(同一插件在同一文档里
|
|
864
|
+
* 是单例,模块也只物化一次,这一层是给 HMR 换代留下的保险)。
|
|
865
|
+
*
|
|
866
|
+
* **不发布任何全局状态**:官方的 `pointerModality` 会往 `document.documentElement` 写
|
|
867
|
+
* `data-input-modality` 属性,那是宿主自己那份 primitives 的职责(它已经在跑),我们只在本文件里
|
|
868
|
+
* 记一个本地标志位(见 `Tooltip.tsx`)。
|
|
869
|
+
*/
|
|
870
|
+
/** 认领标记:既是宿主 HMR 的归档线索,也是「已经装过」的幂等判据。 */
|
|
871
|
+
const STYLE_MARKER = "dsh-session-delete";
|
|
872
|
+
function install() {
|
|
873
|
+
if (typeof document === "undefined") return;
|
|
874
|
+
const head = document.head;
|
|
875
|
+
if (head === null) return;
|
|
876
|
+
if (head.querySelector(`style[data-dsh-session-delete="${STYLE_MARKER}"]`) !== null) return;
|
|
877
|
+
const element = document.createElement("style");
|
|
878
|
+
element.setAttribute("data-dsh-session-delete", STYLE_MARKER);
|
|
879
|
+
element.textContent = `${ui_default}\n${markdown_default}`;
|
|
880
|
+
head.appendChild(element);
|
|
881
|
+
}
|
|
882
|
+
install();
|
|
883
|
+
/**
|
|
884
|
+
* 输入模态:最近一次输入是**指针**还是**键盘**。
|
|
885
|
+
*
|
|
886
|
+
* `Tooltip` 用它挡掉「指针交互之后紧接着到来的 focus」——菜单收起时宿主会把焦点还给触发按钮,
|
|
887
|
+
* 那一拍不该把我们的提示顶出来(官方 `pointerModality()` 的同一条契约,实现也一样:
|
|
888
|
+
* `pointerdown` 置真、`keydown` 置假)。官方还会据此发布 `data-input-modality` 去控制焦点环的
|
|
889
|
+
* 显隐,**那件事不复制**:宿主自己那份 primitives 已经在同一份文档上发布了它,我们再来一份就是
|
|
890
|
+
* 两个写入者抢同一个属性。
|
|
891
|
+
*/
|
|
892
|
+
let pointerModality = false;
|
|
893
|
+
if (typeof window !== "undefined") {
|
|
894
|
+
window.addEventListener("pointerdown", () => {
|
|
895
|
+
pointerModality = true;
|
|
896
|
+
}, true);
|
|
897
|
+
window.addEventListener("keydown", () => {
|
|
898
|
+
pointerModality = false;
|
|
899
|
+
}, true);
|
|
900
|
+
}
|
|
901
|
+
/** 最近一次输入是否来自指针(仅本插件自用)。 */
|
|
902
|
+
function isPointerModality() {
|
|
903
|
+
return pointerModality;
|
|
904
|
+
}
|
|
905
|
+
//#endregion
|
|
906
|
+
//#region src/client/ui/Tooltip.tsx
|
|
907
|
+
/**
|
|
908
|
+
* `Tooltip`(**照抄** `@deepseek-ai/dsh-client-ui-primitives@0.2.0-rc.2` 的 Tooltip,出处与三条改写
|
|
909
|
+
* 规则见 `ui.css` 头注):把提示挂在锚点元素上,hover / 键盘 focus 才出。
|
|
910
|
+
*
|
|
911
|
+
* **保留下来的行为**:`side` 三个方位的定位(`right` 取锚点右缘再让出 10px,`bottom`/`top` 居中并按
|
|
912
|
+
* `gap` 让位)、视口边界收口(`edgeMargin=12`、`bottom`/`top` 装不下就翻面)、等 `ResizeObserver`
|
|
913
|
+
* 量到气泡尺寸再显示(所以不会先闪一个错位的框)、指针交互之后的 focus 不弹(见 `styles.ts` 的
|
|
914
|
+
* `isPointerModality`)、`portal` 时挂到 `document.body`(宿主菜单有裁剪与层叠上下文,锚点在菜单里
|
|
915
|
+
* 时必须 portal,否则气泡被裁掉)。
|
|
916
|
+
*
|
|
917
|
+
* **砍掉的**:`shortcutKeys`(本插件没有快捷键)、`openOnClick`/`pinned` 与随之而来的
|
|
918
|
+
* `useDismissOnOutsidePointer`、`aria-describedby` 关联(官方只在 pinned 时挂)、
|
|
919
|
+
* `TooltipSuppression`(那是给 HoverCard 预览用的互相压制)、`delayMs`/`focusDelayMs`/`maxWidth`
|
|
920
|
+
* (本插件唯一一处用的是零延迟默认值)。**没有砍掉 `side`/`gap`**:定位数学是三行分支,
|
|
921
|
+
* 留着才与官方同形。
|
|
922
|
+
*
|
|
923
|
+
* **与官方的一处差异**:官方把锚点自己的 `ref` 与它的 `ref` 合并后一起挂;这里要求锚点**不带
|
|
924
|
+
* 自己的 ref**(本插件唯一的调用点是一个裸 `<span>`),所以直接挂。要合并再补。
|
|
925
|
+
*/
|
|
926
|
+
function Tooltip({ label, side = "right", gap = 8, disabled = false, portal = false, children }) {
|
|
927
|
+
const anchor = (0, react.useRef)(null);
|
|
928
|
+
const bubble = (0, react.useRef)(null);
|
|
929
|
+
const [rect, setRect] = (0, react.useState)(null);
|
|
930
|
+
const visible = rect !== null && !disabled;
|
|
931
|
+
(0, react.useEffect)(() => {
|
|
932
|
+
const element = bubble.current;
|
|
933
|
+
if (rect === null || !visible || element === null) return;
|
|
934
|
+
const EDGE_MARGIN = 12;
|
|
935
|
+
let size;
|
|
936
|
+
let placement = side;
|
|
937
|
+
const fit = () => {
|
|
938
|
+
if (size === void 0) return;
|
|
939
|
+
const { inlineSize: width, blockSize: height } = size;
|
|
940
|
+
const offset = side === "right" ? 0 : width / 2;
|
|
941
|
+
const left = Math.max(EDGE_MARGIN, Math.min(rect.x - offset, window.innerWidth - EDGE_MARGIN - width));
|
|
942
|
+
const fitsBelow = rect.bottom + gap + height <= window.innerHeight - EDGE_MARGIN;
|
|
943
|
+
const fitsAbove = rect.top - gap - height >= EDGE_MARGIN;
|
|
944
|
+
if (placement === "bottom" && !fitsBelow && fitsAbove) placement = "top";
|
|
945
|
+
else if (placement === "top" && !fitsAbove && fitsBelow) placement = "bottom";
|
|
946
|
+
element.style.left = `${left + offset}px`;
|
|
947
|
+
element.style.top = `${placement === "right" ? (rect.top + rect.bottom) / 2 : placement === "top" ? rect.top - gap : rect.bottom + gap}px`;
|
|
948
|
+
element.dataset.side = placement;
|
|
949
|
+
element.style.visibility = "visible";
|
|
950
|
+
};
|
|
951
|
+
const observer = new ResizeObserver((entries) => {
|
|
952
|
+
size = entries[0]?.borderBoxSize[0];
|
|
953
|
+
fit();
|
|
954
|
+
});
|
|
955
|
+
observer.observe(element, { box: "border-box" });
|
|
956
|
+
window.addEventListener("resize", fit);
|
|
957
|
+
return () => {
|
|
958
|
+
observer.disconnect();
|
|
959
|
+
window.removeEventListener("resize", fit);
|
|
960
|
+
};
|
|
961
|
+
}, [
|
|
962
|
+
gap,
|
|
963
|
+
rect,
|
|
964
|
+
side,
|
|
965
|
+
visible
|
|
966
|
+
]);
|
|
967
|
+
const show = (0, react.useCallback)(() => {
|
|
968
|
+
const element = anchor.current;
|
|
969
|
+
/* v8 ignore next -- 事件是在克隆出来的锚点上冒泡上来的,触发时 ref 必已挂上 */
|
|
970
|
+
if (element === null) return;
|
|
971
|
+
const box = element.getBoundingClientRect();
|
|
972
|
+
setRect({
|
|
973
|
+
x: side === "right" ? box.right + 10 : box.left + box.width / 2,
|
|
974
|
+
top: box.top,
|
|
975
|
+
bottom: box.bottom
|
|
976
|
+
});
|
|
977
|
+
}, [side]);
|
|
978
|
+
const withdraw = (0, react.useCallback)(() => {
|
|
979
|
+
setRect(null);
|
|
980
|
+
}, []);
|
|
981
|
+
(0, react.useEffect)(() => {
|
|
982
|
+
if (disabled) setRect(null);
|
|
983
|
+
}, [disabled]);
|
|
984
|
+
/** 锚点自己带的处理器要照常跑(我们只加不减)——官方也是先 `children.props.onX?.(e)` 再接管。 */
|
|
985
|
+
const inner = children.props;
|
|
986
|
+
const anchorElement = (0, react.cloneElement)(children, {
|
|
987
|
+
ref: (element) => {
|
|
988
|
+
anchor.current = element;
|
|
989
|
+
},
|
|
990
|
+
onMouseEnter: (event) => {
|
|
991
|
+
inner.onMouseEnter?.(event);
|
|
992
|
+
if (!disabled) show();
|
|
993
|
+
},
|
|
994
|
+
onMouseLeave: (event) => {
|
|
995
|
+
inner.onMouseLeave?.(event);
|
|
996
|
+
withdraw();
|
|
997
|
+
},
|
|
998
|
+
onClick: (event) => {
|
|
999
|
+
inner.onClick?.(event);
|
|
1000
|
+
withdraw();
|
|
1001
|
+
},
|
|
1002
|
+
onFocus: (event) => {
|
|
1003
|
+
inner.onFocus?.(event);
|
|
1004
|
+
if (isPointerModality()) return;
|
|
1005
|
+
if (!disabled) show();
|
|
1006
|
+
},
|
|
1007
|
+
onBlur: (event) => {
|
|
1008
|
+
inner.onBlur?.(event);
|
|
1009
|
+
withdraw();
|
|
1010
|
+
}
|
|
1011
|
+
});
|
|
1012
|
+
const content = visible ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1013
|
+
ref: bubble,
|
|
1014
|
+
className: "sd-tooltip-bubble",
|
|
1015
|
+
"data-side": side,
|
|
1016
|
+
"data-portal": portal ? "true" : void 0,
|
|
1017
|
+
style: {
|
|
1018
|
+
left: rect.x,
|
|
1019
|
+
top: rect.top,
|
|
1020
|
+
visibility: "hidden"
|
|
1021
|
+
},
|
|
1022
|
+
role: "tooltip",
|
|
1023
|
+
children: label === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1024
|
+
className: "sd-tooltip-label",
|
|
1025
|
+
children: label
|
|
1026
|
+
})
|
|
1027
|
+
}) : null;
|
|
1028
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [anchorElement, content === null ? null : portal ? (0, react_dom.createPortal)(content, document.body) : content] });
|
|
1029
|
+
}
|
|
1030
|
+
//#endregion
|
|
1031
|
+
//#region src/client/batch-select.tsx
|
|
1032
|
+
/**
|
|
1033
|
+
* 宿主列表批量删除的选择面(第十三批①):多选模式 + 选中集 + 行首勾选 + 行尾兜底 + 整行点击守卫。
|
|
1034
|
+
*
|
|
1035
|
+
* 入口在每行的「⋯」菜单里(`batch-menu-item.tsx`):**按下才进入多选模式**,模式内才渲染勾选框、
|
|
1036
|
+
* 才把整行点击变成"勾/取消";退出模式清空名单。选中集是**模块级现场**(本仓既有的客户端模式,见
|
|
1037
|
+
* `delete-confirm.tsx` / `undo-toast.tsx`):主勾选框住在每行的 `sidebar.session.row.leading` 槽位里,
|
|
1038
|
+
* 行尾兜底住在 `sidebar.workspaces.session.row.action`,浮条与批量确认框住在 `shell.overlay` 上——
|
|
1039
|
+
* 四处隔着槽位边界,只有模块级 store 能让他们看见同一份名单。状态源用 React 自带的
|
|
1040
|
+
* `useSyncExternalStore`,不为此引新的状态库。
|
|
1041
|
+
*
|
|
1042
|
+
* **为什么主勾选在行首**:宿主对行尾 `.rowActions` 是 `display:none`,只有 `row:hover` / 菜单打开
|
|
1043
|
+
* 才出现(app.asar 0.2.0-rc.2 的 Rows 渲染);行首 16px 格在无状态点时是空的,模式内可见。
|
|
1044
|
+
* 宿主在「有状态点(运行中/待确认/未读完成)」与归档行不渲染行首格 ⇒ 这些行走行尾悬停兜底,
|
|
1045
|
+
* 但模式内**点整行**也能选(`useSelectionRowClickGuard`),所以不依赖那颗小框。
|
|
1046
|
+
* 两条勾选不会同时出现(`ui.css` 的 `:has` 兜底隐藏规则,靠宿主行的 `data-row-key`)。
|
|
1047
|
+
*
|
|
1048
|
+
* 名单只收槽位投影原样给的 `sessionId` / `displayTitle`(浏览器半不自己拼 id),**顺序 = 勾选顺序**:
|
|
1049
|
+
* 宿主列表的 DOM 序不归我们读,`/apply` 那条「保持输入顺序」吃到的因此就是勾选序。
|
|
1050
|
+
*/
|
|
1051
|
+
let picks = [];
|
|
1052
|
+
/** 多选模式:入口在每一行的「⋯」菜单里;只有模式内才渲染勾选框、才吃整行点击。 */
|
|
1053
|
+
let selecting = false;
|
|
1054
|
+
const listeners$4 = /* @__PURE__ */ new Set();
|
|
1055
|
+
const emit$3 = () => {
|
|
1056
|
+
for (const listener of listeners$4) listener();
|
|
1057
|
+
};
|
|
1058
|
+
function getPicks() {
|
|
1059
|
+
return picks;
|
|
1060
|
+
}
|
|
1061
|
+
function isSelecting() {
|
|
1062
|
+
return selecting;
|
|
1063
|
+
}
|
|
1064
|
+
/** 「⋯ → 多选」按下时进入;重复进入是幂等的。 */
|
|
1065
|
+
function enterSelection() {
|
|
1066
|
+
if (selecting) return;
|
|
1067
|
+
selecting = true;
|
|
1068
|
+
emit$3();
|
|
1069
|
+
}
|
|
1070
|
+
/** 退出模式并清空名单:多选态和选中集是同一次交互的两半,退出后不该留下看不见的名单。 */
|
|
1071
|
+
function exitSelection() {
|
|
1072
|
+
if (!selecting && picks.length === 0) return;
|
|
1073
|
+
selecting = false;
|
|
1074
|
+
picks = [];
|
|
1075
|
+
emit$3();
|
|
1076
|
+
}
|
|
1077
|
+
function useSelecting() {
|
|
1078
|
+
return (0, react.useSyncExternalStore)(subscribe$3, isSelecting, isSelecting);
|
|
1079
|
+
}
|
|
1080
|
+
/** 翻转一条:没选过就排队尾,选过就摘掉(列表里永远不会有重复 id)。 */
|
|
1081
|
+
function togglePick(item) {
|
|
1082
|
+
picks = picks.findIndex((candidate) => candidate.sessionId === item.sessionId) === -1 ? [...picks, item] : picks.filter((candidate) => candidate.sessionId !== item.sessionId);
|
|
1083
|
+
emit$3();
|
|
1084
|
+
}
|
|
1085
|
+
function clearPicks() {
|
|
1086
|
+
if (picks.length === 0) return;
|
|
1087
|
+
picks = [];
|
|
1088
|
+
emit$3();
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* 「当前可见」的可选行:侧栏里**已渲染**的会话行(`data-row-key="session:…"`)——折叠组、
|
|
1092
|
+
* 归档过滤外的行不在 DOM 里,宿主隐藏的 subagent 也不在;在跑的行跳过(与置灰勾选框同口径)。
|
|
1093
|
+
* 标题/运行态来自行尾兜底登记的 `rowInfo`(模式内它对每个非空行都渲染);DOM 序就是侧栏顺序。
|
|
1094
|
+
*/
|
|
1095
|
+
function visibleSelectableRows() {
|
|
1096
|
+
const rows = [];
|
|
1097
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1098
|
+
for (const row of document.querySelectorAll("[data-row-key^=\"session:\"]")) {
|
|
1099
|
+
const key = row.getAttribute("data-row-key") ?? "";
|
|
1100
|
+
if (!key.startsWith("session:")) continue;
|
|
1101
|
+
const sessionId = key.slice(8);
|
|
1102
|
+
if (sessionId === "" || seen.has(sessionId)) continue;
|
|
1103
|
+
const info = rowInfo.get(sessionId);
|
|
1104
|
+
if (info === void 0 || info.live) continue;
|
|
1105
|
+
seen.add(sessionId);
|
|
1106
|
+
rows.push({
|
|
1107
|
+
sessionId,
|
|
1108
|
+
title: info.title
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
return rows;
|
|
1112
|
+
}
|
|
1113
|
+
/** 全选:把当前可见可选行里还没选的按 DOM 序追加;原有选中保持在前。 */
|
|
1114
|
+
function selectAllVisible() {
|
|
1115
|
+
const chosen = new Set(picks.map((item) => item.sessionId));
|
|
1116
|
+
const additions = visibleSelectableRows().filter((row) => !chosen.has(row.sessionId));
|
|
1117
|
+
if (additions.length === 0) return;
|
|
1118
|
+
picks = [...picks, ...additions];
|
|
1119
|
+
emit$3();
|
|
1120
|
+
}
|
|
1121
|
+
/**
|
|
1122
|
+
* 同一个切换口该显示「全选」还是「取消全选」:
|
|
1123
|
+
* - 有选中、且当前可见可选行全都选中 ⇒ 取消全选;
|
|
1124
|
+
* - 有选中、但当前没有可见可选行(全折叠/过滤掉)⇒ 只能清空,也显示取消全选;
|
|
1125
|
+
* - 其余(含 0 条)⇒ 全选。
|
|
1126
|
+
*/
|
|
1127
|
+
function allVisibleSelected() {
|
|
1128
|
+
if (picks.length === 0) return false;
|
|
1129
|
+
const rows = visibleSelectableRows();
|
|
1130
|
+
if (rows.length === 0) return true;
|
|
1131
|
+
const chosen = new Set(picks.map((item) => item.sessionId));
|
|
1132
|
+
return rows.every((row) => chosen.has(row.sessionId));
|
|
1133
|
+
}
|
|
1134
|
+
/** 执行后收缩:只留失败名单(空数组 = 全清)。成功的随通告从侧栏消失,不该留在名单里。 */
|
|
1135
|
+
function shrinkPicksToFailures(failedSessionIds) {
|
|
1136
|
+
const keep = new Set(failedSessionIds);
|
|
1137
|
+
picks = picks.filter((candidate) => keep.has(candidate.sessionId));
|
|
1138
|
+
emit$3();
|
|
1139
|
+
}
|
|
1140
|
+
function subscribe$3(listener) {
|
|
1141
|
+
listeners$4.add(listener);
|
|
1142
|
+
return () => {
|
|
1143
|
+
listeners$4.delete(listener);
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
function usePicks() {
|
|
1147
|
+
return (0, react.useSyncExternalStore)(subscribe$3, getPicks, getPicks);
|
|
1148
|
+
}
|
|
1149
|
+
/**
|
|
1150
|
+
* 守卫从 `data-row-key` 反解出 sessionId,但拿不到槽位投影的标题/运行态 ⇒ 行尾兜底那颗
|
|
1151
|
+
* (它对每个非空行都渲染,哪怕 CSS 把整排藏起来)在 effect 里把这两件登记进这张表。
|
|
1152
|
+
* 只增不删:会话 id 不复用,行被过滤掉也不会拿旧标题冒充新行。
|
|
1153
|
+
*/
|
|
1154
|
+
const rowInfo = /* @__PURE__ */ new Map();
|
|
1155
|
+
/** 行内这些元素有自己的动作:模式内也不许被"点行=勾选"吃掉。 */
|
|
1156
|
+
const INTERACTIVE_IN_ROW = "button, a, input, select, textarea, [role=\"menuitem\"], [role=\"menu\"], [data-dsh-session-delete=\"batch-pick\"]";
|
|
1157
|
+
/**
|
|
1158
|
+
* 多选态内的整行点击守卫(document 捕获阶段)。
|
|
1159
|
+
*
|
|
1160
|
+
* 为什么必须在这里拦:宿主的会话行自己带 `onClick = open(sessionId)`,第三方没有官方的
|
|
1161
|
+
* "别切会话"开关;模式内点行要变成"勾/取消",只能抢在 React 根监听之前把事件吃掉。
|
|
1162
|
+
* 边界:行里的按钮 / 链接 / 输入 / 菜单项一律放行(否则 ⋯ 菜单、归档、置顶、单删都点不动);
|
|
1163
|
+
* 双击的第二下与 dblclick 不翻转(第一下已经选中,第二下不能又反掉,也不能打开宿主的重命名)。
|
|
1164
|
+
* 认行靠 `data-row-key="session:…"`——宿主将来改掉这个属性,最差是模式内点行不生效,
|
|
1165
|
+
* 勾选框本身仍在。
|
|
1166
|
+
*/
|
|
1167
|
+
function useSelectionRowClickGuard() {
|
|
1168
|
+
const selectingNow = useSelecting();
|
|
1169
|
+
(0, react.useEffect)(() => {
|
|
1170
|
+
if (!selectingNow) return;
|
|
1171
|
+
const handle = (event) => {
|
|
1172
|
+
if (!(event.target instanceof Element)) return;
|
|
1173
|
+
const row = event.target.closest("[data-row-key^=\"session:\"]");
|
|
1174
|
+
if (row === null) return;
|
|
1175
|
+
if (event.target.closest(INTERACTIVE_IN_ROW) !== null) return;
|
|
1176
|
+
event.preventDefault();
|
|
1177
|
+
event.stopPropagation();
|
|
1178
|
+
if (event.type === "dblclick" || event.type === "click" && event.detail > 1) return;
|
|
1179
|
+
const key = row.getAttribute("data-row-key") ?? "";
|
|
1180
|
+
if (!key.startsWith("session:")) return;
|
|
1181
|
+
const sessionId = key.slice(8);
|
|
1182
|
+
if (sessionId === "") return;
|
|
1183
|
+
const info = rowInfo.get(sessionId);
|
|
1184
|
+
if (info?.live === true) return;
|
|
1185
|
+
togglePick({
|
|
1186
|
+
sessionId,
|
|
1187
|
+
title: info?.title ?? ""
|
|
1188
|
+
});
|
|
1189
|
+
};
|
|
1190
|
+
document.addEventListener("click", handle, true);
|
|
1191
|
+
document.addEventListener("dblclick", handle, true);
|
|
1192
|
+
return () => {
|
|
1193
|
+
document.removeEventListener("click", handle, true);
|
|
1194
|
+
document.removeEventListener("dblclick", handle, true);
|
|
1195
|
+
};
|
|
1196
|
+
}, [selectingNow]);
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* 一组会话的子会话计数(`/apply` 的 `action:'count'`,纯读、不进串行链)。
|
|
1200
|
+
*
|
|
1201
|
+
* **一条读不到就整份返回 null** ——界面上那两个数(浮条的 K、确认框的 K 与逐条 M)都是合计,
|
|
1202
|
+
* 缺一条就是假数;「不许拿 0 冒充没有」(10-15 稿 §10)。
|
|
1203
|
+
*/
|
|
1204
|
+
async function fetchCounts(deps, ids) {
|
|
1205
|
+
if (ids.length === 0) return /* @__PURE__ */ new Map();
|
|
1206
|
+
const outcome = await callRemote(() => deps.applyOutcomes("count", ids));
|
|
1207
|
+
if (!outcome.ok) return null;
|
|
1208
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1209
|
+
for (const item of outcome.value) {
|
|
1210
|
+
if (item.action !== "count" || !item.ok) return null;
|
|
1211
|
+
counts.set(item.id, item.value);
|
|
1212
|
+
}
|
|
1213
|
+
return counts.size === ids.length ? counts : null;
|
|
1214
|
+
}
|
|
1215
|
+
/**
|
|
1216
|
+
* 行首 16px 格里的主勾选框:**只在多选模式内渲染**(模式入口在「⋯ → 多选」,不常驻)。
|
|
1217
|
+
*
|
|
1218
|
+
* 用的是回收站多选同款原生 `Checkbox`:它自己会 `stopPropagation`,所以点框不会顺带打开会话
|
|
1219
|
+
* (行首格在宿主行那条 `onClick` 里面)。宿主对「有状态点」和归档行不渲染这一格,
|
|
1220
|
+
* 那些行走 `BatchPickButton` 兜底;两条不会同时出现(见 `ui.css` 的兜底隐藏规则)。
|
|
1221
|
+
*/
|
|
1222
|
+
function BatchPickCheckbox({ sessionId, title, t = zhTranslate }) {
|
|
1223
|
+
const selectingNow = useSelecting();
|
|
1224
|
+
const on = usePicks().some((candidate) => candidate.sessionId === sessionId);
|
|
1225
|
+
if (!selectingNow) return null;
|
|
1226
|
+
const name = title === "" ? t("common.untitled") : title;
|
|
1227
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1228
|
+
className: "sd-batch-pick-leading",
|
|
1229
|
+
"data-dsh-session-delete": "batch-pick",
|
|
1230
|
+
"data-seat": "leading",
|
|
1231
|
+
"data-checked": on ? "true" : "false",
|
|
1232
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Checkbox, {
|
|
1233
|
+
checked: on,
|
|
1234
|
+
label: t("batch.row.pick", { title: name }),
|
|
1235
|
+
onChange: () => {
|
|
1236
|
+
togglePick({
|
|
1237
|
+
sessionId,
|
|
1238
|
+
title
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
})
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
/**
|
|
1245
|
+
* 多选态里行尾悬停排的**兜底**勾选开关:只在宿主不渲染行首格的行上露头(状态点行 / 归档行)。
|
|
1246
|
+
* 座位原文保证「排内点击不误开会话」(`rowActions` 那层自己挡住冒泡),所以这颗按钮不需要
|
|
1247
|
+
* 自己 `stopPropagation`。行首那颗与这颗共用同一份选中集和同一句 aria 名字;模式内点整行也能选。
|
|
1248
|
+
*/
|
|
1249
|
+
function BatchPickButton({ sessionId, title, live = false, t = zhTranslate }) {
|
|
1250
|
+
const selectingNow = useSelecting();
|
|
1251
|
+
const on = usePicks().some((candidate) => candidate.sessionId === sessionId);
|
|
1252
|
+
(0, react.useEffect)(() => {
|
|
1253
|
+
rowInfo.set(sessionId, {
|
|
1254
|
+
title,
|
|
1255
|
+
live
|
|
1256
|
+
});
|
|
1257
|
+
}, [
|
|
1258
|
+
sessionId,
|
|
1259
|
+
title,
|
|
1260
|
+
live
|
|
1261
|
+
]);
|
|
1262
|
+
if (!selectingNow) return null;
|
|
1263
|
+
const name = title === "" ? t("common.untitled") : title;
|
|
1264
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Tooltip, {
|
|
1265
|
+
label: t("menu.live"),
|
|
1266
|
+
side: "right",
|
|
1267
|
+
portal: true,
|
|
1268
|
+
disabled: !live,
|
|
1269
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1270
|
+
className: "sd-batch-pick-action",
|
|
1271
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1272
|
+
type: "button",
|
|
1273
|
+
className: "sd-rowaction-button",
|
|
1274
|
+
"data-dsh-session-delete": "batch-pick",
|
|
1275
|
+
"data-seat": "action",
|
|
1276
|
+
"data-checked": on ? "true" : "false",
|
|
1277
|
+
"aria-pressed": on,
|
|
1278
|
+
"aria-label": t("batch.row.pick", { title: name }),
|
|
1279
|
+
disabled: live,
|
|
1280
|
+
onClick: () => togglePick({
|
|
1281
|
+
sessionId,
|
|
1282
|
+
title
|
|
1283
|
+
}),
|
|
1284
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconCheckOutlineRegular, { size: 14 })
|
|
1285
|
+
})
|
|
1286
|
+
})
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
//#endregion
|
|
1290
|
+
//#region src/client/trash-store.ts
|
|
1291
|
+
/**
|
|
1292
|
+
* 回收站清单的**失效通告台**:动过回收站的动作戳一下,清单面板据此重新取数。
|
|
1293
|
+
*
|
|
1294
|
+
* 为什么需要它(真机反馈):回收站面板是「挂载时取一次数」(`useEffect(reload)`),而删除发生在
|
|
1295
|
+
* **侧栏菜单**那边(`delete-confirm.tsx` 的常驻宿主),两者没有共同的父组件——删完之后中央那个
|
|
1296
|
+
* 面板(如果正开着)不会自己重读,用户得切走再切回来(= 重新挂载)才看得见新条目。撤销(`undo-toast.tsx`)
|
|
1297
|
+
* 同理。
|
|
1298
|
+
*
|
|
1299
|
+
* 形状与两个邻居同款(`undo-toast.tsx` 的通知台、`delete-confirm.tsx` 的现场):**模块级现场 +
|
|
1300
|
+
* `useSyncExternalStore`**,不为此引状态库、也不引入上下文——三样东西都挂在 `shell.overlay`/`main`
|
|
1301
|
+
* 这些各不相邻的座位上,模块级是它们唯一的共同点。
|
|
1302
|
+
*
|
|
1303
|
+
* 语义只有"变过"这一件事:一个单调递增的修订号。面板把它写进 effect 的依赖里,戳一次重读一次;
|
|
1304
|
+
* 不需要知道是谁动的、动了什么(清单本体永远是问宿主要)。
|
|
1305
|
+
*/
|
|
1306
|
+
let revision = 0;
|
|
1307
|
+
const listeners$3 = /* @__PURE__ */ new Set();
|
|
1308
|
+
/** 通告一次"回收站变过了"(删除成功、撤销成功;面板自己的恢复/彻底删除则就地重读)。 */
|
|
1309
|
+
function invalidateTrash() {
|
|
1310
|
+
revision += 1;
|
|
1311
|
+
for (const listener of listeners$3) listener();
|
|
1312
|
+
}
|
|
1313
|
+
function getTrashRevision() {
|
|
1314
|
+
return revision;
|
|
1315
|
+
}
|
|
1316
|
+
function subscribeTrash(listener) {
|
|
1317
|
+
listeners$3.add(listener);
|
|
1318
|
+
return () => {
|
|
1319
|
+
listeners$3.delete(listener);
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
/** 供面板订阅。 */
|
|
1323
|
+
function useTrashRevision() {
|
|
1324
|
+
return (0, react.useSyncExternalStore)(subscribeTrash, getTrashRevision, getTrashRevision);
|
|
1325
|
+
}
|
|
1326
|
+
//#endregion
|
|
1327
|
+
//#region src/client/ui/Button.tsx
|
|
1328
|
+
/**
|
|
1329
|
+
* `Button`(**照抄** `@deepseek-ai/dsh-client-ui-primitives@0.2.0-rc.2` 的 Button,出处与三条改写规则
|
|
1330
|
+
* 见 `ui.css` 头注):`<button type="button">`,`variant` × `size` 两个正交档,可带前导图标。
|
|
1331
|
+
*
|
|
1332
|
+
* 只收本插件用到的面:`primary` / `ghost` / `outline` 三档与 `md`(36px) / `sm`(28px) 两号;
|
|
1333
|
+
* 官方还有 `toolbar` 变体与 `className` 之外的转发面,这里不铺开。
|
|
1334
|
+
*
|
|
1335
|
+
* **转发 `ref`**:官方把 `ref` 交给原生按钮("for focus management and overlay anchors"),
|
|
1336
|
+
* `Modal` 的初始焦点与外壳的对齐测量都吃这个。React 18 的 `forwardRef` 与官方实现同形。
|
|
1337
|
+
*/
|
|
1338
|
+
const Button = (0, react.forwardRef)(function Button({ variant = "ghost", size = "md", icon, className, children, ...rest }, ref) {
|
|
1339
|
+
const classes = [
|
|
1340
|
+
"sd-button",
|
|
1341
|
+
`sd-button-${variant}`,
|
|
1342
|
+
`sd-button-${size}`,
|
|
1343
|
+
className
|
|
1344
|
+
].filter((name) => name !== void 0 && name !== "").join(" ");
|
|
1345
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1346
|
+
ref,
|
|
1347
|
+
type: "button",
|
|
1348
|
+
className: classes,
|
|
1349
|
+
...rest,
|
|
1350
|
+
children: [icon === void 0 || icon === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1351
|
+
className: "sd-button-icon",
|
|
1352
|
+
children: icon
|
|
1353
|
+
}), children]
|
|
1354
|
+
});
|
|
1355
|
+
});
|
|
844
1356
|
//#endregion
|
|
845
1357
|
//#region src/client/ui/Modal.tsx
|
|
846
1358
|
/**
|
|
@@ -1060,45 +1572,56 @@ window.__ModuleLoader__.load({
|
|
|
1060
1572
|
* (它在平台表内,但这里用不着;React 就够,少一条平台依赖)。
|
|
1061
1573
|
*/
|
|
1062
1574
|
let notice = null;
|
|
1063
|
-
let nextSeq$
|
|
1064
|
-
const listeners$
|
|
1065
|
-
const emit$
|
|
1066
|
-
for (const listener of listeners$
|
|
1575
|
+
let nextSeq$2 = 0;
|
|
1576
|
+
const listeners$2 = /* @__PURE__ */ new Set();
|
|
1577
|
+
const emit$2 = () => {
|
|
1578
|
+
for (const listener of listeners$2) listener();
|
|
1067
1579
|
};
|
|
1068
|
-
/**
|
|
1580
|
+
/** 删除成功后发布一条撤销提示(单条)。 */
|
|
1069
1581
|
function publishUndoNotice(input) {
|
|
1070
1582
|
notice = {
|
|
1071
|
-
seq: nextSeq$
|
|
1583
|
+
seq: nextSeq$2 += 1,
|
|
1584
|
+
kind: "one",
|
|
1072
1585
|
entryId: input.entryId,
|
|
1073
1586
|
title: input.title
|
|
1074
1587
|
};
|
|
1075
|
-
emit$
|
|
1588
|
+
emit$2();
|
|
1589
|
+
}
|
|
1590
|
+
/** 批量删除成功后发布一条撤销提示(第十三批②):覆盖上一条,只报这一次移走的条数。 */
|
|
1591
|
+
function publishUndoBatchNotice(input) {
|
|
1592
|
+
notice = {
|
|
1593
|
+
seq: nextSeq$2 += 1,
|
|
1594
|
+
kind: "batch",
|
|
1595
|
+
entryIds: input.entryIds,
|
|
1596
|
+
n: input.n
|
|
1597
|
+
};
|
|
1598
|
+
emit$2();
|
|
1076
1599
|
}
|
|
1077
1600
|
/** 撤销成功后把提示收掉(用户点「撤销」、或横幅自己走完停留时间)。 */
|
|
1078
1601
|
function clearUndoNotice() {
|
|
1079
1602
|
if (notice === null) return;
|
|
1080
1603
|
notice = null;
|
|
1081
|
-
emit$
|
|
1604
|
+
emit$2();
|
|
1082
1605
|
}
|
|
1083
1606
|
function getUndoNotice() {
|
|
1084
1607
|
return notice;
|
|
1085
1608
|
}
|
|
1086
|
-
function subscribe$
|
|
1087
|
-
listeners$
|
|
1609
|
+
function subscribe$2(listener) {
|
|
1610
|
+
listeners$2.add(listener);
|
|
1088
1611
|
return () => {
|
|
1089
|
-
listeners$
|
|
1612
|
+
listeners$2.delete(listener);
|
|
1090
1613
|
};
|
|
1091
1614
|
}
|
|
1092
1615
|
/** 供组件订阅。 */
|
|
1093
1616
|
function useUndoNotice() {
|
|
1094
|
-
return (0, react.useSyncExternalStore)(subscribe$
|
|
1617
|
+
return (0, react.useSyncExternalStore)(subscribe$2, getUndoNotice, getUndoNotice);
|
|
1095
1618
|
}
|
|
1096
1619
|
/** 提示停留时长。设计稿 §7 定死:**6 秒**(Toast 自己的默认是 3 秒)。 */
|
|
1097
1620
|
const UNDO_HOLD_MS = 6e3;
|
|
1098
|
-
/**
|
|
1621
|
+
/** 撤销:单条 `restore(entryId)`、批量**一次** `applyOutcomes('restore', entryIds)`;失败就把原因换到同一条横幅上,不静默。 */
|
|
1099
1622
|
async function undo(deps, current) {
|
|
1100
|
-
if (current.entryId === "") return;
|
|
1101
|
-
const outcome = await callRemote(() => deps.restore(current.entryId));
|
|
1623
|
+
if (current.kind === "one" && current.entryId === "") return;
|
|
1624
|
+
const outcome = current.kind === "one" ? await callRemote(() => deps.restore(current.entryId)) : await callRemote(() => deps.applyOutcomes("restore", current.entryIds));
|
|
1102
1625
|
if (outcome.ok) {
|
|
1103
1626
|
invalidateTrash();
|
|
1104
1627
|
clearUndoNotice();
|
|
@@ -1106,10 +1629,10 @@ window.__ModuleLoader__.load({
|
|
|
1106
1629
|
}
|
|
1107
1630
|
notice = {
|
|
1108
1631
|
...current,
|
|
1109
|
-
seq: nextSeq$
|
|
1632
|
+
seq: nextSeq$2 += 1,
|
|
1110
1633
|
failure: outcome.failure.message
|
|
1111
1634
|
};
|
|
1112
|
-
emit$
|
|
1635
|
+
emit$2();
|
|
1113
1636
|
}
|
|
1114
1637
|
/**
|
|
1115
1638
|
* 常驻的撤销提示宿主。挂 `shell.overlay`(框架级浮层;槽位说明原文 "a toast stack or a status
|
|
@@ -1118,21 +1641,22 @@ window.__ModuleLoader__.load({
|
|
|
1118
1641
|
function UndoToast({ deps, t = zhTranslate }) {
|
|
1119
1642
|
const current = useUndoNotice();
|
|
1120
1643
|
if (current === null) return null;
|
|
1121
|
-
const { seq,
|
|
1644
|
+
const { seq, failure } = current;
|
|
1122
1645
|
if (failure !== void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Toast, {
|
|
1123
1646
|
text: failure,
|
|
1124
1647
|
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconWarningOutlineRegular, { size: 16 }),
|
|
1125
1648
|
holdMs: UNDO_HOLD_MS,
|
|
1126
1649
|
onDone: clearUndoNotice
|
|
1127
1650
|
}, seq);
|
|
1651
|
+
const canUndo = current.kind === "one" ? current.entryId !== "" : current.entryIds.length > 0;
|
|
1128
1652
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Toast, {
|
|
1129
1653
|
tone: "success",
|
|
1130
|
-
text: t("undo.moved", { title: title === "" ? t("undo.session") : title }),
|
|
1654
|
+
text: current.kind === "one" ? t("undo.moved", { title: current.title === "" ? t("undo.session") : current.title }) : t("undo.movedBatch", { n: current.n }),
|
|
1131
1655
|
holdMs: UNDO_HOLD_MS,
|
|
1132
|
-
actions:
|
|
1656
|
+
actions: canUndo ? [{
|
|
1133
1657
|
label: t("undo.undo"),
|
|
1134
1658
|
onClick: () => void undo(deps, current)
|
|
1135
|
-
}],
|
|
1659
|
+
}] : void 0,
|
|
1136
1660
|
onDone: clearUndoNotice
|
|
1137
1661
|
}, seq);
|
|
1138
1662
|
}
|
|
@@ -1174,39 +1698,39 @@ window.__ModuleLoader__.load({
|
|
|
1174
1698
|
border: "0.5px solid var(--dsw-alias-border-l4)",
|
|
1175
1699
|
borderRadius: "var(--dsw-radius-lg)"
|
|
1176
1700
|
};
|
|
1177
|
-
let request = null;
|
|
1178
|
-
let nextSeq = 0;
|
|
1179
|
-
const listeners = /* @__PURE__ */ new Set();
|
|
1180
|
-
const emit = () => {
|
|
1181
|
-
for (const listener of listeners) listener();
|
|
1701
|
+
let request$1 = null;
|
|
1702
|
+
let nextSeq$1 = 0;
|
|
1703
|
+
const listeners$1 = /* @__PURE__ */ new Set();
|
|
1704
|
+
const emit$1 = () => {
|
|
1705
|
+
for (const listener of listeners$1) listener();
|
|
1182
1706
|
};
|
|
1183
1707
|
/** 菜单行调用:请求删除这个会话(确认框由 overlay 上的宿主渲染)。 */
|
|
1184
1708
|
function requestDelete(input) {
|
|
1185
|
-
request = {
|
|
1186
|
-
seq: nextSeq += 1,
|
|
1709
|
+
request$1 = {
|
|
1710
|
+
seq: nextSeq$1 += 1,
|
|
1187
1711
|
sessionId: input.sessionId,
|
|
1188
1712
|
title: input.title
|
|
1189
1713
|
};
|
|
1190
|
-
emit();
|
|
1714
|
+
emit$1();
|
|
1191
1715
|
}
|
|
1192
1716
|
/** 收起确认框(用户点「取消」/「关闭」、或删除成功后收工)。 */
|
|
1193
1717
|
function clearDeleteRequest() {
|
|
1194
|
-
if (request === null) return;
|
|
1195
|
-
request = null;
|
|
1196
|
-
emit();
|
|
1718
|
+
if (request$1 === null) return;
|
|
1719
|
+
request$1 = null;
|
|
1720
|
+
emit$1();
|
|
1197
1721
|
}
|
|
1198
1722
|
function getDeleteRequest() {
|
|
1199
|
-
return request;
|
|
1723
|
+
return request$1;
|
|
1200
1724
|
}
|
|
1201
|
-
function subscribe(listener) {
|
|
1202
|
-
listeners.add(listener);
|
|
1725
|
+
function subscribe$1(listener) {
|
|
1726
|
+
listeners$1.add(listener);
|
|
1203
1727
|
return () => {
|
|
1204
|
-
listeners.delete(listener);
|
|
1728
|
+
listeners$1.delete(listener);
|
|
1205
1729
|
};
|
|
1206
1730
|
}
|
|
1207
1731
|
/** 供宿主组件订阅。 */
|
|
1208
1732
|
function useDeleteRequest() {
|
|
1209
|
-
return (0, react.useSyncExternalStore)(subscribe, getDeleteRequest, getDeleteRequest);
|
|
1733
|
+
return (0, react.useSyncExternalStore)(subscribe$1, getDeleteRequest, getDeleteRequest);
|
|
1210
1734
|
}
|
|
1211
1735
|
/**
|
|
1212
1736
|
* 常驻的确认框宿主。挂 `shell.overlay`(框架级浮层;槽位说明原文 "a toast stack or a status
|
|
@@ -1230,6 +1754,22 @@ window.__ModuleLoader__.load({
|
|
|
1230
1754
|
function ConfirmDialog({ request: current, deps, t }) {
|
|
1231
1755
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
1232
1756
|
const [failure, setFailure] = (0, react.useState)(null);
|
|
1757
|
+
/**
|
|
1758
|
+
* 这条会话在盘上带着多少个后代。删除是按组走的(10-15 稿 §11),而宿主的客户端本来就把孩子那几行
|
|
1759
|
+
* 整排藏了 ⇒ 不说就没人知道点下去侧栏会少一片(issue #3)。那一刻它还没进回收站,清单里没有它,
|
|
1760
|
+
* 所以只能现问一次。
|
|
1761
|
+
*/
|
|
1762
|
+
const [subagents, setSubagents] = (0, react.useState)(0);
|
|
1763
|
+
(0, react.useEffect)(() => {
|
|
1764
|
+
let alive = true;
|
|
1765
|
+
(async () => {
|
|
1766
|
+
const outcome = await callRemote(() => deps.countSubagents(current.sessionId));
|
|
1767
|
+
if (alive) setSubagents(outcome.ok ? outcome.value : 0);
|
|
1768
|
+
})();
|
|
1769
|
+
return () => {
|
|
1770
|
+
alive = false;
|
|
1771
|
+
};
|
|
1772
|
+
}, [deps, current.sessionId]);
|
|
1233
1773
|
const confirm = async () => {
|
|
1234
1774
|
setBusy(true);
|
|
1235
1775
|
setFailure(null);
|
|
@@ -1241,7 +1781,7 @@ window.__ModuleLoader__.load({
|
|
|
1241
1781
|
}
|
|
1242
1782
|
clearDeleteRequest();
|
|
1243
1783
|
invalidateTrash();
|
|
1244
|
-
publishUndoNotice({
|
|
1784
|
+
if (outcome.value !== null) publishUndoNotice({
|
|
1245
1785
|
entryId: outcome.value.id,
|
|
1246
1786
|
title: current.title
|
|
1247
1787
|
});
|
|
@@ -1277,6 +1817,7 @@ window.__ModuleLoader__.load({
|
|
|
1277
1817
|
children: current.title === "" ? t("common.untitled") : current.title
|
|
1278
1818
|
})
|
|
1279
1819
|
}),
|
|
1820
|
+
subagents === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("dialog.subagents", { n: subagents }) }),
|
|
1280
1821
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("dialog.hint") }),
|
|
1281
1822
|
failure === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Tag, {
|
|
1282
1823
|
tone: "danger",
|
|
@@ -1286,153 +1827,356 @@ window.__ModuleLoader__.load({
|
|
|
1286
1827
|
});
|
|
1287
1828
|
}
|
|
1288
1829
|
//#endregion
|
|
1289
|
-
//#region src/client/
|
|
1290
|
-
function MenuItemButton({ children, icon, disabled = false, danger = false, separatorBefore = false, onSelect }) {
|
|
1291
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1292
|
-
className: "sd-menu-item-wrap",
|
|
1293
|
-
children: [separatorBefore ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1294
|
-
className: "sd-menu-separator",
|
|
1295
|
-
role: "separator"
|
|
1296
|
-
}) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1297
|
-
type: "button",
|
|
1298
|
-
role: "menuitem",
|
|
1299
|
-
className: danger ? "sd-menu-item sd-menu-item-danger" : "sd-menu-item",
|
|
1300
|
-
disabled,
|
|
1301
|
-
onClick: onSelect,
|
|
1302
|
-
children: [icon === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1303
|
-
className: "sd-menu-item-icon",
|
|
1304
|
-
children: icon
|
|
1305
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1306
|
-
className: "sd-menu-item-label",
|
|
1307
|
-
children
|
|
1308
|
-
})]
|
|
1309
|
-
})]
|
|
1310
|
-
});
|
|
1311
|
-
}
|
|
1312
|
-
//#endregion
|
|
1313
|
-
//#region src/client/ui/Tooltip.tsx
|
|
1830
|
+
//#region src/client/batch-confirm.tsx
|
|
1314
1831
|
/**
|
|
1315
|
-
* `
|
|
1316
|
-
* 规则见 `ui.css` 头注):把提示挂在锚点元素上,hover / 键盘 focus 才出。
|
|
1317
|
-
*
|
|
1318
|
-
* **保留下来的行为**:`side` 三个方位的定位(`right` 取锚点右缘再让出 10px,`bottom`/`top` 居中并按
|
|
1319
|
-
* `gap` 让位)、视口边界收口(`edgeMargin=12`、`bottom`/`top` 装不下就翻面)、等 `ResizeObserver`
|
|
1320
|
-
* 量到气泡尺寸再显示(所以不会先闪一个错位的框)、指针交互之后的 focus 不弹(见 `styles.ts` 的
|
|
1321
|
-
* `isPointerModality`)、`portal` 时挂到 `document.body`(宿主菜单有裁剪与层叠上下文,锚点在菜单里
|
|
1322
|
-
* 时必须 portal,否则气泡被裁掉)。
|
|
1832
|
+
* 批量删除的确认框(第十三批②)——挂在 `shell.overlay` 上的宿主,加它读的那个模块级现场。
|
|
1323
1833
|
*
|
|
1324
|
-
*
|
|
1325
|
-
* `
|
|
1326
|
-
* `
|
|
1327
|
-
*
|
|
1328
|
-
* 留着才与官方同形。
|
|
1834
|
+
* 为什么又开一个文件而不是把 `delete-confirm.tsx` 撑大:单删那张框是**已交付的面**(设计稿 §7 状态 ②,
|
|
1835
|
+
* 用例钉着),批量形与它只有三件不同——标题随条数、目标卡逐条列、执行是一次批量 `apply`。共用一个壳
|
|
1836
|
+
* 意味着单删的 `busy` / 失败面要跟着学批量形,那是拿已交付的行为去换省一个文件。卡片样式(`TARGET_CARD`)
|
|
1837
|
+
* 与 hint、子会话那两句**复用**单删的,一句不造第二个键。
|
|
1329
1838
|
*
|
|
1330
|
-
*
|
|
1331
|
-
*
|
|
1839
|
+
* 现场是模块级的(同 `delete-confirm.tsx` / `undo-toast.tsx`):请求从浮条那颗按钮发来,确认框住在
|
|
1840
|
+
* `shell.overlay` 上——它必须活在行开关那棵子树之外(槽位行随宿主重排而卸载,见 `delete-confirm.tsx` 头注)。
|
|
1332
1841
|
*/
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
const
|
|
1842
|
+
let request = null;
|
|
1843
|
+
let nextSeq = 0;
|
|
1844
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1845
|
+
const emit = () => {
|
|
1846
|
+
for (const listener of listeners) listener();
|
|
1847
|
+
};
|
|
1848
|
+
/** 浮条那颗按钮调用:请求确认这一批(缺省 = 当前选中集)。空名单不发框。 */
|
|
1849
|
+
function requestDeleteBatch(items = getPicks()) {
|
|
1850
|
+
if (items.length === 0) return;
|
|
1851
|
+
request = {
|
|
1852
|
+
seq: nextSeq += 1,
|
|
1853
|
+
items: [...items]
|
|
1854
|
+
};
|
|
1855
|
+
emit();
|
|
1856
|
+
}
|
|
1857
|
+
function clearDeleteBatchRequest() {
|
|
1858
|
+
if (request === null) return;
|
|
1859
|
+
request = null;
|
|
1860
|
+
emit();
|
|
1861
|
+
}
|
|
1862
|
+
/** 重试轮之后把现场收缩到失败名单(框里只剩还没办成的那几条)。 */
|
|
1863
|
+
function shrinkRequestToFailures(failedSessionIds) {
|
|
1864
|
+
if (request === null) return;
|
|
1865
|
+
const keep = new Set(failedSessionIds);
|
|
1866
|
+
request = {
|
|
1867
|
+
...request,
|
|
1868
|
+
items: request.items.filter((item) => keep.has(item.sessionId))
|
|
1869
|
+
};
|
|
1870
|
+
emit();
|
|
1871
|
+
}
|
|
1872
|
+
function getBatchRequest() {
|
|
1873
|
+
return request;
|
|
1874
|
+
}
|
|
1875
|
+
function subscribe(listener) {
|
|
1876
|
+
listeners.add(listener);
|
|
1877
|
+
return () => {
|
|
1878
|
+
listeners.delete(listener);
|
|
1879
|
+
};
|
|
1880
|
+
}
|
|
1881
|
+
/** 供浮条与装配面订阅。 */
|
|
1882
|
+
function useDeleteBatchRequest() {
|
|
1883
|
+
return (0, react.useSyncExternalStore)(subscribe, getBatchRequest, getBatchRequest);
|
|
1884
|
+
}
|
|
1885
|
+
/** 常驻的批量确认框宿主。没有待确认的批量删除时什么都不渲染。 */
|
|
1886
|
+
function BatchConfirmHost({ deps, t = zhTranslate }) {
|
|
1887
|
+
const current = useDeleteBatchRequest();
|
|
1888
|
+
if (current === null) return null;
|
|
1889
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BatchConfirmDialog, {
|
|
1890
|
+
request: current,
|
|
1891
|
+
deps,
|
|
1892
|
+
t
|
|
1893
|
+
}, current.seq);
|
|
1894
|
+
}
|
|
1895
|
+
/** 目标卡里那一行:标题(可及名走 ellipsis,与单删那张同)+ 有才标的子会话数。 */
|
|
1896
|
+
const TARGET_LINE = {
|
|
1897
|
+
display: "flex",
|
|
1898
|
+
alignItems: "center",
|
|
1899
|
+
gap: "8px",
|
|
1900
|
+
padding: "3px 0"
|
|
1901
|
+
};
|
|
1902
|
+
const TARGET_TITLE = {
|
|
1903
|
+
fontSize: 14,
|
|
1904
|
+
fontWeight: 500,
|
|
1905
|
+
lineHeight: "20px",
|
|
1906
|
+
minWidth: 0,
|
|
1907
|
+
overflow: "hidden",
|
|
1908
|
+
textOverflow: "ellipsis",
|
|
1909
|
+
whiteSpace: "nowrap"
|
|
1910
|
+
};
|
|
1911
|
+
const TARGET_META = {
|
|
1912
|
+
fontSize: 12,
|
|
1913
|
+
lineHeight: "18px",
|
|
1914
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
1915
|
+
};
|
|
1916
|
+
/**
|
|
1917
|
+
* 目标卡要能装几十条:2026-10-17 真机一次选 64 条,卡片把弹框撑出视口,标题和底部按钮都被顶没了。
|
|
1918
|
+
* 沿用回收站批量恢复确认框的 280px 先例——**只在卡片内部滚**,头与动作行留在框里。
|
|
1919
|
+
*/
|
|
1920
|
+
const BATCH_TARGET_CARD = {
|
|
1921
|
+
...TARGET_CARD,
|
|
1922
|
+
maxHeight: 280,
|
|
1923
|
+
overflow: "auto"
|
|
1924
|
+
};
|
|
1925
|
+
function BatchConfirmDialog({ request: current, deps, t }) {
|
|
1926
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
1927
|
+
/** 逐条失败原文:`sessionId → 信封的 message`。失败**不关框**(与单删同款)。 */
|
|
1928
|
+
const [failures, setFailures] = (0, react.useState)(/* @__PURE__ */ new Map());
|
|
1929
|
+
/** 开框那一刻现问的子会话计数;`null` = 有一条读不到 ⇒ 那两半句整句不说(不许拿 0 冒充"没有")。 */
|
|
1930
|
+
const [counts, setCounts] = (0, react.useState)(null);
|
|
1931
|
+
const ids = current.items.map((item) => item.sessionId);
|
|
1932
|
+
const failedIds = [...failures.keys()];
|
|
1933
|
+
/** 首轮发全部;重试轮只发失败的那些(成功的已经不在名单里了)。 */
|
|
1934
|
+
const pendingIds = failedIds.length > 0 ? failedIds : ids;
|
|
1935
|
+
const totalKids = counts === null ? 0 : [...counts.values()].reduce((sum, n) => sum + n, 0);
|
|
1338
1936
|
(0, react.useEffect)(() => {
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
let size;
|
|
1343
|
-
let placement = side;
|
|
1344
|
-
const fit = () => {
|
|
1345
|
-
if (size === void 0) return;
|
|
1346
|
-
const { inlineSize: width, blockSize: height } = size;
|
|
1347
|
-
const offset = side === "right" ? 0 : width / 2;
|
|
1348
|
-
const left = Math.max(EDGE_MARGIN, Math.min(rect.x - offset, window.innerWidth - EDGE_MARGIN - width));
|
|
1349
|
-
const fitsBelow = rect.bottom + gap + height <= window.innerHeight - EDGE_MARGIN;
|
|
1350
|
-
const fitsAbove = rect.top - gap - height >= EDGE_MARGIN;
|
|
1351
|
-
if (placement === "bottom" && !fitsBelow && fitsAbove) placement = "top";
|
|
1352
|
-
else if (placement === "top" && !fitsAbove && fitsBelow) placement = "bottom";
|
|
1353
|
-
element.style.left = `${left + offset}px`;
|
|
1354
|
-
element.style.top = `${placement === "right" ? (rect.top + rect.bottom) / 2 : placement === "top" ? rect.top - gap : rect.bottom + gap}px`;
|
|
1355
|
-
element.dataset.side = placement;
|
|
1356
|
-
element.style.visibility = "visible";
|
|
1357
|
-
};
|
|
1358
|
-
const observer = new ResizeObserver((entries) => {
|
|
1359
|
-
size = entries[0]?.borderBoxSize[0];
|
|
1360
|
-
fit();
|
|
1937
|
+
let alive = true;
|
|
1938
|
+
fetchCounts(deps, current.items.map((item) => item.sessionId)).then((result) => {
|
|
1939
|
+
if (alive) setCounts(result);
|
|
1361
1940
|
});
|
|
1362
|
-
observer.observe(element, { box: "border-box" });
|
|
1363
|
-
window.addEventListener("resize", fit);
|
|
1364
1941
|
return () => {
|
|
1365
|
-
|
|
1366
|
-
window.removeEventListener("resize", fit);
|
|
1942
|
+
alive = false;
|
|
1367
1943
|
};
|
|
1368
|
-
}, [
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
const
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1944
|
+
}, [deps, current.seq]);
|
|
1945
|
+
const run = async () => {
|
|
1946
|
+
setBusy(true);
|
|
1947
|
+
setFailures(/* @__PURE__ */ new Map());
|
|
1948
|
+
const outcome = await callRemote(() => deps.applyOutcomes("delete", pendingIds));
|
|
1949
|
+
setBusy(false);
|
|
1950
|
+
if (!outcome.ok) {
|
|
1951
|
+
setFailures(new Map(pendingIds.map((id) => [id, outcome.failure.message])));
|
|
1952
|
+
return;
|
|
1953
|
+
}
|
|
1954
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1955
|
+
for (const item of outcome.value) if (!byId.has(item.id)) byId.set(item.id, item);
|
|
1956
|
+
const moved = [];
|
|
1957
|
+
const failed = /* @__PURE__ */ new Map();
|
|
1958
|
+
let okCount = 0;
|
|
1959
|
+
for (const id of pendingIds) {
|
|
1960
|
+
const item = byId.get(id);
|
|
1961
|
+
if (item === void 0) {
|
|
1962
|
+
failed.set(id, `批量通道没回这一条的结果:delete ${id}`);
|
|
1963
|
+
continue;
|
|
1964
|
+
}
|
|
1965
|
+
if (!item.ok) {
|
|
1966
|
+
failed.set(id, item.error.message);
|
|
1967
|
+
continue;
|
|
1968
|
+
}
|
|
1969
|
+
okCount += 1;
|
|
1970
|
+
if (item.action === "delete" && item.value !== null) moved.push(item.value.id);
|
|
1971
|
+
}
|
|
1972
|
+
if (okCount > 0) publishUndoBatchNotice({
|
|
1973
|
+
entryIds: moved,
|
|
1974
|
+
n: okCount
|
|
1383
1975
|
});
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
if (disabled) setRect(null);
|
|
1390
|
-
}, [disabled]);
|
|
1391
|
-
/** 锚点自己带的处理器要照常跑(我们只加不减)——官方也是先 `children.props.onX?.(e)` 再接管。 */
|
|
1392
|
-
const inner = children.props;
|
|
1393
|
-
const anchorElement = (0, react.cloneElement)(children, {
|
|
1394
|
-
ref: (element) => {
|
|
1395
|
-
anchor.current = element;
|
|
1396
|
-
},
|
|
1397
|
-
onMouseEnter: (event) => {
|
|
1398
|
-
inner.onMouseEnter?.(event);
|
|
1399
|
-
if (!disabled) show();
|
|
1400
|
-
},
|
|
1401
|
-
onMouseLeave: (event) => {
|
|
1402
|
-
inner.onMouseLeave?.(event);
|
|
1403
|
-
withdraw();
|
|
1404
|
-
},
|
|
1405
|
-
onClick: (event) => {
|
|
1406
|
-
inner.onClick?.(event);
|
|
1407
|
-
withdraw();
|
|
1408
|
-
},
|
|
1409
|
-
onFocus: (event) => {
|
|
1410
|
-
inner.onFocus?.(event);
|
|
1411
|
-
if (isPointerModality()) return;
|
|
1412
|
-
if (!disabled) show();
|
|
1413
|
-
},
|
|
1414
|
-
onBlur: (event) => {
|
|
1415
|
-
inner.onBlur?.(event);
|
|
1416
|
-
withdraw();
|
|
1976
|
+
invalidateTrash();
|
|
1977
|
+
shrinkPicksToFailures([...failed.keys()]);
|
|
1978
|
+
if (failed.size === 0) {
|
|
1979
|
+
clearDeleteBatchRequest();
|
|
1980
|
+
return;
|
|
1417
1981
|
}
|
|
1982
|
+
setFailures(failed);
|
|
1983
|
+
shrinkRequestToFailures([...failed.keys()]);
|
|
1984
|
+
};
|
|
1985
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Modal, {
|
|
1986
|
+
open: true,
|
|
1987
|
+
onClose: clearDeleteBatchRequest,
|
|
1988
|
+
title: current.items.length === 1 ? t("dialog.title") : t("dialog.batch.title", { n: current.items.length }),
|
|
1989
|
+
closeLabel: t("common.close"),
|
|
1990
|
+
footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
1991
|
+
variant: "outline",
|
|
1992
|
+
onClick: clearDeleteBatchRequest,
|
|
1993
|
+
children: t("common.cancel")
|
|
1994
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
1995
|
+
variant: "primary",
|
|
1996
|
+
disabled: busy,
|
|
1997
|
+
onClick: () => void run(),
|
|
1998
|
+
children: failedIds.length > 0 ? t("dialog.batch.retry", { n: failedIds.length }) : t("batch.run", { n: ids.length })
|
|
1999
|
+
})] }),
|
|
2000
|
+
children: [
|
|
2001
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2002
|
+
"data-dsh-session-delete": "batch-target-card",
|
|
2003
|
+
style: BATCH_TARGET_CARD,
|
|
2004
|
+
children: current.items.map((item) => {
|
|
2005
|
+
const kids = counts === null ? 0 : counts.get(item.sessionId) ?? 0;
|
|
2006
|
+
const failure = failures.get(item.sessionId);
|
|
2007
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2008
|
+
"data-dsh-session-delete": "batch-target",
|
|
2009
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2010
|
+
style: TARGET_LINE,
|
|
2011
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2012
|
+
style: TARGET_TITLE,
|
|
2013
|
+
children: item.title === "" ? t("common.untitled") : item.title
|
|
2014
|
+
}), kids > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2015
|
+
style: TARGET_META,
|
|
2016
|
+
children: t("trash.row.subagents", { n: kids })
|
|
2017
|
+
}) : null]
|
|
2018
|
+
}), failure === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Tag, {
|
|
2019
|
+
tone: "danger",
|
|
2020
|
+
children: failure
|
|
2021
|
+
})]
|
|
2022
|
+
}, item.sessionId);
|
|
2023
|
+
})
|
|
2024
|
+
}),
|
|
2025
|
+
totalKids === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("dialog.subagents", { n: totalKids }) }),
|
|
2026
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("dialog.hint") })
|
|
2027
|
+
]
|
|
1418
2028
|
});
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
2029
|
+
}
|
|
2030
|
+
//#endregion
|
|
2031
|
+
//#region src/client/batch-bar.tsx
|
|
2032
|
+
/**
|
|
2033
|
+
* 批量删除的浮条(第十三批①):**多选模式内常驻**(0 条也渲染,既显示模式又给退出入口),
|
|
2034
|
+
* 汇总「已选 N 条」+(读得到时)「含 K 个子会话」;「查看已选」展开限高可滚动的标题清单
|
|
2035
|
+
* (逐条可移除),回答"到底选了哪些"——清单默认收起,不把底部输入区盖住。
|
|
2036
|
+
* 模式内的整行点击守卫也挂在这棵常驻宿主上(见 `batch-select.tsx` 的 `useSelectionRowClickGuard`)。
|
|
2037
|
+
*
|
|
2038
|
+
* 为什么单独一个文件而不是并进 `batch-select.tsx`:那份是"选中集 + 行开关"(被确认框 import),
|
|
2039
|
+
* 而浮条要点开确认框(要 import 它的现场)——两个方向合到一个文件上就是一圈。浮条住在
|
|
2040
|
+
* `shell.overlay` 上,与单删的撤销提示/确认框同类(槽位原文:"a toast stack or a status pill
|
|
2041
|
+
* all belong here")。
|
|
2042
|
+
*
|
|
2043
|
+
* 那个 K 是**勾选变化后防抖 300ms 的一次批量 count**(`/apply` 的 `action:'count'`,一组 id 一次),
|
|
2044
|
+
* 并且带**序号守卫**:在飞的旧响应不许盖掉新名单的读数(与第十二批那条 `reloadSeq` 同一个道理)。
|
|
2045
|
+
* 读不到就整句不说——K 是合计,缺一条就是假数(10-15 稿 §10「不许拿 0 冒充没有」)。
|
|
2046
|
+
*/
|
|
2047
|
+
/** 勾选变化后到发问之间的等待:连勾十条不该发十次 count。 */
|
|
2048
|
+
const COUNT_DEBOUNCE_MS = 300;
|
|
2049
|
+
function BatchBar({ deps, t = zhTranslate }) {
|
|
2050
|
+
const selecting = useSelecting();
|
|
2051
|
+
const picks = usePicks();
|
|
2052
|
+
useSelectionRowClickGuard();
|
|
2053
|
+
const [counts, setCounts] = (0, react.useState)(null);
|
|
2054
|
+
const [expanded, setExpanded] = (0, react.useState)(false);
|
|
2055
|
+
const seqRef = (0, react.useRef)(0);
|
|
2056
|
+
(0, react.useEffect)(() => {
|
|
2057
|
+
const ids = picks.map((item) => item.sessionId);
|
|
2058
|
+
const seq = seqRef.current += 1;
|
|
2059
|
+
const timer = setTimeout(() => {
|
|
2060
|
+
fetchCounts(deps, ids).then((result) => {
|
|
2061
|
+
if (seq === seqRef.current) setCounts(result);
|
|
2062
|
+
});
|
|
2063
|
+
}, COUNT_DEBOUNCE_MS);
|
|
2064
|
+
return () => clearTimeout(timer);
|
|
2065
|
+
}, [deps, picks]);
|
|
2066
|
+
(0, react.useEffect)(() => {
|
|
2067
|
+
if (!selecting || picks.length === 0) setExpanded(false);
|
|
2068
|
+
}, [selecting, picks.length]);
|
|
2069
|
+
if (!selecting) return null;
|
|
2070
|
+
const kids = counts === null ? 0 : picks.reduce((sum, item) => sum + (counts.get(item.sessionId) ?? 0), 0);
|
|
2071
|
+
const allPicked = allVisibleSelected();
|
|
2072
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2073
|
+
className: "sd-batch-bar",
|
|
2074
|
+
"data-dsh-session-delete": "batch-bar",
|
|
2075
|
+
children: [expanded && picks.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2076
|
+
className: "sd-batch-bar-list-head",
|
|
2077
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("batch.bar.count", { n: picks.length }) }), kids > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("batch.bar.kids", { k: kids }) }) : null]
|
|
2078
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
2079
|
+
className: "sd-batch-bar-list",
|
|
2080
|
+
children: picks.map((item) => {
|
|
2081
|
+
const title = item.title === "" ? t("common.untitled") : item.title;
|
|
2082
|
+
const rowKids = counts?.get(item.sessionId) ?? 0;
|
|
2083
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
2084
|
+
className: "sd-batch-bar-item",
|
|
2085
|
+
children: [
|
|
2086
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2087
|
+
className: "sd-batch-bar-title",
|
|
2088
|
+
title: item.title === "" ? void 0 : item.title,
|
|
2089
|
+
children: title
|
|
2090
|
+
}),
|
|
2091
|
+
rowKids > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2092
|
+
className: "sd-batch-bar-meta",
|
|
2093
|
+
children: t("trash.row.subagents", { n: rowKids })
|
|
2094
|
+
}) : null,
|
|
2095
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2096
|
+
type: "button",
|
|
2097
|
+
className: "sd-batch-bar-remove",
|
|
2098
|
+
"aria-label": t("batch.bar.remove", { title }),
|
|
2099
|
+
onClick: () => togglePick(item),
|
|
2100
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconCloseOutlineRegular, { size: 12 })
|
|
2101
|
+
})
|
|
2102
|
+
]
|
|
2103
|
+
}, item.sessionId);
|
|
2104
|
+
})
|
|
2105
|
+
})] }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2106
|
+
className: "sd-batch-bar-main",
|
|
2107
|
+
children: [
|
|
2108
|
+
picks.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2109
|
+
className: "sd-batch-bar-hint",
|
|
2110
|
+
children: t("batch.bar.pickHint")
|
|
2111
|
+
}) : null,
|
|
2112
|
+
picks.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
2113
|
+
variant: "ghost",
|
|
2114
|
+
size: "sm",
|
|
2115
|
+
"aria-expanded": expanded,
|
|
2116
|
+
onClick: () => setExpanded((open) => !open),
|
|
2117
|
+
children: expanded ? t("batch.bar.reviewClose") : t("batch.bar.review")
|
|
2118
|
+
}) : null,
|
|
2119
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
2120
|
+
variant: "outline",
|
|
2121
|
+
size: "sm",
|
|
2122
|
+
onClick: allPicked ? clearPicks : selectAllVisible,
|
|
2123
|
+
children: allPicked ? t("batch.bar.selectNone") : t("batch.bar.selectAll")
|
|
2124
|
+
}),
|
|
2125
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
2126
|
+
variant: "primary",
|
|
2127
|
+
size: "sm",
|
|
2128
|
+
disabled: picks.length === 0,
|
|
2129
|
+
onClick: () => requestDeleteBatch(getPicks()),
|
|
2130
|
+
children: t("batch.run", { n: picks.length })
|
|
2131
|
+
}),
|
|
2132
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
2133
|
+
variant: "ghost",
|
|
2134
|
+
size: "sm",
|
|
2135
|
+
onClick: exitSelection,
|
|
2136
|
+
children: t("trash.toolbar.selectExit")
|
|
2137
|
+
})
|
|
2138
|
+
]
|
|
2139
|
+
})]
|
|
2140
|
+
});
|
|
2141
|
+
}
|
|
2142
|
+
//#endregion
|
|
2143
|
+
//#region src/client/ui/MenuItemButton.tsx
|
|
2144
|
+
function MenuItemButton({ children, icon, disabled = false, danger = false, separatorBefore = false, onSelect }) {
|
|
2145
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2146
|
+
className: "sd-menu-item-wrap",
|
|
2147
|
+
children: [separatorBefore ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2148
|
+
className: "sd-menu-separator",
|
|
2149
|
+
role: "separator"
|
|
2150
|
+
}) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2151
|
+
type: "button",
|
|
2152
|
+
role: "menuitem",
|
|
2153
|
+
className: danger ? "sd-menu-item sd-menu-item-danger" : "sd-menu-item",
|
|
2154
|
+
disabled,
|
|
2155
|
+
onClick: onSelect,
|
|
2156
|
+
children: [icon === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2157
|
+
className: "sd-menu-item-icon",
|
|
2158
|
+
children: icon
|
|
2159
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2160
|
+
className: "sd-menu-item-label",
|
|
2161
|
+
children
|
|
2162
|
+
})]
|
|
2163
|
+
})]
|
|
2164
|
+
});
|
|
2165
|
+
}
|
|
2166
|
+
//#endregion
|
|
2167
|
+
//#region src/client/batch-menu-item.tsx
|
|
2168
|
+
function BatchSelectMenuItem({ dismissMenu, t = zhTranslate }) {
|
|
2169
|
+
const selecting = useSelecting();
|
|
2170
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MenuItemButton, {
|
|
2171
|
+
separatorBefore: true,
|
|
2172
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconCheckOutlineRegular, { size: 14 }),
|
|
2173
|
+
onSelect: () => {
|
|
2174
|
+
if (selecting) exitSelection();
|
|
2175
|
+
else enterSelection();
|
|
2176
|
+
dismissMenu?.();
|
|
1428
2177
|
},
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
className: "sd-tooltip-label",
|
|
1432
|
-
children: label
|
|
1433
|
-
})
|
|
1434
|
-
}) : null;
|
|
1435
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [anchorElement, content === null ? null : portal ? (0, react_dom.createPortal)(content, document.body) : content] });
|
|
2178
|
+
children: selecting ? t("trash.toolbar.selectExit") : t("trash.toolbar.select")
|
|
2179
|
+
});
|
|
1436
2180
|
}
|
|
1437
2181
|
//#endregion
|
|
1438
2182
|
//#region src/client/delete-menu-item.tsx
|
|
@@ -1463,7 +2207,7 @@ window.__ModuleLoader__.load({
|
|
|
1463
2207
|
* 行为同款:hover / 键盘 focus 才出)挂在行上
|
|
1464
2208
|
* (anchor 用一层 `<span>`:`MenuItemButton` 是普通函数组件、接不住 `Tooltip` 注入的 ref)。
|
|
1465
2209
|
*/
|
|
1466
|
-
function DeleteMenuItem({ sessionId, title, live = false, dismissMenu, t = zhTranslate }) {
|
|
2210
|
+
function DeleteMenuItem({ sessionId, title, live = false, separatorBefore = true, dismissMenu, t = zhTranslate }) {
|
|
1467
2211
|
const select = () => {
|
|
1468
2212
|
requestDelete({
|
|
1469
2213
|
sessionId,
|
|
@@ -1477,7 +2221,7 @@ window.__ModuleLoader__.load({
|
|
|
1477
2221
|
portal: true,
|
|
1478
2222
|
disabled: !live,
|
|
1479
2223
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MenuItemButton, {
|
|
1480
|
-
separatorBefore
|
|
2224
|
+
separatorBefore,
|
|
1481
2225
|
danger: true,
|
|
1482
2226
|
disabled: live,
|
|
1483
2227
|
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconTrashOutlineRegular, { size: 14 }),
|
|
@@ -23952,45 +24696,6 @@ window.__ModuleLoader__.load({
|
|
|
23952
24696
|
return width;
|
|
23953
24697
|
}
|
|
23954
24698
|
//#endregion
|
|
23955
|
-
//#region src/client/ui/Checkbox.tsx
|
|
23956
|
-
/**
|
|
23957
|
-
* 勾选框(2026-10-15 加,回收站多选态用)。
|
|
23958
|
-
*
|
|
23959
|
-
* 样式与行为抄自 `@deepseek-ai/dsh-client-ui-primitives` 的 `lib/Checkbox.module.css`
|
|
23960
|
-
* (那四条声明逐字进了 `ui.css` 的 `.sd-checkbox`):**本体是原生 `input[type=checkbox]` +
|
|
23961
|
-
* `accent-color`,不是自绘的圆框**。参考稿上那种圆形勾选框是别家 App 的形状,抄进来就成了
|
|
23962
|
-
* "宿主界面里长出一个外来控件";原生这一款在深浅两套主题下都跟宿主同形,键盘与读屏也白送。
|
|
23963
|
-
*
|
|
23964
|
-
* 一处与官方 `Checkbox` 的差异是**结构上的**:官方那版是 `<label>` 包 input + 可见文字,
|
|
23965
|
-
* 我们这一格只有框、名字走 `aria-label`(行本身就是可点的整块,再挂一份可见文字是重复)。
|
|
23966
|
-
*
|
|
23967
|
-
* **`stopPropagation` 不是可有可无的**:清单整行是个点击目标(多选态下"点行=切换勾选"),
|
|
23968
|
-
* 勾选框住在行里。不拦住冒泡,点框这一下会被行再处理一次 ⇒ 勾上又立刻取消,看起来像"点不动"。
|
|
23969
|
-
*/
|
|
23970
|
-
function Checkbox({ checked, indeterminate = false, onChange, label, disabled = false }) {
|
|
23971
|
-
const input = (0, react.useRef)(null);
|
|
23972
|
-
(0, react.useEffect)(() => {
|
|
23973
|
-
if (input.current !== null) input.current.indeterminate = indeterminate;
|
|
23974
|
-
}, [indeterminate]);
|
|
23975
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
23976
|
-
className: "sd-checkbox",
|
|
23977
|
-
onClick: (event) => {
|
|
23978
|
-
event.stopPropagation();
|
|
23979
|
-
},
|
|
23980
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
23981
|
-
ref: input,
|
|
23982
|
-
type: "checkbox",
|
|
23983
|
-
checked,
|
|
23984
|
-
disabled,
|
|
23985
|
-
"aria-label": label,
|
|
23986
|
-
"aria-checked": indeterminate ? "mixed" : checked,
|
|
23987
|
-
onChange: (event) => {
|
|
23988
|
-
onChange(event.target.checked);
|
|
23989
|
-
}
|
|
23990
|
-
})
|
|
23991
|
-
});
|
|
23992
|
-
}
|
|
23993
|
-
//#endregion
|
|
23994
24699
|
//#region src/client/trash-panel.tsx
|
|
23995
24700
|
/**
|
|
23996
24701
|
* 回收站清单面板(设计稿 §7 状态 ④⑤ + 2026-10-15 §2 的两栏形态)——**中央面板的本体**(`main` 槽的占用者)。
|
|
@@ -24217,6 +24922,13 @@ window.__ModuleLoader__.load({
|
|
|
24217
24922
|
entries: list
|
|
24218
24923
|
})).sort((a, b) => b.latest - a.latest);
|
|
24219
24924
|
}
|
|
24925
|
+
/**
|
|
24926
|
+
* 「这一次彻底删除会释放多少」:那一格连着**折进来的孩子**一起算——彻底删除按组走(10-15 稿 §11),
|
|
24927
|
+
* 点下去页脚合计掉的就是整组。页脚合计与单条确认卡片共用这一个算式,同一屏不许有两套口径(issue #2)。
|
|
24928
|
+
*/
|
|
24929
|
+
function releaseBytesOf(entry) {
|
|
24930
|
+
return entry.sizeBytes + (entry.subagentBytes ?? 0);
|
|
24931
|
+
}
|
|
24220
24932
|
function TrashPanel({ deps, t = zhTranslate }) {
|
|
24221
24933
|
const [entries, setEntries] = (0, react.useState)(null);
|
|
24222
24934
|
const [listFailure, setListFailure] = (0, react.useState)(null);
|
|
@@ -24235,9 +24947,9 @@ window.__ModuleLoader__.load({
|
|
|
24235
24947
|
*/
|
|
24236
24948
|
const [collapsedGroups, setCollapsedGroups] = (0, react.useState)(/* @__PURE__ */ new Set());
|
|
24237
24949
|
/**
|
|
24238
|
-
* 多选态与勾选集合(2026-10-15
|
|
24239
|
-
* 勾选住 `Set<entryId
|
|
24240
|
-
* "我先后点了哪几条"
|
|
24950
|
+
* 多选态与勾选集合(2026-10-15 开的口子先只配了**批量彻底删除**;2026-10-17 补上**批量恢复**)。
|
|
24951
|
+
* 勾选住 `Set<entryId>`,渲染顺序不跟点击顺序——页脚两颗按**清单从上往下**执行,
|
|
24952
|
+
* "我先后点了哪几条"不该影响一批动作的执行顺序读感。
|
|
24241
24953
|
*/
|
|
24242
24954
|
const [selectMode, setSelectMode] = (0, react.useState)(false);
|
|
24243
24955
|
const [checked, setChecked] = (0, react.useState)(/* @__PURE__ */ new Set());
|
|
@@ -24262,8 +24974,15 @@ window.__ModuleLoader__.load({
|
|
|
24262
24974
|
label: t("trash.divider.label")
|
|
24263
24975
|
});
|
|
24264
24976
|
const revision = useTrashRevision();
|
|
24977
|
+
/**
|
|
24978
|
+
* 重读带**到达序号**:在飞的 `list` 不保证按发起顺序落地(对账耗时随盘上条目数摆动),
|
|
24979
|
+
* 后到的旧响应不许盖掉刚发生的动作带来的新状态。只应用序号最大的一份。
|
|
24980
|
+
*/
|
|
24981
|
+
const reloadSeq = (0, react.useRef)(0);
|
|
24265
24982
|
const reload = (0, react.useCallback)(async () => {
|
|
24983
|
+
const seq = ++reloadSeq.current;
|
|
24266
24984
|
const outcome = await callRemote(() => deps.list());
|
|
24985
|
+
if (seq !== reloadSeq.current) return;
|
|
24267
24986
|
if (!outcome.ok) {
|
|
24268
24987
|
setListFailure(outcome.failure.message);
|
|
24269
24988
|
setEntries(null);
|
|
@@ -24317,8 +25036,10 @@ window.__ModuleLoader__.load({
|
|
|
24317
25036
|
const toggleAllGroups = () => {
|
|
24318
25037
|
setCollapsedGroups(allCollapsed ? /* @__PURE__ */ new Set() : new Set(groups.map((group) => group.projectDir)));
|
|
24319
25038
|
};
|
|
24320
|
-
/**
|
|
24321
|
-
|
|
25039
|
+
/** 按**清单从上往下**排好的勾选 id(页脚两颗与批量执行都读这一份)。
|
|
25040
|
+
* 顺序必须走 `groups`(组间按各组最新一条、组内按删除时间),不能用扁平 `entries`:后者是
|
|
25041
|
+
* 全局 `deletedAt` 序,跨项目多选时与屏幕上的分组顺序不同。折叠组里的条目照样算在内。 */
|
|
25042
|
+
const checkedIds = groups.flatMap((group) => group.entries).filter((entry) => checked.has(entry.id)).map((entry) => entry.id);
|
|
24322
25043
|
const toggleCheck = (entryId) => {
|
|
24323
25044
|
setChecked((current) => {
|
|
24324
25045
|
const next = new Set(current);
|
|
@@ -24440,50 +25161,69 @@ window.__ModuleLoader__.load({
|
|
|
24440
25161
|
detailCache.current.delete(entry.id);
|
|
24441
25162
|
await reload();
|
|
24442
25163
|
};
|
|
24443
|
-
|
|
24444
|
-
|
|
24445
|
-
|
|
25164
|
+
/** 一段批量 → 组件状态:成功的丢详情缓存,失败的原文落回它自己那一行。整批一次上屏、一次重读。 */
|
|
25165
|
+
const settleOutcomes = (outcomes) => {
|
|
25166
|
+
const failures = [];
|
|
25167
|
+
for (const outcome of outcomes) if (outcome.ok) detailCache.current.delete(outcome.id);
|
|
25168
|
+
else failures.push({
|
|
25169
|
+
id: outcome.id,
|
|
25170
|
+
message: outcome.error.message
|
|
25171
|
+
});
|
|
25172
|
+
if (failures.length > 0) setActionFailure((current) => {
|
|
25173
|
+
const next = { ...current };
|
|
25174
|
+
for (const failure of failures) next[failure.id] = failure.message;
|
|
25175
|
+
return next;
|
|
25176
|
+
});
|
|
25177
|
+
};
|
|
25178
|
+
/**
|
|
25179
|
+
* 批量彻底删除 / 批量恢复**共用这一条**(2026-10-17 批量重构):一次 `apply` 把整批交给 Node 半,
|
|
25180
|
+
* 逐条成败住在 `ApplyOutcome` 里 ⇒ 组件不再自己拼循环、也不再"每条一次全量重读"。
|
|
25181
|
+
*/
|
|
25182
|
+
const confirmApply = async (action, ids) => {
|
|
24446
25183
|
setBusy(true);
|
|
24447
|
-
|
|
24448
|
-
|
|
24449
|
-
|
|
24450
|
-
|
|
24451
|
-
|
|
24452
|
-
else failures.push({
|
|
24453
|
-
id,
|
|
24454
|
-
message: outcome.failure.message
|
|
24455
|
-
});
|
|
24456
|
-
}
|
|
24457
|
-
setBusy(false);
|
|
24458
|
-
setPending(null);
|
|
24459
|
-
if (failures.length > 0) setActionFailure((current) => {
|
|
24460
|
-
const next = { ...current };
|
|
24461
|
-
for (const failure of failures) next[failure.id] = failure.message;
|
|
24462
|
-
return next;
|
|
24463
|
-
});
|
|
25184
|
+
const outcome = await callRemote(() => deps.applyOutcomes(action, ids));
|
|
25185
|
+
setBusy(false);
|
|
25186
|
+
setPending(null);
|
|
25187
|
+
if (!outcome.ok) {
|
|
25188
|
+
for (const id of ids) failRow(id, outcome.failure.message);
|
|
24464
25189
|
await reload();
|
|
24465
25190
|
return;
|
|
24466
25191
|
}
|
|
24467
|
-
|
|
25192
|
+
settleOutcomes(outcome.value);
|
|
25193
|
+
await reload();
|
|
25194
|
+
};
|
|
25195
|
+
const confirmPurge = async () => {
|
|
25196
|
+
const target = pending;
|
|
25197
|
+
if (target === null) return;
|
|
25198
|
+
if (target.kind === "entries") return confirmApply("purge", target.ids);
|
|
25199
|
+
if (target.kind === "restore") return confirmApply("restore", target.ids);
|
|
25200
|
+
if (target.kind === "entry") return confirmApply("purge", [target.entry.id]);
|
|
25201
|
+
setBusy(true);
|
|
25202
|
+
const outcome = await callRemote(() => deps.emptyTrash());
|
|
24468
25203
|
setBusy(false);
|
|
25204
|
+
setPending(null);
|
|
24469
25205
|
if (!outcome.ok) {
|
|
24470
|
-
|
|
24471
|
-
if (target.kind === "entry") failRow(target.entry.id, outcome.failure.message);
|
|
24472
|
-
else setListFailure(outcome.failure.message);
|
|
25206
|
+
setListFailure(outcome.failure.message);
|
|
24473
25207
|
return;
|
|
24474
25208
|
}
|
|
24475
|
-
|
|
24476
|
-
if (target.kind === "entry") detailCache.current.delete(target.entry.id);
|
|
24477
|
-
else detailCache.current.clear();
|
|
25209
|
+
detailCache.current.clear();
|
|
24478
25210
|
await reload();
|
|
24479
25211
|
};
|
|
24480
|
-
|
|
25212
|
+
/**
|
|
25213
|
+
* 详细信息那块的选中集合:确认框要把"这一批有哪些、要去哪"摆出来,靠的全量条目是从勾选 id 反查
|
|
25214
|
+
* (顺序已经是"清单从上往下")——清单里找不到的 id 是别的窗口刚动过,滤掉不等于丢动作
|
|
25215
|
+
* (重读会把它从勾选里也剔掉)。
|
|
25216
|
+
*/
|
|
25217
|
+
const byIdEntries = (0, react.useMemo)(() => new Map((entries ?? []).map((entry) => [entry.id, entry])), [entries]);
|
|
25218
|
+
const restoreEntries = pending?.kind === "restore" ? pending.ids.map((id) => byIdEntries.get(id)).filter((entry) => entry !== void 0) : [];
|
|
25219
|
+
const totalBytes = (entries ?? []).reduce((sum, entry) => sum + releaseBytesOf(entry), 0);
|
|
24481
25220
|
const now = Date.now();
|
|
24482
25221
|
const hasEntries = entries !== null && entries.length > 0;
|
|
24483
|
-
|
|
24484
|
-
|
|
24485
|
-
|
|
24486
|
-
|
|
25222
|
+
/**
|
|
25223
|
+
* 页脚那句在两种模式(清理 / 多选)下用同一份:多选态现在能删也能恢复,"选中的 N 条不可恢复"
|
|
25224
|
+
* 在准备批量恢复时是假话——那类具体警告归各自的确认框,这里只留两件事:回收站在哪、不自动过期。
|
|
25225
|
+
*/
|
|
25226
|
+
const noteParts = splitOn(t("trash.footer.note", { dir: TRASH_DIR }), TRASH_DIR);
|
|
24487
25227
|
/**
|
|
24488
25228
|
* 选中那条从**全量条目**里找,不从 `ordered`(只含展开的组)里找:把选中那条所在的组折叠起来
|
|
24489
25229
|
* 只是把那一列收掉,右栏那条不该跟着消失——详情读的是这条会话,跟清单怎么分段无关。
|
|
@@ -24733,6 +25473,8 @@ window.__ModuleLoader__.load({
|
|
|
24733
25473
|
const isSelected = entry.id === selectedId;
|
|
24734
25474
|
const isChecked = checked.has(entry.id);
|
|
24735
25475
|
const title = entry.title === "" ? t("common.untitled") : entry.title;
|
|
25476
|
+
const kids = entry.subagentCount ?? 0;
|
|
25477
|
+
const stranded = entry.strandedSubagents ?? 0;
|
|
24736
25478
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
24737
25479
|
id: `sd-trash-${entry.id}`,
|
|
24738
25480
|
role: "option",
|
|
@@ -24782,7 +25524,12 @@ window.__ModuleLoader__.load({
|
|
|
24782
25524
|
color: "var(--dsw-alias-label-tertiary)"
|
|
24783
25525
|
},
|
|
24784
25526
|
title: absoluteText(entry.deletedAt),
|
|
24785
|
-
children:
|
|
25527
|
+
children: [
|
|
25528
|
+
deletedAgo(entry.deletedAt, now, t),
|
|
25529
|
+
fileSizeText(entry.sizeBytes),
|
|
25530
|
+
kids > 0 ? t("trash.row.subagents", { n: kids }) : "",
|
|
25531
|
+
stranded > 0 ? t("trash.row.strandedSubagents", { n: stranded }) : ""
|
|
25532
|
+
].filter(Boolean).join(" · ")
|
|
24786
25533
|
}),
|
|
24787
25534
|
actionFailure[entry.id] === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
24788
25535
|
role: "status",
|
|
@@ -24982,9 +25729,27 @@ window.__ModuleLoader__.load({
|
|
|
24982
25729
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
24983
25730
|
style: {
|
|
24984
25731
|
marginLeft: "auto",
|
|
24985
|
-
flex: "none"
|
|
25732
|
+
flex: "none",
|
|
25733
|
+
display: "flex",
|
|
25734
|
+
alignItems: "center",
|
|
25735
|
+
gap: 8
|
|
24986
25736
|
},
|
|
24987
|
-
children: selectMode ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Tooltip, {
|
|
25737
|
+
children: selectMode ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Tooltip, {
|
|
25738
|
+
label: t("trash.select.needOne"),
|
|
25739
|
+
side: "top",
|
|
25740
|
+
portal: true,
|
|
25741
|
+
disabled: checkedIds.length > 0,
|
|
25742
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
25743
|
+
variant: "outline",
|
|
25744
|
+
size: "sm",
|
|
25745
|
+
disabled: checkedIds.length === 0 || busy,
|
|
25746
|
+
onClick: () => setPending({
|
|
25747
|
+
kind: "restore",
|
|
25748
|
+
ids: checkedIds
|
|
25749
|
+
}),
|
|
25750
|
+
children: t("trash.select.restore", { count: checkedIds.length })
|
|
25751
|
+
}) })
|
|
25752
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Tooltip, {
|
|
24988
25753
|
label: t("trash.select.needOne"),
|
|
24989
25754
|
side: "top",
|
|
24990
25755
|
portal: true,
|
|
@@ -25001,7 +25766,7 @@ window.__ModuleLoader__.load({
|
|
|
25001
25766
|
}),
|
|
25002
25767
|
children: t("trash.select.purge", { count: checkedIds.length })
|
|
25003
25768
|
}) })
|
|
25004
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
25769
|
+
})] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
25005
25770
|
variant: "primary",
|
|
25006
25771
|
size: "sm",
|
|
25007
25772
|
style: BULK_DANGER,
|
|
@@ -25011,17 +25776,76 @@ window.__ModuleLoader__.load({
|
|
|
25011
25776
|
})
|
|
25012
25777
|
})]
|
|
25013
25778
|
}) : null,
|
|
25014
|
-
pending === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
25779
|
+
pending === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TrashConfirm, {
|
|
25015
25780
|
pending,
|
|
25781
|
+
restoreEntries,
|
|
25016
25782
|
busy,
|
|
25017
25783
|
t,
|
|
25018
25784
|
onCancel: () => setPending(null),
|
|
25019
|
-
|
|
25785
|
+
onConfirmPurge: () => void confirmPurge(),
|
|
25786
|
+
onConfirmRestore: (ids) => void confirmApply("restore", ids)
|
|
25020
25787
|
})
|
|
25021
25788
|
]
|
|
25022
25789
|
});
|
|
25023
25790
|
}
|
|
25024
|
-
|
|
25791
|
+
/**
|
|
25792
|
+
* 两种批量动作共用这一个确认框壳:`purge`(不可恢复)与 `restore`(加法,但一批条目多、去向是
|
|
25793
|
+
* 各自的旧项目目录,点之前也要过一道)。标题、正文、确认键的措辞各处一套;批量恢复把选中条目
|
|
25794
|
+
* 列成卡片(标题 + 项目名——恢复的去向是**每条各自的**旧项目目录,逐条列出来是这句话的正文);
|
|
25795
|
+
* 批量彻底删除只给条数——那批的"去哪"没有可说的,而"不可恢复"那句警告才是要读的。
|
|
25796
|
+
*/
|
|
25797
|
+
function TrashConfirm({ pending, restoreEntries, busy, t, onCancel, onConfirmPurge, onConfirmRestore }) {
|
|
25798
|
+
if (pending.kind === "restore") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Modal, {
|
|
25799
|
+
open: true,
|
|
25800
|
+
onClose: onCancel,
|
|
25801
|
+
title: t("trash.confirm.restore", { count: pending.ids.length }),
|
|
25802
|
+
closeLabel: t("common.close"),
|
|
25803
|
+
footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
25804
|
+
variant: "outline",
|
|
25805
|
+
onClick: onCancel,
|
|
25806
|
+
children: t("common.cancel")
|
|
25807
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
25808
|
+
variant: "primary",
|
|
25809
|
+
disabled: busy,
|
|
25810
|
+
onClick: () => onConfirmRestore(pending.ids),
|
|
25811
|
+
children: t("trash.confirm.restoreNow")
|
|
25812
|
+
})] }),
|
|
25813
|
+
children: [restoreEntries.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
25814
|
+
style: {
|
|
25815
|
+
display: "flex",
|
|
25816
|
+
flexDirection: "column",
|
|
25817
|
+
gap: 6,
|
|
25818
|
+
maxHeight: 280,
|
|
25819
|
+
overflow: "auto",
|
|
25820
|
+
marginBlockEnd: 10
|
|
25821
|
+
},
|
|
25822
|
+
children: restoreEntries.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
25823
|
+
style: {
|
|
25824
|
+
...CARD,
|
|
25825
|
+
flexDirection: "column",
|
|
25826
|
+
alignItems: "stretch",
|
|
25827
|
+
gap: 2,
|
|
25828
|
+
padding: "9px 12px"
|
|
25829
|
+
},
|
|
25830
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
25831
|
+
style: {
|
|
25832
|
+
...ELLIPSIS,
|
|
25833
|
+
fontSize: 13.5,
|
|
25834
|
+
lineHeight: "19px"
|
|
25835
|
+
},
|
|
25836
|
+
children: entry.title === "" ? t("common.untitled") : entry.title
|
|
25837
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
25838
|
+
style: {
|
|
25839
|
+
...ELLIPSIS,
|
|
25840
|
+
fontSize: 12,
|
|
25841
|
+
lineHeight: "16px",
|
|
25842
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
25843
|
+
},
|
|
25844
|
+
children: projectLabel(entry.projectDir, t)
|
|
25845
|
+
})]
|
|
25846
|
+
}, entry.id))
|
|
25847
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("trash.confirm.restore.body") })]
|
|
25848
|
+
});
|
|
25025
25849
|
const title = pending.kind === "all" ? t("trash.confirm.all") : pending.kind === "entries" ? t("trash.confirm.bulk", { count: pending.ids.length }) : t("trash.confirm.one");
|
|
25026
25850
|
const strong = t("trash.confirm.body.strong");
|
|
25027
25851
|
const body = splitOn(t("trash.confirm.body", { strong }), strong);
|
|
@@ -25038,7 +25862,7 @@ window.__ModuleLoader__.load({
|
|
|
25038
25862
|
variant: "primary",
|
|
25039
25863
|
style: BULK_DANGER,
|
|
25040
25864
|
disabled: busy,
|
|
25041
|
-
onClick:
|
|
25865
|
+
onClick: onConfirmPurge,
|
|
25042
25866
|
children: t("trash.confirm.purge")
|
|
25043
25867
|
})] }),
|
|
25044
25868
|
children: [pending.kind === "entry" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -25067,7 +25891,7 @@ window.__ModuleLoader__.load({
|
|
|
25067
25891
|
children: t("trash.confirm.one.meta", {
|
|
25068
25892
|
project: projectLabel(pending.entry.projectDir, t),
|
|
25069
25893
|
at: absoluteText(pending.entry.deletedAt),
|
|
25070
|
-
size: fileSizeText(pending.entry
|
|
25894
|
+
size: fileSizeText(releaseBytesOf(pending.entry))
|
|
25071
25895
|
})
|
|
25072
25896
|
})]
|
|
25073
25897
|
}) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [
|
|
@@ -25082,6 +25906,82 @@ window.__ModuleLoader__.load({
|
|
|
25082
25906
|
}
|
|
25083
25907
|
//#endregion
|
|
25084
25908
|
//#region src/client/index.tsx
|
|
25909
|
+
/**
|
|
25910
|
+
* 浏览器半入口(CJS 单文件闭包工厂的 `factory` 体,构建契约见 tsdown.config.ts)。
|
|
25911
|
+
*
|
|
25912
|
+
* 共交付**十个**槽位贡献(第十三批起,批量删除再加五件),分四类:
|
|
25913
|
+
*
|
|
25914
|
+
* - `sidebar.workspaces.session.menu.item` —— 两行:「多选」入口(`BatchSelectMenuItem`,order 850)
|
|
25915
|
+
* 与「删除」菜单行(`DeleteMenuItem`,order 900)。多选入口按下后进入多选模式(再按一次退出)
|
|
25916
|
+
* 并收菜单;删除行**只发请求**(把 `{sessionId, title}` 写进 `delete-confirm.tsx` 的模块级现场)。
|
|
25917
|
+
* - `sidebar.session.row.leading` —— 行首 16px 格的勾选框(`BatchPickCheckbox`,第十三批①)。
|
|
25918
|
+
* **只在多选模式内渲染**(入口在「⋯ → 多选」,不常驻)。宿主在「有状态点(运行中/待确认/未读完成)」
|
|
25919
|
+
* 与归档行不渲染这一格 ⇒ 那些行由下面行尾那颗兜底;槽位只投 `sessionId`,标题
|
|
25920
|
+
* (`displayTitle`)从 root hook 的 `byId[id].title` 读。
|
|
25921
|
+
* - `sidebar.workspaces.session.row.action` —— 行尾悬停排的**兜底勾选开关**(`BatchPickButton`)。
|
|
25922
|
+
* 座位原文:"一行一颗图标按钮(动作不适用于这一行时可以不渲染)、动作归自己",且**排内点击不误开
|
|
25923
|
+
* 会话** ⇒ 兜底开关不需要处理事件传播。两条与前一条不会同时出现(`ui.css` 的 `:has` 隐藏规则)。
|
|
25924
|
+
* 它还负责把每行的标题/运行态登记给模式内的整行点击守卫(`batch-select.tsx`)。
|
|
25925
|
+
* 与 overlay 上的**浮条**(`BatchBar`)、**批量确认框**(`BatchConfirmHost`)合起来才是批量删除那一套。
|
|
25926
|
+
* 模式内点整行会拦截宿主的会话切换(document 捕获守卫);执行仍走 `POST /apply`,单删那三件一律不动。
|
|
25927
|
+
* - `shell.overlay` —— 菜单之外的**四个**常驻宿主(各一次 `slots.inject`,与宿主自己对
|
|
25928
|
+
* `conversation.chat.node` 连写十几次 `inject` 的写法同形):
|
|
25929
|
+
* ① 撤销提示 `UndoToast`;② 删除确认框 `DeleteConfirmHost`;③ 批量浮条 `BatchBar`;
|
|
25930
|
+
* ④ 批量确认框 `BatchConfirmHost`。**它们不是额外入口**:四样东西都必须
|
|
25931
|
+
* 活在菜单之外(真产物 `Menu` 是 `open && jsxs(MenuSurface, { … children: [items.map(renderEntry),
|
|
25932
|
+
* children] })`,`open === false` 时槽位行根本不渲染、行随菜单一起卸载,见 delete-confirm.tsx 的头注)。
|
|
25933
|
+
* - **回收站入口 = 一对座位**(同一次探测之后、同一个同步块里注册):
|
|
25934
|
+
* ① `sidebar.panellist` —— 全局面板列表里的一行(占用者 `TrashPanelIcon`,只出 glyph);
|
|
25935
|
+
* ② `main` —— 同字 key 的中央面板(占用者 `TrashPanel`,清单本体)。
|
|
25936
|
+
* **两条必须同批**:`ctx.layout.selectPanel(panelId)` 逐字 `if (panelId !== null &&
|
|
25937
|
+
* !this.hasMainPanel(panelId)) throw new Error(...)`(asar @21199710)——只有行、没有面板时,
|
|
25938
|
+
* 用户点一下就是运行期抛错;反过来先有面板也没有入口。
|
|
25939
|
+
*
|
|
25940
|
+
* **为什么不是底栏(2026-09-30 实测裁决,设计稿 §10)**:`sidebar.footer.action` 的**惯用形**是
|
|
25941
|
+
* 「42px 整行 + 图标 + 文字 + 右对齐 12px 计数」——官方 `client-ui-cordis` 的占用者 CSS 是
|
|
25942
|
+
* `.layer{flex:none;width:100%;height:42px}`,而座位容器只有 `.footerActions{display:flex}`
|
|
25943
|
+
* (无 gap、无 wrap)⇒ 我们交的 `Button ghost/sm` 小药丸贴底栏,与下方 42px 的「设置」行不同形。
|
|
25944
|
+
* **判据随宿主组成走,不是常量**:2026-09-30 隔离活宿主实测里 `ui-cordis` 虽在组成树里,它的
|
|
25945
|
+
* 客户端半并没有挂上那条占用者(槽位渲染为空、零子节点)——所以"被第二个租户整条挤出去"只在
|
|
25946
|
+
* 占用者真挂上时才发生;不变的是我们**不是这条座位的惯用形**。
|
|
25947
|
+
*
|
|
25948
|
+
* 槽位名与注册形状是从宿主产物里**读出来**的,不是猜的:
|
|
25949
|
+
* - 菜单槽声明:asar @46391872 `ctx.slots.declare({ name: "sidebar.workspaces", children: {
|
|
25950
|
+
* "sidebar.workspaces.session.menu.item": { kind: "list", scope: "root", inject: { hooks: {
|
|
25951
|
+
* menuOpenState: menuOpenStateFactory, shortcuts: ctx.shortcuts.catalog } } } …`;
|
|
25952
|
+
* 数据面投影 @46271806 `renderSlot("sidebar.workspaces.session.menu.item", { sessionId: node.id,
|
|
25953
|
+
* displayTitle: row.title }, { hookContext: menuOpenState })`——`menuOpenStateFactory = (_standard,
|
|
25954
|
+
* state) => () => state`(@46195835),所以这一行拿到的 prop 是 hook `useMenuOpenState`;
|
|
25955
|
+
* 宿主自己的四行注册 @46392466 起(pin 100 / rename 200 / fork 300 / archive 400)——本插件取
|
|
25956
|
+
* **order 900**,落在官方动作之后,并用 `separatorBefore` 与它们隔一条分隔线。
|
|
25957
|
+
* - 浮层槽声明:asar @21206189 `"shell.overlay": { kind: "list", scope: "root" }`。
|
|
25958
|
+
* - 面板行槽:座位目录原文「Global panel icons. Each list id addresses the matching main panel;
|
|
25959
|
+
* the sidebar owns the button and resolves its label from list metadata」;声明在 `ui-sidebar`
|
|
25960
|
+
* 的 `sidebar` 注册项 children 里(asar @44796980 区)`"sidebar.panellist": { kind: "list",
|
|
25961
|
+
* scope: "root" }`;行本体渲染 @44783481 `PanelRow` → `<Tooltip disabled={wide}>` 包一个
|
|
25962
|
+
* `<button class=panelRow aria-current={active ? "page" : undefined} onClick={selectPanel(id)}>`,
|
|
25963
|
+
* 内含 `.panelGlyph`(`renderSlot("sidebar.panellist", { size: wide ? 16 : 18, active }, { only: id })`)
|
|
25964
|
+
* 与 `.panelTitle`(`wide` 时才有)。**名字取自注册元数据**:`syncPanels` 逐字
|
|
25965
|
+
* `label: resolveSlotLabel(options.label) ?? id`(@44795361)——所以本插件把 `label` 写进注册项。
|
|
25966
|
+
* - 中央面板槽:座位目录 `main`(`kind: "keyed"`, `scope: "root"`,"Central panel selected by
|
|
25967
|
+
* sidebar entry id",保留键 `conversation`,`replaceRisk: shadows-shipped-ui`);宿主渲染
|
|
25968
|
+
* @21185725 `renderSlot("main", {}, { entryKey: usePanelInfo((info) => info.activePanelId) ??
|
|
25969
|
+
* "conversation" })`。**id 不得撞车**:座位目录原文「reusing a shipped id puts you in THAT cell
|
|
25970
|
+
* and replaces it」——静默替换比抛错更难发现,所以两个 id 都带插件专属前缀。
|
|
25971
|
+
* - **面板 id 与 main 的 key 是同一个字**(官方:list id 寻址同 id 的中央面板)。`selectPanel`
|
|
25972
|
+
* 只校验 main 里有没有注册、不校验 id 形状(@21199710),点号斜杠都不是问题。
|
|
25973
|
+
*
|
|
25974
|
+
* 宿主 ctx 用**本地窄镜像**(AGENTS.md:只声明用到的成员 + 一次强转;不装宿主类型包、
|
|
25975
|
+
* 不做 `declare module` 增强)。
|
|
25976
|
+
*
|
|
25977
|
+
* **`inject` 只声明一定存在的东西**:全仓只有 `slots` 一项——它是浏览器侧槽位注册表、
|
|
25978
|
+
* 宿主必给。**取数通道不进 `inject`**:通道是"同源 fetch 我们自己的前缀路由"(见 `remote.ts` 头注),
|
|
25979
|
+
* 它是不是在**只有探测才知道**(`await probeChannel(...)`),
|
|
25980
|
+
* 声明它既表达不了这件事,写错的代价又极高——前端启动自检对「有客户端模块但没激活成功」走的是
|
|
25981
|
+
* 致命路径(`Error: web boot: 1 entry did not activate` ⇒ 桌面壳 `reportFatal("web-boot")`
|
|
25982
|
+
* 杀掉整个进程)。
|
|
25983
|
+
* 所以:**`apply` 同步返回、绝不抛**,注册挂在探测的续拍上;探测不过就一个贡献都不注册。
|
|
25984
|
+
*/
|
|
25085
25985
|
const name = "@xrn1997/dsh-session-delete/client";
|
|
25086
25986
|
/**
|
|
25087
25987
|
* 只列**宿主公开面保证存在**的成员:`slots` 是浏览器侧槽位注册表
|
|
@@ -25093,6 +25993,10 @@ window.__ModuleLoader__.load({
|
|
|
25093
25993
|
*/
|
|
25094
25994
|
const inject = ["slots"];
|
|
25095
25995
|
const MENU_SLOT = "sidebar.workspaces.session.menu.item";
|
|
25996
|
+
/** 会话行首 16px 格(无状态点时归第三方装饰);批量删除的主勾选框落这里,多选模式内可见。 */
|
|
25997
|
+
const LEADING_SLOT = "sidebar.session.row.leading";
|
|
25998
|
+
/** 会话行尾的悬停按钮排(宿主自己的归档(100)/置顶(200) 也在这排);状态点/归档行的兜底勾选落这里。 */
|
|
25999
|
+
const ROW_ACTION_SLOT = "sidebar.workspaces.session.row.action";
|
|
25096
26000
|
const OVERLAY_SLOT = "shell.overlay";
|
|
25097
26001
|
const PANEL_SLOT = "sidebar.panellist";
|
|
25098
26002
|
const MAIN_SLOT = "main";
|
|
@@ -25100,22 +26004,50 @@ window.__ModuleLoader__.load({
|
|
|
25100
26004
|
const PLUGIN_ID = "@xrn1997/dsh-session-delete";
|
|
25101
26005
|
/** 面板 id 与 `main` 的 key 同字:官方按 list id 去 `main` 里寻址同 id 的占用者。 */
|
|
25102
26006
|
const PANEL_ID = `${PLUGIN_ID}.trash`;
|
|
25103
|
-
/** 根 hook
|
|
25104
|
-
const
|
|
26007
|
+
/** 根 hook 缺失时的兜底:对空 state 求值(`running` 判 false、`title` 取空串)。恒等身份,不会让 hook 顺序在两次渲染之间变化。 */
|
|
26008
|
+
const NEVER_SESSIONS = (select) => select({});
|
|
25105
26009
|
/** 槽位 hook 缺失时的兜底:什么都不关(同样恒等身份,hook 顺序不随渲染变)。 */
|
|
25106
26010
|
const NEVER_MENU_OPEN_STATE = () => [false, () => void 0];
|
|
25107
26011
|
/** 菜单行的宿主:文案与远程面都从槽位载荷来(`live` 由这一行自己从 root hook 读)。 */
|
|
25108
26012
|
function DeleteRow({ sessionId, displayTitle, useSessions, useMenuOpenState, t }) {
|
|
25109
|
-
const live = (typeof useSessions === "function" ? useSessions :
|
|
26013
|
+
const live = (typeof useSessions === "function" ? useSessions : NEVER_SESSIONS)((state) => state?.byId?.[sessionId]?.running === true);
|
|
25110
26014
|
const [, setMenuOpen] = (typeof useMenuOpenState === "function" ? useMenuOpenState : NEVER_MENU_OPEN_STATE)();
|
|
25111
26015
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DeleteMenuItem, {
|
|
25112
26016
|
sessionId,
|
|
25113
26017
|
title: displayTitle,
|
|
25114
26018
|
live,
|
|
26019
|
+
separatorBefore: false,
|
|
25115
26020
|
t,
|
|
25116
26021
|
dismissMenu: () => setMenuOpen(false)
|
|
25117
26022
|
});
|
|
25118
26023
|
}
|
|
26024
|
+
/** 「多选」入口的宿主:只吃菜单开合与文案;模式状态由 `BatchSelectMenuItem` 自己订阅。 */
|
|
26025
|
+
function SelectRow({ useMenuOpenState, t }) {
|
|
26026
|
+
const [, setMenuOpen] = (typeof useMenuOpenState === "function" ? useMenuOpenState : NEVER_MENU_OPEN_STATE)();
|
|
26027
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BatchSelectMenuItem, {
|
|
26028
|
+
dismissMenu: () => setMenuOpen(false),
|
|
26029
|
+
t
|
|
26030
|
+
});
|
|
26031
|
+
}
|
|
26032
|
+
/** 多选模式行首勾选框的宿主:槽位只投 `sessionId`,标题从 root hook 的 `byId[id].title` 读。 */
|
|
26033
|
+
function PickLeadingRow({ sessionId, useSessions, t }) {
|
|
26034
|
+
const title = (typeof useSessions === "function" ? useSessions : NEVER_SESSIONS)((state) => state?.byId?.[sessionId]?.title?.trim() ?? "");
|
|
26035
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BatchPickCheckbox, {
|
|
26036
|
+
sessionId,
|
|
26037
|
+
title,
|
|
26038
|
+
t
|
|
26039
|
+
});
|
|
26040
|
+
}
|
|
26041
|
+
/** 行尾兜底勾选开关的宿主:投影与菜单行同款(`sessionId` / `displayTitle`),`live` 自己从 root hook 读。 */
|
|
26042
|
+
function PickRow({ sessionId, displayTitle, useSessions, t }) {
|
|
26043
|
+
const live = (typeof useSessions === "function" ? useSessions : NEVER_SESSIONS)((state) => state?.byId?.[sessionId]?.running === true);
|
|
26044
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BatchPickButton, {
|
|
26045
|
+
sessionId,
|
|
26046
|
+
title: displayTitle,
|
|
26047
|
+
live,
|
|
26048
|
+
t
|
|
26049
|
+
});
|
|
26050
|
+
}
|
|
25119
26051
|
/** 撤销提示的宿主:组件本身只吃 `restore` 与文案。 */
|
|
25120
26052
|
function UndoToastSlot({ deps, t }) {
|
|
25121
26053
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UndoToast, {
|
|
@@ -25130,6 +26062,20 @@ window.__ModuleLoader__.load({
|
|
|
25130
26062
|
t
|
|
25131
26063
|
});
|
|
25132
26064
|
}
|
|
26065
|
+
/** 浮条的宿主:选中非空才渲染(组件自己收空态)。 */
|
|
26066
|
+
function BatchBarSlot({ deps, t }) {
|
|
26067
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BatchBar, {
|
|
26068
|
+
deps,
|
|
26069
|
+
t
|
|
26070
|
+
});
|
|
26071
|
+
}
|
|
26072
|
+
/** 批量确认框的宿主:与单删那张同理,必须脱离行开关那棵子树的生命周期。 */
|
|
26073
|
+
function BatchConfirmSlot({ deps, t }) {
|
|
26074
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BatchConfirmHost, {
|
|
26075
|
+
deps,
|
|
26076
|
+
t
|
|
26077
|
+
});
|
|
26078
|
+
}
|
|
25133
26079
|
/** 面板行的占用者:宿主给 `SidebarPanelIconOwnerProps { size, active }`,我们只回一个 glyph。 */
|
|
25134
26080
|
function TrashPanelIconSlot({ size }) {
|
|
25135
26081
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TrashPanelIcon, { size });
|
|
@@ -25139,7 +26085,7 @@ window.__ModuleLoader__.load({
|
|
|
25139
26085
|
*
|
|
25140
26086
|
* - 探测不过(打不通 / 回的不是信封)⇒ **早返回**:一个槽位贡献都不注册。页面上的表现是"本插件
|
|
25141
26087
|
* 不存在",而不是点下去必然失败的入口——半吊子 UI 比没有 UI 更糟(设计稿 §7「未接通态」)。
|
|
25142
|
-
* - 探测过(哪怕 `
|
|
26088
|
+
* - 探测过(哪怕 `ping` 回的是 `{ok:false,error}`)⇒ 注册:那次往返证明路由在、依赖接上了、
|
|
25143
26089
|
* 错误映射也生效了,失败该做的是把原文交给用户看。
|
|
25144
26090
|
*
|
|
25145
26091
|
* 面板那两条是**一对座位**:行的 `selectPanel(id)` 要求 `main` 里有同字 key,缺了面板那一头,
|
|
@@ -25159,6 +26105,24 @@ window.__ModuleLoader__.load({
|
|
|
25159
26105
|
order: 900,
|
|
25160
26106
|
inject: () => ({ t })
|
|
25161
26107
|
}, DeleteRow));
|
|
26108
|
+
client.slots.inject(MENU_SLOT, () => client.slots.register({
|
|
26109
|
+
name: MENU_SLOT,
|
|
26110
|
+
id: `${PLUGIN_ID}.batch-select`,
|
|
26111
|
+
order: 850,
|
|
26112
|
+
inject: () => ({ t })
|
|
26113
|
+
}, SelectRow));
|
|
26114
|
+
client.slots.inject(LEADING_SLOT, () => client.slots.register({
|
|
26115
|
+
name: LEADING_SLOT,
|
|
26116
|
+
id: `${PLUGIN_ID}.batch-pick-leading`,
|
|
26117
|
+
order: 300,
|
|
26118
|
+
inject: () => ({ t })
|
|
26119
|
+
}, PickLeadingRow));
|
|
26120
|
+
client.slots.inject(ROW_ACTION_SLOT, () => client.slots.register({
|
|
26121
|
+
name: ROW_ACTION_SLOT,
|
|
26122
|
+
id: `${PLUGIN_ID}.batch-pick`,
|
|
26123
|
+
order: 300,
|
|
26124
|
+
inject: () => ({ t })
|
|
26125
|
+
}, PickRow));
|
|
25162
26126
|
client.slots.inject(OVERLAY_SLOT, () => client.slots.register({
|
|
25163
26127
|
name: OVERLAY_SLOT,
|
|
25164
26128
|
id: `${PLUGIN_ID}.undo-toast`,
|
|
@@ -25175,6 +26139,22 @@ window.__ModuleLoader__.load({
|
|
|
25175
26139
|
t
|
|
25176
26140
|
})
|
|
25177
26141
|
}, DeleteConfirmSlot));
|
|
26142
|
+
client.slots.inject(OVERLAY_SLOT, () => client.slots.register({
|
|
26143
|
+
name: OVERLAY_SLOT,
|
|
26144
|
+
id: `${PLUGIN_ID}.batch-bar`,
|
|
26145
|
+
inject: () => ({
|
|
26146
|
+
deps,
|
|
26147
|
+
t
|
|
26148
|
+
})
|
|
26149
|
+
}, BatchBarSlot));
|
|
26150
|
+
client.slots.inject(OVERLAY_SLOT, () => client.slots.register({
|
|
26151
|
+
name: OVERLAY_SLOT,
|
|
26152
|
+
id: `${PLUGIN_ID}.batch-confirm`,
|
|
26153
|
+
inject: () => ({
|
|
26154
|
+
deps,
|
|
26155
|
+
t
|
|
26156
|
+
})
|
|
26157
|
+
}, BatchConfirmSlot));
|
|
25178
26158
|
client.slots.inject(PANEL_SLOT, () => client.slots.register({
|
|
25179
26159
|
name: PANEL_SLOT,
|
|
25180
26160
|
id: PANEL_ID,
|