@klarkxy/dsh-model-hub 0.1.0-rc.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -124,6 +124,21 @@ function modelMenuEffortOptions(choice, current) {
124
124
  }
125
125
  //#endregion
126
126
  //#region ../dsh-model-route/src/menu.ts
127
+ /** Localize standard effort ids only when the provider has not supplied a display name. */
128
+ function modelMenuEffortLabel(effort, locale) {
129
+ if (effort.name !== effort.id) return effort.name;
130
+ return {
131
+ off: ["关闭", "Off"],
132
+ none: ["无", "None"],
133
+ minimal: ["最低", "Minimal"],
134
+ low: ["低", "Low"],
135
+ medium: ["中", "Medium"],
136
+ high: ["高", "High"],
137
+ xhigh: ["极高", "Extra high"],
138
+ max: ["最高", "Maximum"],
139
+ ultra: ["超高", "Ultra"]
140
+ }[effort.id]?.[locale?.toLowerCase().startsWith("zh") ? 0 : 1] ?? effort.name;
141
+ }
127
142
  /** The short model label shown inside a provider group. */
128
143
  function modelMenuShortName(choice) {
129
144
  const sep = choice.label.indexOf(" / ");
@@ -174,7 +189,23 @@ const MEASURE_STYLE = {
174
189
  top: 0
175
190
  };
176
191
  function ModelMenu(props) {
177
- const { open, onOpenChange, anchor, choices, selected, onPick, leading = [], onPickLeading, efforts = [], selectedEffort, onPickEffort, modelLabel = "Model", modelValue = "—", effortLabel = "Reasoning", effortValue, defaultEffortLabel = "Default", searchPlaceholder = "Search models…", emptyLabel = "No matching models", clearSearchLabel = "Clear search", className, listClassName } = props;
192
+ const language = props.locale?.toLowerCase().startsWith("zh") ? "zh" : "en";
193
+ const copy = language === "zh" ? {
194
+ model: "模型",
195
+ effort: "思考强度",
196
+ default: "默认",
197
+ search: "搜索模型…",
198
+ empty: "没有匹配的模型",
199
+ clear: "清除搜索"
200
+ } : {
201
+ model: "Model",
202
+ effort: "Reasoning",
203
+ default: "Default",
204
+ search: "Search models…",
205
+ empty: "No matching models",
206
+ clear: "Clear search"
207
+ };
208
+ const { open, onOpenChange, anchor, choices, selected, onPick, leading = [], onPickLeading, efforts = [], selectedEffort, onPickEffort, modelLabel = copy.model, modelValue = "—", effortLabel = copy.effort, effortValue, defaultEffortLabel = copy.default, searchPlaceholder = copy.search, emptyLabel = copy.empty, clearSearchLabel = copy.clear, className, listClassName } = props;
178
209
  const rootRef = (0, react.useRef)(null);
179
210
  const openRef = (0, react.useRef)(open);
180
211
  openRef.current = open;
@@ -195,7 +226,14 @@ function ModelMenu(props) {
195
226
  const effortRows = (0, react.useMemo)(() => [{
196
227
  id: "",
197
228
  name: defaultEffortLabel
198
- }, ...efforts], [defaultEffortLabel, efforts]);
229
+ }, ...efforts.map((effort) => ({
230
+ ...effort,
231
+ name: modelMenuEffortLabel(effort, language)
232
+ }))], [
233
+ defaultEffortLabel,
234
+ efforts,
235
+ language
236
+ ]);
199
237
  const filteredGroups = (0, react.useMemo)(() => groupModelMenuChoices(choices, "").map((group) => ({
200
238
  ...group,
201
239
  items: (0, _deepseek_ai_dsh_client_ui_primitives.rankByName)(group.items.map((choice) => ({
@@ -447,6 +485,7 @@ function ModelMenu(props) {
447
485
  style: menuPos ?? MEASURE_STYLE,
448
486
  role: pane === "model" ? "group" : "menu",
449
487
  "aria-label": modelLabel,
488
+ lang: language,
450
489
  children: [
451
490
  pane === "root" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
452
491
  ref: itemRef(),
@@ -1068,7 +1107,7 @@ ${scope} {
1068
1107
  border: 0.5px solid var(--dsw-alias-border-l3);
1069
1108
  border-radius: var(--dsw-radius-md);
1070
1109
  background: var(--dsw-alias-bg-layer-1);
1071
- font-family: var(--ds-font-family-code, ui-monospace, 'Cascadia Mono', Consolas, monospace);
1110
+ font-family: var(--ds-font-family-code);
1072
1111
  font-size: 12px;
1073
1112
  line-height: 20px;
1074
1113
  color: var(--dsw-alias-label-secondary);
@@ -1144,80 +1183,173 @@ ${scope} {
1144
1183
  `.replace(/^& (\.dsh-ui-[a-z0-9-]+)(?![+>~])/gm, (_match, className) => `&${className}, & ${className}`);
1145
1184
  }
1146
1185
  //#endregion
1147
- //#region src/client.tsx
1186
+ //#region ../dsh-plugin-kit/src/contracts.ts
1148
1187
  /**
1149
- * dsh-model-hub — browser half. One settings-page section listing every
1150
- * detected model-route field across plugins. Each row is edited with the
1151
- * contract's standard ModelMenu (dsh-model-route/ui) and written back through
1152
- * the hub's host RPC — `settings.mutate` on the host, CAS by descriptor
1153
- * revision. An empty route means "follow the host default" and unsets the
1154
- * field.
1188
+ * Remote contribution for model-settings change notifications.
1189
+ * Provided by `@klarkxy/dsh-model-hub` (`ModelSettingsChangesController`);
1190
+ * subscribers degrade silently when that package is not installed.
1191
+ * The stream carries invalidations only — `ready` is never a settings snapshot.
1155
1192
  */
1156
- const name = "dsh-model-hub-client";
1157
- const inject = [
1158
- "slots",
1159
- "connection",
1160
- "remote",
1161
- "remote.session",
1162
- "locale"
1163
- ];
1164
- const CHANNEL = "/dsh-model-hub";
1165
- const BUNDLE = "@klarkxy/dsh-model-hub";
1166
- const TEXT = {
1167
- zh: {
1168
- title: "模型枢纽",
1169
- intro: "一页查看并修改各插件的模型路由;改动立即写入对应插件自己的设置。",
1170
- loading: "正在读取…",
1171
- empty: "没有检测到模型字段。插件可用 x-model-route 标记声明模型字段。",
1172
- failed: (message) => `读取失败:${message}`,
1173
- writeFailed: (message) => `写入失败:${message}`,
1174
- refresh: "刷新",
1175
- follow: "跟随宿主默认",
1176
- viaMarker: "契约标记",
1177
- viaShape: "形状识别",
1178
- model: "模型",
1179
- effort: "思考强度",
1180
- effortDefault: "默认",
1181
- searchModels: "搜索模型…",
1182
- noModels: "没有匹配的模型",
1183
- clearSearch: "清除搜索",
1184
- collectionTemplate: "集合中没有可编辑的条目;请先在所属插件中添加条目。"
1185
- },
1186
- en: {
1187
- title: "Model Hub",
1188
- intro: "View and edit every plugin's model route on one page; changes write into the owning plugin's own settings at once.",
1189
- loading: "Loading…",
1190
- empty: "No model fields detected. A plugin can declare one with the x-model-route marker.",
1191
- failed: (message) => `Load failed: ${message}`,
1192
- writeFailed: (message) => `Write failed: ${message}`,
1193
- refresh: "Refresh",
1194
- follow: "Follow host default",
1195
- viaMarker: "contract marker",
1196
- viaShape: "shape match",
1197
- model: "Model",
1198
- effort: "Reasoning",
1199
- effortDefault: "Default",
1200
- searchModels: "Search models…",
1201
- noModels: "No matching models",
1202
- clearSearch: "Clear search",
1203
- collectionTemplate: "No collection item to edit. Add an item in the owning plugin first."
1204
- }
1193
+ const MODEL_SETTINGS_CHANGES_REMOTE = {
1194
+ package: "@klarkxy/dsh-model-hub",
1195
+ descriptors: [{
1196
+ id: "modelSettingsChanges/watch",
1197
+ service: "modelSettingsChangesController",
1198
+ namespace: "modelSettingsChanges",
1199
+ method: "watch",
1200
+ mode: "stream",
1201
+ invocation: { kind: "direct" },
1202
+ parameters: [],
1203
+ cancellation: { parameter: "signal" }
1204
+ }]
1205
1205
  };
1206
- function useText(locale) {
1207
- const active = typeof locale === "string" ? locale : locale?.getSnapshot?.()?.active;
1208
- const table = String(active || "en").toLowerCase().startsWith("zh") ? TEXT.zh : TEXT.en;
1209
- return (0, react.useMemo)(() => (key, ...args) => {
1210
- const value = table[key];
1211
- return typeof value === "function" ? value(...args) : value;
1212
- }, [table]);
1206
+ //#endregion
1207
+ //#region ../dsh-plugin-kit/src/model-settings-client.ts
1208
+ /** This stream carries invalidations only; ready is never a settings snapshot. */
1209
+ function parseModelSettingsChangeFrame(value) {
1210
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid model settings change frame");
1211
+ const frame = value;
1212
+ if (frame.kind === "ready" && Object.keys(frame).length === 1) return { kind: "ready" };
1213
+ if (frame.kind === "changed" && Object.keys(frame).length === 3 && typeof frame.ns === "string" && frame.ns.length > 0 && typeof frame.revision === "number" && Number.isSafeInteger(frame.revision) && frame.revision >= 0) return {
1214
+ kind: "changed",
1215
+ ns: frame.ns,
1216
+ revision: frame.revision
1217
+ };
1218
+ throw new Error("Invalid model settings change frame");
1213
1219
  }
1214
- const css = `${officialUiCss("dsh-model-hub")}${modelMenuCss}`;
1215
- function Style() {
1216
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: css });
1220
+ /** One contribution mounted by Model Hub; consumers only inject its service. */
1221
+ const modelSettingsChangesRemote = {
1222
+ package: MODEL_SETTINGS_CHANGES_REMOTE.package,
1223
+ descriptors: MODEL_SETTINGS_CHANGES_REMOTE.descriptors.map((descriptor) => ({
1224
+ ...descriptor,
1225
+ result: {
1226
+ mode: "strict",
1227
+ typeSymbol: "ModelSettings.ChangeFrame",
1228
+ create: () => ({ parse: parseModelSettingsChangeFrame })
1229
+ }
1230
+ }))
1231
+ };
1232
+ /** Native RemoteStream owns carrier reconnects; no timers or browser transport.
1233
+ * A factory shares one logical stream while at least one subscriber remains. */
1234
+ function createModelSettingsSubscriber(ctx, namespace) {
1235
+ const listeners = /* @__PURE__ */ new Set();
1236
+ let injection;
1237
+ let current;
1238
+ let lifecycle = 0;
1239
+ let failureMessage;
1240
+ const report = (message) => {
1241
+ failureMessage = message;
1242
+ for (const listener of [...listeners]) try {
1243
+ listener.error?.(message);
1244
+ } catch {}
1245
+ };
1246
+ const invalidate = () => {
1247
+ for (const listener of [...listeners]) {
1248
+ if (!listeners.has(listener)) continue;
1249
+ try {
1250
+ listener.change();
1251
+ } catch (error) {
1252
+ try {
1253
+ listener.error?.(error instanceof Error ? error.message : String(error));
1254
+ } catch {}
1255
+ }
1256
+ }
1257
+ };
1258
+ const start = () => {
1259
+ const life = ++lifecycle;
1260
+ injection = ctx.inject(["remote", "remote.modelSettingsChanges"], (injected) => {
1261
+ if (life !== lifecycle || !listeners.size) return;
1262
+ const scope = injected;
1263
+ const stream = scope.remote.$stream({
1264
+ name: "Model settings sync",
1265
+ open: (signal) => scope.remote.modelSettingsChanges.watch(signal),
1266
+ ended: (accepted) => /* @__PURE__ */ new Error(`Model settings sync stream ended ${accepted ? "after" : "before"} ready; refresh or reopen the page`)
1267
+ });
1268
+ const session = {
1269
+ stream,
1270
+ active: true
1271
+ };
1272
+ current = session;
1273
+ scope.effect(() => () => {
1274
+ session.active = false;
1275
+ if (current === session) {
1276
+ current = void 0;
1277
+ if (life === lifecycle && listeners.size) report("Model settings sync service unavailable");
1278
+ }
1279
+ return stream.dispose();
1280
+ });
1281
+ (async () => {
1282
+ let generation;
1283
+ let ready = false;
1284
+ try {
1285
+ for await (const item of stream) {
1286
+ if (!session.active || life !== lifecycle || !listeners.size) break;
1287
+ if (item.signal.aborted) continue;
1288
+ if (generation !== item.generation) {
1289
+ generation = item.generation;
1290
+ ready = false;
1291
+ }
1292
+ const frame = parseModelSettingsChangeFrame(item.value);
1293
+ if (frame.kind === "ready") {
1294
+ ready = true;
1295
+ failureMessage = void 0;
1296
+ item.accept();
1297
+ invalidate();
1298
+ } else {
1299
+ if (!ready) throw new Error("Model settings sync change arrived before ready");
1300
+ if (namespace === "*" || frame.ns === namespace) invalidate();
1301
+ }
1302
+ }
1303
+ } catch (error) {
1304
+ if (session.active && life === lifecycle) report(error instanceof Error ? error.message : String(error));
1305
+ } finally {
1306
+ if (current === session) current = void 0;
1307
+ session.active = false;
1308
+ await stream.dispose().catch(() => {});
1309
+ }
1310
+ })();
1311
+ });
1312
+ queueMicrotask(() => {
1313
+ if (life === lifecycle && listeners.size && !current) report("Model settings sync service unavailable");
1314
+ });
1315
+ };
1316
+ return Object.freeze({ subscribe(onChange, onError) {
1317
+ const listener = {
1318
+ change: onChange,
1319
+ error: onError
1320
+ };
1321
+ listeners.add(listener);
1322
+ if (!injection) start();
1323
+ else if (failureMessage) queueMicrotask(() => {
1324
+ if (listeners.has(listener) && failureMessage) try {
1325
+ listener.error?.(failureMessage);
1326
+ } catch {}
1327
+ });
1328
+ return () => {
1329
+ if (!listeners.delete(listener) || listeners.size) return;
1330
+ lifecycle++;
1331
+ if (current) {
1332
+ current.active = false;
1333
+ current.stream.dispose().catch(() => {});
1334
+ }
1335
+ current = void 0;
1336
+ failureMessage = void 0;
1337
+ const previous = injection;
1338
+ injection = void 0;
1339
+ try {
1340
+ Promise.resolve(previous?.dispose()).catch(() => {});
1341
+ } catch {}
1342
+ };
1343
+ } });
1217
1344
  }
1345
+ //#endregion
1346
+ //#region src/client-model-menu.tsx
1218
1347
  /** One detected field's standard editor: the same ModelMenu every marked field gets. */
1219
- function FieldMenu({ t, catalog, current, disabled, onSet }) {
1348
+ function FieldMenu({ t, catalog, current, disabled, label, allowFollow = true, followLabel, reasoningEffort = true, onSet }) {
1220
1349
  const [open, setOpen] = (0, react.useState)(false);
1350
+ (0, react.useEffect)(() => {
1351
+ if (disabled) setOpen(false);
1352
+ }, [disabled]);
1221
1353
  const route = normalizeModelRoute(current);
1222
1354
  const follow = !route.provider || !route.model;
1223
1355
  const choices = (0, react.useMemo)(() => parseModelMenuChoices(catalog, follow ? void 0 : route), [
@@ -1227,19 +1359,22 @@ function FieldMenu({ t, catalog, current, disabled, onSet }) {
1227
1359
  route.model
1228
1360
  ]);
1229
1361
  const selected = follow ? void 0 : choices.find((choice) => choice.provider === route.provider && choice.model === route.model);
1230
- const efforts = follow ? [] : modelMenuEffortOptions(selected, route.reasoningEffort || "");
1231
- const currentLabel = follow ? t("follow") : selected?.label ?? `${route.provider} / ${route.model}`;
1232
- const effortName = efforts.find((item) => item.id === (route.reasoningEffort || ""))?.name;
1362
+ const efforts = follow || !reasoningEffort ? [] : modelMenuEffortOptions(selected, route.reasoningEffort || "");
1363
+ const currentLabel = follow ? followLabel || t("follow") : selected?.label ?? `${route.provider} / ${route.model}`;
1364
+ const selectedEffort = efforts.find((item) => item.id === (route.reasoningEffort || ""));
1365
+ const effortName = selectedEffort ? modelMenuEffortLabel(selectedEffort, t.language) : void 0;
1233
1366
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
1234
- open,
1235
- onOpenChange: setOpen,
1367
+ locale: t.language,
1368
+ open: open && !disabled,
1369
+ onOpenChange: (next) => setOpen(next && !disabled),
1236
1370
  listClassName: "dsh-model-hub",
1237
1371
  anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1238
1372
  type: "button",
1239
1373
  className: "dsh-model-menu-trigger",
1240
1374
  disabled,
1375
+ "aria-label": `${label}: ${currentLabel}`,
1241
1376
  "aria-haspopup": "menu",
1242
- "aria-expanded": open,
1377
+ "aria-expanded": open && !disabled,
1243
1378
  children: [
1244
1379
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1245
1380
  className: "dsh-model-menu-triggerLabel",
@@ -1261,11 +1396,11 @@ function FieldMenu({ t, catalog, current, disabled, onSet }) {
1261
1396
  provider: picked.provider,
1262
1397
  model: picked.model
1263
1398
  }),
1264
- leading: [{
1399
+ leading: allowFollow ? [{
1265
1400
  id: "follow",
1266
- label: t("follow"),
1401
+ label: followLabel || t("follow"),
1267
1402
  selected: follow
1268
- }],
1403
+ }] : [],
1269
1404
  onPickLeading: () => onSet({
1270
1405
  provider: "",
1271
1406
  model: ""
@@ -1289,129 +1424,991 @@ function FieldMenu({ t, catalog, current, disabled, onSet }) {
1289
1424
  clearSearchLabel: t("clearSearch")
1290
1425
  });
1291
1426
  }
1427
+ function messageOf(error) {
1428
+ if (error instanceof Error) return error.message;
1429
+ if (error && typeof error === "object" && "message" in error) return String(error.message);
1430
+ return String(error);
1431
+ }
1432
+ function checkedCatalog(value) {
1433
+ if (value && typeof value === "object" && "ok" in value && value.ok === false) throw new Error("error" in value ? messageOf(value.error) : "Request failed");
1434
+ return value;
1435
+ }
1436
+ function catalogFailures(value) {
1437
+ if (!value || typeof value !== "object") return [];
1438
+ const root = "ok" in value && value.ok === true && "value" in value ? value.value : value;
1439
+ if (!root || typeof root !== "object" || !("failures" in root) || !Array.isArray(root.failures)) return [];
1440
+ return root.failures.map((failure) => {
1441
+ if (!failure || typeof failure !== "object") return messageOf(failure);
1442
+ const provider = "name" in failure ? failure.name : "id" in failure ? failure.id : "provider" in failure ? failure.provider : "";
1443
+ return provider ? `${provider}: ${messageOf(failure)}` : messageOf(failure);
1444
+ });
1445
+ }
1446
+ //#endregion
1447
+ //#region src/client-copy.ts
1448
+ function hubText(value, language) {
1449
+ return typeof value === "string" ? value : value?.[language] ?? "";
1450
+ }
1451
+ /** Local validation defaults, without rewriting provider diagnostics. */
1452
+ const hubMessages = [
1453
+ ["请求失败", "Request failed"],
1454
+ ["字段引用无效", "Invalid field reference"],
1455
+ ["请先刷新,以获取有效的设置版本", "A valid expected revision is required; refresh and retry"],
1456
+ ["模型路由无效", "Invalid model route"],
1457
+ ["模型设置提供方已变化,请刷新后重试", "Model settings owner changed; refresh and retry"],
1458
+ ["设置已变化,请刷新后重试", "Settings changed; refresh and retry"],
1459
+ ["此模型字段不可编辑", "Not an editable model field"],
1460
+ ["此设置需要选择具体模型", "This model setting requires a concrete model"],
1461
+ ["模型设置提供方已卸载,请刷新后重试", "Model settings owner unloaded; refresh and retry"],
1462
+ ["自测字段仅用于诊断", "Self-test fields are diagnostic only"],
1463
+ ["提供方设置版本无效", "Invalid owner revision"],
1464
+ ["未检测到此模型字段", "Not a detected model field"],
1465
+ ["没有可编辑的集合条目", "No collection item to edit"],
1466
+ ["设置版本无效", "Invalid revision"],
1467
+ ["ChatGPT 需要 Codex 模型", "ChatGPT requires a Codex model"],
1468
+ ["Codex 模型目录不可用。", "Codex model catalog is unavailable."],
1469
+ ["配置已在别处修改,请刷新后重试。", "Settings changed elsewhere. Refresh and try again."],
1470
+ ["设置已更新,请刷新后重试。", "Settings changed. Refresh and try again."],
1471
+ ["正在保存配置,请稍后重试。", "Settings are being saved. Try again shortly."],
1472
+ ["ChatGPT 插件已卸载。", "The ChatGPT plugin has been unloaded."],
1473
+ ["配置格式无效。", "Invalid settings format."],
1474
+ ["请等当前调用完成后保存配置。", "Wait for the current call to finish before saving settings."],
1475
+ ["无法保存标题设置。", "Could not save title settings."],
1476
+ ["标题设置无效。", "Invalid title settings."],
1477
+ ["模型设置同步服务不可用", "Model settings sync service unavailable"],
1478
+ ["模型设置变更消息无效", "Invalid model settings change frame"],
1479
+ ["模型设置同步尚未就绪便收到变更消息", "Model settings sync change arrived before ready"],
1480
+ ["模型设置同步在就绪后中断;请刷新或重新打开页面", "Model settings sync stream ended after ready; refresh or reopen the page"],
1481
+ ["模型设置同步在就绪前中断;请刷新或重新打开页面", "Model settings sync stream ended before ready; refresh or reopen the page"],
1482
+ ["Git 提交模型设置字段不存在", "Unknown Git Commit model setting"],
1483
+ ["Git 提交插件已停用", "dsh-git-commit: plugin disabled"],
1484
+ ["Git 提交设置已变化;请重新加载后重试", "dsh-git-commit: settings changed; reload and retry"],
1485
+ ["安全自动模型设置字段不存在", "Unknown Safe Auto model setting"],
1486
+ ["设置已变化;请重新加载后重试", "Settings changed; reload and retry"],
1487
+ ["请求无效", "Invalid request"],
1488
+ ["此处只能编辑审查设置", "Only reviewer settings may be edited here"],
1489
+ ["配置必须是对象", "config must be an object"],
1490
+ ["启用状态必须是布尔值", "enabled must be a boolean"],
1491
+ ["静态只读开关必须是布尔值", "staticReadonly must be a boolean"],
1492
+ ["审查模型供应商或模型标识无效", "invalid reviewer provider/model identifier"],
1493
+ ["供应商和模型必须一起配置", "provider and model must be configured together"],
1494
+ ["设置思考强度需要先选择审查模型", "reasoningEffort requires a reviewer model"],
1495
+ ["所选模型不支持该思考强度", "The selected model does not support this reasoning effort"],
1496
+ ["启用状态无效", "Invalid enabled state"],
1497
+ ["找不到原生子代理模型选择设置", "native subagent-model-selection-settings not found"],
1498
+ ["路由需要非空的供应商和模型标识", "A route requires non-empty provider and model ids"],
1499
+ ["供应商和模型标识最长 250 字符", "Route provider and model ids are limited to 250 characters"],
1500
+ ["路由思考强度必须是非空字符串", "A route reasoning effort must be a non-empty string"],
1501
+ ["路由思考强度最长 250 字符", "Route reasoning effort is limited to 250 characters"],
1502
+ ["无效的路由下标", "Invalid route index"],
1503
+ ["没有 profile 目录,无法保存任务倾向", "No profile directory; cast tendencies cannot be stored"],
1504
+ ["任务倾向需要非空的供应商和模型标识", "A tendency requires non-empty provider and model ids"],
1505
+ ["任务倾向必须是字符串", "A tendency must be a string"],
1506
+ ["任务倾向最长 500 字符", "A tendency is limited to 500 characters"],
1507
+ ["添加模型后才能启用子代理阵容", "Add a model before enabling subagent cast"],
1508
+ ["任务倾向已变化,请刷新后重试", "Cast tendencies changed; refresh and retry"],
1509
+ ["任务倾向版本已达上限", "Cast tendency revision limit reached"],
1510
+ ["无法读取任务倾向文件;请恢复 cast.json 的访问权限后刷新", "Cast tendency store cannot be read; restore access to cast.json and refresh"],
1511
+ ["任务倾向文件损坏;请修复 cast.json 后刷新", "Cast tendency store is damaged; repair cast.json and refresh"],
1512
+ ["任务倾向文件版本不受支持;请使用兼容的插件再保存", "Cast tendency store version is unsupported; use a compatible plugin before saving"],
1513
+ ["任务倾向文件格式无效;请修复 cast.json 后刷新", "Cast tendency store schema is invalid; repair cast.json and refresh"],
1514
+ ...[
1515
+ "provider",
1516
+ "model",
1517
+ "reasoningEffort",
1518
+ "reviewerPrompt"
1519
+ ].map((key) => [`${key} 必须是字符串`, `${key} must be a string`]),
1520
+ ...[
1521
+ "timeoutMs",
1522
+ "maxInputBytes",
1523
+ "outputTokens",
1524
+ "maxReviewsPerTask",
1525
+ "consecutiveDenials",
1526
+ "humanApprovalTimeoutMs"
1527
+ ].map((key) => [`${key} 无效`, `invalid ${key}`])
1528
+ ];
1529
+ function hubMessage(message, language) {
1530
+ return hubMessages.find((pair) => pair.includes(message))?.[language === "zh" ? 0 : 1] ?? message;
1531
+ }
1532
+ //#endregion
1533
+ //#region src/client-cast.tsx
1534
+ /**
1535
+ * dsh-model-hub — subagent cast tab. Lists the allowlist of models subagents
1536
+ * may pick from, each with an optional free-text task tendency the main agent
1537
+ * consults when delegating. Every write goes through the hub host's cast RPC
1538
+ * with the native settings revision as CAS; `cast.setTendency` uses its own
1539
+ * durable file revision. The page owns its own fetch,
1540
+ * subscription and write locking, following the HubPage RequestScope pattern.
1541
+ */
1542
+ const TEXT$1 = {
1543
+ zh: {
1544
+ title: "子代理阵容",
1545
+ enabledLabel: "启用子代理阵容",
1546
+ enabledHint: "启用后,主智能体显式为子代理选择模型时须使用此列表;不指定模型时仍继承父会话。任务倾向供委派时参考。",
1547
+ loading: "正在读取…",
1548
+ failed: (message) => `读取失败:${message}`,
1549
+ syncFailed: (message) => `自动同步不可用:${message}`,
1550
+ catalogFailed: (message) => `模型列表读取失败:${message}`,
1551
+ writeFailed: (message) => `未保存:${message}。请刷新后重试。`,
1552
+ saved: "已保存,正在刷新…",
1553
+ savedRefreshFailed: (message) => `已保存;阵容刷新失败:${message}。请刷新以读取最新设置。`,
1554
+ conflict: "阵容设置已在别处修改,当前输入保留。请刷新核对后重试。",
1555
+ tendencyFailed: (message) => `任务倾向不可用:${message}。原文件已保留;修复后刷新。`,
1556
+ draftChanged: (saved) => `此任务倾向已在别处更新。当前已保存:${saved || "(空)"}。输入已保留;刷新核对后再保存。`,
1557
+ refresh: "刷新",
1558
+ refreshing: "正在刷新…",
1559
+ saving: "正在保存…",
1560
+ empty: "先添加模型,再启用显式模型选择。移除最后一个模型会关闭开关。",
1561
+ addModel: "添加模型…",
1562
+ remove: "移除",
1563
+ removeRoute: (label) => `移除 ${label}`,
1564
+ tendencyLabel: (label) => `${label} 的任务倾向`,
1565
+ tendencyPlaceholder: "例如:代码审查、快速问答…",
1566
+ routeExists: (route) => `路由 ${route} 已在列表中`,
1567
+ follow: "跟随宿主默认",
1568
+ model: "模型",
1569
+ effort: "思考强度",
1570
+ effortDefault: "默认",
1571
+ searchModels: "搜索模型…",
1572
+ noModels: "没有匹配的模型",
1573
+ clearSearch: "清除搜索"
1574
+ },
1575
+ en: {
1576
+ title: "Subagent Cast",
1577
+ enabledLabel: "Enable subagent cast",
1578
+ enabledHint: "When enabled, explicit model selection for a subagent must use this list; omitting a model still inherits the parent session. The main agent consults task tendencies when delegating.",
1579
+ loading: "Loading…",
1580
+ failed: (message) => `Load failed: ${message}`,
1581
+ syncFailed: (message) => `Live sync unavailable: ${message}`,
1582
+ catalogFailed: (message) => `Model list failed: ${message}`,
1583
+ writeFailed: (message) => `Not saved: ${message}. Refresh, then try again.`,
1584
+ saved: "Saved; refreshing…",
1585
+ savedRefreshFailed: (message) => `Saved; cast refresh failed: ${message}. Refresh to read the latest settings.`,
1586
+ conflict: "Cast settings changed elsewhere; your draft is preserved. Refresh, review, then try again.",
1587
+ tendencyFailed: (message) => `Task tendencies unavailable: ${message}. The original file is preserved; repair it, then refresh.`,
1588
+ draftChanged: (saved) => `This tendency changed elsewhere. Currently saved: ${saved || "(empty)"}. Your draft is preserved; refresh and review before saving.`,
1589
+ refresh: "Refresh",
1590
+ refreshing: "Refreshing…",
1591
+ saving: "Saving…",
1592
+ empty: "Add a model before enabling explicit model selection. Removing the last model turns the switch off.",
1593
+ addModel: "Add model…",
1594
+ remove: "Remove",
1595
+ removeRoute: (label) => `Remove ${label}`,
1596
+ tendencyLabel: (label) => `Task tendency for ${label}`,
1597
+ tendencyPlaceholder: "e.g. code review, quick Q&A…",
1598
+ routeExists: (route) => `Route ${route} is already in the list`,
1599
+ follow: "Follow host default",
1600
+ model: "Model",
1601
+ effort: "Reasoning",
1602
+ effortDefault: "Default",
1603
+ searchModels: "Search models…",
1604
+ noModels: "No matching models",
1605
+ clearSearch: "Clear search"
1606
+ }
1607
+ };
1608
+ function useText$1(locale) {
1609
+ const subscribe = (0, react.useCallback)((notify) => typeof locale === "object" ? locale.subscribe?.(notify) ?? (() => {}) : () => {}, [locale]);
1610
+ const snapshot = (0, react.useCallback)(() => typeof locale === "string" ? locale : locale?.getSnapshot?.()?.active ?? "en", [locale]);
1611
+ const active = (0, react.useSyncExternalStore)(subscribe, snapshot, snapshot);
1612
+ const table = String(active || "en").toLowerCase().startsWith("zh") ? TEXT$1.zh : TEXT$1.en;
1613
+ return (0, react.useMemo)(() => Object.assign((key, ...args) => {
1614
+ const value = table[key];
1615
+ return typeof value === "function" ? value(...args) : value;
1616
+ }, { language: table === TEXT$1.zh ? "zh" : "en" }), [table]);
1617
+ }
1618
+ const castCss = `
1619
+ .dsh-model-hub .cast-head{display:flex;gap:12px;align-items:center;justify-content:space-between;flex-wrap:wrap}
1620
+ .dsh-model-hub .cast-row{display:grid;grid-template-columns:minmax(180px,1fr) minmax(160px,1fr) auto;gap:12px;align-items:center;padding:8px 0;border-bottom:.5px solid var(--dsw-alias-border-l1)}
1621
+ .dsh-model-hub .cast-row-model{min-width:0}
1622
+ .dsh-model-hub .cast-row-model .dsh-model-menu-root{max-width:100%}
1623
+ .dsh-model-hub .cast-row-model .dsh-model-menu-trigger{max-width:100%}
1624
+ .dsh-model-hub .cast-row-tendency{min-width:0}
1625
+ .dsh-model-hub .cast-banner{display:flex;gap:12px;align-items:center;justify-content:space-between;flex-wrap:wrap}
1626
+ .dsh-model-hub .cast-add{display:flex}
1627
+ .dsh-model-hub .cast-add .dsh-model-menu-root{max-width:100%}
1628
+ @media(max-width:520px){.dsh-model-hub .cast-row{grid-template-columns:minmax(0,1fr);gap:8px}}
1629
+ `;
1630
+ /** CAS rejections all carry a refresh hint in either language. */
1631
+ function isConflict(message) {
1632
+ return /refresh and retry|reload and retry|刷新后重试|重新加载后重试|请先刷新/i.test(message);
1633
+ }
1634
+ /** The host's duplicate-route error interpolates the route; localize it here. */
1635
+ function localizeCastError(message, t) {
1636
+ const duplicate = /^Route "(.+)" already exists$/.exec(message);
1637
+ if (duplicate) return t("routeExists", duplicate[1]);
1638
+ return hubMessage(message, t.language);
1639
+ }
1640
+ function routeInput(route) {
1641
+ const normalized = normalizeModelRoute(route);
1642
+ return normalized.reasoningEffort ? {
1643
+ provider: normalized.provider,
1644
+ model: normalized.model,
1645
+ reasoningEffort: normalized.reasoningEffort
1646
+ } : {
1647
+ provider: normalized.provider,
1648
+ model: normalized.model
1649
+ };
1650
+ }
1651
+ /** One cast entry: model picker, tendency input saved on blur, remove button. */
1652
+ function CastRow({ t, catalog, route, tendencyRevision, draftReview, disabled, tendencyDisabled, menuDisabled, onSetRoute, onSetTendency, onRemove }) {
1653
+ const label = `${route.provider} / ${route.model}`;
1654
+ const saved = route.tendency ?? "";
1655
+ const [draft, setDraft] = (0, react.useState)({
1656
+ text: saved,
1657
+ saved,
1658
+ revision: tendencyRevision,
1659
+ review: draftReview
1660
+ });
1661
+ (0, react.useEffect)(() => {
1662
+ setDraft((current) => {
1663
+ if (current.text.trim() === current.saved || current.text.trim() === saved) return {
1664
+ text: saved,
1665
+ saved,
1666
+ revision: tendencyRevision,
1667
+ review: draftReview
1668
+ };
1669
+ return current.review !== draftReview ? {
1670
+ ...current,
1671
+ saved,
1672
+ revision: tendencyRevision,
1673
+ review: draftReview
1674
+ } : current;
1675
+ });
1676
+ }, [
1677
+ saved,
1678
+ tendencyRevision,
1679
+ draftReview
1680
+ ]);
1681
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1682
+ className: "cast-row",
1683
+ children: [
1684
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1685
+ className: "cast-row-model",
1686
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldMenu, {
1687
+ t,
1688
+ catalog,
1689
+ current: route,
1690
+ label,
1691
+ allowFollow: false,
1692
+ reasoningEffort: false,
1693
+ disabled: menuDisabled,
1694
+ onSet: onSetRoute
1695
+ })
1696
+ }),
1697
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1698
+ className: "cast-row-tendency",
1699
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
1700
+ type: "text",
1701
+ "aria-label": t("tendencyLabel", label),
1702
+ placeholder: t("tendencyPlaceholder"),
1703
+ value: draft.text,
1704
+ disabled: tendencyDisabled,
1705
+ maxLength: 500,
1706
+ onChange: (event) => setDraft((current) => ({
1707
+ ...current,
1708
+ text: event.target.value
1709
+ })),
1710
+ onBlur: () => {
1711
+ const tendency = draft.text.trim();
1712
+ if (!tendencyDisabled && draft.revision !== void 0 && tendency !== saved) onSetTendency(tendency, draft.revision);
1713
+ }
1714
+ }), draft.text.trim() !== saved && draft.revision !== tendencyRevision ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1715
+ role: "status",
1716
+ className: "dsh-ui-help dsh-ui-wrap",
1717
+ children: t("draftChanged", saved)
1718
+ }) : null]
1719
+ }),
1720
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1721
+ variant: "outline",
1722
+ "aria-label": t("removeRoute", label),
1723
+ disabled,
1724
+ onClick: onRemove,
1725
+ children: t("remove")
1726
+ })
1727
+ ]
1728
+ });
1729
+ }
1730
+ function CastPage({ api, locale }) {
1731
+ const t = useText$1(locale);
1732
+ const [cast, setCast] = (0, react.useState)(null);
1733
+ const [catalog, setCatalog] = (0, react.useState)(null);
1734
+ const [loadError, setLoadError] = (0, react.useState)("");
1735
+ const [catalogError, setCatalogError] = (0, react.useState)("");
1736
+ const [syncError, setSyncError] = (0, react.useState)("");
1737
+ const [writeError, setWriteError] = (0, react.useState)("");
1738
+ const [saved, setSaved] = (0, react.useState)(false);
1739
+ const [conflict, setConflict] = (0, react.useState)(false);
1740
+ const [saving, setSaving] = (0, react.useState)("");
1741
+ const [refreshing, setRefreshing] = (0, react.useState)(false);
1742
+ const [draftReview, setDraftReview] = (0, react.useState)(0);
1743
+ const scopeRef = (0, react.useRef)(null);
1744
+ const refresh = (0, react.useCallback)(async (scope, reviewDrafts = false) => {
1745
+ if (!scope.active) return;
1746
+ if (scope.busy) {
1747
+ scope.queued = true;
1748
+ return;
1749
+ }
1750
+ scope.queued = false;
1751
+ scope.busy = true;
1752
+ setRefreshing(true);
1753
+ const [castResult, models] = await Promise.allSettled([Promise.resolve().then(() => scope.api.getCast()), Promise.resolve().then(() => scope.api.modelCatalog()).then(checkedCatalog)]);
1754
+ if (!scope.active) return;
1755
+ if (castResult.status === "fulfilled") {
1756
+ setCast(castResult.value);
1757
+ setLoadError("");
1758
+ setWriteError("");
1759
+ setConflict(false);
1760
+ setSaved(false);
1761
+ if (reviewDrafts) setDraftReview((current) => current + 1);
1762
+ } else setLoadError(messageOf(castResult.reason));
1763
+ if (models.status === "fulfilled") {
1764
+ setCatalog(models.value);
1765
+ setCatalogError("");
1766
+ } else setCatalogError(messageOf(models.reason));
1767
+ scope.busy = false;
1768
+ setRefreshing(false);
1769
+ if (scope.queued) refresh(scope);
1770
+ }, []);
1771
+ (0, react.useEffect)(() => {
1772
+ const scope = {
1773
+ api,
1774
+ active: true,
1775
+ busy: false,
1776
+ queued: false
1777
+ };
1778
+ scopeRef.current = scope;
1779
+ setSaving("");
1780
+ setWriteError("");
1781
+ setSaved(false);
1782
+ setConflict(false);
1783
+ setLoadError("");
1784
+ setCatalogError("");
1785
+ setSyncError("");
1786
+ setCast(null);
1787
+ setCatalog(null);
1788
+ refresh(scope);
1789
+ const unsubscribe = api.subscribeChanges?.(() => {
1790
+ if (!scope.active) return;
1791
+ setSyncError("");
1792
+ refresh(scope);
1793
+ }, (message) => {
1794
+ if (scope.active) setSyncError(message);
1795
+ });
1796
+ return () => {
1797
+ scope.active = false;
1798
+ unsubscribe?.();
1799
+ scopeRef.current = null;
1800
+ };
1801
+ }, [api, refresh]);
1802
+ const mutate = (key, run) => {
1803
+ const scope = scopeRef.current;
1804
+ if (!scope?.active || scope.api !== api || scope.busy || writeError || saved && loadError) return;
1805
+ scope.busy = true;
1806
+ setSaved(false);
1807
+ setSaving(key);
1808
+ Promise.resolve().then(() => run(scope)).then((value) => {
1809
+ if (!scope.active) return;
1810
+ if ("committed" in value) {
1811
+ setSaved(true);
1812
+ scope.queued = true;
1813
+ } else setCast(value);
1814
+ setWriteError("");
1815
+ setConflict(false);
1816
+ }, (error) => {
1817
+ if (scope.active) {
1818
+ const message = messageOf(error);
1819
+ setWriteError(message);
1820
+ setConflict(isConflict(message));
1821
+ }
1822
+ }).finally(() => {
1823
+ if (scope.active) {
1824
+ scope.busy = false;
1825
+ setSaving("");
1826
+ if (scope.queued) refresh(scope);
1827
+ }
1828
+ });
1829
+ };
1830
+ const refreshNow = () => {
1831
+ const scope = scopeRef.current;
1832
+ if (scope?.api === api) refresh(scope, true);
1833
+ };
1834
+ const disabled = saving !== "" || refreshing || writeError !== "" || saved && loadError !== "";
1835
+ const failures = catalogFailures(catalog);
1836
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1837
+ className: "cast-root dsh-ui-stack",
1838
+ "aria-label": t("title"),
1839
+ "aria-busy": saving !== "" || refreshing,
1840
+ children: [
1841
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: castCss }),
1842
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1843
+ className: "cast-head",
1844
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
1845
+ checked: cast?.enabled ?? false,
1846
+ label: t("enabledLabel"),
1847
+ disabled: disabled || cast === null || !cast.enabled && cast.routes.length === 0,
1848
+ onChange: (next) => {
1849
+ if (cast) mutate("enabled", (scope) => scope.api.setCastEnabled({
1850
+ enabled: next,
1851
+ expectedRevision: cast.revision
1852
+ }));
1853
+ }
1854
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1855
+ variant: "ghost",
1856
+ disabled: saving !== "" || refreshing,
1857
+ onClick: refreshNow,
1858
+ children: refreshing ? t("refreshing") : t("refresh")
1859
+ })]
1860
+ }),
1861
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1862
+ className: "dsh-ui-help",
1863
+ children: t("enabledHint")
1864
+ }),
1865
+ loadError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1866
+ role: "alert",
1867
+ className: "dsh-ui-error dsh-ui-wrap",
1868
+ children: t(saved ? "savedRefreshFailed" : "failed", hubMessage(loadError, t.language))
1869
+ }) : null,
1870
+ cast?.tendencyError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1871
+ role: "alert",
1872
+ className: "dsh-ui-error dsh-ui-wrap",
1873
+ children: t("tendencyFailed", hubMessage(cast.tendencyError, t.language))
1874
+ }) : null,
1875
+ syncError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1876
+ role: "alert",
1877
+ className: "dsh-ui-error dsh-ui-wrap",
1878
+ children: t("syncFailed", hubMessage(syncError, t.language))
1879
+ }) : null,
1880
+ catalogError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1881
+ role: "alert",
1882
+ className: "dsh-ui-error dsh-ui-wrap",
1883
+ children: t("catalogFailed", hubMessage(catalogError, t.language))
1884
+ }) : null,
1885
+ failures.map((failure, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1886
+ role: "alert",
1887
+ className: "dsh-ui-error dsh-ui-wrap",
1888
+ children: t("catalogFailed", hubMessage(failure, t.language))
1889
+ }, index)),
1890
+ writeError ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1891
+ className: "dsh-ui-banner dsh-ui-banner--danger cast-banner",
1892
+ role: "alert",
1893
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1894
+ className: "dsh-ui-wrap",
1895
+ children: conflict ? t("conflict") : t("writeFailed", localizeCastError(writeError, t))
1896
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1897
+ variant: "outline",
1898
+ size: "sm",
1899
+ disabled: saving !== "" || refreshing,
1900
+ onClick: refreshNow,
1901
+ children: refreshing ? t("refreshing") : t("refresh")
1902
+ })]
1903
+ }) : null,
1904
+ saving ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1905
+ role: "status",
1906
+ className: "dsh-ui-help",
1907
+ children: t("saving")
1908
+ }) : null,
1909
+ saved && !loadError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1910
+ role: "status",
1911
+ className: "dsh-ui-help",
1912
+ children: t("saved")
1913
+ }) : null,
1914
+ cast === null && !loadError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1915
+ className: "dsh-ui-loading",
1916
+ children: t("loading")
1917
+ }) : null,
1918
+ cast !== null && cast.routes.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1919
+ className: "dsh-ui-help",
1920
+ children: t("empty")
1921
+ }) : null,
1922
+ cast?.routes.map((route, position) => {
1923
+ const handle = route.index ?? position;
1924
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CastRow, {
1925
+ t,
1926
+ catalog,
1927
+ route,
1928
+ tendencyRevision: cast.tendencyRevision,
1929
+ draftReview,
1930
+ tendencyDisabled: disabled || !!cast.tendencyError || cast.tendencyRevision === void 0,
1931
+ disabled,
1932
+ menuDisabled: disabled || catalog === null,
1933
+ onSetRoute: (next) => mutate(`route:${handle}`, (scope) => scope.api.setCastRoute({
1934
+ index: handle,
1935
+ route: routeInput(next),
1936
+ expectedRevision: cast.revision
1937
+ })),
1938
+ onSetTendency: (tendency, expectedRevision) => mutate(`tendency:${handle}`, (scope) => scope.api.setCastTendency({
1939
+ provider: route.provider,
1940
+ model: route.model,
1941
+ tendency,
1942
+ expectedRevision
1943
+ })),
1944
+ onRemove: () => mutate(`remove:${handle}`, (scope) => scope.api.removeCastRoute({
1945
+ index: handle,
1946
+ expectedRevision: cast.revision
1947
+ }))
1948
+ }, JSON.stringify([route.provider, route.model]));
1949
+ }),
1950
+ cast !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1951
+ className: "cast-add",
1952
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldMenu, {
1953
+ t,
1954
+ catalog,
1955
+ current: void 0,
1956
+ label: t("addModel"),
1957
+ allowFollow: false,
1958
+ followLabel: t("addModel"),
1959
+ reasoningEffort: false,
1960
+ disabled: disabled || catalog === null,
1961
+ onSet: (picked) => mutate("add", (scope) => scope.api.addCastRoute({
1962
+ route: routeInput(picked),
1963
+ expectedRevision: cast.revision
1964
+ }))
1965
+ })
1966
+ }) : null
1967
+ ]
1968
+ });
1969
+ }
1970
+ //#endregion
1971
+ //#region src/client.tsx
1972
+ /**
1973
+ * dsh-model-hub — browser half. One settings-page section listing every
1974
+ * detected model-route field across plugins. Each row is edited with the
1975
+ * contract's standard ModelMenu (dsh-model-route/ui) and written back through
1976
+ * the hub's host RPC — `settings.mutate` on the host, CAS by descriptor
1977
+ * revision. An empty route means "follow the host default" and unsets the
1978
+ * field.
1979
+ */
1980
+ const name = "dsh-model-hub-client";
1981
+ const inject = [
1982
+ "slots",
1983
+ "connection",
1984
+ "remote",
1985
+ "remote.session",
1986
+ "locale"
1987
+ ];
1988
+ const CHANNEL = "/dsh-model-hub";
1989
+ const BUNDLE = "@klarkxy/dsh-model-hub";
1990
+ const TEXT = {
1991
+ zh: {
1992
+ title: "模型枢纽",
1993
+ intro: "一页查看并修改各插件的模型路由;改动立即写入对应插件自己的设置。",
1994
+ tabsLabel: "模型枢纽分区",
1995
+ tabFields: "字段总览",
1996
+ tabCast: "子代理阵容",
1997
+ loading: "正在读取…",
1998
+ empty: "没有检测到模型字段。",
1999
+ failed: (message) => `读取失败:${message}`,
2000
+ syncFailed: (message) => `自动同步不可用:${message}`,
2001
+ catalogFailed: (message) => `模型列表读取失败:${message}`,
2002
+ writeFailed: (message) => `未保存:${message}。请刷新后重新选择。`,
2003
+ saved: "已保存,正在刷新…",
2004
+ savedRefreshFailed: (message) => `已保存;页面刷新失败:${message}。请刷新以读取最新设置。`,
2005
+ refresh: "刷新",
2006
+ refreshing: "正在刷新…",
2007
+ saving: "正在保存…",
2008
+ search: "搜索插件、字段或当前模型…",
2009
+ noFields: "没有匹配的模型字段",
2010
+ follow: "跟随宿主默认",
2011
+ restorePluginDefault: "恢复插件默认",
2012
+ model: "模型",
2013
+ effort: "思考强度",
2014
+ effortDefault: "默认",
2015
+ searchModels: "搜索模型…",
2016
+ noModels: "没有匹配的模型",
2017
+ clearSearch: "清除搜索",
2018
+ collectionTemplate: "集合中没有可编辑的条目;请先在所属插件中添加条目。",
2019
+ items: "条目"
2020
+ },
2021
+ en: {
2022
+ title: "Model Hub",
2023
+ intro: "View and edit every plugin's model route on one page; changes write into the owning plugin's own settings at once.",
2024
+ tabsLabel: "Model Hub sections",
2025
+ tabFields: "Fields",
2026
+ tabCast: "Subagent Cast",
2027
+ loading: "Loading…",
2028
+ empty: "No model fields detected.",
2029
+ failed: (message) => `Load failed: ${message}`,
2030
+ syncFailed: (message) => `Live sync unavailable: ${message}`,
2031
+ catalogFailed: (message) => `Model list failed: ${message}`,
2032
+ writeFailed: (message) => `Not saved: ${message}. Refresh, then choose again.`,
2033
+ saved: "Saved; refreshing…",
2034
+ savedRefreshFailed: (message) => `Saved; page refresh failed: ${message}. Refresh to read the latest settings.`,
2035
+ refresh: "Refresh",
2036
+ refreshing: "Refreshing…",
2037
+ saving: "Saving…",
2038
+ search: "Search plugins, fields or current models…",
2039
+ noFields: "No matching model fields",
2040
+ follow: "Follow host default",
2041
+ restorePluginDefault: "Restore plugin default",
2042
+ model: "Model",
2043
+ effort: "Reasoning",
2044
+ effortDefault: "Default",
2045
+ searchModels: "Search models…",
2046
+ noModels: "No matching models",
2047
+ clearSearch: "Clear search",
2048
+ collectionTemplate: "No collection item to edit. Add an item in the owning plugin first.",
2049
+ items: "Items"
2050
+ }
2051
+ };
2052
+ function useText(locale) {
2053
+ const subscribe = (0, react.useCallback)((notify) => typeof locale === "object" ? locale.subscribe?.(notify) ?? (() => {}) : () => {}, [locale]);
2054
+ const snapshot = (0, react.useCallback)(() => typeof locale === "string" ? locale : locale?.getSnapshot?.()?.active ?? "en", [locale]);
2055
+ const active = (0, react.useSyncExternalStore)(subscribe, snapshot, snapshot);
2056
+ const table = String(active || "en").toLowerCase().startsWith("zh") ? TEXT.zh : TEXT.en;
2057
+ return (0, react.useMemo)(() => Object.assign((key, ...args) => {
2058
+ const value = table[key];
2059
+ return typeof value === "function" ? value(...args) : value;
2060
+ }, { language: table === TEXT.zh ? "zh" : "en" }), [table]);
2061
+ }
2062
+ const css = `${officialUiCss("dsh-model-hub")}${modelMenuCss}
2063
+ .dsh-model-hub .hub-toolbar{display:flex;gap:12px;align-items:center}
2064
+ .dsh-model-hub .hub-search{flex:1;min-width:0}
2065
+ .dsh-model-hub .hub-group{min-width:0}
2066
+ .dsh-model-hub .hub-group-head{display:flex;gap:8px;align-items:baseline;flex-wrap:wrap;margin:12px 0 4px}
2067
+ .dsh-model-hub .hub-group-title{font-size:14px;line-height:22px;font-weight:600;margin:0;color:var(--dsw-alias-label-primary)}
2068
+ .dsh-model-hub .hub-namespace{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary);overflow-wrap:anywhere}
2069
+ .dsh-model-hub .hub-row{display:grid;grid-template-columns:minmax(140px,1fr) minmax(180px,1fr);gap:16px;align-items:center;padding:8px 0;border-bottom:.5px solid var(--dsw-alias-border-l1)}
2070
+ .dsh-model-hub .hub-row-copy{min-width:0}
2071
+ .dsh-model-hub .hub-row-label{font-size:13px;line-height:20px;color:var(--dsw-alias-label-primary);overflow-wrap:anywhere}
2072
+ .dsh-model-hub .hub-row-description{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere;margin:2px 0 0}
2073
+ .dsh-model-hub .hub-editor{min-width:0;display:flex;justify-content:flex-end}
2074
+ .dsh-model-hub .hub-editor .dsh-model-menu-root{max-width:100%}
2075
+ .dsh-model-hub .hub-editor .dsh-model-menu-trigger{max-width:100%}
2076
+ @media(max-width:520px){.dsh-model-hub .hub-toolbar{gap:8px}.dsh-model-hub .hub-row{grid-template-columns:minmax(0,1fr);gap:4px}.dsh-model-hub .hub-editor{justify-content:flex-start}.dsh-model-hub .hub-namespace{width:100%}}
2077
+ `;
2078
+ function Style() {
2079
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: css });
2080
+ }
2081
+ function humanize(value) {
2082
+ const text = value.replace(/([a-z\d])([A-Z])/g, "$1 $2").replace(/[-_.]+/g, " ").trim();
2083
+ return text.charAt(0).toUpperCase() + text.slice(1);
2084
+ }
2085
+ function fieldLabel(field, t) {
2086
+ return hubText(field.marker?.label || field.label, t.language) || field.path.map((part) => part === "[]" ? t("items") : part === "model" ? t("model") : /^\d+$/.test(part) ? `#${Number(part) + 1}` : humanize(part)).join(" / ");
2087
+ }
2088
+ function followLabel(field, t) {
2089
+ return typeof field.followLabel === "string" ? field.followLabel : field.followLabel?.[t.language] || t(field.via === "contribution" || field.arrayItem ? "follow" : "restorePluginDefault");
2090
+ }
2091
+ function pluginTitle(entry, t) {
2092
+ return hubText(entry.title, t.language) || humanize((entry.ns.split("/").at(-1) || entry.ns).replace(/^dsh-/, ""));
2093
+ }
1292
2094
  function HubPage({ view, api, locale, initialEntries = null, initialCatalog = null }) {
1293
2095
  const t = useText(locale);
1294
2096
  const [entries, setEntries] = (0, react.useState)(initialEntries);
1295
2097
  const [catalog, setCatalog] = (0, react.useState)(initialCatalog);
1296
- const [error, setError] = (0, react.useState)("");
2098
+ const [listError, setListError] = (0, react.useState)("");
2099
+ const [catalogError, setCatalogError] = (0, react.useState)("");
2100
+ const [writeError, setWriteError] = (0, react.useState)("");
2101
+ const [saved, setSaved] = (0, react.useState)(false);
2102
+ const [syncError, setSyncError] = (0, react.useState)("");
1297
2103
  const [writing, setWriting] = (0, react.useState)("");
2104
+ const [refreshing, setRefreshing] = (0, react.useState)(false);
2105
+ const [query, setQuery] = (0, react.useState)("");
2106
+ const [tab, setTab] = (0, react.useState)("fields");
2107
+ const scopeRef = (0, react.useRef)(null);
2108
+ const refresh = (0, react.useCallback)(async (scope) => {
2109
+ if (!scope.active) return;
2110
+ if (scope.busy) {
2111
+ scope.queued = true;
2112
+ return;
2113
+ }
2114
+ scope.queued = false;
2115
+ scope.busy = true;
2116
+ setRefreshing(true);
2117
+ const [fields, models] = await Promise.allSettled([Promise.resolve().then(() => scope.api.list()), Promise.resolve().then(() => scope.api.modelCatalog()).then(checkedCatalog)]);
2118
+ if (!scope.active) return;
2119
+ if (fields.status === "fulfilled") {
2120
+ setEntries(fields.value);
2121
+ setListError("");
2122
+ setWriteError("");
2123
+ setSaved(false);
2124
+ } else setListError(messageOf(fields.reason));
2125
+ if (models.status === "fulfilled") {
2126
+ setCatalog(models.value);
2127
+ setCatalogError("");
2128
+ } else setCatalogError(messageOf(models.reason));
2129
+ scope.busy = false;
2130
+ setRefreshing(false);
2131
+ if (scope.queued) refresh(scope);
2132
+ }, []);
1298
2133
  (0, react.useEffect)(() => {
1299
- let alive = true;
1300
- api.list().then((value) => {
1301
- if (alive) {
1302
- setEntries(value);
1303
- setError("");
1304
- }
1305
- }, (error) => {
1306
- if (alive) setError(t("failed", error instanceof Error ? error.message : String(error)));
2134
+ if (view === "summary") return;
2135
+ const scope = {
2136
+ api,
2137
+ active: true,
2138
+ busy: false,
2139
+ queued: false
2140
+ };
2141
+ scopeRef.current = scope;
2142
+ setWriting("");
2143
+ setWriteError("");
2144
+ setSaved(false);
2145
+ setListError("");
2146
+ setCatalogError("");
2147
+ setSyncError("");
2148
+ setEntries(initialEntries);
2149
+ setCatalog(initialCatalog);
2150
+ refresh(scope);
2151
+ const unsubscribe = api.subscribeChanges?.(() => {
2152
+ if (!scope.active) return;
2153
+ setSyncError("");
2154
+ refresh(scope);
2155
+ }, (message) => {
2156
+ if (scope.active) setSyncError(message);
1307
2157
  });
1308
- Promise.resolve().then(() => api.modelCatalog()).then((value) => {
1309
- if (alive) setCatalog(value);
1310
- }, () => {});
1311
2158
  return () => {
1312
- alive = false;
2159
+ scope.active = false;
2160
+ unsubscribe?.();
2161
+ scopeRef.current = null;
1313
2162
  };
1314
- }, [api]);
2163
+ }, [
2164
+ api,
2165
+ view,
2166
+ refresh
2167
+ ]);
1315
2168
  const setRoute = (entry, field, route) => {
2169
+ const scope = scopeRef.current;
2170
+ if (!scope?.active || scope.api !== api || scope.busy || writeError) return;
1316
2171
  const key = `${entry.ns}:${field.path.join(".")}`;
1317
- if (writing) return;
2172
+ scope.busy = true;
2173
+ setSaved(false);
1318
2174
  setWriting(key);
1319
- api.setField({
2175
+ Promise.resolve().then(() => scope.api.setField({
1320
2176
  ns: entry.ns,
1321
2177
  path: field.path,
1322
2178
  route,
1323
- expectedRevision: entry.revision
1324
- }).then((value) => {
1325
- setEntries(value);
1326
- setError("");
1327
- }, (error) => setError(t("writeFailed", error instanceof Error ? error.message : String(error)))).finally(() => setWriting(""));
2179
+ expectedRevision: entry.revision,
2180
+ ...entry.ownerId ? { ownerId: entry.ownerId } : {}
2181
+ })).then(() => {
2182
+ if (scope.active) {
2183
+ setSaved(true);
2184
+ setWriteError("");
2185
+ scope.queued = true;
2186
+ }
2187
+ }, (error) => {
2188
+ if (scope.active) setWriteError(messageOf(error));
2189
+ }).finally(() => {
2190
+ if (scope.active) {
2191
+ scope.busy = false;
2192
+ setWriting("");
2193
+ if (scope.queued) refresh(scope);
2194
+ }
2195
+ });
1328
2196
  };
1329
2197
  if (view === "summary") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1330
2198
  className: "dsh-model-hub",
2199
+ lang: t.language,
1331
2200
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Style, {}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1332
2201
  className: "dsh-ui-compact dsh-ui-wrap",
1333
2202
  children: t("intro")
1334
2203
  })]
1335
2204
  });
2205
+ const needle = query.trim().toLocaleLowerCase();
2206
+ const groups = (entries ?? []).map((entry) => ({
2207
+ ...entry,
2208
+ fields: entry.fields.filter((field) => !(entry.ns === "@klarkxy/dsh-model-hub" && field.path.length === 1 && ["selfTestMeta", "selfTestRole"].includes(field.path[0] || "")))
2209
+ })).filter((entry) => entry.ns !== "subagent-model-selection-settings" && (entry.fields.length > 0 || entry.error || entry.catalogError));
2210
+ const visible = groups.map((entry) => ({
2211
+ ...entry,
2212
+ fields: entry.fields.filter((field) => {
2213
+ if (!needle) return true;
2214
+ const route = normalizeModelRoute(field.current);
2215
+ const model = parseModelMenuChoices(entry.catalog !== void 0 ? entry.catalog : catalog, route).find((choice) => choice.provider === route.provider && choice.model === route.model)?.label || "";
2216
+ return [
2217
+ entry.ns,
2218
+ pluginTitle(entry, t),
2219
+ fieldLabel(field, t),
2220
+ hubText(field.description, t.language),
2221
+ field.path.join("."),
2222
+ route.provider,
2223
+ route.model,
2224
+ model,
2225
+ !route.model ? followLabel(field, t) : ""
2226
+ ].some((text) => text?.toLocaleLowerCase().includes(needle));
2227
+ })
2228
+ })).filter((entry) => entry.fields.length > 0 || (entry.error || entry.catalogError) && (!needle || [entry.ns, pluginTitle(entry, t)].some((text) => text.toLocaleLowerCase().includes(needle))));
2229
+ const disabled = writing !== "" || refreshing || writeError !== "" || saved && listError !== "";
2230
+ const failures = catalogFailures(catalog);
1336
2231
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1337
2232
  className: "dsh-model-hub dsh-ui-stack",
2233
+ lang: t.language,
1338
2234
  "aria-label": t("title"),
2235
+ "aria-busy": writing !== "" || refreshing,
1339
2236
  children: [
1340
2237
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Style, {}),
1341
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1342
- className: "dsh-ui-compact dsh-ui-wrap",
1343
- children: t("intro")
2238
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.SegmentedTabs, {
2239
+ value: tab,
2240
+ onChange: setTab,
2241
+ label: t("tabsLabel"),
2242
+ className: "hub-tabs",
2243
+ items: [{
2244
+ value: "fields",
2245
+ label: t("tabFields"),
2246
+ id: "dsh-model-hub-tab-fields",
2247
+ panelId: "dsh-model-hub-panel-fields"
2248
+ }, {
2249
+ value: "cast",
2250
+ label: t("tabCast"),
2251
+ id: "dsh-model-hub-tab-cast",
2252
+ panelId: "dsh-model-hub-panel-cast"
2253
+ }]
1344
2254
  }),
1345
- entries === null && !error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1346
- className: "dsh-ui-loading",
1347
- children: t("loading")
1348
- }) : null,
1349
- entries !== null && entries.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1350
- className: "dsh-ui-help",
1351
- children: t("empty")
1352
- }) : null,
1353
- (entries ?? []).map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
1354
- className: "dsh-ui-card",
1355
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("legend", {
1356
- className: "dsh-ui-heading",
1357
- children: entry.ns
1358
- }), entry.fields.map((field) => {
1359
- const key = `${entry.ns}:${field.path.join(".")}`;
1360
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1361
- className: "dsh-ui-field",
2255
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2256
+ role: "tabpanel",
2257
+ id: "dsh-model-hub-panel-fields",
2258
+ "aria-labelledby": "dsh-model-hub-tab-fields",
2259
+ hidden: tab !== "fields",
2260
+ className: "dsh-ui-stack",
2261
+ children: [
2262
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2263
+ className: "hub-toolbar",
2264
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
2265
+ className: "hub-search",
2266
+ type: "search",
2267
+ "aria-label": t("search"),
2268
+ placeholder: t("search"),
2269
+ value: query,
2270
+ onChange: (event) => setQuery(event.target.value)
2271
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2272
+ variant: "ghost",
2273
+ disabled: writing !== "" || refreshing,
2274
+ onClick: () => {
2275
+ const scope = scopeRef.current;
2276
+ if (scope?.api === api) refresh(scope);
2277
+ },
2278
+ children: refreshing ? t("refreshing") : t("refresh")
2279
+ })]
2280
+ }),
2281
+ listError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2282
+ role: "alert",
2283
+ className: "dsh-ui-error dsh-ui-wrap",
2284
+ children: t(saved ? "savedRefreshFailed" : "failed", hubMessage(listError, t.language))
2285
+ }) : null,
2286
+ syncError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2287
+ role: "alert",
2288
+ className: "dsh-ui-error dsh-ui-wrap",
2289
+ children: t("syncFailed", hubMessage(syncError, t.language))
2290
+ }) : null,
2291
+ catalogError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2292
+ role: "alert",
2293
+ className: "dsh-ui-error dsh-ui-wrap",
2294
+ children: t("catalogFailed", hubMessage(catalogError, t.language))
2295
+ }) : null,
2296
+ failures.map((failure, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2297
+ role: "alert",
2298
+ className: "dsh-ui-error dsh-ui-wrap",
2299
+ children: t("catalogFailed", hubMessage(failure, t.language))
2300
+ }, index)),
2301
+ writeError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2302
+ role: "alert",
2303
+ className: "dsh-ui-error dsh-ui-wrap",
2304
+ children: t("writeFailed", hubMessage(writeError, t.language))
2305
+ }) : null,
2306
+ writing ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2307
+ role: "status",
2308
+ className: "dsh-ui-help",
2309
+ children: t("saving")
2310
+ }) : null,
2311
+ saved && !listError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2312
+ role: "status",
2313
+ className: "dsh-ui-help",
2314
+ children: t("saved")
2315
+ }) : null,
2316
+ entries === null && !listError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2317
+ className: "dsh-ui-loading",
2318
+ children: t("loading")
2319
+ }) : null,
2320
+ entries !== null && groups.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2321
+ className: "dsh-ui-help",
2322
+ children: t("empty")
2323
+ }) : null,
2324
+ groups.length > 0 && visible.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2325
+ className: "dsh-ui-help",
2326
+ children: t("noFields")
2327
+ }) : null,
2328
+ visible.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2329
+ className: "hub-group",
2330
+ "aria-label": pluginTitle(entry, t),
1362
2331
  children: [
1363
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1364
- className: "dsh-ui-label",
1365
- children: field.marker?.label || field.path.join(".")
1366
- }),
1367
- field.editable === false ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1368
- className: "dsh-ui-help",
1369
- children: t("collectionTemplate")
1370
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldMenu, {
1371
- t,
1372
- catalog,
1373
- current: field.current,
1374
- disabled: writing === key,
1375
- onSet: (route) => setRoute(entry, field, route)
2332
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2333
+ className: "hub-group-head",
2334
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
2335
+ className: "hub-group-title",
2336
+ children: pluginTitle(entry, t)
2337
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2338
+ className: "hub-namespace",
2339
+ children: entry.ns
2340
+ })]
1376
2341
  }),
