@xrn1997/dsh-session-delete 0.6.0 → 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.
- package/README.md +22 -13
- package/THIRD_PARTY_NOTICES.md +179 -0
- package/cordis.patch.yml +1 -1
- package/docs/screenshots/screenshot-01.png +0 -0
- package/docs/screenshots/screenshot-02.png +0 -0
- package/docs/screenshots/screenshot-03.png +0 -0
- package/icon.svg +24 -18
- package/lib/client.js +1439 -410
- package/lib/index.js +475 -244
- package/lib/types/api/connection-route.d.ts +1 -1
- package/lib/types/api/dispatch.d.ts +1 -1
- package/lib/types/api/route-table.d.ts +12 -7
- package/lib/types/client/api-base.d.ts +19 -0
- package/lib/types/client/config-settings.d.ts +13 -0
- package/lib/types/client/context-menu-actions.d.ts +47 -0
- package/lib/types/client/context.d.ts +3 -0
- package/lib/types/client/copy.d.ts +42 -7
- package/lib/types/client/delete-confirm.d.ts +2 -2
- package/lib/types/client/detail-blocks.d.ts +25 -0
- package/lib/types/client/index.d.ts +3 -0
- package/lib/types/client/maintenance-settings.d.ts +5 -4
- package/lib/types/client/remote.d.ts +43 -15
- package/lib/types/client/settings-cards.d.ts +19 -0
- package/lib/types/client/settings-view.d.ts +9 -0
- package/lib/types/client/trash-detail.d.ts +1 -1
- package/lib/types/client/trash-nav.d.ts +5 -1
- package/lib/types/client/trash-view.d.ts +34 -5
- package/lib/types/client/ui/MenuItemButton.d.ts +1 -1
- package/lib/types/client/ui/Toast.d.ts +4 -1
- package/lib/types/client/ui/icons.d.ts +23 -7
- package/lib/types/client/ui/recipe.d.ts +2 -0
- package/lib/types/client/ui/styles.d.ts +2 -0
- package/lib/types/client/use-detail-pager.d.ts +33 -0
- package/lib/types/config.d.ts +10 -11
- package/lib/types/deps.d.ts +9 -1
- package/lib/types/index.d.ts +8 -5
- package/lib/types/ports/agent-handles.d.ts +10 -2
- package/lib/types/ports/fs-port.d.ts +10 -0
- package/lib/types/ports/host-port.d.ts +4 -2
- package/lib/types/shared/wire.d.ts +37 -5
- package/lib/types/trash/attachments.d.ts +15 -2
- package/lib/types/verbs/config.d.ts +25 -0
- package/lib/types/verbs/errors.d.ts +3 -1
- package/lib/types/verbs/permanent-removal.d.ts +10 -5
- package/locale/en.json +1 -1
- package/locale/zh.json +6 -6
- package/package.json +30 -19
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.
|
|
236
|
-
"detail.
|
|
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": "回收站清单",
|
|
@@ -264,6 +289,7 @@ window.__ModuleLoader__.load({
|
|
|
264
289
|
"detail.role.assistant": "助手",
|
|
265
290
|
"menu.delete": "删除",
|
|
266
291
|
"menu.live": "会话正在运行,先停止再删除",
|
|
292
|
+
"menu.deleteLive": "删除(运行中)",
|
|
267
293
|
"dialog.title": "删除这个会话?",
|
|
268
294
|
"dialog.subagents": "含 {n} 个子会话,将一起移入回收站。",
|
|
269
295
|
"dialog.hint": "会话会移入回收站,之后可以随时恢复。彻底删除是回收站里的另一个动作。",
|
|
@@ -302,6 +328,7 @@ window.__ModuleLoader__.load({
|
|
|
302
328
|
"trash.empty.hint": "Pick Delete in a session’s “…” menu; the session lands here first and can be restored at any time.",
|
|
303
329
|
"trash.summary": "{count} sessions · {size} reclaimable",
|
|
304
330
|
"trash.group.count": "{count} sessions",
|
|
331
|
+
"trash.group.ungrouped": "Ungrouped",
|
|
305
332
|
"trash.project.unknown": "Unrecorded project",
|
|
306
333
|
"trash.row.deletedNow": "Deleted just now",
|
|
307
334
|
"trash.row.deletedAgo": "Deleted {n}{unit} ago",
|
|
@@ -314,7 +341,8 @@ window.__ModuleLoader__.load({
|
|
|
314
341
|
"trash.unit.months": "mo",
|
|
315
342
|
"trash.unit.years": "y",
|
|
316
343
|
"trash.row.restore": "Restore",
|
|
317
|
-
"trash.row.purge": "Delete permanently
|
|
344
|
+
"trash.row.purge": "Delete permanently",
|
|
345
|
+
"trash.row.purgeOne": "Delete “{title}” permanently",
|
|
318
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.",
|
|
319
347
|
"trash.emptyAll": "Empty the trash…",
|
|
320
348
|
"trash.confirm.all": "Empty the trash?",
|
|
@@ -354,6 +382,22 @@ window.__ModuleLoader__.load({
|
|
|
354
382
|
"trash.attachments.skipped": "Attachments were not cleaned: {reason}",
|
|
355
383
|
"trash.attachments.skippedPartial": "Some attachments were not cleaned: {reason} ({removed} cleaned)",
|
|
356
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",
|
|
357
401
|
"maintenance.title": "Maintenance",
|
|
358
402
|
"maintenance.orphans.title": "Orphan sessions",
|
|
359
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.",
|
|
@@ -382,8 +426,6 @@ window.__ModuleLoader__.load({
|
|
|
382
426
|
"detail.noBody": "This session has nothing to show",
|
|
383
427
|
"detail.field.created": "Created",
|
|
384
428
|
"detail.field.lastActivity": "Last activity",
|
|
385
|
-
"detail.field.deletedAt": "Deleted",
|
|
386
|
-
"detail.deletedAt.combined": "{relative} ({absolute})",
|
|
387
429
|
"detail.field.span": "Active for",
|
|
388
430
|
"detail.field.size": "Size",
|
|
389
431
|
"detail.field.turns": "Turns",
|
|
@@ -415,8 +457,16 @@ window.__ModuleLoader__.load({
|
|
|
415
457
|
"detail.block.truncated": "Truncated",
|
|
416
458
|
"detail.loadOlder": "Load earlier",
|
|
417
459
|
"detail.loadOlder.loading": "Loading earlier…",
|
|
418
|
-
"detail.
|
|
419
|
-
"detail.
|
|
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",
|
|
420
470
|
"detail.injection.runtimeContext": "Runtime context",
|
|
421
471
|
"detail.injection.timeContext": "Time context",
|
|
422
472
|
"detail.injection.agentInstructions": "Agent instructions",
|
|
@@ -439,7 +489,8 @@ window.__ModuleLoader__.load({
|
|
|
439
489
|
"detail.attachment.saving": "Saving…",
|
|
440
490
|
"detail.attachment.saveFailed": "Save failed: {message}",
|
|
441
491
|
"detail.back": "← Back to list",
|
|
442
|
-
"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",
|
|
443
494
|
"detail.subagents.truncated": "Showing the first {shown} of {total} subagent sessions",
|
|
444
495
|
"detail.backToParent": "← Back to parent session",
|
|
445
496
|
"detail.list.label": "Trash list",
|
|
@@ -447,6 +498,7 @@ window.__ModuleLoader__.load({
|
|
|
447
498
|
"detail.role.assistant": "Assistant",
|
|
448
499
|
"menu.delete": "Delete",
|
|
449
500
|
"menu.live": "This session is running. Stop it first.",
|
|
501
|
+
"menu.deleteLive": "Delete (running)",
|
|
450
502
|
"dialog.title": "Delete this session?",
|
|
451
503
|
"dialog.subagents": "It also moves {n} subagent sessions to the trash.",
|
|
452
504
|
"dialog.hint": "The session moves to the trash and can be restored at any time. Permanent deletion is a separate action in the trash.",
|
|
@@ -477,21 +529,42 @@ window.__ModuleLoader__.load({
|
|
|
477
529
|
/** 服务缺席时的缺省:字典里那套 zh。**也是组件 prop 的缺省值**——它表达的是"这次没有 locale 服务",
|
|
478
530
|
* 与运行期 `createTranslate` 的降级分支是同一件事,不是"忘了传"。 */
|
|
479
531
|
const zhTranslate = (key, params) => interpolate(ZH[key], params);
|
|
532
|
+
/** 探测宿主 locale 服务:缺席 / `ctx.get` 抛错都回 `undefined`,不往上抛。 */
|
|
533
|
+
function detectLocaleService(ctx) {
|
|
534
|
+
try {
|
|
535
|
+
const found = ctx.get?.("locale");
|
|
536
|
+
return found == null ? void 0 : found;
|
|
537
|
+
} catch {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
480
541
|
/**
|
|
481
542
|
* 注册字典并交回本插件的 `t`。服务不在时返回 `zhTranslate`(不抛、不 pending)。
|
|
482
543
|
*
|
|
544
|
+
* **空值判据同时认 `undefined` 与 `null`**:宿主 `ctx.get` 可以回 null(同一条窄镜像的读法见
|
|
545
|
+
* `main-view.ts` 的 `isReadyUiWorkspace`)。这里只判 `undefined` 的版本会把 null 当成"服务在",
|
|
546
|
+
* 下一句 `service.register` 抛错——而本函数跑在**任何槽位注册之前**,抛出去等于整个客户端条目
|
|
547
|
+
* 从页面消失(`apply` 只打一条 warn)。注册 / 绑定自身失败同样退回内置 zh:文案降级远好过没有插件。
|
|
548
|
+
* `bind` 在注册之后才跑,它失败时字典已挂上但没人用——留着无害(宿主只是多一份不用的词典),
|
|
549
|
+
* 不为它补撤销路径再引入新的失败面。
|
|
550
|
+
*
|
|
483
551
|
* 注册必须挂在 `ctx.effect` 上:`register` 返回的 disposer 是**官方契约**(卸载时要把字典摘掉),
|
|
484
552
|
* 而且同一命名空间的同一语言**只能注册一次**——effect 没挂,HMR 重挂就撞 "already has locale"。
|
|
485
553
|
*/
|
|
486
554
|
function createTranslate(ctx) {
|
|
487
|
-
const service = ctx
|
|
555
|
+
const service = detectLocaleService(ctx);
|
|
488
556
|
if (service === void 0) return zhTranslate;
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
557
|
+
try {
|
|
558
|
+
ctx.effect(() => service.register(COPY_NS, {
|
|
559
|
+
zh: ZH,
|
|
560
|
+
en: EN
|
|
561
|
+
}), "session-delete: copy dictionaries");
|
|
562
|
+
const bound = service.bind(COPY_NS);
|
|
563
|
+
return (key, params) => bound(key, params);
|
|
564
|
+
} catch (error) {
|
|
565
|
+
console.warn("[dsh-session-delete] locale 服务不可用,退回内置中文:", error);
|
|
566
|
+
return zhTranslate;
|
|
567
|
+
}
|
|
495
568
|
}
|
|
496
569
|
/**
|
|
497
570
|
* `MarkdownBody`(`markdown.tsx`)memo comparator 的**语言探针**:借一句固定键的译文,当
|
|
@@ -534,7 +607,7 @@ window.__ModuleLoader__.load({
|
|
|
534
607
|
];
|
|
535
608
|
}
|
|
536
609
|
/**
|
|
537
|
-
* 绝对时间那一句串。清单行的 `title`、确认框里的meta
|
|
610
|
+
* 绝对时间那一句串。清单行的 `title`、确认框里的meta、详情的会话信息三处要的是同一个写法,
|
|
538
611
|
* 各自 `new Date(...).toLocaleString()` 一遍迟早会漂成两种样子。
|
|
539
612
|
*/
|
|
540
613
|
function absoluteText(at) {
|
|
@@ -555,6 +628,10 @@ window.__ModuleLoader__.load({
|
|
|
555
628
|
function unitWord(unit, t) {
|
|
556
629
|
return unit === "now" ? t("common.justNow") : t(UNIT_KEY[unit]);
|
|
557
630
|
}
|
|
631
|
+
/** 「工作步骤 N 步」:英文 n=1 要单数、中文两条同形;选哪条键只住这里,调用方只报数。 */
|
|
632
|
+
function stepsText(n, t) {
|
|
633
|
+
return t(n === 1 ? "detail.process.step" : "detail.process.steps", { n });
|
|
634
|
+
}
|
|
558
635
|
/** 「7分钟前」这类**不带谓语**的相对短语;`now` 桶不套模板——"刚刚前"不是话。 */
|
|
559
636
|
function agoText(at, now, t) {
|
|
560
637
|
const { unit, n } = relativeTime(at, now);
|
|
@@ -844,11 +921,11 @@ window.__ModuleLoader__.load({
|
|
|
844
921
|
return (0, react.useSyncExternalStore)(enabled ? visibleRows.subscribe : noopSubscribe, enabled ? visibleRows.get : getEmptyRowsSnapshot, enabled ? visibleRows.get : getEmptyRowsSnapshot);
|
|
845
922
|
}
|
|
846
923
|
/**
|
|
847
|
-
* 唯一的取数基础路径:宿主 `/api` +
|
|
924
|
+
* 唯一的取数基础路径:宿主 `/api` + 插件前缀。十段 exact Fetch 路由、客户端探测与附件 URL
|
|
848
925
|
* 都从它起;`SESSION_DELETE_API_PREFIX` 只作为段名的相对前缀与错误原文。
|
|
849
926
|
*/
|
|
850
927
|
const OFFICIAL_SESSION_DELETE_API_PREFIX = `/api/dsh-session-delete-api`;
|
|
851
|
-
/**
|
|
928
|
+
/** 十段段名(相对前缀)。**每项 `{ path, method }` 两键**:某段用什么方法也是线上形状的一部分
|
|
852
929
|
* (ADR-0003),家就在这一张表——路由记录(`src/api/route-table.ts`)逐行投影(`ROUTES.x.method`),
|
|
853
930
|
* 浏览器半的 GET 段同样投影、写段把 `ROUTES.x.method` 交给 `src/client/remote.ts` 的 `post()`,
|
|
854
931
|
* 两半都不逐段手写一份。
|
|
@@ -863,7 +940,8 @@ window.__ModuleLoader__.load({
|
|
|
863
940
|
* 这一段只负责"有多少、在哪、多大"。
|
|
864
941
|
* - `attachment`:附件原始字节(全仓唯一的二进制响应段;失败仍回 JSON 信封)。
|
|
865
942
|
* - `attachmentOrphans`:无引用图片对象的扫描报告(纯读;数量/字节 + 供预览的对象清单)。
|
|
866
|
-
* - `attachmentClean`:清掉报告那一刻无引用的图片对象(聚合回执 `AttachmentCleanupWire`)。
|
|
943
|
+
* - `attachmentClean`:清掉报告那一刻无引用的图片对象(聚合回执 `AttachmentCleanupWire`)。
|
|
944
|
+
* - `config`:三只可调项的读/写共用一段——空体读当前值,带字段写回 profile 的 patch 层。 */
|
|
867
945
|
const ROUTES = {
|
|
868
946
|
list: {
|
|
869
947
|
path: "/list",
|
|
@@ -874,6 +952,11 @@ window.__ModuleLoader__.load({
|
|
|
874
952
|
path: "/ping",
|
|
875
953
|
method: "GET"
|
|
876
954
|
},
|
|
955
|
+
/** 可调项:空体读(与 `/detail` 同法,用 POST 表达一个读);带字段写,写必须进到达序链。 */
|
|
956
|
+
config: {
|
|
957
|
+
path: "/config",
|
|
958
|
+
method: "POST"
|
|
959
|
+
},
|
|
877
960
|
/** 单条详情(读回收站目录里那条会话自己的日志)。 */
|
|
878
961
|
detail: {
|
|
879
962
|
path: "/detail",
|
|
@@ -910,6 +993,22 @@ window.__ModuleLoader__.load({
|
|
|
910
993
|
method: "POST"
|
|
911
994
|
}
|
|
912
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
|
+
};
|
|
913
1012
|
/** 请求参数名:浏览器半发、Node 半读,同名同义(`empty` 没有入参)。
|
|
914
1013
|
* 附件段是 **query 参数**(GET 二进制响应,没有请求体)。 */
|
|
915
1014
|
const PARAMS = {
|
|
@@ -984,6 +1083,38 @@ window.__ModuleLoader__.load({
|
|
|
984
1083
|
return encoded.replace(/~([0-9A-F]{4})/g, (_whole, hex) => String.fromCharCode(parseInt(hex, 16)));
|
|
985
1084
|
}
|
|
986
1085
|
//#endregion
|
|
1086
|
+
//#region src/client/api-base.ts
|
|
1087
|
+
/**
|
|
1088
|
+
* 取数路径的挂载点:官方 `/api` 前缀 + **当前文档所在的目录**。
|
|
1089
|
+
*
|
|
1090
|
+
* 为什么不能写死根绝对路径:宿主自己的客户端模块按「app-directory-relative form」下发 bundle 与
|
|
1091
|
+
* chunk 描述符,浏览器在**自己的挂载点**下解析(`dsh-client-modules` README 原文:*"which a browser
|
|
1092
|
+
* document resolves under its own mount"*);同一个部署可以被反代到 `/dsh/`,此时 `/api/...` 会打到
|
|
1093
|
+
* 反代根、而不是宿主。路由注册侧仍然必须写 `/api/...`(宿主 `connection.fetch.register` 用
|
|
1094
|
+
* `endpointFromPath('/api', path)` 校验),所以本文件只给**浏览器半**投影一层挂载点前缀。
|
|
1095
|
+
*
|
|
1096
|
+
* 根部署(`http://host/`)下 `mountPathOf` 返回空串,拼出来的 URL 与旧版逐字相同;只有挂载部署才
|
|
1097
|
+
* 多出 `/dsh` 这一段。目录取自 `new URL('.', baseURI)`:页面是 `/dsh/`、`/dsh/index.html` 都归到
|
|
1098
|
+
* `/dsh/`;`about:blank` 这类不透明 base 退回根路径(此时相对地址本来也无法成立)。
|
|
1099
|
+
*/
|
|
1100
|
+
/** `baseURI` 所在目录的路径,去掉尾斜杠;根路径回空串。 */
|
|
1101
|
+
function mountPathOf(baseURI) {
|
|
1102
|
+
try {
|
|
1103
|
+
const dir = new URL(".", baseURI).pathname;
|
|
1104
|
+
return dir === "/" ? "" : dir.replace(/\/$/, "");
|
|
1105
|
+
} catch {
|
|
1106
|
+
return "";
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
/** 一段相对段名 → 宿主 `/api` 前缀 + 挂载点。`path` 以 `/` 开头,与 `ROUTES` 的形状一致。 */
|
|
1110
|
+
function apiPathOf(path, baseURI) {
|
|
1111
|
+
return `${mountPathOf(baseURI)}${OFFICIAL_SESSION_DELETE_API_PREFIX}${path}`;
|
|
1112
|
+
}
|
|
1113
|
+
/** 当前文档的取数路径;Node 侧(无 `document`)退回根路径。 */
|
|
1114
|
+
function apiPath(path) {
|
|
1115
|
+
return apiPathOf(path, typeof document === "undefined" ? "http://localhost/" : document.baseURI);
|
|
1116
|
+
}
|
|
1117
|
+
//#endregion
|
|
987
1118
|
//#region src/client/receipt.ts
|
|
988
1119
|
/** 浏览器半自己的未分类诊断码(通道打不通 / 非信封 / 单条协议错)。它不过线,线上失败码仍归 `shared/wire.ts`。 */
|
|
989
1120
|
const CLIENT_UNKNOWN_CODE = "client/unknown";
|
|
@@ -1208,14 +1339,16 @@ window.__ModuleLoader__.load({
|
|
|
1208
1339
|
* 浏览器半的取数通道:同源 fetch 宿主 `connection.fetch` 的 exact Fetch 路由(`/api` + 插件前缀)。
|
|
1209
1340
|
* 信封与失败原文在这一层收口。
|
|
1210
1341
|
*
|
|
1211
|
-
* -
|
|
1342
|
+
* - 十段注册在宿主 `connection.fetch.register` 上,鉴权归宿主 `/api` bridge(ADR-0033);
|
|
1212
1343
|
* 没有旧前缀 fallback——支持面是 web UI 与桌面端,二者的标准组合都装 `client-connection`。
|
|
1213
1344
|
* 官方 remote.<namespace> 仍不可用(第三方拿不到 Typert 描述符的动态发现挂载点)。
|
|
1214
|
-
* -
|
|
1215
|
-
*
|
|
1216
|
-
*
|
|
1217
|
-
*
|
|
1218
|
-
*
|
|
1345
|
+
* - 十段的**段名与方法**住 `src/shared/wire.ts` 的结构化段名表(`ROUTES` 每项 `{ path, method }`),
|
|
1346
|
+
* 本文件只投影段名与方法(GET 段直接取 `ROUTES.x.method`,写段把它交给 `post()`)——方法字面量
|
|
1347
|
+
* 不在本文件出现第二份;取数地址(官方 `/api` 前缀 + 文档挂载点)归 `src/client/api-base.ts`
|
|
1348
|
+
* (根部署下与旧版逐字相同)。
|
|
1349
|
+
* 写动作统一走 apply;`attachment` 是唯一二进制段,由 `src/client/attachment-view.tsx` 经同一投影
|
|
1350
|
+
* 拼地址,不经过本文件的信封通道;`attachment-orphans`(纯读)与 `attachment-clean`(聚合写)是孤儿
|
|
1351
|
+
* 附件的两段。
|
|
1219
1352
|
* - 信封与官方 unary 面同形:{ok:true,value} / {ok:false,error}。HTTP 往返本身失败才 reject;
|
|
1220
1353
|
* 业务失败以信封返回,由 unwrapRemoteResult 在注入面抛出。
|
|
1221
1354
|
* - error 在线上是 {code,message,details?} 普通对象;判别依据 isDSHRemoteError 住在
|
|
@@ -1311,9 +1444,11 @@ window.__ModuleLoader__.load({
|
|
|
1311
1444
|
return body;
|
|
1312
1445
|
}
|
|
1313
1446
|
/**
|
|
1314
|
-
* exact Fetch 路由的 fetch
|
|
1315
|
-
*
|
|
1316
|
-
*
|
|
1447
|
+
* exact Fetch 路由的 fetch 实现。**同源、带挂载点前缀**:不拼 host、不认 origin——页面本身就是从
|
|
1448
|
+
* 宿主页面加载的,同源是这条通道的全部前提;路径只由 `api-base.ts` 从 `document.baseURI` 的目录
|
|
1449
|
+
* 与官方 `/api` 前缀拼出(根部署下与旧版逐字相同,反代到 `/dsh/` 时得到 `/dsh/api/...`)。
|
|
1450
|
+
* 鉴权归宿主 `/api` bridge(ADR-0033)。所以这条通道**只对**宿主页面打开过、带宿主身份的客户端
|
|
1451
|
+
* 成立(凭据是那次访问换来的 cookie,不是插件自己的秘密)。
|
|
1317
1452
|
*/
|
|
1318
1453
|
function createHttpRemote() {
|
|
1319
1454
|
const post = (route, body) => ({
|
|
@@ -1322,20 +1457,25 @@ window.__ModuleLoader__.load({
|
|
|
1322
1457
|
body: JSON.stringify(body)
|
|
1323
1458
|
});
|
|
1324
1459
|
return {
|
|
1325
|
-
list: () => requestEnvelope(
|
|
1326
|
-
ping: () => requestEnvelope(
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1460
|
+
list: () => requestEnvelope(apiPath(ROUTES.list.path), { method: ROUTES.list.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 })),
|
|
1467
|
+
orphans: () => requestEnvelope(apiPath(ROUTES.orphans.path), { method: ROUTES.orphans.method }),
|
|
1468
|
+
attachmentOrphans: () => requestEnvelope(apiPath(ROUTES.attachmentOrphans.path), { method: ROUTES.attachmentOrphans.method }),
|
|
1469
|
+
attachmentClean: () => requestEnvelope(apiPath(ROUTES.attachmentClean.path), post(ROUTES.attachmentClean, {})),
|
|
1470
|
+
detail: (entryId, before) => requestEnvelope(apiPath(ROUTES.detail.path), post(ROUTES.detail, {
|
|
1331
1471
|
[PARAMS.entryId]: entryId,
|
|
1332
1472
|
...before === void 0 ? {} : { [PARAMS.before]: before }
|
|
1333
1473
|
})),
|
|
1334
|
-
apply: (action, ids) => requestEnvelope(
|
|
1474
|
+
apply: (action, ids) => requestEnvelope(apiPath(ROUTES.apply.path), post(ROUTES.apply, {
|
|
1335
1475
|
[PARAMS.action]: action,
|
|
1336
1476
|
[PARAMS.ids]: [...ids]
|
|
1337
1477
|
})),
|
|
1338
|
-
empty: () => requestEnvelope(
|
|
1478
|
+
empty: () => requestEnvelope(apiPath(ROUTES.empty.path), post(ROUTES.empty, {}))
|
|
1339
1479
|
};
|
|
1340
1480
|
}
|
|
1341
1481
|
/**
|
|
@@ -1344,13 +1484,29 @@ window.__ModuleLoader__.load({
|
|
|
1344
1484
|
* - 401/403 是宿主 `/api` 的纯文本身份/归属失败:入口不出现,不装禁用态。
|
|
1345
1485
|
* - 失败信封 / 404 / 非信封同样按不可用处理:`/api` 网关对未知路径也可能回错误形状,
|
|
1346
1486
|
* 不能据此把入口摆出来让每个动作都失败。
|
|
1487
|
+
* - **往返有 5s 上限**:半开连接或宿主没起来时 `activate` 不悬着;超时与网络失败同样是
|
|
1488
|
+
* 「这一代不可用」,不自动重试(一次探测一个结论,刷新页面才会再探)。三种失败都在
|
|
1489
|
+
* console 留下原因——入口不出现是有意的,但"为什么没出现"不能只活在脑子里。
|
|
1347
1490
|
*/
|
|
1348
|
-
async function probeChannel(raw) {
|
|
1491
|
+
async function probeChannel(raw, options = {}) {
|
|
1492
|
+
const controller = new AbortController();
|
|
1493
|
+
const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? 5e3);
|
|
1349
1494
|
try {
|
|
1350
|
-
const result = await raw.ping();
|
|
1351
|
-
|
|
1352
|
-
|
|
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);
|
|
1353
1507
|
return false;
|
|
1508
|
+
} finally {
|
|
1509
|
+
clearTimeout(timer);
|
|
1354
1510
|
}
|
|
1355
1511
|
}
|
|
1356
1512
|
/**
|
|
@@ -1389,6 +1545,8 @@ window.__ModuleLoader__.load({
|
|
|
1389
1545
|
};
|
|
1390
1546
|
return {
|
|
1391
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)),
|
|
1392
1550
|
scanOrphans: () => raw.orphans().then((result) => unwrapRemoteResult(result)),
|
|
1393
1551
|
scanAttachmentOrphans: () => raw.attachmentOrphans().then((result) => unwrapRemoteResult(result)),
|
|
1394
1552
|
cleanAttachmentOrphans: () => raw.attachmentClean().then((result) => unwrapRemoteResult(result)),
|
|
@@ -1425,6 +1583,9 @@ window.__ModuleLoader__.load({
|
|
|
1425
1583
|
function getPicks() {
|
|
1426
1584
|
return getSelection().picks;
|
|
1427
1585
|
}
|
|
1586
|
+
function isSelecting() {
|
|
1587
|
+
return getSelection().selecting;
|
|
1588
|
+
}
|
|
1428
1589
|
/** 供组件订阅:模式与名单是同一次交互的两半,读它们的组件订同一份快照。 */
|
|
1429
1590
|
function useSelection() {
|
|
1430
1591
|
return selectionStore.use();
|
|
@@ -1581,18 +1742,6 @@ window.__ModuleLoader__.load({
|
|
|
1581
1742
|
]
|
|
1582
1743
|
});
|
|
1583
1744
|
}
|
|
1584
|
-
function IconFolderOpenOutlineRegular({ size }) {
|
|
1585
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(IconFrame, {
|
|
1586
|
-
size,
|
|
1587
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
1588
|
-
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",
|
|
1589
|
-
stroke: "currentColor"
|
|
1590
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
1591
|
-
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",
|
|
1592
|
-
stroke: "currentColor"
|
|
1593
|
-
})]
|
|
1594
|
-
});
|
|
1595
|
-
}
|
|
1596
1745
|
function IconCloseOutlineRegular({ size }) {
|
|
1597
1746
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(IconFrame, {
|
|
1598
1747
|
size,
|
|
@@ -1628,7 +1777,51 @@ window.__ModuleLoader__.load({
|
|
|
1628
1777
|
})
|
|
1629
1778
|
});
|
|
1630
1779
|
}
|
|
1631
|
-
/**
|
|
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` 逐字)。 */
|
|
1632
1825
|
function IconChevronDownOutlineRegular({ size }) {
|
|
1633
1826
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconFrame, {
|
|
1634
1827
|
size,
|
|
@@ -1638,7 +1831,7 @@ window.__ModuleLoader__.load({
|
|
|
1638
1831
|
})
|
|
1639
1832
|
});
|
|
1640
1833
|
}
|
|
1641
|
-
/**
|
|
1834
|
+
/** 折叠头与子会话导航行「已折叠 / 前进」那一枚(官方 `IconChevronRightOutlineRegular`)。 */
|
|
1642
1835
|
function IconChevronRightOutlineRegular({ size }) {
|
|
1643
1836
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconFrame, {
|
|
1644
1837
|
size,
|
|
@@ -1648,17 +1841,68 @@ window.__ModuleLoader__.load({
|
|
|
1648
1841
|
})
|
|
1649
1842
|
});
|
|
1650
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
|
+
}
|
|
1651
1889
|
//#endregion
|
|
1652
1890
|
//#region src/client/ui/ui.css?dsh-css-text
|
|
1653
|
-
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 * 选中行同源(`--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 * **另有一处已知差异不住在本文件**:回收站组头那颗按钮的整套 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/* ────────────────────────── Button ────────────────────────── */\r\n\r\n.sd-button {\r\n box-sizing: border-box;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n gap: 4px;\r\n border: none;\r\n border-radius: var(--dsw-radius-md);\r\n cursor: pointer;\r\n font-size: 14px;\r\n line-height: 22px;\r\n color: var(--dsw-alias-label-primary);\r\n background: transparent;\r\n padding: 0 14px;\r\n}\r\n\r\n.sd-button:disabled {\r\n cursor: not-allowed;\r\n opacity: 0.4;\r\n}\r\n\r\n.sd-button-md {\r\n height: 36px;\r\n}\r\n\r\n/* 紧凑档:尺寸不随填充/描边换档(官方原文的注释即此意)。 */\r\n.sd-button-sm {\r\n height: 28px;\r\n font-size: 12px;\r\n line-height: 18px;\r\n padding: 0 10px;\r\n border-radius: var(--dsw-radius-sm);\r\n}\r\n\r\n.sd-button-primary {\r\n background: var(--dsw-alias-button-primary-fill);\r\n color: var(--dsw-alias-label-primary-foreground);\r\n}\r\n\r\n.sd-button-primary:hover:not(:disabled) {\r\n background: var(--dsw-alias-button-primary-hover);\r\n}\r\n\r\n.sd-button-ghost:hover:not(:disabled) {\r\n background: var(--dsw-alias-interactive-bg-hover);\r\n}\r\n\r\n.sd-button-ghost:active:not(:disabled) {\r\n background: var(--dsw-alias-interactive-bg-active);\r\n}\r\n\r\n.sd-button-outline {\r\n border: 0.5px solid var(--dsw-alias-border-l3);\r\n background: transparent;\r\n}\r\n\r\n.sd-button-outline:hover:not(:disabled) {\r\n background: var(--dsw-alias-interactive-bg-hover);\r\n}\r\n\r\n.sd-button-icon {\r\n display: inline-flex;\r\n width: 16px;\r\n height: 16px;\r\n align-items: center;\r\n justify-content: center;\r\n}\r\n\r\n/* ────────────────────────── Tag ────────────────────────── */\r\n\r\n/* 胶囊几何固定:一个 tag 在哪里都是同一个尺寸,只有色板随 tone 走。 */\r\n.sd-tag {\r\n display: inline-flex;\r\n align-items: center;\r\n border-radius: 999px;\r\n corner-shape: round;\r\n padding: 1px 8px;\r\n font-size: 11px;\r\n line-height: 17px;\r\n font-weight: 500;\r\n white-space: nowrap;\r\n}\r\n\r\n/* 状态色自带填充:底色由自身颜色 10% 混出,换色板时底与字一起走,不需要第二个 token。 */\r\n.sd-tag-danger {\r\n background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);\r\n color: var(--dsw-alias-state-error-primary);\r\n}\r\n\r\n/* ────────────────────────── Modal ────────────────────────── */\r\n\r\n/* 整视口层(figma Mask + Dialog):蒙层 + 居中卡片。四边留 24px,上下按外壳公布的浮层内缩加宽,\r\n 高卡片才不会顶到外壳。卡片按这个内边距盒取尺寸:消费方用 `max-height:100%` 收口,不自己算视口。 */\r\n.sd-modal-root {\r\n pointer-events: auto;\r\n position: fixed;\r\n inset: 0;\r\n z-index: 1000;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n padding: max(24px, var(--dsh-frame-overlay-top, 24px)) 24px;\r\n}\r\n\r\n/* 只有绘制避让原生标题栏;根仍然挡住背景点击。 */\r\n.sd-modal-mask {\r\n position: absolute;\r\n inset: var(--dsh-frame-chrome-top, 0px) 0 0;\r\n backdrop-filter: var(--dsw-mask-blur);\r\n}\r\n\r\n.sd-modal-mask::after {\r\n content: '';\r\n position: absolute;\r\n inset: 0;\r\n background: var(--dsw-alias-bg-mask-1);\r\n animation: sd-modal-enter var(--ds-transition-duration) var(--ds-ease-in-out);\r\n}\r\n\r\n.sd-modal-dialog {\r\n box-sizing: border-box;\r\n position: relative;\r\n z-index: 1;\r\n display: flex;\r\n flex-direction: column;\r\n gap: 20px;\r\n width: min(380px, 100%);\r\n padding: 0 0 24px;\r\n overflow: hidden;\r\n border: 0;\r\n border-radius: var(--dsw-radius-panel);\r\n background: var(--dsw-alias-bg-layer-2);\r\n box-shadow: var(--dsw-elevation-prominent);\r\n animation: sd-modal-enter var(--ds-transition-duration) var(--ds-ease-in-out);\r\n}\r\n\r\n@keyframes sd-modal-enter {\r\n from { opacity: 0; }\r\n to { opacity: 1; }\r\n}\r\n\r\n@media (prefers-reduced-motion: reduce) {\r\n .sd-modal-mask::after,\r\n .sd-modal-dialog { animation: none; }\r\n}\r\n\r\n.sd-modal-dialog:focus {\r\n outline: none;\r\n}\r\n\r\n.sd-modal-content {\r\n display: flex;\r\n flex-direction: column;\r\n width: 100%;\r\n}\r\n\r\n/* 头行:标题左、关闭钮右。 */\r\n.sd-modal-header {\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n gap: 8px;\r\n padding: 22px 14px 12px 24px;\r\n}\r\n\r\n.sd-modal-title {\r\n margin: 0;\r\n font-size: 16px;\r\n line-height: 24px;\r\n font-weight: 500;\r\n color: var(--dsw-alias-label-primary);\r\n}\r\n\r\n.sd-modal-close {\r\n flex: none;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n width: 28px;\r\n height: 28px;\r\n border: none;\r\n border-radius: var(--dsw-radius-sm);\r\n background: transparent;\r\n cursor: pointer;\r\n color: var(--dsw-alias-label-secondary);\r\n}\r\n\r\n.sd-modal-close:hover {\r\n background: var(--dsw-alias-interactive-bg-hover);\r\n}\r\n\r\n.sd-modal-body {\r\n display: flex;\r\n flex-direction: column;\r\n min-width: 0;\r\n margin-top: 20px;\r\n padding: 0 24px;\r\n}\r\n\r\n.sd-modal-footer {\r\n display: flex;\r\n align-items: center;\r\n justify-content: flex-end;\r\n gap: 8px;\r\n padding: 0 24px;\r\n}\r\n\r\n/* ────────────────────────── Tooltip ────────────────────────── */\r\n\r\n.sd-tooltip-bubble {\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 8px;\r\n position: fixed;\r\n z-index: 100;\r\n /* 定宽收缩只按 `left` 到视口右缘量,靠右的锚点会提前折行;\r\n `max-content` 只按文案定宽,再用半个视口收口。 */\r\n width: max-content;\r\n max-width: 50vw;\r\n padding: 3px 7px;\r\n border-radius: var(--dsw-radius-sm);\r\n background: var(--dsw-alias-tooltip-bg);\r\n color: var(--dsw-static-neutral-bluish-00);\r\n font-size: 13px;\r\n line-height: 20px;\r\n white-space: pre-line;\r\n /* 不可断的长 token(URL、路径)不许顶破 max-width。 */\r\n overflow-wrap: break-word;\r\n pointer-events: none;\r\n animation: sd-tooltip-in 150ms var(--ds-ease-in-out);\r\n}\r\n\r\n.sd-tooltip-bubble[data-portal] { z-index: 1100; }\r\n.sd-tooltip-label { min-width: 0; }\r\n\r\n.sd-tooltip-bubble[data-side='right'] {\r\n transform: translateY(-50%);\r\n}\r\n\r\n.sd-tooltip-bubble[data-side='bottom'] {\r\n transform: translateX(-50%);\r\n}\r\n\r\n.sd-tooltip-bubble[data-side='top'] {\r\n transform: translate(-50%, -100%);\r\n}\r\n\r\n@keyframes sd-tooltip-in {\r\n from { opacity: 0; }\r\n}\r\n\r\n@media (prefers-reduced-motion: reduce) {\r\n .sd-tooltip-bubble {\r\n animation: none;\r\n }\r\n}\r\n\r\n/* ────────────────────────── Toast ────────────────────────── */\r\n\r\n/* 淡出延迟由组件以 `--dsh-toast-hold` 给出 ⇒ 同一个值同时驱动卸载计时器与这段动画,\r\n 两者不可能再对不上。淡出**时长**仍需与组件里的 FADE_MS 一致(没有 owner 会改它)。 */\r\n.sd-toast {\r\n position: fixed;\r\n top: 40px;\r\n left: 50%;\r\n /* 高过图片灯箱底层的 1000:预览开着时报错也必须读得到。 */\r\n z-index: 1100;\r\n /* 通告不截点击。 */\r\n pointer-events: none;\r\n display: flex;\r\n align-items: center;\r\n gap: 10px;\r\n /* 定宽盒设了 `left` 只按右侧空间收缩(-50% 的位移发生在定尺之后)⇒ 靠窗边的横幅会提前折行。\r\n `max-content` 只按文案定宽,再由下面的 max-width 收口。 */\r\n width: max-content;\r\n max-width: min(640px, calc(100vw - 48px));\r\n padding: 12px 16px;\r\n border-radius: var(--dsw-radius-lg);\r\n /* 设计平台的 toast 表面在**两套主题下都是深的**(浅色 bluish-800 / 深色 bluish-750),\r\n 所以这里的字色与动作色是固定值,不随主题翻转。 */\r\n background: var(--dsw-alias-toast-bg);\r\n color: var(--dsw-alias-toast-label);\r\n font-size: 14px;\r\n line-height: 22px;\r\n box-shadow: var(--dsw-shadow-lv3);\r\n transform: translateX(-50%);\r\n animation:\r\n sd-toast-in 160ms ease-out,\r\n sd-toast-fade 1000ms ease var(--dsh-toast-hold, 3000ms) forwards;\r\n}\r\n\r\n.sd-toast-icon {\r\n display: grid;\r\n place-items: center;\r\n flex: none;\r\n color: var(--dsw-alias-state-warn-label);\r\n}\r\n\r\n.sd-toast-icon-success {\r\n color: var(--dsw-alias-state-success-primary);\r\n}\r\n\r\n.sd-toast-text {\r\n min-width: 0;\r\n}\r\n\r\n.sd-toast-action {\r\n display: inline;\r\n padding: 0;\r\n border: 0;\r\n background: none;\r\n color: var(--dsw-static-deepseek-400);\r\n font: inherit;\r\n cursor: pointer;\r\n /* 动作文字与周围文案之间的呼吸位。 */\r\n margin-inline: 3px;\r\n /* 横幅表面整体是穿透点击的,只有动作文字接指针。 */\r\n pointer-events: auto;\r\n}\r\n\r\n.sd-toast-action:hover {\r\n opacity: 0.8;\r\n}\r\n\r\n/* 本插件自造的第七条(不是抄本,见文件头点名):撤销横幅在飞时那颗动作要**看着就按不动**。\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(--dsw-radius-md);\r\n background: transparent;\r\n cursor: pointer;\r\n font-size: 13px;\r\n line-height: 20px;\r\n color: var(--dsw-alias-label-primary);\r\n text-align: left;\r\n}\r\n\r\n.sd-menu-item:hover:not(:disabled) {\r\n background: var(--dsw-alias-interactive-bg-hover);\r\n}\r\n\r\n/* 方向键移动的是真焦点 ⇒ 键盘所在的那一行拿指针同款填充;填充就是这一行的焦点呈现,\r\n 浏览器默认的 ring 再叠一层就重了。 */\r\n.sd-menu-item:focus-visible:not(:disabled) {\r\n background: var(--dsw-alias-interactive-bg-hover);\r\n outline: none;\r\n}\r\n\r\n.sd-menu-item:disabled {\r\n opacity: 0.4;\r\n cursor: not-allowed;\r\n}\r\n\r\n.sd-menu-item-icon {\r\n display: inline-flex;\r\n flex: none;\r\n width: 14px;\r\n height: 14px;\r\n align-items: center;\r\n justify-content: center;\r\n color: var(--dsw-alias-menu-icon);\r\n}\r\n\r\n.sd-menu-item-icon svg {\r\n width: 14px;\r\n height: 14px;\r\n}\r\n\r\n.sd-menu-item-label {\r\n flex: 1;\r\n min-width: 0;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n}\r\n\r\n/* 破坏性行:error 字/图标 + danger 悬停底。 */\r\n.sd-menu-item-danger {\r\n color: var(--dsw-alias-state-error-primary);\r\n}\r\n\r\n.sd-menu-item-danger .sd-menu-item-icon {\r\n color: var(--dsw-alias-state-error-primary);\r\n}\r\n\r\n.sd-menu-item-danger:hover:not(:disabled) {\r\n background: var(--dsw-alias-interactive-bg-hover-danger);\r\n}\r\n\r\n.sd-menu-item-danger:focus-visible:not(:disabled) {\r\n background: var(--dsw-alias-interactive-bg-hover-danger);\r\n outline: none;\r\n}\r\n\r\n/* 分隔格:发丝线上下的间距。取 l2 而不是 l1——l1 在这个表面上几乎看不见。 */\r\n.sd-menu-separator {\r\n height: 0.5px;\r\n margin: 3px 2px;\r\n background: var(--dsw-alias-border-l2);\r\n}\r\n\r\n/* ────────────────────────── Checkbox ────────────────────────── */\r\n/* 抄 `lib/Checkbox.module.css` 全部四条声明,逐字。勾选框本体是**原生 input** + `accent-color`,\r\n 不是自绘的圆框——回收站多选态用它(清单行左侧那一格)。 */\r\n\r\n.sd-checkbox {\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 6px;\r\n font-size: 14px;\r\n line-height: 20px;\r\n color: var(--dsw-alias-label-primary);\r\n cursor: pointer;\r\n}\r\n\r\n.sd-checkbox input {\r\n flex: 0 0 auto;\r\n width: 16px;\r\n height: 16px;\r\n margin: 0;\r\n accent-color: var(--dsw-alias-brand-primary);\r\n cursor: inherit;\r\n}\r\n\r\n.sd-checkbox input:focus-visible {\r\n outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));\r\n outline-offset: 2px;\r\n}\r\n\r\n.sd-checkbox:has(input:disabled) {\r\n cursor: default;\r\n opacity: 0.5;\r\n}\r\n\r\n/* ────────────────────────── DisclosureRow(分组折叠头) ────────────────────────── */\r\n/* 抄 `lib/DisclosureRow.module.css` 的 `.root`/`.row`/`.leading`/`.title` 与那两枚 glyph 的\r\n 显隐。没抄进来的是 `StateDot`(`svg[data-state]`)那一档——它说\"这是状态标记不是文字装饰\",\r\n 我们的组头没有状态点。行高与 leading 盒走 `--dsh-content-font-delta` 那根轴,\r\n 标题字号走 secondary 档:宿主的\"设置里调正文字号\"会一起推着它走。 */\r\n\r\n.sd-disclosure {\r\n display: flex;\r\n flex-direction: column;\r\n width: 100%;\r\n min-width: 0;\r\n}\r\n\r\n.sd-disclosure-row {\r\n position: relative;\r\n overflow: hidden;\r\n display: flex;\r\n align-items: center;\r\n height: calc(24px + var(--dsh-content-font-delta, 0px));\r\n min-width: 0;\r\n color: var(--dsw-alias-label-tertiary);\r\n transition: color 100ms ease;\r\n}\r\n\r\n.sd-disclosure-row[data-expandable] {\r\n cursor: pointer;\r\n}\r\n\r\n.sd-disclosure-row:hover {\r\n color: var(--dsw-alias-label-secondary);\r\n}\r\n\r\n.sd-disclosure-leading {\r\n position: relative;\r\n flex: none;\r\n width: calc(16px + var(--dsh-content-font-delta, 0px));\r\n height: calc(16px + var(--dsh-content-font-delta, 0px));\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n margin-right: 6px;\r\n padding: 0;\r\n border: none;\r\n background: none;\r\n color: inherit;\r\n}\r\n\r\n.sd-disclosure-leading svg:not([data-state]) {\r\n width: calc(14px + var(--dsh-content-font-delta, 0px));\r\n height: calc(14px + var(--dsh-content-font-delta, 0px));\r\n}\r\n\r\nbutton.sd-disclosure-leading {\r\n cursor: pointer;\r\n}\r\n\r\n.sd-disclosure-title {\r\n flex: none;\r\n font-size: var(--dsh-content-font-size-secondary, 13px);\r\n line-height: calc(24px + var(--dsh-content-font-delta, 0px));\r\n color: inherit;\r\n}\r\n\r\n@media (prefers-reduced-motion: reduce) {\r\n .sd-disclosure-row {\r\n transition: none;\r\n }\r\n}\r\n\r\n/* ────────────────────────── 宽度把手(可拖的分栏线) ────────────────────────── */\r\n/* 抄 `ConversationRoot.module.css` 的 `.widthHandle` 一族:10px 的命中区、`col-resize` 光标、\r\n 平时不画、悬停或拖动时才浮出一条 2px 的 `--dsw-alias-scrollbar-bg-l1` 短标(那条渐变里\r\n `--dsh-width-handle-pointer-y` 的兜底是 50%,宿主靠 JS 跟着指针走;我们不跟,\r\n 停在中间——少一条 pointermove 上的写字,视觉上是同一族东西)。\r\n 与宿主的差别只有位置:它绝对定位在会话栏两侧,我们这根住在两栏之间的那一格 flex 位里,\r\n 所以 `top/bottom/position` 那三样交给内联布局,这里只留视觉与光标。 */\r\n\r\n.sd-width-handle {\r\n position: relative;\r\n z-index: 0;\r\n cursor: col-resize;\r\n align-self: stretch;\r\n flex: 0 0 10px;\r\n width: 10px;\r\n padding: 0;\r\n border: none;\r\n background: none;\r\n}\r\n\r\n.sd-width-handle::after {\r\n content: \"\";\r\n background: linear-gradient(\r\n to bottom,\r\n transparent calc(var(--dsh-width-handle-pointer-y, 50%) - 36px),\r\n var(--dsw-alias-scrollbar-bg-l1) calc(var(--dsh-width-handle-pointer-y, 50%) - 8px),\r\n var(--dsw-alias-scrollbar-bg-l1) calc(var(--dsh-width-handle-pointer-y, 50%) + 8px),\r\n transparent calc(var(--dsh-width-handle-pointer-y, 50%) + 36px)\r\n );\r\n opacity: 0;\r\n pointer-events: none;\r\n border-radius: 2px;\r\n width: 2px;\r\n position: absolute;\r\n top: 0;\r\n bottom: 0;\r\n left: 4px;\r\n}\r\n\r\n.sd-width-handle:hover::after,\r\n.sd-width-handle[data-dragging]::after {\r\n opacity: 1;\r\n}\r\n\r\n.sd-width-handle:focus-visible {\r\n outline: none;\r\n}\r\n\r\n.sd-width-handle:focus-visible::after {\r\n opacity: 1;\r\n}\r\n\r\n/* 本插件自造的第一条(不是抄本,见文件头点名):拖动期间整块面板别把文字选上了。 */\r\n.sd-resizing {\r\n user-select: none;\r\n cursor: col-resize;\r\n}\r\n\r\n/* 本插件自造的第二条(不是抄本,见文件头点名):清单行是 `role=\"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(--dsw-alias-button-ghost-active-border);\r\n outline-offset: 1px;\r\n}\r\n\r\n/* 会话行尾悬停排那颗**兜底**勾选开关(状态点行 / 归档行;行首主勾选是原生 Checkbox)。\r\n 基底**逐字抄** `dsh-client-ui-workspace` 的 `Rows.module.css` 的 `.iconButton` / `.iconButton:hover`,\r\n 只改类名——兜底开关要跟同排宿主自己的归档/置顶同形,尺寸与色档都不自己发明。 */\r\n.sd-rowaction-button {\r\n border-radius: var(--dsw-radius-xs);\r\n cursor: pointer;\r\n width: 16px;\r\n height: 16px;\r\n color: var(--dsw-alias-label-tertiary);\r\n background: 0 0;\r\n border: none;\r\n flex: none;\r\n justify-content: center;\r\n align-items: center;\r\n padding: 0;\r\n display: inline-flex;\r\n}\r\n.sd-rowaction-button:hover {\r\n color: var(--dsw-alias-label-primary);\r\n}\r\n\r\n/* 本插件自造的第三条(不是抄本,见文件头点名):官方那颗只有常态与 hover,勾选开关还要有\r\n 「已按下」与「这条删不动」两档。按下档**复用回收站选中行那两枚** `ghost-active-*`,不新造颜色;\r\n 禁用档走 opacity。 */\r\n.sd-rowaction-button[aria-pressed='true'] {\r\n background: var(--dsw-alias-button-ghost-active-fill);\r\n box-shadow: inset 0 0 0 1px var(--dsw-alias-button-ghost-active-border);\r\n color: var(--dsw-alias-label-primary);\r\n}\r\n.sd-rowaction-button:disabled {\r\n opacity: 0.4;\r\n cursor: not-allowed;\r\n}\r\n\r\n/* 本插件自造的第⑥条(不是抄本,见文件头点名):行首 16px 格里的勾选框(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(--dsw-alias-button-ghost-active-fill);\r\n box-shadow: inset 0 0 0 0.5px var(--dsw-alias-button-ghost-active-border);\r\n}\r\n[data-row-key^=\"session:\"]:has([data-dsh-session-delete=\"batch-pick\"][data-seat=\"leading\"]) .sd-batch-pick-action {\r\n display: none;\r\n}\r\n\r\n/* 本插件自造的第四条(不是抄本,见文件头点名):批量删除的浮条。宿主没有\"浮条\"这件控件,形状照\r\n 本插件自己的浮层口径来——固定底部居中,表面/描边/圆角/高程四件全走 token(与单删那张目标卡同源)。\r\n **层级必须低于确认框(`.sd-modal-root` 1000)**:2026-10-17 真机发现浮条在 1100 时会盖住确认框、\r\n 还能抢它的按钮;撤销横幅与 Tooltip 是瞬时通告、保持 1100。列方向:清单在上、操作行在下;\r\n 清单限高可滚,默认收起(见 `batch-bar.tsx`)。**宽度按内容展开**(`max-content`,最多视口减 48):\r\n 只写 `left:50% + width:auto` 时 shrink-to-fit 只拿左半屏,新增「全选/取消全选」后会把「退出多选」\r\n 挤到第二行;窄到 max-width 还放不下才由 `.sd-batch-bar-main` 的 `flex-wrap` 兜底。 */\r\n.sd-batch-bar {\r\n position: fixed;\r\n bottom: 24px;\r\n left: 50%;\r\n transform: translateX(-50%);\r\n z-index: 900;\r\n width: max-content;\r\n max-width: calc(100vw - 48px);\r\n display: flex;\r\n flex-direction: column;\r\n align-items: stretch;\r\n gap: 8px;\r\n padding: 8px 12px;\r\n background: var(--dsw-alias-bg-layer-2);\r\n border: 0.5px solid var(--dsw-alias-border-l4);\r\n border-radius: var(--dsw-radius-lg);\r\n box-shadow: var(--dsw-elevation-prominent);\r\n font-size: 12px;\r\n line-height: 18px;\r\n}\r\n\r\n.sd-batch-bar-main {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n flex-wrap: wrap;\r\n}\r\n\r\n.sd-batch-bar-hint {\r\n color: var(--dsw-alias-label-tertiary);\r\n}\r\n\r\n.sd-batch-bar-list-head {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n flex-wrap: wrap;\r\n}\r\n\r\n/* 汇总的第二句(含 K 个子会话)比条数轻一档,和列表行里的 chip 同级。 */\r\n.sd-batch-bar-list-head span + span {\r\n color: var(--dsw-alias-label-tertiary);\r\n}\r\n\r\n.sd-batch-bar-list {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 2px;\r\n max-height: 240px;\r\n overflow: auto;\r\n margin: 0;\r\n padding: 0 0 8px;\r\n border-bottom: 0.5px solid var(--dsw-alias-border-l4);\r\n list-style: none;\r\n}\r\n\r\n.sd-batch-bar-item {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n min-width: 0;\r\n}\r\n\r\n.sd-batch-bar-title {\r\n flex: 1;\r\n min-width: 0;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n color: var(--dsw-alias-label-secondary);\r\n}\r\n\r\n.sd-batch-bar-meta {\r\n flex: none;\r\n color: var(--dsw-alias-label-tertiary);\r\n}\r\n\r\n.sd-batch-bar-remove {\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n flex: none;\r\n width: 16px;\r\n height: 16px;\r\n padding: 0;\r\n border: none;\r\n border-radius: var(--dsw-radius-xs);\r\n background: 0 0;\r\n color: var(--dsw-alias-label-tertiary);\r\n cursor: pointer;\r\n}\r\n\r\n.sd-batch-bar-remove:hover {\r\n color: var(--dsw-alias-label-primary);\r\n background: var(--dsw-alias-interactive-bg-hover);\r\n}\r\n";
|
|
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";
|
|
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";
|
|
1654
1898
|
//#endregion
|
|
1655
1899
|
//#region src/client/ui/markdown.css?dsh-css-text
|
|
1656
|
-
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 *
|
|
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";
|
|
1657
1901
|
//#endregion
|
|
1658
1902
|
//#region src/client/ui/styles.ts
|
|
1659
1903
|
/**
|
|
1660
|
-
* 本插件控件的样式装载:把 `ui.css`
|
|
1661
|
-
*
|
|
1904
|
+
* 本插件控件的样式装载:把 `ui.css` / `markdown.css` / `forms.css` / `layout.css` 的文本挂成一个 `<style>`,
|
|
1905
|
+
* **每个 ui 组件 import 本模块**(副作用导入),所以「组件在屏幕上」必然等于「样式在场」。
|
|
1662
1906
|
*
|
|
1663
1907
|
* 为什么是往 DOM 里注 `<style>`、而不是构建期抽成 `.css` 资产:
|
|
1664
1908
|
* 宿主对客户端插件的样式**本来就有这条一等公民通道**——浏览器半的模块表在**工厂物化**
|
|
@@ -1686,7 +1930,7 @@ window.__ModuleLoader__.load({
|
|
|
1686
1930
|
if (head.querySelector(`style[data-dsh-session-delete="${STYLE_MARKER}"]`) !== null) return;
|
|
1687
1931
|
const element = document.createElement("style");
|
|
1688
1932
|
element.setAttribute("data-dsh-session-delete", STYLE_MARKER);
|
|
1689
|
-
element.textContent = `${ui_default}\n${markdown_default}`;
|
|
1933
|
+
element.textContent = `${ui_default}\n${markdown_default}\n${forms_default}\n${layout_default}`;
|
|
1690
1934
|
head.appendChild(element);
|
|
1691
1935
|
}
|
|
1692
1936
|
install();
|
|
@@ -1698,15 +1942,27 @@ window.__ModuleLoader__.load({
|
|
|
1698
1942
|
* `pointerdown` 置真、`keydown` 置假)。官方还会据此发布 `data-input-modality` 去控制焦点环的
|
|
1699
1943
|
* 显隐,**那件事不复制**:宿主自己那份 primitives 已经在同一份文档上发布了它,我们再来一份就是
|
|
1700
1944
|
* 两个写入者抢同一个属性。
|
|
1945
|
+
*
|
|
1946
|
+
* 监听器归 `apply` 的 effect(`src/client/index.tsx` 的 `installPointerModality`):用户在
|
|
1947
|
+
* 卸载 / HMR 换代时要把它们摘掉,模块作用域装上就没人负责收了。样式本身仍是模块作用域——
|
|
1948
|
+
* 宿主在工厂物化时认领 `<style>` 并在条目卸载时统一摘除,这是它给插件 UI 的正式通道。
|
|
1701
1949
|
*/
|
|
1702
1950
|
let pointerModality = false;
|
|
1703
|
-
|
|
1704
|
-
|
|
1951
|
+
/** 装上两个捕获阶段的监听器,返回摘除函数。重复调用会装两套,所以调用点只该是 `apply` 的一个 effect。 */
|
|
1952
|
+
function installPointerModality() {
|
|
1953
|
+
if (typeof window === "undefined") return () => void 0;
|
|
1954
|
+
const onPointerDown = () => {
|
|
1705
1955
|
pointerModality = true;
|
|
1706
|
-
}
|
|
1707
|
-
|
|
1956
|
+
};
|
|
1957
|
+
const onKeyDown = () => {
|
|
1708
1958
|
pointerModality = false;
|
|
1709
|
-
}
|
|
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
|
+
};
|
|
1710
1966
|
}
|
|
1711
1967
|
/** 最近一次输入是否来自指针(仅本插件自用)。 */
|
|
1712
1968
|
function isPointerModality() {
|
|
@@ -2657,8 +2913,8 @@ window.__ModuleLoader__.load({
|
|
|
2657
2913
|
const TARGET_CARD = {
|
|
2658
2914
|
padding: "10px 12px",
|
|
2659
2915
|
background: "var(--dsw-alias-bg-layer-2)",
|
|
2660
|
-
border: "0.5px solid var(--
|
|
2661
|
-
borderRadius: "var(--
|
|
2916
|
+
border: "0.5px solid var(--sd-border-l4)",
|
|
2917
|
+
borderRadius: "var(--sd-radius-lg)"
|
|
2662
2918
|
};
|
|
2663
2919
|
const requestStore$1 = createStore(null);
|
|
2664
2920
|
/** 菜单行调用:请求删除这个会话(确认框由 overlay 上的宿主渲染)。 */
|
|
@@ -2840,7 +3096,7 @@ window.__ModuleLoader__.load({
|
|
|
2840
3096
|
const TARGET_META = {
|
|
2841
3097
|
fontSize: 12,
|
|
2842
3098
|
lineHeight: "18px",
|
|
2843
|
-
color: "var(--
|
|
3099
|
+
color: "var(--sd-label-tertiary)"
|
|
2844
3100
|
};
|
|
2845
3101
|
/**
|
|
2846
3102
|
* 目标卡要能装几十条:选中很多条时卡片不能把弹框撑出视口,标题与底部按钮必须保持可见。
|
|
@@ -3155,6 +3411,77 @@ window.__ModuleLoader__.load({
|
|
|
3155
3411
|
});
|
|
3156
3412
|
}
|
|
3157
3413
|
//#endregion
|
|
3414
|
+
//#region src/client/context-menu-actions.ts
|
|
3415
|
+
/**
|
|
3416
|
+
* 0.2.2+ 会话菜单的数据动作**定义**:只造贡献对象,不碰客户端注册表。
|
|
3417
|
+
* 注册仍统一住 `index.tsx`(客户端条目注册只住那里),这样旧宿主的 legacy 回退与
|
|
3418
|
+
* contextMenus 路径的互斥判断也只有一个地方。
|
|
3419
|
+
*
|
|
3420
|
+
* 为什么需要这一层:0.2.2 把内置会话动作迁到 contextMenus,legacy React 槽位
|
|
3421
|
+
* (`sidebar.workspaces.session.menu.item`) 的条目固定渲染在 `afterContent` 动作之前;Archive
|
|
3422
|
+
* 是 `afterContent: true` 的 footer 动作,于是 legacy 行会跑到 Archive 前面。把这两行也注册成
|
|
3423
|
+
* `afterContent` 数据动作、order 排在 Archive(600) 之后,才能恢复 0.2.1 的顺序:
|
|
3424
|
+
* Archive → 多选 → 删除。
|
|
3425
|
+
*
|
|
3426
|
+
* 数据动作没有 `disabledReason`(底层 MenuRow 有,但 MenuActionView 没透传),所以运行中禁用时
|
|
3427
|
+
* 改 label 为「删除(运行中)」;legacy 路径仍保留原来的 tooltip。
|
|
3428
|
+
*/
|
|
3429
|
+
const ICON_BATCH = "xrn1997.session-delete.batch-select";
|
|
3430
|
+
const ICON_DELETE = "xrn1997.session-delete.delete";
|
|
3431
|
+
const CONTRIBUTION_ID = "xrn1997.session-delete.session-menu";
|
|
3432
|
+
/** 运行中把 label 换成短句:数据动作没有 disabledReason,不能再用 tooltip 解释。 */
|
|
3433
|
+
function deleteLabel(t, live) {
|
|
3434
|
+
return live ? t("menu.deleteLive") : t("menu.delete");
|
|
3435
|
+
}
|
|
3436
|
+
/** 会话是否在跑;服务缺席或读数抛错都按「不知道」处理(Node 半仍会拒绝 live 删除)。 */
|
|
3437
|
+
function liveOf(sessionList, sessionId) {
|
|
3438
|
+
try {
|
|
3439
|
+
return sessionList?.getSnapshot().byId?.[sessionId]?.running === true;
|
|
3440
|
+
} catch {
|
|
3441
|
+
return false;
|
|
3442
|
+
}
|
|
3443
|
+
}
|
|
3444
|
+
/**
|
|
3445
|
+
* 造 0.2.2+ 的会话菜单贡献。order 700 落在内置 Archive(600) 之后;两行都是 `afterContent`,
|
|
3446
|
+
* 所以它们与 Archive 一起排掉 legacy 组件行,恢复「归档在多选/删除之前」。
|
|
3447
|
+
*/
|
|
3448
|
+
function createSessionMenuContribution(t, sessionList) {
|
|
3449
|
+
const resolve = (target) => {
|
|
3450
|
+
const live = liveOf(sessionList, target.sessionId);
|
|
3451
|
+
return [{
|
|
3452
|
+
id: "batch-select",
|
|
3453
|
+
label: isSelecting() ? t("trash.toolbar.selectExit") : t("trash.toolbar.select"),
|
|
3454
|
+
icon: { key: ICON_BATCH },
|
|
3455
|
+
separatorBefore: true,
|
|
3456
|
+
afterContent: true,
|
|
3457
|
+
run: () => {
|
|
3458
|
+
if (isSelecting()) exitSelection();
|
|
3459
|
+
else enterSelection();
|
|
3460
|
+
}
|
|
3461
|
+
}, {
|
|
3462
|
+
id: "delete",
|
|
3463
|
+
label: deleteLabel(t, live),
|
|
3464
|
+
disabled: live,
|
|
3465
|
+
icon: { key: ICON_DELETE },
|
|
3466
|
+
danger: true,
|
|
3467
|
+
afterContent: true,
|
|
3468
|
+
run: () => {
|
|
3469
|
+
requestDelete({
|
|
3470
|
+
sessionId: target.sessionId,
|
|
3471
|
+
title: target.displayTitle
|
|
3472
|
+
});
|
|
3473
|
+
}
|
|
3474
|
+
}];
|
|
3475
|
+
};
|
|
3476
|
+
return {
|
|
3477
|
+
id: CONTRIBUTION_ID,
|
|
3478
|
+
target: "session",
|
|
3479
|
+
order: 700,
|
|
3480
|
+
...sessionList === void 0 ? {} : { observe: () => [sessionList] },
|
|
3481
|
+
resolve
|
|
3482
|
+
};
|
|
3483
|
+
}
|
|
3484
|
+
//#endregion
|
|
3158
3485
|
//#region src/client/delete-menu-item.tsx
|
|
3159
3486
|
/**
|
|
3160
3487
|
* 会话条目「…」菜单里的「删除」行(docs/ui-spec.md 状态 ①)。
|
|
@@ -3216,22 +3543,46 @@ window.__ModuleLoader__.load({
|
|
|
3216
3543
|
minWidth: 0,
|
|
3217
3544
|
padding: "9px 14px",
|
|
3218
3545
|
background: "var(--dsw-alias-bg-layer-2)",
|
|
3219
|
-
border: "0.5px solid var(--
|
|
3220
|
-
borderRadius: "var(--
|
|
3546
|
+
border: "0.5px solid var(--sd-border-l4)",
|
|
3547
|
+
borderRadius: "var(--sd-radius-lg)"
|
|
3221
3548
|
};
|
|
3222
|
-
/**
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
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)"
|
|
3227
3575
|
};
|
|
3228
3576
|
/**
|
|
3229
3577
|
* **整页唯一那块实心红**的写法:`variant="primary"` + 在元素上覆盖 primary 那对填充 token。
|
|
3230
3578
|
* 宿主插件管理页的 danger 按钮就是这个写法(asar @21658274),所以 hover 仍走 `.primary:hover` 类。
|
|
3579
|
+
*
|
|
3580
|
+
* 覆盖的是**本插件别名层**(`--sd-*`,见 `ui.css` 头注)而不是宿主私有 token:别名在 `body` 上
|
|
3581
|
+
* 就把 `var(--dsw-…, 兜底)` 算完并继承下来了,元素上再覆盖底层私有 token 已经传不进继承值。
|
|
3231
3582
|
*/
|
|
3232
3583
|
const BULK_DANGER = {
|
|
3233
|
-
"--
|
|
3234
|
-
"--
|
|
3584
|
+
"--sd-button-primary-fill": "var(--dsw-alias-state-error-primary)",
|
|
3585
|
+
"--sd-button-primary-hover": "var(--dsw-alias-state-error-primary)"
|
|
3235
3586
|
};
|
|
3236
3587
|
/**
|
|
3237
3588
|
* 项目名怎么念:宿主的 `projectKey` 把 cwd 里的 `/`、`\`、`:` 全折成**一个** `-` ⇒ 分隔符那部分有损、
|
|
@@ -3243,128 +3594,447 @@ window.__ModuleLoader__.load({
|
|
|
3243
3594
|
const bare = unescapeProjectKey(projectDir.replace(/^--/, "").replace(/--$/, ""));
|
|
3244
3595
|
return bare === "" || bare === "_no-cwd" ? t("trash.project.unknown") : bare;
|
|
3245
3596
|
}
|
|
3246
|
-
/**
|
|
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
|
+
* 工作区已不存在的条目统一进「未分组」组,且该组永远沉底——它不是一个可比较的项目。 */
|
|
3247
3608
|
function groupByProject(entries, t) {
|
|
3248
3609
|
const byProject = /* @__PURE__ */ new Map();
|
|
3249
3610
|
for (const entry of [...entries].sort((a, b) => b.deletedAt - a.deletedAt)) {
|
|
3250
|
-
const
|
|
3251
|
-
|
|
3611
|
+
const key = groupKeyOf(entry);
|
|
3612
|
+
const list = byProject.get(key);
|
|
3613
|
+
if (list === void 0) byProject.set(key, [entry]);
|
|
3252
3614
|
else list.push(entry);
|
|
3253
3615
|
}
|
|
3254
3616
|
return [...byProject].flatMap(([projectDir, list]) => {
|
|
3255
3617
|
const first = list[0];
|
|
3256
3618
|
if (first === void 0) return [];
|
|
3619
|
+
const ungrouped = projectDir === UNGROUPED_PROJECT_DIR;
|
|
3620
|
+
const label = ungrouped ? t("trash.group.ungrouped") : projectLabel(projectDir, t);
|
|
3257
3621
|
return [{
|
|
3258
3622
|
projectDir,
|
|
3259
|
-
label
|
|
3623
|
+
label,
|
|
3624
|
+
title: ungrouped ? label : projectDir,
|
|
3625
|
+
ungrouped,
|
|
3260
3626
|
latest: first.deletedAt,
|
|
3261
3627
|
entries: list
|
|
3262
3628
|
}];
|
|
3263
|
-
}).sort((a, b) =>
|
|
3629
|
+
}).sort((a, b) => {
|
|
3630
|
+
if (a.ungrouped !== b.ungrouped) return a.ungrouped ? 1 : -1;
|
|
3631
|
+
return b.latest - a.latest;
|
|
3632
|
+
});
|
|
3264
3633
|
}
|
|
3265
3634
|
//#endregion
|
|
3266
|
-
//#region src/client/
|
|
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
|
|
3267
3648
|
/**
|
|
3268
|
-
*
|
|
3649
|
+
* 插件设置页的「可调项」卡:三只数字输入 + 保存(ADR-0043;ADR-0037 留下的「自绘表单」那条路)。
|
|
3269
3650
|
*
|
|
3270
|
-
*
|
|
3271
|
-
* -
|
|
3272
|
-
*
|
|
3273
|
-
*
|
|
3274
|
-
*
|
|
3275
|
-
* - **图裂了就换占位**:缩略图与放大框共用同一份 `failed` 集合,`onError` 之后这一格改成「附件不可用」,
|
|
3276
|
-
* 不把破图留在屏上(放大框里裂了也换)。普通文件走 `<a download>`,没有页面内失败面:服务端失败回
|
|
3277
|
-
* 200 + JSON 信封,浏览器可能把这份错误响应存成文件。
|
|
3278
|
-
* - **放大框复用 `Modal`**:Escape、焦点归位、蒙层点击三件事都在那里,不另造一层浮层。
|
|
3651
|
+
* 为什么自绘而不是等宿主投影:`Config` 是手写 Standard Schema,宿主的字段投影只认 native
|
|
3652
|
+
* Schemastery(ADR-0037 的实测);把宿主 schema 库值 import 进本插件会破「跨插件一律 import type」。
|
|
3653
|
+
* 所以这里画三个输入,保存走 `/config` 的写支(`configEditor.edit`)。写会让 **Node 半**
|
|
3654
|
+
* 这一行重启;浏览器半不随之卸载,卡片用保存回执回填,重开插件页 / 刷新页面才会重新读一次。
|
|
3655
|
+
* `config.saved` 只说明"这一轮已经交出去",不是"页面完成了一次重新读取"。
|
|
3279
3656
|
*
|
|
3280
|
-
*
|
|
3281
|
-
*
|
|
3282
|
-
* (气泡那一侧),助手行左对齐(正文那一侧)。条本身不自认角色,对齐由调用面给。
|
|
3283
|
-
*/
|
|
3284
|
-
/**
|
|
3285
|
-
* 一张附件的取数地址。**只从校验过的 id 与 kind 派生**:不接受路径、不接受任意查询串,
|
|
3286
|
-
* 所以调用面把 URL 交给 `<img src>` / `<a href>` 时不存在"读任意文件"的入口。
|
|
3657
|
+
* 表单只做本地校验(`CONFIG_LIMITS` 与 Node 半归一同一格);服务端的 400 / io 原文在状态行上屏,
|
|
3658
|
+
* 不吞、不改写。读与写都只在用户动作或挂载时发生,没有定时器。
|
|
3287
3659
|
*/
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
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"
|
|
3298
3678
|
}
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3679
|
+
];
|
|
3680
|
+
function draftOf(config) {
|
|
3681
|
+
return {
|
|
3682
|
+
orphanPreviewLimit: String(config.orphanPreviewLimit),
|
|
3683
|
+
maxAttachmentBytes: String(config.maxAttachmentBytes),
|
|
3684
|
+
maxJsonRefBytes: String(config.maxJsonRefBytes)
|
|
3685
|
+
};
|
|
3305
3686
|
}
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
color: "var(--dsw-alias-label-secondary)",
|
|
3328
|
-
textDecoration: "underline"
|
|
3329
|
-
};
|
|
3330
|
-
const UNAVAILABLE = {
|
|
3331
|
-
display: "inline-flex",
|
|
3332
|
-
alignItems: "center",
|
|
3333
|
-
gap: 6,
|
|
3334
|
-
fontSize: 12,
|
|
3335
|
-
color: "var(--dsw-alias-label-tertiary)"
|
|
3336
|
-
};
|
|
3337
|
-
/**
|
|
3338
|
-
* 图裂之后的占位。**仍然说清是哪一件**:名字(没有就用「附件图片」/「文件」)+ 体量 + 「附件不可用」。
|
|
3339
|
-
* 只剩一句「附件不可用」等于把"哪张图没了"也一起丢了。
|
|
3340
|
-
*/
|
|
3341
|
-
function Unavailable({ attachment, t }) {
|
|
3342
|
-
const label = nameOf(attachment) ?? (attachment.kind === "file" ? t("detail.attachment.file") : t("detail.attachment.alt"));
|
|
3343
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3344
|
-
style: UNAVAILABLE,
|
|
3345
|
-
children: [
|
|
3346
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconWarningOutlineRegular, { size: 13 }),
|
|
3347
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3348
|
-
style: { wordBreak: "break-all" },
|
|
3349
|
-
children: label
|
|
3350
|
-
}),
|
|
3351
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: fileSizeText(attachment.bytes) }),
|
|
3352
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("detail.attachment.unavailable") })
|
|
3353
|
-
]
|
|
3354
|
-
});
|
|
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
|
+
};
|
|
3355
3708
|
}
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
*/
|
|
3363
|
-
function AttachmentStrip({ attachments, t, align = "end" }) {
|
|
3364
|
-
const [preview, setPreview] = (0, react.useState)(null);
|
|
3365
|
-
const [failed, setFailed] = (0, react.useState)(/* @__PURE__ */ new Set());
|
|
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);
|
|
3366
3715
|
const [saving, setSaving] = (0, react.useState)(false);
|
|
3367
|
-
const [
|
|
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
|
+
});
|
|
3934
|
+
}
|
|
3935
|
+
//#endregion
|
|
3936
|
+
//#region src/client/attachment-view.tsx
|
|
3937
|
+
/**
|
|
3938
|
+
* 回收站详情与设置页维护卡预览里的一条附件:图片给缩略图 + 放大框,普通文件给下载 chip。
|
|
3939
|
+
*
|
|
3940
|
+
* 三条口径:
|
|
3941
|
+
* - **地址只由 `attachmentId` + `kind` 寻址**:`/attachment` 段以这两个参数找对象;媒体元数据
|
|
3942
|
+
* (`mediaType` / `bytes` / `width` / `height`)只给宿主 `attachments` 服务做引用校验,`name` 只做
|
|
3943
|
+
* 下载名建议,均不参与路径推导(见 `src/api/attachment.ts`)。空串与缺席同义——空串不是一个名字,
|
|
3944
|
+
* 不写进查询串。
|
|
3945
|
+
* - **图裂了就换占位**:缩略图与放大框共用同一份 `failed` 集合,`onError` 之后这一格改成「附件不可用」,
|
|
3946
|
+
* 不把破图留在屏上(放大框里裂了也换)。普通文件走 `<a download>`,没有页面内失败面:服务端失败回
|
|
3947
|
+
* 200 + JSON 信封,浏览器可能把这份错误响应存成文件。
|
|
3948
|
+
* - **放大框复用 `Modal`**:Escape、焦点归位、蒙层点击三件事都在那里,不另造一层浮层。
|
|
3949
|
+
*
|
|
3950
|
+
* 附件是**消息的一部分**,不单开一等区块:调用面把 `AttachmentStrip` 摆在自己那一行的位置
|
|
3951
|
+
* (用户:气泡之上;助手:Markdown 之后),并用 `align` 说自己那一行朝哪边靠——用户行右对齐
|
|
3952
|
+
* (气泡那一侧),助手行左对齐(正文那一侧)。条本身不自认角色,对齐由调用面给。
|
|
3953
|
+
*/
|
|
3954
|
+
/**
|
|
3955
|
+
* 一张附件的取数地址。**只从校验过的 id 与 kind 派生**:不接受路径、不接受任意查询串,
|
|
3956
|
+
* 所以调用面把 URL 交给 `<img src>` / `<a href>` 时不存在"读任意文件"的入口。
|
|
3957
|
+
*/
|
|
3958
|
+
function attachmentUrl(a) {
|
|
3959
|
+
const query = new URLSearchParams({
|
|
3960
|
+
[PARAMS.attachmentId]: a.attachmentId,
|
|
3961
|
+
[PARAMS.attachmentKind]: a.kind,
|
|
3962
|
+
[PARAMS.attachmentBytes]: String(a.bytes)
|
|
3963
|
+
});
|
|
3964
|
+
if (a.kind === "image") {
|
|
3965
|
+
if (a.mediaType !== void 0) query.set(PARAMS.attachmentMediaType, a.mediaType);
|
|
3966
|
+
if (a.width !== void 0) query.set(PARAMS.attachmentWidth, String(a.width));
|
|
3967
|
+
if (a.height !== void 0) query.set(PARAMS.attachmentHeight, String(a.height));
|
|
3968
|
+
}
|
|
3969
|
+
if (a.kind === "file" && a.name !== void 0 && a.name !== "") query.set(PARAMS.attachmentName, a.name);
|
|
3970
|
+
return apiPath(`${ROUTES.attachment.path}?${query.toString()}`);
|
|
3971
|
+
}
|
|
3972
|
+
/** 空串与缺席同义:宿主侧的 `name` 是可选建议名,空串不是名字(兜底文案与"不带 name 查询"都看它)。 */
|
|
3973
|
+
function nameOf(a) {
|
|
3974
|
+
return a.name === void 0 || a.name === "" ? void 0 : a.name;
|
|
3975
|
+
}
|
|
3976
|
+
const IMAGE_BUTTON = {
|
|
3977
|
+
display: "block",
|
|
3978
|
+
padding: 0,
|
|
3979
|
+
border: 0,
|
|
3980
|
+
background: "none",
|
|
3981
|
+
cursor: "zoom-in",
|
|
3982
|
+
maxWidth: "100%"
|
|
3983
|
+
};
|
|
3984
|
+
const THUMB = {
|
|
3985
|
+
display: "block",
|
|
3986
|
+
maxWidth: "100%",
|
|
3987
|
+
maxHeight: 260,
|
|
3988
|
+
objectFit: "contain",
|
|
3989
|
+
borderRadius: "var(--sd-radius-lg)",
|
|
3990
|
+
background: "var(--dsw-alias-bg-layer-2)"
|
|
3991
|
+
};
|
|
3992
|
+
const FILE_CHIP = {
|
|
3993
|
+
display: "inline-block",
|
|
3994
|
+
maxWidth: "100%",
|
|
3995
|
+
wordBreak: "break-all",
|
|
3996
|
+
fontSize: 12,
|
|
3997
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
3998
|
+
textDecoration: "underline"
|
|
3999
|
+
};
|
|
4000
|
+
const UNAVAILABLE = {
|
|
4001
|
+
display: "inline-flex",
|
|
4002
|
+
alignItems: "center",
|
|
4003
|
+
gap: 6,
|
|
4004
|
+
fontSize: 12,
|
|
4005
|
+
color: "var(--sd-label-tertiary)"
|
|
4006
|
+
};
|
|
4007
|
+
/**
|
|
4008
|
+
* 图裂之后的占位。**仍然说清是哪一件**:名字(没有就用「附件图片」/「文件」)+ 体量 + 「附件不可用」。
|
|
4009
|
+
* 只剩一句「附件不可用」等于把"哪张图没了"也一起丢了。
|
|
4010
|
+
*/
|
|
4011
|
+
function Unavailable({ attachment, t }) {
|
|
4012
|
+
const label = nameOf(attachment) ?? (attachment.kind === "file" ? t("detail.attachment.file") : t("detail.attachment.alt"));
|
|
4013
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
4014
|
+
style: UNAVAILABLE,
|
|
4015
|
+
children: [
|
|
4016
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconWarningOutlineRegular, { size: 13 }),
|
|
4017
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4018
|
+
style: { wordBreak: "break-all" },
|
|
4019
|
+
children: label
|
|
4020
|
+
}),
|
|
4021
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: fileSizeText(attachment.bytes) }),
|
|
4022
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("detail.attachment.unavailable") })
|
|
4023
|
+
]
|
|
4024
|
+
});
|
|
4025
|
+
}
|
|
4026
|
+
/**
|
|
4027
|
+
* 一条消息的全部附件。顺序照日志,不重排:用户先发图后发文件,读起来应是他当时的顺序。
|
|
4028
|
+
*
|
|
4029
|
+
* `align` 是这一行的对齐(默认 `end`,与用户气泡同侧):用户行靠右、助手行靠左,条跟着自己那条消息走。
|
|
4030
|
+
* `failed` 按 `attachmentId` 记:同一个内容哈希在一段会话里可能被引用多次,裂一次就都不再画破图。
|
|
4031
|
+
* 四个 `useState` 都在早退之前调用——附件从有到无时 hook 数量不变。
|
|
4032
|
+
*/
|
|
4033
|
+
function AttachmentStrip({ attachments, t, align = "end" }) {
|
|
4034
|
+
const [preview, setPreview] = (0, react.useState)(null);
|
|
4035
|
+
const [failed, setFailed] = (0, react.useState)(/* @__PURE__ */ new Set());
|
|
4036
|
+
const [saving, setSaving] = (0, react.useState)(false);
|
|
4037
|
+
const [saveError, setSaveError] = (0, react.useState)(null);
|
|
3368
4038
|
if (attachments.length === 0) return null;
|
|
3369
4039
|
const markFailed = (id) => setFailed((prev) => new Set(prev).add(id));
|
|
3370
4040
|
const openPreview = (attachment) => {
|
|
@@ -3640,6 +4310,8 @@ window.__ModuleLoader__.load({
|
|
|
3640
4310
|
textOverflow: "ellipsis",
|
|
3641
4311
|
whiteSpace: "nowrap"
|
|
3642
4312
|
};
|
|
4313
|
+
/** 数字等宽:清单行、信息表与计量行共用一处,读数横着扫时才不抖。 */
|
|
4314
|
+
const TABULAR_NUMS = { fontVariantNumeric: "tabular-nums" };
|
|
3643
4315
|
//#endregion
|
|
3644
4316
|
//#region src/client/trash-confirm.tsx
|
|
3645
4317
|
/** 弹出的一批卡片(逐条说清这一批有哪些):长清单在框内滚,标题与动作行留在框里。 */
|
|
@@ -3699,7 +4371,7 @@ window.__ModuleLoader__.load({
|
|
|
3699
4371
|
...ELLIPSIS,
|
|
3700
4372
|
fontSize: 12,
|
|
3701
4373
|
lineHeight: "16px",
|
|
3702
|
-
color: "var(--
|
|
4374
|
+
color: "var(--sd-label-tertiary)"
|
|
3703
4375
|
},
|
|
3704
4376
|
children: projectLabel(entry.projectDir, t)
|
|
3705
4377
|
}),
|
|
@@ -3707,7 +4379,7 @@ window.__ModuleLoader__.load({
|
|
|
3707
4379
|
style: {
|
|
3708
4380
|
fontSize: 12,
|
|
3709
4381
|
lineHeight: "16px",
|
|
3710
|
-
color: "var(--
|
|
4382
|
+
color: "var(--sd-label-tertiary)"
|
|
3711
4383
|
},
|
|
3712
4384
|
children: t("trash.row.workspaceGone")
|
|
3713
4385
|
}) : null
|
|
@@ -3756,7 +4428,7 @@ window.__ModuleLoader__.load({
|
|
|
3756
4428
|
...ELLIPSIS,
|
|
3757
4429
|
fontSize: 12,
|
|
3758
4430
|
lineHeight: "16px",
|
|
3759
|
-
color: "var(--
|
|
4431
|
+
color: "var(--sd-label-tertiary)"
|
|
3760
4432
|
},
|
|
3761
4433
|
children: t("trash.confirm.one.meta", {
|
|
3762
4434
|
project: projectLabel(pending.entry.projectDir, t),
|
|
@@ -3827,7 +4499,7 @@ window.__ModuleLoader__.load({
|
|
|
3827
4499
|
style: {
|
|
3828
4500
|
fontSize: 12,
|
|
3829
4501
|
lineHeight: "17px",
|
|
3830
|
-
color: "var(--
|
|
4502
|
+
color: "var(--sd-label-tertiary)"
|
|
3831
4503
|
},
|
|
3832
4504
|
children: t("trash.orphans.cacheNote")
|
|
3833
4505
|
})
|
|
@@ -3855,7 +4527,7 @@ window.__ModuleLoader__.load({
|
|
|
3855
4527
|
style: {
|
|
3856
4528
|
fontSize: 12,
|
|
3857
4529
|
lineHeight: "16px",
|
|
3858
|
-
color: "var(--
|
|
4530
|
+
color: "var(--sd-label-tertiary)"
|
|
3859
4531
|
},
|
|
3860
4532
|
children: t("trash.orphans.meta", {
|
|
3861
4533
|
size: fileSizeText(report.totalBytes),
|
|
@@ -3885,7 +4557,7 @@ window.__ModuleLoader__.load({
|
|
|
3885
4557
|
display: "inline-flex",
|
|
3886
4558
|
flex: "none",
|
|
3887
4559
|
paddingTop: 2,
|
|
3888
|
-
color: "var(--
|
|
4560
|
+
color: "var(--sd-label-tertiary)"
|
|
3889
4561
|
},
|
|
3890
4562
|
"aria-hidden": "true",
|
|
3891
4563
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconWarningOutlineRegular, { size: 15 })
|
|
@@ -4103,16 +4775,8 @@ window.__ModuleLoader__.load({
|
|
|
4103
4775
|
}
|
|
4104
4776
|
//#endregion
|
|
4105
4777
|
//#region src/client/maintenance-settings.tsx
|
|
4106
|
-
/**
|
|
4107
|
-
const CARD_SHELL =
|
|
4108
|
-
...CARD,
|
|
4109
|
-
flexDirection: "column",
|
|
4110
|
-
alignItems: "stretch",
|
|
4111
|
-
gap: 18,
|
|
4112
|
-
width: "100%",
|
|
4113
|
-
boxSizing: "border-box",
|
|
4114
|
-
padding: "14px 16px"
|
|
4115
|
-
};
|
|
4778
|
+
/** 卡片外壳:与「可调项」卡同款,只差本卡的内容节奏。 */
|
|
4779
|
+
const CARD_SHELL = settingsCardShell(18);
|
|
4116
4780
|
/** 一段的标题行:标题 + 说明 + 状态 + 检查按钮。状态按优先级:清理中 / 检查中 / 检查失败 / 尚未检查 / 没有发现 / N。 */
|
|
4117
4781
|
function MaintenanceSection({ slot, title, hint, count, countKey, section, onScan, t, children }) {
|
|
4118
4782
|
const status = statusText({
|
|
@@ -4157,7 +4821,7 @@ window.__ModuleLoader__.load({
|
|
|
4157
4821
|
style: {
|
|
4158
4822
|
fontSize: 12,
|
|
4159
4823
|
lineHeight: "17px",
|
|
4160
|
-
color: "var(--
|
|
4824
|
+
color: "var(--sd-label-tertiary)"
|
|
4161
4825
|
},
|
|
4162
4826
|
children: hint
|
|
4163
4827
|
})]
|
|
@@ -4169,7 +4833,7 @@ window.__ModuleLoader__.load({
|
|
|
4169
4833
|
paddingTop: 1,
|
|
4170
4834
|
fontSize: 12,
|
|
4171
4835
|
lineHeight: "17px",
|
|
4172
|
-
color: "var(--
|
|
4836
|
+
color: "var(--sd-label-tertiary)",
|
|
4173
4837
|
whiteSpace: "nowrap"
|
|
4174
4838
|
},
|
|
4175
4839
|
children: status
|
|
@@ -4285,7 +4949,7 @@ window.__ModuleLoader__.load({
|
|
|
4285
4949
|
style: {
|
|
4286
4950
|
fontSize: 12,
|
|
4287
4951
|
lineHeight: "17px",
|
|
4288
|
-
color: "var(--
|
|
4952
|
+
color: "var(--sd-label-tertiary)"
|
|
4289
4953
|
},
|
|
4290
4954
|
children: t("maintenance.attachments.previewTruncated", {
|
|
4291
4955
|
shown: items.length,
|
|
@@ -4296,7 +4960,7 @@ window.__ModuleLoader__.load({
|
|
|
4296
4960
|
style: {
|
|
4297
4961
|
fontSize: 12,
|
|
4298
4962
|
lineHeight: "17px",
|
|
4299
|
-
color: "var(--
|
|
4963
|
+
color: "var(--sd-label-tertiary)"
|
|
4300
4964
|
},
|
|
4301
4965
|
children: t("maintenance.attachments.previewHint")
|
|
4302
4966
|
})
|
|
@@ -4352,6 +5016,18 @@ window.__ModuleLoader__.load({
|
|
|
4352
5016
|
});
|
|
4353
5017
|
}
|
|
4354
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
|
|
4355
5031
|
//#region src/client/trash-entry.tsx
|
|
4356
5032
|
/**
|
|
4357
5033
|
* 侧栏面板列表里的「回收站」行图标(`sidebar.panellist` 的占用者;docs/ui-spec.md 状态 ④ 的入口那一行)。
|
|
@@ -26337,6 +27013,106 @@ window.__ModuleLoader__.load({
|
|
|
26337
27013
|
});
|
|
26338
27014
|
}, (prev, next) => prev.text === next.text && prev.probe === next.probe);
|
|
26339
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
|
|
26340
27116
|
//#region src/client/trash-detail.tsx
|
|
26341
27117
|
/**
|
|
26342
27118
|
* 回收站详情块:只画状态,不取数。选中、缓存、失败与重读都在面板控制器里。
|
|
@@ -26346,7 +27122,7 @@ window.__ModuleLoader__.load({
|
|
|
26346
27122
|
* - 标题优先用清册标题,空串才回落到日志的 session/title。
|
|
26347
27123
|
* - 正文是**块**:用户/助手文字常显;思考、工具、注入、系统消息、压缩摘要默认折叠,折叠头给
|
|
26348
27124
|
* 标签 + 首行摘要(工具另给状态)。注入也铺进正文——这是对宿主 Chat 的有意偏离(它隐藏普通注入,
|
|
26349
|
-
*
|
|
27125
|
+
* 而这里的目标是「删之前看全」);条数另进计量行。
|
|
26350
27126
|
*
|
|
26351
27127
|
* 助手正文与思考走 Markdown,用户提问保持原话;工具参数/结果走等宽纯文本(限高可滚)。
|
|
26352
27128
|
* 附件随块位置渲染:用户气泡之上、助手正文之后、工具展开体末尾。
|
|
@@ -26356,17 +27132,25 @@ window.__ModuleLoader__.load({
|
|
|
26356
27132
|
flexDirection: "column",
|
|
26357
27133
|
gap: 12,
|
|
26358
27134
|
boxSizing: "border-box",
|
|
26359
|
-
|
|
27135
|
+
minHeight: "100%",
|
|
26360
27136
|
minWidth: 0
|
|
26361
27137
|
};
|
|
26362
27138
|
/** 返回键排在标题之后:排在之前会把整条标题的起点往右挤(docs/ui-spec.md 状态⑥)。
|
|
26363
27139
|
* 标题排按紧凑按钮(`ui.css` 的 `.sd-button-sm`)预留高度:宽档父详情没有返回键、孩子视图有,
|
|
26364
|
-
*
|
|
27140
|
+
* 不预留时下方会话信息会在父子切换时跳 6px。
|
|
27141
|
+
*
|
|
27142
|
+
* `position: sticky`:长会话读到中段时返回键仍在顶上(触屏没有 Esc 兜底,返回必须常驻)。
|
|
27143
|
+
* 底色用宿主页面底色(`bg-base`)把滚过的正文盖住。 */
|
|
26365
27144
|
const HEAD = {
|
|
26366
27145
|
display: "flex",
|
|
26367
27146
|
alignItems: "flex-start",
|
|
26368
27147
|
gap: 10,
|
|
26369
|
-
minHeight: 28
|
|
27148
|
+
minHeight: 28,
|
|
27149
|
+
position: "sticky",
|
|
27150
|
+
top: 0,
|
|
27151
|
+
zIndex: 1,
|
|
27152
|
+
background: "var(--dsw-alias-bg-base)",
|
|
27153
|
+
paddingBottom: 6
|
|
26370
27154
|
};
|
|
26371
27155
|
const TITLE_LINE = {
|
|
26372
27156
|
...ELLIPSIS,
|
|
@@ -26377,23 +27161,62 @@ window.__ModuleLoader__.load({
|
|
|
26377
27161
|
lineHeight: "22px"
|
|
26378
27162
|
};
|
|
26379
27163
|
const BACK = { flex: "none" };
|
|
26380
|
-
/**
|
|
27164
|
+
/**
|
|
27165
|
+
* 会话信息表:一张两列表,行序按「认出这条会话 → 时间定位 → 精确核对 → 调试属性」排
|
|
27166
|
+
* (docs/ui-spec.md 状态⑥)。标签列自动宽、值列吃余宽;长串省略号收、全称走 `title`。
|
|
27167
|
+
* 不做折叠、不做卡片:这类信息是给核对用的,默认就该看得见。
|
|
27168
|
+
*/
|
|
26381
27169
|
const META = {
|
|
26382
27170
|
display: "grid",
|
|
26383
27171
|
gridTemplateColumns: "auto 1fr",
|
|
26384
27172
|
gap: "3px 12px",
|
|
26385
|
-
margin: 0,
|
|
27173
|
+
margin: "8px 0 0",
|
|
26386
27174
|
fontSize: 12,
|
|
26387
|
-
lineHeight: "
|
|
27175
|
+
lineHeight: "18px"
|
|
26388
27176
|
};
|
|
26389
27177
|
const META_LABEL = {
|
|
26390
|
-
color: "var(--
|
|
27178
|
+
color: "var(--sd-label-tertiary)",
|
|
26391
27179
|
whiteSpace: "nowrap"
|
|
26392
27180
|
};
|
|
26393
27181
|
const META_VALUE = {
|
|
27182
|
+
...ELLIPSIS,
|
|
27183
|
+
...TABULAR_NUMS,
|
|
26394
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,
|
|
26395
27208
|
minWidth: 0,
|
|
26396
|
-
|
|
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
|
|
26397
27220
|
};
|
|
26398
27221
|
/**
|
|
26399
27222
|
* 线程的行距取宿主那两档里的 12px 那档(`ChatView.module.css` 的 `--dsh-chat-flow-gap`:默认 6px,
|
|
@@ -26429,8 +27252,8 @@ window.__ModuleLoader__.load({
|
|
|
26429
27252
|
* 我们一行只有一个气泡,直接把上限挂在气泡上(同一父宽下百分比与 `min()` 的结果一致,少一层)。
|
|
26430
27253
|
*/
|
|
26431
27254
|
const BUBBLE = {
|
|
26432
|
-
background: "var(--
|
|
26433
|
-
borderRadius: "var(--
|
|
27255
|
+
background: "var(--sd-specific-bubble)",
|
|
27256
|
+
borderRadius: "var(--sd-radius-xl)",
|
|
26434
27257
|
maxWidth: "min(calc(var(--dsh-chat-content-width, 748px) * .702), 82%)",
|
|
26435
27258
|
fontSize: "var(--dsh-content-font-size, 14px)",
|
|
26436
27259
|
lineHeight: "calc(22px + var(--dsh-content-font-delta, 0px))",
|
|
@@ -26444,7 +27267,7 @@ window.__ModuleLoader__.load({
|
|
|
26444
27267
|
const META_LINE = {
|
|
26445
27268
|
fontSize: "var(--dsh-content-font-size-secondary, 13px)",
|
|
26446
27269
|
lineHeight: "calc(18px + var(--dsh-content-font-delta-secondary, 0px))",
|
|
26447
|
-
color: "var(--
|
|
27270
|
+
color: "var(--sd-label-tertiary)"
|
|
26448
27271
|
};
|
|
26449
27272
|
/** 读失败与写失败共用的那块(`role="status"`;播报口径与其未验证的边界见 docs/ui-spec.md)。 */
|
|
26450
27273
|
const STATUS_BLOCK = {
|
|
@@ -26453,8 +27276,8 @@ window.__ModuleLoader__.load({
|
|
|
26453
27276
|
gap: 10,
|
|
26454
27277
|
padding: "12px 14px",
|
|
26455
27278
|
background: "var(--dsw-alias-bg-layer-2)",
|
|
26456
|
-
border: "0.5px solid var(--
|
|
26457
|
-
borderRadius: "var(--
|
|
27279
|
+
border: "0.5px solid var(--sd-border-l4)",
|
|
27280
|
+
borderRadius: "var(--sd-radius-lg)"
|
|
26458
27281
|
};
|
|
26459
27282
|
const BLOCK = {
|
|
26460
27283
|
display: "flex",
|
|
@@ -26470,14 +27293,21 @@ window.__ModuleLoader__.load({
|
|
|
26470
27293
|
textAlign: "start",
|
|
26471
27294
|
width: "100%",
|
|
26472
27295
|
minWidth: 0,
|
|
26473
|
-
gap:
|
|
27296
|
+
gap: 8
|
|
26474
27297
|
};
|
|
26475
|
-
/**
|
|
27298
|
+
/**
|
|
27299
|
+
* 标签不参与收缩:摘要是 `1 1 auto`,标签若也可缩,长结果会把「工具调用 / 来源」挤成几个字。
|
|
27300
|
+
*
|
|
27301
|
+
* 颜色**不跟着 `.sd-disclosure-row` 继承**:那颗类给的是 `label-tertiary`,而摘要也是 tertiary,
|
|
27302
|
+
* 两截同色只差一个字重,真机上就糊成一句「系统消息You are an AI agent…」。标签提到 secondary,
|
|
27303
|
+
* 摘要留在 tertiary,中间再留 8px,标签才真的像标签。
|
|
27304
|
+
*/
|
|
26476
27305
|
const BLOCK_TITLE = {
|
|
26477
27306
|
...ELLIPSIS,
|
|
26478
27307
|
flex: "none",
|
|
26479
27308
|
maxWidth: "50%",
|
|
26480
27309
|
fontWeight: 500,
|
|
27310
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
26481
27311
|
fontSize: "var(--dsh-content-font-size-secondary, 13px)"
|
|
26482
27312
|
};
|
|
26483
27313
|
const BLOCK_SUMMARY = {
|
|
@@ -26486,13 +27316,13 @@ window.__ModuleLoader__.load({
|
|
|
26486
27316
|
minWidth: 0,
|
|
26487
27317
|
fontSize: 12,
|
|
26488
27318
|
lineHeight: "18px",
|
|
26489
|
-
color: "var(--
|
|
27319
|
+
color: "var(--sd-label-tertiary)"
|
|
26490
27320
|
};
|
|
26491
27321
|
const BLOCK_META = {
|
|
26492
27322
|
flex: "none",
|
|
26493
|
-
fontSize: 11
|
|
27323
|
+
fontSize: 11,
|
|
26494
27324
|
lineHeight: "18px",
|
|
26495
|
-
color: "var(--
|
|
27325
|
+
color: "var(--sd-label-tertiary)",
|
|
26496
27326
|
whiteSpace: "nowrap"
|
|
26497
27327
|
};
|
|
26498
27328
|
const BLOCK_BODY = {
|
|
@@ -26514,8 +27344,8 @@ window.__ModuleLoader__.load({
|
|
|
26514
27344
|
margin: 0,
|
|
26515
27345
|
padding: "8px 10px",
|
|
26516
27346
|
background: "var(--dsw-alias-bg-layer-2)",
|
|
26517
|
-
border: "0.5px solid var(--
|
|
26518
|
-
borderRadius: "var(--
|
|
27347
|
+
border: "0.5px solid var(--sd-border-l4)",
|
|
27348
|
+
borderRadius: "var(--sd-radius-md)",
|
|
26519
27349
|
fontFamily: "var(--ds-font-family-code)",
|
|
26520
27350
|
fontSize: 12,
|
|
26521
27351
|
lineHeight: "18px",
|
|
@@ -26525,24 +27355,30 @@ window.__ModuleLoader__.load({
|
|
|
26525
27355
|
overflow: "auto"
|
|
26526
27356
|
};
|
|
26527
27357
|
const SECTION_LABEL = {
|
|
26528
|
-
fontSize: 11
|
|
27358
|
+
fontSize: 11,
|
|
26529
27359
|
lineHeight: "16px",
|
|
26530
|
-
color: "var(--
|
|
27360
|
+
color: "var(--sd-label-tertiary)"
|
|
26531
27361
|
};
|
|
26532
27362
|
/**
|
|
26533
|
-
*
|
|
26534
|
-
*
|
|
26535
|
-
*
|
|
26536
|
-
*
|
|
27363
|
+
* 子会话行:与清单行同语言的无边框导航行(悬停底色住 `ui/layout.css`)。两处内联覆盖各有理由:
|
|
27364
|
+
* 宿主类给折叠头钉死行高并随之裁溢出,导航行带内边距,须放开高度按内容撑开,否则裁字;
|
|
27365
|
+
* 指针光标宿主只发给带展开语义的行,导航行不借那套语义,光标就内联自给。
|
|
27366
|
+
* 底色与边框不铺——一列带框的行叠起来就是"一叠卡片",与清单行同一条判据。
|
|
26537
27367
|
*/
|
|
26538
27368
|
const SUBAGENT_ROW = {
|
|
26539
27369
|
...BLOCK_ROW,
|
|
26540
|
-
padding: "6px
|
|
27370
|
+
padding: "6px 8px",
|
|
26541
27371
|
height: "auto",
|
|
26542
27372
|
cursor: "pointer",
|
|
26543
|
-
|
|
26544
|
-
|
|
26545
|
-
|
|
27373
|
+
borderRadius: "var(--sd-radius-md)"
|
|
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
|
|
26546
27382
|
};
|
|
26547
27383
|
const TOOLBAR$1 = {
|
|
26548
27384
|
display: "flex",
|
|
@@ -26552,9 +27388,9 @@ window.__ModuleLoader__.load({
|
|
|
26552
27388
|
minWidth: 0
|
|
26553
27389
|
};
|
|
26554
27390
|
const TRUNCATED = {
|
|
26555
|
-
fontSize: 11
|
|
27391
|
+
fontSize: 11,
|
|
26556
27392
|
lineHeight: "16px",
|
|
26557
|
-
color: "var(--
|
|
27393
|
+
color: "var(--sd-label-tertiary)"
|
|
26558
27394
|
};
|
|
26559
27395
|
const ERROR_TEXT = { color: "var(--dsw-alias-state-error-primary)" };
|
|
26560
27396
|
const HOUR = 36e5;
|
|
@@ -26619,9 +27455,24 @@ window.__ModuleLoader__.load({
|
|
|
26619
27455
|
if (status === "error") return t("detail.block.status.error");
|
|
26620
27456
|
return t("detail.block.status.unfinished");
|
|
26621
27457
|
}
|
|
26622
|
-
|
|
26623
|
-
|
|
26624
|
-
|
|
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
|
+
];
|
|
26625
27476
|
/** 一个可折叠块:折叠头一行(chevron + 标签 + 摘要 + 可选状态 + 时间),展开体由调用方给。 */
|
|
26626
27477
|
function Disclosure({ block, label, title, summary, status, time, open, onToggle, children }) {
|
|
26627
27478
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -26826,9 +27677,9 @@ window.__ModuleLoader__.load({
|
|
|
26826
27677
|
height: "100%",
|
|
26827
27678
|
padding: "24px",
|
|
26828
27679
|
textAlign: "center",
|
|
26829
|
-
fontSize: 12
|
|
27680
|
+
fontSize: 12,
|
|
26830
27681
|
lineHeight: "18px",
|
|
26831
|
-
color: "var(--
|
|
27682
|
+
color: "var(--sd-label-tertiary)"
|
|
26832
27683
|
},
|
|
26833
27684
|
children: t("detail.notSelected")
|
|
26834
27685
|
});
|
|
@@ -26895,7 +27746,7 @@ window.__ModuleLoader__.load({
|
|
|
26895
27746
|
style: {
|
|
26896
27747
|
fontSize: 12,
|
|
26897
27748
|
lineHeight: "17px",
|
|
26898
|
-
color: "var(--
|
|
27749
|
+
color: "var(--sd-label-tertiary)",
|
|
26899
27750
|
wordBreak: "break-word"
|
|
26900
27751
|
},
|
|
26901
27752
|
children: state.message
|
|
@@ -26925,9 +27776,9 @@ window.__ModuleLoader__.load({
|
|
|
26925
27776
|
function LoadingBody({ t }) {
|
|
26926
27777
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
26927
27778
|
style: {
|
|
26928
|
-
fontSize: 12
|
|
27779
|
+
fontSize: 12,
|
|
26929
27780
|
lineHeight: "18px",
|
|
26930
|
-
color: "var(--
|
|
27781
|
+
color: "var(--sd-label-tertiary)"
|
|
26931
27782
|
},
|
|
26932
27783
|
children: t("detail.loading")
|
|
26933
27784
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -26950,35 +27801,62 @@ window.__ModuleLoader__.load({
|
|
|
26950
27801
|
} }, index))
|
|
26951
27802
|
})] });
|
|
26952
27803
|
}
|
|
26953
|
-
function Meta({ label, title, children }) {
|
|
27804
|
+
function Meta({ label, title, code, children }) {
|
|
26954
27805
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", {
|
|
26955
27806
|
style: META_LABEL,
|
|
26956
27807
|
children: label
|
|
26957
27808
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", {
|
|
26958
27809
|
style: META_VALUE,
|
|
26959
27810
|
title,
|
|
26960
|
-
children
|
|
27811
|
+
children: code === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
27812
|
+
style: META_CODE,
|
|
27813
|
+
children
|
|
27814
|
+
}) : children
|
|
26961
27815
|
})] });
|
|
26962
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
|
+
}
|
|
26963
27830
|
/**
|
|
26964
|
-
*
|
|
27831
|
+
* 有内容:会话信息(身份 → 时间 → 核对 → 属性)+ 计量行 + 子会话段 + 工作步骤。
|
|
26965
27832
|
*
|
|
26966
|
-
*
|
|
26967
|
-
* 正文是分页取回来的块。`olderCursor`
|
|
26968
|
-
*
|
|
27833
|
+
* 页面上**两本账分开读**:会话信息里的计数是整条日志的(注入条数也在计量行里);
|
|
27834
|
+
* 正文是分页取回来的块。`olderCursor` 非空时正文工具栏出「加载更早」。
|
|
27835
|
+
* 工作步骤档位与折叠状态只活在这个组件里;切条目用 `key={entry.id}` 重挂载,不会把 A 的
|
|
27836
|
+
* 展开状态带到 B。
|
|
26969
27837
|
*/
|
|
26970
27838
|
function ReadyBody({ entry, detail, detailNav, pager, t }) {
|
|
26971
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());
|
|
26972
27843
|
const [open, setOpen] = (0, react.useState)(() => /* @__PURE__ */ new Set());
|
|
26973
|
-
/**
|
|
26974
|
-
const
|
|
26975
|
-
|
|
26976
|
-
|
|
26977
|
-
|
|
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
|
+
});
|
|
26978
27854
|
const probe = localeProbe(t);
|
|
26979
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) ?? "—";
|
|
26980
27858
|
const disclosureBlocks = detail.blocks.filter(isDisclosureBlock);
|
|
26981
|
-
const
|
|
27859
|
+
const items = threadItems(detail.blocks);
|
|
26982
27860
|
const openChild = detailNav.onOpenChild;
|
|
26983
27861
|
const toggle = (id) => {
|
|
26984
27862
|
setOpen((current) => {
|
|
@@ -26988,37 +27866,38 @@ window.__ModuleLoader__.load({
|
|
|
26988
27866
|
return next;
|
|
26989
27867
|
});
|
|
26990
27868
|
};
|
|
26991
|
-
const
|
|
26992
|
-
|
|
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
|
+
});
|
|
26993
27876
|
};
|
|
26994
|
-
|
|
26995
|
-
|
|
26996
|
-
|
|
26997
|
-
|
|
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();
|
|
26998
27886
|
};
|
|
26999
|
-
(0, react.useLayoutEffect)(() => {
|
|
27000
|
-
const element = scrollRef?.current;
|
|
27001
|
-
const pending = pendingScroll.current;
|
|
27002
|
-
if (element === null || element === void 0 || pending === null) return;
|
|
27003
|
-
if (detail.blocks.length !== pending.blocks) {
|
|
27004
|
-
pendingScroll.current = null;
|
|
27005
|
-
if (lastHeight.current !== null) element.scrollTop += element.scrollHeight - lastHeight.current;
|
|
27006
|
-
return;
|
|
27007
|
-
}
|
|
27008
|
-
if (!loadingOlder) pendingScroll.current = null;
|
|
27009
|
-
}, [
|
|
27010
|
-
detail.blocks.length,
|
|
27011
|
-
loadingOlder,
|
|
27012
|
-
scrollRef
|
|
27013
|
-
]);
|
|
27014
|
-
(0, react.useLayoutEffect)(() => {
|
|
27015
|
-
const element = scrollRef?.current;
|
|
27016
|
-
if (element !== null && element !== void 0) lastHeight.current = element.scrollHeight;
|
|
27017
|
-
});
|
|
27018
27887
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
27019
27888
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dl", {
|
|
27020
27889
|
style: META,
|
|
27021
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
|
+
}),
|
|
27022
27901
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Meta, {
|
|
27023
27902
|
label: t("detail.field.created"),
|
|
27024
27903
|
children: timeText(detail.createdAt)
|
|
@@ -27028,62 +27907,53 @@ window.__ModuleLoader__.load({
|
|
|
27028
27907
|
children: timeText(detail.lastActivity)
|
|
27029
27908
|
}),
|
|
27030
27909
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Meta, {
|
|
27031
|
-
label: t("detail.field.
|
|
27032
|
-
|
|
27910
|
+
label: t("detail.field.sessionId"),
|
|
27911
|
+
title: entry.sessionId,
|
|
27912
|
+
code: true,
|
|
27913
|
+
children: entry.sessionId
|
|
27033
27914
|
}),
|
|
27034
27915
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Meta, {
|
|
27035
|
-
label: t("detail.field.
|
|
27036
|
-
|
|
27037
|
-
|
|
27038
|
-
|
|
27039
|
-
})
|
|
27916
|
+
label: t("detail.field.trashDir"),
|
|
27917
|
+
title: entry.id,
|
|
27918
|
+
code: true,
|
|
27919
|
+
children: entry.id
|
|
27040
27920
|
}),
|
|
27041
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, {
|
|
27042
27931
|
label: t("detail.field.size"),
|
|
27043
|
-
|
|
27932
|
+
value: fileSizeText(entry.sizeBytes)
|
|
27044
27933
|
}),
|
|
27045
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
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, {
|
|
27046
27939
|
label: t("detail.field.turns"),
|
|
27047
|
-
|
|
27940
|
+
value: String(detail.turnCount)
|
|
27048
27941
|
}),
|
|
27049
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
27942
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Metric, {
|
|
27050
27943
|
label: t("detail.field.userMessages"),
|
|
27051
|
-
|
|
27944
|
+
value: String(detail.userMessageCount)
|
|
27052
27945
|
}),
|
|
27053
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
27946
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Metric, {
|
|
27054
27947
|
label: t("detail.field.assistantMessages"),
|
|
27055
|
-
|
|
27948
|
+
value: String(detail.assistantMessageCount)
|
|
27056
27949
|
}),
|
|
27057
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
27950
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Metric, {
|
|
27058
27951
|
label: t("detail.field.toolCalls"),
|
|
27059
|
-
|
|
27952
|
+
value: String(detail.toolCallCount)
|
|
27060
27953
|
}),
|
|
27061
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
27954
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Metric, {
|
|
27062
27955
|
label: t("detail.field.injections"),
|
|
27063
|
-
|
|
27064
|
-
}),
|
|
27065
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Meta, {
|
|
27066
|
-
label: t("detail.field.models"),
|
|
27067
|
-
children: detail.models.join(", ") || "—"
|
|
27068
|
-
}),
|
|
27069
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Meta, {
|
|
27070
|
-
label: t("detail.field.project"),
|
|
27071
|
-
title: project,
|
|
27072
|
-
children: project
|
|
27073
|
-
}),
|
|
27074
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Meta, {
|
|
27075
|
-
label: t("detail.field.sessionId"),
|
|
27076
|
-
title: entry.sessionId,
|
|
27077
|
-
children: entry.sessionId
|
|
27078
|
-
}),
|
|
27079
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Meta, {
|
|
27080
|
-
label: t("detail.field.trashDir"),
|
|
27081
|
-
title: entry.id,
|
|
27082
|
-
children: entry.id
|
|
27083
|
-
}),
|
|
27084
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Meta, {
|
|
27085
|
-
label: t("detail.field.wasArchived"),
|
|
27086
|
-
children: entry.wasArchived ? t("detail.value.yes") : t("detail.value.no")
|
|
27956
|
+
value: String(detail.injectedCount)
|
|
27087
27957
|
})
|
|
27088
27958
|
]
|
|
27089
27959
|
}),
|
|
@@ -27092,7 +27962,7 @@ window.__ModuleLoader__.load({
|
|
|
27092
27962
|
style: {
|
|
27093
27963
|
display: "flex",
|
|
27094
27964
|
flexDirection: "column",
|
|
27095
|
-
gap:
|
|
27965
|
+
gap: 2
|
|
27096
27966
|
},
|
|
27097
27967
|
children: [
|
|
27098
27968
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -27108,8 +27978,18 @@ window.__ModuleLoader__.load({
|
|
|
27108
27978
|
"data-dsh-session-delete": "trash-subagent-row",
|
|
27109
27979
|
"data-entry-id": child.id,
|
|
27110
27980
|
style: SUBAGENT_ROW,
|
|
27981
|
+
title: t("detail.subagents.open"),
|
|
27111
27982
|
onClick: () => openChild(child),
|
|
27112
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
|
+
}),
|
|
27113
27993
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
27114
27994
|
style: {
|
|
27115
27995
|
...ELLIPSIS,
|
|
@@ -27125,6 +28005,15 @@ window.__ModuleLoader__.load({
|
|
|
27125
28005
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
27126
28006
|
style: BLOCK_META,
|
|
27127
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 })
|
|
27128
28017
|
})
|
|
27129
28018
|
]
|
|
27130
28019
|
}, child.id)),
|
|
@@ -27157,13 +28046,20 @@ window.__ModuleLoader__.load({
|
|
|
27157
28046
|
},
|
|
27158
28047
|
children: olderFailure
|
|
27159
28048
|
}),
|
|
27160
|
-
|
|
28049
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
27161
28050
|
style: { marginLeft: "auto" },
|
|
27162
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
27163
|
-
|
|
27164
|
-
|
|
27165
|
-
|
|
27166
|
-
children:
|
|
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))
|
|
27167
28063
|
})
|
|
27168
28064
|
})
|
|
27169
28065
|
]
|
|
@@ -27171,14 +28067,56 @@ window.__ModuleLoader__.load({
|
|
|
27171
28067
|
detail.blocks.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
27172
28068
|
style: META_LINE,
|
|
27173
28069
|
children: t("detail.noBody")
|
|
27174
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.
|
|
28070
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
27175
28071
|
style: THREAD,
|
|
27176
|
-
children:
|
|
28072
|
+
children: [mode === "standard" ? items.map((item) => item.kind === "block" ? renderBlock(item.block, {
|
|
27177
28073
|
open,
|
|
27178
28074
|
toggle,
|
|
27179
28075
|
probe,
|
|
27180
28076
|
t
|
|
27181
|
-
}))
|
|
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, {
|
|
28112
|
+
open,
|
|
28113
|
+
toggle,
|
|
28114
|
+
probe,
|
|
28115
|
+
t
|
|
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]
|
|
27182
28120
|
})
|
|
27183
28121
|
] });
|
|
27184
28122
|
}
|
|
@@ -27304,7 +28242,7 @@ window.__ModuleLoader__.load({
|
|
|
27304
28242
|
* - 方向键走环:未选中时 ↓ 落第一条、↑ 落最后一条;到端回卷。
|
|
27305
28243
|
* - aria-activedescendant 只指向在 DOM 里的行;tab 落点永远有一行拿着。
|
|
27306
28244
|
* - Esc 只在下钻期间生效,回退分两程(先收孩子视图、再退下钻);退出下钻后把焦点交还选中行一次;
|
|
27307
|
-
*
|
|
28245
|
+
* 最上层是确认框/菜单时让路;焦点在面板外时不归清单(`scope`)。
|
|
27308
28246
|
*/
|
|
27309
28247
|
/** 事件是不是清单自己的:从行(`role="row"`)或容器起来才算;行内按钮 / 勾选框的 Enter / Space
|
|
27310
28248
|
* 归它们自己。测试里的窄事件没有 `target`,按清单自己的处理。 */
|
|
@@ -27313,6 +28251,17 @@ window.__ModuleLoader__.load({
|
|
|
27313
28251
|
if (target == null || target === event.currentTarget) return true;
|
|
27314
28252
|
return typeof target.getAttribute === "function" && target.getAttribute("role") === "row";
|
|
27315
28253
|
}
|
|
28254
|
+
/** 这条 Esc 该不该归清单处理(回答的是"清单收不收这一下",不是"target 在不在作用域里")。
|
|
28255
|
+
* 焦点在页面空地(`body` / `html`)时没有"面板外"的证据,按清单处理,保住窄档下钻的可发现性;
|
|
28256
|
+
* `document` 是 `fireEvent.keyDown(document, …)` 这类合成事件的目标,同样没有焦点信息。
|
|
28257
|
+
* 其余按作用域根的包含关系判;根还没挂上时按不处理。 */
|
|
28258
|
+
function shouldHandleEsc(event, scope) {
|
|
28259
|
+
const target = event.target;
|
|
28260
|
+
if (!(target instanceof Node)) return true;
|
|
28261
|
+
if (target === document || target === document.body || target === document.documentElement) return true;
|
|
28262
|
+
const root = scope.current;
|
|
28263
|
+
return root !== null && root.contains(target);
|
|
28264
|
+
}
|
|
27316
28265
|
function useTrashNav(input) {
|
|
27317
28266
|
const { ordered, selectedId, selectMode, drillEnabled, drilled, select, toggleCheck, back, focusRow } = input;
|
|
27318
28267
|
/** Esc 退出下钻后要交还焦点的那一行;等 `drilled` 落地、清单回到 DOM 之后再用一次。 */
|
|
@@ -27353,10 +28302,12 @@ window.__ModuleLoader__.load({
|
|
|
27353
28302
|
selectedId,
|
|
27354
28303
|
toggleCheck
|
|
27355
28304
|
]);
|
|
28305
|
+
const scope = input.scope;
|
|
27356
28306
|
(0, react.useEffect)(() => {
|
|
27357
28307
|
if (!drilled || !drillEnabled) return;
|
|
27358
28308
|
const onKey = (event) => {
|
|
27359
28309
|
if (event.key !== "Escape" || event.defaultPrevented) return;
|
|
28310
|
+
if (!shouldHandleEsc(event, scope)) return;
|
|
27360
28311
|
if (hasTopLayer(document)) return;
|
|
27361
28312
|
refocusRow.current = selectedId;
|
|
27362
28313
|
back();
|
|
@@ -27368,6 +28319,7 @@ window.__ModuleLoader__.load({
|
|
|
27368
28319
|
}, [
|
|
27369
28320
|
drillEnabled,
|
|
27370
28321
|
drilled,
|
|
28322
|
+
scope,
|
|
27371
28323
|
back,
|
|
27372
28324
|
selectedId
|
|
27373
28325
|
]);
|
|
@@ -28040,10 +28992,11 @@ window.__ModuleLoader__.load({
|
|
|
28040
28992
|
* - 动作留在清单行右侧;详情只负责看,失败原文留在出事的那一行。
|
|
28041
28993
|
* - 空态不出现「清空回收站…」;「读不到」与「空的」分开。
|
|
28042
28994
|
* - 危险层级:页脚那排的最后一颗是整页唯一的实心红(非多选是「清空回收站…」,
|
|
28043
|
-
* 多选是「彻底删除 N
|
|
28995
|
+
* 多选是「彻底删除 N 条」);行内「彻底删除」图标默认只给字色,不铺实心底,悬停才转红。
|
|
28044
28996
|
*
|
|
28045
|
-
* 样式:结构与布局用内联样式;配色与圆角走宿主 token
|
|
28046
|
-
*
|
|
28997
|
+
* 样式:结构与布局用内联样式;配色与圆角走宿主 token;控件与清单行样式在 `src/client/ui/`
|
|
28998
|
+
* (`ui.css` 是宿主抄本,`forms.css` / `layout.css` 是自造规则),由 `ui/styles.ts` 注入 style。
|
|
28999
|
+
* 界面规格见 docs/ui-spec.md;
|
|
28047
29000
|
* 改断点或列宽前先看 SPLIT_MIN_WIDTH、LIST_COLUMN_DEFAULT / LIST_COLUMN_MIN、DETAIL_RESERVE 与 usePanelWidth。
|
|
28048
29001
|
*/
|
|
28049
29002
|
/** 回收站相对宿主**数据根**的位置。**不写 `~/.dsh/`**:数据根可以是 `$DSH_HOME` 指的别处。
|
|
@@ -28055,8 +29008,8 @@ window.__ModuleLoader__.load({
|
|
|
28055
29008
|
* 900 这一档清单为 411,刚好守住两边可读下限。改数字前先同步 DETAIL_RESERVE 与 usePanelWidth。
|
|
28056
29009
|
*/
|
|
28057
29010
|
const SPLIT_MIN_WIDTH = 900;
|
|
28058
|
-
/**
|
|
28059
|
-
* 剩下约
|
|
29011
|
+
/** 左栏默认宽:为“行内有动作”设计——两颗 28px 图标按钮加间距与行内边距约 80px,
|
|
29012
|
+
* 剩下约 340px 给标题与元信息。可拖范围见 LIST_COLUMN_MIN / DETAIL_RESERVE。 */
|
|
28060
29013
|
const LIST_COLUMN_DEFAULT = 420;
|
|
28061
29014
|
/** 左栏下限 330:再窄时「删除于 … · 224KB」那行就会开始被省略号截断。
|
|
28062
29015
|
* 按钮 flex:none,不参与这个下限。 */
|
|
@@ -28113,8 +29066,8 @@ window.__ModuleLoader__.load({
|
|
|
28113
29066
|
overflow: "auto",
|
|
28114
29067
|
paddingRight: 10,
|
|
28115
29068
|
paddingBottom: 8,
|
|
28116
|
-
"--dsh-scrollbar-thumb": "var(--
|
|
28117
|
-
"--dsh-scrollbar-thumb-hover": "var(--
|
|
29069
|
+
"--dsh-scrollbar-thumb": "var(--sd-scrollbar-bg-l2)",
|
|
29070
|
+
"--dsh-scrollbar-thumb-hover": "var(--sd-scrollbar-hover-l2)"
|
|
28118
29071
|
};
|
|
28119
29072
|
/** 详情那一栏的外壳(滚动归它自己,正文限宽在内部)。 */
|
|
28120
29073
|
const DETAIL_COLUMN = {
|
|
@@ -28125,8 +29078,8 @@ window.__ModuleLoader__.load({
|
|
|
28125
29078
|
overflow: "auto",
|
|
28126
29079
|
paddingRight: 10,
|
|
28127
29080
|
paddingBottom: 8,
|
|
28128
|
-
"--dsh-scrollbar-thumb": "var(--
|
|
28129
|
-
"--dsh-scrollbar-thumb-hover": "var(--
|
|
29081
|
+
"--dsh-scrollbar-thumb": "var(--sd-scrollbar-bg-l2)",
|
|
29082
|
+
"--dsh-scrollbar-thumb-hover": "var(--sd-scrollbar-hover-l2)"
|
|
28130
29083
|
};
|
|
28131
29084
|
const DETAIL_INNER = {
|
|
28132
29085
|
minWidth: 0,
|
|
@@ -28147,8 +29100,9 @@ window.__ModuleLoader__.load({
|
|
|
28147
29100
|
paddingBottom: 8
|
|
28148
29101
|
};
|
|
28149
29102
|
/**
|
|
28150
|
-
* 行那句「删除于 3
|
|
28151
|
-
*
|
|
29103
|
+
* 行那句「删除于 3小时前」:整句模板住在字典里(两种语言各自钉语序),单位词走 `copy.ts`
|
|
29104
|
+
* 的 `unitWord` / `relativeTime`——子会话行的时间读同一族,别抄成两本账。`now` 桶不套模板:
|
|
29105
|
+
* "删除于 刚刚前"不是话。
|
|
28152
29106
|
*/
|
|
28153
29107
|
function deletedAgo(at, now, t) {
|
|
28154
29108
|
const { unit, n } = relativeTime(at, now);
|
|
@@ -28182,12 +29136,13 @@ window.__ModuleLoader__.load({
|
|
|
28182
29136
|
/** 详情栏的滚动容器:分页 prepend 后按高度差补偿滚动位置(归 `trash-detail.tsx` 消费)。 */
|
|
28183
29137
|
const detailScrollRef = (0, react.useRef)(null);
|
|
28184
29138
|
const width = usePanelWidth(rootRef);
|
|
29139
|
+
const wide = width >= SPLIT_MIN_WIDTH;
|
|
28185
29140
|
/**
|
|
28186
29141
|
* 真走两栏的条件:盒子够宽 **且清单里有东西可看**。空态与读数失败时右栏不出现——那一栏既没有
|
|
28187
29142
|
* 可选项也没有动作,白占一半宽度(docs/ui-spec.md)。失败那块自己的「重试」
|
|
28188
29143
|
* 负责把清单读回来,读回来之后右栏跟着回来。
|
|
28189
29144
|
*/
|
|
28190
|
-
const split =
|
|
29145
|
+
const split = wide && entries !== null && entries.length > 0;
|
|
28191
29146
|
/** 两栏之间那根可拖的把手(窄档不渲染它,但 hook 照样得在——不能在条件里调)。 */
|
|
28192
29147
|
const resize = useColumnResize({
|
|
28193
29148
|
panelWidth: width,
|
|
@@ -28239,7 +29194,8 @@ window.__ModuleLoader__.load({
|
|
|
28239
29194
|
select: controller.select,
|
|
28240
29195
|
toggleCheck,
|
|
28241
29196
|
back: controller.back,
|
|
28242
|
-
focusRow
|
|
29197
|
+
focusRow,
|
|
29198
|
+
scope: rootRef
|
|
28243
29199
|
});
|
|
28244
29200
|
const pending = controller.pending;
|
|
28245
29201
|
const restoreEntries = controller.pendingEntries;
|
|
@@ -28313,7 +29269,7 @@ window.__ModuleLoader__.load({
|
|
|
28313
29269
|
style: {
|
|
28314
29270
|
fontSize: 12,
|
|
28315
29271
|
lineHeight: "17px",
|
|
28316
|
-
color: "var(--
|
|
29272
|
+
color: "var(--sd-label-tertiary)",
|
|
28317
29273
|
wordBreak: "break-word"
|
|
28318
29274
|
},
|
|
28319
29275
|
children: view.failure
|
|
@@ -28330,8 +29286,8 @@ window.__ModuleLoader__.load({
|
|
|
28330
29286
|
}) : view.loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
28331
29287
|
style: {
|
|
28332
29288
|
padding: "20px 2px",
|
|
28333
|
-
fontSize: 12
|
|
28334
|
-
color: "var(--
|
|
29289
|
+
fontSize: 12,
|
|
29290
|
+
color: "var(--sd-label-tertiary)"
|
|
28335
29291
|
},
|
|
28336
29292
|
children: t("trash.loading")
|
|
28337
29293
|
}) : view.empty ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -28356,14 +29312,14 @@ window.__ModuleLoader__.load({
|
|
|
28356
29312
|
flex: "none",
|
|
28357
29313
|
borderRadius: 999,
|
|
28358
29314
|
background: "var(--dsw-alias-bg-layer-2)",
|
|
28359
|
-
color: "var(--
|
|
29315
|
+
color: "var(--sd-label-tertiary)"
|
|
28360
29316
|
},
|
|
28361
29317
|
"aria-hidden": "true",
|
|
28362
29318
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconTrashOutlineRegular, { size: 20 })
|
|
28363
29319
|
}),
|
|
28364
29320
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
28365
29321
|
style: {
|
|
28366
|
-
fontSize:
|
|
29322
|
+
fontSize: 14,
|
|
28367
29323
|
color: "var(--dsw-alias-label-secondary)"
|
|
28368
29324
|
},
|
|
28369
29325
|
children: t("trash.empty.title")
|
|
@@ -28373,7 +29329,7 @@ window.__ModuleLoader__.load({
|
|
|
28373
29329
|
maxWidth: 340,
|
|
28374
29330
|
fontSize: 12,
|
|
28375
29331
|
lineHeight: "18px",
|
|
28376
|
-
color: "var(--
|
|
29332
|
+
color: "var(--sd-label-tertiary)"
|
|
28377
29333
|
},
|
|
28378
29334
|
children: t("trash.empty.hint")
|
|
28379
29335
|
})
|
|
@@ -28385,7 +29341,7 @@ window.__ModuleLoader__.load({
|
|
|
28385
29341
|
style: {
|
|
28386
29342
|
display: "flex",
|
|
28387
29343
|
flexDirection: "column",
|
|
28388
|
-
gap:
|
|
29344
|
+
gap: 2,
|
|
28389
29345
|
minWidth: 0
|
|
28390
29346
|
},
|
|
28391
29347
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -28395,7 +29351,8 @@ window.__ModuleLoader__.load({
|
|
|
28395
29351
|
display: "flex",
|
|
28396
29352
|
alignItems: "center",
|
|
28397
29353
|
gap: 6,
|
|
28398
|
-
minWidth: 0
|
|
29354
|
+
minWidth: 0,
|
|
29355
|
+
marginBottom: 4
|
|
28399
29356
|
},
|
|
28400
29357
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
28401
29358
|
role: "gridcell",
|
|
@@ -28437,18 +29394,17 @@ window.__ModuleLoader__.load({
|
|
|
28437
29394
|
},
|
|
28438
29395
|
children: [
|
|
28439
29396
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
28440
|
-
className: "sd-disclosure-leading",
|
|
29397
|
+
className: "sd-disclosure-leading sd-group-folder",
|
|
28441
29398
|
"aria-hidden": "true",
|
|
28442
|
-
children: isCollapsed(group.projectDir) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
29399
|
+
children: isCollapsed(group.projectDir) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconFolderCloseRegular, { size: 14 }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconFolderOpenRegular, { size: 14 })
|
|
28443
29400
|
}),
|
|
28444
29401
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
28445
|
-
|
|
28446
|
-
display: "inline-flex",
|
|
28447
|
-
flex: "none",
|
|
28448
|
-
color: "var(--dsw-alias-label-tertiary)"
|
|
28449
|
-
},
|
|
29402
|
+
className: "sd-disclosure-leading sd-group-chevron",
|
|
28450
29403
|
"aria-hidden": "true",
|
|
28451
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
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
|
+
})
|
|
28452
29408
|
}),
|
|
28453
29409
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
28454
29410
|
className: "sd-disclosure-title",
|
|
@@ -28458,15 +29414,15 @@ window.__ModuleLoader__.load({
|
|
|
28458
29414
|
flex: "1 1 auto",
|
|
28459
29415
|
fontWeight: 500
|
|
28460
29416
|
},
|
|
28461
|
-
title: group.
|
|
29417
|
+
title: group.title,
|
|
28462
29418
|
children: group.label
|
|
28463
29419
|
}),
|
|
28464
29420
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
28465
29421
|
style: {
|
|
28466
29422
|
marginLeft: "auto",
|
|
28467
29423
|
flex: "none",
|
|
28468
|
-
fontSize: 11
|
|
28469
|
-
color: "var(--
|
|
29424
|
+
fontSize: 11,
|
|
29425
|
+
color: "var(--sd-label-tertiary)"
|
|
28470
29426
|
},
|
|
28471
29427
|
children: t("trash.group.count", { count: group.entries.length })
|
|
28472
29428
|
})
|
|
@@ -28486,7 +29442,7 @@ window.__ModuleLoader__.load({
|
|
|
28486
29442
|
tabIndex: entry.id === nav.tabStopId ? 0 : -1,
|
|
28487
29443
|
"data-dsh-session-delete": "trash-row",
|
|
28488
29444
|
"data-entry-id": entry.id,
|
|
28489
|
-
style: isSelected ?
|
|
29445
|
+
style: isSelected ? ROW_SELECTED : ROW,
|
|
28490
29446
|
onClick: () => {
|
|
28491
29447
|
if (selectMode) {
|
|
28492
29448
|
toggleCheck(entry.id);
|
|
@@ -28531,9 +29487,10 @@ window.__ModuleLoader__.load({
|
|
|
28531
29487
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
28532
29488
|
style: {
|
|
28533
29489
|
...ELLIPSIS,
|
|
29490
|
+
...TABULAR_NUMS,
|
|
28534
29491
|
fontSize: 12,
|
|
28535
29492
|
lineHeight: "16px",
|
|
28536
|
-
color: "var(--dsw-alias-label-
|
|
29493
|
+
color: "var(--dsw-alias-label-secondary)"
|
|
28537
29494
|
},
|
|
28538
29495
|
title: absoluteText(entry.deletedAt),
|
|
28539
29496
|
children: [
|
|
@@ -28543,15 +29500,6 @@ window.__ModuleLoader__.load({
|
|
|
28543
29500
|
stranded > 0 ? t("trash.row.strandedSubagents", { n: stranded }) : ""
|
|
28544
29501
|
].filter(Boolean).join(" · ")
|
|
28545
29502
|
}),
|
|
28546
|
-
entry.workspaceMissing === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
28547
|
-
"data-dsh-session-delete": "trash-row-workspace-gone",
|
|
28548
|
-
style: {
|
|
28549
|
-
fontSize: 12,
|
|
28550
|
-
lineHeight: "16px",
|
|
28551
|
-
color: "var(--dsw-alias-label-tertiary)"
|
|
28552
|
-
},
|
|
28553
|
-
children: t("trash.row.workspaceGone")
|
|
28554
|
-
}) : null,
|
|
28555
29503
|
controller.actionFailure[entry.id] === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
28556
29504
|
role: "status",
|
|
28557
29505
|
style: {
|
|
@@ -28578,23 +29526,25 @@ window.__ModuleLoader__.load({
|
|
|
28578
29526
|
role: "gridcell",
|
|
28579
29527
|
style: {
|
|
28580
29528
|
display: "flex",
|
|
28581
|
-
gap:
|
|
29529
|
+
gap: 2,
|
|
28582
29530
|
flex: "none"
|
|
28583
29531
|
},
|
|
28584
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
28585
|
-
|
|
28586
|
-
|
|
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"),
|
|
28587
29537
|
disabled: controller.busy,
|
|
28588
29538
|
onClick: (event) => {
|
|
28589
29539
|
event.stopPropagation();
|
|
28590
29540
|
controller.restore(entry);
|
|
28591
29541
|
},
|
|
28592
|
-
children:
|
|
28593
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
28594
|
-
|
|
28595
|
-
|
|
28596
|
-
|
|
28597
|
-
|
|
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 }),
|
|
28598
29548
|
disabled: controller.busy,
|
|
28599
29549
|
onClick: (event) => {
|
|
28600
29550
|
event.stopPropagation();
|
|
@@ -28603,7 +29553,7 @@ window.__ModuleLoader__.load({
|
|
|
28603
29553
|
entry
|
|
28604
29554
|
});
|
|
28605
29555
|
},
|
|
28606
|
-
children:
|
|
29556
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconTrashOutlineRegular, { size: 14 })
|
|
28607
29557
|
})]
|
|
28608
29558
|
})
|
|
28609
29559
|
]
|
|
@@ -28676,7 +29626,7 @@ window.__ModuleLoader__.load({
|
|
|
28676
29626
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
28677
29627
|
ref: rootRef,
|
|
28678
29628
|
"data-dsh-session-delete": "trash-panel",
|
|
28679
|
-
className: resize.resizing ? "sd-resizing" : void 0,
|
|
29629
|
+
className: [resize.resizing ? "sd-resizing" : "", wide ? "" : "sd-narrow"].filter(Boolean).join(" ") || void 0,
|
|
28680
29630
|
style: PAGE,
|
|
28681
29631
|
children: [
|
|
28682
29632
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -28702,14 +29652,14 @@ window.__ModuleLoader__.load({
|
|
|
28702
29652
|
style: {
|
|
28703
29653
|
fontSize: 12,
|
|
28704
29654
|
lineHeight: "17px",
|
|
28705
|
-
color: "var(--
|
|
29655
|
+
color: "var(--sd-label-tertiary)"
|
|
28706
29656
|
},
|
|
28707
29657
|
children: t("trash.select.count", { count: checkedIds.length })
|
|
28708
29658
|
}) : hasEntries ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
28709
29659
|
style: {
|
|
28710
29660
|
fontSize: 12,
|
|
28711
29661
|
lineHeight: "17px",
|
|
28712
|
-
color: "var(--
|
|
29662
|
+
color: "var(--sd-label-tertiary)"
|
|
28713
29663
|
},
|
|
28714
29664
|
children: t("trash.summary", {
|
|
28715
29665
|
count: entries.length,
|
|
@@ -28749,9 +29699,9 @@ window.__ModuleLoader__.load({
|
|
|
28749
29699
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
28750
29700
|
style: {
|
|
28751
29701
|
minWidth: 0,
|
|
28752
|
-
fontSize: 11
|
|
29702
|
+
fontSize: 11,
|
|
28753
29703
|
lineHeight: "16px",
|
|
28754
|
-
color: "var(--
|
|
29704
|
+
color: "var(--sd-label-tertiary)"
|
|
28755
29705
|
},
|
|
28756
29706
|
children: [
|
|
28757
29707
|
noteParts[0],
|
|
@@ -28844,28 +29794,39 @@ window.__ModuleLoader__.load({
|
|
|
28844
29794
|
* **砍掉的**:`anchor`(把横幅对齐到某个元素的水平中心——本插件的撤销提示就是窗口居中)、
|
|
28845
29795
|
* `actions[].prefix`(动作文案前的连接词,本插件只用「撤销」两个字)。**没砍 `text`/`icon`/`tone`/
|
|
28846
29796
|
* `holdMs`/`actions`/`onDone`**:唯一的调用点这几样都在用。
|
|
29797
|
+
* **本插件加的**:`paused`(撤销在飞时冻结停留与淡出,失败原文要落回同一条横幅;官方没有这个开关)。
|
|
28847
29798
|
*/
|
|
28848
29799
|
/** 不指定时全不透明的停留时长。 */
|
|
28849
29800
|
const DEFAULT_HOLD_MS = 3e3;
|
|
28850
29801
|
/** 淡出时长。必须与样式表里 `sd-toast-fade` 的时长一致。 */
|
|
28851
29802
|
const FADE_MS = 1e3;
|
|
28852
|
-
function Toast({ text, icon, tone, holdMs = DEFAULT_HOLD_MS, actions, onDone }) {
|
|
29803
|
+
function Toast({ text, icon, tone, holdMs = DEFAULT_HOLD_MS, actions, paused = false, onDone }) {
|
|
28853
29804
|
const latestOnDone = (0, react.useRef)(onDone);
|
|
28854
29805
|
(0, react.useLayoutEffect)(() => {
|
|
28855
29806
|
latestOnDone.current = onDone;
|
|
28856
29807
|
}, [onDone]);
|
|
29808
|
+
/** 暂停时剩下的停留时间:`null` = 还没起表(新挂载)。淡出动画被 `animationPlayState` 冻住,
|
|
29809
|
+
* timer 也从同一冻结点续,两者才不会一个先到终点。 */
|
|
29810
|
+
const remaining = (0, react.useRef)(null);
|
|
28857
29811
|
(0, react.useEffect)(() => {
|
|
29812
|
+
if (paused) return;
|
|
29813
|
+
const wait = remaining.current ?? holdMs + FADE_MS;
|
|
29814
|
+
const deadline = Date.now() + wait;
|
|
28858
29815
|
const timer = setTimeout(() => {
|
|
28859
29816
|
latestOnDone.current();
|
|
28860
|
-
},
|
|
29817
|
+
}, wait);
|
|
28861
29818
|
return () => {
|
|
28862
29819
|
clearTimeout(timer);
|
|
29820
|
+
remaining.current = Math.max(0, deadline - Date.now());
|
|
28863
29821
|
};
|
|
28864
|
-
}, [holdMs]);
|
|
29822
|
+
}, [holdMs, paused]);
|
|
28865
29823
|
return (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
28866
29824
|
className: "sd-toast",
|
|
28867
29825
|
role: "alert",
|
|
28868
|
-
style: {
|
|
29826
|
+
style: {
|
|
29827
|
+
"--dsh-toast-hold": `${String(holdMs)}ms`,
|
|
29828
|
+
...paused ? { animationPlayState: "paused" } : {}
|
|
29829
|
+
},
|
|
28869
29830
|
children: [tone === "success" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
28870
29831
|
className: "sd-toast-icon sd-toast-icon-success",
|
|
28871
29832
|
"aria-hidden": "true",
|
|
@@ -28925,14 +29886,15 @@ window.__ModuleLoader__.load({
|
|
|
28925
29886
|
tone: "success",
|
|
28926
29887
|
text: current.kind === "one" ? t("undo.moved", { title: current.title === "" ? t("undo.session") : current.title }) : t("undo.movedBatch", { n: current.n }),
|
|
28927
29888
|
holdMs: UNDO_HOLD_MS,
|
|
29889
|
+
paused: gate.busy,
|
|
29890
|
+
onDone: clearUndoNotice,
|
|
28928
29891
|
actions: canUndo ? [{
|
|
28929
29892
|
label: t("undo.undo"),
|
|
28930
29893
|
disabled: gate.busy,
|
|
28931
29894
|
onClick: () => {
|
|
28932
29895
|
gate.around(() => restoreFromUndo(deps, current, t));
|
|
28933
29896
|
}
|
|
28934
|
-
}] : void 0
|
|
28935
|
-
onDone: clearUndoNotice
|
|
29897
|
+
}] : void 0
|
|
28936
29898
|
}, seq);
|
|
28937
29899
|
}
|
|
28938
29900
|
//#endregion
|
|
@@ -28942,11 +29904,14 @@ window.__ModuleLoader__.load({
|
|
|
28942
29904
|
*
|
|
28943
29905
|
* 槽位贡献的注册面见 docs/architecture.md §3,语义归属见 §4 的主人清单;
|
|
28944
29906
|
* 本文件只做注册与槽位接线。
|
|
28945
|
-
*
|
|
29907
|
+
* 菜单两行有两条路径:0.2.2+ 走 `ctx.contextMenus` 数据动作(`context-menu-actions.ts`,
|
|
29908
|
+
* afterContent、排在 Archive 之后);旧宿主没有这个服务,才注册 legacy 槽位行。
|
|
28946
29909
|
*
|
|
28947
29910
|
* 槽位出处(宿主产物,不是猜的;换代先复核):
|
|
28948
29911
|
* - sidebar.workspaces.session.menu.item:声明 asar @46391872;数据面投影 @46271806;
|
|
28949
|
-
* menuOpenState hook 工厂 @46195835
|
|
29912
|
+
* menuOpenState hook 工厂 @46195835;宿主官方菜单行 @46392466 起(pin/rename/fork/archive/session-id)。
|
|
29913
|
+
* 0.2.2 起内置动作改住 ctx.contextMenus,此槽位只剩 legacy React 扩展位;本插件把多选/删除
|
|
29914
|
+
* 迁到数据动作,见 `context-menu-actions.ts`。
|
|
28950
29915
|
* - shell.overlay:声明 asar @21206189。
|
|
28951
29916
|
* - sidebar.panellist:声明 @44796980;行渲染 @44783481;label 解析 @44795361(thunk 随 locale)。
|
|
28952
29917
|
* - main:渲染 @21185725;id 与 panellist 同字,selectPanel 只校验已注册 @21199710。
|
|
@@ -28967,8 +29932,12 @@ window.__ModuleLoader__.load({
|
|
|
28967
29932
|
* **`locale` 也不在这里**:宿主自己的 `ui-sidebar` 是硬注入它的,但那是"宿主知道自己那一代"的前提;
|
|
28968
29933
|
* 本插件要跨代活着 ⇒ 走 `ctx.get('locale')` 可选读,缺席时降级到内置 zh 文案(见 `copy.ts` 头注)。
|
|
28969
29934
|
* 写进 inject 就等于"这一代没有 locale 就不让整个客户端条目激活",而那等于宿主进程死。
|
|
29935
|
+
* **`contextMenus` 同理**:0.2.2+ 用 `ctx.get('contextMenus')` 探测,服务比本条目晚到时按
|
|
29936
|
+
* `ctx.inject(['contextMenus'])` 的 scoped 等待切过去;旧宿主没有这个服务时回调不来,legacy 槽位留着。
|
|
29937
|
+
* 条目自己的 `inject` 仍然只有 `slots`——把可能迟到的服务写进去等于让整个客户端条目 pending。
|
|
28970
29938
|
*/
|
|
28971
29939
|
const inject = ["slots"];
|
|
29940
|
+
/** legacy 菜单槽:只在 contextMenus 不可用时注册(0.2.2+ 的内置动作已迁到数据动作)。 */
|
|
28972
29941
|
const MENU_SLOT = "sidebar.workspaces.session.menu.item";
|
|
28973
29942
|
/** 会话行首 16px 格(无状态点时归第三方装饰);批量删除的主勾选框落这里,多选模式内可见。 */
|
|
28974
29943
|
const LEADING_SLOT = "sidebar.session.row.leading";
|
|
@@ -29080,22 +30049,78 @@ window.__ModuleLoader__.load({
|
|
|
29080
30049
|
*/
|
|
29081
30050
|
async function activate(client) {
|
|
29082
30051
|
bindMainView(client);
|
|
30052
|
+
let disposed = false;
|
|
30053
|
+
if (typeof client.effect === "function") client.effect(() => () => {
|
|
30054
|
+
disposed = true;
|
|
30055
|
+
}, "session-delete: activation");
|
|
29083
30056
|
const raw = createHttpRemote();
|
|
29084
30057
|
if (!await probeChannel(raw)) return;
|
|
30058
|
+
if (disposed) return;
|
|
29085
30059
|
const deps = buildDeps(raw);
|
|
29086
30060
|
const t = createTranslate(client);
|
|
29087
|
-
|
|
29088
|
-
|
|
29089
|
-
|
|
29090
|
-
|
|
29091
|
-
|
|
29092
|
-
|
|
29093
|
-
|
|
29094
|
-
|
|
29095
|
-
|
|
29096
|
-
|
|
29097
|
-
|
|
29098
|
-
|
|
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");
|
|
30066
|
+
try {
|
|
30067
|
+
target.effect(() => menus.register(createSessionMenuContribution(t, sessions?.list)), "session-delete: context menu actions");
|
|
30068
|
+
} catch (error) {
|
|
30069
|
+
console.warn("[dsh-session-delete] context menu 注册失败,回退 legacy 菜单行:", error);
|
|
30070
|
+
return false;
|
|
30071
|
+
}
|
|
30072
|
+
try {
|
|
30073
|
+
target.slots.inject("contextMenu.icon", () => target.slots.register({
|
|
30074
|
+
name: "contextMenu.icon",
|
|
30075
|
+
key: ICON_BATCH
|
|
30076
|
+
}, IconCheckOutlineRegular));
|
|
30077
|
+
target.slots.inject("contextMenu.icon", () => target.slots.register({
|
|
30078
|
+
name: "contextMenu.icon",
|
|
30079
|
+
key: ICON_DELETE
|
|
30080
|
+
}, IconTrashOutlineRegular));
|
|
30081
|
+
} catch (error) {
|
|
30082
|
+
console.warn("[dsh-session-delete] context menu 图标注册失败:", error);
|
|
30083
|
+
}
|
|
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({
|
|
30093
|
+
name: MENU_SLOT,
|
|
30094
|
+
id: `${PLUGIN_ID}.delete`,
|
|
30095
|
+
order: 900,
|
|
30096
|
+
inject: () => ({ t })
|
|
30097
|
+
}, DeleteRow)));
|
|
30098
|
+
keep(client.slots.inject(MENU_SLOT, () => client.slots.register({
|
|
30099
|
+
name: MENU_SLOT,
|
|
30100
|
+
id: `${PLUGIN_ID}.batch-select`,
|
|
30101
|
+
order: 850,
|
|
30102
|
+
inject: () => ({ t })
|
|
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
|
+
}
|
|
30123
|
+
}
|
|
29099
30124
|
client.slots.inject(LEADING_SLOT, () => client.slots.register({
|
|
29100
30125
|
name: LEADING_SLOT,
|
|
29101
30126
|
id: `${PLUGIN_ID}.batch-pick-leading`,
|
|
@@ -29161,7 +30186,7 @@ window.__ModuleLoader__.load({
|
|
|
29161
30186
|
deps,
|
|
29162
30187
|
t
|
|
29163
30188
|
})
|
|
29164
|
-
},
|
|
30189
|
+
}, SettingsCards));
|
|
29165
30190
|
}
|
|
29166
30191
|
/**
|
|
29167
30192
|
* 入口:**同步返回、绝不抛**(见文件头注)。
|
|
@@ -29172,7 +30197,11 @@ window.__ModuleLoader__.load({
|
|
|
29172
30197
|
* `ctx.slots.subscribe("sidebar.panellist", syncPanels)`(asar @44795361)。
|
|
29173
30198
|
*/
|
|
29174
30199
|
function apply(ctx) {
|
|
29175
|
-
|
|
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) => {
|
|
29176
30205
|
console.warn("[dsh-session-delete] 入口未注册(取数通道不可用):", error);
|
|
29177
30206
|
});
|
|
29178
30207
|
}
|