@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 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",
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: choice.model
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 root one, handing the keyboard back to its cell. */
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("root");
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("root");
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
- return () => document.removeEventListener("mousedown", closeOutside);
528
- }, [open, onOpenChange]);
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 (open && pane === "model" && activeModelIndex >= 0) itemRefs.current[leading.length + activeModelIndex]?.scrollIntoView({ block: "nearest" });
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
- open,
562
- pane,
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 !== "root") back(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
- if (event.relatedTarget instanceof Node && (rootRef.current?.contains(event.relatedTarget) || menuRef.current?.contains(event.relatedTarget))) return;
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
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
2445
+ renderModelPicker(renderFactorySlot, {
2049
2446
  locale: language,
2050
- open: menuOpen && !locked,
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": "A reviewer model answers approval requests; only calls that pass go ahead, one call at a time."
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "meta": {
3
- "title": "安全自动",
4
- "description": "需要审批的请求交给审核模型判断,通过才放行这一次调用。"
3
+ "title": "Safe Auto",
4
+ "description": "新增「安全自动」审批档位:需要审批的请求交给审核模型判断,通过才放行,并记录完整的审核历史。"
5
5
  }
6
6
  }
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@klarkxy/dsh-safe-auto",
3
- "version": "0.5.3",
4
- "description": "Safe Auto: a reviewer model answers native approval requests, Codex auto-review style; only a pass approves that one call.",
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.6.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.5",
82
- "@deepseek-ai/dsh-client-ui-primitives": "0.2.1-rc.1",
83
- "@deepseek-ai/dsh-storage-domain": "0.2.1-rc.1",
84
- "@klarkxy/dsh-model-route": "0.2.6",
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.1-rc.1"
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": "cfa944cf1f16125ee893d0e6c1ea8fda43fdc594733b3b4478f2f9b636e1444f"
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
- // The query tool and the page share the same read-only store operations.
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.