dsh-model-switch 0.4.20 → 0.4.22

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
@@ -38,6 +38,11 @@ window.__ModuleLoader__.load({
38
38
  provider: "autoReviewProvider",
39
39
  model: "autoReviewModel"
40
40
  });
41
+ /** Compaction summarization route for `/compact` and automatic compaction; both unset means the official default (the conversation target). */
42
+ const COMPACTION_SETTINGS_FIELDS = Object.freeze({
43
+ provider: "compactProvider",
44
+ model: "compactModel"
45
+ });
41
46
  function deriveSubagentSettings(settings) {
42
47
  return {
43
48
  mode: settings.subagentMode,
@@ -64,11 +69,185 @@ window.__ModuleLoader__.load({
64
69
  ...settings.autoReviewModel === void 0 ? {} : { model: settings.autoReviewModel }
65
70
  };
66
71
  }
67
- /** Whether both reviewer fields form a complete route; an incomplete pair still follows the session model. */
68
- function autoReviewRouteConfigured(route) {
72
+ function deriveCompactionSettings(settings) {
73
+ return {
74
+ ...settings.compactProvider === void 0 ? {} : { provider: settings.compactProvider },
75
+ ...settings.compactModel === void 0 ? {} : { model: settings.compactModel }
76
+ };
77
+ }
78
+ /** Whether both fields of a model route form a complete pair; an incomplete pair stays off. */
79
+ function routePairConfigured(route) {
69
80
  return route !== void 0 && (route.provider ?? "").trim() !== "" && (route.model ?? "").trim() !== "";
70
81
  }
71
82
  //#endregion
83
+ //#region src/provider-group.ts
84
+ const ROUTE_FIELDS = Object.freeze([
85
+ "compactProvider",
86
+ "compactModel",
87
+ "autoReviewProvider",
88
+ "autoReviewModel",
89
+ "subagentProvider",
90
+ "subagentModel",
91
+ "subagentReasoningEffort"
92
+ ]);
93
+ /** Own data property, so a literal '__proto__' provider id can never touch a prototype. */
94
+ function setKey(target, key, value) {
95
+ Object.defineProperty(target, key, {
96
+ value,
97
+ enumerable: true,
98
+ writable: true,
99
+ configurable: true
100
+ });
101
+ }
102
+ /** A non-blank string. Blank values behave exactly like an absent field. */
103
+ function present(value) {
104
+ return typeof value === "string" && value.trim() !== "";
105
+ }
106
+ /** Normalize one raw entry to its owning provider and route view; undefined for non-object junk. */
107
+ function normalizeEntry(value) {
108
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
109
+ const source = value;
110
+ const group = {};
111
+ for (const field of ROUTE_FIELDS) {
112
+ const item = source[field];
113
+ if (present(item)) group[field] = item;
114
+ }
115
+ return {
116
+ provider: present(source.provider) ? source.provider : "",
117
+ group
118
+ };
119
+ }
120
+ /**
121
+ * Strip a raw stored value to well-formed groups; never throws. Both storage
122
+ * shapes are accepted: the stored entry array (keys live in each entry's
123
+ * `provider` field) and a hand-written provider → routes map (keys are the
124
+ * providers). All-empty entries stay: they pin a provider to every default.
125
+ */
126
+ function normalizeProviderGroups(raw) {
127
+ const groups = {};
128
+ if (Array.isArray(raw)) {
129
+ for (const entry of raw) {
130
+ const view = normalizeEntry(entry);
131
+ if (view === void 0 || view.provider === "") continue;
132
+ setKey(groups, view.provider, view.group);
133
+ }
134
+ return groups;
135
+ }
136
+ if (raw !== null && typeof raw === "object") for (const [key, value] of Object.entries(raw)) {
137
+ if (key.trim() === "") continue;
138
+ const view = normalizeEntry(value);
139
+ if (view === void 0) continue;
140
+ setKey(groups, key, view.group);
141
+ }
142
+ return groups;
143
+ }
144
+ /**
145
+ * The saved group for a provider.
146
+ * @returns the normalized group, or undefined when the provider has no group —
147
+ * which is what leaves the live routes untouched during a switch.
148
+ */
149
+ function groupOf(groups, provider) {
150
+ if (groups === void 0 || provider.trim() === "") return void 0;
151
+ if (!Object.prototype.hasOwnProperty.call(groups, provider)) return void 0;
152
+ return groups[provider];
153
+ }
154
+ /** A complete pinned pair; either half missing leaves the capability at its default. */
155
+ function pair(group, providerField, modelField) {
156
+ const provider = group[providerField];
157
+ const model = group[modelField];
158
+ return present(provider) && present(model) ? {
159
+ provider,
160
+ model
161
+ } : void 0;
162
+ }
163
+ /**
164
+ * Ordered writes applying one group with full-replacement semantics: pinned
165
+ * capabilities land as pairs, everything else reverts to its default state
166
+ * (compaction/review follow the session model; Subagent follows official
167
+ * inherit). The mode write comes last so a fixed route never publishes with
168
+ * the previous provider's fields still half-written.
169
+ */
170
+ function providerGroupWrites(group) {
171
+ const compact = pair(group, "compactProvider", "compactModel");
172
+ const review = pair(group, "autoReviewProvider", "autoReviewModel");
173
+ const subagent = pair(group, "subagentProvider", "subagentModel");
174
+ const effort = subagent !== void 0 && present(group.subagentReasoningEffort) ? group.subagentReasoningEffort : void 0;
175
+ return [
176
+ {
177
+ field: "compactProvider",
178
+ value: compact?.provider
179
+ },
180
+ {
181
+ field: "compactModel",
182
+ value: compact?.model
183
+ },
184
+ {
185
+ field: "autoReviewProvider",
186
+ value: review?.provider
187
+ },
188
+ {
189
+ field: "autoReviewModel",
190
+ value: review?.model
191
+ },
192
+ {
193
+ field: "subagentProvider",
194
+ value: subagent?.provider
195
+ },
196
+ {
197
+ field: "subagentModel",
198
+ value: subagent?.model
199
+ },
200
+ {
201
+ field: "subagentReasoningEffort",
202
+ value: effort
203
+ },
204
+ {
205
+ field: "subagentMode",
206
+ mode: subagent !== void 0 ? "fixed" : "follow-main"
207
+ }
208
+ ];
209
+ }
210
+ /** Storage entry for one provider's group; absent or blank route fields become ''. */
211
+ function entryOf(provider, group) {
212
+ return {
213
+ provider,
214
+ compactProvider: group.compactProvider ?? "",
215
+ compactModel: group.compactModel ?? "",
216
+ autoReviewProvider: group.autoReviewProvider ?? "",
217
+ autoReviewModel: group.autoReviewModel ?? "",
218
+ subagentProvider: group.subagentProvider ?? "",
219
+ subagentModel: group.subagentModel ?? "",
220
+ subagentReasoningEffort: group.subagentReasoningEffort ?? ""
221
+ };
222
+ }
223
+ /**
224
+ * The stored entry list with one provider's group replaced (an entry) or
225
+ * removed (undefined). Every entry is re-normalized and de-duplicated so a
226
+ * save can never write junk back; the input is never mutated. A blank
227
+ * provider only normalizes.
228
+ */
229
+ function withProviderGroup(storage, provider, group) {
230
+ const groups = { ...normalizeProviderGroups(storage) };
231
+ if (provider.trim() !== "") {
232
+ if (group === void 0) delete groups[provider];
233
+ else setKey(groups, provider, group);
234
+ }
235
+ return Object.entries(groups).map(([key, value]) => entryOf(key, value));
236
+ }
237
+ /**
238
+ * Apply a saved group's routes to the live flat settings.
239
+ * @returns true when a group existed and every write settled; false when the
240
+ * provider has no group (nothing was written). A rejected write propagates.
241
+ */
242
+ async function applyProviderGroupTo(sink, groups, provider) {
243
+ const group = groupOf(groups, provider);
244
+ if (group === void 0) return false;
245
+ for (const write of providerGroupWrites(group)) if (write.field === "subagentMode") await sink.set("subagentMode", write.mode);
246
+ else if (write.value === void 0) await sink.unset(write.field);
247
+ else await sink.set(write.field, write.value);
248
+ return true;
249
+ }
250
+ //#endregion
72
251
  //#region src/client/derived-config-form.ts
73
252
  function deriveConfigForm(source, project, fields) {
74
253
  let sourceSnapshot;
@@ -617,7 +796,53 @@ window.__ModuleLoader__.load({
617
796
  function compact(...values) {
618
797
  return values.filter((value) => value !== void 0 && value !== "").join(" · ");
619
798
  }
799
+ /** A complete pinned pair; either half blank leaves the capability at its default. */
800
+ function draftPair(provider, model) {
801
+ return provider !== void 0 && provider.trim() !== "" && model !== void 0 && model.trim() !== "" ? {
802
+ provider,
803
+ model
804
+ } : void 0;
805
+ }
620
806
  function RouteIcon({ kind }) {
807
+ if (kind === "providerGroups") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
808
+ width: "18",
809
+ height: "18",
810
+ viewBox: "0 0 18 18",
811
+ fill: "none",
812
+ "aria-hidden": "true",
813
+ children: [
814
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
815
+ d: "M3 5.5h12M3 12.5h12",
816
+ stroke: "currentColor",
817
+ strokeLinecap: "round"
818
+ }),
819
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
820
+ cx: "6.5",
821
+ cy: "5.5",
822
+ r: "2",
823
+ stroke: "currentColor"
824
+ }),
825
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
826
+ cx: "11.5",
827
+ cy: "12.5",
828
+ r: "2",
829
+ stroke: "currentColor"
830
+ })
831
+ ]
832
+ });
833
+ if (kind === "compaction") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
834
+ width: "18",
835
+ height: "18",
836
+ viewBox: "0 0 18 18",
837
+ fill: "none",
838
+ "aria-hidden": "true",
839
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
840
+ d: "M5 3.5 9 7.5l4-4M5 14.5 9 10.5l4 4",
841
+ stroke: "currentColor",
842
+ strokeLinecap: "round",
843
+ strokeLinejoin: "round"
844
+ })
845
+ });
621
846
  if (kind === "main") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
