@klarkxy/dsh-model-hub 0.1.1 → 0.1.3

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
@@ -67,12 +67,68 @@ function normalizeModelRoute(value) {
67
67
  }
68
68
  //#endregion
69
69
  //#region ../dsh-model-route/src/catalog.ts
70
+ /** Both services are required: Cordis checks the nested namespace separately. */
71
+ const MODEL_CATALOG_INJECT = ["remote", "remote.session"];
70
72
  function asRecord(value) {
71
73
  return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
72
74
  }
73
75
  function asText(value) {
74
76
  return typeof value === "string" && value.trim() ? value.trim() : void 0;
75
77
  }
78
+ /** A useful diagnostic even when a transport rejects with a plain object. */
79
+ function modelCatalogErrorMessage(value) {
80
+ if (value instanceof Error && value.message) return value.message;
81
+ return asText(asRecord(value)?.message) ?? asText(value) ?? "Unable to load the model catalog.";
82
+ }
83
+ /**
84
+ * Validate and unwrap a successful catalog without discarding provider failures
85
+ * or newer host metadata. Names and metadata remain optional for older hosts.
86
+ * A broken response is never presented as a successfully loaded empty menu.
87
+ */
88
+ function checkedModelCatalog(value) {
89
+ const envelope = asRecord(value);
90
+ if (envelope?.ok === false) throw new Error(modelCatalogErrorMessage(envelope.error ?? envelope));
91
+ const root = envelope?.ok === true ? asRecord(envelope.value) : envelope;
92
+ const malformed = (field) => {
93
+ throw new Error(`Invalid model catalog: ${field}.`);
94
+ };
95
+ if (!root || !Array.isArray(root.groups)) return malformed("groups must be an array");
96
+ for (const value of root.groups) {
97
+ const group = asRecord(value);
98
+ if (!group || !asText(group.id) || !Array.isArray(group.models)) return malformed("each group needs an id and models array");
99
+ for (const value of group.models) {
100
+ const model = asRecord(value);
101
+ if (!model || !asText(model.id)) return malformed("each model needs an id");
102
+ if (model.reasoning !== void 0) {
103
+ const reasoning = asRecord(model.reasoning);
104
+ if (!reasoning || !Array.isArray(reasoning.efforts) || reasoning.efforts.some((value) => !parseEffort(value))) return malformed("reasoning efforts must be an array of identified efforts");
105
+ }
106
+ }
107
+ }
108
+ if (root.failures !== void 0 && (!Array.isArray(root.failures) || root.failures.some((value) => {
109
+ const failure = asRecord(value);
110
+ return !failure || !asText(failure.id) || typeof failure.name !== "string" || typeof failure.message !== "string";
111
+ }))) return malformed("failures must be an array of provider failures");
112
+ return root;
113
+ }
114
+ /** Read through the injected session namespace; absence and rejection are errors. */
115
+ async function readModelCatalog(client) {
116
+ const session = client.remote?.session;
117
+ if (typeof session?.modelCatalog !== "function") throw new Error("Model catalog unavailable: inject both remote and remote.session.");
118
+ return checkedModelCatalog(await session.modelCatalog());
119
+ }
120
+ /** Provider failures are partial results, not a transport failure. */
121
+ function modelCatalogFailures(value) {
122
+ try {
123
+ return checkedModelCatalog(value).failures ?? [];
124
+ } catch {
125
+ return [];
126
+ }
127
+ }
128
+ /** Text adapter for existing model-hub status messages. */
129
+ function catalogFailures(value) {
130
+ return modelCatalogFailures(value).map((failure) => `${failure.name || failure.id}: ${failure.message}`);
131
+ }
76
132
  function parseEffort(value) {
77
133
  const row = asRecord(value);
78
134
  const id = row ? asText(row.id) : void 0;
@@ -169,6 +225,59 @@ function groupModelMenuChoices(choices, query) {
169
225
  return groups;
170
226
  }
171
227
  //#endregion
228
+ //#region ../dsh-model-route/src/catalog-ui.tsx
229
+ /** Also accepts a mapped state from a caller that owns broader async orchestration. */
230
+ function ModelCatalogStatus({ state, locale }) {
231
+ const zh = locale?.toLowerCase().startsWith("zh") === true;
232
+ const copy = zh ? {
233
+ loading: "正在加载模型…",
234
+ error: "模型列表加载失败",
235
+ partial: "部分提供商的模型未能加载",
236
+ retry: "重试"
237
+ } : {
238
+ loading: "Loading models…",
239
+ error: "Unable to load models",
240
+ partial: "Some providers could not load their models",
241
+ retry: "Retry"
242
+ };
243
+ if (!state.loading && !state.error && state.failures.length === 0) return null;
244
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
245
+ className: "dsh-model-catalog-status",
246
+ lang: zh ? "zh" : "en",
247
+ children: [
248
+ state.loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
249
+ role: "status",
250
+ children: copy.loading
251
+ }) : null,
252
+ state.error ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
253
+ role: "alert",
254
+ children: [
255
+ copy.error,
256
+ ": ",
257
+ state.error
258
+ ]
259
+ }) : null,
260
+ state.failures.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
261
+ role: "status",
262
+ 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: [
263
+ failure.name || failure.id,
264
+ ": ",
265
+ failure.message
266
+ ] }, `${failure.id}:${index}`)) })]
267
+ }) : null,
268
+ state.error || state.failures.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
269
+ type: "button",
270
+ size: "sm",
271
+ disabled: state.loading,
272
+ onClick: () => {
273
+ state.refresh();
274
+ },
275
+ children: copy.retry
276
+ }) : null
277
+ ]
278
+ });
279
+ }
280
+ //#endregion
172
281
  //#region ../dsh-model-route/src/ui.tsx
