@fonlan/dsh-model-router 0.3.1 → 0.3.4

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
@@ -27,18 +27,40 @@ window.__ModuleLoader__.load({
27
27
  switchFailed: "切换失败:{message}",
28
28
  reorderFailed: "排序失败:{message}",
29
29
  providerCount: "{count} 个提供商",
30
- routeButtonLabel: "快速切换模型路由",
31
- routeButtonTitle: "切换当前模型的提供商",
32
- routeButtonAria: "切换当前模型提供商,当前为 {provider}",
30
+ seatLabel: "模型选择(含提供商切换)",
31
+ menuAria: "模型、提供商与推理等级",
32
+ menuModel: "模型",
33
+ menuProvider: "提供商",
34
+ menuEffort: "推理等级",
35
+ triggerFallback: "选择模型",
36
+ triggerSelectAria: "选择模型",
37
+ triggerAria: "选择模型,当前 {model}",
38
+ triggerAriaEffort: "选择模型,当前 {model},推理等级 {effort}",
39
+ effortProviderDefault: "Default",
40
+ statusLoading: "正在刷新模型列表…",
41
+ errorAction: "模型操作失败:{message}",
42
+ actionReload: "重新加载",
43
+ warningGroupLoad: "{name} 加载失败:{message}",
44
+ emptyModels: "没有可用的模型。",
45
+ emptyEfforts: "当前模型未提供推理等级。",
33
46
  routeMenuTitle: "{model} · 选择提供商",
34
47
  routeLoading: "加载路由…",
35
- unknownProvider: "未知提供商",
36
48
  noCredentialShort: "未配置凭据",
37
49
  globalRouteHint: "切换后对所有会话的后续请求生效",
38
- showQuickSwitchLabel: "在输入栏显示快速路由切换按钮",
39
- showQuickSwitchDescription: "开启后,当当前模型是 model-router 路由时,在输入框模型选择器右侧显示 provider 快速切换按钮。",
50
+ showQuickSwitchLabel: "在模型选择菜单中显示提供商切换",
51
+ showQuickSwitchDescription: "开启后,当当前模型是 model-router 路由时,模型选择弹出菜单会显示提供商列表入口,可直接切换该模型的激活提供商。",
40
52
  ignorePrefixLabel: "匹配时忽略模型 ID 前缀",
41
- ignorePrefixDescription: "开启后,deepseek/deepseek-v4-flash 与 deepseek-v4-flash 视为同一模型,统一显示不带前缀的模型 id(默认开启)。"
53
+ ignorePrefixDescription: "开启后,deepseek/deepseek-v4-flash 与 deepseek-v4-flash 视为同一模型,统一显示不带前缀的模型 id(默认开启)。",
54
+ sortTitle: "模型显示顺序",
55
+ sortDescription: "模型选择列表中 model-router 分组内模型的排列方式。",
56
+ sortModeCustom: "自定义",
57
+ sortModeName: "按名称",
58
+ sortModeRecent: "最近使用",
59
+ sortModeDescCustom: "按下方拖拽的手动顺序排列",
60
+ sortModeDescName: "按模型名称排序(数字感知)",
61
+ sortModeDescRecent: "最近用过的模型排在最前",
62
+ sortCustomHint: "拖拽调整模型在模型选择列表中的显示顺序",
63
+ sortFailed: "排序设置失败:{message}"
42
64
  };
43
65
  const en = {
44
66
  settingsTitle: "Model Router",
@@ -58,18 +80,40 @@ window.__ModuleLoader__.load({
58
80
  switchFailed: "Switch failed: {message}",
59
81
  reorderFailed: "Reorder failed: {message}",
60
82
  providerCount: "{count} providers",
61
- routeButtonLabel: "Quick model route switcher",
62
- routeButtonTitle: "Switch the current model provider",
63
- routeButtonAria: "Switch current model provider, currently {provider}",
83
+ seatLabel: "Model selection (with provider switching)",
84
+ menuAria: "Model, provider and reasoning effort",
85
+ menuModel: "Model",
86
+ menuProvider: "Provider",
87
+ menuEffort: "Effort",
88
+ triggerFallback: "Select model",
89
+ triggerSelectAria: "Select model",
90
+ triggerAria: "Select model, current {model}",
91
+ triggerAriaEffort: "Select model, current {model}, reasoning effort {effort}",
92
+ effortProviderDefault: "Default",
93
+ statusLoading: "Refreshing model list…",
94
+ errorAction: "Model operation failed: {message}",
95
+ actionReload: "Reload",
96
+ warningGroupLoad: "{name} failed to load: {message}",
97
+ emptyModels: "No models available.",
98
+ emptyEfforts: "This model provides no reasoning effort levels.",
64
99
  routeMenuTitle: "{model} · Choose provider",
65
100
  routeLoading: "Loading routes…",
66
- unknownProvider: "Unknown provider",
67
101
  noCredentialShort: "No credentials",
68
102
  globalRouteHint: "Changes apply to subsequent requests in all sessions",
69
- showQuickSwitchLabel: "Show quick route switcher in the composer",
70
- showQuickSwitchDescription: "When enabled, a provider quick-switch button appears next to the model selector when the current model is a model-router route.",
103
+ showQuickSwitchLabel: "Show provider switching in the model picker",
104
+ showQuickSwitchDescription: "When enabled, the model picker menu shows a provider list entry that switches the active provider, for model-router routes.",
71
105
  ignorePrefixLabel: "Ignore model ID prefix when matching",
72
- ignorePrefixDescription: "When enabled, deepseek/deepseek-v4-flash and deepseek-v4-flash are treated as the same model and shown without the prefix (on by default)."
106
+ ignorePrefixDescription: "When enabled, deepseek/deepseek-v4-flash and deepseek-v4-flash are treated as the same model and shown without the prefix (on by default).",
107
+ sortTitle: "Model display order",
108
+ sortDescription: "How models inside the model-router group are ordered in the model picker.",
109
+ sortModeCustom: "Custom",
110
+ sortModeName: "By name",
111
+ sortModeRecent: "Recently used",
112
+ sortModeDescCustom: "Manual order via drag below",
113
+ sortModeDescName: "Sort by model name (numeric-aware)",
114
+ sortModeDescRecent: "Most recently used models first",
115
+ sortCustomHint: "Drag to reorder models in the model picker",
116
+ sortFailed: "Sort setting failed: {message}"
73
117
  };
74
118
  //#endregion
75
119
  //#region src/client/api.ts
@@ -107,11 +151,13 @@ window.__ModuleLoader__.load({
107
151
  }),
108
152
  refresh: () => call("refresh"),
109
153
  setShowQuickSwitch: (value) => call("set-show-quick-switch", { value }),
110
- setIgnoreModelIdPrefix: (value) => call("set-ignore-model-id-prefix", { value })
154
+ setIgnoreModelIdPrefix: (value) => call("set-ignore-model-id-prefix", { value }),
155
+ setModelSort: (mode) => call("set-model-sort", { mode }),
156
+ setModelOrder: (order) => call("set-model-order", { order })
111
157
  };
112
158
  //#endregion
113
- //#region \0dsh-css:/Users/fonlan/Repos/dsh-model-router/src/client/settings-card.css.mjs
114
- 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";
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";
115
161
  const tagId$1 = "@fonlan/dsh-model-router/settings-card.css";
116
162
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
117
163
  const tag = document.createElement("style");
@@ -182,6 +228,8 @@ window.__ModuleLoader__.load({
182
228
  const [drag, setDrag] = (0, react.useState)(null);
183
229
  const [quickSwitchBusy, setQuickSwitchBusy] = (0, react.useState)(false);
184
230
  const [ignorePrefixBusy, setIgnorePrefixBusy] = (0, react.useState)(false);
231
+ const [sortBusy, setSortBusy] = (0, react.useState)(false);
232
+ const [modelDrag, setModelDrag] = (0, react.useState)(null);
185
233
  const load = (0, react.useCallback)(async () => {
186
234
  setLoading(true);
187
235
  try {
@@ -277,6 +325,48 @@ window.__ModuleLoader__.load({
277
325
  setIgnorePrefixBusy(false);
278
326
  }
279
327
  }, [state]);
328
+ const changeModelSort = (0, react.useCallback)(async (mode) => {
329
+ if (state === null || state.modelSort === mode) return;
330
+ const previous = state.modelSort;
331
+ setState((prev) => prev === null ? prev : {
332
+ ...prev,
333
+ modelSort: mode
334
+ });
335
+ setSortBusy(true);
336
+ try {
337
+ setState(await api.setModelSort(mode));
338
+ setError(null);
339
+ } catch (cause) {
340
+ setState((prev) => prev === null ? prev : {
341
+ ...prev,
342
+ modelSort: previous
343
+ });
344
+ setError(t("sortFailed", { message: cause instanceof Error ? cause.message : String(cause) }));
345
+ } finally {
346
+ setSortBusy(false);
347
+ }
348
+ }, [state, t]);
349
+ const moveModel = (0, react.useCallback)(async (from, to) => {
350
+ if (state === null || from === to) return;
351
+ const order = arrayMove(state.models.map((model) => model.id), from, to);
352
+ setState((prev) => prev === null ? prev : {
353
+ ...prev,
354
+ modelOrder: order
355
+ });
356
+ setSortBusy(true);
357
+ try {
358
+ setState(await api.setModelOrder(order));
359
+ setError(null);
360
+ } catch (cause) {
361
+ setState((prev) => prev === null ? prev : {
362
+ ...prev,
363
+ modelOrder: state.modelOrder
364
+ });
365
+ setError(t("sortFailed", { message: cause instanceof Error ? cause.message : String(cause) }));
366
+ } finally {
367
+ setSortBusy(false);
368
+ }
369
+ }, [state, t]);
280
370
  if (snapshot.status === "unavailable") return null;
281
371
  const writable = snapshot.writable;
282
372
  const models = (0, react.useMemo)(() => state?.models ?? [], [state]);
@@ -361,6 +451,105 @@ window.__ModuleLoader__.load({
361
451
  })]
362
452
  })]
363
453
  }),
