@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.
package/lib/client.js CHANGED
@@ -102,6 +102,7 @@ function parseModelMenuChoices(value, bound) {
102
102
  provider,
103
103
  model,
104
104
  label: `${providerName} / ${name}`,
105
+ providerLabel: providerName,
105
106
  efforts
106
107
  });
107
108
  }
@@ -148,11 +149,11 @@ function modelMenuShortName(choice) {
148
149
  /** Group catalog choices by provider and filter them by a case-insensitive query. */
149
150
  function groupModelMenuChoices(choices, query) {
150
151
  const needle = query.trim().toLowerCase();
151
- const matched = needle ? choices.filter((choice) => choice.label.toLowerCase().includes(needle) || choice.model.toLowerCase().includes(needle) || choice.provider.toLowerCase().includes(needle)) : choices;
152
+ const matched = needle ? choices.filter((choice) => choice.label.toLowerCase().includes(needle) || choice.model.toLowerCase().includes(needle) || choice.provider.toLowerCase().includes(needle) || (choice.providerLabel?.toLowerCase().includes(needle) ?? false)) : choices;
152
153
  const groups = [];
153
154
  for (const choice of matched) {
154
155
  const sep = choice.label.indexOf(" / ");
155
- const name = sep > 0 ? choice.label.slice(0, sep) : choice.provider;
156
+ const name = choice.providerLabel ?? (sep > 0 ? choice.label.slice(0, sep) : choice.provider);
156
157
  const last = groups.at(-1);
157
158
  if (last && last.provider === choice.provider) last.items.push(choice);
158
159
  else groups.push({
@@ -379,7 +380,7 @@ function ModelMenu(props) {
379
380
  empty: "No matching models",
380
381
  clear: "Clear search"
381
382
  };
382
- 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;
383
+ 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;
383
384
  const rootRef = (0, react.useRef)(null);
384
385
  const openRef = (0, react.useRef)(open);
385
386
  openRef.current = open;
@@ -394,9 +395,10 @@ function ModelMenu(props) {
394
395
  const [pane, setPane] = (0, react.useState)("root");
395
396
  const [query, setQuery] = (0, react.useState)("");
396
397
  const [highlightedIndex, setHighlightedIndex] = (0, react.useState)(null);
398
+ const [keyNav, setKeyNav] = (0, react.useState)(0);
397
399
  const [menuPos, setMenuPos] = (0, react.useState)(null);
398
400
  const id = (0, react.useId)();
399
- const showSearch = choices.length > 4;
401
+ const showSearch = alwaysShowSearch || choices.length > 4;
400
402
  const effortRows = (0, react.useMemo)(() => [{
401
403
  id: "",
402
404
  name: defaultEffortLabel
@@ -413,7 +415,12 @@ function ModelMenu(props) {
413
415
  items: (0, _deepseek_ai_dsh_client_ui_primitives.rankByName)(group.items.map((choice) => ({
414
416
  choice,
415
417
  name: modelMenuShortName(choice),
416
- label: choice.model
418
+ label: [
419
+ choice.provider,
420
+ choice.providerLabel,
421
+ choice.model,
422
+ choice.label
423
+ ].filter(Boolean).join(" ")
417
424
  })), showSearch ? query.trim() : "")
418
425
  })).filter((group) => group.items.length > 0), [
419
426
  choices,
@@ -445,10 +452,12 @@ function ModelMenu(props) {
445
452
  paneFocus.current = "drill";
446
453
  setPane(next);
447
454
  };
448
- /** Leave a drilled pane for the root one, handing the keyboard back to its cell. */
455
+ /** Leave a drilled pane for the entry pane, handing the keyboard back to it.
456
+ * A root entry refocuses the cell that opened the pane; a search-first
457
+ * (model) entry refocuses that pane's own intent. */
449
458
  const back = (from) => {
450
- paneFocus.current = from;
451
- setPane("root");
459
+ paneFocus.current = initialPane === "root" ? from : "drill";
460
+ setPane(initialPane);
452
461
  };
453
462
  const moveFocus = (offset) => {
454
463
  const items = itemRefs.current.filter((item) => item !== null && !item.disabled);
@@ -488,10 +497,18 @@ function ModelMenu(props) {
488
497
  }, [requestedOpen, open]);
489
498
  (0, react.useEffect)(() => {
490
499
  if (!open) return;
491
- setPane("root");
500
+ setPane(initialPane);
501
+ if (initialPane === "model") paneFocus.current = "drill";
492
502
  setQuery("");
493
503
  setHighlightedIndex(null);
494
- }, [open]);
504
+ }, [open, initialPane]);
505
+ (0, react.useEffect)(() => {
506
+ if (disabled && open && closeOnDisable) changeOpen(false);
507
+ }, [
508
+ disabled,
509
+ open,
510
+ closeOnDisable
511
+ ]);
495
512
  (0, react.useEffect)(() => {
496
513
  if (!open) return;
497
514
  const closeOutside = (event) => {
@@ -499,9 +516,25 @@ function ModelMenu(props) {
499
516
  if (rootRef.current?.contains(target) || menuRef.current?.contains(target)) return;
500
517
  changeOpen(false);
501
518
  };
519
+ const escapeOutside = (event) => {
520
+ if (event.key !== "Escape" || event.defaultPrevented || event.isComposing) return;
521
+ if (!openRef.current) return;
522
+ event.preventDefault();
523
+ if (pane !== initialPane) back(pane);
524
+ else close(true);
525
+ };
502
526
  document.addEventListener("mousedown", closeOutside);
503
- return () => document.removeEventListener("mousedown", closeOutside);
504
- }, [open, onOpenChange]);
527
+ document.addEventListener("keydown", escapeOutside);
528
+ return () => {
529
+ document.removeEventListener("mousedown", closeOutside);
530
+ document.removeEventListener("keydown", escapeOutside);
531
+ };
532
+ }, [
533
+ open,
534
+ onOpenChange,
535
+ pane,
536
+ initialPane
537
+ ]);
505
538
  (0, react.useEffect)(() => {
506
539
  const intent = paneFocus.current;
507
540
  if (!open || menuPos === null || intent === null) return;
@@ -531,13 +564,21 @@ function ModelMenu(props) {
531
564
  pane,
532
565
  filteredGroups
533
566
  ]);
567
+ const modelPaneOpen = open && pane === "model";
568
+ const scrollSignal = !modelPaneOpen ? "closed" : keyNav === 0 ? "open" : `key:${keyNav}`;
569
+ const scrollSignalRef = (0, react.useRef)(null);
534
570
  (0, react.useLayoutEffect)(() => {
535
- if (open && pane === "model" && activeModelIndex >= 0) itemRefs.current[leading.length + activeModelIndex]?.scrollIntoView({ block: "nearest" });
571
+ if (!modelPaneOpen || activeModelIndex < 0) {
572
+ if (!modelPaneOpen) scrollSignalRef.current = null;
573
+ return;
574
+ }
575
+ if (scrollSignalRef.current === scrollSignal) return;
576
+ scrollSignalRef.current = scrollSignal;
577
+ itemRefs.current[leading.length + activeModelIndex]?.scrollIntoView({ block: "nearest" });
536
578
  }, [
537
- open,
538
- pane,
579
+ modelPaneOpen,
580
+ scrollSignal,
539
581
  activeModelIndex,
540
- visibleModels,
541
582
  leading.length
542
583
  ]);
543
584
  (0, react.useLayoutEffect)(() => {
@@ -552,7 +593,7 @@ function ModelMenu(props) {
552
593
  let x = rect.right - lw, y = rect.top - 8 - lh;
553
594
  if (lw > 0) x = Math.min(Math.max(x, margin), window.innerWidth - lw - margin);
554
595
  if (lh > 0) y = Math.min(Math.max(y, margin), window.innerHeight - lh - margin);
555
- setMenuPos({
596
+ setMenuPos((prev) => prev && prev.left === x && prev.top === y ? prev : {
556
597
  left: x,
557
598
  top: y
558
599
  });
@@ -572,6 +613,13 @@ function ModelMenu(props) {
572
613
  ]);
573
614
  function onKeyDown(event) {
574
615
  if (event.defaultPrevented || event.nativeEvent.isComposing) return;
616
+ if (event.key === "Escape" && open) {
617
+ event.preventDefault();
618
+ if (pane !== initialPane) back(pane);
619
+ else close(true);
620
+ return;
621
+ }
622
+ if (disabled) return;
575
623
  if (!open && [
576
624
  "Enter",
577
625
  " ",
@@ -585,17 +633,12 @@ function ModelMenu(props) {
585
633
  requestOpen();
586
634
  return;
587
635
  }
588
- if (event.key === "Escape" && open) {
589
- event.preventDefault();
590
- if (pane !== "root") back(pane);
591
- else close(true);
592
- return;
593
- }
594
636
  if (!open) return;
595
637
  if (pane === "model" && showSearch && (event.key === "ArrowDown" || event.key === "ArrowUp")) {
596
638
  event.preventDefault();
597
639
  if (visibleModels.length > 0) {
598
640
  setHighlightedIndex((activeModelIndex + (event.key === "ArrowDown" ? 1 : -1) + visibleModels.length) % visibleModels.length);
641
+ setKeyNav((n) => n + 1);
599
642
  searchRef.current?.focus();
600
643
  }
601
644
  return;
@@ -604,19 +647,19 @@ function ModelMenu(props) {
604
647
  if (event.key === "Tab" && visibleModels.length === 0) return;
605
648
  event.preventDefault();
606
649
  const highlighted = visibleModels[activeModelIndex];
607
- if (highlighted) {
650
+ if (highlighted && !busy) {
608
651
  onPick({
609
652
  provider: highlighted.provider,
610
653
  model: highlighted.model
611
654
  });
612
- close(true);
655
+ if (closeOnPick) close(true);
613
656
  }
614
657
  return;
615
658
  }
616
659
  if (event.key === "Tab") {
617
660
  if (event.shiftKey) {
618
661
  event.preventDefault();
619
- if (pane !== "root") back(pane);
662
+ if (pane !== initialPane) back(pane);
620
663
  else close(true);
621
664
  return;
622
665
  }
@@ -640,7 +683,10 @@ function ModelMenu(props) {
640
683
  }
641
684
  }
642
685
  function onBlur(event) {
643
- if (event.relatedTarget instanceof Node && (rootRef.current?.contains(event.relatedTarget) || menuRef.current?.contains(event.relatedTarget))) return;
686
+ const related = event.relatedTarget;
687
+ if (related instanceof Node && (rootRef.current?.contains(related) || menuRef.current?.contains(related) || !related.isConnected)) return;
688
+ if (!closeOnPick && !(related instanceof Node)) return;
689
+ if (busy && related == null) return;
644
690
  if (open) changeOpen(false);
645
691
  }
646
692
  itemRefs.current = [];
@@ -661,11 +707,16 @@ function ModelMenu(props) {
661
707
  "aria-label": modelLabel,
662
708
  lang: language,
663
709
  children: [
710
+ menuHeader ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
711
+ className: "dsh-model-menu-header",
712
+ children: menuHeader
713
+ }) : null,
664
714
  pane === "root" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
665
715
  ref: itemRef(),
666
716
  type: "button",
667
717
  role: "menuitem",
668
718
  className: "dsh-model-menu-cell",
719
+ disabled,
669
720
  onClick: () => drill("model"),
670
721
  children: [
671
722
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -678,11 +729,12 @@ function ModelMenu(props) {
678
729
  }),
679
730
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutlineRegular, { className: "dsh-model-menu-cellChevron" })
680
731
  ]
681
- }), efforts.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
732
+ }), efforts.length > 0 && !reasoningSlider ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
682
733
  ref: itemRef(),
683
734
  type: "button",
684
735
  role: "menuitem",
685
736
  className: "dsh-model-menu-cell",
737
+ disabled,
686
738
  onClick: () => drill("effort"),
687
739
  children: [
688
740
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -701,6 +753,7 @@ function ModelMenu(props) {
701
753
  className: "dsh-model-menu-searchRow",
702
754
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
703
755
  ref: searchRef,
756
+ disabled,
704
757
  className: `dsh-model-menu-search${query !== "" ? " dsh-model-menu-searchWithQuery" : ""}`,
705
758
  type: "text",
706
759
  role: "searchbox",
@@ -717,6 +770,7 @@ function ModelMenu(props) {
717
770
  type: "button",
718
771
  className: "dsh-model-menu-searchClear",
719
772
  "aria-label": clearSearchLabel,
773
+ disabled,
720
774
  onClick: () => {
721
775
  setQuery("");
722
776
  setHighlightedIndex(null);
@@ -739,10 +793,12 @@ function ModelMenu(props) {
739
793
  type: "button",
740
794
  role: "menuitemradio",
741
795
  "aria-checked": item.selected === true,
796
+ disabled: disabled || busy,
742
797
  className: `dsh-model-menu-option${item.selected === true ? " dsh-model-menu-selected" : ""}`,
743
798
  onClick: () => {
799
+ if (disabled || busy) return;
744
800
  onPickLeading?.(item.id);
745
- close(true);
801
+ if (closeOnPick) close(true);
746
802
  },
747
803
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
748
804
  className: "dsh-model-menu-optionCopy",
@@ -768,6 +824,7 @@ function ModelMenu(props) {
768
824
  "aria-checked": checked,
769
825
  id: `${id}-model-${index}`,
770
826
  tabIndex: showSearch ? -1 : 0,
827
+ disabled: disabled || busy,
771
828
  onFocus: () => setHighlightedIndex(index),
772
829
  "data-highlighted": index === activeModelIndex ? "" : void 0,
773
830
  className: `dsh-model-menu-option dsh-model-menu-modelOption${checked ? " dsh-model-menu-selected" : ""}${index === activeModelIndex ? " dsh-model-menu-optionActive" : ""}`,
@@ -777,11 +834,12 @@ function ModelMenu(props) {
777
834
  },
778
835
  title: choice.label,
779
836
  onClick: () => {
837
+ if (disabled || busy) return;
780
838
  onPick({
781
839
  provider: choice.provider,
782
840
  model: choice.model
783
841
  });
784
- close(true);
842
+ if (closeOnPick) close(true);
785
843
  },
786
844
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
787
845
  className: "dsh-model-menu-optionCopy",
@@ -797,6 +855,14 @@ function ModelMenu(props) {
797
855
  })
798
856
  }, group.provider))]
799
857
  }),
858
+ reasoningSlider && efforts.length > 0 && filteredGroups.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EffortSlider, {
859
+ label: effortLabel,
860
+ rows: effortRows,
861
+ selected: selectedEffort ?? "",
862
+ disabled,
863
+ busy,
864
+ onPick: (value) => onPickEffort?.(value)
865
+ }) : null,
800
866
  filteredGroups.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
801
867
  className: "dsh-model-menu-empty",
802
868
  role: "status",
@@ -810,10 +876,12 @@ function ModelMenu(props) {
810
876
  type: "button",
811
877
  role: "menuitemradio",
812
878
  "aria-checked": checked,
879
+ disabled: disabled || busy,
813
880
  className: `dsh-model-menu-option${checked ? " dsh-model-menu-selected" : ""}`,
814
881
  onClick: () => {
882
+ if (disabled || busy) return;
815
883
  onPickEffort?.(row.id || void 0);
816
- close(true);
884
+ if (closeOnPick) close(true);
817
885
  },
818
886
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
819
887
  className: "dsh-model-menu-optionCopy",
@@ -832,7 +900,8 @@ function ModelMenu(props) {
832
900
  const trigger = (0, react.isValidElement)(anchor) ? (0, react.cloneElement)(anchor, {
833
901
  "aria-haspopup": "menu",
834
902
  "aria-expanded": open,
835
- "aria-controls": open ? `${id}-menu` : void 0
903
+ "aria-controls": open ? `${id}-menu` : void 0,
904
+ "aria-disabled": disabled || void 0
836
905
  }) : anchor;
837
906
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
838
907
  ref: rootRef,
@@ -840,7 +909,7 @@ function ModelMenu(props) {
840
909
  onKeyDown,
841
910
  onBlur,
842
911
  onClick: (event) => {
843
- if (event.defaultPrevented) return;
912
+ if (disabled || event.defaultPrevented) return;
844
913
  const button = event.target instanceof Element ? event.target.closest("button") : null;
845
914
  if (!(button instanceof HTMLButtonElement) || button.disabled || !rootRef.current?.contains(button)) return;
846
915
  requestOpen();
@@ -852,6 +921,102 @@ function ModelMenu(props) {
852
921
  });
853
922
  }
854
923
  /**
924
+ * Discrete reasoning-effort slider shown under the model list in enhanced mode.
925
+ *
926
+ * A native range input (`step=1`) over exactly the rows the caller supplies —
927
+ * the default row plus each effort the selected model advertises — so it can
928
+ * never invent a level. It previews locally while dragging or arrowing and
929
+ * commits only when the gesture ends (pointer up, key up, or blur), so a drag
930
+ * does not write on every step. The menu stays open, so adjusting the effort
931
+ * never loses the model selection or the search query.
932
+ *
933
+ * The input itself stays the entire interaction and accessibility surface —
934
+ * keyboard, ARIA, disabled and the native step snapping all come from it —
935
+ * while the track, gradient fill and thumb below are decorative layers painted
936
+ * from the `--_p` percentage the preview position sets on the rail. Value
937
+ * changes only update that custom property, so the CSS transitions animate
938
+ * between levels without remounting nodes or restarting an animation.
939
+ */
940
+ function EffortSlider(props) {
941
+ const { label, rows, selected, disabled = false, busy = false, onPick } = props;
942
+ const active = Math.max(0, rows.findIndex((row) => row.id === selected));
943
+ const [position, setPosition] = (0, react.useState)(active);
944
+ const positionRef = (0, react.useRef)(position);
945
+ (0, react.useEffect)(() => {
946
+ positionRef.current = active;
947
+ setPosition(active);
948
+ }, [active]);
949
+ const preview = (next) => {
950
+ positionRef.current = next;
951
+ setPosition(next);
952
+ };
953
+ const commit = () => {
954
+ if (disabled || busy) return;
955
+ const row = rows[positionRef.current];
956
+ if (row !== void 0) onPick(row.id === "" ? void 0 : row.id);
957
+ };
958
+ const max = Math.max(0, rows.length - 1);
959
+ const percent = max > 0 ? Math.round(position / max * 100) : 0;
960
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
961
+ className: "dsh-model-menu-sliderGroup",
962
+ role: "group",
963
+ "aria-label": label,
964
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
965
+ className: "dsh-model-menu-sliderHead",
966
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
967
+ className: "dsh-model-menu-sliderTitle",
968
+ children: label
969
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
970
+ className: "dsh-model-menu-sliderValue",
971
+ children: rows[position]?.name
972
+ })]
973
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
974
+ className: "dsh-model-menu-sliderRail",
975
+ style: { "--_p": percent },
976
+ children: [
977
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
978
+ type: "range",
979
+ className: "dsh-model-menu-sliderInput",
980
+ min: 0,
981
+ max,
982
+ step: 1,
983
+ value: position,
984
+ disabled,
985
+ "aria-label": label,
986
+ "aria-disabled": disabled || busy || void 0,
987
+ "aria-valuetext": rows[position]?.name,
988
+ onChange: (event) => {
989
+ const value = Number(event.target.value);
990
+ if (Number.isFinite(value)) preview(value);
991
+ },
992
+ onPointerUp: commit,
993
+ onKeyUp: commit,
994
+ onBlur: commit
995
+ }),
996
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
997
+ className: "dsh-model-menu-sliderTrack",
998
+ "aria-hidden": "true",
999
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1000
+ className: "dsh-model-menu-sliderFill",
1001
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dsh-model-menu-sliderFillBar" })
1002
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1003
+ className: "dsh-model-menu-sliderNodes",
1004
+ children: rows.map((row, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1005
+ "data-covered": index <= position ? "" : void 0,
1006
+ className: "dsh-model-menu-sliderNode",
1007
+ style: { "--_n": max > 0 ? Math.round(index / max * 100) : 0 }
1008
+ }, row.id || "__default"))
1009
+ })]
1010
+ }),
1011
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1012
+ className: "dsh-model-menu-sliderThumb",
1013
+ "aria-hidden": "true"
1014
+ })
1015
+ ]
1016
+ })]
1017
+ });
1018
+ }
1019
+ /**
855
1020
  * ModelMenu styles, transcribed from the official ModelSelect.module.css (the
856
1021
  * published dsh-client-ui-model-selection bundle): card capped at
857
1022
  * min(360px, 100vh - 96px), 34px option cells at 13/18 with 5px 7px padding,
@@ -910,6 +1075,7 @@ const modelMenuCss = `
910
1075
  overflow: hidden;
911
1076
  }
912
1077
  .dsh-model-menu-status, .dsh-model-menu-empty { color: var(--dsw-alias-label-tertiary); padding: 8px; font-size: 12px; line-height: 18px; }
1078
+ .dsh-model-menu-header { flex: none; padding: 2px 2px 4px; }
913
1079
  .dsh-model-menu-searchRow { flex-shrink: 0; margin: 2px 0 3px; position: relative; }
914
1080
  .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; }
915
1081
  .dsh-model-menu-searchRow .dsh-model-menu-search:focus-within { border-color: transparent; }
@@ -988,7 +1154,235 @@ const modelMenuCss = `
988
1154
  .dsh-model-menu-cellLabel { white-space: nowrap; flex: none; }
989
1155
  .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; }
990
1156
  .dsh-model-menu-cellChevron { width: 12px; height: 12px; color: var(--dsw-alias-menu-icon); flex: none; }
1157
+ /* Discrete reasoning-effort slider under the model list (enhanced mode).
1158
+ * The painted control is layered over a fully functional native range input:
1159
+ * the input stays on top, transparent, and owns pointer/keyboard/ARIA, while
1160
+ * the decorative track, fill and thumb render beneath it at pointer-events:
1161
+ * none. Position lives in one custom property (--_p, 0–100) on the rail, so
1162
+ * the only animated work a level change triggers is a clip-path/left
1163
+ * transition — no keyframes, no element churn. */
1164
+ .dsh-model-menu-sliderGroup { border-top: 0.5px solid var(--dsw-alias-border-l2); flex: none; margin-top: 4px; padding: 6px 8px 8px; }
1165
+ .dsh-model-menu-sliderHead { align-items: baseline; justify-content: space-between; gap: 8px; margin: 0 2px 4px; display: flex; }
1166
+ .dsh-model-menu-sliderTitle { color: var(--dsw-alias-label-tertiary); font-size: 12px; line-height: 18px; }
1167
+ .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); }
1168
+ .dsh-model-menu-sliderRail { position: relative; direction: ltr; height: 38px; margin-top: 2px; }
1169
+ .dsh-model-menu-sliderRail:has(:disabled) { opacity: 0.45; }
1170
+ .dsh-model-menu-sliderInput {
1171
+ appearance: none;
1172
+ -webkit-appearance: none;
1173
+ box-sizing: border-box;
1174
+ border: 0;
1175
+ cursor: pointer;
1176
+ direction: ltr;
1177
+ opacity: 0;
1178
+ margin: 0;
1179
+ padding: 0;
1180
+ width: 100%;
1181
+ height: 100%;
1182
+ position: absolute;
1183
+ inset: 0;
1184
+ z-index: 1;
1185
+ touch-action: pan-y;
1186
+ }
1187
+ .dsh-model-menu-sliderInput:disabled { cursor: default; }
1188
+ /* The transparent input must keep the painted thumb's geometry: an
1189
+ * appearance:none webkit thumb defaults to 16px, which would shift the
1190
+ * end-point positions 7px off the decorative 30px disc and misplace the
1191
+ * native drag hit target. Track gets the full rail height so the whole band
1192
+ * maps to values identically. */
1193
+ .dsh-model-menu-sliderInput::-webkit-slider-runnable-track { width: 100%; height: 100%; }
1194
+ .dsh-model-menu-sliderInput::-webkit-slider-thumb { appearance: none; -webkit-appearance: none; width: 30px; height: 30px; margin: 0; }
1195
+ .dsh-model-menu-sliderInput::-moz-range-track { width: 100%; height: 100%; }
1196
+ .dsh-model-menu-sliderInput::-moz-range-thumb { width: 30px; height: 30px; margin: 0; border: 0; }
1197
+ .dsh-model-menu-sliderTrack {
1198
+ pointer-events: none;
1199
+ border-radius: 999px;
1200
+ width: 100%;
1201
+ height: 26px;
1202
+ background: var(--dsw-alias-interactive-bg-hover);
1203
+ box-shadow: inset 0 0 0 0.5px var(--dsw-alias-border-l2);
1204
+ position: absolute;
1205
+ inset-inline: 0;
1206
+ top: 50%;
1207
+ transform: translateY(-50%);
1208
+ /* No overflow:hidden — it would clip the fill's drop-shadow glow; the bar's
1209
+ * own clip-path already supplies the rounded reveal shape. */
1210
+ }
1211
+ /* The accent ramp paints the full track width once and is revealed by an
1212
+ * animated clip-path, so raising the effort unwraps more of the spectrum
1213
+ * instead of squashing the same gradient into a narrower bar. The glow lives
1214
+ * on the un-clipped parent: a filter on the clipped element itself would be
1215
+ * clipped away with it. */
1216
+ .dsh-model-menu-sliderFill {
1217
+ display: block;
1218
+ height: 100%;
1219
+ filter: drop-shadow(0 0 5px var(--dsw-alias-state-business-primary));
1220
+ transition: filter var(--ds-transition-duration) var(--ds-ease-in-out);
1221
+ }
1222
+ .dsh-model-menu-sliderInput:hover:not(:disabled) ~ .dsh-model-menu-sliderTrack .dsh-model-menu-sliderFill,
1223
+ .dsh-model-menu-sliderInput:active:not(:disabled) ~ .dsh-model-menu-sliderTrack .dsh-model-menu-sliderFill {
1224
+ filter: drop-shadow(0 0 9px var(--dsw-alias-state-business-primary));
1225
+ }
1226
+ /* The fill stops at the thumb's center, not at the track edge — at a 30px
1227
+ * thumb the end stops sit 15px in from each side, matching the node dots. */
1228
+ .dsh-model-menu-sliderFillBar {
1229
+ display: block;
1230
+ height: 100%;
1231
+ background: linear-gradient(90deg,
1232
+ var(--dsw-alias-state-business-primary),
1233
+ var(--dsw-alias-state-success-primary),
1234
+ var(--dsw-alias-state-warn-primary),
1235
+ var(--dsw-alias-state-error-primary));
1236
+ clip-path: inset(0 calc(100% - 15px - (100% - 30px) * var(--_p, 0) / 100) 0 0 round 999px);
1237
+ transition: clip-path var(--ds-transition-duration) var(--ds-ease-in-out);
1238
+ }
1239
+ /* Stop dots: one per declared row (default + every advertised effort), evenly
1240
+ * spread across the band between the two thumb-center extremes (inset 15px,
1241
+ * i.e. half the 30px thumb). --_n is 0-100 along that band. A covered dot
1242
+ * reads as a pale pin through the gradient; an uncovered one stays a neutral
1243
+ * notch on the empty track. */
1244
+ .dsh-model-menu-sliderNodes {
1245
+ pointer-events: none;
1246
+ display: block;
1247
+ position: absolute;
1248
+ inset-inline: 15px;
1249
+ inset-block: 0;
1250
+ }
1251
+ .dsh-model-menu-sliderNode {
1252
+ pointer-events: none;
1253
+ box-sizing: border-box;
1254
+ width: 4px;
1255
+ height: 4px;
1256
+ border-radius: 50%;
1257
+ background: color-mix(in srgb, var(--dsw-alias-label-secondary) 35%, transparent);
1258
+ position: absolute;
1259
+ left: calc(var(--_n, 0) * 1%);
1260
+ top: 50%;
1261
+ transform: translate(-50%, -50%);
1262
+ transition: background-color var(--ds-transition-duration) var(--ds-ease-in-out),
1263
+ transform var(--ds-transition-duration) var(--ds-ease-in-out);
1264
+ }
1265
+ .dsh-model-menu-sliderNode[data-covered] { background: color-mix(in srgb, var(--dsw-alias-label-primary) 60%, transparent); }
1266
+ .dsh-model-menu-sliderThumb {
1267
+ pointer-events: none;
1268
+ box-sizing: border-box;
1269
+ border-radius: 50%;
1270
+ width: 30px;
1271
+ height: 30px;
1272
+ background: var(--dsw-alias-button-primary-fill);
1273
+ border: 0.5px solid var(--dsw-alias-border-l4);
1274
+ box-shadow: 0 0 0 3px var(--dsw-alias-interactive-bg-hover), var(--dsw-elevation-soft);
1275
+ position: absolute;
1276
+ left: calc((100% - 30px) * var(--_p, 0) / 100);
1277
+ top: 50%;
1278
+ transform: translateY(-50%) scale(0.92);
1279
+ transition: left var(--ds-transition-duration) var(--ds-ease-in-out),
1280
+ transform var(--ds-transition-duration) var(--ds-ease-in-out),
1281
+ box-shadow var(--ds-transition-duration) var(--ds-ease-in-out);
1282
+ }
1283
+ .dsh-model-menu-sliderInput:hover:not(:disabled) ~ .dsh-model-menu-sliderThumb { transform: translateY(-50%) scale(1); }
1284
+ .dsh-model-menu-sliderInput:active:not(:disabled) ~ .dsh-model-menu-sliderThumb {
1285
+ transform: translateY(-50%) scale(1.1);
1286
+ box-shadow: 0 0 0 4px var(--dsw-alias-interactive-bg-hover), 0 0 12px var(--dsw-alias-state-business-primary);
1287
+ }
1288
+ .dsh-model-menu-sliderInput:focus-visible ~ .dsh-model-menu-sliderThumb {
1289
+ box-shadow: 0 0 0 3px var(--dsw-alias-interactive-bg-hover),
1290
+ 0 0 0 calc(3px + var(--dsw-focus-ring-width)) var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
1291
+ }
1292
+ @media (prefers-reduced-motion: reduce) {
1293
+ .dsh-model-menu-sliderFill, .dsh-model-menu-sliderFillBar, .dsh-model-menu-sliderThumb,
1294
+ .dsh-model-menu-sliderNode, .dsh-model-menu-sliderValue { transition: none; }
1295
+ }
991
1296
  `;
1297
+ //#endregion
1298
+ //#region ../dsh-plugin-kit/src/model-picker.tsx
1299
+ /**
1300
+ * Optional shared model picker (`klarkxy.model-picker` Factory).
1301
+ *
1302
+ * `@klarkxy/dsh-model-picker`, when installed, registers one root-scoped
1303
+ * Component Factory under `MODEL_PICKER_FACTORY`: an enhanced model menu with
1304
+ * a searchable provider-grouped list and a reasoning-effort slider, built on
1305
+ * the same ModelMenu recipe. Consumers keep their own ModelMenu as the
1306
+ * `fallback`; when no definition is registered the renderer resolves that
1307
+ * fallback verbatim, so the shared picker is strictly opt-in and every saved
1308
+ * value keeps its existing write path.
1309
+ *
1310
+ * `renderFactorySlot` is a framework-composed prop, never a context: the
1311
+ * component a plugin registers into any slot receives it automatically, but
1312
+ * the plugin must pass it down to whichever child renders the menu (or to the
1313
+ * render callback handed to registerSessionSettingsItem).
1314
+ *
1315
+ * The Factory manages its own open state; a consumer's `open`/`onOpenChange`
1316
+ * pair belongs to the fallback menu only and must not be fed to both.
1317
+ */
1318
+ /**
1319
+ * The trigger every consumer used to hand its own ModelMenu, rebuilt as the
1320
+ * factory's `anchor` local component: identical classes and chevron, the menu
1321
+ * state coming from the occurrence's props instead of local open state.
1322
+ *
1323
+ * `ariaLabel` and `title` are the whole accessible name and tooltip (the
1324
+ * caller formats "field: current route" itself because the factory's resolved
1325
+ * caption may intentionally be a short name). The component identity stays
1326
+ * put across label changes — a new function would unmount the trigger, and
1327
+ * its cleanup would close a menu that `closeOnPick={false}` is keeping open.
1328
+ * The latest label, title, and `onClose` are read from refs.
1329
+ */
1330
+ function useModelMenuAnchor(ariaLabel, title, onClose) {
1331
+ const closeRef = (0, react.useRef)(onClose);
1332
+ const labelRef = (0, react.useRef)(ariaLabel);
1333
+ const titleRef = (0, react.useRef)(title);
1334
+ closeRef.current = onClose;
1335
+ labelRef.current = ariaLabel;
1336
+ titleRef.current = title;
1337
+ return (0, react.useMemo)(() => {
1338
+ const ModelPickerTrigger = (props) => {
1339
+ (0, react.useEffect)(() => () => closeRef.current?.(), []);
1340
+ const record = props;
1341
+ const injected = {};
1342
+ for (const [key, value] of Object.entries(record)) if (key.startsWith("aria-")) injected[key] = value;
1343
+ const extraClass = typeof record.className === "string" ? ` ${record.className}` : "";
1344
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1345
+ type: "button",
1346
+ className: `dsh-model-menu-trigger${extraClass}`,
1347
+ disabled: props.disabled,
1348
+ "aria-haspopup": "menu",
1349
+ "aria-expanded": props.open,
1350
+ ...injected,
1351
+ "aria-label": labelRef.current,
1352
+ title: titleRef.current,
1353
+ children: [
1354
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1355
+ className: "dsh-model-menu-triggerLabel",
1356
+ children: props.label
1357
+ }),
1358
+ props.effortLabel ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1359
+ className: "dsh-model-menu-triggerEffort",
1360
+ children: props.effortLabel
1361
+ }) : null,
1362
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${props.open ? " dsh-model-menu-chevronOpen" : ""}` })
1363
+ ]
1364
+ });
1365
+ };
1366
+ return ModelPickerTrigger;
1367
+ }, []);
1368
+ }
1369
+ /**
1370
+ * Render the shared model picker while it is registered; `options.fallback`
1371
+ * whenever it is not (plugin absent, disabled, or unloaded).
1372
+ *
1373
+ * `options.anchor` is optional: the factory ships a default trigger, so only
1374
+ * consumers that need their own chrome (custom aria-label, tooltip, class)
1375
+ * pass one built with {@link useModelMenuAnchor}. The returned node is never
1376
+ * assumed to be the factory's own output — a defensive `?? fallback` covers a
1377
+ * renderer that yields null for an unregistered definition.
1378
+ */
1379
+ function renderModelPicker(renderFactorySlot, props, options) {
1380
+ if (typeof renderFactorySlot !== "function") return options.fallback;
1381
+ return renderFactorySlot("klarkxy.model-picker", props, {
1382
+ ...options.anchor ? { slots: { ["anchor"]: options.anchor } } : {},
1383
+ fallback: options.fallback
1384
+ }) ?? options.fallback;
1385
+ }
992
1386
  Object.values({
993
1387
  /** Surfaces, from the page up to the topmost nested card. */
994
1388
  surface: [
@@ -1078,7 +1472,9 @@ Object.values({
1078
1472
  "--dsh-scrollbar-thumb-hover",
1079
1473
  "--dsh-content-font-size-secondary",
1080
1474
  "--dsh-content-font-delta",
1081
- "--dsh-frame-top-clearance"
1475
+ "--dsh-frame-top-clearance",
1476
+ "--dsh-composer-model-icon-display",
1477
+ "--dsh-composer-model-text-display"
1082
1478
  ]
1083
1479
  }).flat();
1084
1480
  /**
@@ -1805,7 +2201,7 @@ async function callAuxiliaryRpc(client, endpoint, payload = {}) {
1805
2201
  * The shared card. All state comes from the shared host service over the
1806
2202
  * plugin-kit RPC channel; the card only renders the stored row.
1807
2203
  */
1808
- function AuxiliaryModelCard({ client, subscribe }) {
2204
+ function AuxiliaryModelCard({ client, subscribe, renderFactorySlot }) {
1809
2205
  const language = useAuxiliaryLocale(client);
1810
2206
  const text = auxiliaryCopy[language];
1811
2207
  const [settings, setSettings] = (0, react.useState)();
@@ -1926,6 +2322,17 @@ function AuxiliaryModelCard({ client, subscribe }) {
1926
2322
  refreshCatalog
1927
2323
  ]);
1928
2324
  const locked = busy;
2325
+ const current = settings?.model ?? {
2326
+ provider: "",
2327
+ model: ""
2328
+ };
2329
+ const follow = !current.provider || !current.model;
2330
+ const selected = follow ? void 0 : choices.find((item) => item.provider === current.provider && item.model === current.model);
2331
+ const efforts = follow ? [] : modelMenuEffortOptions(selected, current.reasoningEffort);
2332
+ const currentLabel = follow ? text.modelDefault : selected?.label ?? `${current.provider} / ${current.model}`;
2333
+ const selectedEffort = efforts.find((item) => item.id === (current.reasoningEffort || ""));
2334
+ const effortName = selectedEffort ? modelMenuEffortLabel(selectedEffort, language) : void 0;
2335
+ const anchor = useModelMenuAnchor(`${text.model}: ${settings ? currentLabel : ""}`, void 0, () => setMenuOpen(false));
1929
2336
  if (!settings) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1930
2337
  className: "ams-root dsh-ui-card",
1931
2338
  "data-testid": "auxiliary-model-settings",
@@ -1964,16 +2371,6 @@ function AuxiliaryModelCard({ client, subscribe }) {
1964
2371
  })
1965
2372
  ]
1966
2373
  });