1377
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1378
- className: "dsh-ui-help",
1379
- children: [
1380
- field.marker?.purpose ? `${field.marker.purpose} · ` : "",
1381
- field.via === "marker" ? t("viaMarker") : t("viaShape"),
1382
- " · ",
1383
- field.path.join(".")
1384
- ]
2342
+ entry.error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2343
+ role: "alert",
2344
+ className: "dsh-ui-error dsh-ui-wrap",
2345
+ children: t("failed", hubMessage(hubText(entry.error, t.language), t.language))
2346
+ }) : null,
2347
+ entry.catalogError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2348
+ role: "alert",
2349
+ className: "dsh-ui-error dsh-ui-wrap",
2350
+ children: t("catalogFailed", hubMessage(hubText(entry.catalogError, t.language), t.language))
2351
+ }) : null,
2352
+ entry.catalog !== void 0 ? catalogFailures(entry.catalog).map((failure, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2353
+ role: "alert",
2354
+ className: "dsh-ui-error dsh-ui-wrap",
2355
+ children: t("catalogFailed", hubMessage(failure, t.language))
2356
+ }, index)) : null,
2357
+ entry.fields.map((field) => {
2358
+ const key = `${entry.ns}:${field.path.join(".")}`;
2359
+ const label = fieldLabel(field, t);
2360
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2361
+ className: "hub-row",
2362
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2363
+ className: "hub-row-copy",
2364
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2365
+ className: "hub-row-label",
2366
+ children: label
2367
+ }), field.description ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2368
+ className: "hub-row-description",
2369
+ children: hubText(field.description, t.language)
2370
+ }) : null]
2371
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2372
+ className: "hub-editor",
2373
+ children: field.editable === false ? field.description ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2374
+ className: "dsh-ui-help",
2375
+ children: t("collectionTemplate")
2376
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldMenu, {
2377
+ t,
2378
+ catalog: entry.catalog !== void 0 ? entry.catalog : catalog,
2379
+ current: field.current,
2380
+ label,
2381
+ allowFollow: field.allowFollow,
2382
+ followLabel: followLabel(field, t),
2383
+ reasoningEffort: field.supportsReasoning === false ? false : field.marker?.reasoningEffort,
2384
+ disabled: disabled || !!entry.error || !!entry.catalogError || entry.catalog === void 0 && catalog === null,
2385
+ onSet: (route) => setRoute(entry, field, route)
2386
+ })
2387
+ })]
2388
+ }, key);
1385
2389
  })
