@klarkxy/dsh-model-hub 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -98,7 +98,9 @@ Object.values({
98
98
  "--dsh-scrollbar-thumb-hover",
99
99
  "--dsh-content-font-size-secondary",
100
100
  "--dsh-content-font-delta",
101
- "--dsh-frame-top-clearance"
101
+ "--dsh-frame-top-clearance",
102
+ "--dsh-composer-model-icon-display",
103
+ "--dsh-composer-model-text-display"
102
104
  ]
103
105
  }).flat();
104
106
  /**
@@ -546,6 +548,7 @@ function parseModelMenuChoices(value, bound) {
546
548
  provider,
547
549
  model,
548
550
  label: `${providerName} / ${name}`,
551
+ providerLabel: providerName,
549
552
  efforts
550
553
  });
551
554
  }
@@ -592,11 +595,11 @@ function modelMenuShortName(choice) {
592
595
  /** Group catalog choices by provider and filter them by a case-insensitive query. */
593
596
  function groupModelMenuChoices(choices, query) {
594
597
  const needle = query.trim().toLowerCase();
595
- const matched = needle ? choices.filter((choice) => choice.label.toLowerCase().includes(needle) || choice.model.toLowerCase().includes(needle) || choice.provider.toLowerCase().includes(needle)) : choices;
598
+ 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;
596
599
  const groups = [];
597
600
  for (const choice of matched) {
598
601
  const sep = choice.label.indexOf(" / ");
599
- const name = sep > 0 ? choice.label.slice(0, sep) : choice.provider;
602
+ const name = choice.providerLabel ?? (sep > 0 ? choice.label.slice(0, sep) : choice.provider);
600
603
  const last = groups.at(-1);
601
604
  if (last && last.provider === choice.provider) last.items.push(choice);
602
605
  else groups.push({
@@ -982,7 +985,7 @@ function ModelMenu(props) {
982
985
  empty: "No matching models",
983
986
  clear: "Clear search"
984
987
  };
985
- 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, className, listClassName } = props;
988
+ 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;
986
989
  const rootRef = (0, react.useRef)(null);
987
990
  const openRef = (0, react.useRef)(open);
988
991
  openRef.current = open;
@@ -997,9 +1000,10 @@ function ModelMenu(props) {
997
1000
  const [pane, setPane] = (0, react.useState)("root");
998
1001
  const [query, setQuery] = (0, react.useState)("");
999
1002
  const [highlightedIndex, setHighlightedIndex] = (0, react.useState)(null);
1003
+ const [keyNav, setKeyNav] = (0, react.useState)(0);
1000
1004
  const [menuPos, setMenuPos] = (0, react.useState)(null);
1001
1005
  const id = (0, react.useId)();
1002
- const showSearch = choices.length > 4;
1006
+ const showSearch = alwaysShowSearch || choices.length > 4;
1003
1007
  const effortRows = (0, react.useMemo)(() => [{
1004
1008
  id: "",
1005
1009
  name: defaultEffortLabel
@@ -1016,7 +1020,12 @@ function ModelMenu(props) {
1016
1020
  items: (0, _deepseek_ai_dsh_client_ui_primitives.rankByName)(group.items.map((choice) => ({
1017
1021
  choice,
1018
1022
  name: modelMenuShortName(choice),
1019
- label: choice.model
1023
+ label: [
1024
+ choice.provider,
1025
+ choice.providerLabel,
1026
+ choice.model,
1027
+ choice.label
1028
+ ].filter(Boolean).join(" ")
1020
1029
  })), showSearch ? query.trim() : "")
1021
1030
  })).filter((group) => group.items.length > 0), [
1022
1031
  choices,
@@ -1048,10 +1057,12 @@ function ModelMenu(props) {
1048
1057
  paneFocus.current = "drill";
1049
1058
  setPane(next);
1050
1059
  };
1051
- /** Leave a drilled pane for the root one, handing the keyboard back to its cell. */
1060
+ /** Leave a drilled pane for the entry pane, handing the keyboard back to it.
1061
+ * A root entry refocuses the cell that opened the pane; a search-first
1062
+ * (model) entry refocuses that pane's own intent. */
1052
1063
  const back = (from) => {
1053
- paneFocus.current = from;
1054
- setPane("root");
1064
+ paneFocus.current = initialPane === "root" ? from : "drill";
1065
+ setPane(initialPane);
1055
1066
  };
1056
1067
  const moveFocus = (offset) => {
1057
1068
  const items = itemRefs.current.filter((item) => item !== null && !item.disabled);
@@ -1091,10 +1102,18 @@ function ModelMenu(props) {
1091
1102
  }, [requestedOpen, open]);
1092
1103
  (0, react.useEffect)(() => {
1093
1104
  if (!open) return;
1094
- setPane("root");
1105
+ setPane(initialPane);
1106
+ if (initialPane === "model") paneFocus.current = "drill";
1095
1107
  setQuery("");
1096
1108
  setHighlightedIndex(null);
1097
- }, [open]);
1109
+ }, [open, initialPane]);
1110
+ (0, react.useEffect)(() => {
1111
+ if (disabled && open && closeOnDisable) changeOpen(false);
1112
+ }, [
1113
+ disabled,
1114
+ open,
1115
+ closeOnDisable
1116
+ ]);
1098
1117
  (0, react.useEffect)(() => {
1099
1118
  if (!open) return;
1100
1119
  const closeOutside = (event) => {
@@ -1102,9 +1121,25 @@ function ModelMenu(props) {
1102
1121
  if (rootRef.current?.contains(target) || menuRef.current?.contains(target)) return;
1103
1122
  changeOpen(false);
1104
1123
  };
1124
+ const escapeOutside = (event) => {
1125
+ if (event.key !== "Escape" || event.defaultPrevented || event.isComposing) return;
1126
+ if (!openRef.current) return;
1127
+ event.preventDefault();
1128
+ if (pane !== initialPane) back(pane);
1129
+ else close(true);
1130
+ };
1105
1131
  document.addEventListener("mousedown", closeOutside);
1106
- return () => document.removeEventListener("mousedown", closeOutside);
1107
- }, [open, onOpenChange]);
1132
+ document.addEventListener("keydown", escapeOutside);
1133
+ return () => {
1134
+ document.removeEventListener("mousedown", closeOutside);
1135
+ document.removeEventListener("keydown", escapeOutside);
1136
+ };
1137
+ }, [
1138
+ open,
1139
+ onOpenChange,
1140
+ pane,
1141
+ initialPane
1142
+ ]);
1108
1143
  (0, react.useEffect)(() => {
1109
1144
  const intent = paneFocus.current;
1110
1145
  if (!open || menuPos === null || intent === null) return;
@@ -1134,13 +1169,21 @@ function ModelMenu(props) {
1134
1169
  pane,
1135
1170
  filteredGroups
1136
1171
  ]);
1172
+ const modelPaneOpen = open && pane === "model";
1173
+ const scrollSignal = !modelPaneOpen ? "closed" : keyNav === 0 ? "open" : `key:${keyNav}`;
1174
+ const scrollSignalRef = (0, react.useRef)(null);
1137
1175
  (0, react.useLayoutEffect)(() => {
1138
- if (open && pane === "model" && activeModelIndex >= 0) itemRefs.current[leading.length + activeModelIndex]?.scrollIntoView({ block: "nearest" });
1176
+ if (!modelPaneOpen || activeModelIndex < 0) {
1177
+ if (!modelPaneOpen) scrollSignalRef.current = null;
1178
+ return;
1179
+ }
1180
+ if (scrollSignalRef.current === scrollSignal) return;
1181
+ scrollSignalRef.current = scrollSignal;
1182
+ itemRefs.current[leading.length + activeModelIndex]?.scrollIntoView({ block: "nearest" });
1139
1183
  }, [
1140
- open,
1141
- pane,
1184
+ modelPaneOpen,
1185
+ scrollSignal,
1142
1186
  activeModelIndex,
1143
- visibleModels,
1144
1187
  leading.length
1145
1188
  ]);
1146
1189
  (0, react.useLayoutEffect)(() => {
@@ -1155,7 +1198,7 @@ function ModelMenu(props) {
1155
1198
  let x = rect.right - lw, y = rect.top - 8 - lh;
1156
1199
  if (lw > 0) x = Math.min(Math.max(x, margin), window.innerWidth - lw - margin);
1157
1200
  if (lh > 0) y = Math.min(Math.max(y, margin), window.innerHeight - lh - margin);
1158
- setMenuPos({
1201
+ setMenuPos((prev) => prev && prev.left === x && prev.top === y ? prev : {
1159
1202
  left: x,
1160
1203
  top: y
1161
1204
  });
@@ -1175,6 +1218,13 @@ function ModelMenu(props) {
1175
1218
  ]);
1176
1219
  function onKeyDown(event) {
1177
1220
  if (event.defaultPrevented || event.nativeEvent.isComposing) return;
1221
+ if (event.key === "Escape" && open) {
1222
+ event.preventDefault();
1223
+ if (pane !== initialPane) back(pane);
1224
+ else close(true);
1225
+ return;
1226
+ }
1227
+ if (disabled) return;
1178
1228
  if (!open && [
1179
1229
  "Enter",
1180
1230
  " ",
@@ -1188,17 +1238,12 @@ function ModelMenu(props) {
1188
1238
  requestOpen();
1189
1239
  return;
1190
1240
  }
1191
- if (event.key === "Escape" && open) {
1192
- event.preventDefault();
1193
- if (pane !== "root") back(pane);
1194
- else close(true);
1195
- return;
1196
- }
1197
1241
  if (!open) return;
1198
1242
  if (pane === "model" && showSearch && (event.key === "ArrowDown" || event.key === "ArrowUp")) {
1199
1243
  event.preventDefault();
1200
1244
  if (visibleModels.length > 0) {
1201
1245
  setHighlightedIndex((activeModelIndex + (event.key === "ArrowDown" ? 1 : -1) + visibleModels.length) % visibleModels.length);
1246
+ setKeyNav((n) => n + 1);
1202
1247
  searchRef.current?.focus();
1203
1248
  }
1204
1249
  return;
@@ -1207,19 +1252,19 @@ function ModelMenu(props) {
1207
1252
  if (event.key === "Tab" && visibleModels.length === 0) return;
1208
1253
  event.preventDefault();
1209
1254
  const highlighted = visibleModels[activeModelIndex];
1210
- if (highlighted) {
1255
+ if (highlighted && !busy) {
1211
1256
  onPick({
1212
1257
  provider: highlighted.provider,
1213
1258
  model: highlighted.model
1214
1259
  });
1215
- close(true);
1260
+ if (closeOnPick) close(true);
1216
1261
  }
1217
1262
  return;
1218
1263
  }
1219
1264
  if (event.key === "Tab") {
1220
1265
  if (event.shiftKey) {
1221
1266
  event.preventDefault();
1222
- if (pane !== "root") back(pane);
1267
+ if (pane !== initialPane) back(pane);
1223
1268
  else close(true);
1224
1269
  return;
1225
1270
  }
@@ -1243,7 +1288,10 @@ function ModelMenu(props) {
1243
1288
  }
1244
1289
  }
1245
1290
  function onBlur(event) {
1246
- if (event.relatedTarget instanceof Node && (rootRef.current?.contains(event.relatedTarget) || menuRef.current?.contains(event.relatedTarget))) return;
1291
+ const related = event.relatedTarget;
1292
+ if (related instanceof Node && (rootRef.current?.contains(related) || menuRef.current?.contains(related) || !related.isConnected)) return;
1293
+ if (!closeOnPick && !(related instanceof Node)) return;
1294
+ if (busy && related == null) return;
1247
1295
  if (open) changeOpen(false);
1248
1296
  }
1249
1297
  itemRefs.current = [];
@@ -1264,11 +1312,16 @@ function ModelMenu(props) {
1264
1312
  "aria-label": modelLabel,
1265
1313
  lang: language,
1266
1314
  children: [
1315
+ menuHeader ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1316
+ className: "dsh-model-menu-header",
1317
+ children: menuHeader
1318
+ }) : null,
1267
1319
  pane === "root" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1268
1320
  ref: itemRef(),
1269
1321
  type: "button",
1270
1322
  role: "menuitem",
1271
1323
  className: "dsh-model-menu-cell",
1324
+ disabled,
1272
1325
  onClick: () => drill("model"),
1273
1326
  children: [
1274
1327
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -1281,11 +1334,12 @@ function ModelMenu(props) {
1281
1334
  }),
1282
1335
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutlineRegular, { className: "dsh-model-menu-cellChevron" })
1283
1336
  ]
1284
- }), efforts.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1337
+ }), efforts.length > 0 && !reasoningSlider ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1285
1338
  ref: itemRef(),
1286
1339
  type: "button",
1287
1340
  role: "menuitem",
1288
1341
  className: "dsh-model-menu-cell",
1342
+ disabled,
1289
1343
  onClick: () => drill("effort"),
1290
1344
  children: [
1291
1345
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -1304,6 +1358,7 @@ function ModelMenu(props) {
1304
1358
  className: "dsh-model-menu-searchRow",
1305
1359
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
1306
1360
  ref: searchRef,
1361
+ disabled,
1307
1362
  className: `dsh-model-menu-search${query !== "" ? " dsh-model-menu-searchWithQuery" : ""}`,
1308
1363
  type: "text",
1309
1364
  role: "searchbox",
@@ -1320,6 +1375,7 @@ function ModelMenu(props) {
1320
1375
  type: "button",
1321
1376
  className: "dsh-model-menu-searchClear",
1322
1377
  "aria-label": clearSearchLabel,
1378
+ disabled,
1323
1379
  onClick: () => {
1324
1380
  setQuery("");
1325
1381
  setHighlightedIndex(null);
@@ -1342,10 +1398,12 @@ function ModelMenu(props) {
1342
1398
  type: "button",
1343
1399
  role: "menuitemradio",
1344
1400
  "aria-checked": item.selected === true,
1401
+ disabled: disabled || busy,
1345
1402
  className: `dsh-model-menu-option${item.selected === true ? " dsh-model-menu-selected" : ""}`,
1346
1403
  onClick: () => {
1404
+ if (disabled || busy) return;
1347
1405
  onPickLeading?.(item.id);
1348
- close(true);
1406
+ if (closeOnPick) close(true);
1349
1407
  },
1350
1408
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1351
1409
  className: "dsh-model-menu-optionCopy",
@@ -1371,6 +1429,7 @@ function ModelMenu(props) {
1371
1429
  "aria-checked": checked,
1372
1430
  id: `${id}-model-${index}`,
1373
1431
  tabIndex: showSearch ? -1 : 0,
1432
+ disabled: disabled || busy,
1374
1433
  onFocus: () => setHighlightedIndex(index),
1375
1434
  "data-highlighted": index === activeModelIndex ? "" : void 0,
1376
1435
  className: `dsh-model-menu-option dsh-model-menu-modelOption${checked ? " dsh-model-menu-selected" : ""}${index === activeModelIndex ? " dsh-model-menu-optionActive" : ""}`,
@@ -1380,11 +1439,12 @@ function ModelMenu(props) {
1380
1439
  },
1381
1440
  title: choice.label,
1382
1441
  onClick: () => {
1442
+ if (disabled || busy) return;
1383
1443
  onPick({
1384
1444
  provider: choice.provider,
1385
1445
  model: choice.model
1386
1446
  });
1387
- close(true);
1447
+ if (closeOnPick) close(true);
1388
1448
  },
1389
1449
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1390
1450
  className: "dsh-model-menu-optionCopy",
@@ -1400,6 +1460,14 @@ function ModelMenu(props) {
1400
1460
  })
1401
1461
  }, group.provider))]
1402
1462
  }),
1463
+ reasoningSlider && efforts.length > 0 && filteredGroups.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EffortSlider, {
1464
+ label: effortLabel,
1465
+ rows: effortRows,
1466
+ selected: selectedEffort ?? "",
1467
+ disabled,
1468
+ busy,
1469
+ onPick: (value) => onPickEffort?.(value)
1470
+ }) : null,
1403
1471
  filteredGroups.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1404
1472
  className: "dsh-model-menu-empty",
1405
1473
  role: "status",
@@ -1413,10 +1481,12 @@ function ModelMenu(props) {
1413
1481
  type: "button",
1414
1482
  role: "menuitemradio",
1415
1483
  "aria-checked": checked,
1484
+ disabled: disabled || busy,
1416
1485
  className: `dsh-model-menu-option${checked ? " dsh-model-menu-selected" : ""}`,
1417
1486
  onClick: () => {
1487
+ if (disabled || busy) return;
1418
1488
  onPickEffort?.(row.id || void 0);
1419
- close(true);
1489
+ if (closeOnPick) close(true);
1420
1490
  },
1421
1491
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1422
1492
  className: "dsh-model-menu-optionCopy",
@@ -1435,7 +1505,8 @@ function ModelMenu(props) {
1435
1505
  const trigger = (0, react.isValidElement)(anchor) ? (0, react.cloneElement)(anchor, {
1436
1506
  "aria-haspopup": "menu",
1437
1507
  "aria-expanded": open,
1438
- "aria-controls": open ? `${id}-menu` : void 0
1508
+ "aria-controls": open ? `${id}-menu` : void 0,
1509
+ "aria-disabled": disabled || void 0
1439
1510
  }) : anchor;
1440
1511
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1441
1512
  ref: rootRef,
@@ -1443,7 +1514,7 @@ function ModelMenu(props) {
1443
1514
  onKeyDown,
1444
1515
  onBlur,
1445
1516
  onClick: (event) => {
1446
- if (event.defaultPrevented) return;
1517
+ if (disabled || event.defaultPrevented) return;
1447
1518
  const button = event.target instanceof Element ? event.target.closest("button") : null;
1448
1519
  if (!(button instanceof HTMLButtonElement) || button.disabled || !rootRef.current?.contains(button)) return;
1449
1520
  requestOpen();
@@ -1455,6 +1526,102 @@ function ModelMenu(props) {
1455
1526
  });
1456
1527
  }
1457
1528
  /**
1529
+ * Discrete reasoning-effort slider shown under the model list in enhanced mode.
1530
+ *
1531
+ * A native range input (`step=1`) over exactly the rows the caller supplies —
1532
+ * the default row plus each effort the selected model advertises — so it can
1533
+ * never invent a level. It previews locally while dragging or arrowing and
1534
+ * commits only when the gesture ends (pointer up, key up, or blur), so a drag
1535
+ * does not write on every step. The menu stays open, so adjusting the effort
1536
+ * never loses the model selection or the search query.
1537
+ *
1538
+ * The input itself stays the entire interaction and accessibility surface —
1539
+ * keyboard, ARIA, disabled and the native step snapping all come from it —
1540
+ * while the track, gradient fill and thumb below are decorative layers painted
1541
+ * from the `--_p` percentage the preview position sets on the rail. Value
1542
+ * changes only update that custom property, so the CSS transitions animate
1543
+ * between levels without remounting nodes or restarting an animation.
1544
+ */
1545
+ function EffortSlider(props) {
1546
+ const { label, rows, selected, disabled = false, busy = false, onPick } = props;
1547
+ const active = Math.max(0, rows.findIndex((row) => row.id === selected));
1548
+ const [position, setPosition] = (0, react.useState)(active);
1549
+ const positionRef = (0, react.useRef)(position);
1550
+ (0, react.useEffect)(() => {
1551
+ positionRef.current = active;
1552
+ setPosition(active);
1553
+ }, [active]);
1554
+ const preview = (next) => {
1555
+ positionRef.current = next;
1556
+ setPosition(next);
1557
+ };
1558
+ const commit = () => {
1559
+ if (disabled || busy) return;
1560
+ const row = rows[positionRef.current];
1561
+ if (row !== void 0) onPick(row.id === "" ? void 0 : row.id);
1562
+ };
1563
+ const max = Math.max(0, rows.length - 1);
1564
+ const percent = max > 0 ? Math.round(position / max * 100) : 0;
1565
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1566
+ className: "dsh-model-menu-sliderGroup",
1567
+ role: "group",
1568
+ "aria-label": label,
1569
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1570
+ className: "dsh-model-menu-sliderHead",
1571
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1572
+ className: "dsh-model-menu-sliderTitle",
1573
+ children: label
1574
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1575
+ className: "dsh-model-menu-sliderValue",
1576
+ children: rows[position]?.name
1577
+ })]
1578
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1579
+ className: "dsh-model-menu-sliderRail",
1580
+ style: { "--_p": percent },
1581
+ children: [
1582
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1583
+ type: "range",
1584
+ className: "dsh-model-menu-sliderInput",
1585
+ min: 0,
1586
+ max,
1587
+ step: 1,
1588
+ value: position,
1589
+ disabled,
1590
+ "aria-label": label,
1591
+ "aria-disabled": disabled || busy || void 0,
1592
+ "aria-valuetext": rows[position]?.name,
1593
+ onChange: (event) => {
1594
+ const value = Number(event.target.value);
1595
+ if (Number.isFinite(value)) preview(value);
1596
+ },
1597
+ onPointerUp: commit,
1598
+ onKeyUp: commit,
1599
+ onBlur: commit
1600
+ }),
1601
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1602
+ className: "dsh-model-menu-sliderTrack",
1603
+ "aria-hidden": "true",
1604
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1605
+ className: "dsh-model-menu-sliderFill",
1606
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dsh-model-menu-sliderFillBar" })
1607
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1608
+ className: "dsh-model-menu-sliderNodes",
1609
+ children: rows.map((row, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1610
+ "data-covered": index <= position ? "" : void 0,
1611
+ className: "dsh-model-menu-sliderNode",
1612
+ style: { "--_n": max > 0 ? Math.round(index / max * 100) : 0 }
1613
+ }, row.id || "__default"))
1614
+ })]
1615
+ }),
1616
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1617
+ className: "dsh-model-menu-sliderThumb",
1618
+ "aria-hidden": "true"
1619
+ })
1620
+ ]
1621
+ })]
1622
+ });
1623
+ }
1624
+ /**
1458
1625
  * ModelMenu styles, transcribed from the official ModelSelect.module.css (the
1459
1626
  * published dsh-client-ui-model-selection bundle): card capped at
1460
1627
  * min(360px, 100vh - 96px), 34px option cells at 13/18 with 5px 7px padding,
@@ -1513,6 +1680,7 @@ const modelMenuCss = `
1513
1680
  overflow: hidden;
1514
1681
  }
1515
1682
  .dsh-model-menu-status, .dsh-model-menu-empty { color: var(--dsw-alias-label-tertiary); padding: 8px; font-size: 12px; line-height: 18px; }
1683
+ .dsh-model-menu-header { flex: none; padding: 2px 2px 4px; }
1516
1684
  .dsh-model-menu-searchRow { flex-shrink: 0; margin: 2px 0 3px; position: relative; }
1517
1685
  .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; }
1518
1686
  .dsh-model-menu-searchRow .dsh-model-menu-search:focus-within { border-color: transparent; }
@@ -1591,6 +1759,145 @@ const modelMenuCss = `
1591
1759
  .dsh-model-menu-cellLabel { white-space: nowrap; flex: none; }
1592
1760
  .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; }
1593
1761
  .dsh-model-menu-cellChevron { width: 12px; height: 12px; color: var(--dsw-alias-menu-icon); flex: none; }
1762
+ /* Discrete reasoning-effort slider under the model list (enhanced mode).
1763
+ * The painted control is layered over a fully functional native range input:
1764
+ * the input stays on top, transparent, and owns pointer/keyboard/ARIA, while
1765
+ * the decorative track, fill and thumb render beneath it at pointer-events:
1766
+ * none. Position lives in one custom property (--_p, 0–100) on the rail, so
1767
+ * the only animated work a level change triggers is a clip-path/left
1768
+ * transition — no keyframes, no element churn. */
1769
+ .dsh-model-menu-sliderGroup { border-top: 0.5px solid var(--dsw-alias-border-l2); flex: none; margin-top: 4px; padding: 6px 8px 8px; }
1770
+ .dsh-model-menu-sliderHead { align-items: baseline; justify-content: space-between; gap: 8px; margin: 0 2px 4px; display: flex; }
1771
+ .dsh-model-menu-sliderTitle { color: var(--dsw-alias-label-tertiary); font-size: 12px; line-height: 18px; }
1772
+ .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); }
1773
+ .dsh-model-menu-sliderRail { position: relative; direction: ltr; height: 38px; margin-top: 2px; }
1774
+ .dsh-model-menu-sliderRail:has(:disabled) { opacity: 0.45; }
1775
+ .dsh-model-menu-sliderInput {
1776
+ appearance: none;
1777
+ -webkit-appearance: none;
1778
+ box-sizing: border-box;
1779
+ border: 0;
1780
+ cursor: pointer;
1781
+ direction: ltr;
1782
+ opacity: 0;
1783
+ margin: 0;
1784
+ padding: 0;
1785
+ width: 100%;
1786
+ height: 100%;
1787
+ position: absolute;
1788
+ inset: 0;
1789
+ z-index: 1;
1790
+ touch-action: pan-y;
1791
+ }
1792
+ .dsh-model-menu-sliderInput:disabled { cursor: default; }
1793
+ /* The transparent input must keep the painted thumb's geometry: an
1794
+ * appearance:none webkit thumb defaults to 16px, which would shift the
1795
+ * end-point positions 7px off the decorative 30px disc and misplace the
1796
+ * native drag hit target. Track gets the full rail height so the whole band
1797
+ * maps to values identically. */
1798
+ .dsh-model-menu-sliderInput::-webkit-slider-runnable-track { width: 100%; height: 100%; }
1799
+ .dsh-model-menu-sliderInput::-webkit-slider-thumb { appearance: none; -webkit-appearance: none; width: 30px; height: 30px; margin: 0; }
1800
+ .dsh-model-menu-sliderInput::-moz-range-track { width: 100%; height: 100%; }
1801
+ .dsh-model-menu-sliderInput::-moz-range-thumb { width: 30px; height: 30px; margin: 0; border: 0; }
1802
+ .dsh-model-menu-sliderTrack {
1803
+ pointer-events: none;
1804
+ border-radius: 999px;
1805
+ width: 100%;
1806
+ height: 26px;
1807
+ background: var(--dsw-alias-interactive-bg-hover);
1808
+ box-shadow: inset 0 0 0 0.5px var(--dsw-alias-border-l2);
1809
+ position: absolute;
1810
+ inset-inline: 0;
1811
+ top: 50%;
1812
+ transform: translateY(-50%);
1813
+ /* No overflow:hidden — it would clip the fill's drop-shadow glow; the bar's
1814
+ * own clip-path already supplies the rounded reveal shape. */
1815
+ }
1816
+ /* The accent ramp paints the full track width once and is revealed by an
1817
+ * animated clip-path, so raising the effort unwraps more of the spectrum
1818
+ * instead of squashing the same gradient into a narrower bar. The glow lives
1819
+ * on the un-clipped parent: a filter on the clipped element itself would be
1820
+ * clipped away with it. */
1821
+ .dsh-model-menu-sliderFill {
1822
+ display: block;
1823
+ height: 100%;
1824
+ filter: drop-shadow(0 0 5px var(--dsw-alias-state-business-primary));
1825
+ transition: filter var(--ds-transition-duration) var(--ds-ease-in-out);
1826
+ }
1827
+ .dsh-model-menu-sliderInput:hover:not(:disabled) ~ .dsh-model-menu-sliderTrack .dsh-model-menu-sliderFill,
1828
+ .dsh-model-menu-sliderInput:active:not(:disabled) ~ .dsh-model-menu-sliderTrack .dsh-model-menu-sliderFill {
1829
+ filter: drop-shadow(0 0 9px var(--dsw-alias-state-business-primary));
1830
+ }
1831
+ /* The fill stops at the thumb's center, not at the track edge — at a 30px
1832
+ * thumb the end stops sit 15px in from each side, matching the node dots. */
1833
+ .dsh-model-menu-sliderFillBar {
1834
+ display: block;
1835
+ height: 100%;
1836
+ background: linear-gradient(90deg,
1837
+ var(--dsw-alias-state-business-primary),
1838
+ var(--dsw-alias-state-success-primary),
1839
+ var(--dsw-alias-state-warn-primary),
1840
+ var(--dsw-alias-state-error-primary));
1841
+ clip-path: inset(0 calc(100% - 15px - (100% - 30px) * var(--_p, 0) / 100) 0 0 round 999px);
1842
+ transition: clip-path var(--ds-transition-duration) var(--ds-ease-in-out);
1843
+ }
1844
+ /* Stop dots: one per declared row (default + every advertised effort), evenly
1845
+ * spread across the band between the two thumb-center extremes (inset 15px,
1846
+ * i.e. half the 30px thumb). --_n is 0-100 along that band. A covered dot
1847
+ * reads as a pale pin through the gradient; an uncovered one stays a neutral
1848
+ * notch on the empty track. */
1849
+ .dsh-model-menu-sliderNodes {
1850
+ pointer-events: none;
1851
+ display: block;
1852
+ position: absolute;
1853
+ inset-inline: 15px;
1854
+ inset-block: 0;
1855
+ }
1856
+ .dsh-model-menu-sliderNode {
1857
+ pointer-events: none;
1858
+ box-sizing: border-box;
1859
+ width: 4px;
1860
+ height: 4px;
1861
+ border-radius: 50%;
1862
+ background: color-mix(in srgb, var(--dsw-alias-label-secondary) 35%, transparent);
1863
+ position: absolute;
1864
+ left: calc(var(--_n, 0) * 1%);
1865
+ top: 50%;
1866
+ transform: translate(-50%, -50%);
1867
+ transition: background-color var(--ds-transition-duration) var(--ds-ease-in-out),
1868
+ transform var(--ds-transition-duration) var(--ds-ease-in-out);
1869
+ }
1870
+ .dsh-model-menu-sliderNode[data-covered] { background: color-mix(in srgb, var(--dsw-alias-label-primary) 60%, transparent); }
1871
+ .dsh-model-menu-sliderThumb {
1872
+ pointer-events: none;
1873
+ box-sizing: border-box;
1874
+ border-radius: 50%;
1875
+ width: 30px;
1876
+ height: 30px;
1877
+ background: var(--dsw-alias-button-primary-fill);
1878
+ border: 0.5px solid var(--dsw-alias-border-l4);
1879
+ box-shadow: 0 0 0 3px var(--dsw-alias-interactive-bg-hover), var(--dsw-elevation-soft);
1880
+ position: absolute;
1881
+ left: calc((100% - 30px) * var(--_p, 0) / 100);
1882
+ top: 50%;
1883
+ transform: translateY(-50%) scale(0.92);
1884
+ transition: left var(--ds-transition-duration) var(--ds-ease-in-out),
1885
+ transform var(--ds-transition-duration) var(--ds-ease-in-out),
1886
+ box-shadow var(--ds-transition-duration) var(--ds-ease-in-out);
1887
+ }
1888
+ .dsh-model-menu-sliderInput:hover:not(:disabled) ~ .dsh-model-menu-sliderThumb { transform: translateY(-50%) scale(1); }
1889
+ .dsh-model-menu-sliderInput:active:not(:disabled) ~ .dsh-model-menu-sliderThumb {
1890
+ transform: translateY(-50%) scale(1.1);
1891
+ box-shadow: 0 0 0 4px var(--dsw-alias-interactive-bg-hover), 0 0 12px var(--dsw-alias-state-business-primary);
1892
+ }
1893
+ .dsh-model-menu-sliderInput:focus-visible ~ .dsh-model-menu-sliderThumb {
1894
+ box-shadow: 0 0 0 3px var(--dsw-alias-interactive-bg-hover),
1895
+ 0 0 0 calc(3px + var(--dsw-focus-ring-width)) var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
1896
+ }
1897
+ @media (prefers-reduced-motion: reduce) {
1898
+ .dsh-model-menu-sliderFill, .dsh-model-menu-sliderFillBar, .dsh-model-menu-sliderThumb,
1899
+ .dsh-model-menu-sliderNode, .dsh-model-menu-sliderValue { transition: none; }
1900
+ }
1594
1901
  `;
1595
1902
  //#endregion
1596
1903
  //#region src/client-copy.ts
@@ -1676,6 +1983,95 @@ function hubMessage(message, language) {
1676
1983
  return hubMessages.find((pair) => pair.includes(message))?.[language === "zh" ? 0 : 1] ?? message;
1677
1984
  }
1678
1985
  //#endregion
1986
+ //#region ../dsh-plugin-kit/src/model-picker.tsx
1987
+ /**
1988
+ * Optional shared model picker (`klarkxy.model-picker` Factory).
1989
+ *
1990
+ * `@klarkxy/dsh-model-picker`, when installed, registers one root-scoped
1991
+ * Component Factory under `MODEL_PICKER_FACTORY`: an enhanced model menu with
1992
+ * a searchable provider-grouped list and a reasoning-effort slider, built on
1993
+ * the same ModelMenu recipe. Consumers keep their own ModelMenu as the
1994
+ * `fallback`; when no definition is registered the renderer resolves that
1995
+ * fallback verbatim, so the shared picker is strictly opt-in and every saved
1996
+ * value keeps its existing write path.
1997
+ *
1998
+ * `renderFactorySlot` is a framework-composed prop, never a context: the
1999
+ * component a plugin registers into any slot receives it automatically, but
2000
+ * the plugin must pass it down to whichever child renders the menu (or to the
2001
+ * render callback handed to registerSessionSettingsItem).
2002
+ *
2003
+ * The Factory manages its own open state; a consumer's `open`/`onOpenChange`
2004
+ * pair belongs to the fallback menu only and must not be fed to both.
2005
+ */
2006
+ /**
2007
+ * The trigger every consumer used to hand its own ModelMenu, rebuilt as the
2008
+ * factory's `anchor` local component: identical classes and chevron, the menu
2009
+ * state coming from the occurrence's props instead of local open state.
2010
+ *
2011
+ * `ariaLabel` and `title` are the whole accessible name and tooltip (the
2012
+ * caller formats "field: current route" itself because the factory's resolved
2013
+ * caption may intentionally be a short name). The component identity stays
2014
+ * put across label changes — a new function would unmount the trigger, and
2015
+ * its cleanup would close a menu that `closeOnPick={false}` is keeping open.
2016
+ * The latest label, title, and `onClose` are read from refs.
2017
+ */
2018
+ function useModelMenuAnchor(ariaLabel, title, onClose) {
2019
+ const closeRef = (0, react.useRef)(onClose);
2020
+ const labelRef = (0, react.useRef)(ariaLabel);
2021
+ const titleRef = (0, react.useRef)(title);
2022
+ closeRef.current = onClose;
2023
+ labelRef.current = ariaLabel;
2024
+ titleRef.current = title;
2025
+ return (0, react.useMemo)(() => {
2026
+ const ModelPickerTrigger = (props) => {
2027
+ (0, react.useEffect)(() => () => closeRef.current?.(), []);
2028
+ const record = props;
2029
+ const injected = {};
2030
+ for (const [key, value] of Object.entries(record)) if (key.startsWith("aria-")) injected[key] = value;
2031
+ const extraClass = typeof record.className === "string" ? ` ${record.className}` : "";
2032
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2033
+ type: "button",
2034
+ className: `dsh-model-menu-trigger${extraClass}`,
2035
+ disabled: props.disabled,
2036
+ "aria-haspopup": "menu",
2037
+ "aria-expanded": props.open,
2038
+ ...injected,
2039
+ "aria-label": labelRef.current,
2040
+ title: titleRef.current,
2041
+ children: [
2042
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2043
+ className: "dsh-model-menu-triggerLabel",
2044
+ children: props.label
2045
+ }),
2046
+ props.effortLabel ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2047
+ className: "dsh-model-menu-triggerEffort",
2048
+ children: props.effortLabel
2049
+ }) : null,
2050
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${props.open ? " dsh-model-menu-chevronOpen" : ""}` })
2051
+ ]
2052
+ });
2053
+ };
2054
+ return ModelPickerTrigger;
2055
+ }, []);
2056
+ }
2057
+ /**
2058
+ * Render the shared model picker while it is registered; `options.fallback`
2059
+ * whenever it is not (plugin absent, disabled, or unloaded).
2060
+ *
2061
+ * `options.anchor` is optional: the factory ships a default trigger, so only
2062
+ * consumers that need their own chrome (custom aria-label, tooltip, class)
2063
+ * pass one built with {@link useModelMenuAnchor}. The returned node is never
2064
+ * assumed to be the factory's own output — a defensive `?? fallback` covers a
2065
+ * renderer that yields null for an unregistered definition.
2066
+ */
2067
+ function renderModelPicker(renderFactorySlot, props, options) {
2068
+ if (typeof renderFactorySlot !== "function") return options.fallback;
2069
+ return renderFactorySlot("klarkxy.model-picker", props, {
2070
+ ...options.anchor ? { slots: { ["anchor"]: options.anchor } } : {},
2071
+ fallback: options.fallback
2072
+ }) ?? options.fallback;
2073
+ }
2074
+ //#endregion
1679
2075
  //#region src/client-model-menu.tsx