454
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
455
+ className: "mr-sort",
456
+ children: [
457
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
458
+ className: "mr-sort-head",
459
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
460
+ className: "mr-sort-title",
461
+ children: t("sortTitle")
462
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
463
+ className: "mr-sort-desc",
464
+ children: t("sortDescription")
465
+ })]
466
+ }),
467
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
468
+ className: "mr-sort-modes",
469
+ role: "radiogroup",
470
+ "aria-label": t("sortTitle"),
471
+ children: [
472
+ "custom",
473
+ "name",
474
+ "recent"
475
+ ].map((mode) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
476
+ className: "mr-sort-mode",
477
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
478
+ type: "radio",
479
+ name: "mr-model-sort",
480
+ checked: state?.modelSort === mode,
481
+ disabled: loading || sortBusy || !writable,
482
+ onChange: () => void changeModelSort(mode)
483
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
484
+ className: "mr-sort-mode-copy",
485
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
486
+ className: "mr-sort-mode-label",
487
+ children: t(`sortMode${mode[0].toUpperCase()}${mode.slice(1)}`)
488
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
489
+ className: "mr-sort-mode-desc",
490
+ children: t(`sortModeDesc${mode[0].toUpperCase()}${mode.slice(1)}`)
491
+ })]
492
+ })]
493
+ }, mode))
494
+ }),
495
+ state?.modelSort === "custom" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
496
+ className: "mr-sort-custom",
497
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
498
+ className: "mr-sort-hint",
499
+ children: t("sortCustomHint")
500
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ol", {
501
+ className: "mr-sort-list",
502
+ children: models.map((model, index) => {
503
+ const dropTarget = modelDrag !== null && modelDrag.index !== index;
504
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
505
+ className: [
506
+ "mr-sort-item",
507
+ dropTarget ? "mr-sort-item-drop" : "",
508
+ sortBusy ? "mr-sort-item-busy" : ""
509
+ ].filter(Boolean).join(" "),
510
+ draggable: !sortBusy && writable,
511
+ onDragStart: (event) => {
512
+ setModelDrag({ index });
513
+ event.dataTransfer.effectAllowed = "move";
514
+ try {
515
+ event.dataTransfer.setData("text/plain", model.id);
516
+ } catch {}
517
+ },
518
+ onDragOver: (event) => {
519
+ if (modelDrag !== null && modelDrag.index !== index) {
520
+ event.preventDefault();
521
+ event.dataTransfer.dropEffect = "move";
522
+ }
523
+ },
524
+ onDrop: (event) => {
525
+ if (modelDrag !== null && modelDrag.index !== index) {
526
+ event.preventDefault();
527
+ moveModel(modelDrag.index, index);
528
+ }
529
+ setModelDrag(null);
530
+ },
531
+ onDragEnd: () => setModelDrag(null),
532
+ children: [
533
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
534
+ className: "mr-grip",
535
+ "aria-hidden": "true",
536
+ children: "⠿"
537
+ }),
538
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
539
+ className: "mr-sort-model-name",
540
+ children: model.name
541
+ }),
542
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
543
+ className: "mr-sort-model-id",
544
+ children: model.id
545
+ })
546
+ ]
547
+ }, model.id);
548
+ })
549
+ })]
550
+ })
551
+ ]
552
+ }),
364
553
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
365
554
  className: "mr-toolbar",
