@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 +122 -87
- package/lib/client.js.map +1 -1
- package/package.json +1 -1
- package/src/client/index.tsx +45 -20
- package/src/client/locales.ts +3 -7
- package/src/client/model-seat.tsx +144 -22
- package/src/client/{settings-card.css → settings-section.css} +19 -59
- package/src/client/{settings-card.tsx → settings-section.tsx} +159 -176
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
|
-
|
|
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
|
-
|
|
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-
|
|
160
|
-
const css$1 = "/* Model Router settings
|
|
161
|
-
const tagId$1 = "@fonlan/dsh-model-router/settings-
|
|
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-
|
|
166
|
+
//#region src/client/settings-section.tsx
|
|
171
167
|
/**
|
|
172
|
-
* The Model Router
|
|
168
|
+
* The Model Router settings page (设置 → 侧栏「模型路由」).
|
|
173
169
|
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
* the
|
|
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
|
|
183
|
-
* loading the
|
|
184
|
-
* without the host half) nothing renders
|
|
185
|
-
*
|
|
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
|
|
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
|
|
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)("
|
|
374
|
-
className: "mr-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
}),
|
|
410
|
-
className: "mr-
|
|
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
|
-
|
|
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([
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
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
|
|
1282
|
+
/** The settings namespace this page edits (must match the host half). */
|
|
1258
1283
|
const ROUTER_NS = "model-router";
|
|
1259
|
-
/**
|
|
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
|
|
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.
|
|
1282
|
-
name: "settings.
|
|
1283
|
-
|
|
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
|
-
},
|
|
1320
|
+
}, SettingsSection));
|
|
1286
1321
|
registerModelRouterSeat(ctx);
|
|
1287
1322
|
}
|
|
1288
1323
|
//#endregion
|