@klarkxy/dsh-model-hub 0.1.3 → 0.1.5

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, manage which models subagents may use, and watch which model and reasoning effort each running subagent is on.
7
+ View and change the models your DeepSeek Harness plugins use, control which models subagents may use, and track the model and reasoning effort of each running subagent — all on one page.
8
8
 
9
9
  ## Install
10
10
 
@@ -18,16 +18,16 @@ 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, 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.
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. The plugin that owns the setting saves the change.
22
+ - **Subagent Cast**: toggle the subagent model allowlist 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 pick one explicitly when delegating. For independent reviews and alternative approaches it favors a different model when suitable, but allows the same one when it fits better. Per-model task tendencies you add are optional hints; none are supplied by default. Below the list, a live view shows each running subagent with its current model and reasoning effort; an effort marked adapter default comes from the adapter, not an explicit choice.
23
23
 
24
24
  ## Limits
25
25
 
26
26
  - Only plugins that expose model settings appear.
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.
27
+ - Changes made elsewhere update the page automatically. If an edit conflicts with a newer change, refresh and choose again.
28
+ - If the model list cannot load, use **Retry**. The last available list stays visible; warnings name providers that failed while others stay usable.
29
+ - Selection guidance needs host tools that support choosing a subagent model. Existing conversations keep their saved lists; start a new one to use an updated list.
30
+ - Guidance never forces a model choice or adds subtasks just for variety.
31
31
  - Remote subagents and finished runs have no readable route; the live list covers only live local subagents.
32
32
  - No usage statistics.
33
33
 
@@ -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,16 +18,16 @@ dsh plugin --profile web add @klarkxy/dsh-model-hub
18
18
 
19
19
  打开「插件 → 模型枢纽」。
20
20
 
21
- - **字段总览**:列出已安装插件提供的模型设置,例如自动标题、Git 提交和安全自动。可以按插件、用途或当前模型搜索,再选择新模型;改动由对应插件保存。
22
- - **子代理阵容**:开关子代理模型白名单,添加、替换或移除模型。开启且至少有一个模型时,会引导智能体发现当前会话可用的模型,并在派发时显式选择。独立复核和方案对比会优先考虑合适的不同模型,同模型更适合时仍可使用。你可以自行填写各模型的「任务倾向」作为可选参考;插件不预置模型专用倾向。页面下方实时列出运行中的子代理及各自当前使用的模型和思考强度;标注「适配器默认」的强度来自适配器而非显式选择。
21
+ - **字段总览**:列出已安装插件提供的模型设置,例如自动标题、Git 提交和安全自动。可按插件、用途或当前模型搜索,再选择新模型;改动由对应插件保存。
22
+ - **子代理阵容**:开关子代理模型白名单,添加、替换或移除模型。开启且至少有一个模型时,会引导智能体发现当前会话可用的模型,并在派发时显式选择。独立复核和方案对比会优先考虑合适的不同模型,同模型更适合时仍可使用。自填的模型「任务倾向」只作可选参考,插件不预置任何倾向。列表下方实时显示运行中的子代理及其当前模型和思考强度;标注「适配器默认」的强度来自适配器,而非显式选择。
23
23
 
24
24
  ## 限制
25
25
 
26
- - 只显示提供了模型设置的插件。
27
- - 设置在别处被改动时,刷新后重新选择。
28
- - 模型列表读取失败时,点击「重试」。上次可用的列表会保留;部分提供方读取失败时会显示提示,其他提供方仍可使用。
29
- - 选模引导需要宿主工具支持选择子代理模型。已有会话保留各自的模型名单;修改名单后,新建会话才能使用更新后的名单。
30
- - 引导不强制模型选择,也不要求仅为增加模型种类而新增子任务。
26
+ - 只显示提供模型设置的插件。
27
+ - 别处改动设置时页面自动更新;编辑与较新的改动冲突时,刷新后重新选择。
28
+ - 模型列表读取失败时点「重试」。上次可用的列表保留;部分提供方读取失败会显示提示,其余提供方仍可用。
29
+ - 选模引导需要宿主工具支持选择子代理模型。已有会话保留各自的模型名单;修改后需新建会话才能使用新名单。
30
+ - 引导不强制选模,也不为增加模型种类而新增子任务。
31
31
  - 远程子代理和已结束的子代理没有可读取的路由;运行列表只覆盖存活的本地子代理。
