@deepseek-ai/dsh-client-ui-model-selection 0.2.0-rc.1 → 0.2.0-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
@@ -15,11 +15,11 @@
15
15
  en: d152484eb41ac6b4
16
16
  zh: 09388d293f9be9cb
17
17
  /deepseek-ai-dsh-client-ui-model-selection/use-this-package:
18
- en: e5a1df3a4dfe5041
19
- zh: 6269e73026b32a33
18
+ en: 62235f8c8c9e1e7e
19
+ zh: c18dc9c38ef9d4f9
20
20
  /deepseek-ai-dsh-client-ui-model-selection/use-this-package/model-and-effort:
21
- en: e38d0195677a7b69
22
- zh: 4eea8a9b1664afd9
21
+ en: 48f8157ffdfe6e69
22
+ zh: 64ec9a533e2aa8f4
23
23
  /deepseek-ai-dsh-client-ui-model-selection/use-this-package/unroutable-sessions:
24
24
  en: 7f20428c747ff052
25
25
  zh: 975e6de728e02fa0
@@ -27,8 +27,8 @@
27
27
  en: 261bbf42296243af
28
28
  zh: 72779fb7b8fbc473
29
29
  /deepseek-ai-dsh-client-ui-model-selection/understand-the-implementation:
30
- en: 493088e8de6be91c
31
- zh: 9ecee4605b4dbad4
30
+ en: f3d38594439eab3d
31
+ zh: f255974e61696dca
32
32
  /deepseek-ai-dsh-client-ui-model-selection/further-exploration:
33
33
  en: 14acd4692e923480
34
34
  zh: ff89aed852aa0da8
package/README.md CHANGED
@@ -31,13 +31,17 @@ DeepSeek account and API-key routes appear as separate provider groups, each exp
31
31
 
32
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
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.
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 through root and effort rows, or move the model-list highlight without leaving search. Enter and Tab settle the highlighted model or focused row; Escape and `Shift+Tab` leave a drilled pane first and otherwise close back to the trigger. Drilling focuses model search when shown, otherwise the current model or effort row, 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.
35
35
 
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.
36
+ Mouse selection uses native browser clicks, including their cancellation behavior; a press alone never selects. Opening the root menu focuses its trigger, and clicking the trigger again closes the menu and returns focus there. Root rows and the search-clear button show the same fill for keyboard focus and hover, without a native outline. Selecting a model or effort returns focus to the trigger without a focus ring; leaving the trigger or reopening the menu restores its normal focus indication. 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. Tab returns to model search with the current model highlighted when search is shown, otherwise to the current row.
37
37
 
38
38
  ### Model and effort