622
847
  width: "18",
623
848
  height: "18",
@@ -816,7 +1041,7 @@ window.__ModuleLoader__.load({
816
1041
  })]
817
1042
  });
818
1043
  }
819
- function Actions({ t, busy, disabled, message, onCancel, onSave }) {
1044
+ function Actions({ t, busy, disabled, message, extra, onCancel, onSave }) {
820
1045
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
821
1046
  className: ModelSwitchSettings_module_css_default.cardFooter,
822
1047
  children: [
@@ -824,6 +1049,7 @@ window.__ModuleLoader__.load({
824
1049
  className: cx(ModelSwitchSettings_module_css_default.hint, ModelSwitchSettings_module_css_default.message),
825
1050
  children: message
826
1051
  }),
1052
+ extra,
827
1053
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
828
1054
  type: "button",
829
1055
  className: cx(ModelSwitchSettings_module_css_default.button, ModelSwitchSettings_module_css_default.secondaryButton),
@@ -890,45 +1116,20 @@ window.__ModuleLoader__.load({
890
1116
  const search = props.useSearchSettings((value) => value);
891
1117
  const image = props.useImageSettings((value) => value);
892
1118
  const autoReview = props.useAutoReviewSettings((value) => value);
1119
+ const compaction = props.useCompactionSettings((value) => value);
893
1120
  const switchSettings = props.useSwitchSettings((value) => value);
894
1121
  const [open, setOpen] = (0, react.useState)();
895
1122
  const [subagentDraft, setSubagentDraft, resetSubagent] = useDraft(subagent);
896
1123
  const [searchDraft, setSearchDraft, resetSearch] = useDraft(search);
897
1124
  const [imageDraft, setImageDraft, resetImage] = useDraft(image);
898
1125
  const [autoReviewDraft, setAutoReviewDraft, resetAutoReview] = useDraft(autoReview);
1126
+ const [compactionDraft, setCompactionDraft, resetCompaction] = useDraft(compaction);
899
1127
  const [busy, setBusy] = (0, react.useState)();
900
1128
  const [message, setMessage] = (0, react.useState)();
901
1129
  const [compactBusy, setCompactBusy] = (0, react.useState)(false);
902
1130
  const [compactError, setCompactError] = (0, react.useState)();
903
- const planCompactValue = switchSettings.value?.planCompactThreshold ?? 2e5;
904
- const [planCompactDraft, setPlanCompactDraft] = (0, react.useState)(() => String(planCompactValue));
905
- const [planCompactBusy, setPlanCompactBusy] = (0, react.useState)(false);
906
- const [planCompactError, setPlanCompactError] = (0, react.useState)();
907
- (0, react.useEffect)(() => {
908
- setPlanCompactDraft(String(planCompactValue));
909
- setPlanCompactError(void 0);
910
- }, [planCompactValue]);
911
- /** Commit the threshold on blur/Enter only when it is a non-negative integer. */
912
- const commitPlanCompactThreshold = () => {
913
- const parsed = Number(planCompactDraft);
914
- if (!Number.isInteger(parsed) || parsed < 0) {
915
- setPlanCompactError(props.t("planCompactThresholdInvalid"));
916
- return;
917
- }
918
- if (parsed === planCompactValue) {
919
- setPlanCompactError(void 0);
920
- return;
921
- }
922
- setPlanCompactBusy(true);
923
- setPlanCompactError(void 0);
924
- props.setPlanCompactThreshold(parsed).then(() => {
925
- setPlanCompactBusy(false);
926
- }, (error) => {
927
- setPlanCompactBusy(false);
928
- setPlanCompactDraft(String(planCompactValue));
929
- setPlanCompactError(error instanceof Error ? error.message : props.t("requestFailed"));
930
- });
931
- };
1131
+ const [groupProvider, setGroupProvider] = (0, react.useState)("");
1132
+ const [groupDraft, setGroupDraft] = (0, react.useState)(void 0);
932
1133
  const loadSearchCapabilities = props.loadCapabilities;
933
1134
  const [searchSnapshot, setSearchSnapshot] = (0, react.useState)(void 0);
934
1135
  const [searchError, setSearchError] = (0, react.useState)(void 0);
@@ -967,6 +1168,60 @@ window.__ModuleLoader__.load({
967
1168
  if (timer !== void 0) clearTimeout(timer);
968
1169
  };
969
1170
  }, [loadSearchCapabilities]);
1171
+ const providerGroups = normalizeProviderGroups(switchSettings.value?.providerGroups);
1172
+ /** Seed an unsaved group from the live routes, mirroring how the switches seed from Main. */
1173
+ const seedGroupFromActive = () => {
1174
+ const compact = compaction.value !== void 0 && routePairConfigured(compaction.value) ? {
1175
+ provider: compaction.value.provider ?? "",
1176
+ model: compaction.value.model ?? ""
1177
+ } : void 0;
1178
+ const review = autoReview.value !== void 0 && routePairConfigured(autoReview.value) ? {
1179
+ provider: autoReview.value.provider ?? "",
1180
+ model: autoReview.value.model ?? ""
1181
+ } : void 0;
1182
+ const fixedSubagent = subagent.value !== void 0 && subagent.value.mode === "fixed" && routePairConfigured(subagent.value) ? {
1183
+ provider: subagent.value.provider ?? "",
1184
+ model: subagent.value.model ?? "",
1185
+ reasoningEffort: subagent.value.reasoningEffort
1186
+ } : void 0;
1187
+ return {
1188
+ ...compact === void 0 ? {} : {
1189
+ compactProvider: compact.provider,
1190
+ compactModel: compact.model
1191
+ },
1192
+ ...review === void 0 ? {} : {
1193
+ autoReviewProvider: review.provider,
1194
+ autoReviewModel: review.model
1195
+ },
1196
+ ...fixedSubagent === void 0 ? {} : {
1197
+ subagentProvider: fixedSubagent.provider,
1198
+ subagentModel: fixedSubagent.model,
1199
+ ...fixedSubagent.reasoningEffort === void 0 ? {} : { subagentReasoningEffort: fixedSubagent.reasoningEffort }
1200
+ }
1201
+ };
1202
+ };
1203
+ /** Reload the editor draft: the saved group when one exists, else a seed from the live routes. */
1204
+ const resyncGroupDraft = () => {
1205
+ if (groupProvider === "") {
1206
+ setGroupDraft(void 0);
1207
+ return;
1208
+ }
1209
+ setGroupDraft(groupOf(providerGroups, groupProvider) ?? seedGroupFromActive());
1210
+ };
1211
+ (0, react.useEffect)(() => {
1212
+ if (groupProvider !== "") return;
1213
+ const initial = controller.draft?.provider;
1214
+ if (initial !== void 0 && initial.trim() !== "") setGroupProvider(initial);
1215
+ }, [groupProvider, controller.draft]);
1216
+ (0, react.useEffect)(() => {
1217
+ resyncGroupDraft();
1218
+ }, [
1219
+ groupProvider,
1220
+ switchSettings.value,
1221
+ subagent.value,
1222
+ autoReview.value,
1223
+ compaction.value
1224
+ ]);
970
1225
  if ([
971
1226
  controller.main,
972
1227
  controller.subagent,
@@ -1020,13 +1275,16 @@ window.__ModuleLoader__.load({
1020
1275
  const searchAvailable = searchLive?.searchProviderAdapters.available ?? false;
1021
1276
  const imageChoices = capabilityChoices(groups, imageDraft, props.capabilities.imageProviderAdapters.providers ?? [], "image");
1022
1277
  const autoReviewChoices = deriveRouteChoices(groups, autoReviewDraft);
1023
- const autoReviewOn = autoReviewRouteConfigured(autoReview.value);
1278
+ const autoReviewOn = routePairConfigured(autoReview.value);
1279
+ const compactionChoices = deriveRouteChoices(groups, compactionDraft);
1280
+ const compactionOn = routePairConfigured(compaction.value);
1024
1281
  const synced = [
1025
1282
  main,
1026
1283
  subagent,
1027
1284
  search,
1028
1285
  image,
1029
- autoReview
1286
+ autoReview,
1287
+ compaction
1030
1288
  ].every((snapshot) => snapshot.status === "ready");
1031
1289
  const run = async (route, operation) => {
1032
1290
  if (busy !== void 0) return;
@@ -1092,6 +1350,13 @@ window.__ModuleLoader__.load({
1092
1350
  if (autoReview.value?.model !== autoReviewDraft.model) await props.setCapability("autoReview", "model", autoReviewDraft.model);
1093
1351
  });
1094
1352
  };
1353
+ const saveCompaction = () => {
1354
+ if (compactionDraft === void 0) return;
1355
+ run("compaction", async () => {
1356
+ if (compaction.value?.provider !== compactionDraft.provider) await props.setCapability("compaction", "provider", compactionDraft.provider);
1357
+ if (compaction.value?.model !== compactionDraft.model) await props.setCapability("compaction", "model", compactionDraft.model);
1358
+ });
1359
+ };
1095
1360
  /**
1096
1361
  * The reviewer switch: Off unsets both stored fields (follow the session
1097
1362
  * model); On seeds the route from Main so the pin takes effect immediately
@@ -1129,6 +1394,44 @@ window.__ModuleLoader__.load({
1129
1394
  });
1130
1395
  });
1131
1396
  };
1397
+ /**
1398
+ * The compaction switch: Off unsets both stored fields (official default =
1399
+ * the conversation target); On seeds the route from Main so the pin takes
1400
+ * effect immediately and stays editable below. A failed write reports,
1401
+ * never flips silently.
1402
+ */
1403
+ const persistCompactionEnabled = (enabled) => {
1404
+ if (busy === "compaction" || !compaction.writable || compaction.value === void 0) return;
1405
+ if (!enabled) {
1406
+ setOpen((current) => current === "compaction" ? void 0 : current);
1407
+ run("compaction", async () => {
1408
+ if (compaction.value?.provider !== void 0) await props.setCapability("compaction", "provider", void 0);
1409
+ if (compaction.value?.model !== void 0) await props.setCapability("compaction", "model", void 0);
1410
+ });
1411
+ return;
1412
+ }
1413
+ const seed = draft;
1414
+ if (seed === void 0 || seed.provider.trim() === "" || seed.model.trim() === "") return;
1415
+ setBusy("compaction");
1416
+ setMessage(void 0);
1417
+ (async () => {
1418
+ if (compaction.value?.provider !== seed.provider) await props.setCapability("compaction", "provider", seed.provider);
1419
+ if (compaction.value?.model !== seed.model) await props.setCapability("compaction", "model", seed.model);
1420
+ })().then(() => {
1421
+ setCompactionDraft({
1422
+ provider: seed.provider,
1423
+ model: seed.model
1424
+ });
1425
+ setBusy(void 0);
1426
+ setOpen("compaction");
1427
+ }, (error) => {
1428
+ setBusy(void 0);
1429
+ setMessage({
1430
+ route: "compaction",
1431
+ text: error instanceof Error ? error.message : props.t("requestFailed")
1432
+ });
1433
+ });
1434
+ };
1132
1435
  const mainSummary = draft === void 0 ? props.t("loading") : compact(routeName(groups, draft), mainEffectiveEffort);
1133
1436
  const subagentRole = subagentDraft?.mode === "fixed" && subagentDraft.provider !== void 0 && subagentDraft.provider !== "" ? props.providerRoleOf?.(subagentDraft.provider) : void 0;
1134
1437
  const subagentOn = subagentDraft?.mode === "fixed";
@@ -1136,6 +1439,84 @@ window.__ModuleLoader__.load({
1136
1439
  const subagentDisabled = subagent.status !== "ready" || !subagent.writable || subagentDraft === void 0 || busy === "subagent" || !subagentOn || (subagentDraft.provider ?? "").trim() === "" || (subagentDraft.model ?? "").trim() === "";
1137
1440
  const capabilityDisabled = (route, snapshot, next) => snapshot.status !== "ready" || !snapshot.writable || next === void 0 || busy === route || (next.provider ?? "").trim() === "" || (next.model ?? "").trim() === "";
1138
1441
  const autoReviewDisabled = autoReview.status !== "ready" || !autoReview.writable || autoReviewDraft === void 0 || busy === "autoReview" || !autoReviewOn || (autoReviewDraft.provider ?? "").trim() === "" || (autoReviewDraft.model ?? "").trim() === "";
1442
+ const compactionDisabled = compaction.status !== "ready" || !compaction.writable || compactionDraft === void 0 || busy === "compaction" || !compactionOn || (compactionDraft.provider ?? "").trim() === "" || (compactionDraft.model ?? "").trim() === "";
1443
+ const configuredGroupCount = Object.keys(providerGroups).length;
1444
+ const groupControlsDisabled = busy === "providerGroups" || switchSettings.status !== "ready" || !switchSettings.writable;
1445
+ const groupProviderChoices = deriveRouteChoices(groups, groupProvider === "" ? void 0 : { provider: groupProvider }).providers.map((choice) => groupOf(providerGroups, choice.id) === void 0 ? choice : {
1446
+ ...choice,
1447
+ name: choice.name + " ✓"
1448
+ });
1449
+ const groupCompaction = groupDraft === void 0 ? void 0 : draftPair(groupDraft.compactProvider, groupDraft.compactModel);
1450
+ const groupReview = groupDraft === void 0 ? void 0 : draftPair(groupDraft.autoReviewProvider, groupDraft.autoReviewModel);
1451
+ const groupSubagent = groupDraft === void 0 ? void 0 : draftPair(groupDraft.subagentProvider, groupDraft.subagentModel);
1452
+ const groupCompactionChoices = deriveRouteChoices(groups, groupCompaction);
1453
+ const groupReviewChoices = deriveRouteChoices(groups, groupReview);
1454
+ const groupSubagentChoices = deriveRouteChoices(groups, groupSubagent);
1455
+ const groupSubMatched = matchCatalogModel(groups.find((group) => group.id === (groupDraft?.subagentProvider ?? ""))?.models ?? [], groupDraft?.subagentModel ?? "");
1456
+ const groupSubEfforts = (() => {
1457
+ const efforts = (groupSubMatched?.model.reasoning?.efforts ?? []).map((effort) => ({
1458
+ id: effort.id,
1459
+ name: effort.name
1460
+ }));
1461
+ const current = groupDraft?.subagentReasoningEffort;
1462
+ if (current !== void 0 && current !== "" && !efforts.some((option) => option.id === current)) efforts.push({
1463
+ id: current,
1464
+ name: current
1465
+ });
1466
+ return efforts;
1467
+ })();
1468
+ /** Enable a capability row by pinning it to the Main route, like the switches above seed from Main. */
1469
+ const enableGroupCapability = (kind) => {
1470
+ setGroupDraft((draft) => {
1471
+ const main = controller.draft;
1472
+ if (draft === void 0 || main === void 0) return draft;
1473
+ if (kind === "compaction") return {
1474
+ ...draft,
1475
+ compactProvider: main.provider,
1476
+ compactModel: main.model
1477
+ };
1478
+ if (kind === "autoReview") return {
1479
+ ...draft,
1480
+ autoReviewProvider: main.provider,
1481
+ autoReviewModel: main.model
1482
+ };
1483
+ return {
1484
+ ...draft,
1485
+ subagentProvider: main.provider,
1486
+ subagentModel: main.model,
1487
+ ...main.reasoningEffort === void 0 ? {} : { subagentReasoningEffort: main.reasoningEffort }
1488
+ };
1489
+ });
1490
+ };
1491
+ const disableGroupCapability = (kind) => {
1492
+ setGroupDraft((draft) => {
1493
+ if (draft === void 0) return draft;
1494
+ if (kind === "compaction") return {
1495
+ ...draft,
1496
+ compactProvider: "",
1497
+ compactModel: ""
1498
+ };
1499
+ if (kind === "autoReview") return {
1500
+ ...draft,
1501
+ autoReviewProvider: "",
1502
+ autoReviewModel: ""
1503
+ };
1504
+ return {
1505
+ ...draft,
1506
+ subagentProvider: "",
1507
+ subagentModel: "",
1508
+ subagentReasoningEffort: ""
1509
+ };
1510
+ });
1511
+ };
1512
+ const saveProviderGroupCard = () => {
1513
+ if (groupProvider === "" || groupDraft === void 0) return;
1514
+ run("providerGroups", () => props.saveProviderGroup(groupProvider, groupDraft));
1515
+ };
1516
+ const deleteProviderGroupCard = () => {
1517
+ if (groupProvider === "") return;
1518
+ run("providerGroups", () => props.saveProviderGroup(groupProvider, void 0));
1519
+ };
1139
1520
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("main", {
1140
1521
  className: ModelSwitchSettings_module_css_default.section,
1141
1522
  children: [
@@ -1153,129 +1534,66 @@ window.__ModuleLoader__.load({
1153
1534
  }),
1154
1535
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1155
1536
  className: ModelSwitchSettings_module_css_default.group,
1156
- children: [
1157
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
1158
- className: ModelSwitchSettings_module_css_default.groupLabel,
1159
- children: props.t("sendProtection")
1160
- }),
1161
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1162
- className: ModelSwitchSettings_module_css_default.rowCard,
1163
- children: [
1164
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1165
- className: ModelSwitchSettings_module_css_default.routeIcon,
1166
- "aria-hidden": "true",
1167
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
1168
- width: "18",
1169
- height: "18",
1170
- viewBox: "0 0 18 18",
1171
- fill: "none",
1172
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1173
- d: "M4 5.5h10M4 9h7M4 12.5h5",
1174
- stroke: "currentColor",
1175
- strokeLinecap: "round"
1176
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1177
- d: "M13 11.5v3.5l2-1.6",
1178
- stroke: "currentColor",
1179
- strokeLinecap: "round",
1180
- strokeLinejoin: "round"
1181
- })]
1182
- })
1183
- }),
1184
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1185
- className: ModelSwitchSettings_module_css_default.rowCopy,
1186
- children: [
1187
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1188
- className: ModelSwitchSettings_module_css_default.rowTitle,
1189
- children: props.t("compactOnSwitch")
1190
- }),
1191
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1192
- className: ModelSwitchSettings_module_css_default.hint,
1193
- children: props.t("compactOnSwitchHelp")
1194
- }),
1195
- compactError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1196
- className: cx(ModelSwitchSettings_module_css_default.hint, ModelSwitchSettings_module_css_default.message),
1197
- children: compactError
1198
- })
1199
- ]
1200
- }),
1201
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstantSwitch, {
1202
- checked: switchSettings.value?.compactOnSwitch !== false,
1203
- disabled: !switchSettings.writable || compactBusy,
1204
- label: props.t("compactOnSwitch"),
1205
- onChange: (value) => {
1206
- setCompactBusy(true);
1207
- setCompactError(void 0);
1208
- props.setCompactOnSwitch(value).then(() => {
1209
- setCompactBusy(false);
1210
- }, (error) => {
1211
- setCompactBusy(false);
1212
- setCompactError(error instanceof Error ? error.message : props.t("requestFailed"));
1213
- });
1214
- }
1537
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
1538
+ className: ModelSwitchSettings_module_css_default.groupLabel,
1539
+ children: props.t("sendProtection")
1540
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1541
+ className: ModelSwitchSettings_module_css_default.rowCard,
1542
+ children: [
1543
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1544
+ className: ModelSwitchSettings_module_css_default.routeIcon,
1545
+ "aria-hidden": "true",
1546
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
1547
+ width: "18",
1548
+ height: "18",
1549
+ viewBox: "0 0 18 18",
1550
+ fill: "none",
1551
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1552
+ d: "M4 5.5h10M4 9h7M4 12.5h5",
1553
+ stroke: "currentColor",
1554
+ strokeLinecap: "round"
1555
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1556
+ d: "M13 11.5v3.5l2-1.6",
1557
+ stroke: "currentColor",
1558
+ strokeLinecap: "round",
1559
+ strokeLinejoin: "round"
1560
+ })]
1215
1561
  })