1967
- const current = settings.model ?? {
1968
- provider: "",
1969
- model: ""
1970
- };
1971
- const follow = !current.provider || !current.model;
1972
- const selected = follow ? void 0 : choices.find((item) => item.provider === current.provider && item.model === current.model);
1973
- const efforts = follow ? [] : modelMenuEffortOptions(selected, current.reasoningEffort);
1974
- const currentLabel = follow ? text.modelDefault : selected?.label ?? `${current.provider} / ${current.model}`;
1975
- const selectedEffort = efforts.find((item) => item.id === (current.reasoningEffort || ""));
1976
- const effortName = selectedEffort ? modelMenuEffortLabel(selectedEffort, language) : void 0;
1977
2374
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1978
2375
  className: "ams-root dsh-ui-card",
1979
2376
  "data-testid": "auxiliary-model-settings",
@@ -1997,28 +2394,12 @@ function AuxiliaryModelCard({ client, subscribe }) {
1997
2394
  state: catalogState,
1998
2395
  locale: language
1999
2396
  }),
2000
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
2397
+ renderModelPicker(renderFactorySlot, {
2001
2398
  locale: language,
2002
- open: menuOpen && !locked,
2399
+ disabled: locked,
2400
+ busy: locked,
2003
2401
  onOpenChange: (next) => setMenuOpen(next && !locked),
2004
2402
  listClassName: "ams-root ams-menu-portal",
2005
- anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2006
- type: "button",
2007
- className: "dsh-model-menu-trigger",
2008
- disabled: locked,
2009
- "aria-label": `${text.model}: ${currentLabel}`,
2010
- children: [
2011
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2012
- className: "dsh-model-menu-triggerLabel",
2013
- children: currentLabel
2014
- }),
2015
- !follow && efforts.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2016
- className: "dsh-model-menu-triggerEffort",
2017
- children: effortName ?? text.effortDefault
2018
- }) : null,
2019
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${menuOpen && !locked ? " dsh-model-menu-chevronOpen" : ""}` })
2020
- ]
2021
- }),
2022
2403
  choices,
2023
2404
  selected: follow ? void 0 : {
2024
2405
  provider: current.provider,
@@ -2041,9 +2422,61 @@ function AuxiliaryModelCard({ client, subscribe }) {
2041
2422
  model: current.model,
2042
2423
  ...effort ? { reasoningEffort: effort } : {}
2043
2424
  }),
2425
+ triggerLabel: currentLabel,
2426
+ triggerEffortLabel: !follow && efforts.length ? effortName ?? text.effortDefault : void 0,
2044
2427
  modelLabel: text.model,
2045
2428
  modelValue: currentLabel,
2046
2429
  defaultEffortLabel: text.effortDefault
2430
+ }, {
2431
+ anchor,
2432
+ fallback: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
2433
+ locale: language,
2434
+ open: menuOpen && !locked,
2435
+ onOpenChange: (next) => setMenuOpen(next && !locked),
2436
+ listClassName: "ams-root ams-menu-portal",
2437
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2438
+ type: "button",
2439
+ className: "dsh-model-menu-trigger",
2440
+ disabled: locked,
2441
+ "aria-label": `${text.model}: ${currentLabel}`,
2442
+ children: [
2443
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2444
+ className: "dsh-model-menu-triggerLabel",
2445
+ children: currentLabel
2446
+ }),
2447
+ !follow && efforts.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2448
+ className: "dsh-model-menu-triggerEffort",
2449
+ children: effortName ?? text.effortDefault
2450
+ }) : null,
2451
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${menuOpen && !locked ? " dsh-model-menu-chevronOpen" : ""}` })
2452
+ ]
2453
+ }),
2454
+ choices,
2455
+ selected: follow ? void 0 : {
2456
+ provider: current.provider,
2457
+ model: current.model
2458
+ },
2459
+ onPick: (picked) => save({
2460
+ provider: picked.provider,
2461
+ model: picked.model
2462
+ }),
2463
+ leading: [{
2464
+ id: "default",
2465
+ label: text.modelDefault,
2466
+ selected: follow
2467
+ }],
2468
+ onPickLeading: () => save(null),
2469
+ efforts,
2470
+ selectedEffort: current.reasoningEffort || "",
2471
+ onPickEffort: (effort) => save(follow ? null : {
2472
+ provider: current.provider,
2473
+ model: current.model,
2474
+ ...effort ? { reasoningEffort: effort } : {}
2475
+ }),
2476
+ modelLabel: text.model,
2477
+ modelValue: currentLabel,
2478
+ defaultEffortLabel: text.effortDefault
2479
+ })
2047
2480
  }),
