dsh-model-switch 0.4.23 → 0.4.24

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
@@ -39,6 +39,11 @@ window.__ModuleLoader__.load({
39
39
  provider: "compactProvider",
40
40
  model: "compactModel"
41
41
  });
42
+ /** Automatic session-title naming route; both unset means the official default (the session's own logged route). */
43
+ const TITLE_SETTINGS_FIELDS = Object.freeze({
44
+ provider: "titleProvider",
45
+ model: "titleModel"
46
+ });
42
47
  function deriveSubagentSettings(settings) {
43
48
  return {
44
49
  mode: settings.subagentMode,
@@ -71,6 +76,12 @@ window.__ModuleLoader__.load({
71
76
  ...settings.compactModel === void 0 ? {} : { model: settings.compactModel }
72
77
  };
73
78
  }
79
+ function deriveTitleSettings(settings) {
80
+ return {
81
+ ...settings.titleProvider === void 0 ? {} : { provider: settings.titleProvider },
82
+ ...settings.titleModel === void 0 ? {} : { model: settings.titleModel }
83
+ };
84
+ }
74
85
  /** Whether both fields of a model route form a complete pair; an incomplete pair stays off. */
75
86
  function routePairConfigured(route) {
76
87
  return route !== void 0 && (route.provider ?? "").trim() !== "" && (route.model ?? "").trim() !== "";
@@ -84,7 +95,9 @@ window.__ModuleLoader__.load({
84
95
  "autoReviewModel",
85
96
  "subagentProvider",
86
97
  "subagentModel",
87
- "subagentReasoningEffort"
98
+ "subagentReasoningEffort",
99
+ "titleProvider",
100
+ "titleModel"
88
101
  ]);
89
102
  /** Own data property, so a literal '__proto__' provider id can never touch a prototype. */
90
103
  function setKey(target, key, value) {
@@ -147,62 +160,6 @@ window.__ModuleLoader__.load({
147
160
  if (!Object.prototype.hasOwnProperty.call(groups, provider)) return void 0;
148
161
  return groups[provider];
149
162
  }
150
- /** A complete pinned pair; either half missing leaves the capability at its default. */
151
- function pair(group, providerField, modelField) {
152
- const provider = group[providerField];
153
- const model = group[modelField];
154
- return present(provider) && present(model) ? {
155
- provider,
156
- model
157
- } : void 0;
158
- }
159
- /**
160
- * Ordered writes applying one group with full-replacement semantics: pinned
161
- * capabilities land as pairs, everything else reverts to its default state
162
- * (compaction/review follow the session model; Subagent follows official
163
- * inherit). The mode write comes last so a fixed route never publishes with
164
- * the previous provider's fields still half-written.
165
- */
166
- function providerGroupWrites(group) {
167
- const compact = pair(group, "compactProvider", "compactModel");
168
- const review = pair(group, "autoReviewProvider", "autoReviewModel");
169
- const subagent = pair(group, "subagentProvider", "subagentModel");
170
- const effort = subagent !== void 0 && present(group.subagentReasoningEffort) ? group.subagentReasoningEffort : void 0;
171
- return [
172
- {
173
- field: "compactProvider",
174
- value: compact?.provider
175
- },
176
- {
177
- field: "compactModel",
178
- value: compact?.model
179
- },
180
- {
181
- field: "autoReviewProvider",
182
- value: review?.provider
183
- },
184
- {
185
- field: "autoReviewModel",
186
- value: review?.model
187
- },
188
- {
189
- field: "subagentProvider",
190
- value: subagent?.provider
191
- },
192
- {
193
- field: "subagentModel",
194
- value: subagent?.model
195
- },
196
- {
197
- field: "subagentReasoningEffort",
198
- value: effort
199
- },
200
- {
201
- field: "subagentMode",
202
- mode: subagent !== void 0 ? "fixed" : "follow-main"
203
- }
204
- ];
205
- }
206
163
  /** Storage entry for one provider's group; absent or blank route fields become ''. */
207
164
  function entryOf(provider, group) {
208
165
  return {
@@ -213,7 +170,9 @@ window.__ModuleLoader__.load({
213
170
  autoReviewModel: group.autoReviewModel ?? "",
214
171
  subagentProvider: group.subagentProvider ?? "",
215
172
  subagentModel: group.subagentModel ?? "",
216
- subagentReasoningEffort: group.subagentReasoningEffort ?? ""
173
+ subagentReasoningEffort: group.subagentReasoningEffort ?? "",
174
+ titleProvider: group.titleProvider ?? "",
175
+ titleModel: group.titleModel ?? ""
217
176
  };
218
177
  }
219
178
  /**
@@ -230,19 +189,6 @@ window.__ModuleLoader__.load({
230
189
  }
231
190
  return Object.entries(groups).map(([key, value]) => entryOf(key, value));
232
191
  }
233
- /**
234
- * Apply a saved group's routes to the live flat settings.
235
- * @returns true when a group existed and every write settled; false when the
236
- * provider has no group (nothing was written). A rejected write propagates.
237
- */
238
- async function applyProviderGroupTo(sink, groups, provider) {
239
- const group = groupOf(groups, provider);
240
- if (group === void 0) return false;
241
- for (const write of providerGroupWrites(group)) if (write.field === "subagentMode") await sink.set("subagentMode", write.mode);
242
- else if (write.value === void 0) await sink.unset(write.field);
243
- else await sink.set(write.field, write.value);
244
- return true;
245
- }
246
192
  //#endregion
247
193
  //#region src/client/derived-config-form.ts
248
194
  function deriveConfigForm(source, project, fields) {
@@ -263,11 +209,28 @@ window.__ModuleLoader__.load({
263
209
  const sourceField = fields[field];
264
210
  if (!(operation === "set" ? await source.set(sourceField, value) : await source.unset(sourceField))) throw new Error("settings-rejected");
265
211
  };
212
+ /**
213
+ * Batch write: map every view field through the same `fields` table `set`/`unset`
214
+ * use, so one atomic source mutate can carry a whole row's worth of edits under
215
+ * a single revision fence instead of N independent fenced writes.
216
+ */
217
+ const mutate = async (ops, expectedRevision) => {
218
+ const mapped = ops.map((op) => op.value === void 0 ? {
219
+ op: "unset",
220
+ path: [fields[op.field]]
221
+ } : {
222
+ op: "set",
223
+ path: [fields[op.field]],
224
+ value: op.value
225
+ });
226
+ if (!await source.mutate(mapped, expectedRevision)) throw new Error("settings-rejected");
227
+ };
266
228
  return {
267
229
  getSnapshot,
268
230
  subscribe: (listener) => source.subscribe(listener),
269
231
  set: (field, value) => write("set", field, value),
270
- unset: (field) => write("unset", field)
232
+ unset: (field) => write("unset", field),
233
+ mutate
271
234
  };
272
235
  }
273
236
  //#endregion
@@ -828,6 +791,23 @@ window.__ModuleLoader__.load({
828
791
  strokeLinejoin: "round"
829
792
  })]
830
793
  });
794
+ if (kind === "sessionTitle") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
795
+ width: "18",
796
+ height: "18",
797
+ viewBox: "0 0 18 18",
798
+ fill: "none",
799
+ "aria-hidden": "true",
800
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
801
+ d: "M3 4.5h7.5l4.2 4.2-5.7 5.7L3 9.8V4.5Z",
802
+ stroke: "currentColor",
803
+ strokeLinejoin: "round"
804
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
805
+ cx: "6.2",
806
+ cy: "7.2",
807
+ r: "1.1",
808
+ stroke: "currentColor"
809
+ })]
810
+ });
831
811
  return null;
