dsh-model-switch 0.4.22 → 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",
@@ -604,65 +600,20 @@ window.__ModuleLoader__.load({
604
600
  models
605
601
  };
606
602
  }
607
- function expectedMainRevision(mirror, accepted) {
608
- return Math.max(mirror, accepted ?? mirror);
609
- }
610
- function acceptedRevisionAfterFailure(accepted, error) {
611
- return error instanceof MainSettingsConflictError ? void 0 : accepted;
612
- }
613
- function deriveMainChoices(groups, draft) {
614
- const providers = groups.map((group) => ({
615
- id: group.id,
616
- name: group.name
617
- }));
618
- if (draft !== void 0 && !providers.some((option) => option.id === draft.provider)) providers.push({
619
- id: draft.provider,
620
- name: draft.provider
621
- });
622
- const group = groups.find((item) => item.id === draft?.provider);
623
- const models = (group?.models ?? []).map((model) => ({
624
- id: model.id,
625
- name: model.name
626
- }));
627
- if (draft !== void 0 && !models.some((option) => option.id === draft.model)) models.push({
628
- id: draft.model,
629
- name: draft.model
630
- });
631
- const efforts = ((group?.models.find((item) => item.id === draft?.model))?.reasoning?.efforts ?? []).map((effort) => ({
632
- id: effort.id,
633
- name: effort.name
634
- }));
635
- if (draft?.reasoningEffort !== void 0 && !efforts.some((option) => option.id === draft.reasoningEffort)) efforts.push({
636
- id: draft.reasoningEffort,
637
- name: draft.reasoningEffort
638
- });
639
- return {
640
- providers,
641
- models,
642
- efforts
643
- };
644
- }
645
- function useModelSwitchSettingsController(input) {
646
- const main = input.useMainSettings((value) => value);
647
- const subagent = input.useSubagentSettings((value) => value);
648
- 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) {
649
605
  const [groups, setGroups] = (0, react.useState)([]);
650
- const [busy, setBusy] = (0, react.useState)(false);
651
- const [message, setMessage] = (0, react.useState)();
652
- const locked = (0, react.useRef)(false);
653
- const acceptedRevision = (0, react.useRef)();
654
- (0, react.useEffect)(() => {
655
- setDraft(main.value);
656
- setMessage(void 0);
657
- if (main.revision !== void 0 && (acceptedRevision.current === void 0 || main.revision > acceptedRevision.current)) acceptedRevision.current = main.revision;
658
- }, [main.revision, main.value]);
606
+ const [catalogError, setCatalogError] = (0, react.useState)();
659
607
  (0, react.useEffect)(() => {
660
608
  let live = true;
661
609
  const load = () => {
662
610
  input.loadCatalog().then((value) => {
663
- if (live) setGroups(value);
611
+ if (live) {
612
+ setGroups(value);
613
+ setCatalogError(void 0);
614
+ }
664
615
  }).catch(() => {
665
- if (live) setMessage(input.t("catalogFailed"));
616
+ if (live) setCatalogError(input.t("catalogFailed"));
666
617
  });
667
618
  };
668
619
  load();
@@ -678,63 +629,9 @@ window.__ModuleLoader__.load({
678
629
  input.subscribeProviderOrder,
679
630
  input.t
680
631
  ]);
681
- const { providers, models, efforts } = deriveMainChoices(groups, draft);
682
- const disabled = main.status !== "ready" || !main.writable || draft === void 0 || busy || draft.provider.trim() === "" || draft.model.trim() === "";
683
- const setProvider = (provider) => setDraft((current) => {
684
- if (current === void 0) return current;
685
- const first = groups.find((item) => item.id === provider)?.models[0];
686
- return selectRouteModel(groups, provider, first?.id ?? current.model);
687
- });
688
- const setModel = (id) => setDraft((current) => current === void 0 ? current : selectRouteModel(groups, current.provider, id));
689
- const setReasoningEffort = (value) => setDraft((current) => {
690
- if (current === void 0) return current;
691
- const next = { ...current };
692
- if (value === "") delete next.reasoningEffort;
693
- else next.reasoningEffort = value;
694
- return next;
695
- });
696
- const reset = () => {
697
- setDraft(main.value);
698
- setMessage(void 0);
699
- };
700
- const save = async () => {
701
- if (disabled || locked.current || draft === void 0) return;
702
- const mirrorRevision = main.revision;
703
- if (mirrorRevision === void 0) {
704
- setMessage(input.t("requestFailed"));
705
- return;
706
- }
707
- const expectedRevision = expectedMainRevision(mirrorRevision, acceptedRevision.current);
708
- locked.current = true;
709
- setBusy(true);
710
- setMessage(void 0);
711
- try {
712
- acceptedRevision.current = await input.saveMain(draft, expectedRevision);
713
- setMessage(input.t("saved"));
714
- } catch (error) {
715
- acceptedRevision.current = acceptedRevisionAfterFailure(acceptedRevision.current, error);
716
- setMessage(error instanceof Error ? error.message : input.t("requestFailed"));
717
- } finally {
718
- locked.current = false;
719
- setBusy(false);
720
- }
721
- };
722
632
  return {
723
- main,
724
- subagent,
725
- draft,
726
633
  groups,
727
- providers,
728
- models,
729
- efforts,
730
- busy,
731
- message,
732
- disabled,
733
- setProvider,
734
- setModel,
735
- setReasoningEffort,
736
- reset,
737
- save
634
+ catalogError
738
635
  };
739
636
  }
740
637
  //#endregion
@@ -843,18 +740,6 @@ window.__ModuleLoader__.load({
843
740
  strokeLinejoin: "round"
844
741
  })
845
742
  });
846
- if (kind === "main") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
847
- width: "18",
848
- height: "18",
849
- viewBox: "0 0 18 18",
850
- fill: "none",
851
- "aria-hidden": "true",
852
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
853
- d: "M3 4.5h12v8H8l-3.5 2v-2H3v-8Z",
854
- stroke: "currentColor",
855
- strokeLinejoin: "round"
856
- })
857
- });
858
743
  if (kind === "subagent") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
859
744
  width: "18",
860
745
  height: "18",
