@deepseek-ai/dsh-client-ui-model-selection 0.1.7-alpha.2 → 0.1.7-rc.2

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.i18n.yaml CHANGED
@@ -1,6 +1,46 @@
1
- # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
2
- # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
- # after editing either side, bring the other along and re-record with:
1
+ # Bilingual-pair consistency record for README.md (docs/i18n/README.md): per heading
2
+ # section, a hash of its English and Chinese blocks outside code blocks and generated regions.
3
+ # After editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/client/ui-model-selection/README.md
5
- README.md: 37253cf04242275f22d646da0e65cfd44f4fefbf
6
- README.zh.md: 4e339e6a6c6db8c3125e6ccc19c0fc3af9947f88
5
+ /:
6
+ en: f39813648aeef5a0
7
+ zh: eea25094e8bfc031
8
+ /deepseek-ai-dsh-client-ui-model-selection:
9
+ en: 5e007d579a0e22f0
10
+ zh: edfff0d021712077
11
+ /deepseek-ai-dsh-client-ui-model-selection/summary:
12
+ en: 2fb361ef704528fb
13
+ zh: 2537d2afb4d2cb45
14
+ /deepseek-ai-dsh-client-ui-model-selection/table-of-contents:
15
+ en: d152484eb41ac6b4
16
+ zh: 09388d293f9be9cb
17
+ /deepseek-ai-dsh-client-ui-model-selection/use-this-package:
18
+ en: e5a1df3a4dfe5041
19
+ zh: 6269e73026b32a33
20
+ /deepseek-ai-dsh-client-ui-model-selection/use-this-package/model-and-effort:
21
+ en: e38d0195677a7b69
22
+ zh: 4eea8a9b1664afd9
23
+ /deepseek-ai-dsh-client-ui-model-selection/use-this-package/unroutable-sessions:
24
+ en: 7f20428c747ff052
25
+ zh: 975e6de728e02fa0
26
+ /deepseek-ai-dsh-client-ui-model-selection/use-this-package/selection-failures:
27
+ en: 261bbf42296243af
28
+ zh: 72779fb7b8fbc473
29
+ /deepseek-ai-dsh-client-ui-model-selection/understand-the-implementation:
30
+ en: 493088e8de6be91c
31
+ zh: 9ecee4605b4dbad4
32
+ /deepseek-ai-dsh-client-ui-model-selection/further-exploration:
33
+ en: 14acd4692e923480
34
+ zh: ff89aed852aa0da8
35
+ /deepseek-ai-dsh-client-ui-model-selection/model-experience:
36
+ en: 782f79ed910f426a
37
+ zh: 54a2e3e32a747544
38
+ /deepseek-ai-dsh-client-ui-model-selection/model-experience/kv-cache-effect:
39
+ en: 68e54c00412a40d1
40
+ zh: 0ac96f57fd194d96
41
+ /deepseek-ai-dsh-client-ui-model-selection/known-limitations-and-deferred-work:
42
+ en: 75b37954ceacd92a
43
+ zh: a49d0361486e35c4
44
+ /deepseek-ai-dsh-client-ui-model-selection/known-limitations-and-deferred-work/dev-note:
45
+ en: 83bfc3dae3365b49
46
+ zh: f7dc4d0654ec83e1
package/README.md CHANGED
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
9
9
 
10
10
  ## Summary
11
11
 
12
- The Web GUI lets users switch the model and reasoning effort for an existing session through either the `/model` popup or the composer's model control. Both surfaces present the same provider-grouped choices, and the selected model determines the available effort names and default. A complete selection applies to the next request; a running step keeps the model and effort it started with. If no adapter can serve the session's route, the composer remains disabled until routing becomes available.
12
+ The Web GUI lets users switch the model and reasoning effort for an existing session through either the `/model` popup or the composer's model control. Both surfaces present the same provider-grouped choices, and the selected model determines the available effort names and default. A complete selection applies to the next request; a running step keeps the model and effort it started with. If the selected model is unavailable, the composer stays disabled until the user selects an available model or that exact model becomes available again.
13
13
 
14
14
  ## Table of Contents
15
15
 
@@ -25,19 +25,23 @@ The Web GUI lets users switch the model and reasoning effort for an existing ses
25
25
  <a id="use-this-package"></a>
26
26
  ## Use this package
27
27
 
28
- Mount this plugin alongside `ui-conversation` and the commands package; the composer then shows the model seat next to the pending indicator, and `/model` opens the same directory as a popup. While the seat's menu is open, `↑`/`↓` move focus across the rows of the shown pane, Tab settles the focused row, and Escape and `Shift+Tab` leave a drilled pane first and otherwise close back to the trigger. Drilling lands on the row of the value in use, and going back lands on the cell that opened the pane left. Both surfaces show the host-reported current selection when the exact provider/model pair remains in the advertised groups; a missing catalog row leaves the routable selection intact while the trigger prompts `Select model`.
28
+ DeepSeek account and API-key routes appear as separate provider groups, each exposing the same configured model catalog.
29
+
30
+ The unselected model label uses the same regular weight as an available model name and retains the saved reasoning effort caption for existing and new sessions; effort editing requires an available model. Clicking the unselected trigger opens the model list directly; Escape closes it.
31
+
32
+ Mount this plugin alongside `ui-conversation` and the commands package; the composer then shows the model seat next to the pending indicator, and `/model` opens the same directory as a popup. While the seat's menu is open, `↑`/`↓` move focus across the rows of the shown pane, Tab settles the focused row, and Escape and `Shift+Tab` leave a drilled pane first and otherwise close back to the trigger. Drilling lands on the row of the value in use, and going back lands on the cell that opened the pane left. The composer shows the catalog name while the selected model is available, and its saved `provider/model` ID when the model or provider is removed, including account sign-out. The stored provider, model, and reasoning effort remain unchanged.
33
+
34
+ Mouse selection uses native browser clicks, including their cancellation behavior; a press alone never selects. Opening the menu focuses its trigger, and clicking the trigger again closes the menu and returns focus there. While a selection from either entry is pending, focus stays on the trigger, the trigger shows a spinner in place of its chevron, and each row whose value the selection carries shows one in place of its check; a rejected selection leaves the menu open, and Tab returns to the current row.
29
35
 
30
36
  ### Model and effort
31
37
 
32
- Models stay grouped by provider. The composer menu shows model and effort names only. The `/model` popup shows provider names and catalog descriptions; it localizes the two built-in DeepSeek descriptions and leaves external provider descriptions verbatim. The popup applies the selected model's default effort; the composer can then choose any advertised effort. An adapter without reasoning metadata leaves the Effort row absent; there is no arbitrary effort input.
38
+ Models stay grouped by provider. The composer menu shows model and effort names only, with DeepSeek Account first and DeepSeek second; third-party providers retain their catalog order. Navigation chevrons use `--dsw-alias-menu-icon`. The `/model` popup shows provider names and catalog descriptions; it localizes the two built-in DeepSeek descriptions and leaves external provider descriptions verbatim. The popup applies the selected model's default effort; the composer can then choose any advertised effort. An adapter without reasoning metadata leaves the Effort row absent; there is no arbitrary effort input.
33
39
 
34
40
  The composer replaces the model and effort text with the Models icon when the expanded controls cannot share one line, and restores the text when space permits. The full selection remains available in the trigger's accessible name, tooltip, and menu.
35
41
 
36
42
  ### Unroutable sessions
37
43
 
38
- When the Host reports that no adapter serves the session's route, this plugin raises a composer block and the input goes inert with its own copy; recovering clears it without a reload. A `null` before the first load or after one failed never blocks, and catalog membership never blocks either — a route serving a model it does not advertise is missing from the groups yet usable.
39
-
40
- Only the current Client binding's directory can publish its composer block. Cleanup from an older binding preserves a replacement directory's block; without a replacement directory, cleanup removes the obsolete block.
44
+ Catalog availability does not block sending with a saved selection; request execution reports missing credentials or unavailable models. Refreshes and refresh failures retain the last displayed selection and groups. A Host reset clears that display. Sign-out hides the account provider from the picker while preserving the saved provider/model ID and reasoning effort. Signing in restores the catalog name when that model is available again. Existing session logs remain unchanged.
41
45
 
