@xrn1997/dsh-session-delete 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +18 -10
  2. package/THIRD_PARTY_NOTICES.md +179 -0
  3. package/cordis.patch.yml +1 -1
  4. package/docs/screenshots/screenshot-01.png +0 -0
  5. package/docs/screenshots/screenshot-02.png +0 -0
  6. package/docs/screenshots/screenshot-03.png +0 -0
  7. package/icon.svg +24 -18
  8. package/lib/client.js +1079 -245
  9. package/lib/index.js +206 -71
  10. package/lib/types/api/connection-route.d.ts +1 -1
  11. package/lib/types/api/dispatch.d.ts +1 -1
  12. package/lib/types/api/route-table.d.ts +12 -7
  13. package/lib/types/client/config-settings.d.ts +13 -0
  14. package/lib/types/client/context.d.ts +3 -0
  15. package/lib/types/client/copy.d.ts +34 -7
  16. package/lib/types/client/detail-blocks.d.ts +25 -0
  17. package/lib/types/client/index.d.ts +3 -2
  18. package/lib/types/client/maintenance-settings.d.ts +6 -7
  19. package/lib/types/client/remote.d.ts +32 -8
  20. package/lib/types/client/settings-cards.d.ts +19 -0
  21. package/lib/types/client/settings-view.d.ts +9 -0
  22. package/lib/types/client/trash-detail.d.ts +1 -1
  23. package/lib/types/client/trash-view.d.ts +31 -5
  24. package/lib/types/client/ui/icons.d.ts +23 -7
  25. package/lib/types/client/ui/recipe.d.ts +2 -0
  26. package/lib/types/client/ui/styles.d.ts +2 -0
  27. package/lib/types/client/use-detail-pager.d.ts +33 -0
  28. package/lib/types/config.d.ts +10 -11
  29. package/lib/types/deps.d.ts +9 -1
  30. package/lib/types/index.d.ts +3 -1
  31. package/lib/types/ports/agent-handles.d.ts +10 -2
  32. package/lib/types/shared/wire.d.ts +37 -5
  33. package/lib/types/verbs/config.d.ts +25 -0
  34. package/locale/en.json +1 -1
  35. package/locale/zh.json +6 -6
  36. package/package.json +20 -14
