@klarkxy/dsh-model-picker 0.0.0-stage → 0.1.1

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