@klarkxy/dsh-model-hub 0.1.2 → 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/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  [简体中文](docs/README.zh-CN.md)
6
6
 
7
- See and change the models used by your DeepSeek Harness plugins from one page, and manage which models subagents may use.
7
+ See and change the models used by your DeepSeek Harness plugins from one page, manage which models subagents may use, and watch which model and reasoning effort each running subagent is on.
8
8
 
9
9
  ## Install
10
10
 
@@ -18,13 +18,17 @@ Replace `web` with your profile name and restart the profile.
18
18
 
19
19
  Open **Plugins → Model Hub**.
20
20
 
21
- - **Fields**: every model setting exposed by installed plugins, such as Auto Title, Git Commit, Safe Auto, and ChatGPT. Search by plugin, purpose, or current model, then pick a new model. Changes are saved by the plugin that owns the setting.
22
- - **Subagent Cast**: turn the subagent model allowlist on or off and add, replace, or remove models. You can give each model a short task tendency; the main agent reads it as a hint when choosing a subagent model, not as a rule.
21
+ - **Fields**: every model setting exposed by installed plugins, such as Auto Title, Git Commit, and Safe Auto. Search by plugin, purpose, or current model, then pick a new model. Changes are saved by the plugin that owns the setting.
22
+ - **Subagent Cast**: turn the subagent model allowlist on or off and add, replace, or remove models. When enabled with at least one model, it guides the agent to discover the models available in the current conversation and explicitly choose one when delegating. For independent reviews and alternative approaches, it encourages a different model when suitable, while allowing the same model when it fits better. You can add your own task tendencies for individual models; they remain optional hints, and no model-specific tendencies are supplied by default. Below the list, a live view shows each running subagent with the model and reasoning effort it is currently using; an effort marked as adapter default comes from the adapter rather than an explicit choice.
23
23
 
24
24
  ## Limits
25
25
 
26
26
  - Only plugins that expose model settings appear.
27
27
  - If a setting changed elsewhere, refresh and choose again.
28
+ - If the model list cannot load, use **Retry**. The last available list stays visible; warnings identify providers that could not be loaded while other providers remain usable.
29
+ - Model selection guidance requires host tools that support choosing a subagent model. Existing conversations retain their saved model lists; start a new conversation to use an updated list.
30
+ - Guidance does not force a model choice or add extra subtasks solely for variety.
31
+ - Remote subagents and finished runs have no readable route; the live list covers only live local subagents.
28
32
  - No usage statistics.
29
33
 
30
34
  ## License
@@ -4,7 +4,7 @@
4
4
 
5
5
  [English](../README.md)
6
6
 
7
- 在一个页面查看并修改各个 DeepSeek Harness 插件使用的模型,也可以管理子代理可用的模型。
7
+ 在一个页面查看并修改各个 DeepSeek Harness 插件使用的模型,管理子代理可用的模型,并查看运行中子代理当前使用的模型与思考强度。
8
8
 
9
9
  ## 安装
10
10
 
@@ -18,13 +18,17 @@ dsh plugin --profile web add @klarkxy/dsh-model-hub
18
18
 
19
19
  打开「插件 → 模型枢纽」。
20
20
 
21
- - **字段总览**:列出已安装插件提供的模型设置,例如自动标题、Git 提交、安全自动和 ChatGPT。可以按插件、用途或当前模型搜索,再选择新模型;改动由对应插件保存。
22
- - **子代理阵容**:开关子代理模型白名单,添加、替换或移除模型。每个模型可以写一句「任务倾向」,主智能体选择子代理模型时会参考它,但不强制。
21
+ - **字段总览**:列出已安装插件提供的模型设置,例如自动标题、Git 提交和安全自动。可以按插件、用途或当前模型搜索,再选择新模型;改动由对应插件保存。
22
+ - **子代理阵容**:开关子代理模型白名单,添加、替换或移除模型。开启且至少有一个模型时,会引导智能体发现当前会话可用的模型,并在派发时显式选择。独立复核和方案对比会优先考虑合适的不同模型,同模型更适合时仍可使用。你可以自行填写各模型的「任务倾向」作为可选参考;插件不预置模型专用倾向。页面下方实时列出运行中的子代理及各自当前使用的模型和思考强度;标注「适配器默认」的强度来自适配器而非显式选择。
23
23
 
