@deepseek-ai/dsh-client-ui-model-selection 0.1.7-rc.1 → 0.2.0-rc.1

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: 60a1491779cda03daac53cecf3fe1597e6930414
6
- README.zh.md: b1d297877bfe4f112e07e357f836b4cb06608077
5
+ /:
6
+ en: f39813648aeef5a0
7
+ zh: eea25094e8bfc031
8
+ /deepseek-ai-dsh-client-ui-model-selection:
9
+ en: 4cb6feb12e46f3a0
10
+ zh: eea39dd76fe1fa8b
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
@@ -7,9 +7,11 @@ kind: "package-reference"
7
7
 
8
8
  English | [中文](README.zh.md)
9
9
 
10
+ Desktop product events use the optional [product analytics service](../product-analytics/README.md); ordinary Web interactions are excluded.
11
+
10
12
  ## Summary
11
13
 
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.
14
+ 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
15
 
14
16
  ## Table of Contents
15
17
 
@@ -25,21 +27,23 @@ The Web GUI lets users switch the model and reasoning effort for an existing ses
25
27
  <a id="use-this-package"></a>
26
28
  ## Use this package
27
29
 
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`.
30
+ DeepSeek account and API-key routes appear as separate provider groups, each exposing the same configured model catalog.
31
+
32
+ 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.
33
+
34
+ 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.
29
35
 
30
- 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 is pending, focus stays on the trigger; a rejected selection leaves the menu open, and Tab returns to the current row.
36
+ 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.
31
37
 
32
38
  ### Model and effort
33
39
 
34
- 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.
40
+ 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.
35
41
 
36
42
  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.
37
43
 
38
44
  ### Unroutable sessions
39
45
 
40
- 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.
41
-
42
- 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.
46
+ 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.
43
47
 
44
48
  ### Selection failures
45
49
 
@@ -50,10 +54,12 @@ When another writer owns the Session, model-selection failures tell the user to
50
54
  <a id="understand-the-implementation"></a>
51
55
  ## Understand the implementation
52
56
 
57
+ 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).
58
+
53
59
  <details>
54
60
  <summary>Implementation internals — click to expand</summary>
55
61
 
56
- 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.
62
+ 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.
57
63
 
58
64
  </details>
59
65
 
@@ -65,7 +71,7 @@ Two entries over ONE per-session directory owned by `ModelDirectoryResolver` (`c
65
71
  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.
66
72
 
67
73
  - [ui-commands](../ui-commands/README.md) — the popupSelect shell the `/model` contribution registers into.
68
- - [ui-conversation](../ui-conversation/README.md) — declares the composer's `conversation.input.model` seat and the composer block.
74
+ - [ui-conversation](../ui-conversation/README.md) — declares the composer's `conversation.input.model` seat.
69
75
  - [dsh-agent-default-model](../../core/agent-default-model/README.md) — the default-model service for sessions that never choose.
70
76
  - [Client package map](../README.md) — adjacent browser UI packages.
71
77
 
@@ -84,7 +90,6 @@ Switching the route can reduce or invalidate provider-side cache reuse for subse
84
90
 
85
91
  <a id="known-limitations-and-deferred-work"></a>
86
92
 
87
-
88
93
  These limits define the current model surface. They are current package constraints, not a general model-router comparison or a task backlog.
89
94
 
90
95
  - **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
@@ -7,9 +7,11 @@ kind: "package-reference"
7
7
 
8
8
  [English](README.md) | 中文
9
9
 
10
+ 桌面端产品事件使用可选的[产品埋点服务](../product-analytics/README.zh.md),不包含普通 Web 交互。
11
+
10
12
  ## 概述
11
13
 
12
- Web GUI 允许用户通过 `/model` 弹窗或 composer 模型控件切换既有会话使用的模型与推理(reasoning)强度。两个界面呈现同一组按提供方分组的选择;所选模型决定可用的推理强度名称与默认值。完整选择从下一次请求开始生效;运行中的步骤保留其启动时的模型与推理强度。如果没有适配器可以服务会话路由,composer 会保持停用,直至路由恢复可用。
14
+ Web GUI 允许用户通过 `/model` 弹窗或 composer 模型控件切换既有会话使用的模型与推理(reasoning)强度。两个界面呈现同一组按提供方分组的选择;所选模型决定可用的推理强度名称与默认值。完整选择从下一次请求开始生效;运行中的步骤保留其启动时的模型与推理强度。所选模型不可用时,composer 保持停用,直到用户选择可用模型或同一模型恢复可用。
13
15
 
14
16
  ## 目录
15
17
 
@@ -25,21 +27,23 @@ Web GUI 允许用户通过 `/model` 弹窗或 composer 模型控件切换既有
25
27
  <a id="use-this-package"></a>
26
28
  ## 使用本包
27
29
 