832
812
  }
833
813
  function RouteCard({ title, summary, hint, icon, open, onToggle, disabled = false, badge, badgeWarn = false, trailing, expandable = true, children }) {
@@ -952,17 +932,34 @@ window.__ModuleLoader__.load({
952
932
  ]
953
933
  });
954
934
  }
935
+ /**
936
+ * One editable row draft.
937
+ *
938
+ * Only a PRISTINE draft follows the accepted snapshot. A draft the user has
939
+ * edited is never overwritten by an incoming snapshot — another tab's write, or
940
+ * the recovery read that follows a refused write, would otherwise discard the
941
+ * typed values and make the page look like it rolled back on its own. The user
942
+ * either saves the draft or cancels it back to the accepted value.
943
+ * @param snapshot - the accepted form snapshot for this row.
944
+ * @returns the draft, its setter, and a reset that re-adopts the accepted value.
945
+ */
955
946
  function useDraft(snapshot) {
956
947
  const [draft, setDraft] = (0, react.useState)(snapshot.value);
948
+ const pristine = (0, react.useRef)(true);
957
949
  (0, react.useEffect)(() => {
950
+ if (!pristine.current) return;
958
951
  setDraft(snapshot.value);
959
952
  }, [snapshot.revision, snapshot.value]);
960
953
  return [
961
954
  draft,
962
- setDraft,
963
- () => {
955
+ (0, react.useCallback)((value) => {
956
+ pristine.current = false;
957
+ setDraft(value);
958
+ }, []),
959
+ (0, react.useCallback)(() => {
960
+ pristine.current = true;
964
961
  setDraft(snapshot.value);
965
- }
962
+ }, [snapshot.value])
966
963
  ];
967
964
  }
