@klarkxy/dsh-git-commit 0.3.3 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -96,6 +96,7 @@ function parseModelMenuChoices(value, bound) {
96
96
  provider,
97
97
  model,
98
98
  label: `${providerName} / ${name}`,
99
+ providerLabel: providerName,
99
100
  efforts
100
101
  });
101
102
  }
@@ -142,11 +143,11 @@ function modelMenuShortName(choice) {
142
143
  /** Group catalog choices by provider and filter them by a case-insensitive query. */
143
144
  function groupModelMenuChoices(choices, query) {
144
145
  const needle = query.trim().toLowerCase();
145
- const matched = needle ? choices.filter((choice) => choice.label.toLowerCase().includes(needle) || choice.model.toLowerCase().includes(needle) || choice.provider.toLowerCase().includes(needle)) : choices;
146
+ const matched = needle ? choices.filter((choice) => choice.label.toLowerCase().includes(needle) || choice.model.toLowerCase().includes(needle) || choice.provider.toLowerCase().includes(needle) || (choice.providerLabel?.toLowerCase().includes(needle) ?? false)) : choices;
146
147
  const groups = [];
147
148
  for (const choice of matched) {
148
149
  const sep = choice.label.indexOf(" / ");
149
- const name = sep > 0 ? choice.label.slice(0, sep) : choice.provider;
150
+ const name = choice.providerLabel ?? (sep > 0 ? choice.label.slice(0, sep) : choice.provider);
150
151
  const last = groups.at(-1);
151
152
  if (last && last.provider === choice.provider) last.items.push(choice);
152
153
  else groups.push({
@@ -373,7 +374,7 @@ function ModelMenu(props) {
373
374
  empty: "No matching models",
374
375
  clear: "Clear search"
375
376
  };
376
- 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;
377
+ 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;
377
378
  const rootRef = (0, react.useRef)(null);
378
379
  const openRef = (0, react.useRef)(open);
379
380
  openRef.current = open;
@@ -388,9 +389,10 @@ function ModelMenu(props) {
388
389
  const [pane, setPane] = (0, react.useState)("root");
389
390
  const [query, setQuery] = (0, react.useState)("");
390
391
  const [highlightedIndex, setHighlightedIndex] = (0, react.useState)(null);
392
+ const [keyNav, setKeyNav] = (0, react.useState)(0);
391
393
  const [menuPos, setMenuPos] = (0, react.useState)(null);
392
394
  const id = (0, react.useId)();
393
- const showSearch = choices.length > 4;
395
+ const showSearch = alwaysShowSearch || choices.length > 4;
394
396
  const effortRows = (0, react.useMemo)(() => [{
395
397
  id: "",
396
398
  name: defaultEffortLabel
@@ -407,7 +409,12 @@ function ModelMenu(props) {
407
409
  items: (0, _deepseek_ai_dsh_client_ui_primitives.rankByName)(group.items.map((choice) => ({
408
410
  choice,
409
411
  name: modelMenuShortName(choice),
410
- label: choice.model
412
+ label: [
413
+ choice.provider,
414
+ choice.providerLabel,
415
+ choice.model,
416
+ choice.label
417
+ ].filter(Boolean).join(" ")
411
418
  })), showSearch ? query.trim() : "")
412
419
  })).filter((group) => group.items.length > 0), [
413
420
  choices,
@@ -439,10 +446,12 @@ function ModelMenu(props) {
439
446
  paneFocus.current = "drill";
440
447
  setPane(next);
441
448
  };
442
- /** Leave a drilled pane for the root one, handing the keyboard back to its cell. */
449
+ /** Leave a drilled pane for the entry pane, handing the keyboard back to it.
450
+ * A root entry refocuses the cell that opened the pane; a search-first
451
+ * (model) entry refocuses that pane's own intent. */
443
452
  const back = (from) => {
444
- paneFocus.current = from;
445
- setPane("root");
453
+ paneFocus.current = initialPane === "root" ? from : "drill";
454
+ setPane(initialPane);
446
455
  };
447
456
  const moveFocus = (offset) => {
448
457
  const items = itemRefs.current.filter((item) => item !== null && !item.disabled);
@@ -482,10 +491,18 @@ function ModelMenu(props) {
482
491
  }, [requestedOpen, open]);
483
492
  (0, react.useEffect)(() => {
484
493
  if (!open) return;
485
- setPane("root");
494
+ setPane(initialPane);
495
+ if (initialPane === "model") paneFocus.current = "drill";
486
496
  setQuery("");
487
497
  setHighlightedIndex(null);
488
- }, [open]);
498
+ }, [open, initialPane]);
499
+ (0, react.useEffect)(() => {
500
+ if (disabled && open && closeOnDisable) changeOpen(false);
501
+ }, [
502
+ disabled,
503
+ open,
504
+ closeOnDisable
505
+ ]);
489
506
  (0, react.useEffect)(() => {
490
507
  if (!open) return;
491
508
  const closeOutside = (event) => {
@@ -493,9 +510,25 @@ function ModelMenu(props) {
493
510
  if (rootRef.current?.contains(target) || menuRef.current?.contains(target)) return;
494
511
  changeOpen(false);
495
512
  };
513
+ const escapeOutside = (event) => {
514
+ if (event.key !== "Escape" || event.defaultPrevented || event.isComposing) return;
515
+ if (!openRef.current) return;
516
+ event.preventDefault();
517
+ if (pane !== initialPane) back(pane);
518
+ else close(true);
519
+ };
496
520
  document.addEventListener("mousedown", closeOutside);
497
- return () => document.removeEventListener("mousedown", closeOutside);
498
- }, [open, onOpenChange]);
521
+ document.addEventListener("keydown", escapeOutside);
522
+ return () => {
523
+ document.removeEventListener("mousedown", closeOutside);
524
+ document.removeEventListener("keydown", escapeOutside);
525
+ };
526
+ }, [
527
+ open,
528
+ onOpenChange,
529
+ pane,
530
+ initialPane
531
+ ]);
499
532
  (0, react.useEffect)(() => {
500
533
  const intent = paneFocus.current;
501
534
  if (!open || menuPos === null || intent === null) return;
@@ -525,13 +558,21 @@ function ModelMenu(props) {
525
558
  pane,
526
559
  filteredGroups
527
560
  ]);
561
+ const modelPaneOpen = open && pane === "model";
562
+ const scrollSignal = !modelPaneOpen ? "closed" : keyNav === 0 ? "open" : `key:${keyNav}`;
563
+ const scrollSignalRef = (0, react.useRef)(null);
528
564
  (0, react.useLayoutEffect)(() => {
529
- if (open && pane === "model" && activeModelIndex >= 0) itemRefs.current[leading.length + activeModelIndex]?.scrollIntoView({ block: "nearest" });
565
+ if (!modelPaneOpen || activeModelIndex < 0) {
566
+ if (!modelPaneOpen) scrollSignalRef.current = null;
567
+ return;
568
+ }
569
+ if (scrollSignalRef.current === scrollSignal) return;
570
+ scrollSignalRef.current = scrollSignal;
571
+ itemRefs.current[leading.length + activeModelIndex]?.scrollIntoView({ block: "nearest" });
530
572
  }, [
531
- open,
532
- pane,
573
+ modelPaneOpen,
574
+ scrollSignal,
533
575
  activeModelIndex,
534
- visibleModels,
535
576
  leading.length
536
577
  ]);
537
578
  (0, react.useLayoutEffect)(() => {
@@ -546,7 +587,7 @@ function ModelMenu(props) {
546
587
  let x = rect.right - lw, y = rect.top - 8 - lh;
547
588
  if (lw > 0) x = Math.min(Math.max(x, margin), window.innerWidth - lw - margin);
548
589
  if (lh > 0) y = Math.min(Math.max(y, margin), window.innerHeight - lh - margin);
549
- setMenuPos({
590
+ setMenuPos((prev) => prev && prev.left === x && prev.top === y ? prev : {
550
591
  left: x,
551
592
  top: y
552
593
  });
@@ -566,6 +607,13 @@ function ModelMenu(props) {
566
607
  ]);
567
608
  function onKeyDown(event) {
568
609
  if (event.defaultPrevented || event.nativeEvent.isComposing) return;
610
+ if (event.key === "Escape" && open) {
611
+ event.preventDefault();
612
+ if (pane !== initialPane) back(pane);
613
+ else close(true);
614
+ return;
615
+ }
616
+ if (disabled) return;
569
617
  if (!open && [
570
618
  "Enter",
571
619
  " ",
@@ -579,17 +627,12 @@ function ModelMenu(props) {
579
627
  requestOpen();
580
628
  return;
581
629
  }
582
- if (event.key === "Escape" && open) {
583
- event.preventDefault();
584
- if (pane !== "root") back(pane);
585
- else close(true);
586
- return;
587
- }
588
630
  if (!open) return;
589
631
  if (pane === "model" && showSearch && (event.key === "ArrowDown" || event.key === "ArrowUp")) {
590
632
  event.preventDefault();
591
633
  if (visibleModels.length > 0) {
592
634
  setHighlightedIndex((activeModelIndex + (event.key === "ArrowDown" ? 1 : -1) + visibleModels.length) % visibleModels.length);
635
+ setKeyNav((n) => n + 1);
593
636
  searchRef.current?.focus();
594
637
  }
595
638
  return;
@@ -598,19 +641,19 @@ function ModelMenu(props) {
598
641
  if (event.key === "Tab" && visibleModels.length === 0) return;
599
642
  event.preventDefault();
600
643
  const highlighted = visibleModels[activeModelIndex];
601
- if (highlighted) {
644
+ if (highlighted && !busy) {
602
645
  onPick({
603
646
  provider: highlighted.provider,
604
647
  model: highlighted.model
605
648
  });
606
- close(true);
649
+ if (closeOnPick) close(true);
607
650
  }
608
651
  return;
609
652
  }
610
653
  if (event.key === "Tab") {
611
654
  if (event.shiftKey) {
612
655
  event.preventDefault();
613
- if (pane !== "root") back(pane);
656
+ if (pane !== initialPane) back(pane);
614
657
  else close(true);
615
658
  return;
616
659
  }
@@ -634,7 +677,10 @@ function ModelMenu(props) {
634
677
  }
635
678
  }
636
679
  function onBlur(event) {
637
- if (event.relatedTarget instanceof Node && (rootRef.current?.contains(event.relatedTarget) || menuRef.current?.contains(event.relatedTarget))) return;
680
+ const related = event.relatedTarget;
681
+ if (related instanceof Node && (rootRef.current?.contains(related) || menuRef.current?.contains(related) || !related.isConnected)) return;
682
+ if (!closeOnPick && !(related instanceof Node)) return;
683
+ if (busy && related == null) return;
638
684
  if (open) changeOpen(false);
639
685
  }
640
686
  itemRefs.current = [];
@@ -655,11 +701,16 @@ function ModelMenu(props) {
655
701
  "aria-label": modelLabel,
656
702
  lang: language,
657
703
  children: [
704
+ menuHeader ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
705
+ className: "dsh-model-menu-header",
706
+ children: menuHeader
707
+ }) : null,
658
708
  pane === "root" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
659
709
  ref: itemRef(),
660
710
  type: "button",
661
711
  role: "menuitem",
662
712
  className: "dsh-model-menu-cell",
713
+ disabled,
663
714
  onClick: () => drill("model"),
664
715
  children: [
665
716
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -672,11 +723,12 @@ function ModelMenu(props) {
672
723
  }),
673
724
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutlineRegular, { className: "dsh-model-menu-cellChevron" })
674
725
  ]
675
- }), efforts.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
726
+ }), efforts.length > 0 && !reasoningSlider ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
676
727
  ref: itemRef(),
677
728
  type: "button",
678
729
  role: "menuitem",
679
730
  className: "dsh-model-menu-cell",
731
+ disabled,
680
732
  onClick: () => drill("effort"),
681
733
  children: [
682
734
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -695,6 +747,7 @@ function ModelMenu(props) {
695
747
  className: "dsh-model-menu-searchRow",
696
748
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
697
749
  ref: searchRef,
750
+ disabled,
698
751
  className: `dsh-model-menu-search${query !== "" ? " dsh-model-menu-searchWithQuery" : ""}`,
699
752
  type: "text",
700
753
  role: "searchbox",
@@ -711,6 +764,7 @@ function ModelMenu(props) {
711
764
  type: "button",
712
765
  className: "dsh-model-menu-searchClear",
713
766
  "aria-label": clearSearchLabel,
767
+ disabled,
714
768
  onClick: () => {
715
769
  setQuery("");
716
770
  setHighlightedIndex(null);
@@ -733,10 +787,12 @@ function ModelMenu(props) {
733
787
  type: "button",
734
788
  role: "menuitemradio",
735
789
  "aria-checked": item.selected === true,
790
+ disabled: disabled || busy,
736
791
  className: `dsh-model-menu-option${item.selected === true ? " dsh-model-menu-selected" : ""}`,
737
792
  onClick: () => {
793
+ if (disabled || busy) return;
738
794
  onPickLeading?.(item.id);
739
- close(true);
795
+ if (closeOnPick) close(true);
740
796
  },
741
797
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
742
798
  className: "dsh-model-menu-optionCopy",
@@ -762,6 +818,7 @@ function ModelMenu(props) {
762
818
  "aria-checked": checked,
763
819
  id: `${id}-model-${index}`,
764
820
  tabIndex: showSearch ? -1 : 0,
821
+ disabled: disabled || busy,
765
822
  onFocus: () => setHighlightedIndex(index),
766
823
  "data-highlighted": index === activeModelIndex ? "" : void 0,
767
824
  className: `dsh-model-menu-option dsh-model-menu-modelOption${checked ? " dsh-model-menu-selected" : ""}${index === activeModelIndex ? " dsh-model-menu-optionActive" : ""}`,
@@ -771,11 +828,12 @@ function ModelMenu(props) {
771
828
  },
772
829
  title: choice.label,
773
830
  onClick: () => {
831
+ if (disabled || busy) return;
774
832
  onPick({
775
833
  provider: choice.provider,
776
834
  model: choice.model
777
835
  });
778
- close(true);
836
+ if (closeOnPick) close(true);
779
837
  },
780
838
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
781
839
  className: "dsh-model-menu-optionCopy",
@@ -791,6 +849,14 @@ function ModelMenu(props) {
791
849
  })
792
850
  }, group.provider))]
793
851
  }),
852
+ reasoningSlider && efforts.length > 0 && filteredGroups.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EffortSlider, {
853
+ label: effortLabel,
854
+ rows: effortRows,
855
+ selected: selectedEffort ?? "",
856
+ disabled,
857
+ busy,
858
+ onPick: (value) => onPickEffort?.(value)
859
+ }) : null,
794
860
  filteredGroups.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
795
861
  className: "dsh-model-menu-empty",
796
862
  role: "status",
@@ -804,10 +870,12 @@ function ModelMenu(props) {
804
870
  type: "button",
805
871
  role: "menuitemradio",
806
872
  "aria-checked": checked,
873
+ disabled: disabled || busy,
807
874
  className: `dsh-model-menu-option${checked ? " dsh-model-menu-selected" : ""}`,
808
875
  onClick: () => {
876
+ if (disabled || busy) return;
809
877
  onPickEffort?.(row.id || void 0);
810
- close(true);
878
+ if (closeOnPick) close(true);
811
879
  },
812
880
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
813
881
  className: "dsh-model-menu-optionCopy",
@@ -826,7 +894,8 @@ function ModelMenu(props) {
826
894
  const trigger = (0, react.isValidElement)(anchor) ? (0, react.cloneElement)(anchor, {
827
895
  "aria-haspopup": "menu",
828
896
  "aria-expanded": open,
829
- "aria-controls": open ? `${id}-menu` : void 0
897
+ "aria-controls": open ? `${id}-menu` : void 0,
898
+ "aria-disabled": disabled || void 0
830
899
  }) : anchor;
831
900
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
832
901
  ref: rootRef,
@@ -834,7 +903,7 @@ function ModelMenu(props) {
834
903
  onKeyDown,
835
904
  onBlur,
836
905
  onClick: (event) => {
837
- if (event.defaultPrevented) return;
906
+ if (disabled || event.defaultPrevented) return;
838
907
  const button = event.target instanceof Element ? event.target.closest("button") : null;
839
908
  if (!(button instanceof HTMLButtonElement) || button.disabled || !rootRef.current?.contains(button)) return;
840
909
  requestOpen();
@@ -846,6 +915,102 @@ function ModelMenu(props) {
846
915
  });
847
916
  }
848
917
  /**
918
+ * Discrete reasoning-effort slider shown under the model list in enhanced mode.
919
+ *
920
+ * A native range input (`step=1`) over exactly the rows the caller supplies —
921
+ * the default row plus each effort the selected model advertises — so it can
922
+ * never invent a level. It previews locally while dragging or arrowing and
923
+ * commits only when the gesture ends (pointer up, key up, or blur), so a drag
924
+ * does not write on every step. The menu stays open, so adjusting the effort
925
+ * never loses the model selection or the search query.
926
+ *
927
+ * The input itself stays the entire interaction and accessibility surface —
928
+ * keyboard, ARIA, disabled and the native step snapping all come from it —
929
+ * while the track, gradient fill and thumb below are decorative layers painted
930
+ * from the `--_p` percentage the preview position sets on the rail. Value
931
+ * changes only update that custom property, so the CSS transitions animate
932
+ * between levels without remounting nodes or restarting an animation.
933
+ */
934
+ function EffortSlider(props) {
935
+ const { label, rows, selected, disabled = false, busy = false, onPick } = props;
936
+ const active = Math.max(0, rows.findIndex((row) => row.id === selected));
937
+ const [position, setPosition] = (0, react.useState)(active);
938
+ const positionRef = (0, react.useRef)(position);
939
+ (0, react.useEffect)(() => {
940
+ positionRef.current = active;
941
+ setPosition(active);
942
+ }, [active]);
943
+ const preview = (next) => {
944
+ positionRef.current = next;
945
+ setPosition(next);
946
+ };
947
+ const commit = () => {
948
+ if (disabled || busy) return;
949
+ const row = rows[positionRef.current];
950
+ if (row !== void 0) onPick(row.id === "" ? void 0 : row.id);
951
+ };
952
+ const max = Math.max(0, rows.length - 1);
953
+ const percent = max > 0 ? Math.round(position / max * 100) : 0;
954
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
955
+ className: "dsh-model-menu-sliderGroup",
956
+ role: "group",
957
+ "aria-label": label,
958
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
959
+ className: "dsh-model-menu-sliderHead",
960
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
961
+ className: "dsh-model-menu-sliderTitle",
962
+ children: label
963
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
964
+ className: "dsh-model-menu-sliderValue",
965
+ children: rows[position]?.name
966
+ })]
967
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
968
+ className: "dsh-model-menu-sliderRail",
969
+ style: { "--_p": percent },
970
+ children: [
971
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
972
+ type: "range",
973
+ className: "dsh-model-menu-sliderInput",
974
+ min: 0,
975
+ max,
976
+ step: 1,
977
+ value: position,
978
+ disabled,
979
+ "aria-label": label,
980
+ "aria-disabled": disabled || busy || void 0,
981
+ "aria-valuetext": rows[position]?.name,
982
+ onChange: (event) => {
983
+ const value = Number(event.target.value);
984
+ if (Number.isFinite(value)) preview(value);
985
+ },
986
+ onPointerUp: commit,
987
+ onKeyUp: commit,
988
+ onBlur: commit
989
+ }),
990
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
991
+ className: "dsh-model-menu-sliderTrack",
992
+ "aria-hidden": "true",
993
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
994
+ className: "dsh-model-menu-sliderFill",
995
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dsh-model-menu-sliderFillBar" })
996
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
997
+ className: "dsh-model-menu-sliderNodes",
998
+ children: rows.map((row, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
999
+ "data-covered": index <= position ? "" : void 0,
1000
+ className: "dsh-model-menu-sliderNode",
1001
+ style: { "--_n": max > 0 ? Math.round(index / max * 100) : 0 }
1002
+ }, row.id || "__default"))
1003
+ })]
1004
+ }),
1005
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1006
+ className: "dsh-model-menu-sliderThumb",
1007
+ "aria-hidden": "true"
1008
+ })
1009
+ ]
1010
+ })]
1011
+ });
1012
+ }
1013
+ /**
849
1014
  * ModelMenu styles, transcribed from the official ModelSelect.module.css (the
850
1015
  * published dsh-client-ui-model-selection bundle): card capped at
851
1016
  * min(360px, 100vh - 96px), 34px option cells at 13/18 with 5px 7px padding,
@@ -904,6 +1069,7 @@ const modelMenuCss = `
904
1069
  overflow: hidden;
905
1070
  }
906
1071
  .dsh-model-menu-status, .dsh-model-menu-empty { color: var(--dsw-alias-label-tertiary); padding: 8px; font-size: 12px; line-height: 18px; }
1072
+ .dsh-model-menu-header { flex: none; padding: 2px 2px 4px; }
907
1073
  .dsh-model-menu-searchRow { flex-shrink: 0; margin: 2px 0 3px; position: relative; }
908
1074
  .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; }
909
1075
  .dsh-model-menu-searchRow .dsh-model-menu-search:focus-within { border-color: transparent; }
@@ -982,7 +1148,235 @@ const modelMenuCss = `
982
1148
  .dsh-model-menu-cellLabel { white-space: nowrap; flex: none; }
983
1149
  .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; }
984
1150
  .dsh-model-menu-cellChevron { width: 12px; height: 12px; color: var(--dsw-alias-menu-icon); flex: none; }
1151
+ /* Discrete reasoning-effort slider under the model list (enhanced mode).
1152
+ * The painted control is layered over a fully functional native range input:
1153
+ * the input stays on top, transparent, and owns pointer/keyboard/ARIA, while
1154
+ * the decorative track, fill and thumb render beneath it at pointer-events:
1155
+ * none. Position lives in one custom property (--_p, 0–100) on the rail, so
1156
+ * the only animated work a level change triggers is a clip-path/left
1157
+ * transition — no keyframes, no element churn. */
1158
+ .dsh-model-menu-sliderGroup { border-top: 0.5px solid var(--dsw-alias-border-l2); flex: none; margin-top: 4px; padding: 6px 8px 8px; }
1159
+ .dsh-model-menu-sliderHead { align-items: baseline; justify-content: space-between; gap: 8px; margin: 0 2px 4px; display: flex; }
1160
+ .dsh-model-menu-sliderTitle { color: var(--dsw-alias-label-tertiary); font-size: 12px; line-height: 18px; }
1161
+ .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); }
1162
+ .dsh-model-menu-sliderRail { position: relative; direction: ltr; height: 38px; margin-top: 2px; }
1163
+ .dsh-model-menu-sliderRail:has(:disabled) { opacity: 0.45; }
1164
+ .dsh-model-menu-sliderInput {
1165
+ appearance: none;
1166
+ -webkit-appearance: none;
1167
+ box-sizing: border-box;
1168
+ border: 0;
1169
+ cursor: pointer;
1170
+ direction: ltr;
1171
+ opacity: 0;
1172
+ margin: 0;
1173
+ padding: 0;
1174
+ width: 100%;
1175
+ height: 100%;
1176
+ position: absolute;
1177
+ inset: 0;
1178
+ z-index: 1;
1179
+ touch-action: pan-y;
1180
+ }
1181
+ .dsh-model-menu-sliderInput:disabled { cursor: default; }
1182
+ /* The transparent input must keep the painted thumb's geometry: an
1183
+ * appearance:none webkit thumb defaults to 16px, which would shift the
1184
+ * end-point positions 7px off the decorative 30px disc and misplace the
1185
+ * native drag hit target. Track gets the full rail height so the whole band
1186
+ * maps to values identically. */
1187
+ .dsh-model-menu-sliderInput::-webkit-slider-runnable-track { width: 100%; height: 100%; }
1188
+ .dsh-model-menu-sliderInput::-webkit-slider-thumb { appearance: none; -webkit-appearance: none; width: 30px; height: 30px; margin: 0; }
1189
+ .dsh-model-menu-sliderInput::-moz-range-track { width: 100%; height: 100%; }
1190
+ .dsh-model-menu-sliderInput::-moz-range-thumb { width: 30px; height: 30px; margin: 0; border: 0; }
1191
+ .dsh-model-menu-sliderTrack {
1192
+ pointer-events: none;
1193
+ border-radius: 999px;
1194
+ width: 100%;
1195
+ height: 26px;
1196
+ background: var(--dsw-alias-interactive-bg-hover);
1197
+ box-shadow: inset 0 0 0 0.5px var(--dsw-alias-border-l2);
1198
+ position: absolute;
1199
+ inset-inline: 0;
1200
+ top: 50%;
1201
+ transform: translateY(-50%);
1202
+ /* No overflow:hidden — it would clip the fill's drop-shadow glow; the bar's
1203
+ * own clip-path already supplies the rounded reveal shape. */
1204
+ }
1205
+ /* The accent ramp paints the full track width once and is revealed by an
1206
+ * animated clip-path, so raising the effort unwraps more of the spectrum
1207
+ * instead of squashing the same gradient into a narrower bar. The glow lives
1208
+ * on the un-clipped parent: a filter on the clipped element itself would be
1209
+ * clipped away with it. */
1210
+ .dsh-model-menu-sliderFill {
1211
+ display: block;
1212
+ height: 100%;
1213
+ filter: drop-shadow(0 0 5px var(--dsw-alias-state-business-primary));
1214
+ transition: filter var(--ds-transition-duration) var(--ds-ease-in-out);
1215
+ }
1216
+ .dsh-model-menu-sliderInput:hover:not(:disabled) ~ .dsh-model-menu-sliderTrack .dsh-model-menu-sliderFill,
1217
+ .dsh-model-menu-sliderInput:active:not(:disabled) ~ .dsh-model-menu-sliderTrack .dsh-model-menu-sliderFill {
1218
+ filter: drop-shadow(0 0 9px var(--dsw-alias-state-business-primary));
1219
+ }
1220
+ /* The fill stops at the thumb's center, not at the track edge — at a 30px
1221
+ * thumb the end stops sit 15px in from each side, matching the node dots. */
1222
+ .dsh-model-menu-sliderFillBar {
1223
+ display: block;
1224
+ height: 100%;
1225
+ background: linear-gradient(90deg,
1226
+ var(--dsw-alias-state-business-primary),
1227
+ var(--dsw-alias-state-success-primary),
1228
+ var(--dsw-alias-state-warn-primary),
1229
+ var(--dsw-alias-state-error-primary));
1230
+ clip-path: inset(0 calc(100% - 15px - (100% - 30px) * var(--_p, 0) / 100) 0 0 round 999px);
1231
+ transition: clip-path var(--ds-transition-duration) var(--ds-ease-in-out);
1232
+ }
1233
+ /* Stop dots: one per declared row (default + every advertised effort), evenly
1234
+ * spread across the band between the two thumb-center extremes (inset 15px,
1235
+ * i.e. half the 30px thumb). --_n is 0-100 along that band. A covered dot
1236
+ * reads as a pale pin through the gradient; an uncovered one stays a neutral
1237
+ * notch on the empty track. */
1238
+ .dsh-model-menu-sliderNodes {
1239
+ pointer-events: none;
1240
+ display: block;
1241
+ position: absolute;
1242
+ inset-inline: 15px;
1243
+ inset-block: 0;
1244
+ }
1245
+ .dsh-model-menu-sliderNode {
1246
+ pointer-events: none;
1247
+ box-sizing: border-box;
1248
+ width: 4px;
1249
+ height: 4px;
1250
+ border-radius: 50%;
1251
+ background: color-mix(in srgb, var(--dsw-alias-label-secondary) 35%, transparent);
1252
+ position: absolute;
1253
+ left: calc(var(--_n, 0) * 1%);
1254
+ top: 50%;
1255
+ transform: translate(-50%, -50%);
1256
+ transition: background-color var(--ds-transition-duration) var(--ds-ease-in-out),
1257
+ transform var(--ds-transition-duration) var(--ds-ease-in-out);
1258
+ }
1259
+ .dsh-model-menu-sliderNode[data-covered] { background: color-mix(in srgb, var(--dsw-alias-label-primary) 60%, transparent); }
1260
+ .dsh-model-menu-sliderThumb {
1261
+ pointer-events: none;
1262
+ box-sizing: border-box;
1263
+ border-radius: 50%;
1264
+ width: 30px;
1265
+ height: 30px;
1266
+ background: var(--dsw-alias-button-primary-fill);
1267
+ border: 0.5px solid var(--dsw-alias-border-l4);
1268
+ box-shadow: 0 0 0 3px var(--dsw-alias-interactive-bg-hover), var(--dsw-elevation-soft);
1269
+ position: absolute;
1270
+ left: calc((100% - 30px) * var(--_p, 0) / 100);
1271
+ top: 50%;
1272
+ transform: translateY(-50%) scale(0.92);
1273
+ transition: left var(--ds-transition-duration) var(--ds-ease-in-out),
1274
+ transform var(--ds-transition-duration) var(--ds-ease-in-out),
1275
+ box-shadow var(--ds-transition-duration) var(--ds-ease-in-out);
1276
+ }
1277
+ .dsh-model-menu-sliderInput:hover:not(:disabled) ~ .dsh-model-menu-sliderThumb { transform: translateY(-50%) scale(1); }
1278
+ .dsh-model-menu-sliderInput:active:not(:disabled) ~ .dsh-model-menu-sliderThumb {
1279
+ transform: translateY(-50%) scale(1.1);
1280
+ box-shadow: 0 0 0 4px var(--dsw-alias-interactive-bg-hover), 0 0 12px var(--dsw-alias-state-business-primary);
1281
+ }
1282
+ .dsh-model-menu-sliderInput:focus-visible ~ .dsh-model-menu-sliderThumb {
1283
+ box-shadow: 0 0 0 3px var(--dsw-alias-interactive-bg-hover),
1284
+ 0 0 0 calc(3px + var(--dsw-focus-ring-width)) var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
1285
+ }
1286
+ @media (prefers-reduced-motion: reduce) {
1287
+ .dsh-model-menu-sliderFill, .dsh-model-menu-sliderFillBar, .dsh-model-menu-sliderThumb,
1288
+ .dsh-model-menu-sliderNode, .dsh-model-menu-sliderValue { transition: none; }
1289
+ }
985
1290
  `;
1291
+ //#endregion
1292
+ //#region ../dsh-plugin-kit/src/model-picker.tsx
1293
+ /**
1294
+ * Optional shared model picker (`klarkxy.model-picker` Factory).
1295
+ *
1296
+ * `@klarkxy/dsh-model-picker`, when installed, registers one root-scoped
1297
+ * Component Factory under `MODEL_PICKER_FACTORY`: an enhanced model menu with
1298
+ * a searchable provider-grouped list and a reasoning-effort slider, built on
1299
+ * the same ModelMenu recipe. Consumers keep their own ModelMenu as the
1300
+ * `fallback`; when no definition is registered the renderer resolves that
1301
+ * fallback verbatim, so the shared picker is strictly opt-in and every saved
1302
+ * value keeps its existing write path.
1303
+ *
1304
+ * `renderFactorySlot` is a framework-composed prop, never a context: the
1305
+ * component a plugin registers into any slot receives it automatically, but
1306
+ * the plugin must pass it down to whichever child renders the menu (or to the
1307
+ * render callback handed to registerSessionSettingsItem).
1308
+ *
1309
+ * The Factory manages its own open state; a consumer's `open`/`onOpenChange`
1310
+ * pair belongs to the fallback menu only and must not be fed to both.
1311
+ */
1312
+ /**
1313
+ * The trigger every consumer used to hand its own ModelMenu, rebuilt as the
1314
+ * factory's `anchor` local component: identical classes and chevron, the menu
1315
+ * state coming from the occurrence's props instead of local open state.
1316
+ *
1317
+ * `ariaLabel` and `title` are the whole accessible name and tooltip (the
1318
+ * caller formats "field: current route" itself because the factory's resolved
1319
+ * caption may intentionally be a short name). The component identity stays
1320
+ * put across label changes — a new function would unmount the trigger, and
1321
+ * its cleanup would close a menu that `closeOnPick={false}` is keeping open.
1322
+ * The latest label, title, and `onClose` are read from refs.
1323
+ */
1324
+ function useModelMenuAnchor(ariaLabel, title, onClose) {
1325
+ const closeRef = (0, react.useRef)(onClose);
1326
+ const labelRef = (0, react.useRef)(ariaLabel);
1327
+ const titleRef = (0, react.useRef)(title);
1328
+ closeRef.current = onClose;
1329
+ labelRef.current = ariaLabel;
1330
+ titleRef.current = title;
1331
+ return (0, react.useMemo)(() => {
1332
+ const ModelPickerTrigger = (props) => {
1333
+ (0, react.useEffect)(() => () => closeRef.current?.(), []);
1334
+ const record = props;
1335
+ const injected = {};
1336
+ for (const [key, value] of Object.entries(record)) if (key.startsWith("aria-")) injected[key] = value;
1337
+ const extraClass = typeof record.className === "string" ? ` ${record.className}` : "";
1338
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1339
+ type: "button",
1340
+ className: `dsh-model-menu-trigger${extraClass}`,
1341
+ disabled: props.disabled,
1342
+ "aria-haspopup": "menu",
1343
+ "aria-expanded": props.open,
1344
+ ...injected,
1345
+ "aria-label": labelRef.current,
1346
+ title: titleRef.current,
1347
+ children: [
1348
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1349
+ className: "dsh-model-menu-triggerLabel",
1350
+ children: props.label
1351
+ }),
1352
+ props.effortLabel ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1353
+ className: "dsh-model-menu-triggerEffort",
1354
+ children: props.effortLabel
1355
+ }) : null,
1356
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${props.open ? " dsh-model-menu-chevronOpen" : ""}` })
1357
+ ]
1358
+ });
1359
+ };
1360
+ return ModelPickerTrigger;
1361
+ }, []);
1362
+ }
1363
+ /**
1364
+ * Render the shared model picker while it is registered; `options.fallback`
1365
+ * whenever it is not (plugin absent, disabled, or unloaded).
1366
+ *
1367
+ * `options.anchor` is optional: the factory ships a default trigger, so only
1368
+ * consumers that need their own chrome (custom aria-label, tooltip, class)
1369
+ * pass one built with {@link useModelMenuAnchor}. The returned node is never
1370
+ * assumed to be the factory's own output — a defensive `?? fallback` covers a
1371
+ * renderer that yields null for an unregistered definition.
1372
+ */
1373
+ function renderModelPicker(renderFactorySlot, props, options) {
1374
+ if (typeof renderFactorySlot !== "function") return options.fallback;
1375
+ return renderFactorySlot("klarkxy.model-picker", props, {
1376
+ ...options.anchor ? { slots: { ["anchor"]: options.anchor } } : {},
1377
+ fallback: options.fallback
1378
+ }) ?? options.fallback;
1379
+ }
986
1380
  Object.values({
987
1381
  /** Surfaces, from the page up to the topmost nested card. */
988
1382
  surface: [
@@ -1072,7 +1466,9 @@ Object.values({
1072
1466
  "--dsh-scrollbar-thumb-hover",
1073
1467
  "--dsh-content-font-size-secondary",
1074
1468
  "--dsh-content-font-delta",
1075
- "--dsh-frame-top-clearance"
1469
+ "--dsh-frame-top-clearance",
1470
+ "--dsh-composer-model-icon-display",
1471
+ "--dsh-composer-model-text-display"
1076
1472
  ]
1077
1473
  }).flat();
1078
1474
  /**
@@ -1799,7 +2195,7 @@ async function callAuxiliaryRpc(client, endpoint, payload = {}) {
1799
2195
  * The shared card. All state comes from the shared host service over the
1800
2196
  * plugin-kit RPC channel; the card only renders the stored row.
1801
2197
  */
1802
- function AuxiliaryModelCard({ client, subscribe }) {
2198
+ function AuxiliaryModelCard({ client, subscribe, renderFactorySlot }) {
1803
2199
  const language = useAuxiliaryLocale(client);
1804
2200
  const text = auxiliaryCopy[language];
1805
2201
  const [settings, setSettings] = (0, react.useState)();
@@ -1920,6 +2316,17 @@ function AuxiliaryModelCard({ client, subscribe }) {
1920
2316
  refreshCatalog
1921
2317
  ]);
1922
2318
  const locked = busy;
2319
+ const current = settings?.model ?? {
2320
+ provider: "",
2321
+ model: ""
2322
+ };
2323
+ const follow = !current.provider || !current.model;
2324
+ const selected = follow ? void 0 : choices.find((item) => item.provider === current.provider && item.model === current.model);
2325
+ const efforts = follow ? [] : modelMenuEffortOptions(selected, current.reasoningEffort);
2326
+ const currentLabel = follow ? text.modelDefault : selected?.label ?? `${current.provider} / ${current.model}`;
2327
+ const selectedEffort = efforts.find((item) => item.id === (current.reasoningEffort || ""));
2328
+ const effortName = selectedEffort ? modelMenuEffortLabel(selectedEffort, language) : void 0;
2329
+ const anchor = useModelMenuAnchor(`${text.model}: ${settings ? currentLabel : ""}`, void 0, () => setMenuOpen(false));
1923
2330
  if (!settings) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1924
2331
  className: "ams-root dsh-ui-card",
1925
2332
  "data-testid": "auxiliary-model-settings",
@@ -1958,16 +2365,6 @@ function AuxiliaryModelCard({ client, subscribe }) {
1958
2365
  })
1959
2366
  ]
1960
2367
  });
