@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.
@@ -92,7 +92,9 @@ Object.values({
92
92
  "--dsh-scrollbar-thumb-hover",
93
93
  "--dsh-content-font-size-secondary",
94
94
  "--dsh-content-font-delta",
95
- "--dsh-frame-top-clearance"
95
+ "--dsh-frame-top-clearance",
96
+ "--dsh-composer-model-icon-display",
97
+ "--dsh-composer-model-text-display"
96
98
  ]
97
99
  }).flat();
98
100
  /**
@@ -540,6 +542,7 @@ function parseModelMenuChoices(value, bound) {
540
542
  provider,
541
543
  model,
542
544
  label: `${providerName} / ${name}`,
545
+ providerLabel: providerName,
543
546
  efforts
544
547
  });
545
548
  }
@@ -586,11 +589,11 @@ function modelMenuShortName(choice) {
586
589
  /** Group catalog choices by provider and filter them by a case-insensitive query. */
587
590
  function groupModelMenuChoices(choices, query) {
588
591
  const needle = query.trim().toLowerCase();
589
- const matched = needle ? choices.filter((choice) => choice.label.toLowerCase().includes(needle) || choice.model.toLowerCase().includes(needle) || choice.provider.toLowerCase().includes(needle)) : choices;
592
+ 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;
590
593
  const groups = [];
591
594
  for (const choice of matched) {
592
595
  const sep = choice.label.indexOf(" / ");
593
- const name = sep > 0 ? choice.label.slice(0, sep) : choice.provider;
596
+ const name = choice.providerLabel ?? (sep > 0 ? choice.label.slice(0, sep) : choice.provider);
594
597
  const last = groups.at(-1);
595
598
  if (last && last.provider === choice.provider) last.items.push(choice);
596
599
  else groups.push({
@@ -976,7 +979,7 @@ function ModelMenu(props) {
976
979
  empty: "No matching models",
977
980
  clear: "Clear search"
978
981
  };
979
- 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;
982
+ 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;
980
983
  const rootRef = (0, react.useRef)(null);
981
984
  const openRef = (0, react.useRef)(open);
982
985
  openRef.current = open;
@@ -991,9 +994,10 @@ function ModelMenu(props) {
991
994
  const [pane, setPane] = (0, react.useState)("root");
992
995
  const [query, setQuery] = (0, react.useState)("");
993
996
  const [highlightedIndex, setHighlightedIndex] = (0, react.useState)(null);
997
+ const [keyNav, setKeyNav] = (0, react.useState)(0);
994
998
  const [menuPos, setMenuPos] = (0, react.useState)(null);
995
999
  const id = (0, react.useId)();
996
- const showSearch = choices.length > 4;
1000
+ const showSearch = alwaysShowSearch || choices.length > 4;
997
1001
  const effortRows = (0, react.useMemo)(() => [{
998
1002
  id: "",
999
1003
  name: defaultEffortLabel
@@ -1010,7 +1014,12 @@ function ModelMenu(props) {
1010
1014
  items: (0, _deepseek_ai_dsh_client_ui_primitives.rankByName)(group.items.map((choice) => ({
1011
1015
  choice,
1012
1016
  name: modelMenuShortName(choice),
1013
- label: choice.model
1017
+ label: [
1018
+ choice.provider,
1019
+ choice.providerLabel,
1020
+ choice.model,
1021
+ choice.label
1022
+ ].filter(Boolean).join(" ")
1014
1023
  })), showSearch ? query.trim() : "")
1015
1024
  })).filter((group) => group.items.length > 0), [
1016
1025
  choices,
@@ -1042,10 +1051,12 @@ function ModelMenu(props) {
1042
1051
  paneFocus.current = "drill";
1043
1052
  setPane(next);
1044
1053
  };
1045
- /** Leave a drilled pane for the root one, handing the keyboard back to its cell. */
1054
+ /** Leave a drilled pane for the entry pane, handing the keyboard back to it.
1055
+ * A root entry refocuses the cell that opened the pane; a search-first
1056
+ * (model) entry refocuses that pane's own intent. */
1046
1057
  const back = (from) => {
1047
- paneFocus.current = from;
1048
- setPane("root");
1058
+ paneFocus.current = initialPane === "root" ? from : "drill";
1059
+ setPane(initialPane);
1049
1060
  };
1050
1061
  const moveFocus = (offset) => {
1051
1062
  const items = itemRefs.current.filter((item) => item !== null && !item.disabled);
@@ -1085,10 +1096,18 @@ function ModelMenu(props) {
1085
1096
  }, [requestedOpen, open]);
1086
1097
  (0, react.useEffect)(() => {
1087
1098
  if (!open) return;
1088
- setPane("root");
1099
+ setPane(initialPane);
1100
+ if (initialPane === "model") paneFocus.current = "drill";
1089
1101
  setQuery("");
1090
1102
  setHighlightedIndex(null);
1091
- }, [open]);
1103
+ }, [open, initialPane]);
1104
+ (0, react.useEffect)(() => {
1105
+ if (disabled && open && closeOnDisable) changeOpen(false);
1106
+ }, [
1107
+ disabled,
1108
+ open,
1109
+ closeOnDisable
1110
+ ]);
1092
1111
  (0, react.useEffect)(() => {
1093
1112
  if (!open) return;
1094
1113
  const closeOutside = (event) => {
@@ -1096,9 +1115,25 @@ function ModelMenu(props) {
1096
1115
  if (rootRef.current?.contains(target) || menuRef.current?.contains(target)) return;
1097
1116
  changeOpen(false);
1098
1117
  };
1118
+ const escapeOutside = (event) => {
1119
+ if (event.key !== "Escape" || event.defaultPrevented || event.isComposing) return;
1120
+ if (!openRef.current) return;
1121
+ event.preventDefault();
1122
+ if (pane !== initialPane) back(pane);
1123
+ else close(true);
1124
+ };
1099
1125
  document.addEventListener("mousedown", closeOutside);
1100
- return () => document.removeEventListener("mousedown", closeOutside);
1101
- }, [open, onOpenChange]);
1126
+ document.addEventListener("keydown", escapeOutside);
1127
+ return () => {
1128
+ document.removeEventListener("mousedown", closeOutside);
1129
+ document.removeEventListener("keydown", escapeOutside);
1130
+ };
1131
+ }, [
1132
+ open,
1133
+ onOpenChange,
1134
+ pane,
1135
+ initialPane
1136
+ ]);
1102
1137
  (0, react.useEffect)(() => {
1103
1138
  const intent = paneFocus.current;
1104
1139
  if (!open || menuPos === null || intent === null) return;
@@ -1128,13 +1163,21 @@ function ModelMenu(props) {
1128
1163
  pane,
1129
1164
  filteredGroups
1130
1165
  ]);
1166
+ const modelPaneOpen = open && pane === "model";
1167
+ const scrollSignal = !modelPaneOpen ? "closed" : keyNav === 0 ? "open" : `key:${keyNav}`;
1168
+ const scrollSignalRef = (0, react.useRef)(null);
1131
1169
  (0, react.useLayoutEffect)(() => {
1132
- if (open && pane === "model" && activeModelIndex >= 0) itemRefs.current[leading.length + activeModelIndex]?.scrollIntoView({ block: "nearest" });
1170
+ if (!modelPaneOpen || activeModelIndex < 0) {
1171
+ if (!modelPaneOpen) scrollSignalRef.current = null;
1172
+ return;
1173
+ }
1174
+ if (scrollSignalRef.current === scrollSignal) return;
1175
+ scrollSignalRef.current = scrollSignal;
1176
+ itemRefs.current[leading.length + activeModelIndex]?.scrollIntoView({ block: "nearest" });
1133
1177
  }, [
1134
- open,
1135
- pane,
1178
+ modelPaneOpen,
1179
+ scrollSignal,
1136
1180
  activeModelIndex,
1137
- visibleModels,
1138
1181
  leading.length
1139
1182
  ]);
1140
1183
  (0, react.useLayoutEffect)(() => {
@@ -1149,7 +1192,7 @@ function ModelMenu(props) {
1149
1192
  let x = rect.right - lw, y = rect.top - 8 - lh;
1150
1193
  if (lw > 0) x = Math.min(Math.max(x, margin), window.innerWidth - lw - margin);
1151
1194
  if (lh > 0) y = Math.min(Math.max(y, margin), window.innerHeight - lh - margin);
1152
- setMenuPos({
1195
+ setMenuPos((prev) => prev && prev.left === x && prev.top === y ? prev : {
1153
1196
  left: x,
1154
1197
  top: y
1155
1198
  });
@@ -1169,6 +1212,13 @@ function ModelMenu(props) {
1169
1212
  ]);
1170
1213
  function onKeyDown(event) {
1171
1214
  if (event.defaultPrevented || event.nativeEvent.isComposing) return;
1215
+ if (event.key === "Escape" && open) {
1216
+ event.preventDefault();
1217
+ if (pane !== initialPane) back(pane);
1218
+ else close(true);
1219
+ return;
1220
+ }
1221
+ if (disabled) return;
1172
1222
  if (!open && [
1173
1223
  "Enter",
1174
1224
  " ",
@@ -1182,17 +1232,12 @@ function ModelMenu(props) {
1182
1232
  requestOpen();
1183
1233
  return;
1184
1234
  }
1185
- if (event.key === "Escape" && open) {
1186
- event.preventDefault();
1187
- if (pane !== "root") back(pane);
1188
- else close(true);
1189
- return;
1190
- }
1191
1235
  if (!open) return;
1192
1236
  if (pane === "model" && showSearch && (event.key === "ArrowDown" || event.key === "ArrowUp")) {
1193
1237
  event.preventDefault();
1194
1238
  if (visibleModels.length > 0) {
1195
1239
  setHighlightedIndex((activeModelIndex + (event.key === "ArrowDown" ? 1 : -1) + visibleModels.length) % visibleModels.length);
1240
+ setKeyNav((n) => n + 1);
1196
1241
  searchRef.current?.focus();
1197
1242
  }
1198
1243
  return;
@@ -1201,19 +1246,19 @@ function ModelMenu(props) {
1201
1246
  if (event.key === "Tab" && visibleModels.length === 0) return;
1202
1247
  event.preventDefault();
1203
1248
  const highlighted = visibleModels[activeModelIndex];
1204
- if (highlighted) {
1249
+ if (highlighted && !busy) {
1205
1250
  onPick({
1206
1251
  provider: highlighted.provider,
1207
1252
  model: highlighted.model
1208
1253
  });
1209
- close(true);
1254
+ if (closeOnPick) close(true);
1210
1255
  }
1211
1256
  return;
1212
1257
  }
1213
1258
  if (event.key === "Tab") {
1214
1259
  if (event.shiftKey) {
1215
1260
  event.preventDefault();
1216
- if (pane !== "root") back(pane);
1261
+ if (pane !== initialPane) back(pane);
1217
1262
  else close(true);
1218
1263
  return;
1219
1264
  }
@@ -1237,7 +1282,10 @@ function ModelMenu(props) {
1237
1282
  }
1238
1283
  }
1239
1284
  function onBlur(event) {
1240
- if (event.relatedTarget instanceof Node && (rootRef.current?.contains(event.relatedTarget) || menuRef.current?.contains(event.relatedTarget))) return;
1285
+ const related = event.relatedTarget;
1286
+ if (related instanceof Node && (rootRef.current?.contains(related) || menuRef.current?.contains(related) || !related.isConnected)) return;
1287
+ if (!closeOnPick && !(related instanceof Node)) return;
1288
+ if (busy && related == null) return;
1241
1289
  if (open) changeOpen(false);
1242
1290
  }
1243
1291
  itemRefs.current = [];
@@ -1258,11 +1306,16 @@ function ModelMenu(props) {
1258
1306
  "aria-label": modelLabel,
1259
1307
  lang: language,
1260
1308
  children: [
1309
+ menuHeader ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1310
+ className: "dsh-model-menu-header",
1311
+ children: menuHeader
1312
+ }) : null,
1261
1313
  pane === "root" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1262
1314
  ref: itemRef(),
1263
1315
  type: "button",
1264
1316
  role: "menuitem",
1265
1317
  className: "dsh-model-menu-cell",
1318
+ disabled,
1266
1319
  onClick: () => drill("model"),
1267
1320
  children: [
1268
1321
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -1275,11 +1328,12 @@ function ModelMenu(props) {
1275
1328
  }),
1276
1329
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutlineRegular, { className: "dsh-model-menu-cellChevron" })
1277
1330
  ]
1278
- }), efforts.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1331
+ }), efforts.length > 0 && !reasoningSlider ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1279
1332
  ref: itemRef(),
1280
1333
  type: "button",
1281
1334
  role: "menuitem",
1282
1335
  className: "dsh-model-menu-cell",
1336
+ disabled,
1283
1337
  onClick: () => drill("effort"),
1284
1338
  children: [
1285
1339
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -1298,6 +1352,7 @@ function ModelMenu(props) {
1298
1352
  className: "dsh-model-menu-searchRow",
1299
1353
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
1300
1354
  ref: searchRef,
1355
+ disabled,
1301
1356
  className: `dsh-model-menu-search${query !== "" ? " dsh-model-menu-searchWithQuery" : ""}`,
1302
1357
  type: "text",
1303
1358
  role: "searchbox",
@@ -1314,6 +1369,7 @@ function ModelMenu(props) {
1314
1369
  type: "button",
1315
1370
  className: "dsh-model-menu-searchClear",
1316
1371
  "aria-label": clearSearchLabel,
1372
+ disabled,
1317
1373
  onClick: () => {
1318
1374
  setQuery("");
1319
1375
  setHighlightedIndex(null);
@@ -1336,10 +1392,12 @@ function ModelMenu(props) {
1336
1392
  type: "button",
1337
1393
  role: "menuitemradio",
1338
1394
  "aria-checked": item.selected === true,
1395
+ disabled: disabled || busy,
1339
1396
  className: `dsh-model-menu-option${item.selected === true ? " dsh-model-menu-selected" : ""}`,
1340
1397
  onClick: () => {
1398
+ if (disabled || busy) return;
1341
1399
  onPickLeading?.(item.id);
1342
- close(true);
1400
+ if (closeOnPick) close(true);
1343
1401
  },
1344
1402
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1345
1403
  className: "dsh-model-menu-optionCopy",
@@ -1365,6 +1423,7 @@ function ModelMenu(props) {
1365
1423
  "aria-checked": checked,
1366
1424
  id: `${id}-model-${index}`,
1367
1425
  tabIndex: showSearch ? -1 : 0,
1426
+ disabled: disabled || busy,
1368
1427
  onFocus: () => setHighlightedIndex(index),
1369
1428
  "data-highlighted": index === activeModelIndex ? "" : void 0,
1370
1429
  className: `dsh-model-menu-option dsh-model-menu-modelOption${checked ? " dsh-model-menu-selected" : ""}${index === activeModelIndex ? " dsh-model-menu-optionActive" : ""}`,
@@ -1374,11 +1433,12 @@ function ModelMenu(props) {
1374
1433
  },
1375
1434
  title: choice.label,
1376
1435
  onClick: () => {
1436
+ if (disabled || busy) return;
1377
1437
  onPick({
1378
1438
  provider: choice.provider,
1379
1439
  model: choice.model
1380
1440
  });
1381
- close(true);
1441
+ if (closeOnPick) close(true);
1382
1442
  },
1383
1443
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1384
1444
  className: "dsh-model-menu-optionCopy",
@@ -1394,6 +1454,14 @@ function ModelMenu(props) {
1394
1454
  })
1395
1455
  }, group.provider))]
1396
1456
  }),
1457
+ reasoningSlider && efforts.length > 0 && filteredGroups.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EffortSlider, {
1458
+ label: effortLabel,
1459
+ rows: effortRows,
1460
+ selected: selectedEffort ?? "",
1461
+ disabled,
1462
+ busy,
1463
+ onPick: (value) => onPickEffort?.(value)
1464
+ }) : null,
1397
1465
  filteredGroups.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1398
1466
  className: "dsh-model-menu-empty",
1399
1467
  role: "status",
@@ -1407,10 +1475,12 @@ function ModelMenu(props) {
1407
1475
  type: "button",
1408
1476
  role: "menuitemradio",
1409
1477
  "aria-checked": checked,
1478
+ disabled: disabled || busy,
1410
1479
  className: `dsh-model-menu-option${checked ? " dsh-model-menu-selected" : ""}`,
1411
1480
  onClick: () => {
1481
+ if (disabled || busy) return;
1412
1482
  onPickEffort?.(row.id || void 0);
1413
- close(true);
1483
+ if (closeOnPick) close(true);
1414
1484
  },
1415
1485
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1416
1486
  className: "dsh-model-menu-optionCopy",
@@ -1429,7 +1499,8 @@ function ModelMenu(props) {
1429
1499
  const trigger = (0, react.isValidElement)(anchor) ? (0, react.cloneElement)(anchor, {
1430
1500
  "aria-haspopup": "menu",
1431
1501
  "aria-expanded": open,
1432
- "aria-controls": open ? `${id}-menu` : void 0
1502
+ "aria-controls": open ? `${id}-menu` : void 0,
1503
+ "aria-disabled": disabled || void 0
1433
1504
  }) : anchor;
1434
1505
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1435
1506
  ref: rootRef,
@@ -1437,7 +1508,7 @@ function ModelMenu(props) {
1437
1508
  onKeyDown,
1438
1509
  onBlur,
1439
1510
  onClick: (event) => {
1440
- if (event.defaultPrevented) return;
1511
+ if (disabled || event.defaultPrevented) return;
1441
1512
  const button = event.target instanceof Element ? event.target.closest("button") : null;
1442
1513
  if (!(button instanceof HTMLButtonElement) || button.disabled || !rootRef.current?.contains(button)) return;
1443
1514
  requestOpen();
@@ -1449,6 +1520,102 @@ function ModelMenu(props) {
1449
1520
  });
1450
1521
  }
1451
1522
  /**
1523
+ * Discrete reasoning-effort slider shown under the model list in enhanced mode.
1524
+ *
1525
+ * A native range input (`step=1`) over exactly the rows the caller supplies —
1526
+ * the default row plus each effort the selected model advertises — so it can
1527
+ * never invent a level. It previews locally while dragging or arrowing and
1528
+ * commits only when the gesture ends (pointer up, key up, or blur), so a drag
1529
+ * does not write on every step. The menu stays open, so adjusting the effort
1530
+ * never loses the model selection or the search query.
1531
+ *
1532
+ * The input itself stays the entire interaction and accessibility surface —
1533
+ * keyboard, ARIA, disabled and the native step snapping all come from it —
1534
+ * while the track, gradient fill and thumb below are decorative layers painted
1535
+ * from the `--_p` percentage the preview position sets on the rail. Value
1536
+ * changes only update that custom property, so the CSS transitions animate
1537
+ * between levels without remounting nodes or restarting an animation.
1538
+ */
1539
+ function EffortSlider(props) {
1540
+ const { label, rows, selected, disabled = false, busy = false, onPick } = props;
1541
+ const active = Math.max(0, rows.findIndex((row) => row.id === selected));
1542
+ const [position, setPosition] = (0, react.useState)(active);
1543
+ const positionRef = (0, react.useRef)(position);
1544
+ (0, react.useEffect)(() => {
1545
+ positionRef.current = active;
1546
+ setPosition(active);
1547
+ }, [active]);
1548
+ const preview = (next) => {
1549
+ positionRef.current = next;
1550
+ setPosition(next);
1551
+ };
1552
+ const commit = () => {
1553
+ if (disabled || busy) return;
1554
+ const row = rows[positionRef.current];
1555
+ if (row !== void 0) onPick(row.id === "" ? void 0 : row.id);
1556
+ };
1557
+ const max = Math.max(0, rows.length - 1);
1558
+ const percent = max > 0 ? Math.round(position / max * 100) : 0;
1559
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1560
+ className: "dsh-model-menu-sliderGroup",
1561
+ role: "group",
1562
+ "aria-label": label,
1563
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1564
+ className: "dsh-model-menu-sliderHead",
1565
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1566
+ className: "dsh-model-menu-sliderTitle",
1567
+ children: label
1568
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1569
+ className: "dsh-model-menu-sliderValue",
1570
+ children: rows[position]?.name
1571
+ })]
1572
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1573
+ className: "dsh-model-menu-sliderRail",
1574
+ style: { "--_p": percent },
1575
+ children: [
1576
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1577
+ type: "range",
1578
+ className: "dsh-model-menu-sliderInput",
1579
+ min: 0,
1580
+ max,
1581
+ step: 1,
1582
+ value: position,
1583
+ disabled,
1584
+ "aria-label": label,
1585
+ "aria-disabled": disabled || busy || void 0,
1586
+ "aria-valuetext": rows[position]?.name,
1587
+ onChange: (event) => {
1588
+ const value = Number(event.target.value);
1589
+ if (Number.isFinite(value)) preview(value);
1590
+ },
1591
+ onPointerUp: commit,
1592
+ onKeyUp: commit,
1593
+ onBlur: commit
1594
+ }),
1595
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1596
+ className: "dsh-model-menu-sliderTrack",
1597
+ "aria-hidden": "true",
1598
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1599
+ className: "dsh-model-menu-sliderFill",
1600
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dsh-model-menu-sliderFillBar" })
1601
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1602
+ className: "dsh-model-menu-sliderNodes",
1603
+ children: rows.map((row, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1604
+ "data-covered": index <= position ? "" : void 0,
1605
+ className: "dsh-model-menu-sliderNode",
1606
+ style: { "--_n": max > 0 ? Math.round(index / max * 100) : 0 }
1607
+ }, row.id || "__default"))
1608
+ })]
1609
+ }),
1610
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1611
+ className: "dsh-model-menu-sliderThumb",
1612
+ "aria-hidden": "true"
1613
+ })
1614
+ ]
1615
+ })]
1616
+ });
1617
+ }
1618
+ /**
1452
1619
  * ModelMenu styles, transcribed from the official ModelSelect.module.css (the
1453
1620
  * published dsh-client-ui-model-selection bundle): card capped at
1454
1621
  * min(360px, 100vh - 96px), 34px option cells at 13/18 with 5px 7px padding,
@@ -1507,6 +1674,7 @@ const modelMenuCss = `
1507
1674
  overflow: hidden;
1508
1675
  }
1509
1676
  .dsh-model-menu-status, .dsh-model-menu-empty { color: var(--dsw-alias-label-tertiary); padding: 8px; font-size: 12px; line-height: 18px; }
1677
+ .dsh-model-menu-header { flex: none; padding: 2px 2px 4px; }
1510
1678
  .dsh-model-menu-searchRow { flex-shrink: 0; margin: 2px 0 3px; position: relative; }
1511
1679
  .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; }
1512
1680
  .dsh-model-menu-searchRow .dsh-model-menu-search:focus-within { border-color: transparent; }
@@ -1585,6 +1753,145 @@ const modelMenuCss = `
1585
1753
  .dsh-model-menu-cellLabel { white-space: nowrap; flex: none; }
1586
1754
  .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; }
1587
1755
  .dsh-model-menu-cellChevron { width: 12px; height: 12px; color: var(--dsw-alias-menu-icon); flex: none; }
1756
+ /* Discrete reasoning-effort slider under the model list (enhanced mode).
1757
+ * The painted control is layered over a fully functional native range input:
1758
+ * the input stays on top, transparent, and owns pointer/keyboard/ARIA, while
1759
+ * the decorative track, fill and thumb render beneath it at pointer-events:
1760
+ * none. Position lives in one custom property (--_p, 0–100) on the rail, so
1761
+ * the only animated work a level change triggers is a clip-path/left
1762
+ * transition — no keyframes, no element churn. */
1763
+ .dsh-model-menu-sliderGroup { border-top: 0.5px solid var(--dsw-alias-border-l2); flex: none; margin-top: 4px; padding: 6px 8px 8px; }
1764
+ .dsh-model-menu-sliderHead { align-items: baseline; justify-content: space-between; gap: 8px; margin: 0 2px 4px; display: flex; }
1765
+ .dsh-model-menu-sliderTitle { color: var(--dsw-alias-label-tertiary); font-size: 12px; line-height: 18px; }
1766
+ .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); }
1767
+ .dsh-model-menu-sliderRail { position: relative; direction: ltr; height: 38px; margin-top: 2px; }
1768
+ .dsh-model-menu-sliderRail:has(:disabled) { opacity: 0.45; }
1769
+ .dsh-model-menu-sliderInput {
1770
+ appearance: none;
1771
+ -webkit-appearance: none;
1772
+ box-sizing: border-box;
1773
+ border: 0;
1774
+ cursor: pointer;
1775
+ direction: ltr;
1776
+ opacity: 0;
1777
+ margin: 0;
1778
+ padding: 0;
1779
+ width: 100%;
1780
+ height: 100%;
1781
+ position: absolute;
1782
+ inset: 0;
1783
+ z-index: 1;
1784
+ touch-action: pan-y;
1785
+ }
1786
+ .dsh-model-menu-sliderInput:disabled { cursor: default; }
1787
+ /* The transparent input must keep the painted thumb's geometry: an
1788
+ * appearance:none webkit thumb defaults to 16px, which would shift the
1789
+ * end-point positions 7px off the decorative 30px disc and misplace the
1790
+ * native drag hit target. Track gets the full rail height so the whole band
1791
+ * maps to values identically. */
1792
+ .dsh-model-menu-sliderInput::-webkit-slider-runnable-track { width: 100%; height: 100%; }
1793
+ .dsh-model-menu-sliderInput::-webkit-slider-thumb { appearance: none; -webkit-appearance: none; width: 30px; height: 30px; margin: 0; }
1794
+ .dsh-model-menu-sliderInput::-moz-range-track { width: 100%; height: 100%; }
1795
+ .dsh-model-menu-sliderInput::-moz-range-thumb { width: 30px; height: 30px; margin: 0; border: 0; }
1796
+ .dsh-model-menu-sliderTrack {
1797
+ pointer-events: none;
1798
+ border-radius: 999px;
1799
+ width: 100%;
1800
+ height: 26px;
1801
+ background: var(--dsw-alias-interactive-bg-hover);
1802
+ box-shadow: inset 0 0 0 0.5px var(--dsw-alias-border-l2);
1803
+ position: absolute;
1804
+ inset-inline: 0;
1805
+ top: 50%;
1806
+ transform: translateY(-50%);
1807
+ /* No overflow:hidden — it would clip the fill's drop-shadow glow; the bar's
1808
+ * own clip-path already supplies the rounded reveal shape. */
1809
+ }
1810
+ /* The accent ramp paints the full track width once and is revealed by an
1811
+ * animated clip-path, so raising the effort unwraps more of the spectrum
1812
+ * instead of squashing the same gradient into a narrower bar. The glow lives
1813
+ * on the un-clipped parent: a filter on the clipped element itself would be
1814
+ * clipped away with it. */
1815
+ .dsh-model-menu-sliderFill {
1816
+ display: block;
1817
+ height: 100%;
1818
+ filter: drop-shadow(0 0 5px var(--dsw-alias-state-business-primary));
1819
+ transition: filter var(--ds-transition-duration) var(--ds-ease-in-out);
1820
+ }
1821
+ .dsh-model-menu-sliderInput:hover:not(:disabled) ~ .dsh-model-menu-sliderTrack .dsh-model-menu-sliderFill,
1822
+ .dsh-model-menu-sliderInput:active:not(:disabled) ~ .dsh-model-menu-sliderTrack .dsh-model-menu-sliderFill {
1823
+ filter: drop-shadow(0 0 9px var(--dsw-alias-state-business-primary));
1824
+ }
1825
+ /* The fill stops at the thumb's center, not at the track edge — at a 30px
1826
+ * thumb the end stops sit 15px in from each side, matching the node dots. */
1827
+ .dsh-model-menu-sliderFillBar {
1828
+ display: block;
1829
+ height: 100%;
1830
+ background: linear-gradient(90deg,
1831
+ var(--dsw-alias-state-business-primary),
1832
+ var(--dsw-alias-state-success-primary),
1833
+ var(--dsw-alias-state-warn-primary),
1834
+ var(--dsw-alias-state-error-primary));
1835
+ clip-path: inset(0 calc(100% - 15px - (100% - 30px) * var(--_p, 0) / 100) 0 0 round 999px);
1836
+ transition: clip-path var(--ds-transition-duration) var(--ds-ease-in-out);
1837
+ }
1838
+ /* Stop dots: one per declared row (default + every advertised effort), evenly
1839
+ * spread across the band between the two thumb-center extremes (inset 15px,
1840
+ * i.e. half the 30px thumb). --_n is 0-100 along that band. A covered dot
1841
+ * reads as a pale pin through the gradient; an uncovered one stays a neutral
1842
+ * notch on the empty track. */
1843
+ .dsh-model-menu-sliderNodes {
1844
+ pointer-events: none;
1845
+ display: block;
1846
+ position: absolute;
1847
+ inset-inline: 15px;
1848
+ inset-block: 0;
1849
+ }
1850
+ .dsh-model-menu-sliderNode {
1851
+ pointer-events: none;
1852
+ box-sizing: border-box;
1853
+ width: 4px;
1854
+ height: 4px;
1855
+ border-radius: 50%;
1856
+ background: color-mix(in srgb, var(--dsw-alias-label-secondary) 35%, transparent);
1857
+ position: absolute;
1858
+ left: calc(var(--_n, 0) * 1%);
1859
+ top: 50%;
1860
+ transform: translate(-50%, -50%);
1861
+ transition: background-color var(--ds-transition-duration) var(--ds-ease-in-out),
1862
+ transform var(--ds-transition-duration) var(--ds-ease-in-out);
1863
+ }
1864
+ .dsh-model-menu-sliderNode[data-covered] { background: color-mix(in srgb, var(--dsw-alias-label-primary) 60%, transparent); }
1865
+ .dsh-model-menu-sliderThumb {
1866
+ pointer-events: none;
1867
+ box-sizing: border-box;
1868
+ border-radius: 50%;
1869
+ width: 30px;
1870
+ height: 30px;
1871
+ background: var(--dsw-alias-button-primary-fill);
1872
+ border: 0.5px solid var(--dsw-alias-border-l4);
1873
+ box-shadow: 0 0 0 3px var(--dsw-alias-interactive-bg-hover), var(--dsw-elevation-soft);
1874
+ position: absolute;
1875
+ left: calc((100% - 30px) * var(--_p, 0) / 100);
1876
+ top: 50%;
1877
+ transform: translateY(-50%) scale(0.92);
1878
+ transition: left var(--ds-transition-duration) var(--ds-ease-in-out),
1879
+ transform var(--ds-transition-duration) var(--ds-ease-in-out),
1880
+ box-shadow var(--ds-transition-duration) var(--ds-ease-in-out);
1881
+ }
1882
+ .dsh-model-menu-sliderInput:hover:not(:disabled) ~ .dsh-model-menu-sliderThumb { transform: translateY(-50%) scale(1); }
1883
+ .dsh-model-menu-sliderInput:active:not(:disabled) ~ .dsh-model-menu-sliderThumb {
1884
+ transform: translateY(-50%) scale(1.1);
1885
+ box-shadow: 0 0 0 4px var(--dsw-alias-interactive-bg-hover), 0 0 12px var(--dsw-alias-state-business-primary);
1886
+ }
1887
+ .dsh-model-menu-sliderInput:focus-visible ~ .dsh-model-menu-sliderThumb {
1888
+ box-shadow: 0 0 0 3px var(--dsw-alias-interactive-bg-hover),
1889
+ 0 0 0 calc(3px + var(--dsw-focus-ring-width)) var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
1890
+ }
1891
+ @media (prefers-reduced-motion: reduce) {
1892
+ .dsh-model-menu-sliderFill, .dsh-model-menu-sliderFillBar, .dsh-model-menu-sliderThumb,
1893
+ .dsh-model-menu-sliderNode, .dsh-model-menu-sliderValue { transition: none; }
1894
+ }
1588
1895
  `;
1589
1896
  //#endregion
1590
1897
  //#region src/client-copy.ts
@@ -1670,6 +1977,95 @@ function hubMessage(message, language) {
1670
1977
  return hubMessages.find((pair) => pair.includes(message))?.[language === "zh" ? 0 : 1] ?? message;
1671
1978
  }
1672
1979
  //#endregion
1980
+ //#region ../dsh-plugin-kit/src/model-picker.tsx
1981
+ /**
1982
+ * Optional shared model picker (`klarkxy.model-picker` Factory).
1983
+ *
1984
+ * `@klarkxy/dsh-model-picker`, when installed, registers one root-scoped
1985
+ * Component Factory under `MODEL_PICKER_FACTORY`: an enhanced model menu with
1986
+ * a searchable provider-grouped list and a reasoning-effort slider, built on
1987
+ * the same ModelMenu recipe. Consumers keep their own ModelMenu as the
1988
+ * `fallback`; when no definition is registered the renderer resolves that
1989
+ * fallback verbatim, so the shared picker is strictly opt-in and every saved
1990
+ * value keeps its existing write path.
1991
+ *
1992
+ * `renderFactorySlot` is a framework-composed prop, never a context: the
1993
+ * component a plugin registers into any slot receives it automatically, but
1994
+ * the plugin must pass it down to whichever child renders the menu (or to the
1995
+ * render callback handed to registerSessionSettingsItem).
1996
+ *
1997
+ * The Factory manages its own open state; a consumer's `open`/`onOpenChange`
1998
+ * pair belongs to the fallback menu only and must not be fed to both.
1999
+ */
2000
+ /**
2001
+ * The trigger every consumer used to hand its own ModelMenu, rebuilt as the
2002
+ * factory's `anchor` local component: identical classes and chevron, the menu
2003
+ * state coming from the occurrence's props instead of local open state.
2004
+ *
2005
+ * `ariaLabel` and `title` are the whole accessible name and tooltip (the
2006
+ * caller formats "field: current route" itself because the factory's resolved
2007
+ * caption may intentionally be a short name). The component identity stays
2008
+ * put across label changes — a new function would unmount the trigger, and
2009
+ * its cleanup would close a menu that `closeOnPick={false}` is keeping open.
2010
+ * The latest label, title, and `onClose` are read from refs.
2011
+ */
2012
+ function useModelMenuAnchor(ariaLabel, title, onClose) {
2013
+ const closeRef = (0, react.useRef)(onClose);
2014
+ const labelRef = (0, react.useRef)(ariaLabel);
2015
+ const titleRef = (0, react.useRef)(title);
2016
+ closeRef.current = onClose;
2017
+ labelRef.current = ariaLabel;
2018
+ titleRef.current = title;
2019
+ return (0, react.useMemo)(() => {
2020
+ const ModelPickerTrigger = (props) => {
2021
+ (0, react.useEffect)(() => () => closeRef.current?.(), []);
2022
+ const record = props;
2023
+ const injected = {};
2024
+ for (const [key, value] of Object.entries(record)) if (key.startsWith("aria-")) injected[key] = value;
2025
+ const extraClass = typeof record.className === "string" ? ` ${record.className}` : "";
2026
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2027
+ type: "button",
2028
+ className: `dsh-model-menu-trigger${extraClass}`,
2029
+ disabled: props.disabled,
2030
+ "aria-haspopup": "menu",
2031
+ "aria-expanded": props.open,
2032
+ ...injected,
2033
+ "aria-label": labelRef.current,
2034
+ title: titleRef.current,
2035
+ children: [
2036
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2037
+ className: "dsh-model-menu-triggerLabel",
2038
+ children: props.label
2039
+ }),
2040
+ props.effortLabel ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2041
+ className: "dsh-model-menu-triggerEffort",
2042
+ children: props.effortLabel
2043
+ }) : null,
2044
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${props.open ? " dsh-model-menu-chevronOpen" : ""}` })
2045
+ ]
2046
+ });
2047
+ };
2048
+ return ModelPickerTrigger;
2049
+ }, []);
2050
+ }
2051
+ /**
2052
+ * Render the shared model picker while it is registered; `options.fallback`
2053
+ * whenever it is not (plugin absent, disabled, or unloaded).
2054
+ *
2055
+ * `options.anchor` is optional: the factory ships a default trigger, so only
2056
+ * consumers that need their own chrome (custom aria-label, tooltip, class)
2057
+ * pass one built with {@link useModelMenuAnchor}. The returned node is never
2058
+ * assumed to be the factory's own output — a defensive `?? fallback` covers a
2059
+ * renderer that yields null for an unregistered definition.
2060
+ */
2061
+ function renderModelPicker(renderFactorySlot, props, options) {
2062
+ if (typeof renderFactorySlot !== "function") return options.fallback;
2063
+ return renderFactorySlot("klarkxy.model-picker", props, {
2064
+ ...options.anchor ? { slots: { ["anchor"]: options.anchor } } : {},
2065
+ fallback: options.fallback
2066
+ }) ?? options.fallback;
2067
+ }
2068
+ //#endregion
1673
2069
  //#region src/client-model-menu.tsx