968
965
  function routeName(groups, route) {
@@ -1018,6 +1015,7 @@ window.__ModuleLoader__.load({
1018
1015
  const image = props.useImageSettings((value) => value);
1019
1016
  const autoReview = props.useAutoReviewSettings((value) => value);
1020
1017
  const compaction = props.useCompactionSettings((value) => value);
1018
+ const sessionTitle = props.useTitleSettings((value) => value);
1021
1019
  const switchSettings = props.useSwitchSettings((value) => value);
1022
1020
  const [open, setOpen] = (0, react.useState)();
1023
1021
  const [subagentDraft, setSubagentDraft, resetSubagent] = useDraft(subagent);
@@ -1025,6 +1023,7 @@ window.__ModuleLoader__.load({
1025
1023
  const [imageDraft, setImageDraft, resetImage] = useDraft(image);
1026
1024
  const [autoReviewDraft, setAutoReviewDraft, resetAutoReview] = useDraft(autoReview);
1027
1025
  const [compactionDraft, setCompactionDraft, resetCompaction] = useDraft(compaction);
1026
+ const [titleDraft, setTitleDraft, resetTitle] = useDraft(sessionTitle);
1028
1027
  const [busy, setBusy] = (0, react.useState)();
1029
1028
  const [message, setMessage] = (0, react.useState)();
1030
1029
  const [compactBusy, setCompactBusy] = (0, react.useState)(false);
@@ -1070,15 +1069,17 @@ window.__ModuleLoader__.load({
1070
1069
  };
1071
1070
  }, [loadSearchCapabilities]);
1072
1071
  const providerGroups = normalizeProviderGroups(switchSettings.value?.providerGroups);
1073
- /** Seed an unsaved group from the live routes. */
1072
+ /**
1073
+ * Seed an unsaved group from the live routes.
1074
+ *
1075
+ * Only the two per-session capabilities are seeded: a group carrying a
1076
+ * compaction or reviewer pin would store a value no consumer reads (see
1077
+ * src/session-route.ts), which is worse than storing nothing.
1078
+ */
1074
1079
  const seedGroupFromActive = () => {
1075
- const compact = compaction.value !== void 0 && routePairConfigured(compaction.value) ? {
1076
- provider: compaction.value.provider ?? "",
1077
- model: compaction.value.model ?? ""
1078
- } : void 0;
1079
- const review = autoReview.value !== void 0 && routePairConfigured(autoReview.value) ? {
1080
- provider: autoReview.value.provider ?? "",
1081
- model: autoReview.value.model ?? ""
1080
+ const title = sessionTitle.value !== void 0 && routePairConfigured(sessionTitle.value) ? {
1081
+ provider: sessionTitle.value.provider ?? "",
1082
+ model: sessionTitle.value.model ?? ""
1082
1083
  } : void 0;
1083
1084
  const fixedSubagent = subagent.value !== void 0 && subagent.value.mode === "fixed" && routePairConfigured(subagent.value) ? {
1084
1085
  provider: subagent.value.provider ?? "",
@@ -1086,13 +1087,9 @@ window.__ModuleLoader__.load({
1086
1087
  reasoningEffort: subagent.value.reasoningEffort
1087
1088
  } : void 0;
1088
1089
  return {
1089
- ...compact === void 0 ? {} : {
1090
- compactProvider: compact.provider,
1091
- compactModel: compact.model
1092
- },
1093
- ...review === void 0 ? {} : {
1094
- autoReviewProvider: review.provider,
1095
- autoReviewModel: review.model
1090
+ ...title === void 0 ? {} : {
1091
+ titleProvider: title.provider,
1092
+ titleModel: title.model
1096
1093
  },
1097
1094
  ...fixedSubagent === void 0 ? {} : {
1098
1095
  subagentProvider: fixedSubagent.provider,
@@ -1126,13 +1123,15 @@ window.__ModuleLoader__.load({
1126
1123
  switchSettings.value,
1127
1124
  subagent.value,
1128
1125
  autoReview.value,
1129
- compaction.value
1126
+ compaction.value,
1127
+ sessionTitle.value
1130
1128
  ]);
1131
1129
  if ([
1132
1130
  subagent,
1133
1131
  search,
1134
1132
  image,
1135
- switchSettings
1133
+ switchSettings,
1134
+ sessionTitle
1136
1135
  ].some((form) => form.mode === "memory")) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("main", {
1137
1136
  className: ModelSwitchSettings_module_css_default.section,
1138
1137
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", {
@@ -1182,83 +1181,125 @@ window.__ModuleLoader__.load({
1182
1181
  const autoReviewOn = routePairConfigured(autoReview.value);
1183
1182
  const compactionChoices = deriveRouteChoices(groups, compactionDraft);
1184
1183
  const compactionOn = routePairConfigured(compaction.value);
1184
+ const titleChoices = deriveRouteChoices(groups, titleDraft);
1185
+ const titleOn = routePairConfigured(sessionTitle.value);
1185
1186
  const synced = [
1186
1187
  subagent,
1187
1188
  search,
1188
1189
  image,
1189
1190
  autoReview,
1190
- compaction
1191
+ compaction,
1192
+ sessionTitle
1191
1193
  ].every((snapshot) => snapshot.status === "ready");
1194
+ /**
1195
+ * Run one row save with a visible outcome that ALWAYS settles.
1196
+ *
1197
+ * A stalled write used to latch the page on "保存中…" forever, because `busy`
1198
+ * was only cleared by the settling promise. The 30s deadline reports the stall
1199
+ * instead; a late result is ignored by the race, so it can never overwrite a
1200
+ * newer draft either.
1201
+ * @param route - the row whose save is running, for busy and message routing.
1202
+ * @param operation - the single fenced write to perform.
1203
+ * @returns whether the write was accepted.
1204
+ */
1192
1205
  const run = async (route, operation) => {
1193
- if (busy !== void 0) return;
1206
+ if (busy !== void 0) return false;
1207
+ const timeoutMs = 3e4;
1194
1208
  setBusy(route);
1195
1209
  setMessage(void 0);
1210
+ let timer;
1196
1211
  try {
1197
- await operation();
1212
+ await Promise.race([operation(), new Promise((_resolve, reject) => {
1213
+ timer = setTimeout(() => reject(new Error(props.t("saveTimeout"))), timeoutMs);
1214
+ })]);
1198
1215
  setMessage({
1199
1216
  route,
1200
1217
  text: props.t("saved")
1201
1218
  });
1219
+ return true;
1202
1220
  } catch (error) {
1203
1221
  setMessage({
1204
1222
  route,
1205
1223
  text: error instanceof Error ? error.message : props.t("requestFailed")
1206
1224
  });
1225
+ return false;
1207
1226
  } finally {
1227
+ if (timer !== void 0) clearTimeout(timer);
1208
1228
  setBusy(void 0);
1209
1229
  }
1210
1230
  };
1211
1231
  const saveSubagent = () => {
1212
1232
  if (subagentDraft === void 0 || subagentDraft.mode !== "fixed") return;
1213
- run("subagent", async () => {
1214
- if (subagent.value?.provider !== subagentDraft.provider) await props.setSubagent("provider", subagentDraft.provider);
1215
- const matched = subagentCatalogModel;
1216
- const nextModel = matched?.model.id ?? subagentDraft.model;
1217
- const nextEffort = subagentDraft.reasoningEffort === "" ? void 0 : subagentDraft.reasoningEffort || matched?.effort;
1218
- if (subagent.value?.model !== nextModel) await props.setSubagent("model", nextModel);
1219
- if (subagent.value?.reasoningEffort !== nextEffort) await props.setSubagent("reasoningEffort", nextEffort);
1220
- });
1233
+ const matched = subagentCatalogModel;
1234
+ const provider = (subagentDraft.provider ?? "").trim();
1235
+ const model = (matched?.model.id ?? subagentDraft.model ?? "").trim();
1236
+ if (provider === "" || model === "") return;
1237
+ const nextEffort = subagentDraft.reasoningEffort === "" ? void 0 : subagentDraft.reasoningEffort || matched?.effort;
1238
+ run("subagent", () => props.saveSubagent([
1239
+ {
1240
+ field: "provider",
1241
+ value: provider
1242
+ },
1243
+ {
1244
+ field: "model",
1245
+ value: model
1246
+ },
1247
+ {
1248
+ field: "reasoningEffort",
1249
+ value: nextEffort
1250
+ }
1251
+ ]));
1221
1252
  };
1222
1253
  const persistSubagentEnabled = (enabled) => {
1223
1254
  if (subagentDraft === void 0 || busy === "subagent" || !subagent.writable) return;
1224
- setSubagentDraft({
1225
- ...subagentDraft,
1226
- mode: subagentModeForEnabled(enabled)
1227
- });
1228
- setBusy("subagent");
1229
- setMessage(void 0);
1230
- props.setSubagent("mode", subagentModeForEnabled(enabled)).then(() => {
1231
- setBusy(void 0);
1232
- setOpen((current) => enabled ? "subagent" : current === "subagent" ? void 0 : current);
1233
- }, (error) => {
1234
- setBusy(void 0);
1235
- resetSubagent();
1236
- setMessage({
1237
- route: "subagent",
1238
- text: error instanceof Error ? error.message : props.t("requestFailed")
1255
+ const mode = subagentModeForEnabled(enabled);
1256
+ run("subagent", () => props.saveSubagent([{
1257
+ field: "mode",
1258
+ value: mode
1259
+ }])).then((accepted) => {
1260
+ if (!accepted) {
1261
+ resetSubagent();
1262
+ return;
1263
+ }
1264
+ setSubagentDraft({
1265
+ ...subagentDraft,
1266
+ mode
1239
1267
  });
1268
+ setOpen((current) => enabled ? "subagent" : current === "subagent" ? void 0 : current);
1240
1269
  });
1241
1270
  };
1242
- const saveCapability = (route, current, next) => {
1271
+ /** Both halves of one pinned capability route, written together in one mutation. */
1272
+ const pairOps = (next) => [{
1273
+ field: "provider",
1274
+ value: next.provider
1275
+ }, {
1276
+ field: "model",
1277
+ value: next.model
1278
+ }];
1279
+ const saveCapability = (route, next) => {
1243
1280
  if (next === void 0) return;
1244
- run(route, async () => {
1245
- if (current.value?.provider !== next.provider) await props.setCapability(route, "provider", next.provider);
1246
- if (current.value?.model !== next.model) await props.setCapability(route, "model", next.model);
1247
- });
1281
+ const provider = (next.provider ?? "").trim();
1282
+ const model = (next.model ?? "").trim();
1283
+ if (provider === "" || model === "") return;
1284
+ run(route, () => props.saveCapability(route, [{
1285
+ field: "provider",
1286
+ value: provider
1287
+ }, {
1288
+ field: "model",
1289
+ value: model
1290
+ }]));
1248
1291
  };
1249
1292
  const saveAutoReview = () => {
1250
- if (autoReviewDraft === void 0) return;
1251
- run("autoReview", async () => {
1252
- if (autoReview.value?.provider !== autoReviewDraft.provider) await props.setCapability("autoReview", "provider", autoReviewDraft.provider);
1253
- if (autoReview.value?.model !== autoReviewDraft.model) await props.setCapability("autoReview", "model", autoReviewDraft.model);
1254
- });
1293
+ if (autoReviewDraft === void 0 || (autoReviewDraft.provider ?? "").trim() === "" || (autoReviewDraft.model ?? "").trim() === "") return;
1294
+ run("autoReview", () => props.saveCapability("autoReview", pairOps(autoReviewDraft)));
1255
1295
  };
1256
1296
  const saveCompaction = () => {
1257
- if (compactionDraft === void 0) return;
1258
- run("compaction", async () => {
1259
- if (compaction.value?.provider !== compactionDraft.provider) await props.setCapability("compaction", "provider", compactionDraft.provider);
1260
- if (compaction.value?.model !== compactionDraft.model) await props.setCapability("compaction", "model", compactionDraft.model);
1261
- });
1297
+ if (compactionDraft === void 0 || (compactionDraft.provider ?? "").trim() === "" || (compactionDraft.model ?? "").trim() === "") return;
1298
+ run("compaction", () => props.saveCapability("compaction", pairOps(compactionDraft)));
1299
+ };
1300
+ const saveTitle = () => {
1301
+ if (titleDraft === void 0 || (titleDraft.provider ?? "").trim() === "" || (titleDraft.model ?? "").trim() === "") return;
1302
+ run("sessionTitle", () => props.saveCapability("title", pairOps(titleDraft)));
1262
1303
  };
1263
1304
  /**
1264
1305
  * The reviewer switch: Off unsets both stored fields (follow the session
@@ -1270,32 +1311,24 @@ window.__ModuleLoader__.load({
1270
1311
  if (busy === "autoReview" || !autoReview.writable || autoReview.value === void 0) return;
1271
1312
  if (!enabled) {
1272
1313
  setOpen((current) => current === "autoReview" ? void 0 : current);
1273
- run("autoReview", async () => {
1274
- if (autoReview.value?.provider !== void 0) await props.setCapability("autoReview", "provider", void 0);
1275
- if (autoReview.value?.model !== void 0) await props.setCapability("autoReview", "model", void 0);
1276
- });
1314
+ run("autoReview", () => props.saveCapability("autoReview", [{
1315
+ field: "provider",
1316
+ value: void 0
1317
+ }, {
1318
+ field: "model",
1319
+ value: void 0
1320
+ }]));
1277
1321
  return;
1278
1322
  }
1279
1323
  const seed = firstCatalogRoute(groups);
1280
1324
  if (seed === void 0) return;
1281
- setBusy("autoReview");
1282
- setMessage(void 0);
1283
- (async () => {
1284
- if (autoReview.value?.provider !== seed.provider) await props.setCapability("autoReview", "provider", seed.provider);
1285
- if (autoReview.value?.model !== seed.model) await props.setCapability("autoReview", "model", seed.model);
1286
- })().then(() => {
1325
+ run("autoReview", () => props.saveCapability("autoReview", pairOps(seed))).then((accepted) => {
1326
+ if (!accepted) return;
1287
1327
  setAutoReviewDraft({
1288
1328
  provider: seed.provider,
1289
1329
  model: seed.model
1290
1330
  });
1291
- setBusy(void 0);
1292
1331
  setOpen("autoReview");
1293
- }, (error) => {
1294
- setBusy(void 0);
1295
- setMessage({
1296
- route: "autoReview",
1297
- text: error instanceof Error ? error.message : props.t("requestFailed")
1298
- });
1299
1332
  });
1300
1333
  };
1301
1334
  /**
@@ -1308,32 +1341,54 @@ window.__ModuleLoader__.load({
1308
1341
  if (busy === "compaction" || !compaction.writable || compaction.value === void 0) return;
1309
1342
  if (!enabled) {
1310
1343
  setOpen((current) => current === "compaction" ? void 0 : current);
1311
- run("compaction", async () => {
1312
- if (compaction.value?.provider !== void 0) await props.setCapability("compaction", "provider", void 0);
1313
- if (compaction.value?.model !== void 0) await props.setCapability("compaction", "model", void 0);
1314
- });
1344
+ run("compaction", () => props.saveCapability("compaction", [{
1345
+ field: "provider",
1346
+ value: void 0
1347
+ }, {
1348
+ field: "model",
1349
+ value: void 0
1350
+ }]));
1315
1351
  return;
1316
1352
  }
1317
1353
  const seed = firstCatalogRoute(groups);
1318
1354
  if (seed === void 0) return;
1319
- setBusy("compaction");
1320
- setMessage(void 0);
1321
- (async () => {
1322
- if (compaction.value?.provider !== seed.provider) await props.setCapability("compaction", "provider", seed.provider);
1323
- if (compaction.value?.model !== seed.model) await props.setCapability("compaction", "model", seed.model);
1324
- })().then(() => {
1355
+ run("compaction", () => props.saveCapability("compaction", pairOps(seed))).then((accepted) => {
1356
+ if (!accepted) return;
1325
1357
  setCompactionDraft({
1326
1358
  provider: seed.provider,
1327
1359
  model: seed.model
1328
1360
  });
1329
- setBusy(void 0);
1330
1361
  setOpen("compaction");
1331
- }, (error) => {
1332
- setBusy(void 0);
1333
- setMessage({
1334
- route: "compaction",
1335
- text: error instanceof Error ? error.message : props.t("requestFailed")
1362
+ });
1363
+ };
1364
+ /**
1365
+ * The session-title switch: Off unsets both stored fields (naming follows
1366
+ * the session's own logged route); On pins the route to the catalog head so
1367
+ * the pin takes effect immediately and stays editable below. A failed write
1368
+ * reports, never flips silently.
1369
+ */
1370
+ const persistTitleEnabled = (enabled) => {
1371
+ if (busy === "sessionTitle" || !sessionTitle.writable || sessionTitle.value === void 0) return;
1372
+ if (!enabled) {
1373
+ setOpen((current) => current === "sessionTitle" ? void 0 : current);
1374
+ run("sessionTitle", () => props.saveCapability("title", [{
1375
+ field: "provider",
1376
+ value: void 0
1377
+ }, {
1378
+ field: "model",
1379
+ value: void 0
1380
+ }]));
1381
+ return;
1382
+ }
1383
+ const seed = firstCatalogRoute(groups);
1384
+ if (seed === void 0) return;
1385
+ run("sessionTitle", () => props.saveCapability("title", pairOps(seed))).then((accepted) => {
1386
+ if (!accepted) return;
1387
+ setTitleDraft({
1388
+ provider: seed.provider,
1389
+ model: seed.model
1336
1390
  });
1391
+ setOpen("sessionTitle");
1337
1392
  });
1338
1393
  };
1339
1394
  const subagentRole = subagentDraft?.mode === "fixed" && subagentDraft.provider !== void 0 && subagentDraft.provider !== "" ? props.providerRoleOf?.(subagentDraft.provider) : void 0;
@@ -1343,17 +1398,16 @@ window.__ModuleLoader__.load({
1343
1398
  const capabilityDisabled = (route, snapshot, next) => snapshot.status !== "ready" || !snapshot.writable || next === void 0 || busy === route || (next.provider ?? "").trim() === "" || (next.model ?? "").trim() === "";
1344
1399
  const autoReviewDisabled = autoReview.status !== "ready" || !autoReview.writable || autoReviewDraft === void 0 || busy === "autoReview" || !autoReviewOn || (autoReviewDraft.provider ?? "").trim() === "" || (autoReviewDraft.model ?? "").trim() === "";
1345
1400
  const compactionDisabled = compaction.status !== "ready" || !compaction.writable || compactionDraft === void 0 || busy === "compaction" || !compactionOn || (compactionDraft.provider ?? "").trim() === "" || (compactionDraft.model ?? "").trim() === "";
1401
+ const titleDisabled = sessionTitle.status !== "ready" || !sessionTitle.writable || titleDraft === void 0 || busy === "sessionTitle" || !titleOn || (titleDraft.provider ?? "").trim() === "" || (titleDraft.model ?? "").trim() === "";
1346
1402
  const configuredGroupCount = Object.keys(providerGroups).length;
1347
1403
  const groupControlsDisabled = busy === "providerGroups" || switchSettings.status !== "ready" || !switchSettings.writable;
1348
1404
  const groupProviderChoices = deriveRouteChoices(groups, groupProvider === "" ? void 0 : { provider: groupProvider }).providers.map((choice) => groupOf(providerGroups, choice.id) === void 0 ? choice : {
1349
1405
  ...choice,
1350
1406
  name: choice.name + " ✓"
1351
1407
  });
1352
- const groupCompaction = groupDraft === void 0 ? void 0 : draftPair(groupDraft.compactProvider, groupDraft.compactModel);
1353
- const groupReview = groupDraft === void 0 ? void 0 : draftPair(groupDraft.autoReviewProvider, groupDraft.autoReviewModel);
1408
+ const groupTitle = groupDraft === void 0 ? void 0 : draftPair(groupDraft.titleProvider, groupDraft.titleModel);
1354
1409
  const groupSubagent = groupDraft === void 0 ? void 0 : draftPair(groupDraft.subagentProvider, groupDraft.subagentModel);
1355
- const groupCompactionChoices = deriveRouteChoices(groups, groupCompaction);
1356
- const groupReviewChoices = deriveRouteChoices(groups, groupReview);
1410
+ const groupTitleChoices = deriveRouteChoices(groups, groupTitle);
1357
1411
  const groupSubagentChoices = deriveRouteChoices(groups, groupSubagent);
1358
1412
  const groupSubMatched = matchCatalogModel(groups.find((group) => group.id === (groupDraft?.subagentProvider ?? ""))?.models ?? [], groupDraft?.subagentModel ?? "");
1359
1413
  const groupSubEfforts = (() => {
@@ -1372,30 +1426,25 @@ window.__ModuleLoader__.load({
1372
1426
  * Enable a capability row by pinning it: to that capability's live route
1373
1427
  * when one is configured, else to the catalog head (same sources the
1374
1428
  * editor's first-edit seed uses).
1429
+ *
1430
+ * Only the two capabilities with a public per-session seam are editable here
1431
+ * (see src/session-route.ts): compaction records the summarizer that wrote
1432
+ * each summary durably and auto review carries no session identity at its
1433
+ * interception point, so both stay deployment-wide cards.
1375
1434
  */
1376
1435
  const enableGroupCapability = (kind) => {
1377
1436
  const active = seedGroupFromActive();
1378
1437
  const seed = firstCatalogRoute(groups);
1379
1438
  setGroupDraft((draft) => {
1380
1439
  if (draft === void 0) return draft;
1381
- if (kind === "compaction") {
1382
- const provider = active.compactProvider ?? seed?.provider;
1383
- const model = active.compactModel ?? seed?.model;
1440
+ if (kind === "sessionTitle") {
1441
+ const provider = active.titleProvider ?? seed?.provider;
1442
+ const model = active.titleModel ?? seed?.model;
1384
1443
  if (provider === void 0 || model === void 0) return draft;
1385
1444
  return {
1386
1445
  ...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
1446
+ titleProvider: provider,
1447
+ titleModel: model
1399
1448
  };
1400
1449
  }
1401
1450
  const provider = active.subagentProvider ?? seed?.provider;
@@ -1413,15 +1462,10 @@ window.__ModuleLoader__.load({
1413
1462
  const disableGroupCapability = (kind) => {
1414
1463
  setGroupDraft((draft) => {
1415
1464
  if (draft === void 0) return draft;
1416
- if (kind === "compaction") return {
1417
- ...draft,
1418
- compactProvider: "",
1419
- compactModel: ""
1420
- };
1421
- if (kind === "autoReview") return {
1465
+ if (kind === "sessionTitle") return {
1422
1466
  ...draft,
1423
- autoReviewProvider: "",
1424
- autoReviewModel: ""
1467
+ titleProvider: "",
1468
+ titleModel: ""
1425
1469
  };
1426
1470
  return {
1427
1471
  ...draft,
@@ -1674,7 +1718,7 @@ window.__ModuleLoader__.load({
1674
1718
  setMessage(void 0);
1675
1719
  },
1676
1720
  onSave: () => {
1677
- saveCapability("search", search, searchDraft);
1721
+ saveCapability("search", searchDraft);
1678
1722
  }
1679
1723
  })
1680
1724
  ] })
@@ -1741,7 +1785,7 @@ window.__ModuleLoader__.load({
1741
1785
  setMessage(void 0);
1742
1786
  },
1743
1787
  onSave: () => {
1744
- saveCapability("image", image, imageDraft);
1788
+ saveCapability("image", imageDraft);
1745
1789
  }
1746
1790
  })
1747
1791
  ] })
@@ -1882,6 +1926,70 @@ window.__ModuleLoader__.load({
1882
1926
  onSave: saveCompaction
1883
1927
  })
1884
1928
  ] })
1929
+ }),
1930
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RouteCard, {
1931
+ title: props.t("sessionTitle"),
1932
+ summary: titleDraft === void 0 ? props.t("loading") : titleOn ? routeName(groups, titleDraft) : props.t("titleFollow"),
1933
+ hint: props.t("titleHelp"),
1934
+ icon: "sessionTitle",
1935
+ open: open === "sessionTitle" && titleOn,
1936
+ onToggle: () => {
1937
+ if (titleOn) toggle("sessionTitle");
1938
+ },
1939
+ expandable: titleOn,
1940
+ trailing: titleDraft === void 0 ? void 0 : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstantSwitch, {
1941
+ checked: titleOn,
1942
+ disabled: busy === "sessionTitle" || !sessionTitle.writable,
1943
+ label: props.t("sessionTitle"),
1944
+ onChange: persistTitleEnabled
1945
+ }),
1946
+ children: titleDraft === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1947
+ className: ModelSwitchSettings_module_css_default.hint,
1948
+ children: props.t("loading")
1949
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1950
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1951
+ className: ModelSwitchSettings_module_css_default.hint,
1952
+ children: props.t("titleRouteHelp")
1953
+ }),
1954
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1955
+ className: ModelSwitchSettings_module_css_default.formGrid,
1956
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
1957
+ label: props.t("provider"),
1958
+ value: titleDraft.provider ?? "",
1959
+ disabled: busy === "sessionTitle" || !sessionTitle.writable,
1960
+ choices: titleChoices.providers,
1961
+ onChange: (provider) => {
1962
+ const first = groups.find((group) => group.id === provider)?.models[0];
1963
+ setTitleDraft({
1964
+ provider,
1965
+ ...first === void 0 ? {} : { model: first.id }
1966
+ });
1967
+ }
1968
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
1969
+ label: props.t("model"),
1970
+ value: titleDraft.model ?? "",
1971
+ disabled: busy === "sessionTitle" || !sessionTitle.writable,
1972
+ choices: titleChoices.models,
1973
+ onChange: (model) => {
1974
+ setTitleDraft({
1975
+ ...titleDraft,
1976
+ model
1977
+ });
1978
+ }
1979
+ })]
1980
+ }),
1981
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Actions, {
1982
+ t: props.t,
1983
+ busy: busy === "sessionTitle",
1984
+ disabled: titleDisabled,
1985
+ ...message?.route === "sessionTitle" ? { message: message.text } : {},
1986
+ onCancel: () => {
1987
+ resetTitle();
1988
+ setMessage(void 0);
1989
+ },
1990
+ onSave: saveTitle
1991
+ })
1992
+ ] })
1885
1993
  })
1886
1994
  ]
1887
1995
  }),
@@ -1925,94 +2033,45 @@ window.__ModuleLoader__.load({
1925
2033
  className: ModelSwitchSettings_module_css_default.rowCopy,
1926
2034
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1927
2035
  className: ModelSwitchSettings_module_css_default.rowTitle,
1928
- children: props.t("compaction")
2036
+ children: props.t("sessionTitle")
1929
2037
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1930
2038
  className: ModelSwitchSettings_module_css_default.hint,
1931
- children: groupCompaction === void 0 ? props.t("compactionFollow") : routeName(groups, groupCompaction)
2039
+ children: groupTitle === void 0 ? props.t("titleFollow") : routeName(groups, groupTitle)
1932
2040
  })]
1933
2041
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstantSwitch, {
1934
- checked: groupCompaction !== void 0,
2042
+ checked: groupTitle !== void 0,
1935
2043
  disabled: groupControlsDisabled,
1936
- label: props.t("compaction"),
2044
+ label: props.t("sessionTitle"),
1937
2045
  onChange: (value) => {
1938
- if (value) enableGroupCapability("compaction");
1939
- else disableGroupCapability("compaction");
2046
+ if (value) enableGroupCapability("sessionTitle");
2047
+ else disableGroupCapability("sessionTitle");
1940
2048
  }
1941
2049
  })]
1942
2050
  }),
1943
- groupCompaction === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2051
+ groupTitle === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1944
2052
  className: ModelSwitchSettings_module_css_default.formGrid,
1945
2053
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
1946
2054
  label: props.t("provider"),
1947
- value: groupCompaction.provider,
2055
+ value: groupTitle.provider,
1948
2056
  disabled: groupControlsDisabled,
1949
- choices: groupCompactionChoices.providers,
2057
+ choices: groupTitleChoices.providers,
1950
2058
  onChange: (provider) => {
1951
2059
  const first = groups.find((item) => item.id === provider)?.models[0];
1952
2060
  setGroupDraft((draft) => draft === void 0 ? draft : {
1953
2061
  ...draft,
1954
- compactProvider: provider,
1955
- compactModel: first?.id ?? ""
2062
+ titleProvider: provider,
2063
+ titleModel: first?.id ?? ""
1956
2064
  });
1957
2065
  }
1958
2066
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
1959
2067
  label: props.t("model"),
1960
- value: groupCompaction.model,
2068
+ value: groupTitle.model,
1961
2069
  disabled: groupControlsDisabled,
1962
- choices: groupCompactionChoices.models,
2070
+ choices: groupTitleChoices.models,
1963
2071
  onChange: (model) => {
1964
2072
  setGroupDraft((draft) => draft === void 0 ? draft : {
1965
2073
  ...draft,
1966
- compactModel: model
1967
- });
1968
- }
1969
- })]
1970
- }),
1971
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1972
- className: ModelSwitchSettings_module_css_default.rowCard,
1973
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1974
- className: ModelSwitchSettings_module_css_default.rowCopy,
1975
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1976
- className: ModelSwitchSettings_module_css_default.rowTitle,
1977
- children: props.t("autoReview")
1978
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1979
- className: ModelSwitchSettings_module_css_default.hint,
1980
- children: groupReview === void 0 ? props.t("autoReviewFollow") : routeName(groups, groupReview)
1981
- })]
1982
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstantSwitch, {
1983
- checked: groupReview !== void 0,
1984
- disabled: groupControlsDisabled,
1985
- label: props.t("autoReview"),
1986
- onChange: (value) => {
1987
- if (value) enableGroupCapability("autoReview");
1988
- else disableGroupCapability("autoReview");
1989
- }
1990
- })]
1991
- }),
1992
- groupReview === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1993
- className: ModelSwitchSettings_module_css_default.formGrid,
1994
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
1995
- label: props.t("provider"),
1996
- value: groupReview.provider,
1997
- disabled: groupControlsDisabled,
1998
- choices: groupReviewChoices.providers,
1999
- onChange: (provider) => {
2000
- const first = groups.find((item) => item.id === provider)?.models[0];
2001
- setGroupDraft((draft) => draft === void 0 ? draft : {
2002
- ...draft,
2003
- autoReviewProvider: provider,
2004
- autoReviewModel: first?.id ?? ""
2005
- });
2006
- }
2007
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
2008
- label: props.t("model"),
2009
- value: groupReview.model,
2010
- disabled: groupControlsDisabled,
2011
- choices: groupReviewChoices.models,
2012
- onChange: (model) => {
2013
- setGroupDraft((draft) => draft === void 0 ? draft : {
2014
- ...draft,
2015
- autoReviewModel: model
2074
+ titleModel: model
2016
2075
  });
2017
2076
  }