366
555
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -505,9 +694,9 @@ window.__ModuleLoader__.load({
505
694
  });
506
695
  }
507
696
  //#endregion
508
- //#region \0dsh-css:/Users/fonlan/Repos/dsh-model-router/src/client/route-switcher.css.mjs
509
- 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";
510
- const tagId = "@fonlan/dsh-model-router/route-switcher.css";
697
+ //#region \0dsh-css:/home/runner/work/dsh-model-router/dsh-model-router/src/client/model-seat.css.mjs
698
+ const css = "/*\n * Composer model seat (shadows the host ModelSelect). Styles mirror the host\n * ModelSelect.module.css exactly (same tokens, sizes and spacing) so the\n * replacement is visually indistinguishable, plus the provider pane's title\n * and hint rows.\n */\n\n.mr-seat-root {\n min-width: 0;\n position: relative;\n}\n\n.mr-seat-trigger {\n min-width: 0;\n max-width: 220px;\n height: 28px;\n color: var(--dsw-alias-label-secondary);\n cursor: pointer;\n background: transparent;\n border: none;\n border-radius: 24px;\n outline: none;\n align-items: center;\n gap: 4px;\n padding: 0 4px 0 8px;\n font-size: 13px;\n font-weight: 500;\n line-height: 20px;\n display: flex;\n}\n\n.mr-seat-trigger:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.mr-seat-trigger:focus-visible {\n box-shadow: 0 0 0 2px var(--dsw-alias-border-l3);\n}\n\n.mr-seat-trigger:disabled {\n color: var(--dsw-alias-label-dimmed);\n cursor: default;\n}\n\n.mr-seat-triggerLabel {\n text-overflow: ellipsis;\n white-space: nowrap;\n min-width: 0;\n overflow: hidden;\n}\n\n.mr-seat-triggerEffort {\n color: var(--dsw-alias-label-caption);\n flex: none;\n}\n\n.mr-seat-chevron {\n color: var(--dsw-alias-label-caption);\n flex: none;\n transition: transform 0.12s;\n}\n\n.mr-seat-chevronOpen {\n transform: rotate(180deg);\n}\n\n.mr-seat-menu {\n z-index: 20;\n border: 1px solid var(--dsw-alias-border-inverted);\n background: var(--dsw-specific-menu);\n width: min(240px, 100vw - 32px);\n max-height: min(360px, 100vh - 96px);\n box-shadow: var(--dsw-shadow-lv3);\n color: var(--dsw-alias-label-primary);\n --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);\n --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);\n border-radius: 12px;\n flex-direction: column;\n padding: 4px;\n display: flex;\n position: absolute;\n bottom: calc(100% + 8px);\n right: 0;\n overflow: hidden;\n}\n\n.mr-seat-status,\n.mr-seat-empty {\n color: var(--dsw-alias-label-tertiary);\n padding: 10px;\n font-size: 13px;\n line-height: 20px;\n}\n\n.mr-seat-error,\n.mr-seat-warning {\n background: var(--dsw-alias-interactive-bg-hover-danger);\n color: var(--dsw-alias-state-error-primary);\n border-radius: 8px;\n justify-content: space-between;\n align-items: flex-start;\n gap: 8px;\n margin-bottom: 4px;\n padding: 7px 8px;\n font-size: 12px;\n line-height: 18px;\n display: flex;\n}\n\n.mr-seat-warning {\n background: var(--dsw-alias-bg-module-platform);\n color: var(--dsw-alias-state-warn-label);\n}\n\n.mr-seat-retry {\n color: inherit;\n font: inherit;\n cursor: pointer;\n background: transparent;\n border: none;\n flex: none;\n padding: 0;\n font-weight: 600;\n}\n\n.mr-seat-groups {\n min-height: 0;\n overflow-y: auto;\n}\n\n.mr-seat-group + .mr-seat-group {\n margin-top: 4px;\n}\n\n.mr-seat-groupTitle {\n z-index: 1;\n background: var(--dsw-specific-menu);\n color: var(--dsw-alias-label-tertiary);\n padding: 5px 8px 3px;\n font-size: 12px;\n font-weight: 500;\n line-height: 18px;\n position: sticky;\n top: 0;\n}\n\n/* Provider pane: non-sticky title row above the provider list. */\n.mr-seat-title {\n color: var(--dsw-alias-label-tertiary);\n padding: 5px 8px 3px;\n font-size: 12px;\n font-weight: 500;\n line-height: 18px;\n}\n\n.mr-seat-option {\n width: 100%;\n min-height: 38px;\n color: inherit;\n text-align: left;\n cursor: pointer;\n background: transparent;\n border: none;\n border-radius: 10px;\n outline: none;\n align-items: center;\n gap: 8px;\n padding: 6px 8px;\n display: flex;\n}\n\n.mr-seat-option:hover:not(:disabled),\n.mr-seat-option:focus-visible {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.mr-seat-selected {\n background: transparent;\n}\n\n.mr-seat-option:disabled {\n color: var(--dsw-alias-label-dimmed);\n cursor: default;\n}\n\n.mr-seat-optionCopy {\n flex-direction: column;\n flex: 1;\n min-width: 0;\n display: flex;\n}\n\n.mr-seat-modelName {\n color: inherit;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 14px;\n font-weight: 500;\n line-height: 20px;\n overflow: hidden;\n}\n\n.mr-seat-description {\n color: var(--dsw-alias-label-tertiary);\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 12px;\n line-height: 18px;\n overflow: hidden;\n}\n\n.mr-seat-check {\n color: var(--dsw-alias-label-primary);\n flex: 0 0 18px;\n place-items: center;\n display: grid;\n}\n\n.mr-seat-cell {\n width: 100%;\n height: 40px;\n color: var(--dsw-alias-label-primary);\n cursor: pointer;\n text-align: left;\n background: transparent;\n border: none;\n border-radius: 10px;\n align-items: center;\n gap: 8px;\n padding: 0 10px;\n font-size: 14px;\n line-height: 22px;\n display: flex;\n}\n\n.mr-seat-cell:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.mr-seat-cellLabel {\n text-overflow: ellipsis;\n white-space: nowrap;\n flex: auto;\n min-width: 0;\n overflow: hidden;\n}\n\n.mr-seat-cellValue {\n text-overflow: ellipsis;\n white-space: nowrap;\n min-width: 0;\n color: var(--dsw-alias-label-tertiary);\n flex: 0 auto;\n overflow: hidden;\n}\n\n.mr-seat-cellChevron {\n color: var(--dsw-alias-label-tertiary);\n flex: none;\n}\n\n/* Global hint footer of the provider pane. */\n.mr-seat-hint {\n color: var(--dsw-alias-label-tertiary);\n padding: 5px 8px 3px;\n font-size: 12px;\n line-height: 18px;\n border-top: 1px solid var(--dsw-alias-border-l2);\n margin-top: 4px;\n}\n\n/*\n * Mobile portrait: the composer row is an inline-size container. With the\n * quick route switcher gone from the right tool row, let the model seat\n * absorb the remaining row width (same rule the old switcher enforced).\n * The breakpoint matches DSH's own narrow-composer rule (<=460px).\n */\n@container (width <= 460px) {\n [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.model\"] [aria-haspopup=\"menu\"] {\n width: 100%;\n max-width: 100%;\n }\n}\n";
699
+ const tagId = "@fonlan/dsh-model-router/model-seat.css";
511
700
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
512
701
  const tag = document.createElement("style");