1674
2070
  /**
1675
2071
  * One detected field's standard editor: the same ModelMenu every marked field gets.
@@ -1680,7 +2076,7 @@ function hubMessage(message, language) {
1680
2076
  * the current reasoning effort when the new model advertises it, so switching a
1681
2077
  * route does not silently reset the user's choice.
1682
2078
  */
1683
- function FieldMenu({ t, catalog, current, disabled, label, allowFollow = true, followLabel, reasoningEffort = true, display = "full", onSet }) {
2079
+ function FieldMenu({ t, catalog, current, disabled, label, allowFollow = true, followLabel, reasoningEffort = true, display = "full", onSet, renderFactorySlot }) {
1684
2080
  const [open, setOpen] = (0, react.useState)(false);
1685
2081
  (0, react.useEffect)(() => {
1686
2082
  if (disabled) setOpen(false);
@@ -1701,46 +2097,28 @@ function FieldMenu({ t, catalog, current, disabled, label, allowFollow = true, f
1701
2097
  const currentLabel = display === "short" && !follow ? selected ? modelMenuShortName(selected) : route.model : menuLabel;
1702
2098
  const selectedEffort = efforts.find((item) => item.id === (route.reasoningEffort || ""));
1703
2099
  const effortName = selectedEffort ? modelMenuEffortLabel(selectedEffort, t.language) : void 0;
1704
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
2100
+ const anchor = useModelMenuAnchor(accessible, routeLabel, () => setOpen(false));
2101
+ const pick = (picked) => {
2102
+ const choice = choices.find((item) => item.provider === picked.provider && item.model === picked.model);
2103
+ const effort = route.reasoningEffort;
2104
+ const supported = Boolean(effort) && Boolean(choice?.efforts.some((item) => item.id === effort));
2105
+ onSet({
2106
+ provider: picked.provider,
2107
+ model: picked.model,
2108
+ ...supported ? { reasoningEffort: effort } : {}
2109
+ });
2110
+ };
2111
+ return renderModelPicker(renderFactorySlot, {
1705
2112
  locale: t.language,
1706
- open: open && !disabled,
2113
+ disabled,
1707
2114
  onOpenChange: (next) => setOpen(next && !disabled),
1708
2115
  listClassName: "dsh-model-hub",
1709
- anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1710
- type: "button",
1711
- className: "dsh-model-menu-trigger",
1712
- disabled,
1713
- "aria-label": accessible,
1714
- title: routeLabel,
1715
- "aria-haspopup": "menu",
1716
- "aria-expanded": open && !disabled,
1717
- children: [
1718
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1719
- className: "dsh-model-menu-triggerLabel",
1720
- children: currentLabel
1721
- }),
1722
- !follow && efforts.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1723
- className: "dsh-model-menu-triggerEffort",
1724
- children: ` · ${effortName ?? t("effortDefault")}`
1725
- }) : null,
1726
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${open ? " dsh-model-menu-chevronOpen" : ""}` })
1727
- ]
1728
- }),
1729
2116
  choices,
1730
2117
  selected: follow ? void 0 : {
1731
2118
  provider: route.provider,
1732
2119
  model: route.model
1733
2120
  },
1734
- onPick: (picked) => {
1735
- const choice = choices.find((item) => item.provider === picked.provider && item.model === picked.model);
1736
- const effort = route.reasoningEffort;
1737
- const supported = Boolean(effort) && Boolean(choice?.efforts.some((item) => item.id === effort));
1738
- onSet({
1739
- provider: picked.provider,
1740
- model: picked.model,
1741
- ...supported ? { reasoningEffort: effort } : {}
1742
- });
1743
- },
2121
+ onPick: pick,
1744
2122
  leading: allowFollow ? [{
1745
2123
  id: "follow",
1746
2124
  label: followLabel || t("follow"),
@@ -1760,6 +2138,8 @@ function FieldMenu({ t, catalog, current, disabled, label, allowFollow = true, f
1760
2138
  model: route.model,
1761
2139
  ...effort ? { reasoningEffort: effort } : {}
1762
2140
  }),
2141
+ triggerLabel: currentLabel,
2142
+ triggerEffortLabel: !follow && efforts.length ? ` · ${effortName ?? t("effortDefault")}` : void 0,
1763
2143
  modelLabel: t("model"),
1764
2144
  modelValue: currentLabel,
1765
2145
  effortLabel: t("effort"),
@@ -1767,6 +2147,66 @@ function FieldMenu({ t, catalog, current, disabled, label, allowFollow = true, f
1767
2147
  searchPlaceholder: t("searchModels"),
1768
2148
  emptyLabel: t("noModels"),
1769
2149
  clearSearchLabel: t("clearSearch")
2150
+ }, {
2151
+ anchor,
2152
+ fallback: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
2153
+ locale: t.language,
2154
+ open: open && !disabled,
2155
+ onOpenChange: (next) => setOpen(next && !disabled),
2156
+ listClassName: "dsh-model-hub",
2157
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2158
+ type: "button",
2159
+ className: "dsh-model-menu-trigger",
2160
+ disabled,
2161
+ "aria-label": accessible,
2162
+ title: routeLabel,
2163
+ "aria-haspopup": "menu",
2164
+ "aria-expanded": open && !disabled,
2165
+ children: [
2166
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2167
+ className: "dsh-model-menu-triggerLabel",
2168
+ children: currentLabel
2169
+ }),
2170
+ !follow && efforts.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2171
+ className: "dsh-model-menu-triggerEffort",
2172
+ children: ` · ${effortName ?? t("effortDefault")}`
2173
+ }) : null,
2174
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${open ? " dsh-model-menu-chevronOpen" : ""}` })
2175
+ ]
2176
+ }),
2177
+ choices,
2178
+ selected: follow ? void 0 : {
2179
+ provider: route.provider,
2180
+ model: route.model
2181
+ },
2182
+ onPick: pick,
2183
+ leading: allowFollow ? [{
2184
+ id: "follow",
2185
+ label: followLabel || t("follow"),
2186
+ selected: follow
2187
+ }] : [],
2188
+ onPickLeading: () => onSet({
2189
+ provider: "",
2190
+ model: ""
2191
+ }),
2192
+ efforts,
2193
+ selectedEffort: route.reasoningEffort || "",
2194
+ onPickEffort: (effort) => onSet(follow ? {
2195
+ provider: "",
2196
+ model: ""
2197
+ } : {
2198
+ provider: route.provider,
2199
+ model: route.model,
2200
+ ...effort ? { reasoningEffort: effort } : {}
2201
+ }),
2202
+ modelLabel: t("model"),
2203
+ modelValue: currentLabel,
2204
+ effortLabel: t("effort"),
2205
+ defaultEffortLabel: t("effortDefault"),
2206
+ searchPlaceholder: t("searchModels"),
2207
+ emptyLabel: t("noModels"),
2208
+ clearSearchLabel: t("clearSearch")
2209
+ })
1770
2210
  });