package/lib/client.js CHANGED
@@ -119,6 +119,7 @@ window.__ModuleLoader__.load({
119
119
  "trash.empty.hint": "在会话条目的「…」菜单里选「删除」,会话会先进到这里,随时可以恢复。",
120
120
  "trash.summary": "{count} 个会话 · 可释放 {size}",
121
121
  "trash.group.count": "{count} 个会话",
122
+ "trash.group.ungrouped": "未分组",
122
123
  "trash.project.unknown": "未记录项目",
123
124
  "trash.row.deletedNow": "刚刚删除",
124
125
  "trash.row.deletedAgo": "删除于 {n}{unit}前",
@@ -131,7 +132,8 @@ window.__ModuleLoader__.load({
131
132
  "trash.unit.months": "个月",
132
133
  "trash.unit.years": "年",
133
134
  "trash.row.restore": "恢复",
134
- "trash.row.purge": "彻底删除…",
135
+ "trash.row.purge": "彻底删除",
136
+ "trash.row.purgeOne": "彻底删除「{title}」",
135
137
  "trash.footer.note": "回收站本体在宿主数据根下的 {dir} 里,不自动过期;清空后不可恢复。",
136
138
  "trash.emptyAll": "清空回收站…",
137
139
  "trash.confirm.all": "清空回收站?",
@@ -171,6 +173,22 @@ window.__ModuleLoader__.load({
171
173
  "trash.attachments.skipped": "附件未清理:{reason}",
172
174
  "trash.attachments.skippedPartial": "部分附件未清理:{reason}(已清理 {removed} 个)",
173
175
  "trash.attachments.alsoFailed": ";另有 {count} 个附件删除失败",
176
+ "config.title": "可调项",
177
+ "config.hint": "保存后插件会重启;本页用保存回执回填,重开本页会重新读取。也可以直接改 profile 的 cordis.patch.yml。",
178
+ "config.orphanPreviewLimit": "孤儿附件预览条数",
179
+ "config.maxAttachmentBytes": "附件单次响应上限",
180
+ "config.maxJsonRefBytes": "附件引用扫描的单 JSON 上限",
181
+ "config.unit.items": "条",
182
+ "config.unit.bytes": "字节",
183
+ "config.loading": "正在读当前值…",
184
+ "config.loadFailed": "读取失败:{message}",
185
+ "config.save": "保存",
186
+ "config.saving": "保存中…",
187
+ "config.saved": "已保存;插件正在重启。",
188
+ "config.saveFailed": "保存失败:{message}",
189
+ "config.retry": "重试",
190
+ "config.invalid": "需为 {min}–{max} 的整数",
191
+ "config.unchanged": "没有改动",
174
192
  "maintenance.title": "维护",
175
193
  "maintenance.orphans.title": "孤儿会话",
176
194
  "maintenance.orphans.hint": "父会话已不在会话列表或回收站里的子会话。检查只读,不会自动运行。",
@@ -199,8 +217,6 @@ window.__ModuleLoader__.load({
199
217
  "detail.noBody": "这条会话没有可显示的内容",
200
218
  "detail.field.created": "创建",
201
219
  "detail.field.lastActivity": "最后活动",
202
- "detail.field.deletedAt": "删除于",
203
- "detail.deletedAt.combined": "{relative}({absolute})",
204
220
  "detail.field.span": "活跃跨度",
205
221
  "detail.field.size": "大小",
206
222
  "detail.field.turns": "轮数",
@@ -232,8 +248,16 @@ window.__ModuleLoader__.load({
232
248
  "detail.block.truncated": "已截断",
233
249
  "detail.loadOlder": "加载更早",
234
250
  "detail.loadOlder.loading": "正在加载更早…",
235
- "detail.expandAll": "全部展开",
236
- "detail.collapseAll": "全部折叠",
251
+ "detail.viewMode.label": "工作步骤展示",
252
+ "detail.viewMode.compact": "简洁",
253
+ "detail.viewMode.standard": "标准",
254
+ "detail.viewMode.detailed": "详细",
255
+ "detail.viewMode.verbose": "完全展开",
256
+ "detail.compact.empty": "这段会话只有过程内容;切到「标准」可以看到。",
257
+ "detail.compact.pageOnly": "这一页只有过程内容;点「加载更早」继续往前看。",
258
+ "detail.process.header": "工作步骤",
259
+ "detail.process.step": "{n} 步",
260
+ "detail.process.steps": "{n} 步",
237
261
  "detail.injection.runtimeContext": "运行上下文",
238
262
  "detail.injection.timeContext": "时间上下文",
239
263
  "detail.injection.agentInstructions": "Agent 指令",
@@ -256,7 +280,8 @@ window.__ModuleLoader__.load({
256
280
  "detail.attachment.saving": "保存中…",
257
281
  "detail.attachment.saveFailed": "保存失败:{message}",
258
282
  "detail.back": "← 返回清单",
259
- "detail.subagents.header": "子会话({n}) · {size}",
283
+ "detail.subagents.header": "子会话({n}) · {size},随本条一起恢复或删除",
284
+ "detail.subagents.open": "查看子会话详情",
260
285
  "detail.subagents.truncated": "仅列出前 {shown} 条,共 {total} 条",
261
286
  "detail.backToParent": "← 返回父会话",
262
287
  "detail.list.label": "回收站清单",
@@ -303,6 +328,7 @@ window.__ModuleLoader__.load({
303
328
  "trash.empty.hint": "Pick Delete in a session’s “…” menu; the session lands here first and can be restored at any time.",
304
329
  "trash.summary": "{count} sessions · {size} reclaimable",
305
330
  "trash.group.count": "{count} sessions",
331
+ "trash.group.ungrouped": "Ungrouped",
306
332
  "trash.project.unknown": "Unrecorded project",
307
333
  "trash.row.deletedNow": "Deleted just now",
308
334
  "trash.row.deletedAgo": "Deleted {n}{unit} ago",
@@ -315,7 +341,8 @@ window.__ModuleLoader__.load({
315
341
  "trash.unit.months": "mo",
316
342
  "trash.unit.years": "y",
317
343
  "trash.row.restore": "Restore",
318
- "trash.row.purge": "Delete permanently…",
344
+ "trash.row.purge": "Delete permanently",
345
+ "trash.row.purgeOne": "Delete “{title}” permanently",
319
346
  "trash.footer.note": "The trash itself lives under the host data root in {dir}; it never expires on its own, and emptying it cannot be undone.",
320
347
  "trash.emptyAll": "Empty the trash…",
321
348
  "trash.confirm.all": "Empty the trash?",
@@ -355,6 +382,22 @@ window.__ModuleLoader__.load({
355
382
  "trash.attachments.skipped": "Attachments were not cleaned: {reason}",
356
383
  "trash.attachments.skippedPartial": "Some attachments were not cleaned: {reason} ({removed} cleaned)",
357
384
  "trash.attachments.alsoFailed": "; also {count} attachment deletions failed",
385
+ "config.title": "Options",
386
+ "config.hint": "Saving restarts the plugin. This card fills in from the save receipt; reopening this page reads the values again. You can also edit the profile cordis.patch.yml.",
387
+ "config.orphanPreviewLimit": "Orphan attachment preview limit",
388
+ "config.maxAttachmentBytes": "Attachment response limit",
389
+ "config.maxJsonRefBytes": "Per-JSON reference scan limit",
390
+ "config.unit.items": "items",
391
+ "config.unit.bytes": "bytes",
392
+ "config.loading": "Reading current values…",
393
+ "config.loadFailed": "Could not read: {message}",
394
+ "config.save": "Save",
395
+ "config.saving": "Saving…",
396
+ "config.saved": "Saved; the plugin is restarting.",
397
+ "config.saveFailed": "Could not save: {message}",
398
+ "config.retry": "Retry",
399
+ "config.invalid": "Must be an integer from {min} to {max}",
400
+ "config.unchanged": "No changes",
358
401
  "maintenance.title": "Maintenance",
359
402
  "maintenance.orphans.title": "Orphan sessions",
360
403
  "maintenance.orphans.hint": "Subagent sessions whose parent is no longer in the session list or the trash. Checking only reads; it never runs automatically.",
@@ -383,8 +426,6 @@ window.__ModuleLoader__.load({
383
426
  "detail.noBody": "This session has nothing to show",
384
427
  "detail.field.created": "Created",
385
428
  "detail.field.lastActivity": "Last activity",
386
- "detail.field.deletedAt": "Deleted",
387
- "detail.deletedAt.combined": "{relative} ({absolute})",
388
429
  "detail.field.span": "Active for",
389
430
  "detail.field.size": "Size",
390
431
  "detail.field.turns": "Turns",
@@ -416,8 +457,16 @@ window.__ModuleLoader__.load({
416
457
  "detail.block.truncated": "Truncated",
417
458
  "detail.loadOlder": "Load earlier",
418
459
  "detail.loadOlder.loading": "Loading earlier…",
419
- "detail.expandAll": "Expand all",
420
- "detail.collapseAll": "Collapse all",
460
+ "detail.viewMode.label": "Work steps",
461
+ "detail.viewMode.compact": "Compact",
462
+ "detail.viewMode.standard": "Standard",
463
+ "detail.viewMode.detailed": "Detailed",
464
+ "detail.viewMode.verbose": "Verbose",
465
+ "detail.compact.empty": "This session has only process content; switch to “Standard” to see it.",
466
+ "detail.compact.pageOnly": "Only process content on this page; choose “Load earlier” to keep looking.",
467
+ "detail.process.header": "Work steps",
468
+ "detail.process.step": "{n} step",
469
+ "detail.process.steps": "{n} steps",
421
470
  "detail.injection.runtimeContext": "Runtime context",
422
471
  "detail.injection.timeContext": "Time context",
423
472
  "detail.injection.agentInstructions": "Agent instructions",
@@ -440,7 +489,8 @@ window.__ModuleLoader__.load({
440
489
  "detail.attachment.saving": "Saving…",
441
490
  "detail.attachment.saveFailed": "Save failed: {message}",
442
491
  "detail.back": "← Back to list",
443
- "detail.subagents.header": "Subagent sessions ({n}) · {size}",
492
+ "detail.subagents.header": "Subagent sessions ({n}) · {size}, restored or deleted with this session",
493
+ "detail.subagents.open": "Open subagent session",
444
494
  "detail.subagents.truncated": "Showing the first {shown} of {total} subagent sessions",
445
495
  "detail.backToParent": "← Back to parent session",
446
496
  "detail.list.label": "Trash list",
@@ -557,7 +607,7 @@ window.__ModuleLoader__.load({
557
607
  ];
558
608
  }
559
609
  /**
560
- * 绝对时间那一句串。清单行的 `title`、确认框里的meta、详情的元信息三处要的是同一个写法,
610
+ * 绝对时间那一句串。清单行的 `title`、确认框里的meta、详情的会话信息三处要的是同一个写法,
561
611
  * 各自 `new Date(...).toLocaleString()` 一遍迟早会漂成两种样子。
562
612
  */
563
613
  function absoluteText(at) {
@@ -578,6 +628,10 @@ window.__ModuleLoader__.load({
578
628
  function unitWord(unit, t) {
579
629
  return unit === "now" ? t("common.justNow") : t(UNIT_KEY[unit]);
580
630
  }
631
+ /** 「工作步骤 N 步」:英文 n=1 要单数、中文两条同形;选哪条键只住这里,调用方只报数。 */
632
+ function stepsText(n, t) {
633
+ return t(n === 1 ? "detail.process.step" : "detail.process.steps", { n });
634
+ }
581
635
  /** 「7分钟前」这类**不带谓语**的相对短语;`now` 桶不套模板——"刚刚前"不是话。 */
582
636
  function agoText(at, now, t) {
583
637
  const { unit, n } = relativeTime(at, now);
@@ -867,11 +921,11 @@ window.__ModuleLoader__.load({
867
921
  return (0, react.useSyncExternalStore)(enabled ? visibleRows.subscribe : noopSubscribe, enabled ? visibleRows.get : getEmptyRowsSnapshot, enabled ? visibleRows.get : getEmptyRowsSnapshot);
868
922
  }
869
923
  /**
870
- * 唯一的取数基础路径:宿主 `/api` + 插件前缀。九段 exact Fetch 路由、客户端探测与附件 URL
924
+ * 唯一的取数基础路径:宿主 `/api` + 插件前缀。十段 exact Fetch 路由、客户端探测与附件 URL
871
925
  * 都从它起;`SESSION_DELETE_API_PREFIX` 只作为段名的相对前缀与错误原文。
872
926
  */
873
927
  const OFFICIAL_SESSION_DELETE_API_PREFIX = `/api/dsh-session-delete-api`;
874
- /** 九个段名(相对前缀)。**每项 `{ path, method }` 两键**:某段用什么方法也是线上形状的一部分
928
+ /** 十段段名(相对前缀)。**每项 `{ path, method }` 两键**:某段用什么方法也是线上形状的一部分
875
929
  * (ADR-0003),家就在这一张表——路由记录(`src/api/route-table.ts`)逐行投影(`ROUTES.x.method`),
876
930
  * 浏览器半的 GET 段同样投影、写段把 `ROUTES.x.method` 交给 `src/client/remote.ts` 的 `post()`,
877
931
  * 两半都不逐段手写一份。
@@ -886,7 +940,8 @@ window.__ModuleLoader__.load({
886
940
  * 这一段只负责"有多少、在哪、多大"。
887
941
  * - `attachment`:附件原始字节(全仓唯一的二进制响应段;失败仍回 JSON 信封)。
888
942
  * - `attachmentOrphans`:无引用图片对象的扫描报告(纯读;数量/字节 + 供预览的对象清单)。
889
- * - `attachmentClean`:清掉报告那一刻无引用的图片对象(聚合回执 `AttachmentCleanupWire`)。 */
943
+ * - `attachmentClean`:清掉报告那一刻无引用的图片对象(聚合回执 `AttachmentCleanupWire`)。
944
+ * - `config`:三只可调项的读/写共用一段——空体读当前值,带字段写回 profile 的 patch 层。 */
890
945
  const ROUTES = {
891
946
  list: {
892
947
  path: "/list",
@@ -897,6 +952,11 @@ window.__ModuleLoader__.load({
897
952
  path: "/ping",
898
953
  method: "GET"
899
954
  },
955
+ /** 可调项:空体读(与 `/detail` 同法,用 POST 表达一个读);带字段写,写必须进到达序链。 */
956
+ config: {
957
+ path: "/config",
958
+ method: "POST"
959
+ },
900
960
  /** 单条详情(读回收站目录里那条会话自己的日志)。 */
901
961
  detail: {
902
962
  path: "/detail",
@@ -933,6 +993,22 @@ window.__ModuleLoader__.load({
933
993
  method: "POST"
934
994
  }
935
995
  };
996
+ /** 三只可调项的接受范围(闭区间整数)。Node 半的归一与浏览器半的表单共用这一格:
997
+ * 两处各写一份范围就会「表单说能存、宿主激活时说越界」。 */
998
+ const CONFIG_LIMITS = {
999
+ orphanPreviewLimit: {
1000
+ min: 1,
1001
+ max: 1e4
1002
+ },
1003
+ maxAttachmentBytes: {
1004
+ min: 1,
1005
+ max: 209715200
1006
+ },
1007
+ maxJsonRefBytes: {
1008
+ min: 1,
1009
+ max: 104857600
1010
+ }
1011
+ };
936
1012
  /** 请求参数名:浏览器半发、Node 半读,同名同义(`empty` 没有入参)。
937
1013
  * 附件段是 **query 参数**(GET 二进制响应,没有请求体)。 */
938
1014
  const PARAMS = {
@@ -1263,10 +1339,10 @@ window.__ModuleLoader__.load({
1263
1339
  * 浏览器半的取数通道:同源 fetch 宿主 `connection.fetch` 的 exact Fetch 路由(`/api` + 插件前缀)。
1264
1340
  * 信封与失败原文在这一层收口。
1265
1341
  *
1266
- * - 九段注册在宿主 `connection.fetch.register` 上,鉴权归宿主 `/api` bridge(ADR-0033);
1342
+ * - 十段注册在宿主 `connection.fetch.register` 上,鉴权归宿主 `/api` bridge(ADR-0033);
1267
1343
  * 没有旧前缀 fallback——支持面是 web UI 与桌面端,二者的标准组合都装 `client-connection`。
1268
1344
  * 官方 remote.<namespace> 仍不可用(第三方拿不到 Typert 描述符的动态发现挂载点)。
1269
- * - 九段的**段名与方法**住 `src/shared/wire.ts` 的结构化段名表(`ROUTES` 每项 `{ path, method }`),
1345
+ * - 十段的**段名与方法**住 `src/shared/wire.ts` 的结构化段名表(`ROUTES` 每项 `{ path, method }`),
1270
1346
  * 本文件只投影段名与方法(GET 段直接取 `ROUTES.x.method`,写段把它交给 `post()`)——方法字面量
1271
1347
  * 不在本文件出现第二份;取数地址(官方 `/api` 前缀 + 文档挂载点)归 `src/client/api-base.ts`
1272
1348
  * (根部署下与旧版逐字相同)。
@@ -1382,7 +1458,12 @@ window.__ModuleLoader__.load({
1382
1458
  });
1383
1459
  return {
1384
1460
  list: () => requestEnvelope(apiPath(ROUTES.list.path), { method: ROUTES.list.method }),
1385
- ping: () => requestEnvelope(apiPath(ROUTES.ping.path), { method: ROUTES.ping.method }),
1461
+ ping: (signal) => requestEnvelope(apiPath(ROUTES.ping.path), signal === void 0 ? { method: ROUTES.ping.method } : {
1462
+ method: ROUTES.ping.method,
1463
+ signal
1464
+ }),
1465
+ config: () => requestEnvelope(apiPath(ROUTES.config.path), post(ROUTES.config, {})),
1466
+ saveConfig: (next) => requestEnvelope(apiPath(ROUTES.config.path), post(ROUTES.config, { ...next })),
1386
1467
  orphans: () => requestEnvelope(apiPath(ROUTES.orphans.path), { method: ROUTES.orphans.method }),
1387
1468
  attachmentOrphans: () => requestEnvelope(apiPath(ROUTES.attachmentOrphans.path), { method: ROUTES.attachmentOrphans.method }),
1388
1469
  attachmentClean: () => requestEnvelope(apiPath(ROUTES.attachmentClean.path), post(ROUTES.attachmentClean, {})),
@@ -1403,13 +1484,29 @@ window.__ModuleLoader__.load({
1403
1484
  * - 401/403 是宿主 `/api` 的纯文本身份/归属失败:入口不出现,不装禁用态。
1404
1485
  * - 失败信封 / 404 / 非信封同样按不可用处理:`/api` 网关对未知路径也可能回错误形状,
1405
1486
  * 不能据此把入口摆出来让每个动作都失败。
1487
+ * - **往返有 5s 上限**:半开连接或宿主没起来时 `activate` 不悬着;超时与网络失败同样是
1488
+ * 「这一代不可用」,不自动重试(一次探测一个结论,刷新页面才会再探)。三种失败都在
1489
+ * console 留下原因——入口不出现是有意的,但"为什么没出现"不能只活在脑子里。
1406
1490
  */
1407
- async function probeChannel(raw) {
1491
+ async function probeChannel(raw, options = {}) {
1492
+ const controller = new AbortController();
1493
+ const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? 5e3);
1408
1494
  try {
1409
- const result = await raw.ping();
1410
- return result.ok && result.value === "pong";
1411
- } catch {
1495
+ const result = await raw.ping(controller.signal);
1496
+ if (!result.ok) {
1497
+ console.warn("[dsh-session-delete] 取数通道探测失败,入口不注册:", result.error.code, result.error.message);
1498
+ return false;
1499
+ }
1500
+ if (result.value !== "pong") {
1501
+ console.warn("[dsh-session-delete] 取数通道回的不是 pong,入口不注册:", result.value);
1502
+ return false;
1503
+ }
1504
+ return true;
1505
+ } catch (error) {
1506
+ console.warn("[dsh-session-delete] 取数通道打不通,入口不注册:", error);
1412
1507
  return false;
1508
+ } finally {
1509
+ clearTimeout(timer);
1413
1510
  }
1414
1511
  }
1415
1512
  /**
@@ -1448,6 +1545,8 @@ window.__ModuleLoader__.load({
1448
1545
  };
1449
1546
  return {
1450
1547
  list: () => raw.list().then((result) => unwrapRemoteResult(result)),
1548
+ config: () => raw.config().then((result) => unwrapRemoteResult(result)),
1549
+ saveConfig: (next) => raw.saveConfig(next).then((result) => unwrapRemoteResult(result)),
1451
1550
  scanOrphans: () => raw.orphans().then((result) => unwrapRemoteResult(result)),
1452
1551
  scanAttachmentOrphans: () => raw.attachmentOrphans().then((result) => unwrapRemoteResult(result)),
1453
1552
  cleanAttachmentOrphans: () => raw.attachmentClean().then((result) => unwrapRemoteResult(result)),
@@ -1643,18 +1742,6 @@ window.__ModuleLoader__.load({
1643
1742
  ]
1644
1743
  });
1645
1744
  }
1646
- function IconFolderOpenOutlineRegular({ size }) {
1647
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(IconFrame, {
1648
- size,
1649
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1650
- d: "M12.3994 13.5986H2.04956C1.49728 13.5986 1.04956 13.1509 1.04956 12.5986V3.40137C1.04956 2.84908 1.49728 2.40137 2.04956 2.40137H4.76632C5.01016 2.40137 5.24561 2.49046 5.42836 2.6519L6.94088 3.98799C7.12364 4.14943 7.35908 4.23852 7.60293 4.23852H12.3994C12.9517 4.23852 13.3994 4.68624 13.3994 5.23852V7.16991",
1651
- stroke: "currentColor"
1652
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1653
- d: "M2.55911 7.93683C2.67584 7.49906 3.07229 7.19446 3.52536 7.19446H13.6491C14.3061 7.19446 14.7846 7.81725 14.6153 8.45209L13.4411 12.856C13.3244 13.2938 12.9279 13.5984 12.4748 13.5984H2.35113C1.69411 13.5984 1.21562 12.9756 1.38489 12.3407L2.55911 7.93683Z",
1654
- stroke: "currentColor"
1655
- })]
1656
- });
1657
- }
1658
1745
  function IconCloseOutlineRegular({ size }) {
1659
1746
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(IconFrame, {
1660
1747
  size,
@@ -1690,7 +1777,51 @@ window.__ModuleLoader__.load({
1690
1777
  })
1691
1778
  });
1692
1779
  }
1693
- /** 分组头「展开中」那一枚(官方 `IconChevronDownOutlineRegular`,`viewBox`/`d` 逐字)。 */
1780
+ /** 分组头:展开时(官方 `IconFolderOpenRegular`,16×16;开盖那一片带 0.16 透明度)。 */
1781
+ function IconFolderOpenRegular({ size }) {
1782
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(IconFrame, {
1783
+ size,
1784
+ children: [
1785
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1786
+ d: "M2.55912 7.93683C2.67584 7.49906 3.0723 7.19446 3.52536 7.19446H13.6491C14.3061 7.19446 14.7846 7.81725 14.6153 8.45209L13.4411 12.856C13.3244 13.2938 12.9279 13.5984 12.4748 13.5984H2.35113C1.69411 13.5984 1.21562 12.9756 1.38489 12.3407L2.55912 7.93683Z",
1787
+ fill: "currentColor",
1788
+ opacity: "0.16"
1789
+ }),
1790
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1791
+ d: "M13.6491 6.69446C14.6346 6.69453 15.3522 7.62895 15.0983 8.58118L13.9245 12.9845C13.7494 13.6412 13.1539 14.0988 12.4743 14.0988H2.35126C1.36574 14.0988 0.648153 13.1643 0.902044 12.212L2.07587 7.80774C2.25102 7.15128 2.84567 6.69455 3.52509 6.69446H13.6491ZM3.52509 7.69446C3.29865 7.69455 3.10004 7.84674 3.04169 8.06555L1.86786 12.4698C1.78345 12.7872 2.02285 13.0988 2.35126 13.0988H12.4743C12.7007 13.0988 12.8992 12.9463 12.9577 12.7277L14.1325 8.32336C14.2171 8.00598 13.9776 7.69453 13.6491 7.69446H3.52509Z",
1792
+ fill: "currentColor"
1793
+ }),
1794
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1795
+ d: "M4.7666 1.90137C5.13227 1.90144 5.48571 2.03525 5.75977 2.27734L7.27246 3.61328C7.36379 3.69382 7.48174 3.73828 7.60352 3.73828H12.3994C13.2276 3.73841 13.8993 4.41005 13.8994 5.23828V6.7168C13.8183 6.70327 13.735 6.69436 13.6494 6.69434H12.8994V5.23828C12.8993 4.96233 12.6754 4.73841 12.3994 4.73828H7.60352C7.23781 4.73828 6.88446 4.60438 6.61035 4.3623L5.09766 3.02637C5.00636 2.94576 4.88838 2.90144 4.7666 2.90137H2.0498C1.77366 2.90137 1.5498 3.12523 1.5498 3.40137V9.78223L0.902344 12.2119C0.648452 13.1642 1.36604 14.0986 2.35156 14.0986H2.0498C1.2214 14.0986 0.549838 13.427 0.549805 12.5986V3.40137C0.549805 2.57294 1.22138 1.90137 2.0498 1.90137H4.7666Z",
1796
+ fill: "currentColor"
1797
+ })
1798
+ ]
1799
+ });
1800
+ }
1801
+ /** 分组头:收起时(官方 `IconFolderCloseRegular`,16×16)。 */
1802
+ function IconFolderCloseRegular({ size }) {
1803
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(IconFrame, {
1804
+ size,
1805
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1806
+ d: "M1.50439 3.11059C1.50439 2.55831 1.95211 2.1106 2.50439 2.1106H5.43389C5.67773 2.1106 5.91318 2.19969 6.09593 2.36113L7.71649 3.79265C7.89924 3.95409 8.1347 4.04319 8.3785 4.04319H13.4958C14.0481 4.04319 14.4958 4.4909 14.4958 5.04319V12.8894C14.4958 13.4417 14.0481 13.8894 13.4958 13.8894H2.50439C1.95211 13.8894 1.50439 13.4417 1.50439 12.8894V4.04319V3.11059Z",
1807
+ stroke: "currentColor"
1808
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1809
+ d: "M3.63501 7.66614H12.3647",
1810
+ stroke: "currentColor"
1811
+ })]
1812
+ });
1813
+ }
1814
+ /** 分组头悬停时替换文件夹的折叠指示(官方 `IconTriangleRightFillRegular`);展开时旋转 90°。 */
1815
+ function IconTriangleRightFillRegular({ size }) {
1816
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconFrame, {
1817
+ size,
1818
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1819
+ d: "M5.5 4.5C5.5 4.40714 5.52586 4.31612 5.57467 4.23713C5.62349 4.15815 5.69334 4.09431 5.77639 4.05279C5.85945 4.01126 5.95242 3.99368 6.0449 4.00202C6.13738 4.01036 6.22572 4.04429 6.3 4.1L10.967 7.6C11.0291 7.64657 11.0795 7.70697 11.1142 7.77639C11.1489 7.84582 11.167 7.92238 11.167 8C11.167 8.07762 11.1489 8.15418 11.1142 8.22361C11.0795 8.29303 11.0291 8.35343 10.967 8.4L6.3 11.9C6.22572 11.9557 6.13738 11.9896 6.0449 11.998C5.95242 12.0063 5.85945 11.9887 5.77639 11.9472C5.69334 11.9057 5.62349 11.8419 5.57467 11.7629C5.52586 11.6839 5.5 11.5929 5.5 11.5V4.5Z",
1820
+ fill: "currentColor"
1821
+ })
1822
+ });
1823
+ }
1824
+ /** 折叠头与标准档「工作步骤」组「展开中」那一枚(官方 `IconChevronDownOutlineRegular`,`viewBox`/`d` 逐字)。 */
1694
1825
  function IconChevronDownOutlineRegular({ size }) {
1695
1826
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconFrame, {
1696
1827
  size,
@@ -1700,7 +1831,7 @@ window.__ModuleLoader__.load({
1700
1831
  })
1701
1832
  });
1702
1833
  }
1703
- /** 分组头「已折叠」那一枚(官方 `IconChevronRightOutlineRegular`)。 */
1834
+ /** 折叠头与子会话导航行「已折叠 / 前进」那一枚(官方 `IconChevronRightOutlineRegular`)。 */
1704
1835
  function IconChevronRightOutlineRegular({ size }) {
1705
1836
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconFrame, {
1706
1837
  size,
@@ -1710,17 +1841,68 @@ window.__ModuleLoader__.load({
1710
1841
  })
1711
1842
  });
1712
1843
  }
1844
+ /** 行内「恢复」:**本插件自绘**的回退箭头(↩,16×16 / 1px,与宿主图标同一族几何)。
1845
+ * 官方集里的 `unarchive` 与归档动作的"箱子"轮廓太近、`refresh` 读作"刷新",都不合适。 */
1846
+ function IconRestoreOutlineRegular({ size }) {
1847
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(IconFrame, {
1848
+ size,
1849
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1850
+ d: "M4.4 2.7L1.5 5.6L4.4 8.5",
1851
+ stroke: "currentColor",
1852
+ strokeLinecap: "round",
1853
+ strokeLinejoin: "round"
1854
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1855
+ d: "M1.5 5.6H9.9A4.1 4.1 0 0 1 9.9 13.8H4.7",
1856
+ stroke: "currentColor",
1857
+ strokeLinecap: "round"
1858
+ })]
1859
+ });
1860
+ }
1861
+ /** 子会话行的前导:官方 `IconBranchOutlineRegular`(16×16)。 */
1862
+ function IconBranchOutlineRegular({ size }) {
1863
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(IconFrame, {
1864
+ size,
1865
+ children: [
1866
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1867
+ d: "M1.01503 8.0001L5.6964 8.0001C6.41913 8.0001 6.78049 8.0001 7.12115 7.91951C7.4232 7.84804 7.71233 7.73014 7.97821 7.57C8.27809 7.38939 8.5364 7.13669 9.05303 6.63129L11.3281 4.40564",
1868
+ stroke: "currentColor"
1869
+ }),
1870
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1871
+ d: "M1.01221 7.9999L5.6964 7.9999C6.41913 7.9999 6.78049 7.9999 7.12115 8.08049C7.4232 8.15196 7.71233 8.26986 7.97821 8.43C8.27809 8.61061 8.5364 8.86331 9.05303 9.36871L11.3281 11.5944",
1872
+ stroke: "currentColor"
1873
+ }),
1874
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1875
+ cx: "12.4502",
1876
+ cy: "3.3079",
1877
+ r: "1.56962",
1878
+ stroke: "currentColor"
1879
+ }),
1880
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1881
+ cx: "12.4502",
1882
+ cy: "12.6921",
1883
+ r: "1.56962",
1884
+ stroke: "currentColor"
1885
+ })
1886
+ ]
1887
+ });
1888
+ }
1713
1889
  //#endregion
1714
1890
  //#region src/client/ui/ui.css?dsh-css-text
1715
1891
  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=\"row\"` 且要吃 ↑↓,而这一代 `--dsw-focus-ring-color` 是 `transparent`;\r\n * ③ `.sd-rowaction-button` 的 `[aria-pressed='true']` 与 `:disabled` 两档(2026-10-17)——\r\n * 官方那颗 `iconButton` 只有常态与 hover,勾选开关要有\"按下\"和\"这条删不动\"两档,token 与回收站\r\n * 选中行同源(`--sd-button-ghost-active-*`,别名到 `--dsw-alias-button-ghost-active-*`);④ `.sd-batch-bar`(同日)——宿主没有\"浮条\"\r\n * 这件控件,四件 token 自拼;⑤ 宿主行的批量选中底色与兜底隐藏(同日改版)——宿主的行选中态不归\r\n * 第三方,整行可见性靠 `data-row-key` + `:has`,token 与回收站选中行同源;行首格在场时行尾兜底退场;\r\n * ⑥ `.sd-batch-pick-leading`(同日)——宿主行首那格只给 16×20,勾选框要一层 inline-flex 包装才压得回\r\n * 同样大小,官方没有这层包装;⑦ `.sd-toast-action:disabled`(2026-11-01)——撤销横幅在飞时那颗动作要\r\n * 看着就按不动,官方 Toast 的 action 没有禁用档,而我们这份自带 `color`、浏览器默认置灰不生效,\r\n * 档位与③的禁用档同源(`opacity` + `not-allowed`)。\r\n * 整张表里就这七条是我们自造的,标在这里,\r\n * 下次做「与官方逐条比对」时它属于**已知差异**,不是漂移。\r\n *\r\n * **还有一处系统性改写不算抄本差异**:抄来的每条规则里,配色与几何的 token 引用都走下面那层\r\n * `--sd-*` 别名(本插件新增的一层间接)。逐属性比对时按「别名 → 它引用/兜底的那枚 `--dsw-*`」还原\r\n * 之后再比;这一层不新造颜色、只加兜底,也不重复计入上面七条。\r\n *\r\n * **另有一处已知差异不住在本文件**:回收站组头那颗按钮的整套 UA reset。宿主 `DisclosureRow` 那份是\r\n * `div`,没有这个坑;我们这行要做成可点的 `<button>`,UA 样式表会把背景、边框、内距、字体、居中与\r\n * flex 基线一起渗进来(真机上就是项目名整条居中,2026-10-15 截图)。它写在\r\n * `src/client/trash-panel.tsx` 组头那一行的 inline 样式上,**具体 reset 了哪几项由\r\n * `tests/skeleton.test.ts` 逐条钉住**(不在这里再抄一份名单)——所以「与官方逐条比对」时本表仍然只数\r\n * 上面那些,这条不算进抄本差异。\r\n */\r\n\r\n/* ──────────────── 主题 token 兜底(本插件的别名层) ────────────────\r\n * 宿主 `Theme.listTokens` 的公开面只有 14 个 `--dsw-*`(13 个 `--dsw-alias-*` +\r\n * `--dsw-specific-sidebar-fill`);这张抄本表还用了一批宿主内部 token\r\n * (`label-tertiary` / `border-l3`·`l4` / `markdown-code-*` / `scrollbar-*` …)。它们在同代宿主上\r\n * 都存在,但换代改名时 `var()` 会整条失效、颜色退回继承值。这里给**有公开等价物的语义 token**\r\n * 建一层 `--sd-*` 别名:私有 token 在场时逐字等价;不在时退到最接近的公开 token —— 最差是难看,\r\n * 不会没有颜色。\r\n *\r\n * **为什么挂 `body` 而不是 `:root`**(2026-11-04 当代宿主产物实测):`--dsw-alias-*` 那套主题\r\n * palette 写在 `body` / `body[data-ds-dark-theme]` 两条规则上,主题服务还把当前快照逐条 inline\r\n * 写到 `document.body.style`;`:root` 那份 base.css 只有字体、圆角与动效。别名层若挂 `:root`,\r\n * 引用就在 `html` 上求值——私有 token 与公开兜底都还没定义,整条 `var()` 链在计算值阶段失效,\r\n * 别名拿到的是**保证无效值**,用到它的声明(字色、边框、底色)会被整条丢弃;连 `--dsw-mask-blur`\r\n * 都会拿到兜底 `blur(2px)`,而宿主这一代给的是 `none`。挂 `body` 时:palette 与别名在同一元素上\r\n * (暗色切换一并跟着),base.css 的几何 token 从 `html` 继承下来,两边都取得到;宿主哪天把 palette\r\n * 挪回 `:root` 也仍然可见(自定义属性本来就继承),只有挪到更深的包裹元素才会再次失效——那时先\r\n * 复核这段的落点。\r\n *\r\n * 不在这一层的是:`--dsw-font-markdown-*`(整条 `font` 简写,没有等价公开 token,失效退回继承字体)、\r\n * `--dsw-static-*`(固定色:tooltip 文字、toast 动作等,允许自带颜色)、`--dsw-focus-ring-color`(原处已带 fallback)。\r\n * 新增私有 token 时先在这张表里找等价公开 token;找不到就别进别名层,并在头注记一笔。\r\n */\r\nbody {\r\n --sd-label-tertiary: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary));\r\n --sd-label-caption: var(--dsw-alias-label-caption, var(--dsw-alias-label-secondary));\r\n --sd-label-primary-foreground: var(--dsw-alias-label-primary-foreground, var(--dsw-alias-bg-base));\r\n --sd-border-l3: var(--dsw-alias-border-l3, var(--dsw-alias-border-l2));\r\n --sd-border-l4: var(--dsw-alias-border-l4, var(--dsw-alias-border-l2));\r\n --sd-menu-icon: var(--dsw-alias-menu-icon, var(--dsw-alias-label-secondary));\r\n --sd-link: var(--dsw-alias-link, var(--dsw-alias-brand-primary));\r\n --sd-interactive-bg-hover: var(--dsw-alias-interactive-bg-hover, var(--dsw-alias-bg-layer-2));\r\n --sd-interactive-bg-active: var(--dsw-alias-interactive-bg-active, var(--dsw-alias-bg-layer-2));\r\n --sd-interactive-bg-hover-danger: var(\r\n --dsw-alias-interactive-bg-hover-danger,\r\n color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent)\r\n );\r\n --sd-button-primary-fill: var(--dsw-alias-button-primary-fill, var(--dsw-alias-brand-primary));\r\n --sd-button-primary-hover: var(--dsw-alias-button-primary-hover, var(--dsw-alias-brand-primary));\r\n --sd-button-ghost-active-fill: var(--dsw-alias-button-ghost-active-fill, var(--dsw-alias-bg-layer-2));\r\n --sd-button-ghost-active-border: var(--dsw-alias-button-ghost-active-border, var(--dsw-alias-border-l2));\r\n --sd-markdown-code-block: var(--dsw-alias-markdown-code-block, var(--dsw-alias-bg-layer-2));\r\n --sd-markdown-inline-code: var(--dsw-alias-markdown-inline-code, var(--dsw-alias-bg-layer-2));\r\n --sd-state-business-primary: var(--dsw-alias-state-business-primary, var(--dsw-alias-brand-primary));\r\n --sd-state-warn-label: var(--dsw-alias-state-warn-label, var(--dsw-alias-state-warn-primary));\r\n --sd-toast-bg: var(--dsw-alias-toast-bg, var(--dsw-alias-bg-overlay));\r\n --sd-toast-label: var(--dsw-alias-toast-label, var(--dsw-alias-label-primary));\r\n --sd-tooltip-bg: var(--dsw-alias-tooltip-bg, var(--dsw-alias-bg-overlay));\r\n --sd-specific-bubble: var(--dsw-specific-bubble, var(--dsw-alias-bg-layer-2));\r\n --sd-bg-mask-1: var(\r\n --dsw-alias-bg-mask-1,\r\n color-mix(in srgb, var(--dsw-alias-label-primary) 35%, transparent)\r\n );\r\n --sd-scrollbar-bg-l1: var(--dsw-alias-scrollbar-bg-l1, var(--dsw-alias-border-l1));\r\n --sd-scrollbar-bg-l2: var(--dsw-alias-scrollbar-bg-l2, var(--dsw-alias-border-l1));\r\n --sd-scrollbar-hover-l2: var(--dsw-alias-scrollbar-hover-l2, var(--dsw-alias-border-l2));\r\n --sd-elevation-prominent: var(\r\n --dsw-elevation-prominent,\r\n 0 8px 24px color-mix(in srgb, var(--dsw-alias-label-primary) 18%, transparent)\r\n );\r\n --sd-shadow-lv3: var(\r\n --dsw-shadow-lv3,\r\n 0 1px 3px color-mix(in srgb, var(--dsw-alias-label-primary) 12%, transparent)\r\n );\r\n --sd-radius-xs: var(--dsw-radius-xs, 2px);\r\n --sd-radius-sm: var(--dsw-radius-sm, 4px);\r\n --sd-radius-md: var(--dsw-radius-md, 6px);\r\n --sd-radius-lg: var(--dsw-radius-lg, 8px);\r\n --sd-radius-xl: var(--dsw-radius-xl, 12px);\r\n --sd-radius-panel: var(--dsw-radius-panel, 12px);\r\n --sd-mask-blur: var(--dsw-mask-blur, blur(2px));\r\n --sd-focus-ring-width: var(--dsw-focus-ring-width, 2px);\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(--sd-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(--sd-radius-sm);\r\n}\r\n\r\n.sd-button-primary {\r\n background: var(--sd-button-primary-fill);\r\n color: var(--sd-label-primary-foreground);\r\n}\r\n\r\n.sd-button-primary:hover:not(:disabled) {\r\n background: var(--sd-button-primary-hover);\r\n}\r\n\r\n.sd-button-ghost:hover:not(:disabled) {\r\n background: var(--sd-interactive-bg-hover);\r\n}\r\n\r\n.sd-button-ghost:active:not(:disabled) {\r\n background: var(--sd-interactive-bg-active);\r\n}\r\n\r\n.sd-button-outline {\r\n border: 0.5px solid var(--sd-border-l3);\r\n background: transparent;\r\n}\r\n\r\n.sd-button-outline:hover:not(:disabled) {\r\n background: var(--sd-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(--sd-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(--sd-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(--sd-radius-panel);\r\n background: var(--dsw-alias-bg-layer-2);\r\n box-shadow: var(--sd-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(--sd-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(--sd-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(--sd-radius-sm);\r\n background: var(--sd-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(--sd-radius-lg);\r\n /* 设计平台的 toast 表面在**两套主题下都是深的**(浅色 bluish-800 / 深色 bluish-750),\r\n 所以这里的字色与动作色是固定值,不随主题翻转。 */\r\n background: var(--sd-toast-bg);\r\n color: var(--sd-toast-label);\r\n font-size: 14px;\r\n line-height: 22px;\r\n box-shadow: var(--sd-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(--sd-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/* 本插件自造的第七条(不是抄本,见文件头点名):撤销横幅在飞时那颗动作要**看着就按不动**。\r\n 官方 Toast 的 action 只有常态与 hover,没有禁用档;而 `.sd-toast-action` 自带 `color`,\r\n 浏览器默认的置灰根本不生效(真机上只会看见一个光标照常是手、点下去没反应的「撤销」)。\r\n 禁用档与行尾勾选开关同源(`opacity` + `not-allowed`),不新造颜色。 */\r\n.sd-toast-action:disabled {\r\n opacity: 0.4;\r\n cursor: not-allowed;\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(--sd-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(--sd-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(--sd-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(--sd-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(--sd-interactive-bg-hover-danger);\r\n}\r\n\r\n.sd-menu-item-danger:focus-visible:not(:disabled) {\r\n background: var(--sd-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(--sd-focus-ring-width) solid var(--dsw-focus-ring-color, var(--sd-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(--sd-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(--sd-scrollbar-bg-l1) calc(var(--dsh-width-handle-pointer-y, 50%) - 8px),\r\n var(--sd-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=\"row\"` 的可聚焦 div,而这一代\r\n `--dsw-focus-ring-color` 逐字是 `transparent`(2026-10-15 实测)——不自己描一条,键盘走\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(--sd-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(--sd-radius-xs);\r\n cursor: pointer;\r\n width: 16px;\r\n height: 16px;\r\n color: var(--sd-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(--sd-button-ghost-active-fill);\r\n box-shadow: inset 0 0 0 1px var(--sd-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 格里的勾选框(2026-10-17 改版,**只在\r\n 多选模式内渲染**)。格本身由宿主居中(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(--sd-button-ghost-active-fill);\r\n box-shadow: inset 0 0 0 0.5px var(--sd-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(--sd-border-l4);\r\n border-radius: var(--sd-radius-lg);\r\n box-shadow: var(--sd-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(--sd-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(--sd-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(--sd-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(--sd-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(--sd-radius-xs);\r\n background: 0 0;\r\n color: var(--sd-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(--sd-interactive-bg-hover);\r\n}\r\n";
1716
1892
  //#endregion
1893
+ //#region src/client/ui/forms.css?dsh-css-text
1894
+ var forms_default = "/**\n * 「可调项」卡的数字输入(本插件自造控件)。\n *\n * 为什么不并进 `ui.css`:那份是照宿主 primitives 逐字抄的对照表,`tests/skeleton.test.ts` 的\n * 界面保真门要求它与官方明文逐属性一致;这里是我们自己的表单控件,混进去会把那份核对弄脏。\n *\n * 配色只用宿主公开 token(`--dsw-alias-*`):这份控件不在抄本表里,不需要 `--sd-*` 别名层的\n * 私有 token 兜底;几何走别名层已有的圆角。\n */\n.sd-config-input {\n width: 132px;\n padding: 4px 8px;\n font: inherit;\n font-size: 13px;\n line-height: 19px;\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-bg-layer-1);\n border: 0.5px solid var(--dsw-alias-border-l1);\n border-radius: var(--sd-radius-md);\n box-sizing: border-box;\n}\n\n.sd-config-input:focus-visible {\n outline: 1px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.sd-config-input:disabled {\n opacity: 0.6;\n}\n";
1895
+ //#endregion
1896
+ //#region src/client/ui/layout.css?dsh-css-text
1897
+ var layout_default = "/**\n * 清单行 / 行内动作 / 工作步骤开关的**本插件自造规则**(不在抄本对照表里)。\n *\n * 为什么不并进 `ui.css`:那份是照宿主 `dsh-client-ui-primitives` 明文逐字抄的对照表,\n * `docs/development.md` 的界面保真复核门要求它与官方逐属性一致(差异只允许文件头列出的那几处)。\n * 把自造规则混进去会把那次核对弄脏——同 `forms.css` 文件头给的理由。\n *\n * 为什么非要有个 CSS 文件:`trash-view.ts` 的 `ROW` 是 inline style,而 inline style 表达不了\n * `:hover` / `:focus-within` / `prefers-reduced-motion`。动作列的低强调与提亮、分段开关的选中态\n * 本身就是伪类。\n *\n * 配色只用宿主**公开** token(`--dsw-alias-*`)与本插件的 `--sd-*` 别名层(后者在 `ui.css` 顶部\n * 定义、逐条退到公开 token)。**悬停一律走 `--sd-interactive-bg-hover`**(宿主交互悬停档,半透明叠色):\n * `bg-layer-1` / `bg-layer-2` 在浅色主题下与页面底色同值,拿它们做悬停等于没变色(2026-11-07 真机反馈)。\n */\n\n/* 悬停档为什么是 `--sd-interactive-bg-hover`:判据住文件头注。\n 选中态的底色写在元素上(`ROW_SELECTED`),伪类打不过它,所以选中 + 悬停同时成立时\n 看到的是选中那一档,不会在鼠标经过时闪回浅色。 */\n[data-dsh-session-delete=\"trash-row\"]:hover {\n background: var(--sd-interactive-bg-hover);\n}\n\n/*\n * 行内动作:两颗图标按钮**常驻**,默认低强调,悬停 / 键盘落到这一行 / 这一行被选中时提亮;\n * 窄档(面板根 `.sd-narrow`)常亮——触屏没有 hover,点行即下钻,不能要求\"先选中才出场\"。\n *\n * 用 `opacity` 而不是 `visibility` 或 `display`:两颗按钮始终住在自己的 `gridcell` 里、\n * 始终在可访问性树内(docs/ui-spec.md 状态④)。\n */\n.sd-icon-btn {\n box-sizing: border-box;\n width: 28px;\n height: 28px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: none;\n padding: 0;\n border: none;\n border-radius: var(--sd-radius-sm);\n background: transparent;\n color: var(--sd-label-tertiary);\n cursor: pointer;\n opacity: 0.55;\n transition: opacity 120ms ease-out, background-color 120ms ease-out, color 120ms ease-out;\n}\n\n[data-dsh-session-delete=\"trash-row\"]:hover .sd-icon-btn,\n[data-dsh-session-delete=\"trash-row\"]:focus-within .sd-icon-btn,\n[data-dsh-session-delete=\"trash-row\"][aria-selected='true'] .sd-icon-btn,\n.sd-narrow [data-dsh-session-delete=\"trash-row\"] .sd-icon-btn {\n opacity: 1;\n}\n\n.sd-icon-btn:hover:not(:disabled) {\n background: var(--sd-interactive-bg-hover);\n color: var(--dsw-alias-label-primary);\n}\n\n.sd-icon-btn-danger:hover:not(:disabled) {\n color: var(--dsw-alias-state-error-primary);\n}\n\n/* 禁用态要压过上一组的提亮(那一组是 0,3,0):写入在飞时按钮不能看起来还能点。 */\n[data-dsh-session-delete=\"trash-row\"] .sd-icon-btn:disabled,\n.sd-narrow [data-dsh-session-delete=\"trash-row\"] .sd-icon-btn:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n}\n\n/* 为什么不能依赖宿主焦点环 token:住 `ui.css` 的清单行注释。图标按钮没有裁切,环画在外侧一档。 */\n.sd-icon-btn:focus-visible {\n outline: 1px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/*\n * 工作步骤展示的分段开关(简洁 / 标准 / 详细 / 完全展开)。与工具栏按钮同族的几何;\n * 选中态走宿主\"紧按\"那一档底色与品牌色。原生 `<select>` 与插件其余控件不是一套长相,\n * 所以这里做成四段平铺。\n */\n.sd-seg {\n display: inline-flex;\n flex: none;\n border: 0.5px solid var(--dsw-alias-border-l2);\n border-radius: var(--sd-radius-sm);\n overflow: hidden;\n}\n\n.sd-seg button {\n border: none;\n background: transparent;\n padding: 3px 8px;\n font: inherit;\n font-size: 12px;\n line-height: 18px;\n color: var(--sd-label-tertiary);\n cursor: pointer;\n transition: background-color 120ms ease-out, color 120ms ease-out;\n}\n\n.sd-seg button:hover {\n background: var(--sd-interactive-bg-hover);\n color: var(--dsw-alias-label-secondary);\n}\n\n.sd-seg button[aria-pressed='true'] {\n background: var(--sd-button-ghost-active-fill);\n color: var(--dsw-alias-brand-primary);\n font-weight: 500;\n}\n\n/* `.sd-seg` 的 `overflow: hidden` 会裁掉外描边,环画在内侧一档;颜色与图标按钮同一枚品牌色。 */\n.sd-seg button:focus-visible {\n outline: 1px solid var(--dsw-alias-brand-primary);\n outline-offset: -1px;\n}\n\n/* 子会话导航行:与清单行同语言(边框与底色不铺,见 `trash-detail.tsx` 的 SUBAGENT_ROW)。\n 标题是主要信息,取 primary;折叠头那套类把行色钉成 tertiary,这里显式覆盖。\n 大小/时间仍由各自的 `BLOCK_META` 保持 tertiary。 */\n[data-dsh-session-delete=\"trash-subagent-row\"],\n[data-dsh-session-delete=\"trash-subagent-row\"]:hover {\n color: var(--dsw-alias-label-primary);\n}\n\n[data-dsh-session-delete=\"trash-subagent-row\"]:hover {\n background: var(--sd-interactive-bg-hover);\n}\n\n/*\n * 分组头的开合指示:与宿主工作区行同法(`Rows.module.css` 的 `.projectRow` 那一族)——\n * 默认显示**官方文件夹图标**(展开=开盖 `IconFolderOpenRegular`、收起=闭盒 `IconFolderCloseRegular`);\n * 鼠标经过时文件夹隐去、换成**实心三角**并旋转 90°(150ms,宿主原文的时长)。\n * 触屏没有 hover,文件夹本身就是开合状态;键盘用户的 aria-expanded 仍在按钮上。\n */\n[data-dsh-session-delete=\"trash-group-row\"] .sd-group-chevron {\n display: none;\n}\n\n@media (hover: hover) {\n [data-dsh-session-delete=\"trash-group-row\"]:hover .sd-group-chevron {\n display: inline-flex;\n }\n\n [data-dsh-session-delete=\"trash-group-row\"]:hover .sd-group-folder {\n display: none;\n }\n}\n\n.sd-group-arrow {\n display: inline-flex;\n transition: transform 150ms ease-in-out;\n}\n\n.sd-group-arrow-open {\n transform: rotate(90deg);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .sd-icon-btn,\n .sd-seg button {\n transition: none;\n }\n\n .sd-group-arrow {\n transition: none;\n }\n}\n";
1898
+ //#endregion
1717
1899
  //#region src/client/ui/markdown.css?dsh-css-text
1718
1900
  var markdown_default = "/**\r\n * 回收站详情里**会话正文**的排版(2026-10-15 真机反馈后新增)。\r\n *\r\n * 为什么单独一个文件而不并进 `ui.css`:`ui.css` 是\"照官方 primitives 逐字抄来的那一份表\",\r\n * 那条 fidelity 核对(`docs/development.md` 的界面保真复核门)要的是它与官方明文**逐属性零差异**,只许文件头点名的几类例外\r\n * (类名前缀、`@keyframes` 改名,外加它点名的自造与已声明的差别);条数不钉在这里,钉了就会漂成假基线。这一份抄的是\r\n * **另一个来源**(宿主 markdown 那一张表),混进去就把那份核对弄脏了。\r\n *\r\n * **来源(逐字抄,只改选择器、下面点名的三处,以及 token 引用走 `--sd-*` 别名层)**:\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 * 字体简写 `--dsw-font-markdown-*` 与内容字号 `--dsh-content-font-*` 逐字保留:它们是宿主会话区\r\n * 自己吃的那组,宿主调内容字号时这里跟着一起动。配色/几何的 `--dsw-alias-*` / `--dsw-radius-*`\r\n * 走 `ui.css` 头注那层 `--sd-*` 别名(私有 token 带公开兜底);逐属性复核时按别名还原成它引用的\r\n * `--dsw-*` 再比,不把别名层算成抄本差异。\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 * (图片渲成本地化占位,见 `copy.ts` 的 `detail.image` / `detail.image.alt`)/ `.katex-display` /\r\n * `:global(.md-code-block)` / `.shiki`(高亮由宿主的 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(--sd-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(--sd-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(--sd-markdown-code-block);\r\n border-radius: var(--sd-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(--sd-markdown-inline-code);\r\n border: 0.5px solid var(--dsw-alias-border-l1);\r\n border-radius: var(--sd-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(--sd-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(--sd-label-tertiary);\r\n font-style: italic;\r\n}\r\n";
1719
1901
  //#endregion
1720
1902
  //#region src/client/ui/styles.ts
1721
1903
  /**
1722
- * 本插件控件的样式装载:把 `ui.css` 的文本挂成一个 `<style>`,**每个 ui 组件 import 本模块**
1723
- * (副作用导入),所以「组件在屏幕上」必然等于「样式在场」。
1904
+ * 本插件控件的样式装载:把 `ui.css` / `markdown.css` / `forms.css` / `layout.css` 的文本挂成一个 `<style>`,
1905
+ * **每个 ui 组件 import 本模块**(副作用导入),所以「组件在屏幕上」必然等于「样式在场」。
1724
1906
  *
1725
1907
  * 为什么是往 DOM 里注 `<style>`、而不是构建期抽成 `.css` 资产:
1726
1908
  * 宿主对客户端插件的样式**本来就有这条一等公民通道**——浏览器半的模块表在**工厂物化**
@@ -1748,7 +1930,7 @@ window.__ModuleLoader__.load({
1748
1930
  if (head.querySelector(`style[data-dsh-session-delete="${STYLE_MARKER}"]`) !== null) return;
1749
1931
  const element = document.createElement("style");
1750
1932
  element.setAttribute("data-dsh-session-delete", STYLE_MARKER);
1751
- element.textContent = `${ui_default}\n${markdown_default}`;
1933
+ element.textContent = `${ui_default}\n${markdown_default}\n${forms_default}\n${layout_default}`;
1752
1934
  head.appendChild(element);
1753
1935
  }
1754
1936
  install();
@@ -1760,15 +1942,27 @@ window.__ModuleLoader__.load({
1760
1942
  * `pointerdown` 置真、`keydown` 置假)。官方还会据此发布 `data-input-modality` 去控制焦点环的
1761
1943
  * 显隐,**那件事不复制**:宿主自己那份 primitives 已经在同一份文档上发布了它,我们再来一份就是
1762
1944
  * 两个写入者抢同一个属性。
1945
+ *
1946
+ * 监听器归 `apply` 的 effect(`src/client/index.tsx` 的 `installPointerModality`):用户在
1947
+ * 卸载 / HMR 换代时要把它们摘掉,模块作用域装上就没人负责收了。样式本身仍是模块作用域——
1948
+ * 宿主在工厂物化时认领 `<style>` 并在条目卸载时统一摘除,这是它给插件 UI 的正式通道。
1763
1949
  */
1764
1950
  let pointerModality = false;
1765
- if (typeof window !== "undefined") {
1766
- window.addEventListener("pointerdown", () => {
1951
+ /** 装上两个捕获阶段的监听器,返回摘除函数。重复调用会装两套,所以调用点只该是 `apply` 的一个 effect。 */
1952
+ function installPointerModality() {
1953
+ if (typeof window === "undefined") return () => void 0;
1954
+ const onPointerDown = () => {
1767
1955
  pointerModality = true;
1768
- }, true);
1769
- window.addEventListener("keydown", () => {
1956
+ };
1957
+ const onKeyDown = () => {
1770
1958
  pointerModality = false;
1771
- }, true);
1959
+ };
1960
+ window.addEventListener("pointerdown", onPointerDown, true);
1961
+ window.addEventListener("keydown", onKeyDown, true);
1962
+ return () => {
1963
+ window.removeEventListener("pointerdown", onPointerDown, true);
1964
+ window.removeEventListener("keydown", onKeyDown, true);
1965
+ };
1772
1966
  }
1773
1967
  /** 最近一次输入是否来自指针(仅本插件自用)。 */
1774
1968
  function isPointerModality() {
@@ -3352,11 +3546,32 @@ window.__ModuleLoader__.load({
3352
3546
  border: "0.5px solid var(--sd-border-l4)",
3353
3547
  borderRadius: "var(--sd-radius-lg)"
3354
3548
  };
3355
- /** 选中行:底色与描边都换成宿主"选中"那一档(`trash-panel.tsx` 头注里有这两枚 token 的实测取值)。 */
3356
- const CARD_SELECTED = {
3357
- ...CARD,
3358
- background: "var(--sd-button-ghost-active-fill)",
3359
- border: "0.5px solid var(--sd-button-ghost-active-border)"
3549
+ /**
3550
+ * 清单条目行:与 `CARD` 同族但**不铺底、不描边**。
3551
+ *
3552
+ * 为什么和 `CARD` 分成两份而不改 `CARD`:`CARD` 还被读数失败块与附件回执块借用(`trash-panel.tsx`
3553
+ * 的 `role="status"`、`attachment-view.tsx`),那一类是"要打断你视线的一块",带底色和边框才对;
3554
+ * 清单行是"一列可以扫的东西",边框叠出来就是 2010 年代后台面板的长相。
3555
+ *
3556
+ * 悬停底色与动作列的低强调/提亮住在 `ui/layout.css`:inline style 表达不了 `:hover` /
3557
+ * `:focus-within`,而 `ui.css` 是照宿主明文逐字抄的对照表,混进自造规则会把保真复核弄脏
3558
+ * (同 `forms.css` 文件头给的理由)。
3559
+ */
3560
+ const ROW = {
3561
+ display: "flex",
3562
+ alignItems: "center",
3563
+ gap: 12,
3564
+ minWidth: 0,
3565
+ padding: "8px 10px",
3566
+ borderRadius: "var(--sd-radius-lg)"
3567
+ };
3568
+ /**
3569
+ * 选中行:底色用 inline style **故意压在** `layout.css` 的悬停底色之上——元素上的声明优先于
3570
+ * 样式表里的 `:hover` 规则,选中与悬停同时成立时看到的是选中那一档,不会闪。
3571
+ */
3572
+ const ROW_SELECTED = {
3573
+ ...ROW,
3574
+ background: "var(--sd-button-ghost-active-fill)"
3360
3575
  };
3361
3576
  /**
3362
3577
  * **整页唯一那块实心红**的写法:`variant="primary"` + 在元素上覆盖 primary 那对填充 token。
@@ -3379,24 +3594,343 @@ window.__ModuleLoader__.load({
3379
3594
  const bare = unescapeProjectKey(projectDir.replace(/^--/, "").replace(/--$/, ""));
3380
3595
  return bare === "" || bare === "_no-cwd" ? t("trash.project.unknown") : bare;
3381
3596
  }
3382
- /** 按项目分组:组内按删除时间倒序,组间按各组最新一条倒序(刚删掉的那个项目排在最上面)。 */
3597
+ /**
3598
+ * 「未分组」组的键:工作区已从宿主账本删掉(`workspaceMissing`)的条目都归这里。
3599
+ * 哨兵值不与真实 `projectDir` 同形——空 cwd 已经由 `projectLabel` 念成「未记录项目」,
3600
+ * 两者不是同一件事,不能撞键。
3601
+ */
3602
+ const UNGROUPED_PROJECT_DIR = "__dsh-session-delete__ungrouped";
3603
+ function groupKeyOf(entry) {
3604
+ return entry.workspaceMissing === true ? UNGROUPED_PROJECT_DIR : entry.projectDir;
3605
+ }
3606
+ /** 按项目分组:组内按删除时间倒序,组间按各组最新一条倒序(刚删掉的那个项目排在最上面)。
3607
+ * 工作区已不存在的条目统一进「未分组」组,且该组永远沉底——它不是一个可比较的项目。 */
3383
3608
  function groupByProject(entries, t) {
3384
3609
  const byProject = /* @__PURE__ */ new Map();
3385
3610
  for (const entry of [...entries].sort((a, b) => b.deletedAt - a.deletedAt)) {
3386
- const list = byProject.get(entry.projectDir);
3387
- if (list === void 0) byProject.set(entry.projectDir, [entry]);
3611
+ const key = groupKeyOf(entry);
3612
+ const list = byProject.get(key);
3613
+ if (list === void 0) byProject.set(key, [entry]);
3388
3614
  else list.push(entry);
3389
3615
  }
3390
3616
  return [...byProject].flatMap(([projectDir, list]) => {
3391
3617
  const first = list[0];
3392
3618
  if (first === void 0) return [];
3619
+ const ungrouped = projectDir === UNGROUPED_PROJECT_DIR;
3620
+ const label = ungrouped ? t("trash.group.ungrouped") : projectLabel(projectDir, t);
3393
3621
  return [{
3394
3622
  projectDir,
3395
- label: projectLabel(projectDir, t),
3623
+ label,
3624
+ title: ungrouped ? label : projectDir,
3625
+ ungrouped,
3396
3626
  latest: first.deletedAt,
3397
3627
  entries: list
3398
3628
  }];
3399
- }).sort((a, b) => b.latest - a.latest);
3629
+ }).sort((a, b) => {
3630
+ if (a.ungrouped !== b.ungrouped) return a.ungrouped ? 1 : -1;
3631
+ return b.latest - a.latest;
3632
+ });
3633
+ }
3634
+ //#endregion
3635
+ //#region src/client/settings-view.ts
3636
+ /** 设置页卡片外壳:`gap` 是这一张卡自己的内容节奏,其余两张贴一样。 */
3637
+ const settingsCardShell = (gap) => ({
3638
+ ...CARD,
3639
+ flexDirection: "column",
3640
+ alignItems: "stretch",
3641
+ gap,
3642
+ width: "100%",
3643
+ boxSizing: "border-box",
3644
+ padding: "14px 16px"
3645
+ });
3646
+ //#endregion
3647
+ //#region src/client/config-settings.tsx
3648
+ /**
3649
+ * 插件设置页的「可调项」卡:三只数字输入 + 保存(ADR-0043;ADR-0037 留下的「自绘表单」那条路)。
3650
+ *
3651
+ * 为什么自绘而不是等宿主投影:`Config` 是手写 Standard Schema,宿主的字段投影只认 native
3652
+ * Schemastery(ADR-0037 的实测);把宿主 schema 库值 import 进本插件会破「跨插件一律 import type」。
3653
+ * 所以这里画三个输入,保存走 `/config` 的写支(`configEditor.edit`)。写会让 **Node 半**
3654
+ * 这一行重启;浏览器半不随之卸载,卡片用保存回执回填,重开插件页 / 刷新页面才会重新读一次。
3655
+ * `config.saved` 只说明"这一轮已经交出去",不是"页面完成了一次重新读取"。
3656
+ *
3657
+ * 表单只做本地校验(`CONFIG_LIMITS` 与 Node 半归一同一格);服务端的 400 / io 原文在状态行上屏,
3658
+ * 不吞、不改写。读与写都只在用户动作或挂载时发生,没有定时器。
3659
+ */
3660
+ /** 卡片外壳:与「维护」卡同款,只差本卡的内容节奏。 */
3661
+ const CARD_SHELL$1 = settingsCardShell(14);
3662
+ /** 三个字段的顺序、文案与单位:键名与线上契约同字,顺序就是表单从上到下的顺序。 */
3663
+ const FIELDS = [
3664
+ {
3665
+ key: "orphanPreviewLimit",
3666
+ label: "config.orphanPreviewLimit",
3667
+ unit: "config.unit.items"
3668
+ },
3669
+ {
3670
+ key: "maxAttachmentBytes",
3671
+ label: "config.maxAttachmentBytes",
3672
+ unit: "config.unit.bytes"
3673
+ },
3674
+ {
3675
+ key: "maxJsonRefBytes",
3676
+ label: "config.maxJsonRefBytes",
3677
+ unit: "config.unit.bytes"
3678
+ }
3679
+ ];
3680
+ function draftOf(config) {
3681
+ return {
3682
+ orphanPreviewLimit: String(config.orphanPreviewLimit),
3683
+ maxAttachmentBytes: String(config.maxAttachmentBytes),
3684
+ maxJsonRefBytes: String(config.maxJsonRefBytes)
3685
+ };
3686
+ }
3687
+ /** 空串 / 小数 / 越界都是同一句「需为 min–max 的整数」;第一个不合格的字段决定报在哪一行。 */
3688
+ function parseDraft(draft) {
3689
+ const value = {
3690
+ orphanPreviewLimit: 0,
3691
+ maxAttachmentBytes: 0,
3692
+ maxJsonRefBytes: 0
3693
+ };
3694
+ for (const { key } of FIELDS) {
3695
+ const raw = draft[key].trim();
3696
+ const parsed = raw === "" ? NaN : Number(raw);
3697
+ const { min, max } = CONFIG_LIMITS[key];
3698
+ if (!Number.isInteger(parsed) || parsed < min || parsed > max) return {
3699
+ ok: false,
3700
+ bad: key
3701
+ };
3702
+ value[key] = parsed;
3703
+ }
3704
+ return {
3705
+ ok: true,
3706
+ value
3707
+ };
3708
+ }
3709
+ function ConfigCard({ config, saveConfig, t }) {
3710
+ const idPrefix = (0, react.useId)();
3711
+ const [phase, setPhase] = (0, react.useState)("loading");
3712
+ const [current, setCurrent] = (0, react.useState)(null);
3713
+ const [draft, setDraft] = (0, react.useState)(null);
3714
+ const [failure, setFailure] = (0, react.useState)(null);
3715
+ const [saving, setSaving] = (0, react.useState)(false);
3716
+ const [saved, setSaved] = (0, react.useState)(false);
3717
+ const [reload, setReload] = (0, react.useState)(0);
3718
+ (0, react.useEffect)(() => {
3719
+ let alive = true;
3720
+ setPhase("loading");
3721
+ setFailure(null);
3722
+ setSaved(false);
3723
+ config().then((value) => {
3724
+ if (!alive) return;
3725
+ setCurrent(value);
3726
+ setDraft(draftOf(value));
3727
+ setPhase("ready");
3728
+ }).catch((error) => {
3729
+ if (!alive) return;
3730
+ setPhase("failed");
3731
+ setFailure(error instanceof Error ? error.message : String(error));
3732
+ });
3733
+ return () => {
3734
+ alive = false;
3735
+ };
3736
+ }, [config, reload]);
3737
+ const parsed = draft === null ? {
3738
+ ok: false,
3739
+ bad: null
3740
+ } : parseDraft(draft);
3741
+ const dirty = current !== null && parsed.ok && FIELDS.some(({ key }) => parsed.value[key] !== current[key]);
3742
+ const submit = () => {
3743
+ if (saving || !parsed.ok || !dirty) return;
3744
+ setSaving(true);
3745
+ setFailure(null);
3746
+ setSaved(false);
3747
+ saveConfig(parsed.value).then((value) => {
3748
+ setCurrent(value);
3749
+ setDraft(draftOf(value));
3750
+ setSaved(true);
3751
+ }).catch((error) => {
3752
+ setFailure(error instanceof Error ? error.message : String(error));
3753
+ }).finally(() => {
3754
+ setSaving(false);
3755
+ });
3756
+ };
3757
+ if (phase === "loading") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3758
+ style: {
3759
+ fontSize: 12,
3760
+ lineHeight: "17px",
3761
+ color: "var(--sd-label-tertiary)"
3762
+ },
3763
+ children: t("config.loading")
3764
+ });
3765
+ if (phase === "failed") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3766
+ style: {
3767
+ display: "flex",
3768
+ flexDirection: "column",
3769
+ gap: 8
3770
+ },
3771
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3772
+ role: "alert",
3773
+ style: {
3774
+ fontSize: 12,
3775
+ lineHeight: "17px",
3776
+ color: "var(--dsw-alias-state-error-primary)"
3777
+ },
3778
+ children: t("config.loadFailed", { message: failure ?? "" })
3779
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
3780
+ variant: "outline",
3781
+ size: "sm",
3782
+ onClick: () => {
3783
+ setReload((n) => n + 1);
3784
+ },
3785
+ children: t("config.retry")
3786
+ }) })]
3787
+ });
3788
+ const status = failure !== null ? t("config.saveFailed", { message: failure }) : saved ? t("config.saved") : !parsed.ok || dirty ? "" : t("config.unchanged");
3789
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
3790
+ "aria-busy": saving,
3791
+ onSubmit: (event) => {
3792
+ event.preventDefault();
3793
+ submit();
3794
+ },
3795
+ style: {
3796
+ display: "flex",
3797
+ flexDirection: "column",
3798
+ gap: 10
3799
+ },
3800
+ children: [FIELDS.map(({ key, label, unit }) => {
3801
+ const { min, max } = CONFIG_LIMITS[key];
3802
+ const id = `${idPrefix}-${key}`;
3803
+ const bad = !parsed.ok && parsed.bad === key;
3804
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3805
+ "data-dsh-session-delete": `config-${key}`,
3806
+ style: {
3807
+ display: "flex",
3808
+ flexDirection: "column",
3809
+ gap: 3
3810
+ },
3811
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3812
+ style: {
3813
+ display: "flex",
3814
+ alignItems: "center",
3815
+ gap: 10,
3816
+ flexWrap: "wrap"
3817
+ },
3818
+ children: [
3819
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
3820
+ htmlFor: id,
3821
+ style: {
3822
+ flex: "1 1 220px",
3823
+ fontSize: 13,
3824
+ lineHeight: "19px"
3825
+ },
3826
+ children: t(label)
3827
+ }),
3828
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3829
+ id,
3830
+ className: "sd-config-input",
3831
+ type: "number",
3832
+ inputMode: "numeric",
3833
+ step: 1,
3834
+ min,
3835
+ max,
3836
+ disabled: saving,
3837
+ value: draft?.[key] ?? "",
3838
+ "aria-invalid": bad || void 0,
3839
+ onChange: (event) => {
3840
+ const next = event.target.value;
3841
+ setDraft((old) => old === null ? old : {
3842
+ ...old,
3843
+ [key]: next
3844
+ });
3845
+ setSaved(false);
3846
+ setFailure(null);
3847
+ }
3848
+ }),
3849
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3850
+ style: {
3851
+ fontSize: 12,
3852
+ lineHeight: "17px",
3853
+ color: "var(--sd-label-tertiary)"
3854
+ },
3855
+ children: t(unit)
3856
+ })
3857
+ ]
3858
+ }), bad ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3859
+ role: "alert",
3860
+ style: {
3861
+ fontSize: 12,
3862
+ lineHeight: "17px",
3863
+ color: "var(--dsw-alias-state-error-primary)"
3864
+ },
3865
+ children: t("config.invalid", {
3866
+ min,
3867
+ max
3868
+ })
3869
+ }) : null]
3870
+ }, key);
3871
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3872
+ style: {
3873
+ display: "flex",
3874
+ alignItems: "center",
3875
+ gap: 10,
3876
+ flexWrap: "wrap"
3877
+ },
3878
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
3879
+ variant: "primary",
3880
+ size: "sm",
3881
+ type: "submit",
3882
+ disabled: !parsed.ok || !dirty || saving,
3883
+ children: saving ? t("config.saving") : t("config.save")
3884
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3885
+ role: "status",
3886
+ style: {
3887
+ fontSize: 12,
3888
+ lineHeight: "17px",
3889
+ color: "var(--sd-label-tertiary)"
3890
+ },
3891
+ children: status
3892
+ })]
3893
+ })]
3894
+ });
3895
+ }
3896
+ /**
3897
+ * 卡片入口:两个方法都在才渲染。`useMaintenance` 与 `MaintenanceSettings` 的测试替身只造维护四件,
3898
+ * 所以在那些用例里本卡整体缺席——不让"没给配置 deps"变成一次假渲染。
3899
+ */
3900
+ function ConfigSettings({ deps, t = zhTranslate }) {
3901
+ const config = deps.config;
3902
+ const saveConfig = deps.saveConfig;
3903
+ if (config === void 0 || saveConfig === void 0) return null;
3904
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3905
+ "data-dsh-session-delete": "config-card",
3906
+ style: CARD_SHELL$1,
3907
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3908
+ style: {
3909
+ display: "flex",
3910
+ flexDirection: "column",
3911
+ gap: 2
3912
+ },
3913
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3914
+ style: {
3915
+ fontSize: 16,
3916
+ fontWeight: 600,
3917
+ lineHeight: "24px"
3918
+ },
3919
+ children: t("config.title")
3920
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3921
+ style: {
3922
+ fontSize: 12,
3923
+ lineHeight: "17px",
3924
+ color: "var(--sd-label-tertiary)"
3925
+ },
3926
+ children: t("config.hint")
3927
+ })]
3928
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfigCard, {
3929
+ config,
3930
+ saveConfig,
3931
+ t
3932
+ })]
3933
+ });
3400
3934
  }
