@bittersmilezzz/dsh-model-selector 0.1.20 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -119,6 +119,37 @@ window.__ModuleLoader__.load({
119
119
  }
120
120
  return best?.id;
121
121
  }
122
+ /**
123
+ * 已知档位的秩;适配器自造的非规范 id(如 'turbo')返回 undefined —— 与
124
+ * 「已知但秩为 0」(off)区分开。maxEffortOf 对并列的未知档取首个,把它当
125
+ * 「最强档」提交等于随机挑一个档、还可能被宿主拒,故调用方必须先过这一关。
126
+ */
127
+ function dmsEffortRank(id) {
128
+ return Object.prototype.hasOwnProperty.call(EFFORT_RANK, id) ? EFFORT_RANK[id] : void 0;
129
+ }
130
+ /**
131
+ * 选择模型时自动落到最强档的决策(从 choose() 搬出并修正):
132
+ * - 非推理模型 / 无档位 → 不提交 effort;
133
+ * - 最强档是 'off'(模型只提供 off)→ 不提交;
134
+ * - 最强档是**非规范 id**(rank 未知)→ 不提交:maxEffortOf 对并列未知档取
135
+ * 首个,把它当「最强」提交会挑错档甚至被宿主拒(用户看到「切换失败」);
136
+ * - 否则提交已知最强档;只有它不等于模型声明的默认档时才播报落点。
137
+ */
138
+ function dmsChoosePlan(reasoning) {
139
+ if (reasoning === void 0) return {
140
+ effort: void 0,
141
+ autoRaised: false
142
+ };
143
+ const best = maxEffortOf(reasoning);
144
+ if (best === void 0 || best === "off" || dmsEffortRank(best) === void 0) return {
145
+ effort: void 0,
146
+ autoRaised: false
147
+ };
148
+ return {
149
+ effort: best,
150
+ autoRaised: best !== reasoning.defaultEffort
151
+ };
152
+ }
122
153
  function dmsEffortIndex(levels, id) {
123
154
  return levels.findIndex((level) => level.id === id);
124
155
  }
