@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.
@@ -96,6 +96,7 @@ function parseModelMenuChoices(value, bound) {
96
96
  provider,
97
97
  model,
98
98
  label: `${providerName} / ${name}`,
99
+ providerLabel: providerName,
99
100
  efforts
100
101
  });
101
102
  }
@@ -142,11 +143,11 @@ function modelMenuShortName(choice) {
142
143
  /** Group catalog choices by provider and filter them by a case-insensitive query. */
143
144
  function groupModelMenuChoices(choices, query) {
144
145
  const needle = query.trim().toLowerCase();
145
- const matched = needle ? choices.filter((choice) => choice.label.toLowerCase().includes(needle) || choice.model.toLowerCase().includes(needle) || choice.provider.toLowerCase().includes(needle)) : choices;
146
+ 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;
146
147
  const groups = [];
147
148
  for (const choice of matched) {
148
149
  const sep = choice.label.indexOf(" / ");
149
- const name = sep > 0 ? choice.label.slice(0, sep) : choice.provider;
150
+ const name = choice.providerLabel ?? (sep > 0 ? choice.label.slice(0, sep) : choice.provider);
150
151
  const last = groups.at(-1);
151
152
  if (last && last.provider === choice.provider) last.items.push(choice);
152
153
  else groups.push({
@@ -205,7 +206,7 @@ const AUXILIARY_MODEL_CHANGES_IDENTITY = modelSettingsChangesIdentity("@klarkxy/
205
206
  //#endregion
206
207
  //#region ../dsh-plugin-kit/src/client-utils.ts
207
208
  /** Fixed pause for failing background reads; manual work is never replayed. */
208
- function createPluginReadGate(now = Date.now) {
209
+ function createPluginReadGate(now = Date.now, logger) {
209
210
  let pausedUntil = 0;
210
211
  let epoch = 0;
211
212
  return {
@@ -218,11 +219,17 @@ function createPluginReadGate(now = Date.now) {
218
219
  const current = epoch;
219
220
  try {
220
221
  const value = await read();
221
- if (current === epoch) pausedUntil = 0;
222
+ if (current === epoch) {
223
+ if (pausedUntil > 0) logger?.debug("read-gate.resumed", "Background read gate recovered.");
224
+ pausedUntil = 0;
225
+ }
222
226
  return value;
223
227
  } catch (error) {
224
228
  const message = error instanceof Error ? error.message : "";
225
- 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;
229
+ 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))) {
230
+ pausedUntil = now() + 3e4;
231
+ logger?.warn("read-gate.paused", "Background reads paused for 30s after transport failure.", error);
232
+ }
226
233
  throw error;
227
234
  }
228
235
  }
@@ -519,7 +526,9 @@ Object.values({
519
526
  "--dsh-scrollbar-thumb-hover",
520
527
  "--dsh-content-font-size-secondary",
521
528
  "--dsh-content-font-delta",
522
- "--dsh-frame-top-clearance"
529
+ "--dsh-frame-top-clearance",
530
+ "--dsh-composer-model-icon-display",
531
+ "--dsh-composer-model-text-display"
523
532
  ]
524
533
  }).flat();
525
534
  /**
@@ -1223,7 +1232,7 @@ function ModelMenu(props) {
1223
1232
  empty: "No matching models",
1224
1233
  clear: "Clear search"
1225
1234
  };
1226
- 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;
1235
+ 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;
1227
1236
  const rootRef = (0, react.useRef)(null);
1228
1237
  const openRef = (0, react.useRef)(open);
1229
1238
  openRef.current = open;
@@ -1238,9 +1247,10 @@ function ModelMenu(props) {
1238
1247
  const [pane, setPane] = (0, react.useState)("root");
1239
1248
  const [query, setQuery] = (0, react.useState)("");
1240
1249
  const [highlightedIndex, setHighlightedIndex] = (0, react.useState)(null);
1250
+ const [keyNav, setKeyNav] = (0, react.useState)(0);
1241
1251
  const [menuPos, setMenuPos] = (0, react.useState)(null);
1242
1252
  const id = (0, react.useId)();
1243
- const showSearch = choices.length > 4;
1253
+ const showSearch = alwaysShowSearch || choices.length > 4;
1244
1254
  const effortRows = (0, react.useMemo)(() => [{
1245
1255
  id: "",
1246
1256
  name: defaultEffortLabel
@@ -1257,7 +1267,12 @@ function ModelMenu(props) {
1257
1267
  items: (0, _deepseek_ai_dsh_client_ui_primitives.rankByName)(group.items.map((choice) => ({
1258
1268
  choice,
1259
1269
  name: modelMenuShortName(choice),
1260
- label: choice.model
1270
+ label: [
1271
+ choice.provider,
1272
+ choice.providerLabel,
1273
+ choice.model,
1274
+ choice.label
1275
+ ].filter(Boolean).join(" ")
1261
1276
  })), showSearch ? query.trim() : "")
1262
1277
  })).filter((group) => group.items.length > 0), [
1263
1278
  choices,
@@ -1289,10 +1304,12 @@ function ModelMenu(props) {
1289
1304
  paneFocus.current = "drill";
1290
1305
  setPane(next);
1291
1306
  };
1292
- /** Leave a drilled pane for the root one, handing the keyboard back to its cell. */
1307
+ /** Leave a drilled pane for the entry pane, handing the keyboard back to it.
1308
+ * A root entry refocuses the cell that opened the pane; a search-first
1309
+ * (model) entry refocuses that pane's own intent. */
1293
1310
  const back = (from) => {
1294
- paneFocus.current = from;
1295
- setPane("root");
1311
+ paneFocus.current = initialPane === "root" ? from : "drill";
1312
+ setPane(initialPane);
1296
1313
  };
1297
1314
  const moveFocus = (offset) => {
1298
1315
  const items = itemRefs.current.filter((item) => item !== null && !item.disabled);
@@ -1332,10 +1349,18 @@ function ModelMenu(props) {
1332
1349
  }, [requestedOpen, open]);
1333
1350
  (0, react.useEffect)(() => {
1334
1351
  if (!open) return;
1335
- setPane("root");
1352
+ setPane(initialPane);
1353
+ if (initialPane === "model") paneFocus.current = "drill";
1336
1354
  setQuery("");
1337
1355
  setHighlightedIndex(null);
1338
- }, [open]);
1356
+ }, [open, initialPane]);
1357
+ (0, react.useEffect)(() => {
1358
+ if (disabled && open && closeOnDisable) changeOpen(false);
1359
+ }, [
1360
+ disabled,
1361
+ open,
1362
+ closeOnDisable
1363
+ ]);
1339
1364
  (0, react.useEffect)(() => {
1340
1365
  if (!open) return;
1341
1366
  const closeOutside = (event) => {
@@ -1343,9 +1368,25 @@ function ModelMenu(props) {
1343
1368
  if (rootRef.current?.contains(target) || menuRef.current?.contains(target)) return;
1344
1369
  changeOpen(false);
1345
1370
  };
1371
+ const escapeOutside = (event) => {
1372
+ if (event.key !== "Escape" || event.defaultPrevented || event.isComposing) return;
1373
+ if (!openRef.current) return;
1374
+ event.preventDefault();
1375
+ if (pane !== initialPane) back(pane);
1376
+ else close(true);
1377
+ };
1346
1378
  document.addEventListener("mousedown", closeOutside);
1347
- return () => document.removeEventListener("mousedown", closeOutside);
1348
- }, [open, onOpenChange]);
1379
+ document.addEventListener("keydown", escapeOutside);
1380
+ return () => {
1381
+ document.removeEventListener("mousedown", closeOutside);
1382
+ document.removeEventListener("keydown", escapeOutside);
1383
+ };
1384
+ }, [
1385
+ open,
1386
+ onOpenChange,
1387
+ pane,
1388
+ initialPane
1389
+ ]);
1349
1390
  (0, react.useEffect)(() => {
1350
1391
  const intent = paneFocus.current;
1351
1392
  if (!open || menuPos === null || intent === null) return;
@@ -1375,13 +1416,21 @@ function ModelMenu(props) {
1375
1416
  pane,
1376
1417
  filteredGroups
1377
1418
  ]);
1419
+ const modelPaneOpen = open && pane === "model";
1420
+ const scrollSignal = !modelPaneOpen ? "closed" : keyNav === 0 ? "open" : `key:${keyNav}`;
1421
+ const scrollSignalRef = (0, react.useRef)(null);
1378
1422
  (0, react.useLayoutEffect)(() => {
1379
- if (open && pane === "model" && activeModelIndex >= 0) itemRefs.current[leading.length + activeModelIndex]?.scrollIntoView({ block: "nearest" });
1423
+ if (!modelPaneOpen || activeModelIndex < 0) {
1424
+ if (!modelPaneOpen) scrollSignalRef.current = null;
1425
+ return;
1426
+ }
1427
+ if (scrollSignalRef.current === scrollSignal) return;
1428
+ scrollSignalRef.current = scrollSignal;
1429
+ itemRefs.current[leading.length + activeModelIndex]?.scrollIntoView({ block: "nearest" });
1380
1430
  }, [
1381
- open,
1382
- pane,
1431
+ modelPaneOpen,
1432
+ scrollSignal,
1383
1433
  activeModelIndex,
1384
- visibleModels,
1385
1434
  leading.length
1386
1435
  ]);
1387
1436
  (0, react.useLayoutEffect)(() => {
@@ -1396,7 +1445,7 @@ function ModelMenu(props) {
1396
1445
  let x = rect.right - lw, y = rect.top - 8 - lh;
1397
1446
  if (lw > 0) x = Math.min(Math.max(x, margin), window.innerWidth - lw - margin);
1398
1447
  if (lh > 0) y = Math.min(Math.max(y, margin), window.innerHeight - lh - margin);
1399
- setMenuPos({
1448
+ setMenuPos((prev) => prev && prev.left === x && prev.top === y ? prev : {
1400
1449
  left: x,
1401
1450
  top: y
1402
1451
  });
@@ -1416,6 +1465,13 @@ function ModelMenu(props) {
1416
1465
  ]);
1417
1466
  function onKeyDown(event) {
1418
1467
  if (event.defaultPrevented || event.nativeEvent.isComposing) return;
1468
+ if (event.key === "Escape" && open) {
1469
+ event.preventDefault();
1470
+ if (pane !== initialPane) back(pane);
1471
+ else close(true);
1472
+ return;
1473
+ }
1474
+ if (disabled) return;
1419
1475
  if (!open && [
1420
1476
  "Enter",
1421
1477
  " ",
@@ -1429,17 +1485,12 @@ function ModelMenu(props) {
1429
1485
  requestOpen();
1430
1486
  return;
1431
1487
  }
1432
- if (event.key === "Escape" && open) {
1433
- event.preventDefault();
1434
- if (pane !== "root") back(pane);
1435
- else close(true);
1436
- return;
1437
- }
1438
1488
  if (!open) return;
1439
1489
  if (pane === "model" && showSearch && (event.key === "ArrowDown" || event.key === "ArrowUp")) {
1440
1490
  event.preventDefault();
1441
1491
  if (visibleModels.length > 0) {
1442
1492
  setHighlightedIndex((activeModelIndex + (event.key === "ArrowDown" ? 1 : -1) + visibleModels.length) % visibleModels.length);
1493
+ setKeyNav((n) => n + 1);
1443
1494
  searchRef.current?.focus();
1444
1495
  }
1445
1496
  return;
@@ -1448,19 +1499,19 @@ function ModelMenu(props) {
1448
1499
  if (event.key === "Tab" && visibleModels.length === 0) return;
1449
1500
  event.preventDefault();
1450
1501
  const highlighted = visibleModels[activeModelIndex];
1451
- if (highlighted) {
1502
+ if (highlighted && !busy) {
1452
1503
  onPick({
1453
1504
  provider: highlighted.provider,
1454
1505
  model: highlighted.model
1455
1506
  });
1456
- close(true);
1507
+ if (closeOnPick) close(true);
1457
1508
  }
1458
1509
  return;
1459
1510
  }
1460
1511
  if (event.key === "Tab") {
1461
1512
  if (event.shiftKey) {
1462
1513
  event.preventDefault();
1463
- if (pane !== "root") back(pane);
1514
+ if (pane !== initialPane) back(pane);
1464
1515
  else close(true);
1465
1516
  return;
1466
1517
  }
@@ -1484,7 +1535,10 @@ function ModelMenu(props) {
1484
1535
  }
1485
1536
  }
1486
1537
  function onBlur(event) {
1487
- if (event.relatedTarget instanceof Node && (rootRef.current?.contains(event.relatedTarget) || menuRef.current?.contains(event.relatedTarget))) return;
1538
+ const related = event.relatedTarget;
1539
+ if (related instanceof Node && (rootRef.current?.contains(related) || menuRef.current?.contains(related) || !related.isConnected)) return;
1540
+ if (!closeOnPick && !(related instanceof Node)) return;
1541
+ if (busy && related == null) return;
1488
1542
  if (open) changeOpen(false);
1489
1543
  }
1490
1544
  itemRefs.current = [];
@@ -1505,11 +1559,16 @@ function ModelMenu(props) {
1505
1559
  "aria-label": modelLabel,
1506
1560
  lang: language,
1507
1561
  children: [
1562
+ menuHeader ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1563
+ className: "dsh-model-menu-header",
1564
+ children: menuHeader
1565
+ }) : null,
1508
1566
  pane === "root" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1509
1567
  ref: itemRef(),
1510
1568
  type: "button",
1511
1569
  role: "menuitem",
1512
1570
  className: "dsh-model-menu-cell",
1571
+ disabled,
1513
1572
  onClick: () => drill("model"),
1514
1573
  children: [
1515
1574
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -1522,11 +1581,12 @@ function ModelMenu(props) {
1522
1581
  }),
1523
1582
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutlineRegular, { className: "dsh-model-menu-cellChevron" })
1524
1583
  ]
1525
- }), efforts.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1584
+ }), efforts.length > 0 && !reasoningSlider ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1526
1585
  ref: itemRef(),
1527
1586
  type: "button",
1528
1587
  role: "menuitem",
1529
1588
  className: "dsh-model-menu-cell",
1589
+ disabled,
1530
1590
  onClick: () => drill("effort"),
1531
1591
  children: [
1532
1592
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -1545,6 +1605,7 @@ function ModelMenu(props) {
1545
1605
  className: "dsh-model-menu-searchRow",
1546
1606
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
1547
1607
  ref: searchRef,
1608
+ disabled,
1548
1609
  className: `dsh-model-menu-search${query !== "" ? " dsh-model-menu-searchWithQuery" : ""}`,
1549
1610
  type: "text",
1550
1611
  role: "searchbox",
@@ -1561,6 +1622,7 @@ function ModelMenu(props) {
1561
1622
  type: "button",
1562
1623
  className: "dsh-model-menu-searchClear",
1563
1624
  "aria-label": clearSearchLabel,
1625
+ disabled,
1564
1626
  onClick: () => {
1565
1627
  setQuery("");
1566
1628
  setHighlightedIndex(null);
@@ -1583,10 +1645,12 @@ function ModelMenu(props) {
1583
1645
  type: "button",
1584
1646
  role: "menuitemradio",
1585
1647
  "aria-checked": item.selected === true,
1648
+ disabled: disabled || busy,
1586
1649
  className: `dsh-model-menu-option${item.selected === true ? " dsh-model-menu-selected" : ""}`,
1587
1650
  onClick: () => {
1651
+ if (disabled || busy) return;
1588
1652
  onPickLeading?.(item.id);
1589
- close(true);
1653
+ if (closeOnPick) close(true);
1590
1654
  },
1591
1655
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1592
1656
  className: "dsh-model-menu-optionCopy",
@@ -1612,6 +1676,7 @@ function ModelMenu(props) {
1612
1676
  "aria-checked": checked,
1613
1677
  id: `${id}-model-${index}`,
1614
1678
  tabIndex: showSearch ? -1 : 0,
1679
+ disabled: disabled || busy,
1615
1680
  onFocus: () => setHighlightedIndex(index),
1616
1681
  "data-highlighted": index === activeModelIndex ? "" : void 0,
1617
1682
  className: `dsh-model-menu-option dsh-model-menu-modelOption${checked ? " dsh-model-menu-selected" : ""}${index === activeModelIndex ? " dsh-model-menu-optionActive" : ""}`,
@@ -1621,11 +1686,12 @@ function ModelMenu(props) {
1621
1686
  },
1622
1687
  title: choice.label,
1623
1688
  onClick: () => {
1689
+ if (disabled || busy) return;
1624
1690
  onPick({
1625
1691
  provider: choice.provider,
1626
1692
  model: choice.model
1627
1693
  });
1628
- close(true);
1694
+ if (closeOnPick) close(true);
1629
1695
  },
1630
1696
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1631
1697
  className: "dsh-model-menu-optionCopy",
@@ -1641,6 +1707,14 @@ function ModelMenu(props) {
1641
1707
  })
1642
1708
  }, group.provider))]
1643
1709
  }),
1710
+ reasoningSlider && efforts.length > 0 && filteredGroups.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EffortSlider, {
1711
+ label: effortLabel,
1712
+ rows: effortRows,
1713
+ selected: selectedEffort ?? "",
1714
+ disabled,
1715
+ busy,
1716
+ onPick: (value) => onPickEffort?.(value)
1717
+ }) : null,
1644
1718
  filteredGroups.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1645
1719
  className: "dsh-model-menu-empty",
1646
1720
  role: "status",
@@ -1654,10 +1728,12 @@ function ModelMenu(props) {
1654
1728
  type: "button",
1655
1729
  role: "menuitemradio",
1656
1730
  "aria-checked": checked,
1731
+ disabled: disabled || busy,
1657
1732
  className: `dsh-model-menu-option${checked ? " dsh-model-menu-selected" : ""}`,
1658
1733
  onClick: () => {
1734
+ if (disabled || busy) return;
1659
1735
  onPickEffort?.(row.id || void 0);
1660
- close(true);
1736
+ if (closeOnPick) close(true);
1661
1737
  },
1662
1738
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1663
1739
  className: "dsh-model-menu-optionCopy",
@@ -1676,7 +1752,8 @@ function ModelMenu(props) {
1676
1752
  const trigger = (0, react.isValidElement)(anchor) ? (0, react.cloneElement)(anchor, {
1677
1753
  "aria-haspopup": "menu",
1678
1754
  "aria-expanded": open,
1679
- "aria-controls": open ? `${id}-menu` : void 0
1755
+ "aria-controls": open ? `${id}-menu` : void 0,
1756
+ "aria-disabled": disabled || void 0
1680
1757
  }) : anchor;
1681
1758
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1682
1759
  ref: rootRef,
@@ -1684,7 +1761,7 @@ function ModelMenu(props) {
1684
1761
  onKeyDown,
1685
1762
  onBlur,
1686
1763
  onClick: (event) => {
1687
- if (event.defaultPrevented) return;
1764
+ if (disabled || event.defaultPrevented) return;
1688
1765
  const button = event.target instanceof Element ? event.target.closest("button") : null;
1689
1766
  if (!(button instanceof HTMLButtonElement) || button.disabled || !rootRef.current?.contains(button)) return;
1690
1767
  requestOpen();
@@ -1696,6 +1773,102 @@ function ModelMenu(props) {
1696
1773
  });
1697
1774
  }
1698
1775
  /**
1776
+ * Discrete reasoning-effort slider shown under the model list in enhanced mode.
1777
+ *
1778
+ * A native range input (`step=1`) over exactly the rows the caller supplies —
1779
+ * the default row plus each effort the selected model advertises — so it can
1780
+ * never invent a level. It previews locally while dragging or arrowing and
1781
+ * commits only when the gesture ends (pointer up, key up, or blur), so a drag
1782
+ * does not write on every step. The menu stays open, so adjusting the effort
1783
+ * never loses the model selection or the search query.
1784
+ *
1785
+ * The input itself stays the entire interaction and accessibility surface —
1786
+ * keyboard, ARIA, disabled and the native step snapping all come from it —
1787
+ * while the track, gradient fill and thumb below are decorative layers painted
1788
+ * from the `--_p` percentage the preview position sets on the rail. Value
1789
+ * changes only update that custom property, so the CSS transitions animate
1790
+ * between levels without remounting nodes or restarting an animation.
1791
+ */
1792
+ function EffortSlider(props) {
1793
+ const { label, rows, selected, disabled = false, busy = false, onPick } = props;
1794
+ const active = Math.max(0, rows.findIndex((row) => row.id === selected));
1795
+ const [position, setPosition] = (0, react.useState)(active);
1796
+ const positionRef = (0, react.useRef)(position);
1797
+ (0, react.useEffect)(() => {
1798
+ positionRef.current = active;
1799
+ setPosition(active);
1800
+ }, [active]);
1801
+ const preview = (next) => {
1802
+ positionRef.current = next;
1803
+ setPosition(next);
1804
+ };
1805
+ const commit = () => {
1806
+ if (disabled || busy) return;
1807
+ const row = rows[positionRef.current];
1808
+ if (row !== void 0) onPick(row.id === "" ? void 0 : row.id);
1809
+ };
1810
+ const max = Math.max(0, rows.length - 1);
1811
+ const percent = max > 0 ? Math.round(position / max * 100) : 0;
1812
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1813
+ className: "dsh-model-menu-sliderGroup",
1814
+ role: "group",
1815
+ "aria-label": label,
1816
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1817
+ className: "dsh-model-menu-sliderHead",
1818
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1819
+ className: "dsh-model-menu-sliderTitle",
1820
+ children: label
1821
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1822
+ className: "dsh-model-menu-sliderValue",
1823
+ children: rows[position]?.name
1824
+ })]
1825
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1826
+ className: "dsh-model-menu-sliderRail",
1827
+ style: { "--_p": percent },
1828
+ children: [
1829
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1830
+ type: "range",
1831
+ className: "dsh-model-menu-sliderInput",
1832
+ min: 0,
1833
+ max,
1834
+ step: 1,
1835
+ value: position,
1836
+ disabled,
1837
+ "aria-label": label,
1838
+ "aria-disabled": disabled || busy || void 0,
1839
+ "aria-valuetext": rows[position]?.name,
1840
+ onChange: (event) => {
1841
+ const value = Number(event.target.value);
1842
+ if (Number.isFinite(value)) preview(value);
1843
+ },
1844
+ onPointerUp: commit,
1845
+ onKeyUp: commit,
1846
+ onBlur: commit
1847
+ }),
1848
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1849
+ className: "dsh-model-menu-sliderTrack",
1850
+ "aria-hidden": "true",
1851
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1852
+ className: "dsh-model-menu-sliderFill",
1853
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dsh-model-menu-sliderFillBar" })
1854
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1855
+ className: "dsh-model-menu-sliderNodes",
1856
+ children: rows.map((row, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1857
+ "data-covered": index <= position ? "" : void 0,
1858
+ className: "dsh-model-menu-sliderNode",
1859
+ style: { "--_n": max > 0 ? Math.round(index / max * 100) : 0 }
1860
+ }, row.id || "__default"))
1861
+ })]
1862
+ }),
1863
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1864
+ className: "dsh-model-menu-sliderThumb",
1865
+ "aria-hidden": "true"
1866
+ })
1867
+ ]
1868
+ })]
1869
+ });
1870
+ }
1871
+ /**
1699
1872
  * ModelMenu styles, transcribed from the official ModelSelect.module.css (the
1700
1873
  * published dsh-client-ui-model-selection bundle): card capped at
1701
1874
  * min(360px, 100vh - 96px), 34px option cells at 13/18 with 5px 7px padding,
@@ -1754,6 +1927,7 @@ const modelMenuCss = `
1754
1927
  overflow: hidden;
1755
1928
  }
1756
1929
  .dsh-model-menu-status, .dsh-model-menu-empty { color: var(--dsw-alias-label-tertiary); padding: 8px; font-size: 12px; line-height: 18px; }
1930
+ .dsh-model-menu-header { flex: none; padding: 2px 2px 4px; }
1757
1931
  .dsh-model-menu-searchRow { flex-shrink: 0; margin: 2px 0 3px; position: relative; }
1758
1932
  .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; }
1759
1933
  .dsh-model-menu-searchRow .dsh-model-menu-search:focus-within { border-color: transparent; }
@@ -1832,8 +2006,236 @@ const modelMenuCss = `
1832
2006
  .dsh-model-menu-cellLabel { white-space: nowrap; flex: none; }
1833
2007
  .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; }
1834
2008
  .dsh-model-menu-cellChevron { width: 12px; height: 12px; color: var(--dsw-alias-menu-icon); flex: none; }
2009
+ /* Discrete reasoning-effort slider under the model list (enhanced mode).
2010
+ * The painted control is layered over a fully functional native range input:
2011
+ * the input stays on top, transparent, and owns pointer/keyboard/ARIA, while
2012
+ * the decorative track, fill and thumb render beneath it at pointer-events:
2013
+ * none. Position lives in one custom property (--_p, 0–100) on the rail, so
2014
+ * the only animated work a level change triggers is a clip-path/left
2015
+ * transition — no keyframes, no element churn. */
2016
+ .dsh-model-menu-sliderGroup { border-top: 0.5px solid var(--dsw-alias-border-l2); flex: none; margin-top: 4px; padding: 6px 8px 8px; }
2017
+ .dsh-model-menu-sliderHead { align-items: baseline; justify-content: space-between; gap: 8px; margin: 0 2px 4px; display: flex; }
2018
+ .dsh-model-menu-sliderTitle { color: var(--dsw-alias-label-tertiary); font-size: 12px; line-height: 18px; }
2019
+ .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); }
2020
+ .dsh-model-menu-sliderRail { position: relative; direction: ltr; height: 38px; margin-top: 2px; }
2021
+ .dsh-model-menu-sliderRail:has(:disabled) { opacity: 0.45; }
2022
+ .dsh-model-menu-sliderInput {
2023
+ appearance: none;
2024
+ -webkit-appearance: none;
2025
+ box-sizing: border-box;
2026
+ border: 0;
2027
+ cursor: pointer;
2028
+ direction: ltr;
2029
+ opacity: 0;
2030
+ margin: 0;
2031
+ padding: 0;
2032
+ width: 100%;
2033
+ height: 100%;
2034
+ position: absolute;
2035
+ inset: 0;
2036
+ z-index: 1;
2037
+ touch-action: pan-y;
2038
+ }
2039
+ .dsh-model-menu-sliderInput:disabled { cursor: default; }
2040
+ /* The transparent input must keep the painted thumb's geometry: an
2041
+ * appearance:none webkit thumb defaults to 16px, which would shift the
2042
+ * end-point positions 7px off the decorative 30px disc and misplace the
2043
+ * native drag hit target. Track gets the full rail height so the whole band
2044
+ * maps to values identically. */
2045
+ .dsh-model-menu-sliderInput::-webkit-slider-runnable-track { width: 100%; height: 100%; }
2046
+ .dsh-model-menu-sliderInput::-webkit-slider-thumb { appearance: none; -webkit-appearance: none; width: 30px; height: 30px; margin: 0; }
2047
+ .dsh-model-menu-sliderInput::-moz-range-track { width: 100%; height: 100%; }
2048
+ .dsh-model-menu-sliderInput::-moz-range-thumb { width: 30px; height: 30px; margin: 0; border: 0; }
2049
+ .dsh-model-menu-sliderTrack {
2050
+ pointer-events: none;
2051
+ border-radius: 999px;
2052
+ width: 100%;
2053
+ height: 26px;
2054
+ background: var(--dsw-alias-interactive-bg-hover);
2055
+ box-shadow: inset 0 0 0 0.5px var(--dsw-alias-border-l2);
2056
+ position: absolute;
2057
+ inset-inline: 0;
2058
+ top: 50%;
2059
+ transform: translateY(-50%);
2060
+ /* No overflow:hidden — it would clip the fill's drop-shadow glow; the bar's
2061
+ * own clip-path already supplies the rounded reveal shape. */
2062
+ }
2063
+ /* The accent ramp paints the full track width once and is revealed by an
2064
+ * animated clip-path, so raising the effort unwraps more of the spectrum
2065
+ * instead of squashing the same gradient into a narrower bar. The glow lives
2066
+ * on the un-clipped parent: a filter on the clipped element itself would be
2067
+ * clipped away with it. */
2068
+ .dsh-model-menu-sliderFill {
2069
+ display: block;
2070
+ height: 100%;
2071
+ filter: drop-shadow(0 0 5px var(--dsw-alias-state-business-primary));
2072
+ transition: filter var(--ds-transition-duration) var(--ds-ease-in-out);
2073
+ }
2074
+ .dsh-model-menu-sliderInput:hover:not(:disabled) ~ .dsh-model-menu-sliderTrack .dsh-model-menu-sliderFill,
2075
+ .dsh-model-menu-sliderInput:active:not(:disabled) ~ .dsh-model-menu-sliderTrack .dsh-model-menu-sliderFill {
2076
+ filter: drop-shadow(0 0 9px var(--dsw-alias-state-business-primary));
2077
+ }
2078
+ /* The fill stops at the thumb's center, not at the track edge — at a 30px
2079
+ * thumb the end stops sit 15px in from each side, matching the node dots. */
2080
+ .dsh-model-menu-sliderFillBar {
2081
+ display: block;
2082
+ height: 100%;
2083
+ background: linear-gradient(90deg,
2084
+ var(--dsw-alias-state-business-primary),
2085
+ var(--dsw-alias-state-success-primary),
2086
+ var(--dsw-alias-state-warn-primary),
2087
+ var(--dsw-alias-state-error-primary));
2088
+ clip-path: inset(0 calc(100% - 15px - (100% - 30px) * var(--_p, 0) / 100) 0 0 round 999px);
2089
+ transition: clip-path var(--ds-transition-duration) var(--ds-ease-in-out);
2090
+ }
2091
+ /* Stop dots: one per declared row (default + every advertised effort), evenly
2092
+ * spread across the band between the two thumb-center extremes (inset 15px,
2093
+ * i.e. half the 30px thumb). --_n is 0-100 along that band. A covered dot
2094
+ * reads as a pale pin through the gradient; an uncovered one stays a neutral
2095
+ * notch on the empty track. */
2096
+ .dsh-model-menu-sliderNodes {
2097
+ pointer-events: none;
2098
+ display: block;
2099
+ position: absolute;
2100
+ inset-inline: 15px;
2101
+ inset-block: 0;
2102
+ }
2103
+ .dsh-model-menu-sliderNode {
2104
+ pointer-events: none;
2105
+ box-sizing: border-box;
2106
+ width: 4px;
2107
+ height: 4px;
2108
+ border-radius: 50%;
2109
+ background: color-mix(in srgb, var(--dsw-alias-label-secondary) 35%, transparent);
2110
+ position: absolute;
2111
+ left: calc(var(--_n, 0) * 1%);
2112
+ top: 50%;
2113
+ transform: translate(-50%, -50%);
2114
+ transition: background-color var(--ds-transition-duration) var(--ds-ease-in-out),
2115
+ transform var(--ds-transition-duration) var(--ds-ease-in-out);
2116
+ }
2117
+ .dsh-model-menu-sliderNode[data-covered] { background: color-mix(in srgb, var(--dsw-alias-label-primary) 60%, transparent); }
2118
+ .dsh-model-menu-sliderThumb {
2119
+ pointer-events: none;
2120
+ box-sizing: border-box;
2121
+ border-radius: 50%;
2122
+ width: 30px;
2123
+ height: 30px;
2124
+ background: var(--dsw-alias-button-primary-fill);
2125
+ border: 0.5px solid var(--dsw-alias-border-l4);
2126
+ box-shadow: 0 0 0 3px var(--dsw-alias-interactive-bg-hover), var(--dsw-elevation-soft);
2127
+ position: absolute;
2128
+ left: calc((100% - 30px) * var(--_p, 0) / 100);
2129
+ top: 50%;
2130
+ transform: translateY(-50%) scale(0.92);
2131
+ transition: left var(--ds-transition-duration) var(--ds-ease-in-out),
2132
+ transform var(--ds-transition-duration) var(--ds-ease-in-out),
2133
+ box-shadow var(--ds-transition-duration) var(--ds-ease-in-out);
2134
+ }
2135
+ .dsh-model-menu-sliderInput:hover:not(:disabled) ~ .dsh-model-menu-sliderThumb { transform: translateY(-50%) scale(1); }
2136
+ .dsh-model-menu-sliderInput:active:not(:disabled) ~ .dsh-model-menu-sliderThumb {
2137
+ transform: translateY(-50%) scale(1.1);
2138
+ box-shadow: 0 0 0 4px var(--dsw-alias-interactive-bg-hover), 0 0 12px var(--dsw-alias-state-business-primary);
2139
+ }
2140
+ .dsh-model-menu-sliderInput:focus-visible ~ .dsh-model-menu-sliderThumb {
2141
+ box-shadow: 0 0 0 3px var(--dsw-alias-interactive-bg-hover),
2142
+ 0 0 0 calc(3px + var(--dsw-focus-ring-width)) var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
2143
+ }
2144
+ @media (prefers-reduced-motion: reduce) {
2145
+ .dsh-model-menu-sliderFill, .dsh-model-menu-sliderFillBar, .dsh-model-menu-sliderThumb,
2146
+ .dsh-model-menu-sliderNode, .dsh-model-menu-sliderValue { transition: none; }
2147
+ }
1835
2148
  `;
1836
2149
  //#endregion
2150
+ //#region ../dsh-plugin-kit/src/model-picker.tsx
2151
+ /**
2152
+ * Optional shared model picker (`klarkxy.model-picker` Factory).
2153
+ *
2154
+ * `@klarkxy/dsh-model-picker`, when installed, registers one root-scoped
2155
+ * Component Factory under `MODEL_PICKER_FACTORY`: an enhanced model menu with
2156
+ * a searchable provider-grouped list and a reasoning-effort slider, built on
2157
+ * the same ModelMenu recipe. Consumers keep their own ModelMenu as the
2158
+ * `fallback`; when no definition is registered the renderer resolves that
2159
+ * fallback verbatim, so the shared picker is strictly opt-in and every saved
2160
+ * value keeps its existing write path.
2161
+ *
2162
+ * `renderFactorySlot` is a framework-composed prop, never a context: the
2163
+ * component a plugin registers into any slot receives it automatically, but
2164
+ * the plugin must pass it down to whichever child renders the menu (or to the
2165
+ * render callback handed to registerSessionSettingsItem).
2166
+ *
2167
+ * The Factory manages its own open state; a consumer's `open`/`onOpenChange`
2168
+ * pair belongs to the fallback menu only and must not be fed to both.
2169
+ */
2170
+ /**
2171
+ * The trigger every consumer used to hand its own ModelMenu, rebuilt as the
2172
+ * factory's `anchor` local component: identical classes and chevron, the menu
2173
+ * state coming from the occurrence's props instead of local open state.
2174
+ *
2175
+ * `ariaLabel` and `title` are the whole accessible name and tooltip (the
2176
+ * caller formats "field: current route" itself because the factory's resolved
2177
+ * caption may intentionally be a short name). The component identity stays
2178
+ * put across label changes — a new function would unmount the trigger, and
2179
+ * its cleanup would close a menu that `closeOnPick={false}` is keeping open.
2180
+ * The latest label, title, and `onClose` are read from refs.
2181
+ */
2182
+ function useModelMenuAnchor(ariaLabel, title, onClose) {
2183
+ const closeRef = (0, react.useRef)(onClose);
2184
+ const labelRef = (0, react.useRef)(ariaLabel);
2185
+ const titleRef = (0, react.useRef)(title);
2186
+ closeRef.current = onClose;
2187
+ labelRef.current = ariaLabel;
2188
+ titleRef.current = title;
2189
+ return (0, react.useMemo)(() => {
2190
+ const ModelPickerTrigger = (props) => {
2191
+ (0, react.useEffect)(() => () => closeRef.current?.(), []);
2192
+ const record = props;
2193
+ const injected = {};
2194
+ for (const [key, value] of Object.entries(record)) if (key.startsWith("aria-")) injected[key] = value;
2195
+ const extraClass = typeof record.className === "string" ? ` ${record.className}` : "";
2196
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2197
+ type: "button",
2198
+ className: `dsh-model-menu-trigger${extraClass}`,
2199
+ disabled: props.disabled,
2200
+ "aria-haspopup": "menu",
2201
+ "aria-expanded": props.open,
2202
+ ...injected,
2203
+ "aria-label": labelRef.current,
2204
+ title: titleRef.current,
2205
+ children: [
2206
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2207
+ className: "dsh-model-menu-triggerLabel",
2208
+ children: props.label
2209
+ }),
2210
+ props.effortLabel ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2211
+ className: "dsh-model-menu-triggerEffort",
2212
+ children: props.effortLabel
2213
+ }) : null,
2214
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${props.open ? " dsh-model-menu-chevronOpen" : ""}` })
2215
+ ]
2216
+ });
2217
+ };
2218
+ return ModelPickerTrigger;
2219
+ }, []);
2220
+ }
2221
+ /**
2222
+ * Render the shared model picker while it is registered; `options.fallback`
2223
+ * whenever it is not (plugin absent, disabled, or unloaded).
2224
+ *
2225
+ * `options.anchor` is optional: the factory ships a default trigger, so only
2226
+ * consumers that need their own chrome (custom aria-label, tooltip, class)
2227
+ * pass one built with {@link useModelMenuAnchor}. The returned node is never
2228
+ * assumed to be the factory's own output — a defensive `?? fallback` covers a
2229
+ * renderer that yields null for an unregistered definition.
2230
+ */
2231
+ function renderModelPicker(renderFactorySlot, props, options) {
2232
+ if (typeof renderFactorySlot !== "function") return options.fallback;
2233
+ return renderFactorySlot("klarkxy.model-picker", props, {
2234
+ ...options.anchor ? { slots: { ["anchor"]: options.anchor } } : {},
2235
+ fallback: options.fallback
2236
+ }) ?? options.fallback;
2237
+ }
2238
+ //#endregion
1837
2239
  //#region ../dsh-plugin-kit/src/auxiliary-model-client.tsx