3401
3935
  //#endregion
3402
3936
  //#region src/client/attachment-view.tsx
@@ -3776,6 +4310,8 @@ window.__ModuleLoader__.load({
3776
4310
  textOverflow: "ellipsis",
3777
4311
  whiteSpace: "nowrap"
3778
4312
  };
4313
+ /** 数字等宽:清单行、信息表与计量行共用一处,读数横着扫时才不抖。 */
4314
+ const TABULAR_NUMS = { fontVariantNumeric: "tabular-nums" };
3779
4315
  //#endregion
3780
4316
  //#region src/client/trash-confirm.tsx
3781
4317
  /** 弹出的一批卡片(逐条说清这一批有哪些):长清单在框内滚,标题与动作行留在框里。 */
@@ -4239,16 +4775,8 @@ window.__ModuleLoader__.load({
4239
4775
  }
4240
4776
  //#endregion
4241
4777
  //#region src/client/maintenance-settings.tsx
4242
- /** 卡片外壳:按内容高、铺满宿主给的宽度;不假设自己住在中央面板里(没有 `height:100%`)。 */
4243
- const CARD_SHELL = {
4244
- ...CARD,
4245
- flexDirection: "column",
4246
- alignItems: "stretch",
4247
- gap: 18,
4248
- width: "100%",
4249
- boxSizing: "border-box",
4250
- padding: "14px 16px"
4251
- };
4778
+ /** 卡片外壳:与「可调项」卡同款,只差本卡的内容节奏。 */
4779
+ const CARD_SHELL = settingsCardShell(18);
4252
4780
  /** 一段的标题行:标题 + 说明 + 状态 + 检查按钮。状态按优先级:清理中 / 检查中 / 检查失败 / 尚未检查 / 没有发现 / N。 */
4253
4781
  function MaintenanceSection({ slot, title, hint, count, countKey, section, onScan, t, children }) {
4254
4782
  const status = statusText({
@@ -4439,12 +4967,11 @@ window.__ModuleLoader__.load({
4439
4967
  ]
4440
4968
  });
4441
4969
  }
4442
- function MaintenanceSettings({ deps, t = zhTranslate, view }) {
4970
+ function MaintenanceSettings({ deps, t = zhTranslate }) {
4443
4971
  const maintenance = useMaintenance({
4444
4972
  deps,
4445
4973
  t
4446
4974
  });
4447
- if (view === "summary") return null;
4448
4975
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4449
4976
  "data-dsh-session-delete": "maintenance-card",
4450
4977
  style: CARD_SHELL,
@@ -4489,6 +5016,18 @@ window.__ModuleLoader__.load({
4489
5016
  });
4490
5017
  }
4491
5018
  //#endregion
5019
+ //#region src/client/settings-cards.tsx
5020
+ function SettingsCards({ deps, t = zhTranslate, view }) {
5021
+ if (view === "summary") return null;
5022
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfigSettings, {
5023
+ deps,
5024
+ t
5025
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MaintenanceSettings, {
5026
+ deps,
5027
+ t
5028
+ })] });
5029
+ }
5030
+ //#endregion
4492
5031
  //#region src/client/trash-entry.tsx
4493
5032
  /**
4494
5033
  * 侧栏面板列表里的「回收站」行图标(`sidebar.panellist` 的占用者;docs/ui-spec.md 状态 ④ 的入口那一行)。
@@ -26474,6 +27013,106 @@ window.__ModuleLoader__.load({
26474
27013
  });
26475
27014
  }, (prev, next) => prev.text === next.text && prev.probe === next.probe);
26476
27015
  //#endregion
27016
+ //#region src/client/detail-blocks.ts
27017
+ function isDisclosureBlock(block) {
27018
+ return block.kind !== "user" && block.kind !== "assistant";
27019
+ }
27020
+ /** 这一批块里有没有对话内容(用户或助手);简洁档靠它决定还要不要继续往前翻。 */
27021
+ function hasDialogue(blocks) {
27022
+ return blocks.some((block) => !isDisclosureBlock(block));
27023
+ }
27024
+ function threadItems(blocks) {
27025
+ const items = [];
27026
+ let current = null;
27027
+ for (const block of blocks) {
27028
+ if (isDisclosureBlock(block)) {
27029
+ if (current === null) {
27030
+ current = {
27031
+ kind: "process",
27032
+ id: "after:end",
27033
+ blocks: []
27034
+ };
27035
+ items.push(current);
27036
+ }
27037
+ current.blocks.push(block);
27038
+ continue;
27039
+ }
27040
+ if (current !== null) current.id = `after:${block.id}`;
27041
+ current = null;
27042
+ items.push({
27043
+ kind: "block",
27044
+ block
27045
+ });
27046
+ }
27047
+ return items;
27048
+ }
27049
+ function useDetailPager({ blocks, olderCursor, loadingOlder, olderFailure, compact, onLoadOlder, scrollRef }) {
27050
+ /** 更早一页要 prepend 时的块数;换页回来(组件重挂载)这份意图会丢,那是可接受的边界。 */
27051
+ const pendingScroll = (0, react.useRef)(null);
27052
+ /** 简洁档自动连翻的账:起点块数 + 已经连翻了几页。 */
27053
+ const autoOlder = (0, react.useRef)(null);
27054
+ /** 上一次布局后的滚动高度:合并更早一页时用它算**真正插进来的高度**,不把请求在飞期间的
27055
+ * 滚动或折叠变化也算进去。 */
27056
+ const lastHeight = (0, react.useRef)(null);
27057
+ const loadOlder = () => {
27058
+ const element = scrollRef?.current;
27059
+ if (element !== null && element !== void 0) pendingScroll.current = { blocks: blocks.length };
27060
+ autoOlder.current = {
27061
+ blocks: blocks.length,
27062
+ pages: 0
27063
+ };
27064
+ onLoadOlder();
27065
+ };
27066
+ (0, react.useLayoutEffect)(() => {
27067
+ const element = scrollRef?.current;
27068
+ const pending = pendingScroll.current;
27069
+ if (element === null || element === void 0 || pending === null) return;
27070
+ if (blocks.length !== pending.blocks) {
27071
+ pendingScroll.current = null;
27072
+ if (lastHeight.current !== null) element.scrollTop += element.scrollHeight - lastHeight.current;
27073
+ return;
27074
+ }
27075
+ if (!loadingOlder) pendingScroll.current = null;
27076
+ }, [
27077
+ blocks.length,
27078
+ loadingOlder,
27079
+ scrollRef
27080
+ ]);
27081
+ (0, react.useLayoutEffect)(() => {
27082
+ const element = scrollRef?.current;
27083
+ if (element !== null && element !== void 0) lastHeight.current = element.scrollHeight;
27084
+ });
27085
+ (0, react.useEffect)(() => {
27086
+ const pending = autoOlder.current;
27087
+ if (pending === null) return;
27088
+ if (blocks.length === pending.blocks) {
27089
+ if (!loadingOlder && olderFailure !== null) autoOlder.current = null;
27090
+ return;
27091
+ }
27092
+ const added = blocks.slice(0, blocks.length - pending.blocks);
27093
+ if (!compact || hasDialogue(added) || olderCursor === null || pending.pages >= 10) {
27094
+ autoOlder.current = null;
27095
+ return;
27096
+ }
27097
+ autoOlder.current = {
27098
+ blocks: blocks.length,
27099
+ pages: pending.pages + 1
27100
+ };
27101
+ const element = scrollRef?.current;
27102
+ if (element !== null && element !== void 0) pendingScroll.current = { blocks: blocks.length };
27103
+ onLoadOlder();
27104
+ }, [
27105
+ blocks.length,
27106
+ olderCursor,
27107
+ loadingOlder,
27108
+ olderFailure,
27109
+ compact,
27110
+ onLoadOlder,
27111
+ scrollRef
27112
+ ]);
27113
+ return { loadOlder };
27114
+ }
27115
+ //#endregion
26477
27116
  //#region src/client/trash-detail.tsx
26478
27117
  /**
26479
27118
  * 回收站详情块:只画状态,不取数。选中、缓存、失败与重读都在面板控制器里。
@@ -26483,7 +27122,7 @@ window.__ModuleLoader__.load({
26483
27122
  * - 标题优先用清册标题,空串才回落到日志的 session/title。
26484
27123
  * - 正文是**块**:用户/助手文字常显;思考、工具、注入、系统消息、压缩摘要默认折叠,折叠头给
26485
27124
  * 标签 + 首行摘要(工具另给状态)。注入也铺进正文——这是对宿主 Chat 的有意偏离(它隐藏普通注入,
26486
- * 而这里的目标是「删之前看全」);条数另进元信息表。
27125
+ * 而这里的目标是「删之前看全」);条数另进计量行。
26487
27126
  *
26488
27127
  * 助手正文与思考走 Markdown,用户提问保持原话;工具参数/结果走等宽纯文本(限高可滚)。
26489
27128
  * 附件随块位置渲染:用户气泡之上、助手正文之后、工具展开体末尾。
@@ -26493,17 +27132,25 @@ window.__ModuleLoader__.load({
26493
27132
  flexDirection: "column",
26494
27133
  gap: 12,
26495
27134
  boxSizing: "border-box",
26496
- height: "100%",
27135
+ minHeight: "100%",
26497
27136
  minWidth: 0
26498
27137
  };
26499
27138
  /** 返回键排在标题之后:排在之前会把整条标题的起点往右挤(docs/ui-spec.md 状态⑥)。
26500
27139
  * 标题排按紧凑按钮(`ui.css` 的 `.sd-button-sm`)预留高度:宽档父详情没有返回键、孩子视图有,
26501
- * 不预留时下方元信息会在父子切换时跳 6px。 */
27140
+ * 不预留时下方会话信息会在父子切换时跳 6px。
27141
+ *
27142
+ * `position: sticky`:长会话读到中段时返回键仍在顶上(触屏没有 Esc 兜底,返回必须常驻)。
27143
+ * 底色用宿主页面底色(`bg-base`)把滚过的正文盖住。 */
26502
27144
  const HEAD = {
26503
27145
  display: "flex",
26504
27146
  alignItems: "flex-start",
26505
27147
  gap: 10,
26506
- minHeight: 28
27148
+ minHeight: 28,
27149
+ position: "sticky",
27150
+ top: 0,
27151
+ zIndex: 1,
27152
+ background: "var(--dsw-alias-bg-base)",
27153
+ paddingBottom: 6
26507
27154
  };
26508
27155
  const TITLE_LINE = {
26509
27156
  ...ELLIPSIS,
@@ -26514,23 +27161,62 @@ window.__ModuleLoader__.load({
26514
27161
  lineHeight: "22px"
26515
27162
  };
26516
27163
  const BACK = { flex: "none" };
26517
- /** 元信息那张表:label 一列、值一列,窄档也不横向溢出。 */
27164
+ /**
27165
+ * 会话信息表:一张两列表,行序按「认出这条会话 → 时间定位 → 精确核对 → 调试属性」排
27166
+ * (docs/ui-spec.md 状态⑥)。标签列自动宽、值列吃余宽;长串省略号收、全称走 `title`。
27167
+ * 不做折叠、不做卡片:这类信息是给核对用的,默认就该看得见。
27168
+ */
26518
27169
  const META = {
26519
27170
  display: "grid",
26520
27171
  gridTemplateColumns: "auto 1fr",
26521
27172
  gap: "3px 12px",
26522
- margin: 0,
27173
+ margin: "8px 0 0",
26523
27174
  fontSize: 12,
26524
- lineHeight: "17px"
27175
+ lineHeight: "18px"
26525
27176
  };
26526
27177
  const META_LABEL = {
26527
27178
  color: "var(--sd-label-tertiary)",
26528
27179
  whiteSpace: "nowrap"
26529
27180
  };
26530
27181
  const META_VALUE = {
27182
+ ...ELLIPSIS,
27183
+ ...TABULAR_NUMS,
26531
27184
  margin: 0,
27185
+ minWidth: 0
27186
+ };
27187
+ const META_CODE = {
27188
+ fontFamily: "var(--ds-font-family-code)",
27189
+ fontSize: 11,
27190
+ color: "var(--dsw-alias-label-secondary)"
27191
+ };
27192
+ /**
27193
+ * 计量行:一行内联的「标签 值」对,自动换行。和表分开是因为它们是**可以横着扫的数字**,
27194
+ * 不是要逐行核对的字符串。不插中点分隔符——分隔符是各语言自己的写法(同 `detail.subagents.header`)。
27195
+ */
27196
+ const METRICS = {
27197
+ display: "flex",
27198
+ flexWrap: "wrap",
27199
+ gap: "2px 14px",
27200
+ marginTop: 8,
27201
+ minWidth: 0,
27202
+ fontSize: 12,
27203
+ lineHeight: "18px"
27204
+ };
27205
+ const METRIC = {
27206
+ display: "inline-flex",
27207
+ gap: 6,
26532
27208
  minWidth: 0,
26533
- wordBreak: "break-word"
27209
+ maxWidth: "100%"
27210
+ };
27211
+ const METRIC_LABEL = {
27212
+ flex: "none",
27213
+ color: "var(--sd-label-tertiary)",
27214
+ whiteSpace: "nowrap"
27215
+ };
27216
+ const METRIC_VALUE = {
27217
+ ...ELLIPSIS,
27218
+ ...TABULAR_NUMS,
27219
+ minWidth: 0
26534
27220
  };
26535
27221
  /**
26536
27222
  * 线程的行距取宿主那两档里的 12px 那档(`ChatView.module.css` 的 `--dsh-chat-flow-gap`:默认 6px,
@@ -26607,14 +27293,21 @@ window.__ModuleLoader__.load({
26607
27293
  textAlign: "start",
26608
27294
  width: "100%",
26609
27295
  minWidth: 0,
26610
- gap: 6
27296
+ gap: 8
26611
27297
  };
26612
- /** 标签不参与收缩:摘要是 `1 1 auto`,标签若也可缩,长结果会把「工具调用 / 来源」挤成几个字。 */
27298
+ /**
27299
+ * 标签不参与收缩:摘要是 `1 1 auto`,标签若也可缩,长结果会把「工具调用 / 来源」挤成几个字。
27300
+ *
27301
+ * 颜色**不跟着 `.sd-disclosure-row` 继承**:那颗类给的是 `label-tertiary`,而摘要也是 tertiary,
27302
+ * 两截同色只差一个字重,真机上就糊成一句「系统消息You are an AI agent…」。标签提到 secondary,
27303
+ * 摘要留在 tertiary,中间再留 8px,标签才真的像标签。
27304
+ */
26613
27305
  const BLOCK_TITLE = {
26614
27306
  ...ELLIPSIS,
26615
27307
  flex: "none",
26616
27308
  maxWidth: "50%",
26617
27309
  fontWeight: 500,
27310
+ color: "var(--dsw-alias-label-secondary)",
26618
27311
  fontSize: "var(--dsh-content-font-size-secondary, 13px)"
26619
27312
  };
26620
27313
  const BLOCK_SUMMARY = {
@@ -26627,7 +27320,7 @@ window.__ModuleLoader__.load({
26627
27320
  };
26628
27321
  const BLOCK_META = {
26629
27322
  flex: "none",
26630
- fontSize: 11.5,
27323
+ fontSize: 11,
26631
27324
  lineHeight: "18px",
26632
27325
  color: "var(--sd-label-tertiary)",
26633
27326
  whiteSpace: "nowrap"
@@ -26662,25 +27355,31 @@ window.__ModuleLoader__.load({
26662
27355
  overflow: "auto"
26663
27356
  };
26664
27357
  const SECTION_LABEL = {
26665
- fontSize: 11.5,
27358
+ fontSize: 11,
26666
27359
  lineHeight: "16px",
26667
27360
  color: "var(--sd-label-tertiary)"
26668
27361
  };
26669
27362
  /**
26670
- * 子会话行:折叠头同族的可点行,卡片底 + 小圆角;悬停走 sd-disclosure-row 的宿主配方。
26671
- * 两处内联覆盖各有理由:宿主类给折叠头钉死行高并随之裁溢出,导航行带内边距,须放开
26672
- * 高度按内容撑开,否则裁字;指针光标宿主只发给带展开语义的行,导航行不借那套语义,
26673
- * 光标就内联自给。
27363
+ * 子会话行:与清单行同语言的无边框导航行(悬停底色住 `ui/layout.css`)。两处内联覆盖各有理由:
27364
+ * 宿主类给折叠头钉死行高并随之裁溢出,导航行带内边距,须放开高度按内容撑开,否则裁字;
27365
+ * 指针光标宿主只发给带展开语义的行,导航行不借那套语义,光标就内联自给。
27366
+ * 底色与边框不铺——一列带框的行叠起来就是"一叠卡片",与清单行同一条判据。
26674
27367
  */
26675
27368
  const SUBAGENT_ROW = {
26676
27369
  ...BLOCK_ROW,
26677
- padding: "6px 10px",
27370
+ padding: "6px 8px",
26678
27371
  height: "auto",
26679
27372
  cursor: "pointer",
26680
- background: "var(--dsw-alias-bg-layer-2)",
26681
- border: "0.5px solid var(--sd-border-l4)",
26682
27373
  borderRadius: "var(--sd-radius-md)"
26683
27374
  };
27375
+ /** 工作步骤分组展开后,里面的过程块再缩进一档(与折叠体的前导对齐,前导宽随内容字号一起变)。 */
27376
+ const PROCESS_BODY = {
27377
+ display: "flex",
27378
+ flexDirection: "column",
27379
+ gap: 4,
27380
+ padding: "2px 0 4px calc(22px + var(--dsh-content-font-delta, 0px))",
27381
+ minWidth: 0
27382
+ };
26684
27383
  const TOOLBAR$1 = {
26685
27384
  display: "flex",
26686
27385
  alignItems: "center",
@@ -26689,7 +27388,7 @@ window.__ModuleLoader__.load({
26689
27388
  minWidth: 0
26690
27389
  };
26691
27390
  const TRUNCATED = {
26692
- fontSize: 11.5,
27391
+ fontSize: 11,
26693
27392
  lineHeight: "16px",
26694
27393
  color: "var(--sd-label-tertiary)"
26695
27394
  };
@@ -26756,9 +27455,24 @@ window.__ModuleLoader__.load({
26756
27455
  if (status === "error") return t("detail.block.status.error");
26757
27456
  return t("detail.block.status.unfinished");
26758
27457
  }
26759
- function isDisclosureBlock(block) {
26760
- return block.kind !== "user" && block.kind !== "assistant";
26761
- }
27458
+ const VIEW_MODES = [
27459
+ {
27460
+ key: "compact",
27461
+ label: "detail.viewMode.compact"
27462
+ },
27463
+ {
27464
+ key: "standard",
27465
+ label: "detail.viewMode.standard"
27466
+ },
27467
+ {
27468
+ key: "detailed",
27469
+ label: "detail.viewMode.detailed"
27470
+ },
27471
+ {
27472
+ key: "verbose",
27473
+ label: "detail.viewMode.verbose"
27474
+ }
27475
+ ];
26762
27476
  /** 一个可折叠块:折叠头一行(chevron + 标签 + 摘要 + 可选状态 + 时间),展开体由调用方给。 */
26763
27477
  function Disclosure({ block, label, title, summary, status, time, open, onToggle, children }) {
26764
27478
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -26963,7 +27677,7 @@ window.__ModuleLoader__.load({
26963
27677
  height: "100%",
26964
27678
  padding: "24px",
26965
27679
  textAlign: "center",
26966
- fontSize: 12.5,
27680
+ fontSize: 12,
26967
27681
  lineHeight: "18px",
26968
27682
  color: "var(--sd-label-tertiary)"
26969
27683
  },
@@ -27062,7 +27776,7 @@ window.__ModuleLoader__.load({
27062
27776
  function LoadingBody({ t }) {
27063
27777
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
27064
27778
  style: {
27065
- fontSize: 12.5,
27779
+ fontSize: 12,
27066
27780
  lineHeight: "18px",
27067
27781
  color: "var(--sd-label-tertiary)"
27068
27782
  },
@@ -27087,35 +27801,62 @@ window.__ModuleLoader__.load({
27087
27801
  } }, index))
27088
27802
  })] });
27089
27803
  }
27090
- function Meta({ label, title, children }) {
27804
+ function Meta({ label, title, code, children }) {
27091
27805
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", {
27092
27806
  style: META_LABEL,
27093
27807
  children: label
27094
27808
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", {
27095
27809
  style: META_VALUE,
27096
27810
  title,
27097
- children
27811
+ children: code === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
27812
+ style: META_CODE,
27813
+ children
27814
+ }) : children
27098
27815
  })] });
27099
27816
  }
27817
+ /** 计量行里的一对(标签 + 值):可以横着扫的数字,不占表的行。 */
27818
+ function Metric({ label, value }) {
27819
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
27820
+ style: METRIC,
27821
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
27822
+ style: METRIC_LABEL,
27823
+ children: label
27824
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
27825
+ style: METRIC_VALUE,
27826
+ children: value
27827
+ })]
27828
+ });
27829
+ }
27100
27830
  /**
27101
- * 有内容:元信息(时间 → 体量 → 位置三组的顺序)+ 子会话段 + 折叠块。
27831
+ * 有内容:会话信息(身份 → 时间 → 核对 → 属性)+ 计量行 + 子会话段 + 工作步骤。
27102
27832
  *
27103
- * 页面上**两本账分开读**:元信息里的计数是整条日志的(注入条数也在表里);
27104
- * 正文是分页取回来的块。`olderCursor` 非空时顶部出「加载更早」。
27105
- * 折叠状态只活在这个组件里;切条目用 `key={entry.id}` 重挂载,不会把 A 的展开状态带到 B。
27833
+ * 页面上**两本账分开读**:会话信息里的计数是整条日志的(注入条数也在计量行里);
27834
+ * 正文是分页取回来的块。`olderCursor` 非空时正文工具栏出「加载更早」。
27835
+ * 工作步骤档位与折叠状态只活在这个组件里;切条目用 `key={entry.id}` 重挂载,不会把 A 的
27836
+ * 展开状态带到 B。
27106
27837
  */