173
282
  /**
174
283
  * ModelMenu: the standard editor of one model-route field — the visible half
@@ -1178,6 +1287,29 @@ ${scope} {
1178
1287
  & .dsh-ui-select:disabled { color: var(--dsw-alias-label-tertiary); cursor: default; }
1179
1288
  & .dsh-ui-select[aria-invalid='true'] { border-color: var(--dsw-alias-state-error-primary); }
1180
1289
 
1290
+ /* --- Native textarea. The primitives ship no textarea either, so a settings
1291
+ * prompt keeps the platform element and the same field shell as the select:
1292
+ * 0.5px l4, radius-md, layer-3, 13/1.5 type. It grows vertically instead of
1293
+ * locking to the select's 34px row. --- */
1294
+ & .dsh-ui-textarea {
1295
+ box-sizing: border-box;
1296
+ width: 100%;
1297
+ min-width: 0;
1298
+ resize: vertical;
1299
+ padding: 8px 12px;
1300
+ border: 0.5px solid var(--dsw-alias-border-l4);
1301
+ border-radius: var(--dsw-radius-md);
1302
+ background: var(--dsw-alias-bg-layer-3);
1303
+ font: inherit;
1304
+ font-size: 13px;
1305
+ line-height: 1.5;
1306
+ color: var(--dsw-alias-label-primary);
1307
+ }
1308
+ & .dsh-ui-textarea:focus-visible { outline: none; border-color: var(--dsw-alias-state-business-primary); }
1309
+ & .dsh-ui-textarea:disabled { color: var(--dsw-alias-label-tertiary); cursor: default; }
1310
+ & .dsh-ui-textarea::placeholder { color: var(--dsw-alias-label-dimmed); }
1311
+ & .dsh-ui-textarea[aria-invalid='true'] { border-color: var(--dsw-alias-state-error-primary); }
1312
+
1181
1313
  /* Long panels scroll inside themselves instead of growing the host page. */
