dsh-model-groups 0.0.0-stage → 0.3.2

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 ADDED
@@ -0,0 +1,1336 @@
1
+ // Generated by scripts/build.mjs; edit src instead.
2
+ window.__ModuleLoader__.load({
3
+ id: 'dsh-model-groups',
4
+ factory(require) {
5
+ const react = require('react');
6
+ const react_dom = require('react-dom');
7
+ const react_jsx_runtime = require('react/jsx-runtime');
8
+ const _deepseek_ai_dsh_client_ui_primitives = require('@deepseek-ai/dsh-client-ui-primitives');
9
+ // Only display preferences live here; no models, credentials, or session data.
10
+ const STORAGE_KEY = 'dsh:model-groups:v1';
11
+ const MODE_STORAGE_KEY = 'dsh:model-groups:mode';
12
+
13
+ function getMenuMode(getStorage = () => globalThis.localStorage) {
14
+ try {
15
+ const val = getStorage()?.getItem(MODE_STORAGE_KEY);
16
+ return val === 'inline' ? 'inline' : 'panel';
17
+ } catch {
18
+ return 'panel';
19
+ }
20
+ }
21
+
22
+ function setMenuMode(mode, getStorage = () => globalThis.localStorage) {
23
+ try {
24
+ getStorage()?.setItem(MODE_STORAGE_KEY, mode === 'inline' ? 'inline' : 'panel');
25
+ } catch { /* private mode */ }
26
+ }
27
+
28
+ function parsePreferences(text) {
29
+ try {
30
+ const value = JSON.parse(text);
31
+ if (value?.version !== 1 || !value.expanded || Array.isArray(value.expanded)) return {};
32
+ return Object.fromEntries(Object.entries(value.expanded).filter(([, expanded]) => typeof expanded === 'boolean'));
33
+ } catch {
34
+ return {};
35
+ }
36
+ }
37
+
38
+ function isGroupExpanded(preferences, providerId, currentProvider) {
39
+ return Object.hasOwn(preferences, providerId) ? preferences[providerId] : providerId === currentProvider;
40
+ }
41
+
42
+ /** Search reveals matching models without changing the saved group preference. */
43
+ function visibleModelGroups(groups, preferences, currentProvider, searching) {
44
+ return groups.map(group => ({
45
+ ...group,
46
+ expanded: searching || isGroupExpanded(preferences, group.id, currentProvider),
47
+ }));
48
+ }
49
+
50
+ /** A storage failure degrades to in-memory preferences, never breaks the picker. */
51
+ function createGroupPreferences(getStorage = () => globalThis.localStorage) {
52
+ let preferences = {};
53
+ const listeners = new Set();
54
+ const publish = next => {
55
+ preferences = next;
56
+ for (const listener of listeners) listener();
57
+ };
58
+ try { preferences = parsePreferences(getStorage()?.getItem(STORAGE_KEY)); } catch { /* private mode */ }
59
+ return {
60
+ getSnapshot: () => preferences,
61
+ subscribe(listener) { listeners.add(listener); return () => listeners.delete(listener); },
62
+ setExpanded(providerIds, expanded) {
63
+ const next = { ...preferences, ...Object.fromEntries(providerIds.map(id => [id, expanded])) };
64
+ publish(next);
65
+ try { getStorage()?.setItem(STORAGE_KEY, JSON.stringify({ version: 1, expanded: next })); } catch { /* quota / private mode */ }
66
+ },
67
+ sync(text) { publish(parsePreferences(text)); },
68
+ };
69
+ }
70
+
71
+
72
+ // This namespace is a selection identity, never an upstream model id or secret.
73
+ const KEY_MODEL_PREFIX = '@dsh-key/';
74
+ const KEY_ENTRY_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,79}$/;
75
+ const forbiddenEntry = id => ['__proto__', 'prototype', 'constructor'].includes(id);
76
+
77
+ function decodeKeyModelId(model) {
78
+ if (typeof model !== 'string' || !model.startsWith(KEY_MODEL_PREFIX)) return undefined;
79
+ const parts = model.slice(KEY_MODEL_PREFIX.length).split('/');
80
+ if (parts.length !== 2) return undefined;
81
+ try {
82
+ const entryId = decodeURIComponent(parts[0]);
83
+ const modelId = decodeURIComponent(parts[1]);
84
+ if (!KEY_ENTRY_ID.test(entryId) || forbiddenEntry(entryId) || !modelId || modelId.startsWith(KEY_MODEL_PREFIX)) return undefined;
85
+ if (`${KEY_MODEL_PREFIX}${encodeURIComponent(entryId)}/${encodeURIComponent(modelId)}` !== model) return undefined;
86
+ return { entryId, modelId };
87
+ } catch { return undefined; }
88
+ }
89
+
90
+ function keyEntryGroupKey(provider, entryId) {
91
+ return `@key/${encodeURIComponent(provider)}/${encodeURIComponent(entryId)}`;
92
+ }
93
+
94
+ function keyEntryName(providers, provider, entryId) {
95
+ const entries = Object.hasOwn(providers ?? {}, provider) ? providers[provider]?.keyEntries : undefined;
96
+ const entry = entries && Object.hasOwn(entries, entryId) ? entries[entryId] : undefined;
97
+ return typeof entry?.name === 'string' && entry.name.trim() ? entry.name.trim() : entryId;
98
+ }
99
+
100
+ /** Preserve every original provider fold; only real multi-key routes gain children.
101
+ * `providers` may be undefined while the native settings form is loading. In
102
+ * that case aliases still remain identifiable, without inventing credentials.
103
+ */
104
+ function buildModelHierarchy(groups, providers, preferences, current = null) {
105
+ return groups.map(group => {
106
+ const profile = Object.hasOwn(providers ?? {}, group.id) ? providers[group.id] : undefined;
107
+ const aliases = group.models.filter(model => decodeKeyModelId(model.id));
108
+ const multi = profile?.multiKey === true || (providers === undefined && aliases.length > 0);
109
+ const root = {
110
+ kind: multi ? 'multi' : 'provider', key: group.id, id: group.id,
111
+ name: group.provider?.name ?? group.name ?? group.id, group,
112
+ current: current?.provider === group.id,
113
+ expanded: isGroupExpanded(preferences, group.id, current?.provider),
114
+ };
115
+ if (!multi) {
116
+ // A catalog refresh can race with disabling multi-key. Do not offer stale
117
+ // aliases as if they belonged to the single-key credential.
118
+ return { ...root, group: { ...group, models: group.models.filter(model => !model.id.startsWith(KEY_MODEL_PREFIX)) }, modelCount: group.models.filter(model => !model.id.startsWith(KEY_MODEL_PREFIX)).length };
119
+ }
120
+ const byEntry = new Map();
121
+ const configured = profile?.keyEntries && typeof profile.keyEntries === 'object' ? profile.keyEntries : {};
122
+ for (const entryId of Object.keys(configured)) {
123
+ if (KEY_ENTRY_ID.test(entryId) && !forbiddenEntry(entryId)) byEntry.set(entryId, []);
124
+ }
125
+ for (const model of aliases) {
126
+ const { entryId } = decodeKeyModelId(model.id);
127
+ // Do not revive a removed entry from an old directory snapshot.
128
+ if (providers !== undefined && !Object.hasOwn(configured, entryId)) continue;
129
+ if (!byEntry.has(entryId)) byEntry.set(entryId, []);
130
+ byEntry.get(entryId).push(model);
131
+ }
132
+ const selectedEntry = current?.provider === group.id ? decodeKeyModelId(current.model)?.entryId : undefined;
133
+ const children = [...byEntry].filter(([, models]) => models.length > 0).map(([entryId, models]) => {
134
+ const key = keyEntryGroupKey(group.id, entryId);
135
+ const active = root.current && selectedEntry === entryId;
136
+ return { ...group, key, entryId, name: keyEntryName(providers, group.id, entryId), models, current: active,
137
+ expanded: Object.hasOwn(preferences, key) ? preferences[key] : active };
138
+ });
139
+ // Legacy raw selections keep their exact original credential. Offer only the
140
+ // current raw model here, not an extra editable/key-group configuration.
141
+ if (root.current && !selectedEntry) {
142
+ const legacy = group.models.filter(model => model.id === current.model && !model.id.startsWith(KEY_MODEL_PREFIX));
143
+ if (legacy.length) {
144
+ const key = keyEntryGroupKey(group.id, '@legacy');
145
+ children.push({ ...group, key, entryId: undefined, legacy: true, name: 'Legacy single key', models: legacy, current: true,
146
+ expanded: Object.hasOwn(preferences, key) ? preferences[key] : true });
147
+ }
148
+ }
149
+ return { ...root, children, modelCount: children.reduce((sum, child) => sum + child.models.length, 0) };
150
+ });
151
+ }
152
+
153
+ function visibleHierarchyModels(entries) {
154
+ return entries.flatMap(entry => {
155
+ if (!entry.expanded) return [];
156
+ const groups = entry.kind === 'multi' ? entry.children.filter(child => child.expanded) : [entry.group];
157
+ return groups.flatMap(group => group.models.map(model => ({ provider: group.id, model: model.id })));
158
+ });
159
+ }
160
+
161
+ function hierarchyExpansionKeys(entries) {
162
+ return entries.flatMap(entry => [entry.key, ...(entry.kind === 'multi' ? entry.children.map(child => child.key) : [])]);
163
+ }
164
+
165
+ /** Search temporarily expands matching provider/key paths, never saved folds. */
166
+ function filterModelHierarchy(entries, query, rankModels) {
167
+ const needle = query.trim();
168
+ if (!needle) return entries;
169
+ const matchesName = names => rankModels(names.filter(Boolean).map(name => ({ name })), needle).length > 0;
170
+ return entries.flatMap(entry => {
171
+ const all = matchesName([entry.name, entry.id]);
172
+ if (entry.kind !== 'multi') {
173
+ const models = all ? entry.group.models : rankModels(entry.group.models, needle);
174
+ return models.length ? [{ ...entry, expanded: true, modelCount: models.length, group: { ...entry.group, models } }] : [];
175
+ }
176
+ const children = entry.children.flatMap(child => {
177
+ const models = all || matchesName([child.name, child.entryId]) ? child.models : rankModels(child.models, needle);
178
+ return models.length ? [{ ...child, expanded: true, models }] : [];
179
+ });
180
+ return children.length ? [{ ...entry, expanded: true, children, modelCount: children.reduce((sum, child) => sum + child.models.length, 0) }] : [];
181
+ });
182
+ }
183
+
184
+ // Model picker adapted from DeepSeek Harness 0.2.1-rc.2 (MIT; see LICENSE).
185
+ //#region ../../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs
186
+ function r(e) {
187
+ var t, f, n = "";
188
+ if ("string" == typeof e || "number" == typeof e) n += e;
189
+ else if ("object" == typeof e) if (Array.isArray(e)) {
190
+ var o = e.length;
191
+ for (t = 0; t < o; t++) e[t] && (f = r(e[t])) && (n && (n += " "), n += f);
192
+ } else for (f in e) e[f] && (n && (n += " "), n += f);
193
+ return n;
194
+ }
195
+ function clsx() {
196
+ for (var e, t, f = 0, n = "", o = arguments.length; f < o; f++) (e = arguments[f]) && (t = r(e)) && (n && (n += " "), n += t);
197
+ return n;
198
+ }
199
+ //#endregion
200
+ //#region \0dsh-css:/home/runner/work/deepseek-harness/deepseek-harness/packages/client/ui-model-selection/src/client/ModelSelect.module.css.mjs
201
+ const css = "._7KE1Ra_root{min-width:0;position:relative}._7KE1Ra_trigger{border-radius:var(--dsw-radius-sm);min-width:0;max-width:360px;height:28px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;outline:none;align-items:center;gap:4px;padding:0 4px 0 8px;font-size:13px;font-weight:400;line-height:20px;display:flex}@media (hover:hover){._7KE1Ra_trigger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}}._7KE1Ra_trigger:focus-visible:not([data-selection-focus]){box-shadow:0 0 0 2px var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary))}._7KE1Ra_trigger:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}._7KE1Ra_triggerLabel{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}._7KE1Ra_triggerEffort{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-caption);flex-shrink:1000;overflow:hidden}._7KE1Ra_triggerIcon{display:var(--dsh-composer-model-icon-display,none);flex:none}._7KE1Ra_triggerLabel,._7KE1Ra_triggerEffort{display:var(--dsh-composer-model-text-display,block)}._7KE1Ra_chevron{color:var(--dsw-alias-label-caption);flex:none;transition:transform .12s}._7KE1Ra_chevronOpen{transform:rotate(180deg)}._7KE1Ra_menu{box-sizing:border-box;z-index:1100;width:max-content;min-width:min(240px, calc(100vw - 32px), var(--dsh-anchored-max-width,100vw));max-width:min(420px, calc(100vw - 32px), var(--dsh-anchored-max-width,100vw));max-height:min(360px, calc(100vh - 96px), var(--dsh-anchored-max-height,100vh));--dsw-elevation-stroke-color:var(--dsw-alias-border-l1);box-shadow:var(--dsw-elevation-prominent);color:var(--dsw-alias-label-primary);--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);border:0;flex-direction:column;padding:4px;display:flex;position:fixed;overflow:hidden}._7KE1Ra_status,._7KE1Ra_empty{color:var(--dsw-alias-label-tertiary);padding:8px;font-size:12px;line-height:18px}._7KE1Ra_error,._7KE1Ra_warning{border-radius:var(--dsw-radius-md);background:var(--dsw-alias-interactive-bg-hover-danger);color:var(--dsw-alias-state-error-primary);justify-content:space-between;align-items:flex-start;gap:6px;margin-bottom:3px;padding:6px 7px;font-size:11px;line-height:16px;display:flex}._7KE1Ra_warning{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-state-warn-label)}._7KE1Ra_retry{color:inherit;font:inherit;cursor:pointer;background:0 0;border:none;flex:none;padding:0;font-weight:600}._7KE1Ra_searchRow{flex-shrink:0;margin:2px 0 3px;position:relative}._7KE1Ra_searchRow ._7KE1Ra_search{border-radius:var(--dsw-radius-md);background:0 0;border:0 solid #0000;height:auto;padding:5px 7px;display:flex}._7KE1Ra_searchRow ._7KE1Ra_searchWithQuery{padding-right:34px}._7KE1Ra_searchClear{corner-shape:round;width:24px;height:24px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:50%;justify-content:center;align-items:center;padding:0;display:inline-flex;position:absolute;top:50%;right:4px;transform:translateY(-50%)}._7KE1Ra_searchClear:focus-visible{background:var(--dsw-alias-interactive-bg-hover);outline:none}@media (hover:hover){._7KE1Ra_searchClear:hover{background:var(--dsw-alias-interactive-bg-hover);outline:none}}._7KE1Ra_searchRow ._7KE1Ra_search:focus-within{border-color:#0000}._7KE1Ra_searchRow ._7KE1Ra_search input{padding:0;font-size:12px;line-height:normal}@media (pointer:coarse){._7KE1Ra_searchRow ._7KE1Ra_search input{font-size:16px}}._7KE1Ra_searchRow ._7KE1Ra_search input::placeholder{color:var(--dsw-alias-label-caption)}._7KE1Ra_groups:not([hidden]){min-height:0;overflow:hidden auto}._7KE1Ra_option{box-sizing:border-box;border-radius:var(--dsw-radius-md);width:auto;min-width:100%;min-height:34px;color:inherit;text-align:left;cursor:pointer;background:0 0;border:none;outline:none;align-items:center;gap:6px;padding:5px 7px;display:flex}._7KE1Ra_option:focus-visible,._7KE1Ra_optionActive:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}._7KE1Ra_modelOption{max-width:100%}@media (hover:hover){._7KE1Ra_option:not(._7KE1Ra_modelOption):hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}}._7KE1Ra_selected{background:0 0}._7KE1Ra_option:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}._7KE1Ra_optionCopy{flex-direction:column;flex:1;min-width:0;display:flex}._7KE1Ra_modelName{color:inherit;text-overflow:ellipsis;white-space:nowrap;font-size:13px;font-weight:400;line-height:18px;overflow:hidden}._7KE1Ra_badgedName{vertical-align:baseline;white-space:nowrap;align-items:center;gap:4px;max-width:100%;display:inline-flex}._7KE1Ra_badgedNameText{text-overflow:ellipsis;min-width:0;overflow:hidden}._7KE1Ra_badge{flex:none}._7KE1Ra_check{color:var(--dsw-alias-label-primary);flex:0 0 14px;place-items:center;display:grid}._7KE1Ra_check svg{width:14px;height:14px}._7KE1Ra_cell{box-sizing:border-box;border-radius:var(--dsw-radius-md);width:auto;min-width:100%;height:34px;color:var(--dsw-alias-label-primary);cursor:pointer;text-align:left;background:0 0;border:none;outline:none;align-items:center;gap:6px;padding:0 8px;font-size:13px;line-height:20px;display:flex}._7KE1Ra_cell:focus-visible{background:var(--dsw-alias-interactive-bg-hover)}@media (hover:hover){._7KE1Ra_cell:hover{background:var(--dsw-alias-interactive-bg-hover)}}._7KE1Ra_cellLabel{white-space:nowrap;flex:none}._7KE1Ra_cellValue{text-overflow:ellipsis;white-space:nowrap;text-align:right;min-width:0;color:var(--dsw-alias-label-tertiary);flex:auto;overflow:hidden}._7KE1Ra_cellChevron{width:12px;height:12px;color:var(--dsw-alias-menu-icon);flex:none}";
202
+ const tagId = "dsh-model-groups/ModelSelect.css";
203
+ function installModelSelectStyles() {
204
+ if (typeof document === "undefined" || document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") !== null) return;
205
+ const tag = document.createElement("style");
206
+ tag.dataset.plugin = "dsh-model-groups";
207
+ tag.dataset.pluginCss = tagId;
208
+ tag.textContent = css;
209
+ document.head.appendChild(tag);
210
+ }
211
+ var ModelSelect_module_css_default = {
212
+ "badge": "_7KE1Ra_badge",
213
+ "badgedName": "_7KE1Ra_badgedName",
214
+ "badgedNameText": "_7KE1Ra_badgedNameText",
215
+ "cell": "_7KE1Ra_cell",
216
+ "cellChevron": "_7KE1Ra_cellChevron",
217
+ "cellLabel": "_7KE1Ra_cellLabel",
218
+ "cellValue": "_7KE1Ra_cellValue",
219
+ "check": "_7KE1Ra_check",
220
+ "chevron": "_7KE1Ra_chevron",
221
+ "chevronOpen": "_7KE1Ra_chevronOpen",
222
+ "empty": "_7KE1Ra_empty",
223
+ "error": "_7KE1Ra_error",
224
+ "groups": "_7KE1Ra_groups",
225
+ "menu": "_7KE1Ra_menu",
226
+ "modelName": "_7KE1Ra_modelName",
227
+ "modelOption": "_7KE1Ra_modelOption",
228
+ "option": "_7KE1Ra_option",
229
+ "optionActive": "_7KE1Ra_optionActive",
230
+ "optionCopy": "_7KE1Ra_optionCopy",
231
+ "retry": "_7KE1Ra_retry",
232
+ "root": "_7KE1Ra_root",
233
+ "search": "_7KE1Ra_search",
234
+ "searchClear": "_7KE1Ra_searchClear",
235
+ "searchRow": "_7KE1Ra_searchRow",
236
+ "searchWithQuery": "_7KE1Ra_searchWithQuery",
237
+ "selected": "_7KE1Ra_selected",
238
+ "status": "_7KE1Ra_status",
239
+ "trigger": "_7KE1Ra_trigger",
240
+ "triggerEffort": "_7KE1Ra_triggerEffort",
241
+ "triggerIcon": "_7KE1Ra_triggerIcon",
242
+ "triggerLabel": "_7KE1Ra_triggerLabel",
243
+ "warning": "_7KE1Ra_warning"
244
+ };
245
+ //#endregion
246
+ //#region lib/types/client/provider-order.js
247
+ /** Shared provider display order for the composer and command model pickers. */
248
+ /**
249
+ * Put the account and official providers first, preserving every other relative order.
250
+ * @param groups - Provider groups in catalog order.
251
+ * @returns a sorted copy; model order within each group is unchanged.
252
+ */
253
+ function orderModelProviders(groups) {
254
+ return groups.toSorted((left, right) => (left.id === "deepseek-account" ? 0 : left.id === "deepseek-official" ? 1 : 2) - (right.id === "deepseek-account" ? 0 : right.id === "deepseek-official" ? 1 : 2));
255
+ }
256
+ //#endregion
257
+ //#region lib/types/client/ModelSelect.js
258
+ /**
259
+ * ModelSelect: the composer's named model seat (`conversation.input.model`).
260
+ * Two-level selection per figma 496:26454's MenuDropdown: the root menu is
261
+ * the Model / Effort row pair (label + current value + a right chevron),
262
+ * each drilling into its own list — the provider-grouped model list over
263
+ * the shared directory, and the effort levels. The trigger (313:14108's
264
+ * ToggleButton) shows both: model name + effort in the caption tone.
265
+ * Model catalogs above four entries show search, which retains focus while
266
+ * ↑/↓ cycle the highlighted result; Enter and Tab accept it. Smaller model
267
+ * catalogs, root panes, and effort panes move focus between rows. Escape and Shift+Tab leave a drilled pane first and otherwise close
268
+ * back to the trigger. A drilled pane focuses the current effort or model
269
+ * search field. Provider headings paint their background only while pinned
270
+ * by scrolling. Clearing a query restores the full list and search focus.
271
+ * Selecting restores trigger focus without a ring until the trigger loses focus
272
+ * or the menu reopens. Model names match a case-insensitive ordered subsequence
273
+ * within each provider group, ranked by
274
+ * prefix, alignment score, then catalog order. Returning to the root pane
275
+ * hands focus back to the cell that opened it. Data and submission ride the
276
+ * same per-session ModelDirectory as the /model popup; exact-model reasoning
277
+ * metadata and the selected effort come from the Host rather than a
278
+ * client-owned vocabulary. A rejected selection announces through the shared
279
+ * transient Toast anchored to the composer card; the in-menu strip with
280
+ * Retry remains the catalog-load surface. While the directory's pending
281
+ * selection is unsettled, the trigger shows a spinner in place of its
282
+ * chevron, and each row whose value that selection carries shows one in place
283
+ * of its check mark.
284
+ */
285
+ /** Unplaced portal card: hidden but laid out at a fixed origin so offsetWidth/offsetHeight are real (Menu primitive's measure pass). */
286
+ const MEASURE_STYLE = {
287
+ visibility: "hidden",
288
+ left: 0,
289
+ top: 0
290
+ };
291
+ /**
292
+ * Render the composer model seat.
293
+ * @param props - owner share (locked) + injected face (shared directory
294
+ * store/verbs) + the standard locale seat.
295
+ * @returns the trigger and, while open, the two-level menu.
296
+ */
297
+ function ModelSelect({ locked, available, directory, load, select, t, renderSlot, providersForm }) {
298
+ const preferences = react.useSyncExternalStore(groupPreferences.subscribe, groupPreferences.getSnapshot);
299
+ const configuration = react.useSyncExternalStore(listener => providersForm?.subscribe(listener) ?? (() => {}), () => providersForm?.getSnapshot() ?? EMPTY_PROVIDERS_SNAPSHOT);
300
+ const providers = configuration.status === 'ready' ? (configuration.value?.providers ?? {}) : undefined;
301
+ const text = (zh, en) => t("menu.model") === "模型" ? zh : en;
302
+ const state = (0, react.useSyncExternalStore)((fn) => directory.subscribe(fn), () => directory.getSnapshot());
303
+ const [open, setOpen] = (0, react.useState)(false);
304
+ const [pane, setPane] = (0, react.useState)("root");
305
+ const [query, setQuery] = (0, react.useState)("");
306
+ const [highlightedIndex, setHighlightedIndex] = (0, react.useState)(null);
307
+ const [selectionFocus, setSelectionFocus] = (0, react.useState)(false);
308
+ const [menuMode, setLocalMenuMode] = (0, react.useState)(() => getMenuMode());
309
+ const [activeFlyout, setActiveFlyout] = (0, react.useState)(null);
310
+ const [activeSubFlyout, setActiveSubFlyout] = (0, react.useState)(null);
311
+ const flyoutTimerRef = (0, react.useRef)(null);
312
+ const subFlyoutTimerRef = (0, react.useRef)(null);
313
+ const flyoutRef = (0, react.useRef)(null);
314
+ const subFlyoutRef = (0, react.useRef)(null);
315
+ const lastActionRef = (0, react.useRef)("load");
316
+
317
+ const switchMode = (mode) => {
318
+ setLocalMenuMode(mode);
319
+ setMenuMode(mode);
320
+ setActiveFlyout(null);
321
+ setActiveSubFlyout(null);
322
+ };
323
+
324
+ const clearFlyoutTimer = () => {
325
+ if (flyoutTimerRef.current !== null) {
326
+ clearTimeout(flyoutTimerRef.current);
327
+ flyoutTimerRef.current = null;
328
+ }
329
+ if (subFlyoutTimerRef.current !== null) {
330
+ clearTimeout(subFlyoutTimerRef.current);
331
+ subFlyoutTimerRef.current = null;
332
+ }
333
+ };
334
+
335
+ // 完全读取并匹配官方原版主卡片尺寸(宽、高严格保持与主卡片完全一致)
336
+ const triggerFlyout = (entry, anchorEl) => {
337
+ clearFlyoutTimer();
338
+ if (activeFlyout?.key === entry.key) return;
339
+ setActiveSubFlyout(null);
340
+ const menuEl = menuRef.current;
341
+ const menuRect = menuEl ? menuEl.getBoundingClientRect() : anchorEl.getBoundingClientRect();
342
+ const margin = 12;
343
+ const winW = window.innerWidth;
344
+ const winH = window.innerHeight;
345
+ // 直接读取官方主卡片的实际渲染宽度与高度
346
+ const panelW = menuRect.width > 0 ? menuRect.width : 280;
347
+ const panelH = menuRect.height > 0 ? menuRect.height : 360;
348
+
349
+ // 紧靠主卡片左侧,0px 缝隙严丝合缝
350
+ let left = menuRect.left - panelW;
351
+ if (left < margin) {
352
+ if (winW - menuRect.right >= panelW + margin) {
353
+ left = menuRect.right;
354
+ } else {
355
+ left = Math.max(margin, Math.min(menuRect.left, winW - panelW - margin));
356
+ }
357
+ }
358
+
359
+ // 顶部与官方主卡片绝对平齐,高度与官方主卡片 1:1 等高
360
+ let top = menuRect.top;
361
+ setActiveFlyout({
362
+ key: entry.key,
363
+ entry,
364
+ pos: {
365
+ left: Math.round(left),
366
+ top: Math.round(top),
367
+ width: Math.round(panelW),
368
+ height: Math.round(panelH)
369
+ }
370
+ });
371
+ };
372
+
373
+ const triggerSubFlyout = (childGroup, anchorEl) => {
374
+ clearFlyoutTimer();
375
+ if (activeSubFlyout?.key === childGroup.key) return;
376
+ const flyoutEl = flyoutRef.current;
377
+ const flyoutRect = flyoutEl ? flyoutEl.getBoundingClientRect() : anchorEl.getBoundingClientRect();
378
+ const margin = 12;
379
+ const winW = window.innerWidth;
380
+ const winH = window.innerHeight;
381
+ const panelW = flyoutRect.width > 0 ? flyoutRect.width : 280;
382
+ const panelH = flyoutRect.height > 0 ? flyoutRect.height : 360;
383
+
384
+ let left = flyoutRect.left - panelW;
385
+ if (left < margin) {
386
+ if (winW - flyoutRect.right >= panelW + margin) {
387
+ left = flyoutRect.right;
388
+ } else {
389
+ left = Math.max(margin, Math.min(flyoutRect.left, winW - panelW - margin));
390
+ }
391
+ }
392
+
393
+ let top = flyoutRect.top;
394
+ setActiveSubFlyout({
395
+ key: childGroup.key,
396
+ childGroup,
397
+ pos: {
398
+ left: Math.round(left),
399
+ top: Math.round(top),
400
+ width: Math.round(panelW),
401
+ height: Math.round(panelH)
402
+ }
403
+ });
404
+ };
405
+
406
+ const scheduleCloseFlyout = (delay = 240) => {
407
+ clearFlyoutTimer();
408
+ flyoutTimerRef.current = setTimeout(() => {
409
+ setActiveFlyout(null);
410
+ setActiveSubFlyout(null);
411
+ }, delay);
412
+ };
413
+
414
+ const scheduleCloseSubFlyout = (delay = 240) => {
415
+ if (subFlyoutTimerRef.current !== null) clearTimeout(subFlyoutTimerRef.current);
416
+ subFlyoutTimerRef.current = setTimeout(() => {
417
+ setActiveSubFlyout(null);
418
+ }, delay);
419
+ };
420
+ const [toast, setToast] = (0, react.useState)(null);
421
+ const toastSeq = (0, react.useRef)(0);
422
+ const rootRef = (0, react.useRef)(null);
423
+ const triggerRef = (0, react.useRef)(null);
424
+ const searchRef = (0, react.useRef)(null);
425
+ const menuRef = (0, react.useRef)(null);
426
+ const groupsRef = (0, react.useRef)(null);
427
+ const [menuPos, setMenuPos] = (0, react.useState)(null);
428
+ const itemRefs = (0, react.useRef)([]);
429
+ const id = (0, react.useId)();
430
+ const accountProviderName = t("provider.account");
431
+ const groups = (0, react.useMemo)(() => orderModelProviders(state.groups).map((group) => ({
432
+ ...group,
433
+ provider: {
434
+ id: group.id,
435
+ name: group.id === "deepseek-account" ? accountProviderName : group.name
436
+ }
437
+ })), [state.groups, accountProviderName]);
438
+ const choices = (0, react.useMemo)(() => groups.flatMap((group) => group.models.map((model) => ({
439
+ group,
440
+ model,
441
+ selection: {
442
+ provider: group.id,
443
+ model: model.id,
444
+ ...model.reasoning?.defaultEffort === void 0 ? {} : { reasoningEffort: model.reasoning.defaultEffort }
445
+ }
446
+ }))), [groups]);
447
+ const showSearch = choices.length > 4;
448
+ const searching = showSearch && query.trim() !== "";
449
+ const fullHierarchy = react.useMemo(() => buildModelHierarchy(groups, providers, preferences, state.current), [groups, providers, preferences, state.current]);
450
+ const displayGroups = react.useMemo(() => filterModelHierarchy(fullHierarchy, showSearch ? query : "", _deepseek_ai_dsh_client_ui_primitives.rankByName), [fullHierarchy, query, showSearch]);
451
+ const expansionKeys = react.useMemo(() => hierarchyExpansionKeys(fullHierarchy), [fullHierarchy]);
452
+ const providerCount = fullHierarchy.length;
453
+ const visibleModels = react.useMemo(() => visibleHierarchyModels(displayGroups), [displayGroups]);
454
+ const currentVisibleIndex = visibleModels.findIndex((model) => model.provider === state.current?.provider && model.model === state.current.model);
455
+ const activeModelIndex = Math.min(highlightedIndex ?? Math.max(0, currentVisibleIndex), visibleModels.length - 1);
456
+ const currentChoice = choices[state.current === null ? -1 : choices.findIndex((c) => c.selection.provider === state.current?.provider && c.selection.model === state.current.model)];
457
+ const reasoning = currentChoice?.model.reasoning;
458
+ const effectiveEffort = state.current?.reasoningEffort ?? reasoning?.defaultEffort;
459
+ const effortLabel = currentChoice === void 0 ? void 0 : reasoning === void 0 ? state.retainedEffort : effectiveEffort === void 0 ? t("effort.providerDefault") : reasoning.efforts.find((level) => level.id === effectiveEffort)?.name ?? effectiveEffort;
460
+ const effortChoices = (0, react.useMemo)(() => reasoning === void 0 ? [] : [...reasoning.defaultEffort === void 0 ? [{
461
+ key: "provider-default",
462
+ effort: void 0,
463
+ label: t("effort.providerDefault")
464
+ }] : [], ...reasoning.efforts.map((effort) => ({
465
+ key: `effort:${effort.id}`,
466
+ effort: effort.id,
467
+ label: effort.name
468
+ }))], [reasoning, t]);
469
+ const { pending } = state;
470
+ const busy = pending !== null;
471
+ /** Group headers and bulk buttons; the only focus targets when every group is collapsed. */
472
+ const groupControls = () => [...(menuRef.current?.querySelectorAll("[data-group-bulk]:not(:disabled), [data-group-toggle]:not(:disabled)") ?? [])];
473
+ const reload = () => {
474
+ lastActionRef.current = "load";
475
+ load();
476
+ };
477
+ (0, react.useEffect)(() => {
478
+ if (!open) return;
479
+ const closeOutside = (event) => {
480
+ if (rootRef.current?.contains(event.target) === true) return;
481
+ if (menuRef.current?.contains(event.target) === true) return;
482
+ if (flyoutRef.current?.contains(event.target) === true) return;
483
+ if (subFlyoutRef.current?.contains(event.target) === true) return;
484
+ setOpen(false);
485
+ setActiveFlyout(null);
486
+ setActiveSubFlyout(null);
487
+ };
488
+ document.addEventListener("pointerdown", closeOutside);
489
+ return () => {
490
+ document.removeEventListener("pointerdown", closeOutside);
491
+ };
492
+ }, [open]);
493
+ (0, react.useLayoutEffect)(() => {
494
+ if (!showSearch) {
495
+ setQuery("");
496
+ setHighlightedIndex(null);
497
+ }
498
+ }, [showSearch]);
499
+ const paneFocus = (0, react.useRef)(null);
500
+ const previousShowSearch = (0, react.useRef)(showSearch);
501
+ (0, react.useEffect)(() => {
502
+ const changedSearchMode = previousShowSearch.current !== showSearch;
503
+ previousShowSearch.current = showSearch;
504
+ const intent = paneFocus.current ?? (changedSearchMode && pane === "model" ? "drill" : null);
505
+ paneFocus.current = null;
506
+ if (!open || intent === null) return;
507
+ if (intent === "drill") {
508
+ // Never focus the search field here: on touch devices that raises the
509
+ // on-screen keyboard the moment the model list opens. Land on the
510
+ // current row so arrow keys and Enter still work without an IME.
511
+ (menuRef.current?.querySelector("[role=\"menuitemradio\"][aria-checked=\"true\"]:not([disabled])") ?? itemRefs.current.find((item) => item !== null && !item.disabled) ?? groupControls()[0] ?? triggerRef.current)?.focus();
512
+ return;
513
+ }
514
+ const cell = itemRefs.current[intent === "effort" ? 1 : 0];
515
+ (cell !== null && cell !== void 0 && !cell.disabled ? cell : triggerRef.current)?.focus();
516
+ }, [
517
+ open,
518
+ pane,
519
+ showSearch
520
+ ]);
521
+ (0, react.useEffect)(() => {
522
+ const viewport = groupsRef.current;
523
+ if (viewport === null) return;
524
+ return (0, _deepseek_ai_dsh_client_ui_primitives.observeStickyMenuGroups)(viewport);
525
+ }, [
526
+ available,
527
+ open,
528
+ pane,
529
+ displayGroups
530
+ ]);
531
+ (0, react.useLayoutEffect)(() => {
532
+ // Folding should not scroll away from the heading the user just pressed.
533
+ if (document.activeElement?.matches?.("[data-group-toggle], [data-group-bulk]")) return;
534
+ if (open && pane === "model" && activeModelIndex >= 0) itemRefs.current[activeModelIndex]?.scrollIntoView({ block: "nearest" });
535
+ }, [
536
+ open,
537
+ pane,
538
+ activeModelIndex,
539
+ visibleModels
540
+ ]);
541
+ (0, react.useLayoutEffect)(() => {
542
+ if (!available || !open) {
543
+ setMenuPos(null);
544
+ return;
545
+ }
546
+ const place = () => {
547
+ /* v8 ignore next 2 -- the trigger ref is attached whenever the menu is open. */
548
+ const rect = triggerRef.current?.getBoundingClientRect();
549
+ if (rect === void 0) return;
550
+ const MARGIN = 12;
551
+ const winW = window.innerWidth;
552
+ const winH = window.innerHeight;
553
+ const lw = menuRef.current?.offsetWidth ?? 0;
554
+ const lh = menuRef.current?.offsetHeight ?? 0;
555
+ // 窄屏(手机竖屏等)优先居中并确保不会超出屏幕左右
556
+ let x;
557
+ if (winW <= 480) {
558
+ x = Math.max(MARGIN, (winW - lw) / 2);
559
+ } else {
560
+ x = rect.right - lw;
561
+ if (lw > 0) x = Math.min(Math.max(x, MARGIN), winW - lw - MARGIN);
562
+ }
563
+ let y = rect.top - 8 - lh;
564
+ // 如果上方空间不够容纳弹窗,向下弹出
565
+ if (y < MARGIN && rect.bottom + 8 + lh <= winH - MARGIN) {
566
+ y = rect.bottom + 8;
567
+ } else {
568
+ if (lh > 0) y = Math.min(Math.max(y, MARGIN), winH - lh - MARGIN);
569
+ }
570
+ setMenuPos({
571
+ left: Math.round(x),
572
+ top: Math.round(y)
573
+ });
574
+ };
575
+ place();
576
+ window.addEventListener("scroll", place, true);
577
+ window.addEventListener("resize", place);
578
+ const menu = menuRef.current;
579
+ const observer = typeof ResizeObserver !== "undefined" && menu !== null ? new ResizeObserver(place) : null;
580
+ if (menu !== null) observer?.observe(menu);
581
+ return () => {
582
+ observer?.disconnect();
583
+ window.removeEventListener("scroll", place, true);
584
+ window.removeEventListener("resize", place);
585
+ };
586
+ }, [
587
+ available,
588
+ open,
589
+ pane,
590
+ state,
591
+ query
592
+ ]);
593
+ if (!available) return null;
594
+ const show = () => {
595
+ setSelectionFocus(false);
596
+ triggerRef.current?.focus();
597
+ setQuery("");
598
+ setHighlightedIndex(null);
599
+ if (currentChoice === void 0) paneFocus.current = "drill";
600
+ setPane(currentChoice === void 0 ? "model" : "root");
601
+ setOpen(true);
602
+ reload();
603
+ };
604
+ const changeQuery = (next) => {
605
+ setQuery(next);
606
+ setHighlightedIndex(0);
607
+ };
608
+ const close = (restoreFocus = false) => {
609
+ setOpen(false);
610
+ setPane("root");
611
+ setActiveFlyout(null);
612
+ setActiveSubFlyout(null);
613
+ if (restoreFocus) queueMicrotask(() => {
614
+ triggerRef.current?.focus();
615
+ });
616
+ };
617
+ const closeAfterSelection = () => {
618
+ setSelectionFocus(true);
619
+ close(true);
620
+ };
621
+ const drill = (next) => {
622
+ setQuery("");
623
+ setHighlightedIndex(null);
624
+ paneFocus.current = "drill";
625
+ setPane(next);
626
+ };
627
+ /** Leave a drilled pane for the root one, handing the keyboard back to its cell. */
628
+ const back = (from) => {
629
+ paneFocus.current = from;
630
+ setPane("root");
631
+ };
632
+ const moveFocus = (offset) => {
633
+ const items = itemRefs.current.filter((item) => item !== null);
634
+ if (items.length === 0) return;
635
+ const active = items.findIndex((item) => item === document.activeElement);
636
+ items[active === -1 ? offset > 0 ? 0 : items.length - 1 : (active + offset + items.length) % items.length]?.focus();
637
+ };
638
+ const onRootKeyDown = (event) => {
639
+ if (event.nativeEvent.isComposing) return;
640
+ if (event.key === "Escape" && open) {
641
+ event.preventDefault();
642
+ if (pane !== "root" && currentChoice !== void 0) back(pane);
643
+ else close(true);
644
+ return;
645
+ }
646
+ if (!open) return;
647
+ if (pane === "model" && (event.key === "ArrowDown" || event.key === "ArrowUp") && event.target !== searchRef.current) {
648
+ event.preventDefault();
649
+ const rows = [...(menuRef.current?.querySelectorAll('[data-group-bulk]:not(:disabled), [data-group-toggle]:not(:disabled), [data-model-choice]:not(:disabled)') ?? [])];
650
+ const at = rows.indexOf(document.activeElement);
651
+ rows[at < 0 ? (event.key === 'ArrowDown' ? 0 : rows.length - 1) : (at + (event.key === 'ArrowDown' ? 1 : -1) + rows.length) % rows.length]?.focus();
652
+ return;
653
+ }
654
+ if (pane === "model" && event.target instanceof HTMLButtonElement && event.target.matches("[data-group-toggle]")) {
655
+ if ((event.key === 'ArrowLeft' && event.target.getAttribute('aria-expanded') === 'true') || (event.key === 'ArrowRight' && event.target.getAttribute('aria-expanded') === 'false')) {
656
+ event.preventDefault();
657
+ event.target.click();
658
+ }
659
+ return;
660
+ }
661
+ if (pane === "model" && showSearch && (event.key === "ArrowDown" || event.key === "ArrowUp")) {
662
+ event.preventDefault();
663
+ if (!busy && visibleModels.length > 0) {
664
+ setHighlightedIndex((activeModelIndex + (event.key === "ArrowDown" ? 1 : -1) + visibleModels.length) % visibleModels.length);
665
+ searchRef.current?.focus();
666
+ }
667
+ return;
668
+ }
669
+ if (pane === "model" && showSearch && event.target instanceof HTMLInputElement && (event.key === "Enter" || event.key === "Tab" && !event.shiftKey)) {
670
+ if (event.key === "Tab" && visibleModels.length === 0) return;
671
+ event.preventDefault();
672
+ const highlighted = visibleModels[activeModelIndex];
673
+ if (!busy && highlighted !== void 0) choose(highlighted);
674
+ return;
675
+ }
676
+ if (event.key === "Tab" && event.shiftKey && pane === "model" && showSearch && document.activeElement === searchRef.current) {
677
+ const controls = groupControls();
678
+ if (controls.length > 0) {
679
+ event.preventDefault();
680
+ controls[0].focus();
681
+ return;
682
+ }
683
+ }
684
+ if (event.key === "Tab") {
685
+ if (event.shiftKey) {
686
+ event.preventDefault();
687
+ if (pane !== "root" && currentChoice !== void 0) back(pane);
688
+ else close(true);
689
+ return;
690
+ }
691
+ const focused = document.activeElement;
692
+ const rows = itemRefs.current.filter((item) => item !== null);
693
+ if (focused instanceof HTMLButtonElement && rows.includes(focused)) {
694
+ event.preventDefault();
695
+ focused.click();
696
+ return;
697
+ }
698
+ if (focused !== triggerRef.current) return;
699
+ event.preventDefault();
700
+ if (pane === "model" && showSearch) {
701
+ setHighlightedIndex(null);
702
+ searchRef.current?.focus();
703
+ return;
704
+ }
705
+ (menuRef.current?.querySelector("[role=\"menuitemradio\"][aria-checked=\"true\"]:not([disabled])") ?? rows.find((item) => !item.disabled) ?? groupControls()[0])?.focus();
706
+ return;
707
+ }
708
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
709
+ event.preventDefault();
710
+ if (itemRefs.current.some((item) => item !== null)) moveFocus(event.key === "ArrowDown" ? 1 : -1);
711
+ else groupControls()[0]?.focus();
712
+ }
713
+ };
714
+ const settleSelection = (result) => {
715
+ if (result === void 0) return;
716
+ if (result.ok) {
717
+ if (rootRef.current !== null) closeAfterSelection();
718
+ return;
719
+ }
720
+ const { error } = result;
721
+ toastSeq.current += 1;
722
+ setToast({
723
+ seq: toastSeq.current,
724
+ text: error.code === "session/writer-held" ? t("error.sessionInUse") : t("error.action", { message: `${error.code}: ${error.message}` })
725
+ });
726
+ };
727
+ const submit = (selection) => {
728
+ lastActionRef.current = "select";
729
+ setSelectionFocus(true);
730
+ triggerRef.current?.focus();
731
+ select(selection).then(settleSelection);
732
+ };
733
+ const choose = (selection) => {
734
+ if (state.current?.provider === selection.provider && state.current.model === selection.model) {
735
+ closeAfterSelection();
736
+ return;
737
+ }
738
+ submit(selection);
739
+ };
740
+ const chooseEffort = (effort) => {
741
+ if (state.current === null) return;
742
+ if (effectiveEffort === effort) {
743
+ closeAfterSelection();
744
+ return;
745
+ }
746
+ submit({
747
+ provider: state.current.provider,
748
+ model: state.current.model,
749
+ ...effort === void 0 ? {} : { reasoningEffort: effort }
750
+ });
751
+ };
752
+ const waiting = currentChoice === void 0 && state.status === "loading";
753
+ const currentKey = decodeKeyModelId(state.current?.model);
754
+ const currentSource = currentChoice === undefined ? '' : currentKey ? `${currentChoice.group.provider.name} / ${keyEntryName(providers, currentChoice.group.id, currentKey.entryId)}` : currentChoice.group.provider.name;
755
+ const modelLabel = waiting ? t("trigger.loading") : currentChoice === undefined ? t("trigger.fallback") : `${currentChoice.model.name} · ${currentSource}`;
756
+ const triggerLabel = effortLabel === void 0 ? modelLabel : `${modelLabel} · ${effortLabel}`;
757
+ const triggerAria = waiting ? t("trigger.loading") : currentChoice === void 0 ? t("trigger.selectAria") : effortLabel === void 0 ? t("trigger.aria", { model: modelLabel }) : t("trigger.ariaEffort", {
758
+ model: modelLabel,
759
+ effort: effortLabel
760
+ });
761
+ const renderModelLabel = (provider, model, placement, selected) => {
762
+ // The label child slot is declared by the shipped picker's own entry; if this
763
+ // bundle ever runs without it, degrade to the plain name instead of throwing.
764
+ let label;
765
+ try {
766
+ label = renderSlot("conversation.input.model.label", {
767
+ provider,
768
+ model,
769
+ placement,
770
+ selected
771
+ }, { fallback: model.name });
772
+ } catch {
773
+ label = model.name;
774
+ }
775
+ return (0, react_jsx_runtime.jsx)("span", {
776
+ "data-provider-id": provider.id,
777
+ "data-model-id": model.id,
778
+ children: label
779
+ });
780
+ };
781
+ let itemIndex = 0;
782
+ let modelIndex = 0;
783
+ const itemRef = () => {
784
+ const at = itemIndex++;
785
+ return (node) => {
786
+ itemRefs.current[at] = node;
787
+ };
788
+ };
789
+ const renderModels = (group, direct = false) => group.models.map(model => {
790
+ const index = modelIndex++;
791
+ const selected = state.current?.provider === group.id && state.current.model === model.id;
792
+ return react_jsx_runtime.jsxs('button', {
793
+ ref: itemRef(), type: 'button', role: 'menuitemradio',
794
+ 'aria-checked': selected, id: `${id}-model-${index}`,
795
+ 'data-model-choice': '', 'data-provider-id': group.id, 'data-model-id': model.id,
796
+ tabIndex: showSearch ? -1 : 0,
797
+ onFocus: () => setHighlightedIndex(index),
798
+ 'data-highlighted': index === activeModelIndex ? '' : undefined,
799
+ className: clsx(ModelSelect_module_css_default.option, ModelSelect_module_css_default.modelOption, selected && ModelSelect_module_css_default.selected, index === activeModelIndex && ModelSelect_module_css_default.optionActive),
800
+ onMouseMove: busy || index === activeModelIndex ? undefined : () => setHighlightedIndex(index),
801
+ title: `${model.name} · ${group.provider.name} (${group.id})`, disabled: busy,
802
+ onClick: () => choose({ provider: group.id, model: model.id }),
803
+ children: [
804
+ react_jsx_runtime.jsxs('span', { className: ModelSelect_module_css_default.optionCopy, children: [
805
+ react_jsx_runtime.jsx('span', { className: ModelSelect_module_css_default.modelName, children: renderModelLabel(group.provider, model, 'option', selected) }),
806
+ direct && react_jsx_runtime.jsx('span', { className: 'dsh-mg-source', children: group.provider.name }),
807
+ ] }),
808
+ react_jsx_runtime.jsx('span', { className: ModelSelect_module_css_default.check, children: pending?.provider === group.id && pending.model === model.id ? react_jsx_runtime.jsx(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: 'ongoing' }) : selected ? react_jsx_runtime.jsx(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, {}) : null }),
809
+ ],
810
+ }, `${group.id}/${model.id}`);
811
+ });
812
+ const renderHeading = (key, name, expanded, count, current, contentId, major = false, route = '') => react_jsx_runtime.jsx('div', {
813
+ 'data-menu-group-heading': major ? '' : undefined,
814
+ className: clsx('dsh-mg-groupHeading', major ? 'dsh-mg-majorHeading' : 'dsh-mg-minorHeading'),
815
+ children: react_jsx_runtime.jsxs('button', {
816
+ type: 'button', id: `${contentId}-heading`, 'data-group-toggle': key,
817
+ 'aria-expanded': expanded, 'aria-controls': contentId, disabled: busy || searching,
818
+ title: searching ? text('搜索时自动展开,清空搜索后恢复', 'Expanded during search; clearing restores your groups') : route || name,
819
+ onClick: () => { setHighlightedIndex(null); groupPreferences.setExpanded([key], !expanded); },
820
+ children: [
821
+ react_jsx_runtime.jsx(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutlineRegular, { className: expanded ? 'dsh-mg-groupArrow dsh-mg-groupArrowOpen' : 'dsh-mg-groupArrow', size: 14 }),
822
+ react_jsx_runtime.jsx('span', { className: 'dsh-mg-groupName', children: name }),
823
+ current && react_jsx_runtime.jsx('span', { className: 'dsh-mg-currentBadge', children: text('当前', 'Current') }),
824
+ react_jsx_runtime.jsx('span', { className: 'dsh-mg-groupCount', children: count }),
825
+ ],
826
+ }),
827
+ });
828
+ const renderHierarchy = () => displayGroups.map((entry, index) => {
829
+ const providerId = `${id}-provider-${index}`;
830
+ const isFlyoutMode = menuMode === 'panel' && !searching;
831
+ const isFlyoutActive = activeFlyout?.key === entry.key;
832
+
833
+ return react_jsx_runtime.jsxs('section', {
834
+ role: 'group', 'aria-labelledby': `${providerId}-heading`, 'data-menu-group': '', 'data-provider-group': entry.id,
835
+ onMouseEnter: (e) => {
836
+ if (isFlyoutMode) {
837
+ clearFlyoutTimer();
838
+ triggerFlyout(entry, e.currentTarget);
839
+ }
840
+ },
841
+ onMouseLeave: () => {
842
+ if (isFlyoutMode) scheduleCloseFlyout();
843
+ },
844
+ children: [
845
+ react_jsx_runtime.jsx('span', { 'aria-hidden': true, 'data-menu-group-start': '', className: 'dsh-mg-groupStart' }),
846
+ react_jsx_runtime.jsx('div', {
847
+ 'data-menu-group-heading': '',
848
+ className: clsx('dsh-mg-groupHeading', 'dsh-mg-majorHeading', isFlyoutActive && 'dsh-mg-active-heading'),
849
+ children: react_jsx_runtime.jsxs('button', {
850
+ type: 'button', id: `${providerId}-heading`, 'data-group-toggle': entry.key,
851
+ 'aria-expanded': isFlyoutMode ? isFlyoutActive : entry.expanded,
852
+ 'aria-controls': providerId, disabled: busy || searching,
853
+ title: searching ? text('搜索时自动展开,清空搜索后恢复', 'Expanded during search; clearing restores your groups') : entry.id || entry.name,
854
+ onClick: (e) => {
855
+ if (isFlyoutMode) {
856
+ if (isFlyoutActive) setActiveFlyout(null);
857
+ else triggerFlyout(entry, e.currentTarget);
858
+ } else {
859
+ setHighlightedIndex(null);
860
+ groupPreferences.setExpanded([entry.key], !entry.expanded);
861
+ }
862
+ },
863
+ children: [
864
+ react_jsx_runtime.jsx(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutlineRegular, {
865
+ className: (isFlyoutMode ? isFlyoutActive : entry.expanded) ? 'dsh-mg-groupArrow dsh-mg-groupArrowOpen' : 'dsh-mg-groupArrow',
866
+ size: 14
867
+ }),
868
+ react_jsx_runtime.jsx('span', { className: 'dsh-mg-groupName', children: entry.name }),
869
+ entry.current && react_jsx_runtime.jsx('span', { className: 'dsh-mg-currentBadge', children: text('当前', 'Current') }),
870
+ react_jsx_runtime.jsx('span', { className: 'dsh-mg-groupCount', children: entry.modelCount }),
871
+ ],
872
+ }),
873
+ }),
874
+ !isFlyoutMode && react_jsx_runtime.jsx('div', {
875
+ id: providerId, hidden: !entry.expanded, className: entry.kind === 'multi' ? 'dsh-mg-majorContent' : undefined,
876
+ children: entry.expanded && (entry.kind !== 'multi' ? renderModels(entry.group) : entry.children.map((group, childIndex) => {
877
+ const keyId = `${providerId}-key-${childIndex}`;
878
+ const name = group.legacy ? text('原单密钥(兼容旧会话)', 'Legacy single key (existing session)') : group.name;
879
+ return react_jsx_runtime.jsxs('section', {
880
+ role: 'group', 'aria-labelledby': `${keyId}-heading`, 'data-key-entry': group.entryId ?? '@legacy',
881
+ children: [
882
+ renderHeading(group.key, name, group.expanded, group.models.length, group.current, keyId, false, `${entry.name} / ${name}`),
883
+ react_jsx_runtime.jsx('div', { id: keyId, hidden: !group.expanded, className: 'dsh-mg-minorContent', children: group.expanded && renderModels(group) }),
884
+ ],
885
+ }, group.key);
886
+ }))
887
+ }),
888
+ ],
889
+ }, entry.key);
890
+ });
891
+ return (0, react_jsx_runtime.jsxs)("div", {
892
+ ref: rootRef,
893
+ className: ModelSelect_module_css_default.root,
894
+ "data-model-groups-plugin": "0.3.0",
895
+ onKeyDown: onRootKeyDown,
896
+ onMouseDown: (event) => {
897
+ if (event.target instanceof Element && event.target.closest("[data-group-toggle], [data-group-bulk]") !== null) return;
898
+ if (event.target instanceof Element && event.target.closest("button") !== null) event.preventDefault();
899
+ },
900
+ children: [
901
+ (0, react_jsx_runtime.jsxs)("button", {
902
+ ref: triggerRef,
903
+ type: "button",
904
+ className: ModelSelect_module_css_default.trigger,
905
+ "aria-label": triggerAria,
906
+ "aria-haspopup": "menu",
907
+ "aria-expanded": open,
908
+ "aria-controls": open ? `${id}-menu` : void 0,
909
+ title: triggerLabel,
910
+ "aria-busy": busy,
911
+ "data-selection-focus": selectionFocus ? "" : void 0,
912
+ onBlur: () => {
913
+ setSelectionFocus(false);
914
+ },
915
+ disabled: locked,
916
+ onClick: () => {
917
+ if (open) close(true);
918
+ else show();
919
+ },
920
+ children: [
921
+ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconDataOutlineRegular, {
922
+ className: ModelSelect_module_css_default.triggerIcon,
923
+ size: 16
924
+ }),
925
+ (0, react_jsx_runtime.jsx)("span", {
926
+ className: ModelSelect_module_css_default.triggerLabel,
927
+ children: currentChoice === void 0 ? modelLabel : react_jsx_runtime.jsxs(react_jsx_runtime.Fragment, { children: [renderModelLabel(currentChoice.group.provider, currentChoice.model, "trigger", true), react_jsx_runtime.jsx('span', { className: 'dsh-mg-triggerSource', children: ` · ${currentSource}` })] })
928
+ }),
929
+ effortLabel !== void 0 && (0, react_jsx_runtime.jsx)("span", {
930
+ className: ModelSelect_module_css_default.triggerEffort,
931
+ children: effortLabel
932
+ }),
933
+ busy ? (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: "ongoing" }) : (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: clsx(ModelSelect_module_css_default.chevron, open && ModelSelect_module_css_default.chevronOpen) })
934
+ ]
935
+ }),
936
+ open && (0, react_dom.createPortal)((0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.MenuSurface, {
937
+ ref: menuRef,
938
+ id: `${id}-menu`,
939
+ className: ModelSelect_module_css_default.menu,
940
+ style: menuPos ?? MEASURE_STYLE,
941
+ role: pane === "model" ? "group" : "menu",
942
+ "aria-label": t("menu.aria"),
943
+ "aria-busy": state.status === "loading" || busy,
944
+ children: [
945
+ pane === "root" && (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsxs)("button", {
946
+ ref: itemRef(),
947
+ type: "button",
948
+ role: "menuitem",
949
+ className: ModelSelect_module_css_default.cell,
950
+ onClick: () => {
951
+ drill("model");
952
+ },
953
+ children: [
954
+ (0, react_jsx_runtime.jsx)("span", {
955
+ className: ModelSelect_module_css_default.cellLabel,
956
+ children: t("menu.model")
957
+ }),
958
+ (0, react_jsx_runtime.jsx)("span", {
959
+ className: ModelSelect_module_css_default.cellValue,
960
+ children: currentChoice === void 0 ? modelLabel : react_jsx_runtime.jsxs(react_jsx_runtime.Fragment, { children: [renderModelLabel(currentChoice.group.provider, currentChoice.model, "menu", true), ` · ${currentSource}`] })
961
+ }),
962
+ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutlineRegular, { className: ModelSelect_module_css_default.cellChevron })
963
+ ]
964
+ }), reasoning !== void 0 && (0, react_jsx_runtime.jsxs)("button", {
965
+ ref: itemRef(),
966
+ type: "button",
967
+ role: "menuitem",
968
+ className: ModelSelect_module_css_default.cell,
969
+ onClick: () => {
970
+ drill("effort");
971
+ },
972
+ children: [
973
+ (0, react_jsx_runtime.jsx)("span", {
974
+ className: ModelSelect_module_css_default.cellLabel,
975
+ children: t("menu.effort")
976
+ }),
977
+ (0, react_jsx_runtime.jsx)("span", {
978
+ className: ModelSelect_module_css_default.cellValue,
979
+ children: effortLabel
980
+ }),
981
+ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutlineRegular, { className: ModelSelect_module_css_default.cellChevron })
982
+ ]
983
+ })] }),
984
+ pane === "model" && (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
985
+ showSearch && (0, react_jsx_runtime.jsxs)("div", {
986
+ className: ModelSelect_module_css_default.searchRow,
987
+ children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
988
+ ref: searchRef,
989
+ className: clsx(ModelSelect_module_css_default.search, query !== "" && ModelSelect_module_css_default.searchWithQuery),
990
+ type: "text",
991
+ role: "searchbox",
992
+ "aria-label": t("search.placeholder"),
993
+ "aria-controls": `${id}-models`,
994
+ "aria-activedescendant": activeModelIndex < 0 ? void 0 : `${id}-model-${activeModelIndex}`,
995
+ placeholder: t("search.placeholder"),
996
+ value: query,
997
+ readOnly: busy,
998
+ onChange: (event) => {
999
+ changeQuery(event.target.value);
1000
+ }
1001
+ }), query !== "" && (0, react_jsx_runtime.jsx)("button", {
1002
+ type: "button",
1003
+ className: ModelSelect_module_css_default.searchClear,
1004
+ "aria-label": t("search.clear"),
1005
+ disabled: busy,
1006
+ onClick: () => {
1007
+ changeQuery("");
1008
+ searchRef.current?.focus();
1009
+ },
1010
+ children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseFillRegular, {})
1011
+ })]
1012
+ }),
1013
+ choices.length > 0 && react_jsx_runtime.jsxs("div", {
1014
+ className: "dsh-mg-toolbar",
1015
+ role: "group",
1016
+ "aria-label": text("模型分组", "Model groups"),
1017
+ children: [
1018
+ react_jsx_runtime.jsx("span", { className: "dsh-mg-summary", children: searching ? text("搜索中 · 展开匹配", "Search active") : text(`${providerCount} 供应商 · ${fullHierarchy.reduce((sum, entry) => sum + entry.modelCount, 0)} 模型`, `${providerCount} providers · ${fullHierarchy.reduce((sum, entry) => sum + entry.modelCount, 0)} models`) }),
1019
+ react_jsx_runtime.jsxs("div", {
1020
+ className: "dsh-mg-toolbar-actions",
1021
+ children: [true, false].map(expanded => react_jsx_runtime.jsx("button", {
1022
+ type: "button", "data-group-bulk": expanded ? "expand" : "collapse",
1023
+ disabled: busy || searching || expansionKeys.length === 0,
1024
+ onClick: () => { setHighlightedIndex(null); groupPreferences.setExpanded(expansionKeys, expanded); },
1025
+ children: expanded ? text("展开全部", "Expand") : text("收起全部", "Collapse"),
1026
+ }, String(expanded)))
1027
+ })
1028
+ ],
1029
+ }),
1030
+ state.status === "loading" && (0, react_jsx_runtime.jsx)("div", {
1031
+ className: ModelSelect_module_css_default.status,
1032
+ children: t("status.loading")
1033
+ }),
1034
+ state.error !== null && lastActionRef.current === "load" && (0, react_jsx_runtime.jsxs)("div", {
1035
+ className: ModelSelect_module_css_default.error,
1036
+ children: [(0, react_jsx_runtime.jsx)("span", { children: t("error.action", { message: state.error }) }), (0, react_jsx_runtime.jsx)("button", {
1037
+ type: "button",
1038
+ className: ModelSelect_module_css_default.retry,
1039
+ onClick: reload,
1040
+ children: t("retry")
1041
+ })]
1042
+ }),
1043
+ state.failures.map((failure) => (0, react_jsx_runtime.jsxs)("div", {
1044
+ className: ModelSelect_module_css_default.warning,
1045
+ children: [(0, react_jsx_runtime.jsx)("span", { children: t("warning.groupLoad", {
1046
+ name: failure.id === "deepseek-account" ? t("provider.account") : failure.name,
1047
+ message: failure.message
1048
+ }) }), (0, react_jsx_runtime.jsx)("button", {
1049
+ type: "button",
1050
+ className: ModelSelect_module_css_default.retry,
1051
+ onClick: reload,
1052
+ children: t("retry")
1053
+ })]
1054
+ }, failure.id)),
1055
+ (0, react_jsx_runtime.jsx)("div", {
1056
+ ref: groupsRef,
1057
+ id: `${id}-models`,
1058
+ className: clsx(ModelSelect_module_css_default.groups, "scrollable"),
1059
+ role: "menu",
1060
+ "aria-label": t("menu.model"),
1061
+ hidden: displayGroups.length === 0,
1062
+ children: renderHierarchy()
1063
+ }),
1064
+ state.status === "ready" && displayGroups.length === 0 && (0, react_jsx_runtime.jsx)("div", {
1065
+ className: ModelSelect_module_css_default.empty,
1066
+ role: "status",
1067
+ children: t(choices.length === 0 ? "empty.models" : "search.empty")
1068
+ }),
1069
+ !searching && choices.length > 0 && react_jsx_runtime.jsxs("div", {
1070
+ className: "dsh-mg-modebar",
1071
+ children: [
1072
+ react_jsx_runtime.jsx("span", { children: text("展示模式", "View") }),
1073
+ react_jsx_runtime.jsxs("div", {
1074
+ style: { display: "flex", gap: "6px" },
1075
+ children: [
1076
+ react_jsx_runtime.jsx("button", {
1077
+ type: "button",
1078
+ className: "dsh-mg-mode-btn",
1079
+ "data-active": menuMode === "panel",
1080
+ onClick: () => switchMode("panel"),
1081
+ children: text("侧边面板", "Flyout"),
1082
+ }),
1083
+ react_jsx_runtime.jsx("button", {
1084
+ type: "button",
1085
+ className: "dsh-mg-mode-btn",
1086
+ "data-active": menuMode === "inline",
1087
+ onClick: () => switchMode("inline"),
1088
+ children: text("原地折叠", "Inline"),
1089
+ }),
1090
+ ]
1091
+ })
1092
+ ]
1093
+ })
1094
+ ] }),
1095
+ pane === "effort" && (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [state.error !== null && lastActionRef.current === "load" && (0, react_jsx_runtime.jsxs)("div", {
1096
+ className: ModelSelect_module_css_default.error,
1097
+ children: [(0, react_jsx_runtime.jsx)("span", { children: t("error.action", { message: state.error }) }), (0, react_jsx_runtime.jsx)("button", {
1098
+ type: "button",
1099
+ className: ModelSelect_module_css_default.retry,
1100
+ onClick: reload,
1101
+ children: t("action.reload")
1102
+ })]
1103
+ }), effortChoices.length === 0 ? (0, react_jsx_runtime.jsx)("div", {
1104
+ className: ModelSelect_module_css_default.empty,
1105
+ children: t("empty.efforts")
1106
+ }) : effortChoices.map((level) => (0, react_jsx_runtime.jsxs)("button", {
1107
+ ref: itemRef(),
1108
+ type: "button",
1109
+ role: "menuitemradio",
1110
+ "aria-checked": effectiveEffort === level.effort,
1111
+ className: clsx(ModelSelect_module_css_default.option, effectiveEffort === level.effort && ModelSelect_module_css_default.selected),
1112
+ disabled: busy,
1113
+ onClick: () => {
1114
+ chooseEffort(level.effort);
1115
+ },
1116
+ children: [(0, react_jsx_runtime.jsx)("span", {
1117
+ className: ModelSelect_module_css_default.optionCopy,
1118
+ children: (0, react_jsx_runtime.jsx)("span", {
1119
+ className: ModelSelect_module_css_default.modelName,
1120
+ children: level.label
1121
+ })
1122
+ }), (0, react_jsx_runtime.jsx)("span", {
1123
+ className: ModelSelect_module_css_default.check,
1124
+ children: pending !== null && pending.provider === state.current?.provider && pending.model === state.current.model && pending.reasoningEffort === level.effort ? (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: "ongoing" }) : effectiveEffort === level.effort ? (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, {}) : null
1125
+ })]
1126
+ }, level.key))] })
1127
+ ]
1128
+ }), document.body),
1129
+ open && pane === "model" && menuMode === "panel" && activeFlyout !== null && (0, react_dom.createPortal)(
1130
+ react_jsx_runtime.jsx(_deepseek_ai_dsh_client_ui_primitives.MenuSurface, {
1131
+ ref: flyoutRef,
1132
+ className: ModelSelect_module_css_default.menu,
1133
+ style: {
1134
+ left: activeFlyout.pos.left,
1135
+ top: activeFlyout.pos.top,
1136
+ width: activeFlyout.pos.width,
1137
+ height: activeFlyout.pos.height,
1138
+ position: "fixed",
1139
+ margin: 0
1140
+ },
1141
+ onMouseEnter: clearFlyoutTimer,
1142
+ onMouseLeave: () => scheduleCloseFlyout(200),
1143
+ onMouseDown: (e) => {
1144
+ if (e.target instanceof Element && e.target.closest("button") !== null) e.preventDefault();
1145
+ },
1146
+ children: react_jsx_runtime.jsx("div", {
1147
+ className: clsx(ModelSelect_module_css_default.groups, "scrollable"),
1148
+ children: react_jsx_runtime.jsx(_deepseek_ai_dsh_client_ui_primitives.MenuGroup, {
1149
+ label: `${activeFlyout.entry.name} (${activeFlyout.entry.modelCount})`,
1150
+ children: activeFlyout.entry.kind !== "multi" ? renderModels(activeFlyout.entry.group) : activeFlyout.entry.children.map((childGroup) => {
1151
+ const name = childGroup.legacy ? text("原单密钥(兼容旧会话)", "Legacy single key (existing session)") : childGroup.name;
1152
+ const isSubActive = activeSubFlyout?.key === childGroup.key;
1153
+ return react_jsx_runtime.jsxs("button", {
1154
+ type: "button",
1155
+ className: clsx(ModelSelect_module_css_default.option, isSubActive && ModelSelect_module_css_default.optionActive),
1156
+ "data-active": isSubActive ? "true" : "false",
1157
+ onMouseEnter: (e) => {
1158
+ clearFlyoutTimer();
1159
+ triggerSubFlyout(childGroup, e.currentTarget);
1160
+ },
1161
+ onMouseLeave: () => scheduleCloseSubFlyout(200),
1162
+ onClick: (e) => {
1163
+ if (isSubActive) setActiveSubFlyout(null);
1164
+ else triggerSubFlyout(childGroup, e.currentTarget);
1165
+ },
1166
+ children: [
1167
+ react_jsx_runtime.jsx("span", { className: ModelSelect_module_css_default.modelName, children: name }),
1168
+ childGroup.current && react_jsx_runtime.jsx("span", { className: "dsh-mg-currentBadge", children: text("当前", "Current") }),
1169
+ react_jsx_runtime.jsx("span", { className: "dsh-mg-groupCount", children: childGroup.models.length }),
1170
+ react_jsx_runtime.jsx(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutlineRegular, { className: ModelSelect_module_css_default.cellChevron, size: 12 }),
1171
+ ]
1172
+ }, childGroup.key);
1173
+ })
1174
+ })
1175
+ })
1176
+ }),
1177
+ document.body
1178
+ ),
1179
+ open && pane === "model" && menuMode === "panel" && activeSubFlyout !== null && (0, react_dom.createPortal)(
1180
+ react_jsx_runtime.jsx(_deepseek_ai_dsh_client_ui_primitives.MenuSurface, {
1181
+ ref: subFlyoutRef,
1182
+ className: ModelSelect_module_css_default.menu,
1183
+ style: {
1184
+ left: activeSubFlyout.pos.left,
1185
+ top: activeSubFlyout.pos.top,
1186
+ width: activeSubFlyout.pos.width,
1187
+ height: activeSubFlyout.pos.height,
1188
+ position: "fixed",
1189
+ margin: 0
1190
+ },
1191
+ onMouseEnter: clearFlyoutTimer,
1192
+ onMouseLeave: () => scheduleCloseSubFlyout(200),
1193
+ onMouseDown: (e) => {
1194
+ if (e.target instanceof Element && e.target.closest("button") !== null) e.preventDefault();
1195
+ },
1196
+ children: react_jsx_runtime.jsx("div", {
1197
+ className: clsx(ModelSelect_module_css_default.groups, "scrollable"),
1198
+ children: react_jsx_runtime.jsx(_deepseek_ai_dsh_client_ui_primitives.MenuGroup, {
1199
+ label: `${activeSubFlyout.childGroup.legacy ? text("原单密钥(兼容旧会话)", "Legacy single key (existing session)") : activeSubFlyout.childGroup.name} (${activeSubFlyout.childGroup.models.length})`,
1200
+ children: renderModels(activeSubFlyout.childGroup)
1201
+ })
1202
+ })
1203
+ }),
1204
+ document.body
1205
+ ),
1206
+ toast !== null && (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Toast, {
1207
+ text: toast.text,
1208
+ icon: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconWarningOutlineRegular, {}),
1209
+ anchor: rootRef.current?.closest("[data-composer-card]") ?? null,
1210
+ onDone: () => {
1211
+ setToast(null);
1212
+ }
1213
+ }, toast.seq)
1214
+ ]
1215
+ });
1216
+ }
1217
+
1218
+ // Degraded but usable selector: shown only if the grouped picker itself throws.
1219
+ // Deliberately primitive-free and flat so it cannot fail the same way.
1220
+ function FallbackModelSelect({ available, directory, select, t, providersForm }) {
1221
+ const state = (0, react.useSyncExternalStore)((fn) => directory.subscribe(fn), () => directory.getSnapshot());
1222
+ const configuration = react.useSyncExternalStore(fn => providersForm?.subscribe(fn) ?? (() => {}), () => providersForm?.getSnapshot() ?? EMPTY_PROVIDERS_SNAPSHOT);
1223
+ const providers = configuration.status === 'ready' ? (configuration.value?.providers ?? {}) : undefined;
1224
+ const [open, setOpen] = (0, react.useState)(false);
1225
+ if (!available) return null;
1226
+ const label = typeof t === 'function' ? t('menu.model') : '模型';
1227
+ const current = state.current ?? null;
1228
+ const models = (state.groups ?? []).flatMap(group => {
1229
+ const profile = Object.hasOwn(providers ?? {}, group.id) ? providers[group.id] : undefined;
1230
+ const multi = profile?.multiKey === true || (providers === undefined && group.models.some(model => decodeKeyModelId(model.id)));
1231
+ return group.models.flatMap(model => {
1232
+ const key = decodeKeyModelId(model.id);
1233
+ if (key) {
1234
+ if (!multi || (providers !== undefined && !Object.hasOwn(profile?.keyEntries ?? {}, key.entryId))) return [];
1235
+ } else if (model.id.startsWith(KEY_MODEL_PREFIX) || (multi && (current?.provider !== group.id || current?.model !== model.id))) return [];
1236
+ const providerName = group.name ?? group.id;
1237
+ return [{ provider: group.id, providerName: key ? `${providerName} / ${keyEntryName(providers, group.id, key.entryId)}` : providerName, model }];
1238
+ });
1239
+ });
1240
+ const active = models.find((entry) => entry.provider === current?.provider && entry.model.id === current?.model);
1241
+ const choose = (entry) => {
1242
+ setOpen(false);
1243
+ select({
1244
+ provider: entry.provider,
1245
+ model: entry.model.id,
1246
+ ...(entry.model.reasoning?.defaultEffort === void 0 ? {} : { reasoningEffort: entry.model.reasoning.defaultEffort }),
1247
+ });
1248
+ };
1249
+ return react.createElement('div', { className: 'dsh-mg-root' },
1250
+ react.createElement('button', {
1251
+ type: 'button',
1252
+ className: 'dsh-mg-trigger',
1253
+ 'aria-haspopup': 'menu',
1254
+ 'aria-expanded': open,
1255
+ onClick: () => setOpen(!open),
1256
+ }, react.createElement('span', { className: 'dsh-mg-triggerLabel' }, active?.model.name ?? current?.model ?? label)),
1257
+ open ? react.createElement('div', { className: 'dsh-mg-menu', role: 'menu', style: { left: 12, bottom: 60, top: 'auto' } },
1258
+ models.map((entry) => react.createElement('button', {
1259
+ key: entry.provider + '/' + entry.model.id,
1260
+ type: 'button',
1261
+ role: 'menuitemradio',
1262
+ 'aria-checked': entry === active,
1263
+ className: 'dsh-mg-row',
1264
+ onClick: () => choose(entry),
1265
+ }, entry.model.name ?? entry.model.id, react.createElement('span', { className: 'dsh-mg-rowHint' }, entry.providerName)))) : null);
1266
+ }
1267
+
1268
+ // Last line of defence: a broken grouped picker must never blank the model seat.
1269
+ class ModelSeatBoundary extends react.Component {
1270
+ static getDerivedStateFromError() {
1271
+ return { failed: true };
1272
+ }
1273
+ constructor(props) {
1274
+ super(props);
1275
+ this.state = { failed: false };
1276
+ }
1277
+ componentDidCatch(error) {
1278
+ try {
1279
+ console.error('[dsh-model-groups] grouped picker failed, falling back', error);
1280
+ } catch {}
1281
+ }
1282
+ render() {
1283
+ if (this.state.failed) return react.createElement(FallbackModelSelect, this.props);
1284
+ return react.createElement(ModelSelect, this.props);
1285
+ }
1286
+ }
1287
+
1288
+
1289
+ const MODEL_GROUPS_CSS = "/* DSH Model Groups: 100% native token-compatible styling */\n\n.dsh-mg-toolbar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 4px;\n flex-shrink: 0;\n margin: 2px 0 4px;\n padding: 0 4px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.dsh-mg-summary {\n flex: 1;\n min-width: 0;\n padding: 0 2px;\n color: var(--dsw-alias-label-tertiary);\n font-size: 11px;\n line-height: 16px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.dsh-mg-toolbar-actions {\n display: flex;\n align-items: center;\n gap: 4px;\n flex: none;\n}\n\n.dsh-mg-toolbar button {\n flex: none;\n height: 22px;\n padding: 0 6px;\n border: 0;\n border-radius: var(--dsw-radius-sm);\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font: inherit;\n font-size: 11px;\n line-height: 22px;\n cursor: pointer;\n transition: background .12s, color .12s;\n}\n\n.dsh-mg-toolbar button:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-primary);\n}\n\n.dsh-mg-toolbar button:disabled {\n color: var(--dsw-alias-label-dimmed);\n cursor: default;\n}\n\n.dsh-mg-groupStart {\n display: block;\n height: 0;\n pointer-events: none;\n}\n\n.dsh-mg-groupHeading {\n border-radius: var(--dsw-radius-md);\n margin: 1px 0;\n}\n\n.dsh-mg-majorHeading {\n position: static;\n}\n\n.dsh-mg-minorHeading {\n position: relative;\n}\n\n.dsh-mg-majorContent {\n margin-left: 6px;\n padding-left: 6px;\n border-left: 1px solid var(--dsw-alias-border-l1);\n}\n\n.dsh-mg-minorContent {\n margin-left: 6px;\n padding-left: 6px;\n border-left: 1px solid var(--dsw-alias-border-l1);\n}\n\n.dsh-mg-source {\n display: block;\n color: var(--dsw-alias-label-tertiary);\n font-size: 11px;\n line-height: 16px;\n}\n\n.dsh-mg-triggerSource {\n color: var(--dsw-alias-label-caption);\n font-size: 13px;\n}\n\n.dsh-mg-groupHeading button {\n box-sizing: border-box;\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n min-height: 32px;\n padding: 4px 7px;\n border: 0;\n border-radius: var(--dsw-radius-md);\n background: transparent;\n color: var(--dsw-alias-label-primary);\n font: inherit;\n font-size: 13px;\n font-weight: 500;\n line-height: 18px;\n text-align: left;\n cursor: pointer;\n transition: background .12s;\n}\n\n.dsh-mg-groupHeading button:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dsh-mg-groupHeading button:disabled {\n cursor: default;\n}\n\n.dsh-mg-groupHeading button:focus-visible,\n.dsh-mg-toolbar button:focus-visible {\n outline: none;\n box-shadow: 0 0 0 2px var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));\n}\n\n.dsh-mg-groupArrow {\n flex: none;\n width: 14px;\n height: 14px;\n color: var(--dsw-alias-label-caption);\n transition: transform .12s;\n}\n\n.dsh-mg-groupArrowOpen {\n transform: rotate(90deg);\n}\n\n.dsh-mg-groupName {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.dsh-mg-groupCount {\n flex: none;\n color: var(--dsw-alias-label-tertiary);\n font-size: 11px;\n font-weight: 400;\n line-height: 16px;\n}\n\n.dsh-mg-currentBadge {\n flex: none;\n color: var(--dsw-alias-state-business-primary);\n font-size: 11px;\n font-weight: 500;\n line-height: 16px;\n}\n\n.dsh-mg-groups {\n min-height: 0;\n}\n\n.dsh-mg-groups [hidden] {\n display: none !important;\n}\n\n/* ── 飞出卡片:与官方原版菜单 100% 结构一致,剔除 header 伪分层与白色叠加 ── */\n.dsh-mg-flyout-subgroup-list {\n display: flex;\n flex-direction: column;\n}\n\n.dsh-mg-subgroup-item {\n box-sizing: border-box;\n border-radius: var(--dsw-radius-md);\n width: auto;\n min-width: 100%;\n min-height: 34px;\n color: var(--dsw-alias-label-primary);\n text-align: left;\n cursor: pointer;\n background: 0 0;\n border: none;\n outline: none;\n align-items: center;\n gap: 6px;\n padding: 5px 7px;\n display: flex;\n font: inherit;\n font-size: 13px;\n line-height: 18px;\n transition: background .12s;\n}\n\n.dsh-mg-subgroup-item:hover,\n.dsh-mg-subgroup-item[data-active=\"true\"] {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dsh-mg-subgroup-name {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.dsh-mg-subgroup-count {\n flex: none;\n font-size: 11px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.dsh-mg-subgroup-arrow {\n flex: none;\n color: var(--dsw-alias-menu-icon, var(--dsw-alias-label-caption));\n}\n\n.dsh-mg-active-heading {\n background: var(--dsw-alias-interactive-bg-hover) !important;\n}\n\n/* ── 底部模式切换:原生轻量化,无突兀高饱和蓝块 ── */\n.dsh-mg-modebar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 4px 6px 2px;\n margin-top: 3px;\n border-top: 1px solid var(--dsw-alias-border-l1);\n font-size: 11px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.dsh-mg-mode-btn {\n border: 0;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font: inherit;\n font-size: 11px;\n padding: 2px 6px;\n border-radius: var(--dsw-radius-sm);\n cursor: pointer;\n transition: background .12s, color .12s;\n}\n\n.dsh-mg-mode-btn:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-primary);\n}\n\n.dsh-mg-mode-btn[data-active=\"true\"] {\n background: var(--dsw-alias-interactive-bg-active, rgba(255, 255, 255, 0.1));\n color: var(--dsw-alias-label-primary);\n font-weight: 500;\n}\n\n@media (pointer: coarse) {\n .dsh-mg-groupHeading button { min-height: 36px; }\n .dsh-mg-toolbar button { height: 26px; }\n .dsh-mg-mode-btn { min-height: 24px; padding: 2px 8px; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .dsh-mg-groupArrow { transition: none; }\n}\n\n";
1290
+ function installModelGroupsStyles() {
1291
+ const tagId = 'dsh-model-groups/groups.css';
1292
+ if (document.querySelector('style[data-plugin-css="' + tagId + '"]') !== null) return;
1293
+ const style = document.createElement('style');
1294
+ style.dataset.plugin = 'dsh-model-groups';
1295
+ style.dataset.pluginCss = tagId;
1296
+ style.textContent = MODEL_GROUPS_CSS;
1297
+ document.head.appendChild(style);
1298
+ }
1299
+
1300
+ const groupPreferences = createGroupPreferences();
1301
+ const EMPTY_PROVIDERS_SNAPSHOT = Object.freeze({ status: 'unavailable', value: {} });
1302
+
1303
+ const inject = ['slots', 'modelDirectories', 'sessions', 'locale', 'remote', 'remote.session', 'remote.llm', 'configForms'];
1304
+ function apply(ctx) {
1305
+ installModelSelectStyles();
1306
+ installModelGroupsStyles();
1307
+ const providersForm = ctx.configForms?.get('llm-pi-ai');
1308
+ const onStorage = event => {
1309
+ if (event.key === STORAGE_KEY || event.key === null) groupPreferences.sync(event.key === null ? null : event.newValue);
1310
+ };
1311
+ window.addEventListener('storage', onStorage);
1312
+ ctx.on('dispose', () => {
1313
+ window.removeEventListener('storage', event => onStorage(event));
1314
+ document.querySelectorAll('style[data-plugin="dsh-model-groups"]').forEach(style => style.remove());
1315
+ });
1316
+ return ctx.slots.inject('conversation.input.model', () => ctx.slots.register({
1317
+ name: 'conversation.input.model',
1318
+ priority: -10,
1319
+ locale: 'model',
1320
+ inject(sessionId) {
1321
+ const directory = ctx.modelDirectories.directoryFor(sessionId);
1322
+ const available = ctx.sessions.subagentAddress(sessionId) === undefined;
1323
+ return {
1324
+ available,
1325
+ directory: directory.store,
1326
+ providersForm,
1327
+ load() { if (available) directory.load().catch(() => {}); },
1328
+ select: selection => available ? directory.select(selection) : Promise.resolve(undefined),
1329
+ };
1330
+ },
1331
+ }, ModelSeatBoundary));
1332
+ }
1333
+
1334
+ return { apply, inject };
1335
+ },
1336
+ });