2018
2077
  })]
@@ -2154,13 +2213,18 @@ window.__ModuleLoader__.load({
2154
2213
  compactionFollow: "跟随会话模型",
2155
2214
  compactionHelp: "为官方压缩(/compact 与官方自动压缩)指定摘要模型;关闭(两个字段都留空)时使用官方默认,即当前会话的目标模型。",
2156
2215
  compactionRouteHelp: "摘要请求固定使用这里的 Provider/模型,与会话目标模型无关;保存后对下一次压缩立即生效。保存时若恰有压缩进行中,可能被打断并在下次重试。",
2216
+ sessionTitle: "会话命名模型",
2217
+ titleFollow: "跟随会话模型",
2218
+ titleHelp: "为官方自动命名(新会话首条消息后生成标题)指定模型;关闭(两个字段都留空)时命名跟随当前会话模型。",
2219
+ titleRouteHelp: "命名请求固定使用这里的 Provider/模型(不带推理强度);关闭时跟随会话模型。保存后立即对下一次命名生效,不会中断正在进行的命名,会话日志记录的也是实际使用的模型。",
2157
2220
  providerGroups: "供应商配置组",
2158
2221
  providerGroupsCard: "按供应商预设路由",
2159
- providerGroupsSummary: "会话模型切换到该供应商时自动应用对应组",
2160
- providerGroupsHelp: "为压缩、子代理、审查按供应商分别保存路由。",
2161
- providerGroupsBodyHelp: "每个供应商保存一组压缩、子代理、审查路由;本会话的模型切换到该供应商时(无论通过插件的模型选择器还是 TUI 等其他界面),这三条路由自动写入上方对应卡片。切换到未配置的供应商时现有路由保持不变;应用会覆盖三张卡片的当前值(含手动修改)。",
2222
+ providerGroupsSummary: "使用该供应商的会话自动采用对应组",
2223
+ providerGroupsHelp: "为会话命名与子代理按供应商分别保存路由。",
2224
+ providerGroupsBodyHelp: "每个供应商保存一组会话命名与子代理路由。会话自身的供应商与该组一致时,该会话的命名与子代理按组解析,否则回落到上方卡片里的全局设置。切换某个会话的模型不会写入任何全局设置,因此不同供应商的多个会话互不影响。压缩与自动审查仍是全局卡片:压缩事件要如实记录实际写入摘要的模型,审查的拦截点没有会话身份。",
2162
2225
  providerGroupPick: "选择供应商以编辑其配置组。",
2163
2226
  providerGroupDelete: "删除该组",
2227
+ saveTimeout: "保存超时,请重试",
2164
2228
  "reason.central-subagent-routing": "Alpha.4 没有全局子代理启动路由接口。",
2165
2229
  "reason.packaged-preset-roots": "Alpha.4 不支持插件提供额外 preset root。",
2166
2230
  "reason.tool-owner-suppression": "Alpha.4 没有工具所有者或来源抑制接口。",
@@ -2209,13 +2273,18 @@ window.__ModuleLoader__.load({
2209
2273
  compactionFollow: "Follows the conversation model",
2210
2274
  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.",
2211
2275
  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.",
2276
+ sessionTitle: "Session title model",
2277
+ titleFollow: "Follows the session model",
2278
+ titleHelp: "Pick the model used to name new sessions automatically (right after the first prompt). When off (both fields empty), naming follows the session model.",
2279
+ titleRouteHelp: "Naming requests use exactly this provider/model (no reasoning effort); when off they follow the session model. A saved change applies to the very next title, never interrupts a running one, and the session log records the route actually used.",
2212
2280
  providerGroups: "Provider groups",
2213
2281
  providerGroupsCard: "Per-provider route presets",
2214
- providerGroupsSummary: "Applied automatically when this session switches to that provider",
2215
- providerGroupsHelp: "Save compaction, subagent, and review routes per provider.",
2216
- providerGroupsBodyHelp: "Each provider stores one compaction, subagent, and review route group. When this session switches to that provider — through the plugin picker or any other surface such as the TUI — these three routes are written into the cards above. Switching to a provider without a group leaves the current routes untouched. Applying a group overwrites those three cards, including manual edits.",
2282
+ providerGroupsSummary: "Sessions using that provider adopt this group",
2283
+ providerGroupsHelp: "Save session-naming and subagent routes per provider.",
2284
+ providerGroupsBodyHelp: "Each provider stores one session-naming and subagent route group. A session running on that provider resolves its naming and subagent calls from the group, and falls back to the cards above otherwise. Switching a session’s model writes no global setting, so sessions on different providers never affect each other. Compaction and auto review stay global cards: the compaction event must record the model that actually wrote each summary, and the reviewer gate carries no session identity.",
2217
2285
  providerGroupPick: "Pick a provider to edit its group.",
2218
2286
  providerGroupDelete: "Delete group",
2287
+ saveTimeout: "The save timed out; try again",
2219
2288
  "reason.central-subagent-routing": "Alpha.4 exposes no global Subagent start-routing seam.",
2220
2289
  "reason.packaged-preset-roots": "Alpha.4 exposes no plugin-owned preset root.",
2221
2290
  "reason.tool-owner-suppression": "Alpha.4 exposes no tool owner or provenance suppression seam.",
@@ -3704,66 +3773,6 @@ window.__ModuleLoader__.load({
3704
3773
  }, operations);
3705
3774
  }
3706
3775
  //#endregion
3707
- //#region src/client/picker/provider-group-watch.ts
3708
- /**
3709
- * Watch one directory store and apply groups on provider changes.
3710
- *
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.
3719
- */
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;
3730
- }
3731
- let running = false;
3732
- let pending;
3733
- const run = async (provider) => {
3734
- running = true;
3735
- try {
3736
- await options.apply(provider);
3737
- } catch (error) {
3738
- onFailure(error);
3739
- } finally {
3740
- running = false;
3741
- if (pending !== void 0) {
3742
- const next = pending;
3743
- pending = void 0;
3744
- run(next);
3745
- }
3746
- }
3747
- };
3748
- const check = () => {
3749
- const current = options.source.getSnapshot().current;
3750
- if (current === null) return;
3751
- if (!baselined) {
3752
- baselined = true;
3753
- lastSeen = current.provider;
3754
- return;
3755
- }
3756
- if (current.provider === lastSeen) return;
3757
- lastSeen = current.provider;
3758
- if (running) {
3759
- pending = current.provider;
3760
- return;
3761
- }
3762
- run(current.provider);
3763
- };
3764
- return { dispose: options.source.subscribe(check) };
3765
- }
3766
- //#endregion
3767
3776
  //#region src/picker/pending-dismissal.ts
3768
3777
  /** Raised when the running Host exposes neither withdrawal verb. */
3769
3778
  var PlanDismissalUnsupportedError = class extends Error {
@@ -4557,23 +4566,6 @@ window.__ModuleLoader__.load({
4557
4566
  ], (scope) => {
4558
4567
  const models = scope.modelDirectories;
4559
4568
  const sessions = scope.sessions;
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
- });
4577
4569
  let directoryService;
4578
4570
  const orderStore = providerOrderStore(scope.configForms.get(PROVIDERS_CONFIG_ID));
4579
4571
  scope.inject(["providerDirectory"], (directoryScope) => {
@@ -4604,7 +4596,6 @@ window.__ModuleLoader__.load({
4604
4596
  };
4605
4597
  const roleOf = (key) => readProviderRole(directoryService, key);
4606
4598
  const providerLockStore = createProviderLockStore(readLock);
4607
- if (available) watchGroupsFor(sessionId, directory.store);
4608
4599
  return {
4609
4600
  available,
4610
4601
  roleOf,
@@ -4743,6 +4734,7 @@ window.__ModuleLoader__.load({
4743
4734
  const image = deriveConfigForm(owned, deriveImageSettings, IMAGE_SETTINGS_FIELDS);
4744
4735
  const autoReview = deriveConfigForm(owned, deriveAutoReviewSettings, AUTO_REVIEW_SETTINGS_FIELDS);
4745
4736
  const compaction = deriveConfigForm(owned, deriveCompactionSettings, COMPACTION_SETTINGS_FIELDS);
4737
+ const sessionTitle = deriveConfigForm(owned, deriveTitleSettings, TITLE_SETTINGS_FIELDS);
4746
4738
  const connectionRpc = (() => {
4747
4739
  try {
4748
4740
  const rpc = ctx.get("connection", false)?.rpc;
@@ -4780,13 +4772,13 @@ window.__ModuleLoader__.load({
4780
4772
  imageSettings: image,
4781
4773
  autoReviewSettings: autoReview,
4782
4774
  compactionSettings: compaction,
4775
+ titleSettings: sessionTitle,
4783
4776
  switchSettings: owned
4784
4777
  },
4785
4778
  capabilities: RUNTIME_CAPABILITIES,
4786
- setSubagent: (field, value) => value === void 0 ? subagent.unset(field) : subagent.set(field, value),
4787
- setCapability: (route, field, value) => {
4788
- const scope = route === "search" ? search : route === "image" ? image : route === "compaction" ? compaction : autoReview;
4789
- return value === void 0 ? scope.unset(field) : scope.set(field, value);
4779
+ saveSubagent: (ops) => subagent.mutate(ops),
4780
+ saveCapability: (route, ops) => {
4781
+ return (route === "search" ? search : route === "image" ? image : route === "compaction" ? compaction : route === "title" ? sessionTitle : autoReview).mutate(ops);
4790
4782
  },
4791
4783
  setCompactOnSwitch: async (value) => {
4792
4784
  if (!await owned.set("compactOnSwitch", value)) throw new Error("settings-rejected");