@klarkxy/dsh-model-hub 0.1.2 → 0.1.4

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
@@ -5,30 +5,7 @@ window.__ModuleLoader__.load({
5
5
  var module = { exports: {} };
6
6
  var exports = module.exports;
7
7
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
8
- //#region \0rolldown/runtime.js
9
- var __create = Object.create;
10
- var __defProp = Object.defineProperty;
11
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
12
- var __getOwnPropNames = Object.getOwnPropertyNames;
13
- var __getProtoOf = Object.getPrototypeOf;
14
- var __hasOwnProp = Object.prototype.hasOwnProperty;
15
- var __copyProps = (to, from, except, desc) => {
16
- if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
17
- key = keys[i];
18
- if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
19
- get: ((k) => from[k]).bind(null, key),
20
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
21
- });
22
- }
23
- return to;
24
- };
25
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
26
- value: mod,
27
- enumerable: true
28
- }) : target, mod));
29
- //#endregion
30
8
  let react = require("react");
31
- react = __toESM(react, 1);
32
9
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
33
10
  let react_dom = require("react-dom");
34
11
  let react_jsx_runtime = require("react/jsx-runtime");
@@ -67,12 +44,68 @@ function normalizeModelRoute(value) {
67
44
  }
68
45
  //#endregion
69
46
  //#region ../dsh-model-route/src/catalog.ts
47
+ /** Both services are required: Cordis checks the nested namespace separately. */
48
+ const MODEL_CATALOG_INJECT = ["remote", "remote.session"];
70
49
  function asRecord(value) {
71
50
  return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
72
51
  }
73
52
  function asText(value) {
74
53
  return typeof value === "string" && value.trim() ? value.trim() : void 0;
75
54
  }