513
702
  tag.dataset.plugin = "@fonlan/dsh-model-router";
@@ -516,28 +705,41 @@ window.__ModuleLoader__.load({
516
705
  document.head.appendChild(tag);
517
706
  }
518
707
  //#endregion
519
- //#region src/client/route-switcher.tsx
708
+ //#region src/client/model-seat.tsx
520
709
  const ROUTER_PROVIDER_ID = "model-router";
521
710
  /**
522
- * The quick route switcher lives beside DSH's model seat in the composer row.
523
- * It reads the same per-session model directory as the host model picker, but
524
- * only appears when the selected model is the virtual model-router route.
711
+ * The composer model seat, shadowing the host ModelSelect at a lower slot
712
+ * priority. It renders the same two-level Model / Effort menu as the host
713
+ * (model list grouped by provider, reasoning effort levels), plus a third
714
+ * Provider row that opens the model-router provider list — the former quick
715
+ * route-switcher button moved into the model picker, on par with the model
716
+ * and effort menus.
717
+ *
718
+ * Shadowing contract: the host ui-model-selection registers the
719
+ * `conversation.input.model` occupant at priority 0; this entry registers at
720
+ * priority -1, so the renderer projects this component as the single-slot
721
+ * winner while the host entry stays as the abdication fallback. Both entries
722
+ * share the same per-session ModelDirectory, so selections stay consistent
723
+ * with the /model popup and the host picker.
525
724
  */
526
- function RouteSwitcher({ session, available, directory, load, t }) {
725
+ function ModelRouterSeat({ locked, available, directory, load, select, t }) {
527
726
  const directoryState = (0, react.useSyncExternalStore)((onChange) => directory.subscribe(onChange), () => directory.getSnapshot(), () => directory.getSnapshot());
528
727
  const current = directoryState.current;
728
+ const [open, setOpen] = (0, react.useState)(false);
729
+ const [pane, setPane] = (0, react.useState)("root");
730
+ const lastActionRef = (0, react.useRef)("load");
731
+ const [toast, setToast] = (0, react.useState)(null);
732
+ const toastSeq = (0, react.useRef)(0);
733
+ const rootRef = (0, react.useRef)(null);
734
+ const triggerRef = (0, react.useRef)(null);
735
+ const itemRefs = (0, react.useRef)([]);
736
+ const id = (0, react.useId)();
529
737
  const [routerState, setRouterState] = (0, react.useState)(null);
530
738
  const [routerLoading, setRouterLoading] = (0, react.useState)(false);
531
739
  const [routerError, setRouterError] = (0, react.useState)(null);
532
- const [open, setOpen] = (0, react.useState)(false);
533
740
  const [busyProvider, setBusyProvider] = (0, react.useState)(null);
534
741
  const isRouterModel = available && current?.provider === ROUTER_PROVIDER_ID;
535
742
  const modelId = isRouterModel ? current.model : null;
536
- /**
537
- * Fetch the router state. `silent` skips the busy state: background
538
- * refreshes (directory-ready, model switch) should never gray out the
539
- * button — only the initial load and explicit user actions do.
540
- */
541
743
  const loadRouterState = (0, react.useCallback)(async (silent = false) => {
542
744
  if (!silent) setRouterLoading(true);
543
745
  try {
@@ -556,9 +758,9 @@ window.__ModuleLoader__.load({
556
758
  (0, react.useEffect)(() => {
557
759
  if (!available) return;
558
760
  loadRouterState();
559
- }, [available]);
761
+ }, [available, loadRouterState]);
560
762
  (0, react.useEffect)(() => {
561
- setOpen(false);
763
+ setPane("root");
562
764
  setBusyProvider(null);
563
765
  setRouterError(null);
564
766
  }, [modelId]);
@@ -611,107 +813,429 @@ window.__ModuleLoader__.load({
611
813
  routeModel,
612
814
  t
613
815
  ]);
614
- if (session.removed || !isRouterModel) return null;
615
- if (routerState?.showQuickSwitch === false) return null;
616
- const activeName = providers.find((provider) => provider.id === routeModel?.active)?.name ?? routeModel?.active ?? t("routeLoading");
617
- if (routerState !== null && routerState.providers.length < 2) return null;
618
- if (routeModel !== void 0 && providers.length < 2) return null;
619
- const menuItems = routeModel === void 0 ? [{
620
- type: "label",
621
- id: "route-status",
622
- text: routerError ?? t("routeLoading")
623
- }] : [
624
- {
625
- type: "label",
626
- id: "route-model",
627
- text: t("routeMenuTitle", { model: modelId })
628
- },
629
- ...providers.map((provider) => ({
630
- id: provider.id,
631
- label: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
632
- className: "mr-route-item-copy",
633
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
634
- className: "mr-route-item-name",
635
- children: provider.name
636
- }), !provider.credentialConfigured && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
637
- className: "mr-route-item-note",
638
- children: t("noCredentialShort")
639
- })]
816
+ const choices = (0, react.useMemo)(() => directoryState.groups.flatMap((group) => group.models.map((model) => ({
817
+ group,
818
+ model,
819
+ selection: {
820
+ provider: group.id,
821
+ model: model.id,
822
+ ...model.reasoning?.defaultEffort === void 0 ? {} : { reasoningEffort: model.reasoning.defaultEffort }
823
+ }
824
+ }))), [directoryState.groups]);
825
+ const currentChoice = choices[current === null ? -1 : choices.findIndex((c) => c.selection.provider === current?.provider && c.selection.model === current.model)];
826
+ const reasoning = currentChoice?.model.reasoning;
827
+ const effectiveEffort = current?.reasoningEffort ?? reasoning?.defaultEffort;
828
+ const effortLabel = reasoning === void 0 ? void 0 : effectiveEffort === void 0 ? t("effortProviderDefault") : reasoning.efforts.find((level) => level.id === effectiveEffort)?.name ?? effectiveEffort;
829
+ const effortChoices = (0, react.useMemo)(() => reasoning === void 0 ? [] : [...reasoning.defaultEffort === void 0 ? [{
830
+ key: "provider-default",
831
+ effort: void 0,
832
+ label: t("effortProviderDefault")
833
+ }] : [], ...reasoning.efforts.map((effort) => ({
834
+ key: `effort:${effort.id}`,
835
+ effort: effort.id,
836
+ label: effort.name,
837
+ ...effort.description === void 0 ? {} : { description: effort.description }
838
+ }))], [reasoning, t]);
839
+ const busy = directoryState.status === "selecting";
840
+ const reload = () => {
841
+ lastActionRef.current = "load";
842
+ load();
843
+ };
844
+ (0, react.useEffect)(() => {
845
+ if (!open) return;
846
+ const closeOutside = (event) => {
847
+ if (!rootRef.current?.contains(event.target)) setOpen(false);
848
+ };
849
+ document.addEventListener("mousedown", closeOutside);
850
+ return () => {
851
+ document.removeEventListener("mousedown", closeOutside);
852
+ };
853
+ }, [open]);
854
+ if (!available) return null;
855
+ const show = () => {
856
+ setPane("root");
857
+ setOpen(true);
858
+ reload();
859
+ loadRouterState();
860
+ };
861
+ const close = (restoreFocus = false) => {
862
+ setOpen(false);
863
+ setPane("root");
864
+ if (restoreFocus) queueMicrotask(() => {
865
+ triggerRef.current?.focus();
866
+ });
867
+ };
868
+ const moveFocus = (offset) => {
869
+ const items = itemRefs.current.filter((item) => item !== null);
870
+ if (items.length === 0) return;
871
+ const active = items.findIndex((item) => item === document.activeElement);
872
+ items[(Math.max(active, 0) + offset + items.length) % items.length]?.focus();
873
+ };
874
+ const onRootKeyDown = (event) => {
875
+ if (event.key === "Escape" && open) {
876
+ event.preventDefault();
877
+ if (pane !== "root") setPane("root");
878
+ else close(true);
879
+ return;
880
+ }
881
+ if (!open) return;
882
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
883
+ event.preventDefault();
884
+ moveFocus(event.key === "ArrowDown" ? 1 : -1);
885
+ }
886
+ };
887
+ const onBlur = (event) => {
888
+ if (event.relatedTarget instanceof Node && rootRef.current?.contains(event.relatedTarget)) return;
889
+ close();
890
+ };
891
+ const settleSelection = (accepted) => {
892
+ if (accepted) {
893
+ if (rootRef.current !== null) close(true);
894
+ return;
895
+ }
896
+ const message = directory.getSnapshot().error;
897
+ if (message !== null) {
898
+ toastSeq.current += 1;
899
+ setToast({
900
+ seq: toastSeq.current,
901
+ text: t("errorAction", { message })
902
+ });
903
+ }
904
+ };
905
+ const choose = (selection) => {
906
+ if (current !== null && current.provider === selection.provider && current.model === selection.model) {
907
+ close(true);
908
+ return;
909
+ }
910
+ lastActionRef.current = "select";
911
+ select(selection).then(settleSelection);
912
+ };
913
+ const chooseEffort = (effort) => {
914
+ if (current === null) return;
915
+ if (effectiveEffort === effort) {
916
+ close(true);
917
+ return;
918
+ }
919
+ const selection = {
920
+ provider: current.provider,
921
+ model: current.model,
922
+ ...effort === void 0 ? {} : { reasoningEffort: effort }
923
+ };
924
+ lastActionRef.current = "select";
925
+ select(selection).then(settleSelection);
926
+ };
927
+ const modelLabel = currentChoice?.model.name ?? t("triggerFallback");
928
+ const triggerLabel = effortLabel === void 0 ? modelLabel : `${modelLabel} · ${effortLabel}`;
929
+ const triggerAria = currentChoice === void 0 ? t("triggerSelectAria") : effortLabel === void 0 ? t("triggerAria", { model: modelLabel }) : t("triggerAriaEffort", {
930
+ model: modelLabel,
931
+ effort: effortLabel
932
+ });
933
+ const activeProviderName = providers.find((provider) => provider.id === routeModel?.active)?.name ?? routeModel?.active ?? t("routeLoading");
934
+ const providerRowVisible = isRouterModel && routerState !== null && routerState.showQuickSwitch !== false && routerState.providers.length >= 2 && routeModel !== void 0 && providers.length >= 2;
935
+ itemRefs.current = [];
936
+ let itemIndex = 0;
937
+ const itemRef = () => {
938
+ const at = itemIndex++;
939
+ return (node) => {
940
+ itemRefs.current[at] = node;
941
+ };
942
+ };
943
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
944
+ ref: rootRef,
945
+ className: "mr-seat-root",
946
+ onKeyDown: onRootKeyDown,
947
+ onBlur,
948
+ children: [
949
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
950
+ ref: triggerRef,
951
+ type: "button",
952
+ className: "mr-seat-trigger",
953
+ "aria-label": triggerAria,
954
+ "aria-haspopup": "menu",
955
+ "aria-expanded": open,
956
+ "aria-controls": open ? `${id}-menu` : void 0,
957
+ title: triggerLabel,
958
+ disabled: locked,
959
+ onClick: () => {
960
+ if (open) close();
961
+ else show();
962
+ },
963
+ children: [
964
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
965
+ className: "mr-seat-triggerLabel",
966
+ children: modelLabel
967
+ }),
968
+ effortLabel !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
969
+ className: "mr-seat-triggerEffort",
970
+ children: effortLabel
971
+ }),
972
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
973
+ className: `mr-seat-chevron${open ? " mr-seat-chevronOpen" : ""}`,
974
+ "aria-hidden": true,
975
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, {})
976
+ })
977
+ ]
640
978
  }),