39
39
 
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.
40
+ The button's model menu shows search only when its full catalog contains more than four models; smaller catalogs focus the current model or first row and support keyboard navigation directly. The `/model` command always keeps its search field. Both search fields match model names case-insensitively, including nonconsecutive characters in order; leading and trailing spaces are ignored. Within each provider, prefix matches come first, followed by alignment score and then catalog order. Empty groups are hidden. Empty catalogs and queries without matches are announced through a status region. Search keeps focus while arrow keys cycle the highlighted result across provider groups; Enter or Tab selects it. Left and right arrows keep their native caret behavior. Opening highlights the current model or the first available row, and editing the query resets the highlight to the first result. Reopening the model pane clears the query.
41
+
42
+ Model and reasoning-effort names use weight 400 (regular) in the composer menu, including the selected item. The search field follows the command popup's compact treatment with transparent background and border in both palettes, with no leading icon and a caption-tone placeholder. The clear button appears for a nonempty query and restores the full list with focus in the search field.
43
+
44
+ Both entries group models by provider, with DeepSeek Account first and DeepSeek second; third-party providers retain their catalog order. Both use the shared, asynchronously observed sticky headings from [ui-primitives](../ui-primitives/README.md#understand-the-implementation): transparent at rest, with the theme's 94%-opaque fill only while pinned, and `md` corners outside macOS Desktop. The composer menu shows model and effort names only. Navigation chevrons use `--dsw-alias-menu-icon`. The `/model` popup shows provider names as group headings and model names as rows, without repeating the provider on each row or showing catalog descriptions. Its search placeholder, no-match text, and empty-catalog text use the same localized labels as the composer model menu. 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.
41
45
 
42
46
  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.
43
47
 
@@ -59,6 +63,8 @@ Menus use the shared `MenuSurface` material, including the macOS backing for bac
59
63
  <details>
60
64
  <summary>Implementation internals — click to expand</summary>
61
65
 
66
+ The composer `ModelSelect` and `/model` option builder share [provider ordering](src/client/provider-order.ts), while both searches use `rankByName` within each provider. The command supplies optional groups and `searchMode: 'fuzzy-label'` through the [popupSelect API](../ui-commands/README.md#use-this-package); both entries use `MenuGroup` and rebuild its sticky observer when the rendered groups change. The command popup fills the composer overlay, while the button retains its compact menu.
67
+
62
68
  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.
63
69
 
64
70
  </details>
package/README.zh.md CHANGED
@@ -31,13 +31,17 @@ DeepSeek 账号和 API Key 路由显示为独立提供方分组,各自展示
31
31
 
32
32
  未选模型文案与可用模型名使用相同的常规字重,并在既有会话与新会话中保留已保存的推理强度显示;调整推理强度需要先选择可用模型。点击未选模型控件直接打开模型列表,Escape 关闭列表。
33
33
 
34
- 与 `ui-conversation` 及命令包一起挂载本插件;composer 随即在待处理指示器旁显示模型位,`/model` 则以弹窗打开同一份目录。模型位菜单打开期间,`↑`/`↓` 在所显示面板的行间移动焦点,`Tab` 选定聚焦行,Escape 与 `Shift+Tab` 先退出已下钻的面板,否则关闭并回到触发器。下钻落在正在使用的那一行,返回则落在打开该面板的格子上。所选模型可用时,composer 显示目录中的模型名称;模型或提供方被删除时(包括账号退登),改为显示已保存的 `provider/model` ID。保存的提供方、模型和推理强度均保持不变。
34
+ 与 `ui-conversation` 及命令包一起挂载本插件;composer 随即在待处理指示器旁显示模型位,`/model` 则以弹窗打开同一份目录。模型位菜单打开期间,`↑`/`↓` 在根菜单和推理等级行间移动焦点,或在模型列表中移动高亮并让焦点保留在搜索框。Enter 和 Tab 选定高亮模型或聚焦行;Escape 与 `Shift+Tab` 先退出已下钻的面板,否则关闭并回到触发器。下钻时若显示模型搜索框则聚焦搜索框,否则聚焦当前模型或推理强度行,返回则落在打开该面板的格子上。所选模型可用时,composer 显示目录中的模型名称;模型或提供方被删除时(包括账号退登),改为显示已保存的 `provider/model` ID。保存的提供方、模型和推理强度均保持不变。
35
35
 
36
- 鼠标选择沿用浏览器原生点击及其取消行为;仅按下按钮不会选定。打开菜单时聚焦触发按钮,再次点击触发按钮会关闭菜单并把焦点还给它。等待任一入口发起的选择结果时,焦点停在触发按钮,触发按钮以加载图标代替下拉箭头,该选择包含的值所在行以加载图标代替勾选标记;选择被拒绝后菜单保持打开,Tab 可回到当前选中的行。
36
+ 鼠标选择沿用浏览器原生点击及其取消行为;仅按下按钮不会选定。打开根菜单时聚焦触发按钮,再次点击触发按钮会关闭菜单并把焦点还给它。根菜单行和搜索清除按钮的键盘焦点与悬停使用相同底色,不显示原生焦点轮廓。选定模型或推理等级后,焦点返回触发按钮但不显示焦点环;焦点离开按钮或重新打开菜单后恢复正常焦点提示。等待任一入口发起的选择结果时,焦点停在触发按钮,触发按钮以加载图标代替下拉箭头,该选择包含的值所在行以加载图标代替勾选标记;选择被拒绝后菜单保持打开。显示模型搜索框时,Tab 返回搜索框并高亮当前模型,否则返回当前选中行。
37
37
 
38
38
  ### 模型与推理强度
39
39
 
40
- 模型按提供方分组。composer 菜单只显示模型与推理强度名称,DeepSeek 账号排第一,DeepSeek 排第二,第三方提供方保持目录原有顺序。导航箭头使用 `--dsw-alias-menu-icon` 文本色。`/model` 弹窗显示提供方名称与目录说明;其中两个内置 DeepSeek 模型的说明使用当前语言,外部提供方说明保持原文。弹窗应用所选模型的默认推理强度;composer 随后可以选择任一已公布的推理强度。适配器没有推理元数据时不显示 Effort 行;不存在任意推理强度输入。
40
+ 按钮模型菜单仅在完整目录超过四个模型时显示搜索框;较小目录聚焦当前模型或首行,直接支持键盘导航。`/model` 命令始终保留搜索框。两个搜索框均按模型名称进行不区分大小写的匹配,支持按顺序输入不连续字符,并忽略首尾空格。每个提供方内的结果优先按前缀匹配、其次按匹配得分、最后按目录顺序排列。没有匹配项的分组会隐藏。空目录和无匹配结果均通过状态区域通知屏幕阅读器。方向键在各提供方分组的结果间循环移动高亮,焦点保留在搜索框;Enter 或 Tab 选定高亮项。左右方向键仍用于移动输入光标。打开时高亮当前模型或首个可用选项,修改搜索内容后高亮首个结果。重新打开模型面板会清空搜索内容。
41
+
42
+ composer 菜单中的模型名称和推理等级均使用 400(regular)字重,包括选中项。搜索框沿用 command 弹窗的紧凑样式,在浅/深色主题下均保持背景和边框透明,不显示前置图标,占位文字使用 caption 色。搜索内容非空时显示清除按钮;点击后恢复完整列表,并将焦点留在搜索框。
43
+
44
+ 两个入口均按提供方分组,DeepSeek 账号排第一,DeepSeek 排第二,第三方提供方保持目录原有顺序。两者采用 [ui-primitives](../ui-primitives/README.zh.md#understand-the-implementation) 异步观察的共享吸顶标题:原位透明,仅吸顶时使用主题的 94% 不透明填充,macOS 桌面端以外使用 `md` 圆角。composer 菜单只显示模型与推理强度名称。导航箭头使用 `--dsw-alias-menu-icon` 文本色。`/model` 弹窗以提供方名称作为分组标题、模型名称作为选项行,不在每行重复提供方,也不显示目录说明。搜索占位文字、无匹配提示和空目录提示与 composer 模型菜单共用同一组本地化文案。弹窗应用所选模型的默认推理强度;composer 随后可以选择任一已公布的推理强度。适配器没有推理元数据时不显示 Effort 行;不存在任意推理强度输入。
41
45
 
42
46
  展开的控件无法排在同一行时,composer 将模型与推理强度文字替换为模型图标;空间足够后恢复文字。触发器的无障碍名称、提示和菜单仍提供完整选择。
43
47
 
@@ -59,6 +63,8 @@ DeepSeek 账号和 API Key 路由显示为独立提供方分组,各自展示
59
63
  <details>
60
64
  <summary>实现细节——点击展开</summary>
61
65
 
66
+ composer 的 `ModelSelect` 与 `/model` 选项构建器共用[提供方排序](src/client/provider-order.ts),两个搜索框均在每个提供方内使用 `rankByName`。命令通过 [popupSelect API](../ui-commands/README.zh.md#use-this-package) 提供可选分组与 `searchMode: 'fuzzy-label'`;两个入口均使用 `MenuGroup`,并在渲染分组变化时重建其吸顶观察器。命令弹窗撑满 composer 浮层,按钮则保留紧凑菜单。
67
+
62
68
  两个入口共用一份由 `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` 与凭据更新事件上重拉。
63
69
 
64
70
  </details>
package/lib/client.js CHANGED
@@ -423,7 +423,7 @@ window.__ModuleLoader__.load({
423
423
  }
424
424
  //#endregion
425
425
  //#region \0dsh-css:/home/runner/work/deepseek-harness/deepseek-harness/packages/client/ui-model-selection/src/client/ModelSelect.module.css.mjs
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}";
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:not([data-selection-focus]){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_searchRow{flex-shrink:0;margin:2px 0 3px;position:relative}._7KE1Ra_searchRow ._7KE1Ra_search{border-radius:var(--dsw-radius-md);background:0 0;border:0 solid #0000;height:auto;padding:5px 7px;display:flex}._7KE1Ra_searchRow ._7KE1Ra_searchWithQuery{padding-right:34px}._7KE1Ra_searchClear{corner-shape:round;width:24px;height:24px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:50%;justify-content:center;align-items:center;padding:0;display:inline-flex;position:absolute;top:50%;right:4px;transform:translateY(-50%)}._7KE1Ra_searchClear:hover,._7KE1Ra_searchClear:focus-visible{background:var(--dsw-alias-interactive-bg-hover);outline:none}._7KE1Ra_searchRow ._7KE1Ra_search:focus-within{border-color:#0000}._7KE1Ra_searchRow ._7KE1Ra_search input{padding:0;font-size:12px;line-height:normal}._7KE1Ra_searchRow ._7KE1Ra_search input::placeholder{color:var(--dsw-alias-label-caption)}._7KE1Ra_groups{min-height:0;overflow-y:auto}._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:not(._7KE1Ra_modelOption):hover:not(:disabled),._7KE1Ra_option:focus-visible,._7KE1Ra_optionActive:not(:disabled){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:400;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;outline:none;align-items:center;gap:6px;padding:0 8px;font-size:13px;line-height:20px;display:flex}._7KE1Ra_cell:hover,._7KE1Ra_cell:focus-visible{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}";
427
427
  const tagId = "@deepseek-ai/dsh-client-ui-model-selection/ModelSelect.module.css";
428
428
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
429
429
  const tag = document.createElement("style");
@@ -442,15 +442,19 @@ window.__ModuleLoader__.load({
442
442
  "chevronOpen": "_7KE1Ra_chevronOpen",
443
443
  "empty": "_7KE1Ra_empty",
444
444
  "error": "_7KE1Ra_error",
445
- "group": "_7KE1Ra_group",
446
- "groupTitle": "_7KE1Ra_groupTitle",
447
445
  "groups": "_7KE1Ra_groups",
448
446
  "menu": "_7KE1Ra_menu",
449
447
  "modelName": "_7KE1Ra_modelName",
448
+ "modelOption": "_7KE1Ra_modelOption",
450
449
  "option": "_7KE1Ra_option",
450
+ "optionActive": "_7KE1Ra_optionActive",
451
451
  "optionCopy": "_7KE1Ra_optionCopy",
452
452
  "retry": "_7KE1Ra_retry",
453
453
  "root": "_7KE1Ra_root",
454
+ "search": "_7KE1Ra_search",
455
+ "searchClear": "_7KE1Ra_searchClear",
456
+ "searchRow": "_7KE1Ra_searchRow",
457
+ "searchWithQuery": "_7KE1Ra_searchWithQuery",
454
458
  "selected": "_7KE1Ra_selected",
455
459
  "status": "_7KE1Ra_status",
456
460
  "trigger": "_7KE1Ra_trigger",
@@ -460,6 +464,17 @@ window.__ModuleLoader__.load({
460
464
  "warning": "_7KE1Ra_warning"
461
465
  };
462
466
  //#endregion
467
+ //#region lib/types/client/provider-order.js
468
+ /** Shared provider display order for the composer and command model pickers. */
469
+ /**
470
+ * Put the account and official providers first, preserving every other relative order.
471
+ * @param groups - Provider groups in catalog order.
472
+ * @returns a sorted copy; model order within each group is unchanged.
473
+ */
474
+ function orderModelProviders(groups) {
475
+ return 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));
476
+ }
477
+ //#endregion
463
478
  //#region lib/types/client/ModelSelect.js
464
479
  /**
465
480
  * ModelSelect: the composer's named model seat (`conversation.input.model`).
@@ -468,19 +483,25 @@ window.__ModuleLoader__.load({
468
483
  * each drilling into its own list — the provider-grouped model list over
469
484
  * the shared directory, and the effort levels. The trigger (313:14108's
470
485
  * ToggleButton) shows both: model name + effort in the caption tone.
471
- * While open, ↑/↓ move focus across the rows of the shown pane (wrapping; a
472
- * step taken while the trigger still holds focus enters at the near end), Tab
473
- * settles like Enter, and Escape and Shift+Tab leave a drilled pane first and
474
- * otherwise close back to the trigger. A drilled pane hands focus to the row
475
- * of the value in use, and returning to the root pane hands it back to the
476
- * cell that opened it. Data and submission ride the SAME per-session
477
- * ModelDirectory as the /model popup; exact-model reasoning metadata and the
478
- * selected effort come from the Host rather than a client-owned vocabulary. A
479
- * rejected selection announces through the shared transient Toast anchored to
480
- * the composer card; the in-menu strip with Retry remains the catalog-load
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.
486
+ * Model catalogs above four entries show search, which retains focus while
487
+ * ↑/↓ cycle the highlighted result; Enter and Tab accept it. Smaller model
488
+ * catalogs, root panes, and effort panes move focus between rows. Escape and Shift+Tab leave a drilled pane first and otherwise close
489
+ * back to the trigger. A drilled pane focuses the current effort or model
490
+ * search field. Provider headings paint their background only while pinned
491
+ * by scrolling. Clearing a query restores the full list and search focus.
492
+ * Selecting restores trigger focus without a ring until the trigger loses focus
493
+ * or the menu reopens. Model names match a case-insensitive ordered subsequence
494
+ * within each provider group, ranked by
495
+ * prefix, alignment score, then catalog order. Returning to the root pane
496
+ * hands focus back to the cell that opened it. Data and submission ride the
497
+ * same per-session ModelDirectory as the /model popup; exact-model reasoning
498
+ * metadata and the selected effort come from the Host rather than a
499
+ * client-owned vocabulary. A rejected selection announces through the shared
500
+ * transient Toast anchored to the composer card; the in-menu strip with
501
+ * Retry remains the catalog-load surface. While the directory's pending
502
+ * selection is unsettled, the trigger shows a spinner in place of its
503
+ * chevron, and each row whose value that selection carries shows one in place
504
+ * of its check mark.
484
505
  */
485
506
  /** Unplaced portal card: hidden but laid out at a fixed origin so offsetWidth/offsetHeight are real (Menu primitive's measure pass). */
486
507
  const MEASURE_STYLE = {
@@ -498,16 +519,21 @@ window.__ModuleLoader__.load({
498
519
  const state = (0, react.useSyncExternalStore)((fn) => directory.subscribe(fn), () => directory.getSnapshot());
499
520
  const [open, setOpen] = (0, react.useState)(false);
500
521
  const [pane, setPane] = (0, react.useState)("root");
522
+ const [query, setQuery] = (0, react.useState)("");
523
+ const [highlightedIndex, setHighlightedIndex] = (0, react.useState)(null);
524
+ const [selectionFocus, setSelectionFocus] = (0, react.useState)(false);
501
525
  const lastActionRef = (0, react.useRef)("load");
502
526
  const [toast, setToast] = (0, react.useState)(null);
503
527
  const toastSeq = (0, react.useRef)(0);
504
528
  const rootRef = (0, react.useRef)(null);
505
529
  const triggerRef = (0, react.useRef)(null);
530
+ const searchRef = (0, react.useRef)(null);
506
531
  const menuRef = (0, react.useRef)(null);
532
+ const groupsRef = (0, react.useRef)(null);
507
533
  const [menuPos, setMenuPos] = (0, react.useState)(null);
508
534
  const itemRefs = (0, react.useRef)([]);
509
535
  const id = (0, react.useId)();
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]);
536
+ const groups = (0, react.useMemo)(() => orderModelProviders(state.groups), [state.groups]);
511
537
  const choices = (0, react.useMemo)(() => groups.flatMap((group) => group.models.map((model) => ({
512
538
  group,
513
539
  model,
@@ -517,6 +543,21 @@ window.__ModuleLoader__.load({
517
543
  ...model.reasoning?.defaultEffort === void 0 ? {} : { reasoningEffort: model.reasoning.defaultEffort }
518
544
  }
519
545
  }))), [groups]);
546
+ const showSearch = choices.length > 4;
547
+ const filteredGroups = (0, react.useMemo)(() => groups.map((group) => ({
548
+ ...group,
549
+ models: (0, _deepseek_ai_dsh_client_ui_primitives.rankByName)(group.models, showSearch ? query.trim() : "")
550
+ })).filter((group) => group.models.length > 0), [
551
+ groups,
552
+ query,
553
+ showSearch
554
+ ]);
555
+ const visibleModels = (0, react.useMemo)(() => filteredGroups.flatMap((group) => group.models.map((model) => ({
556
+ provider: group.id,
557
+ model: model.id
558
+ }))), [filteredGroups]);
559
+ const currentVisibleIndex = visibleModels.findIndex((model) => model.provider === state.current?.provider && model.model === state.current.model);
560
+ const activeModelIndex = Math.min(highlightedIndex ?? Math.max(0, currentVisibleIndex), visibleModels.length - 1);
520
561
  const currentChoice = choices[state.current === null ? -1 : choices.findIndex((c) => c.selection.provider === state.current?.provider && c.selection.model === state.current.model)];
521
562
  const reasoning = currentChoice?.model.reasoning;
522
563
  const effectiveEffort = state.current?.reasoningEffort ?? reasoning?.defaultEffort;
@@ -548,18 +589,53 @@ window.__ModuleLoader__.load({
548
589
  document.removeEventListener("mousedown", closeOutside);
549
590
  };
550
591
  }, [open]);
592
+ (0, react.useLayoutEffect)(() => {
593
+ if (!showSearch) {
594
+ setQuery("");
595
+ setHighlightedIndex(null);
596
+ }
597
+ }, [showSearch]);
551
598
  const paneFocus = (0, react.useRef)(null);
599
+ const previousShowSearch = (0, react.useRef)(showSearch);
552
600
  (0, react.useEffect)(() => {
553
- const intent = paneFocus.current;
601
+ const changedSearchMode = previousShowSearch.current !== showSearch;
602
+ previousShowSearch.current = showSearch;
603
+ const intent = paneFocus.current ?? (changedSearchMode && pane === "model" ? "drill" : null);
554
604
  paneFocus.current = null;
555
605
  if (!open || intent === null) return;
556
606
  if (intent === "drill") {
607
+ if (pane === "model" && showSearch) {
608
+ searchRef.current?.focus();
609
+ return;
610
+ }
557
611
  (menuRef.current?.querySelector("[role=\"menuitemradio\"][aria-checked=\"true\"]:not([disabled])") ?? itemRefs.current.find((item) => item !== null && !item.disabled) ?? triggerRef.current)?.focus();
558
612
  return;
559
613
  }
560
614
  const cell = itemRefs.current[intent === "effort" ? 1 : 0];
561
615
  (cell !== null && cell !== void 0 && !cell.disabled ? cell : triggerRef.current)?.focus();
562
- }, [open, pane]);
616
+ }, [
617
+ open,
618
+ pane,
619
+ showSearch
620
+ ]);
621
+ (0, react.useEffect)(() => {
622
+ const viewport = groupsRef.current;
623
+ if (viewport === null) return;
624
+ return (0, _deepseek_ai_dsh_client_ui_primitives.observeStickyMenuGroups)(viewport);
625
+ }, [
626
+ available,
627
+ open,
628
+ pane,
629
+ filteredGroups
630
+ ]);
631
+ (0, react.useLayoutEffect)(() => {
632
+ if (open && pane === "model" && activeModelIndex >= 0) itemRefs.current[activeModelIndex]?.scrollIntoView({ block: "nearest" });
633
+ }, [
634
+ open,
635
+ pane,
636
+ activeModelIndex,
637
+ visibleModels
638
+ ]);
563
639
  (0, react.useLayoutEffect)(() => {
564
640
  if (!open) {
565
641
  setMenuPos(null);
@@ -591,16 +667,24 @@ window.__ModuleLoader__.load({
591
667
  }, [
592
668
  open,
593
669
  pane,
594
- state
670
+ state,
671
+ query
595
672
  ]);
596
673
  if (!available) return null;
597
674
  const show = () => {
675
+ setSelectionFocus(false);
598
676
  triggerRef.current?.focus();
677
+ setQuery("");
678
+ setHighlightedIndex(null);
599
679
  if (state.current === null) paneFocus.current = "drill";
600
680
  setPane(state.current === null ? "model" : "root");
601
681
  setOpen(true);
602
682
  reload();
603
683
  };
684
+ const changeQuery = (next) => {
685
+ setQuery(next);
686
+ setHighlightedIndex(0);
687
+ };
604
688
  const close = (restoreFocus = false) => {
605
689
  setOpen(false);
606
690
  setPane("root");
@@ -608,7 +692,13 @@ window.__ModuleLoader__.load({
608
692
  triggerRef.current?.focus();
609
693
  });
610
694
  };
695
+ const closeAfterSelection = () => {
696
+ setSelectionFocus(true);
697
+ close(true);
698
+ };
611
699
  const drill = (next) => {
700
+ setQuery("");
701
+ setHighlightedIndex(null);
612
702
  paneFocus.current = "drill";
613
703
  setPane(next);
614
704
  };
@@ -624,6 +714,7 @@ window.__ModuleLoader__.load({
624
714
  items[active === -1 ? offset > 0 ? 0 : items.length - 1 : (active + offset + items.length) % items.length]?.focus();
625
715
  };
626
716
  const onRootKeyDown = (event) => {
717
+ if (event.nativeEvent.isComposing) return;
627
718
  if (event.key === "Escape" && open) {
628
719
  event.preventDefault();
629
720
  if (pane !== "root" && state.current !== null) back(pane);
@@ -631,6 +722,21 @@ window.__ModuleLoader__.load({
631
722
  return;
632
723
  }
633
724
  if (!open) return;
725
+ if (pane === "model" && showSearch && (event.key === "ArrowDown" || event.key === "ArrowUp")) {
726
+ event.preventDefault();
727
+ if (!busy && visibleModels.length > 0) {
728
+ setHighlightedIndex((activeModelIndex + (event.key === "ArrowDown" ? 1 : -1) + visibleModels.length) % visibleModels.length);
729
+ searchRef.current?.focus();
730
+ }
731
+ return;
732
+ }
733
+ if (pane === "model" && showSearch && event.target instanceof HTMLInputElement && (event.key === "Enter" || event.key === "Tab" && !event.shiftKey)) {
734
+ if (event.key === "Tab" && visibleModels.length === 0) return;
735
+ event.preventDefault();
736
+ const highlighted = visibleModels[activeModelIndex];
737
+ if (!busy && highlighted !== void 0) choose(highlighted);
738
+ return;
739
+ }
634
740
  if (event.key === "Tab") {
635
741
  if (event.shiftKey) {
636
742
  event.preventDefault();
@@ -647,6 +753,11 @@ window.__ModuleLoader__.load({
647
753
  }
648
754
  if (focused !== triggerRef.current) return;
649
755
  event.preventDefault();
756
+ if (pane === "model" && showSearch) {
757
+ setHighlightedIndex(null);
758
+ searchRef.current?.focus();
759
+ return;
760
+ }
650
761
  (menuRef.current?.querySelector("[role=\"menuitemradio\"][aria-checked=\"true\"]:not([disabled])") ?? rows.find((item) => !item.disabled))?.focus();
651
762
  return;
652
763
  }
@@ -662,7 +773,7 @@ window.__ModuleLoader__.load({
662
773
  const settleSelection = (result) => {
663
774
  if (result === void 0) return;
664
775
  if (result.ok) {
665
- if (rootRef.current !== null) close(true);
776
+ if (rootRef.current !== null) closeAfterSelection();
666
777
  return;
667
778
  }
668
779
  const { error } = result;
@@ -674,12 +785,13 @@ window.__ModuleLoader__.load({
674
785
  };
675
786
  const submit = (selection) => {
676
787
  lastActionRef.current = "select";
788
+ setSelectionFocus(true);
677
789
  triggerRef.current?.focus();
678
790
  select(selection).then(settleSelection);
679
791
  };
680
792
  const choose = (selection) => {
681
793
  if (state.current?.provider === selection.provider && state.current.model === selection.model) {
682
- close(true);
794
+ closeAfterSelection();
683
795
  return;
684
796
  }
685
797
  submit(selection);
@@ -687,7 +799,7 @@ window.__ModuleLoader__.load({
687
799
  const chooseEffort = (effort) => {
688
800
  if (state.current === null) return;
689
801
  if (effectiveEffort === effort) {
690
- close(true);
802
+ closeAfterSelection();
691
803
  return;
692
804
  }
693
805
  submit({
@@ -705,6 +817,7 @@ window.__ModuleLoader__.load({
705
817
  });
706
818
  itemRefs.current = [];
707
819
  let itemIndex = 0;
820
+ let modelIndex = 0;
708
821
  const itemRef = () => {
709
822
  const at = itemIndex++;
710
823
  return (node) => {
@@ -730,6 +843,10 @@ window.__ModuleLoader__.load({
730
843
  "aria-controls": open ? `${id}-menu` : void 0,
731
844
  title: triggerLabel,
732
845
  "aria-busy": busy,
846
+ "data-selection-focus": selectionFocus ? "" : void 0,
847
+ onBlur: () => {
848
+ setSelectionFocus(false);
849
+ },
733
850
  disabled: locked,
734
851
  onClick: () => {
735
852
  if (open) close(true);
@@ -756,7 +873,7 @@ window.__ModuleLoader__.load({
756
873
  id: `${id}-menu`,
757
874
  className: ModelSelect_module_css_default.menu,
758
875
  style: menuPos ?? MEASURE_STYLE,
759
- role: "menu",
876
+ role: pane === "model" ? "group" : "menu",
760
877
  "aria-label": t("menu.aria"),
761
878
  "aria-busy": state.status === "loading" || busy,
762
879
  children: [
@@ -800,6 +917,34 @@ window.__ModuleLoader__.load({
800
917
  ]
801
918
  })] }),
802
919
  pane === "model" && (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
920
+ showSearch && (0, react_jsx_runtime.jsxs)("div", {
921
+ className: ModelSelect_module_css_default.searchRow,
922
+ children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
923
+ ref: searchRef,
924
+ className: clsx(ModelSelect_module_css_default.search, query !== "" && ModelSelect_module_css_default.searchWithQuery),
925
+ type: "text",
926
+ role: "searchbox",
927
+ "aria-label": t("search.placeholder"),
928
+ "aria-controls": `${id}-models`,
929
+ "aria-activedescendant": activeModelIndex < 0 ? void 0 : `${id}-model-${activeModelIndex}`,
930
+ placeholder: t("search.placeholder"),
931
+ value: query,
932
+ readOnly: busy,
933
+ onChange: (event) => {
934
+ changeQuery(event.target.value);
935
+ }
936
+ }), query !== "" && (0, react_jsx_runtime.jsx)("button", {
937
+ type: "button",
938
+ className: ModelSelect_module_css_default.searchClear,
939
+ "aria-label": t("search.clear"),
940
+ disabled: busy,
941
+ onClick: () => {
942
+ changeQuery("");
943
+ searchRef.current?.focus();
944
+ },
945
+ children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseFillRegular, {})
946
+ })]
947
+ }),
803
948
  state.status === "loading" && (0, react_jsx_runtime.jsx)("div", {
804
949
  className: ModelSelect_module_css_default.status,
805
950
  children: t("status.loading")
@@ -826,25 +971,34 @@ window.__ModuleLoader__.load({
826
971
  })]
827
972
  }, failure.id)),
828
973
  (0, react_jsx_runtime.jsx)("div", {
974
+ ref: groupsRef,
975
+ id: `${id}-models`,
829
976
  className: clsx(ModelSelect_module_css_default.groups, "scrollable"),
830
- children: groups.map((group) => {
831
- const headingId = `${id}-${group.id}`;
832
- return (0, react_jsx_runtime.jsxs)("section", {
833
- role: "group",
834
- "aria-labelledby": headingId,
835
- className: ModelSelect_module_css_default.group,
836
- children: [(0, react_jsx_runtime.jsx)("div", {
837
- className: ModelSelect_module_css_default.groupTitle,
838
- id: headingId,
839
- children: group.id === "deepseek-account" ? t("provider.account") : group.name
840
- }), group.models.map((model) => {
977
+ role: "menu",
978
+ "aria-label": t("menu.model"),
979
+ hidden: filteredGroups.length === 0,
980
+ children: filteredGroups.map((group) => {
981
+ return (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MenuGroup, {
982
+ label: group.id === "deepseek-account" ? t("provider.account") : group.name,
983
+ children: group.models.map((model) => {
984
+ const index = modelIndex++;
841
985
  const selected = state.current?.provider === group.id && state.current.model === model.id;
842
986
  return (0, react_jsx_runtime.jsxs)("button", {
843
987
  ref: itemRef(),
844
988
  type: "button",
845
989
  role: "menuitemradio",
846
990
  "aria-checked": selected,
847
- className: clsx(ModelSelect_module_css_default.option, selected && ModelSelect_module_css_default.selected),
991
+ id: `${id}-model-${index}`,
992
+ tabIndex: showSearch ? -1 : 0,
993
+ onFocus: () => {
994
+ setHighlightedIndex(index);
995
+ },
996
+ "data-highlighted": index === activeModelIndex ? "" : void 0,
997
+ className: clsx(ModelSelect_module_css_default.option, ModelSelect_module_css_default.modelOption, selected && ModelSelect_module_css_default.selected, index === activeModelIndex && ModelSelect_module_css_default.optionActive),
998
+ onMouseMove: busy || index === activeModelIndex ? void 0 : () => {
999
+ if (showSearch) setHighlightedIndex(index);
1000
+ else itemRefs.current[index]?.focus();
1001
+ },
848
1002
  title: model.name,
849
1003
  disabled: busy,
850
1004
  onClick: () => {
@@ -864,13 +1018,14 @@ window.__ModuleLoader__.load({
864
1018
  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
865
1019
  })]
866
1020
  }, model.id);
867
- })]
1021
+ })
868
1022
  }, group.id);
869
1023
  })
870
1024
  }),
871
- state.status === "ready" && choices.length === 0 && (0, react_jsx_runtime.jsx)("div", {
1025
+ state.status === "ready" && filteredGroups.length === 0 && (0, react_jsx_runtime.jsx)("div", {
872
1026
  className: ModelSelect_module_css_default.empty,
873
- children: t("empty.models")
1027
+ role: "status",
1028
+ children: t(choices.length === 0 ? "empty.models" : "search.empty")
874
1029
  })
875
1030
  ] }),
876
1031
  pane === "effort" && (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [state.error !== null && lastActionRef.current === "load" && (0, react_jsx_runtime.jsxs)("div", {
@@ -935,8 +1090,6 @@ window.__ModuleLoader__.load({
935
1090
  "command.label": "模型",
936
1091
  "command.description": "选择本会话使用的模型",
937
1092
  "option.loadError": "目录加载失败:{message}",
938
- "option.deepseekV4Flash.description": "快速、高效且经济;适合目标明确、常规或并行任务。",
939
- "option.deepseekV4Pro.description": "更强的自主编码、知识与复杂推理能力;适合复杂或质量优先的任务,但成本更高。",
940
1093
  "trigger.fallback": "请选择模型",
941
1094
  "trigger.loading": "正在加载模型…",
942
1095
  "trigger.selectAria": "请选择模型",
@@ -951,6 +1104,9 @@ window.__ModuleLoader__.load({
951
1104
  "error.sessionInUse": "当前会话已被占用,可能是其他正在运行的 DSH 导致的(如其他 dsh web、桌面端),请退出其他正在运行的 DSH 后重试。",
952
1105
  "action.reload": "重新加载",
953
1106
  "warning.groupLoad": "{name} 加载失败:{message}",
1107
+ "search.placeholder": "搜索模型…",
1108
+ "search.clear": "清除搜索",
1109
+ "search.empty": "没有匹配的模型。",
954
1110
  "empty.models": "没有可用的模型。",
955
1111
  "empty.efforts": "当前模型未提供推理等级。"
956
1112
  };
@@ -960,8 +1116,6 @@ window.__ModuleLoader__.load({
960
1116
  "command.label": "Model",
961
1117
  "command.description": "Select the model for this conversation",
962
1118
  "option.loadError": "Catalog failed to load: {message}",
963
- "option.deepseekV4Flash.description": "Fast, efficient, and economical; suited to focused, routine, or parallel tasks.",
964
- "option.deepseekV4Pro.description": "Stronger agentic coding, knowledge, and difficult reasoning; suited to complex or quality-critical tasks at higher cost.",
965
1119
  "trigger.fallback": "Select model",
966
1120
  "trigger.loading": "Loading models…",
967
1121
  "trigger.selectAria": "Select model",
@@ -976,6 +1130,9 @@ window.__ModuleLoader__.load({
976
1130
  "error.sessionInUse": "This session is already in use, possibly by another running DSH instance (such as dsh web or the desktop app). Quit other running DSH instances and try again.",
977
1131
  "action.reload": "Reload",
978
1132
  "warning.groupLoad": "{name} failed to load: {message}",
1133
+ "search.placeholder": "Search models…",
1134
+ "search.clear": "Clear search",
1135
+ "search.empty": "No matching models.",
979
1136
  "empty.models": "No models available.",
980
1137
  "empty.efforts": "This model provides no reasoning effort levels."
981
1138
  };
@@ -985,30 +1142,20 @@ window.__ModuleLoader__.load({
985
1142
  function rowId(providerId, modelId) {
986
1143
  return `${providerId}/${modelId}`;
987
1144
  }
988
- const BUILTIN_DESCRIPTION_KEYS = {
989
- "deepseek-account/deepseek-v4-flash": "option.deepseekV4Flash.description",
990
- "deepseek-account/deepseek-v4-pro": "option.deepseekV4Pro.description",
991
- "deepseek-official/deepseek-v4-flash": "option.deepseekV4Flash.description",
992
- "deepseek-official/deepseek-v4-pro": "option.deepseekV4Pro.description"
993
- };
994
- function descriptionOf(providerId, model, t) {
995
- const key = BUILTIN_DESCRIPTION_KEYS[rowId(providerId, model.id)];
996
- return key !== void 0 && model.description === en[key] ? t(key) : model.description;
997
- }
998
1145
  /** Flatten the directory into popup rows; failure rows are listed for visibility but never selectable. */
999
1146
  function optionsOf(directory, t) {
1000
1147
  const rows = [];
1001
- for (const group of directory.groups) {
1148
+ for (const group of orderModelProviders(directory.groups)) {
1002
1149
  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
- }
1150
+ for (const model of group.models) rows.push({
1151
+ id: rowId(group.id, model.id),
1152
+ label: model.name,
1153
+ group: {
1154
+ name: group.id,
1155
+ label: name
1156
+ },
1157
+ ...directory.current !== null && directory.current.provider === group.id && directory.current.model === model.id ? { active: true } : {}
1158
+ });
1012
1159
  }
1013
1160
  for (const failure of directory.failures) rows.push({
1014
1161
  id: `failure/${failure.id}`,
@@ -1071,6 +1218,12 @@ window.__ModuleLoader__.load({
1071
1218
  available: (session) => sessions.subagentAddress(session.sessionId) === void 0,
1072
1219
  ui: {
1073
1220
  kind: "popupSelect",
1221
+ searchMode: "fuzzy-label",
1222
+ searchLabels: () => ({
1223
+ placeholder: t("search.placeholder"),
1224
+ empty: t("empty.models"),
1225
+ noResults: t("search.empty")
1226
+ }),
1074
1227
  options: async (session) => {
1075
1228
  if (sessions.subagentAddress(session.sessionId) !== void 0) throw new Error("model selection is unavailable for addressed subagent sessions");
1076
1229
  return optionsOf(await models.directoryFor(session.sessionId).load(), t);
@@ -13,8 +13,6 @@ export declare const zh: {
13
13
  'command.label': string;
14
14
  'command.description': string;
15
15
  'option.loadError': string;
16
- 'option.deepseekV4Flash.description': string;
17
- 'option.deepseekV4Pro.description': string;
18
16
  'trigger.fallback': string;
19
17
  'trigger.loading': string;
20
18
  'trigger.selectAria': string;
@@ -29,6 +27,9 @@ export declare const zh: {
29
27
  'error.sessionInUse': string;
30
28
  'action.reload': string;
31
29
  'warning.groupLoad': string;
30
+ 'search.placeholder': string;
31
+ 'search.clear': string;
32
+ 'search.empty': string;
32
33
  'empty.models': string;
33
34
  'empty.efforts': string;
34
35
  };
@@ -40,8 +41,6 @@ export declare const en: {
40
41
  'command.label': string;
41
42
  'command.description': string;
42
43
  'option.loadError': string;
43
- 'option.deepseekV4Flash.description': string;
44
- 'option.deepseekV4Pro.description': string;
45
44
  'trigger.fallback': string;
46
45
  'trigger.loading': string;
47
46
  'trigger.selectAria': string;
@@ -56,6 +55,9 @@ export declare const en: {
56
55
  'error.sessionInUse': string;
57
56
  'action.reload': string;
58
57
  'warning.groupLoad': string;
58
+ 'search.placeholder': string;
59
+ 'search.clear': string;
60
+ 'search.empty': string;
59
61
  'empty.models': string;
60
62
  'empty.efforts': string;
61
63
  };
@@ -0,0 +1,10 @@
1
+ /** Shared provider display order for the composer and command model pickers. */
2
+ /**
3
+ * Put the account and official providers first, preserving every other relative order.
4
+ * @param groups - Provider groups in catalog order.
5
+ * @returns a sorted copy; model order within each group is unchanged.
6
+ */
7
+ export declare function orderModelProviders<T extends {
8
+ readonly id: string;
9
+ }>(groups: readonly T[]): T[];
10
+ //# sourceMappingURL=provider-order.d.ts.map
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.2.0-rc.1",
4
+ "version": "0.2.0-rc.2",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -46,23 +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-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",
49
+ "@deepseek-ai/dsh-api-session-controller": "0.2.0-rc.2",
50
+ "@deepseek-ai/dsh-client-locale": "0.2.0-rc.2",
51
+ "@deepseek-ai/dsh-api-remotes": "0.2.0-rc.2",
52
+ "@deepseek-ai/dsh-client-ui-commands": "0.2.0-rc.2",
53
+ "@deepseek-ai/dsh-client-test-runtime": "0.2.0-rc.2",
54
+ "@deepseek-ai/dsh-client-ui-input-trigger": "0.2.0-rc.2",
55
+ "@deepseek-ai/dsh-client-ui-slots": "0.2.0-rc.2",
56
+ "@deepseek-ai/dsh-typert-protocol": "0.2.0-rc.2",
58
57
  "@deepseek-ai/cordis": "~4.0.4",
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"
58
+ "@deepseek-ai/dsh-client-store": "0.2.0-rc.2",
59
+ "@deepseek-ai/dsh-client-ui-primitives": "0.2.0-rc.2",
60
+ "@deepseek-ai/dsh-client-ui-conversation": "0.2.0-rc.2",
61
+ "@deepseek-ai/dsh-session": "0.2.0-rc.2",
62
+ "@deepseek-ai/dsh-util-values": "0.2.0-rc.2",
63
+ "@deepseek-ai/dsh-client-ui-renderer": "0.2.0-rc.2",
64
+ "@deepseek-ai/dsh-client-ui-session": "0.2.0-rc.2",
65
+ "@deepseek-ai/dsh-client-product-analytics": "0.2.0-rc.2"
66
66
  },
67
67
  "files": [
68
68
  "lib/index.js",