@fonlan/dsh-model-router 0.3.4 → 0.3.6

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
@@ -11,9 +11,7 @@ window.__ModuleLoader__.load({
11
11
  const LOCALE_NS = "model-router";
12
12
  const zh = {
13
13
  settingsTitle: "模型路由",
14
- settingsCardDescription: "模型聚合、提供商切换与优先级顺序",
15
- expand: "展开",
16
- collapse: "收起",
14
+ sectionSub: "模型聚合、提供商切换与优先级顺序",
17
15
  readOnly: "本部署的设置为只读。",
18
16
  loading: "加载中…",
19
17
  loadFailed: "加载失败:{message}",
@@ -64,9 +62,7 @@ window.__ModuleLoader__.load({
64
62
  };
65
63
  const en = {
66
64
  settingsTitle: "Model Router",
67
- settingsCardDescription: "Model aggregation, provider switching and preference order",
68
- expand: "Expand",
69
- collapse: "Collapse",
65
+ sectionSub: "Model aggregation, provider switching and preference order",
70
66
  readOnly: "This deployment stores settings read-only.",
71
67
  loading: "Loading…",
72
68
  loadFailed: "Load failed: {message}",
@@ -156,9 +152,9 @@ window.__ModuleLoader__.load({
156
152
  setModelOrder: (order) => call("set-model-order", { order })
157
153
  };
158
154
  //#endregion
159
- //#region \0dsh-css:/home/runner/work/dsh-model-router/dsh-model-router/src/client/settings-card.css.mjs
160
- const css$1 = "/* Model Router settings card — chrome aligned with the built-in plugin cards\n (设置 → 插件 → 插件配置), theme-consistent via DSW alias variables. */\n.mr-settings-card {\n border: 1px solid var(--dsw-alias-border-l2, #2a3346);\n background: var(--dsw-alias-bg-layer-3, #0d1017);\n border-radius: 12px;\n list-style: none;\n transition: border-color .16s, background .16s;\n}\n\n.mr-settings-card:hover {\n border-color: var(--dsw-alias-label-dimmed, #4a5468);\n}\n\n.mr-settings-card[data-open] {\n background: var(--dsw-alias-bg-layer-2, #141a26);\n border-color: var(--dsw-alias-label-dimmed, #4a5468);\n}\n\n.mr-settings-head {\n appearance: none;\n width: 100%;\n font: inherit;\n color: inherit;\n text-align: left;\n cursor: pointer;\n background: transparent;\n border: 0;\n border-radius: 12px;\n align-items: center;\n gap: 12px;\n padding: 14px 16px;\n display: flex;\n}\n\n.mr-settings-head:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary, #4f8cff);\n outline-offset: -2px;\n}\n\n.mr-settings-head-text {\n flex-direction: column;\n flex: 1;\n gap: 4px;\n min-width: 0;\n display: flex;\n}\n\n.mr-settings-title {\n color: var(--dsw-alias-label-primary, #e6e6e6);\n font-size: 15px;\n font-weight: 600;\n line-height: 1.4;\n}\n\n.mr-settings-sub {\n color: var(--dsw-alias-label-tertiary, #8b93a7);\n font-size: 13px;\n line-height: 1.5;\n}\n\n.mr-settings-chevron {\n color: var(--dsw-alias-label-tertiary, #8b93a7);\n flex: none;\n transition: transform .16s;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.mr-settings-chevron[data-open] {\n transform: rotate(180deg);\n}\n\n.mr-settings-body {\n border-top: 1px solid var(--dsw-alias-border-l2, #2a3346);\n margin: 0 16px;\n padding: 12px 0 8px;\n display: flex;\n flex-direction: column;\n gap: 12px;\n}\n\n.mr-settings-readonly {\n color: var(--dsw-alias-label-tertiary, #8b93a7);\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n}\n\n/* ── card body: the router controls ─────────────────────────────────────── */\n\n.mr-root {\n display: flex;\n flex-direction: column;\n gap: 12px;\n padding: 0 0 4px;\n}\n\n.mr-quick-switch {\n display: flex;\n align-items: flex-start;\n gap: 8px;\n padding: 8px 10px;\n border: 1px solid var(--dsw-alias-border-inverted);\n border-radius: 10px;\n background: var(--dsw-alias-bg-module-platform);\n cursor: pointer;\n}\n\n.mr-quick-switch input[type=\"checkbox\"] {\n margin-top: 2px;\n accent-color: var(--dsw-alias-accent);\n}\n\n.mr-quick-switch-copy {\n display: flex;\n flex-direction: column;\n gap: 2px;\n min-width: 0;\n}\n\n.mr-quick-switch-label {\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n line-height: 20px;\n}\n\n.mr-quick-switch-desc {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n line-height: 18px;\n}\n\n.mr-toolbar {\n display: flex;\n justify-content: space-between;\n align-items: center;\n}\n\n.mr-count {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n line-height: 18px;\n}\n\n.mr-button {\n color: var(--dsw-alias-label-primary);\n cursor: pointer;\n background: var(--dsw-alias-interactive-bg-hover);\n border: 1px solid var(--dsw-alias-border-l3);\n border-radius: 8px;\n font-size: 12px;\n line-height: 18px;\n padding: 3px 10px;\n}\n\n.mr-button:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n filter: brightness(1.08);\n}\n\n.mr-button:disabled {\n opacity: 0.5;\n cursor: default;\n}\n\n.mr-error {\n background: var(--dsw-alias-interactive-bg-hover-danger);\n color: var(--dsw-alias-state-error-primary);\n border-radius: 8px;\n padding: 7px 8px;\n font-size: 12px;\n line-height: 18px;\n}\n\n.mr-empty {\n color: var(--dsw-alias-label-tertiary);\n padding: 16px 0;\n font-size: 13px;\n line-height: 20px;\n}\n\n.mr-models {\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.mr-card {\n border: 1px solid var(--dsw-alias-border-inverted);\n border-radius: 12px;\n padding: 10px 12px;\n background: var(--dsw-alias-bg-module-platform);\n transition: opacity 0.12s;\n}\n\n.mr-card-busy {\n opacity: 0.6;\n pointer-events: none;\n}\n\n.mr-card-header {\n display: flex;\n align-items: baseline;\n gap: 8px;\n min-width: 0;\n}\n\n.mr-model-name {\n color: var(--dsw-alias-label-primary);\n font-size: 14px;\n font-weight: 600;\n line-height: 20px;\n}\n\n.mr-model-id {\n color: var(--dsw-alias-label-tertiary);\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n font-size: 12px;\n line-height: 18px;\n text-overflow: ellipsis;\n white-space: nowrap;\n overflow: hidden;\n min-width: 0;\n flex: 1;\n}\n\n.mr-provider-count {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n line-height: 18px;\n flex: none;\n}\n\n.mr-drag-hint {\n margin: 2px 0 6px;\n color: var(--dsw-alias-label-caption);\n font-size: 11px;\n line-height: 16px;\n}\n\n.mr-providers {\n list-style: none;\n margin: 0;\n padding: 0;\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.mr-provider {\n display: flex;\n align-items: center;\n gap: 6px;\n border: 1px solid transparent;\n border-radius: 10px;\n padding: 2px 8px 2px 4px;\n}\n\n.mr-provider-active {\n background: var(--dsw-alias-interactive-bg-hover);\n border-color: var(--dsw-alias-border-l3);\n}\n\n.mr-provider-nocred {\n opacity: 0.55;\n}\n\n.mr-provider-drop {\n border-color: var(--dsw-alias-border-brand);\n}\n\n.mr-grip {\n color: var(--dsw-alias-label-caption);\n cursor: grab;\n font-size: 12px;\n line-height: 20px;\n flex: none;\n user-select: none;\n}\n\n.mr-grip:active {\n cursor: grabbing;\n}\n\n.mr-provider-button {\n flex: 1;\n min-width: 0;\n display: flex;\n align-items: baseline;\n gap: 6px;\n color: var(--dsw-alias-label-primary);\n background: none;\n border: none;\n text-align: left;\n cursor: pointer;\n font: inherit;\n padding: 4px 0;\n}\n\n.mr-provider-button:disabled {\n cursor: default;\n}\n\n.mr-provider-name {\n font-size: 13px;\n line-height: 20px;\n text-overflow: ellipsis;\n white-space: nowrap;\n overflow: hidden;\n}\n\n.mr-provider-nocred-label {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n line-height: 18px;\n flex: none;\n}\n\n.mr-active-badge {\n flex: none;\n color: var(--dsw-alias-label-inverted, #fff);\n background: var(--dsw-alias-border-brand, #4d6bfe);\n border-radius: 999px;\n font-size: 11px;\n line-height: 16px;\n padding: 0 8px;\n}\n\n/* ── model display order ────────────────────────────────────────────────── */\n\n.mr-sort {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 8px 10px;\n border: 1px solid var(--dsw-alias-border-inverted);\n border-radius: 10px;\n background: var(--dsw-alias-bg-module-platform);\n}\n\n.mr-sort-head {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.mr-sort-title {\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n line-height: 20px;\n}\n\n.mr-sort-desc {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n line-height: 18px;\n}\n\n.mr-sort-modes {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.mr-sort-mode {\n display: flex;\n align-items: flex-start;\n gap: 8px;\n border: 1px solid transparent;\n border-radius: 8px;\n padding: 5px 6px;\n cursor: pointer;\n}\n\n.mr-sort-mode:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.mr-sort-mode input[type=\"radio\"] {\n margin-top: 2px;\n accent-color: var(--dsw-alias-accent);\n}\n\n.mr-sort-mode-copy {\n display: flex;\n flex-direction: column;\n gap: 1px;\n min-width: 0;\n}\n\n.mr-sort-mode-label {\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n line-height: 20px;\n}\n\n.mr-sort-mode-desc {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n line-height: 18px;\n}\n\n.mr-sort-custom {\n display: flex;\n flex-direction: column;\n gap: 6px;\n border-top: 1px solid var(--dsw-alias-border-l2, #2a3346);\n padding-top: 8px;\n}\n\n.mr-sort-hint {\n color: var(--dsw-alias-label-caption);\n margin: 0;\n font-size: 11px;\n line-height: 16px;\n}\n\n.mr-sort-list {\n list-style: none;\n margin: 0;\n padding: 0;\n display: flex;\n flex-direction: column;\n gap: 4px;\n max-height: 280px;\n overflow-y: auto;\n}\n\n.mr-sort-item {\n display: flex;\n align-items: center;\n gap: 6px;\n border: 1px solid var(--dsw-alias-border-l2, #2a3346);\n border-radius: 8px;\n padding: 4px 8px 4px 4px;\n background: var(--dsw-alias-bg-layer-1, #0d1017);\n}\n\n.mr-sort-item-drop {\n border-color: var(--dsw-alias-border-brand);\n}\n\n.mr-sort-item-busy {\n opacity: 0.6;\n pointer-events: none;\n}\n\n.mr-sort-model-name {\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n line-height: 20px;\n text-overflow: ellipsis;\n white-space: nowrap;\n overflow: hidden;\n min-width: 0;\n}\n\n.mr-sort-model-id {\n color: var(--dsw-alias-label-tertiary);\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n font-size: 11px;\n line-height: 16px;\n text-overflow: ellipsis;\n white-space: nowrap;\n overflow: hidden;\n flex: 1;\n text-align: right;\n}\n";
161
- const tagId$1 = "@fonlan/dsh-model-router/settings-card.css";
155
+ //#region \0dsh-css:/home/runner/work/dsh-model-router/dsh-model-router/src/client/settings-section.css.mjs
156
+ const css$1 = "/* Model Router settings page — the page owns one entry in the settings\n sidebar and renders its content in the panel's content column. Styles are\n theme-consistent via DSW alias variables; the page draws its own static\n header (title plus a one-line summary) over the always-visible controls. */\n\n/* page layout */\n\n.mr-page {\n display: flex;\n flex-direction: column;\n gap: 16px;\n max-width: 720px;\n}\n\n.mr-page-head {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.mr-page-title {\n margin: 0;\n color: var(--dsw-alias-label-primary, #e6e6e6);\n font-size: 15px;\n font-weight: 600;\n line-height: 1.4;\n}\n\n.mr-page-sub {\n margin: 0;\n color: var(--dsw-alias-label-tertiary, #8b93a7);\n font-size: 13px;\n line-height: 1.5;\n}\n\n/* body layout */\n\n.mr-body {\n display: flex;\n flex-direction: column;\n gap: 12px;\n}\n\n.mr-settings-readonly {\n color: var(--dsw-alias-label-tertiary, #8b93a7);\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n}\n\n/* ── page body: the router controls ─────────────────────────────────────── */\n\n.mr-root {\n display: flex;\n flex-direction: column;\n gap: 12px;\n padding: 0 0 4px;\n}\n\n.mr-quick-switch {\n display: flex;\n align-items: flex-start;\n gap: 8px;\n padding: 8px 10px;\n border: 1px solid var(--dsw-alias-border-inverted);\n border-radius: 10px;\n background: var(--dsw-alias-bg-module-platform);\n cursor: pointer;\n}\n\n.mr-quick-switch input[type=\"checkbox\"] {\n margin-top: 2px;\n accent-color: var(--dsw-alias-accent);\n}\n\n.mr-quick-switch-copy {\n display: flex;\n flex-direction: column;\n gap: 2px;\n min-width: 0;\n}\n\n.mr-quick-switch-label {\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n line-height: 20px;\n}\n\n.mr-quick-switch-desc {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n line-height: 18px;\n}\n\n.mr-toolbar {\n display: flex;\n justify-content: space-between;\n align-items: center;\n}\n\n.mr-count {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n line-height: 18px;\n}\n\n.mr-button {\n color: var(--dsw-alias-label-primary);\n cursor: pointer;\n background: var(--dsw-alias-interactive-bg-hover);\n border: 1px solid var(--dsw-alias-border-l3);\n border-radius: 8px;\n font-size: 12px;\n line-height: 18px;\n padding: 3px 10px;\n}\n\n.mr-button:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n filter: brightness(1.08);\n}\n\n.mr-button:disabled {\n opacity: 0.5;\n cursor: default;\n}\n\n.mr-error {\n background: var(--dsw-alias-interactive-bg-hover-danger);\n color: var(--dsw-alias-state-error-primary);\n border-radius: 8px;\n padding: 7px 8px;\n font-size: 12px;\n line-height: 18px;\n}\n\n.mr-empty {\n color: var(--dsw-alias-label-tertiary);\n padding: 16px 0;\n font-size: 13px;\n line-height: 20px;\n}\n\n.mr-models {\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.mr-card {\n border: 1px solid var(--dsw-alias-border-inverted);\n border-radius: 12px;\n padding: 10px 12px;\n background: var(--dsw-alias-bg-module-platform);\n transition: opacity 0.12s;\n}\n\n.mr-card-busy {\n opacity: 0.6;\n pointer-events: none;\n}\n\n.mr-card-header {\n display: flex;\n align-items: baseline;\n gap: 8px;\n min-width: 0;\n}\n\n.mr-model-name {\n color: var(--dsw-alias-label-primary);\n font-size: 14px;\n font-weight: 600;\n line-height: 20px;\n}\n\n.mr-model-id {\n color: var(--dsw-alias-label-tertiary);\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n font-size: 12px;\n line-height: 18px;\n text-overflow: ellipsis;\n white-space: nowrap;\n overflow: hidden;\n min-width: 0;\n flex: 1;\n}\n\n.mr-provider-count {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n line-height: 18px;\n flex: none;\n}\n\n.mr-drag-hint {\n margin: 2px 0 6px;\n color: var(--dsw-alias-label-caption);\n font-size: 11px;\n line-height: 16px;\n}\n\n.mr-providers {\n list-style: none;\n margin: 0;\n padding: 0;\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.mr-provider {\n display: flex;\n align-items: center;\n gap: 6px;\n border: 1px solid transparent;\n border-radius: 10px;\n padding: 2px 8px 2px 4px;\n}\n\n.mr-provider-active {\n background: var(--dsw-alias-interactive-bg-hover);\n border-color: var(--dsw-alias-border-l3);\n}\n\n.mr-provider-nocred {\n opacity: 0.55;\n}\n\n.mr-provider-drop {\n border-color: var(--dsw-alias-border-brand);\n}\n\n.mr-grip {\n color: var(--dsw-alias-label-caption);\n cursor: grab;\n font-size: 12px;\n line-height: 20px;\n flex: none;\n user-select: none;\n}\n\n.mr-grip:active {\n cursor: grabbing;\n}\n\n.mr-provider-button {\n flex: 1;\n min-width: 0;\n display: flex;\n align-items: baseline;\n gap: 6px;\n color: var(--dsw-alias-label-primary);\n background: none;\n border: none;\n text-align: left;\n cursor: pointer;\n font: inherit;\n padding: 4px 0;\n}\n\n.mr-provider-button:disabled {\n cursor: default;\n}\n\n.mr-provider-name {\n font-size: 13px;\n line-height: 20px;\n text-overflow: ellipsis;\n white-space: nowrap;\n overflow: hidden;\n}\n\n.mr-provider-nocred-label {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n line-height: 18px;\n flex: none;\n}\n\n.mr-active-badge {\n flex: none;\n color: var(--dsw-alias-label-inverted, #fff);\n background: var(--dsw-alias-border-brand, #4d6bfe);\n border-radius: 999px;\n font-size: 11px;\n line-height: 16px;\n padding: 0 8px;\n}\n\n/* ── model display order ────────────────────────────────────────────────── */\n\n.mr-sort {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 8px 10px;\n border: 1px solid var(--dsw-alias-border-inverted);\n border-radius: 10px;\n background: var(--dsw-alias-bg-module-platform);\n}\n\n.mr-sort-head {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.mr-sort-title {\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n line-height: 20px;\n}\n\n.mr-sort-desc {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n line-height: 18px;\n}\n\n.mr-sort-modes {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.mr-sort-mode {\n display: flex;\n align-items: flex-start;\n gap: 8px;\n border: 1px solid transparent;\n border-radius: 8px;\n padding: 5px 6px;\n cursor: pointer;\n}\n\n.mr-sort-mode:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.mr-sort-mode input[type=\"radio\"] {\n margin-top: 2px;\n accent-color: var(--dsw-alias-accent);\n}\n\n.mr-sort-mode-copy {\n display: flex;\n flex-direction: column;\n gap: 1px;\n min-width: 0;\n}\n\n.mr-sort-mode-label {\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n line-height: 20px;\n}\n\n.mr-sort-mode-desc {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n line-height: 18px;\n}\n\n.mr-sort-custom {\n display: flex;\n flex-direction: column;\n gap: 6px;\n border-top: 1px solid var(--dsw-alias-border-l2, #2a3346);\n padding-top: 8px;\n}\n\n.mr-sort-hint {\n color: var(--dsw-alias-label-caption);\n margin: 0;\n font-size: 11px;\n line-height: 16px;\n}\n\n.mr-sort-list {\n list-style: none;\n margin: 0;\n padding: 0;\n display: flex;\n flex-direction: column;\n gap: 4px;\n max-height: 280px;\n overflow-y: auto;\n}\n\n.mr-sort-item {\n display: flex;\n align-items: center;\n gap: 6px;\n border: 1px solid var(--dsw-alias-border-l2, #2a3346);\n border-radius: 8px;\n padding: 4px 8px 4px 4px;\n background: var(--dsw-alias-bg-layer-1, #0d1017);\n}\n\n.mr-sort-item-drop {\n border-color: var(--dsw-alias-border-brand);\n}\n\n.mr-sort-item-busy {\n opacity: 0.6;\n pointer-events: none;\n}\n\n.mr-sort-model-name {\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n line-height: 20px;\n text-overflow: ellipsis;\n white-space: nowrap;\n overflow: hidden;\n min-width: 0;\n}\n\n.mr-sort-model-id {\n color: var(--dsw-alias-label-tertiary);\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n font-size: 11px;\n line-height: 16px;\n text-overflow: ellipsis;\n white-space: nowrap;\n overflow: hidden;\n flex: 1;\n text-align: right;\n}\n";
157
+ const tagId$1 = "@fonlan/dsh-model-router/settings-section.css";
162
158
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
163
159
  const tag = document.createElement("style");
164
160
  tag.dataset.plugin = "@fonlan/dsh-model-router";
@@ -167,23 +163,23 @@ window.__ModuleLoader__.load({
167
163
  document.head.appendChild(tag);
168
164
  }
169
165
  //#endregion
170
- //#region src/client/settings-card.tsx
166
+ //#region src/client/settings-section.tsx
171
167
  /**
172
- * The Model Router Settings Card (设置 → 插件 → 插件配置 → 模型路由).
168
+ * The Model Router settings page (设置 → 侧栏「模型路由」).
173
169
  *
174
- * Renders its own expandable chrome aligned with the built-in plugin cards
175
- * (external plugins cannot import PluginCard): a header naming the plugin and
176
- * what its settings govern, disclosing the router controls in place. All
177
- * reads/mutations go through the plugin's fenced API; the server persists
178
- * through the `model-router` settings namespace, so a switch here is live for
179
- * the next request globally.
170
+ * Registers into the settings.section slot, so the page owns one entry in the
171
+ * settings sidebar and renders its content in the panel's content column. The
172
+ * page draws its own static header (title plus a one-line summary) over the
173
+ * router controls: every routed model with its providers, their order and the
174
+ * provider the model currently routes to. All reads/mutations go through the
175
+ * plugin's fenced API; the server persists through the `model-router` settings
176
+ * namespace, so a switch here is live for the next request globally.
180
177
  *
181
178
  * The bound settings scope (the same `model-router` namespace the host half
182
- * registers) supplies the card's dispatch state: while the namespace is merely
183
- * loading the card stays mounted, and when it is unavailable (deployment
184
- * without the host half) nothing renders — matching the built-in cards. A
185
- * read-only deployment shows the built-in card's banner and disables the
186
- * mutation controls.
179
+ * registers) supplies the page's dispatch state: while the namespace is merely
180
+ * loading the page stays mounted, and when it is unavailable (deployment
181
+ * without the host half) nothing renders. A read-only deployment shows the
182
+ * banner and disables the mutation controls.
187
183
  */
188
184
  function arrayMove(list, from, to) {
189
185
  const next = [...list];
@@ -208,7 +204,7 @@ window.__ModuleLoader__.load({
208
204
  }, [ctx]);
209
205
  return (0, react.useSyncExternalStore)(subscribe, getSnapshot);
210
206
  }
211
- function makeSettingsCard(ctx) {
207
+ function makeSettingsSection(ctx) {
212
208
  const t = (() => {
213
209
  try {
214
210
  return ctx.locale.bind(LOCALE_NS);
@@ -216,11 +212,10 @@ window.__ModuleLoader__.load({
216
212
  return (key) => key;
217
213
  }
218
214
  })();
219
- return function ModelRouterSettingsCard(props) {
215
+ return function ModelRouterSettingsSection(props) {
220
216
  const { scope } = props;
221
217
  const snapshot = (0, react.useSyncExternalStore)((listener) => scope.subscribe(listener), () => scope.getSnapshot());
222
218
  useLocaleRevision(ctx);
223
- const [open, setOpen] = (0, react.useState)(false);
224
219
  const [state, setState] = (0, react.useState)(null);
225
220
  const [error, setError] = (0, react.useState)(null);
226
221
  const [loading, setLoading] = (0, react.useState)(true);
@@ -370,44 +365,19 @@ window.__ModuleLoader__.load({
370
365
  if (snapshot.status === "unavailable") return null;
371
366
  const writable = snapshot.writable;
372
367
  const models = (0, react.useMemo)(() => state?.models ?? [], [state]);
373
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
374
- className: "mr-settings-card",
375
- "data-open": open ? "" : void 0,
376
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
377
- type: "button",
378
- className: "mr-settings-head",
379
- "aria-expanded": open,
380
- "aria-label": (open ? t("collapse") : t("expand")) + ":" + t("settingsTitle"),
381
- onClick: () => setOpen(!open),
382
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
383
- className: "mr-settings-head-text",
384
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
385
- className: "mr-settings-title",
386
- children: t("settingsTitle")
387
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
388
- className: "mr-settings-sub",
389
- children: t("settingsCardDescription")
390
- })]
391
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
392
- className: "mr-settings-chevron",
393
- "data-open": open ? "" : void 0,
394
- "aria-hidden": "true",
395
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
396
- width: "14",
397
- height: "14",
398
- viewBox: "0 0 14 14",
399
- fill: "none",
400
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
401
- d: "M3.5 5.25L7 8.75L10.5 5.25",
402
- stroke: "currentColor",
403
- strokeWidth: "1.5",
404
- strokeLinecap: "round",
405
- strokeLinejoin: "round"
406
- })
407
- })
368
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
369
+ className: "mr-page",
370
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
371
+ className: "mr-page-head",
372
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
373
+ className: "mr-page-title",
374
+ children: t("settingsTitle")
375
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
376
+ className: "mr-page-sub",
377
+ children: t("sectionSub")
408
378
  })]
409
- }), open && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
410
- className: "mr-settings-body",
379
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
380
+ className: "mr-body",
411
381
  children: [!writable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
412
382
  className: "mr-settings-readonly",
413
383
  role: "status",
@@ -1225,38 +1195,98 @@ window.__ModuleLoader__.load({
1225
1195
  ]
1226
1196
  });
1227
1197
  }
1228
- /** Register the model seat onto the composer's named model slot (priority -1 shadows the host seat). */
1198
+ const EMPTY_DIRECTORY_STATE = {
1199
+ current: null,
1200
+ routable: null,
1201
+ groups: [],
1202
+ failures: [],
1203
+ status: "idle",
1204
+ error: null
1205
+ };
1206
+ /**
1207
+ * Inert face for the pathological case where the session directory cannot be
1208
+ * resolved even after `remote.session` mounted. The seat renders nothing
1209
+ * (available: false), but crucially the slot entry never throws, so it is
1210
+ * never abdicated and the host seat can keep the cell alive on the next
1211
+ * registration pass.
1212
+ */
1213
+ const INERT_DIRECTORY_STORE = {
1214
+ subscribe: () => () => {},
1215
+ getSnapshot: () => EMPTY_DIRECTORY_STATE,
1216
+ update: () => {},
1217
+ set: () => {}
1218
+ };
1219
+ /**
1220
+ * Build the seat face without ever throwing out of the slot `inject` hook.
1221
+ *
1222
+ * The seat registration below depends on `remote.session` (the namespace is
1223
+ * mounted asynchronously at startup), so by the time this runs the host
1224
+ * `modelDirectories.directoryFor(sessionId)` resolves — the original race is
1225
+ * gone by construction. The try/catch here is belt-and-braces: a throwing
1226
+ * inject would permanently abdicate this slot entry in the renderer
1227
+ * (`reportEntryError` with `abdicate` for single slots), after which the host
1228
+ * seat takes over and the Provider row never appears again.
1229
+ */
1230
+ function seatFace(models, sessions, sessionId) {
1231
+ let directory = null;
1232
+ try {
1233
+ directory = models.directoryFor(sessionId);
1234
+ } catch {}
1235
+ if (directory === null) return {
1236
+ available: false,
1237
+ directory: INERT_DIRECTORY_STORE,
1238
+ load: () => {},
1239
+ select: () => Promise.resolve(false)
1240
+ };
1241
+ let available = true;
1242
+ try {
1243
+ available = sessions.subagentAddress(sessionId) === void 0;
1244
+ } catch {}
1245
+ return {
1246
+ available,
1247
+ directory: directory.store,
1248
+ load: () => {
1249
+ if (available) directory?.load().catch(() => {});
1250
+ },
1251
+ select: (selection) => available ? directory.select(selection).then(() => true, () => false) : Promise.resolve(false)
1252
+ };
1253
+ }
1254
+ /**
1255
+ * Register the model seat onto the composer's named model slot (priority -1
1256
+ * shadows the host seat). The inject block additionally depends on
1257
+ * `remote.session` so the seat registers only after the remote session
1258
+ * namespace has mounted — `directoryFor` can throw while it is still mounting,
1259
+ * and a throwing inject permanently abdicates the entry (the host seat then
1260
+ * wins and the Provider row never appears).
1261
+ */
1229
1262
  function registerModelRouterSeat(ctx) {
1230
1263
  const t = ctx.locale.bind(LOCALE_NS);
1231
- ctx.inject(["slots", "modelDirectories"], (scope) => {
1232
- const models = scope.modelDirectories;
1233
- const sessions = scope.sessions;
1234
- scope.slots.inject("conversation.input.model", () => scope.slots.register({
1264
+ ctx.inject([
1265
+ "slots",
1266
+ "modelDirectories",
1267
+ "remote.session"
1268
+ ], (scope) => {
1269
+ const face = scope;
1270
+ face.slots.inject("conversation.input.model", () => face.slots.register({
1235
1271
  name: "conversation.input.model",
1236
1272
  id: "model-router-model-seat",
1237
1273
  priority: -1,
1238
1274
  label: () => t("seatLabel"),
1239
1275
  locale: LOCALE_NS,
1240
- inject: (sessionId) => {
1241
- const directory = models.directoryFor(sessionId);
1242
- const available = sessions.subagentAddress(sessionId) === void 0;
1243
- return {
1244
- available,
1245
- directory: directory.store,
1246
- load: () => {
1247
- if (available) directory.load().catch(() => {});
1248
- },
1249
- select: (selection) => available ? directory.select(selection).then(() => true, () => false) : Promise.resolve(false)
1250
- };
1251
- }
1276
+ inject: (sessionId) => seatFace(face.modelDirectories, face.sessions, sessionId)
1252
1277
  }, ModelRouterSeat));
1253
1278
  });
1254
1279
  }
1255
1280
  //#endregion
1256
1281
  //#region src/client/index.tsx
1257
- /** The settings namespace this card edits (must match the host half). */
1282
+ /** The settings namespace this page edits (must match the host half). */
1258
1283
  const ROUTER_NS = "model-router";
1259
- /** Services required before mounting (provided by the client runtime). */
1284
+ /** The settings sidebar entry this page owns (must stay stable). */
1285
+ const SECTION_ID = "model-router";
1286
+ /** Services required before mounting (provided by the client runtime).
1287
+ * `remote.session` is included like the host ui-model-selection declares it:
1288
+ * the seat's `directoryFor` depends on the namespace, which mounts
1289
+ * asynchronously at startup. */
1260
1290
  const inject = [
1261
1291
  "slots",
1262
1292
  "locale",
@@ -1264,7 +1294,8 @@ window.__ModuleLoader__.load({
1264
1294
  "sessions",
1265
1295
  "settingsScope",
1266
1296
  "connection",
1267
- "remote"
1297
+ "remote",
1298
+ "remote.session"
1268
1299
  ];
1269
1300
  /** Client plugin body. */
1270
1301
  function apply(ctx) {
@@ -1275,14 +1306,18 @@ window.__ModuleLoader__.load({
1275
1306
  });
1276
1307
  return () => off();
1277
1308
  }, "model-router: dictionaries");
1278
- const SettingsCard = makeSettingsCard(ctx);
1309
+ const t = ctx.locale.bind(LOCALE_NS);
1310
+ const SettingsSection = makeSettingsSection(ctx);
1279
1311
  const services = ctx;
1280
1312
  const scope = services.settingsScope.bind({ namespace: ROUTER_NS });
1281
- services.slots.inject("settings.plugin.item", () => services.slots.register({
1282
- name: "settings.plugin.item",
1283
- key: ROUTER_NS,
1313
+ services.slots.inject("settings.section", () => services.slots.register({
1314
+ name: "settings.section",
1315
+ id: SECTION_ID,
1316
+ order: 300,
1317
+ label: () => t("settingsTitle"),
1318
+ locale: LOCALE_NS,
1284
1319
  inject: () => ({ scope })
1285
- }, SettingsCard));
1320
+ }, SettingsSection));
1286
1321
  registerModelRouterSeat(ctx);
1287
1322
  }
1288
1323
  //#endregion