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