1680
2076
  /**
1681
2077
  * One detected field's standard editor: the same ModelMenu every marked field gets.
@@ -1686,7 +2082,7 @@ function hubMessage(message, language) {
1686
2082
  * the current reasoning effort when the new model advertises it, so switching a
1687
2083
  * route does not silently reset the user's choice.
1688
2084
  */
1689
- function FieldMenu({ t, catalog, current, disabled, label, allowFollow = true, followLabel, reasoningEffort = true, display = "full", onSet }) {
2085
+ function FieldMenu({ t, catalog, current, disabled, label, allowFollow = true, followLabel, reasoningEffort = true, display = "full", onSet, renderFactorySlot }) {
1690
2086
  const [open, setOpen] = (0, react.useState)(false);
1691
2087
  (0, react.useEffect)(() => {
1692
2088
  if (disabled) setOpen(false);
@@ -1707,46 +2103,28 @@ function FieldMenu({ t, catalog, current, disabled, label, allowFollow = true, f
1707
2103
  const currentLabel = display === "short" && !follow ? selected ? modelMenuShortName(selected) : route.model : menuLabel;
1708
2104
  const selectedEffort = efforts.find((item) => item.id === (route.reasoningEffort || ""));
1709
2105
  const effortName = selectedEffort ? modelMenuEffortLabel(selectedEffort, t.language) : void 0;
1710
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
2106
+ const anchor = useModelMenuAnchor(accessible, routeLabel, () => setOpen(false));
2107
+ const pick = (picked) => {
2108
+ const choice = choices.find((item) => item.provider === picked.provider && item.model === picked.model);
2109
+ const effort = route.reasoningEffort;
2110
+ const supported = Boolean(effort) && Boolean(choice?.efforts.some((item) => item.id === effort));
2111
+ onSet({
2112
+ provider: picked.provider,
2113
+ model: picked.model,
2114
+ ...supported ? { reasoningEffort: effort } : {}
2115
+ });
2116
+ };
2117
+ return renderModelPicker(renderFactorySlot, {
1711
2118
  locale: t.language,
1712
- open: open && !disabled,
2119
+ disabled,
1713
2120
  onOpenChange: (next) => setOpen(next && !disabled),
1714
2121
  listClassName: "dsh-model-hub",
1715
- anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1716
- type: "button",
1717
- className: "dsh-model-menu-trigger",
1718
- disabled,
1719
- "aria-label": accessible,
1720
- title: routeLabel,
1721
- "aria-haspopup": "menu",
1722
- "aria-expanded": open && !disabled,
1723
- children: [
1724
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1725
- className: "dsh-model-menu-triggerLabel",
1726
- children: currentLabel
1727
- }),
1728
- !follow && efforts.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1729
- className: "dsh-model-menu-triggerEffort",
1730
- children: ` · ${effortName ?? t("effortDefault")}`
1731
- }) : null,
1732
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${open ? " dsh-model-menu-chevronOpen" : ""}` })
1733
- ]
1734
- }),
1735
2122
  choices,
1736
2123
  selected: follow ? void 0 : {
1737
2124
  provider: route.provider,
1738
2125
  model: route.model
1739
2126
  },
1740
- onPick: (picked) => {
1741
- const choice = choices.find((item) => item.provider === picked.provider && item.model === picked.model);
1742
- const effort = route.reasoningEffort;
1743
- const supported = Boolean(effort) && Boolean(choice?.efforts.some((item) => item.id === effort));
1744
- onSet({
1745
- provider: picked.provider,
1746
- model: picked.model,
1747
- ...supported ? { reasoningEffort: effort } : {}
1748
- });
1749
- },
2127
+ onPick: pick,
1750
2128
  leading: allowFollow ? [{
1751
2129
  id: "follow",
1752
2130
  label: followLabel || t("follow"),
@@ -1766,6 +2144,8 @@ function FieldMenu({ t, catalog, current, disabled, label, allowFollow = true, f
1766
2144
  model: route.model,
1767
2145
  ...effort ? { reasoningEffort: effort } : {}
1768
2146
  }),
2147
+ triggerLabel: currentLabel,
2148
+ triggerEffortLabel: !follow && efforts.length ? ` · ${effortName ?? t("effortDefault")}` : void 0,
1769
2149
  modelLabel: t("model"),
1770
2150
  modelValue: currentLabel,
1771
2151
  effortLabel: t("effort"),
@@ -1773,6 +2153,66 @@ function FieldMenu({ t, catalog, current, disabled, label, allowFollow = true, f
1773
2153
  searchPlaceholder: t("searchModels"),
1774
2154
  emptyLabel: t("noModels"),
1775
2155
  clearSearchLabel: t("clearSearch")
2156
+ }, {
2157
+ anchor,
2158
+ fallback: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
2159
+ locale: t.language,
2160
+ open: open && !disabled,
2161
+ onOpenChange: (next) => setOpen(next && !disabled),
2162
+ listClassName: "dsh-model-hub",
2163
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2164
+ type: "button",
2165
+ className: "dsh-model-menu-trigger",
2166
+ disabled,
2167
+ "aria-label": accessible,
2168
+ title: routeLabel,
2169
+ "aria-haspopup": "menu",
2170
+ "aria-expanded": open && !disabled,
2171
+ children: [
2172
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2173
+ className: "dsh-model-menu-triggerLabel",
2174
+ children: currentLabel
2175
+ }),
2176
+ !follow && efforts.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2177
+ className: "dsh-model-menu-triggerEffort",
2178
+ children: ` · ${effortName ?? t("effortDefault")}`
2179
+ }) : null,
2180
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${open ? " dsh-model-menu-chevronOpen" : ""}` })
2181
+ ]
2182
+ }),
2183
+ choices,
2184
+ selected: follow ? void 0 : {
2185
+ provider: route.provider,
2186
+ model: route.model
2187
+ },
2188
+ onPick: pick,
2189
+ leading: allowFollow ? [{
2190
+ id: "follow",
2191
+ label: followLabel || t("follow"),
2192
+ selected: follow
2193
+ }] : [],
2194
+ onPickLeading: () => onSet({
2195
+ provider: "",
2196
+ model: ""
2197
+ }),
2198
+ efforts,
2199
+ selectedEffort: route.reasoningEffort || "",
2200
+ onPickEffort: (effort) => onSet(follow ? {
2201
+ provider: "",
2202
+ model: ""
2203
+ } : {
2204
+ provider: route.provider,
2205
+ model: route.model,
2206
+ ...effort ? { reasoningEffort: effort } : {}
2207
+ }),
2208
+ modelLabel: t("model"),
2209
+ modelValue: currentLabel,
2210
+ effortLabel: t("effort"),
2211
+ defaultEffortLabel: t("effortDefault"),
2212
+ searchPlaceholder: t("searchModels"),
2213
+ emptyLabel: t("noModels"),
2214
+ clearSearchLabel: t("clearSearch")
2215
+ })
1776
2216
  });