55
+ /** A useful diagnostic even when a transport rejects with a plain object. */
56
+ function modelCatalogErrorMessage(value) {
57
+ if (value instanceof Error && value.message) return value.message;
58
+ return asText(asRecord(value)?.message) ?? asText(value) ?? "Unable to load the model catalog.";
59
+ }
60
+ /**
61
+ * Validate and unwrap a successful catalog without discarding provider failures
62
+ * or newer host metadata. Names and metadata remain optional for older hosts.
63
+ * A broken response is never presented as a successfully loaded empty menu.
64
+ */
65
+ function checkedModelCatalog(value) {
66
+ const envelope = asRecord(value);
67
+ if (envelope?.ok === false) throw new Error(modelCatalogErrorMessage(envelope.error ?? envelope));
68
+ const root = envelope?.ok === true ? asRecord(envelope.value) : envelope;
69
+ const malformed = (field) => {
70
+ throw new Error(`Invalid model catalog: ${field}.`);
71
+ };
72
+ if (!root || !Array.isArray(root.groups)) return malformed("groups must be an array");
73
+ for (const value of root.groups) {
74
+ const group = asRecord(value);
75
+ if (!group || !asText(group.id) || !Array.isArray(group.models)) return malformed("each group needs an id and models array");
76
+ for (const value of group.models) {
77
+ const model = asRecord(value);
78
+ if (!model || !asText(model.id)) return malformed("each model needs an id");
79
+ if (model.reasoning !== void 0) {
80
+ const reasoning = asRecord(model.reasoning);
81
+ if (!reasoning || !Array.isArray(reasoning.efforts) || reasoning.efforts.some((value) => !parseEffort(value))) return malformed("reasoning efforts must be an array of identified efforts");
82
+ }
83
+ }
84
+ }
85
+ if (root.failures !== void 0 && (!Array.isArray(root.failures) || root.failures.some((value) => {
86
+ const failure = asRecord(value);
87
+ return !failure || !asText(failure.id) || typeof failure.name !== "string" || typeof failure.message !== "string";
88
+ }))) return malformed("failures must be an array of provider failures");
89
+ return root;
90
+ }
91
+ /** Read through the injected session namespace; absence and rejection are errors. */
92
+ async function readModelCatalog(client) {
93
+ const session = client.remote?.session;
94
+ if (typeof session?.modelCatalog !== "function") throw new Error("Model catalog unavailable: inject both remote and remote.session.");
95
+ return checkedModelCatalog(await session.modelCatalog());
96
+ }
97
+ /** Provider failures are partial results, not a transport failure. */
98
+ function modelCatalogFailures(value) {
99
+ try {
100
+ return checkedModelCatalog(value).failures ?? [];
101
+ } catch {
102
+ return [];
103
+ }
104
+ }
105
+ /** Text adapter for existing model-hub status messages. */
106
+ function catalogFailures(value) {
107
+ return modelCatalogFailures(value).map((failure) => `${failure.name || failure.id}: ${failure.message}`);
108
+ }
76
109
  function parseEffort(value) {
77
110
  const row = asRecord(value);
78
111
  const id = row ? asText(row.id) : void 0;
@@ -169,6 +202,59 @@ function groupModelMenuChoices(choices, query) {
169
202
  return groups;
170
203
  }
171
204
  //#endregion
205
+ //#region ../dsh-model-route/src/catalog-ui.tsx
206
+ /** Also accepts a mapped state from a caller that owns broader async orchestration. */
207
+ function ModelCatalogStatus({ state, locale }) {
208
+ const zh = locale?.toLowerCase().startsWith("zh") === true;
209
+ const copy = zh ? {
210
+ loading: "正在加载模型…",
211
+ error: "模型列表加载失败",
212
+ partial: "部分提供商的模型未能加载",
213
+ retry: "重试"
214
+ } : {
215
+ loading: "Loading models…",
216
+ error: "Unable to load models",
217
+ partial: "Some providers could not load their models",
218
+ retry: "Retry"
219
+ };
220
+ if (!state.loading && !state.error && state.failures.length === 0) return null;
221
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
222
+ className: "dsh-model-catalog-status",
223
+ lang: zh ? "zh" : "en",
224
+ children: [
225
+ state.loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
226
+ role: "status",
227
+ children: copy.loading
228
+ }) : null,
229
+ state.error ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
230
+ role: "alert",
231
+ children: [
232
+ copy.error,
233
+ ": ",
234
+ state.error
235
+ ]
236
+ }) : null,
237
+ state.failures.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
238
+ role: "status",
239
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: copy.partial }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", { children: state.failures.map((failure, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
240
+ failure.name || failure.id,
241
+ ": ",
242
+ failure.message
243
+ ] }, `${failure.id}:${index}`)) })]
244
+ }) : null,
245
+ state.error || state.failures.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
246
+ type: "button",
247
+ size: "sm",
248
+ disabled: state.loading,
249
+ onClick: () => {
250
+ state.refresh();
251
+ },
252
+ children: copy.retry
253
+ }) : null
254
+ ]
255
+ });
256
+ }
257
+ //#endregion
172
258
  //#region ../dsh-model-route/src/ui.tsx
173
259
  /**
174
260
  * ModelMenu: the standard editor of one model-route field — the visible half
@@ -1178,6 +1264,29 @@ ${scope} {
1178
1264
  & .dsh-ui-select:disabled { color: var(--dsw-alias-label-tertiary); cursor: default; }
1179
1265
  & .dsh-ui-select[aria-invalid='true'] { border-color: var(--dsw-alias-state-error-primary); }
1180
1266
 
1267
+ /* --- Native textarea. The primitives ship no textarea either, so a settings
1268
+ * prompt keeps the platform element and the same field shell as the select:
1269
+ * 0.5px l4, radius-md, layer-3, 13/1.5 type. It grows vertically instead of
1270
+ * locking to the select's 34px row. --- */
1271
+ & .dsh-ui-textarea {
1272
+ box-sizing: border-box;
1273
+ width: 100%;
1274
+ min-width: 0;
1275
+ resize: vertical;
1276
+ padding: 8px 12px;
1277
+ border: 0.5px solid var(--dsw-alias-border-l4);
1278
+ border-radius: var(--dsw-radius-md);
1279
+ background: var(--dsw-alias-bg-layer-3);
1280
+ font: inherit;
1281
+ font-size: 13px;
1282
+ line-height: 1.5;
1283
+ color: var(--dsw-alias-label-primary);
1284
+ }
1285
+ & .dsh-ui-textarea:focus-visible { outline: none; border-color: var(--dsw-alias-state-business-primary); }
1286
+ & .dsh-ui-textarea:disabled { color: var(--dsw-alias-label-tertiary); cursor: default; }
1287
+ & .dsh-ui-textarea::placeholder { color: var(--dsw-alias-label-dimmed); }
1288
+ & .dsh-ui-textarea[aria-invalid='true'] { border-color: var(--dsw-alias-state-error-primary); }
1289
+
1181
1290
  /* Long panels scroll inside themselves instead of growing the host page. */
