@klarkxy/dsh-current-title 0.3.3 → 0.3.5

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
@@ -102,6 +102,7 @@ function parseModelMenuChoices(value, bound) {
102
102
  provider,
103
103
  model,
104
104
  label: `${providerName} / ${name}`,
105
+ providerLabel: providerName,
105
106
  efforts
106
107
  });
107
108
  }
@@ -148,11 +149,11 @@ function modelMenuShortName(choice) {
148
149
  /** Group catalog choices by provider and filter them by a case-insensitive query. */
149
150
  function groupModelMenuChoices(choices, query) {
150
151
  const needle = query.trim().toLowerCase();
151
- const matched = needle ? choices.filter((choice) => choice.label.toLowerCase().includes(needle) || choice.model.toLowerCase().includes(needle) || choice.provider.toLowerCase().includes(needle)) : choices;
152
+ 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;
152
153
  const groups = [];
153
154
  for (const choice of matched) {
154
155
  const sep = choice.label.indexOf(" / ");
155
- const name = sep > 0 ? choice.label.slice(0, sep) : choice.provider;
156
+ const name = choice.providerLabel ?? (sep > 0 ? choice.label.slice(0, sep) : choice.provider);
156
157
  const last = groups.at(-1);
157
158
  if (last && last.provider === choice.provider) last.items.push(choice);
158
159
  else groups.push({
@@ -211,7 +212,7 @@ const AUXILIARY_MODEL_CHANGES_IDENTITY = modelSettingsChangesIdentity("@klarkxy/
211
212
  //#endregion
212
213
  //#region ../dsh-plugin-kit/src/client-utils.ts
213
214
  /** Fixed pause for failing background reads; manual work is never replayed. */
214
- function createPluginReadGate(now = Date.now) {
215
+ function createPluginReadGate(now = Date.now, logger) {
215
216
  let pausedUntil = 0;
216
217
  let epoch = 0;
217
218
  return {
@@ -224,11 +225,17 @@ function createPluginReadGate(now = Date.now) {
224
225
  const current = epoch;
225
226
  try {
226
227
  const value = await read();
227
- if (current === epoch) pausedUntil = 0;
228
+ if (current === epoch) {
229
+ if (pausedUntil > 0) logger?.debug("read-gate.resumed", "Background read gate recovered.");
230
+ pausedUntil = 0;
231
+ }
228
232
  return value;
229
233
  } catch (error) {
230
234
  const message = error instanceof Error ? error.message : "";
231
- if (current === epoch && (/(?:^|\s)transport failure for .+: HTTP (?:404|405|503)\b/.test(message) || error instanceof TypeError && /^(?:Failed to fetch|fetch failed|NetworkError when attempting to fetch resource\.?)$/i.test(message))) pausedUntil = now() + 3e4;
235
+ if (current === epoch && (/(?:^|\s)transport failure for .+: HTTP (?:404|405|503)\b/.test(message) || error instanceof TypeError && /^(?:Failed to fetch|fetch failed|NetworkError when attempting to fetch resource\.?)$/i.test(message))) {
236
+ pausedUntil = now() + 3e4;
237
+ logger?.warn("read-gate.paused", "Background reads paused for 30s after transport failure.", error);
238
+ }
232
239
  throw error;
233
240
  }
234
241
  }
@@ -525,7 +532,9 @@ Object.values({
525
532
  "--dsh-scrollbar-thumb-hover",
526
533
  "--dsh-content-font-size-secondary",
527
534
  "--dsh-content-font-delta",
528
- "--dsh-frame-top-clearance"
535
+ "--dsh-frame-top-clearance",
536
+ "--dsh-composer-model-icon-display",
537
+ "--dsh-composer-model-text-display"
529
538
  ]
530
539
  }).flat();
531
540
  /**
@@ -1229,7 +1238,7 @@ function ModelMenu(props) {
1229
1238
  empty: "No matching models",
1230
1239
  clear: "Clear search"
1231
1240
  };
1232
- 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;
1241
+ 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;
1233
1242
  const rootRef = (0, react.useRef)(null);
1234
1243
  const openRef = (0, react.useRef)(open);
1235
1244
  openRef.current = open;
@@ -1244,9 +1253,10 @@ function ModelMenu(props) {
1244
1253
  const [pane, setPane] = (0, react.useState)("root");
1245
1254
  const [query, setQuery] = (0, react.useState)("");
1246
1255
  const [highlightedIndex, setHighlightedIndex] = (0, react.useState)(null);
1256
+ const [keyNav, setKeyNav] = (0, react.useState)(0);
1247
1257
  const [menuPos, setMenuPos] = (0, react.useState)(null);
1248
1258
  const id = (0, react.useId)();
1249
- const showSearch = choices.length > 4;
1259
+ const showSearch = alwaysShowSearch || choices.length > 4;
1250
1260
  const effortRows = (0, react.useMemo)(() => [{
1251
1261
  id: "",
1252
1262
  name: defaultEffortLabel
@@ -1263,7 +1273,12 @@ function ModelMenu(props) {
1263
1273
  items: (0, _deepseek_ai_dsh_client_ui_primitives.rankByName)(group.items.map((choice) => ({
1264
1274
  choice,
1265
1275
  name: modelMenuShortName(choice),
1266
- label: choice.model
1276
+ label: [
1277
+ choice.provider,
1278
+ choice.providerLabel,
1279
+ choice.model,
1280
+ choice.label
1281
+ ].filter(Boolean).join(" ")
1267
1282
  })), showSearch ? query.trim() : "")
1268
1283
  })).filter((group) => group.items.length > 0), [
1269
1284
  choices,
@@ -1295,10 +1310,12 @@ function ModelMenu(props) {
1295
1310
  paneFocus.current = "drill";
1296
1311
  setPane(next);
1297
1312
  };
1298
- /** Leave a drilled pane for the root one, handing the keyboard back to its cell. */
1313
+ /** Leave a drilled pane for the entry pane, handing the keyboard back to it.
1314
+ * A root entry refocuses the cell that opened the pane; a search-first
1315
+ * (model) entry refocuses that pane's own intent. */
1299
1316
  const back = (from) => {
1300
- paneFocus.current = from;
1301
- setPane("root");
1317
+ paneFocus.current = initialPane === "root" ? from : "drill";
1318
+ setPane(initialPane);
1302
1319
  };
1303
1320
  const moveFocus = (offset) => {
1304
1321
  const items = itemRefs.current.filter((item) => item !== null && !item.disabled);
@@ -1338,10 +1355,18 @@ function ModelMenu(props) {
1338
1355
  }, [requestedOpen, open]);
1339
1356
  (0, react.useEffect)(() => {
1340
1357
  if (!open) return;
1341
- setPane("root");
1358
+ setPane(initialPane);
1359
+ if (initialPane === "model") paneFocus.current = "drill";
1342
1360
  setQuery("");
1343
1361
  setHighlightedIndex(null);
1344
- }, [open]);
1362
+ }, [open, initialPane]);
1363
+ (0, react.useEffect)(() => {
1364
+ if (disabled && open && closeOnDisable) changeOpen(false);
1365
+ }, [
1366
+ disabled,
1367
+ open,
1368
+ closeOnDisable
1369
+ ]);
1345
1370
  (0, react.useEffect)(() => {
1346
1371
  if (!open) return;
1347
1372
  const closeOutside = (event) => {
@@ -1349,9 +1374,25 @@ function ModelMenu(props) {
1349
1374
  if (rootRef.current?.contains(target) || menuRef.current?.contains(target)) return;
1350
1375
  changeOpen(false);
1351
1376
  };
1377
+ const escapeOutside = (event) => {
1378
+ if (event.key !== "Escape" || event.defaultPrevented || event.isComposing) return;
1379
+ if (!openRef.current) return;
1380
+ event.preventDefault();
1381
+ if (pane !== initialPane) back(pane);
1382
+ else close(true);
1383
+ };
1352
1384
  document.addEventListener("mousedown", closeOutside);
1353
- return () => document.removeEventListener("mousedown", closeOutside);
1354
- }, [open, onOpenChange]);
1385
+ document.addEventListener("keydown", escapeOutside);
1386
+ return () => {
1387
+ document.removeEventListener("mousedown", closeOutside);
1388
+ document.removeEventListener("keydown", escapeOutside);
1389
+ };
1390
+ }, [
1391
+ open,
1392
+ onOpenChange,
1393
+ pane,
1394
+ initialPane
1395
+ ]);
1355
1396
  (0, react.useEffect)(() => {
1356
1397
  const intent = paneFocus.current;
1357
1398
  if (!open || menuPos === null || intent === null) return;
@@ -1381,13 +1422,21 @@ function ModelMenu(props) {
1381
1422
  pane,
1382
1423
  filteredGroups
1383
1424
  ]);
1425
+ const modelPaneOpen = open && pane === "model";
1426
+ const scrollSignal = !modelPaneOpen ? "closed" : keyNav === 0 ? "open" : `key:${keyNav}`;
1427
+ const scrollSignalRef = (0, react.useRef)(null);
1384
1428
  (0, react.useLayoutEffect)(() => {
1385
- if (open && pane === "model" && activeModelIndex >= 0) itemRefs.current[leading.length + activeModelIndex]?.scrollIntoView({ block: "nearest" });
1429
+ if (!modelPaneOpen || activeModelIndex < 0) {
1430
+ if (!modelPaneOpen) scrollSignalRef.current = null;
1431
+ return;
1432
+ }
1433
+ if (scrollSignalRef.current === scrollSignal) return;
1434
+ scrollSignalRef.current = scrollSignal;
1435
+ itemRefs.current[leading.length + activeModelIndex]?.scrollIntoView({ block: "nearest" });
1386
1436
  }, [
1387
- open,
1388
- pane,
1437
+ modelPaneOpen,
1438
+ scrollSignal,
1389
1439
  activeModelIndex,
1390
- visibleModels,
1391
1440
  leading.length
1392
1441
  ]);
1393
1442
  (0, react.useLayoutEffect)(() => {
@@ -1402,7 +1451,7 @@ function ModelMenu(props) {
1402
1451
  let x = rect.right - lw, y = rect.top - 8 - lh;
1403
1452
  if (lw > 0) x = Math.min(Math.max(x, margin), window.innerWidth - lw - margin);
1404
1453
  if (lh > 0) y = Math.min(Math.max(y, margin), window.innerHeight - lh - margin);
1405
- setMenuPos({
1454
+ setMenuPos((prev) => prev && prev.left === x && prev.top === y ? prev : {
1406
1455
  left: x,
1407
1456
  top: y
1408
1457
  });
@@ -1422,6 +1471,13 @@ function ModelMenu(props) {
1422
1471
  ]);
1423
1472
  function onKeyDown(event) {
1424
1473
  if (event.defaultPrevented || event.nativeEvent.isComposing) return;
1474
+ if (event.key === "Escape" && open) {
1475
+ event.preventDefault();
1476
+ if (pane !== initialPane) back(pane);
1477
+ else close(true);
1478
+ return;
1479
+ }
1480
+ if (disabled) return;
1425
1481
  if (!open && [
1426
1482
  "Enter",
1427
1483
  " ",
@@ -1435,17 +1491,12 @@ function ModelMenu(props) {
1435
1491
  requestOpen();
1436
1492
  return;
1437
1493
  }
1438
- if (event.key === "Escape" && open) {
1439
- event.preventDefault();
1440
- if (pane !== "root") back(pane);
1441
- else close(true);
1442
- return;
1443
- }
1444
1494
  if (!open) return;
1445
1495
  if (pane === "model" && showSearch && (event.key === "ArrowDown" || event.key === "ArrowUp")) {
1446
1496
  event.preventDefault();
1447
1497
  if (visibleModels.length > 0) {
1448
1498
  setHighlightedIndex((activeModelIndex + (event.key === "ArrowDown" ? 1 : -1) + visibleModels.length) % visibleModels.length);
1499
+ setKeyNav((n) => n + 1);
1449
1500
  searchRef.current?.focus();
1450
1501
  }
1451
1502
  return;
@@ -1454,19 +1505,19 @@ function ModelMenu(props) {
1454
1505
  if (event.key === "Tab" && visibleModels.length === 0) return;
1455
1506
  event.preventDefault();
1456
1507
  const highlighted = visibleModels[activeModelIndex];
1457
- if (highlighted) {
1508
+ if (highlighted && !busy) {
1458
1509
  onPick({
1459
1510
  provider: highlighted.provider,
1460
1511
  model: highlighted.model
1461
1512
  });
1462
- close(true);
1513
+ if (closeOnPick) close(true);
1463
1514
  }
1464
1515
  return;
1465
1516
  }
1466
1517
  if (event.key === "Tab") {
1467
1518
  if (event.shiftKey) {
1468
1519
  event.preventDefault();
1469
- if (pane !== "root") back(pane);
1520
+ if (pane !== initialPane) back(pane);
1470
1521
  else close(true);
1471
1522
  return;
1472
1523
  }
@@ -1490,7 +1541,10 @@ function ModelMenu(props) {
1490
1541
  }
1491
1542
  }
1492
1543
  function onBlur(event) {
1493
- if (event.relatedTarget instanceof Node && (rootRef.current?.contains(event.relatedTarget) || menuRef.current?.contains(event.relatedTarget))) return;
1544
+ const related = event.relatedTarget;
1545
+ if (related instanceof Node && (rootRef.current?.contains(related) || menuRef.current?.contains(related) || !related.isConnected)) return;
1546
+ if (!closeOnPick && !(related instanceof Node)) return;
1547
+ if (busy && related == null) return;
1494
1548
  if (open) changeOpen(false);
1495
1549
  }
1496
1550
  itemRefs.current = [];
@@ -1511,11 +1565,16 @@ function ModelMenu(props) {
1511
1565
  "aria-label": modelLabel,
1512
1566
  lang: language,
1513
1567
  children: [
1568
+ menuHeader ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1569
+ className: "dsh-model-menu-header",
1570
+ children: menuHeader
1571
+ }) : null,
1514
1572
  pane === "root" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1515
1573
  ref: itemRef(),
1516
1574
  type: "button",
1517
1575
  role: "menuitem",
1518
1576
  className: "dsh-model-menu-cell",
1577
+ disabled,
1519
1578
  onClick: () => drill("model"),
1520
1579
  children: [
1521
1580
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -1528,11 +1587,12 @@ function ModelMenu(props) {
1528
1587
  }),
1529
1588
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutlineRegular, { className: "dsh-model-menu-cellChevron" })
1530
1589
  ]
1531
- }), efforts.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1590
+ }), efforts.length > 0 && !reasoningSlider ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1532
1591
  ref: itemRef(),
1533
1592
  type: "button",
1534
1593
  role: "menuitem",
1535
1594
  className: "dsh-model-menu-cell",
1595
+ disabled,
1536
1596
  onClick: () => drill("effort"),
1537
1597
  children: [
1538
1598
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -1551,6 +1611,7 @@ function ModelMenu(props) {
1551
1611
  className: "dsh-model-menu-searchRow",
1552
1612
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
1553
1613
  ref: searchRef,
1614
+ disabled,
1554
1615
  className: `dsh-model-menu-search${query !== "" ? " dsh-model-menu-searchWithQuery" : ""}`,
1555
1616
  type: "text",
1556
1617
  role: "searchbox",
@@ -1567,6 +1628,7 @@ function ModelMenu(props) {
1567
1628
  type: "button",
1568
1629
  className: "dsh-model-menu-searchClear",
1569
1630
  "aria-label": clearSearchLabel,
1631
+ disabled,
1570
1632
  onClick: () => {
1571
1633
  setQuery("");
1572
1634
  setHighlightedIndex(null);
@@ -1589,10 +1651,12 @@ function ModelMenu(props) {
1589
1651
  type: "button",
1590
1652
  role: "menuitemradio",
1591
1653
  "aria-checked": item.selected === true,
1654
+ disabled: disabled || busy,
1592
1655
  className: `dsh-model-menu-option${item.selected === true ? " dsh-model-menu-selected" : ""}`,
1593
1656
  onClick: () => {
1657
+ if (disabled || busy) return;
1594
1658
  onPickLeading?.(item.id);
1595
- close(true);
1659
+ if (closeOnPick) close(true);
1596
1660
  },
1597
1661
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1598
1662
  className: "dsh-model-menu-optionCopy",
@@ -1618,6 +1682,7 @@ function ModelMenu(props) {
1618
1682
  "aria-checked": checked,
1619
1683
  id: `${id}-model-${index}`,
1620
1684
  tabIndex: showSearch ? -1 : 0,
1685
+ disabled: disabled || busy,
1621
1686
  onFocus: () => setHighlightedIndex(index),
1622
1687
  "data-highlighted": index === activeModelIndex ? "" : void 0,
1623
1688
  className: `dsh-model-menu-option dsh-model-menu-modelOption${checked ? " dsh-model-menu-selected" : ""}${index === activeModelIndex ? " dsh-model-menu-optionActive" : ""}`,
@@ -1627,11 +1692,12 @@ function ModelMenu(props) {
1627
1692
  },
1628
1693
  title: choice.label,
1629
1694
  onClick: () => {
1695
+ if (disabled || busy) return;
1630
1696
  onPick({
1631
1697
  provider: choice.provider,
1632
1698
  model: choice.model
1633
1699
  });
1634
- close(true);
1700
+ if (closeOnPick) close(true);
1635
1701
  },
1636
1702
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1637
1703
  className: "dsh-model-menu-optionCopy",
@@ -1647,6 +1713,14 @@ function ModelMenu(props) {
1647
1713
  })
1648
1714
  }, group.provider))]
1649
1715
  }),
1716
+ reasoningSlider && efforts.length > 0 && filteredGroups.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EffortSlider, {
1717
+ label: effortLabel,
1718
+ rows: effortRows,
1719
+ selected: selectedEffort ?? "",
1720
+ disabled,
1721
+ busy,
1722
+ onPick: (value) => onPickEffort?.(value)
1723
+ }) : null,
1650
1724
  filteredGroups.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1651
1725
  className: "dsh-model-menu-empty",
1652
1726
  role: "status",
@@ -1660,10 +1734,12 @@ function ModelMenu(props) {
1660
1734
  type: "button",
1661
1735
  role: "menuitemradio",
1662
1736
  "aria-checked": checked,
1737
+ disabled: disabled || busy,
1663
1738
  className: `dsh-model-menu-option${checked ? " dsh-model-menu-selected" : ""}`,
1664
1739
  onClick: () => {
1740
+ if (disabled || busy) return;
1665
1741
  onPickEffort?.(row.id || void 0);
1666
- close(true);
1742
+ if (closeOnPick) close(true);
1667
1743
  },
1668
1744
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1669
1745
  className: "dsh-model-menu-optionCopy",
@@ -1682,7 +1758,8 @@ function ModelMenu(props) {
1682
1758
  const trigger = (0, react.isValidElement)(anchor) ? (0, react.cloneElement)(anchor, {
1683
1759
  "aria-haspopup": "menu",
1684
1760
  "aria-expanded": open,
1685
- "aria-controls": open ? `${id}-menu` : void 0
1761
+ "aria-controls": open ? `${id}-menu` : void 0,
1762
+ "aria-disabled": disabled || void 0
1686
1763
  }) : anchor;
1687
1764
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1688
1765
  ref: rootRef,
@@ -1690,7 +1767,7 @@ function ModelMenu(props) {
1690
1767
  onKeyDown,
1691
1768
  onBlur,
1692
1769
  onClick: (event) => {
1693
- if (event.defaultPrevented) return;
1770
+ if (disabled || event.defaultPrevented) return;
1694
1771
  const button = event.target instanceof Element ? event.target.closest("button") : null;
1695
1772
  if (!(button instanceof HTMLButtonElement) || button.disabled || !rootRef.current?.contains(button)) return;
1696
1773
  requestOpen();
@@ -1702,6 +1779,102 @@ function ModelMenu(props) {
1702
1779
  });
1703
1780
  }
1704
1781
  /**
1782
+ * Discrete reasoning-effort slider shown under the model list in enhanced mode.
1783
+ *
1784
+ * A native range input (`step=1`) over exactly the rows the caller supplies —
1785
+ * the default row plus each effort the selected model advertises — so it can
1786
+ * never invent a level. It previews locally while dragging or arrowing and
1787
+ * commits only when the gesture ends (pointer up, key up, or blur), so a drag
1788
+ * does not write on every step. The menu stays open, so adjusting the effort
1789
+ * never loses the model selection or the search query.
1790
+ *
1791
+ * The input itself stays the entire interaction and accessibility surface —
1792
+ * keyboard, ARIA, disabled and the native step snapping all come from it —
1793
+ * while the track, gradient fill and thumb below are decorative layers painted
1794
+ * from the `--_p` percentage the preview position sets on the rail. Value
1795
+ * changes only update that custom property, so the CSS transitions animate
1796
+ * between levels without remounting nodes or restarting an animation.
1797
+ */
1798
+ function EffortSlider(props) {
1799
+ const { label, rows, selected, disabled = false, busy = false, onPick } = props;
1800
+ const active = Math.max(0, rows.findIndex((row) => row.id === selected));
1801
+ const [position, setPosition] = (0, react.useState)(active);
1802
+ const positionRef = (0, react.useRef)(position);
1803
+ (0, react.useEffect)(() => {
1804
+ positionRef.current = active;
1805
+ setPosition(active);
1806
+ }, [active]);
1807
+ const preview = (next) => {
1808
+ positionRef.current = next;
1809
+ setPosition(next);
1810
+ };
1811
+ const commit = () => {
1812
+ if (disabled || busy) return;
1813
+ const row = rows[positionRef.current];
1814
+ if (row !== void 0) onPick(row.id === "" ? void 0 : row.id);
1815
+ };
1816
+ const max = Math.max(0, rows.length - 1);
1817
+ const percent = max > 0 ? Math.round(position / max * 100) : 0;
1818
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1819
+ className: "dsh-model-menu-sliderGroup",
1820
+ role: "group",
1821
+ "aria-label": label,
1822
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1823
+ className: "dsh-model-menu-sliderHead",
1824
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1825
+ className: "dsh-model-menu-sliderTitle",
1826
+ children: label
1827
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1828
+ className: "dsh-model-menu-sliderValue",
1829
+ children: rows[position]?.name
1830
+ })]
1831
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1832
+ className: "dsh-model-menu-sliderRail",
1833
+ style: { "--_p": percent },
1834
+ children: [
1835
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1836
+ type: "range",
1837
+ className: "dsh-model-menu-sliderInput",
1838
+ min: 0,
1839
+ max,
1840
+ step: 1,
1841
+ value: position,
1842
+ disabled,
1843
+ "aria-label": label,
1844
+ "aria-disabled": disabled || busy || void 0,
1845
+ "aria-valuetext": rows[position]?.name,
1846
+ onChange: (event) => {
1847
+ const value = Number(event.target.value);
1848
+ if (Number.isFinite(value)) preview(value);
1849
+ },
1850
+ onPointerUp: commit,
1851
+ onKeyUp: commit,
1852
+ onBlur: commit
1853
+ }),
1854
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1855
+ className: "dsh-model-menu-sliderTrack",
1856
+ "aria-hidden": "true",
1857
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1858
+ className: "dsh-model-menu-sliderFill",
1859
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dsh-model-menu-sliderFillBar" })
1860
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1861
+ className: "dsh-model-menu-sliderNodes",
1862
+ children: rows.map((row, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1863
+ "data-covered": index <= position ? "" : void 0,
1864
+ className: "dsh-model-menu-sliderNode",
1865
+ style: { "--_n": max > 0 ? Math.round(index / max * 100) : 0 }
1866
+ }, row.id || "__default"))
1867
+ })]
1868
+ }),
1869
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1870
+ className: "dsh-model-menu-sliderThumb",
1871
+ "aria-hidden": "true"
1872
+ })
1873
+ ]
1874
+ })]
1875
+ });
1876
+ }
1877
+ /**
1705
1878
  * ModelMenu styles, transcribed from the official ModelSelect.module.css (the
1706
1879
  * published dsh-client-ui-model-selection bundle): card capped at
1707
1880
  * min(360px, 100vh - 96px), 34px option cells at 13/18 with 5px 7px padding,
@@ -1760,6 +1933,7 @@ const modelMenuCss = `
1760
1933
  overflow: hidden;
1761
1934
  }
1762
1935
  .dsh-model-menu-status, .dsh-model-menu-empty { color: var(--dsw-alias-label-tertiary); padding: 8px; font-size: 12px; line-height: 18px; }
1936
+ .dsh-model-menu-header { flex: none; padding: 2px 2px 4px; }
1763
1937
  .dsh-model-menu-searchRow { flex-shrink: 0; margin: 2px 0 3px; position: relative; }
1764
1938
  .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; }
1765
1939
  .dsh-model-menu-searchRow .dsh-model-menu-search:focus-within { border-color: transparent; }
@@ -1838,8 +2012,236 @@ const modelMenuCss = `
1838
2012
  .dsh-model-menu-cellLabel { white-space: nowrap; flex: none; }
1839
2013
  .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; }
1840
2014
  .dsh-model-menu-cellChevron { width: 12px; height: 12px; color: var(--dsw-alias-menu-icon); flex: none; }
2015
+ /* Discrete reasoning-effort slider under the model list (enhanced mode).
2016
+ * The painted control is layered over a fully functional native range input:
2017
+ * the input stays on top, transparent, and owns pointer/keyboard/ARIA, while
2018
+ * the decorative track, fill and thumb render beneath it at pointer-events:
2019
+ * none. Position lives in one custom property (--_p, 0–100) on the rail, so
2020
+ * the only animated work a level change triggers is a clip-path/left
2021
+ * transition — no keyframes, no element churn. */
2022
+ .dsh-model-menu-sliderGroup { border-top: 0.5px solid var(--dsw-alias-border-l2); flex: none; margin-top: 4px; padding: 6px 8px 8px; }
2023
+ .dsh-model-menu-sliderHead { align-items: baseline; justify-content: space-between; gap: 8px; margin: 0 2px 4px; display: flex; }
2024
+ .dsh-model-menu-sliderTitle { color: var(--dsw-alias-label-tertiary); font-size: 12px; line-height: 18px; }
2025
+ .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); }
2026
+ .dsh-model-menu-sliderRail { position: relative; direction: ltr; height: 38px; margin-top: 2px; }
2027
+ .dsh-model-menu-sliderRail:has(:disabled) { opacity: 0.45; }
2028
+ .dsh-model-menu-sliderInput {
2029
+ appearance: none;
2030
+ -webkit-appearance: none;
2031
+ box-sizing: border-box;
2032
+ border: 0;
2033
+ cursor: pointer;
2034
+ direction: ltr;
2035
+ opacity: 0;
2036
+ margin: 0;
2037
+ padding: 0;
2038
+ width: 100%;
2039
+ height: 100%;
2040
+ position: absolute;
2041
+ inset: 0;
2042
+ z-index: 1;
2043
+ touch-action: pan-y;
2044
+ }
2045
+ .dsh-model-menu-sliderInput:disabled { cursor: default; }
2046
+ /* The transparent input must keep the painted thumb's geometry: an
2047
+ * appearance:none webkit thumb defaults to 16px, which would shift the
2048
+ * end-point positions 7px off the decorative 30px disc and misplace the
2049
+ * native drag hit target. Track gets the full rail height so the whole band
2050
+ * maps to values identically. */
2051
+ .dsh-model-menu-sliderInput::-webkit-slider-runnable-track { width: 100%; height: 100%; }
2052
+ .dsh-model-menu-sliderInput::-webkit-slider-thumb { appearance: none; -webkit-appearance: none; width: 30px; height: 30px; margin: 0; }
2053
+ .dsh-model-menu-sliderInput::-moz-range-track { width: 100%; height: 100%; }
2054
+ .dsh-model-menu-sliderInput::-moz-range-thumb { width: 30px; height: 30px; margin: 0; border: 0; }
2055
+ .dsh-model-menu-sliderTrack {
2056
+ pointer-events: none;
2057
+ border-radius: 999px;
2058
+ width: 100%;
2059
+ height: 26px;
2060
+ background: var(--dsw-alias-interactive-bg-hover);
2061
+ box-shadow: inset 0 0 0 0.5px var(--dsw-alias-border-l2);
2062
+ position: absolute;
2063
+ inset-inline: 0;
2064
+ top: 50%;
2065
+ transform: translateY(-50%);
2066
+ /* No overflow:hidden — it would clip the fill's drop-shadow glow; the bar's
2067
+ * own clip-path already supplies the rounded reveal shape. */
2068
+ }
2069
+ /* The accent ramp paints the full track width once and is revealed by an
2070
+ * animated clip-path, so raising the effort unwraps more of the spectrum
2071
+ * instead of squashing the same gradient into a narrower bar. The glow lives
2072
+ * on the un-clipped parent: a filter on the clipped element itself would be
2073
+ * clipped away with it. */
2074
+ .dsh-model-menu-sliderFill {
2075
+ display: block;
2076
+ height: 100%;
2077
+ filter: drop-shadow(0 0 5px var(--dsw-alias-state-business-primary));
2078
+ transition: filter var(--ds-transition-duration) var(--ds-ease-in-out);
2079
+ }
2080
+ .dsh-model-menu-sliderInput:hover:not(:disabled) ~ .dsh-model-menu-sliderTrack .dsh-model-menu-sliderFill,
2081
+ .dsh-model-menu-sliderInput:active:not(:disabled) ~ .dsh-model-menu-sliderTrack .dsh-model-menu-sliderFill {
2082
+ filter: drop-shadow(0 0 9px var(--dsw-alias-state-business-primary));
2083
+ }
2084
+ /* The fill stops at the thumb's center, not at the track edge — at a 30px
2085
+ * thumb the end stops sit 15px in from each side, matching the node dots. */
2086
+ .dsh-model-menu-sliderFillBar {
2087
+ display: block;
2088
+ height: 100%;
2089
+ background: linear-gradient(90deg,
2090
+ var(--dsw-alias-state-business-primary),
2091
+ var(--dsw-alias-state-success-primary),
2092
+ var(--dsw-alias-state-warn-primary),
2093
+ var(--dsw-alias-state-error-primary));
2094
+ clip-path: inset(0 calc(100% - 15px - (100% - 30px) * var(--_p, 0) / 100) 0 0 round 999px);
2095
+ transition: clip-path var(--ds-transition-duration) var(--ds-ease-in-out);
2096
+ }
2097
+ /* Stop dots: one per declared row (default + every advertised effort), evenly
2098
+ * spread across the band between the two thumb-center extremes (inset 15px,
2099
+ * i.e. half the 30px thumb). --_n is 0-100 along that band. A covered dot
2100
+ * reads as a pale pin through the gradient; an uncovered one stays a neutral
2101
+ * notch on the empty track. */
2102
+ .dsh-model-menu-sliderNodes {
2103
+ pointer-events: none;
2104
+ display: block;
2105
+ position: absolute;
2106
+ inset-inline: 15px;
2107
+ inset-block: 0;
2108
+ }
2109
+ .dsh-model-menu-sliderNode {
2110
+ pointer-events: none;
2111
+ box-sizing: border-box;
2112
+ width: 4px;
2113
+ height: 4px;
2114
+ border-radius: 50%;
2115
+ background: color-mix(in srgb, var(--dsw-alias-label-secondary) 35%, transparent);
2116
+ position: absolute;
2117
+ left: calc(var(--_n, 0) * 1%);
2118
+ top: 50%;
2119
+ transform: translate(-50%, -50%);
2120
+ transition: background-color var(--ds-transition-duration) var(--ds-ease-in-out),
2121
+ transform var(--ds-transition-duration) var(--ds-ease-in-out);
2122
+ }
2123
+ .dsh-model-menu-sliderNode[data-covered] { background: color-mix(in srgb, var(--dsw-alias-label-primary) 60%, transparent); }
2124
+ .dsh-model-menu-sliderThumb {
2125
+ pointer-events: none;
2126
+ box-sizing: border-box;
2127
+ border-radius: 50%;
2128
+ width: 30px;
2129
+ height: 30px;
2130
+ background: var(--dsw-alias-button-primary-fill);
2131
+ border: 0.5px solid var(--dsw-alias-border-l4);
2132
+ box-shadow: 0 0 0 3px var(--dsw-alias-interactive-bg-hover), var(--dsw-elevation-soft);
2133
+ position: absolute;
2134
+ left: calc((100% - 30px) * var(--_p, 0) / 100);
2135
+ top: 50%;
2136
+ transform: translateY(-50%) scale(0.92);
2137
+ transition: left var(--ds-transition-duration) var(--ds-ease-in-out),
2138
+ transform var(--ds-transition-duration) var(--ds-ease-in-out),
2139
+ box-shadow var(--ds-transition-duration) var(--ds-ease-in-out);
2140
+ }
2141
+ .dsh-model-menu-sliderInput:hover:not(:disabled) ~ .dsh-model-menu-sliderThumb { transform: translateY(-50%) scale(1); }
2142
+ .dsh-model-menu-sliderInput:active:not(:disabled) ~ .dsh-model-menu-sliderThumb {
2143
+ transform: translateY(-50%) scale(1.1);
2144
+ box-shadow: 0 0 0 4px var(--dsw-alias-interactive-bg-hover), 0 0 12px var(--dsw-alias-state-business-primary);
2145
+ }
2146
+ .dsh-model-menu-sliderInput:focus-visible ~ .dsh-model-menu-sliderThumb {
2147
+ box-shadow: 0 0 0 3px var(--dsw-alias-interactive-bg-hover),
2148
+ 0 0 0 calc(3px + var(--dsw-focus-ring-width)) var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
2149
+ }
2150
+ @media (prefers-reduced-motion: reduce) {
2151
+ .dsh-model-menu-sliderFill, .dsh-model-menu-sliderFillBar, .dsh-model-menu-sliderThumb,
2152
+ .dsh-model-menu-sliderNode, .dsh-model-menu-sliderValue { transition: none; }
2153
+ }
1841
2154
  `;
1842
2155
  //#endregion
2156
+ //#region ../dsh-plugin-kit/src/model-picker.tsx
2157
+ /**
2158
+ * Optional shared model picker (`klarkxy.model-picker` Factory).
2159
+ *
2160
+ * `@klarkxy/dsh-model-picker`, when installed, registers one root-scoped
2161
+ * Component Factory under `MODEL_PICKER_FACTORY`: an enhanced model menu with
2162
+ * a searchable provider-grouped list and a reasoning-effort slider, built on
2163
+ * the same ModelMenu recipe. Consumers keep their own ModelMenu as the
2164
+ * `fallback`; when no definition is registered the renderer resolves that
2165
+ * fallback verbatim, so the shared picker is strictly opt-in and every saved
2166
+ * value keeps its existing write path.
2167
+ *
2168
+ * `renderFactorySlot` is a framework-composed prop, never a context: the
2169
+ * component a plugin registers into any slot receives it automatically, but
2170
+ * the plugin must pass it down to whichever child renders the menu (or to the
2171
+ * render callback handed to registerSessionSettingsItem).
2172
+ *
2173
+ * The Factory manages its own open state; a consumer's `open`/`onOpenChange`
2174
+ * pair belongs to the fallback menu only and must not be fed to both.
2175
+ */
2176
+ /**
2177
+ * The trigger every consumer used to hand its own ModelMenu, rebuilt as the
2178
+ * factory's `anchor` local component: identical classes and chevron, the menu
2179
+ * state coming from the occurrence's props instead of local open state.
2180
+ *
2181
+ * `ariaLabel` and `title` are the whole accessible name and tooltip (the
2182
+ * caller formats "field: current route" itself because the factory's resolved
2183
+ * caption may intentionally be a short name). The component identity stays
2184
+ * put across label changes — a new function would unmount the trigger, and
2185
+ * its cleanup would close a menu that `closeOnPick={false}` is keeping open.
2186
+ * The latest label, title, and `onClose` are read from refs.
2187
+ */
2188
+ function useModelMenuAnchor(ariaLabel, title, onClose) {
2189
+ const closeRef = (0, react.useRef)(onClose);
2190
+ const labelRef = (0, react.useRef)(ariaLabel);
2191
+ const titleRef = (0, react.useRef)(title);
2192
+ closeRef.current = onClose;
2193
+ labelRef.current = ariaLabel;
2194
+ titleRef.current = title;
2195
+ return (0, react.useMemo)(() => {
2196
+ const ModelPickerTrigger = (props) => {
2197
+ (0, react.useEffect)(() => () => closeRef.current?.(), []);
2198
+ const record = props;
2199
+ const injected = {};
2200
+ for (const [key, value] of Object.entries(record)) if (key.startsWith("aria-")) injected[key] = value;
2201
+ const extraClass = typeof record.className === "string" ? ` ${record.className}` : "";
2202
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2203
+ type: "button",
2204
+ className: `dsh-model-menu-trigger${extraClass}`,
2205
+ disabled: props.disabled,
2206
+ "aria-haspopup": "menu",
2207
+ "aria-expanded": props.open,
2208
+ ...injected,
2209
+ "aria-label": labelRef.current,
2210
+ title: titleRef.current,
2211
+ children: [
2212
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2213
+ className: "dsh-model-menu-triggerLabel",
2214
+ children: props.label
2215
+ }),
2216
+ props.effortLabel ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2217
+ className: "dsh-model-menu-triggerEffort",
2218
+ children: props.effortLabel
2219
+ }) : null,
2220
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${props.open ? " dsh-model-menu-chevronOpen" : ""}` })
2221
+ ]
2222
+ });
2223
+ };
2224
+ return ModelPickerTrigger;
2225
+ }, []);
2226
+ }
2227
+ /**
2228
+ * Render the shared model picker while it is registered; `options.fallback`
2229
+ * whenever it is not (plugin absent, disabled, or unloaded).
2230
+ *
2231
+ * `options.anchor` is optional: the factory ships a default trigger, so only
2232
+ * consumers that need their own chrome (custom aria-label, tooltip, class)
2233
+ * pass one built with {@link useModelMenuAnchor}. The returned node is never
2234
+ * assumed to be the factory's own output — a defensive `?? fallback` covers a
2235
+ * renderer that yields null for an unregistered definition.
2236
+ */
2237
+ function renderModelPicker(renderFactorySlot, props, options) {
2238
+ if (typeof renderFactorySlot !== "function") return options.fallback;
2239
+ return renderFactorySlot("klarkxy.model-picker", props, {
2240
+ ...options.anchor ? { slots: { ["anchor"]: options.anchor } } : {},
2241
+ fallback: options.fallback
2242
+ }) ?? options.fallback;
2243
+ }
2244
+ //#endregion
1843
2245
  //#region ../dsh-plugin-kit/src/auxiliary-model-client.tsx