1771
2211
  }
1772
2212
  function messageOf(error) {
@@ -1902,7 +2342,7 @@ function routeInput(route) {
1902
2342
  };
1903
2343
  }
1904
2344
  /** One cast entry: model picker, tendency input saved on blur, remove button. */
1905
- function CastRow({ t, catalog, route, tendencyRevision, draftReview, disabled, tendencyDisabled, menuDisabled, onSetRoute, onSetTendency, onRemove }) {
2345
+ function CastRow({ t, catalog, route, tendencyRevision, draftReview, disabled, tendencyDisabled, menuDisabled, renderFactorySlot, onSetRoute, onSetTendency, onRemove }) {
1906
2346
  const label = `${route.provider} / ${route.model}`;
1907
2347
  const saved = route.tendency ?? "";
1908
2348
  const [draft, setDraft] = (0, react.useState)({
@@ -1945,6 +2385,7 @@ function CastRow({ t, catalog, route, tendencyRevision, draftReview, disabled, t
1945
2385
  reasoningEffort: true,
1946
2386
  display: "short",
1947
2387
  disabled: menuDisabled,
2388
+ renderFactorySlot,
1948
2389
  onSet: onSetRoute
1949
2390
  })
1950
2391
  }),
@@ -1981,7 +2422,7 @@ function CastRow({ t, catalog, route, tendencyRevision, draftReview, disabled, t
1981
2422
  ]
1982
2423
  });
1983
2424
  }