@@ -168,12 +199,15 @@ window.__ModuleLoader__.load({
168
199
  * 只有当自回滚以来没有任何新的提交改写 committedRef(仍等于回滚前的档位)、
169
200
  * 提交纪元未推进(epoch === epochAtCommit,即期间没有发起过新提交——提交失败
170
201
  * 回滚后 committedRef 会回到与上一轮回滚相同的值,值比较无法区分「无新提交」
171
- * 与「新提交失败回滚」,纪元才能区分)、且当前无拖动/提交在途时,迟到结果才
172
- * 仍对应当前唯一的意图链——此时 UI 处于「回滚但后端已生效」的错位态,应补
173
- * 一次同步而不是保持回滚。否则以新操作链为准。
202
+ * 与「新提交失败回滚」,纪元才能区分)、当前无拖动/提交在途,**且生效模型仍是
203
+ * 发起提交时的那个**(sameModel)时,迟到结果才仍对应当前唯一的意图链——此时
204
+ * UI 处于「回滚但后端已生效」的错位态,应补一次同步而不是保持回滚。
205
+ * 否则以新操作链为准:模型被外部改写(另一入口切换 / 宿主推送新投影 / 重连后
206
+ * 同步出别的选择)时,next/index 是在旧模型的档位表上算出来的,写回 UI 会显示
207
+ * 一个后端并未生效的档位。
174
208
  */
175
- function dmsShouldAdoptLateSuccess(committed, previous, dragging, committing, epoch, epochAtCommit) {
176
- return committed === previous && epoch === epochAtCommit && !dragging && !committing;
209
+ function dmsShouldAdoptLateSuccess(committed, previous, dragging, committing, epoch, epochAtCommit, sameModel) {
210
+ return sameModel && committed === previous && epoch === epochAtCommit && !dragging && !committing;
177
211
  }
178
212
  /**
179
213
  * 拖动终止事件是否对应当前活动拖动:pointerActive 为假(拖动已结束/从未开始)
@@ -525,6 +559,11 @@ window.__ModuleLoader__.load({
525
559
  * 拖出屏外)时,返回钳到视口内的 left 值,调用方改用 left 锚定。
526
560
  * 返回 undefined 表示保持默认右锚定。
527
561
  * menuWidth 传菜单实际渲染宽度(offsetWidth),与 CSS `min(280px, 100vw-32px)` 解耦。
562
+ *
563
+ * **坐标系**:入参与返回值都是**视口坐标**。菜单是 .dms-root(position:
564
+ * relative)内的 absolute 元素,left 的参考系是 root 的 padding box —— 调用方
565
+ * 必须减去 root 的左缘再写进 style.left,否则窄窗口下菜单整体右移、横向溢出
566
+ * 时整幅画到屏外。
528
567
  */
529
568
  function dmsMenuLeft(rootRight, menuWidth, viewportWidth, margin) {
530
569
  if (rootRight - menuWidth >= margin && rootRight <= viewportWidth - margin) return void 0;
@@ -533,6 +572,26 @@ window.__ModuleLoader__.load({
533
572
  //#endregion
534
573
  //#region src/client/roving.ts
535
574
  /**
575
+ * dsh-model-selector — 菜单 roving tabindex 的纯决策逻辑(node 可测)。
576
+ *
577
+ * 模型列表可达数百行,若每行都是 Tab 停靠点,Tab 键会陷入行海;roving
578
+ * tabindex(WAI-ARIA menu 模式的通行做法)只让「活动行」留在 Tab 序里,
579
+ * 其余行 tabindex=-1、仅方向键可达。本模块只负责一件事:给定当前渲染的
580
+ * 行集合与选中行,算出 Tab 从搜索框进入列表时应停靠的默认行键。
581
+ */
582
+ /**
583
+ * 模型行键的**唯一构造点**:菜单 DOM 的 `data-row-key`、roving 决策与行焦点
584
+ * 回调必须同源。此前这三处各自拼模板串,一次局部改名就能让 dmsDefaultRowKey
585
+ * 返回的键匹配不到任何行(Tab 落点静默失效),且测试拿自制副本验证不到。
586
+ */
587
+ function dmsRowKey(providerId, modelId) {
588
+ return `${providerId}\u0000${modelId}`;
589
+ }
590
+ /** 组头行键(分组视图里组头也参与方向键导航)。 */
591
+ function dmsHeaderKey(providerId) {
592
+ return `header:${providerId}`;
593
+ }
594
+ /**
536
595
  * Tab 默认落点:搜索态取「选中行若在命中集内,否则首个命中」;分组态取
537
596
  * 「选中行若可见,否则首个可见模型行」;全部折叠(无可见模型行)时回退到
538
597
  * 首个组头。返回 null 表示当前没有任何可聚焦行(空态由菜单外的 role=status
@@ -547,6 +606,144 @@ window.__ModuleLoader__.load({
547
606
  return set.headerKeys[0] ?? null;
548
607
  }
549
608
  //#endregion
609
+ //#region src/client/search.ts
610
+ /**
611
+ * 搜索归一化:NFKD 分解 → 去组合标记 → 小写。
612
+ *
613
+ * 比裸 `toLowerCase()` 宽容:`'İstanbul'` 能被 `'istanbul'` 命中(裸 toLowerCase
614
+ * 把 U+0130 折成 `'i̇'` 两个码位,与 ASCII `'i'` 反而不匹配 → 漏检;`'café'` 同理)。
615
+ * 注意它**可能改变码位长度**,所以不能用它的下标去切原串 —— 高亮区间另走
616
+ * {@link dmsNameHit},只在长度不变时给区间。
617
+ */
618
+ function dmsFold(text) {
619
+ return text.normalize("NFKD").replace(/\p{M}+/gu, "").toLowerCase();
620
+ }
621
+ /** 搜索词归一化(去首尾空白后折叠)。空串 = 非搜索态。 */
622
+ function dmsNormalizeQuery(raw) {
623
+ return dmsFold(raw.trim());
624
+ }
625
+ /** 模型条目的搜索文本(haystack)。与 choices 构建处同源,避免两处漂移。 */
626
+ function dmsHaystack(group, model) {
627
+ return dmsFold(`${model.name}\n${model.description ?? ""}\n${group.name}\n${model.id}\n${group.id}`);
628
+ }
629
+ /**
630
+ * 名称内的命中区间:折叠后长度不变时用下标定位;长度变化(含会改变码位数的
631
+ * 大小写/变音符映射)时返回 null —— 宁可不高亮,也不能用错位的区间去切原串
632
+ * (那会把高亮画到别的字符上,甚至越界)。
633
+ */
634
+ function dmsNameHit(name, normalizedQuery) {
635
+ if (normalizedQuery === "") return null;
636
+ const folded = dmsFold(name);
637
+ if (folded.length !== name.length) return null;
638
+ const at = folded.indexOf(normalizedQuery);
639
+ return at < 0 ? null : {
640
+ start: at,
641
+ end: at + normalizedQuery.length
642
+ };
643
+ }
644
+ /**
645
+ * 搜索:遍历 choices 计数全部命中,只构造窗口内(前 limit 条)的渲染条目 ——
646
+ * 宽泛关键词(单字母)命中数百条时,每次击键省掉数百次对象分配与名称匹配;
647
+ * total 单独累计,播报与「仅显示前 N 条」提示仍然准确。
648
+ */
649
+ function dmsSearchHits(choices, normalizedQuery, limit) {
650
+ if (normalizedQuery === "") return null;
651
+ const items = [];
652
+ let total = 0;
653
+ for (const choice of choices) {
654
+ if (!choice.haystack.includes(normalizedQuery)) continue;
655
+ total += 1;
656
+ if (items.length >= limit) continue;
657
+ items.push({
658
+ group: choice.group,
659
+ model: choice.model,
660
+ nameHit: dmsNameHit(choice.model.name, normalizedQuery)
661
+ });
662
+ }
663
+ return {
664
+ items,
665
+ total
666
+ };
667
+ }
668
+ /**
669
+ * 空态判定:**无模型优先于无命中** —— 目录为空而用户又输了关键词时,真相是
670
+ * 「没有可用的模型」(该引导用户去配置/加载目录),不是「没有匹配关键词的模型」
671
+ * (那会引导用户去改关键词,而无论改什么都不会有结果)。
672
+ */
673
+ function dmsMenuEmptyState(input) {
674
+ if (input.status === "ready" && input.choiceCount === 0) return "noModels";
675
+ if (input.searching && input.hitCount === 0) return "noHits";
676
+ return null;
677
+ }
678
+ //#endregion
679
+ //#region src/client/keys.ts
680
+ /**
681
+ * 按键 → 动作。无行可去时对方向键/Home/End 返回 none 而不是吞键:列表为空
682
+ * (加载中/无命中/全折叠)时搜索框里的上下键仍应有移光标语义,preventDefault
683
+ * 由调用方在真正执行动作时才发。
684
+ */
685
+ function dmsMenuKeyAction(input) {
686
+ if (input.composing) return { type: "none" };
687
+ const { key, open, fromSearch, inMenu, onNonInput, hasQuery, rowCount, hitCount } = input;
688
+ if (key === "Escape" && open) {
689
+ if (hasQuery && inMenu) return {
690
+ type: "clearQuery",
691
+ focusSearch: !fromSearch
692
+ };
693
+ return { type: "close" };
694
+ }
695
+ if (!open) return { type: "none" };
696
+ if ((key === "ArrowDown" || key === "ArrowUp") && (onNonInput || fromSearch)) {
697
+ if (rowCount === 0) return { type: "none" };
698
+ return {
699
+ type: "moveFocus",
700
+ offset: key === "ArrowDown" ? 1 : -1
701
+ };
702
+ }
703
+ if ((key === "Home" || key === "End") && onNonInput) {
704
+ if (rowCount === 0) return { type: "none" };
705
+ return {
706
+ type: "focusEdge",
707
+ last: key === "End"
708
+ };
709
+ }
710
+ if (key === "Enter" && fromSearch && hitCount > 0) return { type: "chooseFirstHit" };
711
+ return { type: "none" };
712
+ }
713
+ /**
714
+ * roving 环绕下标:焦点不在任何行上(activeIndex < 0)时向下从首行进、向上从
715
+ * 末行进;已在某行上时按 offset 循环。length <= 0 返回 -1(无可去之处)。
716
+ */
717
+ function dmsNextRowIndex(activeIndex, offset, length) {
718
+ if (length <= 0) return -1;
719
+ return ((activeIndex < 0 ? offset > 0 ? -1 : 0 : activeIndex) + offset + length) % length;
720
+ }
721
+ //#endregion
722
+ //#region src/client/copy.ts
723
+ /**
724
+ * trigger 文案。目录成员资格只是参考(routable 契约):current 匹配不到任何
725
+ * group 不代表没有选择 —— 此时显示 provider/model 原始 id,而不是「选择模型」。
726
+ */
727
+ function dmsTriggerCopy(input, t) {
728
+ const { current, modelName, providerName, effortLabel, waiting } = input;
729
+ const modelLabel = modelName ?? (waiting ? t("trigger.loading") : current === null ? t("trigger.fallback") : `${current.provider}/${current.model}`);
730
+ const providerLabel = modelName === void 0 ? void 0 : providerName;
731
+ const label = effortLabel === void 0 ? modelLabel : `${modelLabel} · ${effortLabel}`;
732
+ const title = providerLabel === void 0 ? label : `${providerLabel} · ${label}`;
733
+ const named = providerLabel === void 0 ? modelLabel : `${providerLabel} ${modelLabel}`;
734
+ const ariaModel = modelName === void 0 ? current === null ? null : `${current.provider}/${current.model}` : named;
735
+ return {
736
+ modelLabel,
737
+ providerLabel,
738
+ label,
739
+ title,
740
+ aria: waiting ? t("trigger.loading") : ariaModel === null ? t("trigger.selectAria") : effortLabel === void 0 ? t("trigger.aria", { model: ariaModel }) : t("trigger.ariaEffort", {
741
+ model: ariaModel,
742
+ effort: effortLabel
743
+ })
744
+ };
745
+ }
746
+ //#endregion
550
747
  //#region src/client/ModelSelect.tsx
551
748
  /**
552
749
  * ModelSelect: the enhanced composer model seat (`conversation.input.model`).
@@ -582,12 +779,14 @@ window.__ModuleLoader__.load({
582
779
  * 必须释放滑杆的 committing 锁并回滚,否则滑杆被锁死到菜单关闭。
583
780
  * 调法:后端/网络慢时调大避免误回滚(滑杆锁死时间随之变长);调小更早释放交互。 */
584
781
  const EFFORT_COMMIT_TIMEOUT_MS = 12e3;
585
- const EffortSlider = react.memo(function EffortSlider({ state, select, t }) {
782
+ const EffortSlider = react.memo(function EffortSlider({ state, select, t, onSelectFailure }) {
586
783
  const levels = dmsSliderLevels(state);
587
784
  const [effort, setEffort] = react.useState(() => levels[dmsEffectiveEffortIndex(levels, state)]?.id ?? "");
588
785
  const [preview, setPreview] = react.useState(() => dmsEffectiveEffortIndex(levels, state));
589
786
  const [committing, setCommitting] = react.useState(false);
590
787
  const [localError, setLocalError] = react.useState(null);
788
+ const stateRef = react.useRef(state);
789
+ stateRef.current = state;
591
790
  const canvasRef = react.useRef(null);
592
791
  const inputRef = react.useRef(null);
593
792
  const committedRef = react.useRef("");
@@ -616,7 +815,7 @@ window.__ModuleLoader__.load({
616
815
  setLocalError(null);
617
816
  }, [
618
817
  available,
619
- levels.length,
818
+ levels,
620
819
  state.current?.provider,
621
820
  state.current?.model,
622
821
  state.current?.reasoningEffort
@@ -658,7 +857,13 @@ window.__ModuleLoader__.load({
658
857
  if (dmsEffortBusy(committingRef.current, state.status)) return;
659
858
  const index = dmsClampIndex(raw, levels.length);
660
859
  const next = levels[index]?.id;
661
- if (next === void 0 || next === committedRef.current) return;
860
+ if (next === void 0) return;
861
+ if (next === committedRef.current) {
862
+ previewRef.current = index;
863
+ setPreview(index);
864
+ setEffort(next);
865
+ return;
866
+ }
662
867
  committingRef.current = true;
663
868
  const previous = committedRef.current;
664
869
  commitEpochRef.current += 1;
@@ -671,6 +876,7 @@ window.__ModuleLoader__.load({
671
876
  try {
672
877
  const current = state.current;
673
878
  if (current === null) throw new Error(t("empty.efforts"));
879
+ const modelKeyAtCommit = dmsRowKey(current.provider, current.model);
674
880
  if (!await new Promise((resolve, reject) => {
675
881
  let settled = false;
676
882
  const timer = window.setTimeout(() => {
@@ -686,7 +892,9 @@ window.__ModuleLoader__.load({
686
892
  reasoningEffort: next
687
893
  }).then((accepted) => {
688
894
  if (settled) {
689
- if (accepted && mountedRef.current && dmsShouldAdoptLateSuccess(committedRef.current, previous, draggingRef.current, committingRef.current, commitEpochRef.current, epochAtCommit)) {
895
+ const latest = stateRef.current.current;
896
+ const sameModel = latest !== null && dmsRowKey(latest.provider, latest.model) === modelKeyAtCommit;
897
+ if (accepted && mountedRef.current && dmsShouldAdoptLateSuccess(committedRef.current, previous, draggingRef.current, committingRef.current, commitEpochRef.current, epochAtCommit, sameModel)) {
690
898
  committedRef.current = next;
691
899
  previewRef.current = index;
692
900
  setEffort(next);
@@ -714,6 +922,7 @@ window.__ModuleLoader__.load({
714
922
  setPreview(index);
715
923
  }
716
924
  } catch (cause) {
925
+ onSelectFailure();
717
926
  const restore = Math.max(0, dmsEffortIndex(levels, previous));
718
927
  committedRef.current = previous;
719
928
  previewRef.current = restore;
@@ -730,7 +939,8 @@ window.__ModuleLoader__.load({
730
939
  levels,
731
940
  select,
732
941
  state,
733
- t
942
+ t,
943
+ onSelectFailure
734
944
  ]);
735
945
  const drag = useEffortDrag({
736
946
  levelCount: () => levels.length,
@@ -818,14 +1028,14 @@ window.__ModuleLoader__.load({
818
1028
  className: "dms-effort-desc",
819
1029
  children: effortDesc
820
1030
  }),
821
- error === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1031
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
822
1032
  className: "dms-sr",
823
1033
  role: "status",
824
- children: error
1034
+ children: error ?? ""
825
1035
  }),
826
- error === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1036
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
827
1037
  className: "dms-effort-error",
828
- children: error
1038
+ children: error ?? ""
829
1039
  })
830
1040
  ]
831
1041
  });
@@ -839,7 +1049,7 @@ window.__ModuleLoader__.load({
839
1049
  * 但 100 条命中里绝大多数命中区间不变——按值比较 start/end,值相同即跳过
840
1050
  * 重渲染,搜索才不卡顿。
841
1051
  */
842
- const ModelOption = react.memo(function ModelOption({ group, model, showProvider, selected, busy, rowKey, active, onRowFocus, nameHit, t, onChoose }) {
1052
+ const ModelOption = react.memo(function ModelOption({ group, model, showProvider, selected, busy, rowKey, active, searchMode, onRowFocus, nameHit, t, onChoose }) {
843
1053
  const hit = nameHit === void 0 || nameHit === null || nameHit.start === nameHit.end ? null : [
844
1054
  model.name.slice(0, nameHit.start),
845
1055
  model.name.slice(nameHit.start, nameHit.end),
@@ -847,8 +1057,9 @@ window.__ModuleLoader__.load({
847
1057
  ];
848
1058
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
849
1059
  type: "button",
850
- role: "menuitemradio",
851
- "aria-checked": selected,
1060
+ role: searchMode ? "option" : "menuitemradio",
1061
+ "aria-checked": searchMode ? void 0 : selected,
1062
+ "aria-selected": searchMode ? selected : void 0,
852
1063
  "data-row-key": rowKey,
853
1064
  tabIndex: active ? 0 : -1,
854
1065
  className: `dms-model-option${selected ? " dms-model-optionSelected" : ""}`,
@@ -903,6 +1114,7 @@ window.__ModuleLoader__.load({
903
1114
  if (prev.busy !== next.busy) return false;
904
1115
  if (prev.rowKey !== next.rowKey) return false;
905
1116
  if (prev.active !== next.active) return false;
1117
+ if (prev.searchMode !== next.searchMode) return false;
906
1118
  if (prev.onRowFocus !== next.onRowFocus) return false;
907
1119
  if (prev.t !== next.t) return false;
908
1120
  if (prev.onChoose !== next.onChoose) return false;
@@ -932,6 +1144,12 @@ window.__ModuleLoader__.load({
932
1144
  failed
933
1145
  });
934
1146
  }, []);
1147
+ const markSelectFailure = react.useCallback(() => {
1148
+ lastActionRef.current = "select";
1149
+ }, []);
1150
+ const dismissToast = react.useCallback(() => {
1151
+ setToast(null);
1152
+ }, []);
935
1153
  const lastActionRef = react.useRef("load");
936
1154
  const rootRef = react.useRef(null);
937
1155
  const triggerRef = react.useRef(null);
@@ -941,7 +1159,7 @@ window.__ModuleLoader__.load({
941
1159
  const choices = react.useMemo(() => state.groups.flatMap((group) => group.models.map((model) => ({
942
1160
  group,
943
1161
  model,
944
- haystack: `${model.name}\n${model.description ?? ""}\n${group.name}\n${model.id}\n${group.id}`.toLowerCase(),
1162
+ haystack: dmsHaystack(group, model),
945
1163
  selection: {
946
1164
  provider: group.id,
947
1165
  model: model.id,
@@ -957,41 +1175,30 @@ window.__ModuleLoader__.load({
957
1175
  const effectiveEffort = state.current?.reasoningEffort ?? reasoning?.defaultEffort;
958
1176
  const effortLabel = reasoning === void 0 ? void 0 : effectiveEffort === void 0 ? t("effort.providerDefault") : reasoning.efforts.find((level) => level.id === effectiveEffort)?.name ?? effectiveEffort;
959
1177
  const busy = state.status === "selecting";
960
- const normalized = query.trim().toLowerCase();
961
- const hits = react.useMemo(() => {
962
- if (normalized === "") return null;
963
- const items = [];
964
- let total = 0;
965
- for (const choice of choices) {
966
- if (!choice.haystack.includes(normalized)) continue;
967
- total += 1;
968
- if (items.length >= MAX_VISIBLE_HITS) continue;
969
- let nameHit = null;
970
- const at = choice.model.name.toLowerCase().indexOf(normalized);
971
- if (at >= 0) nameHit = {
972
- start: at,
973
- end: at + normalized.length
974
- };
975
- items.push({
976
- group: choice.group,
977
- model: choice.model,
978
- nameHit
979
- });
980
- }
981
- return {
982
- items,
983
- total
984
- };
985
- }, [choices, normalized]);
1178
+ const chooseInputRef = react.useRef({
1179
+ busy,
1180
+ current: state.current,
1181
+ choices,
1182
+ open
1183
+ });
1184
+ chooseInputRef.current = {
1185
+ busy,
1186
+ current: state.current,
1187
+ choices,
1188
+ open
1189
+ };
1190
+ const normalized = dmsNormalizeQuery(query);
1191
+ const hits = react.useMemo(() => dmsSearchHits(choices, normalized, MAX_VISIBLE_HITS), [choices, normalized]);
1192
+ const visibleModelKeys = react.useMemo(() => state.groups.flatMap((g) => collapsed.has(g.id) ? [] : g.models.map((m) => dmsRowKey(g.id, m.id))), [state.groups, collapsed]);
986
1193
  const defaultRowKey = react.useMemo(() => dmsDefaultRowKey({
987
- hitKeys: hits === null ? null : hits.items.map((h) => `${h.group.id}\u0000${h.model.id}`),
988
- modelKeys: state.groups.flatMap((g) => collapsed.has(g.id) ? [] : g.models.map((m) => `${g.id}\u0000${m.id}`)),
989
- headerKeys: state.groups.map((g) => `header:${g.id}`),
990
- selectedKey: state.current === null ? null : `${state.current.provider}\u0000${state.current.model}`
1194
+ hitKeys: hits === null ? null : hits.items.map((h) => dmsRowKey(h.group.id, h.model.id)),
1195
+ modelKeys: visibleModelKeys,
1196
+ headerKeys: state.groups.map((g) => dmsHeaderKey(g.id)),
1197
+ selectedKey: state.current === null ? null : dmsRowKey(state.current.provider, state.current.model)
991
1198
  }), [
992
1199
  hits,
1200
+ visibleModelKeys,
993
1201
  state.groups,
994
- collapsed,
995
1202
  state.current
996
1203
  ]);
997
1204
  react.useEffect(() => {
@@ -1028,7 +1235,9 @@ window.__ModuleLoader__.load({
1028
1235
  setMenuAbove(dmsMenuAbove(rect.top, rect.bottom, window.innerHeight));
1029
1236
  setBelowMaxHeight(dmsBelowMaxHeight(rect.bottom, window.innerHeight, 420));
1030
1237
  const menuWidth = menuRef.current?.offsetWidth ?? 0;
1031
- setMenuLeft(dmsMenuLeft(rect.right, menuWidth, window.innerWidth, 12));
1238
+ const left = dmsMenuLeft(rect.right, menuWidth, window.innerWidth, 12);
1239
+ const rootLeft = rootRef.current?.getBoundingClientRect().left ?? 0;
1240
+ setMenuLeft(left === void 0 ? void 0 : left - rootLeft);
1032
1241
  };
1033
1242
  measure();
1034
1243
  let raf = 0;
@@ -1111,42 +1320,47 @@ window.__ModuleLoader__.load({
1111
1320
  state.groups.length,
1112
1321
  reload
1113
1322
  ]);
1323
+ const focusTrigger = () => {
1324
+ const trigger = triggerRef.current;
1325
+ if (trigger !== null && !trigger.disabled) {
1326
+ trigger.focus();
1327
+ return;
1328
+ }
1329
+ rootRef.current?.focus();
1330
+ };
1114
1331
  const close = react.useCallback((restoreFocus = false) => {
1115
1332
  setOpen(false);
1116
1333
  setNotice(null);
1117
1334
  setQuery("");
1118
- if (restoreFocus) queueMicrotask(() => {
1119
- triggerRef.current?.focus();
1120
- });
1335
+ if (restoreFocus) queueMicrotask(focusTrigger);
1121
1336
  }, []);
1122
1337
  const dismissOnOutsidePointer = react.useCallback(() => {
1123
1338
  close(true);
1124
1339
  }, [close]);
1125
1340
  (0, _deepseek_ai_dsh_client_ui_primitives.useDismissOnOutsidePointer)(rootRef, open, dismissOnOutsidePointer);
1126
1341
  react.useEffect(() => {
1127
- if (locked && open) close();
1342
+ if (locked && open) close(true);
1128
1343
  }, [
1129
1344
  locked,
1130
1345
  open,
1131
1346
  close
1132
1347
  ]);
1133
1348
  const choose = react.useCallback((selection) => {
1134
- if (busy) return;
1135
- if (state.current?.provider === selection.provider && state.current.model === selection.model) {
1349
+ const { busy: isBusy, current, choices: catalog } = chooseInputRef.current;
1350
+ if (isBusy) return;
1351
+ if (current?.provider === selection.provider && current.model === selection.model) {
1136
1352
  setNotice(t("notice.already"));
1137
1353
  return;
1138
1354
  }
1139
- const target = choices.find((c) => c.selection.provider === selection.provider && c.selection.model === selection.model);
1140
- const max = target?.model.reasoning === void 0 ? void 0 : maxEffortOf(target.model.reasoning);
1141
- const effort = max === "off" ? void 0 : max;
1355
+ const target = catalog.find((c) => c.selection.provider === selection.provider && c.selection.model === selection.model);
1356
+ const plan = dmsChoosePlan(target?.model.reasoning);
1142
1357
  const full = {
1143
1358
  provider: selection.provider,
1144
1359
  model: selection.model,
1145
- ...effort === void 0 ? {} : { reasoningEffort: effort }
1360
+ ...plan.effort === void 0 ? {} : { reasoningEffort: plan.effort }
1146
1361
  };
1147
1362
  lastActionRef.current = "select";
1148
- const autoRaised = effort !== void 0 && effort !== target?.model.reasoning?.defaultEffort;
1149
- const autoName = target?.model.reasoning?.efforts.find((level) => level.id === effort)?.name ?? effort ?? "";
1363
+ const autoName = plan.effort === void 0 ? "" : target?.model.reasoning?.efforts.find((level) => level.id === plan.effort)?.name ?? plan.effort;
1150
1364
  (async () => {
1151
1365
  try {
1152
1366
  if (!await new Promise((resolve, reject) => {
@@ -1172,18 +1386,13 @@ window.__ModuleLoader__.load({
1172
1386
  showToast(message !== null ? t("error.action", { message }) : t("notice.selectFailed"));
1173
1387
  return;
1174
1388
  }
1175
- if (rootRef.current !== null) {
1176
- close(true);
1177
- if (autoRaised) showToast(t("toast.effortAuto", { effort: autoName }), false);
1178
- }
1389
+ if (chooseInputRef.current.open) close(true);
1390
+ if (plan.autoRaised) showToast(t("toast.effortAuto", { effort: autoName }), false);
1179
1391
  } catch {
1180
1392
  showToast(t("notice.selectFailed"));
1181
1393
  }
1182
1394
  })();
1183
1395
  }, [
1184
- busy,
1185
- state.current,
1186
- choices,
1187
1396
  select,
1188
1397
  t,
1189
1398
  directory,
@@ -1204,8 +1413,7 @@ window.__ModuleLoader__.load({
1204
1413
  if (menu === null) return;
1205
1414
  const items = [...menu.querySelectorAll("[data-row-key]")];
1206
1415
  if (items.length === 0) return;
1207
- const active = items.findIndex((item) => item === document.activeElement);
1208
- items[((active < 0 ? offset > 0 ? -1 : 0 : active) + offset + items.length) % items.length]?.focus();
1416
+ items[dmsNextRowIndex(items.findIndex((item) => item === document.activeElement), offset, items.length)]?.focus();
1209
1417
  };
1210
1418
  const focusEdge = (last) => {
1211
1419
  const items = menuRef.current?.querySelectorAll("[data-row-key]");
@@ -1213,65 +1421,70 @@ window.__ModuleLoader__.load({
1213
1421
  (last ? items[items.length - 1] : items[0])?.focus();
1214
1422
  };
1215
1423
  const onRootKeyDown = (event) => {
1216
- if (event.nativeEvent.isComposing) return;
1217
1424
  const target = event.target;
1218
- if (event.key === "Escape" && open) {
1219
- if (query !== "" && rootRef.current?.contains(document.activeElement) === true) {
1220
- const fromSearch = target instanceof HTMLInputElement && target === searchRef.current;
1425
+ const action = dmsMenuKeyAction({
1426
+ key: event.key,
1427
+ composing: event.nativeEvent.isComposing || event.nativeEvent.keyCode === 229,
1428
+ open,
1429
+ fromSearch: target instanceof HTMLInputElement && target === searchRef.current,
1430
+ inMenu: menuRef.current?.contains(document.activeElement) === true,
1431
+ onNonInput: !(target instanceof HTMLInputElement),
1432
+ hasQuery: query !== "",
1433
+ rowCount: menuRef.current?.querySelectorAll("[data-row-key]").length ?? 0,
1434
+ hitCount: hits?.items.length ?? 0
1435
+ });
1436
+ switch (action.type) {
1437
+ case "none": return;
1438
+ case "clearQuery":
1439
+ event.preventDefault();
1221
1440
  setNotice(null);
1222
1441
  setQuery("");
1223
- if (!fromSearch) searchRef.current?.focus();
1442
+ if (action.focusSearch) searchRef.current?.focus();
1224
1443
  return;
1225
- }
1226
- event.preventDefault();
1227
- close(true);
1228
- return;
1229
- }
1230
- if (!open) return;
1231
- const fromSearch = target instanceof HTMLInputElement && target === searchRef.current;
1232
- if ((event.key === "ArrowDown" || event.key === "ArrowUp") && (!(target instanceof HTMLInputElement) || fromSearch)) {
1233
- event.preventDefault();
1234
- moveFocus(event.key === "ArrowDown" ? 1 : -1);
1235
- return;
1236
- }
1237
- if ((event.key === "Home" || event.key === "End") && !(target instanceof HTMLInputElement)) {
1238
- event.preventDefault();
1239
- focusEdge(event.key === "End");
1240
- return;
1241
- }
1242
- if (event.key === "Enter" && fromSearch) {
1243
- if (hits !== null && hits.items.length > 0) {
1444
+ case "close":
1445
+ event.preventDefault();
1446
+ close(true);
1447
+ return;
1448
+ case "moveFocus":
1449
+ event.preventDefault();
1450
+ moveFocus(action.offset);
1451
+ return;
1452
+ case "focusEdge":
1453
+ event.preventDefault();
1454
+ focusEdge(action.last);
1455
+ return;
1456
+ case "chooseFirstHit": {
1457
+ const first = hits?.items[0];
1458
+ if (first === void 0) return;
1244
1459
  event.preventDefault();
1245
- const first = hits.items[0];
1246
1460
  choose({
1247
1461
  provider: first.group.id,
1248
1462
  model: first.model.id
1249
1463
  });
1464
+ return;
1250
1465
  }
1251
1466
  }
1252
1467
  };
1253
1468
  const onBlur = (event) => {
1254
- if (busy) return;
1255
1469
  const related = event.relatedTarget;
1470
+ if (related instanceof Node && rootRef.current?.contains(related) === true) return;
1256
1471
  if (related instanceof Node) {
1257
- if (rootRef.current?.contains(related)) return;
1258
1472
  close();
1259
1473
  return;
1260
1474
  }
1261
- close();
1475
+ if (!document.hasFocus()) close();
1262
1476
  };
1263
- const waiting = state.current === null && state.status === "loading";
1264
- const modelLabel = currentChoice?.model.name ?? (waiting ? t("trigger.loading") : state.current === null ? t("trigger.fallback") : `${state.current.provider}/${state.current.model}`);
1265
- const providerLabel = currentChoice?.group.name;
1266
- const triggerLabel = effortLabel === void 0 ? modelLabel : `${modelLabel} · ${effortLabel}`;
1267
- const triggerTitle = providerLabel === void 0 ? triggerLabel : `${providerLabel} · ${triggerLabel}`;
1268
- const triggerAria = waiting ? t("trigger.loading") : currentChoice === void 0 ? state.current === null ? t("trigger.selectAria") : t("trigger.aria", { model: `${state.current.provider}/${state.current.model}` }) : effortLabel === void 0 ? t("trigger.aria", { model: providerLabel === void 0 ? modelLabel : `${providerLabel} ${modelLabel}` }) : t("trigger.ariaEffort", {
1269
- model: providerLabel === void 0 ? modelLabel : `${providerLabel} ${modelLabel}`,
1270
- effort: effortLabel
1271
- });
1477
+ const trigger = dmsTriggerCopy({
1478
+ current: state.current,
1479
+ modelName: currentChoice?.model.name,
1480
+ providerName: currentChoice?.group.name,
1481
+ effortLabel,
1482
+ waiting: state.current === null && state.status === "loading"
1483
+ }, t);
1272
1484
  const renderErrorStrip = () => {
1273
1485
  if (state.error !== null && lastActionRef.current === "load") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1274
1486
  className: "dms-error",
1487
+ role: "alert",
1275
1488
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("error.action", { message: state.error }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1276
1489
  type: "button",
1277
1490
  className: "dms-retry",
@@ -1293,11 +1506,16 @@ window.__ModuleLoader__.load({
1293
1506
  children: t("action.reload")
1294
1507
  })]
1295
1508
  }, failure.id));
1296
- const noHits = hits !== null && hits.items.length === 0;
1297
- const noModels = hits === null && state.status === "ready" && choices.length === 0;
1509
+ const emptyState = dmsMenuEmptyState({
1510
+ status: state.status,
1511
+ choiceCount: choices.length,
1512
+ searching: hits !== null,
1513
+ hitCount: hits?.total ?? 0
1514
+ });
1298
1515
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1299
1516
  ref: rootRef,
1300
1517
  className: "dms-root",
1518
+ tabIndex: -1,
1301
1519
  onKeyDown: onRootKeyDown,
1302
1520
  onBlur,
1303
1521
  children: [
@@ -1305,21 +1523,21 @@ window.__ModuleLoader__.load({
1305
1523
  ref: triggerRef,
1306
1524
  type: "button",
1307
1525
  className: "dms-trigger",
1308
- "aria-label": triggerAria,
1526
+ "aria-label": trigger.aria,
1309
1527
  "aria-haspopup": "menu",
1310
1528
  "aria-expanded": open,
1311
1529
  "aria-controls": open ? `${id}-menu` : void 0,
1312
- title: triggerTitle,
1530
+ title: trigger.title,
1313
1531
  disabled: locked,
1314
1532
  onClick: () => open ? close() : show(),
1315
1533
  children: [
1316
1534
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1317
1535
  className: "dms-triggerLabel",
1318
- children: modelLabel
1536
+ children: trigger.modelLabel
1319
1537
  }),
1320
- providerLabel !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1538
+ trigger.providerLabel !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1321
1539
  className: "dms-triggerProvider",
1322
- children: providerLabel
1540
+ children: trigger.providerLabel
1323
1541
  }),
1324
1542
  effortLabel !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1325
1543
  className: "dms-triggerEffort",
@@ -1343,15 +1561,16 @@ window.__ModuleLoader__.load({
1343
1561
  }
1344
1562
  },
1345
1563
  className: "dms-menu dms-menuModel" + (menuAbove ? "" : " dms-menuBelow"),
1346
- "aria-busy": state.status === "loading" || busy,
1347
1564
  children: [
1348
1565
  state.status === "loading" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1349
1566
  className: "dms-status",
1567
+ role: "status",
1350
1568
  children: t("status.loading")
1351
1569
  }),
1352
1570
  renderErrorStrip(),
1353
1571
  state.failures.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1354
1572
  className: "dms-failures",
1573
+ role: "alert",
1355
1574
  children: renderFailures(state.failures)
1356
1575
  }),
1357
1576
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -1363,7 +1582,10 @@ window.__ModuleLoader__.load({
1363
1582
  value: query,
1364
1583
  placeholder: t("search.placeholder"),
1365
1584
  "aria-label": t("search.placeholder"),
1366
- "aria-controls": `${id}-groups`,
1585
+ role: "combobox",
1586
+ "aria-expanded": hits !== null,
1587
+ "aria-autocomplete": "list",
1588
+ "aria-controls": emptyState === null ? `${id}-groups` : void 0,
1367
1589
  autoComplete: "off",
1368
1590
  enterKeyHint: "search",
1369
1591
  spellCheck: false,
@@ -1387,28 +1609,34 @@ window.__ModuleLoader__.load({
1387
1609
  })
1388
1610
  })]
1389
1611
  }),
1390
- hits !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1612
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1391
1613
  className: "dms-sr",
1392
1614
  role: "status",
1393
- children: t("search.status", { count: String(hits.total) })
1615
+ children: hits !== null && hits.total > 0 ? t("search.status", { count: String(hits.total) }) : ""
1616
+ }),
1617
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1618
+ className: "dms-sr",
1619
+ role: "note",
1620
+ children: t("badge.reasoningHint")
1394
1621
  }),
1395
- noHits && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1622
+ emptyState === "noHits" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1396
1623
  className: "dms-empty dms-groupsFill",
1397
1624
  role: "status",
1398
1625
  children: t("search.noMatch", { query: query.trim() })
1399
1626
  }),
1400
- noModels && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1627
+ emptyState === "noModels" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1401
1628
  className: "dms-empty dms-groupsFill",
1402
1629
  role: "status",
1403
1630
  children: t("empty.models")
1404
1631
  }),
1405
- !noHits && !noModels && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1632
+ emptyState === null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1406
1633
  className: "dms-groups",
1407
1634
  id: `${id}-groups`,
1408
- role: "menu",
1635
+ role: hits !== null ? "listbox" : "menu",
1636
+ "aria-busy": state.status === "loading" || busy,
1409
1637
  "aria-label": t("menu.aria"),
1410
1638
  children: hits !== null ? hits.items.map((hit) => {
1411
- const rowKey = `${hit.group.id}\u0000${hit.model.id}`;
1639
+ const rowKey = dmsRowKey(hit.group.id, hit.model.id);
1412
1640
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelOption, {
1413
1641
  group: hit.group,
1414
1642
  model: hit.model,
@@ -1417,6 +1645,7 @@ window.__ModuleLoader__.load({
1417
1645
  busy,
1418
1646
  rowKey,
1419
1647
  active: rowKey === activeRowKey,
1648
+ searchMode: true,
1420
1649
  onRowFocus,
1421
1650
  nameHit: hit.nameHit,
1422
1651
  t,
@@ -1424,6 +1653,7 @@ window.__ModuleLoader__.load({
1424
1653
  }, rowKey);
1425
1654
  }) : state.groups.map((group) => {
1426
1655
  const headingId = `${id}-${group.id}`;
1656
+ const headerKey = dmsHeaderKey(group.id);
1427
1657
  const isCollapsed = collapsed.has(group.id);
1428
1658
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1429
1659
  "data-group-id": group.id,
@@ -1433,14 +1663,15 @@ window.__ModuleLoader__.load({
1433
1663
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1434
1664
  type: "button",
1435
1665
  role: "menuitem",
1436
- "data-row-key": `header:${group.id}`,
1437
- tabIndex: -1,
1666
+ "data-row-key": headerKey,
1667
+ tabIndex: activeRowKey === headerKey ? 0 : -1,
1438
1668
  className: "dms-groupHeader",
1439
1669
  "aria-expanded": !isCollapsed,
1440
1670
  "aria-label": t("group.toggleAria", {
1441
1671
  name: group.name,
1442
1672
  count: String(group.models.length)
1443
1673
  }),
1674
+ onFocus: () => onRowFocus(headerKey),
1444
1675
  onClick: () => toggleCollapse(group.id),
1445
1676
  children: [
1446
1677
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -1459,7 +1690,7 @@ window.__ModuleLoader__.load({
1459
1690
  })
1460
1691
  ]
1461
1692
  }), !isCollapsed && group.models.map((model) => {
1462
- const rowKey = `${group.id}\u0000${model.id}`;
1693
+ const rowKey = dmsRowKey(group.id, model.id);
1463
1694
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelOption, {
1464
1695
  group,
1465
1696
  model,
@@ -1468,6 +1699,7 @@ window.__ModuleLoader__.load({
1468
1699
  busy,
1469
1700
  rowKey,
1470
1701
  active: rowKey === activeRowKey,
1702
+ searchMode: false,
1471
1703
  onRowFocus,
1472
1704
  t,
1473
1705
  onChoose: choose
@@ -1492,13 +1724,14 @@ window.__ModuleLoader__.load({
1492
1724
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EffortSlider, {
1493
1725
  state,
1494
1726
  select,
1495
- t
1727
+ t,
1728
+ onSelectFailure: markSelectFailure
1496
1729
  })]
1497
1730
  }),
1498
- notice !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1731
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1499
1732
  className: "dms-notice",
1500
1733
  role: "status",
1501
- children: notice
1734
+ children: notice ?? ""
1502
1735
  })
1503
1736
  ]
1504
1737
  }) : null,
@@ -1506,9 +1739,7 @@ window.__ModuleLoader__.load({
1506
1739
  text: toast.text,
1507
1740
  icon: toast.failed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconWarningOutline16, {}) : void 0,
1508
1741
  anchor: rootRef.current?.closest("[data-composer-card]") ?? null,
1509
- onDone: () => {
1510
- setToast(null);
1511
- }
1742
+ onDone: dismissToast
1512
1743
  }, toast.seq)
1513
1744
  ]
1514
1745
  });
@@ -1522,12 +1753,18 @@ window.__ModuleLoader__.load({
1522
1753
  * apply and removed again on unload. Class names are prefixed `dms-` so they
1523
1754
  * cannot collide with CSS-module hashes from other plugins. Colors come only
1524
1755
  * from `--dsw-*` theme tokens, matching the shipped Menu material.
1756
+ *
1757
+ * 显式 `: string` 注解:不加注解时 tsc 会把整份 CSS 推断成字面量类型并写进
1758
+ * lib/types/client/styles.d.ts(19KB 的 d.ts 只为声明一个常量),发布包里
1759
+ * 白白多出十几 KB 且每次改样式都全量重写。
1525
1760
  */
1526
1761
  const CSS = `
1527
1762
  .dms-root {
1528
1763
  position: relative;
1529
1764
  min-width: 0;
1530
1765
  }
1766
+ /* root 只作为焦点落点(tabIndex=-1):不画焦点环,真正的可聚焦控件在它内部。 */
1767
+ .dms-root:focus { outline: none; }
1531
1768
 
1532
1769
  .dms-trigger {
1533
1770
  display: flex;
@@ -1789,6 +2026,8 @@ window.__ModuleLoader__.load({
1789
2026
  font-size: 12px;
1790
2027
  line-height: 18px;
1791
2028
  }
2029
+ /* 常驻 live region(内容随状态更新):空时收起,不占位。 */
2030
+ .dms-notice:empty { display: none; }
1792
2031
  .dms-effort {
1793
2032
  display: flex;
1794
2033
  align-items: center;
@@ -1959,6 +2198,8 @@ window.__ModuleLoader__.load({
1959
2198
  }
1960
2199
  .dms-effort.is-busy { opacity: .72; }
1961
2200
  .dms-effort-error { flex: 1 0 100%; margin-top: 8px; padding: 6px 10px; border-radius: 8px; color: var(--dsw-alias-state-error-primary, #c83e4d); background: var(--dsw-alias-state-error-tertiary, rgba(220,55,70,.08)); font-size: 11px; line-height: 1.5; }
2201
+ /* 常驻容器(内容随状态更新):空时收起,不占位。 */
2202
+ .dms-effort-error:empty { display: none; }
1962
2203
  /* 视觉隐藏工具类(SR 播报/错误原文:不占布局但可被读屏读到)。 */
1963
2204
  .dms-sr {
1964
2205
  position: absolute;
@@ -1999,14 +2240,21 @@ window.__ModuleLoader__.load({
1999
2240
  }
2000
2241
  .dms-model-option-desc { display: block; margin-top: 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsw-alias-label-tertiary, #9296a0); font-size: 10px; }
2001
2242
  .dms-model-check { color: var(--dsw-static-deepseek-500, #4d70ff); font-size: 15px; text-align: center; }
2002
- /* 方向键导航把焦点落在选项上,此前没有任何焦点指示。 */
2003
- .dms-model-option:focus-visible { background: var(--dsw-alias-interactive-bg-hover); }
2243
+ /* 方向键导航把焦点落在选项上。选中态是「背景 + 加粗 + 勾号」,悬停是「背景」,
2244
+ 两者与焦点指示必须两两可辨 —— 只用同一个 hover 背景 token 会让「焦点落在
2245
+ 选中行上」完全不可见(roving 的默认落点恰恰优先选中行)。 */
2246
+ .dms-model-option:focus-visible {
2247
+ background: var(--dsw-alias-interactive-bg-hover);
2248
+ box-shadow: inset 0 0 0 2px var(--dsw-alias-border-l3);
2249
+ }
2004
2250
  /* 当前选中行:仅靠勾号不足以在密集列表里定位。 */
2005
2251
  .dms-model-optionSelected { background: var(--dsw-alias-interactive-bg-hover); }
2006
2252
  .dms-model-optionSelected .dms-model-option-name { font-weight: 600; }
2007
2253
  /* select 进行中:官方以 disabled 变灰表达,这里用 aria-busy 驱动同样的视觉、
2008
- 不夺键盘焦点(aria-disabled 保连续性,见 ModelOption)。 */
2009
- .dms-menu[aria-busy="true"] .dms-model-option { color: var(--dsw-alias-label-dimmed); }
2254
+ 不夺键盘焦点(aria-disabled 保连续性,见 ModelOption)。aria-busy 挂在
2255
+ role=menu/listbox 容器上(.dms-groups)—— 挂在外层 .dms-menu 会罩住
2256
+ live region、压制其播报。 */
2257
+ .dms-groups[aria-busy="true"] .dms-model-option { color: var(--dsw-alias-label-dimmed); }
2010
2258
  /* 搜索结果里的供应商标是区分跨供应商同名模型的唯一线索,之前无任何样式。 */
2011
2259
  .dms-model-option-provider {
2012
2260
  display: block;
@@ -2118,6 +2366,14 @@ body:not([data-ds-dark-theme]) .dms-effort.is-dragging .dms-effort-knob {
2118
2366
  //#region src/client/index.ts
2119
2367
  /** Dictionary namespace owned by this plugin. */
2120
2368
  const NS = "modelSelector";
2369
+ /**
2370
+ * 座位优先级。ui-conversation 把 `conversation.input.model` 声明为 kind 'single'
2371
+ * (单槽按 priority 升序取最低者渲染),官方占用者
2372
+ * (@deepseek-ai/dsh-client-ui-model-selection)注册时不传 priority(即默认 0),
2373
+ * 故 -1 恒胜出。**改这个值等于静默换回官方 UI** —— test/plugin-contract.test.mjs
2374
+ * 直接断言本常量(而不是扫源码文本,那样一行注释就能骗过它),注册处也不许再写裸字面量。
2375
+ */
2376
+ const SEAT_PRIORITY = -1;
2121
2377
  /** Required services: the registry, session lookup, locale, the slot seat, and the model directory's Remote faces. */
2122
2378
  const inject = [
2123
2379
  "slots",
@@ -2175,10 +2431,11 @@ body:not([data-ds-dark-theme]) .dms-effort.is-dragging .dms-effort-knob {
2175
2431
  });
2176
2432
  }
2177
2433
  //#endregion
2434
+ exports.SEAT_PRIORITY = SEAT_PRIORITY;
2178
2435
  exports.apply = apply;
2179
2436
  exports.inject = inject;
2180
2437
  return module.exports;
2181
- }
2438
+ /* __DMS_SRC_HASH__ = "eead5888f44285e2028692c6971ab7d6d8cfeb16272176f9c2c392186c440c04" */ }
2182
2439
  });
2183
2440
 
2184
2441
  //# sourceMappingURL=client.js.map