dsh-model-switch 0.4.21 → 0.4.23

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
@@ -10,11 +10,7 @@ window.__ModuleLoader__.load({
10
10
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
11
11
  //#region src/client-contract.ts
12
12
  const MODEL_SWITCH_CONFIG_ID = "model-switch";
13
- const MAIN_DEFAULT_CONFIG_ID = "agent-default-model";
14
13
  const PROVIDERS_CONFIG_ID = "llm-providers-ui";
15
- var MainSettingsConflictError = class extends Error {
16
- name = "MainSettingsConflictError";
17
- };
18
14
  const SUBAGENT_SETTINGS_FIELDS = Object.freeze({
19
15
  mode: "subagentMode",
20
16
  provider: "subagentProvider",
@@ -80,6 +76,174 @@ window.__ModuleLoader__.load({
80
76
  return route !== void 0 && (route.provider ?? "").trim() !== "" && (route.model ?? "").trim() !== "";
81
77
  }
82
78
  //#endregion
79
+ //#region src/provider-group.ts
80
+ const ROUTE_FIELDS = Object.freeze([
81
+ "compactProvider",
82
+ "compactModel",
83
+ "autoReviewProvider",
84
+ "autoReviewModel",
85
+ "subagentProvider",
86
+ "subagentModel",
87
+ "subagentReasoningEffort"
88
+ ]);
89
+ /** Own data property, so a literal '__proto__' provider id can never touch a prototype. */
90
+ function setKey(target, key, value) {
91
+ Object.defineProperty(target, key, {
92
+ value,
93
+ enumerable: true,
94
+ writable: true,
95
+ configurable: true
96
+ });
97
+ }
98
+ /** A non-blank string. Blank values behave exactly like an absent field. */
99
+ function present(value) {
100
+ return typeof value === "string" && value.trim() !== "";
101
+ }
102
+ /** Normalize one raw entry to its owning provider and route view; undefined for non-object junk. */
103
+ function normalizeEntry(value) {
104
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
105
+ const source = value;
106
+ const group = {};
107
+ for (const field of ROUTE_FIELDS) {
108
+ const item = source[field];
109
+ if (present(item)) group[field] = item;
110
+ }
111
+ return {
112
+ provider: present(source.provider) ? source.provider : "",
113
+ group
114
+ };
115
+ }
116
+ /**
117
+ * Strip a raw stored value to well-formed groups; never throws. Both storage
118
+ * shapes are accepted: the stored entry array (keys live in each entry's
119
+ * `provider` field) and a hand-written provider → routes map (keys are the
120
+ * providers). All-empty entries stay: they pin a provider to every default.
121
+ */
122
+ function normalizeProviderGroups(raw) {
123
+ const groups = {};
124
+ if (Array.isArray(raw)) {
125
+ for (const entry of raw) {
126
+ const view = normalizeEntry(entry);
127
+ if (view === void 0 || view.provider === "") continue;
128
+ setKey(groups, view.provider, view.group);
129
+ }
130
+ return groups;
131
+ }
132
+ if (raw !== null && typeof raw === "object") for (const [key, value] of Object.entries(raw)) {
133
+ if (key.trim() === "") continue;
134
+ const view = normalizeEntry(value);
135
+ if (view === void 0) continue;
136
+ setKey(groups, key, view.group);
137
+ }
138
+ return groups;
139
+ }
140
+ /**
141
+ * The saved group for a provider.
142
+ * @returns the normalized group, or undefined when the provider has no group —
143
+ * which is what leaves the live routes untouched during a switch.
144
+ */
145
+ function groupOf(groups, provider) {
146
+ if (groups === void 0 || provider.trim() === "") return void 0;
147
+ if (!Object.prototype.hasOwnProperty.call(groups, provider)) return void 0;
148
+ return groups[provider];
149
+ }
150
+ /** A complete pinned pair; either half missing leaves the capability at its default. */
151
+ function pair(group, providerField, modelField) {
152
+ const provider = group[providerField];
153
+ const model = group[modelField];
154
+ return present(provider) && present(model) ? {
155
+ provider,
156
+ model
157
+ } : void 0;
158
+ }
159
+ /**
160
+ * Ordered writes applying one group with full-replacement semantics: pinned
161
+ * capabilities land as pairs, everything else reverts to its default state
162
+ * (compaction/review follow the session model; Subagent follows official
163
+ * inherit). The mode write comes last so a fixed route never publishes with
164
+ * the previous provider's fields still half-written.
165
+ */
166
+ function providerGroupWrites(group) {
167
+ const compact = pair(group, "compactProvider", "compactModel");
168
+ const review = pair(group, "autoReviewProvider", "autoReviewModel");
169
+ const subagent = pair(group, "subagentProvider", "subagentModel");
170
+ const effort = subagent !== void 0 && present(group.subagentReasoningEffort) ? group.subagentReasoningEffort : void 0;
171
+ return [
172
+ {
173
+ field: "compactProvider",
174
+ value: compact?.provider
175
+ },
176
+ {
177
+ field: "compactModel",
178
+ value: compact?.model
179
+ },
180
+ {
181
+ field: "autoReviewProvider",
182
+ value: review?.provider
183
+ },
184
+ {
185
+ field: "autoReviewModel",
186
+ value: review?.model
187
+ },
188
+ {
189
+ field: "subagentProvider",
190
+ value: subagent?.provider
191
+ },
192
+ {
193
+ field: "subagentModel",
194
+ value: subagent?.model
195
+ },
196
+ {
197
+ field: "subagentReasoningEffort",
198
+ value: effort
199
+ },
200
+ {
201
+ field: "subagentMode",
202
+ mode: subagent !== void 0 ? "fixed" : "follow-main"
203
+ }
204
+ ];
205
+ }
206
+ /** Storage entry for one provider's group; absent or blank route fields become ''. */
207
+ function entryOf(provider, group) {
208
+ return {
209
+ provider,
210
+ compactProvider: group.compactProvider ?? "",
211
+ compactModel: group.compactModel ?? "",
212
+ autoReviewProvider: group.autoReviewProvider ?? "",
213
+ autoReviewModel: group.autoReviewModel ?? "",
214
+ subagentProvider: group.subagentProvider ?? "",
215
+ subagentModel: group.subagentModel ?? "",
216
+ subagentReasoningEffort: group.subagentReasoningEffort ?? ""
217
+ };
218
+ }
219
+ /**
220
+ * The stored entry list with one provider's group replaced (an entry) or
221
+ * removed (undefined). Every entry is re-normalized and de-duplicated so a
222
+ * save can never write junk back; the input is never mutated. A blank
223
+ * provider only normalizes.
224
+ */
225
+ function withProviderGroup(storage, provider, group) {
226
+ const groups = { ...normalizeProviderGroups(storage) };
227
+ if (provider.trim() !== "") {
228
+ if (group === void 0) delete groups[provider];
229
+ else setKey(groups, provider, group);
230
+ }
231
+ return Object.entries(groups).map(([key, value]) => entryOf(key, value));
232
+ }
233
+ /**
234
+ * Apply a saved group's routes to the live flat settings.
235
+ * @returns true when a group existed and every write settled; false when the
236
+ * provider has no group (nothing was written). A rejected write propagates.
237
+ */
238
+ async function applyProviderGroupTo(sink, groups, provider) {
239
+ const group = groupOf(groups, provider);
240
+ if (group === void 0) return false;
241
+ for (const write of providerGroupWrites(group)) if (write.field === "subagentMode") await sink.set("subagentMode", write.mode);
242
+ else if (write.value === void 0) await sink.unset(write.field);
243
+ else await sink.set(write.field, write.value);
244
+ return true;
245
+ }
246
+ //#endregion
83
247
  //#region src/client/derived-config-form.ts
84
248
  function deriveConfigForm(source, project, fields) {
85
249
  let sourceSnapshot;
@@ -436,65 +600,20 @@ window.__ModuleLoader__.load({
436
600
  models
437
601
  };
438
602
  }
439
- function expectedMainRevision(mirror, accepted) {
440
- return Math.max(mirror, accepted ?? mirror);
441
- }
442
- function acceptedRevisionAfterFailure(accepted, error) {
443
- return error instanceof MainSettingsConflictError ? void 0 : accepted;
444
- }
445
- function deriveMainChoices(groups, draft) {
446
- const providers = groups.map((group) => ({
447
- id: group.id,
448
- name: group.name
449
- }));
450
- if (draft !== void 0 && !providers.some((option) => option.id === draft.provider)) providers.push({
451
- id: draft.provider,
452
- name: draft.provider
453
- });
454
- const group = groups.find((item) => item.id === draft?.provider);
455
- const models = (group?.models ?? []).map((model) => ({
456
- id: model.id,
457
- name: model.name
458
- }));
459
- if (draft !== void 0 && !models.some((option) => option.id === draft.model)) models.push({
460
- id: draft.model,
461
- name: draft.model
462
- });
463
- const efforts = ((group?.models.find((item) => item.id === draft?.model))?.reasoning?.efforts ?? []).map((effort) => ({
464
- id: effort.id,
465
- name: effort.name
466
- }));
467
- if (draft?.reasoningEffort !== void 0 && !efforts.some((option) => option.id === draft.reasoningEffort)) efforts.push({
468
- id: draft.reasoningEffort,
469
- name: draft.reasoningEffort
470
- });
471
- return {
472
- providers,
473
- models,
474
- efforts
475
- };
476
- }
477
- function useModelSwitchSettingsController(input) {
478
- const main = input.useMainSettings((value) => value);
479
- const subagent = input.useSubagentSettings((value) => value);
480
- const [draft, setDraft] = (0, react.useState)(main.value);
603
+ /** Load the model catalog backing every route editor, reloading on provider-order changes. */
604
+ function useSettingsCatalog(input) {
481
605
  const [groups, setGroups] = (0, react.useState)([]);
482
- const [busy, setBusy] = (0, react.useState)(false);
483
- const [message, setMessage] = (0, react.useState)();
484
- const locked = (0, react.useRef)(false);
485
- const acceptedRevision = (0, react.useRef)();
486
- (0, react.useEffect)(() => {
487
- setDraft(main.value);
488
- setMessage(void 0);
489
- if (main.revision !== void 0 && (acceptedRevision.current === void 0 || main.revision > acceptedRevision.current)) acceptedRevision.current = main.revision;
490
- }, [main.revision, main.value]);
606
+ const [catalogError, setCatalogError] = (0, react.useState)();
491
607
  (0, react.useEffect)(() => {
492
608
  let live = true;
493
609
  const load = () => {
494
610
  input.loadCatalog().then((value) => {
495
- if (live) setGroups(value);
611
+ if (live) {
612
+ setGroups(value);
613
+ setCatalogError(void 0);
614
+ }
496
615
  }).catch(() => {
497
- if (live) setMessage(input.t("catalogFailed"));
616
+ if (live) setCatalogError(input.t("catalogFailed"));
498
617
  });
499
618
  };
500
619
  load();
@@ -510,63 +629,9 @@ window.__ModuleLoader__.load({
510
629
  input.subscribeProviderOrder,
511
630
  input.t
512
631
  ]);
513
- const { providers, models, efforts } = deriveMainChoices(groups, draft);
514
- const disabled = main.status !== "ready" || !main.writable || draft === void 0 || busy || draft.provider.trim() === "" || draft.model.trim() === "";
515
- const setProvider = (provider) => setDraft((current) => {
516
- if (current === void 0) return current;
517
- const first = groups.find((item) => item.id === provider)?.models[0];
518
- return selectRouteModel(groups, provider, first?.id ?? current.model);
519
- });
520
- const setModel = (id) => setDraft((current) => current === void 0 ? current : selectRouteModel(groups, current.provider, id));
521
- const setReasoningEffort = (value) => setDraft((current) => {
522
- if (current === void 0) return current;
523
- const next = { ...current };
524
- if (value === "") delete next.reasoningEffort;
525
- else next.reasoningEffort = value;
526
- return next;
527
- });
528
- const reset = () => {
529
- setDraft(main.value);
530
- setMessage(void 0);
531
- };
532
- const save = async () => {
533
- if (disabled || locked.current || draft === void 0) return;
534
- const mirrorRevision = main.revision;
535
- if (mirrorRevision === void 0) {
536
- setMessage(input.t("requestFailed"));
537
- return;
538
- }
539
- const expectedRevision = expectedMainRevision(mirrorRevision, acceptedRevision.current);
540
- locked.current = true;
541
- setBusy(true);
542
- setMessage(void 0);
543
- try {
544
- acceptedRevision.current = await input.saveMain(draft, expectedRevision);
545
- setMessage(input.t("saved"));
546
- } catch (error) {
547
- acceptedRevision.current = acceptedRevisionAfterFailure(acceptedRevision.current, error);
548
- setMessage(error instanceof Error ? error.message : input.t("requestFailed"));
549
- } finally {
550
- locked.current = false;
551
- setBusy(false);
552
- }
553
- };
554
632
  return {
555
- main,
556
- subagent,
557
- draft,
558
633
  groups,
559
- providers,
560
- models,
561
- efforts,
562
- busy,
563
- message,
564
- disabled,
565
- setProvider,
566
- setModel,
567
- setReasoningEffort,
568
- reset,
569
- save
634
+ catalogError
570
635
  };
571
636
  }
572
637
  //#endregion
@@ -628,29 +693,50 @@ window.__ModuleLoader__.load({
628
693
  function compact(...values) {
629
694
  return values.filter((value) => value !== void 0 && value !== "").join(" · ");
630
695
  }
696
+ /** A complete pinned pair; either half blank leaves the capability at its default. */
697
+ function draftPair(provider, model) {
698
+ return provider !== void 0 && provider.trim() !== "" && model !== void 0 && model.trim() !== "" ? {
699
+ provider,
700
+ model
701
+ } : void 0;
702
+ }
631
703
  function RouteIcon({ kind }) {
632
- if (kind === "compaction") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
704
+ if (kind === "providerGroups") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
633
705
  width: "18",
634
706
  height: "18",
635
707
  viewBox: "0 0 18 18",
636
708
  fill: "none",
637
709
  "aria-hidden": "true",
638
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
639
- d: "M5 3.5 9 7.5l4-4M5 14.5 9 10.5l4 4",
640
- stroke: "currentColor",
641
- strokeLinecap: "round",
642
- strokeLinejoin: "round"
643
- })
710
+ children: [
711
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
712
+ d: "M3 5.5h12M3 12.5h12",
713
+ stroke: "currentColor",
714
+ strokeLinecap: "round"
715
+ }),
716
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
717
+ cx: "6.5",
718
+ cy: "5.5",
719
+ r: "2",
720
+ stroke: "currentColor"
721
+ }),
722
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
723
+ cx: "11.5",
724
+ cy: "12.5",
725
+ r: "2",
726
+ stroke: "currentColor"
727
+ })
728
+ ]
644
729
  });
645
- if (kind === "main") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
730
+ if (kind === "compaction") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
646
731
  width: "18",
647
732
  height: "18",
648
733
  viewBox: "0 0 18 18",
649
734
  fill: "none",
650
735
  "aria-hidden": "true",
651
736
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
652
- d: "M3 4.5h12v8H8l-3.5 2v-2H3v-8Z",
737
+ d: "M5 3.5 9 7.5l4-4M5 14.5 9 10.5l4 4",
653
738
  stroke: "currentColor",
739
+ strokeLinecap: "round",
654
740
  strokeLinejoin: "round"
655
741
  })
