@klarkxy/dsh-current-title 0.3.3 → 0.3.4
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/README.md +2 -0
- package/docs/README.zh-CN.md +2 -0
- package/lib/client.inner.cjs +567 -88
- package/lib/client.inner.cjs.map +1 -1
- package/lib/client.js +567 -88
- package/lib/index.js +3 -2
- package/lib/index.js.map +1 -1
- package/package.json +10 -9
package/lib/client.inner.cjs
CHANGED
|
@@ -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({
|
|
@@ -519,7 +520,9 @@ Object.values({
|
|
|
519
520
|
"--dsh-scrollbar-thumb-hover",
|
|
520
521
|
"--dsh-content-font-size-secondary",
|
|
521
522
|
"--dsh-content-font-delta",
|
|
522
|
-
"--dsh-frame-top-clearance"
|
|
523
|
+
"--dsh-frame-top-clearance",
|
|
524
|
+
"--dsh-composer-model-icon-display",
|
|
525
|
+
"--dsh-composer-model-text-display"
|
|
523
526
|
]
|
|
524
527
|
}).flat();
|
|
525
528
|
/**
|
|
@@ -1223,7 +1226,7 @@ function ModelMenu(props) {
|
|
|
1223
1226
|
empty: "No matching models",
|
|
1224
1227
|
clear: "Clear search"
|
|
1225
1228
|
};
|
|
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;
|
|
1229
|
+
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
1230
|
const rootRef = (0, react.useRef)(null);
|
|
1228
1231
|
const openRef = (0, react.useRef)(open);
|
|
1229
1232
|
openRef.current = open;
|
|
@@ -1238,9 +1241,10 @@ function ModelMenu(props) {
|
|
|
1238
1241
|
const [pane, setPane] = (0, react.useState)("root");
|
|
1239
1242
|
const [query, setQuery] = (0, react.useState)("");
|
|
1240
1243
|
const [highlightedIndex, setHighlightedIndex] = (0, react.useState)(null);
|
|
1244
|
+
const [keyNav, setKeyNav] = (0, react.useState)(0);
|
|
1241
1245
|
const [menuPos, setMenuPos] = (0, react.useState)(null);
|
|
1242
1246
|
const id = (0, react.useId)();
|
|
1243
|
-
const showSearch = choices.length > 4;
|
|
1247
|
+
const showSearch = alwaysShowSearch || choices.length > 4;
|
|
1244
1248
|
const effortRows = (0, react.useMemo)(() => [{
|
|
1245
1249
|
id: "",
|
|
1246
1250
|
name: defaultEffortLabel
|
|
@@ -1257,7 +1261,12 @@ function ModelMenu(props) {
|
|
|
1257
1261
|
items: (0, _deepseek_ai_dsh_client_ui_primitives.rankByName)(group.items.map((choice) => ({
|
|
1258
1262
|
choice,
|
|
1259
1263
|
name: modelMenuShortName(choice),
|
|
1260
|
-
label:
|
|
1264
|
+
label: [
|
|
1265
|
+
choice.provider,
|
|
1266
|
+
choice.providerLabel,
|
|
1267
|
+
choice.model,
|
|
1268
|
+
choice.label
|
|
1269
|
+
].filter(Boolean).join(" ")
|
|
1261
1270
|
})), showSearch ? query.trim() : "")
|
|
1262
1271
|
})).filter((group) => group.items.length > 0), [
|
|
1263
1272
|
choices,
|
|
@@ -1289,10 +1298,12 @@ function ModelMenu(props) {
|
|
|
1289
1298
|
paneFocus.current = "drill";
|
|
1290
1299
|
setPane(next);
|
|
1291
1300
|
};
|
|
1292
|
-
/** Leave a drilled pane for the
|
|
1301
|
+
/** Leave a drilled pane for the entry pane, handing the keyboard back to it.
|
|
1302
|
+
* A root entry refocuses the cell that opened the pane; a search-first
|
|
1303
|
+
* (model) entry refocuses that pane's own intent. */
|
|
1293
1304
|
const back = (from) => {
|
|
1294
|
-
paneFocus.current = from;
|
|
1295
|
-
setPane(
|
|
1305
|
+
paneFocus.current = initialPane === "root" ? from : "drill";
|
|
1306
|
+
setPane(initialPane);
|
|
1296
1307
|
};
|
|
1297
1308
|
const moveFocus = (offset) => {
|
|
1298
1309
|
const items = itemRefs.current.filter((item) => item !== null && !item.disabled);
|
|
@@ -1332,10 +1343,18 @@ function ModelMenu(props) {
|
|
|
1332
1343
|
}, [requestedOpen, open]);
|
|
1333
1344
|
(0, react.useEffect)(() => {
|
|
1334
1345
|
if (!open) return;
|
|
1335
|
-
setPane(
|
|
1346
|
+
setPane(initialPane);
|
|
1347
|
+
if (initialPane === "model") paneFocus.current = "drill";
|
|
1336
1348
|
setQuery("");
|
|
1337
1349
|
setHighlightedIndex(null);
|
|
1338
|
-
}, [open]);
|
|
1350
|
+
}, [open, initialPane]);
|
|
1351
|
+
(0, react.useEffect)(() => {
|
|
1352
|
+
if (disabled && open && closeOnDisable) changeOpen(false);
|
|
1353
|
+
}, [
|
|
1354
|
+
disabled,
|
|
1355
|
+
open,
|
|
1356
|
+
closeOnDisable
|
|
1357
|
+
]);
|
|
1339
1358
|
(0, react.useEffect)(() => {
|
|
1340
1359
|
if (!open) return;
|
|
1341
1360
|
const closeOutside = (event) => {
|
|
@@ -1343,9 +1362,25 @@ function ModelMenu(props) {
|
|
|
1343
1362
|
if (rootRef.current?.contains(target) || menuRef.current?.contains(target)) return;
|
|
1344
1363
|
changeOpen(false);
|
|
1345
1364
|
};
|
|
1365
|
+
const escapeOutside = (event) => {
|
|
1366
|
+
if (event.key !== "Escape" || event.defaultPrevented || event.isComposing) return;
|
|
1367
|
+
if (!openRef.current) return;
|
|
1368
|
+
event.preventDefault();
|
|
1369
|
+
if (pane !== initialPane) back(pane);
|
|
1370
|
+
else close(true);
|
|
1371
|
+
};
|
|
1346
1372
|
document.addEventListener("mousedown", closeOutside);
|
|
1347
|
-
|
|
1348
|
-
|
|
1373
|
+
document.addEventListener("keydown", escapeOutside);
|
|
1374
|
+
return () => {
|
|
1375
|
+
document.removeEventListener("mousedown", closeOutside);
|
|
1376
|
+
document.removeEventListener("keydown", escapeOutside);
|
|
1377
|
+
};
|
|
1378
|
+
}, [
|
|
1379
|
+
open,
|
|
1380
|
+
onOpenChange,
|
|
1381
|
+
pane,
|
|
1382
|
+
initialPane
|
|
1383
|
+
]);
|
|
1349
1384
|
(0, react.useEffect)(() => {
|
|
1350
1385
|
const intent = paneFocus.current;
|
|
1351
1386
|
if (!open || menuPos === null || intent === null) return;
|
|
@@ -1375,13 +1410,21 @@ function ModelMenu(props) {
|
|
|
1375
1410
|
pane,
|
|
1376
1411
|
filteredGroups
|
|
1377
1412
|
]);
|
|
1413
|
+
const modelPaneOpen = open && pane === "model";
|
|
1414
|
+
const scrollSignal = !modelPaneOpen ? "closed" : keyNav === 0 ? "open" : `key:${keyNav}`;
|
|
1415
|
+
const scrollSignalRef = (0, react.useRef)(null);
|
|
1378
1416
|
(0, react.useLayoutEffect)(() => {
|
|
1379
|
-
if (
|
|
1417
|
+
if (!modelPaneOpen || activeModelIndex < 0) {
|
|
1418
|
+
if (!modelPaneOpen) scrollSignalRef.current = null;
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1421
|
+
if (scrollSignalRef.current === scrollSignal) return;
|
|
1422
|
+
scrollSignalRef.current = scrollSignal;
|
|
1423
|
+
itemRefs.current[leading.length + activeModelIndex]?.scrollIntoView({ block: "nearest" });
|
|
1380
1424
|
}, [
|
|
1381
|
-
|
|
1382
|
-
|
|
1425
|
+
modelPaneOpen,
|
|
1426
|
+
scrollSignal,
|
|
1383
1427
|
activeModelIndex,
|
|
1384
|
-
visibleModels,
|
|
1385
1428
|
leading.length
|
|
1386
1429
|
]);
|
|
1387
1430
|
(0, react.useLayoutEffect)(() => {
|
|
@@ -1396,7 +1439,7 @@ function ModelMenu(props) {
|
|
|
1396
1439
|
let x = rect.right - lw, y = rect.top - 8 - lh;
|
|
1397
1440
|
if (lw > 0) x = Math.min(Math.max(x, margin), window.innerWidth - lw - margin);
|
|
1398
1441
|
if (lh > 0) y = Math.min(Math.max(y, margin), window.innerHeight - lh - margin);
|
|
1399
|
-
setMenuPos({
|
|
1442
|
+
setMenuPos((prev) => prev && prev.left === x && prev.top === y ? prev : {
|
|
1400
1443
|
left: x,
|
|
1401
1444
|
top: y
|
|
1402
1445
|
});
|
|
@@ -1416,6 +1459,13 @@ function ModelMenu(props) {
|
|
|
1416
1459
|
]);
|
|
1417
1460
|
function onKeyDown(event) {
|
|
1418
1461
|
if (event.defaultPrevented || event.nativeEvent.isComposing) return;
|
|
1462
|
+
if (event.key === "Escape" && open) {
|
|
1463
|
+
event.preventDefault();
|
|
1464
|
+
if (pane !== initialPane) back(pane);
|
|
1465
|
+
else close(true);
|
|
1466
|
+
return;
|
|
1467
|
+
}
|
|
1468
|
+
if (disabled) return;
|
|
1419
1469
|
if (!open && [
|
|
1420
1470
|
"Enter",
|
|
1421
1471
|
" ",
|
|
@@ -1429,17 +1479,12 @@ function ModelMenu(props) {
|
|
|
1429
1479
|
requestOpen();
|
|
1430
1480
|
return;
|
|
1431
1481
|
}
|
|
1432
|
-
if (event.key === "Escape" && open) {
|
|
1433
|
-
event.preventDefault();
|
|
1434
|
-
if (pane !== "root") back(pane);
|
|
1435
|
-
else close(true);
|
|
1436
|
-
return;
|
|
1437
|
-
}
|
|
1438
1482
|
if (!open) return;
|
|
1439
1483
|
if (pane === "model" && showSearch && (event.key === "ArrowDown" || event.key === "ArrowUp")) {
|
|
1440
1484
|
event.preventDefault();
|
|
1441
1485
|
if (visibleModels.length > 0) {
|
|
1442
1486
|
setHighlightedIndex((activeModelIndex + (event.key === "ArrowDown" ? 1 : -1) + visibleModels.length) % visibleModels.length);
|
|
1487
|
+
setKeyNav((n) => n + 1);
|
|
1443
1488
|
searchRef.current?.focus();
|
|
1444
1489
|
}
|
|
1445
1490
|
return;
|
|
@@ -1448,19 +1493,19 @@ function ModelMenu(props) {
|
|
|
1448
1493
|
if (event.key === "Tab" && visibleModels.length === 0) return;
|
|
1449
1494
|
event.preventDefault();
|
|
1450
1495
|
const highlighted = visibleModels[activeModelIndex];
|
|
1451
|
-
if (highlighted) {
|
|
1496
|
+
if (highlighted && !busy) {
|
|
1452
1497
|
onPick({
|
|
1453
1498
|
provider: highlighted.provider,
|
|
1454
1499
|
model: highlighted.model
|
|
1455
1500
|
});
|
|
1456
|
-
close(true);
|
|
1501
|
+
if (closeOnPick) close(true);
|
|
1457
1502
|
}
|
|
1458
1503
|
return;
|
|
1459
1504
|
}
|
|
1460
1505
|
if (event.key === "Tab") {
|
|
1461
1506
|
if (event.shiftKey) {
|
|
1462
1507
|
event.preventDefault();
|
|
1463
|
-
if (pane !==
|
|
1508
|
+
if (pane !== initialPane) back(pane);
|
|
1464
1509
|
else close(true);
|
|
1465
1510
|
return;
|
|
1466
1511
|
}
|
|
@@ -1484,7 +1529,10 @@ function ModelMenu(props) {
|
|
|
1484
1529
|
}
|
|
1485
1530
|
}
|
|
1486
1531
|
function onBlur(event) {
|
|
1487
|
-
|
|
1532
|
+
const related = event.relatedTarget;
|
|
1533
|
+
if (related instanceof Node && (rootRef.current?.contains(related) || menuRef.current?.contains(related) || !related.isConnected)) return;
|
|
1534
|
+
if (!closeOnPick && !(related instanceof Node)) return;
|
|
1535
|
+
if (busy && related == null) return;
|
|
1488
1536
|
if (open) changeOpen(false);
|
|
1489
1537
|
}
|
|
1490
1538
|
itemRefs.current = [];
|
|
@@ -1505,11 +1553,16 @@ function ModelMenu(props) {
|
|
|
1505
1553
|
"aria-label": modelLabel,
|
|
1506
1554
|
lang: language,
|
|
1507
1555
|
children: [
|
|
1556
|
+
menuHeader ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1557
|
+
className: "dsh-model-menu-header",
|
|
1558
|
+
children: menuHeader
|
|
1559
|
+
}) : null,
|
|
1508
1560
|
pane === "root" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1509
1561
|
ref: itemRef(),
|
|
1510
1562
|
type: "button",
|
|
1511
1563
|
role: "menuitem",
|
|
1512
1564
|
className: "dsh-model-menu-cell",
|
|
1565
|
+
disabled,
|
|
1513
1566
|
onClick: () => drill("model"),
|
|
1514
1567
|
children: [
|
|
1515
1568
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -1522,11 +1575,12 @@ function ModelMenu(props) {
|
|
|
1522
1575
|
}),
|
|
1523
1576
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutlineRegular, { className: "dsh-model-menu-cellChevron" })
|
|
1524
1577
|
]
|
|
1525
|
-
}), efforts.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1578
|
+
}), efforts.length > 0 && !reasoningSlider ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1526
1579
|
ref: itemRef(),
|
|
1527
1580
|
type: "button",
|
|
1528
1581
|
role: "menuitem",
|
|
1529
1582
|
className: "dsh-model-menu-cell",
|
|
1583
|
+
disabled,
|
|
1530
1584
|
onClick: () => drill("effort"),
|
|
1531
1585
|
children: [
|
|
1532
1586
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -1545,6 +1599,7 @@ function ModelMenu(props) {
|
|
|
1545
1599
|
className: "dsh-model-menu-searchRow",
|
|
1546
1600
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
|
|
1547
1601
|
ref: searchRef,
|
|
1602
|
+
disabled,
|
|
1548
1603
|
className: `dsh-model-menu-search${query !== "" ? " dsh-model-menu-searchWithQuery" : ""}`,
|
|
1549
1604
|
type: "text",
|
|
1550
1605
|
role: "searchbox",
|
|
@@ -1561,6 +1616,7 @@ function ModelMenu(props) {
|
|
|
1561
1616
|
type: "button",
|
|
1562
1617
|
className: "dsh-model-menu-searchClear",
|
|
1563
1618
|
"aria-label": clearSearchLabel,
|
|
1619
|
+
disabled,
|
|
1564
1620
|
onClick: () => {
|
|
1565
1621
|
setQuery("");
|
|
1566
1622
|
setHighlightedIndex(null);
|
|
@@ -1583,10 +1639,12 @@ function ModelMenu(props) {
|
|
|
1583
1639
|
type: "button",
|
|
1584
1640
|
role: "menuitemradio",
|
|
1585
1641
|
"aria-checked": item.selected === true,
|
|
1642
|
+
disabled: disabled || busy,
|
|
1586
1643
|
className: `dsh-model-menu-option${item.selected === true ? " dsh-model-menu-selected" : ""}`,
|
|
1587
1644
|
onClick: () => {
|
|
1645
|
+
if (disabled || busy) return;
|
|
1588
1646
|
onPickLeading?.(item.id);
|
|
1589
|
-
close(true);
|
|
1647
|
+
if (closeOnPick) close(true);
|
|
1590
1648
|
},
|
|
1591
1649
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1592
1650
|
className: "dsh-model-menu-optionCopy",
|
|
@@ -1612,6 +1670,7 @@ function ModelMenu(props) {
|
|
|
1612
1670
|
"aria-checked": checked,
|
|
1613
1671
|
id: `${id}-model-${index}`,
|
|
1614
1672
|
tabIndex: showSearch ? -1 : 0,
|
|
1673
|
+
disabled: disabled || busy,
|
|
1615
1674
|
onFocus: () => setHighlightedIndex(index),
|
|
1616
1675
|
"data-highlighted": index === activeModelIndex ? "" : void 0,
|
|
1617
1676
|
className: `dsh-model-menu-option dsh-model-menu-modelOption${checked ? " dsh-model-menu-selected" : ""}${index === activeModelIndex ? " dsh-model-menu-optionActive" : ""}`,
|
|
@@ -1621,11 +1680,12 @@ function ModelMenu(props) {
|
|
|
1621
1680
|
},
|
|
1622
1681
|
title: choice.label,
|
|
1623
1682
|
onClick: () => {
|
|
1683
|
+
if (disabled || busy) return;
|
|
1624
1684
|
onPick({
|
|
1625
1685
|
provider: choice.provider,
|
|
1626
1686
|
model: choice.model
|
|
1627
1687
|
});
|
|
1628
|
-
close(true);
|
|
1688
|
+
if (closeOnPick) close(true);
|
|
1629
1689
|
},
|
|
1630
1690
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1631
1691
|
className: "dsh-model-menu-optionCopy",
|
|
@@ -1641,6 +1701,14 @@ function ModelMenu(props) {
|
|
|
1641
1701
|
})
|
|
1642
1702
|
}, group.provider))]
|
|
1643
1703
|
}),
|
|
1704
|
+
reasoningSlider && efforts.length > 0 && filteredGroups.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EffortSlider, {
|
|
1705
|
+
label: effortLabel,
|
|
1706
|
+
rows: effortRows,
|
|
1707
|
+
selected: selectedEffort ?? "",
|
|
1708
|
+
disabled,
|
|
1709
|
+
busy,
|
|
1710
|
+
onPick: (value) => onPickEffort?.(value)
|
|
1711
|
+
}) : null,
|
|
1644
1712
|
filteredGroups.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1645
1713
|
className: "dsh-model-menu-empty",
|
|
1646
1714
|
role: "status",
|
|
@@ -1654,10 +1722,12 @@ function ModelMenu(props) {
|
|
|
1654
1722
|
type: "button",
|
|
1655
1723
|
role: "menuitemradio",
|
|
1656
1724
|
"aria-checked": checked,
|
|
1725
|
+
disabled: disabled || busy,
|
|
1657
1726
|
className: `dsh-model-menu-option${checked ? " dsh-model-menu-selected" : ""}`,
|
|
1658
1727
|
onClick: () => {
|
|
1728
|
+
if (disabled || busy) return;
|
|
1659
1729
|
onPickEffort?.(row.id || void 0);
|
|
1660
|
-
close(true);
|
|
1730
|
+
if (closeOnPick) close(true);
|
|
1661
1731
|
},
|
|
1662
1732
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1663
1733
|
className: "dsh-model-menu-optionCopy",
|
|
@@ -1676,7 +1746,8 @@ function ModelMenu(props) {
|
|
|
1676
1746
|
const trigger = (0, react.isValidElement)(anchor) ? (0, react.cloneElement)(anchor, {
|
|
1677
1747
|
"aria-haspopup": "menu",
|
|
1678
1748
|
"aria-expanded": open,
|
|
1679
|
-
"aria-controls": open ? `${id}-menu` : void 0
|
|
1749
|
+
"aria-controls": open ? `${id}-menu` : void 0,
|
|
1750
|
+
"aria-disabled": disabled || void 0
|
|
1680
1751
|
}) : anchor;
|
|
1681
1752
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1682
1753
|
ref: rootRef,
|
|
@@ -1684,7 +1755,7 @@ function ModelMenu(props) {
|
|
|
1684
1755
|
onKeyDown,
|
|
1685
1756
|
onBlur,
|
|
1686
1757
|
onClick: (event) => {
|
|
1687
|
-
if (event.defaultPrevented) return;
|
|
1758
|
+
if (disabled || event.defaultPrevented) return;
|
|
1688
1759
|
const button = event.target instanceof Element ? event.target.closest("button") : null;
|
|
1689
1760
|
if (!(button instanceof HTMLButtonElement) || button.disabled || !rootRef.current?.contains(button)) return;
|
|
1690
1761
|
requestOpen();
|
|
@@ -1696,6 +1767,102 @@ function ModelMenu(props) {
|
|
|
1696
1767
|
});
|
|
1697
1768
|
}
|
|
1698
1769
|
/**
|
|
1770
|
+
* Discrete reasoning-effort slider shown under the model list in enhanced mode.
|
|
1771
|
+
*
|
|
1772
|
+
* A native range input (`step=1`) over exactly the rows the caller supplies —
|
|
1773
|
+
* the default row plus each effort the selected model advertises — so it can
|
|
1774
|
+
* never invent a level. It previews locally while dragging or arrowing and
|
|
1775
|
+
* commits only when the gesture ends (pointer up, key up, or blur), so a drag
|
|
1776
|
+
* does not write on every step. The menu stays open, so adjusting the effort
|
|
1777
|
+
* never loses the model selection or the search query.
|
|
1778
|
+
*
|
|
1779
|
+
* The input itself stays the entire interaction and accessibility surface —
|
|
1780
|
+
* keyboard, ARIA, disabled and the native step snapping all come from it —
|
|
1781
|
+
* while the track, gradient fill and thumb below are decorative layers painted
|
|
1782
|
+
* from the `--_p` percentage the preview position sets on the rail. Value
|
|
1783
|
+
* changes only update that custom property, so the CSS transitions animate
|
|
1784
|
+
* between levels without remounting nodes or restarting an animation.
|
|
1785
|
+
*/
|
|
1786
|
+
function EffortSlider(props) {
|
|
1787
|
+
const { label, rows, selected, disabled = false, busy = false, onPick } = props;
|
|
1788
|
+
const active = Math.max(0, rows.findIndex((row) => row.id === selected));
|
|
1789
|
+
const [position, setPosition] = (0, react.useState)(active);
|
|
1790
|
+
const positionRef = (0, react.useRef)(position);
|
|
1791
|
+
(0, react.useEffect)(() => {
|
|
1792
|
+
positionRef.current = active;
|
|
1793
|
+
setPosition(active);
|
|
1794
|
+
}, [active]);
|
|
1795
|
+
const preview = (next) => {
|
|
1796
|
+
positionRef.current = next;
|
|
1797
|
+
setPosition(next);
|
|
1798
|
+
};
|
|
1799
|
+
const commit = () => {
|
|
1800
|
+
if (disabled || busy) return;
|
|
1801
|
+
const row = rows[positionRef.current];
|
|
1802
|
+
if (row !== void 0) onPick(row.id === "" ? void 0 : row.id);
|
|
1803
|
+
};
|
|
1804
|
+
const max = Math.max(0, rows.length - 1);
|
|
1805
|
+
const percent = max > 0 ? Math.round(position / max * 100) : 0;
|
|
1806
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1807
|
+
className: "dsh-model-menu-sliderGroup",
|
|
1808
|
+
role: "group",
|
|
1809
|
+
"aria-label": label,
|
|
1810
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1811
|
+
className: "dsh-model-menu-sliderHead",
|
|
1812
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1813
|
+
className: "dsh-model-menu-sliderTitle",
|
|
1814
|
+
children: label
|
|
1815
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1816
|
+
className: "dsh-model-menu-sliderValue",
|
|
1817
|
+
children: rows[position]?.name
|
|
1818
|
+
})]
|
|
1819
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1820
|
+
className: "dsh-model-menu-sliderRail",
|
|
1821
|
+
style: { "--_p": percent },
|
|
1822
|
+
children: [
|
|
1823
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1824
|
+
type: "range",
|
|
1825
|
+
className: "dsh-model-menu-sliderInput",
|
|
1826
|
+
min: 0,
|
|
1827
|
+
max,
|
|
1828
|
+
step: 1,
|
|
1829
|
+
value: position,
|
|
1830
|
+
disabled,
|
|
1831
|
+
"aria-label": label,
|
|
1832
|
+
"aria-disabled": disabled || busy || void 0,
|
|
1833
|
+
"aria-valuetext": rows[position]?.name,
|
|
1834
|
+
onChange: (event) => {
|
|
1835
|
+
const value = Number(event.target.value);
|
|
1836
|
+
if (Number.isFinite(value)) preview(value);
|
|
1837
|
+
},
|
|
1838
|
+
onPointerUp: commit,
|
|
1839
|
+
onKeyUp: commit,
|
|
1840
|
+
onBlur: commit
|
|
1841
|
+
}),
|
|
1842
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1843
|
+
className: "dsh-model-menu-sliderTrack",
|
|
1844
|
+
"aria-hidden": "true",
|
|
1845
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1846
|
+
className: "dsh-model-menu-sliderFill",
|
|
1847
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dsh-model-menu-sliderFillBar" })
|
|
1848
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1849
|
+
className: "dsh-model-menu-sliderNodes",
|
|
1850
|
+
children: rows.map((row, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1851
|
+
"data-covered": index <= position ? "" : void 0,
|
|
1852
|
+
className: "dsh-model-menu-sliderNode",
|
|
1853
|
+
style: { "--_n": max > 0 ? Math.round(index / max * 100) : 0 }
|
|
1854
|
+
}, row.id || "__default"))
|
|
1855
|
+
})]
|
|
1856
|
+
}),
|
|
1857
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1858
|
+
className: "dsh-model-menu-sliderThumb",
|
|
1859
|
+
"aria-hidden": "true"
|
|
1860
|
+
})
|
|
1861
|
+
]
|
|
1862
|
+
})]
|
|
1863
|
+
});
|
|
1864
|
+
}
|
|
1865
|
+
/**
|
|
1699
1866
|
* ModelMenu styles, transcribed from the official ModelSelect.module.css (the
|
|
1700
1867
|
* published dsh-client-ui-model-selection bundle): card capped at
|
|
1701
1868
|
* min(360px, 100vh - 96px), 34px option cells at 13/18 with 5px 7px padding,
|
|
@@ -1754,6 +1921,7 @@ const modelMenuCss = `
|
|
|
1754
1921
|
overflow: hidden;
|
|
1755
1922
|
}
|
|
1756
1923
|
.dsh-model-menu-status, .dsh-model-menu-empty { color: var(--dsw-alias-label-tertiary); padding: 8px; font-size: 12px; line-height: 18px; }
|
|
1924
|
+
.dsh-model-menu-header { flex: none; padding: 2px 2px 4px; }
|
|
1757
1925
|
.dsh-model-menu-searchRow { flex-shrink: 0; margin: 2px 0 3px; position: relative; }
|
|
1758
1926
|
.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
1927
|
.dsh-model-menu-searchRow .dsh-model-menu-search:focus-within { border-color: transparent; }
|
|
@@ -1832,8 +2000,236 @@ const modelMenuCss = `
|
|
|
1832
2000
|
.dsh-model-menu-cellLabel { white-space: nowrap; flex: none; }
|
|
1833
2001
|
.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
2002
|
.dsh-model-menu-cellChevron { width: 12px; height: 12px; color: var(--dsw-alias-menu-icon); flex: none; }
|
|
2003
|
+
/* Discrete reasoning-effort slider under the model list (enhanced mode).
|
|
2004
|
+
* The painted control is layered over a fully functional native range input:
|
|
2005
|
+
* the input stays on top, transparent, and owns pointer/keyboard/ARIA, while
|
|
2006
|
+
* the decorative track, fill and thumb render beneath it at pointer-events:
|
|
2007
|
+
* none. Position lives in one custom property (--_p, 0–100) on the rail, so
|
|
2008
|
+
* the only animated work a level change triggers is a clip-path/left
|
|
2009
|
+
* transition — no keyframes, no element churn. */
|
|
2010
|
+
.dsh-model-menu-sliderGroup { border-top: 0.5px solid var(--dsw-alias-border-l2); flex: none; margin-top: 4px; padding: 6px 8px 8px; }
|
|
2011
|
+
.dsh-model-menu-sliderHead { align-items: baseline; justify-content: space-between; gap: 8px; margin: 0 2px 4px; display: flex; }
|
|
2012
|
+
.dsh-model-menu-sliderTitle { color: var(--dsw-alias-label-tertiary); font-size: 12px; line-height: 18px; }
|
|
2013
|
+
.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); }
|
|
2014
|
+
.dsh-model-menu-sliderRail { position: relative; direction: ltr; height: 38px; margin-top: 2px; }
|
|
2015
|
+
.dsh-model-menu-sliderRail:has(:disabled) { opacity: 0.45; }
|
|
2016
|
+
.dsh-model-menu-sliderInput {
|
|
2017
|
+
appearance: none;
|
|
2018
|
+
-webkit-appearance: none;
|
|
2019
|
+
box-sizing: border-box;
|
|
2020
|
+
border: 0;
|
|
2021
|
+
cursor: pointer;
|
|
2022
|
+
direction: ltr;
|
|
2023
|
+
opacity: 0;
|
|
2024
|
+
margin: 0;
|
|
2025
|
+
padding: 0;
|
|
2026
|
+
width: 100%;
|
|
2027
|
+
height: 100%;
|
|
2028
|
+
position: absolute;
|
|
2029
|
+
inset: 0;
|
|
2030
|
+
z-index: 1;
|
|
2031
|
+
touch-action: pan-y;
|
|
2032
|
+
}
|
|
2033
|
+
.dsh-model-menu-sliderInput:disabled { cursor: default; }
|
|
2034
|
+
/* The transparent input must keep the painted thumb's geometry: an
|
|
2035
|
+
* appearance:none webkit thumb defaults to 16px, which would shift the
|
|
2036
|
+
* end-point positions 7px off the decorative 30px disc and misplace the
|
|
2037
|
+
* native drag hit target. Track gets the full rail height so the whole band
|
|
2038
|
+
* maps to values identically. */
|
|
2039
|
+
.dsh-model-menu-sliderInput::-webkit-slider-runnable-track { width: 100%; height: 100%; }
|
|
2040
|
+
.dsh-model-menu-sliderInput::-webkit-slider-thumb { appearance: none; -webkit-appearance: none; width: 30px; height: 30px; margin: 0; }
|
|
2041
|
+
.dsh-model-menu-sliderInput::-moz-range-track { width: 100%; height: 100%; }
|
|
2042
|
+
.dsh-model-menu-sliderInput::-moz-range-thumb { width: 30px; height: 30px; margin: 0; border: 0; }
|
|
2043
|
+
.dsh-model-menu-sliderTrack {
|
|
2044
|
+
pointer-events: none;
|
|
2045
|
+
border-radius: 999px;
|
|
2046
|
+
width: 100%;
|
|
2047
|
+
height: 26px;
|
|
2048
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
2049
|
+
box-shadow: inset 0 0 0 0.5px var(--dsw-alias-border-l2);
|
|
2050
|
+
position: absolute;
|
|
2051
|
+
inset-inline: 0;
|
|
2052
|
+
top: 50%;
|
|
2053
|
+
transform: translateY(-50%);
|
|
2054
|
+
/* No overflow:hidden — it would clip the fill's drop-shadow glow; the bar's
|
|
2055
|
+
* own clip-path already supplies the rounded reveal shape. */
|
|
2056
|
+
}
|
|
2057
|
+
/* The accent ramp paints the full track width once and is revealed by an
|
|
2058
|
+
* animated clip-path, so raising the effort unwraps more of the spectrum
|
|
2059
|
+
* instead of squashing the same gradient into a narrower bar. The glow lives
|
|
2060
|
+
* on the un-clipped parent: a filter on the clipped element itself would be
|
|
2061
|
+
* clipped away with it. */
|
|
2062
|
+
.dsh-model-menu-sliderFill {
|
|
2063
|
+
display: block;
|
|
2064
|
+
height: 100%;
|
|
2065
|
+
filter: drop-shadow(0 0 5px var(--dsw-alias-state-business-primary));
|
|
2066
|
+
transition: filter var(--ds-transition-duration) var(--ds-ease-in-out);
|
|
2067
|
+
}
|
|
2068
|
+
.dsh-model-menu-sliderInput:hover:not(:disabled) ~ .dsh-model-menu-sliderTrack .dsh-model-menu-sliderFill,
|
|
2069
|
+
.dsh-model-menu-sliderInput:active:not(:disabled) ~ .dsh-model-menu-sliderTrack .dsh-model-menu-sliderFill {
|
|
2070
|
+
filter: drop-shadow(0 0 9px var(--dsw-alias-state-business-primary));
|
|
2071
|
+
}
|
|
2072
|
+
/* The fill stops at the thumb's center, not at the track edge — at a 30px
|
|
2073
|
+
* thumb the end stops sit 15px in from each side, matching the node dots. */
|
|
2074
|
+
.dsh-model-menu-sliderFillBar {
|
|
2075
|
+
display: block;
|
|
2076
|
+
height: 100%;
|
|
2077
|
+
background: linear-gradient(90deg,
|
|
2078
|
+
var(--dsw-alias-state-business-primary),
|
|
2079
|
+
var(--dsw-alias-state-success-primary),
|
|
2080
|
+
var(--dsw-alias-state-warn-primary),
|
|
2081
|
+
var(--dsw-alias-state-error-primary));
|
|
2082
|
+
clip-path: inset(0 calc(100% - 15px - (100% - 30px) * var(--_p, 0) / 100) 0 0 round 999px);
|
|
2083
|
+
transition: clip-path var(--ds-transition-duration) var(--ds-ease-in-out);
|
|
2084
|
+
}
|
|
2085
|
+
/* Stop dots: one per declared row (default + every advertised effort), evenly
|
|
2086
|
+
* spread across the band between the two thumb-center extremes (inset 15px,
|
|
2087
|
+
* i.e. half the 30px thumb). --_n is 0-100 along that band. A covered dot
|
|
2088
|
+
* reads as a pale pin through the gradient; an uncovered one stays a neutral
|
|
2089
|
+
* notch on the empty track. */
|
|
2090
|
+
.dsh-model-menu-sliderNodes {
|
|
2091
|
+
pointer-events: none;
|
|
2092
|
+
display: block;
|
|
2093
|
+
position: absolute;
|
|
2094
|
+
inset-inline: 15px;
|
|
2095
|
+
inset-block: 0;
|
|
2096
|
+
}
|
|
2097
|
+
.dsh-model-menu-sliderNode {
|
|
2098
|
+
pointer-events: none;
|
|
2099
|
+
box-sizing: border-box;
|
|
2100
|
+
width: 4px;
|
|
2101
|
+
height: 4px;
|
|
2102
|
+
border-radius: 50%;
|
|
2103
|
+
background: color-mix(in srgb, var(--dsw-alias-label-secondary) 35%, transparent);
|
|
2104
|
+
position: absolute;
|
|
2105
|
+
left: calc(var(--_n, 0) * 1%);
|
|
2106
|
+
top: 50%;
|
|
2107
|
+
transform: translate(-50%, -50%);
|
|
2108
|
+
transition: background-color var(--ds-transition-duration) var(--ds-ease-in-out),
|
|
2109
|
+
transform var(--ds-transition-duration) var(--ds-ease-in-out);
|
|
2110
|
+
}
|
|
2111
|
+
.dsh-model-menu-sliderNode[data-covered] { background: color-mix(in srgb, var(--dsw-alias-label-primary) 60%, transparent); }
|
|
2112
|
+
.dsh-model-menu-sliderThumb {
|
|
2113
|
+
pointer-events: none;
|
|
2114
|
+
box-sizing: border-box;
|
|
2115
|
+
border-radius: 50%;
|
|
2116
|
+
width: 30px;
|
|
2117
|
+
height: 30px;
|
|
2118
|
+
background: var(--dsw-alias-button-primary-fill);
|
|
2119
|
+
border: 0.5px solid var(--dsw-alias-border-l4);
|
|
2120
|
+
box-shadow: 0 0 0 3px var(--dsw-alias-interactive-bg-hover), var(--dsw-elevation-soft);
|
|
2121
|
+
position: absolute;
|
|
2122
|
+
left: calc((100% - 30px) * var(--_p, 0) / 100);
|
|
2123
|
+
top: 50%;
|
|
2124
|
+
transform: translateY(-50%) scale(0.92);
|
|
2125
|
+
transition: left var(--ds-transition-duration) var(--ds-ease-in-out),
|
|
2126
|
+
transform var(--ds-transition-duration) var(--ds-ease-in-out),
|
|
2127
|
+
box-shadow var(--ds-transition-duration) var(--ds-ease-in-out);
|
|
2128
|
+
}
|
|
2129
|
+
.dsh-model-menu-sliderInput:hover:not(:disabled) ~ .dsh-model-menu-sliderThumb { transform: translateY(-50%) scale(1); }
|
|
2130
|
+
.dsh-model-menu-sliderInput:active:not(:disabled) ~ .dsh-model-menu-sliderThumb {
|
|
2131
|
+
transform: translateY(-50%) scale(1.1);
|
|
2132
|
+
box-shadow: 0 0 0 4px var(--dsw-alias-interactive-bg-hover), 0 0 12px var(--dsw-alias-state-business-primary);
|
|
2133
|
+
}
|
|
2134
|
+
.dsh-model-menu-sliderInput:focus-visible ~ .dsh-model-menu-sliderThumb {
|
|
2135
|
+
box-shadow: 0 0 0 3px var(--dsw-alias-interactive-bg-hover),
|
|
2136
|
+
0 0 0 calc(3px + var(--dsw-focus-ring-width)) var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
|
|
2137
|
+
}
|
|
2138
|
+
@media (prefers-reduced-motion: reduce) {
|
|
2139
|
+
.dsh-model-menu-sliderFill, .dsh-model-menu-sliderFillBar, .dsh-model-menu-sliderThumb,
|
|
2140
|
+
.dsh-model-menu-sliderNode, .dsh-model-menu-sliderValue { transition: none; }
|
|
2141
|
+
}
|
|
1835
2142
|
`;
|
|
1836
2143
|
//#endregion
|
|
2144
|
+
//#region ../dsh-plugin-kit/src/model-picker.tsx
|
|
2145
|
+
/**
|
|
2146
|
+
* Optional shared model picker (`klarkxy.model-picker` Factory).
|
|
2147
|
+
*
|
|
2148
|
+
* `@klarkxy/dsh-model-picker`, when installed, registers one root-scoped
|
|
2149
|
+
* Component Factory under `MODEL_PICKER_FACTORY`: an enhanced model menu with
|
|
2150
|
+
* a searchable provider-grouped list and a reasoning-effort slider, built on
|
|
2151
|
+
* the same ModelMenu recipe. Consumers keep their own ModelMenu as the
|
|
2152
|
+
* `fallback`; when no definition is registered the renderer resolves that
|
|
2153
|
+
* fallback verbatim, so the shared picker is strictly opt-in and every saved
|
|
2154
|
+
* value keeps its existing write path.
|
|
2155
|
+
*
|
|
2156
|
+
* `renderFactorySlot` is a framework-composed prop, never a context: the
|
|
2157
|
+
* component a plugin registers into any slot receives it automatically, but
|
|
2158
|
+
* the plugin must pass it down to whichever child renders the menu (or to the
|
|
2159
|
+
* render callback handed to registerSessionSettingsItem).
|
|
2160
|
+
*
|
|
2161
|
+
* The Factory manages its own open state; a consumer's `open`/`onOpenChange`
|
|
2162
|
+
* pair belongs to the fallback menu only and must not be fed to both.
|
|
2163
|
+
*/
|
|
2164
|
+
/**
|
|
2165
|
+
* The trigger every consumer used to hand its own ModelMenu, rebuilt as the
|
|
2166
|
+
* factory's `anchor` local component: identical classes and chevron, the menu
|
|
2167
|
+
* state coming from the occurrence's props instead of local open state.
|
|
2168
|
+
*
|
|
2169
|
+
* `ariaLabel` and `title` are the whole accessible name and tooltip (the
|
|
2170
|
+
* caller formats "field: current route" itself because the factory's resolved
|
|
2171
|
+
* caption may intentionally be a short name). The component identity stays
|
|
2172
|
+
* put across label changes — a new function would unmount the trigger, and
|
|
2173
|
+
* its cleanup would close a menu that `closeOnPick={false}` is keeping open.
|
|
2174
|
+
* The latest label, title, and `onClose` are read from refs.
|
|
2175
|
+
*/
|
|
2176
|
+
function useModelMenuAnchor(ariaLabel, title, onClose) {
|
|
2177
|
+
const closeRef = (0, react.useRef)(onClose);
|
|
2178
|
+
const labelRef = (0, react.useRef)(ariaLabel);
|
|
2179
|
+
const titleRef = (0, react.useRef)(title);
|
|
2180
|
+
closeRef.current = onClose;
|
|
2181
|
+
labelRef.current = ariaLabel;
|
|
2182
|
+
titleRef.current = title;
|
|
2183
|
+
return (0, react.useMemo)(() => {
|
|
2184
|
+
const ModelPickerTrigger = (props) => {
|
|
2185
|
+
(0, react.useEffect)(() => () => closeRef.current?.(), []);
|
|
2186
|
+
const record = props;
|
|
2187
|
+
const injected = {};
|
|
2188
|
+
for (const [key, value] of Object.entries(record)) if (key.startsWith("aria-")) injected[key] = value;
|
|
2189
|
+
const extraClass = typeof record.className === "string" ? ` ${record.className}` : "";
|
|
2190
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2191
|
+
type: "button",
|
|
2192
|
+
className: `dsh-model-menu-trigger${extraClass}`,
|
|
2193
|
+
disabled: props.disabled,
|
|
2194
|
+
"aria-haspopup": "menu",
|
|
2195
|
+
"aria-expanded": props.open,
|
|
2196
|
+
...injected,
|
|
2197
|
+
"aria-label": labelRef.current,
|
|
2198
|
+
title: titleRef.current,
|
|
2199
|
+
children: [
|
|
2200
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2201
|
+
className: "dsh-model-menu-triggerLabel",
|
|
2202
|
+
children: props.label
|
|
2203
|
+
}),
|
|
2204
|
+
props.effortLabel ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2205
|
+
className: "dsh-model-menu-triggerEffort",
|
|
2206
|
+
children: props.effortLabel
|
|
2207
|
+
}) : null,
|
|
2208
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${props.open ? " dsh-model-menu-chevronOpen" : ""}` })
|
|
2209
|
+
]
|
|
2210
|
+
});
|
|
2211
|
+
};
|
|
2212
|
+
return ModelPickerTrigger;
|
|
2213
|
+
}, []);
|
|
2214
|
+
}
|
|
2215
|
+
/**
|
|
2216
|
+
* Render the shared model picker while it is registered; `options.fallback`
|
|
2217
|
+
* whenever it is not (plugin absent, disabled, or unloaded).
|
|
2218
|
+
*
|
|
2219
|
+
* `options.anchor` is optional: the factory ships a default trigger, so only
|
|
2220
|
+
* consumers that need their own chrome (custom aria-label, tooltip, class)
|
|
2221
|
+
* pass one built with {@link useModelMenuAnchor}. The returned node is never
|
|
2222
|
+
* assumed to be the factory's own output — a defensive `?? fallback` covers a
|
|
2223
|
+
* renderer that yields null for an unregistered definition.
|
|
2224
|
+
*/
|
|
2225
|
+
function renderModelPicker(renderFactorySlot, props, options) {
|
|
2226
|
+
if (typeof renderFactorySlot !== "function") return options.fallback;
|
|
2227
|
+
return renderFactorySlot("klarkxy.model-picker", props, {
|
|
2228
|
+
...options.anchor ? { slots: { ["anchor"]: options.anchor } } : {},
|
|
2229
|
+
fallback: options.fallback
|
|
2230
|
+
}) ?? options.fallback;
|
|
2231
|
+
}
|
|
2232
|
+
//#endregion
|
|
1837
2233
|
//#region ../dsh-plugin-kit/src/auxiliary-model-client.tsx
|
|
1838
2234
|
const AUXILIARY_MODEL_PACKAGE = "@klarkxy/dsh-plugin-kit";
|
|
1839
2235
|
const AUXILIARY_MODEL_SEAT_ID = "auxiliary-model";
|
|
@@ -1911,7 +2307,7 @@ async function callAuxiliaryRpc(client, endpoint, payload = {}) {
|
|
|
1911
2307
|
* The shared card. All state comes from the shared host service over the
|
|
1912
2308
|
* plugin-kit RPC channel; the card only renders the stored row.
|
|
1913
2309
|
*/
|
|
1914
|
-
function AuxiliaryModelCard({ client, subscribe }) {
|
|
2310
|
+
function AuxiliaryModelCard({ client, subscribe, renderFactorySlot }) {
|
|
1915
2311
|
const language = useAuxiliaryLocale(client);
|
|
1916
2312
|
const text = auxiliaryCopy[language];
|
|
1917
2313
|
const [settings, setSettings] = (0, react.useState)();
|
|
@@ -2032,6 +2428,17 @@ function AuxiliaryModelCard({ client, subscribe }) {
|
|
|
2032
2428
|
refreshCatalog
|
|
2033
2429
|
]);
|
|
2034
2430
|
const locked = busy;
|
|
2431
|
+
const current = settings?.model ?? {
|
|
2432
|
+
provider: "",
|
|
2433
|
+
model: ""
|
|
2434
|
+
};
|
|
2435
|
+
const follow = !current.provider || !current.model;
|
|
2436
|
+
const selected = follow ? void 0 : choices.find((item) => item.provider === current.provider && item.model === current.model);
|
|
2437
|
+
const efforts = follow ? [] : modelMenuEffortOptions(selected, current.reasoningEffort);
|
|
2438
|
+
const currentLabel = follow ? text.modelDefault : selected?.label ?? `${current.provider} / ${current.model}`;
|
|
2439
|
+
const selectedEffort = efforts.find((item) => item.id === (current.reasoningEffort || ""));
|
|
2440
|
+
const effortName = selectedEffort ? modelMenuEffortLabel(selectedEffort, language) : void 0;
|
|
2441
|
+
const anchor = useModelMenuAnchor(`${text.model}: ${settings ? currentLabel : ""}`, void 0, () => setMenuOpen(false));
|
|
2035
2442
|
if (!settings) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2036
2443
|
className: "ams-root dsh-ui-card",
|
|
2037
2444
|
"data-testid": "auxiliary-model-settings",
|
|
@@ -2070,16 +2477,6 @@ function AuxiliaryModelCard({ client, subscribe }) {
|
|
|
2070
2477
|
})
|
|
2071
2478
|
]
|
|
2072
2479
|
});
|
|
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
2480
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2084
2481
|
className: "ams-root dsh-ui-card",
|
|
2085
2482
|
"data-testid": "auxiliary-model-settings",
|
|
@@ -2103,28 +2500,12 @@ function AuxiliaryModelCard({ client, subscribe }) {
|
|
|
2103
2500
|
state: catalogState,
|
|
2104
2501
|
locale: language
|
|
2105
2502
|
}),
|
|
2106
|
-
|
|
2503
|
+
renderModelPicker(renderFactorySlot, {
|
|
2107
2504
|
locale: language,
|
|
2108
|
-
|
|
2505
|
+
disabled: locked,
|
|
2506
|
+
busy: locked,
|
|
2109
2507
|
onOpenChange: (next) => setMenuOpen(next && !locked),
|
|
2110
2508
|
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
2509
|
choices,
|
|
2129
2510
|
selected: follow ? void 0 : {
|
|
2130
2511
|
provider: current.provider,
|
|
@@ -2147,9 +2528,61 @@ function AuxiliaryModelCard({ client, subscribe }) {
|
|
|
2147
2528
|
model: current.model,
|
|
2148
2529
|
...effort ? { reasoningEffort: effort } : {}
|
|
2149
2530
|
}),
|
|
2531
|
+
triggerLabel: currentLabel,
|
|
2532
|
+
triggerEffortLabel: !follow && efforts.length ? effortName ?? text.effortDefault : void 0,
|
|
2150
2533
|
modelLabel: text.model,
|
|
2151
2534
|
modelValue: currentLabel,
|
|
2152
2535
|
defaultEffortLabel: text.effortDefault
|
|
2536
|
+
}, {
|
|
2537
|
+
anchor,
|
|
2538
|
+
fallback: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
|
|
2539
|
+
locale: language,
|
|
2540
|
+
open: menuOpen && !locked,
|
|
2541
|
+
onOpenChange: (next) => setMenuOpen(next && !locked),
|
|
2542
|
+
listClassName: "ams-root ams-menu-portal",
|
|
2543
|
+
anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2544
|
+
type: "button",
|
|
2545
|
+
className: "dsh-model-menu-trigger",
|
|
2546
|
+
disabled: locked,
|
|
2547
|
+
"aria-label": `${text.model}: ${currentLabel}`,
|
|
2548
|
+
children: [
|
|
2549
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2550
|
+
className: "dsh-model-menu-triggerLabel",
|
|
2551
|
+
children: currentLabel
|
|
2552
|
+
}),
|
|
2553
|
+
!follow && efforts.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2554
|
+
className: "dsh-model-menu-triggerEffort",
|
|
2555
|
+
children: effortName ?? text.effortDefault
|
|
2556
|
+
}) : null,
|
|
2557
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${menuOpen && !locked ? " dsh-model-menu-chevronOpen" : ""}` })
|
|
2558
|
+
]
|
|
2559
|
+
}),
|
|
2560
|
+
choices,
|
|
2561
|
+
selected: follow ? void 0 : {
|
|
2562
|
+
provider: current.provider,
|
|
2563
|
+
model: current.model
|
|
2564
|
+
},
|
|
2565
|
+
onPick: (picked) => save({
|
|
2566
|
+
provider: picked.provider,
|
|
2567
|
+
model: picked.model
|
|
2568
|
+
}),
|
|
2569
|
+
leading: [{
|
|
2570
|
+
id: "default",
|
|
2571
|
+
label: text.modelDefault,
|
|
2572
|
+
selected: follow
|
|
2573
|
+
}],
|
|
2574
|
+
onPickLeading: () => save(null),
|
|
2575
|
+
efforts,
|
|
2576
|
+
selectedEffort: current.reasoningEffort || "",
|
|
2577
|
+
onPickEffort: (effort) => save(follow ? null : {
|
|
2578
|
+
provider: current.provider,
|
|
2579
|
+
model: current.model,
|
|
2580
|
+
...effort ? { reasoningEffort: effort } : {}
|
|
2581
|
+
}),
|
|
2582
|
+
modelLabel: text.model,
|
|
2583
|
+
modelValue: currentLabel,
|
|
2584
|
+
defaultEffortLabel: text.effortDefault
|
|
2585
|
+
})
|
|
2153
2586
|
}),
|
|
2154
2587
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2155
2588
|
className: "dsh-ui-hint",
|
|
@@ -2251,9 +2684,10 @@ function acquireAuxiliaryModelCard(root, ctx) {
|
|
|
2251
2684
|
let unprovide;
|
|
2252
2685
|
const watchLease = acquireWatchCoordinator(root).mount();
|
|
2253
2686
|
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, {
|
|
2687
|
+
const render = (props) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AuxiliaryModelCard, {
|
|
2255
2688
|
client: root,
|
|
2256
|
-
subscribe: subscriber.subscribe
|
|
2689
|
+
subscribe: subscriber.subscribe,
|
|
2690
|
+
renderFactorySlot: props.renderFactorySlot
|
|
2257
2691
|
});
|
|
2258
2692
|
const service = {
|
|
2259
2693
|
retain() {
|
|
@@ -2715,6 +3149,7 @@ function TitleSettings(props) {
|
|
|
2715
3149
|
const currentLabel = follow ? text.modelDefault : selectedModel?.label ?? `${savedModel.provider} / ${savedModel.model}`;
|
|
2716
3150
|
const selectedEffort = efforts.find((item) => item.id === (savedModel.reasoningEffort || ""));
|
|
2717
3151
|
const effortName = selectedEffort ? modelMenuEffortLabel(selectedEffort, seat.locale) : void 0;
|
|
3152
|
+
const anchor = useModelMenuAnchor(`${text.model}: ${currentLabel}`, void 0, () => setMenuOpen(false));
|
|
2718
3153
|
const title = status?.session?.title;
|
|
2719
3154
|
const pinned = status?.session?.pinned === true;
|
|
2720
3155
|
const weOwn = status?.support?.weOwn === true;
|
|
@@ -2926,28 +3361,12 @@ function TitleSettings(props) {
|
|
|
2926
3361
|
state: catalogState,
|
|
2927
3362
|
locale: seat.locale
|
|
2928
3363
|
}),
|
|
2929
|
-
|
|
3364
|
+
renderModelPicker(props.renderFactorySlot, {
|
|
2930
3365
|
locale: seat.locale,
|
|
2931
|
-
|
|
3366
|
+
disabled: busy || loadingSettings || !status,
|
|
3367
|
+
busy: busy || loadingSettings,
|
|
2932
3368
|
onOpenChange: (next) => setMenuOpen(next && Boolean(status) && !busy && !loadingSettings),
|
|
2933
3369
|
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
3370
|
choices,
|
|
2952
3371
|
selected: follow ? void 0 : {
|
|
2953
3372
|
provider: savedModel.provider,
|
|
@@ -2976,9 +3395,67 @@ function TitleSettings(props) {
|
|
|
2976
3395
|
model: savedModel.model,
|
|
2977
3396
|
...effort ? { reasoningEffort: effort } : {}
|
|
2978
3397
|
} }),
|
|
3398
|
+
triggerLabel: currentLabel,
|
|
3399
|
+
triggerEffortLabel: !follow && efforts.length ? effortName ?? text.modelDefault : void 0,
|
|
2979
3400
|
modelLabel: text.model,
|
|
2980
3401
|
modelValue: currentLabel,
|
|
2981
3402
|
defaultEffortLabel: text.modelDefault
|
|
3403
|
+
}, {
|
|
3404
|
+
anchor,
|
|
3405
|
+
fallback: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
|
|
3406
|
+
locale: seat.locale,
|
|
3407
|
+
open: menuOpen && Boolean(status) && !busy && !loadingSettings,
|
|
3408
|
+
onOpenChange: (next) => setMenuOpen(next && Boolean(status) && !busy && !loadingSettings),
|
|
3409
|
+
listClassName: "ct-root",
|
|
3410
|
+
anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
3411
|
+
type: "button",
|
|
3412
|
+
className: "dsh-model-menu-trigger",
|
|
3413
|
+
disabled: busy || loadingSettings || !status,
|
|
3414
|
+
"aria-label": `${text.model}: ${currentLabel}`,
|
|
3415
|
+
children: [
|
|
3416
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3417
|
+
className: "dsh-model-menu-triggerLabel",
|
|
3418
|
+
children: currentLabel
|
|
3419
|
+
}),
|
|
3420
|
+
!follow && efforts.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3421
|
+
className: "dsh-model-menu-triggerEffort",
|
|
3422
|
+
children: effortName ?? text.modelDefault
|
|
3423
|
+
}) : null,
|
|
3424
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${menuOpen ? " dsh-model-menu-chevronOpen" : ""}` })
|
|
3425
|
+
]
|
|
3426
|
+
}),
|
|
3427
|
+
choices,
|
|
3428
|
+
selected: follow ? void 0 : {
|
|
3429
|
+
provider: savedModel.provider,
|
|
3430
|
+
model: savedModel.model
|
|
3431
|
+
},
|
|
3432
|
+
onPick: (picked) => saveSettings({ model: {
|
|
3433
|
+
provider: picked.provider,
|
|
3434
|
+
model: picked.model
|
|
3435
|
+
} }),
|
|
3436
|
+
leading: [{
|
|
3437
|
+
id: "default",
|
|
3438
|
+
label: text.modelDefault,
|
|
3439
|
+
selected: follow
|
|
3440
|
+
}],
|
|
3441
|
+
onPickLeading: () => saveSettings({ model: {
|
|
3442
|
+
provider: "",
|
|
3443
|
+
model: ""
|
|
3444
|
+
} }),
|
|
3445
|
+
efforts,
|
|
3446
|
+
selectedEffort: savedModel.reasoningEffort || "",
|
|
3447
|
+
onPickEffort: (effort) => saveSettings({ model: follow ? {
|
|
3448
|
+
provider: "",
|
|
3449
|
+
model: ""
|
|
3450
|
+
} : {
|
|
3451
|
+
provider: savedModel.provider,
|
|
3452
|
+
model: savedModel.model,
|
|
3453
|
+
...effort ? { reasoningEffort: effort } : {}
|
|
3454
|
+
} }),
|
|
3455
|
+
modelLabel: text.model,
|
|
3456
|
+
modelValue: currentLabel,
|
|
3457
|
+
defaultEffortLabel: text.modelDefault
|
|
3458
|
+
})
|
|
2982
3459
|
}),
|
|
2983
3460
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2984
3461
|
className: "dsh-ui-meta",
|
|
@@ -3046,7 +3523,8 @@ function TitleSettingsItem(props) {
|
|
|
3046
3523
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TitleSettings, {
|
|
3047
3524
|
client: props.client,
|
|
3048
3525
|
marker: props.marker,
|
|
3049
|
-
subscribeSettings: props.subscribeSettings
|
|
3526
|
+
subscribeSettings: props.subscribeSettings,
|
|
3527
|
+
renderFactorySlot: props.renderFactorySlot
|
|
3050
3528
|
})
|
|
3051
3529
|
}) : null]
|
|
3052
3530
|
});
|
|
@@ -3084,10 +3562,11 @@ function apply(ctx) {
|
|
|
3084
3562
|
ctx.effect(() => registerSessionSettingsItem(ctx, {
|
|
3085
3563
|
id: "current-title",
|
|
3086
3564
|
order: 70
|
|
3087
|
-
}, () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TitleSettingsItem, {
|
|
3565
|
+
}, (slotProps) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TitleSettingsItem, {
|
|
3088
3566
|
client,
|
|
3089
3567
|
marker,
|
|
3090
|
-
subscribeSettings
|
|
3568
|
+
subscribeSettings,
|
|
3569
|
+
renderFactorySlot: slotProps?.renderFactorySlot
|
|
3091
3570
|
})), "current-title.settings");
|
|
3092
3571
|
ctx.effect(() => registerAuxiliaryModelSettings(ctx), "current-title.auxiliary-model");
|
|
3093
3572
|
ctx.effect(() => {
|