@klarkxy/dsh-current-title 0.2.4 → 0.3.0

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.
@@ -1,6 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  let react = require("react");
3
3
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
4
+ let react_dom = require("react-dom");
4
5
  let react_jsx_runtime = require("react/jsx-runtime");
5
6
  //#region ../dsh-plugin-kit/src/client-utils.ts
6
7
  /** Fixed pause for failing background reads; manual work is never replayed. */
@@ -477,6 +478,29 @@ ${scope} {
477
478
  & .dsh-ui-select:disabled { color: var(--dsw-alias-label-tertiary); cursor: default; }
478
479
  & .dsh-ui-select[aria-invalid='true'] { border-color: var(--dsw-alias-state-error-primary); }
479
480
 
481
+ /* --- Native textarea. The primitives ship no textarea either, so a settings
482
+ * prompt keeps the platform element and the same field shell as the select:
483
+ * 0.5px l4, radius-md, layer-3, 13/1.5 type. It grows vertically instead of
484
+ * locking to the select's 34px row. --- */
485
+ & .dsh-ui-textarea {
486
+ box-sizing: border-box;
487
+ width: 100%;
488
+ min-width: 0;
489
+ resize: vertical;
490
+ padding: 8px 12px;
491
+ border: 0.5px solid var(--dsw-alias-border-l4);
492
+ border-radius: var(--dsw-radius-md);
493
+ background: var(--dsw-alias-bg-layer-3);
494
+ font: inherit;
495
+ font-size: 13px;
496
+ line-height: 1.5;
497
+ color: var(--dsw-alias-label-primary);
498
+ }
499
+ & .dsh-ui-textarea:focus-visible { outline: none; border-color: var(--dsw-alias-state-business-primary); }
500
+ & .dsh-ui-textarea:disabled { color: var(--dsw-alias-label-tertiary); cursor: default; }
501
+ & .dsh-ui-textarea::placeholder { color: var(--dsw-alias-label-dimmed); }
502
+ & .dsh-ui-textarea[aria-invalid='true'] { border-color: var(--dsw-alias-state-error-primary); }
503
+
480
504
  /* Long panels scroll inside themselves instead of growing the host page. */
481
505
  & .dsh-ui-panel { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
482
506
  & .dsh-ui-panel-wide { max-width: 100%; }
@@ -489,25 +513,18 @@ ${scope} {
489
513
  }
490
514
  //#endregion
491
515
  //#region ../dsh-plugin-kit/src/contracts.ts
492
- /**
493
- * Remote contribution for model-settings change notifications.
494
- * Provided by `@klarkxy/dsh-model-hub` (`ModelSettingsChangesController`);
495
- * subscribers degrade silently when that package is not installed.
496
- * The stream carries invalidations only — `ready` is never a settings snapshot.
497
- */
498
- const MODEL_SETTINGS_CHANGES_REMOTE = {
499
- package: "@klarkxy/dsh-model-hub",
500
- descriptors: [{
501
- id: "modelSettingsChanges/watch",
502
- service: "modelSettingsChangesController",
503
- namespace: "modelSettingsChanges",
504
- method: "watch",
505
- mode: "stream",
506
- invocation: { kind: "direct" },
507
- parameters: [],
508
- cancellation: { parameter: "signal" }
509
- }]
510
- };
516
+ /** `@klarkxy/dsh-git-commit` becomes `KlarkxyDshGitCommitModelSettingsChanges`. */
517
+ function modelSettingsChangesIdentity(packageName) {
518
+ const scoped = /^@([^/]+)\/(.+)$/.exec(packageName);
519
+ const parts = (scoped ? `${scoped[1]}/${scoped[2]}` : packageName).split(/[^A-Za-z0-9]+/).filter(Boolean);
520
+ if (!parts.length) throw new Error("Model settings package name has no RPC segment");
521
+ const segment = `${parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("")}ModelSettingsChanges`;
522
+ if (!/^[A-Z][A-Za-z0-9]*$/.test(segment)) throw new Error(`Model settings sync name is not an RPC segment: ${segment}`);
523
+ return {
524
+ service: segment,
525
+ namespace: segment
526
+ };
527
+ }
511
528
  //#endregion
512
529
  //#region ../dsh-plugin-kit/src/model-settings-client.ts
513
530
  /** This stream carries invalidations only; ready is never a settings snapshot. */
@@ -522,17 +539,44 @@ function parseModelSettingsChangeFrame(value) {
522
539
  };
523
540
  throw new Error("Invalid model settings change frame");
524
541
  }
525
- MODEL_SETTINGS_CHANGES_REMOTE.package, MODEL_SETTINGS_CHANGES_REMOTE.descriptors.map((descriptor) => ({
526
- ...descriptor,
527
- result: {
528
- mode: "strict",
529
- typeSymbol: "ModelSettings.ChangeFrame",
530
- create: () => ({ parse: parseModelSettingsChangeFrame })
531
- }
532
- }));
542
+ /** One owner's contribution. Each package mounts only its own endpoint. */
543
+ function modelSettingsChangesRemote(packageName, identity = modelSettingsChangesIdentity(packageName)) {
544
+ return {
545
+ package: packageName,
546
+ descriptors: [{
547
+ id: `${identity.namespace}/watch`,
548
+ service: identity.service,
549
+ namespace: identity.namespace,
550
+ method: "watch",
551
+ mode: "stream",
552
+ invocation: { kind: "direct" },
553
+ parameters: [],
554
+ cancellation: { parameter: "signal" },
555
+ result: {
556
+ mode: "strict",
557
+ typeSymbol: "ModelSettings.ChangeFrame",
558
+ create: () => ({ parse: parseModelSettingsChangeFrame })
559
+ }
560
+ }]
561
+ };
562
+ }
563
+ /** Mount from a synchronous apply. Disposal waits for an in-flight mount and
564
+ * then withdraws it, so the contribution cannot outlive the plugin. */
565
+ function mountModelSettingsChanges(ctx, packageName) {
566
+ const contribution = modelSettingsChangesRemote(packageName);
567
+ ctx.inject(["remote"], (injected) => {
568
+ const scope = injected;
569
+ const mounting = scope.remote.$mount(contribution);
570
+ mounting.catch(() => {});
571
+ scope.effect(() => async () => {
572
+ await (await mounting.catch(() => void 0))?.();
573
+ });
574
+ });
575
+ }
533
576
  /** Native RemoteStream owns carrier reconnects; no timers or browser transport.
534
577
  * A factory shares one logical stream while at least one subscriber remains. */
535
- function createModelSettingsSubscriber(ctx, namespace) {
578
+ function createModelSettingsSubscriber(ctx, packageName, identity = modelSettingsChangesIdentity(packageName), accept = (ns) => ns === packageName) {
579
+ const endpoint = identity;
536
580
  const listeners = /* @__PURE__ */ new Set();
537
581
  let injection;
538
582
  let current;
@@ -558,12 +602,18 @@ function createModelSettingsSubscriber(ctx, namespace) {
558
602
  };
559
603
  const start = () => {
560
604
  const life = ++lifecycle;
561
- injection = ctx.inject(["remote", "remote.modelSettingsChanges"], (injected) => {
605
+ injection = ctx.inject(["remote", `remote.${endpoint.namespace}`], (injected) => {
562
606
  if (life !== lifecycle || !listeners.size) return;
563
607
  const scope = injected;
608
+ const service = scope.remote[endpoint.namespace];
609
+ if (!service || !("watch" in service) || !service.watch) {
610
+ report("Model settings sync service unavailable");
611
+ return;
612
+ }
613
+ const watch = service.watch;
564
614
  const stream = scope.remote.$stream({
565
615
  name: "Model settings sync",
566
- open: (signal) => scope.remote.modelSettingsChanges.watch(signal),
616
+ open: (signal) => watch(signal),
567
617
  ended: (accepted) => /* @__PURE__ */ new Error(`Model settings sync stream ended ${accepted ? "after" : "before"} ready; refresh or reopen the page`)
568
618
  });
569
619
  const session = {
@@ -598,7 +648,7 @@ function createModelSettingsSubscriber(ctx, namespace) {
598
648
  invalidate();
599
649
  } else {
600
650
  if (!ready) throw new Error("Model settings sync change arrived before ready");
601
- if (namespace === "*" || frame.ns === namespace) invalidate();
651
+ if (accept(frame.ns)) invalidate();
602
652
  }
603
653
  }
604
654
  } catch (error) {
@@ -644,6 +694,965 @@ function createModelSettingsSubscriber(ctx, namespace) {
644
694
  } });
645
695
  }
646
696
  //#endregion
697
+ //#region ../dsh-model-route/src/catalog.ts
698
+ /** Both services are required: Cordis checks the nested namespace separately. */
699
+ const MODEL_CATALOG_INJECT = ["remote", "remote.session"];
700
+ function asRecord(value) {
701
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
702
+ }
703
+ function asText(value) {
704
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
705
+ }
706
+ /** A useful diagnostic even when a transport rejects with a plain object. */
707
+ function modelCatalogErrorMessage(value) {
708
+ if (value instanceof Error && value.message) return value.message;
709
+ return asText(asRecord(value)?.message) ?? asText(value) ?? "Unable to load the model catalog.";
710
+ }
711
+ /**
712
+ * Validate and unwrap a successful catalog without discarding provider failures
713
+ * or newer host metadata. Names and metadata remain optional for older hosts.
714
+ * A broken response is never presented as a successfully loaded empty menu.
715
+ */
716
+ function checkedModelCatalog(value) {
717
+ const envelope = asRecord(value);
718
+ if (envelope?.ok === false) throw new Error(modelCatalogErrorMessage(envelope.error ?? envelope));
719
+ const root = envelope?.ok === true ? asRecord(envelope.value) : envelope;
720
+ const malformed = (field) => {
721
+ throw new Error(`Invalid model catalog: ${field}.`);
722
+ };
723
+ if (!root || !Array.isArray(root.groups)) return malformed("groups must be an array");
724
+ for (const value of root.groups) {
725
+ const group = asRecord(value);
726
+ if (!group || !asText(group.id) || !Array.isArray(group.models)) return malformed("each group needs an id and models array");
727
+ for (const value of group.models) {
728
+ const model = asRecord(value);
729
+ if (!model || !asText(model.id)) return malformed("each model needs an id");
730
+ if (model.reasoning !== void 0) {
731
+ const reasoning = asRecord(model.reasoning);
732
+ if (!reasoning || !Array.isArray(reasoning.efforts) || reasoning.efforts.some((value) => !parseEffort(value))) return malformed("reasoning efforts must be an array of identified efforts");
733
+ }
734
+ }
735
+ }
736
+ if (root.failures !== void 0 && (!Array.isArray(root.failures) || root.failures.some((value) => {
737
+ const failure = asRecord(value);
738
+ return !failure || !asText(failure.id) || typeof failure.name !== "string" || typeof failure.message !== "string";
739
+ }))) return malformed("failures must be an array of provider failures");
740
+ return root;
741
+ }
742
+ /** Read through the injected session namespace; absence and rejection are errors. */
743
+ async function readModelCatalog(client) {
744
+ const session = client.remote?.session;
745
+ if (typeof session?.modelCatalog !== "function") throw new Error("Model catalog unavailable: inject both remote and remote.session.");
746
+ return checkedModelCatalog(await session.modelCatalog());
747
+ }
748
+ /** Provider failures are partial results, not a transport failure. */
749
+ function modelCatalogFailures(value) {
750
+ try {
751
+ return checkedModelCatalog(value).failures ?? [];
752
+ } catch {
753
+ return [];
754
+ }
755
+ }
756
+ function parseEffort(value) {
757
+ const row = asRecord(value);
758
+ const id = row ? asText(row.id) : void 0;
759
+ if (!id) return void 0;
760
+ return {
761
+ id,
762
+ name: row ? asText(row.name) ?? id : id
763
+ };
764
+ }
765
+ /** Parse the host session catalog into flat choices, keeping a saved route selectable. */
766
+ function parseModelMenuChoices(value, bound) {
767
+ const envelope = asRecord(value);
768
+ const root = (envelope && envelope.ok === true ? asRecord(envelope.value) : void 0) ?? envelope ?? {};
769
+ const groups = Array.isArray(root.groups) ? root.groups : [];
770
+ const choices = [];
771
+ for (const group of groups) {
772
+ const groupRow = asRecord(group);
773
+ const provider = groupRow ? asText(groupRow.id) : void 0;
774
+ if (!provider || !groupRow || !Array.isArray(groupRow.models)) continue;
775
+ const providerName = asText(groupRow.name) ?? provider;
776
+ for (const entry of groupRow.models) {
777
+ const modelRow = asRecord(entry);
778
+ const model = modelRow ? asText(modelRow.id) : void 0;
779
+ if (!model || !modelRow) continue;
780
+ const name = asText(modelRow.name) ?? model;
781
+ const reasoning = asRecord(modelRow.reasoning);
782
+ const efforts = reasoning && Array.isArray(reasoning.efforts) ? reasoning.efforts.flatMap((effort) => {
783
+ const parsed = parseEffort(effort);
784
+ return parsed ? [parsed] : [];
785
+ }) : [];
786
+ choices.push({
787
+ provider,
788
+ model,
789
+ label: `${providerName} / ${name}`,
790
+ efforts
791
+ });
792
+ }
793
+ }
794
+ if (bound?.provider && bound.model && !choices.some((item) => item.provider === bound.provider && item.model === bound.model)) choices.push({
795
+ provider: bound.provider,
796
+ model: bound.model,
797
+ label: `${bound.provider} / ${bound.model}`,
798
+ efforts: []
799
+ });
800
+ return choices;
801
+ }
802
+ /** Reasoning-effort options for one choice; the saved value stays selectable. */
803
+ function modelMenuEffortOptions(choice, current) {
804
+ const listed = (choice?.efforts ?? []).map((item) => ({ ...item }));
805
+ if (current && !listed.some((item) => item.id === current)) listed.push({
806
+ id: current,
807
+ name: current
808
+ });
809
+ return listed;
810
+ }
811
+ //#endregion
812
+ //#region ../dsh-model-route/src/menu.ts
813
+ /** Localize standard effort ids only when the provider has not supplied a display name. */
814
+ function modelMenuEffortLabel(effort, locale) {
815
+ if (effort.name !== effort.id) return effort.name;
816
+ return {
817
+ off: ["关闭", "Off"],
818
+ none: ["无", "None"],
819
+ minimal: ["最低", "Minimal"],
820
+ low: ["低", "Low"],
821
+ medium: ["中", "Medium"],
822
+ high: ["高", "High"],
823
+ xhigh: ["极高", "Extra high"],
824
+ max: ["最高", "Maximum"],
825
+ ultra: ["超高", "Ultra"]
826
+ }[effort.id]?.[locale?.toLowerCase().startsWith("zh") ? 0 : 1] ?? effort.name;
827
+ }
828
+ /** The short model label shown inside a provider group. */
829
+ function modelMenuShortName(choice) {
830
+ const sep = choice.label.indexOf(" / ");
831
+ return sep > 0 ? choice.label.slice(sep + 3) : choice.model;
832
+ }
833
+ /** Group catalog choices by provider and filter them by a case-insensitive query. */
834
+ function groupModelMenuChoices(choices, query) {
835
+ const needle = query.trim().toLowerCase();
836
+ const matched = needle ? choices.filter((choice) => choice.label.toLowerCase().includes(needle) || choice.model.toLowerCase().includes(needle) || choice.provider.toLowerCase().includes(needle)) : choices;
837
+ const groups = [];
838
+ for (const choice of matched) {
839
+ const sep = choice.label.indexOf(" / ");
840
+ const name = sep > 0 ? choice.label.slice(0, sep) : choice.provider;
841
+ const last = groups.at(-1);
842
+ if (last && last.provider === choice.provider) last.items.push(choice);
843
+ else groups.push({
844
+ provider: choice.provider,
845
+ name,
846
+ items: [choice]
847
+ });
848
+ }
849
+ return groups;
850
+ }
851
+ //#endregion
852
+ //#region ../dsh-model-route/src/catalog-ui.tsx
853
+ /**
854
+ * Latest refresh wins, including retries while a previous request is pending.
855
+ * Keep load stable (for example with useCallback); changing it invalidates the
856
+ * previous source and fetches again, so switching clients cannot publish stale data.
857
+ */
858
+ function useModelCatalog(load, options = {}) {
859
+ const enabled = options.enabled !== false;
860
+ const enabledRef = (0, react.useRef)(enabled);
861
+ enabledRef.current = enabled;
862
+ const live = (0, react.useRef)(false);
863
+ const source = (0, react.useRef)(load);
864
+ const revision = (0, react.useRef)(0);
865
+ const [state, setState] = (0, react.useState)(() => {
866
+ try {
867
+ return {
868
+ catalog: options.initialCatalog === void 0 ? void 0 : checkedModelCatalog(options.initialCatalog),
869
+ loading: enabled,
870
+ error: ""
871
+ };
872
+ } catch (error) {
873
+ return {
874
+ catalog: void 0,
875
+ loading: enabled,
876
+ error: modelCatalogErrorMessage(error)
877
+ };
878
+ }
879
+ });
880
+ const refresh = (0, react.useCallback)(async () => {
881
+ if (!live.current || !enabledRef.current) return;
882
+ const request = ++revision.current;
883
+ setState((previous) => ({
884
+ ...previous,
885
+ loading: true,
886
+ error: ""
887
+ }));
888
+ try {
889
+ const catalog = checkedModelCatalog(await load());
890
+ if (live.current && request === revision.current) setState({
891
+ catalog,
892
+ loading: false,
893
+ error: ""
894
+ });
895
+ } catch (error) {
896
+ if (live.current && request === revision.current) setState((previous) => ({
897
+ ...previous,
898
+ loading: false,
899
+ error: modelCatalogErrorMessage(error)
900
+ }));
901
+ }
902
+ }, [load, enabled]);
903
+ (0, react.useEffect)(() => {
904
+ live.current = true;
905
+ if (source.current !== load) {
906
+ source.current = load;
907
+ setState({
908
+ catalog: void 0,
909
+ loading: enabled,
910
+ error: ""
911
+ });
912
+ }
913
+ if (enabled) refresh();
914
+ else setState((previous) => ({
915
+ ...previous,
916
+ loading: false
917
+ }));
918
+ return () => {
919
+ live.current = false;
920
+ ++revision.current;
921
+ };
922
+ }, [enabled, refresh]);
923
+ (0, react.useEffect)(() => {
924
+ if (!enabled) return;
925
+ const reload = () => {
926
+ refresh();
927
+ };
928
+ if (typeof window !== "undefined") window.addEventListener("focus", reload);
929
+ let unsubscribe;
930
+ try {
931
+ unsubscribe = options.subscribe?.(reload);
932
+ } catch (error) {
933
+ setState((previous) => ({
934
+ ...previous,
935
+ error: modelCatalogErrorMessage(error)
936
+ }));
937
+ }
938
+ return () => {
939
+ if (typeof window !== "undefined") window.removeEventListener("focus", reload);
940
+ unsubscribe?.();
941
+ };
942
+ }, [
943
+ enabled,
944
+ refresh,
945
+ options.subscribe
946
+ ]);
947
+ const choices = (0, react.useMemo)(() => parseModelMenuChoices(state.catalog), [state.catalog]);
948
+ const failures = (0, react.useMemo)(() => state.catalog === void 0 ? [] : modelCatalogFailures(state.catalog), [state.catalog]);
949
+ return {
950
+ ...state,
951
+ choices,
952
+ failures,
953
+ refresh
954
+ };
955
+ }
956
+ /** Also accepts a mapped state from a caller that owns broader async orchestration. */
957
+ function ModelCatalogStatus({ state, locale }) {
958
+ const zh = locale?.toLowerCase().startsWith("zh") === true;
959
+ const copy = zh ? {
960
+ loading: "正在加载模型…",
961
+ error: "模型列表加载失败",
962
+ partial: "部分提供商的模型未能加载",
963
+ retry: "重试"
964
+ } : {
965
+ loading: "Loading models…",
966
+ error: "Unable to load models",
967
+ partial: "Some providers could not load their models",
968
+ retry: "Retry"
969
+ };
970
+ if (!state.loading && !state.error && state.failures.length === 0) return null;
971
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
972
+ className: "dsh-model-catalog-status",
973
+ lang: zh ? "zh" : "en",
974
+ children: [
975
+ state.loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
976
+ role: "status",
977
+ children: copy.loading
978
+ }) : null,
979
+ state.error ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
980
+ role: "alert",
981
+ children: [
982
+ copy.error,
983
+ ": ",
984
+ state.error
985
+ ]
986
+ }) : null,
987
+ state.failures.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
988
+ role: "status",
989
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: copy.partial }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", { children: state.failures.map((failure, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
990
+ failure.name || failure.id,
991
+ ": ",
992
+ failure.message
993
+ ] }, `${failure.id}:${index}`)) })]
994
+ }) : null,
995
+ state.error || state.failures.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
996
+ type: "button",
997
+ size: "sm",
998
+ disabled: state.loading,
999
+ onClick: () => {
1000
+ state.refresh();
1001
+ },
1002
+ children: copy.retry
1003
+ }) : null
1004
+ ]
1005
+ });
1006
+ }
1007
+ //#endregion
1008
+ //#region ../dsh-model-route/src/ui.tsx
1009
+ /**
1010
+ * ModelMenu: the standard editor of one model-route field — the visible half
1011
+ * of the x-model-route contract. A feature plugin's settings page renders it
1012
+ * for its own marked field; the model hub renders one per detected field.
1013
+ *
1014
+ * It is a plugin-side copy of the host's composer model picker
1015
+ * (dsh-client-ui-model-selection's ModelSelect), rebuilt from the official
1016
+ * primitives and the published bundle's behavior: one card that opens on the
1017
+ * root pane (Model / Effort cells), drills into the provider-grouped,
1018
+ * searchable model list or the effort list, and Escapes back before closing.
1019
+ * Sticky group headings, subsequence ranking, highlight walking and focus
1020
+ * hand-back all match the host. SYNC POINT: when the host's ModelSelect or
1021
+ * ModelSelect.module.css changes, this copy must be re-transcribed; styles
1022
+ * below may only reference --dsw-* theme tokens, never literal colors.
1023
+ *
1024
+ * It statically imports the primitives, so only browser clients should load
1025
+ * this entry; host code and node tests use the core entry (`./index.ts`).
1026
+ */
1027
+ /** Unplaced portal card: hidden but laid out at a fixed origin so measurements are real. */
1028
+ const MEASURE_STYLE = {
1029
+ visibility: "hidden",
1030
+ left: 0,
1031
+ top: 0
1032
+ };
1033
+ function ModelMenu(props) {
1034
+ const language = props.locale?.toLowerCase().startsWith("zh") ? "zh" : "en";
1035
+ const copy = language === "zh" ? {
1036
+ model: "模型",
1037
+ effort: "思考强度",
1038
+ default: "默认",
1039
+ search: "搜索模型…",
1040
+ empty: "没有匹配的模型",
1041
+ clear: "清除搜索"
1042
+ } : {
1043
+ model: "Model",
1044
+ effort: "Reasoning",
1045
+ default: "Default",
1046
+ search: "Search models…",
1047
+ empty: "No matching models",
1048
+ clear: "Clear search"
1049
+ };
1050
+ 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;
1051
+ const rootRef = (0, react.useRef)(null);
1052
+ const openRef = (0, react.useRef)(open);
1053
+ openRef.current = open;
1054
+ const pendingOpenRef = (0, react.useRef)(false);
1055
+ const pendingRestoreFocus = (0, react.useRef)(false);
1056
+ const [requestedOpen, setRequestedOpen] = (0, react.useState)(false);
1057
+ const menuRef = (0, react.useRef)(null);
1058
+ const groupsRef = (0, react.useRef)(null);
1059
+ const searchRef = (0, react.useRef)(null);
1060
+ const itemRefs = (0, react.useRef)([]);
1061
+ const paneFocus = (0, react.useRef)(null);
1062
+ const [pane, setPane] = (0, react.useState)("root");
1063
+ const [query, setQuery] = (0, react.useState)("");
1064
+ const [highlightedIndex, setHighlightedIndex] = (0, react.useState)(null);
1065
+ const [menuPos, setMenuPos] = (0, react.useState)(null);
1066
+ const id = (0, react.useId)();
1067
+ const showSearch = choices.length > 4;
1068
+ const effortRows = (0, react.useMemo)(() => [{
1069
+ id: "",
1070
+ name: defaultEffortLabel
1071
+ }, ...efforts.map((effort) => ({
1072
+ ...effort,
1073
+ name: modelMenuEffortLabel(effort, language)
1074
+ }))], [
1075
+ defaultEffortLabel,
1076
+ efforts,
1077
+ language
1078
+ ]);
1079
+ const filteredGroups = (0, react.useMemo)(() => groupModelMenuChoices(choices, "").map((group) => ({
1080
+ ...group,
1081
+ items: (0, _deepseek_ai_dsh_client_ui_primitives.rankByName)(group.items.map((choice) => ({
1082
+ choice,
1083
+ name: modelMenuShortName(choice),
1084
+ label: choice.model
1085
+ })), showSearch ? query.trim() : "")
1086
+ })).filter((group) => group.items.length > 0), [
1087
+ choices,
1088
+ query,
1089
+ showSearch
1090
+ ]);
1091
+ const visibleModels = (0, react.useMemo)(() => filteredGroups.flatMap((group) => group.items.map((item) => item.choice)), [filteredGroups]);
1092
+ const currentVisibleIndex = visibleModels.findIndex((choice) => choice.provider === selected?.provider && choice.model === selected.model);
1093
+ const activeModelIndex = Math.min(highlightedIndex ?? Math.max(0, currentVisibleIndex), visibleModels.length - 1);
1094
+ const changeOpen = (next) => {
1095
+ if (openRef.current === next) return;
1096
+ openRef.current = next;
1097
+ onOpenChange(next);
1098
+ };
1099
+ const requestOpen = () => {
1100
+ if (open || pendingOpenRef.current) return;
1101
+ pendingOpenRef.current = true;
1102
+ pendingRestoreFocus.current = false;
1103
+ setRequestedOpen(true);
1104
+ };
1105
+ const close = (restoreFocus = false) => {
1106
+ const active = document.activeElement;
1107
+ pendingRestoreFocus.current = restoreFocus && (active === document.body || rootRef.current?.contains(active) === true || menuRef.current?.contains(active) === true);
1108
+ changeOpen(false);
1109
+ };
1110
+ const drill = (next) => {
1111
+ setQuery("");
1112
+ setHighlightedIndex(null);
1113
+ paneFocus.current = "drill";
1114
+ setPane(next);
1115
+ };
1116
+ /** Leave a drilled pane for the root one, handing the keyboard back to its cell. */
1117
+ const back = (from) => {
1118
+ paneFocus.current = from;
1119
+ setPane("root");
1120
+ };
1121
+ const moveFocus = (offset) => {
1122
+ const items = itemRefs.current.filter((item) => item !== null && !item.disabled);
1123
+ if (!items.length) return;
1124
+ const active = items.findIndex((item) => item === document.activeElement);
1125
+ items[active === -1 ? offset > 0 ? 0 : items.length - 1 : (active + offset + items.length) % items.length]?.focus();
1126
+ };
1127
+ (0, react.useLayoutEffect)(() => {
1128
+ if (open || !pendingRestoreFocus.current) return;
1129
+ const active = document.activeElement;
1130
+ if (active !== document.body && !rootRef.current?.contains(active)) {
1131
+ pendingRestoreFocus.current = false;
1132
+ return;
1133
+ }
1134
+ const trigger = rootRef.current?.querySelector("button");
1135
+ if (!trigger || trigger.disabled) return;
1136
+ trigger.focus();
1137
+ if (document.activeElement === trigger) pendingRestoreFocus.current = false;
1138
+ });
1139
+ (0, react.useEffect)(() => {
1140
+ if (open || !pendingRestoreFocus.current) return;
1141
+ const cancelOutside = (event) => {
1142
+ if (event.target instanceof Node && !rootRef.current?.contains(event.target)) pendingRestoreFocus.current = false;
1143
+ };
1144
+ document.addEventListener("focusin", cancelOutside);
1145
+ document.addEventListener("pointerdown", cancelOutside);
1146
+ return () => {
1147
+ document.removeEventListener("focusin", cancelOutside);
1148
+ document.removeEventListener("pointerdown", cancelOutside);
1149
+ };
1150
+ });
1151
+ (0, react.useEffect)(() => {
1152
+ if (!requestedOpen) return;
1153
+ pendingOpenRef.current = false;
1154
+ setRequestedOpen(false);
1155
+ if (!open) changeOpen(true);
1156
+ }, [requestedOpen, open]);
1157
+ (0, react.useEffect)(() => {
1158
+ if (!open) return;
1159
+ setPane("root");
1160
+ setQuery("");
1161
+ setHighlightedIndex(null);
1162
+ }, [open]);
1163
+ (0, react.useEffect)(() => {
1164
+ if (!open) return;
1165
+ const closeOutside = (event) => {
1166
+ const target = event.target;
1167
+ if (rootRef.current?.contains(target) || menuRef.current?.contains(target)) return;
1168
+ changeOpen(false);
1169
+ };
1170
+ document.addEventListener("mousedown", closeOutside);
1171
+ return () => document.removeEventListener("mousedown", closeOutside);
1172
+ }, [open, onOpenChange]);
1173
+ (0, react.useEffect)(() => {
1174
+ const intent = paneFocus.current;
1175
+ if (!open || menuPos === null || intent === null) return;
1176
+ if (intent !== "drill" && pane !== "root") return;
1177
+ paneFocus.current = null;
1178
+ if (intent === "drill") {
1179
+ if (pane === "model" && showSearch) {
1180
+ searchRef.current?.focus();
1181
+ return;
1182
+ }
1183
+ (menuRef.current?.querySelector("[role=\"menuitemradio\"][aria-checked=\"true\"]:not([disabled])") ?? itemRefs.current.find((item) => item !== null && !item.disabled))?.focus();
1184
+ return;
1185
+ }
1186
+ itemRefs.current[intent === "effort" ? 1 : 0]?.focus();
1187
+ }, [
1188
+ open,
1189
+ pane,
1190
+ showSearch,
1191
+ menuPos
1192
+ ]);
1193
+ (0, react.useEffect)(() => {
1194
+ const viewport = groupsRef.current;
1195
+ if (viewport === null) return;
1196
+ return (0, _deepseek_ai_dsh_client_ui_primitives.observeStickyMenuGroups)(viewport);
1197
+ }, [
1198
+ open,
1199
+ pane,
1200
+ filteredGroups
1201
+ ]);
1202
+ (0, react.useLayoutEffect)(() => {
1203
+ if (open && pane === "model" && activeModelIndex >= 0) itemRefs.current[leading.length + activeModelIndex]?.scrollIntoView({ block: "nearest" });
1204
+ }, [
1205
+ open,
1206
+ pane,
1207
+ activeModelIndex,
1208
+ visibleModels,
1209
+ leading.length
1210
+ ]);
1211
+ (0, react.useLayoutEffect)(() => {
1212
+ if (!open) {
1213
+ setMenuPos(null);
1214
+ return;
1215
+ }
1216
+ const place = () => {
1217
+ const rect = rootRef.current?.getBoundingClientRect();
1218
+ if (!rect) return;
1219
+ const margin = 12, lw = menuRef.current?.offsetWidth ?? 0, lh = menuRef.current?.offsetHeight ?? 0;
1220
+ let x = rect.right - lw, y = rect.top - 8 - lh;
1221
+ if (lw > 0) x = Math.min(Math.max(x, margin), window.innerWidth - lw - margin);
1222
+ if (lh > 0) y = Math.min(Math.max(y, margin), window.innerHeight - lh - margin);
1223
+ setMenuPos({
1224
+ left: x,
1225
+ top: y
1226
+ });
1227
+ };
1228
+ place();
1229
+ window.addEventListener("scroll", place, true);
1230
+ window.addEventListener("resize", place);
1231
+ return () => {
1232
+ window.removeEventListener("scroll", place, true);
1233
+ window.removeEventListener("resize", place);
1234
+ };
1235
+ }, [
1236
+ open,
1237
+ pane,
1238
+ choices,
1239
+ query
1240
+ ]);
1241
+ function onKeyDown(event) {
1242
+ if (event.defaultPrevented || event.nativeEvent.isComposing) return;
1243
+ if (!open && [
1244
+ "Enter",
1245
+ " ",
1246
+ "ArrowDown",
1247
+ "ArrowUp"
1248
+ ].includes(event.key)) {
1249
+ const button = event.target instanceof Element ? event.target.closest("button") : null;
1250
+ if (!(button instanceof HTMLButtonElement) || button.disabled || !rootRef.current?.contains(button)) return;
1251
+ event.preventDefault();
1252
+ paneFocus.current = "model";
1253
+ requestOpen();
1254
+ return;
1255
+ }
1256
+ if (event.key === "Escape" && open) {
1257
+ event.preventDefault();
1258
+ if (pane !== "root") back(pane);
1259
+ else close(true);
1260
+ return;
1261
+ }
1262
+ if (!open) return;
1263
+ if (pane === "model" && showSearch && (event.key === "ArrowDown" || event.key === "ArrowUp")) {
1264
+ event.preventDefault();
1265
+ if (visibleModels.length > 0) {
1266
+ setHighlightedIndex((activeModelIndex + (event.key === "ArrowDown" ? 1 : -1) + visibleModels.length) % visibleModels.length);
1267
+ searchRef.current?.focus();
1268
+ }
1269
+ return;
1270
+ }
1271
+ if (pane === "model" && showSearch && event.target instanceof HTMLInputElement && (event.key === "Enter" || event.key === "Tab" && !event.shiftKey)) {
1272
+ if (event.key === "Tab" && visibleModels.length === 0) return;
1273
+ event.preventDefault();
1274
+ const highlighted = visibleModels[activeModelIndex];
1275
+ if (highlighted) {
1276
+ onPick({
1277
+ provider: highlighted.provider,
1278
+ model: highlighted.model
1279
+ });
1280
+ close(true);
1281
+ }
1282
+ return;
1283
+ }
1284
+ if (event.key === "Tab") {
1285
+ if (event.shiftKey) {
1286
+ event.preventDefault();
1287
+ if (pane !== "root") back(pane);
1288
+ else close(true);
1289
+ return;
1290
+ }
1291
+ const focused = document.activeElement;
1292
+ const rows = itemRefs.current.filter((item) => item !== null);
1293
+ if (focused instanceof HTMLButtonElement && rows.includes(focused)) {
1294
+ event.preventDefault();
1295
+ focused.click();
1296
+ return;
1297
+ }
1298
+ if (pane === "model" && showSearch) {
1299
+ event.preventDefault();
1300
+ setHighlightedIndex(null);
1301
+ searchRef.current?.focus();
1302
+ }
1303
+ return;
1304
+ }
1305
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
1306
+ event.preventDefault();
1307
+ moveFocus(event.key === "ArrowDown" ? 1 : -1);
1308
+ }
1309
+ }
1310
+ function onBlur(event) {
1311
+ if (event.relatedTarget instanceof Node && (rootRef.current?.contains(event.relatedTarget) || menuRef.current?.contains(event.relatedTarget))) return;
1312
+ if (open) changeOpen(false);
1313
+ }
1314
+ itemRefs.current = [];
1315
+ let itemIndex = 0;
1316
+ let modelIndex = 0;
1317
+ const itemRef = () => {
1318
+ const at = itemIndex++;
1319
+ return (node) => {
1320
+ itemRefs.current[at] = node;
1321
+ };
1322
+ };
1323
+ const menu = open && typeof document !== "undefined" ? (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.MenuSurface, {
1324
+ ref: menuRef,
1325
+ id: `${id}-menu`,
1326
+ className: `dsh-model-menu-menu${listClassName ? ` ${listClassName}` : ""}`,
1327
+ style: menuPos ?? MEASURE_STYLE,
1328
+ role: pane === "model" ? "group" : "menu",
1329
+ "aria-label": modelLabel,
1330
+ lang: language,
1331
+ children: [
1332
+ pane === "root" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1333
+ ref: itemRef(),
1334
+ type: "button",
1335
+ role: "menuitem",
1336
+ className: "dsh-model-menu-cell",
1337
+ onClick: () => drill("model"),
1338
+ children: [
1339
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1340
+ className: "dsh-model-menu-cellLabel",
1341
+ children: modelLabel
1342
+ }),
1343
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1344
+ className: "dsh-model-menu-cellValue",
1345
+ children: modelValue
1346
+ }),
1347
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutlineRegular, { className: "dsh-model-menu-cellChevron" })
1348
+ ]
1349
+ }), efforts.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1350
+ ref: itemRef(),
1351
+ type: "button",
1352
+ role: "menuitem",
1353
+ className: "dsh-model-menu-cell",
1354
+ onClick: () => drill("effort"),
1355
+ children: [
1356
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1357
+ className: "dsh-model-menu-cellLabel",
1358
+ children: effortLabel
1359
+ }),
1360
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1361
+ className: "dsh-model-menu-cellValue",
1362
+ children: effortValue ?? effortRows.find((row) => row.id === (selectedEffort ?? ""))?.name ?? defaultEffortLabel
1363
+ }),
1364
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutlineRegular, { className: "dsh-model-menu-cellChevron" })
1365
+ ]
1366
+ }) : null] }) : null,
1367
+ pane === "model" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1368
+ showSearch ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1369
+ className: "dsh-model-menu-searchRow",
1370
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
1371
+ ref: searchRef,
1372
+ className: `dsh-model-menu-search${query !== "" ? " dsh-model-menu-searchWithQuery" : ""}`,
1373
+ type: "text",
1374
+ role: "searchbox",
1375
+ "aria-label": searchPlaceholder,
1376
+ "aria-controls": `${id}-models`,
1377
+ "aria-activedescendant": activeModelIndex < 0 ? void 0 : `${id}-model-${activeModelIndex}`,
1378
+ placeholder: searchPlaceholder,
1379
+ value: query,
1380
+ onChange: (event) => {
1381
+ setQuery(event.target.value);
1382
+ setHighlightedIndex(0);
1383
+ }
1384
+ }), query !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1385
+ type: "button",
1386
+ className: "dsh-model-menu-searchClear",
1387
+ "aria-label": clearSearchLabel,
1388
+ onClick: () => {
1389
+ setQuery("");
1390
+ setHighlightedIndex(null);
1391
+ searchRef.current?.focus();
1392
+ },
1393
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseFillRegular, {})
1394
+ }) : null]
1395
+ }) : null,
1396
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1397
+ ref: groupsRef,
1398
+ id: `${id}-models`,
1399
+ className: "dsh-model-menu-groups scrollable",
1400
+ role: "menu",
1401
+ "aria-label": modelLabel,
1402
+ hidden: filteredGroups.length === 0,
1403
+ children: [leading.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1404
+ className: "dsh-model-menu-leading",
1405
+ children: leading.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1406
+ ref: itemRef(),
1407
+ type: "button",
1408
+ role: "menuitemradio",
1409
+ "aria-checked": item.selected === true,
1410
+ className: `dsh-model-menu-option${item.selected === true ? " dsh-model-menu-selected" : ""}`,
1411
+ onClick: () => {
1412
+ onPickLeading?.(item.id);
1413
+ close(true);
1414
+ },
1415
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1416
+ className: "dsh-model-menu-optionCopy",
1417
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1418
+ className: "dsh-model-menu-modelName",
1419
+ children: item.label
1420
+ })
1421
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1422
+ className: "dsh-model-menu-check",
1423
+ children: item.selected === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, {}) : null
1424
+ })]
1425
+ }, item.id))
1426
+ }) : null, filteredGroups.map((group) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MenuGroup, {
1427
+ label: group.name,
1428
+ children: group.items.map((item) => {
1429
+ const index = modelIndex++;
1430
+ const choice = item.choice;
1431
+ const checked = selected?.provider === choice.provider && selected.model === choice.model;
1432
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1433
+ ref: itemRef(),
1434
+ type: "button",
1435
+ role: "menuitemradio",
1436
+ "aria-checked": checked,
1437
+ id: `${id}-model-${index}`,
1438
+ tabIndex: showSearch ? -1 : 0,
1439
+ onFocus: () => setHighlightedIndex(index),
1440
+ "data-highlighted": index === activeModelIndex ? "" : void 0,
1441
+ className: `dsh-model-menu-option dsh-model-menu-modelOption${checked ? " dsh-model-menu-selected" : ""}${index === activeModelIndex ? " dsh-model-menu-optionActive" : ""}`,
1442
+ onMouseMove: index === activeModelIndex ? void 0 : () => {
1443
+ if (showSearch) setHighlightedIndex(index);
1444
+ else itemRefs.current[leading.length + index]?.focus();
1445
+ },
1446
+ title: choice.label,
1447
+ onClick: () => {
1448
+ onPick({
1449
+ provider: choice.provider,
1450
+ model: choice.model
1451
+ });
1452
+ close(true);
1453
+ },
1454
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1455
+ className: "dsh-model-menu-optionCopy",
1456
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1457
+ className: "dsh-model-menu-modelName",
1458
+ children: modelMenuShortName(choice)
1459
+ })
1460
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1461
+ className: "dsh-model-menu-check",
1462
+ children: checked ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, {}) : null
1463
+ })]
1464
+ }, `${choice.provider}/${choice.model}`);
1465
+ })
1466
+ }, group.provider))]
1467
+ }),
1468
+ filteredGroups.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1469
+ className: "dsh-model-menu-empty",
1470
+ role: "status",
1471
+ children: emptyLabel
1472
+ }) : null
1473
+ ] }) : null,
1474
+ pane === "effort" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: effortRows.map((row) => {
1475
+ const checked = (selectedEffort ?? "") === row.id;
1476
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1477
+ ref: itemRef(),
1478
+ type: "button",
1479
+ role: "menuitemradio",
1480
+ "aria-checked": checked,
1481
+ className: `dsh-model-menu-option${checked ? " dsh-model-menu-selected" : ""}`,
1482
+ onClick: () => {
1483
+ onPickEffort?.(row.id || void 0);
1484
+ close(true);
1485
+ },
1486
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1487
+ className: "dsh-model-menu-optionCopy",
1488
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1489
+ className: "dsh-model-menu-modelName",
1490
+ children: row.name
1491
+ })
1492
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1493
+ className: "dsh-model-menu-check",
1494
+ children: checked ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, {}) : null
1495
+ })]
1496
+ }, row.id || "__default");
1497
+ }) }) : null
1498
+ ]
1499
+ }), document.body) : null;
1500
+ const trigger = (0, react.isValidElement)(anchor) ? (0, react.cloneElement)(anchor, {
1501
+ "aria-haspopup": "menu",
1502
+ "aria-expanded": open,
1503
+ "aria-controls": open ? `${id}-menu` : void 0
1504
+ }) : anchor;
1505
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1506
+ ref: rootRef,
1507
+ className: `dsh-model-menu-root${className ? ` ${className}` : ""}`,
1508
+ onKeyDown,
1509
+ onBlur,
1510
+ onClick: (event) => {
1511
+ if (event.defaultPrevented) return;
1512
+ const button = event.target instanceof Element ? event.target.closest("button") : null;
1513
+ if (!(button instanceof HTMLButtonElement) || button.disabled || !rootRef.current?.contains(button)) return;
1514
+ requestOpen();
1515
+ },
1516
+ onMouseDown: (event) => {
1517
+ if (event.target instanceof Element && menuRef.current?.contains(event.target) && event.target.closest("button") !== null) event.preventDefault();
1518
+ },
1519
+ children: [trigger, menu]
1520
+ });
1521
+ }
1522
+ /**
1523
+ * ModelMenu styles, transcribed from the official ModelSelect.module.css (the
1524
+ * published dsh-client-ui-model-selection bundle): card capped at
1525
+ * min(360px, 100vh - 96px), 34px option cells at 13/18 with 5px 7px padding,
1526
+ * 34px root cells at 13/20, a borderless 12px search row with a clear button,
1527
+ * sticky group headings (MenuGroup's own CSS), and the l1-stroke prominent
1528
+ * elevation at portal z-index 1100. The trigger mirrors the composer's 28px
1529
+ * model pill, chevron rotating open. MenuSurface paints fill and backdrop.
1530
+ * Only --dsw-* theme tokens may appear here — a literal color is a bug.
1531
+ */
1532
+ const modelMenuCss = `
1533
+ .dsh-model-menu-root { min-width: 0; position: relative; display: inline-flex; }
1534
+ .dsh-model-menu-trigger {
1535
+ border-radius: var(--dsw-radius-sm);
1536
+ min-width: 0;
1537
+ max-width: min(360px, 45cqw);
1538
+ height: 28px;
1539
+ color: var(--dsw-alias-label-secondary);
1540
+ cursor: pointer;
1541
+ background: none;
1542
+ border: none;
1543
+ outline: none;
1544
+ align-items: center;
1545
+ gap: 4px;
1546
+ padding: 0 4px 0 8px;
1547
+ font-size: 13px;
1548
+ font-weight: 400;
1549
+ line-height: 20px;
1550
+ display: flex;
1551
+ }
1552
+ .dsh-model-menu-trigger:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
1553
+ .dsh-model-menu-trigger:focus-visible:not([data-selection-focus]) { box-shadow: 0 0 0 2px var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); }
1554
+ .dsh-model-menu-trigger:disabled { color: var(--dsw-alias-label-dimmed); cursor: default; }
1555
+ .dsh-model-menu-triggerLabel { text-overflow: ellipsis; white-space: nowrap; min-width: 0; overflow: hidden; }
1556
+ .dsh-model-menu-triggerEffort { text-overflow: ellipsis; white-space: nowrap; min-width: 0; color: var(--dsw-alias-label-caption); flex-shrink: 1000; overflow: hidden; }
1557
+ .dsh-model-menu-chevron { color: var(--dsw-alias-label-caption); flex: none; transition: transform 0.12s; }
1558
+ .dsh-model-menu-chevronOpen { transform: rotate(180deg); }
1559
+ .dsh-model-menu-menu {
1560
+ z-index: 1100;
1561
+ --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
1562
+ width: max-content;
1563
+ min-width: min(240px, 100vw - 32px);
1564
+ max-width: min(420px, 100vw - 32px);
1565
+ max-height: min(360px, 100vh - 96px);
1566
+ box-shadow: var(--dsw-elevation-prominent);
1567
+ color: var(--dsw-alias-label-primary);
1568
+ --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);
1569
+ --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);
1570
+ border: 0;
1571
+ flex-direction: column;
1572
+ padding: 4px;
1573
+ display: flex;
1574
+ position: fixed;
1575
+ overflow: hidden;
1576
+ }
1577
+ .dsh-model-menu-status, .dsh-model-menu-empty { color: var(--dsw-alias-label-tertiary); padding: 8px; font-size: 12px; line-height: 18px; }
1578
+ .dsh-model-menu-searchRow { flex-shrink: 0; margin: 2px 0 3px; position: relative; }
1579
+ .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; }
1580
+ .dsh-model-menu-searchRow .dsh-model-menu-search:focus-within { border-color: transparent; }
1581
+ .dsh-model-menu-searchWithQuery { padding-right: 34px; }
1582
+ .dsh-model-menu-searchRow .dsh-model-menu-search input { padding: 0; font-size: 12px; line-height: normal; }
1583
+ .dsh-model-menu-searchRow .dsh-model-menu-search input::placeholder { color: var(--dsw-alias-label-caption); }
1584
+ .dsh-model-menu-searchClear {
1585
+ width: 24px;
1586
+ height: 24px;
1587
+ color: var(--dsw-alias-label-secondary);
1588
+ cursor: pointer;
1589
+ background: none;
1590
+ border: none;
1591
+ border-radius: 50%;
1592
+ justify-content: center;
1593
+ align-items: center;
1594
+ padding: 0;
1595
+ display: inline-flex;
1596
+ position: absolute;
1597
+ top: 50%;
1598
+ right: 4px;
1599
+ transform: translateY(-50%);
1600
+ }
1601
+ .dsh-model-menu-searchClear:hover, .dsh-model-menu-searchClear:focus-visible { background: var(--dsw-alias-interactive-bg-hover); outline: none; }
1602
+ .dsh-model-menu-groups { min-height: 0; overflow-y: auto; }
1603
+ .dsh-model-menu-leading { border-bottom: 0.5px solid var(--dsw-alias-border-l2); margin-bottom: 3px; padding-bottom: 3px; }
1604
+ .dsh-model-menu-option {
1605
+ box-sizing: border-box;
1606
+ border-radius: var(--dsw-radius-md);
1607
+ width: auto;
1608
+ min-width: 100%;
1609
+ min-height: 34px;
1610
+ color: inherit;
1611
+ text-align: left;
1612
+ cursor: pointer;
1613
+ background: none;
1614
+ border: none;
1615
+ outline: none;
1616
+ align-items: center;
1617
+ gap: 6px;
1618
+ padding: 5px 7px;
1619
+ display: flex;
1620
+ }
1621
+ .dsh-model-menu-option:not(.dsh-model-menu-modelOption):hover:not(:disabled),
1622
+ .dsh-model-menu-option:focus-visible,
1623
+ .dsh-model-menu-optionActive:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
1624
+ .dsh-model-menu-modelOption:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
1625
+ .dsh-model-menu-selected { background: none; }
1626
+ .dsh-model-menu-option:disabled { color: var(--dsw-alias-label-dimmed); cursor: default; }
1627
+ .dsh-model-menu-optionCopy { flex-direction: column; flex: 1; min-width: 0; display: flex; }
1628
+ .dsh-model-menu-modelName { color: inherit; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; font-weight: 400; line-height: 18px; overflow: hidden; }
1629
+ .dsh-model-menu-check { color: var(--dsw-alias-label-primary); flex: 0 0 14px; place-items: center; display: grid; }
1630
+ .dsh-model-menu-check svg { width: 14px; height: 14px; }
1631
+ .dsh-model-menu-cell {
1632
+ box-sizing: border-box;
1633
+ border-radius: var(--dsw-radius-md);
1634
+ width: auto;
1635
+ min-width: 100%;
1636
+ height: 34px;
1637
+ color: var(--dsw-alias-label-primary);
1638
+ cursor: pointer;
1639
+ text-align: left;
1640
+ background: none;
1641
+ border: none;
1642
+ outline: none;
1643
+ align-items: center;
1644
+ gap: 6px;
1645
+ padding: 0 8px;
1646
+ font-size: 13px;
1647
+ line-height: 20px;
1648
+ display: flex;
1649
+ }
1650
+ .dsh-model-menu-cell:hover, .dsh-model-menu-cell:focus-visible { background: var(--dsw-alias-interactive-bg-hover); }
1651
+ .dsh-model-menu-cellLabel { white-space: nowrap; flex: none; }
1652
+ .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; }
1653
+ .dsh-model-menu-cellChevron { width: 12px; height: 12px; color: var(--dsw-alias-menu-icon); flex: none; }
1654
+ `;
1655
+ //#endregion
647
1656
  //#region src/contracts.ts