24
24
  ## 限制
25
25
 
26
26
  - 只显示提供了模型设置的插件。
27
27
  - 设置在别处被改动时,刷新后重新选择。
28
+ - 模型列表读取失败时,点击「重试」。上次可用的列表会保留;部分提供方读取失败时会显示提示,其他提供方仍可使用。
29
+ - 选模引导需要宿主工具支持选择子代理模型。已有会话保留各自的模型名单;修改名单后,新建会话才能使用更新后的名单。
30
+ - 引导不强制模型选择,也不要求仅为增加模型种类而新增子任务。
31
+ - 远程子代理和已结束的子代理没有可读取的路由;运行列表只覆盖存活的本地子代理。
28
32
  - 不提供用量统计。
29
33
 
30
34
  ## 许可证
@@ -61,12 +61,68 @@ function normalizeModelRoute(value) {
61
61
  }
62
62
  //#endregion
63
63
  //#region ../dsh-model-route/src/catalog.ts
64
+ /** Both services are required: Cordis checks the nested namespace separately. */
65
+ const MODEL_CATALOG_INJECT = ["remote", "remote.session"];
64
66
  function asRecord(value) {
65
67
  return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
66
68
  }
67
69
  function asText(value) {
68
70
  return typeof value === "string" && value.trim() ? value.trim() : void 0;
69
71
  }