1216
- ]
1217
- }),
1218
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1219
- className: ModelSwitchSettings_module_css_default.rowCard,
1220
- children: [
1221
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1222
- className: ModelSwitchSettings_module_css_default.routeIcon,
1223
- "aria-hidden": "true",
1224
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
1225
- width: "18",
1226
- height: "18",
1227
- viewBox: "0 0 18 18",
1228
- fill: "none",
1229
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1230
- d: "M4 5.5h10M4 9h7M4 12.5h5",
1231
- stroke: "currentColor",
1232
- strokeLinecap: "round"
1233
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1234
- d: "M13 11.5v3.5l2-1.6",
1235
- stroke: "currentColor",
1236
- strokeLinecap: "round",
1237
- strokeLinejoin: "round"
1238
- })]
1562
+ }),
1563
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1564
+ className: ModelSwitchSettings_module_css_default.rowCopy,
1565
+ children: [
1566
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1567
+ className: ModelSwitchSettings_module_css_default.rowTitle,
1568
+ children: props.t("compactOnSwitch")
1569
+ }),
1570
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1571
+ className: ModelSwitchSettings_module_css_default.hint,
1572
+ children: props.t("compactOnSwitchHelp")
1573
+ }),
1574
+ compactError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1575
+ className: cx(ModelSwitchSettings_module_css_default.hint, ModelSwitchSettings_module_css_default.message),
1576
+ children: compactError
1239
1577
  })