28
- 与 `ui-conversation` 及命令包一起挂载本插件;composer 随即在待处理指示器旁显示模型位,`/model` 则以弹窗打开同一份目录。模型位菜单打开期间,`↑`/`↓` 在所显示面板的行间移动焦点,`Tab` 选定聚焦行,Escape 与 `Shift+Tab` 先退出已下钻的面板,否则关闭并回到触发器。下钻落在正在使用的那一行,返回则落在打开该面板的格子上。当确切提供方/模型对仍在已公布分组中时,两个界面都显示 Host 报告的当前选择;目录行缺席时,可路由的选择保持不变,触发器提示 `Select model`。
30
+ DeepSeek 账号和 API Key 路由显示为独立提供方分组,各自展示相同的已配置模型目录。
31
+
32
+ 未选模型文案与可用模型名使用相同的常规字重,并在既有会话与新会话中保留已保存的推理强度显示;调整推理强度需要先选择可用模型。点击未选模型控件直接打开模型列表,Escape 关闭列表。
33
+
34
+ 与 `ui-conversation` 及命令包一起挂载本插件;composer 随即在待处理指示器旁显示模型位,`/model` 则以弹窗打开同一份目录。模型位菜单打开期间,`↑`/`↓` 在所显示面板的行间移动焦点,`Tab` 选定聚焦行,Escape 与 `Shift+Tab` 先退出已下钻的面板,否则关闭并回到触发器。下钻落在正在使用的那一行,返回则落在打开该面板的格子上。所选模型可用时,composer 显示目录中的模型名称;模型或提供方被删除时(包括账号退登),改为显示已保存的 `provider/model` ID。保存的提供方、模型和推理强度均保持不变。
29
35
 
30
- 鼠标选择沿用浏览器原生点击及其取消行为;仅按下按钮不会选定。打开菜单时聚焦触发按钮,再次点击触发按钮会关闭菜单并把焦点还给它。等待选择结果时,焦点停在触发按钮;选择被拒绝后菜单保持打开,Tab 可回到当前选中的行。
36
+ 鼠标选择沿用浏览器原生点击及其取消行为;仅按下按钮不会选定。打开菜单时聚焦触发按钮,再次点击触发按钮会关闭菜单并把焦点还给它。等待任一入口发起的选择结果时,焦点停在触发按钮,触发按钮以加载图标代替下拉箭头,该选择包含的值所在行以加载图标代替勾选标记;选择被拒绝后菜单保持打开,Tab 可回到当前选中的行。
31
37
 
32
38
  ### 模型与推理强度
33
39
 
34
- 模型按提供方分组。composer 菜单只显示模型与推理强度名称。`/model` 弹窗显示提供方名称与目录说明;其中两个内置 DeepSeek 模型的说明使用当前语言,外部提供方说明保持原文。弹窗应用所选模型的默认推理强度;composer 随后可以选择任一已公布的推理强度。适配器没有推理元数据时不显示 Effort 行;不存在任意推理强度输入。
40
+ 模型按提供方分组。composer 菜单只显示模型与推理强度名称,DeepSeek 账号排第一,DeepSeek 排第二,第三方提供方保持目录原有顺序。导航箭头使用 `--dsw-alias-menu-icon` 文本色。`/model` 弹窗显示提供方名称与目录说明;其中两个内置 DeepSeek 模型的说明使用当前语言,外部提供方说明保持原文。弹窗应用所选模型的默认推理强度;composer 随后可以选择任一已公布的推理强度。适配器没有推理元数据时不显示 Effort 行;不存在任意推理强度输入。
35
41
 
36
42
  展开的控件无法排在同一行时,composer 将模型与推理强度文字替换为模型图标;空间足够后恢复文字。触发器的无障碍名称、提示和菜单仍提供完整选择。
37
43
 
38
44
  ### 不可路由的会话
39
45
 
40
- 当 Host 报告没有适配器服务该会话的路由时,本插件注册一个 composer 阻塞块,输入框随之停用并显示本插件自己的文案;恢复后无需重新加载即清除。首次加载之前或加载失败之后的 `null` 绝不阻断;目录成员关系同样不阻断——一条仍在服务、只是不公布该模型的路由不在分组里,却可用。
41
-
42
- 只有当前 Client binding 的目录可以发布对应的 composer 阻塞块。旧 binding 的清理保留后继目录的阻塞块;若尚无后继目录,则移除已失效的阻塞块。
46
+ 目录可用性不会阻止使用已保存选择发送消息;请求执行负责报告凭据缺失或模型不可用。刷新及刷新失败期间保留上次显示的选择和分组。Host 重置时清空这些显示。退登后选择器隐藏账号提供方,同时保留已保存的提供方/模型 ID 和推理强度。再次登录且该模型可用时恢复目录名称。既有会话日志保持不变。
43
47
 
44
48
  ### 选择失败
45
49
 
@@ -50,10 +54,12 @@ Web GUI 允许用户通过 `/model` 弹窗或 composer 模型控件切换既有
50
54
  <a id="understand-the-implementation"></a>
51
55
  ## 理解实现
52
56
 