72
+ /** A useful diagnostic even when a transport rejects with a plain object. */
73
+ function modelCatalogErrorMessage(value) {
74
+ if (value instanceof Error && value.message) return value.message;
75
+ return asText(asRecord(value)?.message) ?? asText(value) ?? "Unable to load the model catalog.";
76
+ }
77
+ /**
78
+ * Validate and unwrap a successful catalog without discarding provider failures
79
+ * or newer host metadata. Names and metadata remain optional for older hosts.
80
+ * A broken response is never presented as a successfully loaded empty menu.
81
+ */
82
+ function checkedModelCatalog(value) {
83
+ const envelope = asRecord(value);
84
+ if (envelope?.ok === false) throw new Error(modelCatalogErrorMessage(envelope.error ?? envelope));
85
+ const root = envelope?.ok === true ? asRecord(envelope.value) : envelope;
86
+ const malformed = (field) => {
87
+ throw new Error(`Invalid model catalog: ${field}.`);
88
+ };
89
+ if (!root || !Array.isArray(root.groups)) return malformed("groups must be an array");
90
+ for (const value of root.groups) {
91
+ const group = asRecord(value);
92
+ if (!group || !asText(group.id) || !Array.isArray(group.models)) return malformed("each group needs an id and models array");
93
+ for (const value of group.models) {
94
+ const model = asRecord(value);
95
+ if (!model || !asText(model.id)) return malformed("each model needs an id");
96
+ if (model.reasoning !== void 0) {
97
+ const reasoning = asRecord(model.reasoning);
98
+ if (!reasoning || !Array.isArray(reasoning.efforts) || reasoning.efforts.some((value) => !parseEffort(value))) return malformed("reasoning efforts must be an array of identified efforts");
99
+ }
100
+ }
101
+ }
102
+ if (root.failures !== void 0 && (!Array.isArray(root.failures) || root.failures.some((value) => {
103
+ const failure = asRecord(value);
104
+ return !failure || !asText(failure.id) || typeof failure.name !== "string" || typeof failure.message !== "string";
105
+ }))) return malformed("failures must be an array of provider failures");
106
+ return root;
107
+ }
108
+ /** Read through the injected session namespace; absence and rejection are errors. */
109
+ async function readModelCatalog(client) {
110
+ const session = client.remote?.session;
111
+ if (typeof session?.modelCatalog !== "function") throw new Error("Model catalog unavailable: inject both remote and remote.session.");
112
+ return checkedModelCatalog(await session.modelCatalog());
113
+ }
114
+ /** Provider failures are partial results, not a transport failure. */
115
+ function modelCatalogFailures(value) {
116
+ try {
117
+ return checkedModelCatalog(value).failures ?? [];
118
+ } catch {
119
+ return [];
120
+ }
121
+ }
122
+ /** Text adapter for existing model-hub status messages. */
123
+ function catalogFailures(value) {
124
+ return modelCatalogFailures(value).map((failure) => `${failure.name || failure.id}: ${failure.message}`);
125
+ }
70
126
  function parseEffort(value) {
71
127
  const row = asRecord(value);
72
128
  const id = row ? asText(row.id) : void 0;
@@ -163,6 +219,59 @@ function groupModelMenuChoices(choices, query) {
163
219
  return groups;
164
220
  }
165
221
  //#endregion
222
+ //#region ../dsh-model-route/src/catalog-ui.tsx
223
+ /** Also accepts a mapped state from a caller that owns broader async orchestration. */
224
+ function ModelCatalogStatus({ state, locale }) {
225
+ const zh = locale?.toLowerCase().startsWith("zh") === true;
226
+ const copy = zh ? {
227
+ loading: "正在加载模型…",
228
+ error: "模型列表加载失败",
229
+ partial: "部分提供商的模型未能加载",
230
+ retry: "重试"
231
+ } : {
232
+ loading: "Loading models…",
233
+ error: "Unable to load models",
234
+ partial: "Some providers could not load their models",
235
+ retry: "Retry"
236
+ };
237
+ if (!state.loading && !state.error && state.failures.length === 0) return null;
238
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
239
+ className: "dsh-model-catalog-status",
240
+ lang: zh ? "zh" : "en",
241
+ children: [
242
+ state.loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
243
+ role: "status",
244
+ children: copy.loading
245
+ }) : null,
246
+ state.error ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
247
+ role: "alert",
248
+ children: [
249
+ copy.error,
250
+ ": ",
251
+ state.error
252
+ ]
253
+ }) : null,
254
+ state.failures.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
255
+ role: "status",
256
+ 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: [
257
+ failure.name || failure.id,
258
+ ": ",
259
+ failure.message
260
+ ] }, `${failure.id}:${index}`)) })]
261
+ }) : null,
262
+ state.error || state.failures.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
263
+ type: "button",
264
+ size: "sm",
265
+ disabled: state.loading,
266
+ onClick: () => {
267
+ state.refresh();
268
+ },
269
+ children: copy.retry
270
+ }) : null
271
+ ]
272
+ });
273
+ }
274
+ //#endregion
166
275
  //#region ../dsh-model-route/src/ui.tsx
167
276
  /**
168
277
  * ModelMenu: the standard editor of one model-route field — the visible half
@@ -1172,6 +1281,29 @@ ${scope} {
1172
1281
  & .dsh-ui-select:disabled { color: var(--dsw-alias-label-tertiary); cursor: default; }
1173
1282
  & .dsh-ui-select[aria-invalid='true'] { border-color: var(--dsw-alias-state-error-primary); }
1174
1283
 
1284
+ /* --- Native textarea. The primitives ship no textarea either, so a settings
1285
+ * prompt keeps the platform element and the same field shell as the select:
1286
+ * 0.5px l4, radius-md, layer-3, 13/1.5 type. It grows vertically instead of
1287
+ * locking to the select's 34px row. --- */
1288
+ & .dsh-ui-textarea {
1289
+ box-sizing: border-box;
1290
+ width: 100%;
1291
+ min-width: 0;
1292
+ resize: vertical;
1293
+ padding: 8px 12px;
1294
+ border: 0.5px solid var(--dsw-alias-border-l4);
1295
+ border-radius: var(--dsw-radius-md);
1296
+ background: var(--dsw-alias-bg-layer-3);
1297
+ font: inherit;
1298
+ font-size: 13px;
1299
+ line-height: 1.5;
1300
+ color: var(--dsw-alias-label-primary);
1301
+ }
1302
+ & .dsh-ui-textarea:focus-visible { outline: none; border-color: var(--dsw-alias-state-business-primary); }
1303
+ & .dsh-ui-textarea:disabled { color: var(--dsw-alias-label-tertiary); cursor: default; }
1304
+ & .dsh-ui-textarea::placeholder { color: var(--dsw-alias-label-dimmed); }
1305
+ & .dsh-ui-textarea[aria-invalid='true'] { border-color: var(--dsw-alias-state-error-primary); }
1306
+
1175
1307
  /* Long panels scroll inside themselves instead of growing the host page. */