1386
2390
  ]
1387
- }, key);
1388
- })]
1389
- }, entry.ns)),
1390
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1391
- className: "dsh-ui-actions",
1392
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1393
- variant: "ghost",
1394
- disabled: writing !== "",
1395
- onClick: () => {
1396
- setEntries(null);
1397
- api.list().then((value) => {
1398
- setEntries(value);
1399
- setError("");
1400
- }, (error) => setError(t("failed", error instanceof Error ? error.message : String(error))));
1401
- },
1402
- children: t("refresh")
1403
- })
2391
+ }, entry.ns))
2392
+ ]
1404
2393
  }),
1405
- error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1406
- role: "alert",
1407
- className: "dsh-ui-error dsh-ui-wrap",
1408
- children: error
1409
- }) : null
2394
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2395
+ role: "tabpanel",
2396
+ id: "dsh-model-hub-panel-cast",
2397
+ "aria-labelledby": "dsh-model-hub-tab-cast",
2398
+ hidden: tab !== "cast",
2399
+ children: tab === "cast" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CastPage, {
2400
+ api,
2401
+ locale
2402
+ }) : null
2403
+ })
1410
2404
  ]
1411
2405
  });
1412
2406
  }
1413
- function apply(ctx) {
2407
+ async function apply(ctx) {
1414
2408
  const client = ctx;
2409
+ const disposeRemote = await client.remote.$mount(modelSettingsChangesRemote);
2410
+ client.effect(() => disposeRemote);
2411
+ const changes = createModelSettingsSubscriber(ctx, "*");
1415
2412
  const call = async (endpoint, payload) => {
1416
2413
  const result = await client.connection.rpc.call(CHANNEL, endpoint, payload);
1417
2414
  if (!result?.ok) throw new Error(result?.error?.message || "Request failed");
@@ -1420,7 +2417,14 @@ function apply(ctx) {
1420
2417
  const api = Object.freeze({
1421
2418
  list: () => call("fields.list", {}),
1422
2419
  setField: (payload) => call("fields.set", payload),
1423
- modelCatalog: () => client.remote.session.modelCatalog()
2420
+ modelCatalog: () => client.remote.session.modelCatalog(),
2421
+ subscribeChanges: changes.subscribe,
2422
+ getCast: () => call("cast.get", {}),
2423
+ setCastEnabled: (payload) => call("cast.setEnabled", payload),
2424
+ addCastRoute: (payload) => call("cast.addRoute", payload),
2425
+ setCastRoute: (payload) => call("cast.setRoute", payload),
2426
+ removeCastRoute: (payload) => call("cast.removeRoute", payload),
2427
+ setCastTendency: (payload) => call("cast.setTendency", payload)
1424
2428
  });
1425
2429
  client.slots.inject("plugins.bundle.config", () => client.slots.register({
1426
2430
  name: "plugins.bundle.config",
@@ -1437,6 +2441,7 @@ exports.CHANNEL = CHANNEL;
1437
2441
  exports.FieldMenu = FieldMenu;
1438
2442
  exports.HubPage = HubPage;
1439
2443
  exports.apply = apply;
2444
+ exports.catalogFailures = catalogFailures;
1440
2445
  exports.inject = inject;
1441
2446
  exports.name = name;
1442
2447