1838
2240
  const AUXILIARY_MODEL_PACKAGE = "@klarkxy/dsh-plugin-kit";
1839
2241
  const AUXILIARY_MODEL_SEAT_ID = "auxiliary-model";
@@ -1911,7 +2313,7 @@ async function callAuxiliaryRpc(client, endpoint, payload = {}) {
1911
2313
  * The shared card. All state comes from the shared host service over the
1912
2314
  * plugin-kit RPC channel; the card only renders the stored row.
1913
2315
  */
1914
- function AuxiliaryModelCard({ client, subscribe }) {
2316
+ function AuxiliaryModelCard({ client, subscribe, renderFactorySlot }) {
1915
2317
  const language = useAuxiliaryLocale(client);
1916
2318
  const text = auxiliaryCopy[language];
1917
2319
  const [settings, setSettings] = (0, react.useState)();
@@ -2032,6 +2434,17 @@ function AuxiliaryModelCard({ client, subscribe }) {
2032
2434
  refreshCatalog
2033
2435
  ]);
2034
2436
  const locked = busy;
2437
+ const current = settings?.model ?? {
2438
+ provider: "",
2439
+ model: ""
2440
+ };
2441
+ const follow = !current.provider || !current.model;
2442
+ const selected = follow ? void 0 : choices.find((item) => item.provider === current.provider && item.model === current.model);
2443
+ const efforts = follow ? [] : modelMenuEffortOptions(selected, current.reasoningEffort);
2444
+ const currentLabel = follow ? text.modelDefault : selected?.label ?? `${current.provider} / ${current.model}`;
2445
+ const selectedEffort = efforts.find((item) => item.id === (current.reasoningEffort || ""));
2446
+ const effortName = selectedEffort ? modelMenuEffortLabel(selectedEffort, language) : void 0;
2447
+ const anchor = useModelMenuAnchor(`${text.model}: ${settings ? currentLabel : ""}`, void 0, () => setMenuOpen(false));
2035
2448
  if (!settings) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2036
2449
  className: "ams-root dsh-ui-card",
2037
2450
  "data-testid": "auxiliary-model-settings",
@@ -2070,16 +2483,6 @@ function AuxiliaryModelCard({ client, subscribe }) {
2070
2483
  })
2071
2484
  ]