1240
- }),
1241
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1242
- className: ModelSwitchSettings_module_css_default.rowCopy,
1243
- children: [
1244
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1245
- className: ModelSwitchSettings_module_css_default.rowTitle,
1246
- children: props.t("planCompactThreshold")
1247
- }),
1248
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1249
- className: ModelSwitchSettings_module_css_default.hint,
1250
- children: props.t("planCompactThresholdHelp")
1251
- }),
1252
- planCompactError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1253
- className: cx(ModelSwitchSettings_module_css_default.hint, ModelSwitchSettings_module_css_default.message),
1254
- children: planCompactError
1255
- })
1256
- ]
1257
- }),
1258
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1259
- className: ModelSwitchSettings_module_css_default.input,
1260
- type: "number",
1261
- min: 0,
1262
- step: 1e3,
1263
- inputMode: "numeric",
1264
- "aria-label": props.t("planCompactThreshold"),
1265
- value: planCompactDraft,
1266
- disabled: !switchSettings.writable || planCompactBusy,
1267
- onChange: (event) => {
1268
- setPlanCompactDraft(event.target.value);
1269
- setPlanCompactError(void 0);
1270
- },
1271
- onBlur: commitPlanCompactThreshold,
1272
- onKeyDown: (event) => {
1273
- if (event.key === "Enter") commitPlanCompactThreshold();
1274
- }
1275
- })
1276
- ]
1277
- })
1278
- ]
1578
+ ]
1579
+ }),
1580
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstantSwitch, {
1581
+ checked: switchSettings.value?.compactOnSwitch !== false,
1582
+ disabled: !switchSettings.writable || compactBusy,
1583
+ label: props.t("compactOnSwitch"),
1584
+ onChange: (value) => {
1585
+ setCompactBusy(true);
1586
+ setCompactError(void 0);
1587
+ props.setCompactOnSwitch(value).then(() => {
1588
+ setCompactBusy(false);
1589
+ }, (error) => {
1590
+ setCompactBusy(false);
1591
+ setCompactError(error instanceof Error ? error.message : props.t("requestFailed"));
1592
+ });
1593
+ }
1594
+ })
1595
+ ]
1596
+ })]
1279
1597
  }),
1280
1598
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1281
1599
  className: ModelSwitchSettings_module_css_default.group,
@@ -1632,8 +1950,295 @@ window.__ModuleLoader__.load({
1632
1950
  onSave: saveAutoReview
1633
1951
  })
1634
1952
  ] })