656
742
  });
@@ -840,7 +926,7 @@ window.__ModuleLoader__.load({
840
926
  })]
841
927
  });
842
928
  }
843
- function Actions({ t, busy, disabled, message, onCancel, onSave }) {
929
+ function Actions({ t, busy, disabled, message, extra, onCancel, onSave }) {
844
930
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
845
931
  className: ModelSwitchSettings_module_css_default.cardFooter,
846
932
  children: [
@@ -848,6 +934,7 @@ window.__ModuleLoader__.load({
848
934
  className: cx(ModelSwitchSettings_module_css_default.hint, ModelSwitchSettings_module_css_default.message),
849
935
  children: message
850
936
  }),
937
+ extra,
851
938
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
852
939
  type: "button",
853
940
  className: cx(ModelSwitchSettings_module_css_default.button, ModelSwitchSettings_module_css_default.secondaryButton),
@@ -889,6 +976,22 @@ window.__ModuleLoader__.load({
889
976
  const matched = matchCatalogModel(groups.find((group) => group.id === route.provider)?.models ?? [], route.model);
890
977
  return matched?.effort ?? matched?.model.reasoning?.defaultEffort;
891
978
  }
979
+ /**
980
+ * The effort caption for the subagent summary: a stored effort is the pinned
981
+ * one ("已固定 · High"); an unset effort follows the provider default and says
982
+ * so ("Provider 默认 · High"), using the same effort-name lookup the picker does.
983
+ */
984
+ function subagentEffortSegment(t, matched, pinned, defaultEffort) {
985
+ const effortName = (id) => matched?.model.reasoning?.efforts.find((level) => level.id === id)?.name ?? id;
986
+ if (pinned !== "") return compact(t("effortPinned"), effortName(pinned));
987
+ return defaultEffort === void 0 ? t("providerDefaultShort") : compact(t("providerDefaultShort"), effortName(defaultEffort));
988
+ }
989
+ /** The catalog head route; capability switches pin here when turned On. */
990
+ function firstCatalogRoute(groups) {
991
+ const provider = groups[0];
992
+ const model = provider?.models[0];
993
+ return provider === void 0 || model === void 0 ? void 0 : selectRouteModel(groups, provider.id, model.id);
994
+ }
892
995
  function capabilityChoices(groups, route, providers, kind) {
893
996
  const choices = deriveRouteChoices(groups, route, providers);
894
997
  if (kind === "image" && route?.provider === "grok") {
@@ -909,8 +1012,8 @@ window.__ModuleLoader__.load({
909
1012
  return choices;
910
1013
  }
911
1014
  function ModelSwitchSettings(props) {
912
- const controller = useModelSwitchSettingsController(props);
913
- const { main, subagent, draft, groups } = controller;
1015
+ const { groups, catalogError } = useSettingsCatalog(props);
1016
+ const subagent = props.useSubagentSettings((value) => value);
914
1017
  const search = props.useSearchSettings((value) => value);
915
1018
  const image = props.useImageSettings((value) => value);
916
1019
  const autoReview = props.useAutoReviewSettings((value) => value);
@@ -926,6 +1029,8 @@ window.__ModuleLoader__.load({
926
1029
  const [message, setMessage] = (0, react.useState)();
927
1030
  const [compactBusy, setCompactBusy] = (0, react.useState)(false);
928
1031
  const [compactError, setCompactError] = (0, react.useState)();
1032
+ const [groupProvider, setGroupProvider] = (0, react.useState)("");
1033
+ const [groupDraft, setGroupDraft] = (0, react.useState)(void 0);
929
1034
  const loadSearchCapabilities = props.loadCapabilities;
930
1035
  const [searchSnapshot, setSearchSnapshot] = (0, react.useState)(void 0);
931
1036
  const [searchError, setSearchError] = (0, react.useState)(void 0);
@@ -964,9 +1069,67 @@ window.__ModuleLoader__.load({
964
1069
  if (timer !== void 0) clearTimeout(timer);
965
1070
  };
966
1071
  }, [loadSearchCapabilities]);
1072
+ const providerGroups = normalizeProviderGroups(switchSettings.value?.providerGroups);
1073
+ /** Seed an unsaved group from the live routes. */
1074
+ const seedGroupFromActive = () => {
1075
+ const compact = compaction.value !== void 0 && routePairConfigured(compaction.value) ? {
1076
+ provider: compaction.value.provider ?? "",
1077
+ model: compaction.value.model ?? ""
1078
+ } : void 0;
1079
+ const review = autoReview.value !== void 0 && routePairConfigured(autoReview.value) ? {
1080
+ provider: autoReview.value.provider ?? "",
1081
+ model: autoReview.value.model ?? ""
1082
+ } : void 0;
1083
+ const fixedSubagent = subagent.value !== void 0 && subagent.value.mode === "fixed" && routePairConfigured(subagent.value) ? {
1084
+ provider: subagent.value.provider ?? "",
1085
+ model: subagent.value.model ?? "",
1086
+ reasoningEffort: subagent.value.reasoningEffort
1087
+ } : void 0;
1088
+ return {
1089
+ ...compact === void 0 ? {} : {
1090
+ compactProvider: compact.provider,
1091
+ compactModel: compact.model
1092
+ },
1093
+ ...review === void 0 ? {} : {
1094
+ autoReviewProvider: review.provider,
1095
+ autoReviewModel: review.model
1096
+ },
1097
+ ...fixedSubagent === void 0 ? {} : {
1098
+ subagentProvider: fixedSubagent.provider,
1099
+ subagentModel: fixedSubagent.model,
1100
+ ...fixedSubagent.reasoningEffort === void 0 ? {} : { subagentReasoningEffort: fixedSubagent.reasoningEffort }
1101
+ }
1102
+ };
1103
+ };
1104
+ /** Reload the editor draft: the saved group when one exists, else a seed from the live routes. */
1105
+ const resyncGroupDraft = () => {
1106
+ if (groupProvider === "") {
1107
+ setGroupDraft(void 0);
1108
+ return;
1109
+ }
1110
+ setGroupDraft(groupOf(providerGroups, groupProvider) ?? seedGroupFromActive());
1111
+ };
1112
+ /** Open the editor on the provider in use: the first configured group, else the catalog head. */
1113
+ (0, react.useEffect)(() => {
1114
+ if (groupProvider !== "") return;
1115
+ const initial = Object.keys(providerGroups)[0] ?? groups[0]?.id;
1116
+ if (initial !== void 0 && initial.trim() !== "") setGroupProvider(initial);
1117
+ }, [
1118
+ groupProvider,
1119
+ providerGroups,
1120
+ groups
1121
+ ]);
1122
+ (0, react.useEffect)(() => {
1123
+ resyncGroupDraft();
1124
+ }, [
1125
+ groupProvider,
1126
+ switchSettings.value,
1127
+ subagent.value,
1128
+ autoReview.value,
1129
+ compaction.value
1130
+ ]);
967
1131
  if ([
968
- controller.main,
969
- controller.subagent,
1132
+ subagent,
970
1133
  search,
971
1134
  image,
972
1135
  switchSettings
@@ -1009,8 +1172,7 @@ window.__ModuleLoader__.load({
1009
1172
  });
1010
1173
  return efforts;
1011
1174
  })();
1012
- const defaultEffort = subagentDraft?.reasoningEffort ?? routeDefaultEffort(groups, subagentRoute);
1013
- const mainEffectiveEffort = draft?.reasoningEffort ?? routeDefaultEffort(groups, draft);
1175
+ const defaultEffort = routeDefaultEffort(groups, subagentRoute);
1014
1176
  const searchLive = searchSnapshot?.capabilities;
1015
1177
  const searchGroups = searchGroupsFromCapabilities(searchLive);
1016
1178
  const searchChoices = capabilityChoices(searchGroups, searchDraft, void 0, "search");
@@ -1021,7 +1183,6 @@ window.__ModuleLoader__.load({
1021
1183
  const compactionChoices = deriveRouteChoices(groups, compactionDraft);
1022
1184
  const compactionOn = routePairConfigured(compaction.value);
1023
1185
  const synced = [
1024
- main,
1025
1186
  subagent,
1026
1187
  search,
1027
1188
  image,
@@ -1101,8 +1262,9 @@ window.__ModuleLoader__.load({
1101
1262
  };
1102
1263
  /**
1103
1264
  * The reviewer switch: Off unsets both stored fields (follow the session
1104
- * model); On seeds the route from Main so the pin takes effect immediately
1105
- * and stays editable below. A failed write reports, never flips silently.
1265
+ * model); On pins the route to the catalog head so the pin takes effect
1266
+ * immediately and stays editable below. A failed write reports, never
1267
+ * flips silently.
1106
1268
  */
1107
1269
  const persistAutoReviewEnabled = (enabled) => {
1108
1270
  if (busy === "autoReview" || !autoReview.writable || autoReview.value === void 0) return;
@@ -1114,8 +1276,8 @@ window.__ModuleLoader__.load({
1114
1276
  });
1115
1277
  return;
1116
1278
  }
1117
- const seed = draft;
1118
- if (seed === void 0 || seed.provider.trim() === "" || seed.model.trim() === "") return;
1279
+ const seed = firstCatalogRoute(groups);
1280
+ if (seed === void 0) return;
1119
1281
  setBusy("autoReview");
1120
1282
  setMessage(void 0);
1121
1283
  (async () => {
@@ -1138,9 +1300,9 @@ window.__ModuleLoader__.load({
1138
1300
  };
1139
1301
  /**
1140
1302
  * The compaction switch: Off unsets both stored fields (official default =
1141
- * the conversation target); On seeds the route from Main so the pin takes
1142
- * effect immediately and stays editable below. A failed write reports,
1143
- * never flips silently.
1303
+ * the conversation target); On pins the route to the catalog head so the
1304
+ * pin takes effect immediately and stays editable below. A failed write
1305
+ * reports, never flips silently.
1144
1306
  */
1145
1307
  const persistCompactionEnabled = (enabled) => {
1146
1308
  if (busy === "compaction" || !compaction.writable || compaction.value === void 0) return;
@@ -1152,8 +1314,8 @@ window.__ModuleLoader__.load({
1152
1314
  });
1153
1315
  return;
1154
1316
  }
1155
- const seed = draft;
1156
- if (seed === void 0 || seed.provider.trim() === "" || seed.model.trim() === "") return;
1317
+ const seed = firstCatalogRoute(groups);
1318
+ if (seed === void 0) return;
1157
1319
  setBusy("compaction");
1158
1320
  setMessage(void 0);
1159
1321
  (async () => {
@@ -1174,14 +1336,109 @@ window.__ModuleLoader__.load({
1174
1336
  });
1175
1337
  });
1176
1338
  };
1177
- const mainSummary = draft === void 0 ? props.t("loading") : compact(routeName(groups, draft), mainEffectiveEffort);
1178
1339
  const subagentRole = subagentDraft?.mode === "fixed" && subagentDraft.provider !== void 0 && subagentDraft.provider !== "" ? props.providerRoleOf?.(subagentDraft.provider) : void 0;
1179
1340
  const subagentOn = subagentDraft?.mode === "fixed";
1180
- const subagentSummary = subagentDraft === void 0 ? props.t("loading") : subagentOn ? compact(routeName(groups, subagentRoute), isAgentRole(subagentRole) ? props.t("agentBadge") : void 0, defaultEffort === void 0 ? props.t("providerDefaultShort") : compact(props.t("providerDefaultShort"), defaultEffort)) : props.t("subagentOff");
1341
+ const subagentSummary = subagentDraft === void 0 ? props.t("loading") : subagentOn ? compact(routeName(groups, subagentRoute), isAgentRole(subagentRole) ? props.t("agentBadge") : void 0, subagentEffortSegment(props.t, subagentCatalogModel, (subagentDraft.reasoningEffort ?? "").trim(), defaultEffort)) : props.t("subagentOff");
1181
1342
  const subagentDisabled = subagent.status !== "ready" || !subagent.writable || subagentDraft === void 0 || busy === "subagent" || !subagentOn || (subagentDraft.provider ?? "").trim() === "" || (subagentDraft.model ?? "").trim() === "";
1182
1343
  const capabilityDisabled = (route, snapshot, next) => snapshot.status !== "ready" || !snapshot.writable || next === void 0 || busy === route || (next.provider ?? "").trim() === "" || (next.model ?? "").trim() === "";
1183
1344
  const autoReviewDisabled = autoReview.status !== "ready" || !autoReview.writable || autoReviewDraft === void 0 || busy === "autoReview" || !autoReviewOn || (autoReviewDraft.provider ?? "").trim() === "" || (autoReviewDraft.model ?? "").trim() === "";
1184
1345
  const compactionDisabled = compaction.status !== "ready" || !compaction.writable || compactionDraft === void 0 || busy === "compaction" || !compactionOn || (compactionDraft.provider ?? "").trim() === "" || (compactionDraft.model ?? "").trim() === "";
1346
+ const configuredGroupCount = Object.keys(providerGroups).length;
1347
+ const groupControlsDisabled = busy === "providerGroups" || switchSettings.status !== "ready" || !switchSettings.writable;
1348
+ const groupProviderChoices = deriveRouteChoices(groups, groupProvider === "" ? void 0 : { provider: groupProvider }).providers.map((choice) => groupOf(providerGroups, choice.id) === void 0 ? choice : {
1349
+ ...choice,
1350
+ name: choice.name + " ✓"
1351
+ });
1352
+ const groupCompaction = groupDraft === void 0 ? void 0 : draftPair(groupDraft.compactProvider, groupDraft.compactModel);
1353
+ const groupReview = groupDraft === void 0 ? void 0 : draftPair(groupDraft.autoReviewProvider, groupDraft.autoReviewModel);
1354
+ const groupSubagent = groupDraft === void 0 ? void 0 : draftPair(groupDraft.subagentProvider, groupDraft.subagentModel);
1355
+ const groupCompactionChoices = deriveRouteChoices(groups, groupCompaction);
1356
+ const groupReviewChoices = deriveRouteChoices(groups, groupReview);
1357
+ const groupSubagentChoices = deriveRouteChoices(groups, groupSubagent);
1358
+ const groupSubMatched = matchCatalogModel(groups.find((group) => group.id === (groupDraft?.subagentProvider ?? ""))?.models ?? [], groupDraft?.subagentModel ?? "");
1359
+ const groupSubEfforts = (() => {
1360
+ const efforts = (groupSubMatched?.model.reasoning?.efforts ?? []).map((effort) => ({
1361
+ id: effort.id,
1362
+ name: effort.name
1363
+ }));
1364
+ const current = groupDraft?.subagentReasoningEffort;
1365
+ if (current !== void 0 && current !== "" && !efforts.some((option) => option.id === current)) efforts.push({
1366
+ id: current,
1367
+ name: current
1368
+ });
1369
+ return efforts;
1370
+ })();
1371
+ /**
1372
+ * Enable a capability row by pinning it: to that capability's live route
1373
+ * when one is configured, else to the catalog head (same sources the
1374
+ * editor's first-edit seed uses).
1375
+ */
1376
+ const enableGroupCapability = (kind) => {
1377
+ const active = seedGroupFromActive();
1378
+ const seed = firstCatalogRoute(groups);
1379
+ setGroupDraft((draft) => {
1380
+ if (draft === void 0) return draft;
1381
+ if (kind === "compaction") {
1382
+ const provider = active.compactProvider ?? seed?.provider;
1383
+ const model = active.compactModel ?? seed?.model;
1384
+ if (provider === void 0 || model === void 0) return draft;
1385
+ return {
1386
+ ...draft,
1387
+ compactProvider: provider,
1388
+ compactModel: model
1389
+ };
1390
+ }
1391
+ if (kind === "autoReview") {
1392
+ const provider = active.autoReviewProvider ?? seed?.provider;
1393
+ const model = active.autoReviewModel ?? seed?.model;
1394
+ if (provider === void 0 || model === void 0) return draft;
1395
+ return {
1396
+ ...draft,
1397
+ autoReviewProvider: provider,
1398
+ autoReviewModel: model
1399
+ };
1400
+ }
1401
+ const provider = active.subagentProvider ?? seed?.provider;
1402
+ const model = active.subagentModel ?? seed?.model;
1403
+ if (provider === void 0 || model === void 0) return draft;
1404
+ const effort = active.subagentProvider !== void 0 ? active.subagentReasoningEffort : seed?.reasoningEffort;
1405
+ return {
1406
+ ...draft,
1407
+ subagentProvider: provider,
1408
+ subagentModel: model,
1409
+ ...effort === void 0 || effort === "" ? {} : { subagentReasoningEffort: effort }
1410
+ };
1411
+ });
1412
+ };
1413
+ const disableGroupCapability = (kind) => {
1414
+ setGroupDraft((draft) => {
1415
+ if (draft === void 0) return draft;
1416
+ if (kind === "compaction") return {
1417
+ ...draft,
1418
+ compactProvider: "",
1419
+ compactModel: ""
1420
+ };
1421
+ if (kind === "autoReview") return {
1422
+ ...draft,
1423
+ autoReviewProvider: "",
1424
+ autoReviewModel: ""
1425
+ };
1426
+ return {
1427
+ ...draft,
1428
+ subagentProvider: "",
1429
+ subagentModel: "",
1430
+ subagentReasoningEffort: ""
1431
+ };
1432
+ });
1433
+ };
1434
+ const saveProviderGroupCard = () => {
1435
+ if (groupProvider === "" || groupDraft === void 0) return;
1436
+ run("providerGroups", () => props.saveProviderGroup(groupProvider, groupDraft));
1437
+ };
1438
+ const deleteProviderGroupCard = () => {
1439
+ if (groupProvider === "") return;
1440
+ run("providerGroups", () => props.saveProviderGroup(groupProvider, void 0));
1441
+ };
1185
1442
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("main", {
1186
1443
  className: ModelSwitchSettings_module_css_default.section,
1187
1444
  children: [
@@ -1197,6 +1454,10 @@ window.__ModuleLoader__.load({
1197
1454
  className: ModelSwitchSettings_module_css_default.saved,
1198
1455
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", { className: ModelSwitchSettings_module_css_default.savedDot }), synced ? props.t("settingsSynced") : props.t("loading")]
1199
1456
  }),
1457
+ catalogError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1458
+ className: cx(ModelSwitchSettings_module_css_default.hint, ModelSwitchSettings_module_css_default.message),
1459
+ children: catalogError
1460
+ }),
1200
1461
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1201
1462
  className: ModelSwitchSettings_module_css_default.group,
1202
1463
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
@@ -1262,150 +1523,92 @@ window.__ModuleLoader__.load({
1262
1523
  }),
1263
1524
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1264
1525
  className: ModelSwitchSettings_module_css_default.group,
1265
- children: [
1266
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
1267
- className: ModelSwitchSettings_module_css_default.groupLabel,
1268
- children: props.t("conversationRoutes")
1526
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
1527
+ className: ModelSwitchSettings_module_css_default.groupLabel,
1528
+ children: props.t("conversationRoutes")
1529
+ }), props.capabilities.centralSubagentRouting.available ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RouteCard, {
1530
+ title: props.t("subagent"),
1531
+ summary: subagentSummary,
1532
+ hint: props.t("subagentHelp"),
1533
+ icon: "subagent",
1534
+ open: open === "subagent" && subagentOn,
1535
+ onToggle: () => {
1536
+ if (subagentOn) toggle("subagent");
1537
+ },
1538
+ expandable: subagentOn,
1539
+ trailing: subagentDraft === void 0 ? void 0 : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstantSwitch, {
1540
+ checked: subagentOn,
1541
+ disabled: busy === "subagent" || !subagent.writable,
1542
+ label: props.t("subagent"),
1543
+ onChange: persistSubagentEnabled
1269
1544
  }),
1270
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RouteCard, {
1271
- title: props.t("main"),
1272
- summary: mainSummary,
1273
- icon: "main",
1274
- open: open === "main",
1275
- onToggle: () => {
1276
- toggle("main");
1277
- },
1278
- badge: props.t("defaultBadge"),
1279
- children: draft === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1280
- className: ModelSwitchSettings_module_css_default.hint,
1281
- children: props.t("loading")
1282
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1283
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1284
- className: ModelSwitchSettings_module_css_default.formGrid,
1285
- children: [
1286
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
1287
- label: props.t("provider"),
1288
- value: draft.provider,
1289
- disabled: controller.busy || !main.writable,
1290
- choices: controller.providers,
1291
- onChange: controller.setProvider
1292
- }),
1293
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
1294
- label: props.t("model"),
1295
- value: draft.model,
1296
- disabled: controller.busy || !main.writable,
1297
- choices: controller.models,
1298
- onChange: controller.setModel
1299
- }),
1300
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
1301
- label: props.t("effort"),
1302
- value: draft.reasoningEffort ?? "",
1303
- disabled: controller.busy || !main.writable,
1304
- choices: controller.efforts,
1305
- onChange: controller.setReasoningEffort
1306
- })
1307
- ]
1545
+ children: subagentDraft === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1546
+ className: ModelSwitchSettings_module_css_default.hint,
1547
+ children: props.t("loading")
1548
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1549
+ className: ModelSwitchSettings_module_css_default.formGrid,
1550
+ children: [
1551
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
1552
+ label: props.t("provider"),
1553
+ value: subagentDraft.provider ?? "",
1554
+ disabled: busy === "subagent" || !subagent.writable,
1555
+ choices: subagentChoices.providers,
1556
+ onChange: (provider) => {
1557
+ const first = groups.find((group) => group.id === provider)?.models[0];
1558
+ setSubagentDraft({
1559
+ mode: "fixed",
1560
+ ...selectRouteModel(groups, provider, first?.id ?? subagentDraft.model ?? "")
1561
+ });
1562
+ }
1308
1563
  }),
1309
- !main.writable && main.status === "ready" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1310
- className: ModelSwitchSettings_module_css_default.hint,
1311
- children: props.t("readonly")
1312
- }) : null,
1313
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Actions, {
1314
- t: props.t,
1315
- busy: controller.busy,
1316
- disabled: controller.disabled,
1317
- ...controller.message === void 0 ? {} : { message: controller.message },
1318
- onCancel: controller.reset,
1319
- onSave: () => {
1320
- controller.save();
1564
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
1565
+ label: props.t("model"),
1566
+ value: subagentCatalogModel?.model.id ?? subagentDraft.model ?? "",
1567
+ disabled: busy === "subagent" || !subagent.writable,
1568
+ choices: subagentChoices.models,
1569
+ onChange: (model) => {
1570
+ setSubagentDraft({
1571
+ mode: "fixed",
1572
+ ...selectRouteModel(groups, subagentDraft.provider ?? "", model)
1573
+ });
1574
+ }
1575
+ }),
1576
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
1577
+ label: props.t("effort"),
1578
+ value: subagentDraft.reasoningEffort || subagentCatalogModel?.effort || "",
1579
+ disabled: busy === "subagent" || !subagent.writable,
1580
+ choices: subagentEfforts,
1581
+ onChange: (effort) => {
1582
+ const matched = subagentCatalogModel;
1583
+ setSubagentDraft({
1584
+ ...subagentDraft,
1585
+ ...matched?.model.id === void 0 ? {} : { model: matched.model.id },
1586
+ reasoningEffort: effort
1587
+ });
1321
1588
  }
1322
1589
  })
1323
- ] })
1324
- }),
1325
- props.capabilities.centralSubagentRouting.available ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RouteCard, {
1326
- title: props.t("subagent"),
1327
- summary: subagentSummary,
1328
- hint: props.t("subagentHelp"),
1329
- icon: "subagent",
1330
- open: open === "subagent" && subagentOn,
1331
- onToggle: () => {
1332
- if (subagentOn) toggle("subagent");
1590
+ ]
1591
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Actions, {
1592
+ t: props.t,
1593
+ busy: busy === "subagent",
1594
+ disabled: subagentDisabled,
1595
+ ...message?.route === "subagent" ? { message: message.text } : {},
1596
+ onCancel: () => {
1597
+ resetSubagent();
1598
+ setMessage(void 0);
1333
1599
  },
1334
- expandable: subagentOn,
1335
- trailing: subagentDraft === void 0 ? void 0 : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstantSwitch, {
1336
- checked: subagentOn,
1337
- disabled: busy === "subagent" || !subagent.writable,
1338
- label: props.t("subagent"),
1339
- onChange: persistSubagentEnabled
1340
- }),
1341
- children: subagentDraft === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1342
- className: ModelSwitchSettings_module_css_default.hint,
1343
- children: props.t("loading")
1344
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1345
- className: ModelSwitchSettings_module_css_default.formGrid,
1346
- children: [
1347
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
1348
- label: props.t("provider"),
1349
- value: subagentDraft.provider ?? "",
1350
- disabled: busy === "subagent" || !subagent.writable,
1351
- choices: subagentChoices.providers,
1352
- onChange: (provider) => {
1353
- const first = groups.find((group) => group.id === provider)?.models[0];
1354
- setSubagentDraft({
1355
- mode: "fixed",
1356
- ...selectRouteModel(groups, provider, first?.id ?? subagentDraft.model ?? "")
1357
- });
1358
- }
1359
- }),
1360
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
1361
- label: props.t("model"),
1362
- value: subagentCatalogModel?.model.id ?? subagentDraft.model ?? "",
1363
- disabled: busy === "subagent" || !subagent.writable,
1364
- choices: subagentChoices.models,
1365
- onChange: (model) => {
1366
- setSubagentDraft({
1367
- mode: "fixed",
1368
- ...selectRouteModel(groups, subagentDraft.provider ?? "", model)
1369
- });
1370
- }
1371
- }),
1372
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
1373
- label: props.t("effort"),
1374
- value: subagentDraft.reasoningEffort || subagentCatalogModel?.effort || "",
1375
- disabled: busy === "subagent" || !subagent.writable,
1376
- choices: subagentEfforts,
1377
- onChange: (effort) => {
1378
- const matched = subagentCatalogModel;
1379
- setSubagentDraft({
1380
- ...subagentDraft,
1381
- ...matched?.model.id === void 0 ? {} : { model: matched.model.id },
1382
- reasoningEffort: effort
1383
- });
1384
- }
1385
- })
1386
- ]
1387
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Actions, {
1388
- t: props.t,
1389
- busy: busy === "subagent",
1390
- disabled: subagentDisabled,
1391
- ...message?.route === "subagent" ? { message: message.text } : {},
1392
- onCancel: () => {
1393
- resetSubagent();
1394
- setMessage(void 0);
1395
- },
1396
- onSave: saveSubagent
1397
- })] })
1398
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RouteCard, {
1399
- title: props.t("subagent"),
1400
- summary: unavailable("centralSubagentRouting"),
1401
- icon: "subagent",
1402
- open: false,
1403
- onToggle: () => {},
1404
- disabled: true,
1405
- badge: props.t("unavailable"),
1406
- badgeWarn: true
1407
- })
1408
- ]
1600
+ onSave: saveSubagent
1601
+ })] })
1602
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RouteCard, {
1603
+ title: props.t("subagent"),
1604
+ summary: unavailable("centralSubagentRouting"),
1605
+ icon: "subagent",
1606
+ open: false,
1607
+ onToggle: () => {},
1608
+ disabled: true,
1609
+ badge: props.t("unavailable"),
1610
+ badgeWarn: true
1611
+ })]
1409
1612
  }),