1176
1308
  & .dsh-ui-panel { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
1177
1309
  & .dsh-ui-panel-wide { max-width: 100%; }
@@ -1429,20 +1561,6 @@ function messageOf(error) {
1429
1561
  if (error && typeof error === "object" && "message" in error) return String(error.message);
1430
1562
  return String(error);
1431
1563
  }
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
1564
  //#endregion
1447
1565
  //#region src/client-copy.ts
1448
1566
  function hubText(value, language) {
@@ -1547,7 +1665,6 @@ const TEXT$1 = {
1547
1665
  loading: "正在读取…",
1548
1666
  failed: (message) => `读取失败:${message}`,
1549
1667
  syncFailed: (message) => `自动同步不可用:${message}`,
1550
- catalogFailed: (message) => `模型列表读取失败:${message}`,
1551
1668
  writeFailed: (message) => `未保存:${message}。请刷新后重试。`,
1552
1669
  saved: "已保存,正在刷新…",
1553
1670
  savedRefreshFailed: (message) => `已保存;阵容刷新失败:${message}。请刷新以读取最新设置。`,
@@ -1570,7 +1687,13 @@ const TEXT$1 = {
1570
1687
  effortDefault: "默认",
1571
1688
  searchModels: "搜索模型…",
1572
1689
  noModels: "没有匹配的模型",
1573
- clearSearch: "清除搜索"
1690
+ clearSearch: "清除搜索",
1691
+ runningTitle: "运行中的子代理",
1692
+ runningEmpty: "当前没有运行中的子代理。",
1693
+ runningPending: "尚未发起模型请求",
1694
+ runningRemote: "远程子代理,无法读取路由",
1695
+ runningFailed: (message) => `运行状态读取失败:${message}`,
1696
+ effortAdapterDefault: (effort) => `${effort}(适配器默认)`
1574
1697
  },
1575
1698
  en: {
1576
1699
  title: "Subagent Cast",
@@ -1579,7 +1702,6 @@ const TEXT$1 = {
1579
1702
  loading: "Loading…",
1580
1703
  failed: (message) => `Load failed: ${message}`,
1581
1704
  syncFailed: (message) => `Live sync unavailable: ${message}`,
1582
- catalogFailed: (message) => `Model list failed: ${message}`,
1583
1705
  writeFailed: (message) => `Not saved: ${message}. Refresh, then try again.`,
1584
1706
  saved: "Saved; refreshing…",
1585
1707
  savedRefreshFailed: (message) => `Saved; cast refresh failed: ${message}. Refresh to read the latest settings.`,
@@ -1602,7 +1724,13 @@ const TEXT$1 = {
1602
1724
  effortDefault: "Default",
1603
1725
  searchModels: "Search models…",
1604
1726
  noModels: "No matching models",
1605
- clearSearch: "Clear search"
1727
+ clearSearch: "Clear search",
1728
+ runningTitle: "Running subagents",
1729
+ runningEmpty: "No subagents are running.",
1730
+ runningPending: "No model request yet",
1731
+ runningRemote: "Remote subagent; route unavailable",
1732
+ runningFailed: (message) => `Running state failed: ${message}`,
1733
+ effortAdapterDefault: (effort) => `${effort} (adapter default)`
1606
1734
  }
1607
1735
  };
1608
1736
  function useText$1(locale) {
@@ -1625,6 +1753,11 @@ const castCss = `
1625
1753
  .dsh-model-hub .cast-banner{display:flex;gap:12px;align-items:center;justify-content:space-between;flex-wrap:wrap}
1626
1754
  .dsh-model-hub .cast-add{display:flex}
1627
1755
  .dsh-model-hub .cast-add .dsh-model-menu-root{max-width:100%}
1756
+ .dsh-model-hub .cast-running{margin-top:16px}
1757
+ .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)}
1758
+ .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)}
1759
+ .dsh-model-hub .cast-running-label{font-size:13px;line-height:20px;color:var(--dsw-alias-label-primary);overflow-wrap:anywhere;min-width:0}
1760
+ .dsh-model-hub .cast-running-route{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere;min-width:0}
1628
1761
  @media(max-width:520px){.dsh-model-hub .cast-row{grid-template-columns:minmax(0,1fr);gap:8px}}
1629
1762
  `;
1630
1763
  /** CAS rejections all carry a refresh hint in either language. */
@@ -1648,6 +1781,14 @@ function routeInput(route) {
1648
1781
  model: normalized.model
1649
1782
  };
1650
1783
  }
1784
+ /** One live subagent row: label (or short id) plus its effective route. */
1785
+ function runningRouteText(entry, t) {
1786
+ if (!entry.local) return t("runningRemote");
1787
+ const route = entry.route;
1788
+ if (!route) return t("runningPending");
1789
+ const effort = route.reasoningEffort ? entry.effortIsAdapterDefault ? t("effortAdapterDefault", route.reasoningEffort) : route.reasoningEffort : "";
1790
+ return `${route.provider} / ${route.model}${effort ? ` · ${effort}` : ""}`;
1791
+ }
1651
1792
  /** One cast entry: model picker, tendency input saved on blur, remove button. */
1652
1793
  function CastRow({ t, catalog, route, tendencyRevision, draftReview, disabled, tendencyDisabled, menuDisabled, onSetRoute, onSetTendency, onRemove }) {
1653
1794
  const label = `${route.provider} / ${route.model}`;
@@ -1731,6 +1872,8 @@ function CastPage({ api, locale }) {
1731
1872
  const t = useText$1(locale);
1732
1873
  const [cast, setCast] = (0, react.useState)(null);
1733
1874
  const [catalog, setCatalog] = (0, react.useState)(null);
1875
+ const [running, setRunning] = (0, react.useState)(null);
1876
+ const [runningError, setRunningError] = (0, react.useState)("");
1734
1877
  const [loadError, setLoadError] = (0, react.useState)("");
1735
1878
  const [catalogError, setCatalogError] = (0, react.useState)("");
1736
1879
  const [syncError, setSyncError] = (0, react.useState)("");
@@ -1750,7 +1893,11 @@ function CastPage({ api, locale }) {
1750
1893
  scope.queued = false;
1751
1894
  scope.busy = true;
1752
1895
  setRefreshing(true);
1753
- const [castResult, models] = await Promise.allSettled([Promise.resolve().then(() => scope.api.getCast()), Promise.resolve().then(() => scope.api.modelCatalog()).then(checkedCatalog)]);
1896
+ const [castResult, models, live] = await Promise.allSettled([
1897
+ Promise.resolve().then(() => scope.api.getCast()),
1898
+ Promise.resolve().then(() => scope.api.modelCatalog()).then(checkedModelCatalog),
1899
+ Promise.resolve().then(() => scope.api.listRunning())
1900
+ ]);
1754
1901
  if (!scope.active) return;
1755
1902
  if (castResult.status === "fulfilled") {
1756
1903
  setCast(castResult.value);
@@ -1764,6 +1911,10 @@ function CastPage({ api, locale }) {
1764
1911
  setCatalog(models.value);
1765
1912
  setCatalogError("");
1766
1913
  } else setCatalogError(messageOf(models.reason));
1914
+ if (live.status === "fulfilled") {
1915
+ setRunning(live.value);
1916
+ setRunningError("");
1917
+ } else setRunningError(messageOf(live.reason));
1767
1918
  scope.busy = false;
1768
1919
  setRefreshing(false);
1769
1920
  if (scope.queued) refresh(scope);
@@ -1785,6 +1936,8 @@ function CastPage({ api, locale }) {
1785
1936
  setSyncError("");
1786
1937
  setCast(null);
1787
1938
  setCatalog(null);
1939
+ setRunning(null);
1940
+ setRunningError("");
1788
1941
  refresh(scope);
1789
1942
  const unsubscribe = api.subscribeChanges?.(() => {
1790
1943
  if (!scope.active) return;
@@ -1827,12 +1980,17 @@ function CastPage({ api, locale }) {
1827
1980
  }
1828
1981
  });
1829
1982
  };
1830
- const refreshNow = () => {
1983
+ const refreshNow = async () => {
1831
1984
  const scope = scopeRef.current;
1832
- if (scope?.api === api) refresh(scope, true);
1985
+ if (scope?.api === api && !scope.busy) await refresh(scope, true);
1833
1986
  };
1834
1987
  const disabled = saving !== "" || refreshing || writeError !== "" || saved && loadError !== "";
1835
- const failures = catalogFailures(catalog);
1988
+ const catalogStatus = {
1989
+ loading: refreshing,
1990
+ error: catalogError ? hubMessage(catalogError, t.language) : "",
1991
+ failures: modelCatalogFailures(catalog),
1992
+ refresh: refreshNow
1993
+ };
1836
1994
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1837
1995
  className: "cast-root dsh-ui-stack",
1838
1996
  "aria-label": t("title"),
@@ -1877,16 +2035,10 @@ function CastPage({ api, locale }) {
1877
2035
  className: "dsh-ui-error dsh-ui-wrap",
1878
2036
  children: t("syncFailed", hubMessage(syncError, t.language))
1879
2037
  }) : 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)),
2038
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelCatalogStatus, {
2039
+ state: catalogStatus,
2040
+ locale: t.language
2041
+ }),
1890
2042
  writeError ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1891
2043
  className: "dsh-ui-banner dsh-ui-banner--danger cast-banner",
1892
2044
  role: "alert",
@@ -1963,7 +2115,40 @@ function CastPage({ api, locale }) {
1963
2115
  expectedRevision: cast.revision
1964
2116
  }))
1965
2117
  })
1966
- }) : null
2118
+ }) : null,
2119
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2120
+ className: "cast-running",
2121
+ "aria-label": t("runningTitle"),
2122
+ children: [
2123
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
2124
+ className: "cast-running-title",
2125
+ children: t("runningTitle")
2126
+ }),
2127
+ runningError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2128
+ role: "alert",
2129
+ className: "dsh-ui-error dsh-ui-wrap",
2130
+ children: t("runningFailed", hubMessage(runningError, t.language))
2131
+ }) : null,
2132
+ running === null && !runningError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2133
+ className: "dsh-ui-loading",
2134
+ children: t("loading")
2135
+ }) : null,
2136
+ running !== null && running.entries.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2137
+ className: "dsh-ui-help",
2138
+ children: t("runningEmpty")
2139
+ }) : null,
2140
+ running?.entries.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2141
+ className: "cast-running-row",
2142
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2143
+ className: "cast-running-label",
2144
+ children: entry.label || entry.id
2145
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2146
+ className: "cast-running-route",
2147
+ children: runningRouteText(entry, t)
2148
+ })]
2149
+ }, entry.id))
2150
+ ]
2151
+ })
1967
2152
  ]