2072
2485
  });
2073
- const current = settings.model ?? {
2074
- provider: "",
2075
- model: ""
2076
- };
2077
- const follow = !current.provider || !current.model;
2078
- const selected = follow ? void 0 : choices.find((item) => item.provider === current.provider && item.model === current.model);
2079
- const efforts = follow ? [] : modelMenuEffortOptions(selected, current.reasoningEffort);
2080
- const currentLabel = follow ? text.modelDefault : selected?.label ?? `${current.provider} / ${current.model}`;
2081
- const selectedEffort = efforts.find((item) => item.id === (current.reasoningEffort || ""));
2082
- const effortName = selectedEffort ? modelMenuEffortLabel(selectedEffort, language) : void 0;
2083
2486
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2084
2487
  className: "ams-root dsh-ui-card",
2085
2488
  "data-testid": "auxiliary-model-settings",
@@ -2103,28 +2506,12 @@ function AuxiliaryModelCard({ client, subscribe }) {
2103
2506
  state: catalogState,
2104
2507
  locale: language
2105
2508
  }),
2106
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
2509
+ renderModelPicker(renderFactorySlot, {
2107
2510
  locale: language,
2108
- open: menuOpen && !locked,
2511
+ disabled: locked,
2512
+ busy: locked,
2109
2513
  onOpenChange: (next) => setMenuOpen(next && !locked),
2110
2514
  listClassName: "ams-root ams-menu-portal",
2111
- anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2112
- type: "button",
2113
- className: "dsh-model-menu-trigger",
2114
- disabled: locked,
2115
- "aria-label": `${text.model}: ${currentLabel}`,
2116
- children: [
2117
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2118
- className: "dsh-model-menu-triggerLabel",
2119
- children: currentLabel
2120
- }),
2121
- !follow && efforts.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2122
- className: "dsh-model-menu-triggerEffort",
2123
- children: effortName ?? text.effortDefault
2124
- }) : null,
2125
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${menuOpen && !locked ? " dsh-model-menu-chevronOpen" : ""}` })
2126
- ]
2127
- }),
2128
2515
  choices,
2129
2516
  selected: follow ? void 0 : {
2130
2517
  provider: current.provider,
@@ -2147,9 +2534,61 @@ function AuxiliaryModelCard({ client, subscribe }) {
2147
2534
  model: current.model,
2148
2535
  ...effort ? { reasoningEffort: effort } : {}
2149
2536
  }),
2537
+ triggerLabel: currentLabel,
2538
+ triggerEffortLabel: !follow && efforts.length ? effortName ?? text.effortDefault : void 0,
2150
2539
  modelLabel: text.model,
2151
2540
  modelValue: currentLabel,
2152
2541
  defaultEffortLabel: text.effortDefault
2542
+ }, {
2543
+ anchor,
2544
+ fallback: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
2545
+ locale: language,
2546
+ open: menuOpen && !locked,
2547
+ onOpenChange: (next) => setMenuOpen(next && !locked),
2548
+ listClassName: "ams-root ams-menu-portal",
2549
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2550
+ type: "button",
2551
+ className: "dsh-model-menu-trigger",
2552
+ disabled: locked,
2553
+ "aria-label": `${text.model}: ${currentLabel}`,
2554
+ children: [
2555
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2556
+ className: "dsh-model-menu-triggerLabel",
2557
+ children: currentLabel
2558
+ }),
2559
+ !follow && efforts.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2560
+ className: "dsh-model-menu-triggerEffort",
2561
+ children: effortName ?? text.effortDefault
2562
+ }) : null,
2563
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${menuOpen && !locked ? " dsh-model-menu-chevronOpen" : ""}` })
2564
+ ]
2565
+ }),
2566
+ choices,
2567
+ selected: follow ? void 0 : {
2568
+ provider: current.provider,
2569
+ model: current.model
2570
+ },
2571
+ onPick: (picked) => save({
2572
+ provider: picked.provider,
2573
+ model: picked.model
2574
+ }),
2575
+ leading: [{
2576
+ id: "default",
2577
+ label: text.modelDefault,
2578
+ selected: follow
2579
+ }],
2580
+ onPickLeading: () => save(null),
2581
+ efforts,
2582
+ selectedEffort: current.reasoningEffort || "",
2583
+ onPickEffort: (effort) => save(follow ? null : {
2584
+ provider: current.provider,
2585
+ model: current.model,
2586
+ ...effort ? { reasoningEffort: effort } : {}
2587
+ }),
2588
+ modelLabel: text.model,
2589
+ modelValue: currentLabel,
2590
+ defaultEffortLabel: text.effortDefault
2591
+ })
2153
2592
  }),