1410
1613
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1411
1614
  className: ModelSwitchSettings_module_css_default.group,
@@ -1681,6 +1884,229 @@ window.__ModuleLoader__.load({
1681
1884
  ] })
1682
1885
  })
1683
1886
  ]
1887
+ }),
1888
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1889
+ className: ModelSwitchSettings_module_css_default.group,
1890
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
1891
+ className: ModelSwitchSettings_module_css_default.groupLabel,
1892
+ children: props.t("providerGroups")
1893
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(RouteCard, {
1894
+ title: props.t("providerGroupsCard"),
1895
+ summary: props.t("providerGroupsSummary"),
1896
+ hint: props.t("providerGroupsHelp"),
1897
+ icon: "providerGroups",
1898
+ open: open === "providerGroups",
1899
+ onToggle: () => {
1900
+ toggle("providerGroups");
1901
+ },
1902
+ ...configuredGroupCount > 0 ? { badge: String(configuredGroupCount) } : {},
1903
+ children: [
1904
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1905
+ className: ModelSwitchSettings_module_css_default.hint,
1906
+ children: props.t("providerGroupsBodyHelp")
1907
+ }),
1908
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1909
+ className: ModelSwitchSettings_module_css_default.formGrid,
1910
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
1911
+ label: props.t("provider"),
1912
+ value: groupProvider,
1913
+ disabled: groupControlsDisabled,
1914
+ choices: groupProviderChoices,
1915
+ onChange: setGroupProvider
1916
+ })
1917
+ }),
1918
+ groupProvider === "" || groupDraft === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1919
+ className: ModelSwitchSettings_module_css_default.hint,
1920
+ children: props.t("providerGroupPick")
1921
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1922
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1923
+ className: ModelSwitchSettings_module_css_default.rowCard,
1924
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1925
+ className: ModelSwitchSettings_module_css_default.rowCopy,
1926
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1927
+ className: ModelSwitchSettings_module_css_default.rowTitle,
1928
+ children: props.t("compaction")
1929
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1930
+ className: ModelSwitchSettings_module_css_default.hint,
1931
+ children: groupCompaction === void 0 ? props.t("compactionFollow") : routeName(groups, groupCompaction)
1932
+ })]
1933
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstantSwitch, {
1934
+ checked: groupCompaction !== void 0,
1935
+ disabled: groupControlsDisabled,
1936
+ label: props.t("compaction"),
1937
+ onChange: (value) => {
1938
+ if (value) enableGroupCapability("compaction");
1939
+ else disableGroupCapability("compaction");
1940
+ }
1941
+ })]
1942
+ }),
1943
+ groupCompaction === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1944
+ className: ModelSwitchSettings_module_css_default.formGrid,
1945
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
1946
+ label: props.t("provider"),
1947
+ value: groupCompaction.provider,
1948
+ disabled: groupControlsDisabled,
1949
+ choices: groupCompactionChoices.providers,
1950
+ onChange: (provider) => {
1951
+ const first = groups.find((item) => item.id === provider)?.models[0];
1952
+ setGroupDraft((draft) => draft === void 0 ? draft : {
1953
+ ...draft,
1954
+ compactProvider: provider,
1955
+ compactModel: first?.id ?? ""
1956
+ });
1957
+ }
1958
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
1959
+ label: props.t("model"),
1960
+ value: groupCompaction.model,
1961
+ disabled: groupControlsDisabled,
1962
+ choices: groupCompactionChoices.models,
1963
+ onChange: (model) => {
1964
+ setGroupDraft((draft) => draft === void 0 ? draft : {
1965
+ ...draft,
1966
+ compactModel: model
1967
+ });
1968
+ }
1969
+ })]
1970
+ }),
1971
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1972
+ className: ModelSwitchSettings_module_css_default.rowCard,
1973
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1974
+ className: ModelSwitchSettings_module_css_default.rowCopy,
1975
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1976
+ className: ModelSwitchSettings_module_css_default.rowTitle,
1977
+ children: props.t("autoReview")
1978
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1979
+ className: ModelSwitchSettings_module_css_default.hint,
1980
+ children: groupReview === void 0 ? props.t("autoReviewFollow") : routeName(groups, groupReview)
1981
+ })]
1982
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstantSwitch, {
1983
+ checked: groupReview !== void 0,
1984
+ disabled: groupControlsDisabled,
1985
+ label: props.t("autoReview"),
1986
+ onChange: (value) => {
1987
+ if (value) enableGroupCapability("autoReview");
1988
+ else disableGroupCapability("autoReview");
1989
+ }
1990
+ })]
1991
+ }),
1992
+ groupReview === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1993
+ className: ModelSwitchSettings_module_css_default.formGrid,
1994
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
1995
+ label: props.t("provider"),
1996
+ value: groupReview.provider,
1997
+ disabled: groupControlsDisabled,
1998
+ choices: groupReviewChoices.providers,
1999
+ onChange: (provider) => {
2000
+ const first = groups.find((item) => item.id === provider)?.models[0];
2001
+ setGroupDraft((draft) => draft === void 0 ? draft : {
2002
+ ...draft,
2003
+ autoReviewProvider: provider,
2004
+ autoReviewModel: first?.id ?? ""
2005
+ });
2006
+ }
2007
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
2008
+ label: props.t("model"),
2009
+ value: groupReview.model,
2010
+ disabled: groupControlsDisabled,
2011
+ choices: groupReviewChoices.models,
2012
+ onChange: (model) => {
2013
+ setGroupDraft((draft) => draft === void 0 ? draft : {
2014
+ ...draft,
2015
+ autoReviewModel: model
2016
+ });
2017
+ }
2018
+ })]
2019
+ }),
2020
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2021
+ className: ModelSwitchSettings_module_css_default.rowCard,
2022
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2023
+ className: ModelSwitchSettings_module_css_default.rowCopy,
2024
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2025
+ className: ModelSwitchSettings_module_css_default.rowTitle,
2026
+ children: props.t("subagent")
2027
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2028
+ className: ModelSwitchSettings_module_css_default.hint,
2029
+ children: groupSubagent === void 0 ? props.t("subagentOff") : routeName(groups, groupSubagent)
2030
+ })]
2031
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstantSwitch, {
2032
+ checked: groupSubagent !== void 0,
2033
+ disabled: groupControlsDisabled,
2034
+ label: props.t("subagent"),
2035
+ onChange: (value) => {
2036
+ if (value) enableGroupCapability("subagent");
2037
+ else disableGroupCapability("subagent");
2038
+ }
2039
+ })]
2040
+ }),
2041
+ groupSubagent === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2042
+ className: ModelSwitchSettings_module_css_default.formGrid,
2043
+ children: [
2044
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
2045
+ label: props.t("provider"),
2046
+ value: groupSubagent.provider,
2047
+ disabled: groupControlsDisabled,
2048
+ choices: groupSubagentChoices.providers,
2049
+ onChange: (provider) => {
2050
+ const first = groups.find((item) => item.id === provider)?.models[0];
2051
+ const routed = selectRouteModel(groups, provider, first?.id ?? groupSubagent.model);
2052
+ setGroupDraft((draft) => draft === void 0 ? draft : {
2053
+ ...draft,
2054
+ subagentProvider: routed.provider,
2055
+ subagentModel: routed.model,
2056
+ subagentReasoningEffort: routed.reasoningEffort ?? ""
2057
+ });
2058
+ }
2059
+ }),
2060
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
2061
+ label: props.t("model"),
2062
+ value: groupSubagent.model,
2063
+ disabled: groupControlsDisabled,
2064
+ choices: groupSubagentChoices.models,
2065
+ onChange: (model) => {
2066
+ const routed = selectRouteModel(groups, groupSubagent.provider, model);
2067
+ setGroupDraft((draft) => draft === void 0 ? draft : {
2068
+ ...draft,
2069
+ subagentProvider: routed.provider,
2070
+ subagentModel: routed.model,
2071
+ subagentReasoningEffort: routed.reasoningEffort ?? ""
2072
+ });
2073
+ }
2074
+ }),
2075
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
2076
+ label: props.t("effort"),
2077
+ value: groupDraft.subagentReasoningEffort ?? "",
2078
+ disabled: groupControlsDisabled,
2079
+ choices: groupSubEfforts,
2080
+ onChange: (effort) => {
2081
+ setGroupDraft((draft) => draft === void 0 ? draft : {
2082
+ ...draft,
2083
+ subagentReasoningEffort: effort
2084
+ });
2085
+ }
2086
+ })
2087
+ ]
2088
+ }),
2089
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Actions, {
2090
+ t: props.t,
2091
+ busy: busy === "providerGroups",
2092
+ disabled: groupControlsDisabled || groupProvider === "",
2093
+ ...message?.route === "providerGroups" ? { message: message.text } : {},
2094
+ extra: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2095
+ type: "button",
2096
+ className: cx(ModelSwitchSettings_module_css_default.button, ModelSwitchSettings_module_css_default.secondaryButton),
2097
+ disabled: groupControlsDisabled || groupOf(providerGroups, groupProvider) === void 0,
2098
+ onClick: deleteProviderGroupCard,
2099
+ children: props.t("providerGroupDelete")
2100
+ }),
2101
+ onCancel: () => {
2102
+ resyncGroupDraft();
2103
+ setMessage(void 0);
2104
+ },
2105
+ onSave: saveProviderGroupCard
2106
+ })
2107
+ ] })
2108
+ ]
2109
+ })]
1684
2110
  })