2048
2481
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2049
2482
  className: "dsh-ui-hint",
@@ -2145,9 +2578,10 @@ function acquireAuxiliaryModelCard(root, ctx) {
2145
2578
  let unprovide;
2146
2579
  const watchLease = acquireWatchCoordinator(root).mount();
2147
2580
  const subscriber = createModelSettingsSubscriber(ctx.root, AUXILIARY_MODEL_PACKAGE, AUXILIARY_MODEL_CHANGES_IDENTITY, (ns) => ns === AUXILIARY_MODEL_NS);
2148
- const render = () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AuxiliaryModelCard, {
2581
+ const render = (props) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AuxiliaryModelCard, {
2149
2582
  client: root,
2150
- subscribe: subscriber.subscribe
2583
+ subscribe: subscriber.subscribe,
2584
+ renderFactorySlot: props.renderFactorySlot
2151
2585
  });
2152
2586
  const service = {
2153
2587
  retain() {
@@ -2403,7 +2837,7 @@ function GitBranchIcon() {
2403
2837
  ]
2404
2838
  });
2405
2839
  }
2406
- function GitCommitAction({ sessionId, useSession, call, t, client, subscribeSettings }) {
2840
+ function GitCommitAction({ sessionId, useSession, call, t, client, subscribeSettings, renderFactorySlot }) {
2407
2841
  const [open, setOpen] = (0, react.useState)(false);
2408
2842
  const [status, setStatus] = (0, react.useState)();
2409
2843
  const [loading, setLoading] = (0, react.useState)(false);
@@ -2619,6 +3053,7 @@ function GitCommitAction({ sessionId, useSession, call, t, client, subscribeSett
2619
3053
  client,
2620
3054
  subscribeSettings,
2621
3055
  disabled: running,
3056
+ renderFactorySlot,
2622
3057
  onSaving: markWrite,
2623
3058
  onWriteFailed: markWriteFailed,
2624
3059
  onReady: markSeen,
@@ -2919,6 +3354,17 @@ function GitCommitSettingsPanel(props) {
2919
3354
  props.disabled
2920
3355
  ]);
2921
3356
  const locked = busy || controlsLocked;
3357
+ const current = settings?.model ?? {
3358
+ provider: "",
3359
+ model: ""
3360
+ };
3361
+ const follow = !current.provider || !current.model;
3362
+ const selected = follow ? void 0 : choices.find((item) => item.provider === current.provider && item.model === current.model);
3363
+ const efforts = follow ? [] : modelMenuEffortOptions(selected, current.reasoningEffort);
3364
+ const currentLabel = follow ? text.modelDefault : selected?.label ?? `${current.provider} / ${current.model}`;
3365
+ const selectedEffort = efforts.find((item) => item.id === (current.reasoningEffort || ""));
3366
+ const effortName = selectedEffort ? modelMenuEffortLabel(selectedEffort, language) : void 0;
3367
+ const anchor = useModelMenuAnchor(`${text.model}: ${settings ? currentLabel : ""}`, void 0, () => setMenuOpen(false));
2922
3368
  if (!settings) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2923
3369
  className: "gcm-root gcm-settings dsh-ui-stack",
2924
3370
  children: [
@@ -2945,13 +3391,6 @@ function GitCommitSettingsPanel(props) {
2945
3391
  })
2946
3392
  ]
2947
3393
  });