1777
2217
  }
1778
2218
  function messageOf(error) {
@@ -1908,7 +2348,7 @@ function routeInput(route) {
1908
2348
  };
1909
2349
  }
1910
2350
  /** One cast entry: model picker, tendency input saved on blur, remove button. */
1911
- function CastRow({ t, catalog, route, tendencyRevision, draftReview, disabled, tendencyDisabled, menuDisabled, onSetRoute, onSetTendency, onRemove }) {
2351
+ function CastRow({ t, catalog, route, tendencyRevision, draftReview, disabled, tendencyDisabled, menuDisabled, renderFactorySlot, onSetRoute, onSetTendency, onRemove }) {
1912
2352
  const label = `${route.provider} / ${route.model}`;
1913
2353
  const saved = route.tendency ?? "";
1914
2354
  const [draft, setDraft] = (0, react.useState)({
@@ -1951,6 +2391,7 @@ function CastRow({ t, catalog, route, tendencyRevision, draftReview, disabled, t
1951
2391
  reasoningEffort: true,
1952
2392
  display: "short",
1953
2393
  disabled: menuDisabled,
2394
+ renderFactorySlot,
1954
2395
  onSet: onSetRoute
1955
2396
  })
1956
2397
  }),
@@ -1987,7 +2428,7 @@ function CastRow({ t, catalog, route, tendencyRevision, draftReview, disabled, t
1987
2428
  ]
1988
2429
  });
1989
2430
  }
