@xrn1997/dsh-session-delete 0.5.0 → 0.6.1
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/LICENSE +201 -201
- package/README.md +165 -141
- package/cordis.patch.yml +7 -3
- package/icon.svg +18 -18
- package/lib/client.js +749 -430
- package/lib/index.js +850 -484
- package/lib/types/api/attachment-orphans.d.ts +9 -3
- package/lib/types/api/attachment.d.ts +17 -7
- package/lib/types/api/connection-route.d.ts +23 -0
- package/lib/types/api/dispatch.d.ts +24 -17
- package/lib/types/api/route-table.d.ts +25 -9
- package/lib/types/client/api-base.d.ts +19 -0
- package/lib/types/client/batch-bar.d.ts +1 -1
- package/lib/types/client/batch-pick.d.ts +19 -0
- package/lib/types/client/batch-select.d.ts +7 -5
- package/lib/types/client/context-menu-actions.d.ts +47 -0
- package/lib/types/client/copy.d.ts +8 -0
- package/lib/types/client/delete-confirm.d.ts +2 -2
- package/lib/types/client/host-rows.d.ts +27 -32
- package/lib/types/client/index.d.ts +2 -0
- package/lib/types/client/main-view.d.ts +2 -1
- package/lib/types/client/maintenance-settings.d.ts +3 -1
- package/lib/types/client/pending-open.d.ts +17 -0
- package/lib/types/client/remote.d.ts +22 -19
- package/lib/types/client/store.d.ts +1 -7
- package/lib/types/client/trash-nav.d.ts +5 -1
- package/lib/types/client/trash-view.d.ts +3 -0
- package/lib/types/client/ui/MenuItemButton.d.ts +1 -1
- package/lib/types/client/ui/Toast.d.ts +4 -1
- package/lib/types/client/use-trash-panel-controller.d.ts +1 -1
- package/lib/types/config.d.ts +45 -0
- package/lib/types/deps.d.ts +8 -3
- package/lib/types/domain/closure.d.ts +3 -0
- package/lib/types/index.d.ts +18 -16
- package/lib/types/ports/attachments-port.d.ts +40 -0
- 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 +18 -5
- package/lib/types/trash/attachments.d.ts +17 -2
- package/lib/types/verbs/errors.d.ts +3 -1
- package/lib/types/verbs/orphan-subagents.d.ts +3 -2
- package/lib/types/verbs/permanent-removal.d.ts +10 -5
- package/locale/en.json +6 -6
- package/locale/zh.json +6 -6
- package/package.json +122 -117
- package/lib/types/api/request-trust.d.ts +0 -45
- package/lib/types/ports/connection-port.d.ts +0 -33
package/lib/client.js
CHANGED
|
@@ -264,6 +264,7 @@ window.__ModuleLoader__.load({
|
|
|
264
264
|
"detail.role.assistant": "助手",
|
|
265
265
|
"menu.delete": "删除",
|
|
266
266
|
"menu.live": "会话正在运行,先停止再删除",
|
|
267
|
+
"menu.deleteLive": "删除(运行中)",
|
|
267
268
|
"dialog.title": "删除这个会话?",
|
|
268
269
|
"dialog.subagents": "含 {n} 个子会话,将一起移入回收站。",
|
|
269
270
|
"dialog.hint": "会话会移入回收站,之后可以随时恢复。彻底删除是回收站里的另一个动作。",
|
|
@@ -447,6 +448,7 @@ window.__ModuleLoader__.load({
|
|
|
447
448
|
"detail.role.assistant": "Assistant",
|
|
448
449
|
"menu.delete": "Delete",
|
|
449
450
|
"menu.live": "This session is running. Stop it first.",
|
|
451
|
+
"menu.deleteLive": "Delete (running)",
|
|
450
452
|
"dialog.title": "Delete this session?",
|
|
451
453
|
"dialog.subagents": "It also moves {n} subagent sessions to the trash.",
|
|
452
454
|
"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 +479,42 @@ window.__ModuleLoader__.load({
|
|
|
477
479
|
/** 服务缺席时的缺省:字典里那套 zh。**也是组件 prop 的缺省值**——它表达的是"这次没有 locale 服务",
|
|
478
480
|
* 与运行期 `createTranslate` 的降级分支是同一件事,不是"忘了传"。 */
|
|
479
481
|
const zhTranslate = (key, params) => interpolate(ZH[key], params);
|
|
482
|
+
/** 探测宿主 locale 服务:缺席 / `ctx.get` 抛错都回 `undefined`,不往上抛。 */
|
|
483
|
+
function detectLocaleService(ctx) {
|
|
484
|
+
try {
|
|
485
|
+
const found = ctx.get?.("locale");
|
|
486
|
+
return found == null ? void 0 : found;
|
|
487
|
+
} catch {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
480
491
|
/**
|
|
481
492
|
* 注册字典并交回本插件的 `t`。服务不在时返回 `zhTranslate`(不抛、不 pending)。
|
|
482
493
|
*
|
|
494
|
+
* **空值判据同时认 `undefined` 与 `null`**:宿主 `ctx.get` 可以回 null(同一条窄镜像的读法见
|
|
495
|
+
* `main-view.ts` 的 `isReadyUiWorkspace`)。这里只判 `undefined` 的版本会把 null 当成"服务在",
|
|
496
|
+
* 下一句 `service.register` 抛错——而本函数跑在**任何槽位注册之前**,抛出去等于整个客户端条目
|
|
497
|
+
* 从页面消失(`apply` 只打一条 warn)。注册 / 绑定自身失败同样退回内置 zh:文案降级远好过没有插件。
|
|
498
|
+
* `bind` 在注册之后才跑,它失败时字典已挂上但没人用——留着无害(宿主只是多一份不用的词典),
|
|
499
|
+
* 不为它补撤销路径再引入新的失败面。
|
|
500
|
+
*
|
|
483
501
|
* 注册必须挂在 `ctx.effect` 上:`register` 返回的 disposer 是**官方契约**(卸载时要把字典摘掉),
|
|
484
502
|
* 而且同一命名空间的同一语言**只能注册一次**——effect 没挂,HMR 重挂就撞 "already has locale"。
|
|
485
503
|
*/
|
|
486
504
|
function createTranslate(ctx) {
|
|
487
|
-
const service = ctx
|
|
505
|
+
const service = detectLocaleService(ctx);
|
|
488
506
|
if (service === void 0) return zhTranslate;
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
507
|
+
try {
|
|
508
|
+
ctx.effect(() => service.register(COPY_NS, {
|
|
509
|
+
zh: ZH,
|
|
510
|
+
en: EN
|
|
511
|
+
}), "session-delete: copy dictionaries");
|
|
512
|
+
const bound = service.bind(COPY_NS);
|
|
513
|
+
return (key, params) => bound(key, params);
|
|
514
|
+
} catch (error) {
|
|
515
|
+
console.warn("[dsh-session-delete] locale 服务不可用,退回内置中文:", error);
|
|
516
|
+
return zhTranslate;
|
|
517
|
+
}
|
|
495
518
|
}
|
|
496
519
|
/**
|
|
497
520
|
* `MarkdownBody`(`markdown.tsx`)memo comparator 的**语言探针**:借一句固定键的译文,当
|
|
@@ -575,8 +598,8 @@ window.__ModuleLoader__.load({
|
|
|
575
598
|
* 在这之前每个文件各写一遍「`let` 现场 + 监听集合 + `notify` + `useSyncExternalStore`」,
|
|
576
599
|
* 各自的 seq / revision 也在分头维护。现在订阅/通告只住这一处。
|
|
577
600
|
*
|
|
578
|
-
* 另有一个**懒读快照**变体(`createLazyStore
|
|
579
|
-
*
|
|
601
|
+
* 另有一个**懒读快照**变体(`createLazyStore`):值由组件树之外的事实源(如宿主行槽位登记)现读,
|
|
602
|
+
* 脏了才重读;订阅脚手架仍住这里。
|
|
580
603
|
*
|
|
581
604
|
* 还有一个**最新票句柄**(`createLatestWins`):并发的异步读里"只有最新那次可以落地"原先在每个
|
|
582
605
|
* 调用方各写一遍自增序号,现在只住这一处。它只拥有 compare-and-set——防抖、轮询、焦点策略仍是
|
|
@@ -637,7 +660,7 @@ window.__ModuleLoader__.load({
|
|
|
637
660
|
}
|
|
638
661
|
};
|
|
639
662
|
}
|
|
640
|
-
function createLazyStore(initial, read
|
|
663
|
+
function createLazyStore(initial, read) {
|
|
641
664
|
let cached = initial;
|
|
642
665
|
let dirty = true;
|
|
643
666
|
const listeners = /* @__PURE__ */ new Set();
|
|
@@ -657,17 +680,11 @@ window.__ModuleLoader__.load({
|
|
|
657
680
|
notify();
|
|
658
681
|
},
|
|
659
682
|
subscribe(listener) {
|
|
660
|
-
|
|
683
|
+
if (listeners.size === 0) dirty = true;
|
|
661
684
|
listeners.add(listener);
|
|
662
|
-
if (reattaching) {
|
|
663
|
-
dirty = true;
|
|
664
|
-
lifecycle.onFirstSubscribe?.();
|
|
665
|
-
}
|
|
666
685
|
return () => {
|
|
667
686
|
listeners.delete(listener);
|
|
668
|
-
if (listeners.size
|
|
669
|
-
lifecycle.onLastUnsubscribe?.();
|
|
670
|
-
dirty = true;
|
|
687
|
+
if (listeners.size === 0) dirty = true;
|
|
671
688
|
};
|
|
672
689
|
}
|
|
673
690
|
};
|
|
@@ -700,150 +717,97 @@ window.__ModuleLoader__.load({
|
|
|
700
717
|
return ref;
|
|
701
718
|
}
|
|
702
719
|
//#endregion
|
|
703
|
-
//#region src/client/batch-selection-store.ts
|
|
704
|
-
/**
|
|
705
|
-
* 宿主列表批量删除的**选中集现场**:多选模式 + 选中名单,一份合并快照。
|
|
706
|
-
*
|
|
707
|
-
* 从 `batch-select.tsx` 拆出来的理由:多选态与选中集是**同一次交互的两半**(进入模式才出现勾选框,
|
|
708
|
-
* 退出模式连名单一起清),读写它们的却有四个座位——菜单里的入口(`batch-menu-item.tsx`)、行首
|
|
709
|
-
* 勾选框、行尾兜底、overlay 上的浮条与确认框。状态住这一处,`batch-select.tsx` 只剩控件,
|
|
710
|
-
* `host-rows.ts` 只回答"页面上现在有哪些行、点行算不算选中"。订阅/通告那套仍只住在 `store.ts`
|
|
711
|
-
* 的 `createStore` 里,本文件不另写一份。
|
|
712
|
-
*
|
|
713
|
-
* **合并快照**而不是两个 store:两半的变化总是成对出现(`exitSelection` 清两半),订阅者
|
|
714
|
-
* (勾选框、浮条)两半都要读;分两份只会让"进模式的那一帧"出现两次通告、两个快照不同步的缝。
|
|
715
|
-
*
|
|
716
|
-
* 名单只收槽位投影原样给的 `sessionId` / `displayTitle`(浏览器半不自己拼 id):**逐条勾选序 = 名单序**;
|
|
717
|
-
* 「全选」追加的条目按 `host-rows.ts` 读到的 DOM 序接在后面。`/apply` 那条「保持输入顺序」吃到的就是这份名单序。
|
|
718
|
-
*/
|
|
719
|
-
const INITIAL = {
|
|
720
|
-
selecting: false,
|
|
721
|
-
picks: []
|
|
722
|
-
};
|
|
723
|
-
const selectionStore = createStore(INITIAL);
|
|
724
|
-
function getSelection() {
|
|
725
|
-
return selectionStore.get();
|
|
726
|
-
}
|
|
727
|
-
function getPicks() {
|
|
728
|
-
return getSelection().picks;
|
|
729
|
-
}
|
|
730
|
-
/** 供组件订阅:模式与名单是同一次交互的两半,读它们的组件订同一份快照。 */
|
|
731
|
-
function useSelection() {
|
|
732
|
-
return selectionStore.use();
|
|
733
|
-
}
|
|
734
|
-
/** 「⋯ → 多选」按下时进入;重复进入是幂等的(同一份快照,不发通告)。 */
|
|
735
|
-
function enterSelection() {
|
|
736
|
-
selectionStore.update((current) => current.selecting ? current : {
|
|
737
|
-
...current,
|
|
738
|
-
selecting: true
|
|
739
|
-
});
|
|
740
|
-
}
|
|
741
|
-
/** 退出模式并清空名单:多选态和选中集是同一次交互的两半,退出后不该留下看不见的名单。 */
|
|
742
|
-
function exitSelection() {
|
|
743
|
-
selectionStore.update((current) => current.selecting || current.picks.length > 0 ? INITIAL : current);
|
|
744
|
-
}
|
|
745
|
-
/** 翻转一条:没选过就排队尾,选过就摘掉(列表里永远不会有重复 id)。 */
|
|
746
|
-
function togglePick(item) {
|
|
747
|
-
selectionStore.update((current) => {
|
|
748
|
-
const at = current.picks.findIndex((candidate) => candidate.sessionId === item.sessionId);
|
|
749
|
-
return {
|
|
750
|
-
...current,
|
|
751
|
-
picks: at === -1 ? [...current.picks, item] : current.picks.filter((candidate) => candidate.sessionId !== item.sessionId)
|
|
752
|
-
};
|
|
753
|
-
});
|
|
754
|
-
}
|
|
755
|
-
/** 清空名单但**留在多选态**(「取消全选」那颗);已经空了就不发通告。 */
|
|
756
|
-
function clearPicks() {
|
|
757
|
-
selectionStore.update((current) => current.picks.length === 0 ? current : {
|
|
758
|
-
...current,
|
|
759
|
-
picks: []
|
|
760
|
-
});
|
|
761
|
-
}
|
|
762
|
-
/** 只追加还没选过的条目;名单序 = 原有选中在前、新条目按传入序接在后面。没有新条目就不发通告。 */
|
|
763
|
-
function addPicks(items) {
|
|
764
|
-
selectionStore.update((current) => {
|
|
765
|
-
const chosen = new Set(current.picks.map((item) => item.sessionId));
|
|
766
|
-
const additions = [];
|
|
767
|
-
for (const item of items) {
|
|
768
|
-
if (chosen.has(item.sessionId)) continue;
|
|
769
|
-
chosen.add(item.sessionId);
|
|
770
|
-
additions.push(item);
|
|
771
|
-
}
|
|
772
|
-
return additions.length === 0 ? current : {
|
|
773
|
-
...current,
|
|
774
|
-
picks: [...current.picks, ...additions]
|
|
775
|
-
};
|
|
776
|
-
});
|
|
777
|
-
}
|
|
778
|
-
/** 执行后收缩:只留失败名单(空数组 = 全清)。成功的随通告从侧栏消失,不该留在名单里。 */
|
|
779
|
-
function shrinkPicksToFailures(failedSessionIds) {
|
|
780
|
-
const keep = new Set(failedSessionIds);
|
|
781
|
-
selectionStore.update((current) => ({
|
|
782
|
-
...current,
|
|
783
|
-
picks: current.picks.filter((item) => keep.has(item.sessionId))
|
|
784
|
-
}));
|
|
785
|
-
}
|
|
786
|
-
//#endregion
|
|
787
720
|
//#region src/client/host-rows.ts
|
|
788
721
|
/**
|
|
789
|
-
*
|
|
722
|
+
* 宿主会话行的适配:**可见行由槽位组件登记**;选择动作与模式内整行点击守卫住 `batch-pick.ts`。
|
|
790
723
|
*
|
|
791
|
-
* 选中集(模式 + 名单)只住 batch-selection-store.ts
|
|
792
|
-
*
|
|
793
|
-
*
|
|
794
|
-
*
|
|
724
|
+
* 选中集(模式 + 名单)只住 `batch-selection-store.ts`;本文件只回答页面上的「当前有哪些可勾选的
|
|
725
|
+
* 会话行」:行槽位开关的挂载登记、渲染快照与按 id 的行查找。「这一行可不可勾」的 live 判据
|
|
726
|
+
* 也只住这里(`pickableRowOf` / `readVisibleSelectableRows`)。DOM 只用于守卫认行,且那处 DOM
|
|
727
|
+
* 读取随守卫住在 `batch-pick.ts`(ADR-0034 记下的受约束偏离)。
|
|
728
|
+
*
|
|
729
|
+
* 为什么不再扫 DOM:宿主的行槽位只在行可见时挂载,行首/行尾两颗开关(`BatchPickCheckbox` /
|
|
730
|
+
* `BatchPickButton`)都会登记 `{title, live}`;挂载/卸载就是「这一行在不在可见集合」的契约,
|
|
731
|
+
* 不需要 MutationObserver。
|
|
795
732
|
*/
|
|
796
733
|
/**
|
|
797
|
-
* 守卫从 `data-row-key` 反解出 sessionId,但拿不到槽位投影的标题/运行态 ⇒
|
|
798
|
-
*
|
|
799
|
-
*
|
|
800
|
-
*
|
|
734
|
+
* 守卫从 `data-row-key` 反解出 sessionId,但拿不到槽位投影的标题/运行态 ⇒ 行首/行尾开关
|
|
735
|
+
* 在 effect 里把这两件登记进这张表。生命周期跟着**行槽位组件的挂载**走:卸载即注销
|
|
736
|
+
* ({@link unregisterHostRowInfo}),表的大小被"当前渲染的行"钉住,而不是随用过的会话只增不减;
|
|
737
|
+
* 行重挂载时 effect 会重新登记。**这张表就是可见行的事实源**(ADR-0034),不再由 DOM 行列表反查。
|
|
801
738
|
*
|
|
802
|
-
* **按挂载实例计数**:同一个会话在 DOM
|
|
739
|
+
* **按挂载实例计数**:同一个会话在 DOM 里可能有两行(重复行),两行各挂一组开关;
|
|
803
740
|
* 其中一颗卸载时不能把另一颗还在用的登记一起摘掉——那会让这一行从"全选"里消失、标题也变空。
|
|
804
741
|
*/
|
|
805
742
|
const rowInfo = /* @__PURE__ */ new Map();
|
|
806
743
|
/**
|
|
807
|
-
*
|
|
808
|
-
*
|
|
809
|
-
* 不算(标题只认 `rowInfo` 的登记值)。首个订阅者起开始观察,最后一个走了断开。
|
|
744
|
+
* 渲染侧快照:`rowInfo` 变了才标脏;订阅脚手架住 `store.ts` 的 `createLazyStore`。
|
|
745
|
+
* `readVisibleSelectableRows` 按登记顺序(宿主渲染顺序)返回,不再观察 DOM。
|
|
810
746
|
*/
|
|
811
747
|
const EMPTY_ROWS = [];
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
childList: true,
|
|
818
|
-
subtree: true,
|
|
819
|
-
attributes: true,
|
|
820
|
-
attributeFilter: ["data-row-key"]
|
|
821
|
-
});
|
|
822
|
-
}
|
|
823
|
-
function stopObserving() {
|
|
824
|
-
observer?.disconnect();
|
|
825
|
-
observer = void 0;
|
|
826
|
-
}
|
|
827
|
-
const visibleRows = createLazyStore(EMPTY_ROWS, readVisibleSelectableRows, {
|
|
828
|
-
onFirstSubscribe: startObserving,
|
|
829
|
-
onLastUnsubscribe: stopObserving
|
|
830
|
-
});
|
|
748
|
+
const visibleRows = createLazyStore(EMPTY_ROWS, readVisibleSelectableRows);
|
|
749
|
+
const EMPTY_ROW_INFO = {
|
|
750
|
+
title: "",
|
|
751
|
+
live: false
|
|
752
|
+
};
|
|
831
753
|
/**
|
|
832
|
-
*
|
|
833
|
-
*
|
|
834
|
-
* 不该把快照翻新一遍。
|
|
754
|
+
* 行槽位开关登记一行:模式内与模式外都要登记(守卫进模式的那一帧就得有标题)。
|
|
755
|
+
* 每次登记算**多一个挂载实例**;不给 info 时只占位,随后由 {@link updateHostRowInfo} 填值。
|
|
835
756
|
*/
|
|
836
757
|
function registerHostRowInfo(sessionId, info) {
|
|
837
758
|
const current = rowInfo.get(sessionId);
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
759
|
+
if (current === void 0) {
|
|
760
|
+
rowInfo.set(sessionId, {
|
|
761
|
+
info: info ?? EMPTY_ROW_INFO,
|
|
762
|
+
owners: 1
|
|
763
|
+
});
|
|
764
|
+
visibleRows.invalidate();
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
current.owners += 1;
|
|
768
|
+
if (info !== void 0 && (current.info.title !== info.title || current.info.live !== info.live)) {
|
|
769
|
+
current.info = info;
|
|
770
|
+
visibleRows.invalidate();
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* 标题/运行态变了就地更新:**不注销、不重登记**,否则这一行会被挪到登记序末尾(ADR-0034 决定 2)。
|
|
775
|
+
*/
|
|
776
|
+
function updateHostRowInfo(sessionId, info) {
|
|
777
|
+
const current = rowInfo.get(sessionId);
|
|
778
|
+
if (current === void 0) return;
|
|
779
|
+
const title = info.title === "" ? current.info.title : info.title;
|
|
780
|
+
if (current.info.title === title && current.info.live === info.live) return;
|
|
781
|
+
current.info = {
|
|
782
|
+
title,
|
|
783
|
+
live: info.live
|
|
784
|
+
};
|
|
785
|
+
visibleRows.invalidate();
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* 槽位组件共用的登记生命周期:挂载登记、卸载注销;标题/运行态变化只走更新口。
|
|
789
|
+
* 两次 effect 分工是顺序契约:先占住首次登记序,再填值。
|
|
790
|
+
*/
|
|
791
|
+
function useHostRowRegistration(sessionId, info) {
|
|
792
|
+
(0, react.useEffect)(() => {
|
|
793
|
+
registerHostRowInfo(sessionId);
|
|
794
|
+
return () => {
|
|
795
|
+
unregisterHostRowInfo(sessionId);
|
|
796
|
+
};
|
|
797
|
+
}, [sessionId]);
|
|
798
|
+
const { title, live } = info;
|
|
799
|
+
(0, react.useEffect)(() => {
|
|
800
|
+
updateHostRowInfo(sessionId, {
|
|
801
|
+
title,
|
|
802
|
+
live
|
|
803
|
+
});
|
|
804
|
+
}, [
|
|
805
|
+
sessionId,
|
|
806
|
+
title,
|
|
807
|
+
live
|
|
808
|
+
]);
|
|
845
809
|
}
|
|
846
|
-
/**
|
|
810
|
+
/** 行槽位开关卸载时调用:那一行已经离开渲染树,登记跟着走。
|
|
847
811
|
* 还有别的挂载实例在用同一行(重复行)就只减计数,不摘登记。 */
|
|
848
812
|
function unregisterHostRowInfo(sessionId) {
|
|
849
813
|
const current = rowInfo.get(sessionId);
|
|
@@ -855,24 +819,29 @@ window.__ModuleLoader__.load({
|
|
|
855
819
|
rowInfo.delete(sessionId);
|
|
856
820
|
visibleRows.invalidate();
|
|
857
821
|
}
|
|
822
|
+
/** `live` 行不可勾:与置灰的勾选框、守卫拦下的整行点击同一条判据。 */
|
|
823
|
+
function isPickable(info) {
|
|
824
|
+
return !info.live;
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* 守卫与全选共用的一行读数:这一行此刻可勾选时交回登记(含标题);未登记或在跑回 `undefined`。
|
|
828
|
+
* 登记表是可见行的事实源,所以"没登记"就是"这一行此刻不在可见集合里"。
|
|
829
|
+
*/
|
|
830
|
+
function pickableRowOf(sessionId) {
|
|
831
|
+
const entry = rowInfo.get(sessionId);
|
|
832
|
+
return entry === void 0 || !isPickable(entry.info) ? void 0 : entry.info;
|
|
833
|
+
}
|
|
858
834
|
/**
|
|
859
|
-
*
|
|
860
|
-
*
|
|
861
|
-
*
|
|
835
|
+
* 「当前可见」的可选行:**已登记**的会话行(槽位开关还挂在渲染树上)——折叠组、归档过滤外的行
|
|
836
|
+
* 没有挂载,因此没有登记;在跑的行跳过(与置灰勾选框同口径)。标题/运行态来自登记值;
|
|
837
|
+
* 顺序是登记顺序(宿主按列表顺序挂载行)。
|
|
862
838
|
*
|
|
863
839
|
* 这是**当场读**:事件路径(点「全选」那一刻的页面)与测试用它;渲染侧读下面的快照。
|
|
864
840
|
*/
|
|
865
841
|
function readVisibleSelectableRows() {
|
|
866
842
|
const rows = [];
|
|
867
|
-
const
|
|
868
|
-
|
|
869
|
-
const key = row.getAttribute("data-row-key") ?? "";
|
|
870
|
-
if (!key.startsWith("session:")) continue;
|
|
871
|
-
const sessionId = key.slice(8);
|
|
872
|
-
if (sessionId === "" || seen.has(sessionId)) continue;
|
|
873
|
-
const entry = rowInfo.get(sessionId);
|
|
874
|
-
if (entry === void 0 || entry.info.live) continue;
|
|
875
|
-
seen.add(sessionId);
|
|
843
|
+
for (const [sessionId, entry] of rowInfo) {
|
|
844
|
+
if (!isPickable(entry.info)) continue;
|
|
876
845
|
rows.push({
|
|
877
846
|
sessionId,
|
|
878
847
|
title: entry.info.title
|
|
@@ -881,9 +850,8 @@ window.__ModuleLoader__.load({
|
|
|
881
850
|
return rows;
|
|
882
851
|
}
|
|
883
852
|
/**
|
|
884
|
-
* `enabled === false`
|
|
885
|
-
* 浮条是 `shell.overlay`
|
|
886
|
-
* 都会白叫醒它重渲染一次。
|
|
853
|
+
* `enabled === false` 时的稳定替身:同一个空表、不订阅、不读登记。
|
|
854
|
+
* 浮条是 `shell.overlay` 的常驻槽位,模式外若照常订阅,每次行登记都会白叫醒它重渲染一次。
|
|
887
855
|
*/
|
|
888
856
|
function getEmptyRowsSnapshot() {
|
|
889
857
|
return EMPTY_ROWS;
|
|
@@ -892,86 +860,17 @@ window.__ModuleLoader__.load({
|
|
|
892
860
|
return () => {};
|
|
893
861
|
}
|
|
894
862
|
/**
|
|
895
|
-
* 渲染侧只读快照 +
|
|
896
|
-
*
|
|
863
|
+
* 渲染侧只读快照 + 登记订阅;`enabled === false` 不订阅也不读登记——行列表只随多选态存在,
|
|
864
|
+
* 模式外不必付订阅的成本。
|
|
897
865
|
*/
|
|
898
866
|
function useVisibleSelectableRows(enabled) {
|
|
899
867
|
return (0, react.useSyncExternalStore)(enabled ? visibleRows.subscribe : noopSubscribe, enabled ? visibleRows.get : getEmptyRowsSnapshot, enabled ? visibleRows.get : getEmptyRowsSnapshot);
|
|
900
868
|
}
|
|
901
|
-
/** 全选:把当前可见可选行按 DOM 序追加;已在名单里的由 `addPicks` 自己跳过,原有选中保持在前。 */
|
|
902
|
-
function selectAllVisible() {
|
|
903
|
-
addPicks(readVisibleSelectableRows());
|
|
904
|
-
}
|
|
905
|
-
/**
|
|
906
|
-
* 同一个切换口该显示「全选」还是「取消全选」:
|
|
907
|
-
* - 有选中、且当前可见可选行全都选中 ⇒ 取消全选;
|
|
908
|
-
* - 有选中、但当前没有可见可选行(全折叠/过滤掉)⇒ 只能清空,也显示取消全选;
|
|
909
|
-
* - 其余(含 0 条)⇒ 全选。
|
|
910
|
-
*/
|
|
911
|
-
function areAllVisibleRowsSelected(rows, picks) {
|
|
912
|
-
if (picks.length === 0) return false;
|
|
913
|
-
if (rows.length === 0) return true;
|
|
914
|
-
const chosen = new Set(picks.map((item) => item.sessionId));
|
|
915
|
-
return rows.every((row) => chosen.has(row.sessionId));
|
|
916
|
-
}
|
|
917
|
-
/** 行内这些元素有自己的动作:模式内也不许被"点行=勾选"吃掉。 */
|
|
918
|
-
const INTERACTIVE_IN_ROW = "button, a, input, select, textarea, [role=\"menuitem\"], [role=\"menu\"], [data-dsh-session-delete=\"batch-pick\"]";
|
|
919
869
|
/**
|
|
920
|
-
*
|
|
921
|
-
*
|
|
922
|
-
* 边界:行里的按钮 / 链接 / 输入 / 菜单项一律放行(否则 ⋯ 菜单、归档、置顶、单删都点不动);
|
|
923
|
-
* 双击的第二下与 dblclick 不翻转(第一下已经选中,第二下不能又反掉,也不能打开宿主的重命名)。
|
|
924
|
-
* 认行靠 `data-row-key="session:…"`——宿主将来改掉这个属性,最差是模式内点行不生效,
|
|
925
|
-
* 勾选框本身仍在。模式外不挂监听:这时点行必须还是宿主的"打开会话"。
|
|
870
|
+
* 唯一的取数基础路径:宿主 `/api` + 插件前缀。九段 exact Fetch 路由、客户端探测与附件 URL
|
|
871
|
+
* 都从它起;`SESSION_DELETE_API_PREFIX` 只作为段名的相对前缀与错误原文。
|
|
926
872
|
*/
|
|
927
|
-
|
|
928
|
-
const selecting = useSelection().selecting;
|
|
929
|
-
(0, react.useEffect)(() => {
|
|
930
|
-
if (!selecting) return;
|
|
931
|
-
const handle = (event) => {
|
|
932
|
-
if (!(event.target instanceof Element)) return;
|
|
933
|
-
const row = event.target.closest("[data-row-key^=\"session:\"]");
|
|
934
|
-
if (row === null) return;
|
|
935
|
-
if (event.target.closest(INTERACTIVE_IN_ROW) !== null) return;
|
|
936
|
-
event.preventDefault();
|
|
937
|
-
event.stopPropagation();
|
|
938
|
-
if (event.type === "dblclick" || event.type === "click" && event.detail > 1) return;
|
|
939
|
-
const key = row.getAttribute("data-row-key") ?? "";
|
|
940
|
-
if (!key.startsWith("session:")) return;
|
|
941
|
-
const sessionId = key.slice(8);
|
|
942
|
-
if (sessionId === "") return;
|
|
943
|
-
const entry = rowInfo.get(sessionId);
|
|
944
|
-
if (entry?.info.live === true) return;
|
|
945
|
-
togglePick({
|
|
946
|
-
sessionId,
|
|
947
|
-
title: entry?.info.title ?? ""
|
|
948
|
-
});
|
|
949
|
-
};
|
|
950
|
-
document.addEventListener("click", handle, true);
|
|
951
|
-
document.addEventListener("dblclick", handle, true);
|
|
952
|
-
return () => {
|
|
953
|
-
document.removeEventListener("click", handle, true);
|
|
954
|
-
document.removeEventListener("dblclick", handle, true);
|
|
955
|
-
};
|
|
956
|
-
}, [selecting]);
|
|
957
|
-
}
|
|
958
|
-
//#endregion
|
|
959
|
-
//#region src/shared/wire.ts
|
|
960
|
-
/**
|
|
961
|
-
* 跨半 wire 契约:前缀、九个端点、请求/响应形状与失败码。
|
|
962
|
-
* Node 半(src/api/dispatch.ts)与浏览器半(src/client/remote.ts)共用这一份,不各写字面量。
|
|
963
|
-
*
|
|
964
|
-
* 构建约束:本文件零运行时依赖、无 import;浏览器半把整个文件 inline 进 bundle。
|
|
965
|
-
* 信封 {ok:true,value} / {ok:false,error:{code,message,details?}} 也是两半的共同契约。
|
|
966
|
-
*/
|
|
967
|
-
/**
|
|
968
|
-
* 插件专属前缀。
|
|
969
|
-
*
|
|
970
|
-
* 为什么必须带插件名:宿主 web server 的 `register({kind:'prefix', path})` 对**重复的 (kind, path)
|
|
971
|
-
* 直接抛错**(`dsh-host-webserver` 原文 `webserver: duplicate ${route.kind} route "${route.path}"`),
|
|
972
|
-
* 撞了就是整棵插件树 boot 失败——所以这条路径是与生态的硬边界,不是显示好不好看的问题。
|
|
973
|
-
*/
|
|
974
|
-
const SESSION_DELETE_API_PREFIX = "/dsh-session-delete-api";
|
|
873
|
+
const OFFICIAL_SESSION_DELETE_API_PREFIX = `/api/dsh-session-delete-api`;
|
|
975
874
|
/** 九个段名(相对前缀)。**每项 `{ path, method }` 两键**:某段用什么方法也是线上形状的一部分
|
|
976
875
|
* (ADR-0003),家就在这一张表——路由记录(`src/api/route-table.ts`)逐行投影(`ROUTES.x.method`),
|
|
977
876
|
* 浏览器半的 GET 段同样投影、写段把 `ROUTES.x.method` 交给 `src/client/remote.ts` 的 `post()`,
|
|
@@ -1044,7 +943,14 @@ window.__ModuleLoader__.load({
|
|
|
1044
943
|
ids: "ids",
|
|
1045
944
|
attachmentId: "id",
|
|
1046
945
|
attachmentKind: "kind",
|
|
1047
|
-
attachmentName: "name"
|
|
946
|
+
attachmentName: "name",
|
|
947
|
+
/** 图片引用的媒体类型 / 宽高:只给宿主 `attachments.readImage` 做引用校验,不参与路径拼接。 */
|
|
948
|
+
attachmentMediaType: "mediaType",
|
|
949
|
+
/** 字节数:图片支与文件支都带——它是宿主读面的引用一部分,并先过 `maxAttachmentBytes` 闸
|
|
950
|
+
* (别为一张已知超限的图让宿主整份读出来)。 */
|
|
951
|
+
attachmentBytes: "bytes",
|
|
952
|
+
attachmentWidth: "width",
|
|
953
|
+
attachmentHeight: "height"
|
|
1048
954
|
};
|
|
1049
955
|
/**
|
|
1050
956
|
* 进程内判别「这已经是一次失败」——**按标记位认,不按 `instanceof`**(跨 realm / 跨 bundle 都不成立)。
|
|
@@ -1101,6 +1007,38 @@ window.__ModuleLoader__.load({
|
|
|
1101
1007
|
return encoded.replace(/~([0-9A-F]{4})/g, (_whole, hex) => String.fromCharCode(parseInt(hex, 16)));
|
|
1102
1008
|
}
|
|
1103
1009
|
//#endregion
|
|
1010
|
+
//#region src/client/api-base.ts
|
|
1011
|
+
/**
|
|
1012
|
+
* 取数路径的挂载点:官方 `/api` 前缀 + **当前文档所在的目录**。
|
|
1013
|
+
*
|
|
1014
|
+
* 为什么不能写死根绝对路径:宿主自己的客户端模块按「app-directory-relative form」下发 bundle 与
|
|
1015
|
+
* chunk 描述符,浏览器在**自己的挂载点**下解析(`dsh-client-modules` README 原文:*"which a browser
|
|
1016
|
+
* document resolves under its own mount"*);同一个部署可以被反代到 `/dsh/`,此时 `/api/...` 会打到
|
|
1017
|
+
* 反代根、而不是宿主。路由注册侧仍然必须写 `/api/...`(宿主 `connection.fetch.register` 用
|
|
1018
|
+
* `endpointFromPath('/api', path)` 校验),所以本文件只给**浏览器半**投影一层挂载点前缀。
|
|
1019
|
+
*
|
|
1020
|
+
* 根部署(`http://host/`)下 `mountPathOf` 返回空串,拼出来的 URL 与旧版逐字相同;只有挂载部署才
|
|
1021
|
+
* 多出 `/dsh` 这一段。目录取自 `new URL('.', baseURI)`:页面是 `/dsh/`、`/dsh/index.html` 都归到
|
|
1022
|
+
* `/dsh/`;`about:blank` 这类不透明 base 退回根路径(此时相对地址本来也无法成立)。
|
|
1023
|
+
*/
|
|
1024
|
+
/** `baseURI` 所在目录的路径,去掉尾斜杠;根路径回空串。 */
|
|
1025
|
+
function mountPathOf(baseURI) {
|
|
1026
|
+
try {
|
|
1027
|
+
const dir = new URL(".", baseURI).pathname;
|
|
1028
|
+
return dir === "/" ? "" : dir.replace(/\/$/, "");
|
|
1029
|
+
} catch {
|
|
1030
|
+
return "";
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
/** 一段相对段名 → 宿主 `/api` 前缀 + 挂载点。`path` 以 `/` 开头,与 `ROUTES` 的形状一致。 */
|
|
1034
|
+
function apiPathOf(path, baseURI) {
|
|
1035
|
+
return `${mountPathOf(baseURI)}${OFFICIAL_SESSION_DELETE_API_PREFIX}${path}`;
|
|
1036
|
+
}
|
|
1037
|
+
/** 当前文档的取数路径;Node 侧(无 `document`)退回根路径。 */
|
|
1038
|
+
function apiPath(path) {
|
|
1039
|
+
return apiPathOf(path, typeof document === "undefined" ? "http://localhost/" : document.baseURI);
|
|
1040
|
+
}
|
|
1041
|
+
//#endregion
|
|
1104
1042
|
//#region src/client/receipt.ts
|
|
1105
1043
|
/** 浏览器半自己的未分类诊断码(通道打不通 / 非信封 / 单条协议错)。它不过线,线上失败码仍归 `shared/wire.ts`。 */
|
|
1106
1044
|
const CLIENT_UNKNOWN_CODE = "client/unknown";
|
|
@@ -1322,15 +1260,19 @@ window.__ModuleLoader__.load({
|
|
|
1322
1260
|
//#endregion
|
|
1323
1261
|
//#region src/client/remote.ts
|
|
1324
1262
|
/**
|
|
1325
|
-
* 浏览器半的取数通道:同源 fetch
|
|
1263
|
+
* 浏览器半的取数通道:同源 fetch 宿主 `connection.fetch` 的 exact Fetch 路由(`/api` + 插件前缀)。
|
|
1264
|
+
* 信封与失败原文在这一层收口。
|
|
1326
1265
|
*
|
|
1327
|
-
* -
|
|
1328
|
-
*
|
|
1266
|
+
* - 九段注册在宿主 `connection.fetch.register` 上,鉴权归宿主 `/api` bridge(ADR-0033);
|
|
1267
|
+
* 没有旧前缀 fallback——支持面是 web UI 与桌面端,二者的标准组合都装 `client-connection`。
|
|
1268
|
+
* 官方 remote.<namespace> 仍不可用(第三方拿不到 Typert 描述符的动态发现挂载点)。
|
|
1329
1269
|
* - 九段的**段名与方法**住 `src/shared/wire.ts` 的结构化段名表(`ROUTES` 每项 `{ path, method }`),
|
|
1330
|
-
*
|
|
1331
|
-
*
|
|
1332
|
-
*
|
|
1333
|
-
*
|
|
1270
|
+
* 本文件只投影段名与方法(GET 段直接取 `ROUTES.x.method`,写段把它交给 `post()`)——方法字面量
|
|
1271
|
+
* 不在本文件出现第二份;取数地址(官方 `/api` 前缀 + 文档挂载点)归 `src/client/api-base.ts`
|
|
1272
|
+
* (根部署下与旧版逐字相同)。
|
|
1273
|
+
* 写动作统一走 apply;`attachment` 是唯一二进制段,由 `src/client/attachment-view.tsx` 经同一投影
|
|
1274
|
+
* 拼地址,不经过本文件的信封通道;`attachment-orphans`(纯读)与 `attachment-clean`(聚合写)是孤儿
|
|
1275
|
+
* 附件的两段。
|
|
1334
1276
|
* - 信封与官方 unary 面同形:{ok:true,value} / {ok:false,error}。HTTP 往返本身失败才 reject;
|
|
1335
1277
|
* 业务失败以信封返回,由 unwrapRemoteResult 在注入面抛出。
|
|
1336
1278
|
* - error 在线上是 {code,message,details?} 普通对象;判别依据 isDSHRemoteError 住在
|
|
@@ -1396,11 +1338,14 @@ window.__ModuleLoader__.load({
|
|
|
1396
1338
|
};
|
|
1397
1339
|
}
|
|
1398
1340
|
}
|
|
1341
|
+
/** 宿主 `/api` 的 401/403 是纯文本,不是本插件信封:固定中文原文只住这里(浏览器半唯一消费者)。 */
|
|
1342
|
+
const UNTRUSTED_MESSAGE = "非受信来源";
|
|
1343
|
+
const UNAUTHENTICATED_MESSAGE = "需要宿主的 Web 身份:请用 dsh web 打印的那一行 URL 打开页面后重试";
|
|
1399
1344
|
/**
|
|
1400
1345
|
* 一次 HTTP 往返 → 信封。**`ok:false` 也是落地**(返回,不抛);只有往返本身失败才抛:
|
|
1401
|
-
* 打不通(宿主没起来 /
|
|
1402
|
-
*
|
|
1403
|
-
*
|
|
1346
|
+
* 打不通(宿主没起来 / 路由没挂)或回的不是信封(中间层回 `{}` / `{"error":"…"}` 这类非信封 JSON,
|
|
1347
|
+
* 或 SPA 兜底的 index.html——判形住 `isRemoteResult`,几种都落同一条诊断)。两种抛都带原文,
|
|
1348
|
+
* 组件侧的 `failureOf` 会把它们显示成 `client/unknown` + 原因。
|
|
1404
1349
|
*/
|
|
1405
1350
|
async function requestEnvelope(path, init) {
|
|
1406
1351
|
let response;
|
|
@@ -1415,51 +1360,54 @@ window.__ModuleLoader__.load({
|
|
|
1415
1360
|
} catch {
|
|
1416
1361
|
body = void 0;
|
|
1417
1362
|
}
|
|
1418
|
-
if (!isRemoteResult(body))
|
|
1363
|
+
if (!isRemoteResult(body)) {
|
|
1364
|
+
if (response.status === 401) throw new Error(UNAUTHENTICATED_MESSAGE);
|
|
1365
|
+
if (response.status === 403) throw new Error(UNTRUSTED_MESSAGE);
|
|
1366
|
+
throw new Error(`取数通道回的不是信封(HTTP ${String(response.status)})`);
|
|
1367
|
+
}
|
|
1419
1368
|
return body;
|
|
1420
1369
|
}
|
|
1421
1370
|
/**
|
|
1422
|
-
*
|
|
1423
|
-
*
|
|
1424
|
-
*
|
|
1425
|
-
*
|
|
1371
|
+
* exact Fetch 路由的 fetch 实现。**同源、带挂载点前缀**:不拼 host、不认 origin——页面本身就是从
|
|
1372
|
+
* 宿主页面加载的,同源是这条通道的全部前提;路径只由 `api-base.ts` 从 `document.baseURI` 的目录
|
|
1373
|
+
* 与官方 `/api` 前缀拼出(根部署下与旧版逐字相同,反代到 `/dsh/` 时得到 `/dsh/api/...`)。
|
|
1374
|
+
* 鉴权归宿主 `/api` bridge(ADR-0033)。所以这条通道**只对**宿主页面打开过、带宿主身份的客户端
|
|
1375
|
+
* 成立(凭据是那次访问换来的 cookie,不是插件自己的秘密)。
|
|
1426
1376
|
*/
|
|
1427
|
-
function createHttpRemote(
|
|
1377
|
+
function createHttpRemote() {
|
|
1428
1378
|
const post = (route, body) => ({
|
|
1429
1379
|
method: route.method,
|
|
1430
1380
|
headers: { "content-type": "application/json" },
|
|
1431
1381
|
body: JSON.stringify(body)
|
|
1432
1382
|
});
|
|
1433
1383
|
return {
|
|
1434
|
-
list: () => requestEnvelope(
|
|
1435
|
-
ping: () => requestEnvelope(
|
|
1436
|
-
orphans: () => requestEnvelope(
|
|
1437
|
-
attachmentOrphans: () => requestEnvelope(
|
|
1438
|
-
attachmentClean: () => requestEnvelope(
|
|
1439
|
-
detail: (entryId, before) => requestEnvelope(
|
|
1384
|
+
list: () => requestEnvelope(apiPath(ROUTES.list.path), { method: ROUTES.list.method }),
|
|
1385
|
+
ping: () => requestEnvelope(apiPath(ROUTES.ping.path), { method: ROUTES.ping.method }),
|
|
1386
|
+
orphans: () => requestEnvelope(apiPath(ROUTES.orphans.path), { method: ROUTES.orphans.method }),
|
|
1387
|
+
attachmentOrphans: () => requestEnvelope(apiPath(ROUTES.attachmentOrphans.path), { method: ROUTES.attachmentOrphans.method }),
|
|
1388
|
+
attachmentClean: () => requestEnvelope(apiPath(ROUTES.attachmentClean.path), post(ROUTES.attachmentClean, {})),
|
|
1389
|
+
detail: (entryId, before) => requestEnvelope(apiPath(ROUTES.detail.path), post(ROUTES.detail, {
|
|
1440
1390
|
[PARAMS.entryId]: entryId,
|
|
1441
1391
|
...before === void 0 ? {} : { [PARAMS.before]: before }
|
|
1442
1392
|
})),
|
|
1443
|
-
apply: (action, ids) => requestEnvelope(
|
|
1393
|
+
apply: (action, ids) => requestEnvelope(apiPath(ROUTES.apply.path), post(ROUTES.apply, {
|
|
1444
1394
|
[PARAMS.action]: action,
|
|
1445
1395
|
[PARAMS.ids]: [...ids]
|
|
1446
1396
|
})),
|
|
1447
|
-
empty: () => requestEnvelope(
|
|
1397
|
+
empty: () => requestEnvelope(apiPath(ROUTES.empty.path), post(ROUTES.empty, {}))
|
|
1448
1398
|
};
|
|
1449
1399
|
}
|
|
1450
1400
|
/**
|
|
1451
|
-
*
|
|
1452
|
-
*
|
|
1453
|
-
* 判据是**「拿到信封」而不是「操作成功」**:`{ok:false, error}` 也是一次成功的往返(路由在、依赖接上了、
|
|
1454
|
-
* 错误映射也生效了),那时该做的是把入口显示出来、让原文上屏;只有往返本身失败才算通道缺席 ⇒
|
|
1455
|
-
* 一个槽位贡献都不注册(docs/architecture.md §3「通道探测决定入口出现与否」)。
|
|
1401
|
+
* 探测唯一通道:一次 `GET /ping` 回成功信封(`pong`)才注册入口。
|
|
1456
1402
|
*
|
|
1457
|
-
*
|
|
1403
|
+
* - 401/403 是宿主 `/api` 的纯文本身份/归属失败:入口不出现,不装禁用态。
|
|
1404
|
+
* - 失败信封 / 404 / 非信封同样按不可用处理:`/api` 网关对未知路径也可能回错误形状,
|
|
1405
|
+
* 不能据此把入口摆出来让每个动作都失败。
|
|
1458
1406
|
*/
|
|
1459
1407
|
async function probeChannel(raw) {
|
|
1460
1408
|
try {
|
|
1461
|
-
await raw.ping();
|
|
1462
|
-
return
|
|
1409
|
+
const result = await raw.ping();
|
|
1410
|
+
return result.ok && result.value === "pong";
|
|
1463
1411
|
} catch {
|
|
1464
1412
|
return false;
|
|
1465
1413
|
}
|
|
@@ -1509,6 +1457,93 @@ window.__ModuleLoader__.load({
|
|
|
1509
1457
|
};
|
|
1510
1458
|
}
|
|
1511
1459
|
//#endregion
|
|
1460
|
+
//#region src/client/batch-selection-store.ts
|
|
1461
|
+
/**
|
|
1462
|
+
* 宿主列表批量删除的**选中集现场**:多选模式 + 选中名单,一份合并快照。
|
|
1463
|
+
*
|
|
1464
|
+
* 从 `batch-select.tsx` 拆出来的理由:多选态与选中集是**同一次交互的两半**(进入模式才出现勾选框,
|
|
1465
|
+
* 退出模式连名单一起清),读写它们的却有四个座位——菜单里的入口(`batch-menu-item.tsx`)、行首
|
|
1466
|
+
* 勾选框、行尾兜底、overlay 上的浮条与确认框。状态住这一处,`batch-select.tsx` 只剩控件,
|
|
1467
|
+
* `host-rows.ts` 只回答"页面上现在有哪些行、点行算不算选中"。订阅/通告那套仍只住在 `store.ts`
|
|
1468
|
+
* 的 `createStore` 里,本文件不另写一份。
|
|
1469
|
+
*
|
|
1470
|
+
* **合并快照**而不是两个 store:两半的变化总是成对出现(`exitSelection` 清两半),订阅者
|
|
1471
|
+
* (勾选框、浮条)两半都要读;分两份只会让"进模式的那一帧"出现两次通告、两个快照不同步的缝。
|
|
1472
|
+
*
|
|
1473
|
+
* 名单只收槽位投影原样给的 `sessionId` / `displayTitle`(浏览器半不自己拼 id):**逐条勾选序 = 名单序**;
|
|
1474
|
+
* 「全选」追加的条目按 `host-rows.ts` 的槽位登记序接在后面。`/apply` 那条「保持输入顺序」吃到的就是这份名单序。
|
|
1475
|
+
*/
|
|
1476
|
+
const INITIAL = {
|
|
1477
|
+
selecting: false,
|
|
1478
|
+
picks: []
|
|
1479
|
+
};
|
|
1480
|
+
const selectionStore = createStore(INITIAL);
|
|
1481
|
+
function getSelection() {
|
|
1482
|
+
return selectionStore.get();
|
|
1483
|
+
}
|
|
1484
|
+
function getPicks() {
|
|
1485
|
+
return getSelection().picks;
|
|
1486
|
+
}
|
|
1487
|
+
function isSelecting() {
|
|
1488
|
+
return getSelection().selecting;
|
|
1489
|
+
}
|
|
1490
|
+
/** 供组件订阅:模式与名单是同一次交互的两半,读它们的组件订同一份快照。 */
|
|
1491
|
+
function useSelection() {
|
|
1492
|
+
return selectionStore.use();
|
|
1493
|
+
}
|
|
1494
|
+
/** 「⋯ → 多选」按下时进入;重复进入是幂等的(同一份快照,不发通告)。 */
|
|
1495
|
+
function enterSelection() {
|
|
1496
|
+
selectionStore.update((current) => current.selecting ? current : {
|
|
1497
|
+
...current,
|
|
1498
|
+
selecting: true
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1501
|
+
/** 退出模式并清空名单:多选态和选中集是同一次交互的两半,退出后不该留下看不见的名单。 */
|
|
1502
|
+
function exitSelection() {
|
|
1503
|
+
selectionStore.update((current) => current.selecting || current.picks.length > 0 ? INITIAL : current);
|
|
1504
|
+
}
|
|
1505
|
+
/** 翻转一条:没选过就排队尾,选过就摘掉(列表里永远不会有重复 id)。 */
|
|
1506
|
+
function togglePick(item) {
|
|
1507
|
+
selectionStore.update((current) => {
|
|
1508
|
+
const at = current.picks.findIndex((candidate) => candidate.sessionId === item.sessionId);
|
|
1509
|
+
return {
|
|
1510
|
+
...current,
|
|
1511
|
+
picks: at === -1 ? [...current.picks, item] : current.picks.filter((candidate) => candidate.sessionId !== item.sessionId)
|
|
1512
|
+
};
|
|
1513
|
+
});
|
|
1514
|
+
}
|
|
1515
|
+
/** 清空名单但**留在多选态**(「取消全选」那颗);已经空了就不发通告。 */
|
|
1516
|
+
function clearPicks() {
|
|
1517
|
+
selectionStore.update((current) => current.picks.length === 0 ? current : {
|
|
1518
|
+
...current,
|
|
1519
|
+
picks: []
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1522
|
+
/** 只追加还没选过的条目;名单序 = 原有选中在前、新条目按传入序接在后面。没有新条目就不发通告。 */
|
|
1523
|
+
function addPicks(items) {
|
|
1524
|
+
selectionStore.update((current) => {
|
|
1525
|
+
const chosen = new Set(current.picks.map((item) => item.sessionId));
|
|
1526
|
+
const additions = [];
|
|
1527
|
+
for (const item of items) {
|
|
1528
|
+
if (chosen.has(item.sessionId)) continue;
|
|
1529
|
+
chosen.add(item.sessionId);
|
|
1530
|
+
additions.push(item);
|
|
1531
|
+
}
|
|
1532
|
+
return additions.length === 0 ? current : {
|
|
1533
|
+
...current,
|
|
1534
|
+
picks: [...current.picks, ...additions]
|
|
1535
|
+
};
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1538
|
+
/** 执行后收缩:只留失败名单(空数组 = 全清)。成功的随通告从侧栏消失,不该留在名单里。 */
|
|
1539
|
+
function shrinkPicksToFailures(failedSessionIds) {
|
|
1540
|
+
const keep = new Set(failedSessionIds);
|
|
1541
|
+
selectionStore.update((current) => ({
|
|
1542
|
+
...current,
|
|
1543
|
+
picks: current.picks.filter((item) => keep.has(item.sessionId))
|
|
1544
|
+
}));
|
|
1545
|
+
}
|
|
1546
|
+
//#endregion
|
|
1512
1547
|
//#region src/client/ui/Checkbox.tsx
|
|
1513
1548
|
/**
|
|
1514
1549
|
* 勾选框,回收站多选态用。
|
|
@@ -1677,10 +1712,10 @@ window.__ModuleLoader__.load({
|
|
|
1677
1712
|
}
|
|
1678
1713
|
//#endregion
|
|
1679
1714
|
//#region src/client/ui/ui.css?dsh-css-text
|
|
1680
|
-
var ui_default = "/*\n * 本插件自己的控件样式(**不再 require 宿主的 `@deepseek-ai/dsh-client-ui-primitives`**)。\n *\n * 出处:逐条抄自 `@deepseek-ai/dsh-client-ui-primitives@0.2.0-rc.2` 随包发布的明文 CSS\n * (`lib/Button.module.css`、`Tag.module.css`、`Modal.module.css`、`Tooltip.module.css`、\n * `Toast.module.css`、`Menu.module.css`;2026-10-15 又加两张:`lib/Checkbox.module.css`、\n * `lib/DisclosureRow.module.css`,以及 `dsh-client-ui-conversation` 里的\n * `ConversationRoot.module.css` 的 `.widthHandle` 那一族——宿主自己那根可拖的宽度把手;\n * 2026-10-17 又加一张,也是首张来自 ui-primitives 之外的包:`dsh-client-ui-workspace` 的\n * `Rows.module.css` 的 `.iconButton` 那一族——会话行尾悬停排那颗按钮的基底(行尾兜底勾选开关\n * 要跟宿主自己的归档/置顶同形)。\n * 这是官方给第三方 UI 插件的处方:\n * 「copy markup, CSS, and behavior from the primitive into the plugin … Rename copied classes under\n * your plugin's prefix, keep only `--dsw-alias-*` token references」(宿主技能\n * `cordis-plugin-development/references/practices.md` 原文)。理由是那个包**随宿主换代而变、且不经类型检查**,\n * 一个抛错的组件会把整个槽位条目打空(控制台 `slot entry crashed in '<slot>'`)。\n *\n * 三条改写规则:\n * 1. **类名全部加本插件前缀 `sd-`**(原来是 CSS module 的哈希名,全局无冲突由前缀承担)。\n * 2. **配色一律走 token**,一个颜色的字面量都没有——所以宿主换主题、改名 token 时最差是难看,\n * 不会崩。几何量(圆角/阴影/层级)同样取 token;**只有字号/行高/尺寸是字面量 px**,与官方 CSS 逐字一致。\n * 3. **`@keyframes` 全部改名**(`sd-` 前缀):它们是全局命名空间,撞名会互相覆盖。\n *\n * 只收本插件真正用到的部分:Button 的 `primary`/`ghost`/`outline` 三档与 `md`/`sm` 两号\n * (没有 `toolbar` 变体、没有 `lg`)、Tag 的 `danger` 一档、Modal 的全部骨架、Tooltip 的三个方位、\n * Toast 全部、Menu 行的全部(含 danger 态与 separator)、Checkbox 的全部、DisclosureRow 的\n * 折叠头那一半(没有 `data-state` 那档状态点)、宽度把手的视觉与光标。**没抄进来的就是本插件不用的**,\n * 不预先铺开——宿主那套里还有几十个变体与控件,抄全了等于把它的表面积整个搬进来。\n *\n * **七处不是抄本**:① `.sd-resizing`(拖动期间挂在面板根上、关掉文字选中)——宿主那份没有对应的类,\n * 它的把手由 JS 侧的 pointer capture 承担;② `[data-dsh-session-delete=\"trash-row\"]:focus-visible`\n * (清单那一行的键盘焦点描边,复用选中态那枚 `ghost-active-border`)——宿主那套表里行不是可聚焦\n * div,我们这一行是 `role=\"row\"` 且要吃 ↑↓,而这一代 `--dsw-focus-ring-color` 是 `transparent`;\n * ③ `.sd-rowaction-button` 的 `[aria-pressed='true']` 与 `:disabled` 两档(2026-10-17)——\n * 官方那颗 `iconButton` 只有常态与 hover,勾选开关要有\"按下\"和\"这条删不动\"两档,token 与回收站\n * 选中行同源(`--dsw-alias-button-ghost-active-*`);④ `.sd-batch-bar`(同日)——宿主没有\"浮条\"\n * 这件控件,四件 token 自拼;⑤ 宿主行的批量选中底色与兜底隐藏(同日改版)——宿主的行选中态不归\n * 第三方,整行可见性靠 `data-row-key` + `:has`,token 与回收站选中行同源;行首格在场时行尾兜底退场;\n * ⑥ `.sd-batch-pick-leading`(同日)——宿主行首那格只给 16×20,勾选框要一层 inline-flex 包装才压得回\n * 同样大小,官方没有这层包装;⑦ `.sd-toast-action:disabled`(2026-11-01)——撤销横幅在飞时那颗动作要\n * 看着就按不动,官方 Toast 的 action 没有禁用档,而我们这份自带 `color`、浏览器默认置灰不生效,\n * 档位与③的禁用档同源(`opacity` + `not-allowed`)。\n * 整张表里就这七条是我们自造的,标在这里,\n * 下次做「与官方逐条比对」时它属于**已知差异**,不是漂移。\n *\n * **另有一处已知差异不住在本文件**:回收站组头那颗按钮的整套 UA reset。宿主 `DisclosureRow` 那份是\n * `div`,没有这个坑;我们这行要做成可点的 `<button>`,UA 样式表会把背景、边框、内距、字体、居中与\n * flex 基线一起渗进来(真机上就是项目名整条居中,2026-10-15 截图)。它写在\n * `src/client/trash-panel.tsx` 组头那一行的 inline 样式上,**具体 reset 了哪几项由\n * `tests/skeleton.test.ts` 逐条钉住**(不在这里再抄一份名单)——所以「与官方逐条比对」时本表仍然只数\n * 上面那些,这条不算进抄本差异。\n */\n\n/* ────────────────────────── Button ────────────────────────── */\n\n.sd-button {\n box-sizing: border-box;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 4px;\n border: none;\n border-radius: var(--dsw-radius-md);\n cursor: pointer;\n font-size: 14px;\n line-height: 22px;\n color: var(--dsw-alias-label-primary);\n background: transparent;\n padding: 0 14px;\n}\n\n.sd-button:disabled {\n cursor: not-allowed;\n opacity: 0.4;\n}\n\n.sd-button-md {\n height: 36px;\n}\n\n/* 紧凑档:尺寸不随填充/描边换档(官方原文的注释即此意)。 */\n.sd-button-sm {\n height: 28px;\n font-size: 12px;\n line-height: 18px;\n padding: 0 10px;\n border-radius: var(--dsw-radius-sm);\n}\n\n.sd-button-primary {\n background: var(--dsw-alias-button-primary-fill);\n color: var(--dsw-alias-label-primary-foreground);\n}\n\n.sd-button-primary:hover:not(:disabled) {\n background: var(--dsw-alias-button-primary-hover);\n}\n\n.sd-button-ghost:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.sd-button-ghost:active:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-active);\n}\n\n.sd-button-outline {\n border: 0.5px solid var(--dsw-alias-border-l3);\n background: transparent;\n}\n\n.sd-button-outline:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.sd-button-icon {\n display: inline-flex;\n width: 16px;\n height: 16px;\n align-items: center;\n justify-content: center;\n}\n\n/* ────────────────────────── Tag ────────────────────────── */\n\n/* 胶囊几何固定:一个 tag 在哪里都是同一个尺寸,只有色板随 tone 走。 */\n.sd-tag {\n display: inline-flex;\n align-items: center;\n border-radius: 999px;\n corner-shape: round;\n padding: 1px 8px;\n font-size: 11px;\n line-height: 17px;\n font-weight: 500;\n white-space: nowrap;\n}\n\n/* 状态色自带填充:底色由自身颜色 10% 混出,换色板时底与字一起走,不需要第二个 token。 */\n.sd-tag-danger {\n background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);\n color: var(--dsw-alias-state-error-primary);\n}\n\n/* ────────────────────────── Modal ────────────────────────── */\n\n/* 整视口层(figma Mask + Dialog):蒙层 + 居中卡片。四边留 24px,上下按外壳公布的浮层内缩加宽,\n 高卡片才不会顶到外壳。卡片按这个内边距盒取尺寸:消费方用 `max-height:100%` 收口,不自己算视口。 */\n.sd-modal-root {\n pointer-events: auto;\n position: fixed;\n inset: 0;\n z-index: 1000;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: max(24px, var(--dsh-frame-overlay-top, 24px)) 24px;\n}\n\n/* 只有绘制避让原生标题栏;根仍然挡住背景点击。 */\n.sd-modal-mask {\n position: absolute;\n inset: var(--dsh-frame-chrome-top, 0px) 0 0;\n backdrop-filter: var(--dsw-mask-blur);\n}\n\n.sd-modal-mask::after {\n content: '';\n position: absolute;\n inset: 0;\n background: var(--dsw-alias-bg-mask-1);\n animation: sd-modal-enter var(--ds-transition-duration) var(--ds-ease-in-out);\n}\n\n.sd-modal-dialog {\n box-sizing: border-box;\n position: relative;\n z-index: 1;\n display: flex;\n flex-direction: column;\n gap: 20px;\n width: min(380px, 100%);\n padding: 0 0 24px;\n overflow: hidden;\n border: 0;\n border-radius: var(--dsw-radius-panel);\n background: var(--dsw-alias-bg-layer-2);\n box-shadow: var(--dsw-elevation-prominent);\n animation: sd-modal-enter var(--ds-transition-duration) var(--ds-ease-in-out);\n}\n\n@keyframes sd-modal-enter {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .sd-modal-mask::after,\n .sd-modal-dialog { animation: none; }\n}\n\n.sd-modal-dialog:focus {\n outline: none;\n}\n\n.sd-modal-content {\n display: flex;\n flex-direction: column;\n width: 100%;\n}\n\n/* 头行:标题左、关闭钮右。 */\n.sd-modal-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n padding: 22px 14px 12px 24px;\n}\n\n.sd-modal-title {\n margin: 0;\n font-size: 16px;\n line-height: 24px;\n font-weight: 500;\n color: var(--dsw-alias-label-primary);\n}\n\n.sd-modal-close {\n flex: none;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n border: none;\n border-radius: var(--dsw-radius-sm);\n background: transparent;\n cursor: pointer;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sd-modal-close:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.sd-modal-body {\n display: flex;\n flex-direction: column;\n min-width: 0;\n margin-top: 20px;\n padding: 0 24px;\n}\n\n.sd-modal-footer {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 8px;\n padding: 0 24px;\n}\n\n/* ────────────────────────── Tooltip ────────────────────────── */\n\n.sd-tooltip-bubble {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n position: fixed;\n z-index: 100;\n /* 定宽收缩只按 `left` 到视口右缘量,靠右的锚点会提前折行;\n `max-content` 只按文案定宽,再用半个视口收口。 */\n width: max-content;\n max-width: 50vw;\n padding: 3px 7px;\n border-radius: var(--dsw-radius-sm);\n background: var(--dsw-alias-tooltip-bg);\n color: var(--dsw-static-neutral-bluish-00);\n font-size: 13px;\n line-height: 20px;\n white-space: pre-line;\n /* 不可断的长 token(URL、路径)不许顶破 max-width。 */\n overflow-wrap: break-word;\n pointer-events: none;\n animation: sd-tooltip-in 150ms var(--ds-ease-in-out);\n}\n\n.sd-tooltip-bubble[data-portal] { z-index: 1100; }\n.sd-tooltip-label { min-width: 0; }\n\n.sd-tooltip-bubble[data-side='right'] {\n transform: translateY(-50%);\n}\n\n.sd-tooltip-bubble[data-side='bottom'] {\n transform: translateX(-50%);\n}\n\n.sd-tooltip-bubble[data-side='top'] {\n transform: translate(-50%, -100%);\n}\n\n@keyframes sd-tooltip-in {\n from { opacity: 0; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .sd-tooltip-bubble {\n animation: none;\n }\n}\n\n/* ────────────────────────── Toast ────────────────────────── */\n\n/* 淡出延迟由组件以 `--dsh-toast-hold` 给出 ⇒ 同一个值同时驱动卸载计时器与这段动画,\n 两者不可能再对不上。淡出**时长**仍需与组件里的 FADE_MS 一致(没有 owner 会改它)。 */\n.sd-toast {\n position: fixed;\n top: 40px;\n left: 50%;\n /* 高过图片灯箱底层的 1000:预览开着时报错也必须读得到。 */\n z-index: 1100;\n /* 通告不截点击。 */\n pointer-events: none;\n display: flex;\n align-items: center;\n gap: 10px;\n /* 定宽盒设了 `left` 只按右侧空间收缩(-50% 的位移发生在定尺之后)⇒ 靠窗边的横幅会提前折行。\n `max-content` 只按文案定宽,再由下面的 max-width 收口。 */\n width: max-content;\n max-width: min(640px, calc(100vw - 48px));\n padding: 12px 16px;\n border-radius: var(--dsw-radius-lg);\n /* 设计平台的 toast 表面在**两套主题下都是深的**(浅色 bluish-800 / 深色 bluish-750),\n 所以这里的字色与动作色是固定值,不随主题翻转。 */\n background: var(--dsw-alias-toast-bg);\n color: var(--dsw-alias-toast-label);\n font-size: 14px;\n line-height: 22px;\n box-shadow: var(--dsw-shadow-lv3);\n transform: translateX(-50%);\n animation:\n sd-toast-in 160ms ease-out,\n sd-toast-fade 1000ms ease var(--dsh-toast-hold, 3000ms) forwards;\n}\n\n.sd-toast-icon {\n display: grid;\n place-items: center;\n flex: none;\n color: var(--dsw-alias-state-warn-label);\n}\n\n.sd-toast-icon-success {\n color: var(--dsw-alias-state-success-primary);\n}\n\n.sd-toast-text {\n min-width: 0;\n}\n\n.sd-toast-action {\n display: inline;\n padding: 0;\n border: 0;\n background: none;\n color: var(--dsw-static-deepseek-400);\n font: inherit;\n cursor: pointer;\n /* 动作文字与周围文案之间的呼吸位。 */\n margin-inline: 3px;\n /* 横幅表面整体是穿透点击的,只有动作文字接指针。 */\n pointer-events: auto;\n}\n\n.sd-toast-action:hover {\n opacity: 0.8;\n}\n\n/* 本插件自造的第七条(不是抄本,见文件头点名):撤销横幅在飞时那颗动作要**看着就按不动**。\n 官方 Toast 的 action 只有常态与 hover,没有禁用档;而 `.sd-toast-action` 自带 `color`,\n 浏览器默认的置灰根本不生效(真机上只会看见一个光标照常是手、点下去没反应的「撤销」)。\n 禁用档与行尾勾选开关同源(`opacity` + `not-allowed`),不新造颜色。 */\n.sd-toast-action:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n}\n\n@keyframes sd-toast-in {\n from {\n opacity: 0;\n transform: translate(-50%, -6px);\n }\n\n to {\n opacity: 1;\n transform: translate(-50%, 0);\n }\n}\n\n@keyframes sd-toast-fade {\n to {\n opacity: 0;\n visibility: hidden;\n }\n}\n\n/* 减动效去掉滑入;延迟淡出(透明度变化、不是位移)仍然在定时卸载前收掉横幅。 */\n@media (prefers-reduced-motion: reduce) {\n .sd-toast {\n animation: sd-toast-fade 1000ms ease var(--dsh-toast-hold, 3000ms) forwards;\n }\n}\n\n/* ────────────────────────── 菜单行 ────────────────────────── */\n\n.sd-menu-item-wrap {\n position: relative;\n}\n\n/* 菜单格:紧凑文字、图标与选中间距。 */\n.sd-menu-item {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n min-height: 34px;\n padding: 6px 8px;\n border: none;\n border-radius: var(--dsw-radius-md);\n background: transparent;\n cursor: pointer;\n font-size: 13px;\n line-height: 20px;\n color: var(--dsw-alias-label-primary);\n text-align: left;\n}\n\n.sd-menu-item:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n/* 方向键移动的是真焦点 ⇒ 键盘所在的那一行拿指针同款填充;填充就是这一行的焦点呈现,\n 浏览器默认的 ring 再叠一层就重了。 */\n.sd-menu-item:focus-visible:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n outline: none;\n}\n\n.sd-menu-item:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n}\n\n.sd-menu-item-icon {\n display: inline-flex;\n flex: none;\n width: 14px;\n height: 14px;\n align-items: center;\n justify-content: center;\n color: var(--dsw-alias-menu-icon);\n}\n\n.sd-menu-item-icon svg {\n width: 14px;\n height: 14px;\n}\n\n.sd-menu-item-label {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/* 破坏性行:error 字/图标 + danger 悬停底。 */\n.sd-menu-item-danger {\n color: var(--dsw-alias-state-error-primary);\n}\n\n.sd-menu-item-danger .sd-menu-item-icon {\n color: var(--dsw-alias-state-error-primary);\n}\n\n.sd-menu-item-danger:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover-danger);\n}\n\n.sd-menu-item-danger:focus-visible:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover-danger);\n outline: none;\n}\n\n/* 分隔格:发丝线上下的间距。取 l2 而不是 l1——l1 在这个表面上几乎看不见。 */\n.sd-menu-separator {\n height: 0.5px;\n margin: 3px 2px;\n background: var(--dsw-alias-border-l2);\n}\n\n/* ────────────────────────── Checkbox ────────────────────────── */\n/* 抄 `lib/Checkbox.module.css` 全部四条声明,逐字。勾选框本体是**原生 input** + `accent-color`,\n 不是自绘的圆框——回收站多选态用它(清单行左侧那一格)。 */\n\n.sd-checkbox {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-size: 14px;\n line-height: 20px;\n color: var(--dsw-alias-label-primary);\n cursor: pointer;\n}\n\n.sd-checkbox input {\n flex: 0 0 auto;\n width: 16px;\n height: 16px;\n margin: 0;\n accent-color: var(--dsw-alias-brand-primary);\n cursor: inherit;\n}\n\n.sd-checkbox input:focus-visible {\n outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));\n outline-offset: 2px;\n}\n\n.sd-checkbox:has(input:disabled) {\n cursor: default;\n opacity: 0.5;\n}\n\n/* ────────────────────────── DisclosureRow(分组折叠头) ────────────────────────── */\n/* 抄 `lib/DisclosureRow.module.css` 的 `.root`/`.row`/`.leading`/`.title` 与那两枚 glyph 的\n 显隐。没抄进来的是 `StateDot`(`svg[data-state]`)那一档——它说\"这是状态标记不是文字装饰\",\n 我们的组头没有状态点。行高与 leading 盒走 `--dsh-content-font-delta` 那根轴,\n 标题字号走 secondary 档:宿主的\"设置里调正文字号\"会一起推着它走。 */\n\n.sd-disclosure {\n display: flex;\n flex-direction: column;\n width: 100%;\n min-width: 0;\n}\n\n.sd-disclosure-row {\n position: relative;\n overflow: hidden;\n display: flex;\n align-items: center;\n height: calc(24px + var(--dsh-content-font-delta, 0px));\n min-width: 0;\n color: var(--dsw-alias-label-tertiary);\n transition: color 100ms ease;\n}\n\n.sd-disclosure-row[data-expandable] {\n cursor: pointer;\n}\n\n.sd-disclosure-row:hover {\n color: var(--dsw-alias-label-secondary);\n}\n\n.sd-disclosure-leading {\n position: relative;\n flex: none;\n width: calc(16px + var(--dsh-content-font-delta, 0px));\n height: calc(16px + var(--dsh-content-font-delta, 0px));\n display: inline-flex;\n align-items: center;\n justify-content: center;\n margin-right: 6px;\n padding: 0;\n border: none;\n background: none;\n color: inherit;\n}\n\n.sd-disclosure-leading svg:not([data-state]) {\n width: calc(14px + var(--dsh-content-font-delta, 0px));\n height: calc(14px + var(--dsh-content-font-delta, 0px));\n}\n\nbutton.sd-disclosure-leading {\n cursor: pointer;\n}\n\n.sd-disclosure-title {\n flex: none;\n font-size: var(--dsh-content-font-size-secondary, 13px);\n line-height: calc(24px + var(--dsh-content-font-delta, 0px));\n color: inherit;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .sd-disclosure-row {\n transition: none;\n }\n}\n\n/* ────────────────────────── 宽度把手(可拖的分栏线) ────────────────────────── */\n/* 抄 `ConversationRoot.module.css` 的 `.widthHandle` 一族:10px 的命中区、`col-resize` 光标、\n 平时不画、悬停或拖动时才浮出一条 2px 的 `--dsw-alias-scrollbar-bg-l1` 短标(那条渐变里\n `--dsh-width-handle-pointer-y` 的兜底是 50%,宿主靠 JS 跟着指针走;我们不跟,\n 停在中间——少一条 pointermove 上的写字,视觉上是同一族东西)。\n 与宿主的差别只有位置:它绝对定位在会话栏两侧,我们这根住在两栏之间的那一格 flex 位里,\n 所以 `top/bottom/position` 那三样交给内联布局,这里只留视觉与光标。 */\n\n.sd-width-handle {\n position: relative;\n z-index: 0;\n cursor: col-resize;\n align-self: stretch;\n flex: 0 0 10px;\n width: 10px;\n padding: 0;\n border: none;\n background: none;\n}\n\n.sd-width-handle::after {\n content: \"\";\n background: linear-gradient(\n to bottom,\n transparent calc(var(--dsh-width-handle-pointer-y, 50%) - 36px),\n var(--dsw-alias-scrollbar-bg-l1) calc(var(--dsh-width-handle-pointer-y, 50%) - 8px),\n var(--dsw-alias-scrollbar-bg-l1) calc(var(--dsh-width-handle-pointer-y, 50%) + 8px),\n transparent calc(var(--dsh-width-handle-pointer-y, 50%) + 36px)\n );\n opacity: 0;\n pointer-events: none;\n border-radius: 2px;\n width: 2px;\n position: absolute;\n top: 0;\n bottom: 0;\n left: 4px;\n}\n\n.sd-width-handle:hover::after,\n.sd-width-handle[data-dragging]::after {\n opacity: 1;\n}\n\n.sd-width-handle:focus-visible {\n outline: none;\n}\n\n.sd-width-handle:focus-visible::after {\n opacity: 1;\n}\n\n/* 本插件自造的第一条(不是抄本,见文件头点名):拖动期间整块面板别把文字选上了。 */\n.sd-resizing {\n user-select: none;\n cursor: col-resize;\n}\n\n/* 本插件自造的第二条(不是抄本,见文件头点名):清单行是 `role=\"row\"` 的可聚焦 div,而这一代\n `--dsw-focus-ring-color` 逐字是 `transparent`(2026-10-15 实测)——不自己描一条,键盘走\n ↑↓ 选会话时就没有任何焦点可见性。描边**复用选中态那枚** `ghost-active-border`,不新造颜色;\n 0.5px 与 `outline-offset: 1px` 是行的描边宽度与一档外移,不动布局(outline 不进滚动区)。 */\n[data-dsh-session-delete=\"trash-row\"]:focus-visible {\n outline: 0.5px solid var(--dsw-alias-button-ghost-active-border);\n outline-offset: 1px;\n}\n\n/* 会话行尾悬停排那颗**兜底**勾选开关(状态点行 / 归档行;行首主勾选是原生 Checkbox)。\n 基底**逐字抄** `dsh-client-ui-workspace` 的 `Rows.module.css` 的 `.iconButton` / `.iconButton:hover`,\n 只改类名——兜底开关要跟同排宿主自己的归档/置顶同形,尺寸与色档都不自己发明。 */\n.sd-rowaction-button {\n border-radius: var(--dsw-radius-xs);\n cursor: pointer;\n width: 16px;\n height: 16px;\n color: var(--dsw-alias-label-tertiary);\n background: 0 0;\n border: none;\n flex: none;\n justify-content: center;\n align-items: center;\n padding: 0;\n display: inline-flex;\n}\n.sd-rowaction-button:hover {\n color: var(--dsw-alias-label-primary);\n}\n\n/* 本插件自造的第三条(不是抄本,见文件头点名):官方那颗只有常态与 hover,勾选开关还要有\n 「已按下」与「这条删不动」两档。按下档**复用回收站选中行那两枚** `ghost-active-*`,不新造颜色;\n 禁用档走 opacity。 */\n.sd-rowaction-button[aria-pressed='true'] {\n background: var(--dsw-alias-button-ghost-active-fill);\n box-shadow: inset 0 0 0 1px var(--dsw-alias-button-ghost-active-border);\n color: var(--dsw-alias-label-primary);\n}\n.sd-rowaction-button:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n}\n\n/* 本插件自造的第⑥条(不是抄本,见文件头点名):行首 16px 格里的勾选框(2026-10-17 改版,**只在\n 多选模式内渲染**)。格本身由宿主居中(16×20),这里只把包装层压成同样大小;勾选框本体是回收站\n 多选同款原生 input(`.sd-checkbox`,见上)。 */\n.sd-batch-pick-leading {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n}\n\n/* 本插件自造的第⑤条(不是抄本,见文件头点名):宿主行自己的选中态只归它自己,批量选择要有整行\n 底色;行首格在场时行尾兜底要退场(两条入口不许同屏)。两条都只认宿主行已有的\n `data-row-key`;将来这个属性改名,最差是底色不亮 / 兜底多露一颗,勾选本身仍在。 */\n[data-row-key^=\"session:\"]:has([data-dsh-session-delete=\"batch-pick\"][data-checked=\"true\"]) {\n background: var(--dsw-alias-button-ghost-active-fill);\n box-shadow: inset 0 0 0 0.5px var(--dsw-alias-button-ghost-active-border);\n}\n[data-row-key^=\"session:\"]:has([data-dsh-session-delete=\"batch-pick\"][data-seat=\"leading\"]) .sd-batch-pick-action {\n display: none;\n}\n\n/* 本插件自造的第四条(不是抄本,见文件头点名):批量删除的浮条。宿主没有\"浮条\"这件控件,形状照\n 本插件自己的浮层口径来——固定底部居中,表面/描边/圆角/高程四件全走 token(与单删那张目标卡同源)。\n **层级必须低于确认框(`.sd-modal-root` 1000)**:2026-10-17 真机发现浮条在 1100 时会盖住确认框、\n 还能抢它的按钮;撤销横幅与 Tooltip 是瞬时通告、保持 1100。列方向:清单在上、操作行在下;\n 清单限高可滚,默认收起(见 `batch-bar.tsx`)。**宽度按内容展开**(`max-content`,最多视口减 48):\n 只写 `left:50% + width:auto` 时 shrink-to-fit 只拿左半屏,新增「全选/取消全选」后会把「退出多选」\n 挤到第二行;窄到 max-width 还放不下才由 `.sd-batch-bar-main` 的 `flex-wrap` 兜底。 */\n.sd-batch-bar {\n position: fixed;\n bottom: 24px;\n left: 50%;\n transform: translateX(-50%);\n z-index: 900;\n width: max-content;\n max-width: calc(100vw - 48px);\n display: flex;\n flex-direction: column;\n align-items: stretch;\n gap: 8px;\n padding: 8px 12px;\n background: var(--dsw-alias-bg-layer-2);\n border: 0.5px solid var(--dsw-alias-border-l4);\n border-radius: var(--dsw-radius-lg);\n box-shadow: var(--dsw-elevation-prominent);\n font-size: 12px;\n line-height: 18px;\n}\n\n.sd-batch-bar-main {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n.sd-batch-bar-hint {\n color: var(--dsw-alias-label-tertiary);\n}\n\n.sd-batch-bar-list-head {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n/* 汇总的第二句(含 K 个子会话)比条数轻一档,和列表行里的 chip 同级。 */\n.sd-batch-bar-list-head span + span {\n color: var(--dsw-alias-label-tertiary);\n}\n\n.sd-batch-bar-list {\n display: flex;\n flex-direction: column;\n gap: 2px;\n max-height: 240px;\n overflow: auto;\n margin: 0;\n padding: 0 0 8px;\n border-bottom: 0.5px solid var(--dsw-alias-border-l4);\n list-style: none;\n}\n\n.sd-batch-bar-item {\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 0;\n}\n\n.sd-batch-bar-title {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sd-batch-bar-meta {\n flex: none;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.sd-batch-bar-remove {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: none;\n width: 16px;\n height: 16px;\n padding: 0;\n border: none;\n border-radius: var(--dsw-radius-xs);\n background: 0 0;\n color: var(--dsw-alias-label-tertiary);\n cursor: pointer;\n}\n\n.sd-batch-bar-remove:hover {\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-interactive-bg-hover);\n}\n";
|
|
1715
|
+
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";
|
|
1681
1716
|
//#endregion
|
|
1682
1717
|
//#region src/client/ui/markdown.css?dsh-css-text
|
|
1683
|
-
var markdown_default = "/**\n * 回收站详情里**会话正文**的排版(2026-10-15 真机反馈后新增)。\n *\n * 为什么单独一个文件而不并进 `ui.css`:`ui.css` 是\"照官方 primitives 逐字抄来的那一份表\",\n * 那条 fidelity 核对(`docs/development.md` 的界面保真复核门)要的是它与官方明文**逐属性零差异**,只许文件头点名的几类例外\n * (类名前缀、`@keyframes` 改名,外加它点名的自造与已声明的差别);条数不钉在这里,钉了就会漂成假基线。这一份抄的是\n * **另一个来源**(宿主 markdown 那一张表),混进去就把那份核对弄脏了。\n *\n *
|
|
1718
|
+
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";
|
|
1684
1719
|
//#endregion
|
|
1685
1720
|
//#region src/client/ui/styles.ts
|
|
1686
1721
|
/**
|
|
@@ -1867,21 +1902,6 @@ window.__ModuleLoader__.load({
|
|
|
1867
1902
|
//#endregion
|
|
1868
1903
|
//#region src/client/batch-select.tsx
|
|
1869
1904
|
/**
|
|
1870
|
-
* 宿主列表批量删除的**控件面**:行首勾选框、行尾兜底开关,
|
|
1871
|
-
* 以及确认框/浮条读的那一份计数问句。
|
|
1872
|
-
*
|
|
1873
|
-
* 入口在每行的「⋯」菜单里(`batch-menu-item.tsx`):**按下才进入多选模式**,模式内才渲染勾选框、
|
|
1874
|
-
* 才把整行点击变成"勾/取消";退出模式清空名单。选中集只住 `batch-selection-store.ts`(模式 + 名单一份
|
|
1875
|
-
* 合并快照);"哪些行现在可见、点行算不算选中"只住 `host-rows.ts`。四处隔着槽位边界,只有模块级
|
|
1876
|
-
* store 能让他们看见同一份名单;本文件不再持有任何现场。
|
|
1877
|
-
*
|
|
1878
|
-
* **为什么主勾选在行首**:宿主对行尾 `.rowActions` 是 `display:none`,只有 `row:hover` / 菜单打开
|
|
1879
|
-
* 才出现(app.asar 0.2.0-rc.2 的 Rows 渲染);行首 16px 格在无状态点时是空的,模式内可见。
|
|
1880
|
-
* 宿主在「有状态点(运行中/待确认/未读完成)」与归档行不渲染行首格 ⇒ 这些行走行尾悬停兜底,
|
|
1881
|
-
* 但模式内**点整行**也能选(`host-rows.ts` 的 `useSelectionRowClickGuard`),所以不依赖那颗小框。
|
|
1882
|
-
* 两条勾选不会同时出现(`ui.css` 的 `:has` 兜底隐藏规则,靠宿主行的 `data-row-key`)。
|
|
1883
|
-
*/
|
|
1884
|
-
/**
|
|
1885
1905
|
* 一组会话的子会话计数(`/apply` 的 `action:'count'`,纯读、不进串行链)。
|
|
1886
1906
|
*
|
|
1887
1907
|
* **一条读不到就整份返回 null** ——界面上那两个数(浮条的 K、确认框的 K 与逐条 M)都是合计,
|
|
@@ -1902,8 +1922,12 @@ window.__ModuleLoader__.load({
|
|
|
1902
1922
|
* (行首格在宿主行那条 `onClick` 里面)。宿主对「有状态点」和归档行不渲染这一格,
|
|
1903
1923
|
* 那些行走 `BatchPickButton` 兜底;两条不会同时出现(见 `ui.css` 的兜底隐藏规则)。
|
|
1904
1924
|
*/
|
|
1905
|
-
function BatchPickCheckbox({ sessionId, title, t = zhTranslate }) {
|
|
1925
|
+
function BatchPickCheckbox({ sessionId, title, live = false, t = zhTranslate }) {
|
|
1906
1926
|
const { selecting, picks } = useSelection();
|
|
1927
|
+
useHostRowRegistration(sessionId, {
|
|
1928
|
+
title,
|
|
1929
|
+
live
|
|
1930
|
+
});
|
|
1907
1931
|
const on = picks.some((candidate) => candidate.sessionId === sessionId);
|
|
1908
1932
|
if (!selecting) return null;
|
|
1909
1933
|
const name = title === "" ? t("common.untitled") : title;
|
|
@@ -1932,19 +1956,10 @@ window.__ModuleLoader__.load({
|
|
|
1932
1956
|
function BatchPickButton({ sessionId, title, live = false, t = zhTranslate }) {
|
|
1933
1957
|
const { selecting, picks } = useSelection();
|
|
1934
1958
|
const on = picks.some((candidate) => candidate.sessionId === sessionId);
|
|
1935
|
-
(
|
|
1936
|
-
registerHostRowInfo(sessionId, {
|
|
1937
|
-
title,
|
|
1938
|
-
live
|
|
1939
|
-
});
|
|
1940
|
-
return () => {
|
|
1941
|
-
unregisterHostRowInfo(sessionId);
|
|
1942
|
-
};
|
|
1943
|
-
}, [
|
|
1944
|
-
sessionId,
|
|
1959
|
+
useHostRowRegistration(sessionId, {
|
|
1945
1960
|
title,
|
|
1946
1961
|
live
|
|
1947
|
-
|
|
1962
|
+
});
|
|
1948
1963
|
if (!selecting) return null;
|
|
1949
1964
|
const name = title === "" ? t("common.untitled") : title;
|
|
1950
1965
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Tooltip, {
|
|
@@ -2291,15 +2306,104 @@ window.__ModuleLoader__.load({
|
|
|
2291
2306
|
}));
|
|
2292
2307
|
}
|
|
2293
2308
|
//#endregion
|
|
2309
|
+
//#region src/client/pending-open.ts
|
|
2310
|
+
/**
|
|
2311
|
+
* 重开等待的唯一生命周期:一张票、一条订阅、一个放弃 timer、一次 settle/cleanup。
|
|
2312
|
+
*
|
|
2313
|
+
* ADR-0036 的语义不变:**订阅优先、出现即开、最多等一个放弃边界、不轮询**。这里把这段状态机
|
|
2314
|
+
* 收成一个可取消句柄:调用方 `schedule` 一次、`cancel` 一次;票、订阅、timer 与 settle 标志
|
|
2315
|
+
* 都关在 implementation 里,不再由 main-view 在四个调用点成对手工清理。
|
|
2316
|
+
*
|
|
2317
|
+
* 依赖(store 的 subscribe、是否已出现、是否还能抢焦点、怎么打开)由调用方交进来——本模块不认宿主。
|
|
2318
|
+
*/
|
|
2319
|
+
/** 默认放弃边界:给宿主 `api-session/added` 到客户端摘要落地留一拍(ADR-0036 的产品决定)。 */
|
|
2320
|
+
const DEFAULT_GIVE_UP_MS = 2e3;
|
|
2321
|
+
function createPendingOpen(giveUpMs = DEFAULT_GIVE_UP_MS) {
|
|
2322
|
+
const latest = createLatestWins();
|
|
2323
|
+
let cleanup;
|
|
2324
|
+
const cancel = () => {
|
|
2325
|
+
latest.invalidate();
|
|
2326
|
+
const stop = cleanup;
|
|
2327
|
+
cleanup = void 0;
|
|
2328
|
+
stop?.();
|
|
2329
|
+
};
|
|
2330
|
+
return {
|
|
2331
|
+
cancel,
|
|
2332
|
+
schedule(input) {
|
|
2333
|
+
cancel();
|
|
2334
|
+
const ticket = latest.issue();
|
|
2335
|
+
let settled = false;
|
|
2336
|
+
let unsubscribe;
|
|
2337
|
+
let timer;
|
|
2338
|
+
const unsubscribeNow = () => {
|
|
2339
|
+
if (unsubscribe === void 0) return;
|
|
2340
|
+
const stop = unsubscribe;
|
|
2341
|
+
unsubscribe = void 0;
|
|
2342
|
+
try {
|
|
2343
|
+
stop();
|
|
2344
|
+
} catch {}
|
|
2345
|
+
};
|
|
2346
|
+
const finish = () => {
|
|
2347
|
+
if (settled) return;
|
|
2348
|
+
settled = true;
|
|
2349
|
+
if (timer !== void 0) {
|
|
2350
|
+
clearTimeout(timer);
|
|
2351
|
+
timer = void 0;
|
|
2352
|
+
}
|
|
2353
|
+
unsubscribeNow();
|
|
2354
|
+
if (cleanup === finish) cleanup = void 0;
|
|
2355
|
+
};
|
|
2356
|
+
const open = () => {
|
|
2357
|
+
if (!ticket.current || !input.canOpen()) {
|
|
2358
|
+
finish();
|
|
2359
|
+
return;
|
|
2360
|
+
}
|
|
2361
|
+
try {
|
|
2362
|
+
input.open();
|
|
2363
|
+
} finally {
|
|
2364
|
+
finish();
|
|
2365
|
+
}
|
|
2366
|
+
};
|
|
2367
|
+
if (input.isReady()) {
|
|
2368
|
+
open();
|
|
2369
|
+
return;
|
|
2370
|
+
}
|
|
2371
|
+
const subscribe = input.subscribe;
|
|
2372
|
+
if (subscribe === void 0) return;
|
|
2373
|
+
cleanup = finish;
|
|
2374
|
+
let stop;
|
|
2375
|
+
try {
|
|
2376
|
+
stop = subscribe(() => {
|
|
2377
|
+
if (input.isReady()) open();
|
|
2378
|
+
});
|
|
2379
|
+
} catch {
|
|
2380
|
+
finish();
|
|
2381
|
+
return;
|
|
2382
|
+
}
|
|
2383
|
+
unsubscribe = stop;
|
|
2384
|
+
if (settled) {
|
|
2385
|
+
unsubscribeNow();
|
|
2386
|
+
return;
|
|
2387
|
+
}
|
|
2388
|
+
timer = setTimeout(finish, giveUpMs);
|
|
2389
|
+
if (input.isReady()) open();
|
|
2390
|
+
}
|
|
2391
|
+
};
|
|
2392
|
+
}
|
|
2393
|
+
//#endregion
|
|
2294
2394
|
//#region src/client/main-view.ts
|
|
2295
2395
|
let client;
|
|
2296
2396
|
/** 我们自己关掉的主视图会话:只有这里记过的,撤销恢复后才会重开;面板恢复用 `forgetMainView` 清掉。 */
|
|
2297
2397
|
let closedByUs = /* @__PURE__ */ new Set();
|
|
2298
|
-
/**
|
|
2299
|
-
const
|
|
2398
|
+
/** 在等的重开:票、订阅、放弃 timer 与 cleanup 都归这个句柄(`src/client/pending-open.ts`)。 */
|
|
2399
|
+
const pendingOpen = createPendingOpen();
|
|
2300
2400
|
/** 入口激活时绑一次;之后每次调用再读服务(客户端插件激活顺序早于 `uiWorkspace` 也没关系)。 */
|
|
2301
2401
|
function bindMainView(ctx) {
|
|
2302
2402
|
client = ctx;
|
|
2403
|
+
if (typeof ctx.effect === "function") ctx.effect(() => () => {
|
|
2404
|
+
pendingOpen.cancel();
|
|
2405
|
+
closedByUs = /* @__PURE__ */ new Set();
|
|
2406
|
+
}, "session-delete: main-view");
|
|
2303
2407
|
}
|
|
2304
2408
|
/** 宿主服务是否可用:把检查写成类型谓词,调用点拿到的就是 `ReadyUiWorkspace`。
|
|
2305
2409
|
* `ctx.get` 的窄镜像声明成可能回 null(宿主可回 null),所以空值判据必须同时认 undefined / null。 */
|
|
@@ -2330,7 +2434,7 @@ window.__ModuleLoader__.load({
|
|
|
2330
2434
|
*/
|
|
2331
2435
|
function afterDelete(sessionIds) {
|
|
2332
2436
|
if (sessionIds.length === 0) return;
|
|
2333
|
-
|
|
2437
|
+
pendingOpen.cancel();
|
|
2334
2438
|
const ui = uiWorkspaceOf();
|
|
2335
2439
|
if (ui === void 0) return;
|
|
2336
2440
|
const current = currentSessionOf(ui);
|
|
@@ -2349,7 +2453,7 @@ window.__ModuleLoader__.load({
|
|
|
2349
2453
|
*/
|
|
2350
2454
|
function afterRestore(sessionIds) {
|
|
2351
2455
|
if (sessionIds.length === 0) return;
|
|
2352
|
-
|
|
2456
|
+
pendingOpen.cancel();
|
|
2353
2457
|
const targets = sessionIds.filter((id) => closedByUs.has(id));
|
|
2354
2458
|
if (targets.length === 0) return;
|
|
2355
2459
|
for (const id of targets) closedByUs.delete(id);
|
|
@@ -2366,19 +2470,23 @@ window.__ModuleLoader__.load({
|
|
|
2366
2470
|
}
|
|
2367
2471
|
/**
|
|
2368
2472
|
* 把会话重新打开:`api-session/added` 从宿主发出到客户端摘要表落地有一拍(WS 与 HTTP 响应各走
|
|
2369
|
-
* 各的),`openSession` 的目标必须先出现在清单里才不会抛「unknown session
|
|
2370
|
-
*
|
|
2473
|
+
* 各的),`openSession` 的目标必须先出现在清单里才不会抛「unknown session」——先查一次快照,没有就
|
|
2474
|
+
* **订阅**宿主快照 store;出现且票仍是当前、焦点没被抢走时打开。最多等 2 秒,一个放弃 timer,不轮询。
|
|
2475
|
+
* 期间用户若已经选了别的会话就不再抢焦点。等不到就什么都不做,行还在那里。
|
|
2371
2476
|
*
|
|
2372
|
-
*
|
|
2373
|
-
*
|
|
2477
|
+
* 这段状态机住 `src/client/pending-open.ts` 的 `createPendingOpen`:这里只把「查什么、能不能开、
|
|
2478
|
+
* 怎么开」交进去;取消也只剩 `pendingOpen.cancel()` 一次。没有 `subscribe` 的旧 store 直接放弃
|
|
2479
|
+
* 等待,不退回轮询(ADR-0036)。
|
|
2374
2480
|
*/
|
|
2375
2481
|
function openWhenListed(sessionId) {
|
|
2376
2482
|
const ui = uiWorkspaceOf();
|
|
2377
2483
|
if (ui === void 0) return;
|
|
2378
|
-
const
|
|
2484
|
+
const store = ui.sessions?.list;
|
|
2485
|
+
if (store === void 0) return;
|
|
2486
|
+
const subscribe = typeof store.subscribe === "function" ? store.subscribe.bind(store) : void 0;
|
|
2379
2487
|
const listReady = () => {
|
|
2380
2488
|
try {
|
|
2381
|
-
return
|
|
2489
|
+
return store.getSnapshot()?.byId?.[sessionId] !== void 0;
|
|
2382
2490
|
} catch {
|
|
2383
2491
|
return false;
|
|
2384
2492
|
}
|
|
@@ -2387,28 +2495,16 @@ window.__ModuleLoader__.load({
|
|
|
2387
2495
|
const current = currentSessionOf(ui);
|
|
2388
2496
|
return current === void 0 || current === sessionId;
|
|
2389
2497
|
};
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
open();
|
|
2399
|
-
return;
|
|
2400
|
-
}
|
|
2401
|
-
let attempts = 0;
|
|
2402
|
-
const tick = () => {
|
|
2403
|
-
if (!ticket.current) return;
|
|
2404
|
-
attempts += 1;
|
|
2405
|
-
if (listReady()) {
|
|
2406
|
-
open();
|
|
2407
|
-
return;
|
|
2498
|
+
pendingOpen.schedule({
|
|
2499
|
+
subscribe,
|
|
2500
|
+
isReady: listReady,
|
|
2501
|
+
canOpen: canTakeFocus,
|
|
2502
|
+
open: () => {
|
|
2503
|
+
try {
|
|
2504
|
+
ui.openSession(sessionId);
|
|
2505
|
+
} catch {}
|
|
2408
2506
|
}
|
|
2409
|
-
|
|
2410
|
-
};
|
|
2411
|
-
setTimeout(tick, 100);
|
|
2507
|
+
});
|
|
2412
2508
|
}
|
|
2413
2509
|
//#endregion
|
|
2414
2510
|
//#region src/client/write-settlement.ts
|
|
@@ -2623,8 +2719,8 @@ window.__ModuleLoader__.load({
|
|
|
2623
2719
|
const TARGET_CARD = {
|
|
2624
2720
|
padding: "10px 12px",
|
|
2625
2721
|
background: "var(--dsw-alias-bg-layer-2)",
|
|
2626
|
-
border: "0.5px solid var(--
|
|
2627
|
-
borderRadius: "var(--
|
|
2722
|
+
border: "0.5px solid var(--sd-border-l4)",
|
|
2723
|
+
borderRadius: "var(--sd-radius-lg)"
|
|
2628
2724
|
};
|
|
2629
2725
|
const requestStore$1 = createStore(null);
|
|
2630
2726
|
/** 菜单行调用:请求删除这个会话(确认框由 overlay 上的宿主渲染)。 */
|
|
@@ -2806,7 +2902,7 @@ window.__ModuleLoader__.load({
|
|
|
2806
2902
|
const TARGET_META = {
|
|
2807
2903
|
fontSize: 12,
|
|
2808
2904
|
lineHeight: "18px",
|
|
2809
|
-
color: "var(--
|
|
2905
|
+
color: "var(--sd-label-tertiary)"
|
|
2810
2906
|
};
|
|
2811
2907
|
/**
|
|
2812
2908
|
* 目标卡要能装几十条:选中很多条时卡片不能把弹框撑出视口,标题与底部按钮必须保持可见。
|
|
@@ -2896,12 +2992,81 @@ window.__ModuleLoader__.load({
|
|
|
2896
2992
|
});
|
|
2897
2993
|
}
|
|
2898
2994
|
//#endregion
|
|
2995
|
+
//#region src/client/batch-pick.ts
|
|
2996
|
+
/**
|
|
2997
|
+
* 批量选择的**动作面**:全选、全选判定与模式内整行点击守卫。
|
|
2998
|
+
*
|
|
2999
|
+
* 可见行的事实源仍住 `host-rows.ts`(槽位登记 + 快照);选中集住 `batch-selection-store.ts`。
|
|
3000
|
+
* 本文件只把两者接起来,于是依赖方向单向:`batch-pick` → `host-rows` / `batch-selection-store`;
|
|
3001
|
+
* 浮条与行开关 → `batch-pick` / `host-rows`。`host-rows` 不再 import 任何选择动作。
|
|
3002
|
+
*
|
|
3003
|
+
* DOM 只在这里读一次 `data-row-key`(ADR-0034 记下的受约束偏离):守卫把指针下的行映射成
|
|
3004
|
+
* sessionId,再查登记;它不枚举行,也不维护行列表。
|
|
3005
|
+
*/
|
|
3006
|
+
/** 全选:把当前可见可选行按登记序追加;已在名单里的由 `addPicks` 自己跳过,原有选中保持在前。 */
|
|
3007
|
+
function selectAllVisible() {
|
|
3008
|
+
addPicks(readVisibleSelectableRows());
|
|
3009
|
+
}
|
|
3010
|
+
/**
|
|
3011
|
+
* 同一个切换口该显示「全选」还是「取消全选」:
|
|
3012
|
+
* - 有选中、且当前可见可选行全都选中 ⇒ 取消全选;
|
|
3013
|
+
* - 有选中、但当前没有可见可选行(全折叠/过滤掉)⇒ 只能清空,也显示取消全选;
|
|
3014
|
+
* - 其余(含 0 条)⇒ 全选。
|
|
3015
|
+
*/
|
|
3016
|
+
function areAllVisibleRowsSelected(rows, picks) {
|
|
3017
|
+
if (picks.length === 0) return false;
|
|
3018
|
+
if (rows.length === 0) return true;
|
|
3019
|
+
const chosen = new Set(picks.map((item) => item.sessionId));
|
|
3020
|
+
return rows.every((row) => chosen.has(row.sessionId));
|
|
3021
|
+
}
|
|
3022
|
+
/** 行内这些元素有自己的动作:模式内也不许被"点行=勾选"吃掉。 */
|
|
3023
|
+
const INTERACTIVE_IN_ROW = "button, a, input, select, textarea, [role=\"menuitem\"], [role=\"menu\"], [data-dsh-session-delete=\"batch-pick\"]";
|
|
3024
|
+
/**
|
|
3025
|
+
* 多选态内的整行点击守卫(document 捕获阶段)。
|
|
3026
|
+
*
|
|
3027
|
+
* 边界:行里的按钮 / 链接 / 输入 / 菜单项一律放行(否则 ⋯ 菜单、归档、置顶、单删都点不动);
|
|
3028
|
+
* 双击的第二下与 dblclick 不翻转(第一下已经选中,第二下不能又反掉,也不能打开宿主的重命名)。
|
|
3029
|
+
* 认行靠 `data-row-key="session:…"`——**这是本文件唯一一处 DOM 读**(ADR-0034):宿主将来改掉这个
|
|
3030
|
+
* 属性,最差是模式内点行不生效,勾选框本身仍在。模式外不挂监听:这时点行必须还是宿主的"打开会话"。
|
|
3031
|
+
*/
|
|
3032
|
+
function useSelectionRowClickGuard() {
|
|
3033
|
+
const selecting = useSelection().selecting;
|
|
3034
|
+
(0, react.useEffect)(() => {
|
|
3035
|
+
if (!selecting) return;
|
|
3036
|
+
const handle = (event) => {
|
|
3037
|
+
if (!(event.target instanceof Element)) return;
|
|
3038
|
+
const row = event.target.closest("[data-row-key^=\"session:\"]");
|
|
3039
|
+
if (row === null) return;
|
|
3040
|
+
if (event.target.closest(INTERACTIVE_IN_ROW) !== null) return;
|
|
3041
|
+
event.preventDefault();
|
|
3042
|
+
event.stopPropagation();
|
|
3043
|
+
if (event.type === "dblclick" || event.type === "click" && event.detail > 1) return;
|
|
3044
|
+
const key = row.getAttribute("data-row-key") ?? "";
|
|
3045
|
+
if (!key.startsWith("session:")) return;
|
|
3046
|
+
const sessionId = key.slice(8);
|
|
3047
|
+
if (sessionId === "") return;
|
|
3048
|
+
const info = pickableRowOf(sessionId);
|
|
3049
|
+
if (info === void 0) return;
|
|
3050
|
+
togglePick({
|
|
3051
|
+
sessionId,
|
|
3052
|
+
title: info.title
|
|
3053
|
+
});
|
|
3054
|
+
};
|
|
3055
|
+
document.addEventListener("click", handle, true);
|
|
3056
|
+
document.addEventListener("dblclick", handle, true);
|
|
3057
|
+
return () => {
|
|
3058
|
+
document.removeEventListener("click", handle, true);
|
|
3059
|
+
document.removeEventListener("dblclick", handle, true);
|
|
3060
|
+
};
|
|
3061
|
+
}, [selecting]);
|
|
3062
|
+
}
|
|
3063
|
+
//#endregion
|
|
2899
3064
|
//#region src/client/batch-bar.tsx
|
|
2900
3065
|
/**
|
|
2901
3066
|
* 批量删除的浮条:**多选模式内常驻**(0 条也渲染,既显示模式又给退出入口),
|
|
2902
3067
|
* 汇总「已选 N 条」+(读得到时)「含 K 个子会话」;「查看已选」展开限高可滚动的标题清单
|
|
2903
3068
|
* (逐条可移除),回答"到底选了哪些"——清单默认收起,不把底部输入区盖住。
|
|
2904
|
-
* 模式内的整行点击守卫也挂在这棵常驻宿主上(见 `
|
|
3069
|
+
* 模式内的整行点击守卫也挂在这棵常驻宿主上(见 `batch-pick.ts` 的 `useSelectionRowClickGuard`)。
|
|
2905
3070
|
*
|
|
2906
3071
|
* 为什么单独一个文件而不是并进 `batch-select.tsx`:那份是"行开关控件",而浮条要点开确认框
|
|
2907
3072
|
* (要 import 它的现场)——两个方向合到一个文件上就是一圈。浮条住在
|
|
@@ -3052,6 +3217,77 @@ window.__ModuleLoader__.load({
|
|
|
3052
3217
|
});
|
|
3053
3218
|
}
|
|
3054
3219
|
//#endregion
|
|
3220
|
+
//#region src/client/context-menu-actions.ts
|
|
3221
|
+
/**
|
|
3222
|
+
* 0.2.2+ 会话菜单的数据动作**定义**:只造贡献对象,不碰客户端注册表。
|
|
3223
|
+
* 注册仍统一住 `index.tsx`(客户端条目注册只住那里),这样旧宿主的 legacy 回退与
|
|
3224
|
+
* contextMenus 路径的互斥判断也只有一个地方。
|
|
3225
|
+
*
|
|
3226
|
+
* 为什么需要这一层:0.2.2 把内置会话动作迁到 contextMenus,legacy React 槽位
|
|
3227
|
+
* (`sidebar.workspaces.session.menu.item`) 的条目固定渲染在 `afterContent` 动作之前;Archive
|
|
3228
|
+
* 是 `afterContent: true` 的 footer 动作,于是 legacy 行会跑到 Archive 前面。把这两行也注册成
|
|
3229
|
+
* `afterContent` 数据动作、order 排在 Archive(600) 之后,才能恢复 0.2.1 的顺序:
|
|
3230
|
+
* Archive → 多选 → 删除。
|
|
3231
|
+
*
|
|
3232
|
+
* 数据动作没有 `disabledReason`(底层 MenuRow 有,但 MenuActionView 没透传),所以运行中禁用时
|
|
3233
|
+
* 改 label 为「删除(运行中)」;legacy 路径仍保留原来的 tooltip。
|
|
3234
|
+
*/
|
|
3235
|
+
const ICON_BATCH = "xrn1997.session-delete.batch-select";
|
|
3236
|
+
const ICON_DELETE = "xrn1997.session-delete.delete";
|
|
3237
|
+
const CONTRIBUTION_ID = "xrn1997.session-delete.session-menu";
|
|
3238
|
+
/** 运行中把 label 换成短句:数据动作没有 disabledReason,不能再用 tooltip 解释。 */
|
|
3239
|
+
function deleteLabel(t, live) {
|
|
3240
|
+
return live ? t("menu.deleteLive") : t("menu.delete");
|
|
3241
|
+
}
|
|
3242
|
+
/** 会话是否在跑;服务缺席或读数抛错都按「不知道」处理(Node 半仍会拒绝 live 删除)。 */
|
|
3243
|
+
function liveOf(sessionList, sessionId) {
|
|
3244
|
+
try {
|
|
3245
|
+
return sessionList?.getSnapshot().byId?.[sessionId]?.running === true;
|
|
3246
|
+
} catch {
|
|
3247
|
+
return false;
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
/**
|
|
3251
|
+
* 造 0.2.2+ 的会话菜单贡献。order 700 落在内置 Archive(600) 之后;两行都是 `afterContent`,
|
|
3252
|
+
* 所以它们与 Archive 一起排掉 legacy 组件行,恢复「归档在多选/删除之前」。
|
|
3253
|
+
*/
|
|
3254
|
+
function createSessionMenuContribution(t, sessionList) {
|
|
3255
|
+
const resolve = (target) => {
|
|
3256
|
+
const live = liveOf(sessionList, target.sessionId);
|
|
3257
|
+
return [{
|
|
3258
|
+
id: "batch-select",
|
|
3259
|
+
label: isSelecting() ? t("trash.toolbar.selectExit") : t("trash.toolbar.select"),
|
|
3260
|
+
icon: { key: ICON_BATCH },
|
|
3261
|
+
separatorBefore: true,
|
|
3262
|
+
afterContent: true,
|
|
3263
|
+
run: () => {
|
|
3264
|
+
if (isSelecting()) exitSelection();
|
|
3265
|
+
else enterSelection();
|
|
3266
|
+
}
|
|
3267
|
+
}, {
|
|
3268
|
+
id: "delete",
|
|
3269
|
+
label: deleteLabel(t, live),
|
|
3270
|
+
disabled: live,
|
|
3271
|
+
icon: { key: ICON_DELETE },
|
|
3272
|
+
danger: true,
|
|
3273
|
+
afterContent: true,
|
|
3274
|
+
run: () => {
|
|
3275
|
+
requestDelete({
|
|
3276
|
+
sessionId: target.sessionId,
|
|
3277
|
+
title: target.displayTitle
|
|
3278
|
+
});
|
|
3279
|
+
}
|
|
3280
|
+
}];
|
|
3281
|
+
};
|
|
3282
|
+
return {
|
|
3283
|
+
id: CONTRIBUTION_ID,
|
|
3284
|
+
target: "session",
|
|
3285
|
+
order: 700,
|
|
3286
|
+
...sessionList === void 0 ? {} : { observe: () => [sessionList] },
|
|
3287
|
+
resolve
|
|
3288
|
+
};
|
|
3289
|
+
}
|
|
3290
|
+
//#endregion
|
|
3055
3291
|
//#region src/client/delete-menu-item.tsx
|
|
3056
3292
|
/**
|
|
3057
3293
|
* 会话条目「…」菜单里的「删除」行(docs/ui-spec.md 状态 ①)。
|
|
@@ -3113,22 +3349,25 @@ window.__ModuleLoader__.load({
|
|
|
3113
3349
|
minWidth: 0,
|
|
3114
3350
|
padding: "9px 14px",
|
|
3115
3351
|
background: "var(--dsw-alias-bg-layer-2)",
|
|
3116
|
-
border: "0.5px solid var(--
|
|
3117
|
-
borderRadius: "var(--
|
|
3352
|
+
border: "0.5px solid var(--sd-border-l4)",
|
|
3353
|
+
borderRadius: "var(--sd-radius-lg)"
|
|
3118
3354
|
};
|
|
3119
3355
|
/** 选中行:底色与描边都换成宿主"选中"那一档(`trash-panel.tsx` 头注里有这两枚 token 的实测取值)。 */
|
|
3120
3356
|
const CARD_SELECTED = {
|
|
3121
3357
|
...CARD,
|
|
3122
|
-
background: "var(--
|
|
3123
|
-
border: "0.5px solid var(--
|
|
3358
|
+
background: "var(--sd-button-ghost-active-fill)",
|
|
3359
|
+
border: "0.5px solid var(--sd-button-ghost-active-border)"
|
|
3124
3360
|
};
|
|
3125
3361
|
/**
|
|
3126
3362
|
* **整页唯一那块实心红**的写法:`variant="primary"` + 在元素上覆盖 primary 那对填充 token。
|
|
3127
3363
|
* 宿主插件管理页的 danger 按钮就是这个写法(asar @21658274),所以 hover 仍走 `.primary:hover` 类。
|
|
3364
|
+
*
|
|
3365
|
+
* 覆盖的是**本插件别名层**(`--sd-*`,见 `ui.css` 头注)而不是宿主私有 token:别名在 `body` 上
|
|
3366
|
+
* 就把 `var(--dsw-…, 兜底)` 算完并继承下来了,元素上再覆盖底层私有 token 已经传不进继承值。
|
|
3128
3367
|
*/
|
|
3129
3368
|
const BULK_DANGER = {
|
|
3130
|
-
"--
|
|
3131
|
-
"--
|
|
3369
|
+
"--sd-button-primary-fill": "var(--dsw-alias-state-error-primary)",
|
|
3370
|
+
"--sd-button-primary-hover": "var(--dsw-alias-state-error-primary)"
|
|
3132
3371
|
};
|
|
3133
3372
|
/**
|
|
3134
3373
|
* 项目名怎么念:宿主的 `projectKey` 把 cwd 里的 `/`、`\`、`:` 全折成**一个** `-` ⇒ 分隔符那部分有损、
|
|
@@ -3165,8 +3404,10 @@ window.__ModuleLoader__.load({
|
|
|
3165
3404
|
* 回收站详情与设置页维护卡预览里的一条附件:图片给缩略图 + 放大框,普通文件给下载 chip。
|
|
3166
3405
|
*
|
|
3167
3406
|
* 三条口径:
|
|
3168
|
-
* - **地址只由 `attachmentId` + `kind`
|
|
3169
|
-
*
|
|
3407
|
+
* - **地址只由 `attachmentId` + `kind` 寻址**:`/attachment` 段以这两个参数找对象;媒体元数据
|
|
3408
|
+
* (`mediaType` / `bytes` / `width` / `height`)只给宿主 `attachments` 服务做引用校验,`name` 只做
|
|
3409
|
+
* 下载名建议,均不参与路径推导(见 `src/api/attachment.ts`)。空串与缺席同义——空串不是一个名字,
|
|
3410
|
+
* 不写进查询串。
|
|
3170
3411
|
* - **图裂了就换占位**:缩略图与放大框共用同一份 `failed` 集合,`onError` 之后这一格改成「附件不可用」,
|
|
3171
3412
|
* 不把破图留在屏上(放大框里裂了也换)。普通文件走 `<a download>`,没有页面内失败面:服务端失败回
|
|
3172
3413
|
* 200 + JSON 信封,浏览器可能把这份错误响应存成文件。
|
|
@@ -3183,10 +3424,16 @@ window.__ModuleLoader__.load({
|
|
|
3183
3424
|
function attachmentUrl(a) {
|
|
3184
3425
|
const query = new URLSearchParams({
|
|
3185
3426
|
[PARAMS.attachmentId]: a.attachmentId,
|
|
3186
|
-
[PARAMS.attachmentKind]: a.kind
|
|
3427
|
+
[PARAMS.attachmentKind]: a.kind,
|
|
3428
|
+
[PARAMS.attachmentBytes]: String(a.bytes)
|
|
3187
3429
|
});
|
|
3430
|
+
if (a.kind === "image") {
|
|
3431
|
+
if (a.mediaType !== void 0) query.set(PARAMS.attachmentMediaType, a.mediaType);
|
|
3432
|
+
if (a.width !== void 0) query.set(PARAMS.attachmentWidth, String(a.width));
|
|
3433
|
+
if (a.height !== void 0) query.set(PARAMS.attachmentHeight, String(a.height));
|
|
3434
|
+
}
|
|
3188
3435
|
if (a.kind === "file" && a.name !== void 0 && a.name !== "") query.set(PARAMS.attachmentName, a.name);
|
|
3189
|
-
return `${
|
|
3436
|
+
return apiPath(`${ROUTES.attachment.path}?${query.toString()}`);
|
|
3190
3437
|
}
|
|
3191
3438
|
/** 空串与缺席同义:宿主侧的 `name` 是可选建议名,空串不是名字(兜底文案与"不带 name 查询"都看它)。 */
|
|
3192
3439
|
function nameOf(a) {
|
|
@@ -3205,7 +3452,7 @@ window.__ModuleLoader__.load({
|
|
|
3205
3452
|
maxWidth: "100%",
|
|
3206
3453
|
maxHeight: 260,
|
|
3207
3454
|
objectFit: "contain",
|
|
3208
|
-
borderRadius: "var(--
|
|
3455
|
+
borderRadius: "var(--sd-radius-lg)",
|
|
3209
3456
|
background: "var(--dsw-alias-bg-layer-2)"
|
|
3210
3457
|
};
|
|
3211
3458
|
const FILE_CHIP = {
|
|
@@ -3221,7 +3468,7 @@ window.__ModuleLoader__.load({
|
|
|
3221
3468
|
alignItems: "center",
|
|
3222
3469
|
gap: 6,
|
|
3223
3470
|
fontSize: 12,
|
|
3224
|
-
color: "var(--
|
|
3471
|
+
color: "var(--sd-label-tertiary)"
|
|
3225
3472
|
};
|
|
3226
3473
|
/**
|
|
3227
3474
|
* 图裂之后的占位。**仍然说清是哪一件**:名字(没有就用「附件图片」/「文件」)+ 体量 + 「附件不可用」。
|
|
@@ -3588,7 +3835,7 @@ window.__ModuleLoader__.load({
|
|
|
3588
3835
|
...ELLIPSIS,
|
|
3589
3836
|
fontSize: 12,
|
|
3590
3837
|
lineHeight: "16px",
|
|
3591
|
-
color: "var(--
|
|
3838
|
+
color: "var(--sd-label-tertiary)"
|
|
3592
3839
|
},
|
|
3593
3840
|
children: projectLabel(entry.projectDir, t)
|
|
3594
3841
|
}),
|
|
@@ -3596,7 +3843,7 @@ window.__ModuleLoader__.load({
|
|
|
3596
3843
|
style: {
|
|
3597
3844
|
fontSize: 12,
|
|
3598
3845
|
lineHeight: "16px",
|
|
3599
|
-
color: "var(--
|
|
3846
|
+
color: "var(--sd-label-tertiary)"
|
|
3600
3847
|
},
|
|
3601
3848
|
children: t("trash.row.workspaceGone")
|
|
3602
3849
|
}) : null
|
|
@@ -3645,7 +3892,7 @@ window.__ModuleLoader__.load({
|
|
|
3645
3892
|
...ELLIPSIS,
|
|
3646
3893
|
fontSize: 12,
|
|
3647
3894
|
lineHeight: "16px",
|
|
3648
|
-
color: "var(--
|
|
3895
|
+
color: "var(--sd-label-tertiary)"
|
|
3649
3896
|
},
|
|
3650
3897
|
children: t("trash.confirm.one.meta", {
|
|
3651
3898
|
project: projectLabel(pending.entry.projectDir, t),
|
|
@@ -3716,7 +3963,7 @@ window.__ModuleLoader__.load({
|
|
|
3716
3963
|
style: {
|
|
3717
3964
|
fontSize: 12,
|
|
3718
3965
|
lineHeight: "17px",
|
|
3719
|
-
color: "var(--
|
|
3966
|
+
color: "var(--sd-label-tertiary)"
|
|
3720
3967
|
},
|
|
3721
3968
|
children: t("trash.orphans.cacheNote")
|
|
3722
3969
|
})
|
|
@@ -3744,7 +3991,7 @@ window.__ModuleLoader__.load({
|
|
|
3744
3991
|
style: {
|
|
3745
3992
|
fontSize: 12,
|
|
3746
3993
|
lineHeight: "16px",
|
|
3747
|
-
color: "var(--
|
|
3994
|
+
color: "var(--sd-label-tertiary)"
|
|
3748
3995
|
},
|
|
3749
3996
|
children: t("trash.orphans.meta", {
|
|
3750
3997
|
size: fileSizeText(report.totalBytes),
|
|
@@ -3774,7 +4021,7 @@ window.__ModuleLoader__.load({
|
|
|
3774
4021
|
display: "inline-flex",
|
|
3775
4022
|
flex: "none",
|
|
3776
4023
|
paddingTop: 2,
|
|
3777
|
-
color: "var(--
|
|
4024
|
+
color: "var(--sd-label-tertiary)"
|
|
3778
4025
|
},
|
|
3779
4026
|
"aria-hidden": "true",
|
|
3780
4027
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconWarningOutlineRegular, { size: 15 })
|
|
@@ -4046,7 +4293,7 @@ window.__ModuleLoader__.load({
|
|
|
4046
4293
|
style: {
|
|
4047
4294
|
fontSize: 12,
|
|
4048
4295
|
lineHeight: "17px",
|
|
4049
|
-
color: "var(--
|
|
4296
|
+
color: "var(--sd-label-tertiary)"
|
|
4050
4297
|
},
|
|
4051
4298
|
children: hint
|
|
4052
4299
|
})]
|
|
@@ -4058,7 +4305,7 @@ window.__ModuleLoader__.load({
|
|
|
4058
4305
|
paddingTop: 1,
|
|
4059
4306
|
fontSize: 12,
|
|
4060
4307
|
lineHeight: "17px",
|
|
4061
|
-
color: "var(--
|
|
4308
|
+
color: "var(--sd-label-tertiary)",
|
|
4062
4309
|
whiteSpace: "nowrap"
|
|
4063
4310
|
},
|
|
4064
4311
|
children: status
|
|
@@ -4130,7 +4377,7 @@ window.__ModuleLoader__.load({
|
|
|
4130
4377
|
] });
|
|
4131
4378
|
}
|
|
4132
4379
|
/** 预览 + 确认:先摆出这些图片(点开可放大),再决定清不清;只清 `.dsh` 内无引用的图片副本。
|
|
4133
|
-
* `items`
|
|
4380
|
+
* `items` 只是预览清单(服务端按配置给条数,默认最多 200 条),`count` 才是这次要清理的总数。 */
|
|
4134
4381
|
function AttachmentConfirm({ count, items, busy, t, onCancel, onConfirm }) {
|
|
4135
4382
|
const { close, closeDisabled } = useIdleClose(busy, onCancel);
|
|
4136
4383
|
const attachments = items.map((item) => ({
|
|
@@ -4174,7 +4421,7 @@ window.__ModuleLoader__.load({
|
|
|
4174
4421
|
style: {
|
|
4175
4422
|
fontSize: 12,
|
|
4176
4423
|
lineHeight: "17px",
|
|
4177
|
-
color: "var(--
|
|
4424
|
+
color: "var(--sd-label-tertiary)"
|
|
4178
4425
|
},
|
|
4179
4426
|
children: t("maintenance.attachments.previewTruncated", {
|
|
4180
4427
|
shown: items.length,
|
|
@@ -4185,18 +4432,19 @@ window.__ModuleLoader__.load({
|
|
|
4185
4432
|
style: {
|
|
4186
4433
|
fontSize: 12,
|
|
4187
4434
|
lineHeight: "17px",
|
|
4188
|
-
color: "var(--
|
|
4435
|
+
color: "var(--sd-label-tertiary)"
|
|
4189
4436
|
},
|
|
4190
4437
|
children: t("maintenance.attachments.previewHint")
|
|
4191
4438
|
})
|
|
4192
4439
|
]
|
|
4193
4440
|
});
|
|
4194
4441
|
}
|
|
4195
|
-
function MaintenanceSettings({ deps, t = zhTranslate }) {
|
|
4442
|
+
function MaintenanceSettings({ deps, t = zhTranslate, view }) {
|
|
4196
4443
|
const maintenance = useMaintenance({
|
|
4197
4444
|
deps,
|
|
4198
4445
|
t
|
|
4199
4446
|
});
|
|
4447
|
+
if (view === "summary") return null;
|
|
4200
4448
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4201
4449
|
"data-dsh-session-delete": "maintenance-card",
|
|
4202
4450
|
style: CARD_SHELL,
|
|
@@ -26248,11 +26496,24 @@ window.__ModuleLoader__.load({
|
|
|
26248
26496
|
height: "100%",
|
|
26249
26497
|
minWidth: 0
|
|
26250
26498
|
};
|
|
26499
|
+
/** 返回键排在标题之后:排在之前会把整条标题的起点往右挤(docs/ui-spec.md 状态⑥)。
|
|
26500
|
+
* 标题排按紧凑按钮(`ui.css` 的 `.sd-button-sm`)预留高度:宽档父详情没有返回键、孩子视图有,
|
|
26501
|
+
* 不预留时下方元信息会在父子切换时跳 6px。 */
|
|
26251
26502
|
const HEAD = {
|
|
26252
26503
|
display: "flex",
|
|
26253
26504
|
alignItems: "flex-start",
|
|
26254
|
-
gap: 10
|
|
26505
|
+
gap: 10,
|
|
26506
|
+
minHeight: 28
|
|
26507
|
+
};
|
|
26508
|
+
const TITLE_LINE = {
|
|
26509
|
+
...ELLIPSIS,
|
|
26510
|
+
flex: "1 1 auto",
|
|
26511
|
+
minWidth: 0,
|
|
26512
|
+
fontSize: 15,
|
|
26513
|
+
fontWeight: 600,
|
|
26514
|
+
lineHeight: "22px"
|
|
26255
26515
|
};
|
|
26516
|
+
const BACK = { flex: "none" };
|
|
26256
26517
|
/** 元信息那张表:label 一列、值一列,窄档也不横向溢出。 */
|
|
26257
26518
|
const META = {
|
|
26258
26519
|
display: "grid",
|
|
@@ -26263,7 +26524,7 @@ window.__ModuleLoader__.load({
|
|
|
26263
26524
|
lineHeight: "17px"
|
|
26264
26525
|
};
|
|
26265
26526
|
const META_LABEL = {
|
|
26266
|
-
color: "var(--
|
|
26527
|
+
color: "var(--sd-label-tertiary)",
|
|
26267
26528
|
whiteSpace: "nowrap"
|
|
26268
26529
|
};
|
|
26269
26530
|
const META_VALUE = {
|
|
@@ -26305,8 +26566,8 @@ window.__ModuleLoader__.load({
|
|
|
26305
26566
|
* 我们一行只有一个气泡,直接把上限挂在气泡上(同一父宽下百分比与 `min()` 的结果一致,少一层)。
|
|
26306
26567
|
*/
|
|
26307
26568
|
const BUBBLE = {
|
|
26308
|
-
background: "var(--
|
|
26309
|
-
borderRadius: "var(--
|
|
26569
|
+
background: "var(--sd-specific-bubble)",
|
|
26570
|
+
borderRadius: "var(--sd-radius-xl)",
|
|
26310
26571
|
maxWidth: "min(calc(var(--dsh-chat-content-width, 748px) * .702), 82%)",
|
|
26311
26572
|
fontSize: "var(--dsh-content-font-size, 14px)",
|
|
26312
26573
|
lineHeight: "calc(22px + var(--dsh-content-font-delta, 0px))",
|
|
@@ -26320,7 +26581,7 @@ window.__ModuleLoader__.load({
|
|
|
26320
26581
|
const META_LINE = {
|
|
26321
26582
|
fontSize: "var(--dsh-content-font-size-secondary, 13px)",
|
|
26322
26583
|
lineHeight: "calc(18px + var(--dsh-content-font-delta-secondary, 0px))",
|
|
26323
|
-
color: "var(--
|
|
26584
|
+
color: "var(--sd-label-tertiary)"
|
|
26324
26585
|
};
|
|
26325
26586
|
/** 读失败与写失败共用的那块(`role="status"`;播报口径与其未验证的边界见 docs/ui-spec.md)。 */
|
|
26326
26587
|
const STATUS_BLOCK = {
|
|
@@ -26329,8 +26590,8 @@ window.__ModuleLoader__.load({
|
|
|
26329
26590
|
gap: 10,
|
|
26330
26591
|
padding: "12px 14px",
|
|
26331
26592
|
background: "var(--dsw-alias-bg-layer-2)",
|
|
26332
|
-
border: "0.5px solid var(--
|
|
26333
|
-
borderRadius: "var(--
|
|
26593
|
+
border: "0.5px solid var(--sd-border-l4)",
|
|
26594
|
+
borderRadius: "var(--sd-radius-lg)"
|
|
26334
26595
|
};
|
|
26335
26596
|
const BLOCK = {
|
|
26336
26597
|
display: "flex",
|
|
@@ -26362,13 +26623,13 @@ window.__ModuleLoader__.load({
|
|
|
26362
26623
|
minWidth: 0,
|
|
26363
26624
|
fontSize: 12,
|
|
26364
26625
|
lineHeight: "18px",
|
|
26365
|
-
color: "var(--
|
|
26626
|
+
color: "var(--sd-label-tertiary)"
|
|
26366
26627
|
};
|
|
26367
26628
|
const BLOCK_META = {
|
|
26368
26629
|
flex: "none",
|
|
26369
26630
|
fontSize: 11.5,
|
|
26370
26631
|
lineHeight: "18px",
|
|
26371
|
-
color: "var(--
|
|
26632
|
+
color: "var(--sd-label-tertiary)",
|
|
26372
26633
|
whiteSpace: "nowrap"
|
|
26373
26634
|
};
|
|
26374
26635
|
const BLOCK_BODY = {
|
|
@@ -26390,8 +26651,8 @@ window.__ModuleLoader__.load({
|
|
|
26390
26651
|
margin: 0,
|
|
26391
26652
|
padding: "8px 10px",
|
|
26392
26653
|
background: "var(--dsw-alias-bg-layer-2)",
|
|
26393
|
-
border: "0.5px solid var(--
|
|
26394
|
-
borderRadius: "var(--
|
|
26654
|
+
border: "0.5px solid var(--sd-border-l4)",
|
|
26655
|
+
borderRadius: "var(--sd-radius-md)",
|
|
26395
26656
|
fontFamily: "var(--ds-font-family-code)",
|
|
26396
26657
|
fontSize: 12,
|
|
26397
26658
|
lineHeight: "18px",
|
|
@@ -26403,7 +26664,7 @@ window.__ModuleLoader__.load({
|
|
|
26403
26664
|
const SECTION_LABEL = {
|
|
26404
26665
|
fontSize: 11.5,
|
|
26405
26666
|
lineHeight: "16px",
|
|
26406
|
-
color: "var(--
|
|
26667
|
+
color: "var(--sd-label-tertiary)"
|
|
26407
26668
|
};
|
|
26408
26669
|
/**
|
|
26409
26670
|
* 子会话行:折叠头同族的可点行,卡片底 + 小圆角;悬停走 sd-disclosure-row 的宿主配方。
|
|
@@ -26417,8 +26678,8 @@ window.__ModuleLoader__.load({
|
|
|
26417
26678
|
height: "auto",
|
|
26418
26679
|
cursor: "pointer",
|
|
26419
26680
|
background: "var(--dsw-alias-bg-layer-2)",
|
|
26420
|
-
border: "0.5px solid var(--
|
|
26421
|
-
borderRadius: "var(--
|
|
26681
|
+
border: "0.5px solid var(--sd-border-l4)",
|
|
26682
|
+
borderRadius: "var(--sd-radius-md)"
|
|
26422
26683
|
};
|
|
26423
26684
|
const TOOLBAR$1 = {
|
|
26424
26685
|
display: "flex",
|
|
@@ -26430,7 +26691,7 @@ window.__ModuleLoader__.load({
|
|
|
26430
26691
|
const TRUNCATED = {
|
|
26431
26692
|
fontSize: 11.5,
|
|
26432
26693
|
lineHeight: "16px",
|
|
26433
|
-
color: "var(--
|
|
26694
|
+
color: "var(--sd-label-tertiary)"
|
|
26434
26695
|
};
|
|
26435
26696
|
const ERROR_TEXT = { color: "var(--dsw-alias-state-error-primary)" };
|
|
26436
26697
|
const HOUR = 36e5;
|
|
@@ -26704,7 +26965,7 @@ window.__ModuleLoader__.load({
|
|
|
26704
26965
|
textAlign: "center",
|
|
26705
26966
|
fontSize: 12.5,
|
|
26706
26967
|
lineHeight: "18px",
|
|
26707
|
-
color: "var(--
|
|
26968
|
+
color: "var(--sd-label-tertiary)"
|
|
26708
26969
|
},
|
|
26709
26970
|
children: t("detail.notSelected")
|
|
26710
26971
|
});
|
|
@@ -26715,32 +26976,27 @@ window.__ModuleLoader__.load({
|
|
|
26715
26976
|
style: SHELL,
|
|
26716
26977
|
children: [
|
|
26717
26978
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
26979
|
+
"data-dsh-session-delete": "trash-detail-head",
|
|
26718
26980
|
style: HEAD,
|
|
26719
|
-
children: [
|
|
26981
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
26982
|
+
style: TITLE_LINE,
|
|
26983
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
26984
|
+
title,
|
|
26985
|
+
children: title
|
|
26986
|
+
})
|
|
26987
|
+
}), detailNav.parentContext !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
26720
26988
|
variant: "ghost",
|
|
26721
26989
|
size: "sm",
|
|
26722
|
-
style:
|
|
26990
|
+
style: BACK,
|
|
26723
26991
|
onClick: detailNav.parentContext.onBack,
|
|
26724
26992
|
title: detailNav.parentContext.title === "" ? t("common.untitled") : detailNav.parentContext.title,
|
|
26725
26993
|
children: t("detail.backToParent")
|
|
26726
26994
|
}) : wide ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
26727
26995
|
variant: "ghost",
|
|
26728
26996
|
size: "sm",
|
|
26729
|
-
style:
|
|
26997
|
+
style: BACK,
|
|
26730
26998
|
onClick: onBack,
|
|
26731
26999
|
children: t("detail.back")
|
|
26732
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
26733
|
-
style: { minWidth: 0 },
|
|
26734
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
26735
|
-
style: {
|
|
26736
|
-
...ELLIPSIS,
|
|
26737
|
-
fontSize: 15,
|
|
26738
|
-
fontWeight: 600,
|
|
26739
|
-
lineHeight: "22px"
|
|
26740
|
-
},
|
|
26741
|
-
title,
|
|
26742
|
-
children: title
|
|
26743
|
-
})
|
|
26744
27000
|
})]
|
|
26745
27001
|
}),
|
|
26746
27002
|
state.kind === "loading" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LoadingBody, { t }) : null,
|
|
@@ -26776,7 +27032,7 @@ window.__ModuleLoader__.load({
|
|
|
26776
27032
|
style: {
|
|
26777
27033
|
fontSize: 12,
|
|
26778
27034
|
lineHeight: "17px",
|
|
26779
|
-
color: "var(--
|
|
27035
|
+
color: "var(--sd-label-tertiary)",
|
|
26780
27036
|
wordBreak: "break-word"
|
|
26781
27037
|
},
|
|
26782
27038
|
children: state.message
|
|
@@ -26808,7 +27064,7 @@ window.__ModuleLoader__.load({
|
|
|
26808
27064
|
style: {
|
|
26809
27065
|
fontSize: 12.5,
|
|
26810
27066
|
lineHeight: "18px",
|
|
26811
|
-
color: "var(--
|
|
27067
|
+
color: "var(--sd-label-tertiary)"
|
|
26812
27068
|
},
|
|
26813
27069
|
children: t("detail.loading")
|
|
26814
27070
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -27185,7 +27441,7 @@ window.__ModuleLoader__.load({
|
|
|
27185
27441
|
* - 方向键走环:未选中时 ↓ 落第一条、↑ 落最后一条;到端回卷。
|
|
27186
27442
|
* - aria-activedescendant 只指向在 DOM 里的行;tab 落点永远有一行拿着。
|
|
27187
27443
|
* - Esc 只在下钻期间生效,回退分两程(先收孩子视图、再退下钻);退出下钻后把焦点交还选中行一次;
|
|
27188
|
-
*
|
|
27444
|
+
* 最上层是确认框/菜单时让路;焦点在面板外时不归清单(`scope`)。
|
|
27189
27445
|
*/
|
|
27190
27446
|
/** 事件是不是清单自己的:从行(`role="row"`)或容器起来才算;行内按钮 / 勾选框的 Enter / Space
|
|
27191
27447
|
* 归它们自己。测试里的窄事件没有 `target`,按清单自己的处理。 */
|
|
@@ -27194,6 +27450,17 @@ window.__ModuleLoader__.load({
|
|
|
27194
27450
|
if (target == null || target === event.currentTarget) return true;
|
|
27195
27451
|
return typeof target.getAttribute === "function" && target.getAttribute("role") === "row";
|
|
27196
27452
|
}
|
|
27453
|
+
/** 这条 Esc 该不该归清单处理(回答的是"清单收不收这一下",不是"target 在不在作用域里")。
|
|
27454
|
+
* 焦点在页面空地(`body` / `html`)时没有"面板外"的证据,按清单处理,保住窄档下钻的可发现性;
|
|
27455
|
+
* `document` 是 `fireEvent.keyDown(document, …)` 这类合成事件的目标,同样没有焦点信息。
|
|
27456
|
+
* 其余按作用域根的包含关系判;根还没挂上时按不处理。 */
|
|
27457
|
+
function shouldHandleEsc(event, scope) {
|
|
27458
|
+
const target = event.target;
|
|
27459
|
+
if (!(target instanceof Node)) return true;
|
|
27460
|
+
if (target === document || target === document.body || target === document.documentElement) return true;
|
|
27461
|
+
const root = scope.current;
|
|
27462
|
+
return root !== null && root.contains(target);
|
|
27463
|
+
}
|
|
27197
27464
|
function useTrashNav(input) {
|
|
27198
27465
|
const { ordered, selectedId, selectMode, drillEnabled, drilled, select, toggleCheck, back, focusRow } = input;
|
|
27199
27466
|
/** Esc 退出下钻后要交还焦点的那一行;等 `drilled` 落地、清单回到 DOM 之后再用一次。 */
|
|
@@ -27234,10 +27501,12 @@ window.__ModuleLoader__.load({
|
|
|
27234
27501
|
selectedId,
|
|
27235
27502
|
toggleCheck
|
|
27236
27503
|
]);
|
|
27504
|
+
const scope = input.scope;
|
|
27237
27505
|
(0, react.useEffect)(() => {
|
|
27238
27506
|
if (!drilled || !drillEnabled) return;
|
|
27239
27507
|
const onKey = (event) => {
|
|
27240
27508
|
if (event.key !== "Escape" || event.defaultPrevented) return;
|
|
27509
|
+
if (!shouldHandleEsc(event, scope)) return;
|
|
27241
27510
|
if (hasTopLayer(document)) return;
|
|
27242
27511
|
refocusRow.current = selectedId;
|
|
27243
27512
|
back();
|
|
@@ -27249,6 +27518,7 @@ window.__ModuleLoader__.load({
|
|
|
27249
27518
|
}, [
|
|
27250
27519
|
drillEnabled,
|
|
27251
27520
|
drilled,
|
|
27521
|
+
scope,
|
|
27252
27522
|
back,
|
|
27253
27523
|
selectedId
|
|
27254
27524
|
]);
|
|
@@ -27994,8 +28264,8 @@ window.__ModuleLoader__.load({
|
|
|
27994
28264
|
overflow: "auto",
|
|
27995
28265
|
paddingRight: 10,
|
|
27996
28266
|
paddingBottom: 8,
|
|
27997
|
-
"--dsh-scrollbar-thumb": "var(--
|
|
27998
|
-
"--dsh-scrollbar-thumb-hover": "var(--
|
|
28267
|
+
"--dsh-scrollbar-thumb": "var(--sd-scrollbar-bg-l2)",
|
|
28268
|
+
"--dsh-scrollbar-thumb-hover": "var(--sd-scrollbar-hover-l2)"
|
|
27999
28269
|
};
|
|
28000
28270
|
/** 详情那一栏的外壳(滚动归它自己,正文限宽在内部)。 */
|
|
28001
28271
|
const DETAIL_COLUMN = {
|
|
@@ -28006,8 +28276,8 @@ window.__ModuleLoader__.load({
|
|
|
28006
28276
|
overflow: "auto",
|
|
28007
28277
|
paddingRight: 10,
|
|
28008
28278
|
paddingBottom: 8,
|
|
28009
|
-
"--dsh-scrollbar-thumb": "var(--
|
|
28010
|
-
"--dsh-scrollbar-thumb-hover": "var(--
|
|
28279
|
+
"--dsh-scrollbar-thumb": "var(--sd-scrollbar-bg-l2)",
|
|
28280
|
+
"--dsh-scrollbar-thumb-hover": "var(--sd-scrollbar-hover-l2)"
|
|
28011
28281
|
};
|
|
28012
28282
|
const DETAIL_INNER = {
|
|
28013
28283
|
minWidth: 0,
|
|
@@ -28120,7 +28390,8 @@ window.__ModuleLoader__.load({
|
|
|
28120
28390
|
select: controller.select,
|
|
28121
28391
|
toggleCheck,
|
|
28122
28392
|
back: controller.back,
|
|
28123
|
-
focusRow
|
|
28393
|
+
focusRow,
|
|
28394
|
+
scope: rootRef
|
|
28124
28395
|
});
|
|
28125
28396
|
const pending = controller.pending;
|
|
28126
28397
|
const restoreEntries = controller.pendingEntries;
|
|
@@ -28194,7 +28465,7 @@ window.__ModuleLoader__.load({
|
|
|
28194
28465
|
style: {
|
|
28195
28466
|
fontSize: 12,
|
|
28196
28467
|
lineHeight: "17px",
|
|
28197
|
-
color: "var(--
|
|
28468
|
+
color: "var(--sd-label-tertiary)",
|
|
28198
28469
|
wordBreak: "break-word"
|
|
28199
28470
|
},
|
|
28200
28471
|
children: view.failure
|
|
@@ -28212,7 +28483,7 @@ window.__ModuleLoader__.load({
|
|
|
28212
28483
|
style: {
|
|
28213
28484
|
padding: "20px 2px",
|
|
28214
28485
|
fontSize: 12.5,
|
|
28215
|
-
color: "var(--
|
|
28486
|
+
color: "var(--sd-label-tertiary)"
|
|
28216
28487
|
},
|
|
28217
28488
|
children: t("trash.loading")
|
|
28218
28489
|
}) : view.empty ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -28237,7 +28508,7 @@ window.__ModuleLoader__.load({
|
|
|
28237
28508
|
flex: "none",
|
|
28238
28509
|
borderRadius: 999,
|
|
28239
28510
|
background: "var(--dsw-alias-bg-layer-2)",
|
|
28240
|
-
color: "var(--
|
|
28511
|
+
color: "var(--sd-label-tertiary)"
|
|
28241
28512
|
},
|
|
28242
28513
|
"aria-hidden": "true",
|
|
28243
28514
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconTrashOutlineRegular, { size: 20 })
|
|
@@ -28254,7 +28525,7 @@ window.__ModuleLoader__.load({
|
|
|
28254
28525
|
maxWidth: 340,
|
|
28255
28526
|
fontSize: 12,
|
|
28256
28527
|
lineHeight: "18px",
|
|
28257
|
-
color: "var(--
|
|
28528
|
+
color: "var(--sd-label-tertiary)"
|
|
28258
28529
|
},
|
|
28259
28530
|
children: t("trash.empty.hint")
|
|
28260
28531
|
})
|
|
@@ -28326,7 +28597,7 @@ window.__ModuleLoader__.load({
|
|
|
28326
28597
|
style: {
|
|
28327
28598
|
display: "inline-flex",
|
|
28328
28599
|
flex: "none",
|
|
28329
|
-
color: "var(--
|
|
28600
|
+
color: "var(--sd-label-tertiary)"
|
|
28330
28601
|
},
|
|
28331
28602
|
"aria-hidden": "true",
|
|
28332
28603
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconFolderOpenOutlineRegular, { size: 13 })
|
|
@@ -28347,7 +28618,7 @@ window.__ModuleLoader__.load({
|
|
|
28347
28618
|
marginLeft: "auto",
|
|
28348
28619
|
flex: "none",
|
|
28349
28620
|
fontSize: 11.5,
|
|
28350
|
-
color: "var(--
|
|
28621
|
+
color: "var(--sd-label-tertiary)"
|
|
28351
28622
|
},
|
|
28352
28623
|
children: t("trash.group.count", { count: group.entries.length })
|
|
28353
28624
|
})
|
|
@@ -28414,7 +28685,7 @@ window.__ModuleLoader__.load({
|
|
|
28414
28685
|
...ELLIPSIS,
|
|
28415
28686
|
fontSize: 12,
|
|
28416
28687
|
lineHeight: "16px",
|
|
28417
|
-
color: "var(--
|
|
28688
|
+
color: "var(--sd-label-tertiary)"
|
|
28418
28689
|
},
|
|
28419
28690
|
title: absoluteText(entry.deletedAt),
|
|
28420
28691
|
children: [
|
|
@@ -28429,7 +28700,7 @@ window.__ModuleLoader__.load({
|
|
|
28429
28700
|
style: {
|
|
28430
28701
|
fontSize: 12,
|
|
28431
28702
|
lineHeight: "16px",
|
|
28432
|
-
color: "var(--
|
|
28703
|
+
color: "var(--sd-label-tertiary)"
|
|
28433
28704
|
},
|
|
28434
28705
|
children: t("trash.row.workspaceGone")
|
|
28435
28706
|
}) : null,
|
|
@@ -28583,14 +28854,14 @@ window.__ModuleLoader__.load({
|
|
|
28583
28854
|
style: {
|
|
28584
28855
|
fontSize: 12,
|
|
28585
28856
|
lineHeight: "17px",
|
|
28586
|
-
color: "var(--
|
|
28857
|
+
color: "var(--sd-label-tertiary)"
|
|
28587
28858
|
},
|
|
28588
28859
|
children: t("trash.select.count", { count: checkedIds.length })
|
|
28589
28860
|
}) : hasEntries ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
28590
28861
|
style: {
|
|
28591
28862
|
fontSize: 12,
|
|
28592
28863
|
lineHeight: "17px",
|
|
28593
|
-
color: "var(--
|
|
28864
|
+
color: "var(--sd-label-tertiary)"
|
|
28594
28865
|
},
|
|
28595
28866
|
children: t("trash.summary", {
|
|
28596
28867
|
count: entries.length,
|
|
@@ -28632,7 +28903,7 @@ window.__ModuleLoader__.load({
|
|
|
28632
28903
|
minWidth: 0,
|
|
28633
28904
|
fontSize: 11.5,
|
|
28634
28905
|
lineHeight: "16px",
|
|
28635
|
-
color: "var(--
|
|
28906
|
+
color: "var(--sd-label-tertiary)"
|
|
28636
28907
|
},
|
|
28637
28908
|
children: [
|
|
28638
28909
|
noteParts[0],
|
|
@@ -28725,28 +28996,39 @@ window.__ModuleLoader__.load({
|
|
|
28725
28996
|
* **砍掉的**:`anchor`(把横幅对齐到某个元素的水平中心——本插件的撤销提示就是窗口居中)、
|
|
28726
28997
|
* `actions[].prefix`(动作文案前的连接词,本插件只用「撤销」两个字)。**没砍 `text`/`icon`/`tone`/
|
|
28727
28998
|
* `holdMs`/`actions`/`onDone`**:唯一的调用点这几样都在用。
|
|
28999
|
+
* **本插件加的**:`paused`(撤销在飞时冻结停留与淡出,失败原文要落回同一条横幅;官方没有这个开关)。
|
|
28728
29000
|
*/
|
|
28729
29001
|
/** 不指定时全不透明的停留时长。 */
|
|
28730
29002
|
const DEFAULT_HOLD_MS = 3e3;
|
|
28731
29003
|
/** 淡出时长。必须与样式表里 `sd-toast-fade` 的时长一致。 */
|
|
28732
29004
|
const FADE_MS = 1e3;
|
|
28733
|
-
function Toast({ text, icon, tone, holdMs = DEFAULT_HOLD_MS, actions, onDone }) {
|
|
29005
|
+
function Toast({ text, icon, tone, holdMs = DEFAULT_HOLD_MS, actions, paused = false, onDone }) {
|
|
28734
29006
|
const latestOnDone = (0, react.useRef)(onDone);
|
|
28735
29007
|
(0, react.useLayoutEffect)(() => {
|
|
28736
29008
|
latestOnDone.current = onDone;
|
|
28737
29009
|
}, [onDone]);
|
|
29010
|
+
/** 暂停时剩下的停留时间:`null` = 还没起表(新挂载)。淡出动画被 `animationPlayState` 冻住,
|
|
29011
|
+
* timer 也从同一冻结点续,两者才不会一个先到终点。 */
|
|
29012
|
+
const remaining = (0, react.useRef)(null);
|
|
28738
29013
|
(0, react.useEffect)(() => {
|
|
29014
|
+
if (paused) return;
|
|
29015
|
+
const wait = remaining.current ?? holdMs + FADE_MS;
|
|
29016
|
+
const deadline = Date.now() + wait;
|
|
28739
29017
|
const timer = setTimeout(() => {
|
|
28740
29018
|
latestOnDone.current();
|
|
28741
|
-
},
|
|
29019
|
+
}, wait);
|
|
28742
29020
|
return () => {
|
|
28743
29021
|
clearTimeout(timer);
|
|
29022
|
+
remaining.current = Math.max(0, deadline - Date.now());
|
|
28744
29023
|
};
|
|
28745
|
-
}, [holdMs]);
|
|
29024
|
+
}, [holdMs, paused]);
|
|
28746
29025
|
return (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
28747
29026
|
className: "sd-toast",
|
|
28748
29027
|
role: "alert",
|
|
28749
|
-
style: {
|
|
29028
|
+
style: {
|
|
29029
|
+
"--dsh-toast-hold": `${String(holdMs)}ms`,
|
|
29030
|
+
...paused ? { animationPlayState: "paused" } : {}
|
|
29031
|
+
},
|
|
28750
29032
|
children: [tone === "success" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
28751
29033
|
className: "sd-toast-icon sd-toast-icon-success",
|
|
28752
29034
|
"aria-hidden": "true",
|
|
@@ -28806,14 +29088,15 @@ window.__ModuleLoader__.load({
|
|
|
28806
29088
|
tone: "success",
|
|
28807
29089
|
text: current.kind === "one" ? t("undo.moved", { title: current.title === "" ? t("undo.session") : current.title }) : t("undo.movedBatch", { n: current.n }),
|
|
28808
29090
|
holdMs: UNDO_HOLD_MS,
|
|
29091
|
+
paused: gate.busy,
|
|
29092
|
+
onDone: clearUndoNotice,
|
|
28809
29093
|
actions: canUndo ? [{
|
|
28810
29094
|
label: t("undo.undo"),
|
|
28811
29095
|
disabled: gate.busy,
|
|
28812
29096
|
onClick: () => {
|
|
28813
29097
|
gate.around(() => restoreFromUndo(deps, current, t));
|
|
28814
29098
|
}
|
|
28815
|
-
}] : void 0
|
|
28816
|
-
onDone: clearUndoNotice
|
|
29099
|
+
}] : void 0
|
|
28817
29100
|
}, seq);
|
|
28818
29101
|
}
|
|
28819
29102
|
//#endregion
|
|
@@ -28823,11 +29106,14 @@ window.__ModuleLoader__.load({
|
|
|
28823
29106
|
*
|
|
28824
29107
|
* 槽位贡献的注册面见 docs/architecture.md §3,语义归属见 §4 的主人清单;
|
|
28825
29108
|
* 本文件只做注册与槽位接线。
|
|
28826
|
-
*
|
|
29109
|
+
* 菜单两行有两条路径:0.2.2+ 走 `ctx.contextMenus` 数据动作(`context-menu-actions.ts`,
|
|
29110
|
+
* afterContent、排在 Archive 之后);旧宿主没有这个服务,才注册 legacy 槽位行。
|
|
28827
29111
|
*
|
|
28828
29112
|
* 槽位出处(宿主产物,不是猜的;换代先复核):
|
|
28829
29113
|
* - sidebar.workspaces.session.menu.item:声明 asar @46391872;数据面投影 @46271806;
|
|
28830
|
-
* menuOpenState hook 工厂 @46195835
|
|
29114
|
+
* menuOpenState hook 工厂 @46195835;宿主官方菜单行 @46392466 起(pin/rename/fork/archive/session-id)。
|
|
29115
|
+
* 0.2.2 起内置动作改住 ctx.contextMenus,此槽位只剩 legacy React 扩展位;本插件把多选/删除
|
|
29116
|
+
* 迁到数据动作,见 `context-menu-actions.ts`。
|
|
28831
29117
|
* - shell.overlay:声明 asar @21206189。
|
|
28832
29118
|
* - sidebar.panellist:声明 @44796980;行渲染 @44783481;label 解析 @44795361(thunk 随 locale)。
|
|
28833
29119
|
* - main:渲染 @21185725;id 与 panellist 同字,selectPanel 只校验已注册 @21199710。
|
|
@@ -28848,8 +29134,11 @@ window.__ModuleLoader__.load({
|
|
|
28848
29134
|
* **`locale` 也不在这里**:宿主自己的 `ui-sidebar` 是硬注入它的,但那是"宿主知道自己那一代"的前提;
|
|
28849
29135
|
* 本插件要跨代活着 ⇒ 走 `ctx.get('locale')` 可选读,缺席时降级到内置 zh 文案(见 `copy.ts` 头注)。
|
|
28850
29136
|
* 写进 inject 就等于"这一代没有 locale 就不让整个客户端条目激活",而那等于宿主进程死。
|
|
29137
|
+
* **`contextMenus` 同理**:0.2.2+ 用 `ctx.get('contextMenus')` 探测,包本身在 package.json 的
|
|
29138
|
+
* `dsh.client.inject` 里排到前面;旧宿主没有这个服务时回退 legacy 槽位。
|
|
28851
29139
|
*/
|
|
28852
29140
|
const inject = ["slots"];
|
|
29141
|
+
/** legacy 菜单槽:只在 contextMenus 不可用时注册(0.2.2+ 的内置动作已迁到数据动作)。 */
|
|
28853
29142
|
const MENU_SLOT = "sidebar.workspaces.session.menu.item";
|
|
28854
29143
|
/** 会话行首 16px 格(无状态点时归第三方装饰);批量删除的主勾选框落这里,多选模式内可见。 */
|
|
28855
29144
|
const LEADING_SLOT = "sidebar.session.row.leading";
|
|
@@ -28891,10 +29180,13 @@ window.__ModuleLoader__.load({
|
|
|
28891
29180
|
}
|
|
28892
29181
|
/** 多选模式行首勾选框的宿主:槽位只投 `sessionId`,标题从 root hook 的 `byId[id].title` 读。 */
|
|
28893
29182
|
function PickLeadingRow({ sessionId, useSessions, t }) {
|
|
28894
|
-
const
|
|
29183
|
+
const select = typeof useSessions === "function" ? useSessions : NEVER_SESSIONS;
|
|
29184
|
+
const title = select((state) => state?.byId?.[sessionId]?.title?.trim() ?? "");
|
|
29185
|
+
const live = select((state) => state?.byId?.[sessionId]?.running === true);
|
|
28895
29186
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BatchPickCheckbox, {
|
|
28896
29187
|
sessionId,
|
|
28897
29188
|
title,
|
|
29189
|
+
live,
|
|
28898
29190
|
t
|
|
28899
29191
|
});
|
|
28900
29192
|
}
|
|
@@ -28943,10 +29235,12 @@ window.__ModuleLoader__.load({
|
|
|
28943
29235
|
/**
|
|
28944
29236
|
* **探测 → 注册**:取数通道在不在,只有真发一次请求才知道。
|
|
28945
29237
|
*
|
|
28946
|
-
* - 探测不过(打不通 /
|
|
28947
|
-
*
|
|
28948
|
-
*
|
|
28949
|
-
*
|
|
29238
|
+
* - 探测不过(打不通 / 回的不是信封 / 401·403 表示没有宿主身份 / 失败信封)⇒ **早返回**:
|
|
29239
|
+
* 一个槽位贡献都不注册。页面上的表现是"本插件不存在",而不是点下去必然失败的入口——半吊子 UI
|
|
29240
|
+
* 比没有 UI 更糟(docs/architecture.md §3「通道探测决定入口出现与否」)。
|
|
29241
|
+
* - 探测过(`ping` 回成功信封的 `pong`)⇒ 注册:那次往返证明路由在、依赖接上了、鉴权也过了。
|
|
29242
|
+
*
|
|
29243
|
+
* 支持面是 web UI 与桌面端,二者都从宿主 `/api` 取数(ADR-0033);没有旧前缀可退回。
|
|
28950
29244
|
*
|
|
28951
29245
|
* 面板那两条是**一对座位**:行的 `selectPanel(id)` 要求 `main` 里有同字 key,缺了面板那一头,
|
|
28952
29246
|
* 用户点一下就是宿主自己的 `layout.selectPanel: main panel "…" is not registered`。
|
|
@@ -28960,18 +29254,43 @@ window.__ModuleLoader__.load({
|
|
|
28960
29254
|
if (!await probeChannel(raw)) return;
|
|
28961
29255
|
const deps = buildDeps(raw);
|
|
28962
29256
|
const t = createTranslate(client);
|
|
28963
|
-
|
|
28964
|
-
|
|
28965
|
-
|
|
28966
|
-
|
|
28967
|
-
|
|
28968
|
-
|
|
28969
|
-
|
|
28970
|
-
|
|
28971
|
-
|
|
28972
|
-
|
|
28973
|
-
|
|
28974
|
-
|
|
29257
|
+
let usesContextMenus = false;
|
|
29258
|
+
const contextMenus = client.get?.("contextMenus");
|
|
29259
|
+
if (contextMenus !== null && contextMenus !== void 0 && typeof contextMenus.register === "function") {
|
|
29260
|
+
const sessions = client.get?.("sessions");
|
|
29261
|
+
try {
|
|
29262
|
+
client.effect(() => contextMenus.register(createSessionMenuContribution(t, sessions?.list)), "session-delete: context menu actions");
|
|
29263
|
+
usesContextMenus = true;
|
|
29264
|
+
} catch (error) {
|
|
29265
|
+
console.warn("[dsh-session-delete] context menu 注册失败,回退 legacy 菜单行:", error);
|
|
29266
|
+
}
|
|
29267
|
+
if (usesContextMenus) try {
|
|
29268
|
+
client.slots.inject("contextMenu.icon", () => client.slots.register({
|
|
29269
|
+
name: "contextMenu.icon",
|
|
29270
|
+
key: ICON_BATCH
|
|
29271
|
+
}, IconCheckOutlineRegular));
|
|
29272
|
+
client.slots.inject("contextMenu.icon", () => client.slots.register({
|
|
29273
|
+
name: "contextMenu.icon",
|
|
29274
|
+
key: ICON_DELETE
|
|
29275
|
+
}, IconTrashOutlineRegular));
|
|
29276
|
+
} catch (error) {
|
|
29277
|
+
console.warn("[dsh-session-delete] context menu 图标注册失败:", error);
|
|
29278
|
+
}
|
|
29279
|
+
}
|
|
29280
|
+
if (!usesContextMenus) {
|
|
29281
|
+
client.slots.inject(MENU_SLOT, () => client.slots.register({
|
|
29282
|
+
name: MENU_SLOT,
|
|
29283
|
+
id: `${PLUGIN_ID}.delete`,
|
|
29284
|
+
order: 900,
|
|
29285
|
+
inject: () => ({ t })
|
|
29286
|
+
}, DeleteRow));
|
|
29287
|
+
client.slots.inject(MENU_SLOT, () => client.slots.register({
|
|
29288
|
+
name: MENU_SLOT,
|
|
29289
|
+
id: `${PLUGIN_ID}.batch-select`,
|
|
29290
|
+
order: 850,
|
|
29291
|
+
inject: () => ({ t })
|
|
29292
|
+
}, SelectRow));
|
|
29293
|
+
}
|
|
28975
29294
|
client.slots.inject(LEADING_SLOT, () => client.slots.register({
|
|
28976
29295
|
name: LEADING_SLOT,
|
|
28977
29296
|
id: `${PLUGIN_ID}.batch-pick-leading`,
|