648
1657
  const RPC_CHANNEL = "/dsh-current-title";
649
1658
  const TITLE_CLIENT_SERVICE = "dshCurrentTitleClient";
@@ -652,6 +1661,8 @@ const TITLE_CLIENT_SERVICE = "dshCurrentTitleClient";
652
1661
  const titleCopy = {
653
1662
  zh: {
654
1663
  label: "自动标题",
1664
+ session: "当前会话",
1665
+ noSession: "没有打开的会话。打开一个会话后,可以在这里查看并重新生成标题。",
655
1666
  pinned: "已手动命名",
656
1667
  generating: "正在生成标题…",
657
1668
  regenerate: "重新生成",
@@ -671,10 +1682,13 @@ const titleCopy = {
671
1682
  retry: "重试",
672
1683
  externalChange: "设置已在别处修改。提示词草稿已保留,请重新加载后确认。",
673
1684
  syncUnavailable: "自动刷新不可用,请手动重新加载设置。",
674
- reload: "重新加载(丢弃草稿)"
1685
+ reload: "重新加载(丢弃草稿)",
1686
+ defaults: "默认设置"
675
1687
  },
676
1688
  en: {
677
1689
  label: "Auto Title",
1690
+ session: "Current session",
1691
+ noSession: "No session is open. Open one to see its title and regenerate it here.",
678
1692
  pinned: "Renamed manually",
679
1693
  generating: "Generating title…",
680
1694
  regenerate: "Regenerate",
@@ -694,7 +1708,8 @@ const titleCopy = {
694
1708
  retry: "Retry",
695
1709
  externalChange: "Settings changed elsewhere. Prompt draft preserved; reload and review.",
696
1710
  syncUnavailable: "Automatic refresh unavailable; reload settings manually.",
697
- reload: "Reload (discard draft)"
1711
+ reload: "Reload (discard draft)",
1712
+ defaults: "Default settings"
698
1713
  }
699
1714
  };
700
1715
  /** Only plugin-owned defaults are translated; model output and remote diagnostics stay intact. */
@@ -841,44 +1856,12 @@ const name = "dsh-current-title-client";
841
1856
  const inject = [
842
1857
  "slots",
843
1858
  "connection",
844
- "remote",
845
- "remote.session",
1859
+ ...MODEL_CATALOG_INJECT,
846
1860
  "sessions",
847
1861
  "locale",
848
1862
  "uiWorkspace",
849
1863
  "uiSession"
850
1864
  ];
851
- function catalogChoices(value, selected) {
852
- const envelope = value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
853
- const root = (envelope?.ok === true && envelope.value && typeof envelope.value === "object" && !Array.isArray(envelope.value) ? envelope.value : void 0) ?? envelope ?? {};
854
- const groups = Array.isArray(root.groups) ? root.groups : [];
855
- const choices = [];
856
- for (const group of groups) {
857
- if (!group || typeof group !== "object") continue;
858
- const row = group;
859
- const provider = typeof row.id === "string" ? row.id : "";
860
- const providerName = typeof row.name === "string" && row.name ? row.name : provider;
861
- if (!provider || !Array.isArray(row.models)) continue;
862
- for (const model of row.models) {
863
- if (!model || typeof model !== "object") continue;
864
- const item = model;
865
- const id = typeof item.id === "string" ? item.id : "";
866
- if (!id) continue;
867
- const name = typeof item.name === "string" && item.name ? item.name : id;
868
- choices.push({
869
- provider,
870
- model: id,
871
- label: `${providerName} / ${name}`
872
- });
873
- }
874
- }
875
- if (selected.provider && selected.model && !choices.some((item) => item.provider === selected.provider && item.model === selected.model)) choices.push({
876
- provider: selected.provider,
877
- model: selected.model,
878
- label: `${selected.provider} / ${selected.model}`
879
- });
880
- return choices;
881
- }
882
1865
  function TitleSettings(props) {
883
1866
  const seat = useNativeSeat(props.client);
884
1867
  const sessionId = seat.sessionId;
@@ -889,7 +1872,10 @@ function TitleSettings(props) {
889
1872
  const [note, setNote] = (0, react.useState)("");
890
1873
  const [error, setError] = (0, react.useState)("");
891
1874
  const [prompt, setPrompt] = (0, react.useState)(void 0);
892
- const [choices, setChoices] = (0, react.useState)([]);
1875
+ const catalogState = useModelCatalog((0, react.useCallback)(() => readModelCatalog(props.client), [props.client]), { subscribe: (0, react.useCallback)((change) => props.client.connection.generation?.subscribe(change), [props.client]) });
1876
+ const { refresh: refreshCatalog } = catalogState;
1877
+ const choices = parseModelMenuChoices(catalogState.catalog, status?.settings?.model);
1878
+ const [menuOpen, setMenuOpen] = (0, react.useState)(false);
893
1879
  const [externalChange, setExternalChange] = (0, react.useState)(false);
894
1880
  const [syncError, setSyncError] = (0, react.useState)("");
895
1881
  const [loadingSettings, setLoadingSettings] = (0, react.useState)(true);
@@ -932,26 +1918,6 @@ function TitleSettings(props) {
932
1918
  work,
933
1919
  readGate
934
1920
  ]);
935
- (0, react.useEffect)(() => {
936
- const catalog = props.client.remote?.session?.modelCatalog;
937
- if (!catalog) return void 0;
938
- let live = true;
939
- catalog().then((value) => {
940
- if (live) setChoices(catalogChoices(value, status?.settings.model ?? {
941
- provider: "",
942
- model: ""
943
- }));
944
- }).catch(() => {
945
- if (live) setChoices([]);
946
- });
947
- return () => {
948
- live = false;
949
- };
950
- }, [
951
- props.client,
952
- status?.settings.model.provider,
953
- status?.settings.model.model
954
- ]);
955
1921
  (0, react.useEffect)(() => {
956
1922
  const generation = work.beginLoad();
957
1923
  reading.current = true;
@@ -1053,6 +2019,7 @@ function TitleSettings(props) {
1053
2019
  const stop = props.subscribeSettings?.(() => {
1054
2020
  if (!live) return;
1055
2021
  setSyncError("");
2022
+ refreshCatalog();
1056
2023
  if (dirty.current && !busyRef.current) {
1057
2024
  external.current = true;
1058
2025
  setExternalChange(true);
@@ -1065,17 +2032,25 @@ function TitleSettings(props) {
1065
2032
  live = false;
1066
2033
  stop?.();
1067
2034
  };
1068
- }, [props.subscribeSettings, props.client]);
1069
- const savedPrompt = status?.settings.prompt ?? "";
1070
- const savedModel = status?.settings.model ?? {
2035
+ }, [
2036
+ props.subscribeSettings,
2037
+ props.client,
2038
+ refreshCatalog
2039
+ ]);
2040
+ const savedPrompt = status?.settings?.prompt ?? "";
2041
+ const savedModel = status?.settings?.model ?? {
1071
2042
  provider: "",
1072
2043
  model: ""
1073
2044
  };
1074
- const selectableChoices = choices.some((choice) => choice.provider === savedModel.provider && choice.model === savedModel.model) ? choices : [...choices, ...catalogChoices({}, savedModel)];
1075
- const modelKey = savedModel.provider && savedModel.model ? `${savedModel.provider}\u001f${savedModel.model}` : "";
2045
+ const follow = !savedModel.provider || !savedModel.model;
2046
+ const selectedModel = follow ? void 0 : choices.find((choice) => choice.provider === savedModel.provider && choice.model === savedModel.model);
2047
+ const efforts = follow ? [] : modelMenuEffortOptions(selectedModel, savedModel.reasoningEffort);
2048
+ const currentLabel = follow ? text.modelDefault : selectedModel?.label ?? `${savedModel.provider} / ${savedModel.model}`;
2049
+ const selectedEffort = efforts.find((item) => item.id === (savedModel.reasoningEffort || ""));
2050
+ const effortName = selectedEffort ? modelMenuEffortLabel(selectedEffort, seat.locale) : void 0;
1076
2051
  const title = status?.session?.title;
1077
2052
  const pinned = status?.session?.pinned === true;
1078
- const weOwn = status?.support.weOwn === true;
2053
+ const weOwn = status?.support?.weOwn === true;
1079
2054
  const canRegenerate = Boolean(sessionId) && weOwn && !busy && !loadingSettings;
1080
2055
  function saveSettings(patch) {
1081
2056
  if (!status || busyRef.current || reading.current || external.current) return;
@@ -1109,225 +2084,272 @@ function TitleSettings(props) {
1109
2084
  className: "dsh-ui-hint",
1110
2085
  children: text.loading
1111
2086
  }) : null,
1112
- title ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1113
- translate: "no",
1114
- className: "dsh-ui-title dsh-ui-wrap",
1115
- children: title
1116
- }) : null,
1117
- pinned ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1118
- className: "dsh-ui-meta",
1119
- children: text.pinned
1120
- }) : null,
1121
- status?.session?.generating ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1122
- role: "status",
1123
- className: "dsh-ui-hint",
1124
- children: text.generating
1125
- }) : null,
1126
- status && !weOwn ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1127
- role: "status",
1128
- className: "dsh-ui-hint",
1129
- children: [text.inactive, status.support.limitation ? ` ${titleMessage(status.support.limitation, seat.locale)}` : ""]
1130
- }) : null,
1131
- error ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1132
- role: "alert",
1133
- className: "dsh-ui-error dsh-ui-wrap",
1134
- children: titleMessage(error, seat.locale)
1135
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1136
- variant: "outline",
1137
- size: "sm",
1138
- disabled: busy,
1139
- onClick: () => {
1140
- readGate.reset();
1141
- refreshStatus();
1142
- },
1143
- children: text.retry
1144
- })] }) : null,
1145
- note ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1146
- role: "status",
1147
- className: "dsh-ui-notice",
1148
- children: titleMessage(note, seat.locale)
1149
- }) : null,
1150
- externalChange ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1151
- role: "alert",
1152
- className: "dsh-ui-error",
1153
- children: text.externalChange
1154
- }) : null,
1155
- syncError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1156
- role: "status",
1157
- className: "dsh-ui-hint",
1158
- children: text.syncUnavailable
1159
- }) : null,
1160
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1161
- className: "dsh-ui-actions",
1162
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1163
- variant: "ghost",
1164
- size: "sm",
1165
- disabled: busy || loadingSettings,
1166
- onClick: () => {
1167
- setPrompt(void 0);
1168
- dirty.current = false;
1169
- external.current = false;
1170
- setExternalChange(false);
1171
- readGate.reset();
1172
- syncRef.current();
1173
- },
1174
- children: text.reload
1175
- }), sessionId ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1176
- variant: "outline",
1177
- size: "md",
1178
- disabled: !canRegenerate,
1179
- onClick: () => {
1180
- if (!sessionId) return;
1181
- readGate.reset();
1182
- const token = work.beginRequest();
1183
- setError("");
1184
- setNote("");
1185
- runTitleRegenerateFlow({
1186
- call,
1187
- sessionId,
1188
- isCurrent: () => work.isRequest(token),
1189
- failedMessage: messages.failed,
1190
- onBusy: changeBusy,
1191
- onStatus: setStatus,
1192
- onError: setError,
1193
- onMarker: commitMarker
1194
- });
1195
- },
1196
- children: text.regenerate
1197
- }) : null]
1198
- }),
1199
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1200
- className: "dsh-ui-field",
2087
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("article", {
2088
+ className: "dsh-ui-card",
2089
+ "data-testid": "current-title-session",
1201
2090
  children: [
1202
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1203
- className: "dsh-ui-label",
1204
- children: text.prompt
2091
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
2092
+ className: "dsh-ui-heading",
2093
+ children: text.session
1205
2094
  }),
1206
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
1207
- className: "ct-prompt",
1208
- value: status ? promptFieldValue(prompt, savedPrompt) : "",
1209
- maxLength: 4e3,
1210
- rows: 5,
1211
- disabled: busy || loadingSettings || !status,
1212
- onChange: (event) => {
1213
- dirty.current = true;
1214
- setPrompt(event.target.value);
1215
- setNote("");
2095
+ title ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2096
+ translate: "no",
2097
+ className: "dsh-ui-title dsh-ui-wrap",
2098
+ children: title
2099
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2100
+ className: "dsh-ui-hint",
2101
+ children: text.noSession
2102
+ }),
2103
+ pinned ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2104
+ className: "dsh-ui-meta",
2105
+ children: text.pinned
2106
+ }) : null,
2107
+ status?.session?.generating ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2108
+ role: "status",
2109
+ className: "dsh-ui-hint",
2110
+ children: text.generating
2111
+ }) : null,
2112
+ status && !weOwn ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
2113
+ role: "status",
2114
+ className: "dsh-ui-hint",
2115
+ children: [text.inactive, status.support?.limitation ? ` ${titleMessage(status.support.limitation, seat.locale)}` : ""]
2116
+ }) : null,
2117
+ error ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2118
+ role: "alert",
2119
+ className: "dsh-ui-error dsh-ui-wrap",
2120
+ children: titleMessage(error, seat.locale)
2121
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2122
+ variant: "outline",
2123
+ size: "sm",
2124
+ disabled: busy,
2125
+ onClick: () => {
2126
+ readGate.reset();
2127
+ refreshStatus();
1216
2128
  },
