@klarkxy/dsh-safe-auto 0.5.3 → 0.5.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 +1 -1
- package/README.zh-CN.md +1 -1
- package/dsh.plugin.json +2 -2
- package/lib/client.js +500 -66
- package/locale/en.json +1 -1
- package/locale/zh.json +2 -2
- package/package.json +18 -10
- package/src/index.js +6 -2
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ Recording covers reviews run after installing the review-history feature. Record
|
|
|
34
34
|
|
|
35
35
|
| Tool | What it does |
|
|
36
36
|
|---|---|
|
|
37
|
-
| `safe_auto_history` | Read-only lookup of the current session's review history. `list` returns summaries and includes a next-page cursor when more records remain; `get` returns one complete record. Use Settings → Session Management → Safe Auto to read history across sessions. Reading history is purely local and leaves the reviewer model idle. |
|
|
37
|
+
| `safe_auto_history` | Read-only lookup of the current session's review history. `list` returns summaries and includes a next-page cursor when more records remain; `get` returns one complete record. Use Settings → Session Management → Safe Auto to read history across sessions. Reading history is purely local and leaves the reviewer model idle. Available only while Safe Auto is enabled in the plugin configuration. |
|
|
38
38
|
|
|
39
39
|
## What gets reviewed
|
|
40
40
|
|
package/README.zh-CN.md
CHANGED
|
@@ -34,7 +34,7 @@ dsh plugin --profile web add @klarkxy/dsh-safe-auto
|
|
|
34
34
|
|
|
35
35
|
| 工具 | 作用 |
|
|
36
36
|
|---|---|
|
|
37
|
-
| `safe_auto_history` | 只读查询当前会话的审核历史:`list` 返回摘要列表,还有后续记录时提供下一页游标;`get` 返回单条完整记录。跨会话历史可在「设置 → 会话管理 →
|
|
37
|
+
| `safe_auto_history` | 只读查询当前会话的审核历史:`list` 返回摘要列表,还有后续记录时提供下一页游标;`get` 返回单条完整记录。跨会话历史可在「设置 → 会话管理 → 安全自动」查看。查询历史仅读取本地记录,保持审核模型空闲。仅在插件配置启用安全自动时可用。 |
|
|
38
38
|
|
|
39
39
|
## 会审核什么
|
|
40
40
|
|
package/dsh.plugin.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "@klarkxy/dsh-safe-auto",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
4
4
|
"main": "src/index.js",
|
|
5
|
-
"description": "Safe Auto: a reviewer model answers native approval requests, Codex auto-review style; only a pass approves that one call.",
|
|
5
|
+
"description": "Safe Auto: a reviewer model answers native approval requests, Codex auto-review style; only a pass approves that one call. The safe_auto_history tool is registered only while Safe Auto is enabled.",
|
|
6
6
|
"engines": {
|
|
7
7
|
"dsh": ">=0.2.1-rc.1"
|
|
8
8
|
},
|
package/lib/client.js
CHANGED
|
@@ -130,6 +130,7 @@ function parseModelMenuChoices(value, bound) {
|
|
|
130
130
|
provider,
|
|
131
131
|
model,
|
|
132
132
|
label: `${providerName} / ${name}`,
|
|
133
|
+
providerLabel: providerName,
|
|
133
134
|
efforts
|
|
134
135
|
});
|
|
135
136
|
}
|
|
@@ -174,11 +175,11 @@ function modelMenuShortName(choice) {
|
|
|
174
175
|
/** Group catalog choices by provider and filter them by a case-insensitive query. */
|
|
175
176
|
function groupModelMenuChoices(choices, query) {
|
|
176
177
|
const needle = query.trim().toLowerCase();
|
|
177
|
-
const matched = needle ? choices.filter((choice) => choice.label.toLowerCase().includes(needle) || choice.model.toLowerCase().includes(needle) || choice.provider.toLowerCase().includes(needle)) : choices;
|
|
178
|
+
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;
|
|
178
179
|
const groups = [];
|
|
179
180
|
for (const choice of matched) {
|
|
180
181
|
const sep = choice.label.indexOf(" / ");
|
|
181
|
-
const name = sep > 0 ? choice.label.slice(0, sep) : choice.provider;
|
|
182
|
+
const name = choice.providerLabel ?? (sep > 0 ? choice.label.slice(0, sep) : choice.provider);
|
|
182
183
|
const last = groups.at(-1);
|
|
183
184
|
if (last && last.provider === choice.provider) last.items.push(choice);
|
|
184
185
|
else groups.push({
|
|
@@ -403,7 +404,7 @@ function ModelMenu(props) {
|
|
|
403
404
|
empty: "No matching models",
|
|
404
405
|
clear: "Clear search"
|
|
405
406
|
};
|
|
406
|
-
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;
|
|
407
|
+
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;
|
|
407
408
|
const rootRef = (0, react.useRef)(null);
|
|
408
409
|
const openRef = (0, react.useRef)(open);
|
|
409
410
|
openRef.current = open;
|
|
@@ -418,9 +419,10 @@ function ModelMenu(props) {
|
|
|
418
419
|
const [pane, setPane] = (0, react.useState)("root");
|
|
419
420
|
const [query, setQuery] = (0, react.useState)("");
|
|
420
421
|
const [highlightedIndex, setHighlightedIndex] = (0, react.useState)(null);
|
|
422
|
+
const [keyNav, setKeyNav] = (0, react.useState)(0);
|
|
421
423
|
const [menuPos, setMenuPos] = (0, react.useState)(null);
|
|
422
424
|
const id = (0, react.useId)();
|
|
423
|
-
const showSearch = choices.length > 4;
|
|
425
|
+
const showSearch = alwaysShowSearch || choices.length > 4;
|
|
424
426
|
const effortRows = (0, react.useMemo)(() => [{
|
|
425
427
|
id: "",
|
|
426
428
|
name: defaultEffortLabel
|
|
@@ -437,7 +439,12 @@ function ModelMenu(props) {
|
|
|
437
439
|
items: (0, _deepseek_ai_dsh_client_ui_primitives.rankByName)(group.items.map((choice) => ({
|
|
438
440
|
choice,
|
|
439
441
|
name: modelMenuShortName(choice),
|
|
440
|
-
label:
|
|
442
|
+
label: [
|
|
443
|
+
choice.provider,
|
|
444
|
+
choice.providerLabel,
|
|
445
|
+
choice.model,
|
|
446
|
+
choice.label
|
|
447
|
+
].filter(Boolean).join(" ")
|
|
441
448
|
})), showSearch ? query.trim() : "")
|
|
442
449
|
})).filter((group) => group.items.length > 0), [
|
|
443
450
|
choices,
|
|
@@ -469,10 +476,12 @@ function ModelMenu(props) {
|
|
|
469
476
|
paneFocus.current = "drill";
|
|
470
477
|
setPane(next);
|
|
471
478
|
};
|
|
472
|
-
/** Leave a drilled pane for the
|
|
479
|
+
/** Leave a drilled pane for the entry pane, handing the keyboard back to it.
|
|
480
|
+
* A root entry refocuses the cell that opened the pane; a search-first
|
|
481
|
+
* (model) entry refocuses that pane's own intent. */
|
|
473
482
|
const back = (from) => {
|
|
474
|
-
paneFocus.current = from;
|
|
475
|
-
setPane(
|
|
483
|
+
paneFocus.current = initialPane === "root" ? from : "drill";
|
|
484
|
+
setPane(initialPane);
|
|
476
485
|
};
|
|
477
486
|
const moveFocus = (offset) => {
|
|
478
487
|
const items = itemRefs.current.filter((item) => item !== null && !item.disabled);
|
|
@@ -512,10 +521,18 @@ function ModelMenu(props) {
|
|
|
512
521
|
}, [requestedOpen, open]);
|
|
513
522
|
(0, react.useEffect)(() => {
|
|
514
523
|
if (!open) return;
|
|
515
|
-
setPane(
|
|
524
|
+
setPane(initialPane);
|
|
525
|
+
if (initialPane === "model") paneFocus.current = "drill";
|
|
516
526
|
setQuery("");
|
|
517
527
|
setHighlightedIndex(null);
|
|
518
|
-
}, [open]);
|
|
528
|
+
}, [open, initialPane]);
|
|
529
|
+
(0, react.useEffect)(() => {
|
|
530
|
+
if (disabled && open && closeOnDisable) changeOpen(false);
|
|
531
|
+
}, [
|
|
532
|
+
disabled,
|
|
533
|
+
open,
|
|
534
|
+
closeOnDisable
|
|
535
|
+
]);
|
|
519
536
|
(0, react.useEffect)(() => {
|
|
520
537
|
if (!open) return;
|
|
521
538
|
const closeOutside = (event) => {
|
|
@@ -523,9 +540,25 @@ function ModelMenu(props) {
|
|
|
523
540
|
if (rootRef.current?.contains(target) || menuRef.current?.contains(target)) return;
|
|
524
541
|
changeOpen(false);
|
|
525
542
|
};
|
|
543
|
+
const escapeOutside = (event) => {
|
|
544
|
+
if (event.key !== "Escape" || event.defaultPrevented || event.isComposing) return;
|
|
545
|
+
if (!openRef.current) return;
|
|
546
|
+
event.preventDefault();
|
|
547
|
+
if (pane !== initialPane) back(pane);
|
|
548
|
+
else close(true);
|
|
549
|
+
};
|
|
526
550
|
document.addEventListener("mousedown", closeOutside);
|
|
527
|
-
|
|
528
|
-
|
|
551
|
+
document.addEventListener("keydown", escapeOutside);
|
|
552
|
+
return () => {
|
|
553
|
+
document.removeEventListener("mousedown", closeOutside);
|
|
554
|
+
document.removeEventListener("keydown", escapeOutside);
|
|
555
|
+
};
|
|
556
|
+
}, [
|
|
557
|
+
open,
|
|
558
|
+
onOpenChange,
|
|
559
|
+
pane,
|
|
560
|
+
initialPane
|
|
561
|
+
]);
|
|
529
562
|
(0, react.useEffect)(() => {
|
|
530
563
|
const intent = paneFocus.current;
|
|
531
564
|
if (!open || menuPos === null || intent === null) return;
|
|
@@ -555,13 +588,21 @@ function ModelMenu(props) {
|
|
|
555
588
|
pane,
|
|
556
589
|
filteredGroups
|
|
557
590
|
]);
|
|
591
|
+
const modelPaneOpen = open && pane === "model";
|
|
592
|
+
const scrollSignal = !modelPaneOpen ? "closed" : keyNav === 0 ? "open" : `key:${keyNav}`;
|
|
593
|
+
const scrollSignalRef = (0, react.useRef)(null);
|
|
558
594
|
(0, react.useLayoutEffect)(() => {
|
|
559
|
-
if (
|
|
595
|
+
if (!modelPaneOpen || activeModelIndex < 0) {
|
|
596
|
+
if (!modelPaneOpen) scrollSignalRef.current = null;
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
if (scrollSignalRef.current === scrollSignal) return;
|
|
600
|
+
scrollSignalRef.current = scrollSignal;
|
|
601
|
+
itemRefs.current[leading.length + activeModelIndex]?.scrollIntoView({ block: "nearest" });
|
|
560
602
|
}, [
|
|
561
|
-
|
|
562
|
-
|
|
603
|
+
modelPaneOpen,
|
|
604
|
+
scrollSignal,
|
|
563
605
|
activeModelIndex,
|
|
564
|
-
visibleModels,
|
|
565
606
|
leading.length
|
|
566
607
|
]);
|
|
567
608
|
(0, react.useLayoutEffect)(() => {
|
|
@@ -576,7 +617,7 @@ function ModelMenu(props) {
|
|
|
576
617
|
let x = rect.right - lw, y = rect.top - 8 - lh;
|
|
577
618
|
if (lw > 0) x = Math.min(Math.max(x, margin), window.innerWidth - lw - margin);
|
|
578
619
|
if (lh > 0) y = Math.min(Math.max(y, margin), window.innerHeight - lh - margin);
|
|
579
|
-
setMenuPos({
|
|
620
|
+
setMenuPos((prev) => prev && prev.left === x && prev.top === y ? prev : {
|
|
580
621
|
left: x,
|
|
581
622
|
top: y
|
|
582
623
|
});
|
|
@@ -596,6 +637,13 @@ function ModelMenu(props) {
|
|
|
596
637
|
]);
|
|
597
638
|
function onKeyDown(event) {
|
|
598
639
|
if (event.defaultPrevented || event.nativeEvent.isComposing) return;
|
|
640
|
+
if (event.key === "Escape" && open) {
|
|
641
|
+
event.preventDefault();
|
|
642
|
+
if (pane !== initialPane) back(pane);
|
|
643
|
+
else close(true);
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
if (disabled) return;
|
|
599
647
|
if (!open && [
|
|
600
648
|
"Enter",
|
|
601
649
|
" ",
|
|
@@ -609,17 +657,12 @@ function ModelMenu(props) {
|
|
|
609
657
|
requestOpen();
|
|
610
658
|
return;
|
|
611
659
|
}
|
|
612
|
-
if (event.key === "Escape" && open) {
|
|
613
|
-
event.preventDefault();
|
|
614
|
-
if (pane !== "root") back(pane);
|
|
615
|
-
else close(true);
|
|
616
|
-
return;
|
|
617
|
-
}
|
|
618
660
|
if (!open) return;
|
|
619
661
|
if (pane === "model" && showSearch && (event.key === "ArrowDown" || event.key === "ArrowUp")) {
|
|
620
662
|
event.preventDefault();
|
|
621
663
|
if (visibleModels.length > 0) {
|
|
622
664
|
setHighlightedIndex((activeModelIndex + (event.key === "ArrowDown" ? 1 : -1) + visibleModels.length) % visibleModels.length);
|
|
665
|
+
setKeyNav((n) => n + 1);
|
|
623
666
|
searchRef.current?.focus();
|
|
624
667
|
}
|
|
625
668
|
return;
|
|
@@ -628,19 +671,19 @@ function ModelMenu(props) {
|
|
|
628
671
|
if (event.key === "Tab" && visibleModels.length === 0) return;
|
|
629
672
|
event.preventDefault();
|
|
630
673
|
const highlighted = visibleModels[activeModelIndex];
|
|
631
|
-
if (highlighted) {
|
|
674
|
+
if (highlighted && !busy) {
|
|
632
675
|
onPick({
|
|
633
676
|
provider: highlighted.provider,
|
|
634
677
|
model: highlighted.model
|
|
635
678
|
});
|
|
636
|
-
close(true);
|
|
679
|
+
if (closeOnPick) close(true);
|
|
637
680
|
}
|
|
638
681
|
return;
|
|
639
682
|
}
|
|
640
683
|
if (event.key === "Tab") {
|
|
641
684
|
if (event.shiftKey) {
|
|
642
685
|
event.preventDefault();
|
|
643
|
-
if (pane !==
|
|
686
|
+
if (pane !== initialPane) back(pane);
|
|
644
687
|
else close(true);
|
|
645
688
|
return;
|
|
646
689
|
}
|
|
@@ -664,7 +707,10 @@ function ModelMenu(props) {
|
|
|
664
707
|
}
|
|
665
708
|
}
|
|
666
709
|
function onBlur(event) {
|
|
667
|
-
|
|
710
|
+
const related = event.relatedTarget;
|
|
711
|
+
if (related instanceof Node && (rootRef.current?.contains(related) || menuRef.current?.contains(related) || !related.isConnected)) return;
|
|
712
|
+
if (!closeOnPick && !(related instanceof Node)) return;
|
|
713
|
+
if (busy && related == null) return;
|
|
668
714
|
if (open) changeOpen(false);
|
|
669
715
|
}
|
|
670
716
|
itemRefs.current = [];
|
|
@@ -685,11 +731,16 @@ function ModelMenu(props) {
|
|
|
685
731
|
"aria-label": modelLabel,
|
|
686
732
|
lang: language,
|
|
687
733
|
children: [
|
|
734
|
+
menuHeader ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
735
|
+
className: "dsh-model-menu-header",
|
|
736
|
+
children: menuHeader
|
|
737
|
+
}) : null,
|
|
688
738
|
pane === "root" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
689
739
|
ref: itemRef(),
|
|
690
740
|
type: "button",
|
|
691
741
|
role: "menuitem",
|
|
692
742
|
className: "dsh-model-menu-cell",
|
|
743
|
+
disabled,
|
|
693
744
|
onClick: () => drill("model"),
|
|
694
745
|
children: [
|
|
695
746
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -702,11 +753,12 @@ function ModelMenu(props) {
|
|
|
702
753
|
}),
|
|
703
754
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutlineRegular, { className: "dsh-model-menu-cellChevron" })
|
|
704
755
|
]
|
|
705
|
-
}), efforts.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
756
|
+
}), efforts.length > 0 && !reasoningSlider ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
706
757
|
ref: itemRef(),
|
|
707
758
|
type: "button",
|
|
708
759
|
role: "menuitem",
|
|
709
760
|
className: "dsh-model-menu-cell",
|
|
761
|
+
disabled,
|
|
710
762
|
onClick: () => drill("effort"),
|
|
711
763
|
children: [
|
|
712
764
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -725,6 +777,7 @@ function ModelMenu(props) {
|
|
|
725
777
|
className: "dsh-model-menu-searchRow",
|
|
726
778
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
|
|
727
779
|
ref: searchRef,
|
|
780
|
+
disabled,
|
|
728
781
|
className: `dsh-model-menu-search${query !== "" ? " dsh-model-menu-searchWithQuery" : ""}`,
|
|
729
782
|
type: "text",
|
|
730
783
|
role: "searchbox",
|
|
@@ -741,6 +794,7 @@ function ModelMenu(props) {
|
|
|
741
794
|
type: "button",
|
|
742
795
|
className: "dsh-model-menu-searchClear",
|
|
743
796
|
"aria-label": clearSearchLabel,
|
|
797
|
+
disabled,
|
|
744
798
|
onClick: () => {
|
|
745
799
|
setQuery("");
|
|
746
800
|
setHighlightedIndex(null);
|
|
@@ -763,10 +817,12 @@ function ModelMenu(props) {
|
|
|
763
817
|
type: "button",
|
|
764
818
|
role: "menuitemradio",
|
|
765
819
|
"aria-checked": item.selected === true,
|
|
820
|
+
disabled: disabled || busy,
|
|
766
821
|
className: `dsh-model-menu-option${item.selected === true ? " dsh-model-menu-selected" : ""}`,
|
|
767
822
|
onClick: () => {
|
|
823
|
+
if (disabled || busy) return;
|
|
768
824
|
onPickLeading?.(item.id);
|
|
769
|
-
close(true);
|
|
825
|
+
if (closeOnPick) close(true);
|
|
770
826
|
},
|
|
771
827
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
772
828
|
className: "dsh-model-menu-optionCopy",
|
|
@@ -792,6 +848,7 @@ function ModelMenu(props) {
|
|
|
792
848
|
"aria-checked": checked,
|
|
793
849
|
id: `${id}-model-${index}`,
|
|
794
850
|
tabIndex: showSearch ? -1 : 0,
|
|
851
|
+
disabled: disabled || busy,
|
|
795
852
|
onFocus: () => setHighlightedIndex(index),
|
|
796
853
|
"data-highlighted": index === activeModelIndex ? "" : void 0,
|
|
797
854
|
className: `dsh-model-menu-option dsh-model-menu-modelOption${checked ? " dsh-model-menu-selected" : ""}${index === activeModelIndex ? " dsh-model-menu-optionActive" : ""}`,
|
|
@@ -801,11 +858,12 @@ function ModelMenu(props) {
|
|
|
801
858
|
},
|
|
802
859
|
title: choice.label,
|
|
803
860
|
onClick: () => {
|
|
861
|
+
if (disabled || busy) return;
|
|
804
862
|
onPick({
|
|
805
863
|
provider: choice.provider,
|
|
806
864
|
model: choice.model
|
|
807
865
|
});
|
|
808
|
-
close(true);
|
|
866
|
+
if (closeOnPick) close(true);
|
|
809
867
|
},
|
|
810
868
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
811
869
|
className: "dsh-model-menu-optionCopy",
|
|
@@ -821,6 +879,14 @@ function ModelMenu(props) {
|
|
|
821
879
|
})
|
|
822
880
|
}, group.provider))]
|
|
823
881
|
}),
|
|
882
|
+
reasoningSlider && efforts.length > 0 && filteredGroups.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EffortSlider, {
|
|
883
|
+
label: effortLabel,
|
|
884
|
+
rows: effortRows,
|
|
885
|
+
selected: selectedEffort ?? "",
|
|
886
|
+
disabled,
|
|
887
|
+
busy,
|
|
888
|
+
onPick: (value) => onPickEffort?.(value)
|
|
889
|
+
}) : null,
|
|
824
890
|
filteredGroups.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
825
891
|
className: "dsh-model-menu-empty",
|
|
826
892
|
role: "status",
|
|
@@ -834,10 +900,12 @@ function ModelMenu(props) {
|
|
|
834
900
|
type: "button",
|
|
835
901
|
role: "menuitemradio",
|
|
836
902
|
"aria-checked": checked,
|
|
903
|
+
disabled: disabled || busy,
|
|
837
904
|
className: `dsh-model-menu-option${checked ? " dsh-model-menu-selected" : ""}`,
|
|
838
905
|
onClick: () => {
|
|
906
|
+
if (disabled || busy) return;
|
|
839
907
|
onPickEffort?.(row.id || void 0);
|
|
840
|
-
close(true);
|
|
908
|
+
if (closeOnPick) close(true);
|
|
841
909
|
},
|
|
842
910
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
843
911
|
className: "dsh-model-menu-optionCopy",
|
|
@@ -856,7 +924,8 @@ function ModelMenu(props) {
|
|
|
856
924
|
const trigger = (0, react.isValidElement)(anchor) ? (0, react.cloneElement)(anchor, {
|
|
857
925
|
"aria-haspopup": "menu",
|
|
858
926
|
"aria-expanded": open,
|
|
859
|
-
"aria-controls": open ? `${id}-menu` : void 0
|
|
927
|
+
"aria-controls": open ? `${id}-menu` : void 0,
|
|
928
|
+
"aria-disabled": disabled || void 0
|
|
860
929
|
}) : anchor;
|
|
861
930
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
862
931
|
ref: rootRef,
|
|
@@ -864,7 +933,7 @@ function ModelMenu(props) {
|
|
|
864
933
|
onKeyDown,
|
|
865
934
|
onBlur,
|
|
866
935
|
onClick: (event) => {
|
|
867
|
-
if (event.defaultPrevented) return;
|
|
936
|
+
if (disabled || event.defaultPrevented) return;
|
|
868
937
|
const button = event.target instanceof Element ? event.target.closest("button") : null;
|
|
869
938
|
if (!(button instanceof HTMLButtonElement) || button.disabled || !rootRef.current?.contains(button)) return;
|
|
870
939
|
requestOpen();
|
|
@@ -876,6 +945,102 @@ function ModelMenu(props) {
|
|
|
876
945
|
});
|
|
877
946
|
}
|
|
878
947
|
/**
|
|
948
|
+
* Discrete reasoning-effort slider shown under the model list in enhanced mode.
|
|
949
|
+
*
|
|
950
|
+
* A native range input (`step=1`) over exactly the rows the caller supplies —
|
|
951
|
+
* the default row plus each effort the selected model advertises — so it can
|
|
952
|
+
* never invent a level. It previews locally while dragging or arrowing and
|
|
953
|
+
* commits only when the gesture ends (pointer up, key up, or blur), so a drag
|
|
954
|
+
* does not write on every step. The menu stays open, so adjusting the effort
|
|
955
|
+
* never loses the model selection or the search query.
|
|
956
|
+
*
|
|
957
|
+
* The input itself stays the entire interaction and accessibility surface —
|
|
958
|
+
* keyboard, ARIA, disabled and the native step snapping all come from it —
|
|
959
|
+
* while the track, gradient fill and thumb below are decorative layers painted
|
|
960
|
+
* from the `--_p` percentage the preview position sets on the rail. Value
|
|
961
|
+
* changes only update that custom property, so the CSS transitions animate
|
|
962
|
+
* between levels without remounting nodes or restarting an animation.
|
|
963
|
+
*/
|
|
964
|
+
function EffortSlider(props) {
|
|
965
|
+
const { label, rows, selected, disabled = false, busy = false, onPick } = props;
|
|
966
|
+
const active = Math.max(0, rows.findIndex((row) => row.id === selected));
|
|
967
|
+
const [position, setPosition] = (0, react.useState)(active);
|
|
968
|
+
const positionRef = (0, react.useRef)(position);
|
|
969
|
+
(0, react.useEffect)(() => {
|
|
970
|
+
positionRef.current = active;
|
|
971
|
+
setPosition(active);
|
|
972
|
+
}, [active]);
|
|
973
|
+
const preview = (next) => {
|
|
974
|
+
positionRef.current = next;
|
|
975
|
+
setPosition(next);
|
|
976
|
+
};
|
|
977
|
+
const commit = () => {
|
|
978
|
+
if (disabled || busy) return;
|
|
979
|
+
const row = rows[positionRef.current];
|
|
980
|
+
if (row !== void 0) onPick(row.id === "" ? void 0 : row.id);
|
|
981
|
+
};
|
|
982
|
+
const max = Math.max(0, rows.length - 1);
|
|
983
|
+
const percent = max > 0 ? Math.round(position / max * 100) : 0;
|
|
984
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
985
|
+
className: "dsh-model-menu-sliderGroup",
|
|
986
|
+
role: "group",
|
|
987
|
+
"aria-label": label,
|
|
988
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
989
|
+
className: "dsh-model-menu-sliderHead",
|
|
990
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
991
|
+
className: "dsh-model-menu-sliderTitle",
|
|
992
|
+
children: label
|
|
993
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
994
|
+
className: "dsh-model-menu-sliderValue",
|
|
995
|
+
children: rows[position]?.name
|
|
996
|
+
})]
|
|
997
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
998
|
+
className: "dsh-model-menu-sliderRail",
|
|
999
|
+
style: { "--_p": percent },
|
|
1000
|
+
children: [
|
|
1001
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1002
|
+
type: "range",
|
|
1003
|
+
className: "dsh-model-menu-sliderInput",
|
|
1004
|
+
min: 0,
|
|
1005
|
+
max,
|
|
1006
|
+
step: 1,
|
|
1007
|
+
value: position,
|
|
1008
|
+
disabled,
|
|
1009
|
+
"aria-label": label,
|
|
1010
|
+
"aria-disabled": disabled || busy || void 0,
|
|
1011
|
+
"aria-valuetext": rows[position]?.name,
|
|
1012
|
+
onChange: (event) => {
|
|
1013
|
+
const value = Number(event.target.value);
|
|
1014
|
+
if (Number.isFinite(value)) preview(value);
|
|
1015
|
+
},
|
|
1016
|
+
onPointerUp: commit,
|
|
1017
|
+
onKeyUp: commit,
|
|
1018
|
+
onBlur: commit
|
|
1019
|
+
}),
|
|
1020
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1021
|
+
className: "dsh-model-menu-sliderTrack",
|
|
1022
|
+
"aria-hidden": "true",
|
|
1023
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1024
|
+
className: "dsh-model-menu-sliderFill",
|
|
1025
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dsh-model-menu-sliderFillBar" })
|
|
1026
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1027
|
+
className: "dsh-model-menu-sliderNodes",
|
|
1028
|
+
children: rows.map((row, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1029
|
+
"data-covered": index <= position ? "" : void 0,
|
|
1030
|
+
className: "dsh-model-menu-sliderNode",
|
|
1031
|
+
style: { "--_n": max > 0 ? Math.round(index / max * 100) : 0 }
|
|
1032
|
+
}, row.id || "__default"))
|
|
1033
|
+
})]
|
|
1034
|
+
}),
|
|
1035
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1036
|
+
className: "dsh-model-menu-sliderThumb",
|
|
1037
|
+
"aria-hidden": "true"
|
|
1038
|
+
})
|
|
1039
|
+
]
|
|
1040
|
+
})]
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
879
1044
|
* ModelMenu styles, transcribed from the official ModelSelect.module.css (the
|
|
880
1045
|
* published dsh-client-ui-model-selection bundle): card capped at
|
|
881
1046
|
* min(360px, 100vh - 96px), 34px option cells at 13/18 with 5px 7px padding,
|
|
@@ -934,6 +1099,7 @@ const modelMenuCss = `
|
|
|
934
1099
|
overflow: hidden;
|
|
935
1100
|
}
|
|
936
1101
|
.dsh-model-menu-status, .dsh-model-menu-empty { color: var(--dsw-alias-label-tertiary); padding: 8px; font-size: 12px; line-height: 18px; }
|
|
1102
|
+
.dsh-model-menu-header { flex: none; padding: 2px 2px 4px; }
|
|
937
1103
|
.dsh-model-menu-searchRow { flex-shrink: 0; margin: 2px 0 3px; position: relative; }
|
|
938
1104
|
.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; }
|
|
939
1105
|
.dsh-model-menu-searchRow .dsh-model-menu-search:focus-within { border-color: transparent; }
|
|
@@ -1012,6 +1178,145 @@ const modelMenuCss = `
|
|
|
1012
1178
|
.dsh-model-menu-cellLabel { white-space: nowrap; flex: none; }
|
|
1013
1179
|
.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; }
|
|
1014
1180
|
.dsh-model-menu-cellChevron { width: 12px; height: 12px; color: var(--dsw-alias-menu-icon); flex: none; }
|
|
1181
|
+
/* Discrete reasoning-effort slider under the model list (enhanced mode).
|
|
1182
|
+
* The painted control is layered over a fully functional native range input:
|
|
1183
|
+
* the input stays on top, transparent, and owns pointer/keyboard/ARIA, while
|
|
1184
|
+
* the decorative track, fill and thumb render beneath it at pointer-events:
|
|
1185
|
+
* none. Position lives in one custom property (--_p, 0–100) on the rail, so
|
|
1186
|
+
* the only animated work a level change triggers is a clip-path/left
|
|
1187
|
+
* transition — no keyframes, no element churn. */
|
|
1188
|
+
.dsh-model-menu-sliderGroup { border-top: 0.5px solid var(--dsw-alias-border-l2); flex: none; margin-top: 4px; padding: 6px 8px 8px; }
|
|
1189
|
+
.dsh-model-menu-sliderHead { align-items: baseline; justify-content: space-between; gap: 8px; margin: 0 2px 4px; display: flex; }
|
|
1190
|
+
.dsh-model-menu-sliderTitle { color: var(--dsw-alias-label-tertiary); font-size: 12px; line-height: 18px; }
|
|
1191
|
+
.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); }
|
|
1192
|
+
.dsh-model-menu-sliderRail { position: relative; direction: ltr; height: 38px; margin-top: 2px; }
|
|
1193
|
+
.dsh-model-menu-sliderRail:has(:disabled) { opacity: 0.45; }
|
|
1194
|
+
.dsh-model-menu-sliderInput {
|
|
1195
|
+
appearance: none;
|
|
1196
|
+
-webkit-appearance: none;
|
|
1197
|
+
box-sizing: border-box;
|
|
1198
|
+
border: 0;
|
|
1199
|
+
cursor: pointer;
|
|
1200
|
+
direction: ltr;
|
|
1201
|
+
opacity: 0;
|
|
1202
|
+
margin: 0;
|
|
1203
|
+
padding: 0;
|
|
1204
|
+
width: 100%;
|
|
1205
|
+
height: 100%;
|
|
1206
|
+
position: absolute;
|
|
1207
|
+
inset: 0;
|
|
1208
|
+
z-index: 1;
|
|
1209
|
+
touch-action: pan-y;
|
|
1210
|
+
}
|
|
1211
|
+
.dsh-model-menu-sliderInput:disabled { cursor: default; }
|
|
1212
|
+
/* The transparent input must keep the painted thumb's geometry: an
|
|
1213
|
+
* appearance:none webkit thumb defaults to 16px, which would shift the
|
|
1214
|
+
* end-point positions 7px off the decorative 30px disc and misplace the
|
|
1215
|
+
* native drag hit target. Track gets the full rail height so the whole band
|
|
1216
|
+
* maps to values identically. */
|
|
1217
|
+
.dsh-model-menu-sliderInput::-webkit-slider-runnable-track { width: 100%; height: 100%; }
|
|
1218
|
+
.dsh-model-menu-sliderInput::-webkit-slider-thumb { appearance: none; -webkit-appearance: none; width: 30px; height: 30px; margin: 0; }
|
|
1219
|
+
.dsh-model-menu-sliderInput::-moz-range-track { width: 100%; height: 100%; }
|
|
1220
|
+
.dsh-model-menu-sliderInput::-moz-range-thumb { width: 30px; height: 30px; margin: 0; border: 0; }
|
|
1221
|
+
.dsh-model-menu-sliderTrack {
|
|
1222
|
+
pointer-events: none;
|
|
1223
|
+
border-radius: 999px;
|
|
1224
|
+
width: 100%;
|
|
1225
|
+
height: 26px;
|
|
1226
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
1227
|
+
box-shadow: inset 0 0 0 0.5px var(--dsw-alias-border-l2);
|
|
1228
|
+
position: absolute;
|
|
1229
|
+
inset-inline: 0;
|
|
1230
|
+
top: 50%;
|
|
1231
|
+
transform: translateY(-50%);
|
|
1232
|
+
/* No overflow:hidden — it would clip the fill's drop-shadow glow; the bar's
|
|
1233
|
+
* own clip-path already supplies the rounded reveal shape. */
|
|
1234
|
+
}
|
|
1235
|
+
/* The accent ramp paints the full track width once and is revealed by an
|
|
1236
|
+
* animated clip-path, so raising the effort unwraps more of the spectrum
|
|
1237
|
+
* instead of squashing the same gradient into a narrower bar. The glow lives
|
|
1238
|
+
* on the un-clipped parent: a filter on the clipped element itself would be
|
|
1239
|
+
* clipped away with it. */
|
|
1240
|
+
.dsh-model-menu-sliderFill {
|
|
1241
|
+
display: block;
|
|
1242
|
+
height: 100%;
|
|
1243
|
+
filter: drop-shadow(0 0 5px var(--dsw-alias-state-business-primary));
|
|
1244
|
+
transition: filter var(--ds-transition-duration) var(--ds-ease-in-out);
|
|
1245
|
+
}
|
|
1246
|
+
.dsh-model-menu-sliderInput:hover:not(:disabled) ~ .dsh-model-menu-sliderTrack .dsh-model-menu-sliderFill,
|
|
1247
|
+
.dsh-model-menu-sliderInput:active:not(:disabled) ~ .dsh-model-menu-sliderTrack .dsh-model-menu-sliderFill {
|
|
1248
|
+
filter: drop-shadow(0 0 9px var(--dsw-alias-state-business-primary));
|
|
1249
|
+
}
|
|
1250
|
+
/* The fill stops at the thumb's center, not at the track edge — at a 30px
|
|
1251
|
+
* thumb the end stops sit 15px in from each side, matching the node dots. */
|
|
1252
|
+
.dsh-model-menu-sliderFillBar {
|
|
1253
|
+
display: block;
|
|
1254
|
+
height: 100%;
|
|
1255
|
+
background: linear-gradient(90deg,
|
|
1256
|
+
var(--dsw-alias-state-business-primary),
|
|
1257
|
+
var(--dsw-alias-state-success-primary),
|
|
1258
|
+
var(--dsw-alias-state-warn-primary),
|
|
1259
|
+
var(--dsw-alias-state-error-primary));
|
|
1260
|
+
clip-path: inset(0 calc(100% - 15px - (100% - 30px) * var(--_p, 0) / 100) 0 0 round 999px);
|
|
1261
|
+
transition: clip-path var(--ds-transition-duration) var(--ds-ease-in-out);
|
|
1262
|
+
}
|
|
1263
|
+
/* Stop dots: one per declared row (default + every advertised effort), evenly
|
|
1264
|
+
* spread across the band between the two thumb-center extremes (inset 15px,
|
|
1265
|
+
* i.e. half the 30px thumb). --_n is 0-100 along that band. A covered dot
|
|
1266
|
+
* reads as a pale pin through the gradient; an uncovered one stays a neutral
|
|
1267
|
+
* notch on the empty track. */
|
|
1268
|
+
.dsh-model-menu-sliderNodes {
|
|
1269
|
+
pointer-events: none;
|
|
1270
|
+
display: block;
|
|
1271
|
+
position: absolute;
|
|
1272
|
+
inset-inline: 15px;
|
|
1273
|
+
inset-block: 0;
|
|
1274
|
+
}
|
|
1275
|
+
.dsh-model-menu-sliderNode {
|
|
1276
|
+
pointer-events: none;
|
|
1277
|
+
box-sizing: border-box;
|
|
1278
|
+
width: 4px;
|
|
1279
|
+
height: 4px;
|
|
1280
|
+
border-radius: 50%;
|
|
1281
|
+
background: color-mix(in srgb, var(--dsw-alias-label-secondary) 35%, transparent);
|
|
1282
|
+
position: absolute;
|
|
1283
|
+
left: calc(var(--_n, 0) * 1%);
|
|
1284
|
+
top: 50%;
|
|
1285
|
+
transform: translate(-50%, -50%);
|
|
1286
|
+
transition: background-color var(--ds-transition-duration) var(--ds-ease-in-out),
|
|
1287
|
+
transform var(--ds-transition-duration) var(--ds-ease-in-out);
|
|
1288
|
+
}
|
|
1289
|
+
.dsh-model-menu-sliderNode[data-covered] { background: color-mix(in srgb, var(--dsw-alias-label-primary) 60%, transparent); }
|
|
1290
|
+
.dsh-model-menu-sliderThumb {
|
|
1291
|
+
pointer-events: none;
|
|
1292
|
+
box-sizing: border-box;
|
|
1293
|
+
border-radius: 50%;
|
|
1294
|
+
width: 30px;
|
|
1295
|
+
height: 30px;
|
|
1296
|
+
background: var(--dsw-alias-button-primary-fill);
|
|
1297
|
+
border: 0.5px solid var(--dsw-alias-border-l4);
|
|
1298
|
+
box-shadow: 0 0 0 3px var(--dsw-alias-interactive-bg-hover), var(--dsw-elevation-soft);
|
|
1299
|
+
position: absolute;
|
|
1300
|
+
left: calc((100% - 30px) * var(--_p, 0) / 100);
|
|
1301
|
+
top: 50%;
|
|
1302
|
+
transform: translateY(-50%) scale(0.92);
|
|
1303
|
+
transition: left var(--ds-transition-duration) var(--ds-ease-in-out),
|
|
1304
|
+
transform var(--ds-transition-duration) var(--ds-ease-in-out),
|
|
1305
|
+
box-shadow var(--ds-transition-duration) var(--ds-ease-in-out);
|
|
1306
|
+
}
|
|
1307
|
+
.dsh-model-menu-sliderInput:hover:not(:disabled) ~ .dsh-model-menu-sliderThumb { transform: translateY(-50%) scale(1); }
|
|
1308
|
+
.dsh-model-menu-sliderInput:active:not(:disabled) ~ .dsh-model-menu-sliderThumb {
|
|
1309
|
+
transform: translateY(-50%) scale(1.1);
|
|
1310
|
+
box-shadow: 0 0 0 4px var(--dsw-alias-interactive-bg-hover), 0 0 12px var(--dsw-alias-state-business-primary);
|
|
1311
|
+
}
|
|
1312
|
+
.dsh-model-menu-sliderInput:focus-visible ~ .dsh-model-menu-sliderThumb {
|
|
1313
|
+
box-shadow: 0 0 0 3px var(--dsw-alias-interactive-bg-hover),
|
|
1314
|
+
0 0 0 calc(3px + var(--dsw-focus-ring-width)) var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
|
|
1315
|
+
}
|
|
1316
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1317
|
+
.dsh-model-menu-sliderFill, .dsh-model-menu-sliderFillBar, .dsh-model-menu-sliderThumb,
|
|
1318
|
+
.dsh-model-menu-sliderNode, .dsh-model-menu-sliderValue { transition: none; }
|
|
1319
|
+
}
|
|
1015
1320
|
`;
|
|
1016
1321
|
Object.values({
|
|
1017
1322
|
/** Surfaces, from the page up to the topmost nested card. */
|
|
@@ -1102,7 +1407,9 @@ Object.values({
|
|
|
1102
1407
|
"--dsh-scrollbar-thumb-hover",
|
|
1103
1408
|
"--dsh-content-font-size-secondary",
|
|
1104
1409
|
"--dsh-content-font-delta",
|
|
1105
|
-
"--dsh-frame-top-clearance"
|
|
1410
|
+
"--dsh-frame-top-clearance",
|
|
1411
|
+
"--dsh-composer-model-icon-display",
|
|
1412
|
+
"--dsh-composer-model-text-display"
|
|
1106
1413
|
]
|
|
1107
1414
|
}).flat();
|
|
1108
1415
|
/**
|
|
@@ -1776,6 +2083,95 @@ function createModelSettingsSubscriber(ctx, packageName, identity = modelSetting
|
|
|
1776
2083
|
} });
|
|
1777
2084
|
}
|
|
1778
2085
|
//#endregion
|
|
2086
|
+
//#region ../dsh-plugin-kit/lib/model-picker.js
|
|
2087
|
+
/**
|
|
2088
|
+
* Optional shared model picker (`klarkxy.model-picker` Factory).
|
|
2089
|
+
*
|
|
2090
|
+
* `@klarkxy/dsh-model-picker`, when installed, registers one root-scoped
|
|
2091
|
+
* Component Factory under `MODEL_PICKER_FACTORY`: an enhanced model menu with
|
|
2092
|
+
* a searchable provider-grouped list and a reasoning-effort slider, built on
|
|
2093
|
+
* the same ModelMenu recipe. Consumers keep their own ModelMenu as the
|
|
2094
|
+
* `fallback`; when no definition is registered the renderer resolves that
|
|
2095
|
+
* fallback verbatim, so the shared picker is strictly opt-in and every saved
|
|
2096
|
+
* value keeps its existing write path.
|
|
2097
|
+
*
|
|
2098
|
+
* `renderFactorySlot` is a framework-composed prop, never a context: the
|
|
2099
|
+
* component a plugin registers into any slot receives it automatically, but
|
|
2100
|
+
* the plugin must pass it down to whichever child renders the menu (or to the
|
|
2101
|
+
* render callback handed to registerSessionSettingsItem).
|
|
2102
|
+
*
|
|
2103
|
+
* The Factory manages its own open state; a consumer's `open`/`onOpenChange`
|
|
2104
|
+
* pair belongs to the fallback menu only and must not be fed to both.
|
|
2105
|
+
*/
|
|
2106
|
+
/**
|
|
2107
|
+
* The trigger every consumer used to hand its own ModelMenu, rebuilt as the
|
|
2108
|
+
* factory's `anchor` local component: identical classes and chevron, the menu
|
|
2109
|
+
* state coming from the occurrence's props instead of local open state.
|
|
2110
|
+
*
|
|
2111
|
+
* `ariaLabel` and `title` are the whole accessible name and tooltip (the
|
|
2112
|
+
* caller formats "field: current route" itself because the factory's resolved
|
|
2113
|
+
* caption may intentionally be a short name). The component identity stays
|
|
2114
|
+
* put across label changes — a new function would unmount the trigger, and
|
|
2115
|
+
* its cleanup would close a menu that `closeOnPick={false}` is keeping open.
|
|
2116
|
+
* The latest label, title, and `onClose` are read from refs.
|
|
2117
|
+
*/
|
|
2118
|
+
function useModelMenuAnchor(ariaLabel, title, onClose) {
|
|
2119
|
+
const closeRef = (0, react.useRef)(onClose);
|
|
2120
|
+
const labelRef = (0, react.useRef)(ariaLabel);
|
|
2121
|
+
const titleRef = (0, react.useRef)(title);
|
|
2122
|
+
closeRef.current = onClose;
|
|
2123
|
+
labelRef.current = ariaLabel;
|
|
2124
|
+
titleRef.current = title;
|
|
2125
|
+
return (0, react.useMemo)(() => {
|
|
2126
|
+
const ModelPickerTrigger = (props) => {
|
|
2127
|
+
(0, react.useEffect)(() => () => closeRef.current?.(), []);
|
|
2128
|
+
const record = props;
|
|
2129
|
+
const injected = {};
|
|
2130
|
+
for (const [key, value] of Object.entries(record)) if (key.startsWith("aria-")) injected[key] = value;
|
|
2131
|
+
const extraClass = typeof record.className === "string" ? ` ${record.className}` : "";
|
|
2132
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2133
|
+
type: "button",
|
|
2134
|
+
className: `dsh-model-menu-trigger${extraClass}`,
|
|
2135
|
+
disabled: props.disabled,
|
|
2136
|
+
"aria-haspopup": "menu",
|
|
2137
|
+
"aria-expanded": props.open,
|
|
2138
|
+
...injected,
|
|
2139
|
+
"aria-label": labelRef.current,
|
|
2140
|
+
title: titleRef.current,
|
|
2141
|
+
children: [
|
|
2142
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2143
|
+
className: "dsh-model-menu-triggerLabel",
|
|
2144
|
+
children: props.label
|
|
2145
|
+
}),
|
|
2146
|
+
props.effortLabel ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2147
|
+
className: "dsh-model-menu-triggerEffort",
|
|
2148
|
+
children: props.effortLabel
|
|
2149
|
+
}) : null,
|
|
2150
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${props.open ? " dsh-model-menu-chevronOpen" : ""}` })
|
|
2151
|
+
]
|
|
2152
|
+
});
|
|
2153
|
+
};
|
|
2154
|
+
return ModelPickerTrigger;
|
|
2155
|
+
}, []);
|
|
2156
|
+
}
|
|
2157
|
+
/**
|
|
2158
|
+
* Render the shared model picker while it is registered; `options.fallback`
|
|
2159
|
+
* whenever it is not (plugin absent, disabled, or unloaded).
|
|
2160
|
+
*
|
|
2161
|
+
* `options.anchor` is optional: the factory ships a default trigger, so only
|
|
2162
|
+
* consumers that need their own chrome (custom aria-label, tooltip, class)
|
|
2163
|
+
* pass one built with {@link useModelMenuAnchor}. The returned node is never
|
|
2164
|
+
* assumed to be the factory's own output — a defensive `?? fallback` covers a
|
|
2165
|
+
* renderer that yields null for an unregistered definition.
|
|
2166
|
+
*/
|
|
2167
|
+
function renderModelPicker(renderFactorySlot, props, options) {
|
|
2168
|
+
if (typeof renderFactorySlot !== "function") return options.fallback;
|
|
2169
|
+
return renderFactorySlot("klarkxy.model-picker", props, {
|
|
2170
|
+
...options.anchor ? { slots: { ["anchor"]: options.anchor } } : {},
|
|
2171
|
+
fallback: options.fallback
|
|
2172
|
+
}) ?? options.fallback;
|
|
2173
|
+
}
|
|
2174
|
+
//#endregion
|
|
1779
2175
|
//#region ../dsh-plugin-kit/lib/auxiliary-model-client.js
|
|
1780
2176
|
const AUXILIARY_MODEL_PACKAGE = "@klarkxy/dsh-plugin-kit";
|
|
1781
2177
|
const AUXILIARY_MODEL_SEAT_ID = "auxiliary-model";
|
|
@@ -1853,7 +2249,7 @@ async function callAuxiliaryRpc(client, endpoint, payload = {}) {
|
|
|
1853
2249
|
* The shared card. All state comes from the shared host service over the
|
|
1854
2250
|
* plugin-kit RPC channel; the card only renders the stored row.
|
|
1855
2251
|
*/
|
|
1856
|
-
function AuxiliaryModelCard({ client, subscribe }) {
|
|
2252
|
+
function AuxiliaryModelCard({ client, subscribe, renderFactorySlot }) {
|
|
1857
2253
|
const language = useAuxiliaryLocale(client);
|
|
1858
2254
|
const text = auxiliaryCopy[language];
|
|
1859
2255
|
const [settings, setSettings] = (0, react.useState)();
|
|
@@ -1974,6 +2370,17 @@ function AuxiliaryModelCard({ client, subscribe }) {
|
|
|
1974
2370
|
refreshCatalog
|
|
1975
2371
|
]);
|
|
1976
2372
|
const locked = busy;
|
|
2373
|
+
const current = settings?.model ?? {
|
|
2374
|
+
provider: "",
|
|
2375
|
+
model: ""
|
|
2376
|
+
};
|
|
2377
|
+
const follow = !current.provider || !current.model;
|
|
2378
|
+
const selected = follow ? void 0 : choices.find((item) => item.provider === current.provider && item.model === current.model);
|
|
2379
|
+
const efforts = follow ? [] : modelMenuEffortOptions(selected, current.reasoningEffort);
|
|
2380
|
+
const currentLabel = follow ? text.modelDefault : selected?.label ?? `${current.provider} / ${current.model}`;
|
|
2381
|
+
const selectedEffort = efforts.find((item) => item.id === (current.reasoningEffort || ""));
|
|
2382
|
+
const effortName = selectedEffort ? modelMenuEffortLabel(selectedEffort, language) : void 0;
|
|
2383
|
+
const anchor = useModelMenuAnchor(`${text.model}: ${settings ? currentLabel : ""}`, void 0, () => setMenuOpen(false));
|
|
1977
2384
|
if (!settings) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1978
2385
|
className: "ams-root dsh-ui-card",
|
|
1979
2386
|
"data-testid": "auxiliary-model-settings",
|
|
@@ -2012,16 +2419,6 @@ function AuxiliaryModelCard({ client, subscribe }) {
|
|
|
2012
2419
|
})
|
|
2013
2420
|
]
|
|
2014
2421
|
});
|
|
2015
|
-
const current = settings.model ?? {
|
|
2016
|
-
provider: "",
|
|
2017
|
-
model: ""
|
|
2018
|
-
};
|
|
2019
|
-
const follow = !current.provider || !current.model;
|
|
2020
|
-
const selected = follow ? void 0 : choices.find((item) => item.provider === current.provider && item.model === current.model);
|
|
2021
|
-
const efforts = follow ? [] : modelMenuEffortOptions(selected, current.reasoningEffort);
|
|
2022
|
-
const currentLabel = follow ? text.modelDefault : selected?.label ?? `${current.provider} / ${current.model}`;
|
|
2023
|
-
const selectedEffort = efforts.find((item) => item.id === (current.reasoningEffort || ""));
|
|
2024
|
-
const effortName = selectedEffort ? modelMenuEffortLabel(selectedEffort, language) : void 0;
|
|
2025
2422
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2026
2423
|
className: "ams-root dsh-ui-card",
|
|
2027
2424
|
"data-testid": "auxiliary-model-settings",
|
|
@@ -2045,28 +2442,12 @@ function AuxiliaryModelCard({ client, subscribe }) {
|
|
|
2045
2442
|
state: catalogState,
|
|
2046
2443
|
locale: language
|
|
2047
2444
|
}),
|
|
2048
|
-
|
|
2445
|
+
renderModelPicker(renderFactorySlot, {
|
|
2049
2446
|
locale: language,
|
|
2050
|
-
|
|
2447
|
+
disabled: locked,
|
|
2448
|
+
busy: locked,
|
|
2051
2449
|
onOpenChange: (next) => setMenuOpen(next && !locked),
|
|
2052
2450
|
listClassName: "ams-root ams-menu-portal",
|
|
2053
|
-
anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2054
|
-
type: "button",
|
|
2055
|
-
className: "dsh-model-menu-trigger",
|
|
2056
|
-
disabled: locked,
|
|
2057
|
-
"aria-label": `${text.model}: ${currentLabel}`,
|
|
2058
|
-
children: [
|
|
2059
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2060
|
-
className: "dsh-model-menu-triggerLabel",
|
|
2061
|
-
children: currentLabel
|
|
2062
|
-
}),
|
|
2063
|
-
!follow && efforts.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2064
|
-
className: "dsh-model-menu-triggerEffort",
|
|
2065
|
-
children: effortName ?? text.effortDefault
|
|
2066
|
-
}) : null,
|
|
2067
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${menuOpen && !locked ? " dsh-model-menu-chevronOpen" : ""}` })
|
|
2068
|
-
]
|
|
2069
|
-
}),
|
|
2070
2451
|
choices,
|
|
2071
2452
|
selected: follow ? void 0 : {
|
|
2072
2453
|
provider: current.provider,
|
|
@@ -2089,9 +2470,61 @@ function AuxiliaryModelCard({ client, subscribe }) {
|
|
|
2089
2470
|
model: current.model,
|
|
2090
2471
|
...effort ? { reasoningEffort: effort } : {}
|
|
2091
2472
|
}),
|
|
2473
|
+
triggerLabel: currentLabel,
|
|
2474
|
+
triggerEffortLabel: !follow && efforts.length ? effortName ?? text.effortDefault : void 0,
|
|
2092
2475
|
modelLabel: text.model,
|
|
2093
2476
|
modelValue: currentLabel,
|
|
2094
2477
|
defaultEffortLabel: text.effortDefault
|
|
2478
|
+
}, {
|
|
2479
|
+
anchor,
|
|
2480
|
+
fallback: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
|
|
2481
|
+
locale: language,
|
|
2482
|
+
open: menuOpen && !locked,
|
|
2483
|
+
onOpenChange: (next) => setMenuOpen(next && !locked),
|
|
2484
|
+
listClassName: "ams-root ams-menu-portal",
|
|
2485
|
+
anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2486
|
+
type: "button",
|
|
2487
|
+
className: "dsh-model-menu-trigger",
|
|
2488
|
+
disabled: locked,
|
|
2489
|
+
"aria-label": `${text.model}: ${currentLabel}`,
|
|
2490
|
+
children: [
|
|
2491
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2492
|
+
className: "dsh-model-menu-triggerLabel",
|
|
2493
|
+
children: currentLabel
|
|
2494
|
+
}),
|
|
2495
|
+
!follow && efforts.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2496
|
+
className: "dsh-model-menu-triggerEffort",
|
|
2497
|
+
children: effortName ?? text.effortDefault
|
|
2498
|
+
}) : null,
|
|
2499
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${menuOpen && !locked ? " dsh-model-menu-chevronOpen" : ""}` })
|
|
2500
|
+
]
|
|
2501
|
+
}),
|
|
2502
|
+
choices,
|
|
2503
|
+
selected: follow ? void 0 : {
|
|
2504
|
+
provider: current.provider,
|
|
2505
|
+
model: current.model
|
|
2506
|
+
},
|
|
2507
|
+
onPick: (picked) => save({
|
|
2508
|
+
provider: picked.provider,
|
|
2509
|
+
model: picked.model
|
|
2510
|
+
}),
|
|
2511
|
+
leading: [{
|
|
2512
|
+
id: "default",
|
|
2513
|
+
label: text.modelDefault,
|
|
2514
|
+
selected: follow
|
|
2515
|
+
}],
|
|
2516
|
+
onPickLeading: () => save(null),
|
|
2517
|
+
efforts,
|
|
2518
|
+
selectedEffort: current.reasoningEffort || "",
|
|
2519
|
+
onPickEffort: (effort) => save(follow ? null : {
|
|
2520
|
+
provider: current.provider,
|
|
2521
|
+
model: current.model,
|
|
2522
|
+
...effort ? { reasoningEffort: effort } : {}
|
|
2523
|
+
}),
|
|
2524
|
+
modelLabel: text.model,
|
|
2525
|
+
modelValue: currentLabel,
|
|
2526
|
+
defaultEffortLabel: text.effortDefault
|
|
2527
|
+
})
|
|
2095
2528
|
}),
|
|
2096
2529
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2097
2530
|
className: "dsh-ui-hint",
|
|
@@ -2193,9 +2626,10 @@ function acquireAuxiliaryModelCard(root, ctx) {
|
|
|
2193
2626
|
let unprovide;
|
|
2194
2627
|
const watchLease = acquireWatchCoordinator(root).mount();
|
|
2195
2628
|
const subscriber = createModelSettingsSubscriber(ctx.root, AUXILIARY_MODEL_PACKAGE, AUXILIARY_MODEL_CHANGES_IDENTITY, (ns) => ns === AUXILIARY_MODEL_NS);
|
|
2196
|
-
const render = () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AuxiliaryModelCard, {
|
|
2629
|
+
const render = (props) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AuxiliaryModelCard, {
|
|
2197
2630
|
client: root,
|
|
2198
|
-
subscribe: subscriber.subscribe
|
|
2631
|
+
subscribe: subscriber.subscribe,
|
|
2632
|
+
renderFactorySlot: props.renderFactorySlot
|
|
2199
2633
|
});
|
|
2200
2634
|
const service = {
|
|
2201
2635
|
retain() {
|
package/locale/en.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"title": "Safe Auto",
|
|
4
|
-
"description": "
|
|
4
|
+
"description": "Adds a Safe Auto approval level: a reviewer model answers approval requests one call at a time, with full review history and audit trails."
|
|
5
5
|
}
|
|
6
6
|
}
|
package/locale/zh.json
CHANGED
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@klarkxy/dsh-safe-auto",
|
|
3
|
-
"version": "0.5.
|
|
4
|
-
"description": "Safe Auto: a reviewer model answers
|
|
3
|
+
"version": "0.5.5",
|
|
4
|
+
"description": "Safe Auto: a reviewer model answers approval requests one call at a time; includes review history with full audit trails.",
|
|
5
5
|
"icon": "./icon.svg",
|
|
6
6
|
"type": "module",
|
|
7
|
+
"homepage": "https://klarkxy.github.io/dsh-plugins/plugins/safe-auto/",
|
|
7
8
|
"main": "src/index.js",
|
|
8
9
|
"exports": {
|
|
9
10
|
".": "./src/index.js",
|
|
@@ -37,10 +38,17 @@
|
|
|
37
38
|
"directory": "plugins/dsh-safe-auto"
|
|
38
39
|
},
|
|
39
40
|
"keywords": [
|
|
41
|
+
"dsh",
|
|
40
42
|
"dsh-plugin",
|
|
43
|
+
"dsh-plugins",
|
|
41
44
|
"deepseek-harness",
|
|
45
|
+
"deepseek-harness-plugin",
|
|
42
46
|
"approval",
|
|
43
|
-
"security"
|
|
47
|
+
"security",
|
|
48
|
+
"safe-auto",
|
|
49
|
+
"approval-workflow",
|
|
50
|
+
"auto-review",
|
|
51
|
+
"audit"
|
|
44
52
|
],
|
|
45
53
|
"dsh": {
|
|
46
54
|
"bundle": {
|
|
@@ -58,7 +66,7 @@
|
|
|
58
66
|
}
|
|
59
67
|
},
|
|
60
68
|
"dependencies": {
|
|
61
|
-
"@klarkxy/dsh-plugin-kit": "0.
|
|
69
|
+
"@klarkxy/dsh-plugin-kit": "0.7.0",
|
|
62
70
|
"zod": "^4.1.5"
|
|
63
71
|
},
|
|
64
72
|
"peerDependencies": {
|
|
@@ -78,18 +86,18 @@
|
|
|
78
86
|
}
|
|
79
87
|
},
|
|
80
88
|
"devDependencies": {
|
|
81
|
-
"@deepseek-ai/cordis": "4.0
|
|
82
|
-
"@deepseek-ai/dsh-client-ui-primitives": "0.2.
|
|
83
|
-
"@deepseek-ai/dsh-storage-domain": "0.2.
|
|
84
|
-
"@klarkxy/dsh-model-route": "0.
|
|
89
|
+
"@deepseek-ai/cordis": "4.1.0",
|
|
90
|
+
"@deepseek-ai/dsh-client-ui-primitives": "0.2.2-rc.1",
|
|
91
|
+
"@deepseek-ai/dsh-storage-domain": "0.2.2-rc.1",
|
|
92
|
+
"@klarkxy/dsh-model-route": "0.3.0",
|
|
85
93
|
"jsdom": "29.1.1",
|
|
86
|
-
"@deepseek-ai/dsh-working-directory": "0.2.
|
|
94
|
+
"@deepseek-ai/dsh-working-directory": "0.2.2-rc.1"
|
|
87
95
|
},
|
|
88
96
|
"publishConfig": {
|
|
89
97
|
"access": "public",
|
|
90
98
|
"registry": "https://registry.npmjs.org/"
|
|
91
99
|
},
|
|
92
100
|
"dshRelease": {
|
|
93
|
-
"contentHash": "
|
|
101
|
+
"contentHash": "34f3f590257f31d65eacf597226d49d7dd3c89c9a65996a225be37bc556bdb2f"
|
|
94
102
|
}
|
|
95
103
|
}
|
package/src/index.js
CHANGED
|
@@ -197,7 +197,12 @@ export function apply(ctx, raw = {}) {
|
|
|
197
197
|
await Promise.allSettled([...pendingWrites]);
|
|
198
198
|
try { await (await ready).close(); } catch { /* mount failure remains visible to history callers */ }
|
|
199
199
|
});
|
|
200
|
-
|
|
200
|
+
if (!base.enabled) return;
|
|
201
|
+
|
|
202
|
+
// The query tool and the page share the same read-only store operations. It
|
|
203
|
+
// registers only while automatic approval is enabled: a disabled profile
|
|
204
|
+
// keeps the runtime service (the UI page reads history through it) without
|
|
205
|
+
// exposing the model tool.
|
|
201
206
|
ctx.inject?.(['tools'], scope => scope.effect(() => scope.tools.register({
|
|
202
207
|
name: 'safe_auto_history',
|
|
203
208
|
description: 'Query persisted Safe Auto reviews for the current session. List returns summaries and a pagination cursor; get returns the full redacted audit record, including review input and rationale. The Safe Auto group under Settings → Session Management provides profile-wide audit history.',
|
|
@@ -233,7 +238,6 @@ export function apply(ctx, raw = {}) {
|
|
|
233
238
|
return page;
|
|
234
239
|
},
|
|
235
240
|
})));
|
|
236
|
-
if (!base.enabled) return;
|
|
237
241
|
|
|
238
242
|
let llm;
|
|
239
243
|
// Optional dependency: losing the model service fails reviews closed instead of unloading the plugin.
|