1990
- function CastPage({ api, locale }) {
2431
+ function CastPage({ api, locale, renderFactorySlot }) {
1991
2432
  const t = useText$1(locale);
1992
2433
  const [cast, setCast] = (0, react.useState)(null);
1993
2434
  const [catalog, setCatalog] = (0, react.useState)(null);
@@ -2223,6 +2664,7 @@ function CastPage({ api, locale }) {
2223
2664
  t,
2224
2665
  catalog,
2225
2666
  route,
2667
+ renderFactorySlot,
2226
2668
  tendencyRevision: cast.tendencyRevision,
2227
2669
  draftReview,
2228
2670
  tendencyDisabled: disabled || !!cast.tendencyError || cast.tendencyRevision === void 0,
@@ -2253,6 +2695,7 @@ function CastPage({ api, locale }) {
2253
2695
  reasoningEffort: true,
2254
2696
  display: "short",
2255
2697
  disabled: disabled || catalog === null,
2698
+ renderFactorySlot,
2256
2699
  onSet: (picked) => {
2257
2700
  if (!picked.provider || !picked.model) {
2258
2701
  setPending(void 0);
@@ -3152,13 +3595,14 @@ function localeActive(locale) {
3152
3595
  return String(typeof locale === "string" ? locale : locale?.getSnapshot?.()?.active ?? "en");
3153
3596
  }
3154
3597
  const css = `${officialUiCss("dsh-model-hub")}${modelMenuCss}`;
3155
- function SubagentModelsPage({ api, locale }) {
3598
+ function SubagentModelsPage({ api, locale, renderFactorySlot }) {
3156
3599
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
3157
3600
  className: "dsh-model-hub dsh-ui-page",
3158
3601
  "aria-label": localeActive(locale).toLowerCase().startsWith("zh") ? "子代理" : "Subagents",
3159
3602
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: css }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CastPage, {
3160
3603
  api,
3161
- locale
3604
+ locale,
3605
+ renderFactorySlot
3162
3606
  })]
3163
3607
  });
3164
3608
  }