@@ -1091,6 +976,22 @@ window.__ModuleLoader__.load({
1091
976
  const matched = matchCatalogModel(groups.find((group) => group.id === route.provider)?.models ?? [], route.model);
1092
977
  return matched?.effort ?? matched?.model.reasoning?.defaultEffort;
1093
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
+ }
1094
995
  function capabilityChoices(groups, route, providers, kind) {
1095
996
  const choices = deriveRouteChoices(groups, route, providers);
1096
997
  if (kind === "image" && route?.provider === "grok") {
@@ -1111,8 +1012,8 @@ window.__ModuleLoader__.load({
1111
1012
  return choices;
1112
1013
  }
1113
1014
  function ModelSwitchSettings(props) {
1114
- const controller = useModelSwitchSettingsController(props);
1115
- const { main, subagent, draft, groups } = controller;
1015
+ const { groups, catalogError } = useSettingsCatalog(props);
1016
+ const subagent = props.useSubagentSettings((value) => value);
1116
1017
  const search = props.useSearchSettings((value) => value);
1117
1018
  const image = props.useImageSettings((value) => value);
1118
1019
  const autoReview = props.useAutoReviewSettings((value) => value);
@@ -1169,7 +1070,7 @@ window.__ModuleLoader__.load({
1169
1070
  };
1170
1071
  }, [loadSearchCapabilities]);
1171
1072
  const providerGroups = normalizeProviderGroups(switchSettings.value?.providerGroups);
1172
- /** Seed an unsaved group from the live routes, mirroring how the switches seed from Main. */
1073
+ /** Seed an unsaved group from the live routes. */
1173
1074
  const seedGroupFromActive = () => {
1174
1075
  const compact = compaction.value !== void 0 && routePairConfigured(compaction.value) ? {
1175
1076
  provider: compaction.value.provider ?? "",
@@ -1208,11 +1109,16 @@ window.__ModuleLoader__.load({
1208
1109
  }
1209
1110
  setGroupDraft(groupOf(providerGroups, groupProvider) ?? seedGroupFromActive());
1210
1111
  };
1112
+ /** Open the editor on the provider in use: the first configured group, else the catalog head. */
1211
1113
  (0, react.useEffect)(() => {
1212
1114
  if (groupProvider !== "") return;
1213
- const initial = controller.draft?.provider;
1115
+ const initial = Object.keys(providerGroups)[0] ?? groups[0]?.id;
1214
1116
  if (initial !== void 0 && initial.trim() !== "") setGroupProvider(initial);
1215
- }, [groupProvider, controller.draft]);
1117
+ }, [
1118
+ groupProvider,
1119
+ providerGroups,
1120
+ groups
1121
+ ]);
1216
1122
  (0, react.useEffect)(() => {
1217
1123
  resyncGroupDraft();
1218
1124
  }, [
@@ -1223,8 +1129,7 @@ window.__ModuleLoader__.load({
1223
1129
  compaction.value
1224
1130
  ]);
1225
1131
  if ([
1226
- controller.main,
1227
- controller.subagent,
1132
+ subagent,
1228
1133
  search,
1229
1134
  image,
1230
1135
  switchSettings
@@ -1267,8 +1172,7 @@ window.__ModuleLoader__.load({
1267
1172
  });
1268
1173
  return efforts;
1269
1174
  })();
1270
- const defaultEffort = subagentDraft?.reasoningEffort ?? routeDefaultEffort(groups, subagentRoute);
1271
- const mainEffectiveEffort = draft?.reasoningEffort ?? routeDefaultEffort(groups, draft);
1175
+ const defaultEffort = routeDefaultEffort(groups, subagentRoute);
1272
1176
  const searchLive = searchSnapshot?.capabilities;
1273
1177
  const searchGroups = searchGroupsFromCapabilities(searchLive);
1274
1178
  const searchChoices = capabilityChoices(searchGroups, searchDraft, void 0, "search");
@@ -1279,7 +1183,6 @@ window.__ModuleLoader__.load({
1279
1183
  const compactionChoices = deriveRouteChoices(groups, compactionDraft);
1280
1184
  const compactionOn = routePairConfigured(compaction.value);
1281
1185
  const synced = [
1282
- main,
1283
1186
  subagent,
1284
1187
  search,
1285
1188
  image,
@@ -1359,8 +1262,9 @@ window.__ModuleLoader__.load({
1359
1262
  };
1360
1263
  /**
1361
1264
  * The reviewer switch: Off unsets both stored fields (follow the session
1362
- * model); On seeds the route from Main so the pin takes effect immediately
1363
- * 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.
1364
1268
  */
1365
1269
  const persistAutoReviewEnabled = (enabled) => {
1366
1270
  if (busy === "autoReview" || !autoReview.writable || autoReview.value === void 0) return;
@@ -1372,8 +1276,8 @@ window.__ModuleLoader__.load({
1372
1276
  });
1373
1277
  return;
1374
1278
  }
1375
- const seed = draft;
1376
- if (seed === void 0 || seed.provider.trim() === "" || seed.model.trim() === "") return;
1279
+ const seed = firstCatalogRoute(groups);
1280
+ if (seed === void 0) return;
1377
1281
  setBusy("autoReview");
1378
1282
  setMessage(void 0);
1379
1283
  (async () => {
@@ -1396,9 +1300,9 @@ window.__ModuleLoader__.load({
1396
1300
  };
1397
1301
  /**
1398
1302
  * 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.
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.
1402
1306
  */
1403
1307
  const persistCompactionEnabled = (enabled) => {
1404
1308
  if (busy === "compaction" || !compaction.writable || compaction.value === void 0) return;
@@ -1410,8 +1314,8 @@ window.__ModuleLoader__.load({
1410
1314
  });
1411
1315
  return;
1412
1316
  }
1413
- const seed = draft;
1414
- if (seed === void 0 || seed.provider.trim() === "" || seed.model.trim() === "") return;
1317
+ const seed = firstCatalogRoute(groups);
1318
+ if (seed === void 0) return;
1415
1319
  setBusy("compaction");
1416
1320
  setMessage(void 0);
1417
1321
  (async () => {
@@ -1432,10 +1336,9 @@ window.__ModuleLoader__.load({
1432
1336
  });
1433
1337
  });
1434
1338
  };
1435
- const mainSummary = draft === void 0 ? props.t("loading") : compact(routeName(groups, draft), mainEffectiveEffort);
1436
1339
  const subagentRole = subagentDraft?.mode === "fixed" && subagentDraft.provider !== void 0 && subagentDraft.provider !== "" ? props.providerRoleOf?.(subagentDraft.provider) : void 0;
1437
1340
  const subagentOn = subagentDraft?.mode === "fixed";
1438
- 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");
1439
1342
  const subagentDisabled = subagent.status !== "ready" || !subagent.writable || subagentDraft === void 0 || busy === "subagent" || !subagentOn || (subagentDraft.provider ?? "").trim() === "" || (subagentDraft.model ?? "").trim() === "";
1440
1343
  const capabilityDisabled = (route, snapshot, next) => snapshot.status !== "ready" || !snapshot.writable || next === void 0 || busy === route || (next.provider ?? "").trim() === "" || (next.model ?? "").trim() === "";
1441
1344
  const autoReviewDisabled = autoReview.status !== "ready" || !autoReview.writable || autoReviewDraft === void 0 || busy === "autoReview" || !autoReviewOn || (autoReviewDraft.provider ?? "").trim() === "" || (autoReviewDraft.model ?? "").trim() === "";
@@ -1465,26 +1368,45 @@ window.__ModuleLoader__.load({
1465
1368
  });
1466
1369
  return efforts;
1467
1370
  })();
1468
- /** Enable a capability row by pinning it to the Main route, like the switches above seed from Main. */
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
+ */
1469
1376
  const enableGroupCapability = (kind) => {
1377
+ const active = seedGroupFromActive();
1378
+ const seed = firstCatalogRoute(groups);
1470
1379
  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
- };
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;
1483
1405
  return {
1484
1406
  ...draft,
1485
- subagentProvider: main.provider,
1486
- subagentModel: main.model,
1487
- ...main.reasoningEffort === void 0 ? {} : { subagentReasoningEffort: main.reasoningEffort }
1407
+ subagentProvider: provider,
1408
+ subagentModel: model,
1409
+ ...effort === void 0 || effort === "" ? {} : { subagentReasoningEffort: effort }
1488
1410
  };
1489
1411
  });
1490
1412
  };
@@ -1532,6 +1454,10 @@ window.__ModuleLoader__.load({
1532
1454
  className: ModelSwitchSettings_module_css_default.saved,
1533
1455
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", { className: ModelSwitchSettings_module_css_default.savedDot }), synced ? props.t("settingsSynced") : props.t("loading")]
1534
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
+ }),
1535
1461
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1536
1462
  className: ModelSwitchSettings_module_css_default.group,
1537
1463
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
@@ -1597,150 +1523,92 @@ window.__ModuleLoader__.load({
1597
1523
  }),
1598
1524
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1599
1525
  className: ModelSwitchSettings_module_css_default.group,
1600
- children: [
1601
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
1602
- className: ModelSwitchSettings_module_css_default.groupLabel,
1603
- 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
1604
1544
  }),
1605
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RouteCard, {
1606
- title: props.t("main"),
1607
- summary: mainSummary,
1608
- icon: "main",
1609
- open: open === "main",
1610
- onToggle: () => {
1611
- toggle("main");
1612
- },
1613
- badge: props.t("defaultBadge"),
1614
- children: draft === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1615
- className: ModelSwitchSettings_module_css_default.hint,
1616
- children: props.t("loading")
1617
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1618
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1619
- className: ModelSwitchSettings_module_css_default.formGrid,
1620
- children: [
1621
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
1622
- label: props.t("provider"),
1623
- value: draft.provider,
1624
- disabled: controller.busy || !main.writable,
1625
- choices: controller.providers,
1626
- onChange: controller.setProvider
1627
- }),
1628
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
1629
- label: props.t("model"),
1630
- value: draft.model,
1631
- disabled: controller.busy || !main.writable,
1632
- choices: controller.models,
1633
- onChange: controller.setModel
1634
- }),
1635
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
1636
- label: props.t("effort"),
1637
- value: draft.reasoningEffort ?? "",
1638
- disabled: controller.busy || !main.writable,
1639
- choices: controller.efforts,
1640
- onChange: controller.setReasoningEffort
1641
- })
1642
- ]
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
+ }
1643
1563
  }),