1968
2153
  });
1969
2154
  }
@@ -1981,8 +2166,7 @@ const name = "dsh-model-hub-client";
1981
2166
  const inject = [
1982
2167
  "slots",
1983
2168
  "connection",
1984
- "remote",
1985
- "remote.session",
2169
+ ...MODEL_CATALOG_INJECT,
1986
2170
  "locale"
1987
2171
  ];
1988
2172
  const CHANNEL = "/dsh-model-hub";
@@ -2114,7 +2298,7 @@ function HubPage({ view, api, locale, initialEntries = null, initialCatalog = nu
2114
2298
  scope.queued = false;
2115
2299
  scope.busy = true;
2116
2300
  setRefreshing(true);
2117
- const [fields, models] = await Promise.allSettled([Promise.resolve().then(() => scope.api.list()), Promise.resolve().then(() => scope.api.modelCatalog()).then(checkedCatalog)]);
2301
+ const [fields, models] = await Promise.allSettled([Promise.resolve().then(() => scope.api.list()), Promise.resolve().then(() => scope.api.modelCatalog()).then(checkedModelCatalog)]);
2118
2302
  if (!scope.active) return;
2119
2303
  if (fields.status === "fulfilled") {
2120
2304
  setEntries(fields.value);
@@ -2227,7 +2411,16 @@ function HubPage({ view, api, locale, initialEntries = null, initialCatalog = nu
2227
2411
  })
2228
2412
  })).filter((entry) => entry.fields.length > 0 || (entry.error || entry.catalogError) && (!needle || [entry.ns, pluginTitle(entry, t)].some((text) => text.toLocaleLowerCase().includes(needle))));
