@lynn123411/dsh-model-submenu 0.3.10 → 0.3.11

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/DETAILS.md CHANGED
@@ -72,13 +72,15 @@ dsh plugin --profile web add @lynn123411/dsh-model-submenu
72
72
 
73
73
  | 状态 | 轨道 | 旋钮 |
74
74
  | --- | --- | --- |
75
- | Off | 灰色,只填到旋钮 | 实心白 |
76
- | 中间档 | 该档的纯色,只填到旋钮 | 实心白 |
77
- | 最高档 | 整条蓝→紫渐变 | 实心白 |
75
+ | Off | 空——填充只有旋钮那么大,完全藏在旋钮底下 | 实心白 |
76
+ | 中间档 | 该档的纯色,右端圆弧落在旋钮底下 | 实心白 |
77
+ | 最高档 | 整条蓝→紫渐变,铺到轨道尽头 | 实心白 |
78
78
  | provider default | 空 | 空心环,停在模型声明的默认档(没声明就停最左) |
79
- | 拖动中 | 跟着指针吸附 | 实心白,松手才提交 |
79
+ | 拖动中 | 跟着指针连续移动 | 实心白,松手吸附到最近一档并提交 |
80
80
 
81
- 轨道高度取这一行的文字行高(20px),旋钮与轨道同高。provider default 不是档位:`effortChoices` 只返回模型声明的真实等级,默认态由 `state.current.reasoningEffort === undefined` 判定,点标签提交 `reasoningEffort: undefined` 切回去。颜色由 `level-color.ts` 的 `levelColor(index, count)` 给出:`0` 是中性灰,其余档位把「Off 以上」均匀铺在蓝→紫的斜坡上,最高档固定取紫端并用整条渐变填充。
81
+ 轨道高 16px、旋钮直径 20px:旋钮比轨道大一圈,两端因此露出轨道自己的圆角,旋钮不会被误读成轨道的端帽。填充是「轨道形状裁到旋钮」——右端圆弧取轨道的半径、圆心落在旋钮底下,所以色块是钻进旋钮而不是在它旁边直切一刀;最高档的填充宽度正好铺满整条轨道。
82
+
83
+ 拖动时旋钮跟的是指针的连续位置(`fractionAt`),不是一档一格:档位取离指针最近的那个刻度(`detentAt`),所以旋钮在两档之间也能停在任意位置,只有跨过中点才换档。松手时旋钮用 160ms 的缓出滑到那个刻度上再提交;拖动期间这条过渡关掉(`data-dragging`),否则旋钮会落后于指针。`prefers-reduced-motion` 下过渡一律关掉。provider default 不是档位:`effortChoices` 只返回模型声明的真实等级,默认态由 `state.current.reasoningEffort === undefined` 判定,点标签提交 `reasoningEffort: undefined` 切回去。颜色由 `level-color.ts` 的 `levelColor(index, count)` 给出:`0` 是中性灰,其余档位把「Off 以上」均匀铺在蓝→紫的斜坡上,最高档固定取紫端并用整条渐变填充。
82
84
 
83
85
  ## 开发副本
84
86
 
@@ -102,7 +104,7 @@ cd plugins/dsh-model-submenu && pnpm check
102
104
  `check` = `typecheck` + `build`(tsdown)+ `smoke` + `test` + `assert:lib-untracked`。
103
105
 
104
106
  - `scripts/smoke-client.mjs` 按 Web shell 的方式装载构建产物 `lib/client.js`(`window.__ModuleLoader__.load` 桩件),喂假 cordis 服务,断言导出身份、`inject` 表逐字相等、三处贡献各自的 list `id`(`model-menu-bridge`、`model-picker`、`model-effort-tint`)、前两处共用同一个 store、着色那条读同一个目录、产物里走的是共享的 `Modal`、产物里带着认宿主菜单面板(`data-menu-material`)、两格(`:scope > button[role=`)、档位文字(`span[class*='cellValue']`)与触发器档位文字(`span[class*="triggerEffort"]`)的四段选择器、滑块以 `role="slider"` 暴露给辅助技术、字典键集中英对齐且带着滑块那三条文案、元信息取数确实问过设置镜像与供应商目录,以及「设置没答案时答未知而不猜」。末尾 7 条是 store 自身的契约:起始关闭、open 发布目标、重复 open 不通知、第二次 open 替换目标、close 清空、退订后静默。
105
- - `scripts/test-favorites.mjs`:收藏表的键名、读写、坏值降级、跨供应商靠前排序。`scripts/test-facts.mjs`:设置值到上下文窗口与图片输入的映射、两类适配器的字段差异、缺字段整段略去。`scripts/test-commit.mjs`:点模型立即提交、已选中的模型不重复提交、记住的档位不在可选档里时不强塞。`scripts/test-rows.mjs`:目录拍平、标签页派生、搜索的两把钥匙。`scripts/test-slider.mjs`:滑块几何与配色——档位到轨道比例、指针到最近档、两端夹取、单档与零宽轨道的退路、Off 的中性灰、蓝到紫的两端、只有最高档用渐变。`scripts/test-render.mjs`:用真 react 把菜单桥、触发器着色与弹窗画进 jsdom,40 条用例锁住「点宿主第一格 → 关菜单 → store → 弹窗」这条链、宿主那两格的下钻确实没执行、点标签切回默认、点滑块以外的位置把焦点交给滑块、刻度数与旋钮位置、数值与箭头隐藏、Off 灰/中间纯色/最高档渐变、触发器档位文字跟着变色且默认态交还宿主颜色、默认态空轨道与空心旋钮、拖动松手才提交一次、按下与松手同任务也不丢提交、末档与进行中不提交、被拒走宿主提示、单档与无档位不挂滑块、卸载后样式写回,以及弹窗侧的收藏置顶、星标独立、提交与拒绝、思考等级记忆、加载与失败、会话不可选时整块不渲染。
107
+ - `scripts/test-favorites.mjs`:收藏表的键名、读写、坏值降级、跨供应商靠前排序。`scripts/test-facts.mjs`:设置值到上下文窗口与图片输入的映射、两类适配器的字段差异、缺字段整段略去。`scripts/test-commit.mjs`:点模型立即提交、已选中的模型不重复提交、记住的档位不在可选档里时不强塞。`scripts/test-rows.mjs`:目录拍平、标签页派生、搜索的两把钥匙。`scripts/test-slider.mjs`:滑块几何与配色——档位到轨道比例、拖动位置是连续比例而不是逐档跳、比例换回最近一档且跨过中点才变、指针到最近档、两端夹取、单档与零宽轨道的退路、填充圆弧始终藏在旋钮底下且末档铺满轨道、Off 的中性灰、蓝到紫的两端、只有最高档用渐变。`scripts/test-render.mjs`:用真 react 把菜单桥、触发器着色与弹窗画进 jsdom,40 条用例锁住「点宿主第一格 → 关菜单 → store → 弹窗」这条链、宿主那两格的下钻确实没执行、点标签切回默认、点滑块以外的位置把焦点交给滑块、刻度数与旋钮位置、数值与箭头隐藏、Off 灰/中间纯色/最高档渐变、触发器档位文字跟着变色且默认态交还宿主颜色、默认态空轨道与空心旋钮、拖动松手才提交一次、按下与松手同任务也不丢提交、末档与进行中不提交、被拒走宿主提示、单档与无档位不挂滑块、卸载后样式写回,以及弹窗侧的收藏置顶、星标独立、提交与拒绝、思考等级记忆、加载与失败、会话不可选时整块不渲染。
106
108
  - `lib/` 是构建镜像,`assert:lib-untracked` 判它没被 git 跟踪。
107
109
 
108
110
  要肉眼确认页面上的形态,用仓库的 `node tools/verify-client.mjs dsh-model-submenu --needle '<你新写的类名或文案>'`。
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  ## 功能
6
6
 
7
7
  - **第一行就是入口**:菜单里的「模型」点下去直接打开弹窗,不再只列当前供应商;顶部搜索框同时搜模型名与供应商名,每个有模型的供应商一个标签页。
8
- - **第二行是滑块**:推理等级不再下钻到档位列表,拖动轨道松手即切换,键盘左右键逐档调整;轨道上的刻度点数就是该模型可选的档位数,高度与这一行的文字齐平。
8
+ - **第二行是滑块**:推理等级不再下钻到档位列表,拖动时旋钮跟着指针走、松手吸附到最近一档并切换,键盘左右键逐档调整;轨道上的刻度点数就是该模型可选的档位数,旋钮比轨道大一圈。
9
9
  - **档位有颜色**:Off 是灰色,往上从蓝走到紫,只有最高档的填充是渐变;输入框触发器上的档位文字跟着同一套颜色走。
10
10
  - **默认档位是一个状态**:点最左边的「推理等级」四个字切回 provider default——轨道空着、旋钮是空心环,不会被误认成最低档;再拖一下滑块就回到明确档位。
11
11
  - **收藏靠前**:每行右侧一个星标,点它只切换收藏、不切换模型;收藏表存在浏览器本地,整台浏览器共用一张,收藏的模型排在列表前面。
package/lib/client.js CHANGED
@@ -198,6 +198,8 @@ window.__ModuleLoader__.load({
198
198
  hidden: true
199
199
  });
200
200
  }
201
+ /** The knob's centre keeps its own radius away from either end of the track. */
202
+ const KNOB_RADIUS = 10;
201
203
  /**
202
204
  * @param index - detent index.
203
205
  * @param count - number of detents.
@@ -208,17 +210,26 @@ window.__ModuleLoader__.load({
208
210
  return Math.min(Math.max(index, 0), count - 1) / (count - 1);
209
211
  }
210
212
  /**
213
+ * Where a pointer sits along the knob's travel. The drag reads this rather than a
214
+ * detent, so the knob follows the pointer instead of jumping a level at a time.
211
215
  * @param clientX - pointer position in viewport coordinates.
212
216
  * @param left - the track's left edge in viewport coordinates.
213
217
  * @param width - the track's width in pixels.
214
- * @param count - number of detents.
215
- * @returns the detent nearest the pointer, clamped to the two ends.
218
+ * @returns the position as a fraction of the travel, clamped to 0 and 1.
216
219
  */