27107
27838
  function ReadyBody({ entry, detail, detailNav, pager, t }) {
27108
27839
  const { loadingOlder, olderFailure, onLoadOlder, scrollRef } = pager;
27840
+ const [mode, setMode] = (0, react.useState)("standard");
27841
+ /** 标准档「工作步骤」组的开合(按组后那条对话块的 id 记,prepend 不换键)。 */
27842
+ const [openGroups, setOpenGroups] = (0, react.useState)(() => /* @__PURE__ */ new Set());
27109
27843
  const [open, setOpen] = (0, react.useState)(() => /* @__PURE__ */ new Set());
27110
- /** 更早一页要 prepend 时的块数;换页回来(组件重挂载)这份意图会丢,那是可接受的边界。 */
27111
- const pendingScroll = (0, react.useRef)(null);
27112
- /** 上一次布局后的滚动高度:合并更早一页时用它算**真正插进来的高度**,不把请求在飞期间的
27113
- * 滚动或折叠变化也算进去。 */
27114
- const lastHeight = (0, react.useRef)(null);
27844
+ /** 手动「加载更早」、简洁档自动连翻与 prepend 后的滚动锚点整块住在 hook 里。 */
27845
+ const { loadOlder } = useDetailPager({
27846
+ blocks: detail.blocks,
27847
+ olderCursor: detail.olderCursor,
27848
+ loadingOlder,
27849
+ olderFailure,
27850
+ compact: mode === "compact",
27851
+ onLoadOlder,
27852
+ scrollRef
27853
+ });
27115
27854
  const probe = localeProbe(t);
27116
27855
  const project = detail.cwd === "" ? t("trash.project.unknown") : detail.cwd;
27856
+ const models = detail.models.join(", ") || "—";
27857
+ const span = spanText(detail.createdAt, detail.lastActivity, t) ?? "—";
27117
27858
  const disclosureBlocks = detail.blocks.filter(isDisclosureBlock);
27118
- const allOpen = disclosureBlocks.length > 0 && disclosureBlocks.every((block) => open.has(block.id));
27859
+ const items = threadItems(detail.blocks);
27119
27860
  const openChild = detailNav.onOpenChild;
27120
27861
  const toggle = (id) => {
27121
27862
  setOpen((current) => {
@@ -27125,37 +27866,38 @@ window.__ModuleLoader__.load({
27125
27866
  return next;
27126
27867
  });
27127
27868
  };
27128
- const toggleAll = () => {
27129
- setOpen(allOpen ? /* @__PURE__ */ new Set() : new Set(disclosureBlocks.map((block) => block.id)));
27869
+ const toggleGroup = (id) => {
27870
+ setOpenGroups((current) => {
27871
+ const next = new Set(current);
27872
+ if (next.has(id)) next.delete(id);
27873
+ else next.add(id);
27874
+ return next;
27875
+ });
27130
27876
  };
27131
- const loadOlder = () => {
27132
- const element = scrollRef?.current;
27133
- if (element !== null && element !== void 0) pendingScroll.current = { blocks: detail.blocks.length };
27134
- onLoadOlder();
27877
+ /** 换档:完全展开 = 详细档 + 把当前全部过程块铺开;离开这一档时收回,档位之间才有可感知的差别。
27878
+ * 之后读者仍可逐条收起(完全展开不是一把锁,只是一次性的默认)。
27879
+ * 切进简洁档时当前页全是过程块 ⇒ 先按同一份预算自动往前翻,不让首屏留白。 */
27880
+ const chooseViewMode = (next) => {
27881
+ setMode(next);
27882
+ if (next === "verbose") {
27883
+ if (mode !== "verbose") setOpen(new Set(disclosureBlocks.map((block) => block.id)));
27884
+ } else if (mode === "verbose") setOpen(/* @__PURE__ */ new Set());
27885
+ if (next === "compact" && mode !== "compact" && detail.blocks.length > 0 && !hasDialogue(detail.blocks) && detail.olderCursor !== null) loadOlder();
27135
27886
  };
27136
- (0, react.useLayoutEffect)(() => {
27137
- const element = scrollRef?.current;
27138
- const pending = pendingScroll.current;
27139
- if (element === null || element === void 0 || pending === null) return;
27140
- if (detail.blocks.length !== pending.blocks) {
27141
- pendingScroll.current = null;
27142
- if (lastHeight.current !== null) element.scrollTop += element.scrollHeight - lastHeight.current;
27143
- return;
27144
- }
27145
- if (!loadingOlder) pendingScroll.current = null;
27146
- }, [
27147
- detail.blocks.length,
27148
- loadingOlder,
27149
- scrollRef
27150
- ]);
27151
- (0, react.useLayoutEffect)(() => {
27152
- const element = scrollRef?.current;
27153
- if (element !== null && element !== void 0) lastHeight.current = element.scrollHeight;
27154
- });
27155
27887
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
27156
27888
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dl", {
27157
27889
  style: META,
27158
27890
  children: [
27891
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Meta, {
27892
+ label: t("detail.field.project"),
27893
+ title: project,
27894
+ children: project
27895
+ }),
27896
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Meta, {
27897
+ label: t("detail.field.models"),
27898
+ title: models,
27899
+ children: models
27900
+ }),
27159
27901
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Meta, {
27160
27902
  label: t("detail.field.created"),
27161
27903
  children: timeText(detail.createdAt)
@@ -27165,62 +27907,53 @@ window.__ModuleLoader__.load({
27165
27907
  children: timeText(detail.lastActivity)
27166
27908
  }),
27167
27909
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Meta, {
27168
- label: t("detail.field.span"),
27169
- children: spanText(detail.createdAt, detail.lastActivity, t) ?? "—"
27910
+ label: t("detail.field.sessionId"),
27911
+ title: entry.sessionId,
27912
+ code: true,
27913
+ children: entry.sessionId
27170
27914
  }),
27171
27915
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Meta, {
27172
- label: t("detail.field.deletedAt"),
27173
- children: t("detail.deletedAt.combined", {
27174
- relative: agoText(entry.deletedAt, Date.now(), t),
27175
- absolute: absoluteText(entry.deletedAt)
27176
- })
27916
+ label: t("detail.field.trashDir"),
27917
+ title: entry.id,
27918
+ code: true,
27919
+ children: entry.id
27177
27920
  }),
27178
27921
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Meta, {
27922
+ label: t("detail.field.wasArchived"),
27923
+ children: entry.wasArchived ? t("detail.value.yes") : t("detail.value.no")
27924
+ })
27925
+ ]
27926
+ }),
27927
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
27928
+ style: METRICS,
27929
+ children: [
27930
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Metric, {
27179
27931
  label: t("detail.field.size"),
27180
- children: fileSizeText(entry.sizeBytes)
27932
+ value: fileSizeText(entry.sizeBytes)
27181
27933
  }),
