@klarkxy/dsh-model-picker 0.0.0-stage → 0.1.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.
@@ -0,0 +1,1720 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let react = require("react");
3
+ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
4
+ let react_dom = require("react-dom");
5
+ let react_jsx_runtime = require("react/jsx-runtime");
6
+ //#region ../dsh-model-route/src/menu.ts
7
+ /** Localize standard effort ids only when the provider has not supplied a display name. */
8
+ function modelMenuEffortLabel(effort, locale) {
9
+ if (effort.name !== effort.id) return effort.name;
10
+ return {
11
+ off: ["关闭", "Off"],
12
+ none: ["无", "None"],
13
+ minimal: ["最低", "Minimal"],
14
+ low: ["低", "Low"],
15
+ medium: ["中", "Medium"],
16
+ high: ["高", "High"],
17
+ xhigh: ["极高", "Extra high"],
18
+ max: ["最高", "Maximum"],
19
+ ultra: ["超高", "Ultra"]
20
+ }[effort.id]?.[locale?.toLowerCase().startsWith("zh") ? 0 : 1] ?? effort.name;
21
+ }
22
+ /** The short model label shown inside a provider group. */
23
+ function modelMenuShortName(choice) {
24
+ const sep = choice.label.indexOf(" / ");
25
+ return sep > 0 ? choice.label.slice(sep + 3) : choice.model;
26
+ }
27
+ /** Group catalog choices by provider and filter them by a case-insensitive query. */
28
+ function groupModelMenuChoices(choices, query) {
29
+ const needle = query.trim().toLowerCase();
30
+ const matched = needle ? choices.filter((choice) => choice.label.toLowerCase().includes(needle) || choice.model.toLowerCase().includes(needle) || choice.provider.toLowerCase().includes(needle) || (choice.providerLabel?.toLowerCase().includes(needle) ?? false)) : choices;
31
+ const groups = [];
32
+ for (const choice of matched) {
33
+ const sep = choice.label.indexOf(" / ");
34
+ const name = choice.providerLabel ?? (sep > 0 ? choice.label.slice(0, sep) : choice.provider);
35
+ const last = groups.at(-1);
36
+ if (last && last.provider === choice.provider) last.items.push(choice);
37
+ else groups.push({
38
+ provider: choice.provider,
39
+ name,
40
+ items: [choice]
41
+ });
42
+ }
43
+ return groups;
44
+ }
45
+ //#endregion
46
+ //#region ../dsh-model-route/src/ui.tsx
47
+ /**
48
+ * ModelMenu: the standard editor of one model-route field — the visible half
49
+ * of the x-model-route contract. A feature plugin's settings page renders it
50
+ * for its own marked field; the model hub renders one per detected field.
51
+ *
52
+ * It is a plugin-side copy of the host's composer model picker
53
+ * (dsh-client-ui-model-selection's ModelSelect), rebuilt from the official
54
+ * primitives and the published bundle's behavior: one card that opens on the
55
+ * root pane (Model / Effort cells), drills into the provider-grouped,
56
+ * searchable model list or the effort list, and Escapes back before closing.
57
+ * Sticky group headings, subsequence ranking, highlight walking and focus
58
+ * hand-back all match the host. SYNC POINT: when the host's ModelSelect or
59
+ * ModelSelect.module.css changes, this copy must be re-transcribed; styles
60
+ * below may only reference allowlisted theme tokens (the --dsw-* set plus the
61
+ * host's --ds-* motion tokens), never literal colors.
62
+ *
63
+ * It statically imports the primitives, so only browser clients should load
64
+ * this entry; host code and node tests use the core entry (`./index.ts`).
65
+ */
66
+ /** Unplaced portal card: hidden but laid out at a fixed origin so measurements are real. */
67
+ const MEASURE_STYLE = {
68
+ visibility: "hidden",
69
+ left: 0,
70
+ top: 0
71
+ };
72
+ function ModelMenu(props) {
73
+ const language = props.locale?.toLowerCase().startsWith("zh") ? "zh" : "en";
74
+ const copy = language === "zh" ? {
75
+ model: "模型",
76
+ effort: "思考强度",
77
+ default: "默认",
78
+ search: "搜索模型…",
79
+ empty: "没有匹配的模型",
80
+ clear: "清除搜索"
81
+ } : {
82
+ model: "Model",
83
+ effort: "Reasoning",
84
+ default: "Default",
85
+ search: "Search models…",
86
+ empty: "No matching models",
87
+ clear: "Clear search"
88
+ };
89
+ const { open, onOpenChange, anchor, choices, selected, onPick, leading = [], onPickLeading, efforts = [], selectedEffort, onPickEffort, modelLabel = copy.model, modelValue = "—", effortLabel = copy.effort, effortValue, defaultEffortLabel = copy.default, searchPlaceholder = copy.search, emptyLabel = copy.empty, clearSearchLabel = copy.clear, initialPane = "root", alwaysShowSearch = false, reasoningSlider = false, menuHeader, disabled = false, busy = false, closeOnPick = true, closeOnDisable = true, className, listClassName } = props;
90
+ const rootRef = (0, react.useRef)(null);
91
+ const openRef = (0, react.useRef)(open);
92
+ openRef.current = open;
93
+ const pendingOpenRef = (0, react.useRef)(false);
94
+ const pendingRestoreFocus = (0, react.useRef)(false);
95
+ const [requestedOpen, setRequestedOpen] = (0, react.useState)(false);
96
+ const menuRef = (0, react.useRef)(null);
97
+ const groupsRef = (0, react.useRef)(null);
98
+ const searchRef = (0, react.useRef)(null);
99
+ const itemRefs = (0, react.useRef)([]);
100
+ const paneFocus = (0, react.useRef)(null);
101
+ const [pane, setPane] = (0, react.useState)("root");
102
+ const [query, setQuery] = (0, react.useState)("");
103
+ const [highlightedIndex, setHighlightedIndex] = (0, react.useState)(null);
104
+ const [keyNav, setKeyNav] = (0, react.useState)(0);
105
+ const [menuPos, setMenuPos] = (0, react.useState)(null);
106
+ const id = (0, react.useId)();
107
+ const showSearch = alwaysShowSearch || choices.length > 4;
108
+ const effortRows = (0, react.useMemo)(() => [{
109
+ id: "",
110
+ name: defaultEffortLabel
111
+ }, ...efforts.map((effort) => ({
112
+ ...effort,
113
+ name: modelMenuEffortLabel(effort, language)
114
+ }))], [
115
+ defaultEffortLabel,
116
+ efforts,
117
+ language
118
+ ]);
119
+ const filteredGroups = (0, react.useMemo)(() => groupModelMenuChoices(choices, "").map((group) => ({
120
+ ...group,
121
+ items: (0, _deepseek_ai_dsh_client_ui_primitives.rankByName)(group.items.map((choice) => ({
122
+ choice,
123
+ name: modelMenuShortName(choice),
124
+ label: [
125
+ choice.provider,
126
+ choice.providerLabel,
127
+ choice.model,
128
+ choice.label
129
+ ].filter(Boolean).join(" ")
130
+ })), showSearch ? query.trim() : "")
131
+ })).filter((group) => group.items.length > 0), [
132
+ choices,
133
+ query,
134
+ showSearch
135
+ ]);
136
+ const visibleModels = (0, react.useMemo)(() => filteredGroups.flatMap((group) => group.items.map((item) => item.choice)), [filteredGroups]);
137
+ const currentVisibleIndex = visibleModels.findIndex((choice) => choice.provider === selected?.provider && choice.model === selected.model);
138
+ const activeModelIndex = Math.min(highlightedIndex ?? Math.max(0, currentVisibleIndex), visibleModels.length - 1);
139
+ const changeOpen = (next) => {
140
+ if (openRef.current === next) return;
141
+ openRef.current = next;
142
+ onOpenChange(next);
143
+ };
144
+ const requestOpen = () => {
145
+ if (open || pendingOpenRef.current) return;
146
+ pendingOpenRef.current = true;
147
+ pendingRestoreFocus.current = false;
148
+ setRequestedOpen(true);
149
+ };
150
+ const close = (restoreFocus = false) => {
151
+ const active = document.activeElement;
152
+ pendingRestoreFocus.current = restoreFocus && (active === document.body || rootRef.current?.contains(active) === true || menuRef.current?.contains(active) === true);
153
+ changeOpen(false);
154
+ };
155
+ const drill = (next) => {
156
+ setQuery("");
157
+ setHighlightedIndex(null);
158
+ paneFocus.current = "drill";
159
+ setPane(next);
160
+ };
161
+ /** Leave a drilled pane for the entry pane, handing the keyboard back to it.
162
+ * A root entry refocuses the cell that opened the pane; a search-first
163
+ * (model) entry refocuses that pane's own intent. */
164
+ const back = (from) => {
165
+ paneFocus.current = initialPane === "root" ? from : "drill";
166
+ setPane(initialPane);
167
+ };
168
+ const moveFocus = (offset) => {
169
+ const items = itemRefs.current.filter((item) => item !== null && !item.disabled);
170
+ if (!items.length) return;
171
+ const active = items.findIndex((item) => item === document.activeElement);
172
+ items[active === -1 ? offset > 0 ? 0 : items.length - 1 : (active + offset + items.length) % items.length]?.focus();
173
+ };
174
+ (0, react.useLayoutEffect)(() => {
175
+ if (open || !pendingRestoreFocus.current) return;
176
+ const active = document.activeElement;
177
+ if (active !== document.body && !rootRef.current?.contains(active)) {
178
+ pendingRestoreFocus.current = false;
179
+ return;
180
+ }
181
+ const trigger = rootRef.current?.querySelector("button");
182
+ if (!trigger || trigger.disabled) return;
183
+ trigger.focus();
184
+ if (document.activeElement === trigger) pendingRestoreFocus.current = false;
185
+ });
186
+ (0, react.useEffect)(() => {
187
+ if (open || !pendingRestoreFocus.current) return;
188
+ const cancelOutside = (event) => {
189
+ if (event.target instanceof Node && !rootRef.current?.contains(event.target)) pendingRestoreFocus.current = false;
190
+ };
191
+ document.addEventListener("focusin", cancelOutside);
192
+ document.addEventListener("pointerdown", cancelOutside);
193
+ return () => {
194
+ document.removeEventListener("focusin", cancelOutside);
195
+ document.removeEventListener("pointerdown", cancelOutside);
196
+ };
197
+ });
198
+ (0, react.useEffect)(() => {
199
+ if (!requestedOpen) return;
200
+ pendingOpenRef.current = false;
201
+ setRequestedOpen(false);
202
+ if (!open) changeOpen(true);
203
+ }, [requestedOpen, open]);
204
+ (0, react.useEffect)(() => {
205
+ if (!open) return;
206
+ setPane(initialPane);
207
+ if (initialPane === "model") paneFocus.current = "drill";
208
+ setQuery("");
209
+ setHighlightedIndex(null);
210
+ }, [open, initialPane]);
211
+ (0, react.useEffect)(() => {
212
+ if (disabled && open && closeOnDisable) changeOpen(false);
213
+ }, [
214
+ disabled,
215
+ open,
216
+ closeOnDisable
217
+ ]);
218
+ (0, react.useEffect)(() => {
219
+ if (!open) return;
220
+ const closeOutside = (event) => {
221
+ const target = event.target;
222
+ if (rootRef.current?.contains(target) || menuRef.current?.contains(target)) return;
223
+ changeOpen(false);
224
+ };
225
+ const escapeOutside = (event) => {
226
+ if (event.key !== "Escape" || event.defaultPrevented || event.isComposing) return;
227
+ if (!openRef.current) return;
228
+ event.preventDefault();
229
+ if (pane !== initialPane) back(pane);
230
+ else close(true);
231
+ };
232
+ document.addEventListener("mousedown", closeOutside);
233
+ document.addEventListener("keydown", escapeOutside);
234
+ return () => {
235
+ document.removeEventListener("mousedown", closeOutside);
236
+ document.removeEventListener("keydown", escapeOutside);
237
+ };
238
+ }, [
239
+ open,
240
+ onOpenChange,
241
+ pane,
242
+ initialPane
243
+ ]);
244
+ (0, react.useEffect)(() => {
245
+ const intent = paneFocus.current;
246
+ if (!open || menuPos === null || intent === null) return;
247
+ if (intent !== "drill" && pane !== "root") return;
248
+ paneFocus.current = null;
249
+ if (intent === "drill") {
250
+ if (pane === "model" && showSearch) {
251
+ searchRef.current?.focus();
252
+ return;
253
+ }
254
+ (menuRef.current?.querySelector("[role=\"menuitemradio\"][aria-checked=\"true\"]:not([disabled])") ?? itemRefs.current.find((item) => item !== null && !item.disabled))?.focus();
255
+ return;
256
+ }
257
+ itemRefs.current[intent === "effort" ? 1 : 0]?.focus();
258
+ }, [
259
+ open,
260
+ pane,
261
+ showSearch,
262
+ menuPos
263
+ ]);
264
+ (0, react.useEffect)(() => {
265
+ const viewport = groupsRef.current;
266
+ if (viewport === null) return;
267
+ return (0, _deepseek_ai_dsh_client_ui_primitives.observeStickyMenuGroups)(viewport);
268
+ }, [
269
+ open,
270
+ pane,
271
+ filteredGroups
272
+ ]);
273
+ const modelPaneOpen = open && pane === "model";
274
+ const scrollSignal = !modelPaneOpen ? "closed" : keyNav === 0 ? "open" : `key:${keyNav}`;
275
+ const scrollSignalRef = (0, react.useRef)(null);
276
+ (0, react.useLayoutEffect)(() => {
277
+ if (!modelPaneOpen || activeModelIndex < 0) {
278
+ if (!modelPaneOpen) scrollSignalRef.current = null;
279
+ return;
280
+ }
281
+ if (scrollSignalRef.current === scrollSignal) return;
282
+ scrollSignalRef.current = scrollSignal;
283
+ itemRefs.current[leading.length + activeModelIndex]?.scrollIntoView({ block: "nearest" });
284
+ }, [
285
+ modelPaneOpen,
286
+ scrollSignal,
287
+ activeModelIndex,
288
+ leading.length
289
+ ]);
290
+ (0, react.useLayoutEffect)(() => {
291
+ if (!open) {
292
+ setMenuPos(null);
293
+ return;
294
+ }
295
+ const place = () => {
296
+ const rect = rootRef.current?.getBoundingClientRect();
297
+ if (!rect) return;
298
+ const margin = 12, lw = menuRef.current?.offsetWidth ?? 0, lh = menuRef.current?.offsetHeight ?? 0;
299
+ let x = rect.right - lw, y = rect.top - 8 - lh;
300
+ if (lw > 0) x = Math.min(Math.max(x, margin), window.innerWidth - lw - margin);
301
+ if (lh > 0) y = Math.min(Math.max(y, margin), window.innerHeight - lh - margin);
302
+ setMenuPos((prev) => prev && prev.left === x && prev.top === y ? prev : {
303
+ left: x,
304
+ top: y
305
+ });
306
+ };
307
+ place();
308
+ window.addEventListener("scroll", place, true);
309
+ window.addEventListener("resize", place);
310
+ return () => {
311
+ window.removeEventListener("scroll", place, true);
312
+ window.removeEventListener("resize", place);
313
+ };
314
+ }, [
315
+ open,
316
+ pane,
317
+ choices,
318
+ query
319
+ ]);
320
+ function onKeyDown(event) {
321
+ if (event.defaultPrevented || event.nativeEvent.isComposing) return;
322
+ if (event.key === "Escape" && open) {
323
+ event.preventDefault();
324
+ if (pane !== initialPane) back(pane);
325
+ else close(true);
326
+ return;
327
+ }
328
+ if (disabled) return;
329
+ if (!open && [
330
+ "Enter",
331
+ " ",
332
+ "ArrowDown",
333
+ "ArrowUp"
334
+ ].includes(event.key)) {
335
+ const button = event.target instanceof Element ? event.target.closest("button") : null;
336
+ if (!(button instanceof HTMLButtonElement) || button.disabled || !rootRef.current?.contains(button)) return;
337
+ event.preventDefault();
338
+ paneFocus.current = "model";
339
+ requestOpen();
340
+ return;
341
+ }
342
+ if (!open) return;
343
+ if (pane === "model" && showSearch && (event.key === "ArrowDown" || event.key === "ArrowUp")) {
344
+ event.preventDefault();
345
+ if (visibleModels.length > 0) {
346
+ setHighlightedIndex((activeModelIndex + (event.key === "ArrowDown" ? 1 : -1) + visibleModels.length) % visibleModels.length);
347
+ setKeyNav((n) => n + 1);
348
+ searchRef.current?.focus();
349
+ }
350
+ return;
351
+ }
352
+ if (pane === "model" && showSearch && event.target instanceof HTMLInputElement && (event.key === "Enter" || event.key === "Tab" && !event.shiftKey)) {
353
+ if (event.key === "Tab" && visibleModels.length === 0) return;
354
+ event.preventDefault();
355
+ const highlighted = visibleModels[activeModelIndex];
356
+ if (highlighted && !busy) {
357
+ onPick({
358
+ provider: highlighted.provider,
359
+ model: highlighted.model
360
+ });
361
+ if (closeOnPick) close(true);
362
+ }
363
+ return;
364
+ }
365
+ if (event.key === "Tab") {
366
+ if (event.shiftKey) {
367
+ event.preventDefault();
368
+ if (pane !== initialPane) back(pane);
369
+ else close(true);
370
+ return;
371
+ }
372
+ const focused = document.activeElement;
373
+ const rows = itemRefs.current.filter((item) => item !== null);
374
+ if (focused instanceof HTMLButtonElement && rows.includes(focused)) {
375
+ event.preventDefault();
376
+ focused.click();
377
+ return;
378
+ }
379
+ if (pane === "model" && showSearch) {
380
+ event.preventDefault();
381
+ setHighlightedIndex(null);
382
+ searchRef.current?.focus();
383
+ }
384
+ return;
385
+ }
386
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
387
+ event.preventDefault();
388
+ moveFocus(event.key === "ArrowDown" ? 1 : -1);
389
+ }
390
+ }
391
+ function onBlur(event) {
392
+ const related = event.relatedTarget;
393
+ if (related instanceof Node && (rootRef.current?.contains(related) || menuRef.current?.contains(related) || !related.isConnected)) return;
394
+ if (!closeOnPick && !(related instanceof Node)) return;
395
+ if (busy && related == null) return;
396
+ if (open) changeOpen(false);
397
+ }
398
+ itemRefs.current = [];
399
+ let itemIndex = 0;
400
+ let modelIndex = 0;
401
+ const itemRef = () => {
402
+ const at = itemIndex++;
403
+ return (node) => {
404
+ itemRefs.current[at] = node;
405
+ };
406
+ };
407
+ const menu = open && typeof document !== "undefined" ? (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.MenuSurface, {
408
+ ref: menuRef,
409
+ id: `${id}-menu`,
410
+ className: `dsh-model-menu-menu${listClassName ? ` ${listClassName}` : ""}`,
411
+ style: menuPos ?? MEASURE_STYLE,
412
+ role: pane === "model" ? "group" : "menu",
413
+ "aria-label": modelLabel,
414
+ lang: language,
415
+ children: [
416
+ menuHeader ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
417
+ className: "dsh-model-menu-header",
418
+ children: menuHeader
419
+ }) : null,
420
+ pane === "root" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
421
+ ref: itemRef(),
422
+ type: "button",
423
+ role: "menuitem",
424
+ className: "dsh-model-menu-cell",
425
+ disabled,
426
+ onClick: () => drill("model"),
427
+ children: [
428
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
429
+ className: "dsh-model-menu-cellLabel",
430
+ children: modelLabel
431
+ }),
432
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
433
+ className: "dsh-model-menu-cellValue",
434
+ children: modelValue
435
+ }),
436
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutlineRegular, { className: "dsh-model-menu-cellChevron" })
437
+ ]
438
+ }), efforts.length > 0 && !reasoningSlider ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
439
+ ref: itemRef(),
440
+ type: "button",
441
+ role: "menuitem",
442
+ className: "dsh-model-menu-cell",
443
+ disabled,
444
+ onClick: () => drill("effort"),
445
+ children: [
446
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
447
+ className: "dsh-model-menu-cellLabel",
448
+ children: effortLabel
449
+ }),
450
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
451
+ className: "dsh-model-menu-cellValue",
452
+ children: effortValue ?? effortRows.find((row) => row.id === (selectedEffort ?? ""))?.name ?? defaultEffortLabel
453
+ }),
454
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutlineRegular, { className: "dsh-model-menu-cellChevron" })
455
+ ]
456
+ }) : null] }) : null,
457
+ pane === "model" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
458
+ showSearch ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
459
+ className: "dsh-model-menu-searchRow",
460
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
461
+ ref: searchRef,
462
+ disabled,
463
+ className: `dsh-model-menu-search${query !== "" ? " dsh-model-menu-searchWithQuery" : ""}`,
464
+ type: "text",
465
+ role: "searchbox",
466
+ "aria-label": searchPlaceholder,
467
+ "aria-controls": `${id}-models`,
468
+ "aria-activedescendant": activeModelIndex < 0 ? void 0 : `${id}-model-${activeModelIndex}`,
469
+ placeholder: searchPlaceholder,
470
+ value: query,
471
+ onChange: (event) => {
472
+ setQuery(event.target.value);
473
+ setHighlightedIndex(0);
474
+ }
475
+ }), query !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
476
+ type: "button",
477
+ className: "dsh-model-menu-searchClear",
478
+ "aria-label": clearSearchLabel,
479
+ disabled,
480
+ onClick: () => {
481
+ setQuery("");
482
+ setHighlightedIndex(null);
483
+ searchRef.current?.focus();
484
+ },
485
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseFillRegular, {})
486
+ }) : null]
487
+ }) : null,
488
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
489
+ ref: groupsRef,
490
+ id: `${id}-models`,
491
+ className: "dsh-model-menu-groups scrollable",
492
+ role: "menu",
493
+ "aria-label": modelLabel,
494
+ hidden: filteredGroups.length === 0,
495
+ children: [leading.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
496
+ className: "dsh-model-menu-leading",
497
+ children: leading.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
498
+ ref: itemRef(),
499
+ type: "button",
500
+ role: "menuitemradio",
501
+ "aria-checked": item.selected === true,
502
+ disabled: disabled || busy,
503
+ className: `dsh-model-menu-option${item.selected === true ? " dsh-model-menu-selected" : ""}`,
504
+ onClick: () => {
505
+ if (disabled || busy) return;
506
+ onPickLeading?.(item.id);
507
+ if (closeOnPick) close(true);
508
+ },
509
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
510
+ className: "dsh-model-menu-optionCopy",
511
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
512
+ className: "dsh-model-menu-modelName",
513
+ children: item.label
514
+ })
515
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
516
+ className: "dsh-model-menu-check",
517
+ children: item.selected === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, {}) : null
518
+ })]
519
+ }, item.id))
520
+ }) : null, filteredGroups.map((group) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MenuGroup, {
521
+ label: group.name,
522
+ children: group.items.map((item) => {
523
+ const index = modelIndex++;
524
+ const choice = item.choice;
525
+ const checked = selected?.provider === choice.provider && selected.model === choice.model;
526
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
527
+ ref: itemRef(),
528
+ type: "button",
529
+ role: "menuitemradio",
530
+ "aria-checked": checked,
531
+ id: `${id}-model-${index}`,
532
+ tabIndex: showSearch ? -1 : 0,
533
+ disabled: disabled || busy,
534
+ onFocus: () => setHighlightedIndex(index),
535
+ "data-highlighted": index === activeModelIndex ? "" : void 0,
536
+ className: `dsh-model-menu-option dsh-model-menu-modelOption${checked ? " dsh-model-menu-selected" : ""}${index === activeModelIndex ? " dsh-model-menu-optionActive" : ""}`,
537
+ onMouseMove: index === activeModelIndex ? void 0 : () => {
538
+ if (showSearch) setHighlightedIndex(index);
539
+ else itemRefs.current[leading.length + index]?.focus();
540
+ },
541
+ title: choice.label,
542
+ onClick: () => {
543
+ if (disabled || busy) return;
544
+ onPick({
545
+ provider: choice.provider,
546
+ model: choice.model
547
+ });
548
+ if (closeOnPick) close(true);
549
+ },
550
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
551
+ className: "dsh-model-menu-optionCopy",
552
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
553
+ className: "dsh-model-menu-modelName",
554
+ children: modelMenuShortName(choice)
555
+ })
556
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
557
+ className: "dsh-model-menu-check",
558
+ children: checked ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, {}) : null
559
+ })]
560
+ }, `${choice.provider}/${choice.model}`);
561
+ })
562
+ }, group.provider))]
563
+ }),
564
+ reasoningSlider && efforts.length > 0 && filteredGroups.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EffortSlider, {
565
+ label: effortLabel,
566
+ rows: effortRows,
567
+ selected: selectedEffort ?? "",
568
+ disabled,
569
+ busy,
570
+ onPick: (value) => onPickEffort?.(value)
571
+ }) : null,
572
+ filteredGroups.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
573
+ className: "dsh-model-menu-empty",
574
+ role: "status",
575
+ children: emptyLabel
576
+ }) : null
577
+ ] }) : null,
578
+ pane === "effort" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: effortRows.map((row) => {
579
+ const checked = (selectedEffort ?? "") === row.id;
580
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
581
+ ref: itemRef(),
582
+ type: "button",
583
+ role: "menuitemradio",
584
+ "aria-checked": checked,
585
+ disabled: disabled || busy,
586
+ className: `dsh-model-menu-option${checked ? " dsh-model-menu-selected" : ""}`,
587
+ onClick: () => {
588
+ if (disabled || busy) return;
589
+ onPickEffort?.(row.id || void 0);
590
+ if (closeOnPick) close(true);
591
+ },
592
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
593
+ className: "dsh-model-menu-optionCopy",
594
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
595
+ className: "dsh-model-menu-modelName",
596
+ children: row.name
597
+ })
598
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
599
+ className: "dsh-model-menu-check",
600
+ children: checked ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, {}) : null
601
+ })]
602
+ }, row.id || "__default");
603
+ }) }) : null
604
+ ]
605
+ }), document.body) : null;
606
+ const trigger = (0, react.isValidElement)(anchor) ? (0, react.cloneElement)(anchor, {
607
+ "aria-haspopup": "menu",
608
+ "aria-expanded": open,
609
+ "aria-controls": open ? `${id}-menu` : void 0,
610
+ "aria-disabled": disabled || void 0
611
+ }) : anchor;
612
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
613
+ ref: rootRef,
614
+ className: `dsh-model-menu-root${className ? ` ${className}` : ""}`,
615
+ onKeyDown,
616
+ onBlur,
617
+ onClick: (event) => {
618
+ if (disabled || event.defaultPrevented) return;
619
+ const button = event.target instanceof Element ? event.target.closest("button") : null;
620
+ if (!(button instanceof HTMLButtonElement) || button.disabled || !rootRef.current?.contains(button)) return;
621
+ requestOpen();
622
+ },
623
+ onMouseDown: (event) => {
624
+ if (event.target instanceof Element && menuRef.current?.contains(event.target) && event.target.closest("button") !== null) event.preventDefault();
625
+ },
626
+ children: [trigger, menu]
627
+ });
628
+ }
629
+ /**
630
+ * Discrete reasoning-effort slider shown under the model list in enhanced mode.
631
+ *
632
+ * A native range input (`step=1`) over exactly the rows the caller supplies —
633
+ * the default row plus each effort the selected model advertises — so it can
634
+ * never invent a level. It previews locally while dragging or arrowing and
635
+ * commits only when the gesture ends (pointer up, key up, or blur), so a drag
636
+ * does not write on every step. The menu stays open, so adjusting the effort
637
+ * never loses the model selection or the search query.
638
+ *
639
+ * The input itself stays the entire interaction and accessibility surface —
640
+ * keyboard, ARIA, disabled and the native step snapping all come from it —
641
+ * while the track, gradient fill and thumb below are decorative layers painted
642
+ * from the `--_p` percentage the preview position sets on the rail. Value
643
+ * changes only update that custom property, so the CSS transitions animate
644
+ * between levels without remounting nodes or restarting an animation.
645
+ */
646
+ function EffortSlider(props) {
647
+ const { label, rows, selected, disabled = false, busy = false, onPick } = props;
648
+ const active = Math.max(0, rows.findIndex((row) => row.id === selected));
649
+ const [position, setPosition] = (0, react.useState)(active);
650
+ const positionRef = (0, react.useRef)(position);
651
+ (0, react.useEffect)(() => {
652
+ positionRef.current = active;
653
+ setPosition(active);
654
+ }, [active]);
655
+ const preview = (next) => {
656
+ positionRef.current = next;
657
+ setPosition(next);
658
+ };
659
+ const commit = () => {
660
+ if (disabled || busy) return;
661
+ const row = rows[positionRef.current];
662
+ if (row !== void 0) onPick(row.id === "" ? void 0 : row.id);
663
+ };
664
+ const max = Math.max(0, rows.length - 1);
665
+ const percent = max > 0 ? Math.round(position / max * 100) : 0;
666
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
667
+ className: "dsh-model-menu-sliderGroup",
668
+ role: "group",
669
+ "aria-label": label,
670
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
671
+ className: "dsh-model-menu-sliderHead",
672
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
673
+ className: "dsh-model-menu-sliderTitle",
674
+ children: label
675
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
676
+ className: "dsh-model-menu-sliderValue",
677
+ children: rows[position]?.name
678
+ })]
679
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
680
+ className: "dsh-model-menu-sliderRail",
681
+ style: { "--_p": percent },
682
+ children: [
683
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
684
+ type: "range",
685
+ className: "dsh-model-menu-sliderInput",
686
+ min: 0,
687
+ max,
688
+ step: 1,
689
+ value: position,
690
+ disabled,
691
+ "aria-label": label,
692
+ "aria-disabled": disabled || busy || void 0,
693
+ "aria-valuetext": rows[position]?.name,
694
+ onChange: (event) => {
695
+ const value = Number(event.target.value);
696
+ if (Number.isFinite(value)) preview(value);
697
+ },
698
+ onPointerUp: commit,
699
+ onKeyUp: commit,
700
+ onBlur: commit
701
+ }),
702
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
703
+ className: "dsh-model-menu-sliderTrack",
704
+ "aria-hidden": "true",
705
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
706
+ className: "dsh-model-menu-sliderFill",
707
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dsh-model-menu-sliderFillBar" })
708
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
709
+ className: "dsh-model-menu-sliderNodes",
710
+ children: rows.map((row, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
711
+ "data-covered": index <= position ? "" : void 0,
712
+ className: "dsh-model-menu-sliderNode",
713
+ style: { "--_n": max > 0 ? Math.round(index / max * 100) : 0 }
714
+ }, row.id || "__default"))
715
+ })]
716
+ }),
717
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
718
+ className: "dsh-model-menu-sliderThumb",
719
+ "aria-hidden": "true"
720
+ })
721
+ ]
722
+ })]
723
+ });
724
+ }
725
+ /**
726
+ * ModelMenu styles, transcribed from the official ModelSelect.module.css (the
727
+ * published dsh-client-ui-model-selection bundle): card capped at
728
+ * min(360px, 100vh - 96px), 34px option cells at 13/18 with 5px 7px padding,
729
+ * 34px root cells at 13/20, a borderless 12px search row with a clear button,
730
+ * sticky group headings (MenuGroup's own CSS), and the l1-stroke prominent
731
+ * elevation at portal z-index 1100. The trigger mirrors the composer's 28px
732
+ * model pill, chevron rotating open. MenuSurface paints fill and backdrop.
733
+ * Only allowlisted --dsw- and --ds- theme tokens may appear here — a literal
734
+ * color is a bug.
735
+ */
736
+ const modelMenuCss = `
737
+ .dsh-model-menu-root { min-width: 0; position: relative; display: inline-flex; }
738
+ .dsh-model-menu-trigger {
739
+ border-radius: var(--dsw-radius-sm);
740
+ min-width: 0;
741
+ max-width: min(360px, 45cqw);
742
+ height: 28px;
743
+ color: var(--dsw-alias-label-secondary);
744
+ cursor: pointer;
745
+ background: none;
746
+ border: none;
747
+ outline: none;
748
+ align-items: center;
749
+ gap: 4px;
750
+ padding: 0 4px 0 8px;
751
+ font-size: 13px;
752
+ font-weight: 400;
753
+ line-height: 20px;
754
+ display: flex;
755
+ }
756
+ .dsh-model-menu-trigger:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
757
+ .dsh-model-menu-trigger:focus-visible:not([data-selection-focus]) { box-shadow: 0 0 0 var(--dsw-focus-ring-width) var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); }
758
+ .dsh-model-menu-trigger:disabled { color: var(--dsw-alias-label-dimmed); cursor: default; }
759
+ .dsh-model-menu-triggerLabel { text-overflow: ellipsis; white-space: nowrap; min-width: 0; overflow: hidden; }
760
+ .dsh-model-menu-triggerEffort { text-overflow: ellipsis; white-space: nowrap; min-width: 0; color: var(--dsw-alias-label-caption); flex-shrink: 1000; overflow: hidden; }
761
+ .dsh-model-menu-chevron { color: var(--dsw-alias-label-caption); flex: none; transition: transform var(--ds-transition-duration) var(--ds-ease-in-out); }
762
+ .dsh-model-menu-chevronOpen { transform: rotate(180deg); }
763
+ /* Portal overlay: the host publishes no z-index token, so the card keeps the
764
+ * native menu layer's 1100 verbatim. */
765
+ .dsh-model-menu-menu {
766
+ z-index: 1100;
767
+ --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
768
+ width: max-content;
769
+ min-width: min(240px, 100vw - 32px);
770
+ max-width: min(420px, 100vw - 32px);
771
+ max-height: min(360px, 100vh - 96px);
772
+ box-shadow: var(--dsw-elevation-prominent);
773
+ color: var(--dsw-alias-label-primary);
774
+ --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);
775
+ --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);
776
+ border: 0;
777
+ flex-direction: column;
778
+ padding: 4px;
779
+ display: flex;
780
+ position: fixed;
781
+ overflow: hidden;
782
+ }
783
+ .dsh-model-menu-status, .dsh-model-menu-empty { color: var(--dsw-alias-label-tertiary); padding: 8px; font-size: 12px; line-height: 18px; }
784
+ .dsh-model-menu-header { flex: none; padding: 2px 2px 4px; }
785
+ .dsh-model-menu-searchRow { flex-shrink: 0; margin: 2px 0 3px; position: relative; }
786
+ .dsh-model-menu-searchRow .dsh-model-menu-search { border-radius: var(--dsw-radius-md); background: none; border: 0 solid transparent; height: auto; padding: 5px 7px; display: flex; }
787
+ .dsh-model-menu-searchRow .dsh-model-menu-search:focus-within { border-color: transparent; }
788
+ .dsh-model-menu-searchWithQuery { padding-right: 34px; }
789
+ .dsh-model-menu-searchRow .dsh-model-menu-search input { padding: 0; font-size: 12px; line-height: normal; }
790
+ .dsh-model-menu-searchRow .dsh-model-menu-search input::placeholder { color: var(--dsw-alias-label-caption); }
791
+ /* The clear button is a circle by geometry (border-radius: 50%), a shape the
792
+ * radius token scale does not describe. */
793
+ .dsh-model-menu-searchClear {
794
+ width: 24px;
795
+ height: 24px;
796
+ color: var(--dsw-alias-label-secondary);
797
+ cursor: pointer;
798
+ background: none;
799
+ border: none;
800
+ border-radius: 50%;
801
+ justify-content: center;
802
+ align-items: center;
803
+ padding: 0;
804
+ display: inline-flex;
805
+ position: absolute;
806
+ top: 50%;
807
+ right: 4px;
808
+ transform: translateY(-50%);
809
+ }
810
+ .dsh-model-menu-searchClear:hover, .dsh-model-menu-searchClear:focus-visible { background: var(--dsw-alias-interactive-bg-hover); outline: none; }
811
+ .dsh-model-menu-groups { min-height: 0; overflow-y: auto; }
812
+ .dsh-model-menu-leading { border-bottom: 0.5px solid var(--dsw-alias-border-l2); margin-bottom: 3px; padding-bottom: 3px; }
813
+ .dsh-model-menu-option {
814
+ box-sizing: border-box;
815
+ border-radius: var(--dsw-radius-md);
816
+ width: auto;
817
+ min-width: 100%;
818
+ min-height: 34px;
819
+ color: inherit;
820
+ text-align: left;
821
+ cursor: pointer;
822
+ background: none;
823
+ border: none;
824
+ outline: none;
825
+ align-items: center;
826
+ gap: 6px;
827
+ padding: 5px 7px;
828
+ display: flex;
829
+ }
830
+ .dsh-model-menu-option:not(.dsh-model-menu-modelOption):hover:not(:disabled),
831
+ .dsh-model-menu-option:focus-visible,
832
+ .dsh-model-menu-optionActive:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
833
+ .dsh-model-menu-modelOption:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
834
+ .dsh-model-menu-selected { background: none; }
835
+ .dsh-model-menu-option:disabled { color: var(--dsw-alias-label-dimmed); cursor: default; }
836
+ .dsh-model-menu-optionCopy { flex-direction: column; flex: 1; min-width: 0; display: flex; }
837
+ .dsh-model-menu-modelName { color: inherit; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; font-weight: 400; line-height: 18px; overflow: hidden; }
838
+ .dsh-model-menu-check { color: var(--dsw-alias-label-primary); flex: 0 0 14px; place-items: center; display: grid; }
839
+ .dsh-model-menu-check svg { width: 14px; height: 14px; }
840
+ .dsh-model-menu-cell {
841
+ box-sizing: border-box;
842
+ border-radius: var(--dsw-radius-md);
843
+ width: auto;
844
+ min-width: 100%;
845
+ height: 34px;
846
+ color: var(--dsw-alias-label-primary);
847
+ cursor: pointer;
848
+ text-align: left;
849
+ background: none;
850
+ border: none;
851
+ outline: none;
852
+ align-items: center;
853
+ gap: 6px;
854
+ padding: 0 8px;
855
+ font-size: 13px;
856
+ line-height: 20px;
857
+ display: flex;
858
+ }
859
+ .dsh-model-menu-cell:hover, .dsh-model-menu-cell:focus-visible { background: var(--dsw-alias-interactive-bg-hover); }
860
+ .dsh-model-menu-cellLabel { white-space: nowrap; flex: none; }
861
+ .dsh-model-menu-cellValue { text-overflow: ellipsis; white-space: nowrap; text-align: right; min-width: 0; color: var(--dsw-alias-label-tertiary); flex: auto; overflow: hidden; }
862
+ .dsh-model-menu-cellChevron { width: 12px; height: 12px; color: var(--dsw-alias-menu-icon); flex: none; }
863
+ /* Discrete reasoning-effort slider under the model list (enhanced mode).
864
+ * The painted control is layered over a fully functional native range input:
865
+ * the input stays on top, transparent, and owns pointer/keyboard/ARIA, while
866
+ * the decorative track, fill and thumb render beneath it at pointer-events:
867
+ * none. Position lives in one custom property (--_p, 0–100) on the rail, so
868
+ * the only animated work a level change triggers is a clip-path/left
869
+ * transition — no keyframes, no element churn. */
870
+ .dsh-model-menu-sliderGroup { border-top: 0.5px solid var(--dsw-alias-border-l2); flex: none; margin-top: 4px; padding: 6px 8px 8px; }
871
+ .dsh-model-menu-sliderHead { align-items: baseline; justify-content: space-between; gap: 8px; margin: 0 2px 4px; display: flex; }
872
+ .dsh-model-menu-sliderTitle { color: var(--dsw-alias-label-tertiary); font-size: 12px; line-height: 18px; }
873
+ .dsh-model-menu-sliderValue { color: var(--dsw-alias-label-primary); font-size: 13px; font-weight: 600; line-height: 20px; transition: color var(--ds-transition-duration) var(--ds-ease-in-out); }
874
+ .dsh-model-menu-sliderRail { position: relative; direction: ltr; height: 38px; margin-top: 2px; }
875
+ .dsh-model-menu-sliderRail:has(:disabled) { opacity: 0.45; }
876
+ .dsh-model-menu-sliderInput {
877
+ appearance: none;
878
+ -webkit-appearance: none;
879
+ box-sizing: border-box;
880
+ border: 0;
881
+ cursor: pointer;
882
+ direction: ltr;
883
+ opacity: 0;
884
+ margin: 0;
885
+ padding: 0;
886
+ width: 100%;
887
+ height: 100%;
888
+ position: absolute;
889
+ inset: 0;
890
+ z-index: 1;
891
+ touch-action: pan-y;
892
+ }
893
+ .dsh-model-menu-sliderInput:disabled { cursor: default; }
894
+ /* The transparent input must keep the painted thumb's geometry: an
895
+ * appearance:none webkit thumb defaults to 16px, which would shift the
896
+ * end-point positions 7px off the decorative 30px disc and misplace the
897
+ * native drag hit target. Track gets the full rail height so the whole band
898
+ * maps to values identically. */
899
+ .dsh-model-menu-sliderInput::-webkit-slider-runnable-track { width: 100%; height: 100%; }
900
+ .dsh-model-menu-sliderInput::-webkit-slider-thumb { appearance: none; -webkit-appearance: none; width: 30px; height: 30px; margin: 0; }
901
+ .dsh-model-menu-sliderInput::-moz-range-track { width: 100%; height: 100%; }
902
+ .dsh-model-menu-sliderInput::-moz-range-thumb { width: 30px; height: 30px; margin: 0; border: 0; }
903
+ .dsh-model-menu-sliderTrack {
904
+ pointer-events: none;
905
+ border-radius: 999px;
906
+ width: 100%;
907
+ height: 26px;
908
+ background: var(--dsw-alias-interactive-bg-hover);
909
+ box-shadow: inset 0 0 0 0.5px var(--dsw-alias-border-l2);
910
+ position: absolute;
911
+ inset-inline: 0;
912
+ top: 50%;
913
+ transform: translateY(-50%);
914
+ /* No overflow:hidden — it would clip the fill's drop-shadow glow; the bar's
915
+ * own clip-path already supplies the rounded reveal shape. */
916
+ }
917
+ /* The accent ramp paints the full track width once and is revealed by an
918
+ * animated clip-path, so raising the effort unwraps more of the spectrum
919
+ * instead of squashing the same gradient into a narrower bar. The glow lives
920
+ * on the un-clipped parent: a filter on the clipped element itself would be
921
+ * clipped away with it. */
922
+ .dsh-model-menu-sliderFill {
923
+ display: block;
924
+ height: 100%;
925
+ filter: drop-shadow(0 0 5px var(--dsw-alias-state-business-primary));
926
+ transition: filter var(--ds-transition-duration) var(--ds-ease-in-out);
927
+ }
928
+ .dsh-model-menu-sliderInput:hover:not(:disabled) ~ .dsh-model-menu-sliderTrack .dsh-model-menu-sliderFill,
929
+ .dsh-model-menu-sliderInput:active:not(:disabled) ~ .dsh-model-menu-sliderTrack .dsh-model-menu-sliderFill {
930
+ filter: drop-shadow(0 0 9px var(--dsw-alias-state-business-primary));
931
+ }
932
+ /* The fill stops at the thumb's center, not at the track edge — at a 30px
933
+ * thumb the end stops sit 15px in from each side, matching the node dots. */
934
+ .dsh-model-menu-sliderFillBar {
935
+ display: block;
936
+ height: 100%;
937
+ background: linear-gradient(90deg,
938
+ var(--dsw-alias-state-business-primary),
939
+ var(--dsw-alias-state-success-primary),
940
+ var(--dsw-alias-state-warn-primary),
941
+ var(--dsw-alias-state-error-primary));
942
+ clip-path: inset(0 calc(100% - 15px - (100% - 30px) * var(--_p, 0) / 100) 0 0 round 999px);
943
+ transition: clip-path var(--ds-transition-duration) var(--ds-ease-in-out);
944
+ }
945
+ /* Stop dots: one per declared row (default + every advertised effort), evenly
946
+ * spread across the band between the two thumb-center extremes (inset 15px,
947
+ * i.e. half the 30px thumb). --_n is 0-100 along that band. A covered dot
948
+ * reads as a pale pin through the gradient; an uncovered one stays a neutral
949
+ * notch on the empty track. */
950
+ .dsh-model-menu-sliderNodes {
951
+ pointer-events: none;
952
+ display: block;
953
+ position: absolute;
954
+ inset-inline: 15px;
955
+ inset-block: 0;
956
+ }
957
+ .dsh-model-menu-sliderNode {
958
+ pointer-events: none;
959
+ box-sizing: border-box;
960
+ width: 4px;
961
+ height: 4px;
962
+ border-radius: 50%;
963
+ background: color-mix(in srgb, var(--dsw-alias-label-secondary) 35%, transparent);
964
+ position: absolute;
965
+ left: calc(var(--_n, 0) * 1%);
966
+ top: 50%;
967
+ transform: translate(-50%, -50%);
968
+ transition: background-color var(--ds-transition-duration) var(--ds-ease-in-out),
969
+ transform var(--ds-transition-duration) var(--ds-ease-in-out);
970
+ }
971
+ .dsh-model-menu-sliderNode[data-covered] { background: color-mix(in srgb, var(--dsw-alias-label-primary) 60%, transparent); }
972
+ .dsh-model-menu-sliderThumb {
973
+ pointer-events: none;
974
+ box-sizing: border-box;
975
+ border-radius: 50%;
976
+ width: 30px;
977
+ height: 30px;
978
+ background: var(--dsw-alias-button-primary-fill);
979
+ border: 0.5px solid var(--dsw-alias-border-l4);
980
+ box-shadow: 0 0 0 3px var(--dsw-alias-interactive-bg-hover), var(--dsw-elevation-soft);
981
+ position: absolute;
982
+ left: calc((100% - 30px) * var(--_p, 0) / 100);
983
+ top: 50%;
984
+ transform: translateY(-50%) scale(0.92);
985
+ transition: left var(--ds-transition-duration) var(--ds-ease-in-out),
986
+ transform var(--ds-transition-duration) var(--ds-ease-in-out),
987
+ box-shadow var(--ds-transition-duration) var(--ds-ease-in-out);
988
+ }
989
+ .dsh-model-menu-sliderInput:hover:not(:disabled) ~ .dsh-model-menu-sliderThumb { transform: translateY(-50%) scale(1); }
990
+ .dsh-model-menu-sliderInput:active:not(:disabled) ~ .dsh-model-menu-sliderThumb {
991
+ transform: translateY(-50%) scale(1.1);
992
+ box-shadow: 0 0 0 4px var(--dsw-alias-interactive-bg-hover), 0 0 12px var(--dsw-alias-state-business-primary);
993
+ }
994
+ .dsh-model-menu-sliderInput:focus-visible ~ .dsh-model-menu-sliderThumb {
995
+ box-shadow: 0 0 0 3px var(--dsw-alias-interactive-bg-hover),
996
+ 0 0 0 calc(3px + var(--dsw-focus-ring-width)) var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
997
+ }
998
+ @media (prefers-reduced-motion: reduce) {
999
+ .dsh-model-menu-sliderFill, .dsh-model-menu-sliderFillBar, .dsh-model-menu-sliderThumb,
1000
+ .dsh-model-menu-sliderNode, .dsh-model-menu-sliderValue { transition: none; }
1001
+ }
1002
+ `;
1003
+ //#endregion
1004
+ //#region ../dsh-model-route/src/model-picker.ts
1005
+ /** Registered Factory name — the `renderFactorySlot`/`registerFactory` key. */
1006
+ const MODEL_PICKER_FACTORY = "klarkxy.model-picker";
1007
+ /** The one caller-selectable local position: the trigger rendered in place. */
1008
+ const MODEL_PICKER_ANCHOR_SLOT = "anchor";
1009
+ //#endregion
1010
+ //#region src/effort-preference.ts
1011
+ const keyOf = (route) => JSON.stringify([route.provider, route.model]);
1012
+ /**
1013
+ * In-memory per-route effort memory. One instance is shared by the composer
1014
+ * seat and any face that submits through `submit`, so a slider pick made in
1015
+ * one surface is what the next model pick re-attaches.
1016
+ */
1017
+ var EffortPreference = class {
1018
+ /** route key → remembered explicit effort (a cleared route has no entry). */
1019
+ efforts = /* @__PURE__ */ new Map();
1020
+ /** The remembered explicit effort for one route, or undefined. */
1021
+ effortFor(route) {
1022
+ return this.efforts.get(keyOf(route));
1023
+ }
1024
+ /**
1025
+ * Record one confirmed pick. `undefined` clears the route's entry —
1026
+ * "Default" is an explicit choice too, and must not be overridden later.
1027
+ * Callers invoke this only after the Host accepted the selection.
1028
+ */
1029
+ remember(route, effort) {
1030
+ const key = keyOf(route);
1031
+ if (effort === void 0) this.efforts.delete(key);
1032
+ else this.efforts.set(key, effort);
1033
+ }
1034
+ /**
1035
+ * Effort to attach to a fresh pick of `route`: the remembered value when
1036
+ * the catalog still declares it for that route, else undefined (the pick
1037
+ * then follows the adapter default). Validation is per-route — a model
1038
+ * without declared efforts can never inherit an effort remembered for it.
1039
+ */
1040
+ resolve(route, reasoning) {
1041
+ const effort = this.efforts.get(keyOf(route));
1042
+ if (effort === void 0) return void 0;
1043
+ return reasoning?.efforts?.includes(effort) ? effort : void 0;
1044
+ }
1045
+ /**
1046
+ * Submit helper for a MODEL pick: merge the remembered effort into
1047
+ * `selection` when no effort is already present and the target route still
1048
+ * supports it, call `select`, and remember the resolved effort only on
1049
+ * success. On failure memory is untouched and the result is returned
1050
+ * verbatim.
1051
+ *
1052
+ * This is NOT the right verb for the slider's "Default" row: a caller
1053
+ * clearing an effort must submit through `select` directly and call
1054
+ * `remember(route, undefined)` only after success — routing the clear
1055
+ * through `submit` would just re-attach the remembered effort.
1056
+ */
1057
+ async submit(selection, reasoning, select) {
1058
+ const effort = selection.reasoningEffort ?? this.resolve(selection, reasoning);
1059
+ const intent = effort === void 0 ? {
1060
+ provider: selection.provider,
1061
+ model: selection.model
1062
+ } : {
1063
+ ...selection,
1064
+ reasoningEffort: effort
1065
+ };
1066
+ const result = await select(intent);
1067
+ if (result?.ok !== true) return result;
1068
+ this.remember(intent, intent.reasoningEffort);
1069
+ return result;
1070
+ }
1071
+ };
1072
+ //#endregion
1073
+ //#region src/client.tsx
1074
+ /**
1075
+ * Model picker, browser half.
1076
+ *
1077
+ * Two surfaces ride one component:
1078
+ *
1079
+ * - `klarkxy.model-picker` (root Factory): the searchable model picker with a
1080
+ * reasoning-effort slider that feature plugins mount through
1081
+ * `renderFactorySlot`. Its props are pure data and callbacks so consumers
1082
+ * never take a runtime dependency on this package — while the definition is
1083
+ * absent the renderer resolves the caller's `options.fallback`.
1084
+ * - `conversation.input.model` (single/session, priority -2): the composer
1085
+ * model seat, shadowing the shipped entry (priority 0) and a reasoning-slider
1086
+ * entry (priority -1). The ledger keeps every occupant, so unmounting this
1087
+ * entry restores the previous winner — the native or reference seat returns
1088
+ * automatically on disable/uninstall. The label child slot stays declared by
1089
+ * the shadowed native entry, which means only it may render it; like
1090
+ * reasoning-slider this seat renders the model name verbatim.
1091
+ *
1092
+ * The seat drives the same per-session ModelDirectory the native seat and the
1093
+ * /model popup share (catalog load, selection submission, pending/error
1094
+ * projection), so selection semantics stay host-owned.
1095
+ */
1096
+ const name = "dsh-model-picker-client";
1097
+ const inject = [
1098
+ "slots",
1099
+ "locale",
1100
+ "sessions"
1101
+ ];
1102
+ const MODEL_SEAT_PRIORITY = -2;
1103
+ const NS = "model-picker";
1104
+ const dictZh = {
1105
+ "trigger.fallback": "请选择模型",
1106
+ "trigger.aria": "选择模型,当前 {model}",
1107
+ "trigger.ariaEffort": "选择模型,当前 {model},推理等级 {effort}",
1108
+ "menu.model": "模型",
1109
+ "menu.effort": "推理等级",
1110
+ "effort.providerDefault": "跟随模型默认",
1111
+ "search.placeholder": "搜索模型…",
1112
+ "search.clear": "清除搜索",
1113
+ "search.empty": "没有匹配的模型。",
1114
+ "empty.models": "暂无可用模型",
1115
+ "status.loading": "正在刷新模型列表…",
1116
+ "error.action": "模型操作失败:{message}",
1117
+ "action.retry": "重试",
1118
+ "warning.unroutable": "当前模型不在可用目录中"
1119
+ };
1120
+ const dictEn = {
1121
+ "trigger.fallback": "Select model",
1122
+ "trigger.aria": "Select model, current {model}",
1123
+ "trigger.ariaEffort": "Select model, current {model}, reasoning effort {effort}",
1124
+ "menu.model": "Model",
1125
+ "menu.effort": "Effort",
1126
+ "effort.providerDefault": "Default",
1127
+ "search.placeholder": "Search models…",
1128
+ "search.clear": "Clear search",
1129
+ "search.empty": "No matching models.",
1130
+ "empty.models": "No models available.",
1131
+ "status.loading": "Refreshing model list…",
1132
+ "error.action": "Model operation failed: {message}",
1133
+ "action.retry": "Retry",
1134
+ "warning.unroutable": "The selected model is not in the available catalog"
1135
+ };
1136
+ function template(text, params) {
1137
+ return params ? text.replace(/\{(\w+)\}/g, (_, k) => String(params[k] ?? `{${k}}`)) : text;
1138
+ }
1139
+ /** Copy resolver for tests and for faces that never got a framework `t`. */
1140
+ function pickerCopy(locale) {
1141
+ const dict = locale?.toLowerCase().startsWith("zh") ? dictZh : dictEn;
1142
+ return (key, params) => template(dict[key], params);
1143
+ }
1144
+ const modelPickerCss = `${modelMenuCss}
1145
+ .dsh-model-picker-status {
1146
+ display: flex; flex-direction: column; gap: 4px; flex: none;
1147
+ }
1148
+ .dsh-model-picker-statusRow {
1149
+ display: flex; align-items: center; justify-content: space-between; gap: 8px;
1150
+ border-radius: var(--dsw-radius-md); padding: 5px 7px;
1151
+ color: var(--dsw-alias-state-error-primary);
1152
+ background: var(--dsw-alias-interactive-bg-hover-danger);
1153
+ font-size: 12px; line-height: 18px;
1154
+ }
1155
+ .dsh-model-picker-statusRow[data-tone="warn"] {
1156
+ color: var(--dsw-alias-label-secondary);
1157
+ background: var(--dsw-alias-interactive-bg-hover);
1158
+ }
1159
+ .dsh-model-picker-statusText { min-width: 0; flex: 1; overflow-wrap: anywhere; }
1160
+ .dsh-model-picker-retry {
1161
+ color: inherit; font: inherit; font-weight: 600; cursor: pointer;
1162
+ background: none; border: none; padding: 0 2px; flex: none;
1163
+ }
1164
+ .dsh-model-picker-retry:hover:not(:disabled) { text-decoration: underline; }
1165
+ .dsh-model-picker-retry:disabled { opacity: 0.6; cursor: default; }
1166
+ .dsh-model-picker-triggerIcon { display: var(--dsh-composer-model-icon-display, none); flex: none; }
1167
+ `;
1168
+ /** Directory provider groups → flat ModelMenu choices. */
1169
+ function directoryChoices(groups, bound) {
1170
+ const choices = [];
1171
+ for (const group of groups) {
1172
+ const providerName = group.name || group.id;
1173
+ for (const model of group.models) choices.push({
1174
+ provider: group.id,
1175
+ model: model.id,
1176
+ label: `${providerName} / ${model.name || model.id}`,
1177
+ providerLabel: providerName,
1178
+ efforts: (model.reasoning?.efforts ?? []).map((effort) => ({
1179
+ id: effort.id,
1180
+ name: effort.name || effort.id
1181
+ }))
1182
+ });
1183
+ }
1184
+ if (bound?.provider && bound.model && !choices.some((item) => item.provider === bound.provider && item.model === bound.model)) choices.push({
1185
+ provider: bound.provider,
1186
+ model: bound.model,
1187
+ label: `${bound.provider} / ${bound.model}`,
1188
+ efforts: []
1189
+ });
1190
+ return choices;
1191
+ }
1192
+ function reasoningOf(groups, provider, model) {
1193
+ return groups.find((group) => group.id === provider)?.models.find((entry) => entry.id === model)?.reasoning;
1194
+ }
1195
+ /** Route key only when the saved model is absent from the catalog. Effort-only
1196
+ * snapshot updates must not change this — a new `current` object is not a new list. */
1197
+ function outsideCatalogKey(groups, current) {
1198
+ if (!current) return "";
1199
+ for (const group of groups) {
1200
+ if (group.id !== current.provider) continue;
1201
+ if (group.models.some((model) => model.id === current.model)) return "";
1202
+ }
1203
+ return `${current.provider}\0${current.model}`;
1204
+ }
1205
+ /**
1206
+ * DOM attributes the anchor forwards: framework-stamped ARIA, identity and
1207
+ * style fields, and event handlers. Everything else in the local props —
1208
+ * injected kit members and hook objects — never reaches the DOM.
1209
+ */
1210
+ const TRIGGER_FORWARD = /^(aria-|data-|id$|className$|style$|title$|role$|tabIndex$|on[A-Z])/;
1211
+ /**
1212
+ * The trigger every occurrence gets unless the caller selects its own
1213
+ * `anchor` local component. It mirrors the composer's model pill: icon +
1214
+ * label + effort caption + chevron (spinner while busy).
1215
+ */
1216
+ function DefaultTrigger(props) {
1217
+ const { open, disabled, busy, label, effortLabel, className, ...rest } = props;
1218
+ const forward = {};
1219
+ for (const [key, value] of Object.entries(rest)) if (TRIGGER_FORWARD.test(key)) forward[key] = value;
1220
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1221
+ type: "button",
1222
+ ...forward,
1223
+ className: `dsh-model-menu-trigger${className ? ` ${className}` : ""}`,
1224
+ disabled: disabled || void 0,
1225
+ title: effortLabel ? `${label} · ${effortLabel}` : label,
1226
+ children: [
1227
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconDataOutlineRegular, {
1228
+ className: "dsh-model-picker-triggerIcon",
1229
+ size: 16
1230
+ }),
1231
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1232
+ className: "dsh-model-menu-triggerLabel",
1233
+ children: label
1234
+ }),
1235
+ effortLabel !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1236
+ className: "dsh-model-menu-triggerEffort",
1237
+ children: effortLabel
1238
+ }) : null,
1239
+ busy ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: "ongoing" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${open ? " dsh-model-menu-chevronOpen" : ""}` })
1240
+ ]
1241
+ });
1242
+ }
1243
+ /**
1244
+ * One picker occurrence. `props` is the consumer's ModelPickerProps plus the
1245
+ * framework seats (`t`, `useFactorySlot`, injected hooks). Open state is per
1246
+ * occurrence; a `disabled` rise closes the menu and every pick is gated so a
1247
+ * busy or disabled picker can never select.
1248
+ */
1249
+ function ModelPicker(props) {
1250
+ const { t, useFactorySlot, useActiveLocale, ...input } = props;
1251
+ const activeLocale = useActiveLocale ? String(useActiveLocale((snapshot) => snapshot.active)) : "en";
1252
+ const [open, setOpen] = (0, react.useState)(false);
1253
+ const disabled = input.disabled === true;
1254
+ const busy = input.busy === true;
1255
+ const gatedRef = (0, react.useRef)(disabled || busy);
1256
+ gatedRef.current = disabled || busy;
1257
+ const setMenuOpenRef = (0, react.useRef)((next) => {});
1258
+ setMenuOpenRef.current = (next) => {
1259
+ if (disabled && next) return;
1260
+ setOpen(next);
1261
+ input.onOpenChange?.(next);
1262
+ };
1263
+ const setMenuOpen = (next) => setMenuOpenRef.current(next);
1264
+ const language = input.locale ?? activeLocale;
1265
+ const selected = input.selected;
1266
+ const selectedChoice = (0, react.useMemo)(() => input.choices.find((choice) => choice.provider === selected?.provider && choice.model === selected.model), [
1267
+ input.choices,
1268
+ selected?.provider,
1269
+ selected?.model
1270
+ ]);
1271
+ const triggerLabel = input.triggerLabel ?? selectedChoice?.label ?? (selected ? `${selected.provider} / ${selected.model}` : t("trigger.fallback"));
1272
+ const effortRows = input.efforts ?? [];
1273
+ const selectedEffort = input.selectedEffort ?? "";
1274
+ const triggerEffortLabel = input.triggerEffortLabel ?? (selectedEffort ? modelMenuEffortLabel(effortRows.find((row) => row.id === selectedEffort) ?? {
1275
+ id: selectedEffort,
1276
+ name: selectedEffort
1277
+ }, language) : effortRows.length > 0 ? input.defaultEffortLabel ?? t("effort.providerDefault") : void 0);
1278
+ const status = input.status;
1279
+ const loading = status?.busy === true && input.choices.length === 0;
1280
+ const menuHeader = status && (loading || status.error || status.warning || (status.failures?.length ?? 0) > 0) ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1281
+ className: "dsh-model-picker-status",
1282
+ role: status.error ? "alert" : "status",
1283
+ children: [
1284
+ loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1285
+ className: "dsh-model-picker-statusRow",
1286
+ "data-tone": "info",
1287
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1288
+ className: "dsh-model-picker-statusText",
1289
+ children: t("status.loading")
1290
+ })
1291
+ }) : null,
1292
+ status.warning ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1293
+ className: "dsh-model-picker-statusRow",
1294
+ "data-tone": "warn",
1295
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1296
+ className: "dsh-model-picker-statusText",
1297
+ children: status.warning
1298
+ })
1299
+ }) : null,
1300
+ status.error ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1301
+ className: "dsh-model-picker-statusRow",
1302
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1303
+ className: "dsh-model-picker-statusText",
1304
+ children: t("error.action", { message: status.error })
1305
+ }), status.retry ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1306
+ type: "button",
1307
+ className: "dsh-model-picker-retry",
1308
+ disabled,
1309
+ onClick: () => status.retry?.(),
1310
+ children: input.retryLabel ?? t("action.retry")
1311
+ }) : null]
1312
+ }) : null,
1313
+ (status.failures ?? []).map((failure) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1314
+ className: "dsh-model-picker-statusRow",
1315
+ "data-tone": "warn",
1316
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1317
+ className: "dsh-model-picker-statusText",
1318
+ children: [
1319
+ failure.name || failure.id,
1320
+ ": ",
1321
+ failure.message
1322
+ ]
1323
+ }), status.retry ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1324
+ type: "button",
1325
+ className: "dsh-model-picker-retry",
1326
+ disabled,
1327
+ onClick: () => status.retry?.(),
1328
+ children: input.retryLabel ?? t("action.retry")
1329
+ }) : null]
1330
+ }, failure.id))
1331
+ ]
1332
+ }) : void 0;
1333
+ const gate = (fn) => (...args) => {
1334
+ if (!gatedRef.current) fn?.(...args);
1335
+ };
1336
+ const Anchor = useFactorySlot(MODEL_PICKER_ANCHOR_SLOT, DefaultTrigger);
1337
+ const anchor = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Anchor, {
1338
+ open,
1339
+ disabled,
1340
+ busy,
1341
+ label: triggerLabel,
1342
+ effortLabel: triggerEffortLabel,
1343
+ className: input.triggerClassName,
1344
+ "aria-label": triggerEffortLabel ? t("trigger.ariaEffort", {
1345
+ model: triggerLabel,
1346
+ effort: triggerEffortLabel
1347
+ }) : t("trigger.aria", { model: triggerLabel })
1348
+ });
1349
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: modelPickerCss }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
1350
+ locale: language,
1351
+ open,
1352
+ onOpenChange: setMenuOpen,
1353
+ anchor,
1354
+ choices: input.choices,
1355
+ selected: input.selected,
1356
+ onPick: gate(input.onPick),
1357
+ leading: input.leading ?? [],
1358
+ onPickLeading: gate(input.onPickLeading),
1359
+ menuHeader,
1360
+ disabled,
1361
+ busy,
1362
+ efforts: effortRows,
1363
+ selectedEffort,
1364
+ onPickEffort: gate(input.onPickEffort),
1365
+ modelLabel: input.modelLabel ?? t("menu.model"),
1366
+ modelValue: input.modelValue ?? triggerLabel,
1367
+ effortLabel: input.effortLabel ?? t("menu.effort"),
1368
+ defaultEffortLabel: input.defaultEffortLabel ?? t("effort.providerDefault"),
1369
+ searchPlaceholder: input.searchPlaceholder ?? t("search.placeholder"),
1370
+ emptyLabel: input.emptyLabel ?? (input.choices.length === 0 ? t("empty.models") : t("search.empty")),
1371
+ clearSearchLabel: input.clearSearchLabel ?? t("search.clear"),
1372
+ initialPane: input.initialPane ?? "model",
1373
+ alwaysShowSearch: input.alwaysShowSearch ?? true,
1374
+ reasoningSlider: input.reasoningSlider ?? true,
1375
+ closeOnPick: false,
1376
+ closeOnDisable: false,
1377
+ listClassName: `dsh-model-picker${input.listClassName ? ` ${input.listClassName}` : ""}`,
1378
+ className: "dsh-model-picker"
1379
+ })] });
1380
+ }
1381
+ /**
1382
+ * The composer model seat: the same injected face the native entry exposes,
1383
+ * rendered through the shared factory so the composer and every consumer
1384
+ * share one picker implementation. The bare ModelMenu below is the last-ditch
1385
+ * path for a missing factory — the seat never loses its control.
1386
+ */
1387
+ function ComposerModelSeat(props) {
1388
+ const { t } = props;
1389
+ const directory = props.directory;
1390
+ const subscribeDirectory = (0, react.useCallback)((onStoreChange) => directory.subscribe(onStoreChange), [directory]);
1391
+ const snapshotDirectory = (0, react.useCallback)(() => directory.getSnapshot(), [directory]);
1392
+ const state = (0, react.useSyncExternalStore)(subscribeDirectory, snapshotDirectory, snapshotDirectory);
1393
+ const [toast, setToast] = (0, react.useState)(null);
1394
+ const toastSeq = (0, react.useRef)(0);
1395
+ const rootRef = (0, react.useRef)(null);
1396
+ const groups = state.groups;
1397
+ const current = state.current;
1398
+ const outsideKey = outsideCatalogKey(groups, current);
1399
+ const choices = (0, react.useMemo)(() => directoryChoices(groups, outsideKey === "" ? void 0 : {
1400
+ provider: outsideKey.slice(0, outsideKey.indexOf("\0")),
1401
+ model: outsideKey.slice(outsideKey.indexOf("\0") + 1)
1402
+ }), [groups, outsideKey]);
1403
+ const selectedChoice = choices.find((choice) => choice.provider === current?.provider && choice.model === current.model);
1404
+ const reasoning = current ? reasoningOf(groups, current.provider, current.model) : void 0;
1405
+ const efforts = (0, react.useMemo)(() => [...selectedChoice?.efforts ?? []], [selectedChoice]);
1406
+ const busy = state.status === "selecting" || state.pending !== null;
1407
+ (0, react.useEffect)(() => {
1408
+ props.load();
1409
+ }, [props.load]);
1410
+ const onMenuOpenChange = (next) => {
1411
+ if (next) props.load();
1412
+ };
1413
+ const report = (error) => {
1414
+ toastSeq.current += 1;
1415
+ setToast({
1416
+ seq: toastSeq.current,
1417
+ text: t("error.action", { message: `${error.code}: ${error.message}` })
1418
+ });
1419
+ };
1420
+ const submit = (selection) => {
1421
+ props.select(selection).then((result) => {
1422
+ if (result !== void 0 && !result.ok) report(result.error);
1423
+ }).catch((error) => report({
1424
+ code: "model-picker/error",
1425
+ message: error instanceof Error ? error.message : String(error)
1426
+ }));
1427
+ };
1428
+ const unroutable = state.routable === false && current !== null;
1429
+ const pickModel = (route) => {
1430
+ if (current?.provider === route.provider && current.model === route.model) return;
1431
+ if (unroutable && route.provider === current.provider && route.model === current.model) return;
1432
+ submit({
1433
+ provider: route.provider,
1434
+ model: route.model
1435
+ });
1436
+ };
1437
+ const pickEffort = (effort) => {
1438
+ if (!current) return;
1439
+ if ((current.reasoningEffort ?? void 0) === effort) return;
1440
+ submit({
1441
+ provider: current.provider,
1442
+ model: current.model,
1443
+ ...effort === void 0 ? {} : { reasoningEffort: effort }
1444
+ });
1445
+ };
1446
+ const modelName = selectedChoice ? selectedChoice.label.split(" / ").slice(1).join(" / ") || selectedChoice.model : current?.model ?? t("trigger.fallback");
1447
+ const effortLabel = reasoning === void 0 ? state.retainedEffort : current?.reasoningEffort !== void 0 ? modelMenuEffortLabel(reasoning.efforts.find((row) => row.id === current?.reasoningEffort) ?? {
1448
+ id: current?.reasoningEffort ?? "",
1449
+ name: current?.reasoningEffort ?? ""
1450
+ }, void 0) : reasoning.efforts.length > 0 || reasoning.defaultEffort !== void 0 ? t("effort.providerDefault") : void 0;
1451
+ if (!props.available) return null;
1452
+ const fallback = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenuFallback, {
1453
+ t,
1454
+ state,
1455
+ choices,
1456
+ efforts,
1457
+ savedEffort: current?.reasoningEffort,
1458
+ effortLabel,
1459
+ modelName,
1460
+ locked: props.locked,
1461
+ busy,
1462
+ onPick: pickModel,
1463
+ onPickEffort: pickEffort,
1464
+ onOpen: onMenuOpenChange,
1465
+ onRetry: () => props.load()
1466
+ });
1467
+ const pickerProps = {
1468
+ choices,
1469
+ selected: current ? {
1470
+ provider: current.provider,
1471
+ model: current.model
1472
+ } : void 0,
1473
+ onPick: pickModel,
1474
+ efforts,
1475
+ selectedEffort: current?.reasoningEffort ?? "",
1476
+ onPickEffort: pickEffort,
1477
+ disabled: props.locked,
1478
+ busy,
1479
+ triggerLabel: modelName,
1480
+ triggerEffortLabel: effortLabel,
1481
+ onOpenChange: onMenuOpenChange,
1482
+ status: {
1483
+ error: state.error,
1484
+ busy: state.status === "loading",
1485
+ warning: unroutable ? t("warning.unroutable") : void 0,
1486
+ failures: state.failures,
1487
+ retry: () => props.load()
1488
+ }
1489
+ };
1490
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1491
+ ref: rootRef,
1492
+ className: "dsh-model-picker",
1493
+ children: [props.renderFactorySlot ? props.renderFactorySlot(MODEL_PICKER_FACTORY, pickerProps, { fallback }) : fallback, toast !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Toast, {
1494
+ text: toast.text,
1495
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconWarningOutlineRegular, {}),
1496
+ anchor: rootRef.current?.closest("[data-composer-card]") ?? rootRef.current,
1497
+ onDone: () => setToast(null)
1498
+ }, toast.seq)]
1499
+ });
1500
+ }
1501
+ /** Bare-menu fallback used only if the factory never registered. */
1502
+ function ModelMenuFallback(props) {
1503
+ const { t, state } = props;
1504
+ const [open, setOpen] = (0, react.useState)(false);
1505
+ (0, react.useEffect)(() => {
1506
+ if (props.locked) setOpen(false);
1507
+ }, [props.locked]);
1508
+ const gated = props.locked || props.busy;
1509
+ const unroutable = state.routable === false && state.current !== null;
1510
+ const loading = state.status === "loading" && props.choices.length === 0;
1511
+ const menuHeader = loading || state.error || unroutable || state.failures.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1512
+ className: "dsh-model-picker-status",
1513
+ role: state.error ? "alert" : "status",
1514
+ children: [
1515
+ loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1516
+ className: "dsh-model-picker-statusRow",
1517
+ "data-tone": "info",
1518
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1519
+ className: "dsh-model-picker-statusText",
1520
+ children: t("status.loading")
1521
+ })
1522
+ }) : null,
1523
+ unroutable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1524
+ className: "dsh-model-picker-statusRow",
1525
+ "data-tone": "warn",
1526
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1527
+ className: "dsh-model-picker-statusText",
1528
+ children: t("warning.unroutable")
1529
+ })
1530
+ }) : null,
1531
+ state.error ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1532
+ className: "dsh-model-picker-statusRow",
1533
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1534
+ className: "dsh-model-picker-statusText",
1535
+ children: t("error.action", { message: state.error })
1536
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1537
+ type: "button",
1538
+ className: "dsh-model-picker-retry",
1539
+ disabled: gated,
1540
+ onClick: props.onRetry,
1541
+ children: t("action.retry")
1542
+ })]
1543
+ }) : null,
1544
+ state.failures.map((failure) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1545
+ className: "dsh-model-picker-statusRow",
1546
+ "data-tone": "warn",
1547
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1548
+ className: "dsh-model-picker-statusText",
1549
+ children: [
1550
+ failure.name || failure.id,
1551
+ ": ",
1552
+ failure.message
1553
+ ]
1554
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1555
+ type: "button",
1556
+ className: "dsh-model-picker-retry",
1557
+ disabled: gated,
1558
+ onClick: props.onRetry,
1559
+ children: t("action.retry")
1560
+ })]
1561
+ }, failure.id))
1562
+ ]
1563
+ }) : void 0;
1564
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: modelPickerCss }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
1565
+ locale: "en",
1566
+ open: open && !props.locked,
1567
+ onOpenChange: (next) => {
1568
+ if (props.locked) return;
1569
+ setOpen(next);
1570
+ if (next) props.onOpen(next);
1571
+ },
1572
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DefaultTrigger, {
1573
+ open,
1574
+ disabled: gated,
1575
+ busy: props.busy,
1576
+ label: props.modelName,
1577
+ effortLabel: props.effortLabel,
1578
+ "aria-label": props.effortLabel ? t("trigger.ariaEffort", {
1579
+ model: props.modelName,
1580
+ effort: props.effortLabel
1581
+ }) : t("trigger.aria", { model: props.modelName })
1582
+ }),
1583
+ choices: props.choices,
1584
+ selected: state.current ? {
1585
+ provider: state.current.provider,
1586
+ model: state.current.model
1587
+ } : void 0,
1588
+ onPick: (route) => {
1589
+ if (!gated) props.onPick(route);
1590
+ },
1591
+ menuHeader,
1592
+ disabled: props.locked,
1593
+ busy: props.busy,
1594
+ efforts: props.efforts,
1595
+ selectedEffort: props.savedEffort ?? "",
1596
+ onPickEffort: (effort) => {
1597
+ if (!gated) props.onPickEffort(effort);
1598
+ },
1599
+ modelLabel: t("menu.model"),
1600
+ modelValue: props.modelName,
1601
+ effortLabel: t("menu.effort"),
1602
+ defaultEffortLabel: t("effort.providerDefault"),
1603
+ searchPlaceholder: t("search.placeholder"),
1604
+ emptyLabel: t("empty.models"),
1605
+ clearSearchLabel: t("search.clear"),
1606
+ initialPane: "model",
1607
+ alwaysShowSearch: true,
1608
+ reasoningSlider: true,
1609
+ closeOnPick: false,
1610
+ listClassName: "dsh-model-picker",
1611
+ className: "dsh-model-picker"
1612
+ })] });
1613
+ }
1614
+ /**
1615
+ * Wrap one per-session `ModelDirectory.select` with the page-lifetime effort
1616
+ * memory: a cross-route pick with no explicit effort re-attaches the route's
1617
+ * last successful human pick (only while the catalog still declares it), and
1618
+ * every confirmed selection records its effort so the Host default save —
1619
+ * `sessionController.selectModel` persists the complete selection through
1620
+ * `agentDefaultModel.saveSelection` — keeps the effort for new Sessions that
1621
+ * follow the default. Memory updates strictly on `result.ok === true`; a
1622
+ * failure leaves the remembered route untouched. Exported for the wiring spec.
1623
+ */
1624
+ function createPreferredSelect(directory, preference) {
1625
+ return async (selection) => {
1626
+ const state = directory.store.getSnapshot();
1627
+ const current = state.current;
1628
+ const sameRoute = current !== null && current.provider === selection.provider && current.model === selection.model;
1629
+ let final = selection;
1630
+ if (!sameRoute && selection.reasoningEffort === void 0) {
1631
+ const reasoning = state.groups.find((group) => group.id === selection.provider)?.models.find((model) => model.id === selection.model)?.reasoning;
1632
+ const effort = preference.resolve(selection, reasoning === void 0 ? void 0 : { efforts: reasoning.efforts.map((row) => row.id) });
1633
+ if (effort !== void 0) final = {
1634
+ ...selection,
1635
+ reasoningEffort: effort
1636
+ };
1637
+ }
1638
+ const result = await directory.select(final);
1639
+ if (result?.ok === true) preference.remember(final, final.reasoningEffort);
1640
+ return result;
1641
+ };
1642
+ }
1643
+ function apply(ctx) {
1644
+ const preference = new EffortPreference();
1645
+ ctx.effect(() => {
1646
+ const zh = ctx.locale?.register(NS, "zh", dictZh);
1647
+ const en = ctx.locale?.register(NS, "en", dictEn);
1648
+ return () => {
1649
+ zh?.();
1650
+ en?.();
1651
+ };
1652
+ });
1653
+ ctx.slots.registerFactory({
1654
+ name: MODEL_PICKER_FACTORY,
1655
+ scope: "root",
1656
+ locale: NS,
1657
+ inject: () => ({ hooks: { activeLocale: ctx.locale } }),
1658
+ slots: { [MODEL_PICKER_ANCHOR_SLOT]: { scope: "root" } }
1659
+ }, ModelPicker);
1660
+ ctx.inject(["modelDirectories"], (scope) => {
1661
+ const directories = scope.modelDirectories;
1662
+ scope.slots.inject("conversation.input.model", () => scope.slots.register({
1663
+ name: "conversation.input.model",
1664
+ priority: MODEL_SEAT_PRIORITY,
1665
+ locale: NS,
1666
+ inject: (sessionId) => {
1667
+ const unavailable = {
1668
+ available: false,
1669
+ directory: {
1670
+ getSnapshot: () => EMPTY_STATE,
1671
+ subscribe: () => () => {},
1672
+ update: () => {},
1673
+ set: () => {}
1674
+ },
1675
+ load: () => {},
1676
+ resolveText: (text) => ctx.locale?.resolveText(text) ?? String(text),
1677
+ select: async () => void 0
1678
+ };
1679
+ if (scope.sessions?.subagentAddress?.(sessionId) !== void 0) return unavailable;
1680
+ try {
1681
+ const directory = directories.directoryFor(sessionId);
1682
+ return {
1683
+ available: true,
1684
+ directory: directory.store,
1685
+ load: () => {
1686
+ directory.load().catch(() => {});
1687
+ },
1688
+ resolveText: (text) => ctx.locale?.resolveText(text) ?? String(text),
1689
+ select: createPreferredSelect(directory, preference)
1690
+ };
1691
+ } catch {
1692
+ return unavailable;
1693
+ }
1694
+ }
1695
+ }, ComposerModelSeat));
1696
+ });
1697
+ }
1698
+ const EMPTY_STATE = {
1699
+ current: null,
1700
+ routable: null,
1701
+ groups: [],
1702
+ failures: [],
1703
+ status: "idle",
1704
+ pending: null,
1705
+ error: null
1706
+ };
1707
+ //#endregion
1708
+ exports.ComposerModelSeat = ComposerModelSeat;
1709
+ exports.ModelPicker = ModelPicker;
1710
+ exports.apply = apply;
1711
+ exports.createPreferredSelect = createPreferredSelect;
1712
+ exports.dictEn = dictEn;
1713
+ exports.dictZh = dictZh;
1714
+ exports.directoryChoices = directoryChoices;
1715
+ exports.inject = inject;
1716
+ exports.modelPickerCss = modelPickerCss;
1717
+ exports.name = name;
1718
+ exports.pickerCopy = pickerCopy;
1719
+
1720
+ //# sourceMappingURL=client.inner.cjs.map