1182
1291
  & .dsh-ui-panel { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
1183
1292
  & .dsh-ui-panel-wide { max-width: 100%; }
@@ -1190,25 +1299,18 @@ ${scope} {
1190
1299
  }
1191
1300
  //#endregion
1192
1301
  //#region ../dsh-plugin-kit/src/contracts.ts
1193
- /**
1194
- * Remote contribution for model-settings change notifications.
1195
- * Provided by `@klarkxy/dsh-model-hub` (`ModelSettingsChangesController`);
1196
- * subscribers degrade silently when that package is not installed.
1197
- * The stream carries invalidations only — `ready` is never a settings snapshot.
1198
- */
1199
- const MODEL_SETTINGS_CHANGES_REMOTE = {
1200
- package: "@klarkxy/dsh-model-hub",
1201
- descriptors: [{
1202
- id: "modelSettingsChanges/watch",
1203
- service: "modelSettingsChangesController",
1204
- namespace: "modelSettingsChanges",
1205
- method: "watch",
1206
- mode: "stream",
1207
- invocation: { kind: "direct" },
1208
- parameters: [],
1209
- cancellation: { parameter: "signal" }
1210
- }]
1211
- };
1302
+ /** `@klarkxy/dsh-git-commit` becomes `KlarkxyDshGitCommitModelSettingsChanges`. */
1303
+ function modelSettingsChangesIdentity(packageName) {
1304
+ const scoped = /^@([^/]+)\/(.+)$/.exec(packageName);
1305
+ const parts = (scoped ? `${scoped[1]}/${scoped[2]}` : packageName).split(/[^A-Za-z0-9]+/).filter(Boolean);
1306
+ if (!parts.length) throw new Error("Model settings package name has no RPC segment");
1307
+ const segment = `${parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("")}ModelSettingsChanges`;
1308
+ if (!/^[A-Z][A-Za-z0-9]*$/.test(segment)) throw new Error(`Model settings sync name is not an RPC segment: ${segment}`);
1309
+ return {
1310
+ service: segment,
1311
+ namespace: segment
1312
+ };
1313
+ }
1212
1314
  //#endregion
1213
1315
  //#region ../dsh-plugin-kit/src/model-settings-client.ts
1214
1316
  /** This stream carries invalidations only; ready is never a settings snapshot. */
@@ -1223,21 +1325,44 @@ function parseModelSettingsChangeFrame(value) {
1223
1325
  };
1224
1326
  throw new Error("Invalid model settings change frame");
1225
1327
  }
1226
- /** One contribution mounted by Model Hub; consumers only inject its service. */
1227
- const modelSettingsChangesRemote = {
1228
- package: MODEL_SETTINGS_CHANGES_REMOTE.package,
1229
- descriptors: MODEL_SETTINGS_CHANGES_REMOTE.descriptors.map((descriptor) => ({
1230
- ...descriptor,
1231
- result: {
1232
- mode: "strict",
1233
- typeSymbol: "ModelSettings.ChangeFrame",
1234
- create: () => ({ parse: parseModelSettingsChangeFrame })
1235
- }
1236
- }))
1237
- };
1328
+ /** One owner's contribution. Each package mounts only its own endpoint. */
1329
+ function modelSettingsChangesRemote(packageName, identity = modelSettingsChangesIdentity(packageName)) {
1330
+ return {
1331
+ package: packageName,
1332
+ descriptors: [{
1333
+ id: `${identity.namespace}/watch`,
1334
+ service: identity.service,
1335
+ namespace: identity.namespace,
1336
+ method: "watch",
1337
+ mode: "stream",
1338
+ invocation: { kind: "direct" },
1339
+ parameters: [],
1340
+ cancellation: { parameter: "signal" },
1341
+ result: {
1342
+ mode: "strict",
1343
+ typeSymbol: "ModelSettings.ChangeFrame",
1344
+ create: () => ({ parse: parseModelSettingsChangeFrame })
1345
+ }
1346
+ }]
1347
+ };
1348
+ }
1349
+ /** Mount from a synchronous apply. Disposal waits for an in-flight mount and
1350
+ * then withdraws it, so the contribution cannot outlive the plugin. */
1351
+ function mountModelSettingsChanges(ctx, packageName) {
1352
+ const contribution = modelSettingsChangesRemote(packageName);
1353
+ ctx.inject(["remote"], (injected) => {
1354
+ const scope = injected;
1355
+ const mounting = scope.remote.$mount(contribution);
1356
+ mounting.catch(() => {});
1357
+ scope.effect(() => async () => {
1358
+ await (await mounting.catch(() => void 0))?.();
1359
+ });
1360
+ });
1361
+ }
1238
1362
  /** Native RemoteStream owns carrier reconnects; no timers or browser transport.
1239
1363
  * A factory shares one logical stream while at least one subscriber remains. */