27182
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Meta, {
27934
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Metric, {
27935
+ label: t("detail.field.span"),
27936
+ value: span
27937
+ }),
27938
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Metric, {
27183
27939
  label: t("detail.field.turns"),
27184
- children: String(detail.turnCount)
27940
+ value: String(detail.turnCount)
27185
27941
  }),
27186
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Meta, {
27942
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Metric, {
27187
27943
  label: t("detail.field.userMessages"),
27188
- children: String(detail.userMessageCount)
27944
+ value: String(detail.userMessageCount)
27189
27945
  }),
27190
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Meta, {
27946
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Metric, {
27191
27947
  label: t("detail.field.assistantMessages"),
27192
- children: String(detail.assistantMessageCount)
27948
+ value: String(detail.assistantMessageCount)
27193
27949
  }),
27194
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Meta, {
27950
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Metric, {
27195
27951
  label: t("detail.field.toolCalls"),
27196
- children: String(detail.toolCallCount)
27952
+ value: String(detail.toolCallCount)
27197
27953
  }),
27198
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Meta, {
27954
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Metric, {
27199
27955
  label: t("detail.field.injections"),
27200
- children: String(detail.injectedCount)
27201
- }),
27202
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Meta, {
27203
- label: t("detail.field.models"),
27204
- children: detail.models.join(", ") || "—"
27205
- }),
27206
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Meta, {
27207
- label: t("detail.field.project"),
27208
- title: project,
27209
- children: project
27210
- }),
27211
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Meta, {
27212
- label: t("detail.field.sessionId"),
27213
- title: entry.sessionId,
27214
- children: entry.sessionId
27215
- }),
27216
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Meta, {
27217
- label: t("detail.field.trashDir"),
27218
- title: entry.id,
27219
- children: entry.id
27220
- }),
27221
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Meta, {
27222
- label: t("detail.field.wasArchived"),
27223
- children: entry.wasArchived ? t("detail.value.yes") : t("detail.value.no")
27956
+ value: String(detail.injectedCount)
27224
27957
  })