1844
2246
  const AUXILIARY_MODEL_PACKAGE = "@klarkxy/dsh-plugin-kit";
1845
2247
  const AUXILIARY_MODEL_SEAT_ID = "auxiliary-model";
@@ -1917,7 +2319,7 @@ async function callAuxiliaryRpc(client, endpoint, payload = {}) {
1917
2319
  * The shared card. All state comes from the shared host service over the
1918
2320
  * plugin-kit RPC channel; the card only renders the stored row.
1919
2321
  */
1920
- function AuxiliaryModelCard({ client, subscribe }) {
2322
+ function AuxiliaryModelCard({ client, subscribe, renderFactorySlot }) {
1921
2323
  const language = useAuxiliaryLocale(client);
1922
2324
  const text = auxiliaryCopy[language];
1923
2325
  const [settings, setSettings] = (0, react.useState)();
@@ -2038,6 +2440,17 @@ function AuxiliaryModelCard({ client, subscribe }) {
2038
2440
  refreshCatalog
2039
2441
  ]);
2040
2442
  const locked = busy;
2443
+ const current = settings?.model ?? {
2444
+ provider: "",
2445
+ model: ""
2446
+ };
2447
+ const follow = !current.provider || !current.model;
2448
+ const selected = follow ? void 0 : choices.find((item) => item.provider === current.provider && item.model === current.model);
2449
+ const efforts = follow ? [] : modelMenuEffortOptions(selected, current.reasoningEffort);
2450
+ const currentLabel = follow ? text.modelDefault : selected?.label ?? `${current.provider} / ${current.model}`;
2451
+ const selectedEffort = efforts.find((item) => item.id === (current.reasoningEffort || ""));
2452
+ const effortName = selectedEffort ? modelMenuEffortLabel(selectedEffort, language) : void 0;
2453
+ const anchor = useModelMenuAnchor(`${text.model}: ${settings ? currentLabel : ""}`, void 0, () => setMenuOpen(false));
2041
2454
  if (!settings) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2042
2455
  className: "ams-root dsh-ui-card",
2043
2456
  "data-testid": "auxiliary-model-settings",
@@ -2076,16 +2489,6 @@ function AuxiliaryModelCard({ client, subscribe }) {
2076
2489
  })
2077
2490
  ]