1961
- const current = settings.model ?? {
1962
- provider: "",
1963
- model: ""
1964
- };
1965
- const follow = !current.provider || !current.model;
1966
- const selected = follow ? void 0 : choices.find((item) => item.provider === current.provider && item.model === current.model);
1967
- const efforts = follow ? [] : modelMenuEffortOptions(selected, current.reasoningEffort);
1968
- const currentLabel = follow ? text.modelDefault : selected?.label ?? `${current.provider} / ${current.model}`;
1969
- const selectedEffort = efforts.find((item) => item.id === (current.reasoningEffort || ""));
1970
- const effortName = selectedEffort ? modelMenuEffortLabel(selectedEffort, language) : void 0;
1971
2368
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1972
2369
  className: "ams-root dsh-ui-card",
1973
2370
  "data-testid": "auxiliary-model-settings",
@@ -1991,28 +2388,12 @@ function AuxiliaryModelCard({ client, subscribe }) {
1991
2388
  state: catalogState,
1992
2389
  locale: language
1993
2390
  }),
1994
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
2391
+ renderModelPicker(renderFactorySlot, {
1995
2392
  locale: language,
1996
- open: menuOpen && !locked,
2393
+ disabled: locked,
2394
+ busy: locked,
1997
2395
  onOpenChange: (next) => setMenuOpen(next && !locked),
1998
2396
  listClassName: "ams-root ams-menu-portal",
1999
- anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2000
- type: "button",
2001
- className: "dsh-model-menu-trigger",
2002
- disabled: locked,
2003
- "aria-label": `${text.model}: ${currentLabel}`,
2004
- children: [
2005
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2006
- className: "dsh-model-menu-triggerLabel",
2007
- children: currentLabel
2008
- }),
2009
- !follow && efforts.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2010
- className: "dsh-model-menu-triggerEffort",
2011
- children: effortName ?? text.effortDefault
2012
- }) : null,
2013
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${menuOpen && !locked ? " dsh-model-menu-chevronOpen" : ""}` })
2014
- ]
2015
- }),
2016
2397
  choices,
2017
2398
  selected: follow ? void 0 : {
2018
2399
  provider: current.provider,
@@ -2035,9 +2416,61 @@ function AuxiliaryModelCard({ client, subscribe }) {
2035
2416
  model: current.model,
2036
2417
  ...effort ? { reasoningEffort: effort } : {}
2037
2418
  }),
2419
+ triggerLabel: currentLabel,
2420
+ triggerEffortLabel: !follow && efforts.length ? effortName ?? text.effortDefault : void 0,
2038
2421
  modelLabel: text.model,
2039
2422
  modelValue: currentLabel,
2040
2423
  defaultEffortLabel: text.effortDefault
2424
+ }, {
2425
+ anchor,
2426
+ fallback: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
2427
+ locale: language,
2428
+ open: menuOpen && !locked,
2429
+ onOpenChange: (next) => setMenuOpen(next && !locked),
2430
+ listClassName: "ams-root ams-menu-portal",
2431
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2432
+ type: "button",
2433
+ className: "dsh-model-menu-trigger",
2434
+ disabled: locked,
2435
+ "aria-label": `${text.model}: ${currentLabel}`,
2436
+ children: [
2437
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2438
+ className: "dsh-model-menu-triggerLabel",
2439
+ children: currentLabel
2440
+ }),
2441
+ !follow && efforts.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2442
+ className: "dsh-model-menu-triggerEffort",
2443
+ children: effortName ?? text.effortDefault
2444
+ }) : null,
2445
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${menuOpen && !locked ? " dsh-model-menu-chevronOpen" : ""}` })
2446
+ ]
2447
+ }),
2448
+ choices,
2449
+ selected: follow ? void 0 : {
2450
+ provider: current.provider,
2451
+ model: current.model
2452
+ },
2453
+ onPick: (picked) => save({
2454
+ provider: picked.provider,
2455
+ model: picked.model
2456
+ }),
2457
+ leading: [{
2458
+ id: "default",
2459
+ label: text.modelDefault,
2460
+ selected: follow
2461
+ }],
2462
+ onPickLeading: () => save(null),
2463
+ efforts,
2464
+ selectedEffort: current.reasoningEffort || "",
2465
+ onPickEffort: (effort) => save(follow ? null : {
2466
+ provider: current.provider,
2467
+ model: current.model,
2468
+ ...effort ? { reasoningEffort: effort } : {}
2469
+ }),
2470
+ modelLabel: text.model,
2471
+ modelValue: currentLabel,
2472
+ defaultEffortLabel: text.effortDefault
2473
+ })
2041
2474
  }),