27225
27958
  ]
27226
27959
  }),
@@ -27229,7 +27962,7 @@ window.__ModuleLoader__.load({
27229
27962
  style: {
27230
27963
  display: "flex",
27231
27964
  flexDirection: "column",
27232
- gap: 6
27965
+ gap: 2
27233
27966
  },
27234
27967
  children: [
27235
27968
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -27245,8 +27978,18 @@ window.__ModuleLoader__.load({
27245
27978
  "data-dsh-session-delete": "trash-subagent-row",
27246
27979
  "data-entry-id": child.id,
27247
27980
  style: SUBAGENT_ROW,
27981
+ title: t("detail.subagents.open"),
27248
27982
  onClick: () => openChild(child),
27249
27983
  children: [
27984
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
27985
+ style: {
27986
+ display: "inline-flex",
27987
+ flex: "none",
27988
+ color: "var(--sd-label-tertiary)"
27989
+ },
27990
+ "aria-hidden": "true",
27991
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconBranchOutlineRegular, { size: 14 })
27992
+ }),
27250
27993
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
27251
27994
  style: {
27252
27995
  ...ELLIPSIS,
@@ -27262,6 +28005,15 @@ window.__ModuleLoader__.load({
27262
28005
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
27263
28006
  style: BLOCK_META,
27264
28007
  children: agoText(child.deletedAt, Date.now(), t)
28008
+ }),
28009
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
28010
+ style: {
28011
+ display: "inline-flex",
28012
+ flex: "none",
28013
+ color: "var(--sd-label-tertiary)"
28014
+ },
28015
+ "aria-hidden": "true",
28016
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconChevronRightOutlineRegular, { size: 14 })
27265
28017
  })
27266
28018
  ]