1182
1314
  & .dsh-ui-panel { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
1183
1315
  & .dsh-ui-panel-wide { max-width: 100%; }
@@ -1435,20 +1567,6 @@ function messageOf(error) {
1435
1567
  if (error && typeof error === "object" && "message" in error) return String(error.message);
1436
1568
  return String(error);
1437
1569
  }
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
1570
  //#endregion
1453
1571
  //#region src/client-copy.ts
1454
1572
  function hubText(value, language) {
@@ -1553,7 +1671,6 @@ const TEXT$1 = {
1553
1671
  loading: "正在读取…",
1554
1672
  failed: (message) => `读取失败:${message}`,
1555
1673
  syncFailed: (message) => `自动同步不可用:${message}`,
1556
- catalogFailed: (message) => `模型列表读取失败:${message}`,
1557
1674
  writeFailed: (message) => `未保存:${message}。请刷新后重试。`,
1558
1675
  saved: "已保存,正在刷新…",
1559
1676
  savedRefreshFailed: (message) => `已保存;阵容刷新失败:${message}。请刷新以读取最新设置。`,
@@ -1576,7 +1693,13 @@ const TEXT$1 = {
1576
1693
  effortDefault: "默认",
1577
1694
  searchModels: "搜索模型…",
1578
1695
  noModels: "没有匹配的模型",
1579
- clearSearch: "清除搜索"
1696
+ clearSearch: "清除搜索",
1697
+ runningTitle: "运行中的子代理",
1698
+ runningEmpty: "当前没有运行中的子代理。",
1699
+ runningPending: "尚未发起模型请求",
1700
+ runningRemote: "远程子代理,无法读取路由",
1701
+ runningFailed: (message) => `运行状态读取失败:${message}`,
1702
+ effortAdapterDefault: (effort) => `${effort}(适配器默认)`
1580
1703
  },
1581
1704
  en: {
1582
1705
  title: "Subagent Cast",
@@ -1585,7 +1708,6 @@ const TEXT$1 = {
1585
1708
  loading: "Loading…",
1586
1709
  failed: (message) => `Load failed: ${message}`,
1587
1710
  syncFailed: (message) => `Live sync unavailable: ${message}`,
1588
- catalogFailed: (message) => `Model list failed: ${message}`,
1589
1711
  writeFailed: (message) => `Not saved: ${message}. Refresh, then try again.`,
1590
1712
  saved: "Saved; refreshing…",
1591
1713
  savedRefreshFailed: (message) => `Saved; cast refresh failed: ${message}. Refresh to read the latest settings.`,
@@ -1608,7 +1730,13 @@ const TEXT$1 = {
1608
1730
  effortDefault: "Default",
1609
1731
  searchModels: "Search models…",
1610
1732
  noModels: "No matching models",
1611
- clearSearch: "Clear search"
1733
+ clearSearch: "Clear search",
1734
+ runningTitle: "Running subagents",
1735
+ runningEmpty: "No subagents are running.",
1736
+ runningPending: "No model request yet",
1737
+ runningRemote: "Remote subagent; route unavailable",
1738
+ runningFailed: (message) => `Running state failed: ${message}`,
1739
+ effortAdapterDefault: (effort) => `${effort} (adapter default)`
1612
1740
  }
1613
1741
  };
1614
1742
  function useText$1(locale) {
@@ -1631,6 +1759,11 @@ const castCss = `
1631
1759
  .dsh-model-hub .cast-banner{display:flex;gap:12px;align-items:center;justify-content:space-between;flex-wrap:wrap}
1632
1760
  .dsh-model-hub .cast-add{display:flex}
1633
1761
  .dsh-model-hub .cast-add .dsh-model-menu-root{max-width:100%}
1762
+ .dsh-model-hub .cast-running{margin-top:16px}
1763
+ .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)}
1764
+ .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)}
1765
+ .dsh-model-hub .cast-running-label{font-size:13px;line-height:20px;color:var(--dsw-alias-label-primary);overflow-wrap:anywhere;min-width:0}
1766
+ .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
1767
  @media(max-width:520px){.dsh-model-hub .cast-row{grid-template-columns:minmax(0,1fr);gap:8px}}
1635
1768
  `;
1636
1769
  /** CAS rejections all carry a refresh hint in either language. */
@@ -1654,6 +1787,14 @@ function routeInput(route) {
1654
1787
  model: normalized.model
1655
1788
  };
1656
1789
  }
1790
+ /** One live subagent row: label (or short id) plus its effective route. */
1791
+ function runningRouteText(entry, t) {
1792
+ if (!entry.local) return t("runningRemote");
1793
+ const route = entry.route;
1794
+ if (!route) return t("runningPending");
1795
+ const effort = route.reasoningEffort ? entry.effortIsAdapterDefault ? t("effortAdapterDefault", route.reasoningEffort) : route.reasoningEffort : "";
1796
+ return `${route.provider} / ${route.model}${effort ? ` · ${effort}` : ""}`;
1797
+ }
1657
1798
  /** One cast entry: model picker, tendency input saved on blur, remove button. */
1658
1799
  function CastRow({ t, catalog, route, tendencyRevision, draftReview, disabled, tendencyDisabled, menuDisabled, onSetRoute, onSetTendency, onRemove }) {
1659
1800
  const label = `${route.provider} / ${route.model}`;
@@ -1737,6 +1878,8 @@ function CastPage({ api, locale }) {
1737
1878
  const t = useText$1(locale);
1738
1879
  const [cast, setCast] = (0, react.useState)(null);
1739
1880
  const [catalog, setCatalog] = (0, react.useState)(null);
1881
+ const [running, setRunning] = (0, react.useState)(null);
1882
+ const [runningError, setRunningError] = (0, react.useState)("");
1740
1883
  const [loadError, setLoadError] = (0, react.useState)("");
1741
1884
  const [catalogError, setCatalogError] = (0, react.useState)("");
1742
1885
  const [syncError, setSyncError] = (0, react.useState)("");
@@ -1756,7 +1899,11 @@ function CastPage({ api, locale }) {
1756
1899
  scope.queued = false;
1757
1900
  scope.busy = true;
1758
1901
  setRefreshing(true);
1759
- const [castResult, models] = await Promise.allSettled([Promise.resolve().then(() => scope.api.getCast()), Promise.resolve().then(() => scope.api.modelCatalog()).then(checkedCatalog)]);
1902
+ const [castResult, models, live] = await Promise.allSettled([
1903
+ Promise.resolve().then(() => scope.api.getCast()),
1904
+ Promise.resolve().then(() => scope.api.modelCatalog()).then(checkedModelCatalog),
1905
+ Promise.resolve().then(() => scope.api.listRunning())
1906
+ ]);
1760
1907
  if (!scope.active) return;
1761
1908
  if (castResult.status === "fulfilled") {
1762
1909
  setCast(castResult.value);
@@ -1770,6 +1917,10 @@ function CastPage({ api, locale }) {
1770
1917
  setCatalog(models.value);
1771
1918
  setCatalogError("");
1772
1919
  } else setCatalogError(messageOf(models.reason));
1920
+ if (live.status === "fulfilled") {
1921
+ setRunning(live.value);
1922
+ setRunningError("");
1923
+ } else setRunningError(messageOf(live.reason));
1773
1924
  scope.busy = false;
1774
1925
  setRefreshing(false);
1775
1926
  if (scope.queued) refresh(scope);
@@ -1791,6 +1942,8 @@ function CastPage({ api, locale }) {
1791
1942
  setSyncError("");
1792
1943
  setCast(null);
1793
1944
  setCatalog(null);
1945
+ setRunning(null);
1946
+ setRunningError("");
1794
1947
  refresh(scope);
1795
1948
  const unsubscribe = api.subscribeChanges?.(() => {
1796
1949
  if (!scope.active) return;
@@ -1833,12 +1986,17 @@ function CastPage({ api, locale }) {
1833
1986
  }
1834
1987
  });
1835
1988
  };
1836
- const refreshNow = () => {
1989
+ const refreshNow = async () => {
1837
1990
  const scope = scopeRef.current;
1838
- if (scope?.api === api) refresh(scope, true);
1991
+ if (scope?.api === api && !scope.busy) await refresh(scope, true);
1839
1992
  };
1840
1993
  const disabled = saving !== "" || refreshing || writeError !== "" || saved && loadError !== "";
1841
- const failures = catalogFailures(catalog);
1994
+ const catalogStatus = {
1995
+ loading: refreshing,
1996
+ error: catalogError ? hubMessage(catalogError, t.language) : "",
1997
+ failures: modelCatalogFailures(catalog),
1998
+ refresh: refreshNow
1999
+ };
1842
2000
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1843
2001
  className: "cast-root dsh-ui-stack",
1844
2002
  "aria-label": t("title"),
@@ -1883,16 +2041,10 @@ function CastPage({ api, locale }) {
1883
2041
  className: "dsh-ui-error dsh-ui-wrap",
1884
2042
  children: t("syncFailed", hubMessage(syncError, t.language))
1885
2043
  }) : 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)),
2044
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelCatalogStatus, {
2045
+ state: catalogStatus,
2046
+ locale: t.language
2047
+ }),
1896
2048
  writeError ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1897
2049
  className: "dsh-ui-banner dsh-ui-banner--danger cast-banner",
1898
2050
  role: "alert",
@@ -1969,7 +2121,40 @@ function CastPage({ api, locale }) {
1969
2121
  expectedRevision: cast.revision
1970
2122
  }))
1971
2123
  })
1972
- }) : null
2124
+ }) : null,
2125
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2126
+ className: "cast-running",
2127
+ "aria-label": t("runningTitle"),
2128
+ children: [
2129
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
2130
+ className: "cast-running-title",
2131
+ children: t("runningTitle")
2132
+ }),
2133
+ runningError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2134
+ role: "alert",
2135
+ className: "dsh-ui-error dsh-ui-wrap",
2136
+ children: t("runningFailed", hubMessage(runningError, t.language))
2137
+ }) : null,
2138
+ running === null && !runningError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2139
+ className: "dsh-ui-loading",
2140
+ children: t("loading")
2141
+ }) : null,
2142
+ running !== null && running.entries.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2143
+ className: "dsh-ui-help",
2144
+ children: t("runningEmpty")
2145
+ }) : null,
2146
+ running?.entries.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2147
+ className: "cast-running-row",
2148
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2149
+ className: "cast-running-label",
2150
+ children: entry.label || entry.id
2151
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2152
+ className: "cast-running-route",
2153
+ children: runningRouteText(entry, t)
2154
+ })]
2155
+ }, entry.id))
2156
+ ]
2157
+ })
1973
2158
  ]