2042
2475
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2043
2476
  className: "dsh-ui-hint",
@@ -2139,9 +2572,10 @@ function acquireAuxiliaryModelCard(root, ctx) {
2139
2572
  let unprovide;
2140
2573
  const watchLease = acquireWatchCoordinator(root).mount();
2141
2574
  const subscriber = createModelSettingsSubscriber(ctx.root, AUXILIARY_MODEL_PACKAGE, AUXILIARY_MODEL_CHANGES_IDENTITY, (ns) => ns === AUXILIARY_MODEL_NS);
2142
- const render = () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AuxiliaryModelCard, {
2575
+ const render = (props) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AuxiliaryModelCard, {
2143
2576
  client: root,
2144
- subscribe: subscriber.subscribe
2577
+ subscribe: subscriber.subscribe,
2578
+ renderFactorySlot: props.renderFactorySlot
2145
2579
  });
2146
2580
  const service = {
2147
2581
  retain() {
@@ -2397,7 +2831,7 @@ function GitBranchIcon() {
2397
2831
  ]
2398
2832
  });
2399
2833
  }
2400
- function GitCommitAction({ sessionId, useSession, call, t, client, subscribeSettings }) {
2834
+ function GitCommitAction({ sessionId, useSession, call, t, client, subscribeSettings, renderFactorySlot }) {
2401
2835
  const [open, setOpen] = (0, react.useState)(false);
2402
2836
  const [status, setStatus] = (0, react.useState)();
2403
2837
  const [loading, setLoading] = (0, react.useState)(false);
@@ -2613,6 +3047,7 @@ function GitCommitAction({ sessionId, useSession, call, t, client, subscribeSett
2613
3047
  client,
2614
3048
  subscribeSettings,
2615
3049
  disabled: running,
3050
+ renderFactorySlot,
2616
3051
  onSaving: markWrite,
2617
3052
  onWriteFailed: markWriteFailed,
2618
3053
  onReady: markSeen,
@@ -2913,6 +3348,17 @@ function GitCommitSettingsPanel(props) {
2913
3348
  props.disabled
2914
3349
  ]);
2915
3350
  const locked = busy || controlsLocked;
3351
+ const current = settings?.model ?? {
3352
+ provider: "",
3353
+ model: ""
3354
+ };
3355
+ const follow = !current.provider || !current.model;
3356
+ const selected = follow ? void 0 : choices.find((item) => item.provider === current.provider && item.model === current.model);
3357
+ const efforts = follow ? [] : modelMenuEffortOptions(selected, current.reasoningEffort);
3358
+ const currentLabel = follow ? text.modelDefault : selected?.label ?? `${current.provider} / ${current.model}`;
3359
+ const selectedEffort = efforts.find((item) => item.id === (current.reasoningEffort || ""));
3360
+ const effortName = selectedEffort ? modelMenuEffortLabel(selectedEffort, language) : void 0;
3361
+ const anchor = useModelMenuAnchor(`${text.model}: ${settings ? currentLabel : ""}`, void 0, () => setMenuOpen(false));
2916
3362
  if (!settings) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2917
3363
  className: "gcm-root gcm-settings dsh-ui-stack",
2918
3364
  children: [
@@ -2939,13 +3385,6 @@ function GitCommitSettingsPanel(props) {
2939
3385
  })
2940
3386
  ]
2941
3387
  });