27267
28019
  }, child.id)),
@@ -27294,13 +28046,20 @@ window.__ModuleLoader__.load({
27294
28046
  },
27295
28047
  children: olderFailure
27296
28048
  }),
27297
- disclosureBlocks.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
28049
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
27298
28050
  style: { marginLeft: "auto" },
27299
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
27300
- variant: "ghost",
27301
- size: "sm",
27302
- onClick: toggleAll,
27303
- children: allOpen ? t("detail.collapseAll") : t("detail.expandAll")
28051
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
28052
+ className: "sd-seg",
28053
+ role: "group",
28054
+ "aria-label": t("detail.viewMode.label"),
28055
+ children: VIEW_MODES.map(({ key, label }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
28056
+ type: "button",
28057
+ "aria-pressed": mode === key,
28058
+ onClick: () => {
28059
+ chooseViewMode(key);
28060
+ },
28061
+ children: t(label)
28062
+ }, key))
27304
28063
  })
27305
28064
  })
27306
28065
  ]
@@ -27308,14 +28067,56 @@ window.__ModuleLoader__.load({
27308
28067
  detail.blocks.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
27309
28068
  style: META_LINE,
27310
28069
  children: t("detail.noBody")
27311
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
28070
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
27312
28071
  style: THREAD,
27313
- children: detail.blocks.map((block) => renderBlock(block, {
28072
+ children: [mode === "standard" ? items.map((item) => item.kind === "block" ? renderBlock(item.block, {
28073
+ open,
28074
+ toggle,
28075
+ probe,
28076
+ t
28077
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
28078
+ type: "button",
28079
+ className: "sd-disclosure-row",
28080
+ "data-dsh-session-delete": "trash-process-head",
28081
+ "data-expandable": "",
28082
+ "aria-expanded": openGroups.has(item.id),
28083
+ onClick: () => {
28084
+ toggleGroup(item.id);
28085
+ },
28086
+ style: BLOCK_ROW,
28087
+ children: [
28088
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
28089
+ className: "sd-disclosure-leading",
28090
+ style: { marginRight: 0 },
28091
+ "aria-hidden": "true",
28092
+ children: openGroups.has(item.id) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconChevronDownOutlineRegular, { size: 14 }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconChevronRightOutlineRegular, { size: 14 })
28093
+ }),
28094
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
28095
+ style: BLOCK_TITLE,
28096
+ children: t("detail.process.header")
28097
+ }),
28098
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
28099
+ style: BLOCK_SUMMARY,
28100
+ children: stepsText(item.blocks.length, t)
28101
+ })
28102
+ ]
28103
+ }), openGroups.has(item.id) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
28104
+ style: PROCESS_BODY,
28105
+ children: item.blocks.map((block) => renderBlock(block, {
28106
+ open,
28107
+ toggle,
28108
+ probe,
28109
+ t
28110
+ }))
28111
+ }) : null] }, item.id)) : detail.blocks.filter((block) => mode !== "compact" || !isDisclosureBlock(block)).map((block) => renderBlock(block, {
27314
28112
  open,
27315
28113
  toggle,
27316
28114
  probe,
27317
28115
  t
27318
- }))
28116
+ })), mode === "compact" && detail.blocks.length > 0 && !hasDialogue(detail.blocks) && !loadingOlder ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
28117
+ style: META_LINE,
28118
+ children: t(detail.olderCursor === null ? "detail.compact.empty" : "detail.compact.pageOnly")
28119
+ }) : null]
27319
28120
  })
27320
28121
  ] });
27321
28122
  }