1685
2111
  ]
1686
2112
  });
@@ -1690,22 +2116,21 @@ window.__ModuleLoader__.load({
1690
2116
  const zh$1 = {
1691
2117
  nav: "模型切换",
1692
2118
  title: "模型切换",
1693
- subtitle: "为主会话、子代理和能力工具设置默认模型。修改只影响新请求。",
2119
+ subtitle: "为子代理与能力路由设置固定模型;当前会话模型用输入框旁的选择器直接切换。",
1694
2120
  conversationRoutes: "对话路由",
1695
2121
  capabilityRoutes: "能力路由",
1696
2122
  sendProtection: "发送保护",
1697
2123
  settingsSynced: "设置已同步",
1698
2124
  remoteSettingsUnavailable: "官方 DSH 在非本机浏览器禁用配置读取与写入;请在 Host 的 127.0.0.1 页面打开设置。",
1699
- defaultBadge: "默认",
1700
2125
  cancel: "取消",
1701
2126
  compactOnSwitch: "切换时压缩",
1702
2127
  compactOnSwitchHelp: "发送且换了模型时,先按目标窗口检查上下文;超了就用切换前的模型压一次历史,再继续请求。只选模型、不发送,不会压缩。官方自动压缩和 /compact 不受影响。压缩会产生额外模型用量,摘要可能省略细节。",
1703
- main: "主模型",
1704
2128
  provider: "提供商",
1705
2129
  model: "模型",
1706
2130
  providerDefault: "使用提供商默认值",
1707
2131
  providerDefaultShort: "Provider 默认",
1708
2132
  effort: "推理强度",
2133
+ effortPinned: "已固定",
1709
2134
  save: "保存",
1710
2135
  saving: "保存中…",
1711
2136
  saved: "已保存",
@@ -1719,10 +2144,8 @@ window.__ModuleLoader__.load({
1719
2144
  imageHelp: "统一 generate_image 会调用所选 Codex 或 Grok Adapter;Provider 原有图片工具仍保留。",
1720
2145
  unavailable: "未接入",
1721
2146
  loading: "正在加载设置…",
1722
- readonly: "设置为只读",
1723
2147
  requestFailed: "保存失败",
1724
2148
  catalogFailed: "无法加载模型目录。",
1725
- conflict: "设置已在其他位置更改,请检查最新值后重试。",
1726
2149
  autoReview: "自动审查模型",
1727
2150
  autoReviewFollow: "跟随会话模型",
1728
2151
  autoReviewHelp: "为官方 auto-review(Auto 权限预设的逐调用审查)指定审查模型;需启用官方 auto-review 能力包。关闭时审查跟随当前会话模型。",
@@ -1731,6 +2154,13 @@ window.__ModuleLoader__.load({
1731
2154
  compactionFollow: "跟随会话模型",
1732
2155
  compactionHelp: "为官方压缩(/compact 与官方自动压缩)指定摘要模型;关闭(两个字段都留空)时使用官方默认,即当前会话的目标模型。",
1733
2156
  compactionRouteHelp: "摘要请求固定使用这里的 Provider/模型,与会话目标模型无关;保存后对下一次压缩立即生效。保存时若恰有压缩进行中,可能被打断并在下次重试。",
2157
+ providerGroups: "供应商配置组",
2158
+ providerGroupsCard: "按供应商预设路由",
2159
+ providerGroupsSummary: "会话模型切换到该供应商时自动应用对应组",
2160
+ providerGroupsHelp: "为压缩、子代理、审查按供应商分别保存路由。",
2161
+ providerGroupsBodyHelp: "每个供应商保存一组压缩、子代理、审查路由;本会话的模型切换到该供应商时(无论通过插件的模型选择器还是 TUI 等其他界面),这三条路由自动写入上方对应卡片。切换到未配置的供应商时现有路由保持不变;应用会覆盖三张卡片的当前值(含手动修改)。",
2162
+ providerGroupPick: "选择供应商以编辑其配置组。",
2163
+ providerGroupDelete: "删除该组",
1734
2164
  "reason.central-subagent-routing": "Alpha.4 没有全局子代理启动路由接口。",
1735
2165
  "reason.packaged-preset-roots": "Alpha.4 不支持插件提供额外 preset root。",
1736
2166
  "reason.tool-owner-suppression": "Alpha.4 没有工具所有者或来源抑制接口。",
@@ -1741,22 +2171,21 @@ window.__ModuleLoader__.load({
1741
2171
  const en$1 = {
1742
2172
  nav: "Model Switch",
1743
2173
  title: "Model Switch",
1744
- subtitle: "Set default models for Main, Subagents, and capability tools. Changes affect new requests only.",
2174
+ subtitle: "Pin routes for Subagents and capability tools; switch the active session model through the composer picker.",
1745
2175
  conversationRoutes: "Conversation routes",
1746
2176
  capabilityRoutes: "Capability routes",
1747
2177
  sendProtection: "Send protection",
1748
2178
  settingsSynced: "Settings synced",
1749
2179
  remoteSettingsUnavailable: "Official DSH disables settings reads and writes in non-loopback browsers. Open Settings on the Host loopback URL to edit model routes.",
1750
- defaultBadge: "Default",
1751
2180
  cancel: "Cancel",
1752
2181
  compactOnSwitch: "Compact on switch",
1753
2182
  compactOnSwitchHelp: "When a sent message uses a new model, check the target window first. If needed, compact history with the previous model, then continue. Choosing a model does not compact. Official automatic compaction and /compact are unchanged. Compaction uses extra model quota and may omit details.",
1754
- main: "Main model",
1755
2183
  provider: "Provider",
1756
2184
  model: "Model",
1757
2185
  providerDefault: "Provider default",
1758
2186
  providerDefaultShort: "Provider default",
1759
2187
  effort: "Reasoning effort",
2188
+ effortPinned: "Pinned",
1760
2189
  save: "Save",
1761
2190
  saving: "Saving…",
1762
2191
  saved: "Saved",
@@ -1770,10 +2199,8 @@ window.__ModuleLoader__.load({
1770
2199
  imageHelp: "The stable generate_image tool calls the selected Codex or Grok Adapter; existing provider image tools remain available.",
1771
2200
  unavailable: "Unavailable",
1772
2201
  loading: "Loading settings…",
1773
- readonly: "Settings are read-only",
1774
2202
  requestFailed: "Save failed",
1775
2203
  catalogFailed: "Could not load the model catalog.",
1776
- conflict: "Settings changed elsewhere. Review the latest values and retry.",
1777
2204
  autoReview: "Auto review model",
1778
2205
  autoReviewFollow: "Follows the session model",
1779
2206
  autoReviewHelp: "Pin the reviewer model for the official auto-review gate (per-call review under the Auto permission preset). Requires the official auto-review bundle. When off, reviews follow the session model.",
@@ -1782,6 +2209,13 @@ window.__ModuleLoader__.load({
1782
2209
  compactionFollow: "Follows the conversation model",
1783
2210
  compactionHelp: "Pick the summarization model for official compaction (/compact and official automatic compaction). When off (both fields empty), the official default applies: the conversation target model.",
1784
2211
  compactionRouteHelp: "Summarization requests use exactly this provider/model regardless of the conversation target; saved changes apply to the very next compaction. Saving while a compaction is running may interrupt it; it retries next time.",
2212
+ providerGroups: "Provider groups",
2213
+ providerGroupsCard: "Per-provider route presets",
2214
+ providerGroupsSummary: "Applied automatically when this session switches to that provider",
2215
+ providerGroupsHelp: "Save compaction, subagent, and review routes per provider.",
2216
+ providerGroupsBodyHelp: "Each provider stores one compaction, subagent, and review route group. When this session switches to that provider — through the plugin picker or any other surface such as the TUI — these three routes are written into the cards above. Switching to a provider without a group leaves the current routes untouched. Applying a group overwrites those three cards, including manual edits.",
2217
+ providerGroupPick: "Pick a provider to edit its group.",
2218
+ providerGroupDelete: "Delete group",
1785
2219
  "reason.central-subagent-routing": "Alpha.4 exposes no global Subagent start-routing seam.",
1786
2220
  "reason.packaged-preset-roots": "Alpha.4 exposes no plugin-owned preset root.",
1787
2221
  "reason.tool-owner-suppression": "Alpha.4 exposes no tool owner or provenance suppression seam.",
@@ -1842,13 +2276,8 @@ window.__ModuleLoader__.load({
1842
2276
  }
1843
2277
  async function approvePlanReview(args) {
1844
2278
  const current = args.current;
1845
- const commit = current !== void 0 && current !== null && current.provider === args.selection.provider && current.model === args.selection.model && current.reasoningEffort === args.selection.reasoningEffort ? {
1846
- committed: true,
1847
- mainDefaultRestored: true
1848
- } : await args.select(args.selection);
1849
- if (!commit.committed) return false;
2279
+ if (!(current !== void 0 && current !== null && current.provider === args.selection.provider && current.model === args.selection.model && current.reasoningEffort === args.selection.reasoningEffort ? { committed: true } : await args.select(args.selection)).committed) return false;
1850
2280
  await args.answer();
1851
- if (!commit.mainDefaultRestored) args.onNotRestored?.(args.selection.model);
1852
2281
  return true;
1853
2282
  }
1854
2283
  function planActionView(state, available, hasExecution) {
@@ -2552,7 +2981,7 @@ window.__ModuleLoader__.load({
2552
2981
  children: mark
2553
2982
  });
2554
2983
  }
2555
- function ComposerPicker({ locked, providerLock = null, agentLocked = false, available, unavailableReason, directory, t, draft, onDraftChange, embedded, tone, resolveInteractionOperations, roleOf }) {
2984
+ function ComposerPicker({ locked, providerLock = null, agentLocked = false, available, directory, t, draft, onDraftChange, embedded, tone, resolveInteractionOperations, roleOf }) {
2556
2985
  const { snapshot: state, getDirectorySnapshot, load, select } = directory;
2557
2986
  const [pane, setPane] = (0, react.useState)("root");
2558
2987
  const [searching, setSearching] = (0, react.useState)(false);
@@ -2635,16 +3064,7 @@ window.__ModuleLoader__.load({
2635
3064
  setQuery("");
2636
3065
  };
2637
3066
  const settleSelection = (outcome) => {
2638
- if (outcome.committed) {
2639
- if (!outcome.mainDefaultRestored) {
2640
- toastSeq.current += 1;
2641
- setToast({
2642
- seq: toastSeq.current,
2643
- text: t("plan.notRestored")
2644
- });
2645
- }
2646
- return;
2647
- }
3067
+ if (outcome.committed) return;
2648
3068
  const message = getDirectorySnapshot().error;
2649
3069
  if (message !== null) {
2650
3070
  toastSeq.current += 1;
@@ -3055,11 +3475,11 @@ window.__ModuleLoader__.load({
3055
3475
  ref: triggerRef,
3056
3476
  type: "button",
3057
3477
  className: ComposerPicker_module_css_default.trigger,
3058
- "aria-label": unavailableReason === void 0 ? triggerAria : `${triggerAria}: ${unavailableReason}`,
3478
+ "aria-label": triggerAria,
3059
3479
  "aria-haspopup": "menu",
3060
3480
  "aria-expanded": open,
3061
3481
  "aria-controls": open ? `${id}-menu` : void 0,
3062
- title: unavailableReason ?? triggerLabel,
3482
+ title: triggerLabel,
3063
3483
  disabled: locked,
3064
3484
  onPointerDown: onTriggerPointerDown,
3065
3485
  onClick: onTriggerClick,
@@ -3284,132 +3704,64 @@ window.__ModuleLoader__.load({
3284
3704
  }, operations);
3285
3705
  }
3286
3706
  //#endregion
3287
- //#region src/client/picker/main-default-restore.ts
3288
- /** Bound on how long the unawaited Host default write is awaited. */
3289
- const DEFAULT_WRITE_WAIT_MS = 750;
3290
- function defaultWait(ms) {
3291
- return new Promise((resolve) => {
3292
- setTimeout(resolve, ms);
3293
- });
3294
- }
3295
- /** A fenced snapshot: ready, Host-synced, writable, with a concrete revision. */
3296
- function fenced(snapshot) {
3297
- if (snapshot.status !== "ready" || snapshot.mode !== "host" || !snapshot.writable) return void 0;
3298
- if (snapshot.value === void 0 || snapshot.revision === void 0) return void 0;
3299
- return {
3300
- revision: snapshot.revision,
3301
- value: snapshot.value
3302
- };
3303
- }
3304
- /** Field writes restoring one complete Main selection; the Host replaces the section. */
3305
- function mainDefaultOps(selection) {
3306
- return [
3307
- {
3308
- op: "set",
3309
- path: ["provider"],
3310
- value: selection.provider
3311
- },
3312
- {
3313
- op: "set",
3314
- path: ["model"],
3315
- value: selection.model
3316
- },
3317
- selection.reasoningEffort === void 0 || selection.reasoningEffort === "" ? {
3318
- op: "unset",
3319
- path: ["reasoningEffort"]
3320
- } : {
3321
- op: "set",
3322
- path: ["reasoningEffort"],
3323
- value: selection.reasoningEffort
3324
- }
3325
- ];
3326
- }
3327
- /** An effort never leaks across a comparison: absent and cleared are the same selection. */
3328
- function sameMainSelection(left, right) {
3329
- if (left === void 0) return false;
3330
- const leftEffort = left.reasoningEffort === void 0 || left.reasoningEffort === "" ? void 0 : left.reasoningEffort;
3331
- const rightEffort = right.reasoningEffort === void 0 || right.reasoningEffort === "" ? void 0 : right.reasoningEffort;
3332
- return left.provider === right.provider && left.model === right.model && leftEffort === rightEffort;
3333
- }
3334
- /** Resolve as soon as the revision leaves `revision`, or after the bounded wait. */
3335
- function waitForAdvance(form, revision, wait, timeoutMs) {
3336
- const current = form.getSnapshot();
3337
- if (current.revision !== revision) return Promise.resolve(current);
3338
- return new Promise((resolve) => {
3339
- let settled = false;
3340
- const finish = (snapshot) => {
3341
- if (settled) return;
3342
- settled = true;
3343
- unsubscribe();
3344
- resolve(snapshot);
3345
- };
3346
- const unsubscribe = form.subscribe(() => {
3347
- const next = form.getSnapshot();
3348
- if (next.revision !== revision) finish(next);
3349
- });
3350
- wait(timeoutMs).then(() => {
3351
- finish(form.getSnapshot());
3352
- });
3353
- });
3354
- }
3707
+ //#region src/client/picker/provider-group-watch.ts
3355
3708
  /**
3356
- * Restore the Main default captured before a Session-only model switch.
3357
- *
3358
- * Never throws and never blocks a Plan answer: a failed compensation is
3359
- * reported, not enforced, because the Session selection the human asked for is
3360
- * already installed by the time this runs.
3709
+ * Watch one directory store and apply groups on provider changes.
3361
3710
  *
3362
- * @param form - the official Main Settings namespace form.
3363
- * @param before - the snapshot captured before the switch.
3364
- * @param options - wait/timeout/attempt overrides for tests.
3365
- * @returns Whether the captured default stands again, and whether a newer edit won.
3711
+ * - The first observed provider (or the one already loaded) is the baseline
3712
+ * and never triggers a write, so mounting and initial load stay silent.
3713
+ * - Applies are serialized: a change arriving mid-apply coalesces to the
3714
+ * latest target and runs once the current apply finishes, so two writes
3715
+ * never interleave pair writes.
3716
+ * - `current` being null (catalog still loading) is ignored, not a change.
3717
+ * @param options - the store to watch and the apply to run.
3718
+ * @returns the watch handle.
3366
3719
  */
3367
- async function restoreMainDefault(form, before, options = {}) {
3368
- const captured = fenced(before);
3369
- if (captured === void 0) return {
3370
- restored: true,
3371
- conflict: false
3372
- };
3373
- const wait = options.wait ?? defaultWait;
3374
- const timeoutMs = options.timeoutMs ?? DEFAULT_WRITE_WAIT_MS;
3375
- const attempts = Math.max(1, options.attempts ?? 2);
3376
- await waitForAdvance(form, captured.revision, wait, timeoutMs);
3377
- const observed = fenced(form.getSnapshot());
3378
- if (observed === void 0) return {
3379
- restored: true,
3380
- conflict: false
3381
- };
3382
- if (observed.revision === captured.revision) return {
3383
- restored: true,
3384
- conflict: false
3385
- };
3386
- if (sameMainSelection(observed.value, captured.value)) return {
3387
- restored: true,
3388
- conflict: false
3389
- };
3390
- for (let attempt = 0; attempt < attempts; attempt += 1) {
3391
- const snapshot = fenced(form.getSnapshot());
3392
- if (snapshot === void 0) return {
3393
- restored: false,
3394
- conflict: false
3395
- };
3396
- if (sameMainSelection(snapshot.value, captured.value)) return {
3397
- restored: true,
3398
- conflict: false
3399
- };
3400
- if (await form.mutate(mainDefaultOps(captured.value), snapshot.revision)) return {
3401
- restored: true,
3402
- conflict: false
3403
- };
3404
- if (form.getSnapshot().revision === snapshot.revision) return {
3405
- restored: false,
3406
- conflict: false
3407
- };
3720
+ function createProviderGroupWatch(options) {
3721
+ const onFailure = options.onFailure ?? ((error) => {
3722
+ console.warn("dsh-model-switch: provider group apply failed", error);
3723
+ });
3724
+ let baselined = false;
3725
+ let lastSeen;
3726
+ const initial = options.source.getSnapshot().current;
3727
+ if (initial !== null) {
3728
+ baselined = true;
3729
+ lastSeen = initial.provider;
3408
3730
  }
3409
- return {
3410
- restored: false,
3411
- conflict: true
3731
+ let running = false;
3732
+ let pending;
3733
+ const run = async (provider) => {
3734
+ running = true;
3735
+ try {
3736
+ await options.apply(provider);
3737
+ } catch (error) {
3738
+ onFailure(error);
3739
+ } finally {
3740
+ running = false;
3741
+ if (pending !== void 0) {
3742
+ const next = pending;
3743
+ pending = void 0;
3744
+ run(next);
3745
+ }
3746
+ }
3747
+ };
3748
+ const check = () => {
3749
+ const current = options.source.getSnapshot().current;
3750
+ if (current === null) return;
3751
+ if (!baselined) {
3752
+ baselined = true;
3753
+ lastSeen = current.provider;
3754
+ return;
3755
+ }
3756
+ if (current.provider === lastSeen) return;
3757
+ lastSeen = current.provider;
3758
+ if (running) {
3759
+ pending = current.provider;
3760
+ return;
3761
+ }
3762
+ run(current.provider);
3412
3763
  };
3764
+ return { dispose: options.source.subscribe(check) };
3413
3765
  }
3414
3766
  //#endregion
3415
3767
  //#region src/picker/pending-dismissal.ts
@@ -3625,12 +3977,6 @@ window.__ModuleLoader__.load({
3625
3977
  throw cause instanceof PlanDismissalUnsupportedError ? new Error(unsupportedMessage, { cause }) : new Error(rejectedMessage, { cause });
3626
3978
  }
3627
3979
  }
3628
- /** Match the model seat's admission guard before letting either picker offer a change. */
3629
- function mainDefaultsUnavailableReason(snapshot, t) {
3630
- if (snapshot.mode === "memory") return t("settings.remoteUnavailable");
3631
- if (snapshot.status === "loading") return t("settings.loading");
3632
- if (snapshot.status !== "ready" || snapshot.mode !== "host" || !snapshot.writable || snapshot.value === void 0 || snapshot.revision === void 0) return t("settings.unavailable");
3633
- }
3634
3980
  /** Inline failed lock-read status; history and log reading stay unaffected. */
3635
3981
  function ProviderLockHint(props) {
3636
3982
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -3646,7 +3992,6 @@ window.__ModuleLoader__.load({
3646
3992
  function PlanReviewCard(props) {
3647
3993
  const snapshot = props.useDirectory((value) => value);
3648
3994
  const order = props.useProviderOrder((value) => value);
3649
- const settingsUnavailableReason = mainDefaultsUnavailableReason((0, react.useSyncExternalStore)(props.subscribeMainDefaults, props.getMainDefaultsSnapshot), props.t);
3650
3995
  const lock = (0, react.useSyncExternalStore)(props.providerLockStore.subscribe, props.providerLockStore.getSnapshot);
3651
3996
  const phase = props.useInput((input) => input.phase);
3652
3997
  const blank = props.useSession((session) => session.blank);
@@ -3682,7 +4027,6 @@ window.__ModuleLoader__.load({
3682
4027
  providerLock,
3683
4028
  agentLocked,
3684
4029
  lockFailed: lock.failed,
3685
- ...settingsUnavailableReason === void 0 ? {} : { settingsUnavailableReason },
3686
4030
  directory: pickerDirectoryViewOrdered(snapshot, props, order, props.catalogRoutes?.() ?? {}),
3687
4031
  t: props.t,
3688
4032
  goalRemote: props.goalRemote,
@@ -3691,7 +4035,7 @@ window.__ModuleLoader__.load({
3691
4035
  ...props.roleOf === void 0 ? {} : { roleOf: props.roleOf }
3692
4036
  }, props.matched.key);
3693
4037
  }
3694
- function PlanReviewState({ matched, review, available, providerLock, agentLocked, lockFailed, settingsUnavailableReason, directory, t, goalRemote, planPreview, resolveInteractionOperations, roleOf }) {
4038
+ function PlanReviewState({ matched, review, available, providerLock, agentLocked, lockFailed, directory, t, goalRemote, planPreview, resolveInteractionOperations, roleOf }) {
3695
4039
  const { snapshot, getDirectorySnapshot, load, select } = directory;
3696
4040
  const [execution, setExecution] = (0, react.useState)(snapshot.current ?? void 0);
3697
4041
  const [feedback, setFeedback] = (0, react.useState)("");
@@ -3754,8 +4098,7 @@ window.__ModuleLoader__.load({
3754
4098
  setNotice(null);
3755
4099
  load();
3756
4100
  };
3757
- const settingsBlock = settingsUnavailableReason !== void 0 && execution !== void 0 && (snapshot.current === null || execution.provider !== snapshot.current.provider || execution.model !== snapshot.current.model || execution.reasoningEffort !== snapshot.current.reasoningEffort);
3758
- const executionAllowed = execution !== void 0 && !settingsBlock && runtimeChoiceAllowed(providerLock, agentLocked, execution.provider, snapshot.current?.provider, isAgentRole(roleOf?.(execution.provider)));
4101
+ const executionAllowed = execution !== void 0 && runtimeChoiceAllowed(providerLock, agentLocked, execution.provider, snapshot.current?.provider, isAgentRole(roleOf?.(execution.provider)));
3759
4102
  const action = planActionView({
3760
4103
  busy,
3761
4104
  blocked,
@@ -3767,11 +4110,7 @@ window.__ModuleLoader__.load({
3767
4110
  select,
3768
4111
  selection: model,
3769
4112
  current: snapshot.current,
3770
- answer: () => respondAnswer(matched, review.id, review.approve.label, t("plan.responseRejected"), true),
3771
- onNotRestored: (switched) => {
3772
- setNotice(t("plan.notRestored", { model: switched }));
3773
- console.warn("dsh-model-switch: the Session model changed but the Main default was not restored", switched);
3774
- }
4113
+ answer: () => respondAnswer(matched, review.id, review.approve.label, t("plan.responseRejected"), true)
3775
4114
  })) {
3776
4115
  const message = getDirectorySnapshot().error;
3777
4116
  throw new Error(message === null ? t("plan.modelFailed") : t("error.action", { message }));
@@ -3843,12 +4182,11 @@ window.__ModuleLoader__.load({
3843
4182
  errorLabel: (message) => t("plan.pickerCrash", { message }),
3844
4183
  retryLabel: t("retry"),
3845
4184
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ComposerPicker, {
3846
- locked: busy || blocked || settingsUnavailableReason !== void 0,
4185
+ locked: busy || blocked,
3847
4186
  providerLock,
3848
4187
  agentLocked,
3849
4188
  ...roleOf === void 0 ? {} : { roleOf },
3850
4189
  available,
3851
- ...settingsUnavailableReason === void 0 ? {} : { unavailableReason: settingsUnavailableReason },
3852
4190
  directory,
3853
4191
  t,
3854
4192
  ...resolveInteractionOperations === void 0 ? {} : { resolveInteractionOperations },
@@ -3882,7 +4220,7 @@ window.__ModuleLoader__.load({
3882
4220
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3883
4221
  className: PlanReviewCard_module_css_default.feedback,
3884
4222
  role: "status",
3885
- children: action.error ?? notice ?? (settingsBlock ? settingsUnavailableReason : null)
4223
+ children: action.error ?? notice
3886
4224
  }),
3887
4225
  decline !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3888
4226
  className: PlanReviewCard_module_css_default.feedbackRow,
@@ -4004,9 +4342,6 @@ window.__ModuleLoader__.load({
4004
4342
  "thinking.off": "关",
4005
4343
  "effort.providerDefault": "Default",
4006
4344
  "status.loading": "正在刷新模型列表…",
4007
- "settings.remoteUnavailable": "官方 DSH 在非本机浏览器禁用配置读取与写入;请在 Host 的 127.0.0.1 页面选择模型。",
4008
- "settings.loading": "正在加载主模型设置;加载完成后才能切换模型。",
4009
- "settings.unavailable": "主模型设置不可写;请在 Host 的本机页面检查设置状态。",
4010
4345
  "error.action": "模型操作失败:{message}",
4011
4346
  "error.picker": "模型选择器出错:{message}(点击重试)",
4012
4347
  "action.reload": "重新加载",
@@ -4027,7 +4362,6 @@ window.__ModuleLoader__.load({
4027
4362
  "plan.pickerCrash": "执行模型选择器出错:{message}",
4028
4363
  "plan.feedback": "拒绝时的反馈",
4029
4364
  "plan.feedbackPlaceholder": "说明要改什么(可留空)",
4030
- "plan.notRestored": "执行模型已切换,但主模型默认值未能还原({model});请在「设置 → 模型」中确认默认模型。",
4031
4365
  "plan.dismissUnsupported": "当前 DSH 版本既未提供 PendingQuestion.dismiss() 也未提供 cancel(),无法回到聊天;请直接在输入框里发送你的意见。",
4032
4366
  "plan.goalRun": "设置为目标并执行",
4033
4367
  "plan.goalFailed": "设置目标失败:{message}",
@@ -4060,9 +4394,6 @@ window.__ModuleLoader__.load({
4060
4394
  "thinking.off": "Off",
4061
4395
  "effort.providerDefault": "Default",
4062
4396
  "status.loading": "Refreshing model list…",
4063
- "settings.remoteUnavailable": "Official DSH disables settings reads and writes in non-loopback browsers. Select a model on the Host loopback URL.",
4064
- "settings.loading": "Main model settings are loading. Wait until they are ready before switching models.",
4065
- "settings.unavailable": "Main model settings are not writable. Check their status on the Host loopback URL.",
4066
4397
  "error.action": "Model operation failed: {message}",
4067
4398
  "error.picker": "Model picker error: {message} (click to retry)",
4068
4399
  "action.reload": "Reload",
@@ -4083,7 +4414,6 @@ window.__ModuleLoader__.load({
4083
4414
  "plan.pickerCrash": "Execution model picker error: {message}",
4084
4415
  "plan.feedback": "Feedback when declining",
4085
4416
  "plan.feedbackPlaceholder": "Say what to change (optional)",
4086
- "plan.notRestored": "The execution model switched, but the Main default could not be restored ({model}); confirm the default in Settings → Models.",
4087
4417
  "plan.dismissUnsupported": "This DSH Host exposes neither PendingQuestion.dismiss() nor cancel(), so the review cannot return to chat; send your feedback as a message instead.",
4088
4418
  "plan.goalRun": "Set as goal and run",
4089
4419
  "plan.goalFailed": "Could not set the goal: {message}",
@@ -4183,7 +4513,6 @@ window.__ModuleLoader__.load({
4183
4513
  function ModelSeat(props) {
4184
4514
  const directory = props.useDirectory((snapshot) => snapshot);
4185
4515
  const order = props.useProviderOrder((value) => value);
4186
- const settingsUnavailableReason = mainDefaultsUnavailableReason((0, react.useSyncExternalStore)(props.subscribeMainDefaults, props.getMainDefaultsSnapshot), props.t);
4187
4516
  const lock = (0, react.useSyncExternalStore)(props.providerLockStore.subscribe, props.providerLockStore.getSnapshot);
4188
4517
  const phase = props.useInput((input) => input.phase);
4189
4518
  const blank = props.useSession((session) => session.blank);
@@ -4199,12 +4528,11 @@ window.__ModuleLoader__.load({
4199
4528
  ]);
4200
4529
  const providerLock = effectiveProviderLock(lock, directory.current?.provider, active, isAgentRole(props.roleOf?.(directory.current?.provider ?? "")));
4201
4530
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [lock.failed && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProviderLockHint, { t: props.t }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ComposerPicker, {
4202
- locked: props.locked || settingsUnavailableReason !== void 0 || lock.failed && directory.current === null,
4531
+ locked: props.locked || lock.failed && directory.current === null,
4203
4532
  providerLock,
4204
4533
  agentLocked: agentProviderLocked(blank, providerLock, active),
4205
4534
  ...props.roleOf === void 0 ? {} : { roleOf: props.roleOf },
4206
4535
  available: props.available,
4207
- ...settingsUnavailableReason === void 0 ? {} : { unavailableReason: settingsUnavailableReason },
4208
4536
  directory: pickerDirectoryViewOrdered(directory, props, order, props.catalogRoutes?.() ?? {}),
4209
4537
  t: props.t,
4210
4538
  ...props.resolveInteractionOperations === void 0 ? {} : { resolveInteractionOperations: props.resolveInteractionOperations }
@@ -4229,7 +4557,23 @@ window.__ModuleLoader__.load({
4229
4557
  ], (scope) => {
4230
4558
  const models = scope.modelDirectories;
4231
4559
  const sessions = scope.sessions;
4232
- const mainDefaults = scope.configForms.get(MAIN_DEFAULT_CONFIG_ID);
4560
+ const ownedForm = scope.configForms.get(MODEL_SWITCH_CONFIG_ID);
4561
+ const groupWatches = /* @__PURE__ */ new Map();
4562
+ const watchGroupsFor = (sessionId, source) => {
4563
+ const key = String(sessionId);
4564
+ if (groupWatches.has(key)) return;
4565
+ const watch = createProviderGroupWatch({
4566
+ source,
4567
+ apply: async (provider) => {
4568
+ await applyProviderGroupTo(ownedForm, normalizeProviderGroups(ownedForm.getSnapshot().value?.providerGroups), provider);
4569
+ }
4570
+ });
4571
+ groupWatches.set(key, watch.dispose);
4572
+ };
4573
+ scope.effect(() => () => {
4574
+ for (const dispose of groupWatches.values()) dispose();
4575
+ groupWatches.clear();
4576
+ });
4233
4577
  let directoryService;
4234
4578
  const orderStore = providerOrderStore(scope.configForms.get(PROVIDERS_CONFIG_ID));
4235
4579
  scope.inject(["providerDirectory"], (directoryScope) => {
@@ -4260,6 +4604,7 @@ window.__ModuleLoader__.load({
4260
4604
  };
4261
4605
  const roleOf = (key) => readProviderRole(directoryService, key);
4262
4606
  const providerLockStore = createProviderLockStore(readLock);
4607
+ if (available) watchGroupsFor(sessionId, directory.store);
4263
4608
  return {
4264
4609
  available,
4265
4610
  roleOf,
@@ -4268,8 +4613,6 @@ window.__ModuleLoader__.load({
4268
4613
  refreshProviderLock: () => {
4269
4614
  providerLockStore.refresh();
4270
4615
  },
4271
- subscribeMainDefaults: (listener) => mainDefaults.subscribe(listener),
4272
- getMainDefaultsSnapshot: () => mainDefaults.getSnapshot(),
4273
4616
  goalRemote: () => goalRemoteFrom(scope),
4274
4617
  planPreview,
4275
4618
  hooks: {
@@ -4282,31 +4625,22 @@ window.__ModuleLoader__.load({
4282
4625
  if (available) directory.load().catch(() => {});
4283
4626
  },
4284
4627
  select: async (selection) => {
4285
- const uncommitted = {
4286
- committed: false,
4287
- mainDefaultRestored: true
4288
- };
4289
- if (!available) return uncommitted;
4628
+ if (!available) return { committed: false };
4290
4629
  const state = await providerLockStore.refresh();
4291
4630
  const currentProvider = directory.store.getSnapshot().current?.provider;
4292
4631
  if (!isProviderAllowed(state, selection.provider, currentProvider, {
4293
4632
  ...readSessionState(sessions, sessionId),
4294
4633
  agent: isAgentRole(roleOf(selection.provider)),
4295
4634
  currentAgent: currentProvider !== void 0 && isAgentRole(roleOf(currentProvider))
4296
- })) return uncommitted;
4297
- const defaultBeforeSwitch = mainDefaults.getSnapshot();
4298
- if (defaultBeforeSwitch.status !== "ready" || defaultBeforeSwitch.mode !== "host" || !defaultBeforeSwitch.writable || defaultBeforeSwitch.value === void 0 || defaultBeforeSwitch.revision === void 0) return uncommitted;
4635
+ })) return { committed: false };
4299
4636
  let result;
4300
4637
  try {
4301
4638
  result = await directory.select(selection);
4302
4639
  } catch {
4303
- return uncommitted;
4640
+ return { committed: false };
4304
4641
  }
4305
- if (!result.ok) return uncommitted;
4306
- return {
4307
- committed: true,
4308
- mainDefaultRestored: (await restoreMainDefault(mainDefaults, defaultBeforeSwitch)).restored
4309
- };
4642
+ if (!result.ok) return { committed: false };
4643
+ return { committed: true };
4310
4644
  }
4311
4645
  };
4312
4646
  };
@@ -4403,46 +4737,12 @@ window.__ModuleLoader__.load({
4403
4737
  }), "dsh-model-switch: localized Settings section");
4404
4738
  const t = ctx.locale.bind(localeNamespace);
4405
4739
  const remote = ctx.remote;
4406
- const mainForm = ctx.configForms.get(MAIN_DEFAULT_CONFIG_ID);
4407
4740
  const owned = ctx.configForms.get(MODEL_SWITCH_CONFIG_ID);
4408
- const main = deriveConfigForm(mainForm, (value) => value, {
4409
- provider: "provider",
4410
- model: "model",
4411
- reasoningEffort: "reasoningEffort"
4412
- });
4413
4741
  const subagent = deriveConfigForm(owned, deriveSubagentSettings, SUBAGENT_SETTINGS_FIELDS);
4414
4742
  const search = deriveConfigForm(owned, deriveSearchSettings, SEARCH_SETTINGS_FIELDS);
4415
4743
  const image = deriveConfigForm(owned, deriveImageSettings, IMAGE_SETTINGS_FIELDS);
4416
4744
  const autoReview = deriveConfigForm(owned, deriveAutoReviewSettings, AUTO_REVIEW_SETTINGS_FIELDS);
4417
4745
  const compaction = deriveConfigForm(owned, deriveCompactionSettings, COMPACTION_SETTINGS_FIELDS);
4418
- const saveMain = async (next, expectedRevision) => {
4419
- if (!await mainForm.mutate([
4420
- {
4421
- op: "set",
4422
- path: ["provider"],
4423
- value: next.provider
4424
- },
4425
- {
4426
- op: "set",
4427
- path: ["model"],
4428
- value: next.model
4429
- },
4430
- next.reasoningEffort === void 0 || next.reasoningEffort === "" ? {
4431
- op: "unset",
4432
- path: ["reasoningEffort"]
4433
- } : {
4434
- op: "set",
4435
- path: ["reasoningEffort"],
4436
- value: next.reasoningEffort
4437
- }
4438
- ], expectedRevision)) {
4439
- if (mainForm.getSnapshot().revision !== expectedRevision) throw new MainSettingsConflictError(t("conflict"));
4440
- throw new Error("settings-rejected");
4441
- }
4442
- const revision = mainForm.getSnapshot().revision;
4443
- if (revision === void 0) throw new Error("settings unavailable after save");
4444
- return revision;
4445
- };
4446
4746
  const connectionRpc = (() => {
4447
4747
  try {
4448
4748
  const rpc = ctx.get("connection", false)?.rpc;
@@ -4475,7 +4775,6 @@ window.__ModuleLoader__.load({
4475
4775
  inject: () => ({
4476
4776
  t,
4477
4777
  hooks: {
4478
- mainSettings: main,
4479
4778
  subagentSettings: subagent,
4480
4779
  searchSettings: search,
4481
4780
  imageSettings: image,
@@ -4484,7 +4783,6 @@ window.__ModuleLoader__.load({
4484
4783
  switchSettings: owned
4485
4784
  },
4486
4785
  capabilities: RUNTIME_CAPABILITIES,
4487
- saveMain,
4488
4786
  setSubagent: (field, value) => value === void 0 ? subagent.unset(field) : subagent.set(field, value),
4489
4787
  setCapability: (route, field, value) => {
4490
4788
  const scope = route === "search" ? search : route === "image" ? image : route === "compaction" ? compaction : autoReview;
@@ -4493,6 +4791,16 @@ window.__ModuleLoader__.load({
4493
4791
  setCompactOnSwitch: async (value) => {
4494
4792
  if (!await owned.set("compactOnSwitch", value)) throw new Error("settings-rejected");
4495
4793
  },
4794
+ saveProviderGroup: async (provider, group) => {
4795
+ const current = owned.getSnapshot().value?.providerGroups;
4796
+ const next = withProviderGroup(current, provider, group);
4797
+ if (next.length === 0) {
4798
+ if (current === void 0) return;
4799
+ if (!await owned.unset("providerGroups")) throw new Error("settings-rejected");
4800
+ return;
4801
+ }
4802
+ if (!await owned.set("providerGroups", next)) throw new Error("settings-rejected");
4803
+ },
4496
4804
  providerRoleOf,
4497
4805
  loadCatalog: async () => {
4498
4806
  const response = await remote.session.modelCatalog();