1984
- function CastPage({ api, locale }) {
2425
+ function CastPage({ api, locale, renderFactorySlot }) {
1985
2426
  const t = useText$1(locale);
1986
2427
  const [cast, setCast] = (0, react.useState)(null);
1987
2428
  const [catalog, setCatalog] = (0, react.useState)(null);
@@ -2217,6 +2658,7 @@ function CastPage({ api, locale }) {
2217
2658
  t,
2218
2659
  catalog,
2219
2660
  route,
2661
+ renderFactorySlot,
2220
2662
  tendencyRevision: cast.tendencyRevision,
2221
2663
  draftReview,
2222
2664
  tendencyDisabled: disabled || !!cast.tendencyError || cast.tendencyRevision === void 0,
@@ -2247,6 +2689,7 @@ function CastPage({ api, locale }) {
2247
2689
  reasoningEffort: true,
2248
2690
  display: "short",
2249
2691
  disabled: disabled || catalog === null,
2692
+ renderFactorySlot,
2250
2693
  onSet: (picked) => {
2251
2694
  if (!picked.provider || !picked.model) {
2252
2695
  setPending(void 0);
@@ -3146,13 +3589,14 @@ function localeActive(locale) {
3146
3589
  return String(typeof locale === "string" ? locale : locale?.getSnapshot?.()?.active ?? "en");
3147
3590
  }
3148
3591
  const css = `${officialUiCss("dsh-model-hub")}${modelMenuCss}`;
3149
- function SubagentModelsPage({ api, locale }) {
3592
+ function SubagentModelsPage({ api, locale, renderFactorySlot }) {
3150
3593
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
3151
3594
  className: "dsh-model-hub dsh-ui-page",
3152
3595
  "aria-label": localeActive(locale).toLowerCase().startsWith("zh") ? "子代理" : "Subagents",
3153
3596
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: css }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CastPage, {
3154
3597
  api,
3155
- locale
3598
+ locale,
3599
+ renderFactorySlot
3156
3600
  })]
3157
3601
  });
3158
3602
  }