@fonlan/dsh-model-router 0.1.0 → 0.2.0

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
@@ -5,6 +5,7 @@
5
5
  ## 功能
6
6
 
7
7
  - **虚拟提供商 `model-router`**:出现在输入框下方的模型选择器(含默认模型设置等所有选择入口)中,列出全部已配置提供商的所有模型,模型条目副文本标注当前激活的提供商(如 `via OpenCode Go`)。
8
+ - **快速路由切换**:当当前会话选择的是 `model-router` 模型时,输入栏模型选择器右侧显示当前 provider 按钮;点击打开 provider 面板,可直接切换该模型的激活 provider。切换对所有会话的后续请求生效,完整排序和全量管理仍在设置 → 模型路由中完成。可在设置 → 模型路由中通过「在输入栏显示快速路由切换按钮」开关控制该按钮是否显示。
8
9
  - **严格按 model id 归并**:同一模型出现在多个提供商时归并为一条;展示名、上下文窗口、推理档位等参数跟随当前激活的提供商。
9
10
  - **请求时实时路由**:会话中始终保存 `provider=model-router`,每次请求发出时进程内解析当前激活提供商并委派给真实 adapter(凭据解析、重试、流式、计量全部走原管线),主 agent、子代理、工具 LLM 调用一视同仁。
10
11
  - **独立设置页**(设置 → 模型路由):每个模型下列出其全部提供商——点击切换当前使用;拖拽调整优先级顺序(供后续自动切换策略使用);未配置 API 密钥的提供商标灰不可选。
@@ -19,6 +20,7 @@
19
20
 