1240
- function createModelSettingsSubscriber(ctx, namespace) {
1364
+ function createModelSettingsSubscriber(ctx, packageName, identity = modelSettingsChangesIdentity(packageName), accept = (ns) => ns === packageName) {
1365
+ const endpoint = identity;
1241
1366
  const listeners = /* @__PURE__ */ new Set();
1242
1367
  let injection;
1243
1368
  let current;
@@ -1263,12 +1388,18 @@ function createModelSettingsSubscriber(ctx, namespace) {
1263
1388
  };
1264
1389
  const start = () => {
1265
1390
  const life = ++lifecycle;
1266
- injection = ctx.inject(["remote", "remote.modelSettingsChanges"], (injected) => {
1391
+ injection = ctx.inject(["remote", `remote.${endpoint.namespace}`], (injected) => {
1267
1392
  if (life !== lifecycle || !listeners.size) return;
1268
1393
  const scope = injected;
1394
+ const service = scope.remote[endpoint.namespace];
1395
+ if (!service || !("watch" in service) || !service.watch) {
1396
+ report("Model settings sync service unavailable");
1397
+ return;
1398
+ }
1399
+ const watch = service.watch;
1269
1400
  const stream = scope.remote.$stream({
1270
1401
  name: "Model settings sync",
1271
- open: (signal) => scope.remote.modelSettingsChanges.watch(signal),
1402
+ open: (signal) => watch(signal),
1272
1403
  ended: (accepted) => /* @__PURE__ */ new Error(`Model settings sync stream ended ${accepted ? "after" : "before"} ready; refresh or reopen the page`)
1273
1404
  });
1274
1405
  const session = {
@@ -1303,7 +1434,7 @@ function createModelSettingsSubscriber(ctx, namespace) {
1303
1434
  invalidate();
1304
1435
  } else {
1305
1436
  if (!ready) throw new Error("Model settings sync change arrived before ready");
1306
- if (namespace === "*" || frame.ns === namespace) invalidate();
1437
+ if (accept(frame.ns)) invalidate();
1307
1438
  }
1308
1439
  }
1309
1440
  } catch (error) {
@@ -1435,20 +1566,6 @@ function messageOf(error) {
1435
1566
  if (error && typeof error === "object" && "message" in error) return String(error.message);
1436
1567
  return String(error);
1437
1568
  }
1438
- function checkedCatalog(value) {
1439
- if (value && typeof value === "object" && "ok" in value && value.ok === false) throw new Error("error" in value ? messageOf(value.error) : "Request failed");
1440
- return value;
1441
- }
1442
- function catalogFailures(value) {
1443
- if (!value || typeof value !== "object") return [];
1444
- const root = "ok" in value && value.ok === true && "value" in value ? value.value : value;
1445
- if (!root || typeof root !== "object" || !("failures" in root) || !Array.isArray(root.failures)) return [];
1446
- return root.failures.map((failure) => {
1447
- if (!failure || typeof failure !== "object") return messageOf(failure);
1448
- const provider = "name" in failure ? failure.name : "id" in failure ? failure.id : "provider" in failure ? failure.provider : "";
1449
- return provider ? `${provider}: ${messageOf(failure)}` : messageOf(failure);
1450
- });
1451
- }
1452
1569
  //#endregion
1453
1570
  //#region src/client-copy.ts
1454
1571
  function hubText(value, language) {
@@ -1553,7 +1670,6 @@ const TEXT$1 = {
1553
1670
  loading: "正在读取…",
1554
1671
  failed: (message) => `读取失败:${message}`,
1555
1672
  syncFailed: (message) => `自动同步不可用:${message}`,
1556
- catalogFailed: (message) => `模型列表读取失败:${message}`,
1557
1673
  writeFailed: (message) => `未保存:${message}。请刷新后重试。`,
1558
1674
  saved: "已保存,正在刷新…",
1559
1675
  savedRefreshFailed: (message) => `已保存;阵容刷新失败:${message}。请刷新以读取最新设置。`,
@@ -1576,7 +1692,13 @@ const TEXT$1 = {
1576
1692
  effortDefault: "默认",
1577
1693
  searchModels: "搜索模型…",
1578
1694
  noModels: "没有匹配的模型",
1579
- clearSearch: "清除搜索"
1695
+ clearSearch: "清除搜索",
1696
+ runningTitle: "运行中的子代理",
1697
+ runningEmpty: "当前没有运行中的子代理。",
1698
+ runningPending: "尚未发起模型请求",
1699
+ runningRemote: "远程子代理,无法读取路由",
1700
+ runningFailed: (message) => `运行状态读取失败:${message}`,
1701
+ effortAdapterDefault: (effort) => `${effort}(适配器默认)`
1580
1702
  },
1581
1703
  en: {
1582
1704
  title: "Subagent Cast",
@@ -1585,7 +1707,6 @@ const TEXT$1 = {
1585
1707
  loading: "Loading…",
1586
1708
  failed: (message) => `Load failed: ${message}`,
1587
1709
  syncFailed: (message) => `Live sync unavailable: ${message}`,
1588
- catalogFailed: (message) => `Model list failed: ${message}`,
1589
1710
  writeFailed: (message) => `Not saved: ${message}. Refresh, then try again.`,
1590
1711
  saved: "Saved; refreshing…",
1591
1712
  savedRefreshFailed: (message) => `Saved; cast refresh failed: ${message}. Refresh to read the latest settings.`,
@@ -1608,7 +1729,13 @@ const TEXT$1 = {
1608
1729
  effortDefault: "Default",
1609
1730
  searchModels: "Search models…",
1610
1731
  noModels: "No matching models",
1611
- clearSearch: "Clear search"
1732
+ clearSearch: "Clear search",
1733
+ runningTitle: "Running subagents",
1734
+ runningEmpty: "No subagents are running.",
1735
+ runningPending: "No model request yet",
1736
+ runningRemote: "Remote subagent; route unavailable",
1737
+ runningFailed: (message) => `Running state failed: ${message}`,
1738
+ effortAdapterDefault: (effort) => `${effort} (adapter default)`
1612
1739
  }
1613
1740
  };
1614
1741
  function useText$1(locale) {
@@ -1631,6 +1758,11 @@ const castCss = `
1631
1758
  .dsh-model-hub .cast-banner{display:flex;gap:12px;align-items:center;justify-content:space-between;flex-wrap:wrap}
1632
1759
  .dsh-model-hub .cast-add{display:flex}
1633
1760
  .dsh-model-hub .cast-add .dsh-model-menu-root{max-width:100%}
1761
+ .dsh-model-hub .cast-running{margin-top:16px}
1762
+ .dsh-model-hub .cast-running-title{font-size:14px;line-height:22px;font-weight:600;margin:0 0 4px;color:var(--dsw-alias-label-primary)}
1763
+ .dsh-model-hub .cast-running-row{display:flex;gap:12px;align-items:baseline;flex-wrap:wrap;padding:6px 0;border-bottom:.5px solid var(--dsw-alias-border-l1)}
1764
+ .dsh-model-hub .cast-running-label{font-size:13px;line-height:20px;color:var(--dsw-alias-label-primary);overflow-wrap:anywhere;min-width:0}
1765
+ .dsh-model-hub .cast-running-route{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere;min-width:0}
1634
1766
  @media(max-width:520px){.dsh-model-hub .cast-row{grid-template-columns:minmax(0,1fr);gap:8px}}
1635
1767
  `;
1636
1768
  /** CAS rejections all carry a refresh hint in either language. */
@@ -1654,6 +1786,14 @@ function routeInput(route) {
1654
1786
  model: normalized.model
1655
1787
  };
1656
1788
  }
1789
+ /** One live subagent row: label (or short id) plus its effective route. */
1790
+ function runningRouteText(entry, t) {
1791
+ if (!entry.local) return t("runningRemote");
1792
+ const route = entry.route;
1793
+ if (!route) return t("runningPending");
1794
+ const effort = route.reasoningEffort ? entry.effortIsAdapterDefault ? t("effortAdapterDefault", route.reasoningEffort) : route.reasoningEffort : "";
1795
+ return `${route.provider} / ${route.model}${effort ? ` · ${effort}` : ""}`;
1796
+ }
1657
1797
  /** One cast entry: model picker, tendency input saved on blur, remove button. */
1658
1798
  function CastRow({ t, catalog, route, tendencyRevision, draftReview, disabled, tendencyDisabled, menuDisabled, onSetRoute, onSetTendency, onRemove }) {
1659
1799
  const label = `${route.provider} / ${route.model}`;
@@ -1737,6 +1877,8 @@ function CastPage({ api, locale }) {
1737
1877
  const t = useText$1(locale);
1738
1878
  const [cast, setCast] = (0, react.useState)(null);
1739
1879
  const [catalog, setCatalog] = (0, react.useState)(null);
1880
+ const [running, setRunning] = (0, react.useState)(null);
1881
+ const [runningError, setRunningError] = (0, react.useState)("");
1740
1882
  const [loadError, setLoadError] = (0, react.useState)("");
1741
1883
  const [catalogError, setCatalogError] = (0, react.useState)("");
1742
1884
  const [syncError, setSyncError] = (0, react.useState)("");
@@ -1756,7 +1898,11 @@ function CastPage({ api, locale }) {
1756
1898
  scope.queued = false;
1757
1899
  scope.busy = true;
1758
1900
  setRefreshing(true);
1759
- const [castResult, models] = await Promise.allSettled([Promise.resolve().then(() => scope.api.getCast()), Promise.resolve().then(() => scope.api.modelCatalog()).then(checkedCatalog)]);
1901
+ const [castResult, models, live] = await Promise.allSettled([
1902
+ Promise.resolve().then(() => scope.api.getCast()),
1903
+ Promise.resolve().then(() => scope.api.modelCatalog()).then(checkedModelCatalog),
1904
+ Promise.resolve().then(() => scope.api.listRunning())
1905
+ ]);
1760
1906
  if (!scope.active) return;
1761
1907
  if (castResult.status === "fulfilled") {
1762
1908
  setCast(castResult.value);
@@ -1770,6 +1916,10 @@ function CastPage({ api, locale }) {
1770
1916
  setCatalog(models.value);
1771
1917
  setCatalogError("");
1772
1918
  } else setCatalogError(messageOf(models.reason));
1919
+ if (live.status === "fulfilled") {
1920
+ setRunning(live.value);
1921
+ setRunningError("");
1922
+ } else setRunningError(messageOf(live.reason));
1773
1923
  scope.busy = false;
1774
1924
  setRefreshing(false);
1775
1925
  if (scope.queued) refresh(scope);
@@ -1791,6 +1941,8 @@ function CastPage({ api, locale }) {
1791
1941
  setSyncError("");
1792
1942
  setCast(null);
1793
1943
  setCatalog(null);
1944
+ setRunning(null);
1945
+ setRunningError("");
1794
1946
  refresh(scope);
1795
1947
  const unsubscribe = api.subscribeChanges?.(() => {
1796
1948
  if (!scope.active) return;
@@ -1833,12 +1985,17 @@ function CastPage({ api, locale }) {
1833
1985
  }
1834
1986
  });
1835
1987
  };
1836
- const refreshNow = () => {
1988
+ const refreshNow = async () => {
1837
1989
  const scope = scopeRef.current;
1838
- if (scope?.api === api) refresh(scope, true);
1990
+ if (scope?.api === api && !scope.busy) await refresh(scope, true);
1839
1991
  };
1840
1992
  const disabled = saving !== "" || refreshing || writeError !== "" || saved && loadError !== "";
1841
- const failures = catalogFailures(catalog);
1993
+ const catalogStatus = {
1994
+ loading: refreshing,
1995
+ error: catalogError ? hubMessage(catalogError, t.language) : "",
1996
+ failures: modelCatalogFailures(catalog),
1997
+ refresh: refreshNow
1998
+ };
1842
1999
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1843
2000
  className: "cast-root dsh-ui-stack",
1844
2001
  "aria-label": t("title"),
@@ -1883,16 +2040,10 @@ function CastPage({ api, locale }) {
1883
2040
  className: "dsh-ui-error dsh-ui-wrap",
1884
2041
  children: t("syncFailed", hubMessage(syncError, t.language))
1885
2042
  }) : null,
1886
- catalogError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1887
- role: "alert",
1888
- className: "dsh-ui-error dsh-ui-wrap",
1889
- children: t("catalogFailed", hubMessage(catalogError, t.language))
1890
- }) : null,
1891
- failures.map((failure, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1892
- role: "alert",
1893
- className: "dsh-ui-error dsh-ui-wrap",
1894
- children: t("catalogFailed", hubMessage(failure, t.language))
1895
- }, index)),
2043
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelCatalogStatus, {
2044
+ state: catalogStatus,
2045
+ locale: t.language
2046
+ }),
1896
2047
  writeError ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1897
2048
  className: "dsh-ui-banner dsh-ui-banner--danger cast-banner",
1898
2049
  role: "alert",
@@ -1969,7 +2120,40 @@ function CastPage({ api, locale }) {
1969
2120
  expectedRevision: cast.revision
1970
2121
  }))
1971
2122
  })
1972
- }) : null
2123
+ }) : null,
2124
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2125
+ className: "cast-running",
2126
+ "aria-label": t("runningTitle"),
2127
+ children: [
2128
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
2129
+ className: "cast-running-title",
2130
+ children: t("runningTitle")
2131
+ }),
2132
+ runningError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2133
+ role: "alert",
2134
+ className: "dsh-ui-error dsh-ui-wrap",
2135
+ children: t("runningFailed", hubMessage(runningError, t.language))
2136
+ }) : null,
2137
+ running === null && !runningError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2138
+ className: "dsh-ui-loading",
2139
+ children: t("loading")
2140
+ }) : null,
2141
+ running !== null && running.entries.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2142
+ className: "dsh-ui-help",
2143
+ children: t("runningEmpty")
2144
+ }) : null,
2145
+ running?.entries.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2146
+ className: "cast-running-row",
2147
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2148
+ className: "cast-running-label",
2149
+ children: entry.label || entry.id
2150
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2151
+ className: "cast-running-route",
2152
+ children: runningRouteText(entry, t)
2153
+ })]
2154
+ }, entry.id))
2155
+ ]
2156
+ })
1973
2157
  ]
