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