42
46
  ### Selection failures
43
47
 
@@ -48,10 +52,12 @@ When another writer owns the Session, model-selection failures tell the user to
48
52
  <a id="understand-the-implementation"></a>
49
53
  ## Understand the implementation
50
54
 
55
+ Menus use the shared `MenuSurface` material, including the macOS backing for background blur; custom content follows the [menu rules](../../../docs/web-styling.md#component-rules).
56
+
51
57
  <details>
52
58
  <summary>Implementation internals — click to expand</summary>
53
59
 
54
- Two entries over ONE per-session directory owned by `ModelDirectoryResolver` (`ctx.modelDirectories`): the `/model` popupSelect contribution (registered through `ctx.commandUi`) and the composer's named `conversation.input.model` seat both load the session's advisory directory through `session.models` and submit through `session.selectModel` via the same `ModelDirectory` instance, so a switch made in either entry is what the other shows next. Directory loads and selections share a generation counter so an older response never overwrites a newer one; a connection reset drops every resident projection and repulls the Host-restored selection before display. Directories are per-session, resolved lazily, and disposed with the session scope; addressed subagent sessions expose neither entry. Every resident directory refetches directly on forwarded `llm/adapters-updated` and `settings/document-updated` owner events.
60
+ Two entries over ONE per-session directory owned by `ModelDirectoryResolver` (`ctx.modelDirectories`): the `/model` popupSelect contribution (registered through `ctx.commandUi`) and the composer's named `conversation.input.model` seat both load the session's available directory through `session.models` and submit through `session.selectModel` via the same `ModelDirectory` instance, so a switch made in either entry is what the other shows next. Directory loads and selections share a generation counter so an older response never overwrites a newer one. The directory publishes the latest submitted selection as `pending` until it settles or a connection reset invalidates it; a connection reset drops every resident projection and repulls the Host-restored selection before display. Directories are per-session, resolved lazily, and disposed with the session scope; addressed subagent sessions expose neither entry. Every resident directory refetches directly on forwarded `llm/adapters-updated`, `settings/document-updated`, and credential update events.
55
61
 
56
62
  </details>
57
63
 
@@ -63,7 +69,7 @@ Two entries over ONE per-session directory owned by `ModelDirectoryResolver` (`c
63
69
  Read these pages when the model surface is not enough. They move from the browser surfaces to the command popup shell and the selection contract.
64
70
 
65
71
  - [ui-commands](../ui-commands/README.md) — the popupSelect shell the `/model` contribution registers into.
66
- - [ui-conversation](../ui-conversation/README.md) — declares the composer's `conversation.input.model` seat and the composer block.
72
+ - [ui-conversation](../ui-conversation/README.md) — declares the composer's `conversation.input.model` seat.
67
73
  - [dsh-agent-default-model](../../core/agent-default-model/README.md) — the default-model service for sessions that never choose.
68
74
  - [Client package map](../README.md) — adjacent browser UI packages.
69
75
 
@@ -82,7 +88,6 @@ Switching the route can reduce or invalidate provider-side cache reuse for subse
82
88
 
83
89
  <a id="known-limitations-and-deferred-work"></a>
84
90
 
85
-
86
91
  These limits define the current model surface. They are current package constraints, not a general model-router comparison or a task backlog.
87
92
 
88
93
  - **No create-time or addressed-subagent selection** — both entries require an existing ordinary session's Agent; there is no draft-phase model choice to fold into session creation, and subagent continuation deliberately exposes no independent model-selection contract.
package/README.zh.md CHANGED
@@ -9,7 +9,7 @@ kind: "package-reference"
9
9
 
10
10
  ## 概述
11
11
 
12
- Web GUI 允许用户通过 `/model` 弹窗或 composer 模型控件切换既有会话使用的模型与推理(reasoning)强度。两个界面呈现同一组按提供方分组的选择;所选模型决定可用的推理强度名称与默认值。完整选择从下一次请求开始生效;运行中的步骤保留其启动时的模型与推理强度。如果没有适配器可以服务会话路由,composer 会保持停用,直至路由恢复可用。
12
+ Web GUI 允许用户通过 `/model` 弹窗或 composer 模型控件切换既有会话使用的模型与推理(reasoning)强度。两个界面呈现同一组按提供方分组的选择;所选模型决定可用的推理强度名称与默认值。完整选择从下一次请求开始生效;运行中的步骤保留其启动时的模型与推理强度。所选模型不可用时,composer 保持停用,直到用户选择可用模型或同一模型恢复可用。
13
13
 
14
14
  ## 目录
15
15
 
@@ -25,19 +25,23 @@ Web GUI 允许用户通过 `/model` 弹窗或 composer 模型控件切换既有
25
25
  <a id="use-this-package"></a>
26
26
  ## 使用本包
27
27
 
28
- 与 `ui-conversation` 及命令包一起挂载本插件;composer 随即在待处理指示器旁显示模型位,`/model` 则以弹窗打开同一份目录。模型位菜单打开期间,`↑`/`↓` 在所显示面板的行间移动焦点,`Tab` 选定聚焦行,Escape 与 `Shift+Tab` 先退出已下钻的面板,否则关闭并回到触发器。下钻落在正在使用的那一行,返回则落在打开该面板的格子上。当确切提供方/模型对仍在已公布分组中时,两个界面都显示 Host 报告的当前选择;目录行缺席时,可路由的选择保持不变,触发器提示 `Select model`。
28
+ DeepSeek 账号和 API Key 路由显示为独立提供方分组,各自展示相同的已配置模型目录。
29
+
30
+ 未选模型文案与可用模型名使用相同的常规字重,并在既有会话与新会话中保留已保存的推理强度显示;调整推理强度需要先选择可用模型。点击未选模型控件直接打开模型列表,Escape 关闭列表。
31
+
32
+ 与 `ui-conversation` 及命令包一起挂载本插件;composer 随即在待处理指示器旁显示模型位,`/model` 则以弹窗打开同一份目录。模型位菜单打开期间,`↑`/`↓` 在所显示面板的行间移动焦点,`Tab` 选定聚焦行,Escape 与 `Shift+Tab` 先退出已下钻的面板,否则关闭并回到触发器。下钻落在正在使用的那一行,返回则落在打开该面板的格子上。所选模型可用时,composer 显示目录中的模型名称;模型或提供方被删除时(包括账号退登),改为显示已保存的 `provider/model` ID。保存的提供方、模型和推理强度均保持不变。
33
+
34
+ 鼠标选择沿用浏览器原生点击及其取消行为;仅按下按钮不会选定。打开菜单时聚焦触发按钮,再次点击触发按钮会关闭菜单并把焦点还给它。等待任一入口发起的选择结果时,焦点停在触发按钮,触发按钮以加载图标代替下拉箭头,该选择包含的值所在行以加载图标代替勾选标记;选择被拒绝后菜单保持打开,Tab 可回到当前选中的行。
29
35
 
30
36
  ### 模型与推理强度
31
37
 
32
- 模型按提供方分组。composer 菜单只显示模型与推理强度名称。`/model` 弹窗显示提供方名称与目录说明;其中两个内置 DeepSeek 模型的说明使用当前语言,外部提供方说明保持原文。弹窗应用所选模型的默认推理强度;composer 随后可以选择任一已公布的推理强度。适配器没有推理元数据时不显示 Effort 行;不存在任意推理强度输入。
38
+ 模型按提供方分组。composer 菜单只显示模型与推理强度名称,DeepSeek 账号排第一,DeepSeek 排第二,第三方提供方保持目录原有顺序。导航箭头使用 `--dsw-alias-menu-icon` 文本色。`/model` 弹窗显示提供方名称与目录说明;其中两个内置 DeepSeek 模型的说明使用当前语言,外部提供方说明保持原文。弹窗应用所选模型的默认推理强度;composer 随后可以选择任一已公布的推理强度。适配器没有推理元数据时不显示 Effort 行;不存在任意推理强度输入。
33
39
 
34
40
  展开的控件无法排在同一行时,composer 将模型与推理强度文字替换为模型图标;空间足够后恢复文字。触发器的无障碍名称、提示和菜单仍提供完整选择。
35
41
 
36
42
  ### 不可路由的会话
37
43
 
38
- 当 Host 报告没有适配器服务该会话的路由时,本插件注册一个 composer 阻塞块,输入框随之停用并显示本插件自己的文案;恢复后无需重新加载即清除。首次加载之前或加载失败之后的 `null` 绝不阻断;目录成员关系同样不阻断——一条仍在服务、只是不公布该模型的路由不在分组里,却可用。
39
-
40
- 只有当前 Client binding 的目录可以发布对应的 composer 阻塞块。旧 binding 的清理保留后继目录的阻塞块;若尚无后继目录,则移除已失效的阻塞块。
44
+ 目录可用性不会阻止使用已保存选择发送消息;请求执行负责报告凭据缺失或模型不可用。刷新及刷新失败期间保留上次显示的选择和分组。Host 重置时清空这些显示。退登后选择器隐藏账号提供方,同时保留已保存的提供方/模型 ID 和推理强度。再次登录且该模型可用时恢复目录名称。既有会话日志保持不变。
41
45
 
42
46
  ### 选择失败
43
47
 
@@ -48,10 +52,12 @@ Web GUI 允许用户通过 `/model` 弹窗或 composer 模型控件切换既有
48
52
  <a id="understand-the-implementation"></a>
49
53
  ## 理解实现
50
54
 
55
+ 菜单采用共享 `MenuSurface` 材质,包括用于背景模糊的 macOS 底层;自定义内容遵循[菜单规则](../../../docs/web-styling.zh.md#component-rules)。
56
+
51
57
  <details>
52
58
  <summary>实现细节——点击展开</summary>
53
59
 
54
- 两个入口共用一份由 `ModelDirectoryResolver`(`ctx.modelDirectories`)持有的会话级目录:`/model` popupSelect 贡献项(经 `ctx.commandUi` 注册)与 composer 的具名 `conversation.input.model` 位都经 `session.models` 加载会话的建议目录、经 `session.selectModel` 通过同一个 `ModelDirectory` 实例提交,因此任一入口所做的切换正是另一个入口接下来显示的。目录加载与选择共享一个代次计数器,旧响应不会覆盖新结果;连接重置丢弃所有常驻投影,并在显示前重新拉取 Host 恢复的选择。目录按会话惰性解析,随会话作用域一并 dispose(资源释放);已寻址 subagent 会话不公开任一入口。每份常驻目录都会直接在转发的 `llm/adapters-updated` 与 `settings/document-updated` owner 事件上重拉。
60
+ 两个入口共用一份由 `ModelDirectoryResolver`(`ctx.modelDirectories`)持有的会话级目录:`/model` popupSelect 贡献项(经 `ctx.commandUi` 注册)与 composer 的具名 `conversation.input.model` 位都经 `session.models` 加载会话的可用目录、经 `session.selectModel` 通过同一个 `ModelDirectory` 实例提交,因此任一入口所做的切换正是另一个入口接下来显示的。目录加载与选择共享一个代次计数器,旧响应不会覆盖新结果。目录把最近一次提交的选择发布为 `pending`,直到它完成或被连接重置作废;连接重置丢弃所有常驻投影,并在显示前重新拉取 Host 恢复的选择。目录按会话惰性解析,随会话作用域一并 dispose(资源释放);已寻址 subagent 会话不公开任一入口。每份常驻目录都会直接在转发的 `llm/adapters-updated`、`settings/document-updated` 与凭据更新事件上重拉。
55
61
 
56
62
  </details>
57
63
 
@@ -63,7 +69,7 @@ Web GUI 允许用户通过 `/model` 弹窗或 composer 模型控件切换既有
63
69
  当仅了解模型界面还不够时,请阅读以下页面。这些页面从浏览器界面逐步深入到命令弹窗外壳与选择约定。
64
70
 
65
71
  - [ui-commands](../ui-commands/README.zh.md)——`/model` 贡献项注册进的 popupSelect 外壳。
66
- - [ui-conversation](../ui-conversation/README.zh.md)——声明 composer 的 `conversation.input.model` 位与 composer 阻塞块。
72
+ - [ui-conversation](../ui-conversation/README.zh.md)——声明 composer 的 `conversation.input.model` 位。
67
73
  - [dsh-agent-default-model](../../core/agent-default-model/README.zh.md)——为从未选择的会话提供默认模型的默认模型服务。
68
74
  - [客户端包映射](../README.zh.md)——相邻的浏览器 UI 包。
69
75
 
@@ -82,7 +88,6 @@ Web GUI 允许用户通过 `/model` 弹窗或 composer 模型控件切换既有
82
88
 
83
89
  <a id="known-limitations-and-deferred-work"></a>
84
90
 
85
-
86
91
  这些限制界定了当前模型选择界面。它们是当前包约束,不是通用模型路由器对比或任务积压。
87
92
 
88
93
  - **无创建期或已寻址 subagent 选择**——两个入口都要求既有普通会话的 agent(智能体);没有可纳入会话创建的草稿阶段模型选择,subagent 继续执行也有意不公开独立的模型选择约定。
package/lib/client.js CHANGED
@@ -84,6 +84,15 @@ window.__ModuleLoader__.load({
84
84
  status: "idle",
85
85
  error: null
86
86
  });
87
+ reasoning = /* @__PURE__ */ new Map();
88
+ /**
89
+ * Read the last advertised reasoning metadata, including unavailable models.
90
+ * @param selection - provider and model whose effort is displayed.
91
+ * @returns reasoning metadata observed during this Host generation.
92
+ */
93
+ reasoningFor(selection) {
94
+ return this.reasoning.get(JSON.stringify([selection.provider, selection.model]));
95
+ }
87
96
  generation = 0;
88
97
  inflight;
89
98
  /**
@@ -108,11 +117,14 @@ window.__ModuleLoader__.load({
108
117
  });
109
118
  const operation = this.ctx.remote.session.modelCatalog().then((response) => {
110
119
  if (!response.ok) throw new Error(`${response.error.code}: ${response.error.message}`);
111
- if (generation === this.generation) this.store.set({
112
- value: response.value,
113
- status: "ready",
114
- error: null
115
- });
120
+ if (generation === this.generation) {
121
+ for (const group of response.value.groups) for (const model of group.models) this.reasoning.set(JSON.stringify([group.id, model.id]), model.reasoning);
122
+ this.store.set({
123
+ value: response.value,
124
+ status: "ready",
125
+ error: null
126
+ });
127
+ }
116
128
  return response.value;
117
129
  }).catch((error) => {
118
130
  if (generation === this.generation) this.store.update((draft) => {
@@ -147,6 +159,7 @@ window.__ModuleLoader__.load({
147
159
  }
148
160
  /** Clear Host-specific values and load the replacement Host generation. */
149
161
  resetGeneration() {
162
+ this.reasoning.clear();
150
163
  this.invalidate(true);
151
164
  this.load().catch(() => {});
152
165
  }
@@ -167,12 +180,12 @@ window.__ModuleLoader__.load({
167
180
  groups: [],
168
181
  failures: [],
169
182
  status: "idle",
183
+ pending: null,
170
184
  error: null
171
185
  });
172
186
  /** Latest selection operation wins; an older response never overwrites a newer one. */
173
187
  generation = 0;
174
188
  disposed = false;
175
- resolved = false;
176
189
  unsubscribeCatalog;
177
190
  unsubscribeSelection;
178
191
  /**
@@ -197,7 +210,7 @@ window.__ModuleLoader__.load({
197
210
  this.syncInputs();
198
211
  }
199
212
  /**
200
- * Ensure the Host generation's shared advisory catalog is loaded.
213
+ * Ensure the Host generation's shared available catalog is loaded.
201
214
  * @returns the fresh directory value.
202
215
  */
203
216
  async load() {
@@ -218,6 +231,7 @@ window.__ModuleLoader__.load({
218
231
  const generation = ++this.generation;
219
232
  this.store.update((s) => {
220
233
  s.status = "selecting";
234
+ s.pending = selection;
221
235
  s.error = null;
222
236
  });
223
237
  const result = await this.sessions.selectModel({
@@ -233,12 +247,14 @@ window.__ModuleLoader__.load({
233
247
  if (!result.ok) {
234
248
  this.store.update((s) => {
235
249
  s.status = "error";
250
+ s.pending = null;
236
251
  s.error = `${result.error.code}: ${result.error.message}`;
237
252
  });
238
253
  return result;
239
254
  }
240
255
  this.store.update((s) => {
241
256
  s.status = "ready";
257
+ s.pending = null;
242
258
  s.error = null;
243
259
  });
244
260
  this.syncInputs();
@@ -255,6 +271,7 @@ window.__ModuleLoader__.load({
255
271
  ++this.generation;
256
272
  this.store.update((state) => {
257
273
  if (state.status === "selecting") state.status = "idle";
274
+ state.pending = null;
258
275
  state.error = null;
259
276
  });
260
277
  this.syncInputs();
@@ -272,32 +289,33 @@ window.__ModuleLoader__.load({
272
289
  if (this.disposed) return;
273
290
  const catalog = this.catalog.store.getSnapshot();
274
291
  const projected = modelSelectionProjection(this.projected.getSnapshot());
292
+ const intended = projected?.next ?? catalog.value?.default;
293
+ const reasoning = intended === void 0 ? void 0 : this.catalog.reasoningFor(intended);
294
+ const effort = intended?.reasoningEffort ?? reasoning?.defaultEffort;
295
+ const retainedEffort = effort === void 0 ? void 0 : reasoning?.efforts.find((level) => level.id === effort)?.name ?? effort;
275
296
  if (catalog.status !== "ready" || catalog.value === null || projected === void 0) {
276
- if (this.resolved) {
277
- if (catalog.status === "error") this.store.update((state) => {
278
- state.status = "error";
279
- state.error = catalog.error;
280
- });
281
- return;
282
- }
283
297
  this.store.set({
284
- current: null,
298
+ current: catalog.value === null ? null : this.store.getSnapshot().current,
299
+ ...retainedEffort === void 0 ? {} : { retainedEffort },
285
300
  routable: null,
286
- groups: [],
287
- failures: [],
301
+ groups: catalog.value?.groups ?? [],
302
+ failures: catalog.value?.failures ?? [],
288
303
  status: catalog.status === "error" ? "error" : "loading",
304
+ pending: this.store.getSnapshot().pending,
289
305
  error: catalog.error
290
306
  });
291
307
  return;
292
308
  }
293
- const current = projected.next ?? catalog.value.default;
294
- this.resolved = true;
309
+ const selection = projected.next ?? catalog.value.default;
310
+ const routable = catalog.value.groups.some((group) => group.id === selection.provider && group.models.some((model) => model.id === selection.model));
295
311
  this.store.set({
296
- current,
297
- routable: catalog.value.routableProviders.includes(current.provider),
312
+ current: selection,
313
+ ...retainedEffort === void 0 ? {} : { retainedEffort },
314
+ routable,
298
315
  groups: catalog.value.groups,
299
316
  failures: catalog.value.failures,
300
317
  status: this.store.getSnapshot().status === "selecting" ? "selecting" : "ready",
318
+ pending: this.store.getSnapshot().pending,
301
319
  error: null
302
320
  });
303
321
  }
@@ -330,15 +348,11 @@ window.__ModuleLoader__.load({
330
348
  ];
331
349
  live = { directories: new WeakMapWithValues() };
332
350
  catalog;
333
- /** Localized composer-block copy; this plugin owns the string it raises. */
334
- blockReason;
335
351
  /**
336
352
  * @param ctx - owning root context (the service registers itself as `models`).
337
- * @param config - the bound translator for this plugin's own dictionary.
338
353
  */
339
- constructor(ctx, config) {
354
+ constructor(ctx) {
340
355
  super(ctx, "modelDirectories");
341
- this.blockReason = config.blockReason;
342
356
  this.catalog = new ModelCatalogDirectory(ctx);
343
357
  this.catalog.load().catch(() => {});
344
358
  ctx.on("connection/reset", () => {
@@ -351,6 +365,9 @@ window.__ModuleLoader__.load({
351
365
  ctx.remote.$on("settings/document-updated", () => {
352
366
  this.catalog.refresh();
353
367
  });
368
+ ctx.remote.$on("credentials/record-updated", () => {
369
+ this.catalog.refresh();
370
+ });
354
371
  ctx.remote.$on("credentials/reference-updated", () => {
355
372
  this.catalog.refresh();
356
373
  });
@@ -372,23 +389,6 @@ window.__ModuleLoader__.load({
372
389
  if (existing !== void 0) return existing;
373
390
  const directory = new ModelDirectory(this.ctx.remote.session, sessionId, () => sessions.subagentAddress(sessionId) === void 0, this.catalog, binding.session.projections.faceOf("modelSelection"));
374
391
  live.directories.set(binding, directory);
375
- const conversation = this.ctx.get("conversation");
376
- if (conversation !== void 0) {
377
- const publish = () => {
378
- if (sessions.binding(sessionId) !== binding) return;
379
- conversation.blocks.set(sessionId, directory.store.getSnapshot().routable === false ? { reason: this.blockReason() } : void 0);
380
- };
381
- publish();
382
- actx.effect(() => {
383
- const stop = directory.store.subscribe(publish);
384
- return () => {
385
- stop();
386
- const current = sessions.binding(sessionId);
387
- if (current !== void 0 && current !== binding && live.directories.get(current) !== void 0) return;
388
- conversation.blocks.set(sessionId, void 0);
389
- };
390
- }, "ui-model-selection: composer block");
391
- }
392
392
  actx.effect(() => () => {
393
393
  directory.dispose();
394
394
  live.directories.delete(binding);
@@ -413,7 +413,7 @@ window.__ModuleLoader__.load({
413
413
  }
414
414
  //#endregion
415
415
  //#region \0dsh-css:/home/runner/work/deepseek-harness/deepseek-harness/packages/client/ui-model-selection/src/client/ModelSelect.module.css.mjs
416
- const css = "._7KE1Ra_root{min-width:0;position:relative}._7KE1Ra_trigger{min-width:0;max-width:min(360px,45cqw);height:28px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:24px;outline:none;align-items:center;gap:4px;padding:0 4px 0 8px;font-size:13px;font-weight:500;line-height:20px;display:flex}._7KE1Ra_trigger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}._7KE1Ra_trigger:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-border-l3)}._7KE1Ra_trigger:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}._7KE1Ra_triggerLabel{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}._7KE1Ra_triggerEffort{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-caption);flex-shrink:1000;overflow:hidden}._7KE1Ra_triggerIcon{display:var(--dsh-composer-model-icon-display,none);flex:none}._7KE1Ra_triggerLabel,._7KE1Ra_triggerEffort{display:var(--dsh-composer-model-text-display,block)}._7KE1Ra_chevron{color:var(--dsw-alias-label-caption);flex:none;transition:transform .12s}._7KE1Ra_chevronOpen{transform:rotate(180deg)}._7KE1Ra_menu{z-index:1100;background:var(--dsw-specific-menu);width:max-content;min-width:min(240px,100vw - 32px);max-width:min(420px,100vw - 32px);max-height:min(360px,100vh - 96px);backdrop-filter:var(--dsw-menu-backdrop-filter);--dsw-elevation-stroke-color:var(--dsw-alias-border-l1);box-shadow:var(--dsw-elevation-prominent);color:var(--dsw-alias-label-primary);--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);border:0;border-radius:16px;flex-direction:column;padding:3px;display:flex;position:fixed;overflow:hidden}._7KE1Ra_status,._7KE1Ra_empty{color:var(--dsw-alias-label-tertiary);padding:8px;font-size:12px;line-height:18px}._7KE1Ra_error,._7KE1Ra_warning{background:var(--dsw-alias-interactive-bg-hover-danger);color:var(--dsw-alias-state-error-primary);border-radius:7px;justify-content:space-between;align-items:flex-start;gap:6px;margin-bottom:3px;padding:6px 7px;font-size:11px;line-height:16px;display:flex}._7KE1Ra_warning{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-state-warn-label)}._7KE1Ra_retry{color:inherit;font:inherit;cursor:pointer;background:0 0;border:none;flex:none;padding:0;font-weight:600}._7KE1Ra_groups{min-height:0;overflow-y:auto}._7KE1Ra_group+._7KE1Ra_group{margin-top:3px}._7KE1Ra_groupTitle{z-index:1;background:var(--dsw-specific-menu);color:var(--dsw-alias-label-tertiary);padding:4px 7px 2px;font-size:11px;font-weight:500;line-height:16px;position:sticky;top:0}._7KE1Ra_option{box-sizing:border-box;width:auto;min-width:100%;min-height:34px;color:inherit;text-align:left;cursor:pointer;background:0 0;border:none;border-radius:8px;outline:none;align-items:center;gap:6px;padding:5px 7px;display:flex}._7KE1Ra_option:hover:not(:disabled),._7KE1Ra_option:focus-visible{background:var(--dsw-alias-interactive-bg-hover)}._7KE1Ra_selected{background:0 0}._7KE1Ra_option:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}._7KE1Ra_optionCopy{flex-direction:column;flex:1;min-width:0;display:flex}._7KE1Ra_modelName{color:inherit;text-overflow:ellipsis;white-space:nowrap;font-size:13px;font-weight:500;line-height:18px;overflow:hidden}._7KE1Ra_check{color:var(--dsw-alias-label-primary);flex:0 0 14px;place-items:center;display:grid}._7KE1Ra_check svg{width:14px;height:14px}._7KE1Ra_cell{box-sizing:border-box;width:auto;min-width:100%;height:34px;color:var(--dsw-alias-label-primary);cursor:pointer;text-align:left;background:0 0;border:none;border-radius:8px;align-items:center;gap:6px;padding:0 8px;font-size:13px;line-height:20px;display:flex}._7KE1Ra_cell:hover{background:var(--dsw-alias-interactive-bg-hover)}._7KE1Ra_cellLabel{white-space:nowrap;flex:none}._7KE1Ra_cellValue{text-overflow:ellipsis;white-space:nowrap;text-align:right;min-width:0;color:var(--dsw-alias-label-tertiary);flex:auto;overflow:hidden}._7KE1Ra_cellChevron{width:12px;height:12px;color:var(--dsw-alias-label-tertiary);flex:none}";
416
+ const css = "._7KE1Ra_root{min-width:0;position:relative}._7KE1Ra_trigger{border-radius:var(--dsw-radius-sm);min-width:0;max-width:min(360px,45cqw);height:28px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;outline:none;align-items:center;gap:4px;padding:0 4px 0 8px;font-size:13px;font-weight:400;line-height:20px;display:flex}._7KE1Ra_trigger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}._7KE1Ra_trigger:focus-visible{box-shadow:0 0 0 2px var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary))}._7KE1Ra_trigger:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}._7KE1Ra_triggerLabel{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}._7KE1Ra_triggerEffort{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-caption);flex-shrink:1000;overflow:hidden}._7KE1Ra_triggerIcon{display:var(--dsh-composer-model-icon-display,none);flex:none}._7KE1Ra_triggerLabel,._7KE1Ra_triggerEffort{display:var(--dsh-composer-model-text-display,block)}._7KE1Ra_chevron{color:var(--dsw-alias-label-caption);flex:none;transition:transform .12s}._7KE1Ra_chevronOpen{transform:rotate(180deg)}._7KE1Ra_menu{z-index:1100;--dsw-elevation-stroke-color:var(--dsw-alias-border-l1);width:max-content;min-width:min(240px,100vw - 32px);max-width:min(420px,100vw - 32px);max-height:min(360px,100vh - 96px);box-shadow:var(--dsw-elevation-prominent);color:var(--dsw-alias-label-primary);--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);border:0;flex-direction:column;padding:4px;display:flex;position:fixed;overflow:hidden}._7KE1Ra_status,._7KE1Ra_empty{color:var(--dsw-alias-label-tertiary);padding:8px;font-size:12px;line-height:18px}._7KE1Ra_error,._7KE1Ra_warning{border-radius:var(--dsw-radius-md);background:var(--dsw-alias-interactive-bg-hover-danger);color:var(--dsw-alias-state-error-primary);justify-content:space-between;align-items:flex-start;gap:6px;margin-bottom:3px;padding:6px 7px;font-size:11px;line-height:16px;display:flex}._7KE1Ra_warning{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-state-warn-label)}._7KE1Ra_retry{color:inherit;font:inherit;cursor:pointer;background:0 0;border:none;flex:none;padding:0;font-weight:600}._7KE1Ra_groups{min-height:0;overflow-y:auto}._7KE1Ra_group+._7KE1Ra_group{margin-top:3px}._7KE1Ra_groupTitle{z-index:1;background:var(--dsw-specific-menu);color:var(--dsw-alias-label-tertiary);padding:4px 7px 2px;font-size:11px;font-weight:500;line-height:16px;position:sticky;top:0}._7KE1Ra_option{box-sizing:border-box;border-radius:var(--dsw-radius-md);width:auto;min-width:100%;min-height:34px;color:inherit;text-align:left;cursor:pointer;background:0 0;border:none;outline:none;align-items:center;gap:6px;padding:5px 7px;display:flex}._7KE1Ra_option:hover:not(:disabled),._7KE1Ra_option:focus-visible{background:var(--dsw-alias-interactive-bg-hover)}._7KE1Ra_selected{background:0 0}._7KE1Ra_option:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}._7KE1Ra_optionCopy{flex-direction:column;flex:1;min-width:0;display:flex}._7KE1Ra_modelName{color:inherit;text-overflow:ellipsis;white-space:nowrap;font-size:13px;font-weight:500;line-height:18px;overflow:hidden}._7KE1Ra_check{color:var(--dsw-alias-label-primary);flex:0 0 14px;place-items:center;display:grid}._7KE1Ra_check svg{width:14px;height:14px}._7KE1Ra_cell{box-sizing:border-box;border-radius:var(--dsw-radius-md);width:auto;min-width:100%;height:34px;color:var(--dsw-alias-label-primary);cursor:pointer;text-align:left;background:0 0;border:none;align-items:center;gap:6px;padding:0 8px;font-size:13px;line-height:20px;display:flex}._7KE1Ra_cell:hover{background:var(--dsw-alias-interactive-bg-hover)}._7KE1Ra_cellLabel{white-space:nowrap;flex:none}._7KE1Ra_cellValue{text-overflow:ellipsis;white-space:nowrap;text-align:right;min-width:0;color:var(--dsw-alias-label-tertiary);flex:auto;overflow:hidden}._7KE1Ra_cellChevron{width:12px;height:12px;color:var(--dsw-alias-menu-icon);flex:none}";
417
417
  const tagId = "@deepseek-ai/dsh-client-ui-model-selection/ModelSelect.module.css";
418
418
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
419
419
  const tag = document.createElement("style");
@@ -468,7 +468,9 @@ window.__ModuleLoader__.load({
468
468
  * selected effort come from the Host rather than a client-owned vocabulary. A
469
469
  * rejected selection announces through the shared transient Toast anchored to
470
470
  * the composer card; the in-menu strip with Retry remains the catalog-load
471
- * surface.
471
+ * surface. While the directory's pending selection is unsettled, the trigger
472
+ * shows a spinner in place of its chevron, and each row whose value that
473
+ * selection carries shows one in place of its check mark.
472
474
  */
473
475
  /** Unplaced portal card: hidden but laid out at a fixed origin so offsetWidth/offsetHeight are real (Menu primitive's measure pass). */
474
476
  const MEASURE_STYLE = {
@@ -495,7 +497,8 @@ window.__ModuleLoader__.load({
495
497
  const [menuPos, setMenuPos] = (0, react.useState)(null);
496
498
  const itemRefs = (0, react.useRef)([]);
497
499
  const id = (0, react.useId)();
498
- const choices = (0, react.useMemo)(() => state.groups.flatMap((group) => group.models.map((model) => ({
500
+ const groups = (0, react.useMemo)(() => state.groups.toSorted((left, right) => (left.id === "deepseek-account" ? 0 : left.id === "deepseek-official" ? 1 : 2) - (right.id === "deepseek-account" ? 0 : right.id === "deepseek-official" ? 1 : 2)), [state.groups]);
501
+ const choices = (0, react.useMemo)(() => groups.flatMap((group) => group.models.map((model) => ({
499
502
  group,
500
503
  model,
501
504
  selection: {
@@ -503,11 +506,11 @@ window.__ModuleLoader__.load({
503
506
  model: model.id,
504
507
  ...model.reasoning?.defaultEffort === void 0 ? {} : { reasoningEffort: model.reasoning.defaultEffort }
505
508
  }
506
- }))), [state.groups]);
509
+ }))), [groups]);
507
510
  const currentChoice = choices[state.current === null ? -1 : choices.findIndex((c) => c.selection.provider === state.current?.provider && c.selection.model === state.current.model)];
508
511
  const reasoning = currentChoice?.model.reasoning;
509
512
  const effectiveEffort = state.current?.reasoningEffort ?? reasoning?.defaultEffort;
510
- const effortLabel = reasoning === void 0 ? void 0 : effectiveEffort === void 0 ? t("effort.providerDefault") : reasoning.efforts.find((level) => level.id === effectiveEffort)?.name ?? effectiveEffort;
513
+ const effortLabel = reasoning === void 0 ? state.retainedEffort : effectiveEffort === void 0 ? t("effort.providerDefault") : reasoning.efforts.find((level) => level.id === effectiveEffort)?.name ?? effectiveEffort;
511
514
  const effortChoices = (0, react.useMemo)(() => reasoning === void 0 ? [] : [...reasoning.defaultEffort === void 0 ? [{
512
515
  key: "provider-default",
513
516
  effort: void 0,
@@ -517,7 +520,8 @@ window.__ModuleLoader__.load({
517
520
  effort: effort.id,
518
521
  label: effort.name
519
522
  }))], [reasoning, t]);
520
- const busy = state.status === "selecting";
523
+ const { pending } = state;
524
+ const busy = pending !== null;
521
525
  const reload = () => {
522
526
  lastActionRef.current = "load";
523
527
  load();
@@ -581,7 +585,9 @@ window.__ModuleLoader__.load({
581
585
  ]);
582
586
  if (!available) return null;
583
587
  const show = () => {
584
- setPane("root");
588
+ triggerRef.current?.focus();
589
+ if (state.current === null) paneFocus.current = "drill";
590
+ setPane(state.current === null ? "model" : "root");
585
591
  setOpen(true);
586
592
  reload();
587
593
  };
@@ -610,7 +616,7 @@ window.__ModuleLoader__.load({
610
616
  const onRootKeyDown = (event) => {
611
617
  if (event.key === "Escape" && open) {
612
618
  event.preventDefault();
613
- if (pane !== "root") back(pane);
619
+ if (pane !== "root" && state.current !== null) back(pane);
614
620
  else close(true);
615
621
  return;
616
622
  }
@@ -618,7 +624,7 @@ window.__ModuleLoader__.load({
618
624
  if (event.key === "Tab") {
619
625
  if (event.shiftKey) {
620
626
  event.preventDefault();
621
- if (pane !== "root") back(pane);
627
+ if (pane !== "root" && state.current !== null) back(pane);
622
628
  else close(true);
623
629
  return;
624
630
  }
@@ -656,13 +662,17 @@ window.__ModuleLoader__.load({
656
662
  text: error.code === "session/writer-held" ? t("error.sessionInUse") : t("error.action", { message: `${error.code}: ${error.message}` })
657
663
  });
658
664
  };
665
+ const submit = (selection) => {
666
+ lastActionRef.current = "select";
667
+ triggerRef.current?.focus();
668
+ select(selection).then(settleSelection);
669
+ };
659
670
  const choose = (selection) => {
660
671
  if (state.current?.provider === selection.provider && state.current.model === selection.model) {
661
672
  close(true);
662
673
  return;
663
674
  }
664
- lastActionRef.current = "select";
665
- select(selection).then(settleSelection);
675
+ submit(selection);
666
676
  };
667
677
  const chooseEffort = (effort) => {
668
678
  if (state.current === null) return;
@@ -670,13 +680,11 @@ window.__ModuleLoader__.load({
670
680
  close(true);
671
681
  return;
672
682
  }
673
- const selection = {
683
+ submit({
674
684
  provider: state.current.provider,
675
685
  model: state.current.model,
676
686
  ...effort === void 0 ? {} : { reasoningEffort: effort }
677
- };
678
- lastActionRef.current = "select";
679
- select(selection).then(settleSelection);
687
+ });
680
688
  };
681
689
  const waiting = state.current === null && state.status === "loading";
682
690
  const modelLabel = waiting ? t("trigger.loading") : currentChoice?.model.name ?? (state.current === null ? t("trigger.fallback") : `${state.current.provider}/${state.current.model}`);
@@ -698,6 +706,9 @@ window.__ModuleLoader__.load({
698
706
  className: ModelSelect_module_css_default.root,
699
707
  onKeyDown: onRootKeyDown,
700
708
  onBlur,
709
+ onMouseDown: (event) => {
710
+ if (event.target instanceof Element && event.target.closest("button") !== null) event.preventDefault();
711
+ },
701
712
  children: [
702
713
  (0, react_jsx_runtime.jsxs)("button", {
703
714
  ref: triggerRef,
@@ -708,9 +719,10 @@ window.__ModuleLoader__.load({
708
719
  "aria-expanded": open,
709
720
  "aria-controls": open ? `${id}-menu` : void 0,
710
721
  title: triggerLabel,
722
+ "aria-busy": busy,
711
723
  disabled: locked,
712
724
  onClick: () => {
713
- if (open) close();
725
+ if (open) close(true);
714
726
  else show();
715
727
  },
716
728
  children: [
@@ -726,10 +738,10 @@ window.__ModuleLoader__.load({
726
738
  className: ModelSelect_module_css_default.triggerEffort,
727
739
  children: effortLabel
728
740
  }),
729
- (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: clsx(ModelSelect_module_css_default.chevron, open && ModelSelect_module_css_default.chevronOpen) })
741
+ busy ? (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: "ongoing" }) : (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: clsx(ModelSelect_module_css_default.chevron, open && ModelSelect_module_css_default.chevronOpen) })
730
742
  ]
731
743
  }),
732
- open && (0, react_dom.createPortal)((0, react_jsx_runtime.jsxs)("div", {
744
+ open && (0, react_dom.createPortal)((0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.MenuSurface, {
733
745
  ref: menuRef,
734
746
  id: `${id}-menu`,
735
747
  className: ModelSelect_module_css_default.menu,
@@ -794,7 +806,7 @@ window.__ModuleLoader__.load({
794
806
  state.failures.map((failure) => (0, react_jsx_runtime.jsxs)("div", {
795
807
  className: ModelSelect_module_css_default.warning,
796
808
  children: [(0, react_jsx_runtime.jsx)("span", { children: t("warning.groupLoad", {
797
- name: failure.name,
809
+ name: failure.id === "deepseek-account" ? t("provider.account") : failure.name,
798
810
  message: failure.message
799
811
  }) }), (0, react_jsx_runtime.jsx)("button", {
800
812
  type: "button",
@@ -805,7 +817,7 @@ window.__ModuleLoader__.load({
805
817
  }, failure.id)),
806
818
  (0, react_jsx_runtime.jsx)("div", {
807
819
  className: clsx(ModelSelect_module_css_default.groups, "scrollable"),
808
- children: state.groups.map((group) => {
820
+ children: groups.map((group) => {
809
821
  const headingId = `${id}-${group.id}`;
810
822
  return (0, react_jsx_runtime.jsxs)("section", {
811
823
  role: "group",
@@ -814,7 +826,7 @@ window.__ModuleLoader__.load({
814
826
  children: [(0, react_jsx_runtime.jsx)("div", {
815
827
  className: ModelSelect_module_css_default.groupTitle,
816
828
  id: headingId,
817
- children: group.name
829
+ children: group.id === "deepseek-account" ? t("provider.account") : group.name
818
830
  }), group.models.map((model) => {
819
831
  const selected = state.current?.provider === group.id && state.current.model === model.id;
820
832
  return (0, react_jsx_runtime.jsxs)("button", {
@@ -839,7 +851,7 @@ window.__ModuleLoader__.load({
839
851
  })
840
852
  }), (0, react_jsx_runtime.jsx)("span", {
841
853
  className: ModelSelect_module_css_default.check,
842
- children: selected ? (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, {}) : null
854
+ children: pending?.provider === group.id && pending.model === model.id ? (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: "ongoing" }) : selected ? (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, {}) : null
843
855
  })]
844
856
  }, model.id);
845
857
  })]
@@ -880,7 +892,7 @@ window.__ModuleLoader__.load({
880
892
  })
881
893
  }), (0, react_jsx_runtime.jsx)("span", {
882
894
  className: ModelSelect_module_css_default.check,
883
- children: effectiveEffort === level.effort ? (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, {}) : null
895
+ children: pending !== null && pending.provider === state.current?.provider && pending.model === state.current.model && pending.reasoningEffort === level.effort ? (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: "ongoing" }) : effectiveEffort === level.effort ? (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, {}) : null
884
896
  })]
885
897
  }, level.key))] })
886
898
  ]
@@ -909,14 +921,15 @@ window.__ModuleLoader__.load({
909
921
  */
910
922
  /** Simplified Chinese dictionary (the key-set source of truth). */
911
923
  const zh = {
924
+ "provider.account": "DeepSeek 账号",
912
925
  "command.label": "模型",
913
926
  "command.description": "选择本会话使用的模型",
914
927
  "option.loadError": "目录加载失败:{message}",
915
928
  "option.deepseekV4Flash.description": "快速、高效且经济;适合目标明确、常规或并行任务。",
916
929
  "option.deepseekV4Pro.description": "更强的自主编码、知识与复杂推理能力;适合复杂或质量优先的任务,但成本更高。",
917
- "trigger.fallback": "选择模型",
930
+ "trigger.fallback": "请选择模型",
918
931
  "trigger.loading": "正在加载模型…",
919
- "trigger.selectAria": "选择模型",
932
+ "trigger.selectAria": "请选择模型",
920
933
  "trigger.aria": "选择模型,当前 {model}",
921
934
  "trigger.ariaEffort": "选择模型,当前 {model},推理等级 {effort}",
922
935
  "menu.aria": "模型与推理等级",
@@ -929,11 +942,11 @@ window.__ModuleLoader__.load({
929
942
  "action.reload": "重新加载",
930
943
  "warning.groupLoad": "{name} 加载失败:{message}",
931
944
  "empty.models": "没有可用的模型。",
932
- "blocked.composer": "当前模型不可用,请先选择模型",
933
945
  "empty.efforts": "当前模型未提供推理等级。"
934
946
  };
935
947
  /** English dictionary, checked complete against the zh key set. */
936
948
  const en = {
949
+ "provider.account": "DeepSeek Account",
937
950
  "command.label": "Model",
938
951
  "command.description": "Select the model for this conversation",
939
952
  "option.loadError": "Catalog failed to load: {message}",
@@ -954,7 +967,6 @@ window.__ModuleLoader__.load({
954
967
  "action.reload": "Reload",
955
968
  "warning.groupLoad": "{name} failed to load: {message}",
956
969
  "empty.models": "No models available.",
957
- "blocked.composer": "This model is unavailable — select one to continue",
958
970
  "empty.efforts": "This model provides no reasoning effort levels."
959
971
  };
960
972
  //#endregion
@@ -964,6 +976,8 @@ window.__ModuleLoader__.load({
964
976
  return `${providerId}/${modelId}`;
965
977
  }
966
978
  const BUILTIN_DESCRIPTION_KEYS = {
979
+ "deepseek-account/deepseek-v4-flash": "option.deepseekV4Flash.description",
980
+ "deepseek-account/deepseek-v4-pro": "option.deepseekV4Pro.description",
967
981
  "deepseek-official/deepseek-v4-flash": "option.deepseekV4Flash.description",
968
982
  "deepseek-official/deepseek-v4-pro": "option.deepseekV4Pro.description"
969
983
  };
@@ -974,18 +988,21 @@ window.__ModuleLoader__.load({
974
988
  /** Flatten the directory into popup rows; failure rows are listed for visibility but never selectable. */
975
989
  function optionsOf(directory, t) {
976
990
  const rows = [];
977
- for (const group of directory.groups) for (const model of group.models) {
978
- const description = descriptionOf(group.id, model, t);
979
- rows.push({
980
- id: rowId(group.id, model.id),
981
- label: model.name,
982
- detail: description !== void 0 ? `${group.name} · ${description}` : group.name,
983
- ...directory.current !== null && directory.current.provider === group.id && directory.current.model === model.id ? { active: true } : {}
984
- });
991
+ for (const group of directory.groups) {
992
+ const name = group.id === "deepseek-account" ? t("provider.account") : group.name;
993
+ for (const model of group.models) {
994
+ const description = descriptionOf(group.id, model, t);
995
+ rows.push({
996
+ id: rowId(group.id, model.id),
997
+ label: model.name,
998
+ detail: description !== void 0 ? `${name} · ${description}` : name,
999
+ ...directory.current !== null && directory.current.provider === group.id && directory.current.model === model.id ? { active: true } : {}
1000
+ });
1001
+ }
985
1002
  }
986
1003
  for (const failure of directory.failures) rows.push({
987
1004
  id: `failure/${failure.id}`,
988
- label: failure.name,
1005
+ label: failure.id === "deepseek-account" ? t("provider.account") : failure.name,
989
1006
  detail: t("option.loadError", { message: failure.message })
990
1007
  });
991
1008
  return rows;
@@ -1031,7 +1048,7 @@ window.__ModuleLoader__.load({
1031
1048
  en
1032
1049
  }), "ui-model-selection: dictionaries");
1033
1050
  const t = ctx.locale.bind(NS);
1034
- ctx.plugin(ModelDirectoryResolver, { blockReason: () => t("blocked.composer") });
1051
+ ctx.plugin(ModelDirectoryResolver);
1035
1052
  ctx.inject(["commandUi", "modelDirectories"], (scope) => {
1036
1053
  const command = scope.get("commandUi");
1037
1054
  const models = scope.modelDirectories;
@@ -1,6 +1,6 @@
1
1
  /** One Host-generation model catalog shared by every Session selector. */
2
2
  import type { Context as ClientContext } from '@deepseek-ai/cordis';
3
- import type { ModelCatalog } from '@deepseek-ai/dsh-api-remotes/client';
3
+ import type { ModelCatalog, ModelSelection, ModelProviderGroup } from '@deepseek-ai/dsh-api-remotes/client';
4
4
  import { type SnapshotStore } from '@deepseek-ai/dsh-client-store';
5
5
  /** Observable lifecycle of the shared model catalog. */
6
6
  export interface ModelCatalogState {
@@ -13,6 +13,13 @@ export declare class ModelCatalogDirectory {
13
13
  private readonly ctx;
14
14
  /** Current shared catalog value and load lifecycle. */
15
15
  readonly store: SnapshotStore<ModelCatalogState>;
16
+ private readonly reasoning;
17
+ /**
18
+ * Read the last advertised reasoning metadata, including unavailable models.
19
+ * @param selection - provider and model whose effort is displayed.
20
+ * @returns reasoning metadata observed during this Host generation.
21
+ */
22
+ reasoningFor(selection: ModelSelection): ModelProviderGroup['models'][number]['reasoning'];
16
23
  private generation;
17
24
  private inflight;
18
25
  /**
@@ -11,15 +11,11 @@ import type { ObservableSnapshot, SnapshotStore } from '@deepseek-ai/dsh-client-
11
11
  import type { ModelCatalogDirectory } from './catalog.ts';
12
12
  /** Directory snapshot both entries render from. */
13
13
  export interface ModelDirectoryState {
14
- /** Effective selection: durable next-request projection, then Host default. */
14
+ /** Saved selection, retained even when its provider or model leaves the catalog. */
15
15
  current: ModelSelection | null;
16
- /**
17
- * Whether an adapter serves the current selection's provider, as the host reports
18
- * it — null before the first load, which is NOT the same as blocked. Read
19
- * this rather than "current matches no group": catalog membership is
20
- * advisory, so a route serving a model it stopped advertising is missing
21
- * from the groups yet perfectly usable.
22
- */
16
+ /** Saved effort caption retained when the selected model is unavailable. */
17
+ retainedEffort?: string;
18
+ /** Whether the current selection is present in the available catalog; null while unresolved. */
23
19
  routable: boolean | null;
24
20
  /** Successfully loaded provider groups (last good load). */
25
21
  groups: readonly ModelProviderGroup[];
@@ -27,6 +23,8 @@ export interface ModelDirectoryState {
27
23
  failures: readonly ModelCatalogFailure[];
28
24
  /** Lifecycle of the in-flight operation. */
29
25
  status: 'idle' | 'loading' | 'ready' | 'selecting' | 'error';
26
+ /** Selection submitted by the latest `select` until it settles; null otherwise. */
27
+ pending: ModelSelection | null;
30
28
  /** Whole-request or selection failure text; null when none. */
31
29
  error: string | null;
32
30
  }
@@ -42,7 +40,6 @@ export declare class ModelDirectory {
42
40
  /** Latest selection operation wins; an older response never overwrites a newer one. */
43
41
  private generation;
44
42
  private disposed;
45
- private resolved;
46
43
  private readonly unsubscribeCatalog;
47
44
  private readonly unsubscribeSelection;
48
45
  /**
@@ -54,7 +51,7 @@ export declare class ModelDirectory {
54
51
  */
55
52
  constructor(sessions: Pick<TypertClientRemote['session'], 'selectModel'>, sessionId: SessionId, available: () => boolean, catalog: ModelCatalogDirectory, projected: ObservableSnapshot<unknown>);
56
53
  /**
57
- * Ensure the Host generation's shared advisory catalog is loaded.
54
+ * Ensure the Host generation's shared available catalog is loaded.
58
55
  * @returns the fresh directory value.
59
56
  */
60
57
  load(): Promise<ModelDirectoryState>;
@@ -9,6 +9,7 @@
9
9
  */
10
10
  /** Simplified Chinese dictionary (the key-set source of truth). */
11
11
  export declare const zh: {
12
+ 'provider.account': string;
12
13
  'command.label': string;
13
14
  'command.description': string;
14
15
  'option.loadError': string;
@@ -29,13 +30,13 @@ export declare const zh: {
29
30
  'action.reload': string;
30
31
  'warning.groupLoad': string;
31
32
  'empty.models': string;
32
- 'blocked.composer': string;
33
33
  'empty.efforts': string;
34
34
  };
35
35
  /** The model namespace key union. */
36
36
  export type ModelKey = keyof typeof zh;
37
37
  /** English dictionary, checked complete against the zh key set. */
38
38
  export declare const en: {
39
+ 'provider.account': string;
39
40
  'command.label': string;
40
41
  'command.description': string;
41
42
  'option.loadError': string;
@@ -56,7 +57,6 @@ export declare const en: {
56
57
  'action.reload': string;
57
58
  'warning.groupLoad': string;
58
59
  'empty.models': string;
59
- 'blocked.composer': string;
60
60
  'empty.efforts': string;
61
61
  };
62
62
  //# sourceMappingURL=locales.d.ts.map
@@ -26,15 +26,10 @@ export declare class ModelDirectoryResolver extends Service {
26
26
  static inject: string[];
27
27
  private readonly live;
28
28
  private readonly catalog;
29
- /** Localized composer-block copy; this plugin owns the string it raises. */
30
- private readonly blockReason;
31
29
  /**
32
30
  * @param ctx - owning root context (the service registers itself as `models`).
33
- * @param config - the bound translator for this plugin's own dictionary.
34
31
  */
35
- constructor(ctx: Context, config: {
36
- blockReason: () => string;
37
- });
32
+ constructor(ctx: Context);
38
33
  /**
39
34
  * Resolve the per-session shared directory (lazy; the scope disposer
40
35
  * removes and disposes it). Unknown sessions fail loud.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-client-ui-model-selection",
3
3
  "description": "Model selection over the shared model catalog, Session projection, and session.selectModel",
4
- "version": "0.1.7-alpha.2",
4
+ "version": "0.1.7-rc.2",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -46,22 +46,22 @@
46
46
  "react-dom": "^18.2.0",
47
47
  "@types/react-dom": "~18.3.0",
48
48
  "clsx": "^2.1.1",
49
- "@deepseek-ai/dsh-api-remotes": "0.1.7-alpha.2",
50
- "@deepseek-ai/dsh-api-session-controller": "0.1.7-alpha.2",
51
- "@deepseek-ai/dsh-client-test-runtime": "0.1.7-alpha.2",
52
- "@deepseek-ai/dsh-client-ui-commands": "0.1.7-alpha.2",
53
- "@deepseek-ai/dsh-client-ui-conversation": "0.1.7-alpha.2",
54
- "@deepseek-ai/dsh-client-ui-input-trigger": "0.1.7-alpha.2",
55
- "@deepseek-ai/dsh-client-ui-primitives": "0.1.7-alpha.2",
56
- "@deepseek-ai/dsh-typert-protocol": "0.1.7-alpha.2",
57
- "@deepseek-ai/dsh-client-ui-slots": "0.1.7-alpha.2",
58
- "@deepseek-ai/dsh-client-store": "0.1.7-alpha.2",
49
+ "@deepseek-ai/dsh-api-remotes": "0.1.7-rc.2",
50
+ "@deepseek-ai/dsh-api-session-controller": "0.1.7-rc.2",
51
+ "@deepseek-ai/dsh-client-locale": "0.1.7-rc.2",
52
+ "@deepseek-ai/dsh-client-test-runtime": "0.1.7-rc.2",
53
+ "@deepseek-ai/dsh-client-ui-commands": "0.1.7-rc.2",
54
+ "@deepseek-ai/dsh-client-ui-conversation": "0.1.7-rc.2",
55
+ "@deepseek-ai/dsh-client-ui-input-trigger": "0.1.7-rc.2",
56
+ "@deepseek-ai/dsh-client-ui-primitives": "0.1.7-rc.2",
57
+ "@deepseek-ai/dsh-client-ui-slots": "0.1.7-rc.2",
58
+ "@deepseek-ai/dsh-typert-protocol": "0.1.7-rc.2",
59
59
  "@deepseek-ai/cordis": "~4.0.4",
60
- "@deepseek-ai/dsh-client-locale": "0.1.7-alpha.2",
61
- "@deepseek-ai/dsh-session": "0.1.7-alpha.2",
62
- "@deepseek-ai/dsh-util-values": "0.1.7-alpha.2",
63
- "@deepseek-ai/dsh-client-ui-session": "0.1.7-alpha.2",
64
- "@deepseek-ai/dsh-client-ui-renderer": "0.1.7-alpha.2"
60
+ "@deepseek-ai/dsh-client-store": "0.1.7-rc.2",
61
+ "@deepseek-ai/dsh-session": "0.1.7-rc.2",
62
+ "@deepseek-ai/dsh-util-values": "0.1.7-rc.2",
63
+ "@deepseek-ai/dsh-client-ui-renderer": "0.1.7-rc.2",
64
+ "@deepseek-ai/dsh-client-ui-session": "0.1.7-rc.2"
65
65
  },
66
66
  "files": [
67
67
  "lib/index.js",