20
21
  ```yaml
21
22
  model-router:
23
+ showQuickSwitch: true # 是否在输入栏显示快速路由切换按钮(默认 true)
22
24
  models:
23
25
  deepseek-v4-flash:
24
26
  order: # 优先级顺序(供后续自动切换使用)
package/lib/client.js CHANGED
@@ -6,6 +6,7 @@ window.__ModuleLoader__.load({
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
  let react = require("react");
8
8
  let react_jsx_runtime = require("react/jsx-runtime");
9
+ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
9
10
  //#region src/client/locales.ts
10
11
  const LOCALE_NS = "model-router";
11
12
  const zh = {
@@ -22,7 +23,17 @@ window.__ModuleLoader__.load({
22
23
  dragHint: "拖拽调整优先级顺序",
23
24
  switchFailed: "切换失败:{message}",
24
25
  reorderFailed: "排序失败:{message}",
25
- providerCount: "{count} 个提供商"
26
+ providerCount: "{count} 个提供商",
27
+ routeButtonLabel: "快速切换模型路由",
28
+ routeButtonTitle: "切换当前模型的提供商",
29
+ routeButtonAria: "切换当前模型提供商,当前为 {provider}",
30
+ routeMenuTitle: "{model} · 选择提供商",
31
+ routeLoading: "加载路由…",
32
+ unknownProvider: "未知提供商",
33
+ noCredentialShort: "未配置凭据",
34
+ globalRouteHint: "切换后对所有会话的后续请求生效",
35
+ showQuickSwitchLabel: "在输入栏显示快速路由切换按钮",
36
+ showQuickSwitchDescription: "开启后,当当前模型是 model-router 路由时,在输入框模型选择器右侧显示 provider 快速切换按钮。"
26
37
  };
27
38
  const en = {
28
39
  settingsTitle: "Model Router",
@@ -38,7 +49,17 @@ window.__ModuleLoader__.load({
38
49
  dragHint: "Drag to adjust preference order",
39
50
  switchFailed: "Switch failed: {message}",
40
51
  reorderFailed: "Reorder failed: {message}",
41
- providerCount: "{count} providers"
52
+ providerCount: "{count} providers",
53
+ routeButtonLabel: "Quick model route switcher",
54
+ routeButtonTitle: "Switch the current model provider",
55
+ routeButtonAria: "Switch current model provider, currently {provider}",
56
+ routeMenuTitle: "{model} · Choose provider",
57
+ routeLoading: "Loading routes…",
58
+ unknownProvider: "Unknown provider",
59
+ noCredentialShort: "No credentials",
60
+ globalRouteHint: "Changes apply to subsequent requests in all sessions",
61
+ showQuickSwitchLabel: "Show quick route switcher in the composer",
62
+ showQuickSwitchDescription: "When enabled, a provider quick-switch button appears next to the model selector when the current model is a model-router route."
42
63
  };
43
64
  //#endregion
44
65
  //#region src/client/api.ts
@@ -74,17 +95,18 @@ window.__ModuleLoader__.load({
74
95
  modelId,
75
96
  order
76
97
  }),
77
- refresh: () => call("refresh")
98
+ refresh: () => call("refresh"),
99
+ setShowQuickSwitch: (value) => call("set-show-quick-switch", { value })
78
100
  };
79
101
  //#endregion
80
102
  //#region \0dsh-css:/Users/fonlan/Repos/dsh-model-router/src/client/settings-section.css.mjs
81
- const css = "/* Model Router settings page — theme-consistent via DSW alias variables. */\n.mr-root {\n display: flex;\n flex-direction: column;\n gap: 12px;\n padding: 4px 0 24px;\n}\n\n.mr-description {\n margin: 0;\n color: var(--dsw-alias-label-secondary);\n font-size: 13px;\n line-height: 20px;\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";
82
- const tagId = "@fonlan/dsh-model-router/settings-section.css";
83
- if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
103
+ const css$1 = "/* Model Router settings page — theme-consistent via DSW alias variables. */\n.mr-root {\n display: flex;\n flex-direction: column;\n gap: 12px;\n padding: 4px 0 24px;\n}\n\n.mr-description {\n margin: 0;\n color: var(--dsw-alias-label-secondary);\n font-size: 13px;\n line-height: 20px;\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";
104
+ const tagId$1 = "@fonlan/dsh-model-router/settings-section.css";
105
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
84
106
  const tag = document.createElement("style");
85
107
  tag.dataset.plugin = "@fonlan/dsh-model-router";
86
- tag.dataset.pluginCss = tagId;
87
- tag.textContent = css;
108
+ tag.dataset.pluginCss = tagId$1;
109
+ tag.textContent = css$1;
88
110
  document.head.appendChild(tag);
89
111
  }
90
112
  //#endregion
@@ -134,6 +156,7 @@ window.__ModuleLoader__.load({
134
156
  const [loading, setLoading] = (0, react.useState)(true);
135
157
  const [busyModel, setBusyModel] = (0, react.useState)(null);
136
158
  const [drag, setDrag] = (0, react.useState)(null);
159
+ const [quickSwitchBusy, setQuickSwitchBusy] = (0, react.useState)(false);
137
160
  const load = (0, react.useCallback)(async () => {
138
161
  setLoading(true);
139
162
  try {
@@ -187,6 +210,27 @@ window.__ModuleLoader__.load({
187
210
  setBusyModel(null);
188
211
  }
189
212
  }, [t]);
213
+ const toggleQuickSwitch = (0, react.useCallback)(async (value) => {
214
+ if (state === null) return;
215
+ const previous = state.showQuickSwitch;
216
+ setState((prev) => prev === null ? prev : {
217
+ ...prev,
218
+ showQuickSwitch: value
219
+ });
220
+ setQuickSwitchBusy(true);
221
+ try {
222
+ setState(await api.setShowQuickSwitch(value));
223
+ setError(null);
224
+ } catch (cause) {
225
+ setState((prev) => prev === null ? prev : {
226
+ ...prev,
227
+ showQuickSwitch: previous
228
+ });
229
+ setError(cause instanceof Error ? cause.message : String(cause));
230
+ } finally {
231
+ setQuickSwitchBusy(false);
232
+ }
233
+ }, [state]);
190
234
  const models = (0, react.useMemo)(() => state?.models ?? [], [state]);
191
235
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
192
236
  className: "mr-root",
@@ -195,6 +239,24 @@ window.__ModuleLoader__.load({
195
239
  className: "mr-description",
196
240
  children: t("settingsDescription")
197
241
  }),
242
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
243
+ className: "mr-quick-switch",
244
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
245
+ type: "checkbox",
246
+ checked: state?.showQuickSwitch ?? true,
247
+ disabled: loading || quickSwitchBusy,
248
+ onChange: (event) => void toggleQuickSwitch(event.target.checked)
249
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
250
+ className: "mr-quick-switch-copy",
251
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
252
+ className: "mr-quick-switch-label",
253
+ children: t("showQuickSwitchLabel")
254
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
255
+ className: "mr-quick-switch-desc",
256
+ children: t("showQuickSwitchDescription")
257
+ })]
258
+ })]
259
+ }),
198
260
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
199
261
  className: "mr-toolbar",
200
262
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -336,9 +398,223 @@ window.__ModuleLoader__.load({
336
398
  });
337
399
  }
338
400
  //#endregion
401
+ //#region \0dsh-css:/Users/fonlan/Repos/dsh-model-router/src/client/route-switcher.css.mjs
402
+ const css = "/* Quick provider route control beside DSH's composer model seat. */\n.mr-route-anchor {\n /* Menu's wrapper span: never let the row crush the trigger. */\n flex: none;\n min-width: 0;\n}\n\n.mr-route-trigger {\n min-width: 0;\n max-width: 180px;\n height: 28px;\n display: flex;\n align-items: center;\n gap: 2px;\n padding: 0 6px;\n border: none;\n border-radius: 24px;\n color: var(--dsw-alias-label-secondary);\n background: transparent;\n cursor: pointer;\n font: inherit;\n font-size: 12px;\n font-weight: 500;\n line-height: 20px;\n outline: none;\n}\n\n.mr-route-trigger:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.mr-route-trigger:focus-visible {\n box-shadow: 0 0 0 2px var(--dsw-alias-border-l3);\n}\n\n.mr-route-trigger:disabled {\n color: var(--dsw-alias-label-dimmed);\n cursor: default;\n opacity: 0.7;\n}\n\n.mr-route-trigger-label {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.mr-route-trigger-chevron {\n color: var(--dsw-alias-label-caption);\n flex: none;\n transition: transform 0.12s;\n}\n\n.mr-route-trigger[aria-expanded=\"true\"] .mr-route-trigger-chevron {\n transform: rotate(180deg);\n}\n\n.mr-route-item-copy {\n min-width: 0;\n display: flex;\n align-items: baseline;\n gap: 6px;\n}\n\n.mr-route-item-name {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.mr-route-item-note {\n flex: none;\n color: var(--dsw-alias-label-tertiary);\n font-size: 11px;\n line-height: 16px;\n}\n\n/*\n * Mobile portrait: the composer row is an inline-size container. Keep the\n * fixed control row intact — attachment, permission, provider (icon-only),\n * context meter, send — and let the model seat absorb the remaining width.\n * The breakpoint matches DSH's own narrow-composer rule (<=460px).\n */\n@container (width <= 460px) {\n .mr-route-anchor {\n flex: 0 0 auto;\n }\n\n .mr-route-trigger {\n /* The trigger now carries globe + chevron, so let it size naturally\n instead of forcing a 28px circle that could overflow the row. */\n height: 28px;\n padding: 4px 6px;\n justify-content: center;\n border-radius: 999px;\n }\n\n /*\n * Make the composer's trailing group (the parent of our [data-slot] right\n * wrapper) shrinkable on narrow rows so the globe/chevron trigger never\n * spills left over the permission control. The model seat inside it\n * absorbs the compression (flex 1 1 0, min-width 0 below).\n */\n div:has(> [data-slot=\"conversation.input.right\"]) {\n flex: 1 1 auto;\n min-width: 0;\n justify-content: flex-end;\n /* Mirror the trailing group's internal gap (12px) so the provider\n trigger has equal spacing to the permission button on its left and\n the model seat on its right. */\n margin-left: 12px;\n }\n\n .mr-route-trigger-label {\n display: none;\n }\n\n /*\n * The model seat: each slot is wrapped in a display:contents\n * [data-slot] div, so the ModelSelect root div (the real flex item of the\n * composer row) is [data-slot=\"conversation.input.model\"] > *. Give it\n * the remaining row width; the other controls keep fixed sizes.\n */\n [data-slot=\"conversation.input.right\"] + [data-slot=\"conversation.input.model\"] > * {\n flex: 1 1 0;\n min-width: 0;\n max-width: 100%;\n }\n\n [data-slot=\"conversation.input.right\"] + [data-slot=\"conversation.input.model\"] [aria-haspopup=\"menu\"] {\n width: 100%;\n max-width: 100%;\n }\n}\n";
403
+ const tagId = "@fonlan/dsh-model-router/route-switcher.css";
404
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
405
+ const tag = document.createElement("style");
406
+ tag.dataset.plugin = "@fonlan/dsh-model-router";
407
+ tag.dataset.pluginCss = tagId;
408
+ tag.textContent = css;
409
+ document.head.appendChild(tag);
410
+ }
411
+ //#endregion
412
+ //#region src/client/route-switcher.tsx
413
+ const ROUTER_PROVIDER_ID = "model-router";
414
+ /**
415
+ * The quick route switcher lives beside DSH's model seat in the composer row.
416
+ * It reads the same per-session model directory as the host model picker, but
417
+ * only appears when the selected model is the virtual model-router route.
418
+ */
419
+ function RouteSwitcher({ session, available, directory, load, t }) {
420
+ const directoryState = (0, react.useSyncExternalStore)((onChange) => directory.subscribe(onChange), () => directory.getSnapshot(), () => directory.getSnapshot());
421
+ const current = directoryState.current;
422
+ const [routerState, setRouterState] = (0, react.useState)(null);
423
+ const [routerLoading, setRouterLoading] = (0, react.useState)(false);
424
+ const [routerError, setRouterError] = (0, react.useState)(null);
425
+ const [open, setOpen] = (0, react.useState)(false);
426
+ const [busyProvider, setBusyProvider] = (0, react.useState)(null);
427
+ const isRouterModel = available && current?.provider === ROUTER_PROVIDER_ID;
428
+ const modelId = isRouterModel ? current.model : null;
429
+ const loadRouterState = (0, react.useCallback)(async () => {
430
+ setRouterLoading(true);
431
+ try {
432
+ setRouterState(await api.state());
433
+ setRouterError(null);
434
+ } catch (cause) {
435
+ setRouterError(cause instanceof Error ? cause.message : String(cause));
436
+ } finally {
437
+ setRouterLoading(false);
438
+ }
439
+ }, []);
440
+ (0, react.useEffect)(() => {
441
+ if (!available) return;
442
+ load();
443
+ }, [available, load]);
444
+ (0, react.useEffect)(() => {
445
+ if (!available) return;
446
+ loadRouterState();
447
+ }, [available]);
448
+ (0, react.useEffect)(() => {
449
+ setOpen(false);
450
+ setBusyProvider(null);
451
+ setRouterState(null);
452
+ setRouterError(null);
453
+ }, [modelId]);
454
+ (0, react.useEffect)(() => {
455
+ if (!isRouterModel || modelId === null || directoryState.status !== "ready") return;
456
+ loadRouterState();
457
+ }, [
458
+ directoryState.status,
459
+ isRouterModel,
460
+ loadRouterState,
461
+ modelId
462
+ ]);
463
+ const routeModel = (0, react.useMemo)(() => modelId === null ? void 0 : routerState?.models.find((model) => model.id === modelId), [modelId, routerState]);
464
+ const providers = (0, react.useMemo)(() => {
465
+ if (routeModel === void 0) return [];
466
+ const byId = new Map(routeModel.providers.map((provider) => [provider.id, provider]));
467
+ const ordered = routeModel.order.map((id) => byId.get(id)).filter((provider) => provider !== void 0);
468
+ const seen = new Set(ordered.map((provider) => provider.id));
469
+ return [...ordered, ...routeModel.providers.filter((provider) => !seen.has(provider.id))];
470
+ }, [routeModel]);
471
+ const switchProvider = (0, react.useCallback)(async (providerId) => {
472
+ if (modelId === null || routeModel === void 0 || routeModel.active === providerId) {
473
+ setOpen(false);
474
+ return;
475
+ }
476
+ setBusyProvider(providerId);
477
+ try {
478
+ const next = await api.switchActive(modelId, providerId);
479
+ setRouterState(next);
480
+ setRouterError(null);
481
+ setOpen(false);
482
+ load();
483
+ } catch (cause) {
484
+ setRouterError(t("switchFailed", { message: cause instanceof Error ? cause.message : String(cause) }));
485
+ } finally {
486
+ setBusyProvider(null);
487
+ }
488
+ }, [
489
+ load,
490
+ modelId,
491
+ routeModel,
492
+ t
493
+ ]);
494
+ if (session.removed || !isRouterModel) return null;
495
+ if (routerState?.showQuickSwitch === false) return null;
496
+ const activeName = providers.find((provider) => provider.id === routeModel?.active)?.name ?? routeModel?.active ?? t("routeLoading");
497
+ if (routeModel !== void 0 && providers.length < 2) return null;
498
+ const menuItems = routeModel === void 0 ? [{
499
+ type: "label",
500
+ id: "route-status",
501
+ text: routerError ?? t("routeLoading")
502
+ }] : [
503
+ {
504
+ type: "label",
505
+ id: "route-model",
506
+ text: t("routeMenuTitle", { model: modelId })
507
+ },
508
+ ...providers.map((provider) => ({
509
+ id: provider.id,
510
+ label: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
511
+ className: "mr-route-item-copy",
512
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
513
+ className: "mr-route-item-name",
514
+ children: provider.name
515
+ }), !provider.credentialConfigured && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
516
+ className: "mr-route-item-note",
517
+ children: t("noCredentialShort")
518
+ })]
519
+ }),
520
+ disabled: !provider.credentialConfigured || busyProvider !== null
521
+ })),
522
+ ...routerError === null ? [] : [{
523
+ id: "route-error",
524
+ label: routerError,
525
+ disabled: true,
526
+ danger: true
527
+ }]
528
+ ];
529
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
530
+ className: "mr-route-anchor",
531
+ open,
532
+ side: "top",
533
+ align: "end",
534
+ portal: true,
535
+ compact: true,
536
+ dense: true,
537
+ items: menuItems,
538
+ selectedId: routeModel?.active ?? void 0,
539
+ onSelect: (id) => {
540
+ if (id === "route-error" || routeModel === void 0) {
541
+ loadRouterState();
542
+ load();
543
+ return;
544
+ }
545
+ switchProvider(id);
546
+ },
547
+ onClose: () => setOpen(false),
548
+ footer: [{
549
+ type: "label",
550
+ id: "route-global-hint",
551
+ text: t("globalRouteHint")
552
+ }],
553
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
554
+ type: "button",
555
+ className: "mr-route-trigger",
556
+ "aria-label": t("routeButtonAria", { provider: activeName }),
557
+ "aria-haspopup": "menu",
558
+ "aria-expanded": open,
559
+ disabled: routerLoading || busyProvider !== null,
560
+ onClick: () => {
561
+ setOpen((value) => !value);
562
+ if (!open) {
563
+ loadRouterState();
564
+ load();
565
+ }
566
+ },
567
+ title: t("routeButtonTitle"),
568
+ children: [
569
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconGlobeOutline14, {}),
570
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
571
+ className: "mr-route-trigger-label",
572
+ children: activeName
573
+ }),
574
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
575
+ className: "mr-route-trigger-chevron",
576
+ "aria-hidden": true,
577
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, {})
578
+ })
579
+ ]
580
+ })
581
+ });
582
+ }
583
+ /** Register the quick route switcher into the composer right-side tool row. */
584
+ function registerRouteSwitcher(ctx) {
585
+ const t = ctx.locale.bind(LOCALE_NS);
586
+ ctx.inject(["slots", "modelDirectories"], (scope) => {
587
+ const models = scope.modelDirectories;
588
+ const sessions = scope.sessions;
589
+ scope.slots.inject("conversation.input.right", () => scope.slots.register({
590
+ name: "conversation.input.right",
591
+ id: "model-router-route-switcher",
592
+ order: 100,
593
+ label: () => t("routeButtonLabel"),
594
+ locale: LOCALE_NS,
595
+ inject: (sessionId) => {
596
+ const directory = models.directoryFor(sessionId);
597
+ const available = sessions.subagentAddress(sessionId) === void 0;
598
+ return {
599
+ available,
600
+ directory: directory.store,
601
+ load: () => {
602
+ if (available) directory.load().catch(() => {});
603
+ }
604
+ };
605
+ }
606
+ }, RouteSwitcher));
607
+ });
608
+ }
609
+ //#endregion
339
610
  //#region src/client/index.tsx
340
611
  /** Services required before mounting (provided by the client runtime). */
341
- const inject = ["slots", "locale"];
612
+ const inject = [
613
+ "slots",
614
+ "locale",
615
+ "modelDirectories",
616
+ "sessions"
617
+ ];
342
618
  /** Client plugin body. */
343
619
  function apply(ctx) {
344
620
  const t = ctx.locale.bind(LOCALE_NS);
@@ -357,6 +633,7 @@ window.__ModuleLoader__.load({
357
633
  label: () => t("settingsTitle"),
358
634
  locale: LOCALE_NS
359
635
  }, SettingsSection));
636
+ registerRouteSwitcher(ctx);
360
637
  }
361
638
  //#endregion
362
639
  exports.apply = apply;
package/lib/client.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":["useCallback","useSyncExternalStore","useState","useMemo"],"sources":["../../../src/client/locales.ts","../../../src/client/api.ts","../../../src/client/settings-section.tsx","../../../src/client/index.tsx"],"sourcesContent":["// Locale namespace = the plugin's cordis id (kebab). DSH flat namespace keys\n// (settings + locale) use the internal id `model-router`; the scoped npm\n// package name is reserved for package-bound identifiers (exported `name`,\n// module-loader id, the /plugins/<pkg>/… route base).\nexport const LOCALE_NS = 'model-router'\n\nexport const zh = {\n settingsTitle: '模型路由',\n settingsDescription: '每个模型按 model id 聚合所有提供商;点击提供商切换该模型当前使用的路由,拖拽调整优先级顺序(供后续自动切换使用)。',\n loading: '加载中…',\n loadFailed: '加载失败:{message}',\n retry: '重试',\n refresh: '刷新',\n empty: '没有可路由的提供商或模型,请先在设置中添加提供商。',\n modelsCount: '共 {count} 个模型',\n activeBadge: '当前',\n noCredential: '未配置 API 密钥,不可选',\n dragHint: '拖拽调整优先级顺序',\n switchFailed: '切换失败:{message}',\n reorderFailed: '排序失败:{message}',\n providerCount: '{count} 个提供商',\n}\n\nexport const en = {\n settingsTitle: 'Model Router',\n settingsDescription: 'Each model aggregates its providers by exact model id. Click a provider to switch where this model currently routes; drag to adjust the preference order (used by automatic failover later).',\n loading: 'Loading…',\n loadFailed: 'Load failed: {message}',\n retry: 'Retry',\n refresh: 'Refresh',\n empty: 'No routable providers or models yet — add providers in settings first.',\n modelsCount: '{count} models',\n activeBadge: 'active',\n noCredential: 'No API key configured — not selectable',\n dragHint: 'Drag to adjust preference order',\n switchFailed: 'Switch failed: {message}',\n reorderFailed: 'Reorder failed: {message}',\n providerCount: '{count} providers',\n}\n","/** Client transport for the model-router settings API. */\nexport interface RouterProviderView {\n id: string\n name: string\n credentialConfigured: boolean\n}\n\nexport interface RouterModelView {\n id: string\n name: string\n active: string | null\n order: string[]\n providers: RouterProviderView[]\n}\n\nexport interface ModelRouterState {\n providers: RouterProviderView[]\n models: RouterModelView[]\n}\n\nexport class ModelRouterApiError extends Error {\n constructor(readonly code: string, message: string) {\n super(message)\n }\n}\n\nasync function call<T>(method: string, payload: Record<string, unknown> = {}): Promise<T> {\n let response: Response\n try {\n response = await fetch(`/plugins/@fonlan/dsh-model-router/api/${method}`, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(payload),\n })\n } catch (error) {\n throw new ModelRouterApiError('network', error instanceof Error ? error.message : String(error))\n }\n const parsed: { ok?: boolean; value?: unknown; error?: { code?: string; message?: string } } | null\n = await response.json().catch(() => null)\n if (!response.ok || parsed === null || parsed.ok !== true || parsed.value === undefined) {\n throw new ModelRouterApiError(\n parsed?.error?.code ?? 'http',\n parsed?.error?.message ?? `HTTP ${response.status}`,\n )\n }\n return parsed.value as T\n}\n\nexport const api = {\n state: () => call<ModelRouterState>('state'),\n switchActive: (modelId: string, providerId: string) =>\n call<ModelRouterState>('switch-active', { modelId, providerId }),\n reorder: (modelId: string, order: string[]) =>\n call<ModelRouterState>('reorder', { modelId, order }),\n refresh: () => call<ModelRouterState>('refresh'),\n}\n","/**\n * The Model Router settings page: one card per merged model, provider chips\n * that switch the active route on click and reorder by native HTML5 drag &\n * drop (preference order for future automatic failover). All reads/mutations\n * go through the plugin's fenced API; the server persists through the\n * settings service, so a switch here is live for the next request globally.\n */\nimport { useCallback, useEffect, useMemo, useState, useSyncExternalStore } from 'react'\nimport type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'\nimport type { Translate } from '@deepseek-ai/dsh-client-ui-slots'\nimport { LOCALE_NS } from './locales'\nimport { api, type ModelRouterState, type RouterModelView } from './api'\nimport './settings-section.css'\n\nfunction arrayMove<T>(list: readonly T[], from: number, to: number): T[] {\n const next = [...list]\n const [item] = next.splice(from, 1)\n next.splice(to, 0, item)\n return next\n}\n\nfunction useLocaleRevision(ctx: ClientContext): number {\n const subscribe = useCallback(\n (onChange: () => void) => {\n try {\n return ctx.locale.subscribe(onChange)\n } catch {\n return () => {}\n }\n },\n [ctx],\n )\n const getSnapshot = useCallback(() => {\n try {\n return ctx.locale.getLocale().revision\n } catch {\n return 0\n }\n }, [ctx])\n return useSyncExternalStore(subscribe, getSnapshot)\n}\n\nexport function makeSettingsSection(ctx: ClientContext): () => JSX.Element {\n // Bound translation is namespace-typed; the section's props use the plain\n // Translate face (string keys), which the dict satisfies structurally.\n const t: Translate = (() => {\n try {\n return ctx.locale.bind(LOCALE_NS) as unknown as Translate\n } catch {\n return (key: string) => key\n }\n })()\n\n return function ModelRouterSettingsSection(): JSX.Element {\n useLocaleRevision(ctx)\n const [state, setState] = useState<ModelRouterState | null>(null)\n const [error, setError] = useState<string | null>(null)\n const [loading, setLoading] = useState(true)\n const [busyModel, setBusyModel] = useState<string | null>(null)\n const [drag, setDrag] = useState<{ modelId: string; index: number } | null>(null)\n\n const load = useCallback(async () => {\n setLoading(true)\n try {\n setState(await api.state())\n setError(null)\n } catch (cause) {\n setError(cause instanceof Error ? cause.message : String(cause))\n } finally {\n setLoading(false)\n }\n }, [])\n\n useEffect(() => {\n void load()\n }, [load])\n\n const switchActive = useCallback(async (model: RouterModelView, providerId: string) => {\n if (model.active === providerId) return\n setBusyModel(model.id)\n try {\n setState(await api.switchActive(model.id, providerId))\n setError(null)\n } catch (cause) {\n setError(t('switchFailed', { message: cause instanceof Error ? cause.message : String(cause) }))\n } finally {\n setBusyModel(null)\n }\n }, [t])\n\n const reorder = useCallback(async (model: RouterModelView, from: number, to: number) => {\n if (from === to) return\n const order = arrayMove(model.order, from, to)\n setState(prev => prev === null ? prev : {\n ...prev,\n models: prev.models.map(m => (m.id === model.id ? { ...m, order } : m)),\n })\n setBusyModel(model.id)\n try {\n setState(await api.reorder(model.id, order))\n setError(null)\n } catch (cause) {\n setError(t('reorderFailed', { message: cause instanceof Error ? cause.message : String(cause) }))\n setState(prev => prev === null ? prev : {\n ...prev,\n models: prev.models.map(m => (m.id === model.id ? { ...m, order: model.order } : m)),\n })\n } finally {\n setBusyModel(null)\n }\n }, [t])\n\n const models = useMemo(() => state?.models ?? [], [state])\n\n return (\n <div className=\"mr-root\">\n <p className=\"mr-description\">{t('settingsDescription')}</p>\n <div className=\"mr-toolbar\">\n <span className=\"mr-count\">{t('modelsCount', { count: models.length })}</span>\n <button type=\"button\" className=\"mr-button\" onClick={() => void load()} disabled={loading}>\n {t('refresh')}\n </button>\n </div>\n {error !== null && (\n <div className=\"mr-error\" role=\"alert\">{error}</div>\n )}\n {loading && models.length === 0 && (\n <div className=\"mr-empty\">{t('loading')}</div>\n )}\n {!loading && models.length === 0 && (\n <div className=\"mr-empty\">{t('empty')}</div>\n )}\n <div className=\"mr-models\">\n {models.map(model => (\n <ModelCard\n key={model.id}\n model={model}\n t={t}\n busy={busyModel === model.id}\n dragging={drag}\n onDragStart={index => setDrag({ modelId: model.id, index })}\n onDragEnd={() => setDrag(null)}\n onDrop={index => {\n if (drag !== null && drag.modelId === model.id) {\n void reorder(model, drag.index, index)\n }\n setDrag(null)\n }}\n onSwitch={providerId => void switchActive(model, providerId)}\n />\n ))}\n </div>\n </div>\n )\n }\n}\n\ninterface ModelCardProps {\n model: RouterModelView\n t: Translate\n busy: boolean\n dragging: { modelId: string; index: number } | null\n onDragStart: (index: number) => void\n onDragEnd: () => void\n onDrop: (index: number) => void\n onSwitch: (providerId: string) => void\n}\n\nfunction ModelCard(props: ModelCardProps): JSX.Element {\n const { model, t, busy, dragging } = props\n return (\n <section className={`mr-card${busy ? ' mr-card-busy' : ''}`}>\n <header className=\"mr-card-header\">\n <span className=\"mr-model-name\">{model.name}</span>\n <span className=\"mr-model-id\">{model.id}</span>\n <span className=\"mr-provider-count\">{t('providerCount', { count: model.providers.length })}</span>\n </header>\n <p className=\"mr-drag-hint\">{t('dragHint')}</p>\n <ul className=\"mr-providers\">\n {model.providers.map((provider, index) => {\n const active = provider.id === model.active\n const selectable = provider.credentialConfigured\n const dropTarget = dragging !== null && dragging.modelId === model.id && dragging.index !== index\n return (\n <li\n key={provider.id}\n className={[\n 'mr-provider',\n active ? 'mr-provider-active' : '',\n !selectable ? 'mr-provider-nocred' : '',\n dropTarget ? 'mr-provider-drop' : '',\n ].filter(Boolean).join(' ')}\n draggable={!busy && model.providers.length > 1}\n title={selectable ? undefined : t('noCredential')}\n onDragStart={(event) => {\n props.onDragStart(index)\n event.dataTransfer.effectAllowed = 'move'\n try {\n event.dataTransfer.setData('text/plain', model.id)\n } catch {\n // drag data is cosmetic for our own handler\n }\n }}\n onDragOver={(event) => {\n if (dragging !== null && dragging.modelId === model.id && dragging.index !== index) {\n event.preventDefault()\n event.dataTransfer.dropEffect = 'move'\n }\n }}\n onDrop={(event) => {\n if (dragging !== null && dragging.modelId === model.id) {\n event.preventDefault()\n props.onDrop(index)\n }\n }}\n onDragEnd={() => props.onDragEnd()}\n >\n <span className=\"mr-grip\" aria-hidden=\"true\">⠿</span>\n <button\n type=\"button\"\n className=\"mr-provider-button\"\n disabled={!selectable || busy || active}\n onClick={() => props.onSwitch(provider.id)}\n >\n <span className=\"mr-provider-name\">{provider.name}</span>\n {!selectable && <span className=\"mr-provider-nocred-label\">· {t('noCredential')}</span>}\n </button>\n {active && <span className=\"mr-active-badge\">{t('activeBadge')}</span>}\n </li>\n )\n })}\n </ul>\n </section>\n )\n}\n","/**\n * @fonlan/dsh-model-router client half: the Model Router settings page plus\n * bilingual dictionaries. The composer model picker needs no client\n * injection — the host catalog already renders the `model-router` group and\n * each model's description carries the active provider.\n */\nimport type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-slots'\nimport type {} from '@deepseek-ai/dsh-client-ui-settings/client'\nimport { LOCALE_NS, zh, en } from './locales'\nimport { makeSettingsSection } from './settings-section'\n\n/** Services required before mounting (provided by the client runtime). */\nexport const inject = ['slots', 'locale']\n\n/** Client plugin body. */\nexport function apply(ctx: ClientContext): void {\n const t = ctx.locale.bind(LOCALE_NS)\n ctx.effect(() => {\n const off = ctx.locale.register(LOCALE_NS, { zh, en })\n return () => off()\n }, 'model-router: dictionaries')\n\n const SettingsSection = makeSettingsSection(ctx)\n ctx.slots.inject('settings.section', () =>\n ctx.slots.register({\n name: 'settings.section',\n id: 'model-router',\n order: 300,\n label: () => t('settingsTitle'),\n locale: LOCALE_NS,\n }, SettingsSection as never),\n )\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.js","names":["useCallback","useSyncExternalStore","useState","useMemo","useSyncExternalStore","useState","useCallback","useMemo","Menu","IconGlobeOutline14","IconChevronDownOutline14"],"sources":["../../../src/client/locales.ts","../../../src/client/api.ts","../../../src/client/settings-section.tsx","../../../src/client/route-switcher.tsx","../../../src/client/index.tsx"],"sourcesContent":["// Locale namespace = the plugin's cordis id (kebab). DSH flat namespace keys\n// (settings + locale) use the internal id `model-router`; the scoped npm\n// package name is reserved for package-bound identifiers (exported `name`,\n// module-loader id, the /plugins/<pkg>/… route base).\nexport const LOCALE_NS = 'model-router'\n\nexport const zh = {\n settingsTitle: '模型路由',\n settingsDescription: '每个模型按 model id 聚合所有提供商;点击提供商切换该模型当前使用的路由,拖拽调整优先级顺序(供后续自动切换使用)。',\n loading: '加载中…',\n loadFailed: '加载失败:{message}',\n retry: '重试',\n refresh: '刷新',\n empty: '没有可路由的提供商或模型,请先在设置中添加提供商。',\n modelsCount: '共 {count} 个模型',\n activeBadge: '当前',\n noCredential: '未配置 API 密钥,不可选',\n dragHint: '拖拽调整优先级顺序',\n switchFailed: '切换失败:{message}',\n reorderFailed: '排序失败:{message}',\n providerCount: '{count} 个提供商',\n routeButtonLabel: '快速切换模型路由',\n routeButtonTitle: '切换当前模型的提供商',\n routeButtonAria: '切换当前模型提供商,当前为 {provider}',\n routeMenuTitle: '{model} · 选择提供商',\n routeLoading: '加载路由…',\n unknownProvider: '未知提供商',\n noCredentialShort: '未配置凭据',\n globalRouteHint: '切换后对所有会话的后续请求生效',\n showQuickSwitchLabel: '在输入栏显示快速路由切换按钮',\n showQuickSwitchDescription: '开启后,当当前模型是 model-router 路由时,在输入框模型选择器右侧显示 provider 快速切换按钮。',\n}\n\nexport const en = {\n settingsTitle: 'Model Router',\n settingsDescription: 'Each model aggregates its providers by exact model id. Click a provider to switch where this model currently routes; drag to adjust the preference order (used by automatic failover later).',\n loading: 'Loading…',\n loadFailed: 'Load failed: {message}',\n retry: 'Retry',\n refresh: 'Refresh',\n empty: 'No routable providers or models yet — add providers in settings first.',\n modelsCount: '{count} models',\n activeBadge: 'active',\n noCredential: 'No API key configured — not selectable',\n dragHint: 'Drag to adjust preference order',\n switchFailed: 'Switch failed: {message}',\n reorderFailed: 'Reorder failed: {message}',\n providerCount: '{count} providers',\n routeButtonLabel: 'Quick model route switcher',\n routeButtonTitle: 'Switch the current model provider',\n routeButtonAria: 'Switch current model provider, currently {provider}',\n routeMenuTitle: '{model} · Choose provider',\n routeLoading: 'Loading routes…',\n unknownProvider: 'Unknown provider',\n noCredentialShort: 'No credentials',\n globalRouteHint: 'Changes apply to subsequent requests in all sessions',\n showQuickSwitchLabel: 'Show quick route switcher in the composer',\n showQuickSwitchDescription: 'When enabled, a provider quick-switch button appears next to the model selector when the current model is a model-router route.',\n}\n","/** Client transport for the model-router settings API. */\nexport interface RouterProviderView {\n id: string\n name: string\n credentialConfigured: boolean\n}\n\nexport interface RouterModelView {\n id: string\n name: string\n active: string | null\n order: string[]\n providers: RouterProviderView[]\n}\n\nexport interface ModelRouterState {\n /** Whether the composer quick route-switcher button is enabled. */\n showQuickSwitch: boolean\n providers: RouterProviderView[]\n models: RouterModelView[]\n}\n\nexport class ModelRouterApiError extends Error {\n constructor(readonly code: string, message: string) {\n super(message)\n }\n}\n\nasync function call<T>(method: string, payload: Record<string, unknown> = {}): Promise<T> {\n let response: Response\n try {\n response = await fetch(`/plugins/@fonlan/dsh-model-router/api/${method}`, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(payload),\n })\n } catch (error) {\n throw new ModelRouterApiError('network', error instanceof Error ? error.message : String(error))\n }\n const parsed: { ok?: boolean; value?: unknown; error?: { code?: string; message?: string } } | null\n = await response.json().catch(() => null)\n if (!response.ok || parsed === null || parsed.ok !== true || parsed.value === undefined) {\n throw new ModelRouterApiError(\n parsed?.error?.code ?? 'http',\n parsed?.error?.message ?? `HTTP ${response.status}`,\n )\n }\n return parsed.value as T\n}\n\nexport const api = {\n state: () => call<ModelRouterState>('state'),\n switchActive: (modelId: string, providerId: string) =>\n call<ModelRouterState>('switch-active', { modelId, providerId }),\n reorder: (modelId: string, order: string[]) =>\n call<ModelRouterState>('reorder', { modelId, order }),\n refresh: () => call<ModelRouterState>('refresh'),\n setShowQuickSwitch: (value: boolean) =>\n call<ModelRouterState>('set-show-quick-switch', { value }),\n}\n","/**\n * The Model Router settings page: one card per merged model, provider chips\n * that switch the active route on click and reorder by native HTML5 drag &\n * drop (preference order for future automatic failover). All reads/mutations\n * go through the plugin's fenced API; the server persists through the\n * settings service, so a switch here is live for the next request globally.\n */\nimport { useCallback, useEffect, useMemo, useState, useSyncExternalStore } from 'react'\nimport type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'\nimport type { Translate } from '@deepseek-ai/dsh-client-ui-slots'\nimport { LOCALE_NS } from './locales'\nimport { api, type ModelRouterState, type RouterModelView } from './api'\nimport './settings-section.css'\n\nfunction arrayMove<T>(list: readonly T[], from: number, to: number): T[] {\n const next = [...list]\n const [item] = next.splice(from, 1)\n next.splice(to, 0, item)\n return next\n}\n\nfunction useLocaleRevision(ctx: ClientContext): number {\n const subscribe = useCallback(\n (onChange: () => void) => {\n try {\n return ctx.locale.subscribe(onChange)\n } catch {\n return () => {}\n }\n },\n [ctx],\n )\n const getSnapshot = useCallback(() => {\n try {\n return ctx.locale.getLocale().revision\n } catch {\n return 0\n }\n }, [ctx])\n return useSyncExternalStore(subscribe, getSnapshot)\n}\n\nexport function makeSettingsSection(ctx: ClientContext): () => JSX.Element {\n // Bound translation is namespace-typed; the section's props use the plain\n // Translate face (string keys), which the dict satisfies structurally.\n const t: Translate = (() => {\n try {\n return ctx.locale.bind(LOCALE_NS) as unknown as Translate\n } catch {\n return (key: string) => key\n }\n })()\n\n return function ModelRouterSettingsSection(): JSX.Element {\n useLocaleRevision(ctx)\n const [state, setState] = useState<ModelRouterState | null>(null)\n const [error, setError] = useState<string | null>(null)\n const [loading, setLoading] = useState(true)\n const [busyModel, setBusyModel] = useState<string | null>(null)\n const [drag, setDrag] = useState<{ modelId: string; index: number } | null>(null)\n const [quickSwitchBusy, setQuickSwitchBusy] = useState(false)\n\n const load = useCallback(async () => {\n setLoading(true)\n try {\n setState(await api.state())\n setError(null)\n } catch (cause) {\n setError(cause instanceof Error ? cause.message : String(cause))\n } finally {\n setLoading(false)\n }\n }, [])\n\n useEffect(() => {\n void load()\n }, [load])\n\n const switchActive = useCallback(async (model: RouterModelView, providerId: string) => {\n if (model.active === providerId) return\n setBusyModel(model.id)\n try {\n setState(await api.switchActive(model.id, providerId))\n setError(null)\n } catch (cause) {\n setError(t('switchFailed', { message: cause instanceof Error ? cause.message : String(cause) }))\n } finally {\n setBusyModel(null)\n }\n }, [t])\n\n const reorder = useCallback(async (model: RouterModelView, from: number, to: number) => {\n if (from === to) return\n const order = arrayMove(model.order, from, to)\n setState(prev => prev === null ? prev : {\n ...prev,\n models: prev.models.map(m => (m.id === model.id ? { ...m, order } : m)),\n })\n setBusyModel(model.id)\n try {\n setState(await api.reorder(model.id, order))\n setError(null)\n } catch (cause) {\n setError(t('reorderFailed', { message: cause instanceof Error ? cause.message : String(cause) }))\n setState(prev => prev === null ? prev : {\n ...prev,\n models: prev.models.map(m => (m.id === model.id ? { ...m, order: model.order } : m)),\n })\n } finally {\n setBusyModel(null)\n }\n }, [t])\n\n const toggleQuickSwitch = useCallback(async (value: boolean) => {\n if (state === null) return\n const previous = state.showQuickSwitch\n setState(prev => prev === null ? prev : { ...prev, showQuickSwitch: value })\n setQuickSwitchBusy(true)\n try {\n setState(await api.setShowQuickSwitch(value))\n setError(null)\n } catch (cause) {\n setState(prev => prev === null ? prev : { ...prev, showQuickSwitch: previous })\n setError(cause instanceof Error ? cause.message : String(cause))\n } finally {\n setQuickSwitchBusy(false)\n }\n }, [state])\n\n const models = useMemo(() => state?.models ?? [], [state])\n\n return (\n <div className=\"mr-root\">\n <p className=\"mr-description\">{t('settingsDescription')}</p>\n <label className=\"mr-quick-switch\">\n <input\n type=\"checkbox\"\n checked={state?.showQuickSwitch ?? true}\n disabled={loading || quickSwitchBusy}\n onChange={(event) => void toggleQuickSwitch(event.target.checked)}\n />\n <span className=\"mr-quick-switch-copy\">\n <span className=\"mr-quick-switch-label\">{t('showQuickSwitchLabel')}</span>\n <span className=\"mr-quick-switch-desc\">{t('showQuickSwitchDescription')}</span>\n </span>\n </label>\n <div className=\"mr-toolbar\">\n <span className=\"mr-count\">{t('modelsCount', { count: models.length })}</span>\n <button type=\"button\" className=\"mr-button\" onClick={() => void load()} disabled={loading}>\n {t('refresh')}\n </button>\n </div>\n {error !== null && (\n <div className=\"mr-error\" role=\"alert\">{error}</div>\n )}\n {loading && models.length === 0 && (\n <div className=\"mr-empty\">{t('loading')}</div>\n )}\n {!loading && models.length === 0 && (\n <div className=\"mr-empty\">{t('empty')}</div>\n )}\n <div className=\"mr-models\">\n {models.map(model => (\n <ModelCard\n key={model.id}\n model={model}\n t={t}\n busy={busyModel === model.id}\n dragging={drag}\n onDragStart={index => setDrag({ modelId: model.id, index })}\n onDragEnd={() => setDrag(null)}\n onDrop={index => {\n if (drag !== null && drag.modelId === model.id) {\n void reorder(model, drag.index, index)\n }\n setDrag(null)\n }}\n onSwitch={providerId => void switchActive(model, providerId)}\n />\n ))}\n </div>\n </div>\n )\n }\n}\n\ninterface ModelCardProps {\n model: RouterModelView\n t: Translate\n busy: boolean\n dragging: { modelId: string; index: number } | null\n onDragStart: (index: number) => void\n onDragEnd: () => void\n onDrop: (index: number) => void\n onSwitch: (providerId: string) => void\n}\n\nfunction ModelCard(props: ModelCardProps): JSX.Element {\n const { model, t, busy, dragging } = props\n return (\n <section className={`mr-card${busy ? ' mr-card-busy' : ''}`}>\n <header className=\"mr-card-header\">\n <span className=\"mr-model-name\">{model.name}</span>\n <span className=\"mr-model-id\">{model.id}</span>\n <span className=\"mr-provider-count\">{t('providerCount', { count: model.providers.length })}</span>\n </header>\n <p className=\"mr-drag-hint\">{t('dragHint')}</p>\n <ul className=\"mr-providers\">\n {model.providers.map((provider, index) => {\n const active = provider.id === model.active\n const selectable = provider.credentialConfigured\n const dropTarget = dragging !== null && dragging.modelId === model.id && dragging.index !== index\n return (\n <li\n key={provider.id}\n className={[\n 'mr-provider',\n active ? 'mr-provider-active' : '',\n !selectable ? 'mr-provider-nocred' : '',\n dropTarget ? 'mr-provider-drop' : '',\n ].filter(Boolean).join(' ')}\n draggable={!busy && model.providers.length > 1}\n title={selectable ? undefined : t('noCredential')}\n onDragStart={(event) => {\n props.onDragStart(index)\n event.dataTransfer.effectAllowed = 'move'\n try {\n event.dataTransfer.setData('text/plain', model.id)\n } catch {\n // drag data is cosmetic for our own handler\n }\n }}\n onDragOver={(event) => {\n if (dragging !== null && dragging.modelId === model.id && dragging.index !== index) {\n event.preventDefault()\n event.dataTransfer.dropEffect = 'move'\n }\n }}\n onDrop={(event) => {\n if (dragging !== null && dragging.modelId === model.id) {\n event.preventDefault()\n props.onDrop(index)\n }\n }}\n onDragEnd={() => props.onDragEnd()}\n >\n <span className=\"mr-grip\" aria-hidden=\"true\">⠿</span>\n <button\n type=\"button\"\n className=\"mr-provider-button\"\n disabled={!selectable || busy || active}\n onClick={() => props.onSwitch(provider.id)}\n >\n <span className=\"mr-provider-name\">{provider.name}</span>\n {!selectable && <span className=\"mr-provider-nocred-label\">· {t('noCredential')}</span>}\n </button>\n {active && <span className=\"mr-active-badge\">{t('activeBadge')}</span>}\n </li>\n )\n })}\n </ul>\n </section>\n )\n}\n","import { useCallback, useEffect, useMemo, useState, useSyncExternalStore } from 'react'\nimport type { ClientContext, SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'\nimport type { ModelDirectoryState } from '@deepseek-ai/dsh-client-ui-model-selection/client'\nimport type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'\nimport { IconChevronDownOutline14, IconGlobeOutline14, Menu, type MenuEntry } from '@deepseek-ai/dsh-client-ui-primitives'\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-model-selection/client'\nimport { api, type ModelRouterState, type RouterModelView } from './api'\nimport { LOCALE_NS } from './locales'\nimport './route-switcher.css'\n\nconst ROUTER_PROVIDER_ID = 'model-router'\n\ninterface RouteSwitcherInjected {\n available: boolean\n directory: SnapshotStore<ModelDirectoryState>\n load: () => void\n}\n\ntype RouteSwitcherProps = PropsRuntime<'conversation.input.right'>\n & PropsLocale<typeof LOCALE_NS>\n & RouteSwitcherInjected\n\ntype RouteProvider = RouterModelView['providers'][number]\n\n/**\n * The quick route switcher lives beside DSH's model seat in the composer row.\n * It reads the same per-session model directory as the host model picker, but\n * only appears when the selected model is the virtual model-router route.\n */\nexport function RouteSwitcher({\n session,\n available,\n directory,\n load,\n t,\n}: RouteSwitcherProps): JSX.Element | null {\n const directoryState = useSyncExternalStore(\n onChange => directory.subscribe(onChange),\n () => directory.getSnapshot(),\n () => directory.getSnapshot(),\n )\n const current = directoryState.current\n const [routerState, setRouterState] = useState<ModelRouterState | null>(null)\n const [routerLoading, setRouterLoading] = useState(false)\n const [routerError, setRouterError] = useState<string | null>(null)\n const [open, setOpen] = useState(false)\n const [busyProvider, setBusyProvider] = useState<string | null>(null)\n\n const isRouterModel = available && current?.provider === ROUTER_PROVIDER_ID\n const modelId = isRouterModel ? current.model : null\n\n const loadRouterState = useCallback(async () => {\n setRouterLoading(true)\n try {\n setRouterState(await api.state())\n setRouterError(null)\n } catch (cause) {\n setRouterError(cause instanceof Error ? cause.message : String(cause))\n } finally {\n setRouterLoading(false)\n }\n }, [])\n\n useEffect(() => {\n if (!available) return\n load()\n }, [available, load])\n\n // Fetch the global toggle (and the model's provider list) on mount too, so a\n // \"hide quick switcher\" setting applies even before the model directory\n // settles. The button stays visible while the value is unknown (default on).\n useEffect(() => {\n if (!available) return\n void loadRouterState()\n }, [available])\n\n useEffect(() => {\n setOpen(false)\n setBusyProvider(null)\n setRouterState(null)\n setRouterError(null)\n }, [modelId])\n\n // The model-selection directory is refreshed when another session changes\n // the router settings document. Re-read the global router state after that\n // refresh so this button never shows a stale active provider. Include the\n // model id: switching between two router models can leave the directory in\n // the same ready state while changing the API row we need to display.\n useEffect(() => {\n if (!isRouterModel || modelId === null || directoryState.status !== 'ready') return\n void loadRouterState()\n }, [directoryState.status, isRouterModel, loadRouterState, modelId])\n\n const routeModel = useMemo(\n () => modelId === null\n ? undefined\n : routerState?.models.find(model => model.id === modelId),\n [modelId, routerState],\n )\n\n const providers = useMemo(() => {\n if (routeModel === undefined) return []\n const byId = new Map(routeModel.providers.map(provider => [provider.id, provider]))\n const ordered = routeModel.order\n .map(id => byId.get(id))\n .filter((provider): provider is RouteProvider => provider !== undefined)\n const seen = new Set(ordered.map(provider => provider.id))\n return [...ordered, ...routeModel.providers.filter(provider => !seen.has(provider.id))]\n }, [routeModel])\n\n const switchProvider = useCallback(async (providerId: string) => {\n if (modelId === null || routeModel === undefined || routeModel.active === providerId) {\n setOpen(false)\n return\n }\n setBusyProvider(providerId)\n try {\n const next = await api.switchActive(modelId, providerId)\n setRouterState(next)\n setRouterError(null)\n setOpen(false)\n // The model directory also listens for settings updates, but an explicit\n // refresh keeps the host model picker description in sync immediately.\n load()\n } catch (cause) {\n setRouterError(t('switchFailed', { message: cause instanceof Error ? cause.message : String(cause) }))\n } finally {\n setBusyProvider(null)\n }\n }, [load, modelId, routeModel, t])\n\n if (session.removed || !isRouterModel) return null\n if (routerState?.showQuickSwitch === false) return null\n\n const activeProvider = providers.find(provider => provider.id === routeModel?.active)\n const activeName = activeProvider?.name ?? routeModel?.active ?? t('routeLoading')\n if (routeModel !== undefined && providers.length < 2) return null\n\n const menuItems: MenuEntry[] = routeModel === undefined\n ? [{\n type: 'label',\n id: 'route-status',\n text: routerError ?? t('routeLoading'),\n }]\n : [\n {\n type: 'label',\n id: 'route-model',\n text: t('routeMenuTitle', { model: modelId }),\n },\n ...providers.map(provider => ({\n id: provider.id,\n label: (\n <span className=\"mr-route-item-copy\">\n <span className=\"mr-route-item-name\">{provider.name}</span>\n {!provider.credentialConfigured && (\n <span className=\"mr-route-item-note\">{t('noCredentialShort')}</span>\n )}\n </span>\n ),\n disabled: !provider.credentialConfigured || busyProvider !== null,\n })),\n ...(routerError === null ? [] : [{\n id: 'route-error',\n label: routerError,\n disabled: true,\n danger: true,\n }]),\n ]\n\n return (\n <Menu\n className=\"mr-route-anchor\"\n open={open}\n side=\"top\"\n align=\"end\"\n portal\n compact\n dense\n items={menuItems}\n selectedId={routeModel?.active ?? undefined}\n onSelect={id => {\n if (id === 'route-error' || routeModel === undefined) {\n void loadRouterState()\n load()\n return\n }\n void switchProvider(id)\n }}\n onClose={() => setOpen(false)}\n footer={[\n {\n type: 'label',\n id: 'route-global-hint',\n text: t('globalRouteHint'),\n },\n ]}\n anchor={(\n <button\n type=\"button\"\n className=\"mr-route-trigger\"\n aria-label={t('routeButtonAria', { provider: activeName })}\n aria-haspopup=\"menu\"\n aria-expanded={open}\n disabled={routerLoading || busyProvider !== null}\n onClick={() => {\n setOpen(value => !value)\n if (!open) {\n void loadRouterState()\n load()\n }\n }}\n title={t('routeButtonTitle')}\n >\n <IconGlobeOutline14 />\n <span className=\"mr-route-trigger-label\">{activeName}</span>\n <span className=\"mr-route-trigger-chevron\" aria-hidden>\n <IconChevronDownOutline14 />\n </span>\n </button>\n )}\n />\n )\n}\n\n/** Register the quick route switcher into the composer right-side tool row. */\nexport function registerRouteSwitcher(ctx: ClientContext): void {\n const t = ctx.locale.bind(LOCALE_NS)\n ctx.inject(['slots', 'modelDirectories'], scope => {\n const models = scope.modelDirectories\n const sessions = scope.sessions\n scope.slots.inject('conversation.input.right', () => scope.slots.register({\n name: 'conversation.input.right',\n id: 'model-router-route-switcher',\n order: 100,\n label: () => t('routeButtonLabel'),\n locale: LOCALE_NS,\n inject: (sessionId) => {\n const directory = models.directoryFor(sessionId)\n const available = sessions.subagentAddress(sessionId) === undefined\n return {\n available,\n directory: directory.store,\n load: () => {\n if (available) directory.load().catch(() => {})\n },\n }\n },\n }, RouteSwitcher as never))\n })\n}","/**\n * @fonlan/dsh-model-router client half: the Model Router settings page plus\n * a quick provider switcher beside DSH's composer model selector.\n */\nimport type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-slots'\nimport type {} from '@deepseek-ai/dsh-client-ui-settings/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-model-selection/client'\nimport { LOCALE_NS, zh, en } from './locales'\nimport { makeSettingsSection } from './settings-section'\nimport { registerRouteSwitcher } from './route-switcher'\n\n/** Services required before mounting (provided by the client runtime). */\nexport const inject = ['slots', 'locale', 'modelDirectories', 'sessions']\n\n/** Client plugin body. */\nexport function apply(ctx: ClientContext): void {\n const t = ctx.locale.bind(LOCALE_NS)\n ctx.effect(() => {\n const off = ctx.locale.register(LOCALE_NS, { zh, en })\n return () => off()\n }, 'model-router: dictionaries')\n\n const SettingsSection = makeSettingsSection(ctx)\n ctx.slots.inject('settings.section', () =>\n ctx.slots.register({\n name: 'settings.section',\n id: 'model-router',\n order: 300,\n label: () => t('settingsTitle'),\n locale: LOCALE_NS,\n }, SettingsSection as never),\n )\n\n registerRouteSwitcher(ctx)\n}\n"],"mappings":"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"}