57
+ 菜单采用共享 `MenuSurface` 材质,包括用于背景模糊的 macOS 底层;自定义内容遵循[菜单规则](../../../docs/web-styling.zh.md#component-rules)。
58
+
53
59
  <details>
54
60
  <summary>实现细节——点击展开</summary>
55
61
 
56
- 两个入口共用一份由 `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 事件上重拉。
62
+ 两个入口共用一份由 `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` 与凭据更新事件上重拉。
57
63
 
58
64
  </details>
59
65
 
@@ -65,7 +71,7 @@ Web GUI 允许用户通过 `/model` 弹窗或 composer 模型控件切换既有
65
71
  当仅了解模型界面还不够时,请阅读以下页面。这些页面从浏览器界面逐步深入到命令弹窗外壳与选择约定。
66
72
 
67
73
  - [ui-commands](../ui-commands/README.zh.md)——`/model` 贡献项注册进的 popupSelect 外壳。
68
- - [ui-conversation](../ui-conversation/README.zh.md)——声明 composer 的 `conversation.input.model` 位与 composer 阻塞块。
74
+ - [ui-conversation](../ui-conversation/README.zh.md)——声明 composer 的 `conversation.input.model` 位。
69
75
  - [dsh-agent-default-model](../../core/agent-default-model/README.zh.md)——为从未选择的会话提供默认模型的默认模型服务。
70
76
  - [客户端包映射](../README.zh.md)——相邻的浏览器 UI 包。
71
77
 
@@ -84,7 +90,6 @@ Web GUI 允许用户通过 `/model` 弹窗或 composer 模型控件切换既有
84
90
 
85
91
  <a id="known-limitations-and-deferred-work"></a>
86
92
 
87
-
88
93
  这些限制界定了当前模型选择界面。它们是当前包约束,不是通用模型路由器对比或任务积压。
89
94
 
90
95
  - **无创建期或已寻址 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
  }
@@ -160,6 +173,8 @@ window.__ModuleLoader__.load({
160
173
  available;
161
174
  catalog;
162
175
  projected;
176
+ isBlank;
177
+ track;
163
178
  /** The shared snapshot both entries render from (uSES-safe store). */
164
179
  store = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)({
165
180
  current: null,
@@ -167,12 +182,12 @@ window.__ModuleLoader__.load({
167
182
  groups: [],
168
183
  failures: [],
169
184
  status: "idle",
185
+ pending: null,
170
186
  error: null
171
187
  });
172
188
  /** Latest selection operation wins; an older response never overwrites a newer one. */
173
189
  generation = 0;
174
190
  disposed = false;
175
- resolved = false;
176
191
  unsubscribeCatalog;
177
192
  unsubscribeSelection;
178
193
  /**
@@ -181,13 +196,17 @@ window.__ModuleLoader__.load({
181
196
  * @param available - whether this session may use Agent-bound model RPCs.
182
197
  * @param catalog - Host-generation catalog shared by every Session.
183
198
  * @param projected - durable model selection projected from Session history.
199
+ * @param isBlank - whether this Session has no first message yet.
200
+ * @param track - desktop-only callback after a successful user selection.
184
201
  */
185
- constructor(sessions, sessionId, available, catalog, projected) {
202
+ constructor(sessions, sessionId, available, catalog, projected, isBlank, track) {
186
203
  this.sessions = sessions;
187
204
  this.sessionId = sessionId;
188
205
  this.available = available;
189
206
  this.catalog = catalog;
190
207
  this.projected = projected;
208
+ this.isBlank = isBlank;
209
+ this.track = track;
191
210
  this.unsubscribeCatalog = catalog.store.subscribe(() => {
192
211
  this.syncInputs();
193
212
  });
@@ -197,7 +216,7 @@ window.__ModuleLoader__.load({
197
216
  this.syncInputs();
198
217
  }
199
218
  /**
200
- * Ensure the Host generation's shared advisory catalog is loaded.
219
+ * Ensure the Host generation's shared available catalog is loaded.
201
220
  * @returns the fresh directory value.
202
221
  */
203
222
  async load() {
@@ -215,9 +234,13 @@ window.__ModuleLoader__.load({
215
234
  */
216
235
  async select(selection) {
217
236
  this.assertAvailable();
237
+ const previous = this.store.getSnapshot().current;
238
+ const previousEffort = previous?.reasoningEffort ?? (previous === null ? void 0 : this.catalog.reasoningFor(previous)?.defaultEffort);
239
+ const nextEffort = selection.reasoningEffort ?? this.catalog.reasoningFor(selection)?.defaultEffort;
218
240
  const generation = ++this.generation;
219
241
  this.store.update((s) => {
220
242
  s.status = "selecting";
243
+ s.pending = selection;
221
244
  s.error = null;
222
245
  });
223
246
  const result = await this.sessions.selectModel({
@@ -233,12 +256,29 @@ window.__ModuleLoader__.load({
233
256
  if (!result.ok) {
234
257
  this.store.update((s) => {
235
258
  s.status = "error";
259
+ s.pending = null;
236
260
  s.error = `${result.error.code}: ${result.error.message}`;
237
261
  });
238
262
  return result;
239
263
  }
264
+ if (previous !== null) {
265
+ const from = `${previous.provider}/${previous.model}`;
266
+ const to = `${selection.provider}/${selection.model}`;
267
+ if (from !== to) this.track?.("model_switch", {
268
+ ...this.isBlank() ? {} : { session_id: this.sessionId },
269
+ switch_from: from,
270
+ switch_to: to
271
+ });
272
+ if (from === to && previousEffort !== nextEffort) this.track?.("thinking_level_switch", {
273
+ ...this.isBlank() ? {} : { session_id: this.sessionId },
274
+ model_name: to,
275
+ switch_from: previousEffort ?? "default",
276
+ switch_to: nextEffort ?? "default"
277
+ });
278
+ }
240
279
  this.store.update((s) => {
241
280
  s.status = "ready";
281
+ s.pending = null;
242
282
  s.error = null;
243
283
  });
244
284
  this.syncInputs();
@@ -255,6 +295,7 @@ window.__ModuleLoader__.load({
255
295
  ++this.generation;
256
296
  this.store.update((state) => {
257
297
  if (state.status === "selecting") state.status = "idle";
298
+ state.pending = null;
258
299
  state.error = null;
259
300
  });
260
301
  this.syncInputs();
@@ -272,32 +313,33 @@ window.__ModuleLoader__.load({
272
313
  if (this.disposed) return;
273
314
  const catalog = this.catalog.store.getSnapshot();
274
315
  const projected = modelSelectionProjection(this.projected.getSnapshot());
316
+ const intended = projected?.next ?? catalog.value?.default;
317
+ const reasoning = intended === void 0 ? void 0 : this.catalog.reasoningFor(intended);
318
+ const effort = intended?.reasoningEffort ?? reasoning?.defaultEffort;
319
+ const retainedEffort = effort === void 0 ? void 0 : reasoning?.efforts.find((level) => level.id === effort)?.name ?? effort;
275
320
  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
321
  this.store.set({
284
- current: null,
322
+ current: catalog.value === null ? null : this.store.getSnapshot().current,
323
+ ...retainedEffort === void 0 ? {} : { retainedEffort },
285
324
  routable: null,
286
- groups: [],
287
- failures: [],
325
+ groups: catalog.value?.groups ?? [],
326
+ failures: catalog.value?.failures ?? [],
288
327
  status: catalog.status === "error" ? "error" : "loading",
328
+ pending: this.store.getSnapshot().pending,
289
329
  error: catalog.error
290
330
  });
291
331
  return;
292
332
  }
293
- const current = projected.next ?? catalog.value.default;
294
- this.resolved = true;
333
+ const selection = projected.next ?? catalog.value.default;
334
+ const routable = catalog.value.groups.some((group) => group.id === selection.provider && group.models.some((model) => model.id === selection.model));
295
335
  this.store.set({
296
- current,
297
- routable: catalog.value.routableProviders.includes(current.provider),
336
+ current: selection,
337
+ ...retainedEffort === void 0 ? {} : { retainedEffort },
338
+ routable,
298
339
  groups: catalog.value.groups,
299
340
  failures: catalog.value.failures,
300
341
  status: this.store.getSnapshot().status === "selecting" ? "selecting" : "ready",
342
+ pending: this.store.getSnapshot().pending,
301
343
  error: null
302
344
  });
303
345
  }
@@ -307,20 +349,6 @@ window.__ModuleLoader__.load({
307
349
  }
308
350
  //#endregion
309
351
  //#region lib/types/client/service.js
310
- /**
311
- * ModelDirectoryResolver (`ctx.modelDirectories`): the root owner of per-session
312
- * {@link ModelDirectory} instances. Both selection entries (the /model popup
313
- * and the composer model seat) resolve their session's directory through
314
- * this service, which is what makes the dual entry one shared state.
315
- *
316
- * Per-session storage follows the client service pattern (InputTriggerService /
317
- * CommandUiRuntime): a lazy service-internal map whose entry is deleted by the
318
- * owning scope's disposer. The host `dsh-scope` ScopedLayers registry does
319
- * does not belong here: it derives scope from the host carrier mechanism
320
- * (object-keyed), while client scopes tag contexts with branded SessionId
321
- * strings, and it models global+shadow named registries — this is a
322
- * per-session singleton with no global layer to merge.
323
- */
324
352
  /** The `ctx.modelDirectories` session model-selection service. */
325
353
  var ModelDirectoryResolver = class extends _deepseek_ai_cordis.Service {
326
354
  static inject = [
@@ -330,15 +358,11 @@ window.__ModuleLoader__.load({
330
358
  ];
331
359
  live = { directories: new WeakMapWithValues() };
332
360
  catalog;
333
- /** Localized composer-block copy; this plugin owns the string it raises. */
334
- blockReason;
335
361
  /**
336
362
  * @param ctx - owning root context (the service registers itself as `models`).
337
- * @param config - the bound translator for this plugin's own dictionary.
338
363
  */
339
- constructor(ctx, config) {
364
+ constructor(ctx) {
340
365
  super(ctx, "modelDirectories");
341
- this.blockReason = config.blockReason;
342
366
  this.catalog = new ModelCatalogDirectory(ctx);
343
367
  this.catalog.load().catch(() => {});
344
368
  ctx.on("connection/reset", () => {
@@ -351,6 +375,9 @@ window.__ModuleLoader__.load({
351
375
  ctx.remote.$on("settings/document-updated", () => {
352
376
  this.catalog.refresh();
353
377
  });
378
+ ctx.remote.$on("credentials/record-updated", () => {
379
+ this.catalog.refresh();
380
+ });
354
381
  ctx.remote.$on("credentials/reference-updated", () => {
355
382
  this.catalog.refresh();
356
383
  });
@@ -370,25 +397,8 @@ window.__ModuleLoader__.load({
370
397
  if (binding === void 0) throw new Error(`ui-model-selection: session "${String(sessionId)}" resolved no binding`);
371
398
  const existing = live.directories.get(binding);
372
399
  if (existing !== void 0) return existing;
373
- const directory = new ModelDirectory(this.ctx.remote.session, sessionId, () => sessions.subagentAddress(sessionId) === void 0, this.catalog, binding.session.projections.faceOf("modelSelection"));
400
+ const directory = new ModelDirectory(this.ctx.remote.session, sessionId, () => sessions.subagentAddress(sessionId) === void 0, this.catalog, binding.session.projections.faceOf("modelSelection"), () => binding.session.getSnapshot().blank, (name, attributes) => this.ctx.get("productAnalytics")?.track(name, attributes));
374
401
  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
402
  actx.effect(() => () => {
393
403
  directory.dispose();
394
404
  live.directories.delete(binding);
@@ -413,7 +423,7 @@ window.__ModuleLoader__.load({
413
423
  }
414
424
  //#endregion
415
425
  //#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}";
426
+ 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
427
  const tagId = "@deepseek-ai/dsh-client-ui-model-selection/ModelSelect.module.css";
418
428
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
419
429
  const tag = document.createElement("style");
@@ -468,7 +478,9 @@ window.__ModuleLoader__.load({
468
478
  * selected effort come from the Host rather than a client-owned vocabulary. A
469
479
  * rejected selection announces through the shared transient Toast anchored to
470
480
  * the composer card; the in-menu strip with Retry remains the catalog-load
471
- * surface.
481
+ * surface. While the directory's pending selection is unsettled, the trigger
482
+ * shows a spinner in place of its chevron, and each row whose value that
483
+ * selection carries shows one in place of its check mark.
472
484
  */
473
485
  /** Unplaced portal card: hidden but laid out at a fixed origin so offsetWidth/offsetHeight are real (Menu primitive's measure pass). */
474
486
  const MEASURE_STYLE = {
@@ -495,7 +507,8 @@ window.__ModuleLoader__.load({
495
507
  const [menuPos, setMenuPos] = (0, react.useState)(null);
496
508
  const itemRefs = (0, react.useRef)([]);
497
509
  const id = (0, react.useId)();
498
- const choices = (0, react.useMemo)(() => state.groups.flatMap((group) => group.models.map((model) => ({
510
+ 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]);
511
+ const choices = (0, react.useMemo)(() => groups.flatMap((group) => group.models.map((model) => ({
499
512
  group,
500
513
  model,
501
514
  selection: {
@@ -503,11 +516,11 @@ window.__ModuleLoader__.load({
503
516
  model: model.id,
504
517
  ...model.reasoning?.defaultEffort === void 0 ? {} : { reasoningEffort: model.reasoning.defaultEffort }
505
518
  }
506
- }))), [state.groups]);
519
+ }))), [groups]);
507
520
  const currentChoice = choices[state.current === null ? -1 : choices.findIndex((c) => c.selection.provider === state.current?.provider && c.selection.model === state.current.model)];
508
521
  const reasoning = currentChoice?.model.reasoning;
509
522
  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;
523
+ const effortLabel = reasoning === void 0 ? state.retainedEffort : effectiveEffort === void 0 ? t("effort.providerDefault") : reasoning.efforts.find((level) => level.id === effectiveEffort)?.name ?? effectiveEffort;
511
524
  const effortChoices = (0, react.useMemo)(() => reasoning === void 0 ? [] : [...reasoning.defaultEffort === void 0 ? [{
512
525
  key: "provider-default",
513
526
  effort: void 0,
@@ -517,7 +530,8 @@ window.__ModuleLoader__.load({
517
530
  effort: effort.id,
518
531
  label: effort.name
519
532
  }))], [reasoning, t]);
520
- const busy = state.status === "selecting";
533
+ const { pending } = state;
534
+ const busy = pending !== null;
521
535
  const reload = () => {
522
536
  lastActionRef.current = "load";
523
537
  load();
@@ -582,7 +596,8 @@ window.__ModuleLoader__.load({
582
596
  if (!available) return null;
583
597
  const show = () => {
584
598
  triggerRef.current?.focus();
585
- setPane("root");
599
+ if (state.current === null) paneFocus.current = "drill";
600
+ setPane(state.current === null ? "model" : "root");
586
601
  setOpen(true);
587
602
  reload();
588
603
  };
@@ -611,7 +626,7 @@ window.__ModuleLoader__.load({
611
626
  const onRootKeyDown = (event) => {
612
627
  if (event.key === "Escape" && open) {
613
628
  event.preventDefault();
614
- if (pane !== "root") back(pane);
629
+ if (pane !== "root" && state.current !== null) back(pane);
615
630
  else close(true);
616
631
  return;
617
632
  }
@@ -619,7 +634,7 @@ window.__ModuleLoader__.load({
619
634
  if (event.key === "Tab") {
620
635
  if (event.shiftKey) {
621
636
  event.preventDefault();
622
- if (pane !== "root") back(pane);
637
+ if (pane !== "root" && state.current !== null) back(pane);
623
638
  else close(true);
624
639
  return;
625
640
  }
@@ -714,6 +729,7 @@ window.__ModuleLoader__.load({
714
729
  "aria-expanded": open,
715
730
  "aria-controls": open ? `${id}-menu` : void 0,
716
731
  title: triggerLabel,
732
+ "aria-busy": busy,
717
733
  disabled: locked,
718
734
  onClick: () => {
719
735
  if (open) close(true);
@@ -732,10 +748,10 @@ window.__ModuleLoader__.load({
732
748
  className: ModelSelect_module_css_default.triggerEffort,
733
749
  children: effortLabel
734
750
  }),
735
- (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) })
751
+ 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) })
736
752
  ]
737
753
  }),
738
- open && (0, react_dom.createPortal)((0, react_jsx_runtime.jsxs)("div", {
754
+ open && (0, react_dom.createPortal)((0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.MenuSurface, {
739
755
  ref: menuRef,
740
756
  id: `${id}-menu`,
741
757
  className: ModelSelect_module_css_default.menu,
@@ -800,7 +816,7 @@ window.__ModuleLoader__.load({
800
816
  state.failures.map((failure) => (0, react_jsx_runtime.jsxs)("div", {
801
817
  className: ModelSelect_module_css_default.warning,
802
818
  children: [(0, react_jsx_runtime.jsx)("span", { children: t("warning.groupLoad", {
803
- name: failure.name,
819
+ name: failure.id === "deepseek-account" ? t("provider.account") : failure.name,
804
820
  message: failure.message
805
821
  }) }), (0, react_jsx_runtime.jsx)("button", {
806
822
  type: "button",
@@ -811,7 +827,7 @@ window.__ModuleLoader__.load({
811
827
  }, failure.id)),
812
828
  (0, react_jsx_runtime.jsx)("div", {
813
829
  className: clsx(ModelSelect_module_css_default.groups, "scrollable"),
814
- children: state.groups.map((group) => {
830
+ children: groups.map((group) => {
815
831
  const headingId = `${id}-${group.id}`;
816
832
  return (0, react_jsx_runtime.jsxs)("section", {
817
833
  role: "group",
@@ -820,7 +836,7 @@ window.__ModuleLoader__.load({
820
836
  children: [(0, react_jsx_runtime.jsx)("div", {
821
837
  className: ModelSelect_module_css_default.groupTitle,
822
838
  id: headingId,
823
- children: group.name
839
+ children: group.id === "deepseek-account" ? t("provider.account") : group.name
824
840
  }), group.models.map((model) => {
825
841
  const selected = state.current?.provider === group.id && state.current.model === model.id;
826
842
  return (0, react_jsx_runtime.jsxs)("button", {
@@ -845,7 +861,7 @@ window.__ModuleLoader__.load({
845
861
  })
846
862
  }), (0, react_jsx_runtime.jsx)("span", {
847
863
  className: ModelSelect_module_css_default.check,
848
- children: selected ? (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, {}) : null
864
+ 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
849
865
  })]
850
866
  }, model.id);
851
867
  })]
@@ -886,7 +902,7 @@ window.__ModuleLoader__.load({
886
902
  })
887
903
  }), (0, react_jsx_runtime.jsx)("span", {
888
904
  className: ModelSelect_module_css_default.check,
889
- children: effectiveEffort === level.effort ? (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, {}) : null
905
+ 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
890
906
  })]
891
907
  }, level.key))] })
892
908
  ]
@@ -915,14 +931,15 @@ window.__ModuleLoader__.load({
915
931
  */
916
932
  /** Simplified Chinese dictionary (the key-set source of truth). */
917
933
  const zh = {
934
+ "provider.account": "DeepSeek 账号",
918
935
  "command.label": "模型",
919
936
  "command.description": "选择本会话使用的模型",
920
937
  "option.loadError": "目录加载失败:{message}",
921
938
  "option.deepseekV4Flash.description": "快速、高效且经济;适合目标明确、常规或并行任务。",
922
939
  "option.deepseekV4Pro.description": "更强的自主编码、知识与复杂推理能力;适合复杂或质量优先的任务,但成本更高。",
923
- "trigger.fallback": "选择模型",
940
+ "trigger.fallback": "请选择模型",
924
941
  "trigger.loading": "正在加载模型…",
925
- "trigger.selectAria": "选择模型",
942
+ "trigger.selectAria": "请选择模型",
926
943
  "trigger.aria": "选择模型,当前 {model}",
927
944
  "trigger.ariaEffort": "选择模型,当前 {model},推理等级 {effort}",
928
945
  "menu.aria": "模型与推理等级",
@@ -935,11 +952,11 @@ window.__ModuleLoader__.load({
935
952
  "action.reload": "重新加载",
936
953
  "warning.groupLoad": "{name} 加载失败:{message}",
937
954
  "empty.models": "没有可用的模型。",
938
- "blocked.composer": "当前模型不可用,请先选择模型",
939
955
  "empty.efforts": "当前模型未提供推理等级。"
940
956
  };
941
957
  /** English dictionary, checked complete against the zh key set. */
942
958
  const en = {
959
+ "provider.account": "DeepSeek Account",
943
960
  "command.label": "Model",
944
961
  "command.description": "Select the model for this conversation",
945
962
  "option.loadError": "Catalog failed to load: {message}",
@@ -960,7 +977,6 @@ window.__ModuleLoader__.load({
960
977
  "action.reload": "Reload",
961
978
  "warning.groupLoad": "{name} failed to load: {message}",
962
979
  "empty.models": "No models available.",
963
- "blocked.composer": "This model is unavailable — select one to continue",
964
980
  "empty.efforts": "This model provides no reasoning effort levels."
965
981
  };
966
982
  //#endregion
@@ -970,6 +986,8 @@ window.__ModuleLoader__.load({
970
986
  return `${providerId}/${modelId}`;
971
987
  }
972
988
  const BUILTIN_DESCRIPTION_KEYS = {
989
+ "deepseek-account/deepseek-v4-flash": "option.deepseekV4Flash.description",
990
+ "deepseek-account/deepseek-v4-pro": "option.deepseekV4Pro.description",
973
991
  "deepseek-official/deepseek-v4-flash": "option.deepseekV4Flash.description",
974
992
  "deepseek-official/deepseek-v4-pro": "option.deepseekV4Pro.description"
975
993
  };
@@ -980,18 +998,21 @@ window.__ModuleLoader__.load({
980
998
  /** Flatten the directory into popup rows; failure rows are listed for visibility but never selectable. */
981
999
  function optionsOf(directory, t) {
982
1000
  const rows = [];
983
- for (const group of directory.groups) for (const model of group.models) {
984
- const description = descriptionOf(group.id, model, t);
985
- rows.push({
986
- id: rowId(group.id, model.id),
987
- label: model.name,
988
- detail: description !== void 0 ? `${group.name} · ${description}` : group.name,
989
- ...directory.current !== null && directory.current.provider === group.id && directory.current.model === model.id ? { active: true } : {}
990
- });
1001
+ for (const group of directory.groups) {
1002
+ const name = group.id === "deepseek-account" ? t("provider.account") : group.name;
1003
+ for (const model of group.models) {
1004
+ const description = descriptionOf(group.id, model, t);
1005
+ rows.push({
1006
+ id: rowId(group.id, model.id),
1007
+ label: model.name,
1008
+ detail: description !== void 0 ? `${name} · ${description}` : name,
1009
+ ...directory.current !== null && directory.current.provider === group.id && directory.current.model === model.id ? { active: true } : {}
1010
+ });
1011
+ }
991
1012
  }
992
1013
  for (const failure of directory.failures) rows.push({
993
1014
  id: `failure/${failure.id}`,
994
- label: failure.name,
1015
+ label: failure.id === "deepseek-account" ? t("provider.account") : failure.name,
995
1016
  detail: t("option.loadError", { message: failure.message })
996
1017
  });
997
1018
  return rows;
@@ -1037,7 +1058,7 @@ window.__ModuleLoader__.load({
1037
1058
  en
1038
1059
  }), "ui-model-selection: dictionaries");
1039
1060
  const t = ctx.locale.bind(NS);
1040
- ctx.plugin(ModelDirectoryResolver, { blockReason: () => t("blocked.composer") });
1061
+ ctx.plugin(ModelDirectoryResolver);
1041
1062
  ctx.inject(["commandUi", "modelDirectories"], (scope) => {
1042
1063
  const command = scope.get("commandUi");
1043
1064
  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
  /**
@@ -4,6 +4,7 @@
4
4
  * Session's durable selection projection, then submit through the same
5
5
  * selectModel call. A switch made in either entry updates this shared state.
6
6
  */
7
+ import type { TrackProductEvent } from '@deepseek-ai/dsh-client-product-analytics/client';
7
8
  import type { ModelCatalogFailure, ModelProviderGroup, ModelSelection } from '@deepseek-ai/dsh-api-session-controller/types';
8
9
  import type { SessionId } from '@deepseek-ai/dsh-api-remotes/client';
9
10
  import type { RemoteResult, TypertClientRemote } from '@deepseek-ai/dsh-typert-protocol';
@@ -11,15 +12,11 @@ import type { ObservableSnapshot, SnapshotStore } from '@deepseek-ai/dsh-client-
11
12
  import type { ModelCatalogDirectory } from './catalog.ts';
12
13
  /** Directory snapshot both entries render from. */
13
14
  export interface ModelDirectoryState {
14
- /** Effective selection: durable next-request projection, then Host default. */
15
+ /** Saved selection, retained even when its provider or model leaves the catalog. */
15
16
  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
- */
17
+ /** Saved effort caption retained when the selected model is unavailable. */
18
+ retainedEffort?: string;
19
+ /** Whether the current selection is present in the available catalog; null while unresolved. */
23
20
  routable: boolean | null;
24
21
  /** Successfully loaded provider groups (last good load). */
25
22
  groups: readonly ModelProviderGroup[];
@@ -27,6 +24,8 @@ export interface ModelDirectoryState {
27
24
  failures: readonly ModelCatalogFailure[];
28
25
  /** Lifecycle of the in-flight operation. */
29
26
  status: 'idle' | 'loading' | 'ready' | 'selecting' | 'error';
27
+ /** Selection submitted by the latest `select` until it settles; null otherwise. */
28
+ pending: ModelSelection | null;
30
29
  /** Whole-request or selection failure text; null when none. */
31
30
  error: string | null;
32
31
  }
@@ -37,12 +36,13 @@ export declare class ModelDirectory {
37
36
  private readonly available;
38
37
  private readonly catalog;
39
38
  private readonly projected;
39
+ private readonly isBlank;
40
+ private readonly track?;
40
41
  /** The shared snapshot both entries render from (uSES-safe store). */
41
42
  readonly store: SnapshotStore<ModelDirectoryState>;
42
43
  /** Latest selection operation wins; an older response never overwrites a newer one. */
43
44
  private generation;
44
45
  private disposed;
45
- private resolved;
46
46
  private readonly unsubscribeCatalog;
47
47
  private readonly unsubscribeSelection;
48
48
  /**
@@ -51,10 +51,12 @@ export declare class ModelDirectory {
51
51
  * @param available - whether this session may use Agent-bound model RPCs.
52
52
  * @param catalog - Host-generation catalog shared by every Session.
53
53
  * @param projected - durable model selection projected from Session history.
54
+ * @param isBlank - whether this Session has no first message yet.
55
+ * @param track - desktop-only callback after a successful user selection.
54
56
  */
55
- constructor(sessions: Pick<TypertClientRemote['session'], 'selectModel'>, sessionId: SessionId, available: () => boolean, catalog: ModelCatalogDirectory, projected: ObservableSnapshot<unknown>);
57
+ constructor(sessions: Pick<TypertClientRemote['session'], 'selectModel'>, sessionId: SessionId, available: () => boolean, catalog: ModelCatalogDirectory, projected: ObservableSnapshot<unknown>, isBlank: () => boolean, track?: TrackProductEvent | undefined);
56
58
  /**
57
- * Ensure the Host generation's shared advisory catalog is loaded.
59
+ * Ensure the Host generation's shared available catalog is loaded.
58
60
  * @returns the fresh directory value.
59
61
  */
60
62
  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
@@ -1,17 +1,3 @@
1
- /**
2
- * ModelDirectoryResolver (`ctx.modelDirectories`): the root owner of per-session
3
- * {@link ModelDirectory} instances. Both selection entries (the /model popup
4
- * and the composer model seat) resolve their session's directory through
5
- * this service, which is what makes the dual entry one shared state.
6
- *
7
- * Per-session storage follows the client service pattern (InputTriggerService /
8
- * CommandUiRuntime): a lazy service-internal map whose entry is deleted by the
9
- * owning scope's disposer. The host `dsh-scope` ScopedLayers registry does
10
- * does not belong here: it derives scope from the host carrier mechanism
11
- * (object-keyed), while client scopes tag contexts with branded SessionId
12
- * strings, and it models global+shadow named registries — this is a
13
- * per-session singleton with no global layer to merge.
14
- */
15
1
  import { Service } from '@deepseek-ai/cordis';
16
2
  import type { Context } from '@deepseek-ai/cordis';
17
3
  import type { SessionId } from '@deepseek-ai/dsh-session/types';
@@ -26,15 +12,10 @@ export declare class ModelDirectoryResolver extends Service {
26
12
  static inject: string[];
27
13
  private readonly live;
28
14
  private readonly catalog;
29
- /** Localized composer-block copy; this plugin owns the string it raises. */
30
- private readonly blockReason;
31
15
  /**
32
16
  * @param ctx - owning root context (the service registers itself as `models`).
33
- * @param config - the bound translator for this plugin's own dictionary.
34
17
  */
35
- constructor(ctx: Context, config: {
36
- blockReason: () => string;
37
- });
18
+ constructor(ctx: Context);
38
19
  /**
39
20
  * Resolve the per-session shared directory (lazy; the scope disposer
40
21
  * 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-rc.1",
4
+ "version": "0.2.0-rc.1",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -46,22 +46,23 @@
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-session-controller": "0.1.7-rc.1",
50
- "@deepseek-ai/dsh-client-locale": "0.1.7-rc.1",
51
- "@deepseek-ai/dsh-client-test-runtime": "0.1.7-rc.1",
52
- "@deepseek-ai/dsh-client-ui-commands": "0.1.7-rc.1",
53
- "@deepseek-ai/dsh-client-ui-conversation": "0.1.7-rc.1",
54
- "@deepseek-ai/dsh-api-remotes": "0.1.7-rc.1",
55
- "@deepseek-ai/dsh-client-ui-input-trigger": "0.1.7-rc.1",
56
- "@deepseek-ai/dsh-client-ui-slots": "0.1.7-rc.1",
57
- "@deepseek-ai/dsh-typert-protocol": "0.1.7-rc.1",
58
- "@deepseek-ai/dsh-client-ui-primitives": "0.1.7-rc.1",
49
+ "@deepseek-ai/dsh-api-remotes": "0.2.0-rc.1",
50
+ "@deepseek-ai/dsh-api-session-controller": "0.2.0-rc.1",
51
+ "@deepseek-ai/dsh-client-locale": "0.2.0-rc.1",
52
+ "@deepseek-ai/dsh-client-test-runtime": "0.2.0-rc.1",
53
+ "@deepseek-ai/dsh-client-ui-conversation": "0.2.0-rc.1",
54
+ "@deepseek-ai/dsh-client-ui-commands": "0.2.0-rc.1",
55
+ "@deepseek-ai/dsh-client-ui-primitives": "0.2.0-rc.1",
56
+ "@deepseek-ai/dsh-client-ui-slots": "0.2.0-rc.1",
57
+ "@deepseek-ai/dsh-typert-protocol": "0.2.0-rc.1",
59
58
  "@deepseek-ai/cordis": "~4.0.4",
60
- "@deepseek-ai/dsh-client-store": "0.1.7-rc.1",
61
- "@deepseek-ai/dsh-util-values": "0.1.7-rc.1",
62
- "@deepseek-ai/dsh-client-ui-renderer": "0.1.7-rc.1",
63
- "@deepseek-ai/dsh-client-ui-session": "0.1.7-rc.1",
64
- "@deepseek-ai/dsh-session": "0.1.7-rc.1"
59
+ "@deepseek-ai/dsh-client-store": "0.2.0-rc.1",
60
+ "@deepseek-ai/dsh-session": "0.2.0-rc.1",
61
+ "@deepseek-ai/dsh-util-values": "0.2.0-rc.1",
62
+ "@deepseek-ai/dsh-client-ui-input-trigger": "0.2.0-rc.1",
63
+ "@deepseek-ai/dsh-client-ui-renderer": "0.2.0-rc.1",
64
+ "@deepseek-ai/dsh-client-ui-session": "0.2.0-rc.1",
65
+ "@deepseek-ai/dsh-client-product-analytics": "0.2.0-rc.1"
65
66
  },
66
67
  "files": [
67
68
  "lib/index.js",