1953
+ }),
1954
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RouteCard, {
1955
+ title: props.t("compaction"),
1956
+ summary: compactionDraft === void 0 ? props.t("loading") : compactionOn ? routeName(groups, compactionDraft) : props.t("compactionFollow"),
1957
+ hint: props.t("compactionHelp"),
1958
+ icon: "compaction",
1959
+ open: open === "compaction" && compactionOn,
1960
+ onToggle: () => {
1961
+ if (compactionOn) toggle("compaction");
1962
+ },
1963
+ expandable: compactionOn,
1964
+ trailing: compactionDraft === void 0 ? void 0 : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstantSwitch, {
1965
+ checked: compactionOn,
1966
+ disabled: busy === "compaction" || !compaction.writable,
1967
+ label: props.t("compaction"),
1968
+ onChange: persistCompactionEnabled
1969
+ }),
1970
+ children: compactionDraft === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1971
+ className: ModelSwitchSettings_module_css_default.hint,
1972
+ children: props.t("loading")
1973
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1974
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1975
+ className: ModelSwitchSettings_module_css_default.hint,
1976
+ children: props.t("compactionRouteHelp")
1977
+ }),
1978
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1979
+ className: ModelSwitchSettings_module_css_default.formGrid,
1980
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
1981
+ label: props.t("provider"),
1982
+ value: compactionDraft.provider ?? "",
1983
+ disabled: busy === "compaction" || !compaction.writable,
1984
+ choices: compactionChoices.providers,
1985
+ onChange: (provider) => {
1986
+ const first = groups.find((group) => group.id === provider)?.models[0];
1987
+ setCompactionDraft({
1988
+ provider,
1989
+ ...first === void 0 ? {} : { model: first.id }
1990
+ });
1991
+ }
1992
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
1993
+ label: props.t("model"),
1994
+ value: compactionDraft.model ?? "",
1995
+ disabled: busy === "compaction" || !compaction.writable,
1996
+ choices: compactionChoices.models,
1997
+ onChange: (model) => {
1998
+ setCompactionDraft({
1999
+ ...compactionDraft,
2000
+ model
2001
+ });
2002
+ }
2003
+ })]
2004
+ }),
2005
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Actions, {
2006
+ t: props.t,
2007
+ busy: busy === "compaction",
2008
+ disabled: compactionDisabled,
2009
+ ...message?.route === "compaction" ? { message: message.text } : {},
2010
+ onCancel: () => {
2011
+ resetCompaction();
2012
+ setMessage(void 0);
2013
+ },
2014
+ onSave: saveCompaction
2015
+ })
2016
+ ] })
1635
2017
  })
1636
2018
  ]
2019
+ }),
2020
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2021
+ className: ModelSwitchSettings_module_css_default.group,
2022
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
2023
+ className: ModelSwitchSettings_module_css_default.groupLabel,
2024
+ children: props.t("providerGroups")
2025
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(RouteCard, {
2026
+ title: props.t("providerGroupsCard"),
2027
+ summary: props.t("providerGroupsSummary"),
2028
+ hint: props.t("providerGroupsHelp"),
2029
+ icon: "providerGroups",
2030
+ open: open === "providerGroups",
2031
+ onToggle: () => {
2032
+ toggle("providerGroups");
2033
+ },
2034
+ ...configuredGroupCount > 0 ? { badge: String(configuredGroupCount) } : {},
2035
+ children: [
2036
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2037
+ className: ModelSwitchSettings_module_css_default.hint,
2038
+ children: props.t("providerGroupsBodyHelp")
2039
+ }),
2040
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2041
+ className: ModelSwitchSettings_module_css_default.formGrid,
2042
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
2043
+ label: props.t("provider"),
2044
+ value: groupProvider,
2045
+ disabled: groupControlsDisabled,
2046
+ choices: groupProviderChoices,
2047
+ onChange: setGroupProvider
2048
+ })
2049
+ }),
2050
+ groupProvider === "" || groupDraft === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2051
+ className: ModelSwitchSettings_module_css_default.hint,
2052
+ children: props.t("providerGroupPick")
2053
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2054
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2055
+ className: ModelSwitchSettings_module_css_default.rowCard,
2056
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2057
+ className: ModelSwitchSettings_module_css_default.rowCopy,
2058
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2059
+ className: ModelSwitchSettings_module_css_default.rowTitle,
2060
+ children: props.t("compaction")
2061
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2062
+ className: ModelSwitchSettings_module_css_default.hint,
2063
+ children: groupCompaction === void 0 ? props.t("compactionFollow") : routeName(groups, groupCompaction)
2064
+ })]
2065
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstantSwitch, {
2066
+ checked: groupCompaction !== void 0,
2067
+ disabled: groupControlsDisabled,
2068
+ label: props.t("compaction"),
2069
+ onChange: (value) => {
2070
+ if (value) enableGroupCapability("compaction");
2071
+ else disableGroupCapability("compaction");
2072
+ }
2073
+ })]
2074
+ }),
2075
+ groupCompaction === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2076
+ className: ModelSwitchSettings_module_css_default.formGrid,
2077
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
2078
+ label: props.t("provider"),
2079
+ value: groupCompaction.provider,
2080
+ disabled: groupControlsDisabled,
2081
+ choices: groupCompactionChoices.providers,
2082
+ onChange: (provider) => {
2083
+ const first = groups.find((item) => item.id === provider)?.models[0];
2084
+ setGroupDraft((draft) => draft === void 0 ? draft : {
2085
+ ...draft,
2086
+ compactProvider: provider,
2087
+ compactModel: first?.id ?? ""
2088
+ });
2089
+ }
2090
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
2091
+ label: props.t("model"),
2092
+ value: groupCompaction.model,
2093
+ disabled: groupControlsDisabled,
2094
+ choices: groupCompactionChoices.models,
2095
+ onChange: (model) => {
2096
+ setGroupDraft((draft) => draft === void 0 ? draft : {
2097
+ ...draft,
2098
+ compactModel: model
2099
+ });
2100
+ }
2101
+ })]
2102
+ }),
2103
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2104
+ className: ModelSwitchSettings_module_css_default.rowCard,
2105
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2106
+ className: ModelSwitchSettings_module_css_default.rowCopy,
2107
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2108
+ className: ModelSwitchSettings_module_css_default.rowTitle,
2109
+ children: props.t("autoReview")
2110
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2111
+ className: ModelSwitchSettings_module_css_default.hint,
2112
+ children: groupReview === void 0 ? props.t("autoReviewFollow") : routeName(groups, groupReview)
2113
+ })]
2114
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstantSwitch, {
2115
+ checked: groupReview !== void 0,
2116
+ disabled: groupControlsDisabled,
2117
+ label: props.t("autoReview"),
2118
+ onChange: (value) => {
2119
+ if (value) enableGroupCapability("autoReview");
2120
+ else disableGroupCapability("autoReview");
2121
+ }
2122
+ })]
2123
+ }),
2124
+ groupReview === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2125
+ className: ModelSwitchSettings_module_css_default.formGrid,
2126
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
2127
+ label: props.t("provider"),
2128
+ value: groupReview.provider,
2129
+ disabled: groupControlsDisabled,
2130
+ choices: groupReviewChoices.providers,
2131
+ onChange: (provider) => {
2132
+ const first = groups.find((item) => item.id === provider)?.models[0];
2133
+ setGroupDraft((draft) => draft === void 0 ? draft : {
2134
+ ...draft,
2135
+ autoReviewProvider: provider,
2136
+ autoReviewModel: first?.id ?? ""
2137
+ });
2138
+ }
2139
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
2140
+ label: props.t("model"),
2141
+ value: groupReview.model,
2142
+ disabled: groupControlsDisabled,
2143
+ choices: groupReviewChoices.models,
2144
+ onChange: (model) => {
2145
+ setGroupDraft((draft) => draft === void 0 ? draft : {
2146
+ ...draft,
2147
+ autoReviewModel: model
2148
+ });
2149
+ }
2150
+ })]
2151
+ }),
2152
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2153
+ className: ModelSwitchSettings_module_css_default.rowCard,
2154
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2155
+ className: ModelSwitchSettings_module_css_default.rowCopy,
2156
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2157
+ className: ModelSwitchSettings_module_css_default.rowTitle,
2158
+ children: props.t("subagent")
2159
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2160
+ className: ModelSwitchSettings_module_css_default.hint,
2161
+ children: groupSubagent === void 0 ? props.t("subagentOff") : routeName(groups, groupSubagent)
2162
+ })]
2163
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstantSwitch, {
2164
+ checked: groupSubagent !== void 0,
2165
+ disabled: groupControlsDisabled,
2166
+ label: props.t("subagent"),
2167
+ onChange: (value) => {
2168
+ if (value) enableGroupCapability("subagent");
2169
+ else disableGroupCapability("subagent");
2170
+ }
2171
+ })]
2172
+ }),
2173
+ groupSubagent === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2174
+ className: ModelSwitchSettings_module_css_default.formGrid,
2175
+ children: [
2176
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
2177
+ label: props.t("provider"),
2178
+ value: groupSubagent.provider,
2179
+ disabled: groupControlsDisabled,
2180
+ choices: groupSubagentChoices.providers,
2181
+ onChange: (provider) => {
2182
+ const first = groups.find((item) => item.id === provider)?.models[0];
2183
+ const routed = selectRouteModel(groups, provider, first?.id ?? groupSubagent.model);
2184
+ setGroupDraft((draft) => draft === void 0 ? draft : {
2185
+ ...draft,
2186
+ subagentProvider: routed.provider,
2187
+ subagentModel: routed.model,
2188
+ subagentReasoningEffort: routed.reasoningEffort ?? ""
2189
+ });
2190
+ }
2191
+ }),
2192
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
2193
+ label: props.t("model"),
2194
+ value: groupSubagent.model,
2195
+ disabled: groupControlsDisabled,
2196
+ choices: groupSubagentChoices.models,
2197
+ onChange: (model) => {
2198
+ const routed = selectRouteModel(groups, groupSubagent.provider, model);
2199
+ setGroupDraft((draft) => draft === void 0 ? draft : {
2200
+ ...draft,
2201
+ subagentProvider: routed.provider,
2202
+ subagentModel: routed.model,
2203
+ subagentReasoningEffort: routed.reasoningEffort ?? ""
2204
+ });
2205
+ }
2206
+ }),
2207
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
2208
+ label: props.t("effort"),
2209
+ value: groupDraft.subagentReasoningEffort ?? "",
2210
+ disabled: groupControlsDisabled,
2211
+ choices: groupSubEfforts,
2212
+ onChange: (effort) => {
2213
+ setGroupDraft((draft) => draft === void 0 ? draft : {
2214
+ ...draft,
2215
+ subagentReasoningEffort: effort
2216
+ });
2217
+ }
2218
+ })
2219
+ ]
2220
+ }),
2221
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Actions, {
2222
+ t: props.t,
2223
+ busy: busy === "providerGroups",
2224
+ disabled: groupControlsDisabled || groupProvider === "",
2225
+ ...message?.route === "providerGroups" ? { message: message.text } : {},
2226
+ extra: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2227
+ type: "button",
2228
+ className: cx(ModelSwitchSettings_module_css_default.button, ModelSwitchSettings_module_css_default.secondaryButton),
2229
+ disabled: groupControlsDisabled || groupOf(providerGroups, groupProvider) === void 0,
2230
+ onClick: deleteProviderGroupCard,
2231
+ children: props.t("providerGroupDelete")
2232
+ }),
2233
+ onCancel: () => {
2234
+ resyncGroupDraft();
2235
+ setMessage(void 0);
2236
+ },
2237
+ onSave: saveProviderGroupCard
2238
+ })
2239
+ ] })
2240
+ ]
2241
+ })]
1637
2242
  })