1217
- onBlur: () => {
1218
- if (!status || busyRef.current || reading.current || external.current) return;
1219
- const next = promptToSave(prompt, savedPrompt);
1220
- if (next === void 0) {
1221
- dirty.current = false;
2129
+ children: text.retry
2130
+ })] }) : null,
2131
+ note ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2132
+ role: "status",
2133
+ className: "dsh-ui-notice",
2134
+ children: titleMessage(note, seat.locale)
2135
+ }) : null,
2136
+ externalChange ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2137
+ role: "alert",
2138
+ className: "dsh-ui-error",
2139
+ children: text.externalChange
2140
+ }) : null,
2141
+ syncError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2142
+ role: "status",
2143
+ className: "dsh-ui-hint",
2144
+ children: text.syncUnavailable
2145
+ }) : null,
2146
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2147
+ className: "dsh-ui-actions",
2148
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2149
+ variant: "ghost",
2150
+ size: "sm",
2151
+ disabled: busy || loadingSettings,
2152
+ onClick: () => {
1222
2153
  setPrompt(void 0);
1223
- return;
1224
- }
1225
- readGate.reset();
1226
- const token = work.beginRequest();
1227
- setError("");
1228
- runTitleSettingsSave({
1229
- call,
1230
- prompt: next,
1231
- expectedRevision: status.settings.revision,
1232
- isCurrent: () => work.isRequest(token),
1233
- savedMessage: messages.saved,
1234
- failedMessage: messages.failed,
1235
- onBusy: changeBusy,
1236
- onSettings: (settings) => {
1237
- setStatus((current) => current ? {
1238
- ...current,
1239
- settings
1240
- } : current);
1241
- dirty.current = false;
1242
- setPrompt(void 0);
2154
+ dirty.current = false;
2155
+ external.current = false;
2156
+ setExternalChange(false);
2157
+ readGate.reset();
2158
+ syncRef.current();
2159
+ },
2160
+ children: text.reload
2161
+ }), sessionId ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2162
+ variant: "outline",
2163
+ size: "md",
2164
+ disabled: !canRegenerate,
2165
+ onClick: () => {
2166
+ if (!sessionId) return;
2167
+ readGate.reset();
2168
+ const token = work.beginRequest();
2169
+ setError("");
2170
+ setNote("");
2171
+ runTitleRegenerateFlow({
2172
+ call,
2173
+ sessionId,
2174
+ isCurrent: () => work.isRequest(token),
2175
+ failedMessage: messages.failed,
2176
+ onBusy: changeBusy,
2177
+ onStatus: setStatus,
2178
+ onError: setError,
2179
+ onMarker: commitMarker
2180
+ });
2181
+ },
2182
+ children: text.regenerate
2183
+ }) : null]
2184
+ })
2185
+ ]
2186
+ }),
2187
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2188
+ className: "dsh-ui-section",
2189
+ "data-testid": "current-title-defaults",
2190
+ children: [
2191
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
2192
+ className: "dsh-ui-heading",
2193
+ children: text.defaults
2194
+ }),
2195
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2196
+ className: "dsh-ui-field",
2197
+ children: [
2198
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2199
+ className: "dsh-ui-label",
2200
+ children: text.prompt
2201
+ }),
2202
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
2203
+ className: "dsh-ui-textarea ct-prompt",
2204
+ value: status ? promptFieldValue(prompt, savedPrompt) : "",
2205
+ maxLength: 4e3,
2206
+ rows: 5,
2207
+ disabled: busy || loadingSettings || !status,
2208
+ onChange: (event) => {
2209
+ dirty.current = true;
2210
+ setPrompt(event.target.value);
2211
+ setNote("");
1243
2212
  },