641
- disabled: !provider.credentialConfigured || busyProvider !== null
642
- })),
643
- ...routerError === null ? [] : [{
644
- id: "route-error",
645
- label: routerError,
646
- disabled: true,
647
- danger: true
648
- }]
649
- ];
650
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
651
- className: "mr-route-anchor",
652
- open,
653
- side: "top",
654
- align: "end",
655
- portal: true,
656
- compact: true,
657
- dense: true,
658
- items: menuItems,
659
- selectedId: routeModel?.active ?? void 0,
660
- onSelect: (id) => {
661
- if (id === "route-error" || routeModel === void 0) {
662
- loadRouterState();
663
- load();
664
- return;
665
- }
666
- switchProvider(id);
667
- },
668
- onClose: () => setOpen(false),
669
- footer: [{
670
- type: "label",
671
- id: "route-global-hint",
672
- text: t("globalRouteHint")
673
- }],
674
- anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
675
- type: "button",
676
- className: "mr-route-trigger",
677
- "aria-label": t("routeButtonAria", { provider: activeName }),
678
- "aria-haspopup": "menu",
679
- "aria-expanded": open,
680
- disabled: routerLoading || busyProvider !== null,
681
- onClick: () => {
682
- setOpen((value) => !value);
683
- if (!open) {
684
- loadRouterState();
685
- load();
686
- }
687
- },
688
- title: t("routeButtonTitle"),
689
- children: [
690
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconGlobeOutline14, {}),
691
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
692
- className: "mr-route-trigger-label",
693
- children: activeName
694
- }),
695
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
696
- className: "mr-route-trigger-chevron",
697
- "aria-hidden": true,
698
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, {})
699
- })
700
- ]
701
- })
979
+ open && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
980
+ id: `${id}-menu`,
981
+ className: "mr-seat-menu",
982
+ role: "menu",
983
+ "aria-label": t("menuAria"),
984
+ "aria-busy": directoryState.status === "loading" || busy,
985
+ children: [
986
+ pane === "root" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
987
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
988
+ ref: itemRef(),
989
+ type: "button",
990
+ role: "menuitem",
991
+ className: "mr-seat-cell",
992
+ onClick: () => setPane("model"),
993
+ children: [
994
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
995
+ className: "mr-seat-cellLabel",
996
+ children: t("menuModel")
997
+ }),
998
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
999
+ className: "mr-seat-cellValue",
1000
+ children: modelLabel
1001
+ }),
1002
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1003
+ className: "mr-seat-cellChevron",
1004
+ "aria-hidden": true,
1005
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutline14, {})
1006
+ })
1007
+ ]
1008
+ }),
1009
+ providerRowVisible && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1010
+ ref: itemRef(),
1011
+ type: "button",
1012
+ role: "menuitem",
1013
+ className: "mr-seat-cell",
1014
+ onClick: () => setPane("provider"),
1015
+ children: [
1016
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1017
+ className: "mr-seat-cellLabel",
1018
+ children: t("menuProvider")
1019
+ }),
1020
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1021
+ className: "mr-seat-cellValue",
1022
+ children: activeProviderName
1023
+ }),
1024
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1025
+ className: "mr-seat-cellChevron",
1026
+ "aria-hidden": true,
1027
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutline14, {})
1028
+ })
1029
+ ]
1030
+ }),
1031
+ reasoning !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1032
+ ref: itemRef(),
1033
+ type: "button",
1034
+ role: "menuitem",
1035
+ className: "mr-seat-cell",
1036
+ onClick: () => setPane("effort"),
1037
+ children: [
1038
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1039
+ className: "mr-seat-cellLabel",
1040
+ children: t("menuEffort")
1041
+ }),
1042
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1043
+ className: "mr-seat-cellValue",
1044
+ children: effortLabel
1045
+ }),
1046
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1047
+ className: "mr-seat-cellChevron",
1048
+ "aria-hidden": true,
1049
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutline14, {})
1050
+ })
1051
+ ]
1052
+ })
1053
+ ] }),
1054
+ pane === "model" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1055
+ directoryState.status === "loading" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1056
+ className: "mr-seat-status",
1057
+ children: t("statusLoading")
1058
+ }),
1059
+ directoryState.error !== null && lastActionRef.current === "load" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1060
+ className: "mr-seat-error",
1061
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("errorAction", { message: directoryState.error }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1062
+ type: "button",
1063
+ className: "mr-seat-retry",
1064
+ onClick: reload,
1065
+ children: t("retry")
1066
+ })]
1067
+ }),
1068
+ directoryState.failures.map((failure) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1069
+ className: "mr-seat-warning",
1070
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("warningGroupLoad", {
1071
+ name: failure.name,
1072
+ message: failure.message
1073
+ }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1074
+ type: "button",
1075
+ className: "mr-seat-retry",
1076
+ onClick: reload,
1077
+ children: t("retry")
1078
+ })]
1079
+ }, failure.id)),
1080
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1081
+ className: "mr-seat-groups",
1082
+ children: directoryState.groups.map((group) => {
1083
+ const headingId = `${id}-${group.id}`;
1084
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1085
+ role: "group",
1086
+ "aria-labelledby": headingId,
1087
+ className: "mr-seat-group",
1088
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1089
+ className: "mr-seat-groupTitle",
1090
+ id: headingId,
1091
+ children: group.name
1092
+ }), group.models.map((model) => {
1093
+ const selected = current?.provider === group.id && current.model === model.id;
1094
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1095
+ ref: itemRef(),
1096
+ type: "button",
1097
+ role: "menuitemradio",
1098
+ "aria-checked": selected,
1099
+ className: `mr-seat-option${selected ? " mr-seat-selected" : ""}`,
1100
+ title: model.name,
1101
+ disabled: busy,
1102
+ onClick: () => choose({
1103
+ provider: group.id,
1104
+ model: model.id
1105
+ }),
1106
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1107
+ className: "mr-seat-optionCopy",
1108
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1109
+ className: "mr-seat-modelName",
1110
+ children: model.name
1111
+ }), model.description !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1112
+ className: "mr-seat-description",
1113
+ children: model.description
1114
+ })]
1115
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1116
+ className: "mr-seat-check",
1117
+ "aria-hidden": true,
1118
+ children: selected ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutline16, {}) : null
1119
+ })]
1120
+ }, model.id);
1121
+ })]
1122
+ }, group.id);
1123
+ })
1124
+ }),
1125
+ directoryState.status === "ready" && choices.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1126
+ className: "mr-seat-empty",
1127
+ children: t("emptyModels")
1128
+ })
1129
+ ] }),
1130
+ pane === "provider" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [routerError !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1131
+ className: "mr-seat-error",
1132
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: routerError }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1133
+ type: "button",
1134
+ className: "mr-seat-retry",
1135
+ onClick: () => {
1136
+ loadRouterState();
1137
+ load();
1138
+ },
1139
+ children: t("retry")
1140
+ })]
1141
+ }), routeModel === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1142
+ className: "mr-seat-status",
1143
+ children: t("routeLoading")
1144
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1145
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1146
+ className: "mr-seat-title",
1147
+ children: t("routeMenuTitle", { model: modelId ?? "" })
1148
+ }),
1149
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1150
+ className: "mr-seat-groups",
1151
+ children: providers.map((provider) => {
1152
+ const selected = routeModel.active === provider.id;
1153
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1154
+ ref: itemRef(),
1155
+ type: "button",
1156
+ role: "menuitemradio",
1157
+ "aria-checked": selected,
1158
+ className: `mr-seat-option${selected ? " mr-seat-selected" : ""}`,
1159
+ disabled: !provider.credentialConfigured || busyProvider !== null,
1160
+ onClick: () => void switchProvider(provider.id),
1161
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1162
+ className: "mr-seat-optionCopy",
1163
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1164
+ className: "mr-seat-modelName",
1165
+ children: provider.name
1166
+ }), !provider.credentialConfigured && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1167
+ className: "mr-seat-description",
1168
+ children: t("noCredentialShort")
1169
+ })]
1170
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1171
+ className: "mr-seat-check",
1172
+ "aria-hidden": true,
1173
+ children: selected ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutline16, {}) : null
1174
+ })]
1175
+ }, provider.id);
1176
+ })
1177
+ }),
1178
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1179
+ className: "mr-seat-hint",
1180
+ children: t("globalRouteHint")
1181
+ })
1182
+ ] })] }),
1183
+ pane === "effort" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [directoryState.error !== null && lastActionRef.current === "load" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1184
+ className: "mr-seat-error",
1185
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("errorAction", { message: directoryState.error }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1186
+ type: "button",
1187
+ className: "mr-seat-retry",
1188
+ onClick: reload,
1189
+ children: t("actionReload")
1190
+ })]
1191
+ }), effortChoices.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1192
+ className: "mr-seat-empty",
1193
+ children: t("emptyEfforts")
1194
+ }) : effortChoices.map((level) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1195
+ ref: itemRef(),
1196
+ type: "button",
1197
+ role: "menuitemradio",
1198
+ "aria-checked": effectiveEffort === level.effort,
1199
+ className: `mr-seat-option${effectiveEffort === level.effort ? " mr-seat-selected" : ""}`,
1200
+ disabled: busy,
1201
+ onClick: () => chooseEffort(level.effort),
1202
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1203
+ className: "mr-seat-optionCopy",
1204
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1205
+ className: "mr-seat-modelName",
1206
+ children: level.label
1207
+ }), level.description !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1208
+ className: "mr-seat-description",
1209
+ children: level.description
1210
+ })]
1211
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1212
+ className: "mr-seat-check",
1213
+ "aria-hidden": true,
1214
+ children: effectiveEffort === level.effort ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutline16, {}) : null
1215
+ })]
1216
+ }, level.key))] })
1217
+ ]
1218
+ }),
1219
+ toast !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Toast, {
1220
+ text: toast.text,
1221
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconWarningOutline16, {}),
1222
+ anchor: rootRef.current?.closest("[data-composer-card]") ?? null,
1223
+ onDone: () => setToast(null)
1224
+ }, toast.seq)
1225
+ ]
702
1226
  });