1644
- !main.writable && main.status === "ready" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1645
- className: ModelSwitchSettings_module_css_default.hint,
1646
- children: props.t("readonly")
1647
- }) : null,
1648
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Actions, {
1649
- t: props.t,
1650
- busy: controller.busy,
1651
- disabled: controller.disabled,
1652
- ...controller.message === void 0 ? {} : { message: controller.message },
1653
- onCancel: controller.reset,
1654
- onSave: () => {
1655
- 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
+ });
1656
1588
  }
1657
1589
  })
1658
- ] })
1659
- }),
1660
- props.capabilities.centralSubagentRouting.available ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RouteCard, {
1661
- title: props.t("subagent"),
1662
- summary: subagentSummary,
1663
- hint: props.t("subagentHelp"),
1664
- icon: "subagent",
1665
- open: open === "subagent" && subagentOn,
1666
- onToggle: () => {
1667
- 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);
1668
1599
  },
1669
- expandable: subagentOn,
1670
- trailing: subagentDraft === void 0 ? void 0 : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstantSwitch, {
1671
- checked: subagentOn,
1672
- disabled: busy === "subagent" || !subagent.writable,
1673
- label: props.t("subagent"),
1674
- onChange: persistSubagentEnabled
1675
- }),
1676
- children: subagentDraft === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1677
- className: ModelSwitchSettings_module_css_default.hint,
1678
- children: props.t("loading")
1679
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1680
- className: ModelSwitchSettings_module_css_default.formGrid,
1681
- children: [
1682
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
1683
- label: props.t("provider"),
1684
- value: subagentDraft.provider ?? "",
1685
- disabled: busy === "subagent" || !subagent.writable,
1686
- choices: subagentChoices.providers,
1687
- onChange: (provider) => {
1688
- const first = groups.find((group) => group.id === provider)?.models[0];
1689
- setSubagentDraft({
1690
- mode: "fixed",
1691
- ...selectRouteModel(groups, provider, first?.id ?? subagentDraft.model ?? "")
1692
- });
1693
- }
1694
- }),
1695
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
1696
- label: props.t("model"),
1697
- value: subagentCatalogModel?.model.id ?? subagentDraft.model ?? "",
1698
- disabled: busy === "subagent" || !subagent.writable,
1699
- choices: subagentChoices.models,
1700
- onChange: (model) => {
1701
- setSubagentDraft({
1702
- mode: "fixed",
1703
- ...selectRouteModel(groups, subagentDraft.provider ?? "", model)
1704
- });
1705
- }
1706
- }),
1707
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
1708
- label: props.t("effort"),
1709
- value: subagentDraft.reasoningEffort || subagentCatalogModel?.effort || "",
1710
- disabled: busy === "subagent" || !subagent.writable,
1711
- choices: subagentEfforts,
1712
- onChange: (effort) => {
1713
- const matched = subagentCatalogModel;
1714
- setSubagentDraft({
1715
- ...subagentDraft,
1716
- ...matched?.model.id === void 0 ? {} : { model: matched.model.id },
1717
- reasoningEffort: effort
1718
- });
1719
- }
1720
- })
1721
- ]
1722
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Actions, {
1723
- t: props.t,
1724
- busy: busy === "subagent",
1725
- disabled: subagentDisabled,
1726
- ...message?.route === "subagent" ? { message: message.text } : {},
1727
- onCancel: () => {
1728
- resetSubagent();
1729
- setMessage(void 0);
1730
- },
1731
- onSave: saveSubagent
1732
- })] })
1733
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RouteCard, {
1734
- title: props.t("subagent"),
1735
- summary: unavailable("centralSubagentRouting"),
1736
- icon: "subagent",
1737
- open: false,
1738
- onToggle: () => {},
1739
- disabled: true,
1740
- badge: props.t("unavailable"),
1741
- badgeWarn: true
1742
- })
1743
- ]
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
+ })]
1744
1612
  }),