1244
- onNote: setNote,
1245
- onError: setError
1246
- });
1247
- }
2213
+ onBlur: () => {
2214
+ if (!status || busyRef.current || reading.current || external.current) return;
2215
+ const next = promptToSave(prompt, savedPrompt);
2216
+ if (next === void 0) {
2217
+ dirty.current = false;
2218
+ setPrompt(void 0);
2219
+ return;
2220
+ }
2221
+ readGate.reset();
2222
+ const token = work.beginRequest();
2223
+ setError("");
2224
+ runTitleSettingsSave({
2225
+ call,
2226
+ prompt: next,
2227
+ expectedRevision: status.settings.revision,
2228
+ isCurrent: () => work.isRequest(token),
2229
+ savedMessage: messages.saved,
2230
+ failedMessage: messages.failed,
2231
+ onBusy: changeBusy,
2232
+ onSettings: (settings) => {
2233
+ setStatus((current) => current ? {
2234
+ ...current,
2235
+ settings
2236
+ } : current);
2237
+ dirty.current = false;
2238
+ setPrompt(void 0);
2239
+ },
2240
+ onNote: setNote,
2241
+ onError: setError
2242
+ });
2243
+ }
2244
+ }),
2245
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2246
+ className: "dsh-ui-help",
2247
+ children: text.promptHint
2248
+ })
2249
+ ]
1248
2250
  }),