217
- function stepAt(clientX, left, width, count) {
218
- if (count <= 1) return 0;
220
+ function fractionAt(clientX, left, width) {
219
221
  const usable = width - 20;
220
222
  if (usable <= 0) return 0;
221
- const fraction = (clientX - left - 10) / usable;
223
+ const fraction = (clientX - left - KNOB_RADIUS) / usable;
224
+ return Math.min(Math.max(fraction, 0), 1);
225
+ }
226
+ /**
227
+ * @param fraction - a knob position between 0 and 1.
228
+ * @param count - number of detents.
229
+ * @returns the detent nearest that position.
230
+ */
231
+ function detentAt(fraction, count) {
232
+ if (count <= 1) return 0;
222
233
  return Math.round(Math.min(Math.max(fraction, 0), 1) * (count - 1));
223
234
  }
224
235
  /**
@@ -226,11 +237,24 @@ window.__ModuleLoader__.load({
226
237
  * @returns the CSS length that puts the knob centre there, inset at both ends.
227
238
  */
228
239
  function knobOffset(fraction) {
229
- return `calc(10px + ${Math.min(Math.max(fraction, 0), 1)} * (100% - 20px))`;
240
+ return `calc(${KNOB_RADIUS}px + ${Math.min(Math.max(fraction, 0), 1)} * (100% - 20px))`;
241
+ }
242
+ /**
243
+ * Width of the colored fill for a knob position.
244
+ *
245
+ * The fill is the track's own shape cut at the knob: its right cap carries the
246
+ * track's radius and ends under the knob, so the color runs into the knob
247
+ * instead of stopping at a straight edge, and the last detent reaches the track's
248
+ * end rather than the knob's centre.
249
+ * @param fraction - a knob position between 0 and 1.
250
+ * @returns the CSS width of the fill.
251
+ */
252
+ function fillWidth(fraction) {
253
+ return `calc(16px + ${Math.min(Math.max(fraction, 0), 1)} * (100% - 16px))`;
230
254
  }
231
255
  //#endregion
232
256
  //#region \0dsh-css:/home/runner/work/my-dsh/my-dsh/plugins/dsh-model-submenu/src/client/ModelOptions.module.css.mjs
233
- const css = ".TcCbqa_pickerHost{display:contents}.TcCbqa_sliderCell{flex:auto;align-self:stretch;align-items:center;min-width:96px;display:flex}.TcCbqa_slider{cursor:pointer;touch-action:none;-webkit-user-select:none;user-select:none;outline:none;flex:1;align-items:center;min-width:0;height:100%;display:flex}.TcCbqa_slider[data-busy]{cursor:default}.TcCbqa_sliderTrack{corner-shape:round;background:var(--dsw-alias-border-l3);border-radius:999px;width:100%;height:20px;position:relative}.TcCbqa_sliderFill{corner-shape:round;pointer-events:none;border-radius:999px;position:absolute;top:0;bottom:0;left:0;overflow:hidden}.TcCbqa_sliderTick{corner-shape:round;background:var(--dsw-alias-label-tertiary);pointer-events:none;border-radius:50%;width:4px;height:4px;margin:-2px 0 0 -2px;position:absolute;top:50%}.TcCbqa_sliderTick[data-on]{background:#00000047}.TcCbqa_sliderKnob{corner-shape:round;background:var(--dsw-alias-label-primary-foreground);width:20px;height:20px;box-shadow:var(--dsw-elevation-soft);pointer-events:none;border-radius:50%;margin:-10px 0 0 -10px;position:absolute;top:50%}.TcCbqa_slider[data-default] .TcCbqa_sliderKnob{background:var(--dsw-alias-bg-layer-1);border:1.5px solid var(--dsw-alias-label-tertiary);box-shadow:none}.TcCbqa_slider[data-busy] .TcCbqa_sliderKnob{opacity:.6}.TcCbqa_slider:focus-visible .TcCbqa_sliderTrack{outline:var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:3px}.TcCbqa_rowBadge{flex:none;margin-inline-start:6px}.TcCbqa_row{align-items:center;gap:2px;display:flex}.TcCbqa_rowPick{box-sizing:border-box;width:100%;min-width:0;min-height:34px;color:inherit;text-align:left;cursor:pointer;border-radius:var(--dsw-radius-md);padding-inline:6px;background:0 0;border:none;outline:none;flex:1;align-items:center;gap:4px;padding:5px 7px;display:flex}.TcCbqa_rowPick:hover:not(:disabled),.TcCbqa_rowPick:focus-visible{background:var(--dsw-alias-interactive-bg-hover)}.TcCbqa_rowPick:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}.TcCbqa_rowCopy{flex-direction:column;flex:1;min-width:0;display:flex}.TcCbqa_rowName{color:inherit;text-overflow:ellipsis;white-space:nowrap;font-size:11.5px;font-weight:400;line-height:17px;overflow:hidden}.TcCbqa_rowMeta{color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;font-size:12px;line-height:18px;overflow:hidden}.TcCbqa_check{color:var(--dsw-alias-label-primary);flex:0 0 15px;place-items:center;display:grid}.TcCbqa_starButton{color:var(--dsw-alias-label-tertiary);cursor:pointer;border-radius:var(--dsw-radius-sm);opacity:0;background:0 0;border:none;outline:none;flex:none;place-items:center;width:26px;height:26px;display:grid}.TcCbqa_starButton:focus-visible{outline:2px solid var(--dsw-alias-border-l3)}.TcCbqa_row:hover .TcCbqa_starButton,.TcCbqa_row:focus-within .TcCbqa_starButton{opacity:1}.TcCbqa_starButton[aria-pressed=true]{color:var(--dsw-alias-state-warn-primary);opacity:1}@media (hover:none){.TcCbqa_starButton{opacity:1}}.TcCbqa_star{flex:none}.TcCbqa_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}.TcCbqa_retry{color:inherit;font:inherit;cursor:pointer;background:0 0;border:none;flex:none;padding:0;font-weight:600;text-decoration:underline}.TcCbqa_pickerDialog{gap:0;width:min(680px,100%);height:min(640px,80vh);padding:0}.TcCbqa_pickerContent,.TcCbqa_pickerContent>:last-child{flex-direction:column;flex:1;min-height:0;display:flex}.TcCbqa_pickerSearch{padding:0 20px 12px}.TcCbqa_pickerSearchInput{width:100%}.TcCbqa_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}.TcCbqa_pickerTabs::-webkit-scrollbar{display:none}.TcCbqa_pickerTab{color:var(--dsw-alias-label-secondary);cursor:pointer;border-radius:var(--dsw-radius-sm);white-space:nowrap;background:0 0;border:none;outline:none;flex:none;padding:6px 10px;font-size:13px;line-height:18px}.TcCbqa_pickerTab:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.TcCbqa_pickerTab[aria-selected=true]{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover);font-weight:600}.TcCbqa_pickerTab:focus-visible{outline:2px solid var(--dsw-alias-border-l3)}.TcCbqa_pickerList{flex:1;min-height:0;padding:8px 12px;overflow-y:auto}.TcCbqa_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}.TcCbqa_footerHint{flex-wrap:wrap;align-items:center;gap:6px;display:flex}.TcCbqa_footerDot{color:var(--dsw-alias-label-dimmed)}";
257
+ const css = ".TcCbqa_pickerHost{display:contents}.TcCbqa_sliderCell{flex:auto;align-self:stretch;align-items:center;min-width:96px;display:flex}.TcCbqa_slider{cursor:pointer;touch-action:none;-webkit-user-select:none;user-select:none;outline:none;flex:1;align-items:center;min-width:0;height:100%;display:flex}.TcCbqa_slider[data-busy]{cursor:default}.TcCbqa_sliderTrack{corner-shape:round;background:var(--dsw-alias-border-l3);border-radius:999px;width:100%;height:16px;position:relative}.TcCbqa_sliderFill{corner-shape:round;pointer-events:none;border-radius:999px;transition:width .16s cubic-bezier(.2,0,0,1);position:absolute;top:0;bottom:0;left:0}.TcCbqa_sliderTick{corner-shape:round;background:var(--dsw-alias-label-tertiary);pointer-events:none;border-radius:50%;width:4px;height:4px;margin:-2px 0 0 -2px;position:absolute;top:50%}.TcCbqa_sliderTick[data-on]{background:#00000047}.TcCbqa_sliderKnob{box-sizing:border-box;corner-shape:round;background:var(--dsw-alias-label-primary-foreground);width:20px;height:20px;box-shadow:var(--dsw-elevation-soft);pointer-events:none;border-radius:50%;margin:-10px 0 0 -10px;transition:left .16s cubic-bezier(.2,0,0,1);position:absolute;top:50%}.TcCbqa_slider[data-dragging] .TcCbqa_sliderKnob,.TcCbqa_slider[data-dragging] .TcCbqa_sliderFill{transition:none}@media (prefers-reduced-motion:reduce){.TcCbqa_sliderKnob,.TcCbqa_sliderFill{transition:none}}.TcCbqa_slider[data-default] .TcCbqa_sliderKnob{background:var(--dsw-alias-bg-layer-1);border:1.5px solid var(--dsw-alias-label-tertiary);box-shadow:none}.TcCbqa_slider[data-busy] .TcCbqa_sliderKnob{opacity:.6}.TcCbqa_slider:focus-visible .TcCbqa_sliderTrack{outline:var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:3px}.TcCbqa_rowBadge{flex:none;margin-inline-start:6px}.TcCbqa_row{align-items:center;gap:2px;display:flex}.TcCbqa_rowPick{box-sizing:border-box;width:100%;min-width:0;min-height:34px;color:inherit;text-align:left;cursor:pointer;border-radius:var(--dsw-radius-md);padding-inline:6px;background:0 0;border:none;outline:none;flex:1;align-items:center;gap:4px;padding:5px 7px;display:flex}.TcCbqa_rowPick:hover:not(:disabled),.TcCbqa_rowPick:focus-visible{background:var(--dsw-alias-interactive-bg-hover)}.TcCbqa_rowPick:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}.TcCbqa_rowCopy{flex-direction:column;flex:1;min-width:0;display:flex}.TcCbqa_rowName{color:inherit;text-overflow:ellipsis;white-space:nowrap;font-size:11.5px;font-weight:400;line-height:17px;overflow:hidden}.TcCbqa_rowMeta{color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;font-size:12px;line-height:18px;overflow:hidden}.TcCbqa_check{color:var(--dsw-alias-label-primary);flex:0 0 15px;place-items:center;display:grid}.TcCbqa_starButton{color:var(--dsw-alias-label-tertiary);cursor:pointer;border-radius:var(--dsw-radius-sm);opacity:0;background:0 0;border:none;outline:none;flex:none;place-items:center;width:26px;height:26px;display:grid}.TcCbqa_starButton:focus-visible{outline:2px solid var(--dsw-alias-border-l3)}.TcCbqa_row:hover .TcCbqa_starButton,.TcCbqa_row:focus-within .TcCbqa_starButton{opacity:1}.TcCbqa_starButton[aria-pressed=true]{color:var(--dsw-alias-state-warn-primary);opacity:1}@media (hover:none){.TcCbqa_starButton{opacity:1}}.TcCbqa_star{flex:none}.TcCbqa_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}.TcCbqa_retry{color:inherit;font:inherit;cursor:pointer;background:0 0;border:none;flex:none;padding:0;font-weight:600;text-decoration:underline}.TcCbqa_pickerDialog{gap:0;width:min(680px,100%);height:min(640px,80vh);padding:0}.TcCbqa_pickerContent,.TcCbqa_pickerContent>:last-child{flex-direction:column;flex:1;min-height:0;display:flex}.TcCbqa_pickerSearch{padding:0 20px 12px}.TcCbqa_pickerSearchInput{width:100%}.TcCbqa_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}.TcCbqa_pickerTabs::-webkit-scrollbar{display:none}.TcCbqa_pickerTab{color:var(--dsw-alias-label-secondary);cursor:pointer;border-radius:var(--dsw-radius-sm);white-space:nowrap;background:0 0;border:none;outline:none;flex:none;padding:6px 10px;font-size:13px;line-height:18px}.TcCbqa_pickerTab:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.TcCbqa_pickerTab[aria-selected=true]{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover);font-weight:600}.TcCbqa_pickerTab:focus-visible{outline:2px solid var(--dsw-alias-border-l3)}.TcCbqa_pickerList{flex:1;min-height:0;padding:8px 12px;overflow-y:auto}.TcCbqa_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}.TcCbqa_footerHint{flex-wrap:wrap;align-items:center;gap:6px;display:flex}.TcCbqa_footerDot{color:var(--dsw-alias-label-dimmed)}";
234
258
  const tagId = "@lynn123411/dsh-model-submenu/ModelOptions.module.css";
235
259
  if (typeof document !== "undefined") {
236
260
  let tag = document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]");
@@ -279,9 +303,10 @@ window.__ModuleLoader__.load({
279
303
  *
280
304
  * The Host's second menu cell is a button that drills into a list of levels.
281
305
  * This slider sits inside that same cell, between the Host's label and value, so
282
- * the levels are reachable without leaving the menu. Levels are discrete: the
283
- * knob snaps to one detent per level, the drag only moves the knob, and the
284
- * release commits once. Keyboard arrows step and commit immediately.
306
+ * the levels are reachable without leaving the menu. The levels are discrete but
307
+ * the drag is not: the knob follows the pointer continuously, the level it
308
+ * reports is the detent nearest that position, and letting go eases the knob onto
309
+ * that detent and commits once. Keyboard arrows step and commit immediately.
285
310
  *
286
311
  * The fill takes the level's color, so the track and the two captions read as
287
312
  * one signal: "off" is neutral, the levels above it walk a blue-to-violet ramp,
@@ -299,22 +324,34 @@ window.__ModuleLoader__.load({
299
324
  const trackRef = (0, react.useRef)(null);
300
325
  const dragRef = (0, react.useRef)(null);
301
326
  const [drag, setDrag] = (0, react.useState)(null);
302
- const active = drag ?? (defaultActive ? defaultIndex : current);
303
- const beginDrag = (index) => {
304
- dragRef.current = index;
305
- setDrag(index);
327
+ const [hold, setHold] = (0, react.useState)(null);
328
+ const dragging = drag !== null;
329
+ const resting = defaultActive ? defaultIndex : current;
330
+ const active = dragging ? detentAt(drag, steps.length) : resting;
331
+ const knobFraction = drag ?? hold ?? offsetOf(resting, steps.length);
332
+ const shown = dragging || hold !== null ? detentAt(knobFraction, steps.length) : resting;
333
+ const beginDrag = (fraction) => {
334
+ dragRef.current = fraction;
335
+ setDrag(fraction);
306
336
  };
307
- const indexAt = (clientX) => {
337
+ (0, react.useEffect)(() => {
338
+ if (!busy) setHold(null);
339
+ }, [busy]);
340
+ (0, react.useEffect)(() => {
341
+ setHold(null);
342
+ }, [current, defaultActive]);
343
+ const fractionAtPointer = (clientX) => {
308
344
  const track = trackRef.current;
309
- if (track === null) return active;
345
+ if (track === null) return dragRef.current ?? knobFraction;
310
346
  const rect = track.getBoundingClientRect();
311
- return stepAt(clientX, rect.left, rect.width, steps.length);
347
+ return fractionAt(clientX, rect.left, rect.width);
312
348
  };
313
349
  const onPointerDown = (event) => {
314
350
  if (busy) return;
315
351
  event.preventDefault();
316
352
  event.stopPropagation();
317
- beginDrag(indexAt(event.clientX));
353
+ setHold(null);
354
+ beginDrag(fractionAtPointer(event.clientX));
318
355
  try {
319
356
  event.currentTarget.setPointerCapture(event.pointerId);
320
357
  } catch {}
@@ -322,15 +359,17 @@ window.__ModuleLoader__.load({
322
359
  };
323
360
  const onPointerMove = (event) => {
324
361
  if (dragRef.current === null || busy) return;
325
- const next = indexAt(event.clientX);
326
- if (next !== dragRef.current) beginDrag(next);
362
+ beginDrag(fractionAtPointer(event.clientX));
327
363
  };
328
364
  const settle = (event, commit) => {
329
365
  if (dragRef.current === null) return;
330
- const next = indexAt(event.clientX);
366
+ const next = detentAt(fractionAtPointer(event.clientX), steps.length);
331
367
  beginDrag(null);
332
368
  if (!commit) return;
333
- if (next !== current || defaultActive) onCommit(next);
369
+ if (next !== current || defaultActive) {
370
+ setHold(offsetOf(next, steps.length));
371
+ onCommit(next);
372
+ }
334
373
  };
335
374
  const onKeyDown = (event) => {
336
375
  if (busy) return;
@@ -341,11 +380,10 @@ window.__ModuleLoader__.load({
341
380
  event.stopPropagation();
342
381
  if (next !== current || defaultActive) onCommit(next);
343
382
  };
344
- const knobFraction = offsetOf(active, steps.length);
345
383
  const offset = knobOffset(knobFraction);
346
- const idle = defaultActive && drag === null;
384
+ const idle = defaultActive && !dragging && hold === null;
347
385
  const filled = idle ? 0 : knobFraction;
348
- const fill = isTopLevel(active, steps.length) ? LEVEL_GRADIENT : levelColor(active, steps.length);
386
+ const fill = isTopLevel(shown, steps.length) ? LEVEL_GRADIENT : levelColor(shown, steps.length);
349
387
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
350
388
  ref: trackRef,
351
389
  className: ModelOptions_module_css_default.slider,
@@ -355,11 +393,11 @@ window.__ModuleLoader__.load({
355
393
  "aria-valuemin": 0,
356
394
  "aria-valuemax": Math.max(0, steps.length - 1),
357
395
  "aria-valuenow": active,
358
- "aria-valuetext": defaultActive && drag === null ? defaultLabel : steps[active]?.label,
396
+ "aria-valuetext": defaultActive && !dragging ? defaultLabel : steps[active]?.label,
359
397
  "aria-disabled": busy || void 0,
360
398
  "data-busy": busy ? "" : void 0,
361
- "data-dragging": drag === null ? void 0 : "",
362
- "data-default": defaultActive && drag === null ? "" : void 0,
399
+ "data-dragging": dragging ? "" : void 0,
400
+ "data-default": idle ? "" : void 0,
363
401
  onPointerDown,
364
402
  onPointerMove,
365
403
  onPointerUp: (event) => {
@@ -379,13 +417,13 @@ window.__ModuleLoader__.load({
379
417
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
380
418
  className: ModelOptions_module_css_default.sliderFill,
381
419
  style: {
382
- width: knobOffset(filled),
420
+ width: fillWidth(filled),
383
421
  background: fill
384
422
  }
385
423
  }),
386
424
  steps.map((step, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
387
425
  className: ModelOptions_module_css_default.sliderTick,
388
- "data-on": index <= active && !idle ? "" : void 0,
426
+ "data-on": index <= shown && !idle ? "" : void 0,
389
427
  style: { left: knobOffset(offsetOf(index, steps.length)) }
390
428
  }, step.key)),
391
429
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
package/lib/client.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":["useRef","useState","useSyncExternalStore","useRef","useState","css","useRef","useState","useSyncExternalStore","useMemo","createPortal","css","css","IconCheckOutlineRegular","getItem","useId","useRef","useState","useMemo","rankByName","isMobileDevice","Modal","css","Input","IconSearchOutlineRegular","Tag","ShortcutKeys","useSyncExternalStore","useState","useRef","useMemo","css","Toast","IconWarningOutlineRegular"],"sources":["../src/client/level-color.ts","../src/client/selection.ts","../src/client/EffortTint.tsx","../src/client/slider.ts","../src/client/EffortSlider.tsx","../src/client/ModelMenuBridge.tsx","../src/client/icons.tsx","../src/client/ModelRow.tsx","../src/client/favorites.ts","../src/client/model-facts.ts","../src/client/rows.ts","../src/client/ModelPicker.tsx","../src/client/effort-memory.ts","../src/client/ModelPickerHost.tsx","../src/client/picker-state.ts","../src/client/facts-source.ts","../src/client/locales.ts","../src/client/index.ts"],"sourcesContent":["/**\n * The accent one reasoning level carries.\n *\n * \"Off\" carries no accent at all — the fill and the caption stay neutral, so\n * switching reasoning off reads as switching it off rather than as the first\n * shade of a ramp. Every level above it walks a blue-to-violet ramp, and the\n * highest level is the only one whose fill is a gradient.\n */\n\n/** Off: the dimmed label tone, the same neutral the Host uses for quiet text. */\nconst NEUTRAL = 'var(--dsw-alias-label-dimmed)';\n\n/** Lowest lit level: the blue the composer already uses for accents. */\nconst LOW = [77, 147, 248] as const;\n\n/** Highest level: violet. */\nconst HIGH = [147, 51, 234] as const;\n\n/**\n * @param t - a point on the ramp, 0 at the lowest lit level and 1 at the top.\n * @returns the ramp color there.\n */\nfunction ramp(t: number): string {\n const mix = (from: number, to: number): number => Math.round(from + (to - from) * t);\n return `rgb(${mix(LOW[0], HIGH[0])}, ${mix(LOW[1], HIGH[1])}, ${mix(LOW[2], HIGH[2])})`;\n}\n\n/**\n * @param index - the level's detent, 0 being \"off\".\n * @param count - number of detents.\n * @returns the level's color as a CSS color string.\n */\nexport function levelColor(index: number, count: number): string {\n if (index <= 0) return NEUTRAL;\n // The top level is always the violet end, even when it is the only lit one.\n if (isTopLevel(index, count)) return ramp(1);\n const span = Math.max(1, count - 2);\n return ramp(Math.min(Math.max((index - 1) / span, 0), 1));\n}\n\n/**\n * @param index - the level's detent.\n * @param count - number of detents.\n * @returns whether this level is the only one painted with a gradient.\n */\nexport function isTopLevel(index: number, count: number): boolean {\n return count > 1 && index === count - 1;\n}\n\n/** The top level's fill: the whole ramp, ending on the color its text takes. */\nexport const LEVEL_GRADIENT = `linear-gradient(90deg, ${ramp(0)}, ${ramp(1)})`;\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'\nimport type { ModelDirectoryState } from '@deepseek-ai/dsh-client-ui-model-selection/client'\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 slider's detents for one model.\n *\n * The provider default is deliberately absent: it is a state, not a level, and\n * giving it a detent would read as the weakest reasoning instead of \"let the\n * provider decide\". The label above the track switches to it.\n * @param reasoning - the catalog's reasoning metadata, undefined when the model has none.\n * @returns one detent per advertised level, in catalog order.\n */\nexport function effortChoices(reasoning: ModelReasoning | undefined): readonly EffortChoice[] {\n if (reasoning === undefined) return []\n return reasoning.efforts.map((level) => ({\n key: level.id,\n effort: level.id as string | undefined,\n label: level.name,\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 * The reasoning metadata the catalog advertises for the model in force.\n * @param state - the session's directory snapshot.\n * @returns the metadata, or undefined when no model is live or it advertises none.\n */\nexport function liveReasoning(state: ModelDirectoryState): ModelReasoning | undefined {\n const current = state.current\n if (current === null) return undefined\n return state.groups\n .find((group) => group.id === current.provider)\n ?.models.find((model) => model.id === current.model)\n ?.reasoning\n}\n\n/**\n * The selection in force, or null while the Host has not resolved one.\n * @param state - the session's directory snapshot.\n * @returns the live selection, with the effort only when one is set.\n */\nexport function 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 : {\n provider: state.current.provider,\n model: state.current.model,\n reasoningEffort: state.current.reasoningEffort,\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 * Tint the composer's effort caption with the level's color.\n *\n * The slider's fill and the menu's value already carry the level's color; the\n * caption on the collapsed trigger is the third place the level shows, and it\n * lives outside the menu. This entry stays mounted for the whole session and\n * writes that one color, so all three read as one signal.\n *\n * The caption is recognized as the trigger's `span[class*='triggerEffort']`\n * child — the same class-substring contract the menu row uses. When the model\n * declares no levels the span does not exist and nothing is written.\n */\nimport { useLayoutEffect, useRef, useState, useSyncExternalStore } from 'react';\nimport type { ModelSelectInjected } from '@deepseek-ai/dsh-client-ui-model-selection/client';\nimport { levelColor } from './level-color.ts';\nimport { effortChoices, liveReasoning } from './selection.ts';\n\n/** Props of the tint: the shared catalog face. */\nexport type EffortTintProps = Pick<ModelSelectInjected, 'available' | 'directory'>;\n\n/**\n * Render nothing and keep the trigger caption colored.\n * @param props - the session's catalog face.\n * @returns an inert marker.\n */\nexport function EffortTint({ available, directory }: EffortTintProps) {\n const markerRef = useRef<HTMLSpanElement | null>(null);\n const [caption, setCaption] = useState<HTMLElement | null>(null);\n const state = useSyncExternalStore(\n (listener) => directory.subscribe(listener),\n () => directory.getSnapshot(),\n );\n const levels = effortChoices(liveReasoning(state));\n const effort = state.current?.reasoningEffort;\n const chosen = effort === undefined ? -1 : levels.findIndex((level) => level.effort === effort);\n // The provider default has no color of its own: the caption keeps the Host's.\n const color = available && chosen >= 0 && levels.length >= 2\n ? levelColor(chosen, levels.length)\n : null;\n\n // The Host rebuilds the trigger on every model change, so the caption is\n // re-read after each render; the identity guard keeps that from looping.\n useLayoutEffect(() => {\n const card = markerRef.current?.closest('[data-composer-card]');\n const next = card?.querySelector<HTMLElement>(\n '[data-slot=\"conversation.input.model\"] button[aria-haspopup=\"menu\"] span[class*=\"triggerEffort\"]',\n ) ?? null;\n if (next !== caption) setCaption(next);\n });\n\n useLayoutEffect(() => {\n if (caption === null || color === null) return;\n const saved = caption.style.color;\n caption.style.color = color;\n return () => {\n caption.style.color = saved;\n };\n }, [caption, color]);\n\n return <span ref={markerRef} hidden />;\n}\n","/**\n * Geometry of the discrete effort slider.\n *\n * The track carries one detent per level and the knob's centre travels between\n * two ends inset by the knob radius, so the knob never overhangs the track. A\n * pointer position becomes the detent nearest it; a detent becomes a fraction\n * of the track that CSS turns into the knob's centre and the filled width.\n */\n\n/**\n * Knob diameter in CSS pixels, and the track's height: the slider stands as tall\n * as the row's text line. The centre stops half of it from each end of the\n * track, so the knob never overhangs.\n */\nexport const KNOB_SIZE = 20;\n\n/**\n * @param index - detent index.\n * @param count - number of detents.\n * @returns the knob centre as a fraction of the track, 0 at the first detent and 1 at the last.\n */\nexport function offsetOf(index: number, count: number): number {\n if (count <= 1) return 0;\n const clamped = Math.min(Math.max(index, 0), count - 1);\n return clamped / (count - 1);\n}\n\n/**\n * @param clientX - pointer position in viewport coordinates.\n * @param left - the track's left edge in viewport coordinates.\n * @param width - the track's width in pixels.\n * @param count - number of detents.\n * @returns the detent nearest the pointer, clamped to the two ends.\n */\nexport function stepAt(clientX: number, left: number, width: number, count: number): number {\n if (count <= 1) return 0;\n const usable = width - KNOB_SIZE;\n if (usable <= 0) return 0;\n const fraction = (clientX - left - KNOB_SIZE / 2) / usable;\n const clamped = Math.min(Math.max(fraction, 0), 1);\n return Math.round(clamped * (count - 1));\n}\n\n/**\n * @param fraction - a knob position between 0 and 1.\n * @returns the CSS length that puts the knob centre there, inset at both ends.\n */\nexport function knobOffset(fraction: number): string {\n const clamped = Math.min(Math.max(fraction, 0), 1);\n return `calc(${KNOB_SIZE / 2}px + ${clamped} * (100% - ${KNOB_SIZE}px))`;\n}\n","/**\n * The reasoning-level slider that replaces the Host's effort drill.\n *\n * The Host's second menu cell is a button that drills into a list of levels.\n * This slider sits inside that same cell, between the Host's label and value, so\n * the levels are reachable without leaving the menu. Levels are discrete: the\n * knob snaps to one detent per level, the drag only moves the knob, and the\n * release commits once. Keyboard arrows step and commit immediately.\n *\n * The fill takes the level's color, so the track and the two captions read as\n * one signal: \"off\" is neutral, the levels above it walk a blue-to-violet ramp,\n * and only the top level fills with the whole gradient.\n *\n * \"Provider default\" is not a detent: it is a state, drawn as an empty track\n * with a hollow knob, so it never reads as the lowest level.\n */\nimport { useRef, useState, type KeyboardEvent, type PointerEvent } from 'react';\nimport type { EffortChoice } from './selection.ts';\nimport { LEVEL_GRADIENT, isTopLevel, levelColor } from './level-color.ts';\nimport { knobOffset, offsetOf, stepAt } from './slider.ts';\nimport css from './ModelOptions.module.css';\n\n/** Props of the slider: the levels, the one in force, and the commit path. */\nexport interface EffortSliderProps {\n /** The levels in catalog order, lowest first. */\n readonly steps: readonly EffortChoice[];\n /** Index of the level in force, already clamped into `steps`. */\n readonly current: number;\n /** Whether the provider default is in force instead of a level. */\n readonly defaultActive: boolean;\n /** Where the knob rests while the provider default is in force. */\n readonly defaultIndex: number;\n /** Localized name of the provider default, read while it is in force. */\n readonly defaultLabel: string;\n /** Whether a selection is in flight; input is ignored while it is. */\n readonly busy: boolean;\n /** Accessible name, taken from the Host's own row label. */\n readonly label: string;\n /** @param index - the level the user settled on. */\n readonly onCommit: (index: number) => void;\n}\n\n/**\n * Render the slider.\n * @param props - levels, the level in force, and the commit path.\n * @returns one focusable slider over the discrete levels.\n */\nexport function EffortSlider({\n steps, current, defaultActive, defaultIndex, defaultLabel, busy, label, onCommit,\n}: EffortSliderProps) {\n const trackRef = useRef<HTMLDivElement | null>(null);\n // The drag index lives in a ref as well as in state: a press and a release can\n // arrive in one task, and the release must not read a render that never ran.\n const dragRef = useRef<number | null>(null);\n const [drag, setDrag] = useState<number | null>(null);\n const resting = defaultActive ? defaultIndex : current;\n const active = drag ?? resting;\n const beginDrag = (index: number | null): void => {\n dragRef.current = index;\n setDrag(index);\n };\n\n const indexAt = (clientX: number): number => {\n const track = trackRef.current;\n if (track === null) return active;\n const rect = track.getBoundingClientRect();\n return stepAt(clientX, rect.left, rect.width, steps.length);\n };\n\n const onPointerDown = (event: PointerEvent<HTMLDivElement>): void => {\n if (busy) return;\n // The Host's menu cell owns this subtree; without these the press would\n // also settle the cell and pull focus out of the slider.\n event.preventDefault();\n event.stopPropagation();\n beginDrag(indexAt(event.clientX));\n try {\n event.currentTarget.setPointerCapture(event.pointerId);\n } catch {\n // A synthetic pointer has no capture to take; the drag still tracks moves.\n }\n event.currentTarget.focus();\n };\n\n const onPointerMove = (event: PointerEvent<HTMLDivElement>): void => {\n if (dragRef.current === null || busy) return;\n const next = indexAt(event.clientX);\n if (next !== dragRef.current) beginDrag(next);\n };\n\n const settle = (event: PointerEvent<HTMLDivElement>, commit: boolean): void => {\n if (dragRef.current === null) return;\n const next = indexAt(event.clientX);\n beginDrag(null);\n if (!commit) return;\n // Leaving the provider default is a change even when the knob lands where\n // the default already pointed.\n if (next !== current || defaultActive) onCommit(next);\n };\n\n const onKeyDown = (event: KeyboardEvent<HTMLDivElement>): void => {\n if (busy) return;\n const last = steps.length - 1;\n const next = event.key === 'ArrowRight' || event.key === 'ArrowUp'\n ? Math.min(active + 1, last)\n : event.key === 'ArrowLeft' || event.key === 'ArrowDown'\n ? Math.max(active - 1, 0)\n : event.key === 'Home'\n ? 0\n : event.key === 'End'\n ? last\n : null;\n if (next === null) return;\n // The menu walks rows with the arrow keys; a slider keeps them for itself.\n event.preventDefault();\n event.stopPropagation();\n if (next !== current || defaultActive) onCommit(next);\n };\n\n const knobFraction = offsetOf(active, steps.length);\n const offset = knobOffset(knobFraction);\n // The provider default fills nothing: the knob shows where it lands, the\n // track stays empty so it never reads as a chosen level.\n const idle = defaultActive && drag === null;\n const filled = idle ? 0 : knobFraction;\n const fill = isTopLevel(active, steps.length) ? LEVEL_GRADIENT : levelColor(active, steps.length);\n return <div\n ref={trackRef}\n className={css.slider}\n role=\"slider\"\n tabIndex={0}\n aria-label={label}\n aria-valuemin={0}\n aria-valuemax={Math.max(0, steps.length - 1)}\n aria-valuenow={active}\n aria-valuetext={defaultActive && drag === null ? defaultLabel : steps[active]?.label}\n aria-disabled={busy || undefined}\n data-busy={busy ? '' : undefined}\n data-dragging={drag === null ? undefined : ''}\n data-default={defaultActive && drag === null ? '' : undefined}\n onPointerDown={onPointerDown}\n onPointerMove={onPointerMove}\n onPointerUp={(event) => { settle(event, true); }}\n onPointerCancel={(event) => { settle(event, false); }}\n onKeyDown={onKeyDown}\n onClick={(event) => { event.stopPropagation(); }}\n >\n <div className={css.sliderTrack} aria-hidden=\"true\">\n <div className={css.sliderFill} style={{ width: knobOffset(filled), background: fill }} />\n {steps.map((step, index) => <span\n key={step.key}\n className={css.sliderTick}\n data-on={index <= active && !idle ? '' : undefined}\n style={{ left: knobOffset(offsetOf(index, steps.length)) }}\n />)}\n <div className={css.sliderKnob} style={{ left: offset }} />\n </div>\n </div>;\n}\n","/**\n * Bridge the Host's own model menu: the model cell opens the all-provider\n * picker, and the effort cell carries the reasoning-level slider.\n *\n * The Host draws the root menu's first two cells itself: the model cell drills\n * into the current provider's list, the effort cell into the levels. This plugin\n * wants the first to open its dialog and the second to stop drilling, and the\n * Host exposes no hook for either. This entry is mounted exactly while the root\n * pane is open, so it can take the click on the menu surface before the cells'\n * own handlers run and mount the slider into the effort cell.\n *\n * The cells are recognized as the menu surface's direct `role=\"menuitem\"`\n * children, which is the Host's root-menu shape; the options slot output sits in\n * its own wrapper, so other plugins' rows never join that list. When the second\n * cell is missing the model declares no reasoning levels and the row is left\n * alone; when the first is missing the click falls through to the Host.\n *\n * Clicking the cell's label switches to the provider default, which is a state\n * rather than a detent: the slider shows it as an empty track under a hollow\n * knob, so it never reads as the weakest level.\n */\nimport { useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';\nimport { createPortal } from 'react-dom';\nimport type { ModelSelectInjected } from '@deepseek-ai/dsh-client-ui-model-selection/client';\nimport type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';\nimport { EffortSlider } from './EffortSlider.tsx';\nimport type { PickerStore } from './picker-state.ts';\nimport { effortChoices, liveReasoning, planEffortCommit, selectionOf, type Selection } from './selection.ts';\nimport css from './ModelOptions.module.css';\n\n/** Props of the bridge: menu state, the owning session, and the seat's injected face. */\nexport type ModelMenuBridgeProps = PropsRuntime<'conversation.input.model.options'>\n & PropsLocale<'modelSubmenu'>\n & Pick<ModelSelectInjected, 'available' | 'directory' | 'select'>\n & {\n /** Session whose catalog and picker this menu drives. */\n readonly sessionId: string;\n /** Carries the open request to the overlay view. */\n readonly picker: PickerStore;\n }\n\n/**\n * Render the marker, redirect the model cell, and mount the effort slider.\n * @param props - menu state, session, catalog face, and localized copy.\n * @returns an inert marker plus the slider while the effort cell is present.\n */\nexport function ModelMenuBridge({\n sessionId, picker, dismiss, reportFailure, available, directory, select, t,\n}: ModelMenuBridgeProps) {\n const markerRef = useRef<HTMLSpanElement | null>(null);\n const sliderRef = useRef<HTMLDivElement | null>(null);\n const [surface, setSurface] = useState<HTMLElement | null>(null);\n const [effortRow, setEffortRow] = useState<HTMLElement | null>(null);\n const [rowLabel, setRowLabel] = useState<string | null>(null);\n\n const state = useSyncExternalStore(\n (listener) => directory.subscribe(listener),\n () => directory.getSnapshot(),\n );\n const current = state.current;\n const reasoning = liveReasoning(state);\n const levels = useMemo(() => effortChoices(reasoning), [reasoning]);\n const declared = levels.findIndex((level) => level.effort === reasoning?.defaultEffort);\n const chosen = current?.reasoningEffort === undefined\n ? -1\n : levels.findIndex((level) => level.effort === current.reasoningEffort);\n // The provider default is a state, not a detent: it is in force whenever the\n // session carries no explicit level.\n const defaultActive = current !== null && current.reasoningEffort === undefined;\n const currentIndex = chosen >= 0 ? chosen : declared >= 0 ? declared : 0;\n const defaultIndex = declared >= 0 ? declared : 0;\n const canDefault = !defaultActive;\n const busy = state.pending !== null;\n // One detent is not a choice: leave the Host's row exactly as it is.\n const showSlider = available && levels.length >= 2 && effortRow !== null;\n\n // The Host rebuilds its cells on every render, so the surface and the effort\n // cell are re-read after each one; the identity guards keep that from looping.\n useLayoutEffect(() => {\n const foundSurface = markerRef.current?.closest('[data-menu-material]');\n const nextSurface = foundSurface instanceof HTMLElement ? foundSurface : null;\n if (nextSurface !== surface) setSurface(nextSurface);\n const cells = nextSurface === null\n ? []\n : [...nextSurface.querySelectorAll<HTMLElement>(':scope > button[role=\"menuitem\"]')];\n const nextRow = cells.length >= 2 ? cells[1] ?? null : null;\n if (nextRow !== effortRow) setEffortRow(nextRow);\n const nextLabel = nextRow?.querySelector(\"span[class*='cellLabel']\")?.textContent?.trim() ?? '';\n if (nextLabel !== (rowLabel ?? '')) setRowLabel(nextLabel);\n });\n\n const submit = (selection: Selection | null): void => {\n if (selection === null) return;\n void select(selection).then((result) => {\n if (result === undefined || result.ok) return;\n reportFailure(result.error.code === 'session/writer-held'\n ? t('error.sessionInUse')\n : t('error.action', { message: `${result.error.code}: ${result.error.message}` }));\n });\n };\n const commit = (index: number): void => {\n const live = selectionOf(state);\n const level = levels[index];\n if (live === null || level === undefined) return;\n submit(planEffortCommit(live, level.effort));\n };\n const commitDefault = (): void => {\n const live = selectionOf(state);\n if (live === null) return;\n submit(planEffortCommit(live, undefined));\n };\n\n // The Host rebuilds `dismiss` on every render, so the listener reads the\n // current one through a ref instead of being torn down and reinstalled.\n const latest = useRef({ sessionId, picker, dismiss, showSlider, canDefault, commitDefault });\n latest.current = { sessionId, picker, dismiss, showSlider, canDefault, commitDefault };\n\n useEffect(() => {\n if (surface === null) return;\n const redirect = (event: MouseEvent): void => {\n const target = event.target;\n if (!(target instanceof Node)) return;\n const cells = [...surface.querySelectorAll<HTMLElement>(':scope > button[role=\"menuitem\"]')];\n const modelCell = cells[0];\n if (modelCell !== undefined && modelCell.contains(target)) {\n // Capture on the surface runs before the React root's bubble listener, so\n // stopping here keeps the cell's own drill handler from ever running.\n event.preventDefault();\n event.stopPropagation();\n const request = latest.current;\n request.dismiss();\n request.picker.open(request.sessionId);\n return;\n }\n const row = cells[1];\n if (row === undefined || !row.contains(target) || !latest.current.showSlider) return;\n // The slider owns its own presses; only the label and the value are inert.\n if (sliderRef.current?.contains(target) === true) return;\n event.preventDefault();\n event.stopPropagation();\n const label = row.querySelector(\"span[class*='cellLabel']\");\n if (latest.current.canDefault && label !== null && label.contains(target)) {\n latest.current.commitDefault();\n return;\n }\n sliderRef.current?.querySelector<HTMLElement>('[role=\"slider\"]')?.focus();\n };\n surface.addEventListener('click', redirect, true);\n return () => {\n surface.removeEventListener('click', redirect, true);\n };\n }, [surface]);\n\n // The slider takes the whole slack beside the label: the value and the\n // chevron go, because the level is already written under the composer and the\n // row no longer drills anywhere. Every write is recorded and put back.\n useLayoutEffect(() => {\n if (effortRow === null || !showSlider) return;\n const label = effortRow.querySelector<HTMLElement>(\"span[class*='cellLabel']\");\n const value = effortRow.querySelector<HTMLElement>(\"span[class*='cellValue']\");\n const chevron = effortRow.querySelector<SVGElement>(\"svg[class*='cellChevron']\");\n const saved = {\n value: value?.style.display ?? '',\n chevron: chevron?.style.display ?? '',\n cursor: label?.style.cursor ?? '',\n title: label?.getAttribute('title') ?? null,\n };\n if (value !== null) value.style.display = 'none';\n if (chevron !== null) chevron.style.display = 'none';\n if (label !== null && canDefault) {\n label.style.cursor = 'pointer';\n label.title = t('effort.useDefault');\n }\n return () => {\n if (value !== null) value.style.display = saved.value;\n if (chevron !== null) chevron.style.display = saved.chevron;\n if (label !== null) {\n label.style.cursor = saved.cursor;\n if (saved.title === null) label.removeAttribute('title');\n else label.title = saved.title;\n }\n };\n }, [effortRow, showSlider, canDefault, t]);\n\n return <>\n <span ref={markerRef} hidden />\n {showSlider && effortRow !== null && createPortal(\n <div ref={sliderRef} className={css.sliderCell}>\n <EffortSlider\n steps={levels}\n current={currentIndex}\n defaultActive={defaultActive}\n defaultIndex={defaultIndex}\n defaultLabel={t('effort.providerDefault')}\n busy={busy}\n label={rowLabel !== null && rowLabel !== '' ? rowLabel : t('effort.label')}\n onCommit={commit}\n />\n </div>,\n effortRow,\n )}\n </>;\n}\n","/**\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 in the picker.\n *\n * The star is its own control, because favouriting there must not switch the\n * model: the row button picks, the star toggles, and the check marks the model\n * in use.\n */\n\nimport type { ReactNode } from 'react';\nimport { IconCheckOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives';\nimport { StarIcon } from './icons.tsx';\nimport css from './ModelOptions.module.css';\n\n/** Props of one row. */\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 /** Whether the model is favorited right now. */\n readonly favorited: boolean;\n /** The metadata line's text. */\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. */\n readonly onToggleFavorite: () => void;\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, favorited, meta, starLabel,\n starTabStop = false, rowTabIndex, onPick, onFocusRow, onToggleFavorite, children,\n}: ModelRowProps) {\n return <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 <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 </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 * 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 * 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 './ModelOptions.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 /**\n * The reasoning level this browser last used for one model, if any.\n * @param provider - the provider route key.\n * @param model - the model id inside that provider.\n * @returns the remembered level, or undefined when none is remembered.\n */\n readonly rememberedOf?: (provider: string, model: string) => string | undefined\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, rememberedOf, 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, rememberedOf?.(row.provider, row.model))\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 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","/**\n * The composer overlay's picker view: the dialog, its catalog, and its verbs.\n *\n * It lives in `conversation.input.overlay` rather than beside the row, because\n * the Host unmounts the model menu as soon as a row is clicked. Everything the\n * dialog reads arrives through this entry's injected face.\n */\nimport { useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';\nimport { IconWarningOutlineRegular, Toast } from '@deepseek-ai/dsh-client-ui-primitives';\nimport type { 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 type { FactsSource } from './facts-source.ts';\nimport {\n readFavorites,\n toggleFavorite,\n type FavoriteModel,\n type FavoritesStorage,\n} from './favorites.ts';\nimport {\n effortFor,\n readEfforts,\n rememberEffort,\n type RememberedEffort,\n} from './effort-memory.ts';\nimport { selectionOf, type Selection } from './selection.ts';\nimport type { PickerStore } from './picker-state.ts';\nimport css from './ModelOptions.module.css';\n\n/** Props of the overlay view: the session, the store, and the seat's injected face. */\nexport type ModelPickerHostProps = PropsLocale<'modelSubmenu'>\n & ModelSelectInjected\n & {\n /** Session whose catalog this view renders. */\n readonly sessionId: string;\n /** The request written by the menu row. */\n readonly picker: PickerStore;\n /** The settings-derived metadata the picker's line reads. */\n readonly factsSource: FactsSource;\n }\n\n/**\n * Render the picker for this session.\n * @param props - session, store, catalog face, and localized copy.\n * @returns the dialog while the store names this session; otherwise nothing.\n */\nexport function ModelPickerHost({\n sessionId, picker, available, directory, load, select, resolveText, factsSource, t,\n}: ModelPickerHostProps) {\n const request = useSyncExternalStore(picker.subscribe, picker.getSnapshot);\n const state = useSyncExternalStore((listener) => directory.subscribe(listener), () => directory.getSnapshot());\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\n const busy = state.status === 'selecting';\n const live = useMemo(() => selectionOf(state), [state]);\n const facts = useSyncExternalStore((listener) => factsSource.subscribe(listener), () => factsSource.getSnapshot());\n const open = request.sessionId === sessionId;\n // The injected face is cached per entry and session, but the read goes through\n // a ref anyway: a catalog load writes the store, so an unstable `load` in the\n // dependency list would reload on every render it caused.\n const loadRef = useRef(load);\n loadRef.current = load;\n\n // Opening reads the catalog again: another client may have changed it, and a\n // failed load has to be retryable from the same gesture.\n useEffect(() => {\n if (open) loadRef.current();\n }, [open]);\n\n // The Host's own menu changes the reasoning level without passing through this\n // plugin, so the memory is written from the directory snapshot too — a picker\n // click alone would only ever record levels chosen in the dialog. The key guard\n // keeps one write per distinct selection instead of one per snapshot.\n const current = state.current;\n const rememberedRef = useRef<string | null>(null);\n useEffect(() => {\n if (current === null) return;\n const key = `${current.provider}/${current.model}/${current.reasoningEffort ?? ''}`;\n if (rememberedRef.current === key) return;\n rememberedRef.current = key;\n setMemory(rememberEffort(storageOf(), current.provider, current.model, current.reasoningEffort));\n }, [current]);\n\n if (!available) return null;\n\n const raiseToast = (text: string): void => {\n toastSeq.current += 1;\n setToast({ seq: toastSeq.current, text });\n };\n const submit = (selection: Selection): void => {\n void select(selection).then((result) => {\n if (result === undefined || result.ok) {\n setMemory(rememberEffort(storageOf(), selection.provider, selection.model, selection.reasoningEffort));\n picker.close();\n return;\n }\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\n return <div ref={rootRef} className={css.pickerHost}>\n <ModelPicker\n open={open}\n resolveText={resolveText}\n state={state}\n live={live}\n facts={facts}\n favorites={favorites}\n busy={busy}\n t={t}\n rememberedOf={(provider, model) => effortFor(memory, provider, model)}\n onPick={submit}\n onToggleFavorite={(provider, model) => {\n setFavorites(toggleFavorite(storageOf(), provider, model));\n }}\n onReload={load}\n onClose={() => {\n picker.close();\n }}\n />\n {toast !== null && <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 </div>;\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 * Which session's picker is open.\n *\n * The row that asks for the picker lives inside the composer model menu, and the\n * Host unmounts that menu the moment a row is clicked. The dialog lives in the\n * composer's overlay slot, which stays mounted. One plugin-wide store carries the\n * request across that unmount — the same split the Host's own command popups use\n * between their controller and their overlay view.\n */\n\n/** What the store publishes. */\nexport interface PickerSnapshot {\n /** Session whose picker is open, or null when none is. */\n readonly sessionId: string | null\n}\n\n/** Open and close verbs plus the observable the overlay view subscribes to. */\nexport interface PickerStore {\n /** @returns the current request. */\n readonly getSnapshot: () => PickerSnapshot\n /** @param listener - called after every change. @returns the unsubscribe. */\n readonly subscribe: (listener: () => void) => () => void\n /** @param sessionId - open the picker for one session, replacing any other. */\n readonly open: (sessionId: string) => void\n /** Close whatever is open. */\n readonly close: () => void\n}\n\n/**\n * @returns a store that starts closed.\n */\nexport function createPickerStore(): PickerStore {\n let snapshot: PickerSnapshot = { sessionId: null };\n const listeners = new Set<() => void>();\n const publish = (next: PickerSnapshot): void => {\n if (next.sessionId === snapshot.sessionId) return;\n snapshot = next;\n for (const listener of listeners) listener();\n };\n return {\n getSnapshot: () => snapshot,\n subscribe: (listener) => {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n open: (sessionId) => {\n publish({ sessionId });\n },\n close: () => {\n publish({ sessionId: null });\n },\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 all-provider model picker. */\n\n/** Dictionary namespace owned by this plugin. */\nexport const NS = 'modelSubmenu'\n\n/** Locale keys the picker renders. */\nexport type ModelSubmenuLocaleKey =\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 | 'effort.label'\n | 'effort.providerDefault'\n | 'effort.useDefault'\n\ndeclare module '@deepseek-ai/dsh-client-ui-slots' {\n interface LocaleNamespaceMap {\n /** The all-provider model picker'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 '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 'effort.label': '推理等级',\n 'effort.providerDefault': 'Default',\n 'effort.useDefault': '切换为默认档位',\n}\n\n/** English dictionary, checked complete against the zh key set. */\nexport const en: Record<ModelSubmenuLocaleKey, string> = {\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 'effort.label': 'Reasoning effort',\n 'effort.providerDefault': 'Default',\n 'effort.useDefault': 'Switch to the provider default',\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 model menu's options slot and the composer overlay 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 { EffortTint } from './EffortTint.tsx'\nimport { ModelMenuBridge } from './ModelMenuBridge.tsx'\nimport { ModelPickerHost } from './ModelPickerHost.tsx'\nimport { createPickerStore } from './picker-state.ts'\nimport { createFactsSource, type DescribeFace } from './facts-source.ts'\nimport { NS, en, zh } from './locales.ts'\n\nexport { EffortTint } from './EffortTint.tsx'\nexport { ModelMenuBridge } from './ModelMenuBridge.tsx'\nexport { ModelPickerHost } from './ModelPickerHost.tsx'\nexport { createPickerStore } from './picker-state.ts'\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 both entries render\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 contribute the\n * bridge that turns the Host's model cell into the picker's door and its effort\n * cell into the reasoning-level slider, the overlay that renders the picker, and\n * the tint that keeps the collapsed trigger's effort caption on the same color.\n *\n * The two are separate entries because the Host unmounts the model menu the\n * moment a row is clicked; the overlay slot stays mounted, so the dialog\n * survives the menu's teardown.\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 picker = createPickerStore()\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 /**\n * Build one entry's business face for a session.\n * @param sessionId - the slot's session scope.\n * @returns the catalog face plus the session and store both entries need.\n */\n const faceOf = (sessionId: string) => {\n // The slot'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 sessionId,\n picker,\n available,\n directory: directory.store,\n factsSource: facts.source,\n load: () => {\n if (available) directory.load().catch(() => {})\n },\n resolveText: (text: Parameters<typeof ctx.locale.resolveText>[0]) => ctx.locale.resolveText(text),\n select: (selection: Parameters<typeof directory.select>[0]) => available\n ? directory.select(selection)\n : Promise.resolve(undefined),\n }\n }\n scope.slots.inject('conversation.input.model.options', () => scope.slots.register({\n name: 'conversation.input.model.options',\n id: 'model-menu-bridge',\n registrant: '@lynn123411/dsh-model-submenu',\n locale: NS,\n inject: faceOf,\n }, ModelMenuBridge))\n scope.slots.inject('conversation.input.overlay', () => scope.slots.register({\n name: 'conversation.input.overlay',\n id: 'model-picker',\n registrant: '@lynn123411/dsh-model-submenu',\n locale: NS,\n inject: faceOf,\n }, ModelPickerHost))\n // The trigger's effort caption lives outside the menu, so its tint needs an\n // entry that stays mounted for the whole session.\n scope.slots.inject('conversation.input.overlay', () => scope.slots.register({\n name: 'conversation.input.overlay',\n id: 'model-effort-tint',\n registrant: '@lynn123411/dsh-model-submenu',\n inject: faceOf,\n }, EffortTint))\n })\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.js","names":["useRef","useState","useSyncExternalStore","useRef","useState","css","useRef","useState","useSyncExternalStore","useMemo","createPortal","css","css","IconCheckOutlineRegular","getItem","useId","useRef","useState","useMemo","rankByName","isMobileDevice","Modal","css","Input","IconSearchOutlineRegular","Tag","ShortcutKeys","useSyncExternalStore","useState","useRef","useMemo","css","Toast","IconWarningOutlineRegular"],"sources":["../src/client/level-color.ts","../src/client/selection.ts","../src/client/EffortTint.tsx","../src/client/slider.ts","../src/client/EffortSlider.tsx","../src/client/ModelMenuBridge.tsx","../src/client/icons.tsx","../src/client/ModelRow.tsx","../src/client/favorites.ts","../src/client/model-facts.ts","../src/client/rows.ts","../src/client/ModelPicker.tsx","../src/client/effort-memory.ts","../src/client/ModelPickerHost.tsx","../src/client/picker-state.ts","../src/client/facts-source.ts","../src/client/locales.ts","../src/client/index.ts"],"sourcesContent":["/**\n * The accent one reasoning level carries.\n *\n * \"Off\" carries no accent at all — the fill and the caption stay neutral, so\n * switching reasoning off reads as switching it off rather than as the first\n * shade of a ramp. Every level above it walks a blue-to-violet ramp, and the\n * highest level is the only one whose fill is a gradient.\n */\n\n/** Off: the dimmed label tone, the same neutral the Host uses for quiet text. */\nconst NEUTRAL = 'var(--dsw-alias-label-dimmed)';\n\n/** Lowest lit level: the blue the composer already uses for accents. */\nconst LOW = [77, 147, 248] as const;\n\n/** Highest level: violet. */\nconst HIGH = [147, 51, 234] as const;\n\n/**\n * @param t - a point on the ramp, 0 at the lowest lit level and 1 at the top.\n * @returns the ramp color there.\n */\nfunction ramp(t: number): string {\n const mix = (from: number, to: number): number => Math.round(from + (to - from) * t);\n return `rgb(${mix(LOW[0], HIGH[0])}, ${mix(LOW[1], HIGH[1])}, ${mix(LOW[2], HIGH[2])})`;\n}\n\n/**\n * @param index - the level's detent, 0 being \"off\".\n * @param count - number of detents.\n * @returns the level's color as a CSS color string.\n */\nexport function levelColor(index: number, count: number): string {\n if (index <= 0) return NEUTRAL;\n // The top level is always the violet end, even when it is the only lit one.\n if (isTopLevel(index, count)) return ramp(1);\n const span = Math.max(1, count - 2);\n return ramp(Math.min(Math.max((index - 1) / span, 0), 1));\n}\n\n/**\n * @param index - the level's detent.\n * @param count - number of detents.\n * @returns whether this level is the only one painted with a gradient.\n */\nexport function isTopLevel(index: number, count: number): boolean {\n return count > 1 && index === count - 1;\n}\n\n/** The top level's fill: the whole ramp, ending on the color its text takes. */\nexport const LEVEL_GRADIENT = `linear-gradient(90deg, ${ramp(0)}, ${ramp(1)})`;\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'\nimport type { ModelDirectoryState } from '@deepseek-ai/dsh-client-ui-model-selection/client'\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 slider's detents for one model.\n *\n * The provider default is deliberately absent: it is a state, not a level, and\n * giving it a detent would read as the weakest reasoning instead of \"let the\n * provider decide\". The label above the track switches to it.\n * @param reasoning - the catalog's reasoning metadata, undefined when the model has none.\n * @returns one detent per advertised level, in catalog order.\n */\nexport function effortChoices(reasoning: ModelReasoning | undefined): readonly EffortChoice[] {\n if (reasoning === undefined) return []\n return reasoning.efforts.map((level) => ({\n key: level.id,\n effort: level.id as string | undefined,\n label: level.name,\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 * The reasoning metadata the catalog advertises for the model in force.\n * @param state - the session's directory snapshot.\n * @returns the metadata, or undefined when no model is live or it advertises none.\n */\nexport function liveReasoning(state: ModelDirectoryState): ModelReasoning | undefined {\n const current = state.current\n if (current === null) return undefined\n return state.groups\n .find((group) => group.id === current.provider)\n ?.models.find((model) => model.id === current.model)\n ?.reasoning\n}\n\n/**\n * The selection in force, or null while the Host has not resolved one.\n * @param state - the session's directory snapshot.\n * @returns the live selection, with the effort only when one is set.\n */\nexport function 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 : {\n provider: state.current.provider,\n model: state.current.model,\n reasoningEffort: state.current.reasoningEffort,\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 * Tint the composer's effort caption with the level's color.\n *\n * The slider's fill and the menu's value already carry the level's color; the\n * caption on the collapsed trigger is the third place the level shows, and it\n * lives outside the menu. This entry stays mounted for the whole session and\n * writes that one color, so all three read as one signal.\n *\n * The caption is recognized as the trigger's `span[class*='triggerEffort']`\n * child — the same class-substring contract the menu row uses. When the model\n * declares no levels the span does not exist and nothing is written.\n */\nimport { useLayoutEffect, useRef, useState, useSyncExternalStore } from 'react';\nimport type { ModelSelectInjected } from '@deepseek-ai/dsh-client-ui-model-selection/client';\nimport { levelColor } from './level-color.ts';\nimport { effortChoices, liveReasoning } from './selection.ts';\n\n/** Props of the tint: the shared catalog face. */\nexport type EffortTintProps = Pick<ModelSelectInjected, 'available' | 'directory'>;\n\n/**\n * Render nothing and keep the trigger caption colored.\n * @param props - the session's catalog face.\n * @returns an inert marker.\n */\nexport function EffortTint({ available, directory }: EffortTintProps) {\n const markerRef = useRef<HTMLSpanElement | null>(null);\n const [caption, setCaption] = useState<HTMLElement | null>(null);\n const state = useSyncExternalStore(\n (listener) => directory.subscribe(listener),\n () => directory.getSnapshot(),\n );\n const levels = effortChoices(liveReasoning(state));\n const effort = state.current?.reasoningEffort;\n const chosen = effort === undefined ? -1 : levels.findIndex((level) => level.effort === effort);\n // The provider default has no color of its own: the caption keeps the Host's.\n const color = available && chosen >= 0 && levels.length >= 2\n ? levelColor(chosen, levels.length)\n : null;\n\n // The Host rebuilds the trigger on every model change, so the caption is\n // re-read after each render; the identity guard keeps that from looping.\n useLayoutEffect(() => {\n const card = markerRef.current?.closest('[data-composer-card]');\n const next = card?.querySelector<HTMLElement>(\n '[data-slot=\"conversation.input.model\"] button[aria-haspopup=\"menu\"] span[class*=\"triggerEffort\"]',\n ) ?? null;\n if (next !== caption) setCaption(next);\n });\n\n useLayoutEffect(() => {\n if (caption === null || color === null) return;\n const saved = caption.style.color;\n caption.style.color = color;\n return () => {\n caption.style.color = saved;\n };\n }, [caption, color]);\n\n return <span ref={markerRef} hidden />;\n}\n","/**\n * Geometry of the discrete effort slider.\n *\n * The track is a capsule and the knob a slightly larger circle riding on it, so\n * the knob overhangs the track instead of merging with its rounded end. The\n * knob's centre travels between two points inset by the knob's radius, so the\n * knob never runs past either end. A pointer position becomes a position along\n * that travel; a position becomes the detent nearest it, or the CSS lengths that\n * put the knob and the fill there.\n */\n\n/**\n * Track height in CSS pixels; the stylesheet keeps the same value. Half of it is\n * the radius of the track's ends and of the fill's cap.\n */\nexport const TRACK_HEIGHT = 16;\n\n/**\n * Knob diameter in CSS pixels. It is larger than the track, so the knob reads as\n * a knob riding on the track rather than as the track's own end cap.\n */\nexport const KNOB_SIZE = 20;\n\n/** The knob's centre keeps its own radius away from either end of the track. */\nconst KNOB_RADIUS = KNOB_SIZE / 2;\n\n/**\n * @param index - detent index.\n * @param count - number of detents.\n * @returns the knob centre as a fraction of the track, 0 at the first detent and 1 at the last.\n */\nexport function offsetOf(index: number, count: number): number {\n if (count <= 1) return 0;\n const clamped = Math.min(Math.max(index, 0), count - 1);\n return clamped / (count - 1);\n}\n\n/**\n * Where a pointer sits along the knob's travel. The drag reads this rather than a\n * detent, so the knob follows the pointer instead of jumping a level at a time.\n * @param clientX - pointer position in viewport coordinates.\n * @param left - the track's left edge in viewport coordinates.\n * @param width - the track's width in pixels.\n * @returns the position as a fraction of the travel, clamped to 0 and 1.\n */\nexport function fractionAt(clientX: number, left: number, width: number): number {\n const usable = width - KNOB_SIZE;\n if (usable <= 0) return 0;\n const fraction = (clientX - left - KNOB_RADIUS) / usable;\n return Math.min(Math.max(fraction, 0), 1);\n}\n\n/**\n * @param fraction - a knob position between 0 and 1.\n * @param count - number of detents.\n * @returns the detent nearest that position.\n */\nexport function detentAt(fraction: number, count: number): number {\n if (count <= 1) return 0;\n const clamped = Math.min(Math.max(fraction, 0), 1);\n return Math.round(clamped * (count - 1));\n}\n\n/**\n * @param clientX - pointer position in viewport coordinates.\n * @param left - the track's left edge in viewport coordinates.\n * @param width - the track's width in pixels.\n * @param count - number of detents.\n * @returns the detent nearest the pointer, clamped to the two ends.\n */\nexport function stepAt(clientX: number, left: number, width: number, count: number): number {\n if (count <= 1) return 0;\n return detentAt(fractionAt(clientX, left, width), count);\n}\n\n/**\n * @param fraction - a knob position between 0 and 1.\n * @returns the CSS length that puts the knob centre there, inset at both ends.\n */\nexport function knobOffset(fraction: number): string {\n const clamped = Math.min(Math.max(fraction, 0), 1);\n return `calc(${KNOB_RADIUS}px + ${clamped} * (100% - ${KNOB_SIZE}px))`;\n}\n\n/**\n * Width of the colored fill for a knob position.\n *\n * The fill is the track's own shape cut at the knob: its right cap carries the\n * track's radius and ends under the knob, so the color runs into the knob\n * instead of stopping at a straight edge, and the last detent reaches the track's\n * end rather than the knob's centre.\n * @param fraction - a knob position between 0 and 1.\n * @returns the CSS width of the fill.\n */\nexport function fillWidth(fraction: number): string {\n const clamped = Math.min(Math.max(fraction, 0), 1);\n return `calc(${TRACK_HEIGHT}px + ${clamped} * (100% - ${TRACK_HEIGHT}px))`;\n}\n","/**\n * The reasoning-level slider that replaces the Host's effort drill.\n *\n * The Host's second menu cell is a button that drills into a list of levels.\n * This slider sits inside that same cell, between the Host's label and value, so\n * the levels are reachable without leaving the menu. The levels are discrete but\n * the drag is not: the knob follows the pointer continuously, the level it\n * reports is the detent nearest that position, and letting go eases the knob onto\n * that detent and commits once. Keyboard arrows step and commit immediately.\n *\n * The fill takes the level's color, so the track and the two captions read as\n * one signal: \"off\" is neutral, the levels above it walk a blue-to-violet ramp,\n * and only the top level fills with the whole gradient.\n *\n * \"Provider default\" is not a detent: it is a state, drawn as an empty track\n * with a hollow knob, so it never reads as the lowest level.\n */\nimport { useEffect, useRef, useState, type KeyboardEvent, type PointerEvent } from 'react';\nimport type { EffortChoice } from './selection.ts';\nimport { LEVEL_GRADIENT, isTopLevel, levelColor } from './level-color.ts';\nimport { detentAt, fillWidth, fractionAt, knobOffset, offsetOf } from './slider.ts';\nimport css from './ModelOptions.module.css';\n\n/** Props of the slider: the levels, the one in force, and the commit path. */\nexport interface EffortSliderProps {\n /** The levels in catalog order, lowest first. */\n readonly steps: readonly EffortChoice[];\n /** Index of the level in force, already clamped into `steps`. */\n readonly current: number;\n /** Whether the provider default is in force instead of a level. */\n readonly defaultActive: boolean;\n /** Where the knob rests while the provider default is in force. */\n readonly defaultIndex: number;\n /** Localized name of the provider default, read while it is in force. */\n readonly defaultLabel: string;\n /** Whether a selection is in flight; input is ignored while it is. */\n readonly busy: boolean;\n /** Accessible name, taken from the Host's own row label. */\n readonly label: string;\n /** @param index - the level the user settled on. */\n readonly onCommit: (index: number) => void;\n}\n\n/**\n * Render the slider.\n * @param props - levels, the level in force, and the commit path.\n * @returns one focusable slider over the discrete levels.\n */\nexport function EffortSlider({\n steps, current, defaultActive, defaultIndex, defaultLabel, busy, label, onCommit,\n}: EffortSliderProps) {\n const trackRef = useRef<HTMLDivElement | null>(null);\n // The drag position lives in a ref as well as in state: a press and a release\n // can arrive in one task, and the release must not read a render that never ran.\n const dragRef = useRef<number | null>(null);\n // While dragging this is where the pointer is along the travel, not a level:\n // the knob has to move with the pointer, or the drag reads as a series of jumps.\n const [drag, setDrag] = useState<number | null>(null);\n // After a release that submits, the knob waits on the detent it settled on\n // instead of springing back to the old level while the write is in flight.\n const [hold, setHold] = useState<number | null>(null);\n const dragging = drag !== null;\n const resting = defaultActive ? defaultIndex : current;\n const active = dragging ? detentAt(drag, steps.length) : resting;\n const knobFraction = drag ?? hold ?? offsetOf(resting, steps.length);\n // The color and the ticks follow the knob, not the level in force: during a\n // drag, and while a commit is in flight, the knob has already gone ahead.\n const shown = dragging || hold !== null ? detentAt(knobFraction, steps.length) : resting;\n const beginDrag = (fraction: number | null): void => {\n dragRef.current = fraction;\n setDrag(fraction);\n };\n\n // The hold ends as soon as the write does, whether it landed on the new level\n // or was rejected: the displayed level is the truth again either way.\n useEffect(() => {\n if (!busy) setHold(null);\n }, [busy]);\n useEffect(() => {\n setHold(null);\n }, [current, defaultActive]);\n\n const fractionAtPointer = (clientX: number): number => {\n const track = trackRef.current;\n if (track === null) return dragRef.current ?? knobFraction;\n const rect = track.getBoundingClientRect();\n return fractionAt(clientX, rect.left, rect.width);\n };\n\n const onPointerDown = (event: PointerEvent<HTMLDivElement>): void => {\n if (busy) return;\n // The Host's menu cell owns this subtree; without these the press would\n // also settle the cell and pull focus out of the slider.\n event.preventDefault();\n event.stopPropagation();\n setHold(null);\n beginDrag(fractionAtPointer(event.clientX));\n try {\n event.currentTarget.setPointerCapture(event.pointerId);\n } catch {\n // A synthetic pointer has no capture to take; the drag still tracks moves.\n }\n event.currentTarget.focus();\n };\n\n const onPointerMove = (event: PointerEvent<HTMLDivElement>): void => {\n if (dragRef.current === null || busy) return;\n beginDrag(fractionAtPointer(event.clientX));\n };\n\n const settle = (event: PointerEvent<HTMLDivElement>, commit: boolean): void => {\n if (dragRef.current === null) return;\n const next = detentAt(fractionAtPointer(event.clientX), steps.length);\n beginDrag(null);\n if (!commit) return;\n // Leaving the provider default is a change even when the knob lands where\n // the default already pointed.\n if (next !== current || defaultActive) {\n setHold(offsetOf(next, steps.length));\n onCommit(next);\n }\n };\n\n const onKeyDown = (event: KeyboardEvent<HTMLDivElement>): void => {\n if (busy) return;\n const last = steps.length - 1;\n const next = event.key === 'ArrowRight' || event.key === 'ArrowUp'\n ? Math.min(active + 1, last)\n : event.key === 'ArrowLeft' || event.key === 'ArrowDown'\n ? Math.max(active - 1, 0)\n : event.key === 'Home'\n ? 0\n : event.key === 'End'\n ? last\n : null;\n if (next === null) return;\n // The menu walks rows with the arrow keys; a slider keeps them for itself.\n event.preventDefault();\n event.stopPropagation();\n if (next !== current || defaultActive) onCommit(next);\n };\n\n const offset = knobOffset(knobFraction);\n // The provider default fills nothing: the knob shows where it lands, the\n // track stays empty so it never reads as a chosen level.\n const idle = defaultActive && !dragging && hold === null;\n const filled = idle ? 0 : knobFraction;\n const fill = isTopLevel(shown, steps.length) ? LEVEL_GRADIENT : levelColor(shown, steps.length);\n return <div\n ref={trackRef}\n className={css.slider}\n role=\"slider\"\n tabIndex={0}\n aria-label={label}\n aria-valuemin={0}\n aria-valuemax={Math.max(0, steps.length - 1)}\n aria-valuenow={active}\n aria-valuetext={defaultActive && !dragging ? defaultLabel : steps[active]?.label}\n aria-disabled={busy || undefined}\n data-busy={busy ? '' : undefined}\n data-dragging={dragging ? '' : undefined}\n data-default={idle ? '' : undefined}\n onPointerDown={onPointerDown}\n onPointerMove={onPointerMove}\n onPointerUp={(event) => { settle(event, true); }}\n onPointerCancel={(event) => { settle(event, false); }}\n onKeyDown={onKeyDown}\n onClick={(event) => { event.stopPropagation(); }}\n >\n <div className={css.sliderTrack} aria-hidden=\"true\">\n <div className={css.sliderFill} style={{ width: fillWidth(filled), background: fill }} />\n {steps.map((step, index) => <span\n key={step.key}\n className={css.sliderTick}\n data-on={index <= shown && !idle ? '' : undefined}\n style={{ left: knobOffset(offsetOf(index, steps.length)) }}\n />)}\n <div className={css.sliderKnob} style={{ left: offset }} />\n </div>\n </div>;\n}\n","/**\n * Bridge the Host's own model menu: the model cell opens the all-provider\n * picker, and the effort cell carries the reasoning-level slider.\n *\n * The Host draws the root menu's first two cells itself: the model cell drills\n * into the current provider's list, the effort cell into the levels. This plugin\n * wants the first to open its dialog and the second to stop drilling, and the\n * Host exposes no hook for either. This entry is mounted exactly while the root\n * pane is open, so it can take the click on the menu surface before the cells'\n * own handlers run and mount the slider into the effort cell.\n *\n * The cells are recognized as the menu surface's direct `role=\"menuitem\"`\n * children, which is the Host's root-menu shape; the options slot output sits in\n * its own wrapper, so other plugins' rows never join that list. When the second\n * cell is missing the model declares no reasoning levels and the row is left\n * alone; when the first is missing the click falls through to the Host.\n *\n * Clicking the cell's label switches to the provider default, which is a state\n * rather than a detent: the slider shows it as an empty track under a hollow\n * knob, so it never reads as the weakest level.\n */\nimport { useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';\nimport { createPortal } from 'react-dom';\nimport type { ModelSelectInjected } from '@deepseek-ai/dsh-client-ui-model-selection/client';\nimport type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';\nimport { EffortSlider } from './EffortSlider.tsx';\nimport type { PickerStore } from './picker-state.ts';\nimport { effortChoices, liveReasoning, planEffortCommit, selectionOf, type Selection } from './selection.ts';\nimport css from './ModelOptions.module.css';\n\n/** Props of the bridge: menu state, the owning session, and the seat's injected face. */\nexport type ModelMenuBridgeProps = PropsRuntime<'conversation.input.model.options'>\n & PropsLocale<'modelSubmenu'>\n & Pick<ModelSelectInjected, 'available' | 'directory' | 'select'>\n & {\n /** Session whose catalog and picker this menu drives. */\n readonly sessionId: string;\n /** Carries the open request to the overlay view. */\n readonly picker: PickerStore;\n }\n\n/**\n * Render the marker, redirect the model cell, and mount the effort slider.\n * @param props - menu state, session, catalog face, and localized copy.\n * @returns an inert marker plus the slider while the effort cell is present.\n */\nexport function ModelMenuBridge({\n sessionId, picker, dismiss, reportFailure, available, directory, select, t,\n}: ModelMenuBridgeProps) {\n const markerRef = useRef<HTMLSpanElement | null>(null);\n const sliderRef = useRef<HTMLDivElement | null>(null);\n const [surface, setSurface] = useState<HTMLElement | null>(null);\n const [effortRow, setEffortRow] = useState<HTMLElement | null>(null);\n const [rowLabel, setRowLabel] = useState<string | null>(null);\n\n const state = useSyncExternalStore(\n (listener) => directory.subscribe(listener),\n () => directory.getSnapshot(),\n );\n const current = state.current;\n const reasoning = liveReasoning(state);\n const levels = useMemo(() => effortChoices(reasoning), [reasoning]);\n const declared = levels.findIndex((level) => level.effort === reasoning?.defaultEffort);\n const chosen = current?.reasoningEffort === undefined\n ? -1\n : levels.findIndex((level) => level.effort === current.reasoningEffort);\n // The provider default is a state, not a detent: it is in force whenever the\n // session carries no explicit level.\n const defaultActive = current !== null && current.reasoningEffort === undefined;\n const currentIndex = chosen >= 0 ? chosen : declared >= 0 ? declared : 0;\n const defaultIndex = declared >= 0 ? declared : 0;\n const canDefault = !defaultActive;\n const busy = state.pending !== null;\n // One detent is not a choice: leave the Host's row exactly as it is.\n const showSlider = available && levels.length >= 2 && effortRow !== null;\n\n // The Host rebuilds its cells on every render, so the surface and the effort\n // cell are re-read after each one; the identity guards keep that from looping.\n useLayoutEffect(() => {\n const foundSurface = markerRef.current?.closest('[data-menu-material]');\n const nextSurface = foundSurface instanceof HTMLElement ? foundSurface : null;\n if (nextSurface !== surface) setSurface(nextSurface);\n const cells = nextSurface === null\n ? []\n : [...nextSurface.querySelectorAll<HTMLElement>(':scope > button[role=\"menuitem\"]')];\n const nextRow = cells.length >= 2 ? cells[1] ?? null : null;\n if (nextRow !== effortRow) setEffortRow(nextRow);\n const nextLabel = nextRow?.querySelector(\"span[class*='cellLabel']\")?.textContent?.trim() ?? '';\n if (nextLabel !== (rowLabel ?? '')) setRowLabel(nextLabel);\n });\n\n const submit = (selection: Selection | null): void => {\n if (selection === null) return;\n void select(selection).then((result) => {\n if (result === undefined || result.ok) return;\n reportFailure(result.error.code === 'session/writer-held'\n ? t('error.sessionInUse')\n : t('error.action', { message: `${result.error.code}: ${result.error.message}` }));\n });\n };\n const commit = (index: number): void => {\n const live = selectionOf(state);\n const level = levels[index];\n if (live === null || level === undefined) return;\n submit(planEffortCommit(live, level.effort));\n };\n const commitDefault = (): void => {\n const live = selectionOf(state);\n if (live === null) return;\n submit(planEffortCommit(live, undefined));\n };\n\n // The Host rebuilds `dismiss` on every render, so the listener reads the\n // current one through a ref instead of being torn down and reinstalled.\n const latest = useRef({ sessionId, picker, dismiss, showSlider, canDefault, commitDefault });\n latest.current = { sessionId, picker, dismiss, showSlider, canDefault, commitDefault };\n\n useEffect(() => {\n if (surface === null) return;\n const redirect = (event: MouseEvent): void => {\n const target = event.target;\n if (!(target instanceof Node)) return;\n const cells = [...surface.querySelectorAll<HTMLElement>(':scope > button[role=\"menuitem\"]')];\n const modelCell = cells[0];\n if (modelCell !== undefined && modelCell.contains(target)) {\n // Capture on the surface runs before the React root's bubble listener, so\n // stopping here keeps the cell's own drill handler from ever running.\n event.preventDefault();\n event.stopPropagation();\n const request = latest.current;\n request.dismiss();\n request.picker.open(request.sessionId);\n return;\n }\n const row = cells[1];\n if (row === undefined || !row.contains(target) || !latest.current.showSlider) return;\n // The slider owns its own presses; only the label and the value are inert.\n if (sliderRef.current?.contains(target) === true) return;\n event.preventDefault();\n event.stopPropagation();\n const label = row.querySelector(\"span[class*='cellLabel']\");\n if (latest.current.canDefault && label !== null && label.contains(target)) {\n latest.current.commitDefault();\n return;\n }\n sliderRef.current?.querySelector<HTMLElement>('[role=\"slider\"]')?.focus();\n };\n surface.addEventListener('click', redirect, true);\n return () => {\n surface.removeEventListener('click', redirect, true);\n };\n }, [surface]);\n\n // The slider takes the whole slack beside the label: the value and the\n // chevron go, because the level is already written under the composer and the\n // row no longer drills anywhere. Every write is recorded and put back.\n useLayoutEffect(() => {\n if (effortRow === null || !showSlider) return;\n const label = effortRow.querySelector<HTMLElement>(\"span[class*='cellLabel']\");\n const value = effortRow.querySelector<HTMLElement>(\"span[class*='cellValue']\");\n const chevron = effortRow.querySelector<SVGElement>(\"svg[class*='cellChevron']\");\n const saved = {\n value: value?.style.display ?? '',\n chevron: chevron?.style.display ?? '',\n cursor: label?.style.cursor ?? '',\n title: label?.getAttribute('title') ?? null,\n };\n if (value !== null) value.style.display = 'none';\n if (chevron !== null) chevron.style.display = 'none';\n if (label !== null && canDefault) {\n label.style.cursor = 'pointer';\n label.title = t('effort.useDefault');\n }\n return () => {\n if (value !== null) value.style.display = saved.value;\n if (chevron !== null) chevron.style.display = saved.chevron;\n if (label !== null) {\n label.style.cursor = saved.cursor;\n if (saved.title === null) label.removeAttribute('title');\n else label.title = saved.title;\n }\n };\n }, [effortRow, showSlider, canDefault, t]);\n\n return <>\n <span ref={markerRef} hidden />\n {showSlider && effortRow !== null && createPortal(\n <div ref={sliderRef} className={css.sliderCell}>\n <EffortSlider\n steps={levels}\n current={currentIndex}\n defaultActive={defaultActive}\n defaultIndex={defaultIndex}\n defaultLabel={t('effort.providerDefault')}\n busy={busy}\n label={rowLabel !== null && rowLabel !== '' ? rowLabel : t('effort.label')}\n onCommit={commit}\n />\n </div>,\n effortRow,\n )}\n </>;\n}\n","/**\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 in the picker.\n *\n * The star is its own control, because favouriting there must not switch the\n * model: the row button picks, the star toggles, and the check marks the model\n * in use.\n */\n\nimport type { ReactNode } from 'react';\nimport { IconCheckOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives';\nimport { StarIcon } from './icons.tsx';\nimport css from './ModelOptions.module.css';\n\n/** Props of one row. */\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 /** Whether the model is favorited right now. */\n readonly favorited: boolean;\n /** The metadata line's text. */\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. */\n readonly onToggleFavorite: () => void;\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, favorited, meta, starLabel,\n starTabStop = false, rowTabIndex, onPick, onFocusRow, onToggleFavorite, children,\n}: ModelRowProps) {\n return <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 <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 </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 * 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 * 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 './ModelOptions.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 /**\n * The reasoning level this browser last used for one model, if any.\n * @param provider - the provider route key.\n * @param model - the model id inside that provider.\n * @returns the remembered level, or undefined when none is remembered.\n */\n readonly rememberedOf?: (provider: string, model: string) => string | undefined\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, rememberedOf, 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, rememberedOf?.(row.provider, row.model))\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 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","/**\n * The composer overlay's picker view: the dialog, its catalog, and its verbs.\n *\n * It lives in `conversation.input.overlay` rather than beside the row, because\n * the Host unmounts the model menu as soon as a row is clicked. Everything the\n * dialog reads arrives through this entry's injected face.\n */\nimport { useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';\nimport { IconWarningOutlineRegular, Toast } from '@deepseek-ai/dsh-client-ui-primitives';\nimport type { 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 type { FactsSource } from './facts-source.ts';\nimport {\n readFavorites,\n toggleFavorite,\n type FavoriteModel,\n type FavoritesStorage,\n} from './favorites.ts';\nimport {\n effortFor,\n readEfforts,\n rememberEffort,\n type RememberedEffort,\n} from './effort-memory.ts';\nimport { selectionOf, type Selection } from './selection.ts';\nimport type { PickerStore } from './picker-state.ts';\nimport css from './ModelOptions.module.css';\n\n/** Props of the overlay view: the session, the store, and the seat's injected face. */\nexport type ModelPickerHostProps = PropsLocale<'modelSubmenu'>\n & ModelSelectInjected\n & {\n /** Session whose catalog this view renders. */\n readonly sessionId: string;\n /** The request written by the menu row. */\n readonly picker: PickerStore;\n /** The settings-derived metadata the picker's line reads. */\n readonly factsSource: FactsSource;\n }\n\n/**\n * Render the picker for this session.\n * @param props - session, store, catalog face, and localized copy.\n * @returns the dialog while the store names this session; otherwise nothing.\n */\nexport function ModelPickerHost({\n sessionId, picker, available, directory, load, select, resolveText, factsSource, t,\n}: ModelPickerHostProps) {\n const request = useSyncExternalStore(picker.subscribe, picker.getSnapshot);\n const state = useSyncExternalStore((listener) => directory.subscribe(listener), () => directory.getSnapshot());\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\n const busy = state.status === 'selecting';\n const live = useMemo(() => selectionOf(state), [state]);\n const facts = useSyncExternalStore((listener) => factsSource.subscribe(listener), () => factsSource.getSnapshot());\n const open = request.sessionId === sessionId;\n // The injected face is cached per entry and session, but the read goes through\n // a ref anyway: a catalog load writes the store, so an unstable `load` in the\n // dependency list would reload on every render it caused.\n const loadRef = useRef(load);\n loadRef.current = load;\n\n // Opening reads the catalog again: another client may have changed it, and a\n // failed load has to be retryable from the same gesture.\n useEffect(() => {\n if (open) loadRef.current();\n }, [open]);\n\n // The Host's own menu changes the reasoning level without passing through this\n // plugin, so the memory is written from the directory snapshot too — a picker\n // click alone would only ever record levels chosen in the dialog. The key guard\n // keeps one write per distinct selection instead of one per snapshot.\n const current = state.current;\n const rememberedRef = useRef<string | null>(null);\n useEffect(() => {\n if (current === null) return;\n const key = `${current.provider}/${current.model}/${current.reasoningEffort ?? ''}`;\n if (rememberedRef.current === key) return;\n rememberedRef.current = key;\n setMemory(rememberEffort(storageOf(), current.provider, current.model, current.reasoningEffort));\n }, [current]);\n\n if (!available) return null;\n\n const raiseToast = (text: string): void => {\n toastSeq.current += 1;\n setToast({ seq: toastSeq.current, text });\n };\n const submit = (selection: Selection): void => {\n void select(selection).then((result) => {\n if (result === undefined || result.ok) {\n setMemory(rememberEffort(storageOf(), selection.provider, selection.model, selection.reasoningEffort));\n picker.close();\n return;\n }\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\n return <div ref={rootRef} className={css.pickerHost}>\n <ModelPicker\n open={open}\n resolveText={resolveText}\n state={state}\n live={live}\n facts={facts}\n favorites={favorites}\n busy={busy}\n t={t}\n rememberedOf={(provider, model) => effortFor(memory, provider, model)}\n onPick={submit}\n onToggleFavorite={(provider, model) => {\n setFavorites(toggleFavorite(storageOf(), provider, model));\n }}\n onReload={load}\n onClose={() => {\n picker.close();\n }}\n />\n {toast !== null && <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 </div>;\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 * Which session's picker is open.\n *\n * The row that asks for the picker lives inside the composer model menu, and the\n * Host unmounts that menu the moment a row is clicked. The dialog lives in the\n * composer's overlay slot, which stays mounted. One plugin-wide store carries the\n * request across that unmount — the same split the Host's own command popups use\n * between their controller and their overlay view.\n */\n\n/** What the store publishes. */\nexport interface PickerSnapshot {\n /** Session whose picker is open, or null when none is. */\n readonly sessionId: string | null\n}\n\n/** Open and close verbs plus the observable the overlay view subscribes to. */\nexport interface PickerStore {\n /** @returns the current request. */\n readonly getSnapshot: () => PickerSnapshot\n /** @param listener - called after every change. @returns the unsubscribe. */\n readonly subscribe: (listener: () => void) => () => void\n /** @param sessionId - open the picker for one session, replacing any other. */\n readonly open: (sessionId: string) => void\n /** Close whatever is open. */\n readonly close: () => void\n}\n\n/**\n * @returns a store that starts closed.\n */\nexport function createPickerStore(): PickerStore {\n let snapshot: PickerSnapshot = { sessionId: null };\n const listeners = new Set<() => void>();\n const publish = (next: PickerSnapshot): void => {\n if (next.sessionId === snapshot.sessionId) return;\n snapshot = next;\n for (const listener of listeners) listener();\n };\n return {\n getSnapshot: () => snapshot,\n subscribe: (listener) => {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n open: (sessionId) => {\n publish({ sessionId });\n },\n close: () => {\n publish({ sessionId: null });\n },\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 all-provider model picker. */\n\n/** Dictionary namespace owned by this plugin. */\nexport const NS = 'modelSubmenu'\n\n/** Locale keys the picker renders. */\nexport type ModelSubmenuLocaleKey =\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 | 'effort.label'\n | 'effort.providerDefault'\n | 'effort.useDefault'\n\ndeclare module '@deepseek-ai/dsh-client-ui-slots' {\n interface LocaleNamespaceMap {\n /** The all-provider model picker'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 '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 'effort.label': '推理等级',\n 'effort.providerDefault': 'Default',\n 'effort.useDefault': '切换为默认档位',\n}\n\n/** English dictionary, checked complete against the zh key set. */\nexport const en: Record<ModelSubmenuLocaleKey, string> = {\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 'effort.label': 'Reasoning effort',\n 'effort.providerDefault': 'Default',\n 'effort.useDefault': 'Switch to the provider default',\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 model menu's options slot and the composer overlay 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 { EffortTint } from './EffortTint.tsx'\nimport { ModelMenuBridge } from './ModelMenuBridge.tsx'\nimport { ModelPickerHost } from './ModelPickerHost.tsx'\nimport { createPickerStore } from './picker-state.ts'\nimport { createFactsSource, type DescribeFace } from './facts-source.ts'\nimport { NS, en, zh } from './locales.ts'\n\nexport { EffortTint } from './EffortTint.tsx'\nexport { ModelMenuBridge } from './ModelMenuBridge.tsx'\nexport { ModelPickerHost } from './ModelPickerHost.tsx'\nexport { createPickerStore } from './picker-state.ts'\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 both entries render\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 contribute the\n * bridge that turns the Host's model cell into the picker's door and its effort\n * cell into the reasoning-level slider, the overlay that renders the picker, and\n * the tint that keeps the collapsed trigger's effort caption on the same color.\n *\n * The two are separate entries because the Host unmounts the model menu the\n * moment a row is clicked; the overlay slot stays mounted, so the dialog\n * survives the menu's teardown.\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 picker = createPickerStore()\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 /**\n * Build one entry's business face for a session.\n * @param sessionId - the slot's session scope.\n * @returns the catalog face plus the session and store both entries need.\n */\n const faceOf = (sessionId: string) => {\n // The slot'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 sessionId,\n picker,\n available,\n directory: directory.store,\n factsSource: facts.source,\n load: () => {\n if (available) directory.load().catch(() => {})\n },\n resolveText: (text: Parameters<typeof ctx.locale.resolveText>[0]) => ctx.locale.resolveText(text),\n select: (selection: Parameters<typeof directory.select>[0]) => available\n ? directory.select(selection)\n : Promise.resolve(undefined),\n }\n }\n scope.slots.inject('conversation.input.model.options', () => scope.slots.register({\n name: 'conversation.input.model.options',\n id: 'model-menu-bridge',\n registrant: '@lynn123411/dsh-model-submenu',\n locale: NS,\n inject: faceOf,\n }, ModelMenuBridge))\n scope.slots.inject('conversation.input.overlay', () => scope.slots.register({\n name: 'conversation.input.overlay',\n id: 'model-picker',\n registrant: '@lynn123411/dsh-model-submenu',\n locale: NS,\n inject: faceOf,\n }, ModelPickerHost))\n // The trigger's effort caption lives outside the menu, so its tint needs an\n // entry that stays mounted for the whole session.\n scope.slots.inject('conversation.input.overlay', () => scope.slots.register({\n name: 'conversation.input.overlay',\n id: 'model-effort-tint',\n registrant: '@lynn123411/dsh-model-submenu',\n inject: faceOf,\n }, EffortTint))\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.10",
3
+ "version": "0.3.11",
4
4
  "description": "增强输入框的模型菜单:第一行直接打开跨供应商的模型弹窗,第二行的推理等级是可拖动的滑块,档位带配色。",
5
5
  "license": "MIT",
6
6
  "packageManager": "pnpm@11.7.0",
@@ -3,9 +3,10 @@
3
3
  *
4
4
  * The Host's second menu cell is a button that drills into a list of levels.
5
5
  * This slider sits inside that same cell, between the Host's label and value, so
6
- * the levels are reachable without leaving the menu. Levels are discrete: the
7
- * knob snaps to one detent per level, the drag only moves the knob, and the
8
- * release commits once. Keyboard arrows step and commit immediately.
6
+ * the levels are reachable without leaving the menu. The levels are discrete but
7
+ * the drag is not: the knob follows the pointer continuously, the level it
8
+ * reports is the detent nearest that position, and letting go eases the knob onto
9
+ * that detent and commits once. Keyboard arrows step and commit immediately.
9
10
  *
10
11
  * The fill takes the level's color, so the track and the two captions read as
11
12
  * one signal: "off" is neutral, the levels above it walk a blue-to-violet ramp,
@@ -14,10 +15,10 @@
14
15
  * "Provider default" is not a detent: it is a state, drawn as an empty track
15
16
  * with a hollow knob, so it never reads as the lowest level.
16
17
  */
17
- import { useRef, useState, type KeyboardEvent, type PointerEvent } from 'react';
18
+ import { useEffect, useRef, useState, type KeyboardEvent, type PointerEvent } from 'react';
18
19
  import type { EffortChoice } from './selection.ts';
19
20
  import { LEVEL_GRADIENT, isTopLevel, levelColor } from './level-color.ts';
20
- import { knobOffset, offsetOf, stepAt } from './slider.ts';
21
+ import { detentAt, fillWidth, fractionAt, knobOffset, offsetOf } from './slider.ts';
21
22
  import css from './ModelOptions.module.css';
22
23
 
23
24
  /** Props of the slider: the levels, the one in force, and the commit path. */
@@ -49,22 +50,41 @@ export function EffortSlider({
49
50
  steps, current, defaultActive, defaultIndex, defaultLabel, busy, label, onCommit,
50
51
  }: EffortSliderProps) {
51
52
  const trackRef = useRef<HTMLDivElement | null>(null);
52
- // The drag index lives in a ref as well as in state: a press and a release can
53
- // arrive in one task, and the release must not read a render that never ran.
53
+ // The drag position lives in a ref as well as in state: a press and a release
54
+ // can arrive in one task, and the release must not read a render that never ran.
54
55
  const dragRef = useRef<number | null>(null);
56
+ // While dragging this is where the pointer is along the travel, not a level:
57
+ // the knob has to move with the pointer, or the drag reads as a series of jumps.
55
58
  const [drag, setDrag] = useState<number | null>(null);
59
+ // After a release that submits, the knob waits on the detent it settled on
60
+ // instead of springing back to the old level while the write is in flight.
61
+ const [hold, setHold] = useState<number | null>(null);
62
+ const dragging = drag !== null;
56
63
  const resting = defaultActive ? defaultIndex : current;
57
- const active = drag ?? resting;
58
- const beginDrag = (index: number | null): void => {
59
- dragRef.current = index;
60
- setDrag(index);
64
+ const active = dragging ? detentAt(drag, steps.length) : resting;
65
+ const knobFraction = drag ?? hold ?? offsetOf(resting, steps.length);
66
+ // The color and the ticks follow the knob, not the level in force: during a
67
+ // drag, and while a commit is in flight, the knob has already gone ahead.
68
+ const shown = dragging || hold !== null ? detentAt(knobFraction, steps.length) : resting;
69
+ const beginDrag = (fraction: number | null): void => {
70
+ dragRef.current = fraction;
71
+ setDrag(fraction);
61
72
  };
62
73
 
63
- const indexAt = (clientX: number): number => {
74
+ // The hold ends as soon as the write does, whether it landed on the new level
75
+ // or was rejected: the displayed level is the truth again either way.
76
+ useEffect(() => {
77
+ if (!busy) setHold(null);
78
+ }, [busy]);
79
+ useEffect(() => {
80
+ setHold(null);
81
+ }, [current, defaultActive]);
82
+
83
+ const fractionAtPointer = (clientX: number): number => {
64
84
  const track = trackRef.current;
65
- if (track === null) return active;
85
+ if (track === null) return dragRef.current ?? knobFraction;
66
86
  const rect = track.getBoundingClientRect();
67
- return stepAt(clientX, rect.left, rect.width, steps.length);
87
+ return fractionAt(clientX, rect.left, rect.width);
68
88
  };
69
89
 
70
90
  const onPointerDown = (event: PointerEvent<HTMLDivElement>): void => {
@@ -73,7 +93,8 @@ export function EffortSlider({
73
93
  // also settle the cell and pull focus out of the slider.
74
94
  event.preventDefault();
75
95
  event.stopPropagation();
76
- beginDrag(indexAt(event.clientX));
96
+ setHold(null);
97
+ beginDrag(fractionAtPointer(event.clientX));
77
98
  try {
78
99
  event.currentTarget.setPointerCapture(event.pointerId);
79
100
  } catch {
@@ -84,18 +105,20 @@ export function EffortSlider({
84
105
 
85
106
  const onPointerMove = (event: PointerEvent<HTMLDivElement>): void => {
86
107
  if (dragRef.current === null || busy) return;
87
- const next = indexAt(event.clientX);
88
- if (next !== dragRef.current) beginDrag(next);
108
+ beginDrag(fractionAtPointer(event.clientX));
89
109
  };
90
110
 
91
111
  const settle = (event: PointerEvent<HTMLDivElement>, commit: boolean): void => {
92
112
  if (dragRef.current === null) return;
93
- const next = indexAt(event.clientX);
113
+ const next = detentAt(fractionAtPointer(event.clientX), steps.length);
94
114
  beginDrag(null);
95
115
  if (!commit) return;
96
116
  // Leaving the provider default is a change even when the knob lands where
97
117
  // the default already pointed.
98
- if (next !== current || defaultActive) onCommit(next);
118
+ if (next !== current || defaultActive) {
119
+ setHold(offsetOf(next, steps.length));
120
+ onCommit(next);
121
+ }
99
122
  };
100
123
 
101
124
  const onKeyDown = (event: KeyboardEvent<HTMLDivElement>): void => {
@@ -117,13 +140,12 @@ export function EffortSlider({
117
140
  if (next !== current || defaultActive) onCommit(next);
118
141
  };
119
142
 
120
- const knobFraction = offsetOf(active, steps.length);
121
143
  const offset = knobOffset(knobFraction);
122
144
  // The provider default fills nothing: the knob shows where it lands, the
123
145
  // track stays empty so it never reads as a chosen level.
124
- const idle = defaultActive && drag === null;
146
+ const idle = defaultActive && !dragging && hold === null;
125
147
  const filled = idle ? 0 : knobFraction;
126
- const fill = isTopLevel(active, steps.length) ? LEVEL_GRADIENT : levelColor(active, steps.length);
148
+ const fill = isTopLevel(shown, steps.length) ? LEVEL_GRADIENT : levelColor(shown, steps.length);
127
149
  return <div
128
150
  ref={trackRef}
129
151
  className={css.slider}
@@ -133,11 +155,11 @@ export function EffortSlider({
133
155
  aria-valuemin={0}
134
156
  aria-valuemax={Math.max(0, steps.length - 1)}
135
157
  aria-valuenow={active}
136
- aria-valuetext={defaultActive && drag === null ? defaultLabel : steps[active]?.label}
158
+ aria-valuetext={defaultActive && !dragging ? defaultLabel : steps[active]?.label}
137
159
  aria-disabled={busy || undefined}
138
160
  data-busy={busy ? '' : undefined}
139
- data-dragging={drag === null ? undefined : ''}
140
- data-default={defaultActive && drag === null ? '' : undefined}
161
+ data-dragging={dragging ? '' : undefined}
162
+ data-default={idle ? '' : undefined}
141
163
  onPointerDown={onPointerDown}
142
164
  onPointerMove={onPointerMove}
143
165
  onPointerUp={(event) => { settle(event, true); }}
@@ -146,11 +168,11 @@ export function EffortSlider({
146
168
  onClick={(event) => { event.stopPropagation(); }}
147
169
  >
148
170
  <div className={css.sliderTrack} aria-hidden="true">
149
- <div className={css.sliderFill} style={{ width: knobOffset(filled), background: fill }} />
171
+ <div className={css.sliderFill} style={{ width: fillWidth(filled), background: fill }} />
150
172
  {steps.map((step, index) => <span
151
173
  key={step.key}
152
174
  className={css.sliderTick}
153
- data-on={index <= active && !idle ? '' : undefined}
175
+ data-on={index <= shown && !idle ? '' : undefined}
154
176
  style={{ left: knobOffset(offsetOf(index, steps.length)) }}
155
177
  />)}
156
178
  <div className={css.sliderKnob} style={{ left: offset }} />
@@ -41,17 +41,22 @@
41
41
  cursor: default;
42
42
  }
43
43
 
44
+ /* The track is a capsule and the knob a larger circle riding on it: the knob
45
+ * overhangs at both ends, so it reads as a knob rather than as the track's own
46
+ * end cap. */
44
47
  .sliderTrack {
45
48
  position: relative;
46
49
  width: 100%;
47
- height: 20px;
50
+ height: 16px;
48
51
  border-radius: 999px;
49
52
  corner-shape: round;
50
53
  background: var(--dsw-alias-border-l3);
51
54
  }
52
55
 
53
- /* The fill takes the level's color and is clipped to the filled part, so the
54
- * color under the knob is the color the level's text takes. */
56
+ /* The fill is the track's own shape cut at the knob: its right cap carries the
57
+ * track's radius and ends under the knob, so the color runs into the knob
58
+ * instead of stopping at a straight edge, and the last detent reaches the
59
+ * track's end. */
55
60
  .sliderFill {
56
61
  position: absolute;
57
62
  top: 0;
@@ -59,8 +64,10 @@
59
64
  left: 0;
60
65
  border-radius: 999px;
61
66
  corner-shape: round;
62
- overflow: hidden;
63
67
  pointer-events: none;
68
+ /* A level change eases the color to its new width; the drag turns this off so
69
+ * the edge stays under the pointer. */
70
+ transition: width 160ms cubic-bezier(0.2, 0, 0, 1);
64
71
  }
65
72
 
66
73
  /* One detent per level: the dot is the only thing that says the levels are
@@ -86,6 +93,9 @@
86
93
  .sliderKnob {
87
94
  position: absolute;
88
95
  top: 50%;
96
+ /* The provider default draws a 1.5px ring; border-box keeps that ring inside
97
+ * the 20px circle the travel is computed for. */
98
+ box-sizing: border-box;
89
99
  width: 20px;
90
100
  height: 20px;
91
101
  margin: -10px 0 0 -10px;
@@ -94,6 +104,21 @@
94
104
  background: var(--dsw-alias-label-primary-foreground);
95
105
  box-shadow: var(--dsw-elevation-soft);
96
106
  pointer-events: none;
107
+ /* Letting go eases the knob onto the detent it settled on; the drag turns this
108
+ * off so the knob sits exactly under the pointer. */
109
+ transition: left 160ms cubic-bezier(0.2, 0, 0, 1);
110
+ }
111
+
112
+ .slider[data-dragging] .sliderKnob,
113
+ .slider[data-dragging] .sliderFill {
114
+ transition: none;
115
+ }
116
+
117
+ @media (prefers-reduced-motion: reduce) {
118
+ .sliderKnob,
119
+ .sliderFill {
120
+ transition: none;
121
+ }
97
122
  }
98
123
 
99
124
  /* Provider default: nothing is filled and the knob is a hollow ring, so the
@@ -1,19 +1,29 @@
1
1
  /**
2
2
  * Geometry of the discrete effort slider.
3
3
  *
4
- * The track carries one detent per level and the knob's centre travels between
5
- * two ends inset by the knob radius, so the knob never overhangs the track. A
6
- * pointer position becomes the detent nearest it; a detent becomes a fraction
7
- * of the track that CSS turns into the knob's centre and the filled width.
4
+ * The track is a capsule and the knob a slightly larger circle riding on it, so
5
+ * the knob overhangs the track instead of merging with its rounded end. The
6
+ * knob's centre travels between two points inset by the knob's radius, so the
7
+ * knob never runs past either end. A pointer position becomes a position along
8
+ * that travel; a position becomes the detent nearest it, or the CSS lengths that
9
+ * put the knob and the fill there.
8
10
  */
9
11
 
10
12
  /**
11
- * Knob diameter in CSS pixels, and the track's height: the slider stands as tall
12
- * as the row's text line. The centre stops half of it from each end of the
13
- * track, so the knob never overhangs.
13
+ * Track height in CSS pixels; the stylesheet keeps the same value. Half of it is
14
+ * the radius of the track's ends and of the fill's cap.
15
+ */
16
+ export const TRACK_HEIGHT = 16;
17
+
18
+ /**
19
+ * Knob diameter in CSS pixels. It is larger than the track, so the knob reads as
20
+ * a knob riding on the track rather than as the track's own end cap.
14
21
  */
15
22
  export const KNOB_SIZE = 20;
16
23
 
24
+ /** The knob's centre keeps its own radius away from either end of the track. */
25
+ const KNOB_RADIUS = KNOB_SIZE / 2;
26
+
17
27
  /**
18
28
  * @param index - detent index.
19
29
  * @param count - number of detents.
@@ -26,26 +36,63 @@ export function offsetOf(index: number, count: number): number {
26
36
  }
27
37
 
28
38
  /**
39
+ * Where a pointer sits along the knob's travel. The drag reads this rather than a
40
+ * detent, so the knob follows the pointer instead of jumping a level at a time.
29
41
  * @param clientX - pointer position in viewport coordinates.
30
42
  * @param left - the track's left edge in viewport coordinates.
31
43
  * @param width - the track's width in pixels.
32
- * @param count - number of detents.
33
- * @returns the detent nearest the pointer, clamped to the two ends.
44
+ * @returns the position as a fraction of the travel, clamped to 0 and 1.
34
45
  */
35
- export function stepAt(clientX: number, left: number, width: number, count: number): number {
36
- if (count <= 1) return 0;
46
+ export function fractionAt(clientX: number, left: number, width: number): number {
37
47
  const usable = width - KNOB_SIZE;
38
48
  if (usable <= 0) return 0;
39
- const fraction = (clientX - left - KNOB_SIZE / 2) / usable;
49
+ const fraction = (clientX - left - KNOB_RADIUS) / usable;
50
+ return Math.min(Math.max(fraction, 0), 1);
51
+ }
52
+
53
+ /**
54
+ * @param fraction - a knob position between 0 and 1.
55
+ * @param count - number of detents.
56
+ * @returns the detent nearest that position.
57
+ */
58
+ export function detentAt(fraction: number, count: number): number {
59
+ if (count <= 1) return 0;
40
60
  const clamped = Math.min(Math.max(fraction, 0), 1);
41
61
  return Math.round(clamped * (count - 1));
42
62
  }
43
63
 
64
+ /**
65
+ * @param clientX - pointer position in viewport coordinates.
66
+ * @param left - the track's left edge in viewport coordinates.
67
+ * @param width - the track's width in pixels.
68
+ * @param count - number of detents.
69
+ * @returns the detent nearest the pointer, clamped to the two ends.
70
+ */
71
+ export function stepAt(clientX: number, left: number, width: number, count: number): number {
72
+ if (count <= 1) return 0;
73
+ return detentAt(fractionAt(clientX, left, width), count);
74
+ }
75
+
44
76
  /**
45
77
  * @param fraction - a knob position between 0 and 1.
46
78
  * @returns the CSS length that puts the knob centre there, inset at both ends.
47
79
  */
48
80
  export function knobOffset(fraction: number): string {
49
81
  const clamped = Math.min(Math.max(fraction, 0), 1);
50
- return `calc(${KNOB_SIZE / 2}px + ${clamped} * (100% - ${KNOB_SIZE}px))`;
82
+ return `calc(${KNOB_RADIUS}px + ${clamped} * (100% - ${KNOB_SIZE}px))`;
83
+ }
84
+
85
+ /**
86
+ * Width of the colored fill for a knob position.
87
+ *
88
+ * The fill is the track's own shape cut at the knob: its right cap carries the
89
+ * track's radius and ends under the knob, so the color runs into the knob
90
+ * instead of stopping at a straight edge, and the last detent reaches the track's
91
+ * end rather than the knob's centre.
92
+ * @param fraction - a knob position between 0 and 1.
93
+ * @returns the CSS width of the fill.
94
+ */
95
+ export function fillWidth(fraction: number): string {
96
+ const clamped = Math.min(Math.max(fraction, 0), 1);
97
+ return `calc(${TRACK_HEIGHT}px + ${clamped} * (100% - ${TRACK_HEIGHT}px))`;
51
98
  }