1974
2158
  });
1975
2159
  }
@@ -1987,8 +2171,7 @@ const name = "dsh-model-hub-client";
1987
2171
  const inject = [
1988
2172
  "slots",
1989
2173
  "connection",
1990
- "remote",
1991
- "remote.session",
2174
+ ...MODEL_CATALOG_INJECT,
1992
2175
  "locale"
1993
2176
  ];
1994
2177
  const CHANNEL = "/dsh-model-hub";
@@ -2120,7 +2303,7 @@ function HubPage({ view, api, locale, initialEntries = null, initialCatalog = nu
2120
2303
  scope.queued = false;
2121
2304
  scope.busy = true;
2122
2305
  setRefreshing(true);
2123
- const [fields, models] = await Promise.allSettled([Promise.resolve().then(() => scope.api.list()), Promise.resolve().then(() => scope.api.modelCatalog()).then(checkedCatalog)]);
2306
+ const [fields, models] = await Promise.allSettled([Promise.resolve().then(() => scope.api.list()), Promise.resolve().then(() => scope.api.modelCatalog()).then(checkedModelCatalog)]);
2124
2307
  if (!scope.active) return;
2125
2308
  if (fields.status === "fulfilled") {
2126
2309
  setEntries(fields.value);
@@ -2233,7 +2416,16 @@ function HubPage({ view, api, locale, initialEntries = null, initialCatalog = nu
2233
2416
  })
2234
2417
  })).filter((entry) => entry.fields.length > 0 || (entry.error || entry.catalogError) && (!needle || [entry.ns, pluginTitle(entry, t)].some((text) => text.toLocaleLowerCase().includes(needle))));