1638
2243
  ]
1639
2244
  });
@@ -1680,9 +2285,18 @@ window.__ModuleLoader__.load({
1680
2285
  autoReviewFollow: "跟随会话模型",
1681
2286
  autoReviewHelp: "为官方 auto-review(Auto 权限预设的逐调用审查)指定审查模型;需启用官方 auto-review 能力包。关闭时审查跟随当前会话模型。",
1682
2287
  autoReviewRouteHelp: "审查请求固定使用这里的 Provider/模型,与会话模型无关;保存后对下一次审查立即生效。",
1683
- planCompactThreshold: "计划执行前压缩阈值",
1684
- planCompactThresholdHelp: "切换模型执行计划时,若上下文估算超过该 Token 数,先用切换后的模型压缩一次历史再提交执行;未切换模型不触发。0 = 关闭。压缩会产生额外模型用量,摘要可能省略细节。",
1685
- planCompactThresholdInvalid: "请输入不小于 0 的整数。",
2288
+ compaction: "压缩模型",
2289
+ compactionFollow: "跟随会话模型",
2290
+ compactionHelp: "为官方压缩(/compact 与官方自动压缩)指定摘要模型;关闭(两个字段都留空)时使用官方默认,即当前会话的目标模型。",
2291
+ compactionRouteHelp: "摘要请求固定使用这里的 Provider/模型,与会话目标模型无关;保存后对下一次压缩立即生效。保存时若恰有压缩进行中,可能被打断并在下次重试。",
2292
+ providerGroups: "供应商配置组",
2293
+ providerGroupsCard: "按供应商预设路由",
2294
+ providerGroupsSummary: "切换主模型供应商时自动应用对应组",
2295
+ providerGroupsHelp: "为压缩、子代理、审查按供应商分别保存路由。",
2296
+ providerGroupsBodyHelp: "每个供应商保存一组压缩、子代理、审查路由;主模型切换到该供应商时,这三条路由自动写入上方对应卡片。切换到未配置的供应商时现有路由保持不变;应用会覆盖三张卡片的当前值(含手动修改)。",
2297
+ providerGroupPick: "选择供应商以编辑其配置组。",
2298
+ providerGroupDelete: "删除该组",
2299
+ providerGroupApplyFailed: "主模型已保存,但供应商组应用失败:",
1686
2300
  "reason.central-subagent-routing": "Alpha.4 没有全局子代理启动路由接口。",
1687
2301
  "reason.packaged-preset-roots": "Alpha.4 不支持插件提供额外 preset root。",
1688
2302
  "reason.tool-owner-suppression": "Alpha.4 没有工具所有者或来源抑制接口。",
@@ -1730,9 +2344,18 @@ window.__ModuleLoader__.load({
1730
2344
  autoReviewFollow: "Follows the session model",
1731
2345
  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.",
1732
2346
  autoReviewRouteHelp: "Review requests use exactly this provider/model regardless of the session model; saved changes apply to the very next review.",
1733
- planCompactThreshold: "Plan-exit compaction threshold",
1734
- planCompactThresholdHelp: "When a Plan is approved with a switched model and the estimated context exceeds this many tokens, compact history once with the switched-to model before committing. Approvals without a model switch never trigger it. 0 disables. Compaction uses extra model quota and may omit details.",
1735
- planCompactThresholdInvalid: "Enter an integer of 0 or more.",
2347
+ compaction: "Compaction model",
2348
+ compactionFollow: "Follows the conversation model",
2349
+ 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.",
2350
+ 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.",
2351
+ providerGroups: "Provider groups",
2352
+ providerGroupsCard: "Per-provider route presets",
2353
+ providerGroupsSummary: "Applied automatically when the main model provider switches",
2354
+ providerGroupsHelp: "Save compaction, subagent, and review routes per provider.",
2355
+ providerGroupsBodyHelp: "Each provider stores one compaction, subagent, and review route group. Switching the main model to that provider writes these three routes 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.",
2356
+ providerGroupPick: "Pick a provider to edit its group.",
2357
+ providerGroupDelete: "Delete group",
2358
+ providerGroupApplyFailed: "Main model saved, but applying the provider group failed: ",
1736
2359
  "reason.central-subagent-routing": "Alpha.4 exposes no global Subagent start-routing seam.",
1737
2360
  "reason.packaged-preset-roots": "Alpha.4 exposes no plugin-owned preset root.",
1738
2361
  "reason.tool-owner-suppression": "Alpha.4 exposes no tool owner or provenance suppression seam.",
@@ -1826,14 +2449,6 @@ window.__ModuleLoader__.load({
1826
2449
  return false;
1827
2450
  }
1828
2451
  }
1829
- //#endregion
1830
- //#region src/picker/plan-compact.ts
1831
- /** Runtime guard for the wire value returned by the Host RPC. */
1832
- function isPlanCompactOutcome(value) {
1833
- if (value === null || typeof value !== "object") return false;
1834
- const status = value.status;
1835
- return status === "disabled" || status === "skipped" || status === "compacted" || status === "empty" || status === "error";
1836
- }
1837
2452
  const CONTEXT_SUFFIX = /-(\d+)(k|m)$/iu;
1838
2453
  /** Peel Fast and `-<n>k` / `-<n>m` in either order. Product names like `-max` stay. */
1839
2454
  function parsePickerId(id) {
@@ -3119,7 +3734,7 @@ window.__ModuleLoader__.load({
3119
3734
  };
3120
3735
  }
3121
3736
  //#endregion
3122
- //#region node_modules/.pnpm/dsh-llm-providers-ui@https+_f1dadb1b56406564fc1bb8f2d953b82d/node_modules/dsh-llm-providers-ui/lib/order.js
3737
+ //#region node_modules/.pnpm/dsh-llm-providers-ui@https+++github.com+NOirBRight+dsh-llm-providers-ui+releases+downlo_f1dadb1b56406564fc1bb8f2d953b82d/node_modules/dsh-llm-providers-ui/lib/order.js
3123
3738
  /** Display order for installed provider cards when the user has not saved one. */
3124
3739
  const PROVIDER_ITEM_ORDER = [
3125
3740
  "llm-cursor",
@@ -3407,7 +4022,28 @@ window.__ModuleLoader__.load({
3407
4022
  };
3408
4023
  }
3409
4024
  /**
3410
- * Make the plan text this session's armed goal, replacing any unfinished goal.
4025
+ * Address the plan document exactly as the official opener does.
4026
+ *
4027
+ * A logged plan (`callId` present) resolves from `dsh-resource://plan/...`
4028
+ * for as long as the session log holds the call; before that the pending
4029
+ * review's own `dsh-resource://plan-review/...` address is the one that
4030
+ * opens its preview.
4031
+ *
4032
+ * @param sessionId - the session that owns the review.
4033
+ * @param ref - logged call identity plus the pending request key.
4034
+ * @returns The plan resource address.
4035
+ */
4036
+ function planDocumentAddress(sessionId, ref) {
4037
+ const encode = encodeURIComponent;
4038
+ return ref.callId === void 0 ? `dsh-resource://plan-review/${encode(sessionId)}/${encode(ref.requestKey)}` : `dsh-resource://plan/${encode(sessionId)}/${encode(ref.callId)}`;
4039
+ }
4040
+ /**
4041
+ * Make this plan the session's armed goal, replacing any unfinished goal.
4042
+ *
4043
+ * The objective is a short instruction carrying the plan document's address,
4044
+ * not the plan's raw markdown: goal objectives are injected into every round,
4045
+ * and a full plan would be truncated by length limits mid-sentence. The round
4046
+ * driver reads the plan from the address instead.
3411
4047
  *
3412
4048
  * Ordering mirrors the card's own commit-then-answer rule: a refusal here keeps
3413
4049
  * the Plan pending instead of approving work that no goal tracks. The replace
@@ -3416,17 +4052,18 @@ window.__ModuleLoader__.load({
3416
4052
  *
3417
4053
  * @param remote - the released goal remote, absent when no goal package is mounted.
3418
4054
  * @param sessionId - the session that owns both the review and the goal.
3419
- * @param plan - the plan markdown under review.
4055
+ * @param plan - the plan markdown under review (validated non-empty).
4056
+ * @param ref - the plan document identity used for its address.
3420
4057
  * @returns Whether the goal now tracks this plan, or why it does not.
3421
4058
  */
3422
- async function setPlanAsGoal(remote, sessionId, plan) {
4059
+ async function setPlanAsGoal(remote, sessionId, plan, ref) {
3423
4060
  if (remote === void 0) return { kind: "unsupported" };
3424
- const objective = plan.trim();
3425
- if (objective === "") return {
4061
+ if (plan.trim() === "") return {
3426
4062
  kind: "failed",
3427
4063
  code: "GOAL_INVALID_OBJECTIVE",
3428
4064
  message: "goal objective must be a non-empty string"
3429
4065
  };
4066
+ const objective = `持续推进当前plan直到完成(plan 文档:${planDocumentAddress(sessionId, ref)})`;
3430
4067
  const created = await remote.create(sessionId, { objective });
3431
4068
  if (created.ok) return { kind: "created" };
3432
4069
  if (created.error.code !== "GOAL_ALREADY_EXISTS") return {
@@ -3624,12 +4261,11 @@ window.__ModuleLoader__.load({
3624
4261
  t: props.t,
3625
4262
  goalRemote: props.goalRemote,
3626
4263
  planPreview: props.planPreview,
3627
- ...props.planCompact === void 0 ? {} : { planCompact: props.planCompact },
3628
4264
  ...props.resolveInteractionOperations === void 0 ? {} : { resolveInteractionOperations: props.resolveInteractionOperations },
3629
4265
  ...props.roleOf === void 0 ? {} : { roleOf: props.roleOf }
3630
4266
  }, props.matched.key);
3631
4267
  }
3632
- function PlanReviewState({ matched, review, available, providerLock, agentLocked, lockFailed, settingsUnavailableReason, directory, t, goalRemote, planPreview, planCompact, resolveInteractionOperations, roleOf }) {
4268
+ function PlanReviewState({ matched, review, available, providerLock, agentLocked, lockFailed, settingsUnavailableReason, directory, t, goalRemote, planPreview, resolveInteractionOperations, roleOf }) {
3633
4269
  const { snapshot, getDirectorySnapshot, load, select } = directory;
3634
4270
  const [execution, setExecution] = (0, react.useState)(snapshot.current ?? void 0);
3635
4271
  const [feedback, setFeedback] = (0, react.useState)("");
@@ -3715,36 +4351,9 @@ window.__ModuleLoader__.load({
3715
4351
  throw new Error(message === null ? t("plan.modelFailed") : t("error.action", { message }));
3716
4352
  }
3717
4353
  };
3718
- /**
3719
- * Compact the history before a switched approval commits anything.
3720
- *
3721
- * Only an approval that changes provider/model triggers the Host check —
3722
- * effort-only changes and same-model approvals execute untouched. A failed
3723
- * compaction keeps the Plan pending and retryable; `empty` announces that
3724
- * nothing safe could be reduced and execution proceeds.
3725
- */
3726
- const compactBeforeRun = async () => {
3727
- if (execution === void 0) return;
3728
- const current = snapshot.current;
3729
- if (!(current === null || current === void 0 || current.provider !== execution.provider || current.model !== execution.model)) return;
3730
- if (planCompact === void 0) {
3731
- console.warn("dsh-model-switch: plan compaction seam absent; executing the switched Plan without it");
3732
- return;
3733
- }
3734
- setNotice(t("plan.compacting", { model: execution.model }));
3735
- let outcome;
3736
- try {
3737
- outcome = await planCompact(matched.sessionId, execution.provider, execution.model);
3738
- } catch (cause) {
3739
- throw new Error(t("plan.compactFailed", { message: cause instanceof Error ? cause.message : String(cause) }));
3740
- }
3741
- if (outcome.status === "error") throw new Error(t("plan.compactFailed", { message: outcome.message ?? "unknown failure" }));
3742
- setNotice(outcome.status === "empty" ? t("plan.compactEmpty") : outcome.status === "compacted" ? t("plan.compacted") : null);
3743
- };
3744
4354
  const onApprove = () => {
3745
4355
  if (execution === void 0 || !executionAllowed || !available || busy || blocked) return;
3746
4356
  settle(async () => {
3747
- await compactBeforeRun();
3748
4357
  await approveWith(execution);
3749
4358
  });
3750
4359
  };
@@ -3759,8 +4368,10 @@ window.__ModuleLoader__.load({
3759
4368
  const onRunAsGoal = () => {
3760
4369
  if (execution === void 0 || !executionAllowed || !available || busy || blocked || goal === void 0) return;
3761
4370
  settle(async () => {
3762
- await compactBeforeRun();
3763
- const handoff = await setPlanAsGoal(goal, matched.sessionId, review.plan);
4371
+ const handoff = await setPlanAsGoal(goal, matched.sessionId, review.plan, {
4372
+ ...review.callId === void 0 ? {} : { callId: review.callId },
4373
+ requestKey: matched.key
4374
+ });
3764
4375
  if (handoff.kind === "unsupported") throw new Error(t("plan.goalUnsupported"));
3765
4376
  if (handoff.kind === "failed") {
3766
4377
  const message = handoff.code === void 0 ? handoff.message : `${handoff.code}: ${handoff.message}`;
@@ -3998,10 +4609,6 @@ window.__ModuleLoader__.load({
3998
4609
  "plan.goalUnsupported": "当前部署未挂载 goal 服务,无法把计划设为目标",
3999
4610
  "plan.responseRejected": "计划答复已被另一客户端处理;已提交的模型切换无法由插件回滚。",
4000
4611
  "plan.cancelRejected": "计划审查已被另一客户端处理,无法返回讨论。",
4001
- "plan.compacting": "正在压缩上下文,将使用 {model}…",
4002
- "plan.compacted": "上下文已压缩,开始执行。",
4003
- "plan.compactEmpty": "没有可压缩的历史,继续执行。",
4004
- "plan.compactFailed": "压缩上下文失败:{message}(计划仍待批准,可重试)",
4005
4612
  "markdown.copy": "复制代码",
4006
4613
  "markdown.copied": "已复制",
4007
4614
  "markdown.footnotes": "脚注"
@@ -4058,10 +4665,6 @@ window.__ModuleLoader__.load({
4058
4665
  "plan.goalUnsupported": "This deployment mounts no goal service, so the Plan cannot be set as a goal",
4059
4666
  "plan.responseRejected": "Another client already handled this Plan response. A committed model change cannot be rolled back by this plugin.",
4060
4667
  "plan.cancelRejected": "Another client already handled this Plan review, so it cannot return to discussion.",
4061
- "plan.compacting": "Compacting context with {model}…",
4062
- "plan.compacted": "Context compacted; starting execution.",
4063
- "plan.compactEmpty": "No compactable history; continuing.",
4064
- "plan.compactFailed": "Context compaction failed: {message} (the Plan stays pending and can be retried)",
4065
4668
  "markdown.copy": "Copy code",
4066
4669
  "markdown.copied": "Copied",
4067
4670
  "markdown.footnotes": "Footnotes"
@@ -4084,45 +4687,6 @@ window.__ModuleLoader__.load({
4084
4687
  }
4085
4688
  const EMPTY_ORDER = [];
4086
4689
  /**
4087
- * Ask the Host to compact this session before a switched approval executes.
4088
- *
4089
- * An absent connection degrades to `skipped` (with a console note): the
4090
- * mechanism protects against long-context timeouts, it must not brick plan
4091
- * approvals in environments that expose no Host RPC at all.
4092
- * @param rpcOf - resolves the live connection RPC.
4093
- * @returns The card-facing runner bound to this seat's connection.
4094
- */
4095
- function planCompactFrom(rpcOf) {
4096
- return async (sessionId, provider, model) => {
4097
- const rpc = rpcOf();
4098
- if (rpc === void 0) {
4099
- console.warn("dsh-model-switch: Host RPC unavailable; executing the Plan without a compaction check");
4100
- return { status: "skipped" };
4101
- }
4102
- try {
4103
- const response = await callPluginRpc(rpc, "plugin-rpc/model-switch", "plan-compact", {
4104
- sessionId,
4105
- provider,
4106
- model
4107
- });
4108
- if (!response.ok) return {
4109
- status: "error",
4110
- message: "the Host rejected the plan compaction request"
4111
- };
4112
- if (!isPlanCompactOutcome(response.value)) return {
4113
- status: "error",
4114
- message: "the Host returned an unreadable plan compaction verdict"
4115
- };
4116
- return response.value;
4117
- } catch (cause) {
4118
- return {
4119
- status: "error",
4120
- message: cause instanceof Error ? cause.message : String(cause)
4121
- };
4122
- }
4123
- };
4124
- }
4125
- /**
4126
4690
  * Resolve the released goal remote without hard-depending on it.
4127
4691
  *
4128
4692
  * `remote.goals` exists only when the deployment mounts `@deepseek-ai/dsh-goal`,
@@ -4240,6 +4804,7 @@ window.__ModuleLoader__.load({
4240
4804
  const models = scope.modelDirectories;
4241
4805
  const sessions = scope.sessions;
4242
4806
  const mainDefaults = scope.configForms.get(MAIN_DEFAULT_CONFIG_ID);
4807
+ const ownedForm = scope.configForms.get(MODEL_SWITCH_CONFIG_ID);
4243
4808
  let directoryService;
4244
4809
  const orderStore = providerOrderStore(scope.configForms.get(PROVIDERS_CONFIG_ID));
4245
4810
  scope.inject(["providerDirectory"], (directoryScope) => {
@@ -4282,7 +4847,6 @@ window.__ModuleLoader__.load({
4282
4847
  getMainDefaultsSnapshot: () => mainDefaults.getSnapshot(),
4283
4848
  goalRemote: () => goalRemoteFrom(scope),
4284
4849
  planPreview,
4285
- planCompact: planCompactFrom(rpcOf),
4286
4850
  hooks: {
4287
4851
  directory: directory.store,
4288
4852
  providerOrder: orderStore
@@ -4314,6 +4878,11 @@ window.__ModuleLoader__.load({
4314
4878
  return uncommitted;
4315
4879
  }
4316
4880
  if (!result.ok) return uncommitted;
4881
+ if (selection.provider !== currentProvider) try {
4882
+ await applyProviderGroupTo(ownedForm, normalizeProviderGroups(ownedForm.getSnapshot().value?.providerGroups), selection.provider);
4883
+ } catch (error) {
4884
+ console.warn("dsh-model-switch: provider group apply failed", error);
4885
+ }
4317
4886
  return {
4318
4887
  committed: true,
4319
4888
  mainDefaultRestored: (await restoreMainDefault(mainDefaults, defaultBeforeSwitch)).restored
@@ -4425,7 +4994,13 @@ window.__ModuleLoader__.load({
4425
4994
  const search = deriveConfigForm(owned, deriveSearchSettings, SEARCH_SETTINGS_FIELDS);
4426
4995
  const image = deriveConfigForm(owned, deriveImageSettings, IMAGE_SETTINGS_FIELDS);
4427
4996
  const autoReview = deriveConfigForm(owned, deriveAutoReviewSettings, AUTO_REVIEW_SETTINGS_FIELDS);
4997
+ const compaction = deriveConfigForm(owned, deriveCompactionSettings, COMPACTION_SETTINGS_FIELDS);
4998
+ /** Adopt the target provider's saved group; a provider without a group leaves the routes untouched. */
4999
+ const applyProviderGroupFor = async (provider) => {
5000
+ await applyProviderGroupTo(owned, normalizeProviderGroups(owned.getSnapshot().value?.providerGroups), provider);
5001
+ };
4428
5002
  const saveMain = async (next, expectedRevision) => {
5003
+ const previousProvider = mainForm.getSnapshot().value?.provider;
4429
5004
  if (!await mainForm.mutate([
4430
5005
  {
4431
5006
  op: "set",
@@ -4451,6 +5026,11 @@ window.__ModuleLoader__.load({
4451
5026
  }
4452
5027
  const revision = mainForm.getSnapshot().revision;
4453
5028
  if (revision === void 0) throw new Error("settings unavailable after save");
5029
+ if (next.provider.trim() !== "" && next.provider !== previousProvider) try {
5030
+ await applyProviderGroupFor(next.provider);
5031
+ } catch (error) {
5032
+ throw new Error(t("providerGroupApplyFailed") + (error instanceof Error ? error.message : t("requestFailed")));
5033
+ }
4454
5034
  return revision;
4455
5035
  };
4456
5036
  const connectionRpc = (() => {
@@ -4490,21 +5070,28 @@ window.__ModuleLoader__.load({
4490
5070
  searchSettings: search,
4491
5071
  imageSettings: image,
4492
5072
  autoReviewSettings: autoReview,
5073
+ compactionSettings: compaction,
4493
5074
  switchSettings: owned
4494
5075
  },
4495
5076
  capabilities: RUNTIME_CAPABILITIES,
4496
5077
  saveMain,
4497
5078
  setSubagent: (field, value) => value === void 0 ? subagent.unset(field) : subagent.set(field, value),
4498
5079
  setCapability: (route, field, value) => {
4499
- const scope = route === "search" ? search : route === "image" ? image : autoReview;
5080
+ const scope = route === "search" ? search : route === "image" ? image : route === "compaction" ? compaction : autoReview;
4500
5081
  return value === void 0 ? scope.unset(field) : scope.set(field, value);
4501
5082
  },
4502
5083
  setCompactOnSwitch: async (value) => {
4503
5084
  if (!await owned.set("compactOnSwitch", value)) throw new Error("settings-rejected");
4504
5085
  },
4505
- setPlanCompactThreshold: async (value) => {
4506
- if (!Number.isInteger(value) || value < 0) throw new Error("settings-rejected");
4507
- if (!await owned.set("planCompactThreshold", value)) throw new Error("settings-rejected");
5086
+ saveProviderGroup: async (provider, group) => {
5087
+ const current = owned.getSnapshot().value?.providerGroups;
5088
+ const next = withProviderGroup(current, provider, group);
5089
+ if (next.length === 0) {
5090
+ if (current === void 0) return;
5091
+ if (!await owned.unset("providerGroups")) throw new Error("settings-rejected");
5092
+ return;
5093
+ }
5094
+ if (!await owned.set("providerGroups", next)) throw new Error("settings-rejected");
4508
5095
  },
4509
5096
  providerRoleOf,
4510
5097
  loadCatalog: async () => {