2154
2593
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2155
2594
  className: "dsh-ui-hint",
@@ -2251,9 +2690,10 @@ function acquireAuxiliaryModelCard(root, ctx) {
2251
2690
  let unprovide;
2252
2691
  const watchLease = acquireWatchCoordinator(root).mount();
2253
2692
  const subscriber = createModelSettingsSubscriber(ctx.root, AUXILIARY_MODEL_PACKAGE, AUXILIARY_MODEL_CHANGES_IDENTITY, (ns) => ns === AUXILIARY_MODEL_NS);
2254
- const render = () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AuxiliaryModelCard, {
2693
+ const render = (props) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AuxiliaryModelCard, {
2255
2694
  client: root,
2256
- subscribe: subscriber.subscribe
2695
+ subscribe: subscriber.subscribe,
2696
+ renderFactorySlot: props.renderFactorySlot
2257
2697
  });
2258
2698
  const service = {
2259
2699
  retain() {
@@ -2715,6 +3155,7 @@ function TitleSettings(props) {
2715
3155
  const currentLabel = follow ? text.modelDefault : selectedModel?.label ?? `${savedModel.provider} / ${savedModel.model}`;
2716
3156
  const selectedEffort = efforts.find((item) => item.id === (savedModel.reasoningEffort || ""));
2717
3157
  const effortName = selectedEffort ? modelMenuEffortLabel(selectedEffort, seat.locale) : void 0;
3158
+ const anchor = useModelMenuAnchor(`${text.model}: ${currentLabel}`, void 0, () => setMenuOpen(false));
2718
3159
  const title = status?.session?.title;
2719
3160
  const pinned = status?.session?.pinned === true;
2720
3161
  const weOwn = status?.support?.weOwn === true;
@@ -2926,28 +3367,12 @@ function TitleSettings(props) {
2926
3367
  state: catalogState,
2927
3368
  locale: seat.locale
2928
3369
  }),
2929
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
3370
+ renderModelPicker(props.renderFactorySlot, {
2930
3371
  locale: seat.locale,
2931
- open: menuOpen && Boolean(status) && !busy && !loadingSettings,
3372
+ disabled: busy || loadingSettings || !status,
3373
+ busy: busy || loadingSettings,
2932
3374
  onOpenChange: (next) => setMenuOpen(next && Boolean(status) && !busy && !loadingSettings),
2933
3375
  listClassName: "ct-root",
2934
- anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2935
- type: "button",
2936
- className: "dsh-model-menu-trigger",
2937
- disabled: busy || loadingSettings || !status,
2938
- "aria-label": `${text.model}: ${currentLabel}`,
2939
- children: [
2940
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2941
- className: "dsh-model-menu-triggerLabel",
2942
- children: currentLabel
2943
- }),
2944
- !follow && efforts.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2945
- className: "dsh-model-menu-triggerEffort",
2946
- children: effortName ?? text.modelDefault
2947
- }) : null,
2948
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${menuOpen ? " dsh-model-menu-chevronOpen" : ""}` })
2949
- ]
2950
- }),
2951
3376
  choices,
2952
3377
  selected: follow ? void 0 : {
2953
3378
  provider: savedModel.provider,
@@ -2976,9 +3401,67 @@ function TitleSettings(props) {
2976
3401
  model: savedModel.model,
2977
3402
  ...effort ? { reasoningEffort: effort } : {}
2978
3403
  } }),
3404
+ triggerLabel: currentLabel,
3405
+ triggerEffortLabel: !follow && efforts.length ? effortName ?? text.modelDefault : void 0,
2979
3406
  modelLabel: text.model,
2980
3407
  modelValue: currentLabel,
2981
3408
  defaultEffortLabel: text.modelDefault
3409
+ }, {
3410
+ anchor,
3411
+ fallback: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
3412
+ locale: seat.locale,
3413
+ open: menuOpen && Boolean(status) && !busy && !loadingSettings,
3414
+ onOpenChange: (next) => setMenuOpen(next && Boolean(status) && !busy && !loadingSettings),
3415
+ listClassName: "ct-root",
3416
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3417
+ type: "button",
3418
+ className: "dsh-model-menu-trigger",
3419
+ disabled: busy || loadingSettings || !status,
3420
+ "aria-label": `${text.model}: ${currentLabel}`,
3421
+ children: [
3422
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3423
+ className: "dsh-model-menu-triggerLabel",
3424
+ children: currentLabel
3425
+ }),
3426
+ !follow && efforts.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3427
+ className: "dsh-model-menu-triggerEffort",
3428
+ children: effortName ?? text.modelDefault
3429
+ }) : null,
3430
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${menuOpen ? " dsh-model-menu-chevronOpen" : ""}` })
3431
+ ]
3432
+ }),
3433
+ choices,
3434
+ selected: follow ? void 0 : {
3435
+ provider: savedModel.provider,
3436
+ model: savedModel.model
3437
+ },
3438
+ onPick: (picked) => saveSettings({ model: {
3439
+ provider: picked.provider,
3440
+ model: picked.model
3441
+ } }),
3442
+ leading: [{
3443
+ id: "default",
3444
+ label: text.modelDefault,
3445
+ selected: follow
3446
+ }],
3447
+ onPickLeading: () => saveSettings({ model: {
3448
+ provider: "",
3449
+ model: ""
3450
+ } }),
3451
+ efforts,
3452
+ selectedEffort: savedModel.reasoningEffort || "",
3453
+ onPickEffort: (effort) => saveSettings({ model: follow ? {
3454
+ provider: "",
3455
+ model: ""
3456
+ } : {
3457
+ provider: savedModel.provider,
3458
+ model: savedModel.model,
3459
+ ...effort ? { reasoningEffort: effort } : {}
3460
+ } }),
3461
+ modelLabel: text.model,
3462
+ modelValue: currentLabel,
3463
+ defaultEffortLabel: text.modelDefault
3464
+ })
2982
3465
  }),
2983
3466
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2984
3467
  className: "dsh-ui-meta",
@@ -3046,7 +3529,8 @@ function TitleSettingsItem(props) {
3046
3529
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TitleSettings, {
3047
3530
  client: props.client,
3048
3531
  marker: props.marker,
3049
- subscribeSettings: props.subscribeSettings
3532
+ subscribeSettings: props.subscribeSettings,
3533
+ renderFactorySlot: props.renderFactorySlot
3050
3534
  })
3051
3535
  }) : null]
3052
3536
  });
@@ -3084,10 +3568,11 @@ function apply(ctx) {
3084
3568
  ctx.effect(() => registerSessionSettingsItem(ctx, {
3085
3569
  id: "current-title",
3086
3570
  order: 70
3087
- }, () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TitleSettingsItem, {
3571
+ }, (slotProps) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TitleSettingsItem, {
3088
3572
  client,
3089
3573
  marker,
3090
- subscribeSettings
3574
+ subscribeSettings,
3575
+ renderFactorySlot: slotProps?.renderFactorySlot
3091
3576
  })), "current-title.settings");
3092
3577
  ctx.effect(() => registerAuxiliaryModelSettings(ctx), "current-title.auxiliary-model");
3093
3578
  ctx.effect(() => {