@lynn123411/dsh-model-submenu 0.3.3 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,14 +8,16 @@
8
8
  - **思考等级就地切换**:一行分段按钮,模型配了几档就显示几档,放不下时这一行横向滚动;点一下只改选中态,菜单关闭时才写入,没有改动就不写。模型不提供思考等级时整行隐藏。
9
9
  - **弹窗列全部模型**:顶部搜索框(模型名与供应商名都搜)、每个有模型的供应商一个标签页、每行下面一行元信息(供应商、上下文窗口、思考、图片输入)。点模型立即切换;星标只切换收藏,不选中、不关弹窗。
10
10
  - **收藏按目录顺序靠前**:收藏表存在浏览器本地,整台浏览器共用一张;目录里消失的模型保留收藏,回来时还在。
11
+ - **目录角标照常显示**:模型目录给模型声明角标时,触发器与两处列表都按目录给的文案与色调画出来,与宿主座位一致。
11
12
  - **失败与加载都有交代**:加载中一行文字、加载失败一行文字带「重新加载」、提交被拒弹出与内置同款的提示气泡;点模型被拒时小菜单保持打开,关闭菜单时被拒则界面回退到提交前。
12
13
 
13
14
  ## 特性
14
15
 
15
16
  - **两层各管一件事**:小菜单只服务当前供应商的常用模型与思考等级,换供应商、跨供应商找模型都走「更多模型…」打开的弹窗;宿主自带的 `/model` 命令保持原样。
16
17
  - **紧凑态照宿主**:输入框放不下一行时只剩图标,跟随宿主给 `conversation.input.model` 的两个显示变量。
18
+ - **定位与进行中反馈照宿主**:菜单定位用宿主的 `useAnchoredPosition`,随 visualViewport、移动键盘与桌面边框自动避让;提交中触发器显示进行中的状态点,弹窗在移动端不自动聚焦搜索框。
17
19
  - **元信息只写有把握的**:上下文窗口与图片输入读宿主设置页那份共享缓存(`ctx.configForms.describe()`):上下文窗口只认模型自己声明的值,图片输入在模型没写时按该供应商的默认模态判断;判不出来的与明确不支持的都整段略去,既不写「未知」也不写「不支持」。工具调用在宿主里没有对应字段,一律不写。
18
- - **接管内置座位**:以 `priority: -100` 占据 `conversation.input.model`,外观沿用内置弹层与 `--dsw-*` 主题变量;形态与装配见 [DETAILS.md](./DETAILS.md)。
20
+ - **接管内置座位**:以 `priority: -100` 占据 `conversation.input.model`,外观沿用内置弹层与 `--dsw-*` 主题变量。座位下的 `conversation.input.model.label` 子座位归声明它的注册项所有,接管方不能再声明它,因此第三方往该子座位写的名称/角标扩展在接管期间不会显示;形态与装配见 [DETAILS.md](./DETAILS.md)。
19
21
 
20
22
  ## 安装
21
23
 
package/lib/client.js CHANGED
@@ -39,7 +39,7 @@ window.__ModuleLoader__.load({
39
39
  }
40
40
  //#endregion
41
41
  //#region \0dsh-css:/Users/tny/Desktop/work/my-dsh/plugins/dsh-model-submenu/src/client/ModelSubmenu.module.css.mjs
42
- const css = ".FOsK3a_root{min-width:0;display:flex;position:relative}.FOsK3a_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}.FOsK3a_trigger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.FOsK3a_trigger:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-border-l3)}.FOsK3a_trigger:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}.FOsK3a_triggerLabel{text-overflow:ellipsis;white-space:nowrap;min-width:0;display:var(--dsh-composer-model-text-display,block);overflow:hidden}.FOsK3a_triggerEffort{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-state-business-primary);display:var(--dsh-composer-model-text-display,block);flex-shrink:1000;overflow:hidden}.FOsK3a_triggerIcon{display:var(--dsh-composer-model-icon-display,none);flex:none}.FOsK3a_chevron{color:var(--dsw-alias-label-caption);flex:none;transition:transform .12s}.FOsK3a_chevronOpen{transform:rotate(180deg)}.FOsK3a_menu{z-index:1100;--dsw-elevation-stroke-color:var(--dsw-alias-border-l1);box-sizing:border-box;width:min(252px,100vw - 24px);max-height:min(70vh,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}.FOsK3a_providerName{text-overflow:ellipsis;white-space:nowrap;min-height:24px;color:var(--dsw-alias-label-tertiary);align-items:center;padding:3px 8px;font-size:12px;font-weight:500;line-height:18px;display:flex;overflow:hidden}.FOsK3a_groups{min-height:0;overflow-y:auto}.FOsK3a_row{align-items:center;gap:2px;display:flex}.FOsK3a_rowPick{box-sizing:border-box;width:100%;min-width:0;min-height:34px;color:inherit;text-align:left;cursor:pointer;padding-inline:6px;background:0 0;border:none;border-radius:10px;outline:none;flex:1;align-items:center;gap:4px;padding:5px 7px;display:flex}.FOsK3a_rowPick:hover:not(:disabled),.FOsK3a_rowPick:focus-visible{background:var(--dsw-alias-interactive-bg-hover)}.FOsK3a_rowPick:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}.FOsK3a_rowCopy{flex-direction:column;flex:1;min-width:0;display:flex}.FOsK3a_rowName{color:inherit;text-overflow:ellipsis;white-space:nowrap;font-size:11.5px;font-weight:400;line-height:17px;overflow:hidden}.FOsK3a_menu .FOsK3a_rowName{font-weight:600}.FOsK3a_rowMeta{color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;font-size:12px;line-height:18px;overflow:hidden}.FOsK3a_check{color:var(--dsw-alias-label-primary);flex:0 0 15px;place-items:center;display:grid}.FOsK3a_starStatic{color:var(--dsw-alias-state-warn-primary);flex:none;place-items:center;width:14px;display:grid}.FOsK3a_starButton{color:var(--dsw-alias-label-tertiary);cursor:pointer;opacity:0;background:0 0;border:none;border-radius:8px;outline:none;flex:none;place-items:center;width:26px;height:26px;display:grid}.FOsK3a_starButton:focus-visible{outline:2px solid var(--dsw-alias-border-l3)}.FOsK3a_row:hover .FOsK3a_starButton,.FOsK3a_row:focus-within .FOsK3a_starButton{opacity:1}.FOsK3a_starButton[aria-pressed=true]{color:var(--dsw-alias-state-warn-primary);opacity:1}@media (hover:none){.FOsK3a_starButton{opacity:1}}.FOsK3a_star{flex:none}.FOsK3a_divider{background:var(--dsw-alias-border-l2);flex:none;height:.5px;margin:3px 2px}.FOsK3a_effortRow{align-items:center;gap:6px;padding:3px 6px;display:flex}.FOsK3a_effortHeading{color:var(--dsw-alias-label-primary);flex:none;font-size:12px;line-height:18px}.FOsK3a_effortTrack{background:var(--dsw-alias-interactive-bg-hover);scrollbar-width:none;border-radius:999px;align-items:center;gap:1px;min-width:0;margin-left:auto;padding:2px;display:flex;overflow-x:auto}.FOsK3a_effortTrack::-webkit-scrollbar{display:none}.FOsK3a_effortSegment{color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:999px;outline:none;flex:none;padding:2px 7px;font-size:11px;line-height:16px}.FOsK3a_effortSegment:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.FOsK3a_effortSegment[aria-checked=true]{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1);box-shadow:var(--dsw-elevation-soft);font-weight:600}.FOsK3a_effortSegment:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}.FOsK3a_effortSegment:focus-visible{outline:2px solid var(--dsw-alias-border-l3);outline-offset:-2px}.FOsK3a_hint{color:var(--dsw-alias-label-tertiary);margin:0;padding:6px 8px;font-size:11px;line-height:16px}.FOsK3a_moreRow{color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:10px;outline:none;align-items:center;gap:6px;min-height:32px;padding:5px 8px;font-size:12px;line-height:18px;display:flex}.FOsK3a_moreRow:hover,.FOsK3a_moreRow:focus-visible{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.FOsK3a_moreIcon{color:var(--dsw-alias-label-tertiary);flex:none}.FOsK3a_notice{color:var(--dsw-alias-label-tertiary);justify-content:space-between;align-items:flex-start;gap:8px;padding:6px 10px;font-size:12px;line-height:18px;display:flex}.FOsK3a_retry{color:inherit;font:inherit;cursor:pointer;background:0 0;border:none;flex:none;padding:0;font-weight:600;text-decoration:underline}.FOsK3a_pickerDialog{gap:0;width:min(680px,100%);height:min(640px,80vh);padding:0}.FOsK3a_pickerContent,.FOsK3a_pickerContent>:last-child{flex-direction:column;flex:1;min-height:0;display:flex}.FOsK3a_pickerSearch{padding:0 20px 12px}.FOsK3a_pickerSearchInput{width:100%}.FOsK3a_pickerTabs{border-bottom:.5px solid var(--dsw-alias-border-l2);overscroll-behavior-x:contain;scrollbar-width:none;flex-wrap:nowrap;gap:2px;padding:0 16px;display:flex;overflow-x:auto}.FOsK3a_pickerTabs::-webkit-scrollbar{display:none}.FOsK3a_pickerTab{color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;background:0 0;border:none;border-radius:8px;outline:none;flex:none;padding:6px 10px;font-size:13px;line-height:18px}.FOsK3a_pickerTab:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.FOsK3a_pickerTab[aria-selected=true]{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover);font-weight:600}.FOsK3a_pickerTab:focus-visible{outline:2px solid var(--dsw-alias-border-l3)}.FOsK3a_pickerList{flex:1;min-height:0;padding:8px 12px;overflow-y:auto}.FOsK3a_pickerFooter{border-top:.5px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary);align-items:center;gap:6px;padding:10px 20px;font-size:12px;line-height:18px;display:flex}.FOsK3a_footerHint{flex-wrap:wrap;align-items:center;gap:6px;display:flex}.FOsK3a_footerDot{color:var(--dsw-alias-label-dimmed)}";
42
+ const css = ".FOsK3a_root{min-width:0;display:flex;position:relative}.FOsK3a_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}.FOsK3a_trigger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.FOsK3a_trigger:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-border-l3)}.FOsK3a_trigger:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}.FOsK3a_triggerLabel{text-overflow:ellipsis;white-space:nowrap;min-width:0;display:var(--dsh-composer-model-text-display,block);overflow:hidden}.FOsK3a_triggerEffort{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-state-business-primary);display:var(--dsh-composer-model-text-display,block);flex-shrink:1000;overflow:hidden}.FOsK3a_triggerIcon{display:var(--dsh-composer-model-icon-display,none);flex:none}.FOsK3a_triggerBadge{flex:none;margin-inline-end:4px}.FOsK3a_rowBadge{flex:none;margin-inline-start:6px}.FOsK3a_chevron{color:var(--dsw-alias-label-caption);flex:none;transition:transform .12s}.FOsK3a_chevronOpen{transform:rotate(180deg)}.FOsK3a_menu{z-index:1100;--dsw-elevation-stroke-color:var(--dsw-alias-border-l1);box-sizing:border-box;width:min(252px,100vw - 24px);max-height:var(--dsh-anchored-max-height,min(70vh, calc(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}.FOsK3a_providerName{text-overflow:ellipsis;white-space:nowrap;min-height:24px;color:var(--dsw-alias-label-tertiary);align-items:center;padding:3px 8px;font-size:12px;font-weight:500;line-height:18px;display:flex;overflow:hidden}.FOsK3a_groups{min-height:0;overflow-y:auto}.FOsK3a_row{align-items:center;gap:2px;display:flex}.FOsK3a_rowPick{box-sizing:border-box;width:100%;min-width:0;min-height:34px;color:inherit;text-align:left;cursor:pointer;padding-inline:6px;background:0 0;border:none;border-radius:10px;outline:none;flex:1;align-items:center;gap:4px;padding:5px 7px;display:flex}.FOsK3a_rowPick:hover:not(:disabled),.FOsK3a_rowPick:focus-visible{background:var(--dsw-alias-interactive-bg-hover)}.FOsK3a_rowPick:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}.FOsK3a_rowCopy{flex-direction:column;flex:1;min-width:0;display:flex}.FOsK3a_rowName{color:inherit;text-overflow:ellipsis;white-space:nowrap;font-size:11.5px;font-weight:400;line-height:17px;overflow:hidden}.FOsK3a_menu .FOsK3a_rowName{font-weight:600}.FOsK3a_rowMeta{color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;font-size:12px;line-height:18px;overflow:hidden}.FOsK3a_check{color:var(--dsw-alias-label-primary);flex:0 0 15px;place-items:center;display:grid}.FOsK3a_starStatic{color:var(--dsw-alias-state-warn-primary);flex:none;place-items:center;width:14px;display:grid}.FOsK3a_starButton{color:var(--dsw-alias-label-tertiary);cursor:pointer;opacity:0;background:0 0;border:none;border-radius:8px;outline:none;flex:none;place-items:center;width:26px;height:26px;display:grid}.FOsK3a_starButton:focus-visible{outline:2px solid var(--dsw-alias-border-l3)}.FOsK3a_row:hover .FOsK3a_starButton,.FOsK3a_row:focus-within .FOsK3a_starButton{opacity:1}.FOsK3a_starButton[aria-pressed=true]{color:var(--dsw-alias-state-warn-primary);opacity:1}@media (hover:none){.FOsK3a_starButton{opacity:1}}.FOsK3a_star{flex:none}.FOsK3a_divider{background:var(--dsw-alias-border-l2);flex:none;height:.5px;margin:3px 2px}.FOsK3a_effortRow{align-items:center;gap:6px;padding:3px 6px;display:flex}.FOsK3a_effortHeading{color:var(--dsw-alias-label-primary);flex:none;font-size:12px;line-height:18px}.FOsK3a_effortTrack{background:var(--dsw-alias-interactive-bg-hover);scrollbar-width:none;border-radius:999px;align-items:center;gap:1px;min-width:0;margin-left:auto;padding:2px;display:flex;overflow-x:auto}.FOsK3a_effortTrack::-webkit-scrollbar{display:none}.FOsK3a_effortSegment{color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:999px;outline:none;flex:none;padding:2px 7px;font-size:11px;line-height:16px}.FOsK3a_effortSegment:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.FOsK3a_effortSegment[aria-checked=true]{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1);box-shadow:var(--dsw-elevation-soft);font-weight:600}.FOsK3a_effortSegment:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}.FOsK3a_effortSegment:focus-visible{outline:2px solid var(--dsw-alias-border-l3);outline-offset:-2px}.FOsK3a_hint{color:var(--dsw-alias-label-tertiary);margin:0;padding:6px 8px;font-size:11px;line-height:16px}.FOsK3a_moreRow{color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:10px;outline:none;align-items:center;gap:6px;min-height:32px;padding:5px 8px;font-size:12px;line-height:18px;display:flex}.FOsK3a_moreRow:hover,.FOsK3a_moreRow:focus-visible{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.FOsK3a_moreIcon{color:var(--dsw-alias-label-tertiary);flex:none}.FOsK3a_notice{color:var(--dsw-alias-label-tertiary);justify-content:space-between;align-items:flex-start;gap:8px;padding:6px 10px;font-size:12px;line-height:18px;display:flex}.FOsK3a_retry{color:inherit;font:inherit;cursor:pointer;background:0 0;border:none;flex:none;padding:0;font-weight:600;text-decoration:underline}.FOsK3a_pickerDialog{gap:0;width:min(680px,100%);height:min(640px,80vh);padding:0}.FOsK3a_pickerContent,.FOsK3a_pickerContent>:last-child{flex-direction:column;flex:1;min-height:0;display:flex}.FOsK3a_pickerSearch{padding:0 20px 12px}.FOsK3a_pickerSearchInput{width:100%}.FOsK3a_pickerTabs{border-bottom:.5px solid var(--dsw-alias-border-l2);overscroll-behavior-x:contain;scrollbar-width:none;flex-wrap:nowrap;gap:2px;padding:0 16px;display:flex;overflow-x:auto}.FOsK3a_pickerTabs::-webkit-scrollbar{display:none}.FOsK3a_pickerTab{color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;background:0 0;border:none;border-radius:8px;outline:none;flex:none;padding:6px 10px;font-size:13px;line-height:18px}.FOsK3a_pickerTab:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.FOsK3a_pickerTab[aria-selected=true]{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover);font-weight:600}.FOsK3a_pickerTab:focus-visible{outline:2px solid var(--dsw-alias-border-l3)}.FOsK3a_pickerList{flex:1;min-height:0;padding:8px 12px;overflow-y:auto}.FOsK3a_pickerFooter{border-top:.5px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary);align-items:center;gap:6px;padding:10px 20px;font-size:12px;line-height:18px;display:flex}.FOsK3a_footerHint{flex-wrap:wrap;align-items:center;gap:6px;display:flex}.FOsK3a_footerDot{color:var(--dsw-alias-label-dimmed)}";
43
43
  const tagId = "@lynn123411/dsh-model-submenu/ModelSubmenu.module.css";
44
44
  if (typeof document !== "undefined") {
45
45
  let tag = document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]");
@@ -80,6 +80,7 @@ window.__ModuleLoader__.load({
80
80
  "retry": "FOsK3a_retry",
81
81
  "root": "FOsK3a_root",
82
82
  "row": "FOsK3a_row",
83
+ "rowBadge": "FOsK3a_rowBadge",
83
84
  "rowCopy": "FOsK3a_rowCopy",
84
85
  "rowMeta": "FOsK3a_rowMeta",
85
86
  "rowName": "FOsK3a_rowName",
@@ -88,6 +89,7 @@ window.__ModuleLoader__.load({
88
89
  "starButton": "FOsK3a_starButton",
89
90
  "starStatic": "FOsK3a_starStatic",
90
91
  "trigger": "FOsK3a_trigger",
92
+ "triggerBadge": "FOsK3a_triggerBadge",
91
93
  "triggerEffort": "FOsK3a_triggerEffort",
92
94
  "triggerIcon": "FOsK3a_triggerIcon",
93
95
  "triggerLabel": "FOsK3a_triggerLabel"
@@ -99,16 +101,20 @@ window.__ModuleLoader__.load({
99
101
  * @param props - the row's content and behaviour.
100
102
  * @returns the row element.
101
103
  */
102
- function ModelRow({ name, selected, busy, star, favorited, meta, starLabel, starTabStop = false, rowTabIndex, onPick, onFocusRow, onToggleFavorite, children }) {
104
+ function ModelRow({ name, badge, selected, busy, star, favorited, meta, starLabel, starTabStop = false, rowTabIndex, onPick, onFocusRow, onToggleFavorite, children }) {
103
105
  const copy = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
104
106
  className: ModelSubmenu_module_css_default.rowCopy,
105
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
106
- className: ModelSubmenu_module_css_default.rowName,
107
- children: name
108
- }), meta !== void 0 && meta !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
109
- className: ModelSubmenu_module_css_default.rowMeta,
110
- children: meta
111
- })]
107
+ children: [
108
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
109
+ className: ModelSubmenu_module_css_default.rowName,
110
+ children: name
111
+ }),
112
+ badge,
113
+ meta !== void 0 && meta !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
114
+ className: ModelSubmenu_module_css_default.rowMeta,
115
+ children: meta
116
+ })
117
+ ]
112
118
  }) });