@@ -28191,10 +28992,11 @@ window.__ModuleLoader__.load({
28191
28992
  * - 动作留在清单行右侧;详情只负责看,失败原文留在出事的那一行。
28192
28993
  * - 空态不出现「清空回收站…」;「读不到」与「空的」分开。
28193
28994
  * - 危险层级:页脚那排的最后一颗是整页唯一的实心红(非多选是「清空回收站…」,
28194
- * 多选是「彻底删除 N 条」);行内「彻底删除…」只给字色,不给底色。
28995
+ * 多选是「彻底删除 N 条」);行内「彻底删除」图标默认只给字色,不铺实心底,悬停才转红。
28195
28996
  *
28196
- * 样式:结构与布局用内联样式;配色与圆角走宿主 token;控件样式在 src/client/ui/ui.css,
28197
- * 由 ui/styles.ts 在模块物化时注入 style。界面规格见 docs/ui-spec.md;
28997
+ * 样式:结构与布局用内联样式;配色与圆角走宿主 token;控件与清单行样式在 `src/client/ui/`
28998
+ * (`ui.css` 是宿主抄本,`forms.css` / `layout.css` 是自造规则),由 `ui/styles.ts` 注入 style。
28999
+ * 界面规格见 docs/ui-spec.md;
28198
29000
  * 改断点或列宽前先看 SPLIT_MIN_WIDTH、LIST_COLUMN_DEFAULT / LIST_COLUMN_MIN、DETAIL_RESERVE 与 usePanelWidth。
28199
29001
  */
28200
29002
  /** 回收站相对宿主**数据根**的位置。**不写 `~/.dsh/`**:数据根可以是 `$DSH_HOME` 指的别处。
@@ -28206,8 +29008,8 @@ window.__ModuleLoader__.load({
28206
29008
  * 900 这一档清单为 411,刚好守住两边可读下限。改数字前先同步 DETAIL_RESERVE 与 usePanelWidth。
28207
29009
  */
28208
29010
  const SPLIT_MIN_WIDTH = 900;
28209
- /** 左栏默认宽:为“行内有动作”设计——两颗按钮约 152px,卡片内边距 28px,
28210
- * 剩下约 240px 给标题与元信息。可拖范围见 LIST_COLUMN_MIN / DETAIL_RESERVE。 */
29011
+ /** 左栏默认宽:为“行内有动作”设计——两颗 28px 图标按钮加间距与行内边距约 80px,
29012
+ * 剩下约 340px 给标题与元信息。可拖范围见 LIST_COLUMN_MIN / DETAIL_RESERVE。 */
28211
29013
  const LIST_COLUMN_DEFAULT = 420;
28212
29014
  /** 左栏下限 330:再窄时「删除于 … · 224KB」那行就会开始被省略号截断。
28213
29015
  * 按钮 flex:none,不参与这个下限。 */
@@ -28298,8 +29100,9 @@ window.__ModuleLoader__.load({
28298
29100
  paddingBottom: 8
28299
29101
  };
28300
29102
  /**
28301
- * 行那句「删除于 3小时前」:整句模板住在字典里(两种语言各自钉语序),单位词与绝对串走 `copy.ts`
28302
- * 那一处——详情那格要的是同一份,别抄成两本账。`now` 桶不套模板:"删除于 刚刚前"不是话。
29103
+ * 行那句「删除于 3小时前」:整句模板住在字典里(两种语言各自钉语序),单位词走 `copy.ts`
29104
+ * 的 `unitWord` / `relativeTime`——子会话行的时间读同一族,别抄成两本账。`now` 桶不套模板:
29105
+ * "删除于 刚刚前"不是话。
28303
29106
  */
28304
29107
  function deletedAgo(at, now, t) {
28305
29108
  const { unit, n } = relativeTime(at, now);
@@ -28333,12 +29136,13 @@ window.__ModuleLoader__.load({
28333
29136
  /** 详情栏的滚动容器:分页 prepend 后按高度差补偿滚动位置(归 `trash-detail.tsx` 消费)。 */
28334
29137
  const detailScrollRef = (0, react.useRef)(null);
28335
29138
  const width = usePanelWidth(rootRef);
29139
+ const wide = width >= SPLIT_MIN_WIDTH;
28336
29140
  /**
28337
29141
  * 真走两栏的条件:盒子够宽 **且清单里有东西可看**。空态与读数失败时右栏不出现——那一栏既没有
28338
29142
  * 可选项也没有动作,白占一半宽度(docs/ui-spec.md)。失败那块自己的「重试」
28339
29143
  * 负责把清单读回来,读回来之后右栏跟着回来。
28340
29144
  */
28341
- const split = width >= SPLIT_MIN_WIDTH && entries !== null && entries.length > 0;
29145
+ const split = wide && entries !== null && entries.length > 0;
28342
29146
  /** 两栏之间那根可拖的把手(窄档不渲染它,但 hook 照样得在——不能在条件里调)。 */
28343
29147
  const resize = useColumnResize({
28344
29148
  panelWidth: width,
@@ -28482,7 +29286,7 @@ window.__ModuleLoader__.load({
28482
29286
  }) : view.loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
28483
29287
  style: {
28484
29288
  padding: "20px 2px",
28485
- fontSize: 12.5,
29289
+ fontSize: 12,
28486
29290
  color: "var(--sd-label-tertiary)"
28487
29291
  },
28488
29292
  children: t("trash.loading")
@@ -28515,7 +29319,7 @@ window.__ModuleLoader__.load({
28515
29319
  }),
28516
29320
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
28517
29321
  style: {
28518
- fontSize: 13.5,
29322
+ fontSize: 14,
28519
29323
  color: "var(--dsw-alias-label-secondary)"
28520
29324
  },
28521
29325
  children: t("trash.empty.title")
@@ -28537,7 +29341,7 @@ window.__ModuleLoader__.load({
28537
29341
  style: {
28538
29342
  display: "flex",
28539
29343
  flexDirection: "column",
28540
- gap: 8,
29344
+ gap: 2,
28541
29345
  minWidth: 0
28542
29346
  },
28543
29347
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -28547,7 +29351,8 @@ window.__ModuleLoader__.load({
28547
29351
  display: "flex",
28548
29352
  alignItems: "center",
28549
29353
  gap: 6,
28550
- minWidth: 0
29354
+ minWidth: 0,
29355
+ marginBottom: 4
28551
29356
  },
28552
29357
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
28553
29358
  role: "gridcell",
@@ -28589,18 +29394,17 @@ window.__ModuleLoader__.load({
28589
29394
  },
28590
29395
  children: [
28591
29396
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
28592
- className: "sd-disclosure-leading",
29397
+ className: "sd-disclosure-leading sd-group-folder",
28593
29398
  "aria-hidden": "true",
28594
- children: isCollapsed(group.projectDir) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconChevronRightOutlineRegular, { size: 14 }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconChevronDownOutlineRegular, { size: 14 })
29399
+ children: isCollapsed(group.projectDir) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconFolderCloseRegular, { size: 14 }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconFolderOpenRegular, { size: 14 })
28595
29400
  }),
28596
29401
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
28597
- style: {
28598
- display: "inline-flex",
28599
- flex: "none",
28600
- color: "var(--sd-label-tertiary)"
28601
- },
29402
+ className: "sd-disclosure-leading sd-group-chevron",
28602
29403
  "aria-hidden": "true",
28603
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconFolderOpenOutlineRegular, { size: 13 })
29404
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
29405
+ className: isCollapsed(group.projectDir) ? "sd-group-arrow" : "sd-group-arrow sd-group-arrow-open",
29406
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconTriangleRightFillRegular, { size: 14 })
29407
+ })
28604
29408
  }),
28605
29409
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
28606
29410
  className: "sd-disclosure-title",
@@ -28610,14 +29414,14 @@ window.__ModuleLoader__.load({
28610
29414
  flex: "1 1 auto",
28611
29415
  fontWeight: 500
28612
29416
  },
28613
- title: group.projectDir,
29417
+ title: group.title,
28614
29418
  children: group.label
28615
29419
  }),
28616
29420
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
28617
29421
  style: {
28618
29422
  marginLeft: "auto",
28619
29423
  flex: "none",
28620
- fontSize: 11.5,
29424
+ fontSize: 11,
28621
29425
  color: "var(--sd-label-tertiary)"
28622
29426
  },
28623
29427
  children: t("trash.group.count", { count: group.entries.length })
@@ -28638,7 +29442,7 @@ window.__ModuleLoader__.load({
28638
29442
  tabIndex: entry.id === nav.tabStopId ? 0 : -1,
28639
29443
  "data-dsh-session-delete": "trash-row",
28640
29444
  "data-entry-id": entry.id,
28641
- style: isSelected ? CARD_SELECTED : CARD,
29445
+ style: isSelected ? ROW_SELECTED : ROW,
28642
29446
  onClick: () => {
28643
29447
  if (selectMode) {
28644
29448
  toggleCheck(entry.id);
@@ -28683,9 +29487,10 @@ window.__ModuleLoader__.load({
28683
29487
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
28684
29488
  style: {
28685
29489
  ...ELLIPSIS,
29490
+ ...TABULAR_NUMS,
28686
29491
  fontSize: 12,
28687
29492
  lineHeight: "16px",
28688
- color: "var(--sd-label-tertiary)"
29493
+ color: "var(--dsw-alias-label-secondary)"
28689
29494
  },
28690
29495
  title: absoluteText(entry.deletedAt),
28691
29496
  children: [
@@ -28695,15 +29500,6 @@ window.__ModuleLoader__.load({
28695
29500
  stranded > 0 ? t("trash.row.strandedSubagents", { n: stranded }) : ""
28696
29501
  ].filter(Boolean).join(" · ")
28697
29502
  }),
28698
- entry.workspaceMissing === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
28699
- "data-dsh-session-delete": "trash-row-workspace-gone",
28700
- style: {
28701
- fontSize: 12,
28702
- lineHeight: "16px",
28703
- color: "var(--sd-label-tertiary)"
28704
- },
28705
- children: t("trash.row.workspaceGone")
28706
- }) : null,
28707
29503
  controller.actionFailure[entry.id] === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
28708
29504
  role: "status",
28709
29505
  style: {
@@ -28730,23 +29526,25 @@ window.__ModuleLoader__.load({
28730
29526
  role: "gridcell",
28731
29527
  style: {
28732
29528
  display: "flex",
28733
- gap: 6,
29529
+ gap: 2,
28734
29530
  flex: "none"
28735
29531
  },
28736
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
28737
- variant: "outline",
28738
- size: "sm",
29532
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
29533
+ type: "button",
29534
+ className: "sd-icon-btn",
29535
+ title: t("trash.row.restore"),
29536
+ "aria-label": t("trash.row.restore"),
28739
29537
  disabled: controller.busy,
28740
29538
  onClick: (event) => {
28741
29539
  event.stopPropagation();
28742
29540
  controller.restore(entry);
28743
29541
  },
28744
- children: t("trash.row.restore")
28745
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
28746
- variant: "ghost",
28747
- size: "sm",
28748
- style: QUIET_DANGER,
28749
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconTrashOutlineRegular, { size: 13 }),
29542
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconRestoreOutlineRegular, { size: 14 })
29543
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
29544
+ type: "button",
29545
+ className: "sd-icon-btn sd-icon-btn-danger",
29546
+ title: t("trash.row.purge"),
29547
+ "aria-label": t("trash.row.purgeOne", { title }),
28750
29548
  disabled: controller.busy,
28751
29549
  onClick: (event) => {
28752
29550
  event.stopPropagation();
@@ -28755,7 +29553,7 @@ window.__ModuleLoader__.load({
28755
29553
  entry
28756
29554
  });
28757
29555
  },
28758
- children: t("trash.row.purge")
29556
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconTrashOutlineRegular, { size: 14 })
28759
29557
  })]
28760
29558
  })
28761
29559
  ]
@@ -28828,7 +29626,7 @@ window.__ModuleLoader__.load({
28828
29626
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
28829
29627
  ref: rootRef,
28830
29628
  "data-dsh-session-delete": "trash-panel",
28831
- className: resize.resizing ? "sd-resizing" : void 0,
29629
+ className: [resize.resizing ? "sd-resizing" : "", wide ? "" : "sd-narrow"].filter(Boolean).join(" ") || void 0,
28832
29630
  style: PAGE,
28833
29631
  children: [
28834
29632
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -28901,7 +29699,7 @@ window.__ModuleLoader__.load({
28901
29699
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
28902
29700
  style: {
28903
29701
  minWidth: 0,
28904
- fontSize: 11.5,
29702
+ fontSize: 11,
28905
29703
  lineHeight: "16px",
28906
29704
  color: "var(--sd-label-tertiary)"
28907
29705
  },
@@ -29134,8 +29932,9 @@ window.__ModuleLoader__.load({
29134
29932
  * **`locale` 也不在这里**:宿主自己的 `ui-sidebar` 是硬注入它的,但那是"宿主知道自己那一代"的前提;
29135
29933
  * 本插件要跨代活着 ⇒ 走 `ctx.get('locale')` 可选读,缺席时降级到内置 zh 文案(见 `copy.ts` 头注)。
29136
29934
  * 写进 inject 就等于"这一代没有 locale 就不让整个客户端条目激活",而那等于宿主进程死。
29137
- * **`contextMenus` 同理**:0.2.2+ 用 `ctx.get('contextMenus')` 探测,包本身在 package.json 的
29138
- * `dsh.client.inject` 里排到前面;旧宿主没有这个服务时回退 legacy 槽位。
29935
+ * **`contextMenus` 同理**:0.2.2+ 用 `ctx.get('contextMenus')` 探测,服务比本条目晚到时按
29936
+ * `ctx.inject(['contextMenus'])` 的 scoped 等待切过去;旧宿主没有这个服务时回调不来,legacy 槽位留着。
29937
+ * 条目自己的 `inject` 仍然只有 `slots`——把可能迟到的服务写进去等于让整个客户端条目 pending。
29139
29938
  */
29140
29939
  const inject = ["slots"];
29141
29940
  /** legacy 菜单槽:只在 contextMenus 不可用时注册(0.2.2+ 的内置动作已迁到数据动作)。 */
@@ -29250,46 +30049,77 @@ window.__ModuleLoader__.load({
29250
30049
  */
29251
30050
  async function activate(client) {
29252
30051
  bindMainView(client);
30052
+ let disposed = false;
30053
+ if (typeof client.effect === "function") client.effect(() => () => {
30054
+ disposed = true;
30055
+ }, "session-delete: activation");
29253
30056
  const raw = createHttpRemote();
29254
30057
  if (!await probeChannel(raw)) return;
30058
+ if (disposed) return;
29255
30059
  const deps = buildDeps(raw);
29256
30060
  const t = createTranslate(client);
29257
- let usesContextMenus = false;
29258
- const contextMenus = client.get?.("contextMenus");
29259
- if (contextMenus !== null && contextMenus !== void 0 && typeof contextMenus.register === "function") {
29260
- const sessions = client.get?.("sessions");
30061
+ /** 数据动作路径:注册 contextMenus 贡献 + 两个 icon key。返回是否成功接管。 */
30062
+ const registerDataMenus = (target) => {
30063
+ const menus = target.get?.("contextMenus");
30064
+ if (menus === null || menus === void 0 || typeof menus.register !== "function") return false;
30065
+ const sessions = target.get?.("sessions");
29261
30066
  try {
29262
- client.effect(() => contextMenus.register(createSessionMenuContribution(t, sessions?.list)), "session-delete: context menu actions");
29263
- usesContextMenus = true;
30067
+ target.effect(() => menus.register(createSessionMenuContribution(t, sessions?.list)), "session-delete: context menu actions");
29264
30068
  } catch (error) {
29265
30069
  console.warn("[dsh-session-delete] context menu 注册失败,回退 legacy 菜单行:", error);
30070
+ return false;
29266
30071
  }
29267
- if (usesContextMenus) try {
29268
- client.slots.inject("contextMenu.icon", () => client.slots.register({
30072
+ try {
30073
+ target.slots.inject("contextMenu.icon", () => target.slots.register({
29269
30074
  name: "contextMenu.icon",
29270
30075
  key: ICON_BATCH
29271
30076
  }, IconCheckOutlineRegular));
29272
- client.slots.inject("contextMenu.icon", () => client.slots.register({
30077
+ target.slots.inject("contextMenu.icon", () => target.slots.register({
29273
30078
  name: "contextMenu.icon",
29274
30079
  key: ICON_DELETE
29275
30080
  }, IconTrashOutlineRegular));
29276
30081
  } catch (error) {
29277
30082
  console.warn("[dsh-session-delete] context menu 图标注册失败:", error);
29278
30083
  }
29279
- }
29280
- if (!usesContextMenus) {
29281
- client.slots.inject(MENU_SLOT, () => client.slots.register({
30084
+ return true;
30085
+ };
30086
+ /** legacy 菜单行:返回两个注册的摘除函数(宿主对 `slots.inject` 的返回值就是 disposer)。 */
30087
+ const registerLegacyMenus = () => {
30088
+ const disposers = [];
30089
+ const keep = (dispose) => {
30090
+ if (typeof dispose === "function") disposers.push(dispose);
30091
+ };
30092
+ keep(client.slots.inject(MENU_SLOT, () => client.slots.register({
29282
30093
  name: MENU_SLOT,
29283
30094
  id: `${PLUGIN_ID}.delete`,
29284
30095
  order: 900,
29285
30096
  inject: () => ({ t })
29286
- }, DeleteRow));
29287
- client.slots.inject(MENU_SLOT, () => client.slots.register({
30097
+ }, DeleteRow)));
30098
+ keep(client.slots.inject(MENU_SLOT, () => client.slots.register({
29288
30099
  name: MENU_SLOT,
29289
30100
  id: `${PLUGIN_ID}.batch-select`,
29290
30101
  order: 850,
29291
30102
  inject: () => ({ t })
29292
- }, SelectRow));
30103
+ }, SelectRow)));
30104
+ return disposers;
30105
+ };
30106
+ if (!registerDataMenus(client)) {
30107
+ const legacy = registerLegacyMenus();
30108
+ let legacyDisposed = false;
30109
+ try {
30110
+ if ("inject" in client) client.inject(["contextMenus"], (child) => {
30111
+ if (!registerDataMenus(child)) return;
30112
+ if (legacyDisposed) return;
30113
+ legacyDisposed = true;
30114
+ for (const dispose of legacy) try {
30115
+ dispose();
30116
+ } catch (error) {
30117
+ console.warn("[dsh-session-delete] legacy 菜单行摘除失败:", error);
30118
+ }
30119
+ });
30120
+ } catch (error) {
30121
+ console.warn("[dsh-session-delete] 等待 contextMenus 就绪失败,legacy 菜单行保留:", error);
30122
+ }
29293
30123
  }
29294
30124
  client.slots.inject(LEADING_SLOT, () => client.slots.register({
29295
30125
  name: LEADING_SLOT,
@@ -29356,7 +30186,7 @@ window.__ModuleLoader__.load({
29356
30186
  deps,
29357
30187
  t
29358
30188
  })
29359
- }, MaintenanceSettings));
30189
+ }, SettingsCards));
29360
30190
  }
29361
30191
  /**
29362
30192
  * 入口:**同步返回、绝不抛**(见文件头注)。
@@ -29367,7 +30197,11 @@ window.__ModuleLoader__.load({
29367
30197
  * `ctx.slots.subscribe("sidebar.panellist", syncPanels)`(asar @44795361)。
29368
30198
  */
29369
30199
  function apply(ctx) {
29370
- activate(ctx).catch((error) => {
30200
+ const client = ctx;
30201
+ if (typeof client.effect === "function") try {
30202
+ client.effect(() => installPointerModality(), "session-delete: pointer modality");
30203
+ } catch {}
30204
+ activate(client).catch((error) => {
29371
30205
  console.warn("[dsh-session-delete] 入口未注册(取数通道不可用):", error);
29372
30206
  });
29373
30207
  }