32
32
  - 不提供用量统计。
33
33
 
@@ -1,28 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- //#region \0rolldown/runtime.js
3
- var __create = Object.create;
4
- var __defProp = Object.defineProperty;
5
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
- var __getOwnPropNames = Object.getOwnPropertyNames;
7
- var __getProtoOf = Object.getPrototypeOf;
8
- var __hasOwnProp = Object.prototype.hasOwnProperty;
9
- var __copyProps = (to, from, except, desc) => {
10
- if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
- key = keys[i];
12
- if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
13
- get: ((k) => from[k]).bind(null, key),
14
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
- });
16
- }
17
- return to;
18
- };
19
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
20
- value: mod,
21
- enumerable: true
22
- }) : target, mod));
23
- //#endregion
24
2
  let react = require("react");
25
- react = __toESM(react, 1);
26
3
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
27
4
  let react_dom = require("react-dom");
28
5
  let react_jsx_runtime = require("react/jsx-runtime");
@@ -220,7 +197,14 @@ function groupModelMenuChoices(choices, query) {
220
197
  }
221
198
  //#endregion
222
199
  //#region ../dsh-model-route/src/catalog-ui.tsx
223
- /** Also accepts a mapped state from a caller that owns broader async orchestration. */
200
+ /**
201
+ * Also accepts a mapped state from a caller that owns broader async orchestration.
202
+ *
203
+ * The dsh-ui-* classes carry the styling and resolve through the consuming
204
+ * plugin's own officialUiCss scope (every consumer mounts its root class, so
205
+ * e.g. `.dsh-model-hub .dsh-ui-loading` paints this); dsh-model-catalog-status
206
+ * stays as a structural hook for tests and layout, never a colour source.
207
+ */
224
208
  function ModelCatalogStatus({ state, locale }) {
225
209
  const zh = locale?.toLowerCase().startsWith("zh") === true;
226
210
  const copy = zh ? {
@@ -236,15 +220,17 @@ function ModelCatalogStatus({ state, locale }) {
236
220
  };
237
221
  if (!state.loading && !state.error && state.failures.length === 0) return null;
238
222
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
239
- className: "dsh-model-catalog-status",
223
+ className: "dsh-model-catalog-status dsh-ui-stack",
240
224
  lang: zh ? "zh" : "en",
241
225
  children: [
242
226
  state.loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
243
227
  role: "status",
228
+ className: "dsh-ui-loading",
244
229
  children: copy.loading
245
230
  }) : null,
246
231
  state.error ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
247
232
  role: "alert",
233
+ className: "dsh-ui-error dsh-ui-wrap",
248
234
  children: [
249
235
  copy.error,
250
236
  ": ",
@@ -253,20 +239,27 @@ function ModelCatalogStatus({ state, locale }) {
253
239
  }) : null,
254
240
  state.failures.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
255
241
  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}`)) })]
242
+ className: "dsh-ui-meta",
243
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: copy.partial }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
244
+ className: "dsh-ui-list",
245
+ children: state.failures.map((failure, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
246
+ failure.name || failure.id,
247
+ ": ",
248
+ failure.message
249
+ ] }, `${failure.id}:${index}`))
250
+ })]
261
251
  }) : 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
252
+ state.error || state.failures.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
253
+ className: "dsh-ui-actions",
254
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
255
+ type: "button",
256
+ size: "sm",
257
+ disabled: state.loading,
258
+ onClick: () => {
259
+ state.refresh();
260
+ },
261
+ children: copy.retry
262
+ })
270
263
  }) : null
271
264
  ]
272
265
  });
@@ -286,7 +279,8 @@ function ModelCatalogStatus({ state, locale }) {
286
279
  * Sticky group headings, subsequence ranking, highlight walking and focus
287
280
  * hand-back all match the host. SYNC POINT: when the host's ModelSelect or
288
281
  * ModelSelect.module.css changes, this copy must be re-transcribed; styles
289
- * below may only reference --dsw-* theme tokens, never literal colors.
282
+ * below may only reference allowlisted theme tokens (the --dsw-* set plus the
283
+ * host's --ds-* motion tokens), never literal colors.
290
284
  *
291
285
  * It statically imports the primitives, so only browser clients should load
292
286
  * this entry; host code and node tests use the core entry (`./index.ts`).
@@ -794,7 +788,8 @@ function ModelMenu(props) {
794
788
  * sticky group headings (MenuGroup's own CSS), and the l1-stroke prominent
795
789
  * elevation at portal z-index 1100. The trigger mirrors the composer's 28px
796
790
  * model pill, chevron rotating open. MenuSurface paints fill and backdrop.
797
- * Only --dsw-* theme tokens may appear here — a literal color is a bug.
791
+ * Only allowlisted --dsw- and --ds- theme tokens may appear here — a literal
792
+ * color is a bug.
798
793
  */
799
794
  const modelMenuCss = `