2948
- const current = settings.model;
2949
- const follow = !current.provider || !current.model;
2950
- const selected = follow ? void 0 : choices.find((item) => item.provider === current.provider && item.model === current.model);
2951
- const efforts = follow ? [] : modelMenuEffortOptions(selected, current.reasoningEffort);
2952
- const currentLabel = follow ? text.modelDefault : selected?.label ?? `${current.provider} / ${current.model}`;
2953
- const selectedEffort = efforts.find((item) => item.id === (current.reasoningEffort || ""));
2954
- const effortName = selectedEffort ? modelMenuEffortLabel(selectedEffort, language) : void 0;
2955
3394
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2956
3395
  className: "gcm-root gcm-settings dsh-ui-stack",
2957
3396
  "data-testid": "git-commit-settings",
@@ -2967,28 +3406,12 @@ function GitCommitSettingsPanel(props) {
2967
3406
  state: catalogState,
2968
3407
  locale: language
2969
3408
  }),
2970
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
3409
+ renderModelPicker(props.renderFactorySlot, {
2971
3410
  locale: language,
2972
- open: menuOpen && !locked,
3411
+ disabled: locked,
3412
+ busy: locked,
2973
3413
  onOpenChange: (next) => setMenuOpen(next && !locked),
2974
3414
  listClassName: "gcm-root gcm-menu-portal",
2975
- anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2976
- type: "button",
2977
- className: "dsh-model-menu-trigger",
2978
- disabled: locked,
2979
- "aria-label": `${text.model}: ${currentLabel}`,
2980
- children: [
2981
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2982
- className: "dsh-model-menu-triggerLabel",
2983
- children: currentLabel
2984
- }),
2985
- !follow && efforts.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2986
- className: "dsh-model-menu-triggerEffort",
2987
- children: effortName ?? text.modelDefault
2988
- }) : null,
2989
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${menuOpen && !locked ? " dsh-model-menu-chevronOpen" : ""}` })
2990
- ]
2991
- }),
2992
3415
  choices,
2993
3416
  selected: follow ? void 0 : {
2994
3417
  provider: current.provider,
@@ -3017,9 +3440,67 @@ function GitCommitSettingsPanel(props) {
3017
3440
  model: current.model,
3018
3441
  ...effort ? { reasoningEffort: effort } : {}
3019
3442
  } }),
3443
+ triggerLabel: currentLabel,
3444
+ triggerEffortLabel: !follow && efforts.length ? effortName ?? text.modelDefault : void 0,
3020
3445
  modelLabel: text.model,
3021
3446
  modelValue: currentLabel,
3022
3447
  defaultEffortLabel: text.modelDefault
3448
+ }, {
3449
+ anchor,
3450
+ fallback: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
3451
+ locale: language,
3452
+ open: menuOpen && !locked,
3453
+ onOpenChange: (next) => setMenuOpen(next && !locked),
3454
+ listClassName: "gcm-root gcm-menu-portal",
3455
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3456
+ type: "button",
3457
+ className: "dsh-model-menu-trigger",
3458
+ disabled: locked,
3459
+ "aria-label": `${text.model}: ${currentLabel}`,
3460
+ children: [
3461
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3462
+ className: "dsh-model-menu-triggerLabel",
3463
+ children: currentLabel
3464
+ }),
3465
+ !follow && efforts.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3466
+ className: "dsh-model-menu-triggerEffort",
3467
+ children: effortName ?? text.modelDefault
3468
+ }) : null,
3469
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${menuOpen && !locked ? " dsh-model-menu-chevronOpen" : ""}` })
3470
+ ]
3471
+ }),
3472
+ choices,
3473
+ selected: follow ? void 0 : {
3474
+ provider: current.provider,
3475
+ model: current.model
3476
+ },
3477
+ onPick: (picked) => save({ model: {
3478
+ provider: picked.provider,
3479
+ model: picked.model
3480
+ } }),
3481
+ leading: [{
3482
+ id: "default",
3483
+ label: text.modelDefault,
3484
+ selected: follow
3485
+ }],
3486
+ onPickLeading: () => save({ model: {
3487
+ provider: "",
3488
+ model: ""
3489
+ } }),
3490
+ efforts,
3491
+ selectedEffort: current.reasoningEffort || "",
3492
+ onPickEffort: (effort) => save({ model: follow ? {
3493
+ provider: "",
3494
+ model: ""
3495
+ } : {
3496
+ provider: current.provider,
3497
+ model: current.model,
3498
+ ...effort ? { reasoningEffort: effort } : {}
3499
+ } }),
3500
+ modelLabel: text.model,
3501
+ modelValue: currentLabel,
3502
+ defaultEffortLabel: text.modelDefault
3503
+ })
3023
3504
  })
3024
3505
  ]
3025
3506
  }),