2229
2413
  const disabled = writing !== "" || refreshing || writeError !== "" || saved && listError !== "";
2230
- const failures = catalogFailures(catalog);
2414
+ const refreshNow = async () => {
2415
+ const scope = scopeRef.current;
2416
+ if (scope?.api === api && !scope.busy) await refresh(scope);
2417
+ };
2418
+ const catalogStatus = {
2419
+ loading: refreshing,
2420
+ error: catalogError ? hubMessage(catalogError, t.language) : "",
2421
+ failures: modelCatalogFailures(catalog),
2422
+ refresh: refreshNow
2423
+ };
2231
2424
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2232
2425
  className: "dsh-model-hub dsh-ui-stack",
2233
2426
  lang: t.language,
@@ -2271,10 +2464,7 @@ function HubPage({ view, api, locale, initialEntries = null, initialCatalog = nu
2271
2464
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2272
2465
  variant: "ghost",
2273
2466
  disabled: writing !== "" || refreshing,
2274
- onClick: () => {
2275
- const scope = scopeRef.current;
2276
- if (scope?.api === api) refresh(scope);
2277
- },
2467
+ onClick: refreshNow,
2278
2468
  children: refreshing ? t("refreshing") : t("refresh")
2279
2469
  })]
2280
2470
  }),