113
119
  const mark = favorited ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StarIcon, {
114
120
  filled: true,
@@ -525,7 +531,7 @@ window.__ModuleLoader__.load({
525
531
  * @param props - the catalog, the live selection, and the picker's verbs.
526
532
  * @returns the dialog; nothing while closed.
527
533
  */
528
- function ModelPicker({ open, state, live, facts, favorites, busy, onPick, onToggleFavorite, onReload, onClose, t }) {
534
+ function ModelPicker({ open, resolveText, state, live, facts, favorites, busy, onPick, onToggleFavorite, onReload, onClose, t }) {
529
535
  const id = (0, react.useId)();
530
536
  const searchRef = (0, react.useRef)(null);
531
537
  const listRef = (0, react.useRef)(null);
@@ -552,7 +558,7 @@ window.__ModuleLoader__.load({
552
558
  setHighlight(0);
553
559
  if (listRef.current !== null) listRef.current.scrollTop = 0;
554
560
  const frame = requestAnimationFrame(() => {
555
- searchRef.current?.focus();
561
+ if (!(0, _deepseek_ai_dsh_client_ui_primitives.isMobileDevice)()) searchRef.current?.focus();
556
562
  });
557
563
  return () => {
558
564
  cancelAnimationFrame(frame);
@@ -712,6 +718,11 @@ window.__ModuleLoader__.load({
712
718
  id: `${id}-row-${index}`,
713
719
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelRow, {
714
720
  name: row.entry.name,
721
+ badge: row.entry.badge === void 0 ? void 0 : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
722
+ tone: row.entry.badge.tone,
723
+ className: ModelSubmenu_module_css_default.rowBadge,
724
+ children: resolveText(row.entry.badge.text)
725
+ }),
715
726
  selected: live?.provider === row.provider && live.model === row.model,
716
727
  busy,
717
728
  star: "interactive",
@@ -850,7 +861,7 @@ window.__ModuleLoader__.load({
850
861
  * Closing the menu never writes anything, and the picker reads the level the
851
862
  * Host already accepted, so its "current" reading is never stale.
852
863
  */
853
- function ModelSubmenu({ locked, available, directory, load, select, t, factsSource }) {
864
+ function ModelSubmenu({ locked, available, directory, load, select, resolveText, t, factsSource }) {
854
865
  const state = (0, react.useSyncExternalStore)((fn) => directory.subscribe(fn), () => directory.getSnapshot());
855
866
  const [open, setOpen] = (0, react.useState)(false);
856
867
  const [pickerOpen, setPickerOpen] = (0, react.useState)(false);
@@ -861,7 +872,6 @@ window.__ModuleLoader__.load({
861
872
  const rootRef = (0, react.useRef)(null);
862
873
  const triggerRef = (0, react.useRef)(null);
863
874
  const menuRef = (0, react.useRef)(null);
864
- const [menuPos, setMenuPos] = (0, react.useState)(null);
865
875
  const id = (0, react.useId)();
866
876
  const groups = state.groups;
867
877
  const current = state.current;
@@ -879,6 +889,17 @@ window.__ModuleLoader__.load({
879
889
  const busy = state.status === "selecting";
880
890
  const live = (0, react.useMemo)(() => selectionOf(state), [state]);
881
891
  const reason = (0, react.useMemo)(() => reasoningOf(groups), [groups]);
892
+ /** Resolve one catalog badge to its localized text and tone, matching the host seat. */
893
+ const badgeOf = (model) => model.badge === void 0 ? void 0 : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
894
+ tone: model.badge.tone,
895
+ className: ModelSubmenu_module_css_default.rowBadge,
896
+ children: resolveText(model.badge.text)
897
+ });
898
+ const triggerBadge = currentModel?.badge === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
899
+ tone: currentModel.badge.tone,
900
+ className: ModelSubmenu_module_css_default.triggerBadge,
901
+ children: resolveText(currentModel.badge.text)
902
+ });
882
903
  const facts = (0, react.useSyncExternalStore)((fn) => factsSource.subscribe(fn), () => factsSource.getSnapshot());
883
904
  const menuRows = (0, react.useMemo)(() => currentGroup === void 0 ? [] : orderWithFavorites(currentGroup.models, (model) => isFavorite(favorites, currentGroup.id, model.id)), [currentGroup, favorites]);
884
905
  const raiseToast = (text) => {
@@ -934,34 +955,16 @@ window.__ModuleLoader__.load({
934
955
  cancelAnimationFrame(frame);
935
956
  };
936
957
  }, [open]);
937
- (0, react.useLayoutEffect)(() => {
938
- if (!open) {
939
- setMenuPos(null);
940
- return;
941
- }
942
- const place = () => {
943
- const rect = triggerRef.current?.getBoundingClientRect();
944
- if (rect === void 0) return;
945
- const MARGIN = 12;
946
- const lw = menuRef.current?.offsetWidth ?? 0;
947
- const lh = menuRef.current?.offsetHeight ?? 0;
948
- let x = rect.right - lw;
949
- let y = rect.top - 8 - lh;
950
- if (lw > 0) x = Math.min(Math.max(x, MARGIN), window.innerWidth - lw - MARGIN);
951
- if (lh > 0) y = Math.min(Math.max(y, MARGIN), window.innerHeight - lh - MARGIN);
952
- setMenuPos({
953
- left: x,
954
- top: y
955
- });
956
- };
957
- place();
958
- window.addEventListener("scroll", place, true);
959
- window.addEventListener("resize", place);
960
- return () => {
961
- window.removeEventListener("scroll", place, true);
962
- window.removeEventListener("resize", place);
963
- };
964
- }, [open, state]);
958
+ const menuPos = (0, _deepseek_ai_dsh_client_ui_primitives.useAnchoredPosition)({
959
+ open: open && available,
960
+ anchorRef: triggerRef,
961
+ panelRef: menuRef,
962
+ side: "top",
963
+ align: "end",
964
+ gap: 8,
965
+ margin: 12,
966
+ layoutKey: `${state.status}:${String(menuRows.length)}:${String(choices.length)}`
967
+ });
965
968
  if (!available) return null;
966
969
  const show = () => {
967
970
  setOpen(true);
@@ -1089,6 +1092,7 @@ window.__ModuleLoader__.load({
1089
1092
  className: ModelSubmenu_module_css_default.triggerIcon,
1090
1093
  size: 16
1091
1094
  }),
1095
+ triggerBadge,
1092
1096
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1093
1097
  className: ModelSubmenu_module_css_default.triggerLabel,
1094
1098
  children: modelLabel
@@ -1097,7 +1101,7 @@ window.__ModuleLoader__.load({
1097
1101
  className: ModelSubmenu_module_css_default.triggerEffort,
1098
1102
  children: effortLabel
1099
1103
  }),
1100
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: cls(ModelSubmenu_module_css_default.chevron, open && ModelSubmenu_module_css_default.chevronOpen) })
1104
+ busy ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: "ongoing" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: cls(ModelSubmenu_module_css_default.chevron, open && ModelSubmenu_module_css_default.chevronOpen) })
1101
1105
  ]
1102
1106
  }),
1103
1107
  open && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.MenuSurface, {
@@ -1145,6 +1149,7 @@ window.__ModuleLoader__.load({
1145
1149
  "aria-label": t("menu.aria"),
1146
1150
  children: [menuRows.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelRow, {
1147
1151
  name: model.name,
1152
+ badge: badgeOf(model),
1148
1153
  selected: current !== null && currentGroup !== void 0 && current.provider === currentGroup.id && current.model === model.id,
1149
1154
  busy,
1150
1155
  star: "static",
@@ -1206,6 +1211,7 @@ window.__ModuleLoader__.load({
1206
1211
  }), document.body),
1207
1212
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelPicker, {
1208
1213
  open: pickerOpen,
1214
+ resolveText,
1209
1215
  state,
1210
1216
  live,
1211
1217
  facts,
@@ -1435,6 +1441,7 @@ window.__ModuleLoader__.load({
1435
1441
  load: () => {
1436
1442
  if (available) directory.load().catch(() => {});
1437
1443
  },
1444
+ resolveText: (text) => ctx.locale.resolveText(text),
1438
1445
  select: (selection) => available ? directory.select(selection) : Promise.resolve(void 0)
1439
1446
  };
1440
1447
  }
package/lib/client.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":["css","IconCheckOutlineRegular","getItem","useId","useRef","useState","useMemo","rankByName","Modal","css","Input","IconSearchOutlineRegular","ShortcutKeys","useSyncExternalStore","useState","useRef","useId","useMemo","useCallback","css","IconFlatListOutlineRegular","IconChevronDownOutlineRegular","createPortal","MenuSurface","Toast","IconWarningOutlineRegular"],"sources":["../src/client/icons.tsx","../src/client/ModelRow.tsx","../src/client/favorites.ts","../src/client/model-facts.ts","../src/client/rows.ts","../src/client/selection.ts","../src/client/ModelPicker.tsx","../src/client/effort-memory.ts","../src/client/ModelSubmenu.tsx","../src/client/facts-source.ts","../src/client/locales.ts","../src/client/index.ts"],"sourcesContent":["/**\n * The one glyph this plugin draws itself.\n *\n * The shared icon library carries no star, so the favorite mark is defined here:\n * a five-point star whose outline and fill use the same path, which keeps the\n * hover-only outline and the standing filled state identical in geometry.\n */\n\n/**\n * Render the favorite star.\n * @param props.filled - draw the solid star; otherwise the outline.\n * @param props.size - square edge in pixels, matching the shared icon set's default.\n * @param props.className - extra class for colour and placement.\n * @returns the inline SVG.\n */\nexport function StarIcon({ filled, size = 14, className }: {\n readonly filled: boolean\n readonly size?: number | undefined\n readonly className?: string | undefined\n}) {\n return (\n <svg\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill={filled ? 'currentColor' : 'none'}\n stroke=\"currentColor\"\n strokeWidth={filled ? 0 : 1.6}\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n className={className}\n >\n <path d=\"M12 3.2l2.7 5.6 6.1.85-4.45 4.3 1.08 6.05L12 17.1l-5.43 2.9 1.08-6.05L3.2 9.65l6.1-.85z\" />\n </svg>\n )\n}\n","/**\n * One pickable model, as both surfaces render it.\n *\n * The menu row is one button that spans the whole width — name, favorite mark,\n * and the check for the model in use all live inside it, so hovering any part of\n * the row (including the star) highlights the row and clicking any part picks\n * the model. The picker row keeps the same shape but lifts the star out into its\n * own control, because favouriting there must not switch the model.\n */\n\nimport type { ReactNode } from 'react'\nimport { IconCheckOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives'\nimport { StarIcon } from './icons.tsx'\nimport css from './ModelSubmenu.module.css'\n\n/** How one row shows its favorite mark: part of the row, or a control of its own. */\nexport type StarMode = 'static' | 'interactive'\n\n/** Props shared by both row shapes. */\nexport interface ModelRowProps {\n /** The model's catalog name. */\n readonly name: string\n /** Whether this row is the model in use. */\n readonly selected: boolean\n /** Whether a selection is in flight, which locks the row. */\n readonly busy: boolean\n /** How the favorite mark behaves on this row. */\n readonly star: StarMode\n /** Whether the model is favorited right now. */\n readonly favorited: boolean\n /** The metadata line's text; undefined in the menu. */\n readonly meta?: string | undefined\n /** Localized accessible name for the star control. */\n readonly starLabel?: string | undefined\n /** Whether the star is a Tab stop right now (the picker's roving highlight). */\n readonly starTabStop?: boolean\n /** Tab order of the row itself; the picker keeps rows out of it so ↑/↓ own the walk. */\n readonly rowTabIndex?: number\n /** Pick this row. */\n readonly onPick: () => void\n /** The row took the keyboard, so its star becomes reachable. */\n readonly onFocusRow?: (() => void) | undefined\n /** Toggle this row's favorite; absent when the star is part of the row. */\n readonly onToggleFavorite?: (() => void) | undefined\n /** Extra row content, such as the picker's disabled reason. */\n readonly children?: ReactNode\n}\n\n/**\n * Render one model row.\n * @param props - the row's content and behaviour.\n * @returns the row element.\n */\nexport function ModelRow({\n name, selected, busy, star, favorited, meta, starLabel,\n starTabStop = false, rowTabIndex, onPick, onFocusRow, onToggleFavorite, children,\n}: ModelRowProps) {\n const copy = (\n <>\n <span className={css.rowCopy}>\n <span className={css.rowName}>{name}</span>\n {meta !== undefined && meta !== '' && <span className={css.rowMeta}>{meta}</span>}\n </span>\n </>\n )\n const mark = favorited ? <StarIcon filled className={css.star} /> : null\n\n // The menu's row is one target: the star and the check sit inside the button,\n // so they inherit its hover fill and its click.\n if (star === 'static') {\n return (\n <button\n type=\"button\"\n role=\"menuitemradio\"\n aria-checked={selected}\n tabIndex={rowTabIndex}\n className={css.rowPick}\n title={name}\n disabled={busy}\n onClick={onPick}\n onFocus={onFocusRow}\n data-selected={selected ? '' : undefined}\n data-favorited={favorited ? '' : undefined}\n >\n {copy}\n {mark !== null && <span className={css.starStatic} aria-hidden=\"true\">{mark}</span>}\n <span className={css.check} aria-hidden={!selected}>{selected ? <IconCheckOutlineRegular /> : null}</span>\n {children}\n </button>\n )\n }\n\n return (\n <div\n className={css.row}\n data-selected={selected ? '' : undefined}\n data-favorited={favorited ? '' : undefined}\n >\n <button\n type=\"button\"\n role=\"menuitemradio\"\n aria-checked={selected}\n tabIndex={rowTabIndex}\n className={css.rowPick}\n title={name}\n disabled={busy}\n onClick={onPick}\n onFocus={onFocusRow}\n >\n {copy}\n </button>\n <button\n type=\"button\"\n className={css.starButton}\n aria-pressed={favorited}\n aria-label={starLabel}\n tabIndex={starTabStop ? 0 : -1}\n disabled={busy}\n onClick={onToggleFavorite}\n >\n <StarIcon filled={favorited} className={css.star} />\n </button>\n <span className={css.check} aria-hidden={!selected}>{selected ? <IconCheckOutlineRegular /> : null}</span>\n {children}\n </div>\n )\n}\n","/**\n * The favorite-model table the picker keeps in the browser.\n *\n * One browser-wide key holds `{ provider, model }` rows: the provider route key\n * and the model id are the catalog's own identity, so a favorite survives a\n * display-name change and never collides across providers. A value that cannot\n * be read or does not parse reads as an empty table — a privacy-mode browser or\n * a hand-edited row must never keep the menu from opening. Rows whose provider\n * leaves the catalog are kept, so restoring the provider restores the star.\n */\n\nimport type { ModelRef } from './rows.ts'\n\n/** One favorited model, identified the way the catalog identifies it. */\nexport type FavoriteModel = ModelRef\n\n/** The single browser-wide storage key holding the whole table. */\nexport const FAVORITES_KEY = 'dsh-model-submenu.favorites'\n\n/** The part of `Storage` this module uses; absent or throwing reads mean \"none\". */\nexport type FavoritesStorage = Pick<Storage, 'getItem' | 'setItem'>\n\n/**\n * Read the table, keeping only rows that name both halves.\n * @param storage - browser storage, or undefined when the browser withholds it.\n * @returns the stored favorites in stored order; empty when unreadable.\n */\nexport function readFavorites(storage: FavoritesStorage | undefined): FavoriteModel[] {\n const raw = getItem(storage)\n if (raw === null) return []\n let parsed: unknown\n try {\n parsed = JSON.parse(raw)\n } catch {\n return []\n }\n if (!Array.isArray(parsed)) return []\n return parsed.filter((row): row is FavoriteModel => typeof row === 'object' && row !== null\n && typeof (row as FavoriteModel).provider === 'string'\n && typeof (row as FavoriteModel).model === 'string')\n}\n\n/**\n * Add or remove one model, writing the whole table back.\n * @param storage - browser storage, or undefined when writes are unavailable.\n * @param provider - the provider route key.\n * @param model - the model id inside that provider.\n * @returns the table after the change, whether or not the write landed.\n */\nexport function toggleFavorite(\n storage: FavoritesStorage | undefined,\n provider: string,\n model: string,\n): FavoriteModel[] {\n const current = readFavorites(storage)\n const next = isFavorite(current, provider, model)\n ? current.filter((row) => row.provider !== provider || row.model !== model)\n : [...current, { provider, model }]\n try {\n storage?.setItem(FAVORITES_KEY, JSON.stringify(next))\n } catch {\n // A full quota or a locked-down browser loses the star, not the menu.\n }\n return next\n}\n\n/**\n * @param favorites - the table to search.\n * @param provider - the provider route key.\n * @param model - the model id inside that provider.\n * @returns whether that exact pair is favorited.\n */\nexport function isFavorite(\n favorites: readonly FavoriteModel[],\n provider: string,\n model: string,\n): boolean {\n return favorites.some((row) => row.provider === provider && row.model === model)\n}\n\n/**\n * Stable-partition a catalog so favorited rows lead, each part keeping the\n * catalog's own order.\n * @param rows - the catalog in its display order.\n * @param favorited - predicate answering whether one row is favorited.\n * @returns a new list; the input order when nothing is favorited.\n */\nexport function orderWithFavorites<T>(\n rows: readonly T[],\n favorited: (row: T) => boolean,\n): readonly T[] {\n const starred = rows.filter(favorited)\n if (starred.length === 0) return rows\n return [...starred, ...rows.filter((row) => !favorited(row))]\n}\n\nfunction getItem(storage: FavoritesStorage | undefined): string | null {\n try {\n return storage?.getItem(FAVORITES_KEY) ?? null\n } catch {\n return null\n }\n}\n","/**\n * What the settings document knows about one model, for the picker's metadata\n * line.\n *\n * The Host's model catalog carries only a name and a reasoning list; context\n * window and accepted modalities live in each provider's settings profile,\n * which the settings mirror already holds in the browser. This module joins\n * the two by the identity both agree on: the catalog's provider route key and\n * model id are exactly the settings profile's provider key and model id.\n *\n * Every field is answered independently. A provider whose profile lists no\n * models (an OAuth route such as the Kimi subscription, or the account\n * provider) answers `unknown` rather than guessing the route default, because\n * the line's rule is to omit what cannot be asserted.\n */\n\n/** Accepted-modality answer for one model. */\nexport type ImageAnswer = 'yes' | 'no' | 'unknown'\n\n/**\n * The model knowledge a shipped adapter carries for its own account route.\n *\n * The account route has no `models` list on purpose: the account listing\n * supplies the catalog and settings only carry per-model overrides. The same\n * adapter family ships a default catalog for its API-key route, and the account\n * route serves the same models, so those entries answer what the account\n * profile cannot. Ids and facts mirror the adapter's own table.\n */\nconst FAMILY_DEFAULTS: Readonly<Record<string, readonly {\n readonly id: string\n readonly contextWindow: number\n readonly images: boolean\n}[]>> = {\n 'deepseek-account': [\n { id: 'deepseek-flash', contextWindow: 1_000_000, images: true },\n { id: 'deepseek-v4-pro', contextWindow: 1_000_000, images: false },\n ],\n}\n\n/** The metadata the settings document can answer for one model. */\nexport interface ModelFacts {\n /** Exact advertised capacity, or undefined when the model declares none. */\n readonly contextWindow: number | undefined\n /** Whether the model accepts images; `unknown` means the document is silent. */\n readonly images: ImageAnswer\n}\n\n/** One provider directory row, as the Host's LLM directory reports it. */\nexport interface ConfigurableProviderRow {\n readonly provider: string\n readonly settingsNs: string\n readonly settingsPath: readonly string[]\n}\n\n/** One settings namespace view, as the describe answer carries it. */\nexport interface NamespaceRow {\n readonly ns: string\n readonly value: unknown\n}\n\n/** What one provider's profile may carry, across both adapter families. */\ninterface ProfileView {\n readonly models?: readonly Record<string, unknown>[]\n readonly defaultContextWindow?: unknown\n readonly defaultInput?: unknown\n}\n\ntype Record_ = Record<string, unknown>\n\n/**\n * Build the fact lookup from the settings answer.\n * @param providers - provider directory rows giving each route's settings address.\n * @param namespaces - the describe answer's namespace views.\n * @returns a lookup answering both fields for one provider and model.\n */\nexport function buildModelFacts(\n providers: readonly ConfigurableProviderRow[],\n namespaces: readonly NamespaceRow[],\n): (provider: string, model: string) => ModelFacts {\n const byName = new Map(namespaces.map((row) => [row.ns, row.value]))\n const profiles = new Map<string, ProfileView>()\n for (const row of providers) {\n const section = byName.get(row.settingsNs)\n const profile = readPath(section, row.settingsPath)\n if (isRecord(profile)) profiles.set(row.provider, profile as ProfileView)\n }\n return (provider, model) => {\n const profile = profiles.get(provider)\n const entry = profile?.models === undefined\n ? undefined\n : profile.models.find((candidate) => candidate.id === model)\n if (profile !== undefined && entry !== undefined) {\n const window = typeof entry.contextWindow === 'number' && entry.contextWindow > 0\n ? entry.contextWindow\n : undefined\n return { contextWindow: window, images: imageAnswer(entry, profile) }\n }\n // A route with no model list of its own (the account route) still serves its\n // adapter family's models; a declared override there wins over the default.\n const override = readOverride(profile, model)\n if (override !== undefined) {\n const window = typeof override.contextWindow === 'number' && override.contextWindow > 0\n ? override.contextWindow\n : undefined\n return {\n contextWindow: window,\n images: Array.isArray(override.inputModalities) ? containsImage(override.inputModalities) : 'unknown',\n }\n }\n const family = FAMILY_DEFAULTS[provider]?.find((candidate) => candidate.id === model)\n return family === undefined\n ? { contextWindow: undefined, images: 'unknown' }\n : { contextWindow: family.contextWindow, images: family.images ? 'yes' : 'no' }\n }\n}\n\n/**\n * Read one model entry's accepted image modalities.\n *\n * The two adapter families disagree about an omission. The Messages family\n * names the field `inputModalities`, and leaving it out means text only. The\n * pi-ai family names it `input`, and an empty or absent list defers to the\n * route's `defaultInput`; with no route default the answer stays unknown.\n */\nfunction imageAnswer(entry: Record_, profile: ProfileView): ImageAnswer {\n if (Array.isArray(entry.inputModalities)) return containsImage(entry.inputModalities)\n const defaultInput = Array.isArray(profile.defaultInput) ? profile.defaultInput : undefined\n if (Array.isArray(entry.input)) {\n if (entry.input.length > 0) return containsImage(entry.input)\n return defaultInput === undefined ? 'unknown' : containsImage(defaultInput)\n }\n return defaultInput === undefined ? 'no' : containsImage(defaultInput)\n}\n\n/** One model's declared override, when the route keeps a `modelOverrides` table. */\nfunction readOverride(profile: ProfileView | undefined, model: string): Record_ | undefined {\n const overrides = (profile as Record_ | undefined)?.modelOverrides\n if (!isRecord(overrides)) return undefined\n const entry = overrides[model]\n return isRecord(entry) ? entry : undefined\n}\n\nfunction containsImage(modalities: readonly unknown[]): ImageAnswer {\n return modalities.includes('image') ? 'yes' : 'no'\n}\n\n/**\n * Abbreviate one capacity the way the metadata line shows it.\n * @param tokens - exact advertised capacity in tokens.\n * @returns e.g. `1M ctx`, `1.5M ctx`, `272k ctx`, or `512 ctx` below a thousand.\n */\nexport function formatContextWindow(tokens: number): string {\n if (tokens >= 1_000_000) return `${trimNumber(tokens / 1_000_000)}M ctx`\n if (tokens >= 1_000) return `${Math.round(tokens / 1_000)}k ctx`\n return `${tokens} ctx`\n}\n\n/**\n * Build the metadata line's segments.\n * @param input.providerName - the provider's display name, omitted inside that provider's own tab.\n * @param input.facts - what the settings document answered.\n * @param input.thinks - whether the catalog advertises reasoning levels for this model.\n * @param input.effortLabel - the localized word for the reasoning segment.\n * @param input.imageLabel - the localized word for the image segment.\n * @returns the segments to join; an empty list means show no line.\n */\nexport function metaSegments(input: {\n readonly providerName?: string\n readonly facts: ModelFacts\n readonly thinks: boolean\n readonly effortLabel: string\n readonly imageLabel: string\n}): readonly string[] {\n const segments: string[] = []\n if (input.providerName !== undefined) segments.push(input.providerName)\n if (input.facts.contextWindow !== undefined) segments.push(formatContextWindow(input.facts.contextWindow))\n if (input.thinks) segments.push(input.effortLabel)\n if (input.facts.images === 'yes') segments.push(input.imageLabel)\n return segments\n}\n\nfunction trimNumber(value: number): string {\n const rounded = Math.round(value * 10) / 10\n return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1)\n}\n\nfunction readPath(value: unknown, path: readonly string[]): unknown {\n let cursor = value\n for (const key of path) {\n if (!isRecord(cursor)) return undefined\n cursor = cursor[key]\n }\n return cursor\n}\n\nfunction isRecord(value: unknown): value is Record_ {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n","/**\n * Catalog flattening, tab derivation, and query matching for the picker.\n *\n * The model catalog groups models by provider; the picker shows one flat list\n * with a tab per provider, so both surfaces read from these rows. Matching\n * delegates to the shell's own ranker, which is why a query behaves the same\n * here as in the built-in menu: case-insensitive, ordered subsequence, prefix\n * hits first.\n */\n\nimport type { ModelDirectoryState } from '@deepseek-ai/dsh-client-ui-model-selection/client'\n\n/** One provider group of the shared catalog. */\nexport type ProviderGroup = ModelDirectoryState['groups'][number]\n\n/** One model inside a provider group. */\nexport type CatalogModel = ProviderGroup['models'][number]\n\n/** One flat catalog row, carrying the provider identity beside the model. */\nexport interface ModelRow extends ModelRef {\n readonly providerName: string\n /** The model id inside its provider, the other half of a selection. */\n readonly model: string\n /** The catalog entry itself, for name and reasoning metadata. */\n readonly entry: CatalogModel\n}\n\n/** A provider and one of its models: the pair every selection and favorite names. */\nexport interface ModelRef {\n readonly provider: string\n readonly model: string\n}\n\n/** One picker tab. */\nexport interface PickerTab {\n readonly id: string\n readonly label: string\n}\n\n/** The tab id standing for every provider. */\nexport const ALL_TAB = '*'\n\n/**\n * Flatten the catalog to one row per model.\n * @param groups - the shared directory's provider groups, in display order.\n * @returns the rows in catalog order.\n */\nexport function flattenGroups(groups: readonly ProviderGroup[]): readonly ModelRow[] {\n return groups.flatMap((group) => group.models.map((model) => ({\n provider: group.id,\n providerName: group.name,\n model: model.id,\n entry: model,\n })))\n}\n\n/**\n * Build the picker's tabs: the all-tab, then one per provider that has a model.\n * @param groups - the shared directory's provider groups.\n * @param allLabel - localized label for the all-tab.\n * @returns the tabs in catalog order.\n */\nexport function pickerTabs(groups: readonly ProviderGroup[], allLabel = 'All'): readonly PickerTab[] {\n return [\n { id: ALL_TAB, label: allLabel },\n ...groups.filter((group) => group.models.length > 0).map((group) => ({ id: group.id, label: group.name })),\n ]\n}\n\n/**\n * @param rows - the flat catalog.\n * @param tab - the tab id; the all-tab keeps every row.\n * @returns the rows the tab shows, in their current order.\n */\nexport function rowsForTab(rows: readonly ModelRow[], tab: string): readonly ModelRow[] {\n return tab === ALL_TAB ? rows : rows.filter((row) => row.provider === tab)\n}\n\n/** One row handed to the shell's ranker, carrying both searchable keys. */\nexport interface SearchCandidate {\n readonly name: string\n readonly label: string\n readonly row: ModelRow\n}\n\n/**\n * Shape rows as the shell's ranker candidates: the model name is the name, the\n * provider display name is the second key, so a query reaches either.\n * @param rows - the rows to search.\n * @returns candidates in row order.\n */\nexport function searchCandidates(rows: readonly ModelRow[]): readonly SearchCandidate[] {\n return rows.map((row) => ({ name: row.entry.name, label: row.providerName, row }))\n}\n\n/**\n * Read the reasoning metadata the catalog advertises for one model.\n * @param groups - the shared directory's provider groups.\n * @returns the metadata, or undefined when that model advertises none.\n */\nexport function reasoningOf(\n groups: readonly ProviderGroup[],\n): (provider: string, model: string) => CatalogModel['reasoning'] {\n return (provider, model) => groups\n .find((group) => group.id === provider)\n ?.models.find((candidate) => candidate.id === model)\n ?.reasoning\n}\n\n/**\n * Filter rows by a query over the model name and the provider name.\n * @param rows - the rows to search.\n * @param query - the typed text; an empty or blank query keeps every row.\n * @param rank - the shell's own ranker, so this search behaves like every other one.\n * @returns the matching rows.\n */\nexport function searchRows(\n rows: readonly ModelRow[],\n query: string,\n rank: (items: readonly SearchCandidate[], query: string) => readonly SearchCandidate[],\n): readonly ModelRow[] {\n const trimmed = query.trim()\n if (trimmed === '') return rows\n return rank(searchCandidates(rows), trimmed).map((match) => match.row)\n}\n","/**\n * When the composer menu writes a selection.\n *\n * Both gestures are immediate: clicking a level submits it, clicking a model\n * submits it. The only quiet path is clicking the model already in use — that\n * closes the menu without a write. A model click also restores the level this\n * browser last used for that model, so switching away and back never silently\n * resets the effort.\n */\n\nimport type { ModelReasoning } from '@deepseek-ai/dsh-api-session-controller/types'\n\n/** One complete selection, as the directory's `select` takes it. */\nexport interface Selection {\n readonly provider: string\n readonly model: string\n readonly reasoningEffort?: string\n}\n\n/** One segment of the reasoning row. */\nexport interface EffortChoice {\n readonly key: string\n readonly effort: string | undefined\n readonly label: string\n}\n\n/**\n * Build the segments for one model's reasoning row.\n * @param reasoning - the catalog's reasoning metadata, undefined when the model has none.\n * @param providerDefaultLabel - the localized label for the \"follow the provider\" segment.\n * @returns the segments in catalog order; the provider-default one leads when the model declares no default.\n */\nexport function effortChoices(\n reasoning: ModelReasoning | undefined,\n providerDefaultLabel: string,\n): readonly EffortChoice[] {\n if (reasoning === undefined) return []\n return [\n ...reasoning.defaultEffort === undefined\n ? [{ key: 'provider-default', effort: undefined, label: providerDefaultLabel }]\n : [],\n ...reasoning.efforts.map((level) => ({\n key: level.id,\n effort: level.id as string | undefined,\n label: level.name,\n })),\n ]\n}\n\n/**\n * The selection a level click submits.\n * @param live - the selection in force right now.\n * @param effort - the clicked level, undefined meaning the provider default.\n * @returns the selection to submit, or null when the level already is the live one.\n */\nexport function planEffortCommit(live: Selection, effort: string | undefined): Selection | null {\n if (sameEffort(live.reasoningEffort, effort)) return null\n return {\n provider: live.provider,\n model: live.model,\n ...effort === undefined ? {} : { reasoningEffort: effort },\n }\n}\n\n/**\n * The selection a model click submits.\n * @param live - the selection in force right now, null before a first selection resolves.\n * @param provider - the clicked model's provider.\n * @param model - the clicked model id.\n * @param reasoningOf - the catalog's reasoning metadata for one provider and model.\n * @param remembered - the level this browser last used for the target model, if any.\n * @returns the selection to submit, or null when the click changes nothing.\n */\nexport function planModelCommit(\n live: Selection | null,\n provider: string,\n model: string,\n reasoningOf: (provider: string, model: string) => ModelReasoning | undefined,\n remembered?: string | undefined,\n): Selection | null {\n if (live !== null && sameModel(live, provider, model)) return null\n const offered = reasoningOf(provider, model)\n // The target model's own memory is restored, so switching away and back never\n // silently resets the effort.\n const carried = offered !== undefined\n && remembered !== undefined\n && offered.efforts.some((level) => level.id === remembered)\n return {\n provider,\n model,\n ...carried ? { reasoningEffort: remembered } : {},\n }\n}\n\n/**\n * @param live - one selection.\n * @param provider - the provider to compare.\n * @param model - the model id to compare.\n * @returns whether both name the same model.\n */\nexport function sameModel(live: Selection, provider: string, model: string): boolean {\n return live.provider === provider && live.model === model\n}\n\n/**\n * @param liveEffort - the level in force, undefined meaning the provider default.\n * @param effort - the level in question.\n * @returns whether they name the same level.\n */\nexport function sameEffort(liveEffort: string | undefined, effort: string | undefined): boolean {\n return liveEffort === effort\n}\n","/**\n * The model picker: the centered dialog opened from the menu's last row.\n *\n * It is the one place every provider is visible at once — a search field, a tab\n * per provider with a model, and one flat list with each model's metadata line.\n * Favorites lead the list and can be toggled from a row's star without\n * selecting that row. A click commits at once; a rejected commit keeps the\n * dialog open so the user can retry or pick another model.\n */\n\nimport { useEffect, useId, useMemo, useRef, useState } from 'react'\nimport {\n IconSearchOutlineRegular,\n Input,\n Modal,\n ShortcutKeys,\n rankByName,\n} from '@deepseek-ai/dsh-client-ui-primitives'\nimport type { ModelDirectoryState } from '@deepseek-ai/dsh-client-ui-model-selection/client'\nimport type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'\nimport { ModelRow } from './ModelRow.tsx'\nimport { isFavorite, orderWithFavorites } from './favorites.ts'\nimport type { ModelFacts } from './model-facts.ts'\nimport { metaSegments } from './model-facts.ts'\nimport {\n ALL_TAB,\n flattenGroups,\n pickerTabs,\n reasoningOf,\n rowsForTab,\n searchRows,\n type ModelRow as CatalogRow,\n type PickerTab,\n} from './rows.ts'\nimport type { ModelFactsLookup } from './facts-source.ts'\nimport type { Selection } from './selection.ts'\nimport { planModelCommit } from './selection.ts'\nimport css from './ModelSubmenu.module.css'\n\n/** Props the seat hands the picker. */\nexport interface ModelPickerProps {\n /** Whether the dialog is showing. */\n readonly open: boolean\n /** The shared directory snapshot. */\n readonly state: ModelDirectoryState\n /** The selection in force, null before a first selection resolves. */\n readonly live: Selection | null\n /** The metadata lookup for the current settings answer. */\n readonly facts: ModelFactsLookup\n /** The favorite table right now. */\n readonly favorites: readonly { readonly provider: string; readonly model: string }[]\n /** Whether a selection is in flight. */\n readonly busy: boolean\n /** Submit one selection. */\n readonly onPick: (selection: Selection) => void\n /** Toggle one model's favorite. */\n readonly onToggleFavorite: (provider: string, model: string) => void\n /** Reload the catalog after a failure. */\n readonly onReload: () => void\n /** Close the dialog, optionally returning focus. */\n readonly onClose: () => void\n}\n\n/**\n * Render the picker.\n * @param props - the catalog, the live selection, and the picker's verbs.\n * @returns the dialog; nothing while closed.\n */\nexport function ModelPicker({\n open, state, live, facts, favorites, busy, onPick, onToggleFavorite, onReload, onClose, t,\n}: ModelPickerProps & PropsLocale<'modelSubmenu'>) {\n const id = useId()\n const searchRef = useRef<HTMLInputElement | null>(null)\n const listRef = useRef<HTMLDivElement | null>(null)\n const [query, setQuery] = useState('')\n const [tab, setTab] = useState(ALL_TAB)\n const [highlight, setHighlight] = useState(0)\n const tabRefs = useRef(new Map<string, HTMLButtonElement>())\n\n const tabs = useMemo(() => pickerTabs(state.groups, t('picker.all')), [state.groups, t])\n const reason = useMemo(() => reasoningOf(state.groups), [state.groups])\n const rows = useMemo(() => orderWithFavorites(\n rowsForTab(flattenGroups(state.groups), tab),\n (row) => isFavorite(favorites, row.provider, row.model),\n ), [state.groups, tab, favorites])\n // Search runs inside the favorite partition, never across it: a query must not\n // pull an unstarred model above a starred one, because favorites lead the list\n // whether or not a query is typed.\n const visible = useMemo(() => orderWithFavorites(\n searchRows(rows, query, rankByName),\n (row) => isFavorite(favorites, row.provider, row.model),\n ), [rows, query, favorites])\n\n useEffect(() => {\n if (!open) return\n setQuery('')\n setTab(ALL_TAB)\n setHighlight(0)\n if (listRef.current !== null) listRef.current.scrollTop = 0\n const frame = requestAnimationFrame(() => { searchRef.current?.focus() })\n return () => { cancelAnimationFrame(frame) }\n }, [open])\n\n useEffect(() => {\n setHighlight((current) => visible.length === 0 ? 0 : Math.min(current, visible.length - 1))\n }, [visible.length])\n\n useEffect(() => {\n const row = listRef.current?.querySelector<HTMLElement>(`[data-row-index=\"${highlight}\"]`)\n row?.scrollIntoView({ block: 'nearest' })\n }, [highlight])\n\n if (!open) return null\n\n const activeRow = visible[highlight]\n const activeId = activeRow === undefined ? undefined : `${id}-row-${highlight}`\n const onSearchKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {\n if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {\n event.preventDefault()\n if (visible.length === 0) return\n setHighlight((current) => {\n const next = current + (event.key === 'ArrowDown' ? 1 : -1)\n return (next + visible.length) % visible.length\n })\n return\n }\n if (event.key === 'Enter' && activeRow !== undefined && !busy) {\n event.preventDefault()\n pick(activeRow)\n }\n }\n const pick = (row: CatalogRow) => {\n const selection = planModelCommit(live, row.provider, row.model, reason)\n if (selection === null) {\n onClose()\n return\n }\n onPick(selection)\n }\n /** Move to another tab and hand it the keyboard, in one step. */\n const goTab = (next: PickerTab | undefined) => {\n if (next === undefined || next.id === tab) return\n setTab(next.id)\n setHighlight(0)\n tabRefs.current.get(next.id)?.focus()\n }\n const onTabKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>) => {\n const index = tabs.findIndex((candidate) => candidate.id === tab)\n if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') {\n event.preventDefault()\n const step = event.key === 'ArrowRight' ? 1 : -1\n goTab(tabs[((index === -1 ? 0 : index) + step + tabs.length) % tabs.length])\n return\n }\n if (event.key === 'Home' || event.key === 'End') {\n event.preventDefault()\n goTab(tabs[event.key === 'Home' ? 0 : tabs.length - 1])\n }\n }\n\n const failures = state.failures.filter((failure) => tab === ALL_TAB || failure.id === tab)\n const failed = state.error !== null && state.status === 'error'\n const loading = state.status === 'loading'\n return (\n <Modal\n open={open}\n onClose={onClose}\n title={t('picker.title')}\n closeLabel={t('picker.close')}\n className={css.pickerDialog ?? ''}\n contentClassName={css.pickerContent ?? ''}\n >\n <div className={css.pickerSearch}>\n <Input\n ref={searchRef}\n className={css.pickerSearchInput ?? ''}\n type=\"text\"\n role=\"searchbox\"\n aria-label={t('picker.search')}\n aria-controls={`${id}-list`}\n aria-activedescendant={activeId}\n placeholder={t('picker.search')}\n icon={<IconSearchOutlineRegular size={14} />}\n value={query}\n readOnly={busy}\n onChange={(event) => { setQuery(event.target.value) }}\n onKeyDown={onSearchKeyDown}\n data-modal-autofocus=\"\"\n />\n </div>\n <div className={css.pickerTabs} role=\"tablist\" aria-label={t('picker.title')}>\n {tabs.map((entry) => (\n <button\n key={entry.id}\n ref={(node) => {\n if (node === null) tabRefs.current.delete(entry.id)\n else tabRefs.current.set(entry.id, node)\n }}\n type=\"button\"\n role=\"tab\"\n id={`${id}-tab-${entry.id}`}\n aria-selected={entry.id === tab}\n aria-controls={`${id}-list`}\n tabIndex={0}\n className={css.pickerTab}\n onClick={() => {\n setTab(entry.id)\n setHighlight(0)\n }}\n onKeyDown={onTabKeyDown}\n onFocus={() => {\n if (entry.id !== tab) {\n setTab(entry.id)\n setHighlight(0)\n }\n }}\n >\n {entry.label}\n </button>\n ))}\n </div>\n <div\n ref={listRef}\n id={`${id}-list`}\n className={css.pickerList}\n role=\"menu\"\n aria-label={t('picker.title')}\n >\n {loading && <div className={css.notice}>{t('status.loading')}</div>}\n {failed && (\n <div className={css.notice}>\n <span>{t('status.failed', { message: state.error ?? '' })}</span>\n <button type=\"button\" className={css.retry} onClick={onReload}>{t('action.reload')}</button>\n </div>\n )}\n {failures.map((failure) => (\n <div className={css.notice} key={failure.id}>\n <span>{t('status.providerFailed', { name: failure.name, message: failure.message })}</span>\n <button type=\"button\" className={css.retry} onClick={onReload}>{t('action.reload')}</button>\n </div>\n ))}\n {visible.length === 0 && !loading && !failed && (\n <div className={css.notice}>{state.groups.length === 0 ? t('status.empty') : t('picker.searchEmpty')}</div>\n )}\n {visible.map((row, index) => (\n <div key={`${row.provider}/${row.model}`} data-row-index={index} id={`${id}-row-${index}`}>\n <ModelRow\n name={row.entry.name}\n selected={live?.provider === row.provider && live.model === row.model}\n busy={busy}\n star=\"interactive\"\n favorited={isFavorite(favorites, row.provider, row.model)}\n meta={metaLine({\n row,\n facts: facts(row.provider, row.model),\n providerName: tab === ALL_TAB ? row.providerName : undefined,\n thinks: row.entry.reasoning !== undefined,\n effortLabel: t('meta.effort'),\n imageLabel: t('meta.images'),\n })}\n starLabel={isFavorite(favorites, row.provider, row.model)\n ? t('picker.unfavorite', { model: row.entry.name })\n : t('picker.favorite', { model: row.entry.name })}\n starTabStop={index === highlight}\n rowTabIndex={-1}\n onPick={() => { pick(row) }}\n onFocusRow={() => { setHighlight(index) }}\n onToggleFavorite={() => { onToggleFavorite(row.provider, row.model) }}\n />\n </div>\n ))}\n </div>\n <div className={css.pickerFooter}>\n <span className={css.footerHint}>\n <ShortcutKeys keys={['↑', '↓']} />\n <span>{t('picker.hintNavigate')}</span>\n <span className={css.footerDot}>·</span>\n <ShortcutKeys keys={['Enter']} />\n <span>{t('picker.hintSelect')}</span>\n <span className={css.footerDot}>·</span>\n <ShortcutKeys keys={['Esc']} />\n <span>{t('picker.hintClose')}</span>\n </span>\n </div>\n </Modal>\n )\n}\n\nfunction metaLine(input: {\n readonly row: CatalogRow\n readonly facts: ModelFacts\n readonly providerName: string | undefined\n readonly thinks: boolean\n readonly effortLabel: string\n readonly imageLabel: string\n}): string {\n return metaSegments({\n ...input.providerName === undefined ? {} : { providerName: input.providerName },\n facts: input.facts,\n thinks: input.thinks,\n effortLabel: input.effortLabel,\n imageLabel: input.imageLabel,\n }).join(' · ')\n}\n\n","/**\n * The per-model reasoning level this browser last chose.\n *\n * The Host stores one level per session, so switching models away and back\n * forgets what was picked for the first one. This module keeps that memory\n * beside the favorite table, keyed the same way: provider route key plus model\n * id. Remembering the provider default removes the row, so \"follow the\n * provider\" stays distinguishable from \"remembered a level\".\n */\n\n/** One remembered reasoning level. */\nexport interface RememberedEffort {\n readonly provider: string\n readonly model: string\n readonly effort: string\n}\n\n/** The single browser-wide storage key holding the whole memory. */\nexport const EFFORTS_KEY = 'dsh-model-submenu.efforts'\n\n/** The part of `Storage` this module uses; absent or throwing reads mean \"none\". */\nexport type EffortStorage = Pick<Storage, 'getItem' | 'setItem'>\n\n/**\n * Read the memory, keeping only rows naming both halves and a level.\n * @param storage - browser storage, or undefined when the browser withholds it.\n * @returns the remembered rows; empty when unreadable.\n */\nexport function readEfforts(storage: EffortStorage | undefined): RememberedEffort[] {\n const raw = getItem(storage)\n if (raw === null) return []\n let parsed: unknown\n try {\n parsed = JSON.parse(raw)\n } catch {\n return []\n }\n if (!Array.isArray(parsed)) return []\n return parsed.filter((row): row is RememberedEffort => typeof row === 'object' && row !== null\n && typeof (row as RememberedEffort).provider === 'string'\n && typeof (row as RememberedEffort).model === 'string'\n && typeof (row as RememberedEffort).effort === 'string'\n && (row as RememberedEffort).effort !== '')\n}\n\n/**\n * @param memory - the rows to search.\n * @param provider - the provider route key.\n * @param model - the model id inside that provider.\n * @returns the level last chosen for that exact model, or undefined.\n */\nexport function effortFor(\n memory: readonly RememberedEffort[],\n provider: string,\n model: string,\n): string | undefined {\n return memory.find((row) => row.provider === provider && row.model === model)?.effort\n}\n\n/**\n * Remember one model's level, or forget it when the choice is the provider default.\n * @param storage - browser storage, or undefined when writes are unavailable.\n * @param provider - the provider route key.\n * @param model - the model id inside that provider.\n * @param effort - the chosen level, undefined meaning the provider default.\n * @returns the memory after the change, whether or not the write landed.\n */\nexport function rememberEffort(\n storage: EffortStorage | undefined,\n provider: string,\n model: string,\n effort: string | undefined,\n): RememberedEffort[] {\n const rest = readEfforts(storage).filter((row) => row.provider !== provider || row.model !== model)\n const next = effort === undefined ? rest : [...rest, { provider, model, effort }]\n try {\n storage?.setItem(EFFORTS_KEY, JSON.stringify(next))\n } catch {\n // A full quota or a locked-down browser loses the memory, not the menu.\n }\n return next\n}\n\nfunction getItem(storage: EffortStorage | undefined): string | null {\n try {\n return storage?.getItem(EFFORTS_KEY) ?? null\n } catch {\n return null\n }\n}\n","import { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react'\nimport { createPortal } from 'react-dom'\nimport {\n IconChevronDownOutlineRegular,\n IconFlatListOutlineRegular,\n IconWarningOutlineRegular,\n MenuSurface,\n Toast,\n} from '@deepseek-ai/dsh-client-ui-primitives'\nimport type { ModelDirectoryState, ModelSelectInjected } from '@deepseek-ai/dsh-client-ui-model-selection/client'\nimport type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'\nimport { ModelPicker } from './ModelPicker.tsx'\nimport { ModelRow } from './ModelRow.tsx'\nimport type { FactsSource } from './facts-source.ts'\nimport {\n isFavorite,\n orderWithFavorites,\n readFavorites,\n toggleFavorite,\n type FavoriteModel,\n type FavoritesStorage,\n} from './favorites.ts'\nimport { effortFor, readEfforts, rememberEffort, type RememberedEffort } from './effort-memory.ts'\nimport type { EffortChoice, Selection } from './selection.ts'\nimport { reasoningOf } from './rows.ts'\nimport { effortChoices, planEffortCommit, planModelCommit } from './selection.ts'\nimport css from './ModelSubmenu.module.css'\n\ntype ModelSubmenuProps = ModelSelectInjected & { locked: boolean } & PropsLocale<'modelSubmenu'> & {\n /** The live settings-derived metadata the picker's line reads. */\n readonly factsSource: FactsSource\n}\n\ntype ProviderGroup = ModelDirectoryState['groups'][number]\ntype CatalogModel = ProviderGroup['models'][number]\n\n/** Unplaced portal card: hidden but laid out at a fixed origin so offsetWidth/offsetHeight are real. */\nconst MEASURE_STYLE = {\n visibility: 'hidden',\n left: 0,\n top: 0,\n} as const\n\nconst cls = (...parts: readonly (string | false | undefined)[]): string => parts.filter((part) => part !== false && part !== undefined).join(' ')\n\n/**\n * The composer's model seat (`conversation.input.model`), taken over from the\n * built-in occupant with a provider-scoped menu: one provider's models above,\n * the reasoning row as segments, the cache warning, and the door to the picker\n * that lists every provider.\n *\n * Every click commits at once: a model, a reasoning level, or the picker door.\n * Closing the menu never writes anything, and the picker reads the level the\n * Host already accepted, so its \"current\" reading is never stale.\n */\nexport function ModelSubmenu({ locked, available, directory, load, select, t, factsSource }: ModelSubmenuProps) {\n const state = useSyncExternalStore((fn) => directory.subscribe(fn), () => directory.getSnapshot())\n const [open, setOpen] = useState(false)\n const [pickerOpen, setPickerOpen] = useState(false)\n const [favorites, setFavorites] = useState<readonly FavoriteModel[]>(() => readFavorites(storageOf()))\n const [memory, setMemory] = useState<readonly RememberedEffort[]>(() => readEfforts(storageOf()))\n const [toast, setToast] = useState<{ seq: number; text: string } | null>(null)\n const toastSeq = useRef(0)\n const rootRef = useRef<HTMLDivElement | null>(null)\n const triggerRef = useRef<HTMLButtonElement | null>(null)\n const menuRef = useRef<HTMLDivElement | null>(null)\n const [menuPos, setMenuPos] = useState<{ left: number; top: number } | null>(null)\n const id = useId()\n\n const groups = state.groups\n const current = state.current\n const currentGroup = current === null ? undefined : groups.find((group) => group.id === current.provider)\n const currentModel = currentGroup?.models.find((model) => model.id === current?.model)\n const reasoning = currentModel?.reasoning\n const effectiveEffort = current?.reasoningEffort ?? reasoning?.defaultEffort\n /**\n * The trigger's caption. A model that advertises no levels still says so in\n * the same place: the blue caption beside the name reads 默认 rather than\n * disappearing, so the shape of the control never changes with the model.\n */\n const effortLabel = reasoning === undefined || effectiveEffort === undefined\n ? t('effort.providerDefault')\n : reasoning.efforts.find((level) => level.id === effectiveEffort)?.name ?? effectiveEffort\n const choices = useMemo(() => effortChoices(reasoning, t('effort.providerDefault')), [reasoning, t])\n const busy = state.status === 'selecting'\n const live = useMemo(() => selectionOf(state), [state])\n const reason = useMemo(() => reasoningOf(groups), [groups])\n const facts = useSyncExternalStore((fn) => factsSource.subscribe(fn), () => factsSource.getSnapshot())\n\n const menuRows = useMemo(() => currentGroup === undefined ? [] : orderWithFavorites(\n currentGroup.models,\n (model) => isFavorite(favorites, currentGroup.id, model.id),\n ), [currentGroup, favorites])\n\n const raiseToast = (text: string) => {\n toastSeq.current += 1\n setToast({ seq: toastSeq.current, text })\n }\n const reload = () => {\n load()\n }\n const submit = (selection: Selection, onDone: (() => void) | null, onRejected?: () => void) => {\n select(selection).then((result) => {\n if (result === undefined || result.ok) {\n // The Host accepted it, so this is now the level this model uses.\n remember(selection.provider, selection.model, selection.reasoningEffort)\n onDone?.()\n return\n }\n onRejected?.()\n raiseToast(result.error.code === 'session/writer-held'\n ? t('error.sessionInUse')\n : t('error.action', { message: `${result.error.code}: ${result.error.message}` }))\n })\n }\n /** Close the menu. Closing writes nothing: every choice already committed on click. */\n const closeMenu = useCallback((restoreFocus: boolean) => {\n setOpen(false)\n if (restoreFocus) queueMicrotask(() => { triggerRef.current?.focus() })\n }, [])\n\n useEffect(() => {\n if (!open) return\n const closeOutside = (event: MouseEvent) => {\n if (rootRef.current?.contains(event.target as Node) === true) return\n if (menuRef.current?.contains(event.target as Node) === true) return\n closeMenu(false)\n }\n document.addEventListener('mousedown', closeOutside)\n return () => {\n document.removeEventListener('mousedown', closeOutside)\n }\n }, [open, closeMenu])\n\n useEffect(() => {\n if (!open) return\n const frame = requestAnimationFrame(() => {\n const checked = menuRef.current?.querySelector<HTMLElement>('[role=\"menuitemradio\"][aria-checked=\"true\"]:not([disabled])')\n const first = menuRef.current?.querySelector<HTMLElement>('[role=\"menuitemradio\"]:not([disabled])')\n const target = checked ?? first ?? triggerRef.current\n target?.scrollIntoView({ block: 'nearest' })\n target?.focus()\n })\n return () => { cancelAnimationFrame(frame) }\n }, [open])\n\n useLayoutEffect(() => {\n if (!open) {\n setMenuPos(null)\n return\n }\n const place = () => {\n const rect = triggerRef.current?.getBoundingClientRect()\n if (rect === undefined) return\n const MARGIN = 12\n const lw = menuRef.current?.offsetWidth ?? 0\n const lh = menuRef.current?.offsetHeight ?? 0\n // Right edges align, so the card grows leftward from the trigger's corner.\n let x = rect.right - lw\n let y = rect.top - 8 - lh\n if (lw > 0) x = Math.min(Math.max(x, MARGIN), window.innerWidth - lw - MARGIN)\n if (lh > 0) y = Math.min(Math.max(y, MARGIN), window.innerHeight - lh - MARGIN)\n setMenuPos({ left: x, top: y })\n }\n place()\n window.addEventListener('scroll', place, true)\n window.addEventListener('resize', place)\n return () => {\n window.removeEventListener('scroll', place, true)\n window.removeEventListener('resize', place)\n }\n }, [open, state])\n\n if (!available) return null\n\n const show = () => {\n setOpen(true)\n reload()\n }\n /** The picker door: the level already landed on click, so only the surface changes. */\n const openPicker = () => {\n setOpen(false)\n setPickerOpen(true)\n }\n /** Record one model's level, so switching away and back restores it. */\n const remember = (provider: string, model: string, effort: string | undefined) => {\n setMemory(rememberEffort(storageOf(), provider, model, effort))\n }\n const chooseModel = (provider: string, model: CatalogModel) => {\n const picked = planModelCommit(live, provider, model.id, reason, effortFor(memory, provider, model.id))\n if (picked === null) {\n setOpen(false)\n queueMicrotask(() => { triggerRef.current?.focus() })\n return\n }\n // A model click is the decision: close at once, and keep the menu open when\n // the host rejects it so the user can try another model.\n setOpen(false)\n submit(picked, () => { queueMicrotask(() => { triggerRef.current?.focus() }) }, () => {\n // The host refused it: put the menu back so the user can pick another.\n setOpen(true)\n })\n }\n const rowButtons = () => [...(menuRef.current?.querySelectorAll<HTMLButtonElement>('[role=\"menuitemradio\"]:not([disabled])') ?? [])]\n const moveFocus = (offset: number) => {\n const items = rowButtons()\n if (items.length === 0) return\n const active = items.findIndex((item) => item === document.activeElement)\n items[active === -1 ? offset > 0 ? 0 : items.length - 1 : (active + offset + items.length) % items.length]?.focus()\n }\n const onEffortKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return\n if (choices.length === 0 || live === null) return\n event.preventDefault()\n const index = choices.findIndex((choice) => choice.effort === effectiveEffort)\n const step = event.key === 'ArrowRight' ? 1 : -1\n const next = choices[((index === -1 ? 0 : index) + step + choices.length) % choices.length]\n if (next === undefined) return\n const pending = planEffortCommit(live, next.effort)\n if (pending !== null) submit(pending, null)\n }\n const onRootKeyDown = (event: React.KeyboardEvent) => {\n if (event.key === 'Escape' && open) {\n event.preventDefault()\n closeMenu(true)\n return\n }\n if (!open) return\n if (event.key === 'Tab') {\n if (event.shiftKey) {\n event.preventDefault()\n closeMenu(true)\n return\n }\n const focused = document.activeElement\n const rows = rowButtons()\n if (focused instanceof HTMLButtonElement && rows.includes(focused)) {\n event.preventDefault()\n focused.click()\n return\n }\n if (focused !== triggerRef.current) return\n event.preventDefault()\n const checked = menuRef.current?.querySelector<HTMLElement>('[role=\"menuitemradio\"][aria-checked=\"true\"]:not([disabled])')\n ;(checked ?? rows[0])?.focus()\n return\n }\n if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {\n event.preventDefault()\n moveFocus(event.key === 'ArrowDown' ? 1 : -1)\n }\n }\n /**\n * The keyboard leaving the seat closes the menu — but that is not one of the\n * four closing gestures that write a mark, so it closes without committing.\n * Otherwise clicking the notice text or a divider inside the menu, which moves\n * focus without unmounting it, would land a level nobody asked to commit.\n */\n const onBlur = (event: React.FocusEvent) => {\n if (pickerOpen) return\n if (event.relatedTarget instanceof Node\n && (rootRef.current?.contains(event.relatedTarget) === true || menuRef.current?.contains(event.relatedTarget) === true)) return\n setOpen(false)\n }\n const toggleFavoriteOf = (provider: string, model: string) => {\n setFavorites(toggleFavorite(storageOf(), provider, model))\n }\n\n const waiting = current === null && state.status === 'loading'\n const modelLabel = waiting ? t('trigger.loading') : currentModel?.name ?? (current === null ? t('trigger.fallback') : `${current.provider}/${current.model}`)\n const triggerAria = waiting\n ? t('trigger.loading')\n : current === null\n ? t('trigger.selectAria')\n : effortLabel === undefined\n ? t('trigger.aria', { model: modelLabel })\n : t('trigger.ariaEffort', { model: modelLabel, effort: effortLabel })\n const providerName = currentGroup?.name\n ?? state.failures.find((failure) => failure.id === current?.provider)?.name\n ?? current?.provider\n ?? ''\n const providerFailure = current === null ? undefined : state.failures.find((failure) => failure.id === current.provider)\n\n return (\n <div ref={rootRef} className={css.root} onKeyDown={onRootKeyDown} onBlur={onBlur}>\n <button\n ref={triggerRef}\n type=\"button\"\n className={css.trigger}\n aria-label={triggerAria}\n aria-haspopup=\"menu\"\n aria-expanded={open}\n aria-controls={open ? `${id}-menu` : undefined}\n title={effortLabel === undefined ? modelLabel : `${modelLabel} · ${effortLabel}`}\n disabled={locked}\n onClick={() => {\n if (open) closeMenu(false)\n else show()\n }}\n >\n <IconFlatListOutlineRegular className={css.triggerIcon} size={16} />\n <span className={css.triggerLabel}>{modelLabel}</span>\n {effortLabel !== undefined && <span className={css.triggerEffort}>{effortLabel}</span>}\n <IconChevronDownOutlineRegular className={cls(css.chevron, open && css.chevronOpen)} />\n </button>\n {open && createPortal((\n <MenuSurface\n ref={menuRef}\n id={`${id}-menu`}\n className={css.menu}\n style={menuPos ?? MEASURE_STYLE}\n role=\"group\"\n aria-label={t('menu.aria')}\n aria-busy={state.status === 'loading' || busy}\n >\n <div className={css.providerName} title={providerName}>{providerName}</div>\n {state.status === 'loading' && <div className={css.notice}>{t('status.loading')}</div>}\n {state.error !== null && state.status === 'error' && (\n <div className={css.notice}>\n <span>{t('status.failed', { message: state.error })}</span>\n <button type=\"button\" className={css.retry} onClick={reload}>{t('action.reload')}</button>\n </div>\n )}\n {providerFailure !== undefined && (\n <div className={css.notice}>\n <span>{t('status.providerFailed', { name: providerFailure.name, message: providerFailure.message })}</span>\n <button type=\"button\" className={css.retry} onClick={reload}>{t('action.reload')}</button>\n </div>\n )}\n <div className={cls(css.groups, 'scrollable')} role=\"menu\" aria-label={t('menu.aria')}>\n {menuRows.map((model) => (\n <ModelRow\n key={model.id}\n name={model.name}\n selected={current !== null && currentGroup !== undefined && current.provider === currentGroup.id && current.model === model.id}\n busy={busy}\n star=\"static\"\n favorited={currentGroup !== undefined && isFavorite(favorites, currentGroup.id, model.id)}\n onPick={() => {\n if (currentGroup !== undefined) chooseModel(currentGroup.id, model)\n }}\n />\n ))}\n {menuRows.length === 0 && state.status === 'ready' && (\n <div className={css.notice}>{t('status.empty')}</div>\n )}\n </div>\n {choices.length > 0 && (\n <>\n <div className={css.divider} />\n <div className={css.effortRow}>\n <span className={css.effortHeading}>{t('menu.effort')}</span>\n <div className={css.effortTrack} role=\"radiogroup\" aria-label={t('menu.effort')} onKeyDown={onEffortKeyDown}>\n {choices.map((choice: EffortChoice) => (\n <button\n key={choice.key}\n type=\"button\"\n role=\"radio\"\n aria-checked={choice.effort === effectiveEffort}\n className={css.effortSegment}\n disabled={busy}\n onClick={() => {\n if (live !== null) {\n const pending = planEffortCommit(live, choice.effort)\n if (pending !== null) submit(pending, null)\n }\n }}\n >\n {choice.label}\n </button>\n ))}\n </div>\n </div>\n <div className={css.divider} />\n </>\n )}\n <p className={css.hint}>{t('menu.hint')}</p>\n <div className={css.divider} />\n <button type=\"button\" role=\"menuitem\" className={css.moreRow} onClick={openPicker}>\n <IconFlatListOutlineRegular className={css.moreIcon} size={16} />\n <span>{t('menu.more')}</span>\n </button>\n </MenuSurface>\n ), document.body)}\n <ModelPicker\n open={pickerOpen}\n state={state}\n live={live}\n facts={facts}\n favorites={favorites}\n busy={busy}\n t={t}\n onPick={(selection) => { submit(selection, () => { setPickerOpen(false) }) }}\n onToggleFavorite={toggleFavoriteOf}\n onReload={reload}\n onClose={() => {\n setPickerOpen(false)\n queueMicrotask(() => { triggerRef.current?.focus() })\n }}\n />\n {toast !== null && (\n <Toast\n key={toast.seq}\n text={toast.text}\n icon={<IconWarningOutlineRegular />}\n anchor={rootRef.current?.closest('[data-composer-card]') ?? null}\n onDone={() => {\n setToast(null)\n }}\n />\n )}\n </div>\n )\n}\n\n/** The selection in force, or null while the host has not resolved one. */\nfunction selectionOf(state: ModelDirectoryState): Selection | null {\n if (state.current === null) return null\n return state.current.reasoningEffort === undefined\n ? { provider: state.current.provider, model: state.current.model }\n : { provider: state.current.provider, model: state.current.model, reasoningEffort: state.current.reasoningEffort }\n}\n\n/** Isolate the browser's storage so a locked-down browser reads as \"no favorites\". */\nfunction storageOf(): FavoritesStorage | undefined {\n try {\n return globalThis.localStorage\n } catch {\n return undefined\n }\n}\n","/**\n * The browser's live answer to \"what does the settings document know about this\n * model?\".\n *\n * Two host answers meet here: the shared settings mirror (which the settings\n * page already keeps warm) and the LLM provider directory that says which\n * settings namespace holds each provider's profile. Both can change while the\n * page is open — a saved profile, a reconnected host — so the join is rebuilt\n * whenever the mirror or the directory reports a change, and readers subscribe\n * to the lookup itself.\n */\n\nimport type { ModelFacts } from './model-facts.ts'\nimport {\n buildModelFacts,\n type ConfigurableProviderRow,\n type NamespaceRow,\n} from './model-facts.ts'\n\n/** One lookup answering both metadata fields for a provider and model. */\nexport type ModelFactsLookup = (provider: string, model: string) => ModelFacts\n\n/** The mirror face this source reads; a subset of the settings provider's. */\nexport interface DescribeFace {\n getSnapshot(): { readonly view: { readonly namespaces: readonly NamespaceRow[] } | undefined }\n subscribe(listener: () => void): () => void\n ensure(): Promise<void>\n}\n\n/** One remote answer, as the gateway returns it. */\nexport interface DirectoryAnswer {\n readonly ok: boolean\n readonly value?: readonly ConfigurableProviderRow[]\n}\n\n/** A stable, subscribable lookup over the current settings answer. */\nexport interface FactsSource {\n /** @returns the lookup for the current answer; the identity changes on every rebuild. */\n getSnapshot(): ModelFactsLookup\n /** @param listener - called after each rebuild. @returns the disposer. */\n subscribe(listener: () => void): () => void\n}\n\nconst EMPTY: ModelFactsLookup = () => ({ contextWindow: undefined, images: 'unknown' })\n\n/**\n * Join the settings mirror with the provider directory.\n * @param deps.describe - the settings mirror's describe face.\n * @param deps.listProviders - reads the LLM provider directory once.\n * @returns the source, plus `start` which wires both reads and returns its disposer.\n */\nexport function createFactsSource(deps: {\n readonly describe: DescribeFace\n readonly listProviders: () => Promise<DirectoryAnswer>\n}): { readonly source: FactsSource; readonly start: () => () => void } {\n const listeners = new Set<() => void>()\n let providers: readonly ConfigurableProviderRow[] = []\n let lookup: ModelFactsLookup = EMPTY\n\n const rebuild = () => {\n const namespaces = deps.describe.getSnapshot().view?.namespaces ?? []\n lookup = providers.length === 0 || namespaces.length === 0\n ? EMPTY\n : buildModelFacts(providers, namespaces)\n for (const listener of [...listeners]) listener()\n }\n\n return {\n source: {\n getSnapshot: () => lookup,\n subscribe: (listener) => {\n listeners.add(listener)\n return () => { listeners.delete(listener) }\n },\n },\n start: () => {\n const off = deps.describe.subscribe(rebuild)\n void deps.describe.ensure().catch(() => { /* a failed read leaves the empty lookup in place */ })\n void deps.listProviders().then(\n (answer) => {\n providers = answer.ok && answer.value !== undefined ? answer.value : []\n rebuild()\n },\n () => { /* the directory being unavailable leaves every model's facts unknown */ },\n )\n rebuild()\n return () => {\n off()\n listeners.clear()\n }\n },\n }\n}\n","/** Locale bundles for the composer model menu and the picker it opens. */\n\n/** Dictionary namespace owned by this plugin. */\nexport const NS = 'modelSubmenu'\n\n/** Locale keys the menu and the picker render. */\nexport type ModelSubmenuLocaleKey =\n | 'trigger.fallback'\n | 'trigger.loading'\n | 'trigger.selectAria'\n | 'trigger.aria'\n | 'trigger.ariaEffort'\n | 'menu.aria'\n | 'menu.effort'\n | 'menu.more'\n | 'menu.hint'\n | 'effort.providerDefault'\n | 'status.loading'\n | 'status.empty'\n | 'status.failed'\n | 'status.providerFailed'\n | 'action.reload'\n | 'error.action'\n | 'error.sessionInUse'\n | 'picker.title'\n | 'picker.close'\n | 'picker.search'\n | 'picker.searchEmpty'\n | 'picker.all'\n | 'picker.favorite'\n | 'picker.unfavorite'\n | 'picker.hintNavigate'\n | 'picker.hintSelect'\n | 'picker.hintClose'\n | 'meta.effort'\n | 'meta.images'\n\ndeclare module '@deepseek-ai/dsh-client-ui-slots' {\n interface LocaleNamespaceMap {\n /** The composer model menu's copy. */\n modelSubmenu: ModelSubmenuLocaleKey\n }\n}\n\n/** Simplified Chinese dictionary (the key-set source of truth). */\nexport const zh: Record<ModelSubmenuLocaleKey, string> = {\n 'trigger.fallback': '选择模型',\n 'trigger.loading': '正在加载模型…',\n 'trigger.selectAria': '选择模型',\n 'trigger.aria': '选择模型,当前 {model}',\n 'trigger.ariaEffort': '选择模型,当前 {model},思考等级 {effort}',\n 'menu.aria': '模型与思考等级',\n 'menu.effort': '思考',\n 'menu.more': '更多模型…',\n 'menu.hint': '提示:切换模型或思考程度会使既有的提示词缓存失效。建议新建会话,避免额外的 token 消耗。',\n 'effort.providerDefault': '默认',\n 'status.loading': '正在加载模型…',\n 'status.empty': '没有可用的模型。',\n 'status.failed': '模型列表加载失败:{message}',\n 'status.providerFailed': '{name} 加载失败:{message}',\n 'action.reload': '重新加载',\n 'error.action': '模型操作失败:{message}',\n 'error.sessionInUse': '当前会话已被占用,可能是其他正在运行的 DSH 导致的(如其他 dsh web、桌面端),请退出其他正在运行的 DSH 后重试。',\n 'picker.title': '切换模型',\n 'picker.close': '关闭',\n 'picker.search': '搜索模型或提供商…',\n 'picker.searchEmpty': '没有匹配的模型。',\n 'picker.all': '全部',\n 'picker.favorite': '收藏 {model}',\n 'picker.unfavorite': '取消收藏 {model}',\n 'picker.hintNavigate': '导航',\n 'picker.hintSelect': '选择',\n 'picker.hintClose': '关闭',\n 'meta.effort': '思考',\n 'meta.images': '图片输入',\n}\n\n/** English dictionary, checked complete against the zh key set. */\nexport const en: Record<ModelSubmenuLocaleKey, string> = {\n 'trigger.fallback': 'Select model',\n 'trigger.loading': 'Loading models…',\n 'trigger.selectAria': 'Select model',\n 'trigger.aria': 'Select model, current {model}',\n 'trigger.ariaEffort': 'Select model, current {model}, effort {effort}',\n 'menu.aria': 'Model and effort',\n 'menu.effort': 'Effort',\n 'menu.more': 'More models…',\n 'menu.hint': 'Changing the model or the effort invalidates the existing prompt cache. Start a new session to avoid the extra token cost.',\n 'effort.providerDefault': 'Default',\n 'status.loading': 'Loading models…',\n 'status.empty': 'No models available.',\n 'status.failed': 'Model list failed to load: {message}',\n 'status.providerFailed': '{name} failed to load: {message}',\n 'action.reload': 'Reload',\n 'error.action': 'Model operation failed: {message}',\n '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.',\n 'picker.title': 'Switch model',\n 'picker.close': 'Close',\n 'picker.search': 'Search models or providers…',\n 'picker.searchEmpty': 'No matching models.',\n 'picker.all': 'All',\n 'picker.favorite': 'Favorite {model}',\n 'picker.unfavorite': 'Remove {model} from favorites',\n 'picker.hintNavigate': 'navigate',\n 'picker.hintSelect': 'select',\n 'picker.hintClose': 'close',\n 'meta.effort': 'Effort',\n 'meta.images': 'Images',\n}\n","import type { Context as ClientContext } from '@deepseek-ai/cordis'\n// Type-only: `slots` and its LocaleNamespaceMap merge come from the renderer and\n// the slot package; the composer seat's SlotMap entry and owner props from\n// ui-conversation; the `modelDirectories` service and the seat's injected face\n// from ui-model-selection's browser half; `configForms` from the settings\n// provider, whose shared mirror answers the metadata line.\nimport type {} from '@deepseek-ai/dsh-client-ui-renderer/client'\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\nimport type {} from '@deepseek-ai/dsh-api-remotes/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-model-selection/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-settings/client'\nimport { ModelSubmenu } from './ModelSubmenu.tsx'\nimport { createFactsSource, type DescribeFace } from './facts-source.ts'\nimport { NS, en, zh } from './locales.ts'\n\nexport { ModelSubmenu } from './ModelSubmenu.tsx'\n\n/**\n * Cordis services required by the browser half. `slots` is the contribution\n * registry, `sessions` tells whether a session may select a model,\n * `modelDirectories` owns the per-session catalog snapshot this seat renders\n * from, and `configForms` carries the settings provider's shared describe\n * mirror, which answers each model's context window and accepted modalities.\n * `remote`, `remote.llm`, and `remote.session` are declared because a service\n * method runs on the calling context: `directoryFor` reads\n * `this.ctx.remote.session`, and the provider directory that names each\n * settings address is read through `this.ctx.remote.llm`.\n */\nexport const inject = [\n 'configForms',\n 'locale',\n 'modelDirectories',\n 'remote',\n 'remote.llm',\n 'remote.session',\n 'sessions',\n 'slots',\n]\n\n/**\n * Client plugin body: register this plugin's dictionaries, then take over the\n * composer model seat below the built-in occupant's priority so this menu\n * renders in its place.\n * @param ctx - client root context.\n */\nexport function apply(ctx: ClientContext): void {\n ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-model-submenu: dictionaries')\n ctx.inject(['slots', 'modelDirectories', 'configForms', 'remote'], (scope) => {\n const models = scope.modelDirectories\n const sessions = scope.sessions\n const facts = createFactsSource({\n describe: scope.configForms.describe() as DescribeFace,\n listProviders: () => scope.remote.llm.listConfigurableProviders(),\n })\n ctx.effect(() => facts.start(), 'dsh-model-submenu: model metadata')\n scope.slots.inject('conversation.input.model', () => scope.slots.register({\n name: 'conversation.input.model',\n priority: -100,\n registrant: '@lynn123411/dsh-model-submenu',\n locale: NS,\n inject: (sessionId) => {\n // The seat's inject parameter is typed as a bare string; the service\n // faces carry the branded SessionId of that same runtime value.\n const key = sessionId as Parameters<typeof models.directoryFor>[0]\n const directory = models.directoryFor(key)\n const available = sessions.subagentAddress(key) === undefined\n return {\n available,\n directory: directory.store,\n factsSource: facts.source,\n load: () => {\n if (available) directory.load().catch(() => {})\n },\n select: (selection) => available ? directory.select(selection) : Promise.resolve(undefined),\n }\n },\n }, ModelSubmenu))\n })\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.js","names":["css","IconCheckOutlineRegular","getItem","useId","useRef","useState","useMemo","rankByName","isMobileDevice","Modal","css","Input","IconSearchOutlineRegular","Tag","ShortcutKeys","useSyncExternalStore","useState","useRef","useId","useMemo","Tag","css","useCallback","useAnchoredPosition","IconFlatListOutlineRegular","StateDot","IconChevronDownOutlineRegular","createPortal","MenuSurface","Toast","IconWarningOutlineRegular"],"sources":["../src/client/icons.tsx","../src/client/ModelRow.tsx","../src/client/favorites.ts","../src/client/model-facts.ts","../src/client/rows.ts","../src/client/selection.ts","../src/client/ModelPicker.tsx","../src/client/effort-memory.ts","../src/client/ModelSubmenu.tsx","../src/client/facts-source.ts","../src/client/locales.ts","../src/client/index.ts"],"sourcesContent":["/**\n * The one glyph this plugin draws itself.\n *\n * The shared icon library carries no star, so the favorite mark is defined here:\n * a five-point star whose outline and fill use the same path, which keeps the\n * hover-only outline and the standing filled state identical in geometry.\n */\n\n/**\n * Render the favorite star.\n * @param props.filled - draw the solid star; otherwise the outline.\n * @param props.size - square edge in pixels, matching the shared icon set's default.\n * @param props.className - extra class for colour and placement.\n * @returns the inline SVG.\n */\nexport function StarIcon({ filled, size = 14, className }: {\n readonly filled: boolean\n readonly size?: number | undefined\n readonly className?: string | undefined\n}) {\n return (\n <svg\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill={filled ? 'currentColor' : 'none'}\n stroke=\"currentColor\"\n strokeWidth={filled ? 0 : 1.6}\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n className={className}\n >\n <path d=\"M12 3.2l2.7 5.6 6.1.85-4.45 4.3 1.08 6.05L12 17.1l-5.43 2.9 1.08-6.05L3.2 9.65l6.1-.85z\" />\n </svg>\n )\n}\n","/**\n * One pickable model, as both surfaces render it.\n *\n * The menu row is one button that spans the whole width — name, favorite mark,\n * and the check for the model in use all live inside it, so hovering any part of\n * the row (including the star) highlights the row and clicking any part picks\n * the model. The picker row keeps the same shape but lifts the star out into its\n * own control, because favouriting there must not switch the model.\n */\n\nimport type { ReactNode } from 'react'\nimport { IconCheckOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives'\nimport { StarIcon } from './icons.tsx'\nimport css from './ModelSubmenu.module.css'\n\n/** How one row shows its favorite mark: part of the row, or a control of its own. */\nexport type StarMode = 'static' | 'interactive'\n\n/** Props shared by both row shapes. */\nexport interface ModelRowProps {\n /** The model's catalog name. */\n readonly name: string\n /** Optional catalog badge shown beside the name. */\n readonly badge?: ReactNode | undefined\n /** Whether this row is the model in use. */\n readonly selected: boolean\n /** Whether a selection is in flight, which locks the row. */\n readonly busy: boolean\n /** How the favorite mark behaves on this row. */\n readonly star: StarMode\n /** Whether the model is favorited right now. */\n readonly favorited: boolean\n /** The metadata line's text; undefined in the menu. */\n readonly meta?: string | undefined\n /** Localized accessible name for the star control. */\n readonly starLabel?: string | undefined\n /** Whether the star is a Tab stop right now (the picker's roving highlight). */\n readonly starTabStop?: boolean\n /** Tab order of the row itself; the picker keeps rows out of it so ↑/↓ own the walk. */\n readonly rowTabIndex?: number\n /** Pick this row. */\n readonly onPick: () => void\n /** The row took the keyboard, so its star becomes reachable. */\n readonly onFocusRow?: (() => void) | undefined\n /** Toggle this row's favorite; absent when the star is part of the row. */\n readonly onToggleFavorite?: (() => void) | undefined\n /** Extra row content, such as the picker's disabled reason. */\n readonly children?: ReactNode\n}\n\n/**\n * Render one model row.\n * @param props - the row's content and behaviour.\n * @returns the row element.\n */\nexport function ModelRow({\n name, badge, selected, busy, star, favorited, meta, starLabel,\n starTabStop = false, rowTabIndex, onPick, onFocusRow, onToggleFavorite, children,\n}: ModelRowProps) {\n const copy = (\n <>\n <span className={css.rowCopy}>\n <span className={css.rowName}>{name}</span>\n {badge}\n {meta !== undefined && meta !== '' && <span className={css.rowMeta}>{meta}</span>}\n </span>\n </>\n )\n const mark = favorited ? <StarIcon filled className={css.star} /> : null\n\n // The menu's row is one target: the star and the check sit inside the button,\n // so they inherit its hover fill and its click.\n if (star === 'static') {\n return (\n <button\n type=\"button\"\n role=\"menuitemradio\"\n aria-checked={selected}\n tabIndex={rowTabIndex}\n className={css.rowPick}\n title={name}\n disabled={busy}\n onClick={onPick}\n onFocus={onFocusRow}\n data-selected={selected ? '' : undefined}\n data-favorited={favorited ? '' : undefined}\n >\n {copy}\n {mark !== null && <span className={css.starStatic} aria-hidden=\"true\">{mark}</span>}\n <span className={css.check} aria-hidden={!selected}>{selected ? <IconCheckOutlineRegular /> : null}</span>\n {children}\n </button>\n )\n }\n\n return (\n <div\n className={css.row}\n data-selected={selected ? '' : undefined}\n data-favorited={favorited ? '' : undefined}\n >\n <button\n type=\"button\"\n role=\"menuitemradio\"\n aria-checked={selected}\n tabIndex={rowTabIndex}\n className={css.rowPick}\n title={name}\n disabled={busy}\n onClick={onPick}\n onFocus={onFocusRow}\n >\n {copy}\n </button>\n <button\n type=\"button\"\n className={css.starButton}\n aria-pressed={favorited}\n aria-label={starLabel}\n tabIndex={starTabStop ? 0 : -1}\n disabled={busy}\n onClick={onToggleFavorite}\n >\n <StarIcon filled={favorited} className={css.star} />\n </button>\n <span className={css.check} aria-hidden={!selected}>{selected ? <IconCheckOutlineRegular /> : null}</span>\n {children}\n </div>\n )\n}\n","/**\n * The favorite-model table the picker keeps in the browser.\n *\n * One browser-wide key holds `{ provider, model }` rows: the provider route key\n * and the model id are the catalog's own identity, so a favorite survives a\n * display-name change and never collides across providers. A value that cannot\n * be read or does not parse reads as an empty table — a privacy-mode browser or\n * a hand-edited row must never keep the menu from opening. Rows whose provider\n * leaves the catalog are kept, so restoring the provider restores the star.\n */\n\nimport type { ModelRef } from './rows.ts'\n\n/** One favorited model, identified the way the catalog identifies it. */\nexport type FavoriteModel = ModelRef\n\n/** The single browser-wide storage key holding the whole table. */\nexport const FAVORITES_KEY = 'dsh-model-submenu.favorites'\n\n/** The part of `Storage` this module uses; absent or throwing reads mean \"none\". */\nexport type FavoritesStorage = Pick<Storage, 'getItem' | 'setItem'>\n\n/**\n * Read the table, keeping only rows that name both halves.\n * @param storage - browser storage, or undefined when the browser withholds it.\n * @returns the stored favorites in stored order; empty when unreadable.\n */\nexport function readFavorites(storage: FavoritesStorage | undefined): FavoriteModel[] {\n const raw = getItem(storage)\n if (raw === null) return []\n let parsed: unknown\n try {\n parsed = JSON.parse(raw)\n } catch {\n return []\n }\n if (!Array.isArray(parsed)) return []\n return parsed.filter((row): row is FavoriteModel => typeof row === 'object' && row !== null\n && typeof (row as FavoriteModel).provider === 'string'\n && typeof (row as FavoriteModel).model === 'string')\n}\n\n/**\n * Add or remove one model, writing the whole table back.\n * @param storage - browser storage, or undefined when writes are unavailable.\n * @param provider - the provider route key.\n * @param model - the model id inside that provider.\n * @returns the table after the change, whether or not the write landed.\n */\nexport function toggleFavorite(\n storage: FavoritesStorage | undefined,\n provider: string,\n model: string,\n): FavoriteModel[] {\n const current = readFavorites(storage)\n const next = isFavorite(current, provider, model)\n ? current.filter((row) => row.provider !== provider || row.model !== model)\n : [...current, { provider, model }]\n try {\n storage?.setItem(FAVORITES_KEY, JSON.stringify(next))\n } catch {\n // A full quota or a locked-down browser loses the star, not the menu.\n }\n return next\n}\n\n/**\n * @param favorites - the table to search.\n * @param provider - the provider route key.\n * @param model - the model id inside that provider.\n * @returns whether that exact pair is favorited.\n */\nexport function isFavorite(\n favorites: readonly FavoriteModel[],\n provider: string,\n model: string,\n): boolean {\n return favorites.some((row) => row.provider === provider && row.model === model)\n}\n\n/**\n * Stable-partition a catalog so favorited rows lead, each part keeping the\n * catalog's own order.\n * @param rows - the catalog in its display order.\n * @param favorited - predicate answering whether one row is favorited.\n * @returns a new list; the input order when nothing is favorited.\n */\nexport function orderWithFavorites<T>(\n rows: readonly T[],\n favorited: (row: T) => boolean,\n): readonly T[] {\n const starred = rows.filter(favorited)\n if (starred.length === 0) return rows\n return [...starred, ...rows.filter((row) => !favorited(row))]\n}\n\nfunction getItem(storage: FavoritesStorage | undefined): string | null {\n try {\n return storage?.getItem(FAVORITES_KEY) ?? null\n } catch {\n return null\n }\n}\n","/**\n * What the settings document knows about one model, for the picker's metadata\n * line.\n *\n * The Host's model catalog carries only a name and a reasoning list; context\n * window and accepted modalities live in each provider's settings profile,\n * which the settings mirror already holds in the browser. This module joins\n * the two by the identity both agree on: the catalog's provider route key and\n * model id are exactly the settings profile's provider key and model id.\n *\n * Every field is answered independently. A provider whose profile lists no\n * models (an OAuth route such as the Kimi subscription, or the account\n * provider) answers `unknown` rather than guessing the route default, because\n * the line's rule is to omit what cannot be asserted.\n */\n\n/** Accepted-modality answer for one model. */\nexport type ImageAnswer = 'yes' | 'no' | 'unknown'\n\n/**\n * The model knowledge a shipped adapter carries for its own account route.\n *\n * The account route has no `models` list on purpose: the account listing\n * supplies the catalog and settings only carry per-model overrides. The same\n * adapter family ships a default catalog for its API-key route, and the account\n * route serves the same models, so those entries answer what the account\n * profile cannot. Ids and facts mirror the adapter's own table.\n */\nconst FAMILY_DEFAULTS: Readonly<Record<string, readonly {\n readonly id: string\n readonly contextWindow: number\n readonly images: boolean\n}[]>> = {\n 'deepseek-account': [\n { id: 'deepseek-flash', contextWindow: 1_000_000, images: true },\n { id: 'deepseek-v4-pro', contextWindow: 1_000_000, images: false },\n ],\n}\n\n/** The metadata the settings document can answer for one model. */\nexport interface ModelFacts {\n /** Exact advertised capacity, or undefined when the model declares none. */\n readonly contextWindow: number | undefined\n /** Whether the model accepts images; `unknown` means the document is silent. */\n readonly images: ImageAnswer\n}\n\n/** One provider directory row, as the Host's LLM directory reports it. */\nexport interface ConfigurableProviderRow {\n readonly provider: string\n readonly settingsNs: string\n readonly settingsPath: readonly string[]\n}\n\n/** One settings namespace view, as the describe answer carries it. */\nexport interface NamespaceRow {\n readonly ns: string\n readonly value: unknown\n}\n\n/** What one provider's profile may carry, across both adapter families. */\ninterface ProfileView {\n readonly models?: readonly Record<string, unknown>[]\n readonly defaultContextWindow?: unknown\n readonly defaultInput?: unknown\n}\n\ntype Record_ = Record<string, unknown>\n\n/**\n * Build the fact lookup from the settings answer.\n * @param providers - provider directory rows giving each route's settings address.\n * @param namespaces - the describe answer's namespace views.\n * @returns a lookup answering both fields for one provider and model.\n */\nexport function buildModelFacts(\n providers: readonly ConfigurableProviderRow[],\n namespaces: readonly NamespaceRow[],\n): (provider: string, model: string) => ModelFacts {\n const byName = new Map(namespaces.map((row) => [row.ns, row.value]))\n const profiles = new Map<string, ProfileView>()\n for (const row of providers) {\n const section = byName.get(row.settingsNs)\n const profile = readPath(section, row.settingsPath)\n if (isRecord(profile)) profiles.set(row.provider, profile as ProfileView)\n }\n return (provider, model) => {\n const profile = profiles.get(provider)\n const entry = profile?.models === undefined\n ? undefined\n : profile.models.find((candidate) => candidate.id === model)\n if (profile !== undefined && entry !== undefined) {\n const window = typeof entry.contextWindow === 'number' && entry.contextWindow > 0\n ? entry.contextWindow\n : undefined\n return { contextWindow: window, images: imageAnswer(entry, profile) }\n }\n // A route with no model list of its own (the account route) still serves its\n // adapter family's models; a declared override there wins over the default.\n const override = readOverride(profile, model)\n if (override !== undefined) {\n const window = typeof override.contextWindow === 'number' && override.contextWindow > 0\n ? override.contextWindow\n : undefined\n return {\n contextWindow: window,\n images: Array.isArray(override.inputModalities) ? containsImage(override.inputModalities) : 'unknown',\n }\n }\n const family = FAMILY_DEFAULTS[provider]?.find((candidate) => candidate.id === model)\n return family === undefined\n ? { contextWindow: undefined, images: 'unknown' }\n : { contextWindow: family.contextWindow, images: family.images ? 'yes' : 'no' }\n }\n}\n\n/**\n * Read one model entry's accepted image modalities.\n *\n * The two adapter families disagree about an omission. The Messages family\n * names the field `inputModalities`, and leaving it out means text only. The\n * pi-ai family names it `input`, and an empty or absent list defers to the\n * route's `defaultInput`; with no route default the answer stays unknown.\n */\nfunction imageAnswer(entry: Record_, profile: ProfileView): ImageAnswer {\n if (Array.isArray(entry.inputModalities)) return containsImage(entry.inputModalities)\n const defaultInput = Array.isArray(profile.defaultInput) ? profile.defaultInput : undefined\n if (Array.isArray(entry.input)) {\n if (entry.input.length > 0) return containsImage(entry.input)\n return defaultInput === undefined ? 'unknown' : containsImage(defaultInput)\n }\n return defaultInput === undefined ? 'no' : containsImage(defaultInput)\n}\n\n/** One model's declared override, when the route keeps a `modelOverrides` table. */\nfunction readOverride(profile: ProfileView | undefined, model: string): Record_ | undefined {\n const overrides = (profile as Record_ | undefined)?.modelOverrides\n if (!isRecord(overrides)) return undefined\n const entry = overrides[model]\n return isRecord(entry) ? entry : undefined\n}\n\nfunction containsImage(modalities: readonly unknown[]): ImageAnswer {\n return modalities.includes('image') ? 'yes' : 'no'\n}\n\n/**\n * Abbreviate one capacity the way the metadata line shows it.\n * @param tokens - exact advertised capacity in tokens.\n * @returns e.g. `1M ctx`, `1.5M ctx`, `272k ctx`, or `512 ctx` below a thousand.\n */\nexport function formatContextWindow(tokens: number): string {\n if (tokens >= 1_000_000) return `${trimNumber(tokens / 1_000_000)}M ctx`\n if (tokens >= 1_000) return `${Math.round(tokens / 1_000)}k ctx`\n return `${tokens} ctx`\n}\n\n/**\n * Build the metadata line's segments.\n * @param input.providerName - the provider's display name, omitted inside that provider's own tab.\n * @param input.facts - what the settings document answered.\n * @param input.thinks - whether the catalog advertises reasoning levels for this model.\n * @param input.effortLabel - the localized word for the reasoning segment.\n * @param input.imageLabel - the localized word for the image segment.\n * @returns the segments to join; an empty list means show no line.\n */\nexport function metaSegments(input: {\n readonly providerName?: string\n readonly facts: ModelFacts\n readonly thinks: boolean\n readonly effortLabel: string\n readonly imageLabel: string\n}): readonly string[] {\n const segments: string[] = []\n if (input.providerName !== undefined) segments.push(input.providerName)\n if (input.facts.contextWindow !== undefined) segments.push(formatContextWindow(input.facts.contextWindow))\n if (input.thinks) segments.push(input.effortLabel)\n if (input.facts.images === 'yes') segments.push(input.imageLabel)\n return segments\n}\n\nfunction trimNumber(value: number): string {\n const rounded = Math.round(value * 10) / 10\n return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1)\n}\n\nfunction readPath(value: unknown, path: readonly string[]): unknown {\n let cursor = value\n for (const key of path) {\n if (!isRecord(cursor)) return undefined\n cursor = cursor[key]\n }\n return cursor\n}\n\nfunction isRecord(value: unknown): value is Record_ {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n","/**\n * Catalog flattening, tab derivation, and query matching for the picker.\n *\n * The model catalog groups models by provider; the picker shows one flat list\n * with a tab per provider, so both surfaces read from these rows. Matching\n * delegates to the shell's own ranker, which is why a query behaves the same\n * here as in the built-in menu: case-insensitive, ordered subsequence, prefix\n * hits first.\n */\n\nimport type { ModelDirectoryState } from '@deepseek-ai/dsh-client-ui-model-selection/client'\n\n/** One provider group of the shared catalog. */\nexport type ProviderGroup = ModelDirectoryState['groups'][number]\n\n/** One model inside a provider group. */\nexport type CatalogModel = ProviderGroup['models'][number]\n\n/** One flat catalog row, carrying the provider identity beside the model. */\nexport interface ModelRow extends ModelRef {\n readonly providerName: string\n /** The model id inside its provider, the other half of a selection. */\n readonly model: string\n /** The catalog entry itself, for name and reasoning metadata. */\n readonly entry: CatalogModel\n}\n\n/** A provider and one of its models: the pair every selection and favorite names. */\nexport interface ModelRef {\n readonly provider: string\n readonly model: string\n}\n\n/** One picker tab. */\nexport interface PickerTab {\n readonly id: string\n readonly label: string\n}\n\n/** The tab id standing for every provider. */\nexport const ALL_TAB = '*'\n\n/**\n * Flatten the catalog to one row per model.\n * @param groups - the shared directory's provider groups, in display order.\n * @returns the rows in catalog order.\n */\nexport function flattenGroups(groups: readonly ProviderGroup[]): readonly ModelRow[] {\n return groups.flatMap((group) => group.models.map((model) => ({\n provider: group.id,\n providerName: group.name,\n model: model.id,\n entry: model,\n })))\n}\n\n/**\n * Build the picker's tabs: the all-tab, then one per provider that has a model.\n * @param groups - the shared directory's provider groups.\n * @param allLabel - localized label for the all-tab.\n * @returns the tabs in catalog order.\n */\nexport function pickerTabs(groups: readonly ProviderGroup[], allLabel = 'All'): readonly PickerTab[] {\n return [\n { id: ALL_TAB, label: allLabel },\n ...groups.filter((group) => group.models.length > 0).map((group) => ({ id: group.id, label: group.name })),\n ]\n}\n\n/**\n * @param rows - the flat catalog.\n * @param tab - the tab id; the all-tab keeps every row.\n * @returns the rows the tab shows, in their current order.\n */\nexport function rowsForTab(rows: readonly ModelRow[], tab: string): readonly ModelRow[] {\n return tab === ALL_TAB ? rows : rows.filter((row) => row.provider === tab)\n}\n\n/** One row handed to the shell's ranker, carrying both searchable keys. */\nexport interface SearchCandidate {\n readonly name: string\n readonly label: string\n readonly row: ModelRow\n}\n\n/**\n * Shape rows as the shell's ranker candidates: the model name is the name, the\n * provider display name is the second key, so a query reaches either.\n * @param rows - the rows to search.\n * @returns candidates in row order.\n */\nexport function searchCandidates(rows: readonly ModelRow[]): readonly SearchCandidate[] {\n return rows.map((row) => ({ name: row.entry.name, label: row.providerName, row }))\n}\n\n/**\n * Read the reasoning metadata the catalog advertises for one model.\n * @param groups - the shared directory's provider groups.\n * @returns the metadata, or undefined when that model advertises none.\n */\nexport function reasoningOf(\n groups: readonly ProviderGroup[],\n): (provider: string, model: string) => CatalogModel['reasoning'] {\n return (provider, model) => groups\n .find((group) => group.id === provider)\n ?.models.find((candidate) => candidate.id === model)\n ?.reasoning\n}\n\n/**\n * Filter rows by a query over the model name and the provider name.\n * @param rows - the rows to search.\n * @param query - the typed text; an empty or blank query keeps every row.\n * @param rank - the shell's own ranker, so this search behaves like every other one.\n * @returns the matching rows.\n */\nexport function searchRows(\n rows: readonly ModelRow[],\n query: string,\n rank: (items: readonly SearchCandidate[], query: string) => readonly SearchCandidate[],\n): readonly ModelRow[] {\n const trimmed = query.trim()\n if (trimmed === '') return rows\n return rank(searchCandidates(rows), trimmed).map((match) => match.row)\n}\n","/**\n * When the composer menu writes a selection.\n *\n * Both gestures are immediate: clicking a level submits it, clicking a model\n * submits it. The only quiet path is clicking the model already in use — that\n * closes the menu without a write. A model click also restores the level this\n * browser last used for that model, so switching away and back never silently\n * resets the effort.\n */\n\nimport type { ModelReasoning } from '@deepseek-ai/dsh-api-session-controller/types'\n\n/** One complete selection, as the directory's `select` takes it. */\nexport interface Selection {\n readonly provider: string\n readonly model: string\n readonly reasoningEffort?: string\n}\n\n/** One segment of the reasoning row. */\nexport interface EffortChoice {\n readonly key: string\n readonly effort: string | undefined\n readonly label: string\n}\n\n/**\n * Build the segments for one model's reasoning row.\n * @param reasoning - the catalog's reasoning metadata, undefined when the model has none.\n * @param providerDefaultLabel - the localized label for the \"follow the provider\" segment.\n * @returns the segments in catalog order; the provider-default one leads when the model declares no default.\n */\nexport function effortChoices(\n reasoning: ModelReasoning | undefined,\n providerDefaultLabel: string,\n): readonly EffortChoice[] {\n if (reasoning === undefined) return []\n return [\n ...reasoning.defaultEffort === undefined\n ? [{ key: 'provider-default', effort: undefined, label: providerDefaultLabel }]\n : [],\n ...reasoning.efforts.map((level) => ({\n key: level.id,\n effort: level.id as string | undefined,\n label: level.name,\n })),\n ]\n}\n\n/**\n * The selection a level click submits.\n * @param live - the selection in force right now.\n * @param effort - the clicked level, undefined meaning the provider default.\n * @returns the selection to submit, or null when the level already is the live one.\n */\nexport function planEffortCommit(live: Selection, effort: string | undefined): Selection | null {\n if (sameEffort(live.reasoningEffort, effort)) return null\n return {\n provider: live.provider,\n model: live.model,\n ...effort === undefined ? {} : { reasoningEffort: effort },\n }\n}\n\n/**\n * The selection a model click submits.\n * @param live - the selection in force right now, null before a first selection resolves.\n * @param provider - the clicked model's provider.\n * @param model - the clicked model id.\n * @param reasoningOf - the catalog's reasoning metadata for one provider and model.\n * @param remembered - the level this browser last used for the target model, if any.\n * @returns the selection to submit, or null when the click changes nothing.\n */\nexport function planModelCommit(\n live: Selection | null,\n provider: string,\n model: string,\n reasoningOf: (provider: string, model: string) => ModelReasoning | undefined,\n remembered?: string | undefined,\n): Selection | null {\n if (live !== null && sameModel(live, provider, model)) return null\n const offered = reasoningOf(provider, model)\n // The target model's own memory is restored, so switching away and back never\n // silently resets the effort.\n const carried = offered !== undefined\n && remembered !== undefined\n && offered.efforts.some((level) => level.id === remembered)\n return {\n provider,\n model,\n ...carried ? { reasoningEffort: remembered } : {},\n }\n}\n\n/**\n * @param live - one selection.\n * @param provider - the provider to compare.\n * @param model - the model id to compare.\n * @returns whether both name the same model.\n */\nexport function sameModel(live: Selection, provider: string, model: string): boolean {\n return live.provider === provider && live.model === model\n}\n\n/**\n * @param liveEffort - the level in force, undefined meaning the provider default.\n * @param effort - the level in question.\n * @returns whether they name the same level.\n */\nexport function sameEffort(liveEffort: string | undefined, effort: string | undefined): boolean {\n return liveEffort === effort\n}\n","/**\n * The model picker: the centered dialog opened from the menu's last row.\n *\n * It is the one place every provider is visible at once — a search field, a tab\n * per provider with a model, and one flat list with each model's metadata line.\n * Favorites lead the list and can be toggled from a row's star without\n * selecting that row. A click commits at once; a rejected commit keeps the\n * dialog open so the user can retry or pick another model.\n */\n\nimport { useEffect, useId, useMemo, useRef, useState } from 'react'\nimport {\n IconSearchOutlineRegular,\n Input,\n Modal,\n ShortcutKeys,\n rankByName,\n} from '@deepseek-ai/dsh-client-ui-primitives'\nimport type { ModelDirectoryState } from '@deepseek-ai/dsh-client-ui-model-selection/client'\nimport type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'\nimport { ModelRow } from './ModelRow.tsx'\nimport { isFavorite, orderWithFavorites } from './favorites.ts'\nimport type { ModelFacts } from './model-facts.ts'\nimport { metaSegments } from './model-facts.ts'\nimport {\n ALL_TAB,\n flattenGroups,\n pickerTabs,\n reasoningOf,\n rowsForTab,\n searchRows,\n type CatalogModel,\n type ModelRow as CatalogRow,\n type PickerTab,\n} from './rows.ts'\nimport { Tag, isMobileDevice } from '@deepseek-ai/dsh-client-ui-primitives'\nimport type { ModelFactsLookup } from './facts-source.ts'\nimport type { Selection } from './selection.ts'\nimport { planModelCommit } from './selection.ts'\nimport css from './ModelSubmenu.module.css'\n\n/** Props the seat hands the picker. */\nexport interface ModelPickerProps {\n /** Whether the dialog is showing. */\n readonly open: boolean\n /** Resolve a catalog badge's localized text for the active language. */\n readonly resolveText: (text: NonNullable<CatalogModel['badge']>['text']) => string\n /** The shared directory snapshot. */\n readonly state: ModelDirectoryState\n /** The selection in force, null before a first selection resolves. */\n readonly live: Selection | null\n /** The metadata lookup for the current settings answer. */\n readonly facts: ModelFactsLookup\n /** The favorite table right now. */\n readonly favorites: readonly { readonly provider: string; readonly model: string }[]\n /** Whether a selection is in flight. */\n readonly busy: boolean\n /** Submit one selection. */\n readonly onPick: (selection: Selection) => void\n /** Toggle one model's favorite. */\n readonly onToggleFavorite: (provider: string, model: string) => void\n /** Reload the catalog after a failure. */\n readonly onReload: () => void\n /** Close the dialog, optionally returning focus. */\n readonly onClose: () => void\n}\n\n/**\n * Render the picker.\n * @param props - the catalog, the live selection, and the picker's verbs.\n * @returns the dialog; nothing while closed.\n */\nexport function ModelPicker({\n open, resolveText, state, live, facts, favorites, busy, onPick, onToggleFavorite, onReload, onClose, t,\n}: ModelPickerProps & PropsLocale<'modelSubmenu'>) {\n const id = useId()\n const searchRef = useRef<HTMLInputElement | null>(null)\n const listRef = useRef<HTMLDivElement | null>(null)\n const [query, setQuery] = useState('')\n const [tab, setTab] = useState(ALL_TAB)\n const [highlight, setHighlight] = useState(0)\n const tabRefs = useRef(new Map<string, HTMLButtonElement>())\n\n const tabs = useMemo(() => pickerTabs(state.groups, t('picker.all')), [state.groups, t])\n const reason = useMemo(() => reasoningOf(state.groups), [state.groups])\n const rows = useMemo(() => orderWithFavorites(\n rowsForTab(flattenGroups(state.groups), tab),\n (row) => isFavorite(favorites, row.provider, row.model),\n ), [state.groups, tab, favorites])\n // Search runs inside the favorite partition, never across it: a query must not\n // pull an unstarred model above a starred one, because favorites lead the list\n // whether or not a query is typed.\n const visible = useMemo(() => orderWithFavorites(\n searchRows(rows, query, rankByName),\n (row) => isFavorite(favorites, row.provider, row.model),\n ), [rows, query, favorites])\n\n useEffect(() => {\n if (!open) return\n setQuery('')\n setTab(ALL_TAB)\n setHighlight(0)\n if (listRef.current !== null) listRef.current.scrollTop = 0\n // 粗指针设备不自动聚焦文本输入:移动端弹键盘会顶起布局,宿主座位也这样守。\n const frame = requestAnimationFrame(() => {\n if (!isMobileDevice()) searchRef.current?.focus()\n })\n return () => { cancelAnimationFrame(frame) }\n }, [open])\n\n useEffect(() => {\n setHighlight((current) => visible.length === 0 ? 0 : Math.min(current, visible.length - 1))\n }, [visible.length])\n\n useEffect(() => {\n const row = listRef.current?.querySelector<HTMLElement>(`[data-row-index=\"${highlight}\"]`)\n row?.scrollIntoView({ block: 'nearest' })\n }, [highlight])\n\n if (!open) return null\n\n const activeRow = visible[highlight]\n const activeId = activeRow === undefined ? undefined : `${id}-row-${highlight}`\n const onSearchKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {\n if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {\n event.preventDefault()\n if (visible.length === 0) return\n setHighlight((current) => {\n const next = current + (event.key === 'ArrowDown' ? 1 : -1)\n return (next + visible.length) % visible.length\n })\n return\n }\n if (event.key === 'Enter' && activeRow !== undefined && !busy) {\n event.preventDefault()\n pick(activeRow)\n }\n }\n const pick = (row: CatalogRow) => {\n const selection = planModelCommit(live, row.provider, row.model, reason)\n if (selection === null) {\n onClose()\n return\n }\n onPick(selection)\n }\n /** Move to another tab and hand it the keyboard, in one step. */\n const goTab = (next: PickerTab | undefined) => {\n if (next === undefined || next.id === tab) return\n setTab(next.id)\n setHighlight(0)\n tabRefs.current.get(next.id)?.focus()\n }\n const onTabKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>) => {\n const index = tabs.findIndex((candidate) => candidate.id === tab)\n if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') {\n event.preventDefault()\n const step = event.key === 'ArrowRight' ? 1 : -1\n goTab(tabs[((index === -1 ? 0 : index) + step + tabs.length) % tabs.length])\n return\n }\n if (event.key === 'Home' || event.key === 'End') {\n event.preventDefault()\n goTab(tabs[event.key === 'Home' ? 0 : tabs.length - 1])\n }\n }\n\n const failures = state.failures.filter((failure) => tab === ALL_TAB || failure.id === tab)\n const failed = state.error !== null && state.status === 'error'\n const loading = state.status === 'loading'\n return (\n <Modal\n open={open}\n onClose={onClose}\n title={t('picker.title')}\n closeLabel={t('picker.close')}\n className={css.pickerDialog ?? ''}\n contentClassName={css.pickerContent ?? ''}\n >\n <div className={css.pickerSearch}>\n <Input\n ref={searchRef}\n className={css.pickerSearchInput ?? ''}\n type=\"text\"\n role=\"searchbox\"\n aria-label={t('picker.search')}\n aria-controls={`${id}-list`}\n aria-activedescendant={activeId}\n placeholder={t('picker.search')}\n icon={<IconSearchOutlineRegular size={14} />}\n value={query}\n readOnly={busy}\n onChange={(event) => { setQuery(event.target.value) }}\n onKeyDown={onSearchKeyDown}\n data-modal-autofocus=\"\"\n />\n </div>\n <div className={css.pickerTabs} role=\"tablist\" aria-label={t('picker.title')}>\n {tabs.map((entry) => (\n <button\n key={entry.id}\n ref={(node) => {\n if (node === null) tabRefs.current.delete(entry.id)\n else tabRefs.current.set(entry.id, node)\n }}\n type=\"button\"\n role=\"tab\"\n id={`${id}-tab-${entry.id}`}\n aria-selected={entry.id === tab}\n aria-controls={`${id}-list`}\n tabIndex={0}\n className={css.pickerTab}\n onClick={() => {\n setTab(entry.id)\n setHighlight(0)\n }}\n onKeyDown={onTabKeyDown}\n onFocus={() => {\n if (entry.id !== tab) {\n setTab(entry.id)\n setHighlight(0)\n }\n }}\n >\n {entry.label}\n </button>\n ))}\n </div>\n <div\n ref={listRef}\n id={`${id}-list`}\n className={css.pickerList}\n role=\"menu\"\n aria-label={t('picker.title')}\n >\n {loading && <div className={css.notice}>{t('status.loading')}</div>}\n {failed && (\n <div className={css.notice}>\n <span>{t('status.failed', { message: state.error ?? '' })}</span>\n <button type=\"button\" className={css.retry} onClick={onReload}>{t('action.reload')}</button>\n </div>\n )}\n {failures.map((failure) => (\n <div className={css.notice} key={failure.id}>\n <span>{t('status.providerFailed', { name: failure.name, message: failure.message })}</span>\n <button type=\"button\" className={css.retry} onClick={onReload}>{t('action.reload')}</button>\n </div>\n ))}\n {visible.length === 0 && !loading && !failed && (\n <div className={css.notice}>{state.groups.length === 0 ? t('status.empty') : t('picker.searchEmpty')}</div>\n )}\n {visible.map((row, index) => (\n <div key={`${row.provider}/${row.model}`} data-row-index={index} id={`${id}-row-${index}`}>\n <ModelRow\n name={row.entry.name}\n badge={row.entry.badge === undefined\n ? undefined\n : <Tag tone={row.entry.badge.tone} className={css.rowBadge}>{resolveText(row.entry.badge.text)}</Tag>}\n selected={live?.provider === row.provider && live.model === row.model}\n busy={busy}\n star=\"interactive\"\n favorited={isFavorite(favorites, row.provider, row.model)}\n meta={metaLine({\n row,\n facts: facts(row.provider, row.model),\n providerName: tab === ALL_TAB ? row.providerName : undefined,\n thinks: row.entry.reasoning !== undefined,\n effortLabel: t('meta.effort'),\n imageLabel: t('meta.images'),\n })}\n starLabel={isFavorite(favorites, row.provider, row.model)\n ? t('picker.unfavorite', { model: row.entry.name })\n : t('picker.favorite', { model: row.entry.name })}\n starTabStop={index === highlight}\n rowTabIndex={-1}\n onPick={() => { pick(row) }}\n onFocusRow={() => { setHighlight(index) }}\n onToggleFavorite={() => { onToggleFavorite(row.provider, row.model) }}\n />\n </div>\n ))}\n </div>\n <div className={css.pickerFooter}>\n <span className={css.footerHint}>\n <ShortcutKeys keys={['↑', '↓']} />\n <span>{t('picker.hintNavigate')}</span>\n <span className={css.footerDot}>·</span>\n <ShortcutKeys keys={['Enter']} />\n <span>{t('picker.hintSelect')}</span>\n <span className={css.footerDot}>·</span>\n <ShortcutKeys keys={['Esc']} />\n <span>{t('picker.hintClose')}</span>\n </span>\n </div>\n </Modal>\n )\n}\n\nfunction metaLine(input: {\n readonly row: CatalogRow\n readonly facts: ModelFacts\n readonly providerName: string | undefined\n readonly thinks: boolean\n readonly effortLabel: string\n readonly imageLabel: string\n}): string {\n return metaSegments({\n ...input.providerName === undefined ? {} : { providerName: input.providerName },\n facts: input.facts,\n thinks: input.thinks,\n effortLabel: input.effortLabel,\n imageLabel: input.imageLabel,\n }).join(' · ')\n}\n\n","/**\n * The per-model reasoning level this browser last chose.\n *\n * The Host stores one level per session, so switching models away and back\n * forgets what was picked for the first one. This module keeps that memory\n * beside the favorite table, keyed the same way: provider route key plus model\n * id. Remembering the provider default removes the row, so \"follow the\n * provider\" stays distinguishable from \"remembered a level\".\n */\n\n/** One remembered reasoning level. */\nexport interface RememberedEffort {\n readonly provider: string\n readonly model: string\n readonly effort: string\n}\n\n/** The single browser-wide storage key holding the whole memory. */\nexport const EFFORTS_KEY = 'dsh-model-submenu.efforts'\n\n/** The part of `Storage` this module uses; absent or throwing reads mean \"none\". */\nexport type EffortStorage = Pick<Storage, 'getItem' | 'setItem'>\n\n/**\n * Read the memory, keeping only rows naming both halves and a level.\n * @param storage - browser storage, or undefined when the browser withholds it.\n * @returns the remembered rows; empty when unreadable.\n */\nexport function readEfforts(storage: EffortStorage | undefined): RememberedEffort[] {\n const raw = getItem(storage)\n if (raw === null) return []\n let parsed: unknown\n try {\n parsed = JSON.parse(raw)\n } catch {\n return []\n }\n if (!Array.isArray(parsed)) return []\n return parsed.filter((row): row is RememberedEffort => typeof row === 'object' && row !== null\n && typeof (row as RememberedEffort).provider === 'string'\n && typeof (row as RememberedEffort).model === 'string'\n && typeof (row as RememberedEffort).effort === 'string'\n && (row as RememberedEffort).effort !== '')\n}\n\n/**\n * @param memory - the rows to search.\n * @param provider - the provider route key.\n * @param model - the model id inside that provider.\n * @returns the level last chosen for that exact model, or undefined.\n */\nexport function effortFor(\n memory: readonly RememberedEffort[],\n provider: string,\n model: string,\n): string | undefined {\n return memory.find((row) => row.provider === provider && row.model === model)?.effort\n}\n\n/**\n * Remember one model's level, or forget it when the choice is the provider default.\n * @param storage - browser storage, or undefined when writes are unavailable.\n * @param provider - the provider route key.\n * @param model - the model id inside that provider.\n * @param effort - the chosen level, undefined meaning the provider default.\n * @returns the memory after the change, whether or not the write landed.\n */\nexport function rememberEffort(\n storage: EffortStorage | undefined,\n provider: string,\n model: string,\n effort: string | undefined,\n): RememberedEffort[] {\n const rest = readEfforts(storage).filter((row) => row.provider !== provider || row.model !== model)\n const next = effort === undefined ? rest : [...rest, { provider, model, effort }]\n try {\n storage?.setItem(EFFORTS_KEY, JSON.stringify(next))\n } catch {\n // A full quota or a locked-down browser loses the memory, not the menu.\n }\n return next\n}\n\nfunction getItem(storage: EffortStorage | undefined): string | null {\n try {\n return storage?.getItem(EFFORTS_KEY) ?? null\n } catch {\n return null\n }\n}\n","import { useCallback, useEffect, useId, useMemo, useRef, useState, useSyncExternalStore } from 'react'\nimport { createPortal } from 'react-dom'\nimport {\n IconChevronDownOutlineRegular,\n IconFlatListOutlineRegular,\n IconWarningOutlineRegular,\n MenuSurface,\n StateDot,\n Tag,\n Toast,\n useAnchoredPosition,\n} from '@deepseek-ai/dsh-client-ui-primitives'\nimport type { ModelDirectoryState, ModelSelectInjected } from '@deepseek-ai/dsh-client-ui-model-selection/client'\nimport type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'\nimport { ModelPicker } from './ModelPicker.tsx'\nimport { ModelRow } from './ModelRow.tsx'\nimport type { FactsSource } from './facts-source.ts'\nimport {\n isFavorite,\n orderWithFavorites,\n readFavorites,\n toggleFavorite,\n type FavoriteModel,\n type FavoritesStorage,\n} from './favorites.ts'\nimport { effortFor, readEfforts, rememberEffort, type RememberedEffort } from './effort-memory.ts'\nimport type { EffortChoice, Selection } from './selection.ts'\nimport { reasoningOf } from './rows.ts'\nimport { effortChoices, planEffortCommit, planModelCommit } from './selection.ts'\nimport css from './ModelSubmenu.module.css'\n\ntype ModelSubmenuProps = ModelSelectInjected & { locked: boolean } & PropsLocale<'modelSubmenu'> & {\n /** The live settings-derived metadata the picker's line reads. */\n readonly factsSource: FactsSource\n}\n\ntype ProviderGroup = ModelDirectoryState['groups'][number]\ntype CatalogModel = ProviderGroup['models'][number]\n\n/** Unplaced portal card: hidden but laid out at a fixed origin so offsetWidth/offsetHeight are real. */\nconst MEASURE_STYLE = {\n visibility: 'hidden',\n left: 0,\n top: 0,\n} as const\n\nconst cls = (...parts: readonly (string | false | undefined)[]): string => parts.filter((part) => part !== false && part !== undefined).join(' ')\n\n/**\n * The composer's model seat (`conversation.input.model`), taken over from the\n * built-in occupant with a provider-scoped menu: one provider's models above,\n * the reasoning row as segments, the cache warning, and the door to the picker\n * that lists every provider.\n *\n * Every click commits at once: a model, a reasoning level, or the picker door.\n * Closing the menu never writes anything, and the picker reads the level the\n * Host already accepted, so its \"current\" reading is never stale.\n */\nexport function ModelSubmenu({ locked, available, directory, load, select, resolveText, t, factsSource }: ModelSubmenuProps) {\n const state = useSyncExternalStore((fn) => directory.subscribe(fn), () => directory.getSnapshot())\n const [open, setOpen] = useState(false)\n const [pickerOpen, setPickerOpen] = useState(false)\n const [favorites, setFavorites] = useState<readonly FavoriteModel[]>(() => readFavorites(storageOf()))\n const [memory, setMemory] = useState<readonly RememberedEffort[]>(() => readEfforts(storageOf()))\n const [toast, setToast] = useState<{ seq: number; text: string } | null>(null)\n const toastSeq = useRef(0)\n const rootRef = useRef<HTMLDivElement | null>(null)\n const triggerRef = useRef<HTMLButtonElement | null>(null)\n const menuRef = useRef<HTMLDivElement | null>(null)\n const id = useId()\n\n const groups = state.groups\n const current = state.current\n const currentGroup = current === null ? undefined : groups.find((group) => group.id === current.provider)\n const currentModel = currentGroup?.models.find((model) => model.id === current?.model)\n const reasoning = currentModel?.reasoning\n const effectiveEffort = current?.reasoningEffort ?? reasoning?.defaultEffort\n /**\n * The trigger's caption. A model that advertises no levels still says so in\n * the same place: the blue caption beside the name reads 默认 rather than\n * disappearing, so the shape of the control never changes with the model.\n */\n const effortLabel = reasoning === undefined || effectiveEffort === undefined\n ? t('effort.providerDefault')\n : reasoning.efforts.find((level) => level.id === effectiveEffort)?.name ?? effectiveEffort\n const choices = useMemo(() => effortChoices(reasoning, t('effort.providerDefault')), [reasoning, t])\n const busy = state.status === 'selecting'\n const live = useMemo(() => selectionOf(state), [state])\n const reason = useMemo(() => reasoningOf(groups), [groups])\n /** Resolve one catalog badge to its localized text and tone, matching the host seat. */\n const badgeOf = (model: CatalogModel) => model.badge === undefined\n ? undefined\n : <Tag tone={model.badge.tone} className={css.rowBadge}>{resolveText(model.badge.text)}</Tag>\n const triggerBadge = currentModel?.badge === undefined\n ? null\n : <Tag tone={currentModel.badge.tone} className={css.triggerBadge}>{resolveText(currentModel.badge.text)}</Tag>\n const facts = useSyncExternalStore((fn) => factsSource.subscribe(fn), () => factsSource.getSnapshot())\n\n const menuRows = useMemo(() => currentGroup === undefined ? [] : orderWithFavorites(\n currentGroup.models,\n (model) => isFavorite(favorites, currentGroup.id, model.id),\n ), [currentGroup, favorites])\n\n const raiseToast = (text: string) => {\n toastSeq.current += 1\n setToast({ seq: toastSeq.current, text })\n }\n const reload = () => {\n load()\n }\n const submit = (selection: Selection, onDone: (() => void) | null, onRejected?: () => void) => {\n select(selection).then((result) => {\n if (result === undefined || result.ok) {\n // The Host accepted it, so this is now the level this model uses.\n remember(selection.provider, selection.model, selection.reasoningEffort)\n onDone?.()\n return\n }\n onRejected?.()\n raiseToast(result.error.code === 'session/writer-held'\n ? t('error.sessionInUse')\n : t('error.action', { message: `${result.error.code}: ${result.error.message}` }))\n })\n }\n /** Close the menu. Closing writes nothing: every choice already committed on click. */\n const closeMenu = useCallback((restoreFocus: boolean) => {\n setOpen(false)\n if (restoreFocus) queueMicrotask(() => { triggerRef.current?.focus() })\n }, [])\n\n useEffect(() => {\n if (!open) return\n const closeOutside = (event: MouseEvent) => {\n if (rootRef.current?.contains(event.target as Node) === true) return\n if (menuRef.current?.contains(event.target as Node) === true) return\n closeMenu(false)\n }\n document.addEventListener('mousedown', closeOutside)\n return () => {\n document.removeEventListener('mousedown', closeOutside)\n }\n }, [open, closeMenu])\n\n useEffect(() => {\n if (!open) return\n const frame = requestAnimationFrame(() => {\n const checked = menuRef.current?.querySelector<HTMLElement>('[role=\"menuitemradio\"][aria-checked=\"true\"]:not([disabled])')\n const first = menuRef.current?.querySelector<HTMLElement>('[role=\"menuitemradio\"]:not([disabled])')\n const target = checked ?? first ?? triggerRef.current\n target?.scrollIntoView({ block: 'nearest' })\n target?.focus()\n })\n return () => { cancelAnimationFrame(frame) }\n }, [open])\n\n const menuPos = useAnchoredPosition({\n open: open && available,\n anchorRef: triggerRef,\n panelRef: menuRef,\n side: 'top',\n align: 'end',\n gap: 8,\n margin: 12,\n layoutKey: `${state.status}:${String(menuRows.length)}:${String(choices.length)}`,\n })\n\n if (!available) return null\n\n const show = () => {\n setOpen(true)\n reload()\n }\n /** The picker door: the level already landed on click, so only the surface changes. */\n const openPicker = () => {\n setOpen(false)\n setPickerOpen(true)\n }\n /** Record one model's level, so switching away and back restores it. */\n const remember = (provider: string, model: string, effort: string | undefined) => {\n setMemory(rememberEffort(storageOf(), provider, model, effort))\n }\n const chooseModel = (provider: string, model: CatalogModel) => {\n const picked = planModelCommit(live, provider, model.id, reason, effortFor(memory, provider, model.id))\n if (picked === null) {\n setOpen(false)\n queueMicrotask(() => { triggerRef.current?.focus() })\n return\n }\n // A model click is the decision: close at once, and keep the menu open when\n // the host rejects it so the user can try another model.\n setOpen(false)\n submit(picked, () => { queueMicrotask(() => { triggerRef.current?.focus() }) }, () => {\n // The host refused it: put the menu back so the user can pick another.\n setOpen(true)\n })\n }\n const rowButtons = () => [...(menuRef.current?.querySelectorAll<HTMLButtonElement>('[role=\"menuitemradio\"]:not([disabled])') ?? [])]\n const moveFocus = (offset: number) => {\n const items = rowButtons()\n if (items.length === 0) return\n const active = items.findIndex((item) => item === document.activeElement)\n items[active === -1 ? offset > 0 ? 0 : items.length - 1 : (active + offset + items.length) % items.length]?.focus()\n }\n const onEffortKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return\n if (choices.length === 0 || live === null) return\n event.preventDefault()\n const index = choices.findIndex((choice) => choice.effort === effectiveEffort)\n const step = event.key === 'ArrowRight' ? 1 : -1\n const next = choices[((index === -1 ? 0 : index) + step + choices.length) % choices.length]\n if (next === undefined) return\n const pending = planEffortCommit(live, next.effort)\n if (pending !== null) submit(pending, null)\n }\n const onRootKeyDown = (event: React.KeyboardEvent) => {\n if (event.key === 'Escape' && open) {\n event.preventDefault()\n closeMenu(true)\n return\n }\n if (!open) return\n if (event.key === 'Tab') {\n if (event.shiftKey) {\n event.preventDefault()\n closeMenu(true)\n return\n }\n const focused = document.activeElement\n const rows = rowButtons()\n if (focused instanceof HTMLButtonElement && rows.includes(focused)) {\n event.preventDefault()\n focused.click()\n return\n }\n if (focused !== triggerRef.current) return\n event.preventDefault()\n const checked = menuRef.current?.querySelector<HTMLElement>('[role=\"menuitemradio\"][aria-checked=\"true\"]:not([disabled])')\n ;(checked ?? rows[0])?.focus()\n return\n }\n if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {\n event.preventDefault()\n moveFocus(event.key === 'ArrowDown' ? 1 : -1)\n }\n }\n /**\n * The keyboard leaving the seat closes the menu — but that is not one of the\n * four closing gestures that write a mark, so it closes without committing.\n * Otherwise clicking the notice text or a divider inside the menu, which moves\n * focus without unmounting it, would land a level nobody asked to commit.\n */\n const onBlur = (event: React.FocusEvent) => {\n if (pickerOpen) return\n if (event.relatedTarget instanceof Node\n && (rootRef.current?.contains(event.relatedTarget) === true || menuRef.current?.contains(event.relatedTarget) === true)) return\n setOpen(false)\n }\n const toggleFavoriteOf = (provider: string, model: string) => {\n setFavorites(toggleFavorite(storageOf(), provider, model))\n }\n\n const waiting = current === null && state.status === 'loading'\n const modelLabel = waiting ? t('trigger.loading') : currentModel?.name ?? (current === null ? t('trigger.fallback') : `${current.provider}/${current.model}`)\n const triggerAria = waiting\n ? t('trigger.loading')\n : current === null\n ? t('trigger.selectAria')\n : effortLabel === undefined\n ? t('trigger.aria', { model: modelLabel })\n : t('trigger.ariaEffort', { model: modelLabel, effort: effortLabel })\n const providerName = currentGroup?.name\n ?? state.failures.find((failure) => failure.id === current?.provider)?.name\n ?? current?.provider\n ?? ''\n const providerFailure = current === null ? undefined : state.failures.find((failure) => failure.id === current.provider)\n\n return (\n <div ref={rootRef} className={css.root} onKeyDown={onRootKeyDown} onBlur={onBlur}>\n <button\n ref={triggerRef}\n type=\"button\"\n className={css.trigger}\n aria-label={triggerAria}\n aria-haspopup=\"menu\"\n aria-expanded={open}\n aria-controls={open ? `${id}-menu` : undefined}\n title={effortLabel === undefined ? modelLabel : `${modelLabel} · ${effortLabel}`}\n disabled={locked}\n onClick={() => {\n if (open) closeMenu(false)\n else show()\n }}\n >\n <IconFlatListOutlineRegular className={css.triggerIcon} size={16} />\n {triggerBadge}\n <span className={css.triggerLabel}>{modelLabel}</span>\n {effortLabel !== undefined && <span className={css.triggerEffort}>{effortLabel}</span>}\n {busy\n ? <StateDot state=\"ongoing\" />\n : <IconChevronDownOutlineRegular className={cls(css.chevron, open && css.chevronOpen)} />}\n </button>\n {open && createPortal((\n <MenuSurface\n ref={menuRef}\n id={`${id}-menu`}\n className={css.menu}\n style={menuPos ?? MEASURE_STYLE}\n role=\"group\"\n aria-label={t('menu.aria')}\n aria-busy={state.status === 'loading' || busy}\n >\n <div className={css.providerName} title={providerName}>{providerName}</div>\n {state.status === 'loading' && <div className={css.notice}>{t('status.loading')}</div>}\n {state.error !== null && state.status === 'error' && (\n <div className={css.notice}>\n <span>{t('status.failed', { message: state.error })}</span>\n <button type=\"button\" className={css.retry} onClick={reload}>{t('action.reload')}</button>\n </div>\n )}\n {providerFailure !== undefined && (\n <div className={css.notice}>\n <span>{t('status.providerFailed', { name: providerFailure.name, message: providerFailure.message })}</span>\n <button type=\"button\" className={css.retry} onClick={reload}>{t('action.reload')}</button>\n </div>\n )}\n <div className={cls(css.groups, 'scrollable')} role=\"menu\" aria-label={t('menu.aria')}>\n {menuRows.map((model) => (\n <ModelRow\n key={model.id}\n name={model.name}\n badge={badgeOf(model)}\n selected={current !== null && currentGroup !== undefined && current.provider === currentGroup.id && current.model === model.id}\n busy={busy}\n star=\"static\"\n favorited={currentGroup !== undefined && isFavorite(favorites, currentGroup.id, model.id)}\n onPick={() => {\n if (currentGroup !== undefined) chooseModel(currentGroup.id, model)\n }}\n />\n ))}\n {menuRows.length === 0 && state.status === 'ready' && (\n <div className={css.notice}>{t('status.empty')}</div>\n )}\n </div>\n {choices.length > 0 && (\n <>\n <div className={css.divider} />\n <div className={css.effortRow}>\n <span className={css.effortHeading}>{t('menu.effort')}</span>\n <div className={css.effortTrack} role=\"radiogroup\" aria-label={t('menu.effort')} onKeyDown={onEffortKeyDown}>\n {choices.map((choice: EffortChoice) => (\n <button\n key={choice.key}\n type=\"button\"\n role=\"radio\"\n aria-checked={choice.effort === effectiveEffort}\n className={css.effortSegment}\n disabled={busy}\n onClick={() => {\n if (live !== null) {\n const pending = planEffortCommit(live, choice.effort)\n if (pending !== null) submit(pending, null)\n }\n }}\n >\n {choice.label}\n </button>\n ))}\n </div>\n </div>\n <div className={css.divider} />\n </>\n )}\n <p className={css.hint}>{t('menu.hint')}</p>\n <div className={css.divider} />\n <button type=\"button\" role=\"menuitem\" className={css.moreRow} onClick={openPicker}>\n <IconFlatListOutlineRegular className={css.moreIcon} size={16} />\n <span>{t('menu.more')}</span>\n </button>\n </MenuSurface>\n ), document.body)}\n <ModelPicker\n open={pickerOpen}\n resolveText={resolveText}\n state={state}\n live={live}\n facts={facts}\n favorites={favorites}\n busy={busy}\n t={t}\n onPick={(selection) => { submit(selection, () => { setPickerOpen(false) }) }}\n onToggleFavorite={toggleFavoriteOf}\n onReload={reload}\n onClose={() => {\n setPickerOpen(false)\n queueMicrotask(() => { triggerRef.current?.focus() })\n }}\n />\n {toast !== null && (\n <Toast\n key={toast.seq}\n text={toast.text}\n icon={<IconWarningOutlineRegular />}\n anchor={rootRef.current?.closest('[data-composer-card]') ?? null}\n onDone={() => {\n setToast(null)\n }}\n />\n )}\n </div>\n )\n}\n\n/** The selection in force, or null while the host has not resolved one. */\nfunction selectionOf(state: ModelDirectoryState): Selection | null {\n if (state.current === null) return null\n return state.current.reasoningEffort === undefined\n ? { provider: state.current.provider, model: state.current.model }\n : { provider: state.current.provider, model: state.current.model, reasoningEffort: state.current.reasoningEffort }\n}\n\n/** Isolate the browser's storage so a locked-down browser reads as \"no favorites\". */\nfunction storageOf(): FavoritesStorage | undefined {\n try {\n return globalThis.localStorage\n } catch {\n return undefined\n }\n}\n","/**\n * The browser's live answer to \"what does the settings document know about this\n * model?\".\n *\n * Two host answers meet here: the shared settings mirror (which the settings\n * page already keeps warm) and the LLM provider directory that says which\n * settings namespace holds each provider's profile. Both can change while the\n * page is open — a saved profile, a reconnected host — so the join is rebuilt\n * whenever the mirror or the directory reports a change, and readers subscribe\n * to the lookup itself.\n */\n\nimport type { ModelFacts } from './model-facts.ts'\nimport {\n buildModelFacts,\n type ConfigurableProviderRow,\n type NamespaceRow,\n} from './model-facts.ts'\n\n/** One lookup answering both metadata fields for a provider and model. */\nexport type ModelFactsLookup = (provider: string, model: string) => ModelFacts\n\n/** The mirror face this source reads; a subset of the settings provider's. */\nexport interface DescribeFace {\n getSnapshot(): { readonly view: { readonly namespaces: readonly NamespaceRow[] } | undefined }\n subscribe(listener: () => void): () => void\n ensure(): Promise<void>\n}\n\n/** One remote answer, as the gateway returns it. */\nexport interface DirectoryAnswer {\n readonly ok: boolean\n readonly value?: readonly ConfigurableProviderRow[]\n}\n\n/** A stable, subscribable lookup over the current settings answer. */\nexport interface FactsSource {\n /** @returns the lookup for the current answer; the identity changes on every rebuild. */\n getSnapshot(): ModelFactsLookup\n /** @param listener - called after each rebuild. @returns the disposer. */\n subscribe(listener: () => void): () => void\n}\n\nconst EMPTY: ModelFactsLookup = () => ({ contextWindow: undefined, images: 'unknown' })\n\n/**\n * Join the settings mirror with the provider directory.\n * @param deps.describe - the settings mirror's describe face.\n * @param deps.listProviders - reads the LLM provider directory once.\n * @returns the source, plus `start` which wires both reads and returns its disposer.\n */\nexport function createFactsSource(deps: {\n readonly describe: DescribeFace\n readonly listProviders: () => Promise<DirectoryAnswer>\n}): { readonly source: FactsSource; readonly start: () => () => void } {\n const listeners = new Set<() => void>()\n let providers: readonly ConfigurableProviderRow[] = []\n let lookup: ModelFactsLookup = EMPTY\n\n const rebuild = () => {\n const namespaces = deps.describe.getSnapshot().view?.namespaces ?? []\n lookup = providers.length === 0 || namespaces.length === 0\n ? EMPTY\n : buildModelFacts(providers, namespaces)\n for (const listener of [...listeners]) listener()\n }\n\n return {\n source: {\n getSnapshot: () => lookup,\n subscribe: (listener) => {\n listeners.add(listener)\n return () => { listeners.delete(listener) }\n },\n },\n start: () => {\n const off = deps.describe.subscribe(rebuild)\n void deps.describe.ensure().catch(() => { /* a failed read leaves the empty lookup in place */ })\n void deps.listProviders().then(\n (answer) => {\n providers = answer.ok && answer.value !== undefined ? answer.value : []\n rebuild()\n },\n () => { /* the directory being unavailable leaves every model's facts unknown */ },\n )\n rebuild()\n return () => {\n off()\n listeners.clear()\n }\n },\n }\n}\n","/** Locale bundles for the composer model menu and the picker it opens. */\n\n/** Dictionary namespace owned by this plugin. */\nexport const NS = 'modelSubmenu'\n\n/** Locale keys the menu and the picker render. */\nexport type ModelSubmenuLocaleKey =\n | 'trigger.fallback'\n | 'trigger.loading'\n | 'trigger.selectAria'\n | 'trigger.aria'\n | 'trigger.ariaEffort'\n | 'menu.aria'\n | 'menu.effort'\n | 'menu.more'\n | 'menu.hint'\n | 'effort.providerDefault'\n | 'status.loading'\n | 'status.empty'\n | 'status.failed'\n | 'status.providerFailed'\n | 'action.reload'\n | 'error.action'\n | 'error.sessionInUse'\n | 'picker.title'\n | 'picker.close'\n | 'picker.search'\n | 'picker.searchEmpty'\n | 'picker.all'\n | 'picker.favorite'\n | 'picker.unfavorite'\n | 'picker.hintNavigate'\n | 'picker.hintSelect'\n | 'picker.hintClose'\n | 'meta.effort'\n | 'meta.images'\n\ndeclare module '@deepseek-ai/dsh-client-ui-slots' {\n interface LocaleNamespaceMap {\n /** The composer model menu's copy. */\n modelSubmenu: ModelSubmenuLocaleKey\n }\n}\n\n/** Simplified Chinese dictionary (the key-set source of truth). */\nexport const zh: Record<ModelSubmenuLocaleKey, string> = {\n 'trigger.fallback': '选择模型',\n 'trigger.loading': '正在加载模型…',\n 'trigger.selectAria': '选择模型',\n 'trigger.aria': '选择模型,当前 {model}',\n 'trigger.ariaEffort': '选择模型,当前 {model},思考等级 {effort}',\n 'menu.aria': '模型与思考等级',\n 'menu.effort': '思考',\n 'menu.more': '更多模型…',\n 'menu.hint': '提示:切换模型或思考程度会使既有的提示词缓存失效。建议新建会话,避免额外的 token 消耗。',\n 'effort.providerDefault': '默认',\n 'status.loading': '正在加载模型…',\n 'status.empty': '没有可用的模型。',\n 'status.failed': '模型列表加载失败:{message}',\n 'status.providerFailed': '{name} 加载失败:{message}',\n 'action.reload': '重新加载',\n 'error.action': '模型操作失败:{message}',\n 'error.sessionInUse': '当前会话已被占用,可能是其他正在运行的 DSH 导致的(如其他 dsh web、桌面端),请退出其他正在运行的 DSH 后重试。',\n 'picker.title': '切换模型',\n 'picker.close': '关闭',\n 'picker.search': '搜索模型或提供商…',\n 'picker.searchEmpty': '没有匹配的模型。',\n 'picker.all': '全部',\n 'picker.favorite': '收藏 {model}',\n 'picker.unfavorite': '取消收藏 {model}',\n 'picker.hintNavigate': '导航',\n 'picker.hintSelect': '选择',\n 'picker.hintClose': '关闭',\n 'meta.effort': '思考',\n 'meta.images': '图片输入',\n}\n\n/** English dictionary, checked complete against the zh key set. */\nexport const en: Record<ModelSubmenuLocaleKey, string> = {\n 'trigger.fallback': 'Select model',\n 'trigger.loading': 'Loading models…',\n 'trigger.selectAria': 'Select model',\n 'trigger.aria': 'Select model, current {model}',\n 'trigger.ariaEffort': 'Select model, current {model}, effort {effort}',\n 'menu.aria': 'Model and effort',\n 'menu.effort': 'Effort',\n 'menu.more': 'More models…',\n 'menu.hint': 'Changing the model or the effort invalidates the existing prompt cache. Start a new session to avoid the extra token cost.',\n 'effort.providerDefault': 'Default',\n 'status.loading': 'Loading models…',\n 'status.empty': 'No models available.',\n 'status.failed': 'Model list failed to load: {message}',\n 'status.providerFailed': '{name} failed to load: {message}',\n 'action.reload': 'Reload',\n 'error.action': 'Model operation failed: {message}',\n '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.',\n 'picker.title': 'Switch model',\n 'picker.close': 'Close',\n 'picker.search': 'Search models or providers…',\n 'picker.searchEmpty': 'No matching models.',\n 'picker.all': 'All',\n 'picker.favorite': 'Favorite {model}',\n 'picker.unfavorite': 'Remove {model} from favorites',\n 'picker.hintNavigate': 'navigate',\n 'picker.hintSelect': 'select',\n 'picker.hintClose': 'close',\n 'meta.effort': 'Effort',\n 'meta.images': 'Images',\n}\n","import type { Context as ClientContext } from '@deepseek-ai/cordis'\n// Type-only: `slots` and its LocaleNamespaceMap merge come from the renderer and\n// the slot package; the composer seat's SlotMap entry and owner props from\n// ui-conversation; the `modelDirectories` service and the seat's injected face\n// from ui-model-selection's browser half; `configForms` from the settings\n// provider, whose shared mirror answers the metadata line.\nimport type {} from '@deepseek-ai/dsh-client-ui-renderer/client'\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\nimport type {} from '@deepseek-ai/dsh-api-remotes/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-model-selection/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-settings/client'\nimport { ModelSubmenu } from './ModelSubmenu.tsx'\nimport { createFactsSource, type DescribeFace } from './facts-source.ts'\nimport { NS, en, zh } from './locales.ts'\n\nexport { ModelSubmenu } from './ModelSubmenu.tsx'\n\n/**\n * Cordis services required by the browser half. `slots` is the contribution\n * registry, `sessions` tells whether a session may select a model,\n * `modelDirectories` owns the per-session catalog snapshot this seat renders\n * from, and `configForms` carries the settings provider's shared describe\n * mirror, which answers each model's context window and accepted modalities.\n * `remote`, `remote.llm`, and `remote.session` are declared because a service\n * method runs on the calling context: `directoryFor` reads\n * `this.ctx.remote.session`, and the provider directory that names each\n * settings address is read through `this.ctx.remote.llm`.\n */\nexport const inject = [\n 'configForms',\n 'locale',\n 'modelDirectories',\n 'remote',\n 'remote.llm',\n 'remote.session',\n 'sessions',\n 'slots',\n]\n\n/**\n * Client plugin body: register this plugin's dictionaries, then take over the\n * composer model seat below the built-in occupant's priority so this menu\n * renders in its place.\n * @param ctx - client root context.\n */\nexport function apply(ctx: ClientContext): void {\n ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-model-submenu: dictionaries')\n ctx.inject(['slots', 'modelDirectories', 'configForms', 'remote'], (scope) => {\n const models = scope.modelDirectories\n const sessions = scope.sessions\n const facts = createFactsSource({\n describe: scope.configForms.describe() as DescribeFace,\n listProviders: () => scope.remote.llm.listConfigurableProviders(),\n })\n ctx.effect(() => facts.start(), 'dsh-model-submenu: model metadata')\n scope.slots.inject('conversation.input.model', () => scope.slots.register({\n name: 'conversation.input.model',\n priority: -100,\n registrant: '@lynn123411/dsh-model-submenu',\n locale: NS,\n inject: (sessionId) => {\n // The seat's inject parameter is typed as a bare string; the service\n // faces carry the branded SessionId of that same runtime value.\n const key = sessionId as Parameters<typeof models.directoryFor>[0]\n const directory = models.directoryFor(key)\n const available = sessions.subagentAddress(key) === undefined\n return {\n available,\n directory: directory.store,\n factsSource: facts.source,\n load: () => {\n if (available) directory.load().catch(() => {})\n },\n resolveText: (text) => ctx.locale.resolveText(text),\n select: (selection) => available ? directory.select(selection) : Promise.resolve(undefined),\n }\n },\n }, ModelSubmenu))\n })\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lynn123411/dsh-model-submenu",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "给输入框的模型选择器换成两层:一层只列当前供应商的模型与思考等级,一层弹窗列全部供应商的模型。",
5
5
  "license": "MIT",
6
6
  "packageManager": "pnpm@11.7.0",
@@ -10,7 +10,7 @@
10
10
  "type": "module",
11
11
  "engines": {
12
12
  "node": "^22.19.0 || >=24.0.0",
13
- "dsh": ">=0.2.1-rc.2"
13
+ "dsh": ">=0.2.2-rc.1"
14
14
  },
15
15
  "main": "lib/index.js",
16
16
  "exports": {
@@ -63,8 +63,8 @@
63
63
  "test": "node scripts/test-favorites.mjs && node scripts/test-facts.mjs && node scripts/test-commit.mjs && node scripts/test-rows.mjs && node scripts/test-effort-memory.mjs && node scripts/test-render.mjs"
64
64
  },
65
65
  "peerDependencies": {
66
- "@deepseek-ai/cordis": "^4.0.5",
67
- "@deepseek-ai/dsh-client-ui-primitives": ">=0.2.1-rc.2",
66
+ "@deepseek-ai/cordis": "^4.1.0",
67
+ "@deepseek-ai/dsh-client-ui-primitives": ">=0.2.2-rc.1",
68
68
  "react": "^18.2.0"
69
69
  },
70
70
  "peerDependenciesMeta": {
@@ -73,19 +73,19 @@
73
73
  }
74
74
  },
75
75
  "devDependencies": {
76
- "@deepseek-ai/cordis": "^4.0.5",
77
- "@deepseek-ai/dsh-api-remotes": ">=0.2.1-rc.2",
78
- "@deepseek-ai/dsh-api-session-controller": ">=0.2.1-rc.2",
79
- "@deepseek-ai/dsh-client-locale": ">=0.2.1-rc.2",
80
- "@deepseek-ai/dsh-client-store": ">=0.2.1-rc.2",
81
- "@deepseek-ai/dsh-client-ui-conversation": ">=0.2.1-rc.2",
82
- "@deepseek-ai/dsh-client-ui-model-selection": ">=0.2.1-rc.2",
83
- "@deepseek-ai/dsh-client-ui-primitives": ">=0.2.1-rc.2",
84
- "@deepseek-ai/dsh-client-ui-renderer": ">=0.2.1-rc.2",
85
- "@deepseek-ai/dsh-client-ui-settings": ">=0.2.1-rc.2",
86
- "@deepseek-ai/dsh-client-ui-slots": ">=0.2.1-rc.2",
87
- "@deepseek-ai/dsh-session": ">=0.2.1-rc.2",
88
- "@deepseek-ai/dsh-typert-protocol": ">=0.2.1-rc.2",
76
+ "@deepseek-ai/cordis": "^4.1.0",
77
+ "@deepseek-ai/dsh-api-remotes": ">=0.2.2-rc.1",
78
+ "@deepseek-ai/dsh-api-session-controller": ">=0.2.2-rc.1",
79
+ "@deepseek-ai/dsh-client-locale": ">=0.2.2-rc.1",
80
+ "@deepseek-ai/dsh-client-store": ">=0.2.2-rc.1",
81
+ "@deepseek-ai/dsh-client-ui-conversation": ">=0.2.2-rc.1",
82
+ "@deepseek-ai/dsh-client-ui-model-selection": ">=0.2.2-rc.1",
83
+ "@deepseek-ai/dsh-client-ui-primitives": ">=0.2.2-rc.1",
84
+ "@deepseek-ai/dsh-client-ui-renderer": ">=0.2.2-rc.1",
85
+ "@deepseek-ai/dsh-client-ui-settings": ">=0.2.2-rc.1",
86
+ "@deepseek-ai/dsh-client-ui-slots": ">=0.2.2-rc.1",
87
+ "@deepseek-ai/dsh-session": ">=0.2.2-rc.1",
88
+ "@deepseek-ai/dsh-typert-protocol": ">=0.2.2-rc.1",
89
89
  "@types/node": "^22.20.0",
90
90
  "@types/react": "~18.3.1",
91
91
  "@types/react-dom": "~18.3.0",
@@ -29,9 +29,11 @@ import {
29
29
  reasoningOf,
30
30
  rowsForTab,
31
31
  searchRows,
32
+ type CatalogModel,
32
33
  type ModelRow as CatalogRow,
33
34
  type PickerTab,
34
35
  } from './rows.ts'
36
+ import { Tag, isMobileDevice } from '@deepseek-ai/dsh-client-ui-primitives'
35
37
  import type { ModelFactsLookup } from './facts-source.ts'
36
38
  import type { Selection } from './selection.ts'
37
39
  import { planModelCommit } from './selection.ts'
@@ -41,6 +43,8 @@ import css from './ModelSubmenu.module.css'
41
43
  export interface ModelPickerProps {
42
44
  /** Whether the dialog is showing. */
43
45
  readonly open: boolean
46
+ /** Resolve a catalog badge's localized text for the active language. */
47
+ readonly resolveText: (text: NonNullable<CatalogModel['badge']>['text']) => string
44
48
  /** The shared directory snapshot. */
45
49
  readonly state: ModelDirectoryState
46
50
  /** The selection in force, null before a first selection resolves. */
@@ -67,7 +71,7 @@ export interface ModelPickerProps {
67
71
  * @returns the dialog; nothing while closed.
68
72
  */
69
73
  export function ModelPicker({
70
- open, state, live, facts, favorites, busy, onPick, onToggleFavorite, onReload, onClose, t,
74
+ open, resolveText, state, live, facts, favorites, busy, onPick, onToggleFavorite, onReload, onClose, t,
71
75
  }: ModelPickerProps & PropsLocale<'modelSubmenu'>) {
72
76
  const id = useId()
73
77
  const searchRef = useRef<HTMLInputElement | null>(null)
@@ -97,7 +101,10 @@ export function ModelPicker({
97
101
  setTab(ALL_TAB)
98
102
  setHighlight(0)
99
103
  if (listRef.current !== null) listRef.current.scrollTop = 0
100
- const frame = requestAnimationFrame(() => { searchRef.current?.focus() })
104
+ // 粗指针设备不自动聚焦文本输入:移动端弹键盘会顶起布局,宿主座位也这样守。
105
+ const frame = requestAnimationFrame(() => {
106
+ if (!isMobileDevice()) searchRef.current?.focus()
107
+ })
101
108
  return () => { cancelAnimationFrame(frame) }
102
109
  }, [open])
103
110
 
@@ -246,6 +253,9 @@ export function ModelPicker({
246
253
  <div key={`${row.provider}/${row.model}`} data-row-index={index} id={`${id}-row-${index}`}>
247
254
  <ModelRow
248
255
  name={row.entry.name}
256
+ badge={row.entry.badge === undefined
257
+ ? undefined
258
+ : <Tag tone={row.entry.badge.tone} className={css.rowBadge}>{resolveText(row.entry.badge.text)}</Tag>}
249
259
  selected={live?.provider === row.provider && live.model === row.model}
250
260
  busy={busy}
251
261
  star="interactive"
@@ -20,6 +20,8 @@ export type StarMode = 'static' | 'interactive'
20
20
  export interface ModelRowProps {
21
21
  /** The model's catalog name. */
22
22
  readonly name: string
23
+ /** Optional catalog badge shown beside the name. */
24
+ readonly badge?: ReactNode | undefined
23
25
  /** Whether this row is the model in use. */
24
26
  readonly selected: boolean
25
27
  /** Whether a selection is in flight, which locks the row. */
@@ -52,13 +54,14 @@ export interface ModelRowProps {
52
54
  * @returns the row element.
53
55
  */
54
56
  export function ModelRow({
55
- name, selected, busy, star, favorited, meta, starLabel,
57
+ name, badge, selected, busy, star, favorited, meta, starLabel,
56
58
  starTabStop = false, rowTabIndex, onPick, onFocusRow, onToggleFavorite, children,
57
59
  }: ModelRowProps) {
58
60
  const copy = (
59
61
  <>
60
62
  <span className={css.rowCopy}>
61
63
  <span className={css.rowName}>{name}</span>
64
+ {badge}
62
65
  {meta !== undefined && meta !== '' && <span className={css.rowMeta}>{meta}</span>}
63
66
  </span>
64
67
  </>
@@ -71,6 +71,16 @@
71
71
  display: var(--dsh-composer-model-icon-display, none);
72
72
  }
73
73
 
74
+ .triggerBadge {
75
+ flex: none;
76
+ margin-inline-end: 4px;
77
+ }
78
+
79
+ .rowBadge {
80
+ flex: none;
81
+ margin-inline-start: 6px;
82
+ }
83
+
74
84
  .chevron {
75
85
  color: var(--dsw-alias-label-caption);
76
86
  flex: none;
@@ -88,7 +98,7 @@
88
98
  * opening the menu never reflows the composer. */
89
99
  box-sizing: border-box;
90
100
  width: min(252px, calc(100vw - 24px));
91
- max-height: min(70vh, calc(100vh - 96px));
101
+ max-height: var(--dsh-anchored-max-height, min(70vh, calc(100vh - 96px)));
92
102
  box-shadow: var(--dsw-elevation-prominent);
93
103
  color: var(--dsw-alias-label-primary);
94
104
  --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);
@@ -1,11 +1,14 @@
1
- import { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react'
1
+ import { useCallback, useEffect, useId, useMemo, useRef, useState, useSyncExternalStore } from 'react'
2
2
  import { createPortal } from 'react-dom'
3
3
  import {
4
4
  IconChevronDownOutlineRegular,
5
5
  IconFlatListOutlineRegular,
6
6
  IconWarningOutlineRegular,
7
7
  MenuSurface,
8
+ StateDot,
9
+ Tag,
8
10
  Toast,
11
+ useAnchoredPosition,
9
12
  } from '@deepseek-ai/dsh-client-ui-primitives'
10
13
  import type { ModelDirectoryState, ModelSelectInjected } from '@deepseek-ai/dsh-client-ui-model-selection/client'
11
14
  import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'
@@ -53,7 +56,7 @@ const cls = (...parts: readonly (string | false | undefined)[]): string => parts
53
56
  * Closing the menu never writes anything, and the picker reads the level the
54
57
  * Host already accepted, so its "current" reading is never stale.
55
58
  */
56
- export function ModelSubmenu({ locked, available, directory, load, select, t, factsSource }: ModelSubmenuProps) {
59
+ export function ModelSubmenu({ locked, available, directory, load, select, resolveText, t, factsSource }: ModelSubmenuProps) {
57
60
  const state = useSyncExternalStore((fn) => directory.subscribe(fn), () => directory.getSnapshot())
58
61
  const [open, setOpen] = useState(false)
59
62
  const [pickerOpen, setPickerOpen] = useState(false)
@@ -64,7 +67,6 @@ export function ModelSubmenu({ locked, available, directory, load, select, t, fa
64
67
  const rootRef = useRef<HTMLDivElement | null>(null)
65
68
  const triggerRef = useRef<HTMLButtonElement | null>(null)
66
69
  const menuRef = useRef<HTMLDivElement | null>(null)
67
- const [menuPos, setMenuPos] = useState<{ left: number; top: number } | null>(null)
68
70
  const id = useId()
69
71
 
70
72
  const groups = state.groups
@@ -85,6 +87,13 @@ export function ModelSubmenu({ locked, available, directory, load, select, t, fa
85
87
  const busy = state.status === 'selecting'
86
88
  const live = useMemo(() => selectionOf(state), [state])
87
89
  const reason = useMemo(() => reasoningOf(groups), [groups])
90
+ /** Resolve one catalog badge to its localized text and tone, matching the host seat. */
91
+ const badgeOf = (model: CatalogModel) => model.badge === undefined
92
+ ? undefined
93
+ : <Tag tone={model.badge.tone} className={css.rowBadge}>{resolveText(model.badge.text)}</Tag>
94
+ const triggerBadge = currentModel?.badge === undefined
95
+ ? null
96
+ : <Tag tone={currentModel.badge.tone} className={css.triggerBadge}>{resolveText(currentModel.badge.text)}</Tag>
88
97
  const facts = useSyncExternalStore((fn) => factsSource.subscribe(fn), () => factsSource.getSnapshot())
89
98
 
90
99
  const menuRows = useMemo(() => currentGroup === undefined ? [] : orderWithFavorites(
@@ -144,32 +153,16 @@ export function ModelSubmenu({ locked, available, directory, load, select, t, fa
144
153
  return () => { cancelAnimationFrame(frame) }
145
154
  }, [open])
146
155
 
147
- useLayoutEffect(() => {
148
- if (!open) {
149
- setMenuPos(null)
150
- return
151
- }
152
- const place = () => {
153
- const rect = triggerRef.current?.getBoundingClientRect()
154
- if (rect === undefined) return
155
- const MARGIN = 12
156
- const lw = menuRef.current?.offsetWidth ?? 0
157
- const lh = menuRef.current?.offsetHeight ?? 0
158
- // Right edges align, so the card grows leftward from the trigger's corner.
159
- let x = rect.right - lw
160
- let y = rect.top - 8 - lh
161
- if (lw > 0) x = Math.min(Math.max(x, MARGIN), window.innerWidth - lw - MARGIN)
162
- if (lh > 0) y = Math.min(Math.max(y, MARGIN), window.innerHeight - lh - MARGIN)
163
- setMenuPos({ left: x, top: y })
164
- }
165
- place()
166
- window.addEventListener('scroll', place, true)
167
- window.addEventListener('resize', place)
168
- return () => {
169
- window.removeEventListener('scroll', place, true)
170
- window.removeEventListener('resize', place)
171
- }
172
- }, [open, state])
156
+ const menuPos = useAnchoredPosition({
157
+ open: open && available,
158
+ anchorRef: triggerRef,
159
+ panelRef: menuRef,
160
+ side: 'top',
161
+ align: 'end',
162
+ gap: 8,
163
+ margin: 12,
164
+ layoutKey: `${state.status}:${String(menuRows.length)}:${String(choices.length)}`,
165
+ })
173
166
 
174
167
  if (!available) return null
175
168
 
@@ -299,9 +292,12 @@ export function ModelSubmenu({ locked, available, directory, load, select, t, fa
299
292
  }}
300
293
  >
301
294
  <IconFlatListOutlineRegular className={css.triggerIcon} size={16} />
295
+ {triggerBadge}
302
296
  <span className={css.triggerLabel}>{modelLabel}</span>
303
297
  {effortLabel !== undefined && <span className={css.triggerEffort}>{effortLabel}</span>}
304
- <IconChevronDownOutlineRegular className={cls(css.chevron, open && css.chevronOpen)} />
298
+ {busy
299
+ ? <StateDot state="ongoing" />
300
+ : <IconChevronDownOutlineRegular className={cls(css.chevron, open && css.chevronOpen)} />}
305
301
  </button>
306
302
  {open && createPortal((
307
303
  <MenuSurface
@@ -332,6 +328,7 @@ export function ModelSubmenu({ locked, available, directory, load, select, t, fa
332
328
  <ModelRow
333
329
  key={model.id}
334
330
  name={model.name}
331
+ badge={badgeOf(model)}
335
332
  selected={current !== null && currentGroup !== undefined && current.provider === currentGroup.id && current.model === model.id}
336
333
  busy={busy}
337
334
  star="static"
@@ -384,6 +381,7 @@ export function ModelSubmenu({ locked, available, directory, load, select, t, fa
384
381
  ), document.body)}
385
382
  <ModelPicker
386
383
  open={pickerOpen}
384
+ resolveText={resolveText}
387
385
  state={state}
388
386
  live={live}
389
387
  facts={facts}
@@ -72,6 +72,7 @@ export function apply(ctx: ClientContext): void {
72
72
  load: () => {
73
73
  if (available) directory.load().catch(() => {})
74
74
  },
75
+ resolveText: (text) => ctx.locale.resolveText(text),
75
76
  select: (selection) => available ? directory.select(selection) : Promise.resolve(undefined),
76
77
  }
77
78
  },