2235
2418
  const disabled = writing !== "" || refreshing || writeError !== "" || saved && listError !== "";
2236
- const failures = catalogFailures(catalog);
2419
+ const refreshNow = async () => {
2420
+ const scope = scopeRef.current;
2421
+ if (scope?.api === api && !scope.busy) await refresh(scope);
2422
+ };
2423
+ const catalogStatus = {
2424
+ loading: refreshing,
2425
+ error: catalogError ? hubMessage(catalogError, t.language) : "",
2426
+ failures: modelCatalogFailures(catalog),
2427
+ refresh: refreshNow
2428
+ };
2237
2429
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2238
2430
  className: "dsh-model-hub dsh-ui-stack",
2239
2431
  lang: t.language,
@@ -2277,10 +2469,7 @@ function HubPage({ view, api, locale, initialEntries = null, initialCatalog = nu
2277
2469
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2278
2470
  variant: "ghost",
2279
2471
  disabled: writing !== "" || refreshing,
2280
- onClick: () => {
2281
- const scope = scopeRef.current;
2282
- if (scope?.api === api) refresh(scope);
2283
- },
2472
+ onClick: refreshNow,
2284
2473
  children: refreshing ? t("refreshing") : t("refresh")
2285
2474
  })]
2286
2475
  }),