2942
- const current = settings.model;
2943
- const follow = !current.provider || !current.model;
2944
- const selected = follow ? void 0 : choices.find((item) => item.provider === current.provider && item.model === current.model);
2945
- const efforts = follow ? [] : modelMenuEffortOptions(selected, current.reasoningEffort);
2946
- const currentLabel = follow ? text.modelDefault : selected?.label ?? `${current.provider} / ${current.model}`;
2947
- const selectedEffort = efforts.find((item) => item.id === (current.reasoningEffort || ""));
2948
- const effortName = selectedEffort ? modelMenuEffortLabel(selectedEffort, language) : void 0;
2949
3388
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2950
3389
  className: "gcm-root gcm-settings dsh-ui-stack",
2951
3390
  "data-testid": "git-commit-settings",
@@ -2961,28 +3400,12 @@ function GitCommitSettingsPanel(props) {
2961
3400
  state: catalogState,
2962
3401
  locale: language
2963
3402
  }),
2964
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
3403
+ renderModelPicker(props.renderFactorySlot, {
2965
3404
  locale: language,
2966
- open: menuOpen && !locked,
3405
+ disabled: locked,
3406
+ busy: locked,
2967
3407
  onOpenChange: (next) => setMenuOpen(next && !locked),
2968
3408
  listClassName: "gcm-root gcm-menu-portal",
2969
- anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2970
- type: "button",
2971
- className: "dsh-model-menu-trigger",
2972
- disabled: locked,
2973
- "aria-label": `${text.model}: ${currentLabel}`,
2974
- children: [
2975
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2976
- className: "dsh-model-menu-triggerLabel",
2977
- children: currentLabel
2978
- }),
2979
- !follow && efforts.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2980
- className: "dsh-model-menu-triggerEffort",
2981
- children: effortName ?? text.modelDefault
2982
- }) : null,
2983
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${menuOpen && !locked ? " dsh-model-menu-chevronOpen" : ""}` })
2984
- ]
2985
- }),
2986
3409
  choices,
2987
3410
  selected: follow ? void 0 : {
2988
3411
  provider: current.provider,
@@ -3011,9 +3434,67 @@ function GitCommitSettingsPanel(props) {
3011
3434
  model: current.model,
3012
3435
  ...effort ? { reasoningEffort: effort } : {}
3013
3436
  } }),
3437
+ triggerLabel: currentLabel,
3438
+ triggerEffortLabel: !follow && efforts.length ? effortName ?? text.modelDefault : void 0,
3014
3439
  modelLabel: text.model,
3015
3440
  modelValue: currentLabel,
3016
3441
  defaultEffortLabel: text.modelDefault
3442
+ }, {
3443
+ anchor,
3444
+ fallback: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
3445
+ locale: language,
3446
+ open: menuOpen && !locked,
3447
+ onOpenChange: (next) => setMenuOpen(next && !locked),
3448
+ listClassName: "gcm-root gcm-menu-portal",
3449
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3450
+ type: "button",
3451
+ className: "dsh-model-menu-trigger",
3452
+ disabled: locked,
3453
+ "aria-label": `${text.model}: ${currentLabel}`,
3454
+ children: [
3455
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3456
+ className: "dsh-model-menu-triggerLabel",
3457
+ children: currentLabel
3458
+ }),
3459
+ !follow && efforts.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3460
+ className: "dsh-model-menu-triggerEffort",
3461
+ children: effortName ?? text.modelDefault
3462
+ }) : null,
3463
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${menuOpen && !locked ? " dsh-model-menu-chevronOpen" : ""}` })
3464
+ ]
3465
+ }),
3466
+ choices,
3467
+ selected: follow ? void 0 : {
3468
+ provider: current.provider,
3469
+ model: current.model
3470
+ },
3471
+ onPick: (picked) => save({ model: {
3472
+ provider: picked.provider,
3473
+ model: picked.model
3474
+ } }),
3475
+ leading: [{
3476
+ id: "default",
3477
+ label: text.modelDefault,
3478
+ selected: follow
3479
+ }],
3480
+ onPickLeading: () => save({ model: {
3481
+ provider: "",
3482
+ model: ""
3483
+ } }),
3484
+ efforts,
3485
+ selectedEffort: current.reasoningEffort || "",
3486
+ onPickEffort: (effort) => save({ model: follow ? {
3487
+ provider: "",
3488
+ model: ""
3489
+ } : {
3490
+ provider: current.provider,
3491
+ model: current.model,
3492
+ ...effort ? { reasoningEffort: effort } : {}
3493
+ } }),
3494
+ modelLabel: text.model,
3495
+ modelValue: currentLabel,
3496
+ defaultEffortLabel: text.modelDefault
3497
+ })
3017
3498
  })
3018
3499
  ]
3019
3500
  }),