800
795
  .dsh-model-menu-root { min-width: 0; position: relative; display: inline-flex; }
@@ -817,12 +812,14 @@ const modelMenuCss = `
817
812
  display: flex;
818
813
  }
819
814
  .dsh-model-menu-trigger:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
820
- .dsh-model-menu-trigger:focus-visible:not([data-selection-focus]) { box-shadow: 0 0 0 2px var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); }
815
+ .dsh-model-menu-trigger:focus-visible:not([data-selection-focus]) { box-shadow: 0 0 0 var(--dsw-focus-ring-width) var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); }
821
816
  .dsh-model-menu-trigger:disabled { color: var(--dsw-alias-label-dimmed); cursor: default; }
822
817
  .dsh-model-menu-triggerLabel { text-overflow: ellipsis; white-space: nowrap; min-width: 0; overflow: hidden; }
823
818
  .dsh-model-menu-triggerEffort { text-overflow: ellipsis; white-space: nowrap; min-width: 0; color: var(--dsw-alias-label-caption); flex-shrink: 1000; overflow: hidden; }
824
- .dsh-model-menu-chevron { color: var(--dsw-alias-label-caption); flex: none; transition: transform 0.12s; }
819
+ .dsh-model-menu-chevron { color: var(--dsw-alias-label-caption); flex: none; transition: transform var(--ds-transition-duration) var(--ds-ease-in-out); }
825
820
  .dsh-model-menu-chevronOpen { transform: rotate(180deg); }
821
+ /* Portal overlay: the host publishes no z-index token, so the card keeps the
822
+ * native menu layer's 1100 verbatim. */
826
823
  .dsh-model-menu-menu {
827
824
  z-index: 1100;
828
825
  --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
@@ -848,6 +845,8 @@ const modelMenuCss = `
848
845
  .dsh-model-menu-searchWithQuery { padding-right: 34px; }
849
846
  .dsh-model-menu-searchRow .dsh-model-menu-search input { padding: 0; font-size: 12px; line-height: normal; }
850
847
  .dsh-model-menu-searchRow .dsh-model-menu-search input::placeholder { color: var(--dsw-alias-label-caption); }