@@ -2294,16 +2483,10 @@ function HubPage({ view, api, locale, initialEntries = null, initialCatalog = nu
2294
2483
  className: "dsh-ui-error dsh-ui-wrap",
2295
2484
  children: t("syncFailed", hubMessage(syncError, t.language))
2296
2485
  }) : null,
2297
- catalogError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2298
- role: "alert",
2299
- className: "dsh-ui-error dsh-ui-wrap",
2300
- children: t("catalogFailed", hubMessage(catalogError, t.language))
2301
- }) : null,
2302
- failures.map((failure, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2303
- role: "alert",
2304
- className: "dsh-ui-error dsh-ui-wrap",
2305
- children: t("catalogFailed", hubMessage(failure, t.language))
2306
- }, index)),
2486
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelCatalogStatus, {
2487
+ state: catalogStatus,
2488
+ locale: t.language
2489
+ }),
2307
2490
  writeError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2308
2491
  role: "alert",
2309
2492
  className: "dsh-ui-error dsh-ui-wrap",
@@ -2410,11 +2593,10 @@ function HubPage({ view, api, locale, initialEntries = null, initialCatalog = nu
2410
2593
  ]
2411
2594
  });
2412
2595
  }
2413
- async function apply(ctx) {
2596
+ function apply(ctx) {
2414
2597
  const client = ctx;
2415
- const disposeRemote = await client.remote.$mount(modelSettingsChangesRemote);
2416
- client.effect(() => disposeRemote);
2417
- const changes = createModelSettingsSubscriber(ctx, "*");
2598
+ mountModelSettingsChanges(ctx, BUNDLE);
2599
+ const changes = createModelSettingsSubscriber(ctx, BUNDLE, modelSettingsChangesIdentity(BUNDLE), () => true);
2418
2600
  const call = async (endpoint, payload) => {
2419
2601
  const result = await client.connection.rpc.call(CHANNEL, endpoint, payload);
2420
2602
  if (!result?.ok) throw new Error(result?.error?.message || "Request failed");
@@ -2423,14 +2605,15 @@ async function apply(ctx) {
2423
2605
  const api = Object.freeze({
2424
2606
  list: () => call("fields.list", {}),
2425
2607
  setField: (payload) => call("fields.set", payload),
2426
- modelCatalog: () => client.remote.session.modelCatalog(),
2608
+ modelCatalog: () => readModelCatalog(client),
2427
2609
  subscribeChanges: changes.subscribe,
2428
2610
  getCast: () => call("cast.get", {}),
2429
2611
  setCastEnabled: (payload) => call("cast.setEnabled", payload),
2430
2612
  addCastRoute: (payload) => call("cast.addRoute", payload),
2431
2613
  setCastRoute: (payload) => call("cast.setRoute", payload),
2432
2614
  removeCastRoute: (payload) => call("cast.removeRoute", payload),
2433
- setCastTendency: (payload) => call("cast.setTendency", payload)
2615
+ setCastTendency: (payload) => call("cast.setTendency", payload),
2616
+ listRunning: () => call("cast.running", {})
2434
2617
  });
2435
2618
  client.slots.inject("plugins.bundle.config", () => client.slots.register({
2436
2619
  name: "plugins.bundle.config",