@xrn1997/dsh-session-delete 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -575,8 +575,8 @@ window.__ModuleLoader__.load({
575
575
  * 在这之前每个文件各写一遍「`let` 现场 + 监听集合 + `notify` + `useSyncExternalStore`」,
576
576
  * 各自的 seq / revision 也在分头维护。现在订阅/通告只住这一处。
577
577
  *
578
- * 另有一个**懒读快照**变体(`createLazyStore`):值由组件树之外的事实源(宿主 DOM)现读,
579
- * 脏了才重读;订阅脚手架仍住这里,观察者生命周期由调用方挂 `onFirstSubscribe` / `onLastUnsubscribe`。
578
+ * 另有一个**懒读快照**变体(`createLazyStore`):值由组件树之外的事实源(如宿主行槽位登记)现读,
579
+ * 脏了才重读;订阅脚手架仍住这里。
580
580
  *
581
581
  * 还有一个**最新票句柄**(`createLatestWins`):并发的异步读里"只有最新那次可以落地"原先在每个
582
582
  * 调用方各写一遍自增序号,现在只住这一处。它只拥有 compare-and-set——防抖、轮询、焦点策略仍是
@@ -637,7 +637,7 @@ window.__ModuleLoader__.load({
637
637
  }
638
638
  };
639
639
  }
640
- function createLazyStore(initial, read, lifecycle = {}) {
640
+ function createLazyStore(initial, read) {
641
641
  let cached = initial;
642
642
  let dirty = true;
643
643
  const listeners = /* @__PURE__ */ new Set();
@@ -657,17 +657,11 @@ window.__ModuleLoader__.load({
657
657
  notify();
658
658
  },
659
659
  subscribe(listener) {
660
- const reattaching = listeners.size === 0;
660
+ if (listeners.size === 0) dirty = true;
661
661
  listeners.add(listener);
662
- if (reattaching) {
663
- dirty = true;
664
- lifecycle.onFirstSubscribe?.();
665
- }
666
662
  return () => {
667
663
  listeners.delete(listener);
668
- if (listeners.size > 0) return;
669
- lifecycle.onLastUnsubscribe?.();
670
- dirty = true;
664
+ if (listeners.size === 0) dirty = true;
671
665
  };
672
666
  }
673
667
  };
@@ -700,150 +694,97 @@ window.__ModuleLoader__.load({
700
694
  return ref;
701
695
  }
702
696
  //#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
697
  //#region src/client/host-rows.ts
788
698
  /**
789
- * 宿主会话行的 DOM 适配:可见行解析、全选判定、模式内整行点击守卫。
699
+ * 宿主会话行的适配:**可见行由槽位组件登记**;选择动作与模式内整行点击守卫住 `batch-pick.ts`。
700
+ *
701
+ * 选中集(模式 + 名单)只住 `batch-selection-store.ts`;本文件只回答页面上的「当前有哪些可勾选的
702
+ * 会话行」:行槽位开关的挂载登记、渲染快照与按 id 的行查找。「这一行可不可勾」的 live 判据
703
+ * 也只住这里(`pickableRowOf` / `readVisibleSelectableRows`)。DOM 只用于守卫认行,且那处 DOM
704
+ * 读取随守卫住在 `batch-pick.ts`(ADR-0034 记下的受约束偏离)。
790
705
  *
791
- * 选中集(模式 + 名单)只住 batch-selection-store.ts;本文件只回答页面上的两件事:
792
- * 当前 DOM 里有哪些可选行,以及在某一行上点击是否算选中。交界是 data-row-key 与行尾登记的
793
- * rowInfo。渲染走 useSyncExternalStore 快照(ADR-0015);事件路径(全选)走当场读。
794
- * 模式内整行点击在 document 捕获阶段拦截,避免触发宿主的会话切换。
706
+ * 为什么不再扫 DOM:宿主的行槽位只在行可见时挂载,行首/行尾两颗开关(`BatchPickCheckbox` /
707
+ * `BatchPickButton`)都会登记 `{title, live}`;挂载/卸载就是「这一行在不在可见集合」的契约,
708
+ * 不需要 MutationObserver。
795
709
  */
796
710
  /**
797
- * 守卫从 `data-row-key` 反解出 sessionId,但拿不到槽位投影的标题/运行态 ⇒ 行尾兜底那颗
798
- * (它对每个非空行都渲染,哪怕 CSS 把整排藏起来)在 effect 里把这两件登记进这张表。
799
- * 生命周期跟着**行尾开关的挂载**走:卸载即注销({@link unregisterHostRowInfo}),
800
- * 表的大小被"当前渲染的行"钉住,而不是随用过的会话只增不减;行重挂载时 effect 会重新登记。
711
+ * 守卫从 `data-row-key` 反解出 sessionId,但拿不到槽位投影的标题/运行态 ⇒ 行首/行尾开关
712
+ * 在 effect 里把这两件登记进这张表。生命周期跟着**行槽位组件的挂载**走:卸载即注销
713
+ * ({@link unregisterHostRowInfo}),表的大小被"当前渲染的行"钉住,而不是随用过的会话只增不减;
714
+ * 行重挂载时 effect 会重新登记。**这张表就是可见行的事实源**(ADR-0034),不再由 DOM 行列表反查。
801
715
  *
802
- * **按挂载实例计数**:同一个会话在 DOM 里可能有两行(重复行),两行各挂一颗行尾开关;
716
+ * **按挂载实例计数**:同一个会话在 DOM 里可能有两行(重复行),两行各挂一组开关;
803
717
  * 其中一颗卸载时不能把另一颗还在用的登记一起摘掉——那会让这一行从"全选"里消失、标题也变空。
804
718
  */
805
719
  const rowInfo = /* @__PURE__ */ new Map();
806
720
  /**
807
- * 渲染侧快照 + DOM 观察:订阅脚手架住 `store.ts` 的 `createLazyStore`;这里只提供读法与
808
- * 观察者生命周期。宿主行的去留只体现在 `data-row-key` 节点上(增删、属性落定),文本变化
809
- * 不算(标题只认 `rowInfo` 的登记值)。首个订阅者起开始观察,最后一个走了断开。
721
+ * 渲染侧快照:`rowInfo` 变了才标脏;订阅脚手架住 `store.ts` 的 `createLazyStore`。
722
+ * `readVisibleSelectableRows` 按登记顺序(宿主渲染顺序)返回,不再观察 DOM。
810
723
  */
811
724
  const EMPTY_ROWS = [];
812
- let observer;
813
- function startObserving() {
814
- if (observer !== void 0 || typeof MutationObserver === "undefined") return;
815
- observer = new MutationObserver(() => visibleRows.invalidate());
816
- observer.observe(document.body, {
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
- });
725
+ const visibleRows = createLazyStore(EMPTY_ROWS, readVisibleSelectableRows);
726
+ const EMPTY_ROW_INFO = {
727
+ title: "",
728
+ live: false
729
+ };
831
730
  /**
832
- * 行尾兜底勾选开关登记一行:模式内与模式外都要登记(守卫进模式的那一帧就得有标题)。
833
- * 每次登记算**多一个挂载实例**;只有标题/运行态**真的变了**才标脏:重渲染时的同值登记
834
- * 不该把快照翻新一遍。
731
+ * 行槽位开关登记一行:模式内与模式外都要登记(守卫进模式的那一帧就得有标题)。
732
+ * 每次登记算**多一个挂载实例**;不给 info 时只占位,随后由 {@link updateHostRowInfo} 填值。
835
733
  */
836
734
  function registerHostRowInfo(sessionId, info) {
837
735
  const current = rowInfo.get(sessionId);
838
- const owners = (current?.owners ?? 0) + 1;
839
- const changed = current === void 0 || current.info.title !== info.title || current.info.live !== info.live;
840
- rowInfo.set(sessionId, {
841
- info,
842
- owners
843
- });
844
- if (changed) visibleRows.invalidate();
736
+ if (current === void 0) {
737
+ rowInfo.set(sessionId, {
738
+ info: info ?? EMPTY_ROW_INFO,
739
+ owners: 1
740
+ });
741
+ visibleRows.invalidate();
742
+ return;
743
+ }
744
+ current.owners += 1;
745
+ if (info !== void 0 && (current.info.title !== info.title || current.info.live !== info.live)) {
746
+ current.info = info;
747
+ visibleRows.invalidate();
748
+ }
845
749
  }
846
- /** 行尾开关卸载时调用:那一行已经离开渲染树,登记跟着走。
750
+ /**
751
+ * 标题/运行态变了就地更新:**不注销、不重登记**,否则这一行会被挪到登记序末尾(ADR-0034 决定 2)。
752
+ */
753
+ function updateHostRowInfo(sessionId, info) {
754
+ const current = rowInfo.get(sessionId);
755
+ if (current === void 0) return;
756
+ const title = info.title === "" ? current.info.title : info.title;
757
+ if (current.info.title === title && current.info.live === info.live) return;
758
+ current.info = {
759
+ title,
760
+ live: info.live
761
+ };
762
+ visibleRows.invalidate();
763
+ }
764
+ /**
765
+ * 槽位组件共用的登记生命周期:挂载登记、卸载注销;标题/运行态变化只走更新口。
766
+ * 两次 effect 分工是顺序契约:先占住首次登记序,再填值。
767
+ */
768
+ function useHostRowRegistration(sessionId, info) {
769
+ (0, react.useEffect)(() => {
770
+ registerHostRowInfo(sessionId);
771
+ return () => {
772
+ unregisterHostRowInfo(sessionId);
773
+ };
774
+ }, [sessionId]);
775
+ const { title, live } = info;
776
+ (0, react.useEffect)(() => {
777
+ updateHostRowInfo(sessionId, {
778
+ title,
779
+ live
780
+ });
781
+ }, [
782
+ sessionId,
783
+ title,
784
+ live
785
+ ]);
786
+ }
787
+ /** 行槽位开关卸载时调用:那一行已经离开渲染树,登记跟着走。
847
788
  * 还有别的挂载实例在用同一行(重复行)就只减计数,不摘登记。 */
848
789
  function unregisterHostRowInfo(sessionId) {
849
790
  const current = rowInfo.get(sessionId);
@@ -855,24 +796,29 @@ window.__ModuleLoader__.load({
855
796
  rowInfo.delete(sessionId);
856
797
  visibleRows.invalidate();
857
798
  }
799
+ /** `live` 行不可勾:与置灰的勾选框、守卫拦下的整行点击同一条判据。 */
800
+ function isPickable(info) {
801
+ return !info.live;
802
+ }
803
+ /**
804
+ * 守卫与全选共用的一行读数:这一行此刻可勾选时交回登记(含标题);未登记或在跑回 `undefined`。
805
+ * 登记表是可见行的事实源,所以"没登记"就是"这一行此刻不在可见集合里"。
806
+ */
807
+ function pickableRowOf(sessionId) {
808
+ const entry = rowInfo.get(sessionId);
809
+ return entry === void 0 || !isPickable(entry.info) ? void 0 : entry.info;
810
+ }
858
811
  /**
859
- * 「当前可见」的可选行:侧栏里**已渲染**的会话行(`data-row-key="session:…"`)——折叠组、
860
- * 归档过滤外的行不在 DOM 里,宿主隐藏的 subagent 也不在;在跑的行跳过(与置灰勾选框同口径)。
861
- * 标题/运行态来自行尾兜底登记的 `rowInfo`(模式内它对每个非空行都渲染);DOM 序就是侧栏顺序。
812
+ * 「当前可见」的可选行:**已登记**的会话行(槽位开关还挂在渲染树上)——折叠组、归档过滤外的行
813
+ * 没有挂载,因此没有登记;在跑的行跳过(与置灰勾选框同口径)。标题/运行态来自登记值;
814
+ * 顺序是登记顺序(宿主按列表顺序挂载行)。
862
815
  *
863
816
  * 这是**当场读**:事件路径(点「全选」那一刻的页面)与测试用它;渲染侧读下面的快照。
864
817
  */
865
818
  function readVisibleSelectableRows() {
866
819
  const rows = [];
867
- const seen = /* @__PURE__ */ new Set();
868
- for (const row of document.querySelectorAll("[data-row-key^=\"session:\"]")) {
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);
820
+ for (const [sessionId, entry] of rowInfo) {
821
+ if (!isPickable(entry.info)) continue;
876
822
  rows.push({
877
823
  sessionId,
878
824
  title: entry.info.title
@@ -881,9 +827,8 @@ window.__ModuleLoader__.load({
881
827
  return rows;
882
828
  }
883
829
  /**
884
- * `enabled === false` 时的稳定替身:同一个空表、不订阅、不读 DOM。
885
- * 浮条是 `shell.overlay` 的常驻槽位,模式外若照常订阅,body 上每次变更(以及每次行登记)
886
- * 都会白叫醒它重渲染一次。
830
+ * `enabled === false` 时的稳定替身:同一个空表、不订阅、不读登记。
831
+ * 浮条是 `shell.overlay` 的常驻槽位,模式外若照常订阅,每次行登记都会白叫醒它重渲染一次。
887
832
  */
888
833
  function getEmptyRowsSnapshot() {
889
834
  return EMPTY_ROWS;
@@ -892,86 +837,17 @@ window.__ModuleLoader__.load({
892
837
  return () => {};
893
838
  }
894
839
  /**
895
- * 渲染侧只读快照 + DOM 观察订阅;首个订阅者起开始观察,最后一个走了断开。
896
- * `enabled === false` 不订阅也不读 DOM——行列表只随多选态存在,模式外不必付观察的成本。
840
+ * 渲染侧只读快照 + 登记订阅;`enabled === false` 不订阅也不读登记——行列表只随多选态存在,
841
+ * 模式外不必付订阅的成本。
897
842
  */
898
843
  function useVisibleSelectableRows(enabled) {
899
844
  return (0, react.useSyncExternalStore)(enabled ? visibleRows.subscribe : noopSubscribe, enabled ? visibleRows.get : getEmptyRowsSnapshot, enabled ? visibleRows.get : getEmptyRowsSnapshot);
900
845
  }
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
846
  /**
920
- * 多选态内的整行点击守卫(document 捕获阶段)。
921
- *
922
- * 边界:行里的按钮 / 链接 / 输入 / 菜单项一律放行(否则 ⋯ 菜单、归档、置顶、单删都点不动);
923
- * 双击的第二下与 dblclick 不翻转(第一下已经选中,第二下不能又反掉,也不能打开宿主的重命名)。
924
- * 认行靠 `data-row-key="session:…"`——宿主将来改掉这个属性,最差是模式内点行不生效,
925
- * 勾选框本身仍在。模式外不挂监听:这时点行必须还是宿主的"打开会话"。
847
+ * 唯一的取数基础路径:宿主 `/api` + 插件前缀。九段 exact Fetch 路由、客户端探测与附件 URL
848
+ * 都从它起;`SESSION_DELETE_API_PREFIX` 只作为段名的相对前缀与错误原文。
926
849
  */
927
- function useSelectionRowClickGuard() {
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";
850
+ const OFFICIAL_SESSION_DELETE_API_PREFIX = `/api/dsh-session-delete-api`;
975
851
  /** 九个段名(相对前缀)。**每项 `{ path, method }` 两键**:某段用什么方法也是线上形状的一部分
976
852
  * (ADR-0003),家就在这一张表——路由记录(`src/api/route-table.ts`)逐行投影(`ROUTES.x.method`),
977
853
  * 浏览器半的 GET 段同样投影、写段把 `ROUTES.x.method` 交给 `src/client/remote.ts` 的 `post()`,
@@ -1044,7 +920,14 @@ window.__ModuleLoader__.load({
1044
920
  ids: "ids",
1045
921
  attachmentId: "id",
1046
922
  attachmentKind: "kind",
1047
- attachmentName: "name"
923
+ attachmentName: "name",
924
+ /** 图片引用的媒体类型 / 宽高:只给宿主 `attachments.readImage` 做引用校验,不参与路径拼接。 */
925
+ attachmentMediaType: "mediaType",
926
+ /** 字节数:图片支与文件支都带——它是宿主读面的引用一部分,并先过 `maxAttachmentBytes` 闸
927
+ * (别为一张已知超限的图让宿主整份读出来)。 */
928
+ attachmentBytes: "bytes",
929
+ attachmentWidth: "width",
930
+ attachmentHeight: "height"
1048
931
  };
1049
932
  /**
1050
933
  * 进程内判别「这已经是一次失败」——**按标记位认,不按 `instanceof`**(跨 realm / 跨 bundle 都不成立)。
@@ -1322,10 +1205,12 @@ window.__ModuleLoader__.load({
1322
1205
  //#endregion
1323
1206
  //#region src/client/remote.ts
1324
1207
  /**
1325
- * 浏览器半的取数通道:同源 fetch /dsh-session-delete-api,并处理信封与失败原文。
1208
+ * 浏览器半的取数通道:同源 fetch 宿主 `connection.fetch` 的 exact Fetch 路由(`/api` + 插件前缀)。
1209
+ * 信封与失败原文在这一层收口。
1326
1210
  *
1327
- * - 路由是宿主 web server 上的插件前缀;官方 remote.<namespace> 需要生成的 Typert 描述符,
1328
- * 第三方无法动态挂载,因此走这条薄线(ADR-0009)。
1211
+ * - 九段注册在宿主 `connection.fetch.register` 上,鉴权归宿主 `/api` bridge(ADR-0033);
1212
+ * 没有旧前缀 fallback——支持面是 web UI 与桌面端,二者的标准组合都装 `client-connection`。
1213
+ * 官方 remote.<namespace> 仍不可用(第三方拿不到 Typert 描述符的动态发现挂载点)。
1329
1214
  * - 九段的**段名与方法**住 `src/shared/wire.ts` 的结构化段名表(`ROUTES` 每项 `{ path, method }`),
1330
1215
  * 本文件只投影路径(`prefix + ROUTES.x.path`)与方法(GET 段直接取 `ROUTES.x.method`,写段把它
1331
1216
  * 交给 `post()`)——方法字面量不在本文件出现第二份;
@@ -1396,11 +1281,14 @@ window.__ModuleLoader__.load({
1396
1281
  };
1397
1282
  }
1398
1283
  }
1284
+ /** 宿主 `/api` 的 401/403 是纯文本,不是本插件信封:固定中文原文只住这里(浏览器半唯一消费者)。 */
1285
+ const UNTRUSTED_MESSAGE = "非受信来源";
1286
+ const UNAUTHENTICATED_MESSAGE = "需要宿主的 Web 身份:请用 dsh web 打印的那一行 URL 打开页面后重试";
1399
1287
  /**
1400
1288
  * 一次 HTTP 往返 → 信封。**`ok:false` 也是落地**(返回,不抛);只有往返本身失败才抛:
1401
- * 打不通(宿主没起来 / 路由没挂)或回的不是信封(前缀路由缺席时请求会落到 SPA 兜底处理器上,
1402
- * 回的是 index.html;中间层也可能回 `{}` / `{"error":"…"}` 这类非信封 JSON——判形住
1403
- * `isRemoteResult`,两种都落同一条诊断)。两种抛都带原文,组件侧的 `failureOf` 会把它们显示成 `client/unknown` + 原因。
1289
+ * 打不通(宿主没起来 / 路由没挂)或回的不是信封(中间层回 `{}` / `{"error":"…"}` 这类非信封 JSON,
1290
+ * 或 SPA 兜底的 index.html——判形住 `isRemoteResult`,几种都落同一条诊断)。两种抛都带原文,
1291
+ * 组件侧的 `failureOf` 会把它们显示成 `client/unknown` + 原因。
1404
1292
  */
1405
1293
  async function requestEnvelope(path, init) {
1406
1294
  let response;
@@ -1415,51 +1303,52 @@ window.__ModuleLoader__.load({
1415
1303
  } catch {
1416
1304
  body = void 0;
1417
1305
  }
1418
- if (!isRemoteResult(body)) throw new Error(`取数通道回的不是信封(HTTP ${String(response.status)})`);
1306
+ if (!isRemoteResult(body)) {
1307
+ if (response.status === 401) throw new Error(UNAUTHENTICATED_MESSAGE);
1308
+ if (response.status === 403) throw new Error(UNTRUSTED_MESSAGE);
1309
+ throw new Error(`取数通道回的不是信封(HTTP ${String(response.status)})`);
1310
+ }
1419
1311
  return body;
1420
1312
  }
1421
1313
  /**
1422
- * 前缀路由的 fetch 实现。**相对路径**:不拼 base、不认 origin——页面本身就是从宿主 web server
1423
- * 加载的,同源是这条薄线的全部前提。本机(含转发落在回环的桌面壳)靠回环 + Origin/Referer 过栅栏;
1424
- * 远程(LAN / 反代)客户端另由宿主 `connection` 判「已认证的 Web 身份」,所以这条通道**只对**
1425
- * `dsh web` 打印的 URL 打开过的页面成立(凭据是那行 URL 换来的 cookie,不是插件自己的秘密)。
1314
+ * exact Fetch 路由的 fetch 实现。**相对路径**:不拼 host、不认 origin——页面本身就是从宿主页面
1315
+ * 加载的,同源是这条通道的全部前提;鉴权归宿主 `/api` bridge(ADR-0033)。所以这条通道**只对**
1316
+ * 宿主页面打开过、带宿主身份的客户端成立(凭据是那次访问换来的 cookie,不是插件自己的秘密)。
1426
1317
  */
1427
- function createHttpRemote(prefix = SESSION_DELETE_API_PREFIX) {
1318
+ function createHttpRemote() {
1428
1319
  const post = (route, body) => ({
1429
1320
  method: route.method,
1430
1321
  headers: { "content-type": "application/json" },
1431
1322
  body: JSON.stringify(body)
1432
1323
  });
1433
1324
  return {
1434
- list: () => requestEnvelope(prefix + ROUTES.list.path, { method: ROUTES.list.method }),
1435
- ping: () => requestEnvelope(prefix + ROUTES.ping.path, { method: ROUTES.ping.method }),
1436
- orphans: () => requestEnvelope(prefix + ROUTES.orphans.path, { method: ROUTES.orphans.method }),
1437
- attachmentOrphans: () => requestEnvelope(prefix + ROUTES.attachmentOrphans.path, { method: ROUTES.attachmentOrphans.method }),
1438
- attachmentClean: () => requestEnvelope(prefix + ROUTES.attachmentClean.path, post(ROUTES.attachmentClean, {})),
1439
- detail: (entryId, before) => requestEnvelope(prefix + ROUTES.detail.path, post(ROUTES.detail, {
1325
+ list: () => requestEnvelope(OFFICIAL_SESSION_DELETE_API_PREFIX + ROUTES.list.path, { method: ROUTES.list.method }),
1326
+ ping: () => requestEnvelope(OFFICIAL_SESSION_DELETE_API_PREFIX + ROUTES.ping.path, { method: ROUTES.ping.method }),
1327
+ orphans: () => requestEnvelope(OFFICIAL_SESSION_DELETE_API_PREFIX + ROUTES.orphans.path, { method: ROUTES.orphans.method }),
1328
+ attachmentOrphans: () => requestEnvelope(OFFICIAL_SESSION_DELETE_API_PREFIX + ROUTES.attachmentOrphans.path, { method: ROUTES.attachmentOrphans.method }),
1329
+ attachmentClean: () => requestEnvelope(OFFICIAL_SESSION_DELETE_API_PREFIX + ROUTES.attachmentClean.path, post(ROUTES.attachmentClean, {})),
1330
+ detail: (entryId, before) => requestEnvelope(OFFICIAL_SESSION_DELETE_API_PREFIX + ROUTES.detail.path, post(ROUTES.detail, {
1440
1331
  [PARAMS.entryId]: entryId,
1441
1332
  ...before === void 0 ? {} : { [PARAMS.before]: before }
1442
1333
  })),
1443
- apply: (action, ids) => requestEnvelope(prefix + ROUTES.apply.path, post(ROUTES.apply, {
1334
+ apply: (action, ids) => requestEnvelope(OFFICIAL_SESSION_DELETE_API_PREFIX + ROUTES.apply.path, post(ROUTES.apply, {
1444
1335
  [PARAMS.action]: action,
1445
1336
  [PARAMS.ids]: [...ids]
1446
1337
  })),
1447
- empty: () => requestEnvelope(prefix + ROUTES.empty.path, post(ROUTES.empty, {}))
1338
+ empty: () => requestEnvelope(OFFICIAL_SESSION_DELETE_API_PREFIX + ROUTES.empty.path, post(ROUTES.empty, {}))
1448
1339
  };
1449
1340
  }
1450
1341
  /**
1451
- * 取数通道探测:一次 `GET /ping` 走通(或走到)就是"通道在"。
1342
+ * 探测唯一通道:一次 `GET /ping` 回成功信封(`pong`)才注册入口。
1452
1343
  *
1453
- * 判据是**「拿到信封」而不是「操作成功」**:`{ok:false, error}` 也是一次成功的往返(路由在、依赖接上了、
1454
- * 错误映射也生效了),那时该做的是把入口显示出来、让原文上屏;只有往返本身失败才算通道缺席 ⇒
1455
- * 一个槽位贡献都不注册(docs/architecture.md §3「通道探测决定入口出现与否」)。
1456
- *
1457
- * 探测用一次零成本的 `GET /ping`,不读清单,避免每次页面加载都跑全表对账。
1344
+ * - 401/403 是宿主 `/api` 的纯文本身份/归属失败:入口不出现,不装禁用态。
1345
+ * - 失败信封 / 404 / 非信封同样按不可用处理:`/api` 网关对未知路径也可能回错误形状,
1346
+ * 不能据此把入口摆出来让每个动作都失败。
1458
1347
  */
1459
1348
  async function probeChannel(raw) {
1460
1349
  try {
1461
- await raw.ping();
1462
- return true;
1350
+ const result = await raw.ping();
1351
+ return result.ok && result.value === "pong";
1463
1352
  } catch {
1464
1353
  return false;
1465
1354
  }
@@ -1509,6 +1398,90 @@ window.__ModuleLoader__.load({
1509
1398
  };
1510
1399
  }
1511
1400
  //#endregion
1401
+ //#region src/client/batch-selection-store.ts
1402
+ /**
1403
+ * 宿主列表批量删除的**选中集现场**:多选模式 + 选中名单,一份合并快照。
1404
+ *
1405
+ * 从 `batch-select.tsx` 拆出来的理由:多选态与选中集是**同一次交互的两半**(进入模式才出现勾选框,
1406
+ * 退出模式连名单一起清),读写它们的却有四个座位——菜单里的入口(`batch-menu-item.tsx`)、行首
1407
+ * 勾选框、行尾兜底、overlay 上的浮条与确认框。状态住这一处,`batch-select.tsx` 只剩控件,
1408
+ * `host-rows.ts` 只回答"页面上现在有哪些行、点行算不算选中"。订阅/通告那套仍只住在 `store.ts`
1409
+ * 的 `createStore` 里,本文件不另写一份。
1410
+ *
1411
+ * **合并快照**而不是两个 store:两半的变化总是成对出现(`exitSelection` 清两半),订阅者
1412
+ * (勾选框、浮条)两半都要读;分两份只会让"进模式的那一帧"出现两次通告、两个快照不同步的缝。
1413
+ *
1414
+ * 名单只收槽位投影原样给的 `sessionId` / `displayTitle`(浏览器半不自己拼 id):**逐条勾选序 = 名单序**;
1415
+ * 「全选」追加的条目按 `host-rows.ts` 的槽位登记序接在后面。`/apply` 那条「保持输入顺序」吃到的就是这份名单序。
1416
+ */
1417
+ const INITIAL = {
1418
+ selecting: false,
1419
+ picks: []
1420
+ };
1421
+ const selectionStore = createStore(INITIAL);
1422
+ function getSelection() {
1423
+ return selectionStore.get();
1424
+ }
1425
+ function getPicks() {
1426
+ return getSelection().picks;
1427
+ }
1428
+ /** 供组件订阅:模式与名单是同一次交互的两半,读它们的组件订同一份快照。 */
1429
+ function useSelection() {
1430
+ return selectionStore.use();
1431
+ }
1432
+ /** 「⋯ → 多选」按下时进入;重复进入是幂等的(同一份快照,不发通告)。 */
1433
+ function enterSelection() {
1434
+ selectionStore.update((current) => current.selecting ? current : {
1435
+ ...current,
1436
+ selecting: true
1437
+ });
1438
+ }
1439
+ /** 退出模式并清空名单:多选态和选中集是同一次交互的两半,退出后不该留下看不见的名单。 */
1440
+ function exitSelection() {
1441
+ selectionStore.update((current) => current.selecting || current.picks.length > 0 ? INITIAL : current);
1442
+ }
1443
+ /** 翻转一条:没选过就排队尾,选过就摘掉(列表里永远不会有重复 id)。 */
1444
+ function togglePick(item) {
1445
+ selectionStore.update((current) => {
1446
+ const at = current.picks.findIndex((candidate) => candidate.sessionId === item.sessionId);
1447
+ return {
1448
+ ...current,
1449
+ picks: at === -1 ? [...current.picks, item] : current.picks.filter((candidate) => candidate.sessionId !== item.sessionId)
1450
+ };
1451
+ });
1452
+ }
1453
+ /** 清空名单但**留在多选态**(「取消全选」那颗);已经空了就不发通告。 */
1454
+ function clearPicks() {
1455
+ selectionStore.update((current) => current.picks.length === 0 ? current : {
1456
+ ...current,
1457
+ picks: []
1458
+ });
1459
+ }
1460
+ /** 只追加还没选过的条目;名单序 = 原有选中在前、新条目按传入序接在后面。没有新条目就不发通告。 */
1461
+ function addPicks(items) {
1462
+ selectionStore.update((current) => {
1463
+ const chosen = new Set(current.picks.map((item) => item.sessionId));
1464
+ const additions = [];
1465
+ for (const item of items) {
1466
+ if (chosen.has(item.sessionId)) continue;
1467
+ chosen.add(item.sessionId);
1468
+ additions.push(item);
1469
+ }
1470
+ return additions.length === 0 ? current : {
1471
+ ...current,
1472
+ picks: [...current.picks, ...additions]
1473
+ };
1474
+ });
1475
+ }
1476
+ /** 执行后收缩:只留失败名单(空数组 = 全清)。成功的随通告从侧栏消失,不该留在名单里。 */
1477
+ function shrinkPicksToFailures(failedSessionIds) {
1478
+ const keep = new Set(failedSessionIds);
1479
+ selectionStore.update((current) => ({
1480
+ ...current,
1481
+ picks: current.picks.filter((item) => keep.has(item.sessionId))
1482
+ }));
1483
+ }
1484
+ //#endregion
1512
1485
  //#region src/client/ui/Checkbox.tsx
1513
1486
  /**
1514
1487
  * 勾选框,回收站多选态用。
@@ -1677,10 +1650,10 @@ window.__ModuleLoader__.load({
1677
1650
  }
1678
1651
  //#endregion
1679
1652
  //#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";
1653
+ var ui_default = "/*\r\n * 本插件自己的控件样式(**不再 require 宿主的 `@deepseek-ai/dsh-client-ui-primitives`**)。\r\n *\r\n * 出处:逐条抄自 `@deepseek-ai/dsh-client-ui-primitives@0.2.0-rc.2` 随包发布的明文 CSS\r\n * (`lib/Button.module.css`、`Tag.module.css`、`Modal.module.css`、`Tooltip.module.css`、\r\n * `Toast.module.css`、`Menu.module.css`;2026-10-15 又加两张:`lib/Checkbox.module.css`、\r\n * `lib/DisclosureRow.module.css`,以及 `dsh-client-ui-conversation` 里的\r\n * `ConversationRoot.module.css` 的 `.widthHandle` 那一族——宿主自己那根可拖的宽度把手;\r\n * 2026-10-17 又加一张,也是首张来自 ui-primitives 之外的包:`dsh-client-ui-workspace` 的\r\n * `Rows.module.css` 的 `.iconButton` 那一族——会话行尾悬停排那颗按钮的基底(行尾兜底勾选开关\r\n * 要跟宿主自己的归档/置顶同形)。\r\n * 这是官方给第三方 UI 插件的处方:\r\n * 「copy markup, CSS, and behavior from the primitive into the plugin … Rename copied classes under\r\n * your plugin's prefix, keep only `--dsw-alias-*` token references」(宿主技能\r\n * `cordis-plugin-development/references/practices.md` 原文)。理由是那个包**随宿主换代而变、且不经类型检查**,\r\n * 一个抛错的组件会把整个槽位条目打空(控制台 `slot entry crashed in '<slot>'`)。\r\n *\r\n * 三条改写规则:\r\n * 1. **类名全部加本插件前缀 `sd-`**(原来是 CSS module 的哈希名,全局无冲突由前缀承担)。\r\n * 2. **配色一律走 token**,一个颜色的字面量都没有——所以宿主换主题、改名 token 时最差是难看,\r\n * 不会崩。几何量(圆角/阴影/层级)同样取 token;**只有字号/行高/尺寸是字面量 px**,与官方 CSS 逐字一致。\r\n * 3. **`@keyframes` 全部改名**(`sd-` 前缀):它们是全局命名空间,撞名会互相覆盖。\r\n *\r\n * 只收本插件真正用到的部分:Button 的 `primary`/`ghost`/`outline` 三档与 `md`/`sm` 两号\r\n * (没有 `toolbar` 变体、没有 `lg`)、Tag 的 `danger` 一档、Modal 的全部骨架、Tooltip 的三个方位、\r\n * Toast 全部、Menu 行的全部(含 danger 态与 separator)、Checkbox 的全部、DisclosureRow 的\r\n * 折叠头那一半(没有 `data-state` 那档状态点)、宽度把手的视觉与光标。**没抄进来的就是本插件不用的**,\r\n * 不预先铺开——宿主那套里还有几十个变体与控件,抄全了等于把它的表面积整个搬进来。\r\n *\r\n * **七处不是抄本**:① `.sd-resizing`(拖动期间挂在面板根上、关掉文字选中)——宿主那份没有对应的类,\r\n * 它的把手由 JS 侧的 pointer capture 承担;② `[data-dsh-session-delete=\"trash-row\"]:focus-visible`\r\n * (清单那一行的键盘焦点描边,复用选中态那枚 `ghost-active-border`)——宿主那套表里行不是可聚焦\r\n * div,我们这一行是 `role=\"row\"` 且要吃 ↑↓,而这一代 `--dsw-focus-ring-color` 是 `transparent`;\r\n * ③ `.sd-rowaction-button` 的 `[aria-pressed='true']` 与 `:disabled` 两档(2026-10-17)——\r\n * 官方那颗 `iconButton` 只有常态与 hover,勾选开关要有\"按下\"和\"这条删不动\"两档,token 与回收站\r\n * 选中行同源(`--dsw-alias-button-ghost-active-*`);④ `.sd-batch-bar`(同日)——宿主没有\"浮条\"\r\n * 这件控件,四件 token 自拼;⑤ 宿主行的批量选中底色与兜底隐藏(同日改版)——宿主的行选中态不归\r\n * 第三方,整行可见性靠 `data-row-key` + `:has`,token 与回收站选中行同源;行首格在场时行尾兜底退场;\r\n * ⑥ `.sd-batch-pick-leading`(同日)——宿主行首那格只给 16×20,勾选框要一层 inline-flex 包装才压得回\r\n * 同样大小,官方没有这层包装;⑦ `.sd-toast-action:disabled`(2026-11-01)——撤销横幅在飞时那颗动作要\r\n * 看着就按不动,官方 Toast 的 action 没有禁用档,而我们这份自带 `color`、浏览器默认置灰不生效,\r\n * 档位与③的禁用档同源(`opacity` + `not-allowed`)。\r\n * 整张表里就这七条是我们自造的,标在这里,\r\n * 下次做「与官方逐条比对」时它属于**已知差异**,不是漂移。\r\n *\r\n * **另有一处已知差异不住在本文件**:回收站组头那颗按钮的整套 UA reset。宿主 `DisclosureRow` 那份是\r\n * `div`,没有这个坑;我们这行要做成可点的 `<button>`,UA 样式表会把背景、边框、内距、字体、居中与\r\n * flex 基线一起渗进来(真机上就是项目名整条居中,2026-10-15 截图)。它写在\r\n * `src/client/trash-panel.tsx` 组头那一行的 inline 样式上,**具体 reset 了哪几项由\r\n * `tests/skeleton.test.ts` 逐条钉住**(不在这里再抄一份名单)——所以「与官方逐条比对」时本表仍然只数\r\n * 上面那些,这条不算进抄本差异。\r\n */\r\n\r\n/* ────────────────────────── Button ────────────────────────── */\r\n\r\n.sd-button {\r\n box-sizing: border-box;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n gap: 4px;\r\n border: none;\r\n border-radius: var(--dsw-radius-md);\r\n cursor: pointer;\r\n font-size: 14px;\r\n line-height: 22px;\r\n color: var(--dsw-alias-label-primary);\r\n background: transparent;\r\n padding: 0 14px;\r\n}\r\n\r\n.sd-button:disabled {\r\n cursor: not-allowed;\r\n opacity: 0.4;\r\n}\r\n\r\n.sd-button-md {\r\n height: 36px;\r\n}\r\n\r\n/* 紧凑档:尺寸不随填充/描边换档(官方原文的注释即此意)。 */\r\n.sd-button-sm {\r\n height: 28px;\r\n font-size: 12px;\r\n line-height: 18px;\r\n padding: 0 10px;\r\n border-radius: var(--dsw-radius-sm);\r\n}\r\n\r\n.sd-button-primary {\r\n background: var(--dsw-alias-button-primary-fill);\r\n color: var(--dsw-alias-label-primary-foreground);\r\n}\r\n\r\n.sd-button-primary:hover:not(:disabled) {\r\n background: var(--dsw-alias-button-primary-hover);\r\n}\r\n\r\n.sd-button-ghost:hover:not(:disabled) {\r\n background: var(--dsw-alias-interactive-bg-hover);\r\n}\r\n\r\n.sd-button-ghost:active:not(:disabled) {\r\n background: var(--dsw-alias-interactive-bg-active);\r\n}\r\n\r\n.sd-button-outline {\r\n border: 0.5px solid var(--dsw-alias-border-l3);\r\n background: transparent;\r\n}\r\n\r\n.sd-button-outline:hover:not(:disabled) {\r\n background: var(--dsw-alias-interactive-bg-hover);\r\n}\r\n\r\n.sd-button-icon {\r\n display: inline-flex;\r\n width: 16px;\r\n height: 16px;\r\n align-items: center;\r\n justify-content: center;\r\n}\r\n\r\n/* ────────────────────────── Tag ────────────────────────── */\r\n\r\n/* 胶囊几何固定:一个 tag 在哪里都是同一个尺寸,只有色板随 tone 走。 */\r\n.sd-tag {\r\n display: inline-flex;\r\n align-items: center;\r\n border-radius: 999px;\r\n corner-shape: round;\r\n padding: 1px 8px;\r\n font-size: 11px;\r\n line-height: 17px;\r\n font-weight: 500;\r\n white-space: nowrap;\r\n}\r\n\r\n/* 状态色自带填充:底色由自身颜色 10% 混出,换色板时底与字一起走,不需要第二个 token。 */\r\n.sd-tag-danger {\r\n background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);\r\n color: var(--dsw-alias-state-error-primary);\r\n}\r\n\r\n/* ────────────────────────── Modal ────────────────────────── */\r\n\r\n/* 整视口层(figma Mask + Dialog):蒙层 + 居中卡片。四边留 24px,上下按外壳公布的浮层内缩加宽,\r\n 高卡片才不会顶到外壳。卡片按这个内边距盒取尺寸:消费方用 `max-height:100%` 收口,不自己算视口。 */\r\n.sd-modal-root {\r\n pointer-events: auto;\r\n position: fixed;\r\n inset: 0;\r\n z-index: 1000;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n padding: max(24px, var(--dsh-frame-overlay-top, 24px)) 24px;\r\n}\r\n\r\n/* 只有绘制避让原生标题栏;根仍然挡住背景点击。 */\r\n.sd-modal-mask {\r\n position: absolute;\r\n inset: var(--dsh-frame-chrome-top, 0px) 0 0;\r\n backdrop-filter: var(--dsw-mask-blur);\r\n}\r\n\r\n.sd-modal-mask::after {\r\n content: '';\r\n position: absolute;\r\n inset: 0;\r\n background: var(--dsw-alias-bg-mask-1);\r\n animation: sd-modal-enter var(--ds-transition-duration) var(--ds-ease-in-out);\r\n}\r\n\r\n.sd-modal-dialog {\r\n box-sizing: border-box;\r\n position: relative;\r\n z-index: 1;\r\n display: flex;\r\n flex-direction: column;\r\n gap: 20px;\r\n width: min(380px, 100%);\r\n padding: 0 0 24px;\r\n overflow: hidden;\r\n border: 0;\r\n border-radius: var(--dsw-radius-panel);\r\n background: var(--dsw-alias-bg-layer-2);\r\n box-shadow: var(--dsw-elevation-prominent);\r\n animation: sd-modal-enter var(--ds-transition-duration) var(--ds-ease-in-out);\r\n}\r\n\r\n@keyframes sd-modal-enter {\r\n from { opacity: 0; }\r\n to { opacity: 1; }\r\n}\r\n\r\n@media (prefers-reduced-motion: reduce) {\r\n .sd-modal-mask::after,\r\n .sd-modal-dialog { animation: none; }\r\n}\r\n\r\n.sd-modal-dialog:focus {\r\n outline: none;\r\n}\r\n\r\n.sd-modal-content {\r\n display: flex;\r\n flex-direction: column;\r\n width: 100%;\r\n}\r\n\r\n/* 头行:标题左、关闭钮右。 */\r\n.sd-modal-header {\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n gap: 8px;\r\n padding: 22px 14px 12px 24px;\r\n}\r\n\r\n.sd-modal-title {\r\n margin: 0;\r\n font-size: 16px;\r\n line-height: 24px;\r\n font-weight: 500;\r\n color: var(--dsw-alias-label-primary);\r\n}\r\n\r\n.sd-modal-close {\r\n flex: none;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n width: 28px;\r\n height: 28px;\r\n border: none;\r\n border-radius: var(--dsw-radius-sm);\r\n background: transparent;\r\n cursor: pointer;\r\n color: var(--dsw-alias-label-secondary);\r\n}\r\n\r\n.sd-modal-close:hover {\r\n background: var(--dsw-alias-interactive-bg-hover);\r\n}\r\n\r\n.sd-modal-body {\r\n display: flex;\r\n flex-direction: column;\r\n min-width: 0;\r\n margin-top: 20px;\r\n padding: 0 24px;\r\n}\r\n\r\n.sd-modal-footer {\r\n display: flex;\r\n align-items: center;\r\n justify-content: flex-end;\r\n gap: 8px;\r\n padding: 0 24px;\r\n}\r\n\r\n/* ────────────────────────── Tooltip ────────────────────────── */\r\n\r\n.sd-tooltip-bubble {\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 8px;\r\n position: fixed;\r\n z-index: 100;\r\n /* 定宽收缩只按 `left` 到视口右缘量,靠右的锚点会提前折行;\r\n `max-content` 只按文案定宽,再用半个视口收口。 */\r\n width: max-content;\r\n max-width: 50vw;\r\n padding: 3px 7px;\r\n border-radius: var(--dsw-radius-sm);\r\n background: var(--dsw-alias-tooltip-bg);\r\n color: var(--dsw-static-neutral-bluish-00);\r\n font-size: 13px;\r\n line-height: 20px;\r\n white-space: pre-line;\r\n /* 不可断的长 token(URL、路径)不许顶破 max-width。 */\r\n overflow-wrap: break-word;\r\n pointer-events: none;\r\n animation: sd-tooltip-in 150ms var(--ds-ease-in-out);\r\n}\r\n\r\n.sd-tooltip-bubble[data-portal] { z-index: 1100; }\r\n.sd-tooltip-label { min-width: 0; }\r\n\r\n.sd-tooltip-bubble[data-side='right'] {\r\n transform: translateY(-50%);\r\n}\r\n\r\n.sd-tooltip-bubble[data-side='bottom'] {\r\n transform: translateX(-50%);\r\n}\r\n\r\n.sd-tooltip-bubble[data-side='top'] {\r\n transform: translate(-50%, -100%);\r\n}\r\n\r\n@keyframes sd-tooltip-in {\r\n from { opacity: 0; }\r\n}\r\n\r\n@media (prefers-reduced-motion: reduce) {\r\n .sd-tooltip-bubble {\r\n animation: none;\r\n }\r\n}\r\n\r\n/* ────────────────────────── Toast ────────────────────────── */\r\n\r\n/* 淡出延迟由组件以 `--dsh-toast-hold` 给出 ⇒ 同一个值同时驱动卸载计时器与这段动画,\r\n 两者不可能再对不上。淡出**时长**仍需与组件里的 FADE_MS 一致(没有 owner 会改它)。 */\r\n.sd-toast {\r\n position: fixed;\r\n top: 40px;\r\n left: 50%;\r\n /* 高过图片灯箱底层的 1000:预览开着时报错也必须读得到。 */\r\n z-index: 1100;\r\n /* 通告不截点击。 */\r\n pointer-events: none;\r\n display: flex;\r\n align-items: center;\r\n gap: 10px;\r\n /* 定宽盒设了 `left` 只按右侧空间收缩(-50% 的位移发生在定尺之后)⇒ 靠窗边的横幅会提前折行。\r\n `max-content` 只按文案定宽,再由下面的 max-width 收口。 */\r\n width: max-content;\r\n max-width: min(640px, calc(100vw - 48px));\r\n padding: 12px 16px;\r\n border-radius: var(--dsw-radius-lg);\r\n /* 设计平台的 toast 表面在**两套主题下都是深的**(浅色 bluish-800 / 深色 bluish-750),\r\n 所以这里的字色与动作色是固定值,不随主题翻转。 */\r\n background: var(--dsw-alias-toast-bg);\r\n color: var(--dsw-alias-toast-label);\r\n font-size: 14px;\r\n line-height: 22px;\r\n box-shadow: var(--dsw-shadow-lv3);\r\n transform: translateX(-50%);\r\n animation:\r\n sd-toast-in 160ms ease-out,\r\n sd-toast-fade 1000ms ease var(--dsh-toast-hold, 3000ms) forwards;\r\n}\r\n\r\n.sd-toast-icon {\r\n display: grid;\r\n place-items: center;\r\n flex: none;\r\n color: var(--dsw-alias-state-warn-label);\r\n}\r\n\r\n.sd-toast-icon-success {\r\n color: var(--dsw-alias-state-success-primary);\r\n}\r\n\r\n.sd-toast-text {\r\n min-width: 0;\r\n}\r\n\r\n.sd-toast-action {\r\n display: inline;\r\n padding: 0;\r\n border: 0;\r\n background: none;\r\n color: var(--dsw-static-deepseek-400);\r\n font: inherit;\r\n cursor: pointer;\r\n /* 动作文字与周围文案之间的呼吸位。 */\r\n margin-inline: 3px;\r\n /* 横幅表面整体是穿透点击的,只有动作文字接指针。 */\r\n pointer-events: auto;\r\n}\r\n\r\n.sd-toast-action:hover {\r\n opacity: 0.8;\r\n}\r\n\r\n/* 本插件自造的第七条(不是抄本,见文件头点名):撤销横幅在飞时那颗动作要**看着就按不动**。\r\n 官方 Toast 的 action 只有常态与 hover,没有禁用档;而 `.sd-toast-action` 自带 `color`,\r\n 浏览器默认的置灰根本不生效(真机上只会看见一个光标照常是手、点下去没反应的「撤销」)。\r\n 禁用档与行尾勾选开关同源(`opacity` + `not-allowed`),不新造颜色。 */\r\n.sd-toast-action:disabled {\r\n opacity: 0.4;\r\n cursor: not-allowed;\r\n}\r\n\r\n@keyframes sd-toast-in {\r\n from {\r\n opacity: 0;\r\n transform: translate(-50%, -6px);\r\n }\r\n\r\n to {\r\n opacity: 1;\r\n transform: translate(-50%, 0);\r\n }\r\n}\r\n\r\n@keyframes sd-toast-fade {\r\n to {\r\n opacity: 0;\r\n visibility: hidden;\r\n }\r\n}\r\n\r\n/* 减动效去掉滑入;延迟淡出(透明度变化、不是位移)仍然在定时卸载前收掉横幅。 */\r\n@media (prefers-reduced-motion: reduce) {\r\n .sd-toast {\r\n animation: sd-toast-fade 1000ms ease var(--dsh-toast-hold, 3000ms) forwards;\r\n }\r\n}\r\n\r\n/* ────────────────────────── 菜单行 ────────────────────────── */\r\n\r\n.sd-menu-item-wrap {\r\n position: relative;\r\n}\r\n\r\n/* 菜单格:紧凑文字、图标与选中间距。 */\r\n.sd-menu-item {\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n width: 100%;\r\n min-height: 34px;\r\n padding: 6px 8px;\r\n border: none;\r\n border-radius: var(--dsw-radius-md);\r\n background: transparent;\r\n cursor: pointer;\r\n font-size: 13px;\r\n line-height: 20px;\r\n color: var(--dsw-alias-label-primary);\r\n text-align: left;\r\n}\r\n\r\n.sd-menu-item:hover:not(:disabled) {\r\n background: var(--dsw-alias-interactive-bg-hover);\r\n}\r\n\r\n/* 方向键移动的是真焦点 ⇒ 键盘所在的那一行拿指针同款填充;填充就是这一行的焦点呈现,\r\n 浏览器默认的 ring 再叠一层就重了。 */\r\n.sd-menu-item:focus-visible:not(:disabled) {\r\n background: var(--dsw-alias-interactive-bg-hover);\r\n outline: none;\r\n}\r\n\r\n.sd-menu-item:disabled {\r\n opacity: 0.4;\r\n cursor: not-allowed;\r\n}\r\n\r\n.sd-menu-item-icon {\r\n display: inline-flex;\r\n flex: none;\r\n width: 14px;\r\n height: 14px;\r\n align-items: center;\r\n justify-content: center;\r\n color: var(--dsw-alias-menu-icon);\r\n}\r\n\r\n.sd-menu-item-icon svg {\r\n width: 14px;\r\n height: 14px;\r\n}\r\n\r\n.sd-menu-item-label {\r\n flex: 1;\r\n min-width: 0;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n}\r\n\r\n/* 破坏性行:error 字/图标 + danger 悬停底。 */\r\n.sd-menu-item-danger {\r\n color: var(--dsw-alias-state-error-primary);\r\n}\r\n\r\n.sd-menu-item-danger .sd-menu-item-icon {\r\n color: var(--dsw-alias-state-error-primary);\r\n}\r\n\r\n.sd-menu-item-danger:hover:not(:disabled) {\r\n background: var(--dsw-alias-interactive-bg-hover-danger);\r\n}\r\n\r\n.sd-menu-item-danger:focus-visible:not(:disabled) {\r\n background: var(--dsw-alias-interactive-bg-hover-danger);\r\n outline: none;\r\n}\r\n\r\n/* 分隔格:发丝线上下的间距。取 l2 而不是 l1——l1 在这个表面上几乎看不见。 */\r\n.sd-menu-separator {\r\n height: 0.5px;\r\n margin: 3px 2px;\r\n background: var(--dsw-alias-border-l2);\r\n}\r\n\r\n/* ────────────────────────── Checkbox ────────────────────────── */\r\n/* 抄 `lib/Checkbox.module.css` 全部四条声明,逐字。勾选框本体是**原生 input** + `accent-color`,\r\n 不是自绘的圆框——回收站多选态用它(清单行左侧那一格)。 */\r\n\r\n.sd-checkbox {\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 6px;\r\n font-size: 14px;\r\n line-height: 20px;\r\n color: var(--dsw-alias-label-primary);\r\n cursor: pointer;\r\n}\r\n\r\n.sd-checkbox input {\r\n flex: 0 0 auto;\r\n width: 16px;\r\n height: 16px;\r\n margin: 0;\r\n accent-color: var(--dsw-alias-brand-primary);\r\n cursor: inherit;\r\n}\r\n\r\n.sd-checkbox input:focus-visible {\r\n outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));\r\n outline-offset: 2px;\r\n}\r\n\r\n.sd-checkbox:has(input:disabled) {\r\n cursor: default;\r\n opacity: 0.5;\r\n}\r\n\r\n/* ────────────────────────── DisclosureRow(分组折叠头) ────────────────────────── */\r\n/* 抄 `lib/DisclosureRow.module.css` 的 `.root`/`.row`/`.leading`/`.title` 与那两枚 glyph 的\r\n 显隐。没抄进来的是 `StateDot`(`svg[data-state]`)那一档——它说\"这是状态标记不是文字装饰\",\r\n 我们的组头没有状态点。行高与 leading 盒走 `--dsh-content-font-delta` 那根轴,\r\n 标题字号走 secondary 档:宿主的\"设置里调正文字号\"会一起推着它走。 */\r\n\r\n.sd-disclosure {\r\n display: flex;\r\n flex-direction: column;\r\n width: 100%;\r\n min-width: 0;\r\n}\r\n\r\n.sd-disclosure-row {\r\n position: relative;\r\n overflow: hidden;\r\n display: flex;\r\n align-items: center;\r\n height: calc(24px + var(--dsh-content-font-delta, 0px));\r\n min-width: 0;\r\n color: var(--dsw-alias-label-tertiary);\r\n transition: color 100ms ease;\r\n}\r\n\r\n.sd-disclosure-row[data-expandable] {\r\n cursor: pointer;\r\n}\r\n\r\n.sd-disclosure-row:hover {\r\n color: var(--dsw-alias-label-secondary);\r\n}\r\n\r\n.sd-disclosure-leading {\r\n position: relative;\r\n flex: none;\r\n width: calc(16px + var(--dsh-content-font-delta, 0px));\r\n height: calc(16px + var(--dsh-content-font-delta, 0px));\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n margin-right: 6px;\r\n padding: 0;\r\n border: none;\r\n background: none;\r\n color: inherit;\r\n}\r\n\r\n.sd-disclosure-leading svg:not([data-state]) {\r\n width: calc(14px + var(--dsh-content-font-delta, 0px));\r\n height: calc(14px + var(--dsh-content-font-delta, 0px));\r\n}\r\n\r\nbutton.sd-disclosure-leading {\r\n cursor: pointer;\r\n}\r\n\r\n.sd-disclosure-title {\r\n flex: none;\r\n font-size: var(--dsh-content-font-size-secondary, 13px);\r\n line-height: calc(24px + var(--dsh-content-font-delta, 0px));\r\n color: inherit;\r\n}\r\n\r\n@media (prefers-reduced-motion: reduce) {\r\n .sd-disclosure-row {\r\n transition: none;\r\n }\r\n}\r\n\r\n/* ────────────────────────── 宽度把手(可拖的分栏线) ────────────────────────── */\r\n/* 抄 `ConversationRoot.module.css` 的 `.widthHandle` 一族:10px 的命中区、`col-resize` 光标、\r\n 平时不画、悬停或拖动时才浮出一条 2px 的 `--dsw-alias-scrollbar-bg-l1` 短标(那条渐变里\r\n `--dsh-width-handle-pointer-y` 的兜底是 50%,宿主靠 JS 跟着指针走;我们不跟,\r\n 停在中间——少一条 pointermove 上的写字,视觉上是同一族东西)。\r\n 与宿主的差别只有位置:它绝对定位在会话栏两侧,我们这根住在两栏之间的那一格 flex 位里,\r\n 所以 `top/bottom/position` 那三样交给内联布局,这里只留视觉与光标。 */\r\n\r\n.sd-width-handle {\r\n position: relative;\r\n z-index: 0;\r\n cursor: col-resize;\r\n align-self: stretch;\r\n flex: 0 0 10px;\r\n width: 10px;\r\n padding: 0;\r\n border: none;\r\n background: none;\r\n}\r\n\r\n.sd-width-handle::after {\r\n content: \"\";\r\n background: linear-gradient(\r\n to bottom,\r\n transparent calc(var(--dsh-width-handle-pointer-y, 50%) - 36px),\r\n var(--dsw-alias-scrollbar-bg-l1) calc(var(--dsh-width-handle-pointer-y, 50%) - 8px),\r\n var(--dsw-alias-scrollbar-bg-l1) calc(var(--dsh-width-handle-pointer-y, 50%) + 8px),\r\n transparent calc(var(--dsh-width-handle-pointer-y, 50%) + 36px)\r\n );\r\n opacity: 0;\r\n pointer-events: none;\r\n border-radius: 2px;\r\n width: 2px;\r\n position: absolute;\r\n top: 0;\r\n bottom: 0;\r\n left: 4px;\r\n}\r\n\r\n.sd-width-handle:hover::after,\r\n.sd-width-handle[data-dragging]::after {\r\n opacity: 1;\r\n}\r\n\r\n.sd-width-handle:focus-visible {\r\n outline: none;\r\n}\r\n\r\n.sd-width-handle:focus-visible::after {\r\n opacity: 1;\r\n}\r\n\r\n/* 本插件自造的第一条(不是抄本,见文件头点名):拖动期间整块面板别把文字选上了。 */\r\n.sd-resizing {\r\n user-select: none;\r\n cursor: col-resize;\r\n}\r\n\r\n/* 本插件自造的第二条(不是抄本,见文件头点名):清单行是 `role=\"row\"` 的可聚焦 div,而这一代\r\n `--dsw-focus-ring-color` 逐字是 `transparent`(2026-10-15 实测)——不自己描一条,键盘走\r\n ↑↓ 选会话时就没有任何焦点可见性。描边**复用选中态那枚** `ghost-active-border`,不新造颜色;\r\n 0.5px 与 `outline-offset: 1px` 是行的描边宽度与一档外移,不动布局(outline 不进滚动区)。 */\r\n[data-dsh-session-delete=\"trash-row\"]:focus-visible {\r\n outline: 0.5px solid var(--dsw-alias-button-ghost-active-border);\r\n outline-offset: 1px;\r\n}\r\n\r\n/* 会话行尾悬停排那颗**兜底**勾选开关(状态点行 / 归档行;行首主勾选是原生 Checkbox)。\r\n 基底**逐字抄** `dsh-client-ui-workspace` 的 `Rows.module.css` 的 `.iconButton` / `.iconButton:hover`,\r\n 只改类名——兜底开关要跟同排宿主自己的归档/置顶同形,尺寸与色档都不自己发明。 */\r\n.sd-rowaction-button {\r\n border-radius: var(--dsw-radius-xs);\r\n cursor: pointer;\r\n width: 16px;\r\n height: 16px;\r\n color: var(--dsw-alias-label-tertiary);\r\n background: 0 0;\r\n border: none;\r\n flex: none;\r\n justify-content: center;\r\n align-items: center;\r\n padding: 0;\r\n display: inline-flex;\r\n}\r\n.sd-rowaction-button:hover {\r\n color: var(--dsw-alias-label-primary);\r\n}\r\n\r\n/* 本插件自造的第三条(不是抄本,见文件头点名):官方那颗只有常态与 hover,勾选开关还要有\r\n 「已按下」与「这条删不动」两档。按下档**复用回收站选中行那两枚** `ghost-active-*`,不新造颜色;\r\n 禁用档走 opacity。 */\r\n.sd-rowaction-button[aria-pressed='true'] {\r\n background: var(--dsw-alias-button-ghost-active-fill);\r\n box-shadow: inset 0 0 0 1px var(--dsw-alias-button-ghost-active-border);\r\n color: var(--dsw-alias-label-primary);\r\n}\r\n.sd-rowaction-button:disabled {\r\n opacity: 0.4;\r\n cursor: not-allowed;\r\n}\r\n\r\n/* 本插件自造的第⑥条(不是抄本,见文件头点名):行首 16px 格里的勾选框(2026-10-17 改版,**只在\r\n 多选模式内渲染**)。格本身由宿主居中(16×20),这里只把包装层压成同样大小;勾选框本体是回收站\r\n 多选同款原生 input(`.sd-checkbox`,见上)。 */\r\n.sd-batch-pick-leading {\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n}\r\n\r\n/* 本插件自造的第⑤条(不是抄本,见文件头点名):宿主行自己的选中态只归它自己,批量选择要有整行\r\n 底色;行首格在场时行尾兜底要退场(两条入口不许同屏)。两条都只认宿主行已有的\r\n `data-row-key`;将来这个属性改名,最差是底色不亮 / 兜底多露一颗,勾选本身仍在。 */\r\n[data-row-key^=\"session:\"]:has([data-dsh-session-delete=\"batch-pick\"][data-checked=\"true\"]) {\r\n background: var(--dsw-alias-button-ghost-active-fill);\r\n box-shadow: inset 0 0 0 0.5px var(--dsw-alias-button-ghost-active-border);\r\n}\r\n[data-row-key^=\"session:\"]:has([data-dsh-session-delete=\"batch-pick\"][data-seat=\"leading\"]) .sd-batch-pick-action {\r\n display: none;\r\n}\r\n\r\n/* 本插件自造的第四条(不是抄本,见文件头点名):批量删除的浮条。宿主没有\"浮条\"这件控件,形状照\r\n 本插件自己的浮层口径来——固定底部居中,表面/描边/圆角/高程四件全走 token(与单删那张目标卡同源)。\r\n **层级必须低于确认框(`.sd-modal-root` 1000)**:2026-10-17 真机发现浮条在 1100 时会盖住确认框、\r\n 还能抢它的按钮;撤销横幅与 Tooltip 是瞬时通告、保持 1100。列方向:清单在上、操作行在下;\r\n 清单限高可滚,默认收起(见 `batch-bar.tsx`)。**宽度按内容展开**(`max-content`,最多视口减 48):\r\n 只写 `left:50% + width:auto` 时 shrink-to-fit 只拿左半屏,新增「全选/取消全选」后会把「退出多选」\r\n 挤到第二行;窄到 max-width 还放不下才由 `.sd-batch-bar-main` 的 `flex-wrap` 兜底。 */\r\n.sd-batch-bar {\r\n position: fixed;\r\n bottom: 24px;\r\n left: 50%;\r\n transform: translateX(-50%);\r\n z-index: 900;\r\n width: max-content;\r\n max-width: calc(100vw - 48px);\r\n display: flex;\r\n flex-direction: column;\r\n align-items: stretch;\r\n gap: 8px;\r\n padding: 8px 12px;\r\n background: var(--dsw-alias-bg-layer-2);\r\n border: 0.5px solid var(--dsw-alias-border-l4);\r\n border-radius: var(--dsw-radius-lg);\r\n box-shadow: var(--dsw-elevation-prominent);\r\n font-size: 12px;\r\n line-height: 18px;\r\n}\r\n\r\n.sd-batch-bar-main {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n flex-wrap: wrap;\r\n}\r\n\r\n.sd-batch-bar-hint {\r\n color: var(--dsw-alias-label-tertiary);\r\n}\r\n\r\n.sd-batch-bar-list-head {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n flex-wrap: wrap;\r\n}\r\n\r\n/* 汇总的第二句(含 K 个子会话)比条数轻一档,和列表行里的 chip 同级。 */\r\n.sd-batch-bar-list-head span + span {\r\n color: var(--dsw-alias-label-tertiary);\r\n}\r\n\r\n.sd-batch-bar-list {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 2px;\r\n max-height: 240px;\r\n overflow: auto;\r\n margin: 0;\r\n padding: 0 0 8px;\r\n border-bottom: 0.5px solid var(--dsw-alias-border-l4);\r\n list-style: none;\r\n}\r\n\r\n.sd-batch-bar-item {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n min-width: 0;\r\n}\r\n\r\n.sd-batch-bar-title {\r\n flex: 1;\r\n min-width: 0;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n color: var(--dsw-alias-label-secondary);\r\n}\r\n\r\n.sd-batch-bar-meta {\r\n flex: none;\r\n color: var(--dsw-alias-label-tertiary);\r\n}\r\n\r\n.sd-batch-bar-remove {\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n flex: none;\r\n width: 16px;\r\n height: 16px;\r\n padding: 0;\r\n border: none;\r\n border-radius: var(--dsw-radius-xs);\r\n background: 0 0;\r\n color: var(--dsw-alias-label-tertiary);\r\n cursor: pointer;\r\n}\r\n\r\n.sd-batch-bar-remove:hover {\r\n color: var(--dsw-alias-label-primary);\r\n background: var(--dsw-alias-interactive-bg-hover);\r\n}\r\n";
1681
1654
  //#endregion
1682
1655
  //#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 * **来源(逐字抄,只改选择器与下面点名的三处)**:\n * `app.asar` 里 `@deepseek-ai/dsh-client-ui-primitives/lib/markdown/MarkdownText.module.css`\n * (`.markdown` 那一支;`.compact` 一支我们不用)+ `lib/markdown/CodeBlock.module.css` 的 `pre`。\n * token 名一个没换:`--dsw-font-markdown-*` / `--dsw-alias-markdown-*` / `--dsh-content-font-*`\n * 全是宿主会话区自己吃的那组,所以宿主调内容字号时,这里跟着一起动。\n *\n * 三处**有意**的差异,每条都是因为我们的 DOM 长不出宿主那个节点:\n * ① 宿主给表包一层 `.tableScroll`(div)再横向滚动;react-markdown 不产这层 ⇒ 滚动挂在 `table`\n * 自己身上(`display:block` + 匿名表盒)。这条得真机量过才算成立。\n * ② 宿主的代码卡有 banner 行(语言名 + 复制),四个圆角被拆成\"上圆角在 banner、下圆角在 pre\";\n * 我们没有 banner ⇒ 圆角整块给 `pre`。\n * ③ 宿主的链接可点(点开外链/文件),我们不碰宿主导航,链接渲成不可点的文本 ⇒ 保留链接色与字重,\n * **去掉** hover 的下划线(那是\"点我\"的邀请)。\n *\n * 抄过来但用不上的选择器已删:`.fileLink` / `.fileMention` / `.linkIcon` / `.image` / `.imageAlt`\n * (图片渲成本地化占位,见 `copy.ts` 的 `detail.image` / `detail.image.alt`)/ `.katex-display` /\n * `:global(.md-code-block)` / `.shiki`(高亮由宿主的 highlighter 提供,我们没有)。\n */\n.sd-md {\n min-width: 0;\n overflow-wrap: anywhere;\n font: var(--dsw-font-markdown-base);\n color: var(--dsw-alias-label-primary);\n}\n\n.sd-md strong {\n font-weight: 600;\n}\n\n.sd-md h1 {\n font: var(--dsw-font-markdown-h1);\n margin: 32px 0 16px;\n}\n\n.sd-md h2 {\n font: var(--dsw-font-markdown-h2);\n margin: 32px 0 16px;\n}\n\n.sd-md h3 {\n font: var(--dsw-font-markdown-h3);\n margin: 32px 0 16px;\n}\n\n.sd-md h4 {\n font: var(--dsw-font-markdown-h4);\n margin: 16px 0;\n}\n\n.sd-md :where(h5, h6) {\n font: var(--dsw-font-markdown-base-strong);\n margin: 16px 0;\n}\n\n.sd-md :where(h1, h2, h3, h4, h5, h6) strong {\n font-weight: inherit;\n}\n\n.sd-md p {\n margin: 16px 0;\n}\n\n/* Tighten h4–h6 against a following list (design: 8px gap). */\n.sd-md :where(h4, h5, h6) + :where(ul, ol) {\n margin-top: 8px;\n}\n\n.sd-md :where(h4, h5, h6):has(+ :where(ul, ol)) {\n margin-bottom: 8px;\n}\n\n/* 见文件头差异③:颜色与字重照抄,hover 的 dotted 下划线不抄——这里没有可点的东西。 */\n.sd-md-link {\n color: var(--dsw-alias-link);\n font-weight: 500;\n}\n\n.sd-md :where(ul, ol) {\n margin: 16px 0;\n padding-left: 18px;\n}\n\n.sd-md li:not(:first-child) {\n margin-top: 6px;\n}\n\n.sd-md li > :where(ul, ol) {\n margin-top: 4px;\n}\n\n.sd-md li::marker {\n line-height: 24px;\n color: var(--dsw-alias-label-secondary);\n}\n\n/* Nested ol under ul/ol: markers inside (models sometimes emit this shape). */\n.sd-md :where(ul, ol) ol {\n list-style-position: inside;\n padding-left: 0;\n}\n\n.sd-md :where(ul, ol) ol li p {\n display: inline;\n}\n\n.sd-md li > p {\n margin: 8px 0;\n}\n\n.sd-md li > *:first-child {\n margin-top: 0;\n}\n\n.sd-md li > *:last-child {\n margin-bottom: 0;\n}\n\n.sd-md hr {\n display: block;\n border: none;\n height: 0.5px;\n margin: 32px 0;\n background: var(--dsw-alias-border-l2);\n}\n\n.sd-md blockquote {\n border-left: 2px solid var(--dsw-alias-label-caption);\n margin: 16px 0 0;\n padding-left: 14px;\n}\n\n/* 代码卡:宿主的卡片由 `.block`(底色+圆角)与其内 `pre`(字号+内边距)两层拼成。\n 我们只有 `pre` 这一层,所以两层的声明合成在这里——见文件头差异②。 */\n.sd-md pre {\n font: var(--dsw-font-markdown-code-block);\n margin: 16px 0;\n padding: 16px;\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-markdown-code-block);\n border-radius: var(--dsw-radius-lg);\n overflow-x: auto;\n white-space: pre-wrap;\n word-break: break-all;\n}\n\n.sd-md pre code {\n font: inherit;\n background: none;\n padding: 0;\n}\n\n.sd-md :not(pre) > code {\n display: inline-flex;\n align-items: center;\n box-sizing: border-box;\n font: var(--dsw-font-markdown-code);\n font-family: var(--ds-font-family-code);\n font-size: 0.875em !important;\n background-color: var(--dsw-alias-markdown-inline-code);\n border: 0.5px solid var(--dsw-alias-border-l1);\n border-radius: var(--dsw-radius-sm);\n padding: 0 5px;\n}\n\n.sd-md :where(h1, h2, h3, h4, h5, h6) code {\n font: inherit;\n font-family: var(--ds-font-family-code);\n}\n\n.sd-md input[type='checkbox'] {\n margin: 0 8px 0 0;\n accent-color: var(--dsw-alias-label-secondary);\n}\n\n/* 见文件头差异①:宿主是 `.tableScroll > table`,我们没有那层 div。 */\n.sd-md table {\n display: block;\n box-sizing: border-box;\n max-width: 100%;\n overflow-x: auto;\n overscroll-behavior-x: contain;\n border-collapse: collapse;\n width: max-content;\n}\n\n.sd-md th {\n text-align: start;\n padding: 10px 16px;\n border-bottom: 0.5px solid var(--dsw-alias-border-l3);\n border-top: none;\n font: var(--dsw-font-markdown-table-head);\n max-width: 320px;\n max-width: min(30vw, 320px);\n min-width: 100px;\n}\n\n.sd-md td {\n padding: 10px 16px;\n border-bottom: 0.5px solid var(--dsw-alias-border-l2);\n font: var(--dsw-font-markdown-table);\n max-width: 320px;\n max-width: min(30vw, 320px);\n min-width: 100px;\n}\n\n.sd-md th:first-child,\n.sd-md td:first-child {\n padding-left: 0;\n}\n\n.sd-md td:last-child {\n padding-right: 0;\n}\n\n.sd-md table code {\n font-size: 11px;\n}\n\n.sd-md > *:first-child,\n.sd-md p:first-child {\n margin-top: 0 !important;\n}\n\n.sd-md > *:last-child,\n.sd-md p:last-child {\n margin-bottom: 0 !important;\n}\n\n/* 图片在回收站里读不到(路径属于会话当时的 cwd),渲成一句说明,字色用宿主的 tertiary。 */\n.sd-md-image {\n color: var(--dsw-alias-label-tertiary);\n font-style: italic;\n}\n";
1656
+ var markdown_default = "/**\r\n * 回收站详情里**会话正文**的排版(2026-10-15 真机反馈后新增)。\r\n *\r\n * 为什么单独一个文件而不并进 `ui.css`:`ui.css` 是\"照官方 primitives 逐字抄来的那一份表\",\r\n * 那条 fidelity 核对(`docs/development.md` 的界面保真复核门)要的是它与官方明文**逐属性零差异**,只许文件头点名的几类例外\r\n * (类名前缀、`@keyframes` 改名,外加它点名的自造与已声明的差别);条数不钉在这里,钉了就会漂成假基线。这一份抄的是\r\n * **另一个来源**(宿主 markdown 那一张表),混进去就把那份核对弄脏了。\r\n *\r\n * **来源(逐字抄,只改选择器与下面点名的三处)**:\r\n * `app.asar` 里 `@deepseek-ai/dsh-client-ui-primitives/lib/markdown/MarkdownText.module.css`\r\n * (`.markdown` 那一支;`.compact` 一支我们不用)+ `lib/markdown/CodeBlock.module.css` 的 `pre`。\r\n * token 名一个没换:`--dsw-font-markdown-*` / `--dsw-alias-markdown-*` / `--dsh-content-font-*`\r\n * 全是宿主会话区自己吃的那组,所以宿主调内容字号时,这里跟着一起动。\r\n *\r\n * 三处**有意**的差异,每条都是因为我们的 DOM 长不出宿主那个节点:\r\n * ① 宿主给表包一层 `.tableScroll`(div)再横向滚动;react-markdown 不产这层 ⇒ 滚动挂在 `table`\r\n * 自己身上(`display:block` + 匿名表盒)。这条得真机量过才算成立。\r\n * ② 宿主的代码卡有 banner 行(语言名 + 复制),四个圆角被拆成\"上圆角在 banner、下圆角在 pre\";\r\n * 我们没有 banner ⇒ 圆角整块给 `pre`。\r\n * ③ 宿主的链接可点(点开外链/文件),我们不碰宿主导航,链接渲成不可点的文本 ⇒ 保留链接色与字重,\r\n * **去掉** hover 的下划线(那是\"点我\"的邀请)。\r\n *\r\n * 抄过来但用不上的选择器已删:`.fileLink` / `.fileMention` / `.linkIcon` / `.image` / `.imageAlt`\r\n * (图片渲成本地化占位,见 `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(--dsw-alias-link);\r\n font-weight: 500;\r\n}\r\n\r\n.sd-md :where(ul, ol) {\r\n margin: 16px 0;\r\n padding-left: 18px;\r\n}\r\n\r\n.sd-md li:not(:first-child) {\r\n margin-top: 6px;\r\n}\r\n\r\n.sd-md li > :where(ul, ol) {\r\n margin-top: 4px;\r\n}\r\n\r\n.sd-md li::marker {\r\n line-height: 24px;\r\n color: var(--dsw-alias-label-secondary);\r\n}\r\n\r\n/* Nested ol under ul/ol: markers inside (models sometimes emit this shape). */\r\n.sd-md :where(ul, ol) ol {\r\n list-style-position: inside;\r\n padding-left: 0;\r\n}\r\n\r\n.sd-md :where(ul, ol) ol li p {\r\n display: inline;\r\n}\r\n\r\n.sd-md li > p {\r\n margin: 8px 0;\r\n}\r\n\r\n.sd-md li > *:first-child {\r\n margin-top: 0;\r\n}\r\n\r\n.sd-md li > *:last-child {\r\n margin-bottom: 0;\r\n}\r\n\r\n.sd-md hr {\r\n display: block;\r\n border: none;\r\n height: 0.5px;\r\n margin: 32px 0;\r\n background: var(--dsw-alias-border-l2);\r\n}\r\n\r\n.sd-md blockquote {\r\n border-left: 2px solid var(--dsw-alias-label-caption);\r\n margin: 16px 0 0;\r\n padding-left: 14px;\r\n}\r\n\r\n/* 代码卡:宿主的卡片由 `.block`(底色+圆角)与其内 `pre`(字号+内边距)两层拼成。\r\n 我们只有 `pre` 这一层,所以两层的声明合成在这里——见文件头差异②。 */\r\n.sd-md pre {\r\n font: var(--dsw-font-markdown-code-block);\r\n margin: 16px 0;\r\n padding: 16px;\r\n color: var(--dsw-alias-label-primary);\r\n background: var(--dsw-alias-markdown-code-block);\r\n border-radius: var(--dsw-radius-lg);\r\n overflow-x: auto;\r\n white-space: pre-wrap;\r\n word-break: break-all;\r\n}\r\n\r\n.sd-md pre code {\r\n font: inherit;\r\n background: none;\r\n padding: 0;\r\n}\r\n\r\n.sd-md :not(pre) > code {\r\n display: inline-flex;\r\n align-items: center;\r\n box-sizing: border-box;\r\n font: var(--dsw-font-markdown-code);\r\n font-family: var(--ds-font-family-code);\r\n font-size: 0.875em !important;\r\n background-color: var(--dsw-alias-markdown-inline-code);\r\n border: 0.5px solid var(--dsw-alias-border-l1);\r\n border-radius: var(--dsw-radius-sm);\r\n padding: 0 5px;\r\n}\r\n\r\n.sd-md :where(h1, h2, h3, h4, h5, h6) code {\r\n font: inherit;\r\n font-family: var(--ds-font-family-code);\r\n}\r\n\r\n.sd-md input[type='checkbox'] {\r\n margin: 0 8px 0 0;\r\n accent-color: var(--dsw-alias-label-secondary);\r\n}\r\n\r\n/* 见文件头差异①:宿主是 `.tableScroll > table`,我们没有那层 div。 */\r\n.sd-md table {\r\n display: block;\r\n box-sizing: border-box;\r\n max-width: 100%;\r\n overflow-x: auto;\r\n overscroll-behavior-x: contain;\r\n border-collapse: collapse;\r\n width: max-content;\r\n}\r\n\r\n.sd-md th {\r\n text-align: start;\r\n padding: 10px 16px;\r\n border-bottom: 0.5px solid var(--dsw-alias-border-l3);\r\n border-top: none;\r\n font: var(--dsw-font-markdown-table-head);\r\n max-width: 320px;\r\n max-width: min(30vw, 320px);\r\n min-width: 100px;\r\n}\r\n\r\n.sd-md td {\r\n padding: 10px 16px;\r\n border-bottom: 0.5px solid var(--dsw-alias-border-l2);\r\n font: var(--dsw-font-markdown-table);\r\n max-width: 320px;\r\n max-width: min(30vw, 320px);\r\n min-width: 100px;\r\n}\r\n\r\n.sd-md th:first-child,\r\n.sd-md td:first-child {\r\n padding-left: 0;\r\n}\r\n\r\n.sd-md td:last-child {\r\n padding-right: 0;\r\n}\r\n\r\n.sd-md table code {\r\n font-size: 11px;\r\n}\r\n\r\n.sd-md > *:first-child,\r\n.sd-md p:first-child {\r\n margin-top: 0 !important;\r\n}\r\n\r\n.sd-md > *:last-child,\r\n.sd-md p:last-child {\r\n margin-bottom: 0 !important;\r\n}\r\n\r\n/* 图片在回收站里读不到(路径属于会话当时的 cwd),渲成一句说明,字色用宿主的 tertiary。 */\r\n.sd-md-image {\r\n color: var(--dsw-alias-label-tertiary);\r\n font-style: italic;\r\n}\r\n";
1684
1657
  //#endregion
1685
1658
  //#region src/client/ui/styles.ts
1686
1659
  /**
@@ -1867,21 +1840,6 @@ window.__ModuleLoader__.load({
1867
1840
  //#endregion
1868
1841
  //#region src/client/batch-select.tsx
1869
1842
  /**
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
1843
  * 一组会话的子会话计数(`/apply` 的 `action:'count'`,纯读、不进串行链)。
1886
1844
  *
1887
1845
  * **一条读不到就整份返回 null** ——界面上那两个数(浮条的 K、确认框的 K 与逐条 M)都是合计,
@@ -1902,8 +1860,12 @@ window.__ModuleLoader__.load({
1902
1860
  * (行首格在宿主行那条 `onClick` 里面)。宿主对「有状态点」和归档行不渲染这一格,
1903
1861
  * 那些行走 `BatchPickButton` 兜底;两条不会同时出现(见 `ui.css` 的兜底隐藏规则)。
1904
1862
  */
1905
- function BatchPickCheckbox({ sessionId, title, t = zhTranslate }) {
1863
+ function BatchPickCheckbox({ sessionId, title, live = false, t = zhTranslate }) {
1906
1864
  const { selecting, picks } = useSelection();
1865
+ useHostRowRegistration(sessionId, {
1866
+ title,
1867
+ live
1868
+ });
1907
1869
  const on = picks.some((candidate) => candidate.sessionId === sessionId);
1908
1870
  if (!selecting) return null;
1909
1871
  const name = title === "" ? t("common.untitled") : title;
@@ -1932,19 +1894,10 @@ window.__ModuleLoader__.load({
1932
1894
  function BatchPickButton({ sessionId, title, live = false, t = zhTranslate }) {
1933
1895
  const { selecting, picks } = useSelection();
1934
1896
  const on = picks.some((candidate) => candidate.sessionId === sessionId);
1935
- (0, react.useEffect)(() => {
1936
- registerHostRowInfo(sessionId, {
1937
- title,
1938
- live
1939
- });
1940
- return () => {
1941
- unregisterHostRowInfo(sessionId);
1942
- };
1943
- }, [
1944
- sessionId,
1897
+ useHostRowRegistration(sessionId, {
1945
1898
  title,
1946
1899
  live
1947
- ]);
1900
+ });
1948
1901
  if (!selecting) return null;
1949
1902
  const name = title === "" ? t("common.untitled") : title;
1950
1903
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Tooltip, {
@@ -2291,15 +2244,104 @@ window.__ModuleLoader__.load({
2291
2244
  }));
2292
2245
  }
2293
2246
  //#endregion
2247
+ //#region src/client/pending-open.ts
2248
+ /**
2249
+ * 重开等待的唯一生命周期:一张票、一条订阅、一个放弃 timer、一次 settle/cleanup。
2250
+ *
2251
+ * ADR-0036 的语义不变:**订阅优先、出现即开、最多等一个放弃边界、不轮询**。这里把这段状态机
2252
+ * 收成一个可取消句柄:调用方 `schedule` 一次、`cancel` 一次;票、订阅、timer 与 settle 标志
2253
+ * 都关在 implementation 里,不再由 main-view 在四个调用点成对手工清理。
2254
+ *
2255
+ * 依赖(store 的 subscribe、是否已出现、是否还能抢焦点、怎么打开)由调用方交进来——本模块不认宿主。
2256
+ */
2257
+ /** 默认放弃边界:给宿主 `api-session/added` 到客户端摘要落地留一拍(ADR-0036 的产品决定)。 */
2258
+ const DEFAULT_GIVE_UP_MS = 2e3;
2259
+ function createPendingOpen(giveUpMs = DEFAULT_GIVE_UP_MS) {
2260
+ const latest = createLatestWins();
2261
+ let cleanup;
2262
+ const cancel = () => {
2263
+ latest.invalidate();
2264
+ const stop = cleanup;
2265
+ cleanup = void 0;
2266
+ stop?.();
2267
+ };
2268
+ return {
2269
+ cancel,
2270
+ schedule(input) {
2271
+ cancel();
2272
+ const ticket = latest.issue();
2273
+ let settled = false;
2274
+ let unsubscribe;
2275
+ let timer;
2276
+ const unsubscribeNow = () => {
2277
+ if (unsubscribe === void 0) return;
2278
+ const stop = unsubscribe;
2279
+ unsubscribe = void 0;
2280
+ try {
2281
+ stop();
2282
+ } catch {}
2283
+ };
2284
+ const finish = () => {
2285
+ if (settled) return;
2286
+ settled = true;
2287
+ if (timer !== void 0) {
2288
+ clearTimeout(timer);
2289
+ timer = void 0;
2290
+ }
2291
+ unsubscribeNow();
2292
+ if (cleanup === finish) cleanup = void 0;
2293
+ };
2294
+ const open = () => {
2295
+ if (!ticket.current || !input.canOpen()) {
2296
+ finish();
2297
+ return;
2298
+ }
2299
+ try {
2300
+ input.open();
2301
+ } finally {
2302
+ finish();
2303
+ }
2304
+ };
2305
+ if (input.isReady()) {
2306
+ open();
2307
+ return;
2308
+ }
2309
+ const subscribe = input.subscribe;
2310
+ if (subscribe === void 0) return;
2311
+ cleanup = finish;
2312
+ let stop;
2313
+ try {
2314
+ stop = subscribe(() => {
2315
+ if (input.isReady()) open();
2316
+ });
2317
+ } catch {
2318
+ finish();
2319
+ return;
2320
+ }
2321
+ unsubscribe = stop;
2322
+ if (settled) {
2323
+ unsubscribeNow();
2324
+ return;
2325
+ }
2326
+ timer = setTimeout(finish, giveUpMs);
2327
+ if (input.isReady()) open();
2328
+ }
2329
+ };
2330
+ }
2331
+ //#endregion
2294
2332
  //#region src/client/main-view.ts
2295
2333
  let client;
2296
2334
  /** 我们自己关掉的主视图会话:只有这里记过的,撤销恢复后才会重开;面板恢复用 `forgetMainView` 清掉。 */
2297
2335
  let closedByUs = /* @__PURE__ */ new Set();
2298
- /** 重开排队的最新票:后一次删除/恢复(或重开)签发新票,前一次的等待随之作废(只开最新那条)。 */
2299
- const reopen = createLatestWins();
2336
+ /** 在等的重开:票、订阅、放弃 timer 与 cleanup 都归这个句柄(`src/client/pending-open.ts`)。 */
2337
+ const pendingOpen = createPendingOpen();
2300
2338
  /** 入口激活时绑一次;之后每次调用再读服务(客户端插件激活顺序早于 `uiWorkspace` 也没关系)。 */
2301
2339
  function bindMainView(ctx) {
2302
2340
  client = ctx;
2341
+ if (typeof ctx.effect === "function") ctx.effect(() => () => {
2342
+ pendingOpen.cancel();
2343
+ closedByUs = /* @__PURE__ */ new Set();
2344
+ }, "session-delete: main-view");
2303
2345
  }
2304
2346
  /** 宿主服务是否可用:把检查写成类型谓词,调用点拿到的就是 `ReadyUiWorkspace`。
2305
2347
  * `ctx.get` 的窄镜像声明成可能回 null(宿主可回 null),所以空值判据必须同时认 undefined / null。 */
@@ -2330,7 +2372,7 @@ window.__ModuleLoader__.load({
2330
2372
  */
2331
2373
  function afterDelete(sessionIds) {
2332
2374
  if (sessionIds.length === 0) return;
2333
- reopen.invalidate();
2375
+ pendingOpen.cancel();
2334
2376
  const ui = uiWorkspaceOf();
2335
2377
  if (ui === void 0) return;
2336
2378
  const current = currentSessionOf(ui);
@@ -2349,7 +2391,7 @@ window.__ModuleLoader__.load({
2349
2391
  */
2350
2392
  function afterRestore(sessionIds) {
2351
2393
  if (sessionIds.length === 0) return;
2352
- reopen.invalidate();
2394
+ pendingOpen.cancel();
2353
2395
  const targets = sessionIds.filter((id) => closedByUs.has(id));
2354
2396
  if (targets.length === 0) return;
2355
2397
  for (const id of targets) closedByUs.delete(id);
@@ -2366,19 +2408,23 @@ window.__ModuleLoader__.load({
2366
2408
  }
2367
2409
  /**
2368
2410
  * 把会话重新打开:`api-session/added` 从宿主发出到客户端摘要表落地有一拍(WS 与 HTTP 响应各走
2369
- * 各的),`openSession` 的目标必须先出现在清单里才不会抛「unknown session」——所以先等它出现,
2370
- * 最多 2 秒;期间用户若已经选了别的会话就不再抢焦点。等不到就什么都不做,行还在那里。
2411
+ * 各的),`openSession` 的目标必须先出现在清单里才不会抛「unknown session」——先查一次快照,没有就
2412
+ * **订阅**宿主快照 store;出现且票仍是当前、焦点没被抢走时打开。最多等 2 秒,一个放弃 timer,不轮询。
2413
+ * 期间用户若已经选了别的会话就不再抢焦点。等不到就什么都不做,行还在那里。
2371
2414
  *
2372
- * **最新票守卫**(`reopen`):开始等待时签一张票,立即开与每次 tick 都核对;后一次删除/
2373
- * 恢复(或 `resetMainView`)签发新票或作废当前票,前一次的等待就不再打开、也不再续时。
2415
+ * 这段状态机住 `src/client/pending-open.ts` 的 `createPendingOpen`:这里只把「查什么、能不能开、
2416
+ * 怎么开」交进去;取消也只剩 `pendingOpen.cancel()` 一次。没有 `subscribe` 的旧 store 直接放弃
2417
+ * 等待,不退回轮询(ADR-0036)。
2374
2418
  */
2375
2419
  function openWhenListed(sessionId) {
2376
2420
  const ui = uiWorkspaceOf();
2377
2421
  if (ui === void 0) return;
2378
- const ticket = reopen.issue();
2422
+ const store = ui.sessions?.list;
2423
+ if (store === void 0) return;
2424
+ const subscribe = typeof store.subscribe === "function" ? store.subscribe.bind(store) : void 0;
2379
2425
  const listReady = () => {
2380
2426
  try {
2381
- return ui.sessions?.list?.getSnapshot()?.byId?.[sessionId] !== void 0;
2427
+ return store.getSnapshot()?.byId?.[sessionId] !== void 0;
2382
2428
  } catch {
2383
2429
  return false;
2384
2430
  }
@@ -2387,28 +2433,16 @@ window.__ModuleLoader__.load({
2387
2433
  const current = currentSessionOf(ui);
2388
2434
  return current === void 0 || current === sessionId;
2389
2435
  };
2390
- const open = () => {
2391
- if (!ticket.current) return;
2392
- if (!canTakeFocus()) return;
2393
- try {
2394
- ui.openSession(sessionId);
2395
- } catch {}
2396
- };
2397
- if (listReady()) {
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;
2436
+ pendingOpen.schedule({
2437
+ subscribe,
2438
+ isReady: listReady,
2439
+ canOpen: canTakeFocus,
2440
+ open: () => {
2441
+ try {
2442
+ ui.openSession(sessionId);
2443
+ } catch {}
2408
2444
  }
2409
- if (attempts < 20) setTimeout(tick, 100);
2410
- };
2411
- setTimeout(tick, 100);
2445
+ });
2412
2446
  }
2413
2447
  //#endregion
2414
2448
  //#region src/client/write-settlement.ts
@@ -2896,12 +2930,81 @@ window.__ModuleLoader__.load({
2896
2930
  });
2897
2931
  }
2898
2932
  //#endregion
2933
+ //#region src/client/batch-pick.ts
2934
+ /**
2935
+ * 批量选择的**动作面**:全选、全选判定与模式内整行点击守卫。
2936
+ *
2937
+ * 可见行的事实源仍住 `host-rows.ts`(槽位登记 + 快照);选中集住 `batch-selection-store.ts`。
2938
+ * 本文件只把两者接起来,于是依赖方向单向:`batch-pick` → `host-rows` / `batch-selection-store`;
2939
+ * 浮条与行开关 → `batch-pick` / `host-rows`。`host-rows` 不再 import 任何选择动作。
2940
+ *
2941
+ * DOM 只在这里读一次 `data-row-key`(ADR-0034 记下的受约束偏离):守卫把指针下的行映射成
2942
+ * sessionId,再查登记;它不枚举行,也不维护行列表。
2943
+ */
2944
+ /** 全选:把当前可见可选行按登记序追加;已在名单里的由 `addPicks` 自己跳过,原有选中保持在前。 */
2945
+ function selectAllVisible() {
2946
+ addPicks(readVisibleSelectableRows());
2947
+ }
2948
+ /**
2949
+ * 同一个切换口该显示「全选」还是「取消全选」:
2950
+ * - 有选中、且当前可见可选行全都选中 ⇒ 取消全选;
2951
+ * - 有选中、但当前没有可见可选行(全折叠/过滤掉)⇒ 只能清空,也显示取消全选;
2952
+ * - 其余(含 0 条)⇒ 全选。
2953
+ */
2954
+ function areAllVisibleRowsSelected(rows, picks) {
2955
+ if (picks.length === 0) return false;
2956
+ if (rows.length === 0) return true;
2957
+ const chosen = new Set(picks.map((item) => item.sessionId));
2958
+ return rows.every((row) => chosen.has(row.sessionId));
2959
+ }
2960
+ /** 行内这些元素有自己的动作:模式内也不许被"点行=勾选"吃掉。 */
2961
+ const INTERACTIVE_IN_ROW = "button, a, input, select, textarea, [role=\"menuitem\"], [role=\"menu\"], [data-dsh-session-delete=\"batch-pick\"]";
2962
+ /**
2963
+ * 多选态内的整行点击守卫(document 捕获阶段)。
2964
+ *
2965
+ * 边界:行里的按钮 / 链接 / 输入 / 菜单项一律放行(否则 ⋯ 菜单、归档、置顶、单删都点不动);
2966
+ * 双击的第二下与 dblclick 不翻转(第一下已经选中,第二下不能又反掉,也不能打开宿主的重命名)。
2967
+ * 认行靠 `data-row-key="session:…"`——**这是本文件唯一一处 DOM 读**(ADR-0034):宿主将来改掉这个
2968
+ * 属性,最差是模式内点行不生效,勾选框本身仍在。模式外不挂监听:这时点行必须还是宿主的"打开会话"。
2969
+ */
2970
+ function useSelectionRowClickGuard() {
2971
+ const selecting = useSelection().selecting;
2972
+ (0, react.useEffect)(() => {
2973
+ if (!selecting) return;
2974
+ const handle = (event) => {
2975
+ if (!(event.target instanceof Element)) return;
2976
+ const row = event.target.closest("[data-row-key^=\"session:\"]");
2977
+ if (row === null) return;
2978
+ if (event.target.closest(INTERACTIVE_IN_ROW) !== null) return;
2979
+ event.preventDefault();
2980
+ event.stopPropagation();
2981
+ if (event.type === "dblclick" || event.type === "click" && event.detail > 1) return;
2982
+ const key = row.getAttribute("data-row-key") ?? "";
2983
+ if (!key.startsWith("session:")) return;
2984
+ const sessionId = key.slice(8);
2985
+ if (sessionId === "") return;
2986
+ const info = pickableRowOf(sessionId);
2987
+ if (info === void 0) return;
2988
+ togglePick({
2989
+ sessionId,
2990
+ title: info.title
2991
+ });
2992
+ };
2993
+ document.addEventListener("click", handle, true);
2994
+ document.addEventListener("dblclick", handle, true);
2995
+ return () => {
2996
+ document.removeEventListener("click", handle, true);
2997
+ document.removeEventListener("dblclick", handle, true);
2998
+ };
2999
+ }, [selecting]);
3000
+ }
3001
+ //#endregion
2899
3002
  //#region src/client/batch-bar.tsx
2900
3003
  /**
2901
3004
  * 批量删除的浮条:**多选模式内常驻**(0 条也渲染,既显示模式又给退出入口),
2902
3005
  * 汇总「已选 N 条」+(读得到时)「含 K 个子会话」;「查看已选」展开限高可滚动的标题清单
2903
3006
  * (逐条可移除),回答"到底选了哪些"——清单默认收起,不把底部输入区盖住。
2904
- * 模式内的整行点击守卫也挂在这棵常驻宿主上(见 `host-rows.ts` 的 `useSelectionRowClickGuard`)。
3007
+ * 模式内的整行点击守卫也挂在这棵常驻宿主上(见 `batch-pick.ts` 的 `useSelectionRowClickGuard`)。
2905
3008
  *
2906
3009
  * 为什么单独一个文件而不是并进 `batch-select.tsx`:那份是"行开关控件",而浮条要点开确认框
2907
3010
  * (要 import 它的现场)——两个方向合到一个文件上就是一圈。浮条住在
@@ -3165,8 +3268,10 @@ window.__ModuleLoader__.load({
3165
3268
  * 回收站详情与设置页维护卡预览里的一条附件:图片给缩略图 + 放大框,普通文件给下载 chip。
3166
3269
  *
3167
3270
  * 三条口径:
3168
- * - **地址只由 `attachmentId` + `kind` 拼**:`/attachment` 段只认这两个参数寻址(`name` 只做下载名建议,
3169
- * 不参与路径推导,见 `src/api/attachment.ts`)。空串与缺席同义——空串不是一个名字,不写进查询串。
3271
+ * - **地址只由 `attachmentId` + `kind` 寻址**:`/attachment` 段以这两个参数找对象;媒体元数据
3272
+ * (`mediaType` / `bytes` / `width` / `height`)只给宿主 `attachments` 服务做引用校验,`name` 只做
3273
+ * 下载名建议,均不参与路径推导(见 `src/api/attachment.ts`)。空串与缺席同义——空串不是一个名字,
3274
+ * 不写进查询串。
3170
3275
  * - **图裂了就换占位**:缩略图与放大框共用同一份 `failed` 集合,`onError` 之后这一格改成「附件不可用」,
3171
3276
  * 不把破图留在屏上(放大框里裂了也换)。普通文件走 `<a download>`,没有页面内失败面:服务端失败回
3172
3277
  * 200 + JSON 信封,浏览器可能把这份错误响应存成文件。
@@ -3183,10 +3288,16 @@ window.__ModuleLoader__.load({
3183
3288
  function attachmentUrl(a) {
3184
3289
  const query = new URLSearchParams({
3185
3290
  [PARAMS.attachmentId]: a.attachmentId,
3186
- [PARAMS.attachmentKind]: a.kind
3291
+ [PARAMS.attachmentKind]: a.kind,
3292
+ [PARAMS.attachmentBytes]: String(a.bytes)
3187
3293
  });
3294
+ if (a.kind === "image") {
3295
+ if (a.mediaType !== void 0) query.set(PARAMS.attachmentMediaType, a.mediaType);
3296
+ if (a.width !== void 0) query.set(PARAMS.attachmentWidth, String(a.width));
3297
+ if (a.height !== void 0) query.set(PARAMS.attachmentHeight, String(a.height));
3298
+ }
3188
3299
  if (a.kind === "file" && a.name !== void 0 && a.name !== "") query.set(PARAMS.attachmentName, a.name);
3189
- return `${SESSION_DELETE_API_PREFIX}${ROUTES.attachment.path}?${query.toString()}`;
3300
+ return `${OFFICIAL_SESSION_DELETE_API_PREFIX}${ROUTES.attachment.path}?${query.toString()}`;
3190
3301
  }
3191
3302
  /** 空串与缺席同义:宿主侧的 `name` 是可选建议名,空串不是名字(兜底文案与"不带 name 查询"都看它)。 */
3192
3303
  function nameOf(a) {
@@ -4130,7 +4241,7 @@ window.__ModuleLoader__.load({
4130
4241
  ] });
4131
4242
  }
4132
4243
  /** 预览 + 确认:先摆出这些图片(点开可放大),再决定清不清;只清 `.dsh` 内无引用的图片副本。
4133
- * `items` 只是预览清单(服务端最多给 200 条),`count` 才是这次要清理的总数。 */
4244
+ * `items` 只是预览清单(服务端按配置给条数,默认最多 200 条),`count` 才是这次要清理的总数。 */
4134
4245
  function AttachmentConfirm({ count, items, busy, t, onCancel, onConfirm }) {
4135
4246
  const { close, closeDisabled } = useIdleClose(busy, onCancel);
4136
4247
  const attachments = items.map((item) => ({
@@ -26248,11 +26359,24 @@ window.__ModuleLoader__.load({
26248
26359
  height: "100%",
26249
26360
  minWidth: 0
26250
26361
  };
26362
+ /** 返回键排在标题之后:排在之前会把整条标题的起点往右挤(docs/ui-spec.md 状态⑥)。
26363
+ * 标题排按紧凑按钮(`ui.css` 的 `.sd-button-sm`)预留高度:宽档父详情没有返回键、孩子视图有,
26364
+ * 不预留时下方元信息会在父子切换时跳 6px。 */
26251
26365
  const HEAD = {
26252
26366
  display: "flex",
26253
26367
  alignItems: "flex-start",
26254
- gap: 10
26368
+ gap: 10,
26369
+ minHeight: 28
26255
26370
  };
26371
+ const TITLE_LINE = {
26372
+ ...ELLIPSIS,
26373
+ flex: "1 1 auto",
26374
+ minWidth: 0,
26375
+ fontSize: 15,
26376
+ fontWeight: 600,
26377
+ lineHeight: "22px"
26378
+ };
26379
+ const BACK = { flex: "none" };
26256
26380
  /** 元信息那张表:label 一列、值一列,窄档也不横向溢出。 */
26257
26381
  const META = {
26258
26382
  display: "grid",
@@ -26715,32 +26839,27 @@ window.__ModuleLoader__.load({
26715
26839
  style: SHELL,
26716
26840
  children: [
26717
26841
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
26842
+ "data-dsh-session-delete": "trash-detail-head",
26718
26843
  style: HEAD,
26719
- children: [detailNav.parentContext !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
26844
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
26845
+ style: TITLE_LINE,
26846
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
26847
+ title,
26848
+ children: title
26849
+ })
26850
+ }), detailNav.parentContext !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
26720
26851
  variant: "ghost",
26721
26852
  size: "sm",
26722
- style: { flex: "none" },
26853
+ style: BACK,
26723
26854
  onClick: detailNav.parentContext.onBack,
26724
26855
  title: detailNav.parentContext.title === "" ? t("common.untitled") : detailNav.parentContext.title,
26725
26856
  children: t("detail.backToParent")
26726
26857
  }) : wide ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
26727
26858
  variant: "ghost",
26728
26859
  size: "sm",
26729
- style: { flex: "none" },
26860
+ style: BACK,
26730
26861
  onClick: onBack,
26731
26862
  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
26863
  })]
26745
26864
  }),
26746
26865
  state.kind === "loading" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LoadingBody, { t }) : null,
@@ -28891,10 +29010,13 @@ window.__ModuleLoader__.load({
28891
29010
  }
28892
29011
  /** 多选模式行首勾选框的宿主:槽位只投 `sessionId`,标题从 root hook 的 `byId[id].title` 读。 */
28893
29012
  function PickLeadingRow({ sessionId, useSessions, t }) {
28894
- const title = (typeof useSessions === "function" ? useSessions : NEVER_SESSIONS)((state) => state?.byId?.[sessionId]?.title?.trim() ?? "");
29013
+ const select = typeof useSessions === "function" ? useSessions : NEVER_SESSIONS;
29014
+ const title = select((state) => state?.byId?.[sessionId]?.title?.trim() ?? "");
29015
+ const live = select((state) => state?.byId?.[sessionId]?.running === true);
28895
29016
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BatchPickCheckbox, {
28896
29017
  sessionId,
28897
29018
  title,
29019
+ live,
28898
29020
  t
28899
29021
  });
28900
29022
  }
@@ -28943,10 +29065,12 @@ window.__ModuleLoader__.load({
28943
29065
  /**
28944
29066
  * **探测 → 注册**:取数通道在不在,只有真发一次请求才知道。
28945
29067
  *
28946
- * - 探测不过(打不通 / 回的不是信封)⇒ **早返回**:一个槽位贡献都不注册。页面上的表现是"本插件
28947
- * 不存在",而不是点下去必然失败的入口——半吊子 UI 比没有 UI 更糟(docs/architecture.md §3「通道探测决定入口出现与否」)。
28948
- * - 探测过(哪怕 `ping` 回的是 `{ok:false,error}`)⇒ 注册:那次往返证明路由在、依赖接上了、
28949
- * 错误映射也生效了,失败该做的是把原文交给用户看。
29068
+ * - 探测不过(打不通 / 回的不是信封 / 401·403 表示没有宿主身份 / 失败信封)⇒ **早返回**:
29069
+ * 一个槽位贡献都不注册。页面上的表现是"本插件不存在",而不是点下去必然失败的入口——半吊子 UI
29070
+ * 比没有 UI 更糟(docs/architecture.md §3「通道探测决定入口出现与否」)。
29071
+ * - 探测过(`ping` 回成功信封的 `pong`)⇒ 注册:那次往返证明路由在、依赖接上了、鉴权也过了。
29072
+ *
29073
+ * 支持面是 web UI 与桌面端,二者都从宿主 `/api` 取数(ADR-0033);没有旧前缀可退回。
28950
29074
  *
28951
29075
  * 面板那两条是**一对座位**:行的 `selectPanel(id)` 要求 `main` 里有同字 key,缺了面板那一头,
28952
29076
  * 用户点一下就是宿主自己的 `layout.selectPanel: main panel "…" is not registered`。