2078
2491
  });
2079
- const current = settings.model ?? {
2080
- provider: "",
2081
- model: ""
2082
- };
2083
- const follow = !current.provider || !current.model;
2084
- const selected = follow ? void 0 : choices.find((item) => item.provider === current.provider && item.model === current.model);
2085
- const efforts = follow ? [] : modelMenuEffortOptions(selected, current.reasoningEffort);
2086
- const currentLabel = follow ? text.modelDefault : selected?.label ?? `${current.provider} / ${current.model}`;
2087
- const selectedEffort = efforts.find((item) => item.id === (current.reasoningEffort || ""));
2088
- const effortName = selectedEffort ? modelMenuEffortLabel(selectedEffort, language) : void 0;
2089
2492
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2090
2493
  className: "ams-root dsh-ui-card",
2091
2494
  "data-testid": "auxiliary-model-settings",
@@ -2109,28 +2512,12 @@ function AuxiliaryModelCard({ client, subscribe }) {
2109
2512
  state: catalogState,
2110
2513
  locale: language
2111
2514
  }),
2112
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
2515
+ renderModelPicker(renderFactorySlot, {
2113
2516
  locale: language,
2114
- open: menuOpen && !locked,
2517
+ disabled: locked,
2518
+ busy: locked,
2115
2519
  onOpenChange: (next) => setMenuOpen(next && !locked),
2116
2520
  listClassName: "ams-root ams-menu-portal",
2117
- anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2118
- type: "button",
2119
- className: "dsh-model-menu-trigger",
2120
- disabled: locked,
2121
- "aria-label": `${text.model}: ${currentLabel}`,
2122
- children: [
2123
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2124
- className: "dsh-model-menu-triggerLabel",
2125
- children: currentLabel
2126
- }),
2127
- !follow && efforts.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2128
- className: "dsh-model-menu-triggerEffort",
2129
- children: effortName ?? text.effortDefault
2130
- }) : null,
2131
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${menuOpen && !locked ? " dsh-model-menu-chevronOpen" : ""}` })
2132
- ]
2133
- }),
2134
2521
  choices,
2135
2522
  selected: follow ? void 0 : {
2136
2523
  provider: current.provider,
@@ -2153,9 +2540,61 @@ function AuxiliaryModelCard({ client, subscribe }) {
2153
2540
  model: current.model,
2154
2541
  ...effort ? { reasoningEffort: effort } : {}
2155
2542
  }),
2543
+ triggerLabel: currentLabel,
2544
+ triggerEffortLabel: !follow && efforts.length ? effortName ?? text.effortDefault : void 0,
2156
2545
  modelLabel: text.model,
2157
2546
  modelValue: currentLabel,
2158
2547
  defaultEffortLabel: text.effortDefault
2548
+ }, {
2549
+ anchor,
2550
+ fallback: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
2551
+ locale: language,
2552
+ open: menuOpen && !locked,
2553
+ onOpenChange: (next) => setMenuOpen(next && !locked),
2554
+ listClassName: "ams-root ams-menu-portal",
2555
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2556
+ type: "button",
2557
+ className: "dsh-model-menu-trigger",
2558
+ disabled: locked,
2559
+ "aria-label": `${text.model}: ${currentLabel}`,
2560
+ children: [
2561
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2562
+ className: "dsh-model-menu-triggerLabel",
2563
+ children: currentLabel
2564
+ }),
2565
+ !follow && efforts.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2566
+ className: "dsh-model-menu-triggerEffort",
2567
+ children: effortName ?? text.effortDefault
2568
+ }) : null,
2569
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${menuOpen && !locked ? " dsh-model-menu-chevronOpen" : ""}` })
2570
+ ]
2571
+ }),
2572
+ choices,
2573
+ selected: follow ? void 0 : {
2574
+ provider: current.provider,
2575
+ model: current.model
2576
+ },
2577
+ onPick: (picked) => save({
2578
+ provider: picked.provider,
2579
+ model: picked.model
2580
+ }),
2581
+ leading: [{
2582
+ id: "default",
2583
+ label: text.modelDefault,
2584
+ selected: follow
2585
+ }],
2586
+ onPickLeading: () => save(null),
2587
+ efforts,
2588
+ selectedEffort: current.reasoningEffort || "",
2589
+ onPickEffort: (effort) => save(follow ? null : {
2590
+ provider: current.provider,
2591
+ model: current.model,
2592
+ ...effort ? { reasoningEffort: effort } : {}
2593
+ }),
2594
+ modelLabel: text.model,
2595
+ modelValue: currentLabel,
2596
+ defaultEffortLabel: text.effortDefault
2597
+ })
2159
2598
  }),