1974
2159
  });
1975
2160
  }
@@ -1987,8 +2172,7 @@ const name = "dsh-model-hub-client";
1987
2172
  const inject = [
1988
2173
  "slots",
1989
2174
  "connection",
1990
- "remote",
1991
- "remote.session",
2175
+ ...MODEL_CATALOG_INJECT,
1992
2176
  "locale"
1993
2177
  ];
1994
2178
  const CHANNEL = "/dsh-model-hub";
@@ -2120,7 +2304,7 @@ function HubPage({ view, api, locale, initialEntries = null, initialCatalog = nu
2120
2304
  scope.queued = false;
2121
2305
  scope.busy = true;
2122
2306
  setRefreshing(true);
2123
- const [fields, models] = await Promise.allSettled([Promise.resolve().then(() => scope.api.list()), Promise.resolve().then(() => scope.api.modelCatalog()).then(checkedCatalog)]);
2307
+ const [fields, models] = await Promise.allSettled([Promise.resolve().then(() => scope.api.list()), Promise.resolve().then(() => scope.api.modelCatalog()).then(checkedModelCatalog)]);
2124
2308
  if (!scope.active) return;
2125
2309
  if (fields.status === "fulfilled") {
2126
2310
  setEntries(fields.value);
@@ -2233,7 +2417,16 @@ function HubPage({ view, api, locale, initialEntries = null, initialCatalog = nu
2233
2417
  })
2234
2418
  })).filter((entry) => entry.fields.length > 0 || (entry.error || entry.catalogError) && (!needle || [entry.ns, pluginTitle(entry, t)].some((text) => text.toLocaleLowerCase().includes(needle))));