848
+ /* The clear button is a circle by geometry (border-radius: 50%), a shape the
849
+ * radius token scale does not describe. */
851
850
  .dsh-model-menu-searchClear {
852
851
  width: 24px;
853
852
  height: 24px;
@@ -1095,6 +1094,17 @@ ${scope} {
1095
1094
  /* --- Layout scaffolding. --- */
1096
1095
  & .dsh-ui-stack { display: flex; flex-direction: column; gap: 12px; }
1097
1096
  & .dsh-ui-stack-lg { display: flex; flex-direction: column; gap: 20px; }
1097
+ /* Plugin detail pages share one reading column. These are layout recipes;
1098
+ * controls keep their own primitive styles. Form spacing follows the host's
1099
+ * settings-form/fields.module.css and SettingsForm.module.css. */
1100
+ & .dsh-ui-page { display: flex; flex-direction: column; gap: 16px; width: 100%; max-width: 760px; min-width: 0; }
1101
+ & .dsh-ui-form { display: flex; flex-direction: column; gap: 0; min-width: 0; }
1102
+ &.dsh-ui-form > .dsh-ui-field, & .dsh-ui-form > .dsh-ui-field { padding: 12px 0; }
1103
+ & .dsh-ui-form-footer { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; padding-top: 16px; min-width: 0; }
1104
+ & .dsh-ui-fields-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 20px; min-width: 0; margin: 0; padding: 0; border: 0; }
1105
+ @media (max-width: 560px) {
1106
+ & .dsh-ui-fields-grid { grid-template-columns: minmax(0, 1fr); }
1107
+ }
1098
1108
  & .dsh-ui-row { display: flex; align-items: center; gap: 8px; min-width: 0; }
1099
1109
  & .dsh-ui-row-wrap { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
1100
1110
  & .dsh-ui-spacer { flex: 1; min-width: 0; }
@@ -1122,6 +1132,9 @@ ${scope} {
1122
1132
  * official primitive, so this only owns the vertical rhythm. --- */
1123
1133
  & .dsh-ui-field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
1124
1134
  & .dsh-ui-field + .dsh-ui-field { border-top: 0.5px solid var(--dsw-alias-border-l2); padding-top: 12px; }
1135
+ /* Grid fields share a row, so the stacked-field divider must not win later in
1136
+ * the cascade. Reset it here, after the field recipe. */
1137
+ & .dsh-ui-fields-grid > .dsh-ui-field { padding: 0; border: 0; }
1125
1138
  & .dsh-ui-label { display: flex; align-items: center; gap: 6px; font-size: 13px; line-height: 20px; font-weight: 500; color: var(--dsw-alias-label-primary); }
1126
1139
  & .dsh-ui-label-row { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
1127
1140
  & .dsh-ui-help { font-size: 12px; line-height: 1.6; color: var(--dsw-alias-label-secondary); }
@@ -1312,29 +1325,22 @@ ${scope} {
1312
1325
  & *, & *::before, & *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
1313
1326
  }
1314
1327
  }
1315
- `.replace(/^& (\.dsh-ui-[a-z0-9-]+)(?![+>~])/gm, (_match, className) => `&${className}, & ${className}`);
1328
+ `.replace(/^& (\.dsh-ui-[a-z0-9-]+)(?=\s*\{)/gm, (_match, className) => `&${className}, & ${className}`);
1316
1329
  }
1317
1330
  //#endregion
1318
1331
  //#region ../dsh-plugin-kit/src/contracts.ts
1319
- /**
1320
- * Remote contribution for model-settings change notifications.
1321
- * Provided by `@klarkxy/dsh-model-hub` (`ModelSettingsChangesController`);
1322
- * subscribers degrade silently when that package is not installed.
1323
- * The stream carries invalidations only — `ready` is never a settings snapshot.
1324
- */
1325
- const MODEL_SETTINGS_CHANGES_REMOTE = {
1326
- package: "@klarkxy/dsh-model-hub",
1327
- descriptors: [{
1328
- id: "modelSettingsChanges/watch",
1329
- service: "modelSettingsChangesController",
1330
- namespace: "modelSettingsChanges",
1331
- method: "watch",
1332
- mode: "stream",
1333
- invocation: { kind: "direct" },
1334
- parameters: [],
1335
- cancellation: { parameter: "signal" }
1336
- }]
1337
- };
1332
+ /** `@klarkxy/dsh-git-commit` becomes `KlarkxyDshGitCommitModelSettingsChanges`. */
1333
+ function modelSettingsChangesIdentity(packageName) {
1334
+ const scoped = /^@([^/]+)\/(.+)$/.exec(packageName);
1335
+ const parts = (scoped ? `${scoped[1]}/${scoped[2]}` : packageName).split(/[^A-Za-z0-9]+/).filter(Boolean);
1336
+ if (!parts.length) throw new Error("Model settings package name has no RPC segment");
1337
+ const segment = `${parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("")}ModelSettingsChanges`;
1338
+ if (!/^[A-Z][A-Za-z0-9]*$/.test(segment)) throw new Error(`Model settings sync name is not an RPC segment: ${segment}`);
1339
+ return {
1340
+ service: segment,
1341
+ namespace: segment
1342
+ };
1343
+ }
1338
1344
  //#endregion
1339
1345
  //#region ../dsh-plugin-kit/src/model-settings-client.ts
1340
1346
  /** This stream carries invalidations only; ready is never a settings snapshot. */
@@ -1349,21 +1355,44 @@ function parseModelSettingsChangeFrame(value) {
1349
1355
  };
1350
1356
  throw new Error("Invalid model settings change frame");
1351
1357
  }
1352
- /** One contribution mounted by Model Hub; consumers only inject its service. */
1353
- const modelSettingsChangesRemote = {
1354
- package: MODEL_SETTINGS_CHANGES_REMOTE.package,
1355
- descriptors: MODEL_SETTINGS_CHANGES_REMOTE.descriptors.map((descriptor) => ({
1356
- ...descriptor,
1357
- result: {
1358
- mode: "strict",
1359
- typeSymbol: "ModelSettings.ChangeFrame",
1360
- create: () => ({ parse: parseModelSettingsChangeFrame })
1361
- }
1362
- }))
1363
- };
1358
+ /** One owner's contribution. Each package mounts only its own endpoint. */
1359
+ function modelSettingsChangesRemote(packageName, identity = modelSettingsChangesIdentity(packageName)) {
1360
+ return {
1361
+ package: packageName,
1362
+ descriptors: [{
1363
+ id: `${identity.namespace}/watch`,
1364
+ service: identity.service,
1365
+ namespace: identity.namespace,
1366
+ method: "watch",
1367
+ mode: "stream",
1368
+ invocation: { kind: "direct" },
1369
+ parameters: [],
1370
+ cancellation: { parameter: "signal" },
1371
+ result: {
1372
+ mode: "strict",
1373
+ typeSymbol: "ModelSettings.ChangeFrame",
1374
+ create: () => ({ parse: parseModelSettingsChangeFrame })
1375
+ }
1376
+ }]
1377
+ };
1378
+ }
1379
+ /** Mount from a synchronous apply. Disposal waits for an in-flight mount and
1380
+ * then withdraws it, so the contribution cannot outlive the plugin. */
1381
+ function mountModelSettingsChanges(ctx, packageName) {
1382
+ const contribution = modelSettingsChangesRemote(packageName);
1383
+ ctx.inject(["remote"], (injected) => {
1384
+ const scope = injected;
1385
+ const mounting = scope.remote.$mount(contribution);
1386
+ mounting.catch(() => {});
1387
+ scope.effect(() => async () => {
1388
+ await (await mounting.catch(() => void 0))?.();
1389
+ });
1390
+ });
1391
+ }
1364
1392
  /** Native RemoteStream owns carrier reconnects; no timers or browser transport.
1365
1393
  * A factory shares one logical stream while at least one subscriber remains. */
1366
- function createModelSettingsSubscriber(ctx, namespace) {
1394
+ function createModelSettingsSubscriber(ctx, packageName, identity = modelSettingsChangesIdentity(packageName), accept = (ns) => ns === packageName) {
1395
+ const endpoint = identity;
1367
1396
  const listeners = /* @__PURE__ */ new Set();
1368
1397
  let injection;
1369
1398
  let current;
@@ -1389,12 +1418,18 @@ function createModelSettingsSubscriber(ctx, namespace) {
1389
1418
  };
1390
1419
  const start = () => {
1391
1420
  const life = ++lifecycle;
1392
- injection = ctx.inject(["remote", "remote.modelSettingsChanges"], (injected) => {
1421
+ injection = ctx.inject(["remote", `remote.${endpoint.namespace}`], (injected) => {
1393
1422
  if (life !== lifecycle || !listeners.size) return;
1394
1423
  const scope = injected;
1424
+ const service = scope.remote[endpoint.namespace];
1425
+ if (!service || !("watch" in service) || !service.watch) {
1426
+ report("Model settings sync service unavailable");
1427
+ return;
1428
+ }
1429
+ const watch = service.watch;
1395
1430
  const stream = scope.remote.$stream({
1396
1431
  name: "Model settings sync",
1397
- open: (signal) => scope.remote.modelSettingsChanges.watch(signal),
1432
+ open: (signal) => watch(signal),
1398
1433
  ended: (accepted) => /* @__PURE__ */ new Error(`Model settings sync stream ended ${accepted ? "after" : "before"} ready; refresh or reopen the page`)
1399
1434
  });
1400
1435
  const session = {
@@ -1429,7 +1464,7 @@ function createModelSettingsSubscriber(ctx, namespace) {
1429
1464
  invalidate();
1430
1465
  } else {
1431
1466
  if (!ready) throw new Error("Model settings sync change arrived before ready");
1432
- if (namespace === "*" || frame.ns === namespace) invalidate();
1467
+ if (accept(frame.ns)) invalidate();
1433
1468
  }
1434
1469
  }
1435
1470
  } catch (error) {
@@ -1992,7 +2027,7 @@ function CastPage({ api, locale }) {
1992
2027
  refresh: refreshNow
1993
2028
  };
1994
2029
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1995
- className: "cast-root dsh-ui-stack",
2030
+ className: "cast-root dsh-model-hub dsh-ui-stack",
1996
2031
  "aria-label": t("title"),
1997
2032
  "aria-busy": saving !== "" || refreshing,
1998
2033
  children: [
@@ -2422,7 +2457,7 @@ function HubPage({ view, api, locale, initialEntries = null, initialCatalog = nu
2422
2457
  refresh: refreshNow
2423
2458
  };
2424
2459
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2425
- className: "dsh-model-hub dsh-ui-stack",
2460
+ className: "dsh-model-hub dsh-ui-page",
2426
2461
  lang: t.language,
2427
2462
  "aria-label": t("title"),
2428
2463
  "aria-busy": writing !== "" || refreshing,
@@ -2588,11 +2623,10 @@ function HubPage({ view, api, locale, initialEntries = null, initialCatalog = nu
2588
2623
  ]
2589
2624
  });
2590
2625
  }
2591
- async function apply(ctx) {
2626
+ function apply(ctx) {
2592
2627
  const client = ctx;
2593
- const disposeRemote = await client.remote.$mount(modelSettingsChangesRemote);
2594
- client.effect(() => disposeRemote);
2595
- const changes = createModelSettingsSubscriber(ctx, "*");
2628
+ mountModelSettingsChanges(ctx, BUNDLE);
2629
+ const changes = createModelSettingsSubscriber(ctx, BUNDLE, modelSettingsChangesIdentity(BUNDLE), () => true);
2596
2630
  const call = async (endpoint, payload) => {
2597
2631
  const result = await client.connection.rpc.call(CHANNEL, endpoint, payload);
2598
2632
  if (!result?.ok) throw new Error(result?.error?.message || "Request failed");