1745
1613
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1746
1614
  className: ModelSwitchSettings_module_css_default.group,
@@ -2248,22 +2116,21 @@ window.__ModuleLoader__.load({
2248
2116
  const zh$1 = {
2249
2117
  nav: "模型切换",
2250
2118
  title: "模型切换",
2251
- subtitle: "为主会话、子代理和能力工具设置默认模型。修改只影响新请求。",
2119
+ subtitle: "为子代理与能力路由设置固定模型;当前会话模型用输入框旁的选择器直接切换。",
2252
2120
  conversationRoutes: "对话路由",
2253
2121
  capabilityRoutes: "能力路由",
2254
2122
  sendProtection: "发送保护",
2255
2123
  settingsSynced: "设置已同步",
2256
2124
  remoteSettingsUnavailable: "官方 DSH 在非本机浏览器禁用配置读取与写入;请在 Host 的 127.0.0.1 页面打开设置。",
2257
- defaultBadge: "默认",
2258
2125
  cancel: "取消",
2259
2126
  compactOnSwitch: "切换时压缩",
2260
2127
  compactOnSwitchHelp: "发送且换了模型时,先按目标窗口检查上下文;超了就用切换前的模型压一次历史,再继续请求。只选模型、不发送,不会压缩。官方自动压缩和 /compact 不受影响。压缩会产生额外模型用量,摘要可能省略细节。",
2261
- main: "主模型",
2262
2128
  provider: "提供商",
2263
2129
  model: "模型",
2264
2130
  providerDefault: "使用提供商默认值",
2265
2131
  providerDefaultShort: "Provider 默认",
2266
2132
  effort: "推理强度",
2133
+ effortPinned: "已固定",
2267
2134
  save: "保存",
2268
2135
  saving: "保存中…",
2269
2136
  saved: "已保存",
@@ -2277,10 +2144,8 @@ window.__ModuleLoader__.load({
2277
2144
  imageHelp: "统一 generate_image 会调用所选 Codex 或 Grok Adapter;Provider 原有图片工具仍保留。",
2278
2145
  unavailable: "未接入",
2279
2146
  loading: "正在加载设置…",
2280
- readonly: "设置为只读",
2281
2147
  requestFailed: "保存失败",
2282
2148
  catalogFailed: "无法加载模型目录。",
2283
- conflict: "设置已在其他位置更改,请检查最新值后重试。",
2284
2149
  autoReview: "自动审查模型",
2285
2150
  autoReviewFollow: "跟随会话模型",
2286
2151
  autoReviewHelp: "为官方 auto-review(Auto 权限预设的逐调用审查)指定审查模型;需启用官方 auto-review 能力包。关闭时审查跟随当前会话模型。",
@@ -2291,12 +2156,11 @@ window.__ModuleLoader__.load({
2291
2156
  compactionRouteHelp: "摘要请求固定使用这里的 Provider/模型,与会话目标模型无关;保存后对下一次压缩立即生效。保存时若恰有压缩进行中,可能被打断并在下次重试。",
2292
2157
  providerGroups: "供应商配置组",
2293
2158
  providerGroupsCard: "按供应商预设路由",
2294
- providerGroupsSummary: "切换主模型供应商时自动应用对应组",
2159
+ providerGroupsSummary: "会话模型切换到该供应商时自动应用对应组",
2295
2160
  providerGroupsHelp: "为压缩、子代理、审查按供应商分别保存路由。",
2296
- providerGroupsBodyHelp: "每个供应商保存一组压缩、子代理、审查路由;主模型切换到该供应商时,这三条路由自动写入上方对应卡片。切换到未配置的供应商时现有路由保持不变;应用会覆盖三张卡片的当前值(含手动修改)。",
2161
+ providerGroupsBodyHelp: "每个供应商保存一组压缩、子代理、审查路由;本会话的模型切换到该供应商时(无论通过插件的模型选择器还是 TUI 等其他界面),这三条路由自动写入上方对应卡片。切换到未配置的供应商时现有路由保持不变;应用会覆盖三张卡片的当前值(含手动修改)。",
2297
2162
  providerGroupPick: "选择供应商以编辑其配置组。",
2298
2163
  providerGroupDelete: "删除该组",
2299
- providerGroupApplyFailed: "主模型已保存,但供应商组应用失败:",
2300
2164
  "reason.central-subagent-routing": "Alpha.4 没有全局子代理启动路由接口。",
2301
2165
  "reason.packaged-preset-roots": "Alpha.4 不支持插件提供额外 preset root。",
2302
2166
  "reason.tool-owner-suppression": "Alpha.4 没有工具所有者或来源抑制接口。",
@@ -2307,22 +2171,21 @@ window.__ModuleLoader__.load({
2307
2171
  const en$1 = {
2308
2172
  nav: "Model Switch",
2309
2173
  title: "Model Switch",
2310
- 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.",
2311
2175
  conversationRoutes: "Conversation routes",
2312
2176
  capabilityRoutes: "Capability routes",
2313
2177
  sendProtection: "Send protection",
2314
2178
  settingsSynced: "Settings synced",
2315
2179
  remoteSettingsUnavailable: "Official DSH disables settings reads and writes in non-loopback browsers. Open Settings on the Host loopback URL to edit model routes.",
2316
- defaultBadge: "Default",
2317
2180
  cancel: "Cancel",
2318
2181
  compactOnSwitch: "Compact on switch",
2319
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.",
2320
- main: "Main model",
2321
2183
  provider: "Provider",
2322
2184
  model: "Model",
2323
2185
  providerDefault: "Provider default",
2324
2186
  providerDefaultShort: "Provider default",
2325
2187
  effort: "Reasoning effort",
2188
+ effortPinned: "Pinned",
2326
2189
  save: "Save",
2327
2190
  saving: "Saving…",
2328
2191
  saved: "Saved",
@@ -2336,10 +2199,8 @@ window.__ModuleLoader__.load({
2336
2199
  imageHelp: "The stable generate_image tool calls the selected Codex or Grok Adapter; existing provider image tools remain available.",
2337
2200
  unavailable: "Unavailable",
2338
2201
  loading: "Loading settings…",
2339
- readonly: "Settings are read-only",
2340
2202
  requestFailed: "Save failed",
2341
2203
  catalogFailed: "Could not load the model catalog.",
2342
- conflict: "Settings changed elsewhere. Review the latest values and retry.",
2343
2204
  autoReview: "Auto review model",
2344
2205
  autoReviewFollow: "Follows the session model",
2345
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.",
@@ -2350,12 +2211,11 @@ window.__ModuleLoader__.load({
2350
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.",
2351
2212
  providerGroups: "Provider groups",
2352
2213
  providerGroupsCard: "Per-provider route presets",
2353
- providerGroupsSummary: "Applied automatically when the main model provider switches",
2214
+ providerGroupsSummary: "Applied automatically when this session switches to that provider",
2354
2215
  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.",
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.",
2356
2217
  providerGroupPick: "Pick a provider to edit its group.",
2357
2218
  providerGroupDelete: "Delete group",
2358
- providerGroupApplyFailed: "Main model saved, but applying the provider group failed: ",
2359
2219
  "reason.central-subagent-routing": "Alpha.4 exposes no global Subagent start-routing seam.",
2360
2220
  "reason.packaged-preset-roots": "Alpha.4 exposes no plugin-owned preset root.",
2361
2221
  "reason.tool-owner-suppression": "Alpha.4 exposes no tool owner or provenance suppression seam.",
@@ -2416,13 +2276,8 @@ window.__ModuleLoader__.load({
2416
2276
  }
2417
2277
  async function approvePlanReview(args) {
2418
2278
  const current = args.current;
2419
- const commit = current !== void 0 && current !== null && current.provider === args.selection.provider && current.model === args.selection.model && current.reasoningEffort === args.selection.reasoningEffort ? {
2420
- committed: true,
2421
- mainDefaultRestored: true
2422
- } : await args.select(args.selection);
2423
- 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;
2424
2280
  await args.answer();
2425
- if (!commit.mainDefaultRestored) args.onNotRestored?.(args.selection.model);
2426
2281
  return true;
2427
2282
  }
2428
2283
  function planActionView(state, available, hasExecution) {
@@ -3126,7 +2981,7 @@ window.__ModuleLoader__.load({
3126
2981
  children: mark
3127
2982
  });
3128
2983
  }
3129
- 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 }) {
3130
2985
  const { snapshot: state, getDirectorySnapshot, load, select } = directory;
3131
2986
  const [pane, setPane] = (0, react.useState)("root");
3132
2987
  const [searching, setSearching] = (0, react.useState)(false);
@@ -3209,16 +3064,7 @@ window.__ModuleLoader__.load({
3209
3064
  setQuery("");
3210
3065
  };
3211
3066
  const settleSelection = (outcome) => {
3212
- if (outcome.committed) {
3213
- if (!outcome.mainDefaultRestored) {
3214
- toastSeq.current += 1;
3215
- setToast({
3216
- seq: toastSeq.current,
3217
- text: t("plan.notRestored")
3218
- });
3219
- }
3220
- return;
3221
- }
3067
+ if (outcome.committed) return;
3222
3068
  const message = getDirectorySnapshot().error;
3223
3069
  if (message !== null) {
3224
3070
  toastSeq.current += 1;
@@ -3629,11 +3475,11 @@ window.__ModuleLoader__.load({
3629
3475
  ref: triggerRef,
3630
3476
  type: "button",
3631
3477
  className: ComposerPicker_module_css_default.trigger,
3632
- "aria-label": unavailableReason === void 0 ? triggerAria : `${triggerAria}: ${unavailableReason}`,
3478
+ "aria-label": triggerAria,
3633
3479
  "aria-haspopup": "menu",
3634
3480
  "aria-expanded": open,
3635
3481
  "aria-controls": open ? `${id}-menu` : void 0,
3636
- title: unavailableReason ?? triggerLabel,
3482
+ title: triggerLabel,
3637
3483
  disabled: locked,
3638
3484
  onPointerDown: onTriggerPointerDown,
3639
3485
  onClick: onTriggerClick,
@@ -3858,132 +3704,64 @@ window.__ModuleLoader__.load({
3858
3704
  }, operations);
3859
3705
  }
3860
3706
  //#endregion
3861
- //#region src/client/picker/main-default-restore.ts
3862
- /** Bound on how long the unawaited Host default write is awaited. */
3863
- const DEFAULT_WRITE_WAIT_MS = 750;
3864
- function defaultWait(ms) {
3865
- return new Promise((resolve) => {
3866
- setTimeout(resolve, ms);
3867
- });
3868
- }
3869
- /** A fenced snapshot: ready, Host-synced, writable, with a concrete revision. */
3870
- function fenced(snapshot) {
3871
- if (snapshot.status !== "ready" || snapshot.mode !== "host" || !snapshot.writable) return void 0;
3872
- if (snapshot.value === void 0 || snapshot.revision === void 0) return void 0;
3873
- return {
3874
- revision: snapshot.revision,
3875
- value: snapshot.value
3876
- };
3877
- }
3878
- /** Field writes restoring one complete Main selection; the Host replaces the section. */
3879
- function mainDefaultOps(selection) {
3880
- return [
3881
- {
3882
- op: "set",
3883
- path: ["provider"],
3884
- value: selection.provider
3885
- },
3886
- {
3887
- op: "set",
3888
- path: ["model"],
3889
- value: selection.model
3890
- },
3891
- selection.reasoningEffort === void 0 || selection.reasoningEffort === "" ? {
3892
- op: "unset",
3893
- path: ["reasoningEffort"]
3894
- } : {
3895
- op: "set",
3896
- path: ["reasoningEffort"],
3897
- value: selection.reasoningEffort
3898
- }
3899
- ];
3900
- }
3901
- /** An effort never leaks across a comparison: absent and cleared are the same selection. */
3902
- function sameMainSelection(left, right) {
3903
- if (left === void 0) return false;
3904
- const leftEffort = left.reasoningEffort === void 0 || left.reasoningEffort === "" ? void 0 : left.reasoningEffort;
3905
- const rightEffort = right.reasoningEffort === void 0 || right.reasoningEffort === "" ? void 0 : right.reasoningEffort;
3906
- return left.provider === right.provider && left.model === right.model && leftEffort === rightEffort;
3907
- }
3908
- /** Resolve as soon as the revision leaves `revision`, or after the bounded wait. */
3909
- function waitForAdvance(form, revision, wait, timeoutMs) {
3910
- const current = form.getSnapshot();
3911
- if (current.revision !== revision) return Promise.resolve(current);
3912
- return new Promise((resolve) => {
3913
- let settled = false;
3914
- const finish = (snapshot) => {
3915
- if (settled) return;
3916
- settled = true;
3917
- unsubscribe();
3918
- resolve(snapshot);
3919
- };
3920
- const unsubscribe = form.subscribe(() => {
3921
- const next = form.getSnapshot();
3922
- if (next.revision !== revision) finish(next);
3923
- });
3924
- wait(timeoutMs).then(() => {
3925
- finish(form.getSnapshot());
3926
- });
3927
- });
3928
- }
3707
+ //#region src/client/picker/provider-group-watch.ts
3929
3708
  /**
3930
- * Restore the Main default captured before a Session-only model switch.
3709
+ * Watch one directory store and apply groups on provider changes.
3931
3710
  *
3932
- * Never throws and never blocks a Plan answer: a failed compensation is
3933
- * reported, not enforced, because the Session selection the human asked for is
3934
- * already installed by the time this runs.
3935
- *
3936
- * @param form - the official Main Settings namespace form.
3937
- * @param before - the snapshot captured before the switch.
3938
- * @param options - wait/timeout/attempt overrides for tests.
3939
- * @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.
3940
3719
  */
3941
- async function restoreMainDefault(form, before, options = {}) {
3942
- const captured = fenced(before);
3943
- if (captured === void 0) return {
3944
- restored: true,
3945
- conflict: false
3946
- };
3947
- const wait = options.wait ?? defaultWait;
3948
- const timeoutMs = options.timeoutMs ?? DEFAULT_WRITE_WAIT_MS;
3949
- const attempts = Math.max(1, options.attempts ?? 2);
3950
- await waitForAdvance(form, captured.revision, wait, timeoutMs);
3951
- const observed = fenced(form.getSnapshot());
3952
- if (observed === void 0) return {
3953
- restored: true,
3954
- conflict: false
3955
- };
3956
- if (observed.revision === captured.revision) return {
3957
- restored: true,
3958
- conflict: false
3959
- };
3960
- if (sameMainSelection(observed.value, captured.value)) return {
3961
- restored: true,
3962
- conflict: false
3963
- };
3964
- for (let attempt = 0; attempt < attempts; attempt += 1) {
3965
- const snapshot = fenced(form.getSnapshot());
3966
- if (snapshot === void 0) return {
3967
- restored: false,
3968
- conflict: false
3969
- };
3970
- if (sameMainSelection(snapshot.value, captured.value)) return {
3971
- restored: true,
3972
- conflict: false
3973
- };
3974
- if (await form.mutate(mainDefaultOps(captured.value), snapshot.revision)) return {
3975
- restored: true,
3976
- conflict: false
3977
- };
3978
- if (form.getSnapshot().revision === snapshot.revision) return {
3979
- restored: false,
3980
- conflict: false
3981
- };
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;
3982
3730
  }
3983
- return {
3984
- restored: false,
3985
- 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
+ }
3986
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);
3763
+ };
3764
+ return { dispose: options.source.subscribe(check) };
3987
3765
  }
3988
3766
  //#endregion
3989
3767
  //#region src/picker/pending-dismissal.ts
@@ -4199,12 +3977,6 @@ window.__ModuleLoader__.load({
4199
3977
  throw cause instanceof PlanDismissalUnsupportedError ? new Error(unsupportedMessage, { cause }) : new Error(rejectedMessage, { cause });
4200
3978
  }
4201
3979
  }
4202
- /** Match the model seat's admission guard before letting either picker offer a change. */
4203
- function mainDefaultsUnavailableReason(snapshot, t) {
4204
- if (snapshot.mode === "memory") return t("settings.remoteUnavailable");
4205
- if (snapshot.status === "loading") return t("settings.loading");
4206
- if (snapshot.status !== "ready" || snapshot.mode !== "host" || !snapshot.writable || snapshot.value === void 0 || snapshot.revision === void 0) return t("settings.unavailable");
4207
- }
4208
3980
  /** Inline failed lock-read status; history and log reading stay unaffected. */
4209
3981
  function ProviderLockHint(props) {
4210
3982
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -4220,7 +3992,6 @@ window.__ModuleLoader__.load({
4220
3992
  function PlanReviewCard(props) {
4221
3993
  const snapshot = props.useDirectory((value) => value);
4222
3994
  const order = props.useProviderOrder((value) => value);
4223
- const settingsUnavailableReason = mainDefaultsUnavailableReason((0, react.useSyncExternalStore)(props.subscribeMainDefaults, props.getMainDefaultsSnapshot), props.t);
4224
3995
  const lock = (0, react.useSyncExternalStore)(props.providerLockStore.subscribe, props.providerLockStore.getSnapshot);
4225
3996
  const phase = props.useInput((input) => input.phase);
4226
3997
  const blank = props.useSession((session) => session.blank);
@@ -4256,7 +4027,6 @@ window.__ModuleLoader__.load({
4256
4027
  providerLock,
4257
4028
  agentLocked,
4258
4029
  lockFailed: lock.failed,
4259
- ...settingsUnavailableReason === void 0 ? {} : { settingsUnavailableReason },
4260
4030
  directory: pickerDirectoryViewOrdered(snapshot, props, order, props.catalogRoutes?.() ?? {}),
4261
4031
  t: props.t,
4262
4032
  goalRemote: props.goalRemote,
@@ -4265,7 +4035,7 @@ window.__ModuleLoader__.load({
4265
4035
  ...props.roleOf === void 0 ? {} : { roleOf: props.roleOf }
4266
4036
  }, props.matched.key);
4267
4037
  }
4268
- 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 }) {
4269
4039
  const { snapshot, getDirectorySnapshot, load, select } = directory;
4270
4040
  const [execution, setExecution] = (0, react.useState)(snapshot.current ?? void 0);
4271
4041
  const [feedback, setFeedback] = (0, react.useState)("");
@@ -4328,8 +4098,7 @@ window.__ModuleLoader__.load({
4328
4098
  setNotice(null);
4329
4099
  load();
4330
4100
  };
4331
- 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);
4332
- 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)));
4333
4102
  const action = planActionView({
4334
4103
  busy,
4335
4104
  blocked,
@@ -4341,11 +4110,7 @@ window.__ModuleLoader__.load({
4341
4110
  select,
4342
4111
  selection: model,
4343
4112
  current: snapshot.current,
4344
- answer: () => respondAnswer(matched, review.id, review.approve.label, t("plan.responseRejected"), true),
4345
- onNotRestored: (switched) => {
4346
- setNotice(t("plan.notRestored", { model: switched }));
4347
- console.warn("dsh-model-switch: the Session model changed but the Main default was not restored", switched);
4348
- }
4113
+ answer: () => respondAnswer(matched, review.id, review.approve.label, t("plan.responseRejected"), true)
4349
4114
  })) {
4350
4115
  const message = getDirectorySnapshot().error;
4351
4116
  throw new Error(message === null ? t("plan.modelFailed") : t("error.action", { message }));
@@ -4417,12 +4182,11 @@ window.__ModuleLoader__.load({
4417
4182
  errorLabel: (message) => t("plan.pickerCrash", { message }),
4418
4183
  retryLabel: t("retry"),
4419
4184
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ComposerPicker, {
4420
- locked: busy || blocked || settingsUnavailableReason !== void 0,
4185
+ locked: busy || blocked,
4421
4186
  providerLock,
4422
4187
  agentLocked,
4423
4188
  ...roleOf === void 0 ? {} : { roleOf },
4424
4189
  available,
4425
- ...settingsUnavailableReason === void 0 ? {} : { unavailableReason: settingsUnavailableReason },
4426
4190
  directory,
4427
4191
  t,
4428
4192
  ...resolveInteractionOperations === void 0 ? {} : { resolveInteractionOperations },
@@ -4456,7 +4220,7 @@ window.__ModuleLoader__.load({
4456
4220
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4457
4221
  className: PlanReviewCard_module_css_default.feedback,
4458
4222
  role: "status",
4459
- children: action.error ?? notice ?? (settingsBlock ? settingsUnavailableReason : null)
4223
+ children: action.error ?? notice
4460
4224
  }),
4461
4225
  decline !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4462
4226
  className: PlanReviewCard_module_css_default.feedbackRow,
@@ -4578,9 +4342,6 @@ window.__ModuleLoader__.load({
4578
4342
  "thinking.off": "关",
4579
4343
  "effort.providerDefault": "Default",
4580
4344
  "status.loading": "正在刷新模型列表…",
4581
- "settings.remoteUnavailable": "官方 DSH 在非本机浏览器禁用配置读取与写入;请在 Host 的 127.0.0.1 页面选择模型。",
4582
- "settings.loading": "正在加载主模型设置;加载完成后才能切换模型。",
4583
- "settings.unavailable": "主模型设置不可写;请在 Host 的本机页面检查设置状态。",
4584
4345
  "error.action": "模型操作失败:{message}",
4585
4346
  "error.picker": "模型选择器出错:{message}(点击重试)",
4586
4347
  "action.reload": "重新加载",
@@ -4601,7 +4362,6 @@ window.__ModuleLoader__.load({
4601
4362
  "plan.pickerCrash": "执行模型选择器出错:{message}",
4602
4363
  "plan.feedback": "拒绝时的反馈",
4603
4364
  "plan.feedbackPlaceholder": "说明要改什么(可留空)",
4604
- "plan.notRestored": "执行模型已切换,但主模型默认值未能还原({model});请在「设置 → 模型」中确认默认模型。",
4605
4365
  "plan.dismissUnsupported": "当前 DSH 版本既未提供 PendingQuestion.dismiss() 也未提供 cancel(),无法回到聊天;请直接在输入框里发送你的意见。",
4606
4366
  "plan.goalRun": "设置为目标并执行",
4607
4367
  "plan.goalFailed": "设置目标失败:{message}",
@@ -4634,9 +4394,6 @@ window.__ModuleLoader__.load({
4634
4394
  "thinking.off": "Off",
4635
4395
  "effort.providerDefault": "Default",
4636
4396
  "status.loading": "Refreshing model list…",
4637
- "settings.remoteUnavailable": "Official DSH disables settings reads and writes in non-loopback browsers. Select a model on the Host loopback URL.",
4638
- "settings.loading": "Main model settings are loading. Wait until they are ready before switching models.",
4639
- "settings.unavailable": "Main model settings are not writable. Check their status on the Host loopback URL.",
4640
4397
  "error.action": "Model operation failed: {message}",
4641
4398
  "error.picker": "Model picker error: {message} (click to retry)",
4642
4399
  "action.reload": "Reload",
@@ -4657,7 +4414,6 @@ window.__ModuleLoader__.load({
4657
4414
  "plan.pickerCrash": "Execution model picker error: {message}",
4658
4415
  "plan.feedback": "Feedback when declining",
4659
4416
  "plan.feedbackPlaceholder": "Say what to change (optional)",
4660
- "plan.notRestored": "The execution model switched, but the Main default could not be restored ({model}); confirm the default in Settings → Models.",
4661
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.",
4662
4418
  "plan.goalRun": "Set as goal and run",
4663
4419
  "plan.goalFailed": "Could not set the goal: {message}",
@@ -4757,7 +4513,6 @@ window.__ModuleLoader__.load({
4757
4513
  function ModelSeat(props) {
4758
4514
  const directory = props.useDirectory((snapshot) => snapshot);
4759
4515
  const order = props.useProviderOrder((value) => value);
4760
- const settingsUnavailableReason = mainDefaultsUnavailableReason((0, react.useSyncExternalStore)(props.subscribeMainDefaults, props.getMainDefaultsSnapshot), props.t);
4761
4516
  const lock = (0, react.useSyncExternalStore)(props.providerLockStore.subscribe, props.providerLockStore.getSnapshot);
4762
4517
  const phase = props.useInput((input) => input.phase);
4763
4518
  const blank = props.useSession((session) => session.blank);
@@ -4773,12 +4528,11 @@ window.__ModuleLoader__.load({
4773
4528
  ]);
4774
4529
  const providerLock = effectiveProviderLock(lock, directory.current?.provider, active, isAgentRole(props.roleOf?.(directory.current?.provider ?? "")));
4775
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, {
4776
- locked: props.locked || settingsUnavailableReason !== void 0 || lock.failed && directory.current === null,
4531
+ locked: props.locked || lock.failed && directory.current === null,
4777
4532
  providerLock,
4778
4533
  agentLocked: agentProviderLocked(blank, providerLock, active),
4779
4534
  ...props.roleOf === void 0 ? {} : { roleOf: props.roleOf },
4780
4535
  available: props.available,
4781
- ...settingsUnavailableReason === void 0 ? {} : { unavailableReason: settingsUnavailableReason },
4782
4536
  directory: pickerDirectoryViewOrdered(directory, props, order, props.catalogRoutes?.() ?? {}),
4783
4537
  t: props.t,
4784
4538
  ...props.resolveInteractionOperations === void 0 ? {} : { resolveInteractionOperations: props.resolveInteractionOperations }
@@ -4803,8 +4557,23 @@ window.__ModuleLoader__.load({
4803
4557
  ], (scope) => {
4804
4558
  const models = scope.modelDirectories;
4805
4559
  const sessions = scope.sessions;
4806
- const mainDefaults = scope.configForms.get(MAIN_DEFAULT_CONFIG_ID);
4807
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
+ });
4808
4577
  let directoryService;
4809
4578
  const orderStore = providerOrderStore(scope.configForms.get(PROVIDERS_CONFIG_ID));
4810
4579
  scope.inject(["providerDirectory"], (directoryScope) => {
@@ -4835,6 +4604,7 @@ window.__ModuleLoader__.load({
4835
4604
  };
4836
4605
  const roleOf = (key) => readProviderRole(directoryService, key);
4837
4606
  const providerLockStore = createProviderLockStore(readLock);
4607
+ if (available) watchGroupsFor(sessionId, directory.store);
4838
4608
  return {
4839
4609
  available,
4840
4610
  roleOf,
@@ -4843,8 +4613,6 @@ window.__ModuleLoader__.load({
4843
4613
  refreshProviderLock: () => {
4844
4614
  providerLockStore.refresh();
4845
4615
  },
4846
- subscribeMainDefaults: (listener) => mainDefaults.subscribe(listener),
4847
- getMainDefaultsSnapshot: () => mainDefaults.getSnapshot(),
4848
4616
  goalRemote: () => goalRemoteFrom(scope),
4849
4617
  planPreview,
4850
4618
  hooks: {
@@ -4857,36 +4625,22 @@ window.__ModuleLoader__.load({
4857
4625
  if (available) directory.load().catch(() => {});
4858
4626
  },
4859
4627
  select: async (selection) => {
4860
- const uncommitted = {
4861
- committed: false,
4862
- mainDefaultRestored: true
4863
- };
4864
- if (!available) return uncommitted;
4628
+ if (!available) return { committed: false };
4865
4629
  const state = await providerLockStore.refresh();
4866
4630
  const currentProvider = directory.store.getSnapshot().current?.provider;
4867
4631
  if (!isProviderAllowed(state, selection.provider, currentProvider, {
4868
4632
  ...readSessionState(sessions, sessionId),
4869
4633
  agent: isAgentRole(roleOf(selection.provider)),
4870
4634
  currentAgent: currentProvider !== void 0 && isAgentRole(roleOf(currentProvider))
4871
- })) return uncommitted;
4872
- const defaultBeforeSwitch = mainDefaults.getSnapshot();
4873
- if (defaultBeforeSwitch.status !== "ready" || defaultBeforeSwitch.mode !== "host" || !defaultBeforeSwitch.writable || defaultBeforeSwitch.value === void 0 || defaultBeforeSwitch.revision === void 0) return uncommitted;
4635
+ })) return { committed: false };
4874
4636
  let result;
4875
4637
  try {
4876
4638
  result = await directory.select(selection);
4877
4639
  } catch {
4878
- return uncommitted;
4879
- }
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);
4640
+ return { committed: false };
4885
4641
  }
4886
- return {
4887
- committed: true,
4888
- mainDefaultRestored: (await restoreMainDefault(mainDefaults, defaultBeforeSwitch)).restored
4889
- };
4642
+ if (!result.ok) return { committed: false };
4643
+ return { committed: true };
4890
4644
  }
4891
4645
  };
4892
4646
  };
@@ -4983,56 +4737,12 @@ window.__ModuleLoader__.load({
4983
4737
  }), "dsh-model-switch: localized Settings section");
4984
4738
  const t = ctx.locale.bind(localeNamespace);
4985
4739
  const remote = ctx.remote;
4986
- const mainForm = ctx.configForms.get(MAIN_DEFAULT_CONFIG_ID);
4987
4740
  const owned = ctx.configForms.get(MODEL_SWITCH_CONFIG_ID);
4988
- const main = deriveConfigForm(mainForm, (value) => value, {
4989
- provider: "provider",
4990
- model: "model",
4991
- reasoningEffort: "reasoningEffort"
4992
- });
4993
4741
  const subagent = deriveConfigForm(owned, deriveSubagentSettings, SUBAGENT_SETTINGS_FIELDS);
4994
4742
  const search = deriveConfigForm(owned, deriveSearchSettings, SEARCH_SETTINGS_FIELDS);
4995
4743
  const image = deriveConfigForm(owned, deriveImageSettings, IMAGE_SETTINGS_FIELDS);
4996
4744
  const autoReview = deriveConfigForm(owned, deriveAutoReviewSettings, AUTO_REVIEW_SETTINGS_FIELDS);
4997
4745
  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
- };
5002
- const saveMain = async (next, expectedRevision) => {
5003
- const previousProvider = mainForm.getSnapshot().value?.provider;
5004
- if (!await mainForm.mutate([
5005
- {
5006
- op: "set",
5007
- path: ["provider"],
5008
- value: next.provider
5009
- },
5010
- {
5011
- op: "set",
5012
- path: ["model"],
5013
- value: next.model
5014
- },
5015
- next.reasoningEffort === void 0 || next.reasoningEffort === "" ? {
5016
- op: "unset",
5017
- path: ["reasoningEffort"]
5018
- } : {
5019
- op: "set",
5020
- path: ["reasoningEffort"],
5021
- value: next.reasoningEffort
5022
- }
5023
- ], expectedRevision)) {
5024
- if (mainForm.getSnapshot().revision !== expectedRevision) throw new MainSettingsConflictError(t("conflict"));
5025
- throw new Error("settings-rejected");
5026
- }
5027
- const revision = mainForm.getSnapshot().revision;
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
- }
5034
- return revision;
5035
- };
5036
4746
  const connectionRpc = (() => {
5037
4747
  try {
5038
4748
  const rpc = ctx.get("connection", false)?.rpc;
@@ -5065,7 +4775,6 @@ window.__ModuleLoader__.load({
5065
4775
  inject: () => ({
5066
4776
  t,
5067
4777
  hooks: {
5068
- mainSettings: main,
5069
4778
  subagentSettings: subagent,
5070
4779
  searchSettings: search,
5071
4780
  imageSettings: image,
@@ -5074,7 +4783,6 @@ window.__ModuleLoader__.load({
5074
4783
  switchSettings: owned
5075
4784
  },
5076
4785
  capabilities: RUNTIME_CAPABILITIES,
5077
- saveMain,
5078
4786
  setSubagent: (field, value) => value === void 0 ? subagent.unset(field) : subagent.set(field, value),
5079
4787
  setCapability: (route, field, value) => {
5080
4788
  const scope = route === "search" ? search : route === "image" ? image : route === "compaction" ? compaction : autoReview;