2160
2599
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2161
2600
  className: "dsh-ui-hint",
@@ -2257,9 +2696,10 @@ function acquireAuxiliaryModelCard(root, ctx) {
2257
2696
  let unprovide;
2258
2697
  const watchLease = acquireWatchCoordinator(root).mount();
2259
2698
  const subscriber = createModelSettingsSubscriber(ctx.root, AUXILIARY_MODEL_PACKAGE, AUXILIARY_MODEL_CHANGES_IDENTITY, (ns) => ns === AUXILIARY_MODEL_NS);
2260
- const render = () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AuxiliaryModelCard, {
2699
+ const render = (props) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AuxiliaryModelCard, {
2261
2700
  client: root,
2262
- subscribe: subscriber.subscribe
2701
+ subscribe: subscriber.subscribe,
2702
+ renderFactorySlot: props.renderFactorySlot
2263
2703
  });
2264
2704
  const service = {
2265
2705
  retain() {
@@ -2721,6 +3161,7 @@ function TitleSettings(props) {
2721
3161
  const currentLabel = follow ? text.modelDefault : selectedModel?.label ?? `${savedModel.provider} / ${savedModel.model}`;
2722
3162
  const selectedEffort = efforts.find((item) => item.id === (savedModel.reasoningEffort || ""));
2723
3163
  const effortName = selectedEffort ? modelMenuEffortLabel(selectedEffort, seat.locale) : void 0;
3164
+ const anchor = useModelMenuAnchor(`${text.model}: ${currentLabel}`, void 0, () => setMenuOpen(false));
2724
3165
  const title = status?.session?.title;
2725
3166
  const pinned = status?.session?.pinned === true;
2726
3167
  const weOwn = status?.support?.weOwn === true;
@@ -2932,28 +3373,12 @@ function TitleSettings(props) {
2932
3373
  state: catalogState,
2933
3374
  locale: seat.locale
2934
3375
  }),
2935
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
3376
+ renderModelPicker(props.renderFactorySlot, {
2936
3377
  locale: seat.locale,
2937
- open: menuOpen && Boolean(status) && !busy && !loadingSettings,
3378
+ disabled: busy || loadingSettings || !status,
3379
+ busy: busy || loadingSettings,
2938
3380
  onOpenChange: (next) => setMenuOpen(next && Boolean(status) && !busy && !loadingSettings),
2939
3381
  listClassName: "ct-root",
2940
- anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2941
- type: "button",
2942
- className: "dsh-model-menu-trigger",
2943
- disabled: busy || loadingSettings || !status,
2944
- "aria-label": `${text.model}: ${currentLabel}`,
2945
- children: [
2946
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2947
- className: "dsh-model-menu-triggerLabel",
2948
- children: currentLabel
2949
- }),
2950
- !follow && efforts.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2951
- className: "dsh-model-menu-triggerEffort",
2952
- children: effortName ?? text.modelDefault
2953
- }) : null,
2954
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${menuOpen ? " dsh-model-menu-chevronOpen" : ""}` })
2955
- ]
2956
- }),
2957
3382
  choices,
2958
3383
  selected: follow ? void 0 : {
2959
3384
  provider: savedModel.provider,
@@ -2982,9 +3407,67 @@ function TitleSettings(props) {
2982
3407
  model: savedModel.model,
2983
3408
  ...effort ? { reasoningEffort: effort } : {}
2984
3409
  } }),
3410
+ triggerLabel: currentLabel,
3411
+ triggerEffortLabel: !follow && efforts.length ? effortName ?? text.modelDefault : void 0,
2985
3412
  modelLabel: text.model,
2986
3413
  modelValue: currentLabel,
2987
3414
  defaultEffortLabel: text.modelDefault
3415
+ }, {
3416
+ anchor,
3417
+ fallback: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
3418
+ locale: seat.locale,
3419
+ open: menuOpen && Boolean(status) && !busy && !loadingSettings,
3420
+ onOpenChange: (next) => setMenuOpen(next && Boolean(status) && !busy && !loadingSettings),
3421
+ listClassName: "ct-root",
3422
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3423
+ type: "button",
3424
+ className: "dsh-model-menu-trigger",
3425
+ disabled: busy || loadingSettings || !status,
3426
+ "aria-label": `${text.model}: ${currentLabel}`,
3427
+ children: [
3428
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3429
+ className: "dsh-model-menu-triggerLabel",
3430
+ children: currentLabel
3431
+ }),
3432
+ !follow && efforts.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3433
+ className: "dsh-model-menu-triggerEffort",
3434
+ children: effortName ?? text.modelDefault
3435
+ }) : null,
3436
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${menuOpen ? " dsh-model-menu-chevronOpen" : ""}` })
3437
+ ]
3438
+ }),
3439
+ choices,
3440
+ selected: follow ? void 0 : {
3441
+ provider: savedModel.provider,
3442
+ model: savedModel.model
3443
+ },
3444
+ onPick: (picked) => saveSettings({ model: {
3445
+ provider: picked.provider,
3446
+ model: picked.model
3447
+ } }),
3448
+ leading: [{
3449
+ id: "default",
3450
+ label: text.modelDefault,
3451
+ selected: follow
3452
+ }],
3453
+ onPickLeading: () => saveSettings({ model: {
3454
+ provider: "",
3455
+ model: ""
3456
+ } }),
3457
+ efforts,
3458
+ selectedEffort: savedModel.reasoningEffort || "",
3459
+ onPickEffort: (effort) => saveSettings({ model: follow ? {
3460
+ provider: "",
3461
+ model: ""
3462
+ } : {
3463
+ provider: savedModel.provider,
3464
+ model: savedModel.model,
3465
+ ...effort ? { reasoningEffort: effort } : {}
3466
+ } }),
3467
+ modelLabel: text.model,
3468
+ modelValue: currentLabel,
3469
+ defaultEffortLabel: text.modelDefault
3470
+ })
2988
3471
  }),
2989
3472
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2990
3473
  className: "dsh-ui-meta",
@@ -3052,7 +3535,8 @@ function TitleSettingsItem(props) {
3052
3535
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TitleSettings, {
3053
3536
  client: props.client,
3054
3537
  marker: props.marker,
3055
- subscribeSettings: props.subscribeSettings
3538
+ subscribeSettings: props.subscribeSettings,
3539
+ renderFactorySlot: props.renderFactorySlot
3056
3540
  })
3057
3541
  }) : null]
3058
3542
  });
@@ -3090,10 +3574,11 @@ function apply(ctx) {
3090
3574
  ctx.effect(() => registerSessionSettingsItem(ctx, {
3091
3575
  id: "current-title",
3092
3576
  order: 70
3093
- }, () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TitleSettingsItem, {
3577
+ }, (slotProps) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TitleSettingsItem, {
3094
3578
  client,
3095
3579
  marker,
3096
- subscribeSettings
3580
+ subscribeSettings,
3581
+ renderFactorySlot: slotProps?.renderFactorySlot
3097
3582
  })), "current-title.settings");
3098
3583
  ctx.effect(() => registerAuxiliaryModelSettings(ctx), "current-title.auxiliary-model");
3099
3584
  ctx.effect(() => {