2235
2419
  const disabled = writing !== "" || refreshing || writeError !== "" || saved && listError !== "";
2236
- const failures = catalogFailures(catalog);
2420
+ const refreshNow = async () => {
2421
+ const scope = scopeRef.current;
2422
+ if (scope?.api === api && !scope.busy) await refresh(scope);
2423
+ };
2424
+ const catalogStatus = {
2425
+ loading: refreshing,
2426
+ error: catalogError ? hubMessage(catalogError, t.language) : "",
2427
+ failures: modelCatalogFailures(catalog),
2428
+ refresh: refreshNow
2429
+ };
2237
2430
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2238
2431
  className: "dsh-model-hub dsh-ui-stack",
2239
2432
  lang: t.language,
@@ -2277,10 +2470,7 @@ function HubPage({ view, api, locale, initialEntries = null, initialCatalog = nu
2277
2470
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2278
2471
  variant: "ghost",
2279
2472
  disabled: writing !== "" || refreshing,
2280
- onClick: () => {
2281
- const scope = scopeRef.current;
2282
- if (scope?.api === api) refresh(scope);
2283
- },
2473
+ onClick: refreshNow,
2284
2474
  children: refreshing ? t("refreshing") : t("refresh")
2285
2475
  })]
2286
2476
  }),
@@ -2294,16 +2484,10 @@ function HubPage({ view, api, locale, initialEntries = null, initialCatalog = nu
2294
2484
  className: "dsh-ui-error dsh-ui-wrap",
2295
2485
  children: t("syncFailed", hubMessage(syncError, t.language))
2296
2486
  }) : 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)),