@@ -2288,16 +2478,10 @@ function HubPage({ view, api, locale, initialEntries = null, initialCatalog = nu
2288
2478
  className: "dsh-ui-error dsh-ui-wrap",
2289
2479
  children: t("syncFailed", hubMessage(syncError, t.language))
2290
2480
  }) : 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)),
2481
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelCatalogStatus, {
2482
+ state: catalogStatus,
2483
+ locale: t.language
2484
+ }),
2301
2485
  writeError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2302
2486
  role: "alert",
2303
2487
  className: "dsh-ui-error dsh-ui-wrap",
@@ -2417,14 +2601,15 @@ async function apply(ctx) {
2417
2601
  const api = Object.freeze({
2418
2602
  list: () => call("fields.list", {}),
2419
2603
  setField: (payload) => call("fields.set", payload),
2420
- modelCatalog: () => client.remote.session.modelCatalog(),
2604
+ modelCatalog: () => readModelCatalog(client),
2421
2605
  subscribeChanges: changes.subscribe,
2422
2606
  getCast: () => call("cast.get", {}),
2423
2607
  setCastEnabled: (payload) => call("cast.setEnabled", payload),
2424
2608
  addCastRoute: (payload) => call("cast.addRoute", payload),
2425
2609
  setCastRoute: (payload) => call("cast.setRoute", payload),
2426
2610
  removeCastRoute: (payload) => call("cast.removeRoute", payload),
2427
- setCastTendency: (payload) => call("cast.setTendency", payload)
2611
+ setCastTendency: (payload) => call("cast.setTendency", payload),
2612
+ listRunning: () => call("cast.running", {})
2428
2613
  });
2429
2614
  client.slots.inject("plugins.bundle.config", () => client.slots.register({
2430
2615
  name: "plugins.bundle.config",