703
1227
  }
704
- /** Register the quick route switcher into the composer right-side tool row. */
705
- function registerRouteSwitcher(ctx) {
1228
+ /** Register the model seat onto the composer's named model slot (priority -1 shadows the host seat). */
1229
+ function registerModelRouterSeat(ctx) {
706
1230
  const t = ctx.locale.bind(LOCALE_NS);
707
1231
  ctx.inject(["slots", "modelDirectories"], (scope) => {
708
1232
  const models = scope.modelDirectories;
709
1233
  const sessions = scope.sessions;
710
- scope.slots.inject("conversation.input.right", () => scope.slots.register({
711
- name: "conversation.input.right",
712
- id: "model-router-route-switcher",
713
- order: 100,
714
- label: () => t("routeButtonLabel"),
1234
+ scope.slots.inject("conversation.input.model", () => scope.slots.register({
1235
+ name: "conversation.input.model",
1236
+ id: "model-router-model-seat",
1237
+ priority: -1,
1238
+ label: () => t("seatLabel"),
715
1239
  locale: LOCALE_NS,
716
1240
  inject: (sessionId) => {
717
1241
  const directory = models.directoryFor(sessionId);
@@ -721,10 +1245,11 @@ window.__ModuleLoader__.load({
721
1245
  directory: directory.store,
722
1246
  load: () => {
723
1247
  if (available) directory.load().catch(() => {});
724
- }
1248
+ },
1249
+ select: (selection) => available ? directory.select(selection).then(() => true, () => false) : Promise.resolve(false)
725
1250
  };
726
1251
  }
727
- }, RouteSwitcher));
1252
+ }, ModelRouterSeat));
728
1253
  });
729
1254
  }
730
1255
  //#endregion
@@ -758,7 +1283,7 @@ window.__ModuleLoader__.load({
758
1283
  key: ROUTER_NS,
759
1284
  inject: () => ({ scope })
760
1285
  }, SettingsCard));
761
- registerRouteSwitcher(ctx);
1286
+ registerModelRouterSeat(ctx);
762
1287
  }
763
1288
  //#endregion
764
1289
  exports.apply = apply;