2487
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelCatalogStatus, {
2488
+ state: catalogStatus,
2489
+ locale: t.language
2490
+ }),
2307
2491
  writeError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2308
2492
  role: "alert",
2309
2493
  className: "dsh-ui-error dsh-ui-wrap",
@@ -2423,14 +2607,15 @@ async function apply(ctx) {
2423
2607
  const api = Object.freeze({
2424
2608
  list: () => call("fields.list", {}),
2425
2609
  setField: (payload) => call("fields.set", payload),
2426
- modelCatalog: () => client.remote.session.modelCatalog(),
2610
+ modelCatalog: () => readModelCatalog(client),
2427
2611
  subscribeChanges: changes.subscribe,
2428
2612
  getCast: () => call("cast.get", {}),
2429
2613
  setCastEnabled: (payload) => call("cast.setEnabled", payload),
2430
2614
  addCastRoute: (payload) => call("cast.addRoute", payload),
2431
2615
  setCastRoute: (payload) => call("cast.setRoute", payload),
2432
2616
  removeCastRoute: (payload) => call("cast.removeRoute", payload),
2433
- setCastTendency: (payload) => call("cast.setTendency", payload)
2617
+ setCastTendency: (payload) => call("cast.setTendency", payload),
2618
+ listRunning: () => call("cast.running", {})
2434
2619
  });
2435
2620
  client.slots.inject("plugins.bundle.config", () => client.slots.register({
2436
2621
  name: "plugins.bundle.config",
package/lib/index.js CHANGED
@@ -6,7 +6,7 @@ import { modelRouteRoleSchemastery, modelRouteSchemastery } from "@klarkxy/dsh-m
6
6
  import { detectModelFields, modelRouteOverride, normalizeModelRoute } from "@klarkxy/dsh-model-route";
7
7
  import { MODEL_SETTINGS_CHANGED_EVENT, collectModelSettings, notifyModelSettingsChanged } from "@klarkxy/dsh-plugin-kit/model-settings";
8
8
  import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
9
- import { MODEL_SETTINGS_CHANGES_REMOTE } from "@klarkxy/dsh-plugin-kit/contracts";
9
+ import { MODEL_SETTINGS_CHANGES_REMOTE, isSubagentSession } from "@klarkxy/dsh-plugin-kit/contracts";
10
10
  import { parseModelSettingsChangeFrame } from "@klarkxy/dsh-plugin-kit/model-settings-client";
11
11
  //#region src/probe.ts
12
12
  /**
@@ -457,9 +457,15 @@ function validateIndex(value) {
457
457
  */
458
458
  function buildCastPrompt(enabled, routes) {
459
459
  if (!enabled || !routes.length) return "";
460
- const annotated = routes.filter((route) => typeof route.tendency === "string" && route.tendency);
461
- if (!annotated.length) return "";
462
- return ["Subagent cast: the user has annotated the models available for subagent delegation with task tendencies. When delegating to a subagent, prefer a model whose tendency matches the task at hand; treat models without a tendency as general-purpose. These tendencies are advisory — the final model choice is always yours.", ...annotated.map((route) => `- ${route.provider}/${route.model}${route.reasoningEffort ? ` (reasoning effort: ${route.reasoningEffort})` : ""}: ${route.tendency}`)].join("\n");
460
+ const annotated = routes.filter((route) => typeof route.tendency === "string" && route.tendency.trim());
461
+ const guidance = [
462
+ "Subagent model selection:",
463
+ "When delegating an independent task, actively choose a suitable model instead of silently inheriting your own. If list_subagent_models and a delegation tool with provider/model fields are available, inspect the available routes before the first model-selectable delegation in this task, reuse those results for later delegations, and supply provider and model together explicitly.",
464
+ "For independent reviews, alternative approaches, or disputed conclusions, prefer a model different from your own when a suitable candidate is available. Choose for task fit and the user's constraints; using the same model is appropriate when it fits better or no suitable alternative is available. Do not create extra subtasks solely for model diversity.",
465
+ "The current Session's discovery results and tool schema are authoritative. Configured model notes may refer to routes unavailable in this Session. If discovery or model-selection fields are unavailable, use the supported delegation behavior; do not invent routes or pass unsupported fields to tools such as subagent_fork. When changing models, omit reasoning_effort unless you have verified a supported value."
466
+ ];
467
+ if (annotated.length) guidance.push("User-provided task tendencies (advisory): prefer a matching tendency among available models. Models without a tendency remain general-purpose candidates; no model-specific strengths are assumed.", ...annotated.map((route) => `- ${route.provider}/${route.model}${route.reasoningEffort ? ` (reasoning effort: ${route.reasoningEffort})` : ""}: ${route.tendency}`));
468
+ return guidance.join("\n");
463
469
  }
464
470
  function createCast(deps) {
465
471
  const castFile = () => {
@@ -683,7 +689,7 @@ function createCast(deps) {
683
689
  return buildCastPrompt(snapshot.enabled, mergeTendencies(snapshot.routes, readTendencies().tendencies));
684
690
  } catch (error) {
685
691
  deps.logger?.warn("model-hub: cast tendencies unavailable: %s", error.message);
686
- return "";
692
+ return buildCastPrompt(snapshot.enabled, snapshot.routes);
687
693
  }
688
694
  };
689
695
  return {
@@ -697,6 +703,128 @@ function createCast(deps) {
697
703
  };
698
704
  }
699
705
  //#endregion
706
+ //#region src/running.ts
707
+ /**
708
+ * dsh-model-hub — running subagent routes (host half).
709
+ *
710
+ * Read-only live view answering "which model and reasoning effort is each
711
+ * running subagent actually using". `subagent/start` and `subagent/end`
712
+ * maintain the live set; each child's effective route is read from its
713
+ * session epoch header (`session.requestHeader()`) and kept current through
714
+ * the session's own `request/header` events (reason `change` covers mid-run
715
+ * model switches). Children already running when this plugin (re)loads are
716
+ * recovered from the live agent registry via `isSubagentSession`.
717
+ *
718
+ * The effective route is session data recorded by the host; this tracker
719
+ * never writes to sessions or settings. Display-only invalidations reuse the
720
+ * model-settings change channel with RUNNING_NAMESPACE so open pages refetch.
721
+ */
722
+ /** Invalidation namespace for the running-subagent view; carries no settings. */
723
+ const RUNNING_NAMESPACE = "@klarkxy/dsh-model-hub/running-subagents";
724
+ function text(value) {
725
+ return typeof value === "string" ? value.trim() : "";
726
+ }
727
+ /** Normalize one epoch header into the display route plus the adapter-default flag. */
728
+ function routeFromHeader(header) {
729
+ const config = header?.config;
730
+ const provider = text(config?.provider);
731
+ const model = text(config?.model);
732
+ if (!provider || !model) return {};
733
+ const effort = text(config?.reasoningEffort);
734
+ return {
735
+ route: effort ? {
736
+ provider,
737
+ model,
738
+ reasoningEffort: effort
739
+ } : {
740
+ provider,
741
+ model
742
+ },
743
+ ...effort && header?.adapterDefaults?.reasoningEffort === true ? { effortIsAdapterDefault: true } : {}
744
+ };
745
+ }
746
+ function sameRoute(left, right) {
747
+ return left.route?.provider === right.route?.provider && left.route?.model === right.route?.model && left.route?.reasoningEffort === right.route?.reasoningEffort && left.effortIsAdapterDefault === right.effortIsAdapterDefault;
748
+ }
749
+ function createRunningTracker(deps) {
750
+ const entries = /* @__PURE__ */ new Map();
751
+ let revision = 0;
752
+ const bump = () => deps.notify(++revision);
753
+ const add = (id, info) => {
754
+ const session = (deps.agents?.get(id))?.session;
755
+ const next = routeFromHeader(session?.requestHeader?.());
756
+ const existing = entries.get(id);
757
+ if (existing) {
758
+ const providerName = info?.provider ?? existing.providerName;
759
+ const local = info?.local ?? existing.local;
760
+ const changed = existing.providerName !== providerName || existing.local !== local || !sameRoute(existing, next);
761
+ existing.providerName = providerName;
762
+ existing.local = local;
763
+ existing.route = next.route;
764
+ existing.effortIsAdapterDefault = next.effortIsAdapterDefault;
765
+ if (changed) bump();
766
+ return;
767
+ }
768
+ entries.set(id, {
769
+ id,
770
+ local: info?.local !== false,
771
+ ...info?.provider ? { providerName: info.provider } : {},
772
+ ...typeof session?.header?.parentSession === "string" && session.header.parentSession ? { parentSessionId: session.header.parentSession } : {},
773
+ ...typeof session?.header?.createdAt === "number" ? { startedAt: session.header.createdAt } : {},
774
+ route: next.route,
775
+ effortIsAdapterDefault: next.effortIsAdapterDefault
776
+ });
777
+ bump();
778
+ };
779
+ const remove = (id) => {
780
+ if (entries.delete(id)) bump();
781
+ };
782
+ try {
783
+ for (const agent of deps.agents?.list() ?? []) if (agent?.id && isSubagentSession(agent.session)) add(agent.id);
784
+ } catch {}
785
+ deps.events.on("subagent/start", (info) => {
786
+ if (typeof info?.id === "string" && info.id) add(info.id, info);
787
+ });
788
+ deps.events.on("subagent/end", (info) => {
789
+ if (typeof info?.id === "string") remove(info.id);
790
+ });
791
+ deps.events.on("agent/disposed", (payload) => {
792
+ if (typeof payload?.agent?.id === "string") remove(payload.agent.id);
793
+ });
794
+ deps.events.on("session/event", (session, event) => {
795
+ if (event?.type !== "request/header") return;
796
+ const id = typeof session?.id === "string" ? session.id : void 0;
797
+ const entry = id ? entries.get(id) : void 0;
798
+ if (!entry) return;
799
+ const next = routeFromHeader(event.data?.header);
800
+ if (sameRoute(entry, next)) return;
801
+ entry.route = next.route;
802
+ entry.effortIsAdapterDefault = next.effortIsAdapterDefault;
803
+ bump();
804
+ });
805
+ return { async list(signal) {
806
+ const pendingParents = /* @__PURE__ */ new Set();
807
+ for (const entry of entries.values()) if (entry.label === void 0 && entry.parentSessionId) pendingParents.add(entry.parentSessionId);
808
+ if (deps.subagents && pendingParents.size) await Promise.allSettled([...pendingParents].map(async (parent) => {
809
+ const children = await deps.subagents.listChildren(parent, signal);
810
+ const labels = new Map(children.map((child) => [child.id, child.label]));
811
+ for (const entry of entries.values()) {
812
+ if (entry.parentSessionId !== parent || entry.label !== void 0) continue;
813
+ const label = labels.get(entry.id);
814
+ if (typeof label === "string" && label.trim()) entry.label = label.trim();
815
+ }
816
+ }));
817
+ const snapshot = [...entries.values()].map((entry) => ({
818
+ ...entry,
819
+ route: entry.route ? { ...entry.route } : void 0
820
+ })).sort((a, b) => (a.startedAt ?? 0) - (b.startedAt ?? 0) || a.id.localeCompare(b.id));
821
+ return {
822
+ revision,
823
+ entries: snapshot
824
+ };
825
+ } };
826
+ }
827
+ //#endregion
700
828
  //#region src/index.ts
701
829
  /**
702
830
  * dsh-model-hub — cross-plugin model settings hub.
@@ -751,6 +879,12 @@ function apply(ctx) {
751
879
  logger: ctx.logger,
752
880
  notifyTendenciesChanged: (revision) => notifyModelSettingsChanged(ctx, CAST_TENDENCIES_NAMESPACE, revision)
753
881
  });
882
+ const running = createRunningTracker({
883
+ events: ctx,
884
+ agents: ctx.get("agents"),
885
+ subagents: ctx.get("subagents"),
886
+ notify: (revision) => notifyModelSettingsChanged(ctx, RUNNING_NAMESPACE, revision)
887
+ });
754
888
  ctx.systemPrompt.section({
755
889
  name: "model-hub:subagent-cast",
756
890
  order: ctx.systemPrompt.getSectionOrder("TOOL_SUBAGENT") + 1,
@@ -812,6 +946,10 @@ function apply(ctx) {
812
946
  ok: true,
813
947
  value: cast.setTendency(row.provider, row.model, row.tendency, row.expectedRevision, signal)
814
948
  };
949
+ if (endpoint === "cast.running") return {
950
+ ok: true,
951
+ value: await running.list(signal)
952
+ };
815
953
  }
816
954
  throw new Error(`Unknown endpoint: ${endpoint}`);
817
955
  } catch (error) {