1249
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1250
- className: "dsh-ui-help",
1251
- children: text.promptHint
2251
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2252
+ className: "dsh-ui-field",
2253
+ children: [
2254
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2255
+ className: "dsh-ui-label",
2256
+ children: text.model
2257
+ }),
2258
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelCatalogStatus, {
2259
+ state: catalogState,
2260
+ locale: seat.locale
2261
+ }),
2262
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
2263
+ locale: seat.locale,
2264
+ open: menuOpen && Boolean(status) && !busy && !loadingSettings,
2265
+ onOpenChange: (next) => setMenuOpen(next && Boolean(status) && !busy && !loadingSettings),
2266
+ listClassName: "ct-root",
2267
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2268
+ type: "button",
2269
+ className: "dsh-model-menu-trigger",
2270
+ disabled: busy || loadingSettings || !status,
2271
+ "aria-label": `${text.model}: ${currentLabel}`,
2272
+ children: [
2273
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2274
+ className: "dsh-model-menu-triggerLabel",
2275
+ children: currentLabel
2276
+ }),
2277
+ !follow && efforts.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2278
+ className: "dsh-model-menu-triggerEffort",
2279
+ children: effortName ?? text.modelDefault
2280
+ }) : null,
2281
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${menuOpen ? " dsh-model-menu-chevronOpen" : ""}` })
2282
+ ]
2283
+ }),
2284
+ choices,
2285
+ selected: follow ? void 0 : {
2286
+ provider: savedModel.provider,
2287
+ model: savedModel.model
2288
+ },
2289
+ onPick: (picked) => saveSettings({ model: {
2290
+ provider: picked.provider,
2291
+ model: picked.model
2292
+ } }),
2293
+ leading: [{
2294
+ id: "default",
2295
+ label: text.modelDefault,
2296
+ selected: follow
2297
+ }],
2298
+ onPickLeading: () => saveSettings({ model: {
2299
+ provider: "",
2300
+ model: ""
2301
+ } }),
2302
+ efforts,
2303
+ selectedEffort: savedModel.reasoningEffort || "",
2304
+ onPickEffort: (effort) => saveSettings({ model: follow ? {
2305
+ provider: "",
2306
+ model: ""
2307
+ } : {
2308
+ provider: savedModel.provider,
2309
+ model: savedModel.model,
2310
+ ...effort ? { reasoningEffort: effort } : {}
2311
+ } }),
2312
+ modelLabel: text.model,
2313
+ modelValue: currentLabel,
2314
+ defaultEffortLabel: text.modelDefault
2315
+ })
2316
+ ]
2317
+ }),
2318
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2319
+ className: "dsh-ui-field",
2320
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2321
+ className: "dsh-ui-label",
2322
+ children: text.cadence
2323
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
2324
+ className: "dsh-ui-select",
2325
+ value: status?.settings?.cadence ?? "all-prompts",
2326
+ disabled: busy || loadingSettings || !status,
2327
+ onChange: (event) => {
2328
+ saveSettings({ cadence: event.target.value === "first-prompt" ? "first-prompt" : "all-prompts" });
2329
+ },
2330
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2331
+ value: "all-prompts",
2332
+ children: text.cadenceAll
2333
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2334
+ value: "first-prompt",
2335
+ children: text.cadenceFirst
2336
+ })]
2337
+ })]
1252
2338
  })
1253
2339
  ]
1254
- }),
1255
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1256
- className: "dsh-ui-field",
1257
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1258
- className: "dsh-ui-label",
1259
- children: text.model
1260
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1261
- className: "dsh-ui-select",
1262
- value: modelKey,
1263
- disabled: busy || loadingSettings || !status,
1264
- onChange: (event) => {
1265
- const value = event.target.value;
1266
- const choice = selectableChoices.find((item) => `${item.provider}\u001f${item.model}` === value);
1267
- saveSettings({ model: choice ? {
1268
- provider: choice.provider,
1269
- model: choice.model
1270
- } : {
1271
- provider: "",
1272
- model: ""
1273
- } });
1274
- },
1275
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1276
- value: "",
1277
- children: text.modelDefault
1278
- }), selectableChoices.map((choice) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1279
- value: `${choice.provider}\u001f${choice.model}`,
1280
- children: choice.label
1281
- }, `${choice.provider}\u001f${choice.model}`))]
1282
- })]
1283
- }),
1284
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1285
- className: "dsh-ui-field",
1286
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1287
- className: "dsh-ui-label",
1288
- children: text.cadence
1289
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1290
- className: "dsh-ui-select",
1291
- value: status?.settings.cadence ?? "all-prompts",
1292
- disabled: busy || loadingSettings || !status,
1293
- onChange: (event) => {
1294
- saveSettings({ cadence: event.target.value === "first-prompt" ? "first-prompt" : "all-prompts" });
1295
- },
1296
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1297
- value: "all-prompts",
1298
- children: text.cadenceAll
1299
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1300
- value: "first-prompt",
1301
- children: text.cadenceFirst
1302
- })]
1303
- })]
1304
2340
  })
1305
2341
  ]
1306
2342
  });
1307
2343
  }
1308
- const styles = `${officialUiCss("ct-root")}
2344
+ const styles = `${officialUiCss("ct-root")}${modelMenuCss}
1309
2345
  .ct-root { max-width: 760px; }
1310
- .ct-prompt {
1311
- box-sizing: border-box;
1312
- width: 100%;
1313
- min-width: 0;
1314
- min-height: 96px;
1315
- resize: vertical;
1316
- padding: 8px 12px;
1317
- border: 0.5px solid var(--dsw-alias-border-l4);
1318
- border-radius: var(--dsw-radius-md);
1319
- background: var(--dsw-alias-bg-layer-3);
1320
- font: inherit;
1321
- font-size: 13px;
1322
- line-height: 1.5;
1323
- color: var(--dsw-alias-label-primary);
1324
- }
1325
- .ct-prompt:disabled { color: var(--dsw-alias-label-tertiary); cursor: default; }
1326
- .ct-prompt::placeholder { color: var(--dsw-alias-label-dimmed); }
2346
+ .ct-prompt { min-height: 96px; }
1327
2347
  `;
1328
2348
  function apply(ctx) {
1329
2349
  const client = ctx;
1330
- const settingsSubscriber = createModelSettingsSubscriber(ctx, "@klarkxy/dsh-current-title");
2350
+ const packageName = "@klarkxy/dsh-current-title";
2351
+ mountModelSettingsChanges(ctx, packageName);
2352
+ const settingsSubscriber = createModelSettingsSubscriber(ctx, packageName, modelSettingsChangesIdentity(packageName));
1331
2353
  const subscribeSettings = (change, error) => settingsSubscriber.subscribe(change, error);
1332
2354
  const marker = createTitleClientMarker();
1333
2355
  ctx.provide(TITLE_CLIENT_SERVICE, marker);