@lynn123411/dsh-model-submenu 0.3.3 → 0.3.10

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/lib/client.js CHANGED
@@ -5,9 +5,570 @@ window.__ModuleLoader__.load({
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
  let react = require("react");
8
+ let react_jsx_runtime = require("react/jsx-runtime");
8
9
  let react_dom = require("react-dom");
9
10
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
10
- let react_jsx_runtime = require("react/jsx-runtime");
11
+ //#region src/client/level-color.ts
12
+ /**
13
+ * The accent one reasoning level carries.
14
+ *
15
+ * "Off" carries no accent at all — the fill and the caption stay neutral, so
16
+ * switching reasoning off reads as switching it off rather than as the first
17
+ * shade of a ramp. Every level above it walks a blue-to-violet ramp, and the
18
+ * highest level is the only one whose fill is a gradient.
19
+ */
20
+ /** Off: the dimmed label tone, the same neutral the Host uses for quiet text. */
21
+ const NEUTRAL = "var(--dsw-alias-label-dimmed)";
22
+ /** Lowest lit level: the blue the composer already uses for accents. */
23
+ const LOW = [
24
+ 77,
25
+ 147,
26
+ 248
27
+ ];
28
+ /** Highest level: violet. */
29
+ const HIGH = [
30
+ 147,
31
+ 51,
32
+ 234
33
+ ];
34
+ /**
35
+ * @param t - a point on the ramp, 0 at the lowest lit level and 1 at the top.
36
+ * @returns the ramp color there.
37
+ */
38
+ function ramp(t) {
39
+ const mix = (from, to) => Math.round(from + (to - from) * t);
40
+ return `rgb(${mix(LOW[0], HIGH[0])}, ${mix(LOW[1], HIGH[1])}, ${mix(LOW[2], HIGH[2])})`;
41
+ }
42
+ /**
43
+ * @param index - the level's detent, 0 being "off".
44
+ * @param count - number of detents.
45
+ * @returns the level's color as a CSS color string.
46
+ */
47
+ function levelColor(index, count) {
48
+ if (index <= 0) return NEUTRAL;
49
+ if (isTopLevel(index, count)) return ramp(1);
50
+ const span = Math.max(1, count - 2);
51
+ return ramp(Math.min(Math.max((index - 1) / span, 0), 1));
52
+ }
53
+ /**
54
+ * @param index - the level's detent.
55
+ * @param count - number of detents.
56
+ * @returns whether this level is the only one painted with a gradient.
57
+ */
58
+ function isTopLevel(index, count) {
59
+ return count > 1 && index === count - 1;
60
+ }
61
+ /** The top level's fill: the whole ramp, ending on the color its text takes. */
62
+ const LEVEL_GRADIENT = `linear-gradient(90deg, ${ramp(0)}, ${ramp(1)})`;
63
+ //#endregion
64
+ //#region src/client/selection.ts
65
+ /**
66
+ * Build the slider's detents for one model.
67
+ *
68
+ * The provider default is deliberately absent: it is a state, not a level, and
69
+ * giving it a detent would read as the weakest reasoning instead of "let the
70
+ * provider decide". The label above the track switches to it.
71
+ * @param reasoning - the catalog's reasoning metadata, undefined when the model has none.
72
+ * @returns one detent per advertised level, in catalog order.
73
+ */
74
+ function effortChoices(reasoning) {
75
+ if (reasoning === void 0) return [];
76
+ return reasoning.efforts.map((level) => ({
77
+ key: level.id,
78
+ effort: level.id,
79
+ label: level.name
80
+ }));
81
+ }
82
+ /**
83
+ * The selection a level click submits.
84
+ * @param live - the selection in force right now.
85
+ * @param effort - the clicked level, undefined meaning the provider default.
86
+ * @returns the selection to submit, or null when the level already is the live one.
87
+ */
88
+ function planEffortCommit(live, effort) {
89
+ if (sameEffort(live.reasoningEffort, effort)) return null;
90
+ return {
91
+ provider: live.provider,
92
+ model: live.model,
93
+ ...effort === void 0 ? {} : { reasoningEffort: effort }
94
+ };
95
+ }
96
+ /**
97
+ * The selection a model click submits.
98
+ * @param live - the selection in force right now, null before a first selection resolves.
99
+ * @param provider - the clicked model's provider.
100
+ * @param model - the clicked model id.
101
+ * @param reasoningOf - the catalog's reasoning metadata for one provider and model.
102
+ * @param remembered - the level this browser last used for the target model, if any.
103
+ * @returns the selection to submit, or null when the click changes nothing.
104
+ */
105
+ function planModelCommit(live, provider, model, reasoningOf, remembered) {
106
+ if (live !== null && sameModel(live, provider, model)) return null;
107
+ const offered = reasoningOf(provider, model);
108
+ return {
109
+ provider,
110
+ model,
111
+ ...offered !== void 0 && remembered !== void 0 && offered.efforts.some((level) => level.id === remembered) ? { reasoningEffort: remembered } : {}
112
+ };
113
+ }
114
+ /**
115
+ * The reasoning metadata the catalog advertises for the model in force.
116
+ * @param state - the session's directory snapshot.
117
+ * @returns the metadata, or undefined when no model is live or it advertises none.
118
+ */
119
+ function liveReasoning(state) {
120
+ const current = state.current;
121
+ if (current === null) return void 0;
122
+ return state.groups.find((group) => group.id === current.provider)?.models.find((model) => model.id === current.model)?.reasoning;
123
+ }
124
+ /**
125
+ * The selection in force, or null while the Host has not resolved one.
126
+ * @param state - the session's directory snapshot.
127
+ * @returns the live selection, with the effort only when one is set.
128
+ */
129
+ function selectionOf(state) {
130
+ if (state.current === null) return null;
131
+ return state.current.reasoningEffort === void 0 ? {
132
+ provider: state.current.provider,
133
+ model: state.current.model
134
+ } : {
135
+ provider: state.current.provider,
136
+ model: state.current.model,
137
+ reasoningEffort: state.current.reasoningEffort
138
+ };
139
+ }
140
+ /**
141
+ * @param live - one selection.
142
+ * @param provider - the provider to compare.
143
+ * @param model - the model id to compare.
144
+ * @returns whether both name the same model.
145
+ */
146
+ function sameModel(live, provider, model) {
147
+ return live.provider === provider && live.model === model;
148
+ }
149
+ /**
150
+ * @param liveEffort - the level in force, undefined meaning the provider default.
151
+ * @param effort - the level in question.
152
+ * @returns whether they name the same level.
153
+ */
154
+ function sameEffort(liveEffort, effort) {
155
+ return liveEffort === effort;
156
+ }
157
+ //#endregion
158
+ //#region src/client/EffortTint.tsx
159
+ /**
160
+ * Tint the composer's effort caption with the level's color.
161
+ *
162
+ * The slider's fill and the menu's value already carry the level's color; the
163
+ * caption on the collapsed trigger is the third place the level shows, and it
164
+ * lives outside the menu. This entry stays mounted for the whole session and
165
+ * writes that one color, so all three read as one signal.
166
+ *
167
+ * The caption is recognized as the trigger's `span[class*='triggerEffort']`
168
+ * child — the same class-substring contract the menu row uses. When the model
169
+ * declares no levels the span does not exist and nothing is written.
170
+ */
171
+ /**
172
+ * Render nothing and keep the trigger caption colored.
173
+ * @param props - the session's catalog face.
174
+ * @returns an inert marker.
175
+ */
176
+ function EffortTint({ available, directory }) {
177
+ const markerRef = (0, react.useRef)(null);
178
+ const [caption, setCaption] = (0, react.useState)(null);
179
+ const state = (0, react.useSyncExternalStore)((listener) => directory.subscribe(listener), () => directory.getSnapshot());
180
+ const levels = effortChoices(liveReasoning(state));
181
+ const effort = state.current?.reasoningEffort;
182
+ const chosen = effort === void 0 ? -1 : levels.findIndex((level) => level.effort === effort);
183
+ const color = available && chosen >= 0 && levels.length >= 2 ? levelColor(chosen, levels.length) : null;
184
+ (0, react.useLayoutEffect)(() => {
185
+ const next = (markerRef.current?.closest("[data-composer-card]"))?.querySelector("[data-slot=\"conversation.input.model\"] button[aria-haspopup=\"menu\"] span[class*=\"triggerEffort\"]") ?? null;
186
+ if (next !== caption) setCaption(next);
187
+ });
188
+ (0, react.useLayoutEffect)(() => {
189
+ if (caption === null || color === null) return;
190
+ const saved = caption.style.color;
191
+ caption.style.color = color;
192
+ return () => {
193
+ caption.style.color = saved;
194
+ };
195
+ }, [caption, color]);
196
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
197
+ ref: markerRef,
198
+ hidden: true
199
+ });
200
+ }
201
+ /**
202
+ * @param index - detent index.
203
+ * @param count - number of detents.
204
+ * @returns the knob centre as a fraction of the track, 0 at the first detent and 1 at the last.
205
+ */
206
+ function offsetOf(index, count) {
207
+ if (count <= 1) return 0;
208
+ return Math.min(Math.max(index, 0), count - 1) / (count - 1);
209
+ }
210
+ /**
211
+ * @param clientX - pointer position in viewport coordinates.
212
+ * @param left - the track's left edge in viewport coordinates.
213
+ * @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.
216
+ */
217
+ function stepAt(clientX, left, width, count) {
218
+ if (count <= 1) return 0;
219
+ const usable = width - 20;
220
+ if (usable <= 0) return 0;
221
+ const fraction = (clientX - left - 10) / usable;
222
+ return Math.round(Math.min(Math.max(fraction, 0), 1) * (count - 1));
223
+ }
224
+ /**
225
+ * @param fraction - a knob position between 0 and 1.
226
+ * @returns the CSS length that puts the knob centre there, inset at both ends.
227
+ */
228
+ function knobOffset(fraction) {
229
+ return `calc(10px + ${Math.min(Math.max(fraction, 0), 1)} * (100% - 20px))`;
230
+ }
231
+ //#endregion
232
+ //#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)}";
234
+ const tagId = "@lynn123411/dsh-model-submenu/ModelOptions.module.css";
235
+ if (typeof document !== "undefined") {
236
+ let tag = document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]");
237
+ if (tag === null) {
238
+ tag = document.createElement("style");
239
+ tag.dataset.plugin = "@lynn123411/dsh-model-submenu";
240
+ tag.dataset.pluginCss = tagId;
241
+ document.head.appendChild(tag);
242
+ }
243
+ tag.textContent = css;
244
+ }
245
+ var ModelOptions_module_css_default = {
246
+ "check": "TcCbqa_check",
247
+ "footerDot": "TcCbqa_footerDot",
248
+ "footerHint": "TcCbqa_footerHint",
249
+ "notice": "TcCbqa_notice",
250
+ "pickerContent": "TcCbqa_pickerContent",
251
+ "pickerDialog": "TcCbqa_pickerDialog",
252
+ "pickerFooter": "TcCbqa_pickerFooter",
253
+ "pickerHost": "TcCbqa_pickerHost",
254
+ "pickerList": "TcCbqa_pickerList",
255
+ "pickerSearch": "TcCbqa_pickerSearch",
256
+ "pickerSearchInput": "TcCbqa_pickerSearchInput",
257
+ "pickerTab": "TcCbqa_pickerTab",
258
+ "pickerTabs": "TcCbqa_pickerTabs",
259
+ "retry": "TcCbqa_retry",
260
+ "row": "TcCbqa_row",
261
+ "rowBadge": "TcCbqa_rowBadge",
262
+ "rowCopy": "TcCbqa_rowCopy",
263
+ "rowMeta": "TcCbqa_rowMeta",
264
+ "rowName": "TcCbqa_rowName",
265
+ "rowPick": "TcCbqa_rowPick",
266
+ "slider": "TcCbqa_slider",
267
+ "sliderCell": "TcCbqa_sliderCell",
268
+ "sliderFill": "TcCbqa_sliderFill",
269
+ "sliderKnob": "TcCbqa_sliderKnob",
270
+ "sliderTick": "TcCbqa_sliderTick",
271
+ "sliderTrack": "TcCbqa_sliderTrack",
272
+ "star": "TcCbqa_star",
273
+ "starButton": "TcCbqa_starButton"
274
+ };
275
+ //#endregion
276
+ //#region src/client/EffortSlider.tsx
277
+ /**
278
+ * The reasoning-level slider that replaces the Host's effort drill.
279
+ *
280
+ * The Host's second menu cell is a button that drills into a list of levels.
281
+ * 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.
285
+ *
286
+ * The fill takes the level's color, so the track and the two captions read as
287
+ * one signal: "off" is neutral, the levels above it walk a blue-to-violet ramp,
288
+ * and only the top level fills with the whole gradient.
289
+ *
290
+ * "Provider default" is not a detent: it is a state, drawn as an empty track
291
+ * with a hollow knob, so it never reads as the lowest level.
292
+ */
293
+ /**
294
+ * Render the slider.
295
+ * @param props - levels, the level in force, and the commit path.
296
+ * @returns one focusable slider over the discrete levels.
297
+ */
298
+ function EffortSlider({ steps, current, defaultActive, defaultIndex, defaultLabel, busy, label, onCommit }) {
299
+ const trackRef = (0, react.useRef)(null);
300
+ const dragRef = (0, react.useRef)(null);
301
+ 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);
306
+ };
307
+ const indexAt = (clientX) => {
308
+ const track = trackRef.current;
309
+ if (track === null) return active;
310
+ const rect = track.getBoundingClientRect();
311
+ return stepAt(clientX, rect.left, rect.width, steps.length);
312
+ };
313
+ const onPointerDown = (event) => {
314
+ if (busy) return;
315
+ event.preventDefault();
316
+ event.stopPropagation();
317
+ beginDrag(indexAt(event.clientX));
318
+ try {
319
+ event.currentTarget.setPointerCapture(event.pointerId);
320
+ } catch {}
321
+ event.currentTarget.focus();
322
+ };
323
+ const onPointerMove = (event) => {
324
+ if (dragRef.current === null || busy) return;
325
+ const next = indexAt(event.clientX);
326
+ if (next !== dragRef.current) beginDrag(next);
327
+ };
328
+ const settle = (event, commit) => {
329
+ if (dragRef.current === null) return;
330
+ const next = indexAt(event.clientX);
331
+ beginDrag(null);
332
+ if (!commit) return;
333
+ if (next !== current || defaultActive) onCommit(next);
334
+ };
335
+ const onKeyDown = (event) => {
336
+ if (busy) return;
337
+ const last = steps.length - 1;
338
+ const next = event.key === "ArrowRight" || event.key === "ArrowUp" ? Math.min(active + 1, last) : event.key === "ArrowLeft" || event.key === "ArrowDown" ? Math.max(active - 1, 0) : event.key === "Home" ? 0 : event.key === "End" ? last : null;
339
+ if (next === null) return;
340
+ event.preventDefault();
341
+ event.stopPropagation();
342
+ if (next !== current || defaultActive) onCommit(next);
343
+ };
344
+ const knobFraction = offsetOf(active, steps.length);
345
+ const offset = knobOffset(knobFraction);
346
+ const idle = defaultActive && drag === null;
347
+ const filled = idle ? 0 : knobFraction;
348
+ const fill = isTopLevel(active, steps.length) ? LEVEL_GRADIENT : levelColor(active, steps.length);
349
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
350
+ ref: trackRef,
351
+ className: ModelOptions_module_css_default.slider,
352
+ role: "slider",
353
+ tabIndex: 0,
354
+ "aria-label": label,
355
+ "aria-valuemin": 0,
356
+ "aria-valuemax": Math.max(0, steps.length - 1),
357
+ "aria-valuenow": active,
358
+ "aria-valuetext": defaultActive && drag === null ? defaultLabel : steps[active]?.label,
359
+ "aria-disabled": busy || void 0,
360
+ "data-busy": busy ? "" : void 0,
361
+ "data-dragging": drag === null ? void 0 : "",
362
+ "data-default": defaultActive && drag === null ? "" : void 0,
363
+ onPointerDown,
364
+ onPointerMove,
365
+ onPointerUp: (event) => {
366
+ settle(event, true);
367
+ },
368
+ onPointerCancel: (event) => {
369
+ settle(event, false);
370
+ },
371
+ onKeyDown,
372
+ onClick: (event) => {
373
+ event.stopPropagation();
374
+ },
375
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
376
+ className: ModelOptions_module_css_default.sliderTrack,
377
+ "aria-hidden": "true",
378
+ children: [
379
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
380
+ className: ModelOptions_module_css_default.sliderFill,
381
+ style: {
382
+ width: knobOffset(filled),
383
+ background: fill
384
+ }
385
+ }),
386
+ steps.map((step, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
387
+ className: ModelOptions_module_css_default.sliderTick,
388
+ "data-on": index <= active && !idle ? "" : void 0,
389
+ style: { left: knobOffset(offsetOf(index, steps.length)) }
390
+ }, step.key)),
391
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
392
+ className: ModelOptions_module_css_default.sliderKnob,
393
+ style: { left: offset }
394
+ })
395
+ ]
396
+ })
397
+ });
398
+ }
399
+ //#endregion
400
+ //#region src/client/ModelMenuBridge.tsx
401
+ /**
402
+ * Bridge the Host's own model menu: the model cell opens the all-provider
403
+ * picker, and the effort cell carries the reasoning-level slider.
404
+ *
405
+ * The Host draws the root menu's first two cells itself: the model cell drills
406
+ * into the current provider's list, the effort cell into the levels. This plugin
407
+ * wants the first to open its dialog and the second to stop drilling, and the
408
+ * Host exposes no hook for either. This entry is mounted exactly while the root
409
+ * pane is open, so it can take the click on the menu surface before the cells'
410
+ * own handlers run and mount the slider into the effort cell.
411
+ *
412
+ * The cells are recognized as the menu surface's direct `role="menuitem"`
413
+ * children, which is the Host's root-menu shape; the options slot output sits in
414
+ * its own wrapper, so other plugins' rows never join that list. When the second
415
+ * cell is missing the model declares no reasoning levels and the row is left
416
+ * alone; when the first is missing the click falls through to the Host.
417
+ *
418
+ * Clicking the cell's label switches to the provider default, which is a state
419
+ * rather than a detent: the slider shows it as an empty track under a hollow
420
+ * knob, so it never reads as the weakest level.
421
+ */
422
+ /**
423
+ * Render the marker, redirect the model cell, and mount the effort slider.
424
+ * @param props - menu state, session, catalog face, and localized copy.
425
+ * @returns an inert marker plus the slider while the effort cell is present.
426
+ */
427
+ function ModelMenuBridge({ sessionId, picker, dismiss, reportFailure, available, directory, select, t }) {
428
+ const markerRef = (0, react.useRef)(null);
429
+ const sliderRef = (0, react.useRef)(null);
430
+ const [surface, setSurface] = (0, react.useState)(null);
431
+ const [effortRow, setEffortRow] = (0, react.useState)(null);
432
+ const [rowLabel, setRowLabel] = (0, react.useState)(null);
433
+ const state = (0, react.useSyncExternalStore)((listener) => directory.subscribe(listener), () => directory.getSnapshot());
434
+ const current = state.current;
435
+ const reasoning = liveReasoning(state);
436
+ const levels = (0, react.useMemo)(() => effortChoices(reasoning), [reasoning]);
437
+ const declared = levels.findIndex((level) => level.effort === reasoning?.defaultEffort);
438
+ const chosen = current?.reasoningEffort === void 0 ? -1 : levels.findIndex((level) => level.effort === current.reasoningEffort);
439
+ const defaultActive = current !== null && current.reasoningEffort === void 0;
440
+ const currentIndex = chosen >= 0 ? chosen : declared >= 0 ? declared : 0;
441
+ const defaultIndex = declared >= 0 ? declared : 0;
442
+ const canDefault = !defaultActive;
443
+ const busy = state.pending !== null;
444
+ const showSlider = available && levels.length >= 2 && effortRow !== null;
445
+ (0, react.useLayoutEffect)(() => {
446
+ const foundSurface = markerRef.current?.closest("[data-menu-material]");
447
+ const nextSurface = foundSurface instanceof HTMLElement ? foundSurface : null;
448
+ if (nextSurface !== surface) setSurface(nextSurface);
449
+ const cells = nextSurface === null ? [] : [...nextSurface.querySelectorAll(":scope > button[role=\"menuitem\"]")];
450
+ const nextRow = cells.length >= 2 ? cells[1] ?? null : null;
451
+ if (nextRow !== effortRow) setEffortRow(nextRow);
452
+ const nextLabel = nextRow?.querySelector("span[class*='cellLabel']")?.textContent?.trim() ?? "";
453
+ if (nextLabel !== (rowLabel ?? "")) setRowLabel(nextLabel);
454
+ });
455
+ const submit = (selection) => {
456
+ if (selection === null) return;
457
+ select(selection).then((result) => {
458
+ if (result === void 0 || result.ok) return;
459
+ reportFailure(result.error.code === "session/writer-held" ? t("error.sessionInUse") : t("error.action", { message: `${result.error.code}: ${result.error.message}` }));
460
+ });
461
+ };
462
+ const commit = (index) => {
463
+ const live = selectionOf(state);
464
+ const level = levels[index];
465
+ if (live === null || level === void 0) return;
466
+ submit(planEffortCommit(live, level.effort));
467
+ };
468
+ const commitDefault = () => {
469
+ const live = selectionOf(state);
470
+ if (live === null) return;
471
+ submit(planEffortCommit(live, void 0));
472
+ };
473
+ const latest = (0, react.useRef)({
474
+ sessionId,
475
+ picker,
476
+ dismiss,
477
+ showSlider,
478
+ canDefault,
479
+ commitDefault
480
+ });
481
+ latest.current = {
482
+ sessionId,
483
+ picker,
484
+ dismiss,
485
+ showSlider,
486
+ canDefault,
487
+ commitDefault
488
+ };
489
+ (0, react.useEffect)(() => {
490
+ if (surface === null) return;
491
+ const redirect = (event) => {
492
+ const target = event.target;
493
+ if (!(target instanceof Node)) return;
494
+ const cells = [...surface.querySelectorAll(":scope > button[role=\"menuitem\"]")];
495
+ const modelCell = cells[0];
496
+ if (modelCell !== void 0 && modelCell.contains(target)) {
497
+ event.preventDefault();
498
+ event.stopPropagation();
499
+ const request = latest.current;
500
+ request.dismiss();
501
+ request.picker.open(request.sessionId);
502
+ return;
503
+ }
504
+ const row = cells[1];
505
+ if (row === void 0 || !row.contains(target) || !latest.current.showSlider) return;
506
+ if (sliderRef.current?.contains(target) === true) return;
507
+ event.preventDefault();
508
+ event.stopPropagation();
509
+ const label = row.querySelector("span[class*='cellLabel']");
510
+ if (latest.current.canDefault && label !== null && label.contains(target)) {
511
+ latest.current.commitDefault();
512
+ return;
513
+ }
514
+ sliderRef.current?.querySelector("[role=\"slider\"]")?.focus();
515
+ };
516
+ surface.addEventListener("click", redirect, true);
517
+ return () => {
518
+ surface.removeEventListener("click", redirect, true);
519
+ };
520
+ }, [surface]);
521
+ (0, react.useLayoutEffect)(() => {
522
+ if (effortRow === null || !showSlider) return;
523
+ const label = effortRow.querySelector("span[class*='cellLabel']");
524
+ const value = effortRow.querySelector("span[class*='cellValue']");
525
+ const chevron = effortRow.querySelector("svg[class*='cellChevron']");
526
+ const saved = {
527
+ value: value?.style.display ?? "",
528
+ chevron: chevron?.style.display ?? "",
529
+ cursor: label?.style.cursor ?? "",
530
+ title: label?.getAttribute("title") ?? null
531
+ };
532
+ if (value !== null) value.style.display = "none";
533
+ if (chevron !== null) chevron.style.display = "none";
534
+ if (label !== null && canDefault) {
535
+ label.style.cursor = "pointer";
536
+ label.title = t("effort.useDefault");
537
+ }
538
+ return () => {
539
+ if (value !== null) value.style.display = saved.value;
540
+ if (chevron !== null) chevron.style.display = saved.chevron;
541
+ if (label !== null) {
542
+ label.style.cursor = saved.cursor;
543
+ if (saved.title === null) label.removeAttribute("title");
544
+ else label.title = saved.title;
545
+ }
546
+ };
547
+ }, [
548
+ effortRow,
549
+ showSlider,
550
+ canDefault,
551
+ t
552
+ ]);
553
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
554
+ ref: markerRef,
555
+ hidden: true
556
+ }), showSlider && effortRow !== null && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
557
+ ref: sliderRef,
558
+ className: ModelOptions_module_css_default.sliderCell,
559
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EffortSlider, {
560
+ steps: levels,
561
+ current: currentIndex,
562
+ defaultActive,
563
+ defaultIndex,
564
+ defaultLabel: t("effort.providerDefault"),
565
+ busy,
566
+ label: rowLabel !== null && rowLabel !== "" ? rowLabel : t("effort.label"),
567
+ onCommit: commit
568
+ })
569
+ }), effortRow)] });
570
+ }
571
+ //#endregion
11
572
  //#region src/client/icons.tsx
12
573
  /**
13
574
  * The one glyph this plugin draws itself.
@@ -38,111 +599,15 @@ window.__ModuleLoader__.load({
38
599
  });
39
600
  }
40
601
  //#endregion
41
- //#region \0dsh-css:/Users/tny/Desktop/work/my-dsh/plugins/dsh-model-submenu/src/client/ModelSubmenu.module.css.mjs
42
- const css = ".FOsK3a_root{min-width:0;display:flex;position:relative}.FOsK3a_trigger{min-width:0;max-width:min(360px,45cqw);height:28px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:24px;outline:none;align-items:center;gap:4px;padding:0 4px 0 8px;font-size:13px;font-weight:500;line-height:20px;display:flex}.FOsK3a_trigger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.FOsK3a_trigger:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-border-l3)}.FOsK3a_trigger:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}.FOsK3a_triggerLabel{text-overflow:ellipsis;white-space:nowrap;min-width:0;display:var(--dsh-composer-model-text-display,block);overflow:hidden}.FOsK3a_triggerEffort{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-state-business-primary);display:var(--dsh-composer-model-text-display,block);flex-shrink:1000;overflow:hidden}.FOsK3a_triggerIcon{display:var(--dsh-composer-model-icon-display,none);flex:none}.FOsK3a_chevron{color:var(--dsw-alias-label-caption);flex:none;transition:transform .12s}.FOsK3a_chevronOpen{transform:rotate(180deg)}.FOsK3a_menu{z-index:1100;--dsw-elevation-stroke-color:var(--dsw-alias-border-l1);box-sizing:border-box;width:min(252px,100vw - 24px);max-height:min(70vh,100vh - 96px);box-shadow:var(--dsw-elevation-prominent);color:var(--dsw-alias-label-primary);--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);border:0;flex-direction:column;padding:4px;display:flex;position:fixed;overflow:hidden}.FOsK3a_providerName{text-overflow:ellipsis;white-space:nowrap;min-height:24px;color:var(--dsw-alias-label-tertiary);align-items:center;padding:3px 8px;font-size:12px;font-weight:500;line-height:18px;display:flex;overflow:hidden}.FOsK3a_groups{min-height:0;overflow-y:auto}.FOsK3a_row{align-items:center;gap:2px;display:flex}.FOsK3a_rowPick{box-sizing:border-box;width:100%;min-width:0;min-height:34px;color:inherit;text-align:left;cursor:pointer;padding-inline:6px;background:0 0;border:none;border-radius:10px;outline:none;flex:1;align-items:center;gap:4px;padding:5px 7px;display:flex}.FOsK3a_rowPick:hover:not(:disabled),.FOsK3a_rowPick:focus-visible{background:var(--dsw-alias-interactive-bg-hover)}.FOsK3a_rowPick:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}.FOsK3a_rowCopy{flex-direction:column;flex:1;min-width:0;display:flex}.FOsK3a_rowName{color:inherit;text-overflow:ellipsis;white-space:nowrap;font-size:11.5px;font-weight:400;line-height:17px;overflow:hidden}.FOsK3a_menu .FOsK3a_rowName{font-weight:600}.FOsK3a_rowMeta{color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;font-size:12px;line-height:18px;overflow:hidden}.FOsK3a_check{color:var(--dsw-alias-label-primary);flex:0 0 15px;place-items:center;display:grid}.FOsK3a_starStatic{color:var(--dsw-alias-state-warn-primary);flex:none;place-items:center;width:14px;display:grid}.FOsK3a_starButton{color:var(--dsw-alias-label-tertiary);cursor:pointer;opacity:0;background:0 0;border:none;border-radius:8px;outline:none;flex:none;place-items:center;width:26px;height:26px;display:grid}.FOsK3a_starButton:focus-visible{outline:2px solid var(--dsw-alias-border-l3)}.FOsK3a_row:hover .FOsK3a_starButton,.FOsK3a_row:focus-within .FOsK3a_starButton{opacity:1}.FOsK3a_starButton[aria-pressed=true]{color:var(--dsw-alias-state-warn-primary);opacity:1}@media (hover:none){.FOsK3a_starButton{opacity:1}}.FOsK3a_star{flex:none}.FOsK3a_divider{background:var(--dsw-alias-border-l2);flex:none;height:.5px;margin:3px 2px}.FOsK3a_effortRow{align-items:center;gap:6px;padding:3px 6px;display:flex}.FOsK3a_effortHeading{color:var(--dsw-alias-label-primary);flex:none;font-size:12px;line-height:18px}.FOsK3a_effortTrack{background:var(--dsw-alias-interactive-bg-hover);scrollbar-width:none;border-radius:999px;align-items:center;gap:1px;min-width:0;margin-left:auto;padding:2px;display:flex;overflow-x:auto}.FOsK3a_effortTrack::-webkit-scrollbar{display:none}.FOsK3a_effortSegment{color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:999px;outline:none;flex:none;padding:2px 7px;font-size:11px;line-height:16px}.FOsK3a_effortSegment:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.FOsK3a_effortSegment[aria-checked=true]{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1);box-shadow:var(--dsw-elevation-soft);font-weight:600}.FOsK3a_effortSegment:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}.FOsK3a_effortSegment:focus-visible{outline:2px solid var(--dsw-alias-border-l3);outline-offset:-2px}.FOsK3a_hint{color:var(--dsw-alias-label-tertiary);margin:0;padding:6px 8px;font-size:11px;line-height:16px}.FOsK3a_moreRow{color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:10px;outline:none;align-items:center;gap:6px;min-height:32px;padding:5px 8px;font-size:12px;line-height:18px;display:flex}.FOsK3a_moreRow:hover,.FOsK3a_moreRow:focus-visible{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.FOsK3a_moreIcon{color:var(--dsw-alias-label-tertiary);flex:none}.FOsK3a_notice{color:var(--dsw-alias-label-tertiary);justify-content:space-between;align-items:flex-start;gap:8px;padding:6px 10px;font-size:12px;line-height:18px;display:flex}.FOsK3a_retry{color:inherit;font:inherit;cursor:pointer;background:0 0;border:none;flex:none;padding:0;font-weight:600;text-decoration:underline}.FOsK3a_pickerDialog{gap:0;width:min(680px,100%);height:min(640px,80vh);padding:0}.FOsK3a_pickerContent,.FOsK3a_pickerContent>:last-child{flex-direction:column;flex:1;min-height:0;display:flex}.FOsK3a_pickerSearch{padding:0 20px 12px}.FOsK3a_pickerSearchInput{width:100%}.FOsK3a_pickerTabs{border-bottom:.5px solid var(--dsw-alias-border-l2);overscroll-behavior-x:contain;scrollbar-width:none;flex-wrap:nowrap;gap:2px;padding:0 16px;display:flex;overflow-x:auto}.FOsK3a_pickerTabs::-webkit-scrollbar{display:none}.FOsK3a_pickerTab{color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;background:0 0;border:none;border-radius:8px;outline:none;flex:none;padding:6px 10px;font-size:13px;line-height:18px}.FOsK3a_pickerTab:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.FOsK3a_pickerTab[aria-selected=true]{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover);font-weight:600}.FOsK3a_pickerTab:focus-visible{outline:2px solid var(--dsw-alias-border-l3)}.FOsK3a_pickerList{flex:1;min-height:0;padding:8px 12px;overflow-y:auto}.FOsK3a_pickerFooter{border-top:.5px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary);align-items:center;gap:6px;padding:10px 20px;font-size:12px;line-height:18px;display:flex}.FOsK3a_footerHint{flex-wrap:wrap;align-items:center;gap:6px;display:flex}.FOsK3a_footerDot{color:var(--dsw-alias-label-dimmed)}";
43
- const tagId = "@lynn123411/dsh-model-submenu/ModelSubmenu.module.css";
44
- if (typeof document !== "undefined") {
45
- let tag = document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]");
46
- if (tag === null) {
47
- tag = document.createElement("style");
48
- tag.dataset.plugin = "@lynn123411/dsh-model-submenu";
49
- tag.dataset.pluginCss = tagId;
50
- document.head.appendChild(tag);
51
- }
52
- tag.textContent = css;
53
- }
54
- var ModelSubmenu_module_css_default = {
55
- "check": "FOsK3a_check",
56
- "chevron": "FOsK3a_chevron",
57
- "chevronOpen": "FOsK3a_chevronOpen",
58
- "divider": "FOsK3a_divider",
59
- "effortHeading": "FOsK3a_effortHeading",
60
- "effortRow": "FOsK3a_effortRow",
61
- "effortSegment": "FOsK3a_effortSegment",
62
- "effortTrack": "FOsK3a_effortTrack",
63
- "footerDot": "FOsK3a_footerDot",
64
- "footerHint": "FOsK3a_footerHint",
65
- "groups": "FOsK3a_groups",
66
- "hint": "FOsK3a_hint",
67
- "menu": "FOsK3a_menu",
68
- "moreIcon": "FOsK3a_moreIcon",
69
- "moreRow": "FOsK3a_moreRow",
70
- "notice": "FOsK3a_notice",
71
- "pickerContent": "FOsK3a_pickerContent",
72
- "pickerDialog": "FOsK3a_pickerDialog",
73
- "pickerFooter": "FOsK3a_pickerFooter",
74
- "pickerList": "FOsK3a_pickerList",
75
- "pickerSearch": "FOsK3a_pickerSearch",
76
- "pickerSearchInput": "FOsK3a_pickerSearchInput",
77
- "pickerTab": "FOsK3a_pickerTab",
78
- "pickerTabs": "FOsK3a_pickerTabs",
79
- "providerName": "FOsK3a_providerName",
80
- "retry": "FOsK3a_retry",
81
- "root": "FOsK3a_root",
82
- "row": "FOsK3a_row",
83
- "rowCopy": "FOsK3a_rowCopy",
84
- "rowMeta": "FOsK3a_rowMeta",
85
- "rowName": "FOsK3a_rowName",
86
- "rowPick": "FOsK3a_rowPick",
87
- "star": "FOsK3a_star",
88
- "starButton": "FOsK3a_starButton",
89
- "starStatic": "FOsK3a_starStatic",
90
- "trigger": "FOsK3a_trigger",
91
- "triggerEffort": "FOsK3a_triggerEffort",
92
- "triggerIcon": "FOsK3a_triggerIcon",
93
- "triggerLabel": "FOsK3a_triggerLabel"
94
- };
95
- //#endregion
96
602
  //#region src/client/ModelRow.tsx
97
603
  /**
98
604
  * Render one model row.
99
605
  * @param props - the row's content and behaviour.
100
606
  * @returns the row element.
101
607
  */
102
- function ModelRow({ name, selected, busy, star, favorited, meta, starLabel, starTabStop = false, rowTabIndex, onPick, onFocusRow, onToggleFavorite, children }) {
103
- const copy = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
104
- className: ModelSubmenu_module_css_default.rowCopy,
105
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
106
- className: ModelSubmenu_module_css_default.rowName,
107
- children: name
108
- }), meta !== void 0 && meta !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
109
- className: ModelSubmenu_module_css_default.rowMeta,
110
- children: meta
111
- })]
112
- }) });
113
- const mark = favorited ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StarIcon, {
114
- filled: true,
115
- className: ModelSubmenu_module_css_default.star
116
- }) : null;
117
- if (star === "static") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
118
- type: "button",
119
- role: "menuitemradio",
120
- "aria-checked": selected,
121
- tabIndex: rowTabIndex,
122
- className: ModelSubmenu_module_css_default.rowPick,
123
- title: name,
124
- disabled: busy,
125
- onClick: onPick,
126
- onFocus: onFocusRow,
127
- "data-selected": selected ? "" : void 0,
128
- "data-favorited": favorited ? "" : void 0,
129
- children: [
130
- copy,
131
- mark !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
132
- className: ModelSubmenu_module_css_default.starStatic,
133
- "aria-hidden": "true",
134
- children: mark
135
- }),
136
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
137
- className: ModelSubmenu_module_css_default.check,
138
- "aria-hidden": !selected,
139
- children: selected ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, {}) : null
140
- }),
141
- children
142
- ]
143
- });
608
+ function ModelRow({ name, badge, selected, busy, favorited, meta, starLabel, starTabStop = false, rowTabIndex, onPick, onFocusRow, onToggleFavorite, children }) {
144
609
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
145
- className: ModelSubmenu_module_css_default.row,
610
+ className: ModelOptions_module_css_default.row,
146
611
  "data-selected": selected ? "" : void 0,
147
612
  "data-favorited": favorited ? "" : void 0,
148
613
  children: [
@@ -151,16 +616,29 @@ window.__ModuleLoader__.load({
151
616
  role: "menuitemradio",
152
617
  "aria-checked": selected,
153
618
  tabIndex: rowTabIndex,
154
- className: ModelSubmenu_module_css_default.rowPick,
619
+ className: ModelOptions_module_css_default.rowPick,
155
620
  title: name,
156
621
  disabled: busy,
157
622
  onClick: onPick,
158
623
  onFocus: onFocusRow,
159
- children: copy
624
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
625
+ className: ModelOptions_module_css_default.rowCopy,
626
+ children: [
627
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
628
+ className: ModelOptions_module_css_default.rowName,
629
+ children: name
630
+ }),
631
+ badge,
632
+ meta !== void 0 && meta !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
633
+ className: ModelOptions_module_css_default.rowMeta,
634
+ children: meta
635
+ })
636
+ ]
637
+ })
160
638
  }),
161
639
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
162
640
  type: "button",
163
- className: ModelSubmenu_module_css_default.starButton,
641
+ className: ModelOptions_module_css_default.starButton,
164
642
  "aria-pressed": favorited,
165
643
  "aria-label": starLabel,
166
644
  tabIndex: starTabStop ? 0 : -1,
@@ -168,11 +646,11 @@ window.__ModuleLoader__.load({
168
646
  onClick: onToggleFavorite,
169
647
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StarIcon, {
170
648
  filled: favorited,
171
- className: ModelSubmenu_module_css_default.star
649
+ className: ModelOptions_module_css_default.star
172
650
  })
173
651
  }),
174
652
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
175
- className: ModelSubmenu_module_css_default.check,
653
+ className: ModelOptions_module_css_default.check,
176
654
  "aria-hidden": !selected,
177
655
  children: selected ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, {}) : null
178
656
  }),
@@ -441,75 +919,6 @@ window.__ModuleLoader__.load({
441
919
  return rank(searchCandidates(rows), trimmed).map((match) => match.row);
442
920
  }
443
921
  //#endregion
444
- //#region src/client/selection.ts
445
- /**
446
- * Build the segments for one model's reasoning row.
447
- * @param reasoning - the catalog's reasoning metadata, undefined when the model has none.
448
- * @param providerDefaultLabel - the localized label for the "follow the provider" segment.
449
- * @returns the segments in catalog order; the provider-default one leads when the model declares no default.
450
- */
451
- function effortChoices(reasoning, providerDefaultLabel) {
452
- if (reasoning === void 0) return [];
453
- return [...reasoning.defaultEffort === void 0 ? [{
454
- key: "provider-default",
455
- effort: void 0,
456
- label: providerDefaultLabel
457
- }] : [], ...reasoning.efforts.map((level) => ({
458
- key: level.id,
459
- effort: level.id,
460
- label: level.name
461
- }))];
462
- }
463
- /**
464
- * The selection a level click submits.
465
- * @param live - the selection in force right now.
466
- * @param effort - the clicked level, undefined meaning the provider default.
467
- * @returns the selection to submit, or null when the level already is the live one.
468
- */
469
- function planEffortCommit(live, effort) {
470
- if (sameEffort(live.reasoningEffort, effort)) return null;
471
- return {
472
- provider: live.provider,
473
- model: live.model,
474
- ...effort === void 0 ? {} : { reasoningEffort: effort }
475
- };
476
- }
477
- /**
478
- * The selection a model click submits.
479
- * @param live - the selection in force right now, null before a first selection resolves.
480
- * @param provider - the clicked model's provider.
481
- * @param model - the clicked model id.
482
- * @param reasoningOf - the catalog's reasoning metadata for one provider and model.
483
- * @param remembered - the level this browser last used for the target model, if any.
484
- * @returns the selection to submit, or null when the click changes nothing.
485
- */
486
- function planModelCommit(live, provider, model, reasoningOf, remembered) {
487
- if (live !== null && sameModel(live, provider, model)) return null;
488
- const offered = reasoningOf(provider, model);
489
- return {
490
- provider,
491
- model,
492
- ...offered !== void 0 && remembered !== void 0 && offered.efforts.some((level) => level.id === remembered) ? { reasoningEffort: remembered } : {}
493
- };
494
- }
495
- /**
496
- * @param live - one selection.
497
- * @param provider - the provider to compare.
498
- * @param model - the model id to compare.
499
- * @returns whether both name the same model.
500
- */
501
- function sameModel(live, provider, model) {
502
- return live.provider === provider && live.model === model;
503
- }
504
- /**
505
- * @param liveEffort - the level in force, undefined meaning the provider default.
506
- * @param effort - the level in question.
507
- * @returns whether they name the same level.
508
- */
509
- function sameEffort(liveEffort, effort) {
510
- return liveEffort === effort;
511
- }
512
- //#endregion
513
922
  //#region src/client/ModelPicker.tsx
514
923
  /**
515
924
  * The model picker: the centered dialog opened from the menu's last row.
@@ -525,7 +934,7 @@ window.__ModuleLoader__.load({
525
934
  * @param props - the catalog, the live selection, and the picker's verbs.
526
935
  * @returns the dialog; nothing while closed.
527
936
  */
528
- function ModelPicker({ open, state, live, facts, favorites, busy, onPick, onToggleFavorite, onReload, onClose, t }) {
937
+ function ModelPicker({ open, resolveText, state, live, facts, favorites, busy, rememberedOf, onPick, onToggleFavorite, onReload, onClose, t }) {
529
938
  const id = (0, react.useId)();
530
939
  const searchRef = (0, react.useRef)(null);
531
940
  const listRef = (0, react.useRef)(null);
@@ -552,7 +961,7 @@ window.__ModuleLoader__.load({
552
961
  setHighlight(0);
553
962
  if (listRef.current !== null) listRef.current.scrollTop = 0;
554
963
  const frame = requestAnimationFrame(() => {
555
- searchRef.current?.focus();
964
+ if (!(0, _deepseek_ai_dsh_client_ui_primitives.isMobileDevice)()) searchRef.current?.focus();
556
965
  });
557
966
  return () => {
558
967
  cancelAnimationFrame(frame);
@@ -582,7 +991,7 @@ window.__ModuleLoader__.load({
582
991
  }
583
992
  };
584
993
  const pick = (row) => {
585
- const selection = planModelCommit(live, row.provider, row.model, reason);
994
+ const selection = planModelCommit(live, row.provider, row.model, reason, rememberedOf?.(row.provider, row.model));
586
995
  if (selection === null) {
587
996
  onClose();
588
997
  return;
@@ -617,14 +1026,14 @@ window.__ModuleLoader__.load({
617
1026
  onClose,
618
1027
  title: t("picker.title"),
619
1028
  closeLabel: t("picker.close"),
620
- className: ModelSubmenu_module_css_default.pickerDialog ?? "",
621
- contentClassName: ModelSubmenu_module_css_default.pickerContent ?? "",
1029
+ className: ModelOptions_module_css_default.pickerDialog ?? "",
1030
+ contentClassName: ModelOptions_module_css_default.pickerContent ?? "",
622
1031
  children: [
623
1032
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
624
- className: ModelSubmenu_module_css_default.pickerSearch,
1033
+ className: ModelOptions_module_css_default.pickerSearch,
625
1034
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
626
1035
  ref: searchRef,
627
- className: ModelSubmenu_module_css_default.pickerSearchInput ?? "",
1036
+ className: ModelOptions_module_css_default.pickerSearchInput ?? "",
628
1037
  type: "text",
629
1038
  role: "searchbox",
630
1039
  "aria-label": t("picker.search"),
@@ -642,7 +1051,7 @@ window.__ModuleLoader__.load({
642
1051
  })
643
1052
  }),
644
1053
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
645
- className: ModelSubmenu_module_css_default.pickerTabs,
1054
+ className: ModelOptions_module_css_default.pickerTabs,
646
1055
  role: "tablist",
647
1056
  "aria-label": t("picker.title"),
648
1057
  children: tabs.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
@@ -656,7 +1065,7 @@ window.__ModuleLoader__.load({
656
1065
  "aria-selected": entry.id === tab,
657
1066
  "aria-controls": `${id}-list`,
658
1067
  tabIndex: 0,
659
- className: ModelSubmenu_module_css_default.pickerTab,
1068
+ className: ModelOptions_module_css_default.pickerTab,
660
1069
  onClick: () => {
661
1070
  setTab(entry.id);
662
1071
  setHighlight(0);
@@ -674,37 +1083,37 @@ window.__ModuleLoader__.load({
674
1083
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
675
1084
  ref: listRef,
676
1085
  id: `${id}-list`,
677
- className: ModelSubmenu_module_css_default.pickerList,
1086
+ className: ModelOptions_module_css_default.pickerList,
678
1087
  role: "menu",
679
1088
  "aria-label": t("picker.title"),
680
1089
  children: [
681
1090
  loading && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
682
- className: ModelSubmenu_module_css_default.notice,
1091
+ className: ModelOptions_module_css_default.notice,
683
1092
  children: t("status.loading")
684
1093
  }),
685
1094
  failed && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
686
- className: ModelSubmenu_module_css_default.notice,
1095
+ className: ModelOptions_module_css_default.notice,
687
1096
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("status.failed", { message: state.error ?? "" }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
688
1097
  type: "button",
689
- className: ModelSubmenu_module_css_default.retry,
1098
+ className: ModelOptions_module_css_default.retry,
690
1099
  onClick: onReload,
691
1100
  children: t("action.reload")
692
1101
  })]
693
1102
  }),
694
1103
  failures.map((failure) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
695
- className: ModelSubmenu_module_css_default.notice,
1104
+ className: ModelOptions_module_css_default.notice,
696
1105
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("status.providerFailed", {
697
1106
  name: failure.name,
698
1107
  message: failure.message
699
1108
  }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
700
1109
  type: "button",
701
- className: ModelSubmenu_module_css_default.retry,
1110
+ className: ModelOptions_module_css_default.retry,
702
1111
  onClick: onReload,
703
1112
  children: t("action.reload")
704
1113
  })]
705
1114
  }, failure.id)),
706
1115
  visible.length === 0 && !loading && !failed && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
707
- className: ModelSubmenu_module_css_default.notice,
1116
+ className: ModelOptions_module_css_default.notice,
708
1117
  children: state.groups.length === 0 ? t("status.empty") : t("picker.searchEmpty")
709
1118
  }),
710
1119
  visible.map((row, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -712,9 +1121,13 @@ window.__ModuleLoader__.load({
712
1121
  id: `${id}-row-${index}`,
713
1122
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelRow, {
714
1123
  name: row.entry.name,
1124
+ badge: row.entry.badge === void 0 ? void 0 : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
1125
+ tone: row.entry.badge.tone,
1126
+ className: ModelOptions_module_css_default.rowBadge,
1127
+ children: resolveText(row.entry.badge.text)
1128
+ }),
715
1129
  selected: live?.provider === row.provider && live.model === row.model,
716
1130
  busy,
717
- star: "interactive",
718
1131
  favorited: isFavorite(favorites, row.provider, row.model),
719
1132
  meta: metaLine({
720
1133
  row,
@@ -741,20 +1154,20 @@ window.__ModuleLoader__.load({
741
1154
  ]
742
1155
  }),
743
1156
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
744
- className: ModelSubmenu_module_css_default.pickerFooter,
1157
+ className: ModelOptions_module_css_default.pickerFooter,
745
1158
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
746
- className: ModelSubmenu_module_css_default.footerHint,
1159
+ className: ModelOptions_module_css_default.footerHint,
747
1160
  children: [
748
1161
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.ShortcutKeys, { keys: ["↑", "↓"] }),
749
1162
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("picker.hintNavigate") }),
750
1163
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
751
- className: ModelSubmenu_module_css_default.footerDot,
1164
+ className: ModelOptions_module_css_default.footerDot,
752
1165
  children: "·"
753
1166
  }),
754
1167
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.ShortcutKeys, { keys: ["Enter"] }),
755
1168
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("picker.hintSelect") }),
756
1169
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
757
- className: ModelSubmenu_module_css_default.footerDot,
1170
+ className: ModelOptions_module_css_default.footerDot,
758
1171
  children: "·"
759
1172
  }),
760
1173
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.ShortcutKeys, { keys: ["Esc"] }),
@@ -832,55 +1245,46 @@ window.__ModuleLoader__.load({
832
1245
  }
833
1246
  }
834
1247
  //#endregion
835
- //#region src/client/ModelSubmenu.tsx
836
- /** Unplaced portal card: hidden but laid out at a fixed origin so offsetWidth/offsetHeight are real. */
837
- const MEASURE_STYLE = {
838
- visibility: "hidden",
839
- left: 0,
840
- top: 0
841
- };
842
- const cls = (...parts) => parts.filter((part) => part !== false && part !== void 0).join(" ");
1248
+ //#region src/client/ModelPickerHost.tsx
843
1249
  /**
844
- * The composer's model seat (`conversation.input.model`), taken over from the
845
- * built-in occupant with a provider-scoped menu: one provider's models above,
846
- * the reasoning row as segments, the cache warning, and the door to the picker
847
- * that lists every provider.
1250
+ * The composer overlay's picker view: the dialog, its catalog, and its verbs.
848
1251
  *
849
- * Every click commits at once: a model, a reasoning level, or the picker door.
850
- * Closing the menu never writes anything, and the picker reads the level the
851
- * Host already accepted, so its "current" reading is never stale.
1252
+ * It lives in `conversation.input.overlay` rather than beside the row, because
1253
+ * the Host unmounts the model menu as soon as a row is clicked. Everything the
1254
+ * dialog reads arrives through this entry's injected face.
852
1255
  */
853
- function ModelSubmenu({ locked, available, directory, load, select, t, factsSource }) {
854
- const state = (0, react.useSyncExternalStore)((fn) => directory.subscribe(fn), () => directory.getSnapshot());
855
- const [open, setOpen] = (0, react.useState)(false);
856
- const [pickerOpen, setPickerOpen] = (0, react.useState)(false);
1256
+ /**
1257
+ * Render the picker for this session.
1258
+ * @param props - session, store, catalog face, and localized copy.
1259
+ * @returns the dialog while the store names this session; otherwise nothing.
1260
+ */
1261
+ function ModelPickerHost({ sessionId, picker, available, directory, load, select, resolveText, factsSource, t }) {
1262
+ const request = (0, react.useSyncExternalStore)(picker.subscribe, picker.getSnapshot);
1263
+ const state = (0, react.useSyncExternalStore)((listener) => directory.subscribe(listener), () => directory.getSnapshot());
857
1264
  const [favorites, setFavorites] = (0, react.useState)(() => readFavorites(storageOf()));
858
1265
  const [memory, setMemory] = (0, react.useState)(() => readEfforts(storageOf()));
859
1266
  const [toast, setToast] = (0, react.useState)(null);
860
1267
  const toastSeq = (0, react.useRef)(0);
861
1268
  const rootRef = (0, react.useRef)(null);
862
- const triggerRef = (0, react.useRef)(null);
863
- const menuRef = (0, react.useRef)(null);
864
- const [menuPos, setMenuPos] = (0, react.useState)(null);
865
- const id = (0, react.useId)();
866
- const groups = state.groups;
867
- const current = state.current;
868
- const currentGroup = current === null ? void 0 : groups.find((group) => group.id === current.provider);
869
- const currentModel = currentGroup?.models.find((model) => model.id === current?.model);
870
- const reasoning = currentModel?.reasoning;
871
- const effectiveEffort = current?.reasoningEffort ?? reasoning?.defaultEffort;
872
- /**
873
- * The trigger's caption. A model that advertises no levels still says so in
874
- * the same place: the blue caption beside the name reads 默认 rather than
875
- * disappearing, so the shape of the control never changes with the model.
876
- */
877
- const effortLabel = reasoning === void 0 || effectiveEffort === void 0 ? t("effort.providerDefault") : reasoning.efforts.find((level) => level.id === effectiveEffort)?.name ?? effectiveEffort;
878
- const choices = (0, react.useMemo)(() => effortChoices(reasoning, t("effort.providerDefault")), [reasoning, t]);
879
1269
  const busy = state.status === "selecting";
880
1270
  const live = (0, react.useMemo)(() => selectionOf(state), [state]);
881
- const reason = (0, react.useMemo)(() => reasoningOf(groups), [groups]);
882
- const facts = (0, react.useSyncExternalStore)((fn) => factsSource.subscribe(fn), () => factsSource.getSnapshot());
883
- const menuRows = (0, react.useMemo)(() => currentGroup === void 0 ? [] : orderWithFavorites(currentGroup.models, (model) => isFavorite(favorites, currentGroup.id, model.id)), [currentGroup, favorites]);
1271
+ const facts = (0, react.useSyncExternalStore)((listener) => factsSource.subscribe(listener), () => factsSource.getSnapshot());
1272
+ const open = request.sessionId === sessionId;
1273
+ const loadRef = (0, react.useRef)(load);
1274
+ loadRef.current = load;
1275
+ (0, react.useEffect)(() => {
1276
+ if (open) loadRef.current();
1277
+ }, [open]);
1278
+ const current = state.current;
1279
+ const rememberedRef = (0, react.useRef)(null);
1280
+ (0, react.useEffect)(() => {
1281
+ if (current === null) return;
1282
+ const key = `${current.provider}/${current.model}/${current.reasoningEffort ?? ""}`;
1283
+ if (rememberedRef.current === key) return;
1284
+ rememberedRef.current = key;
1285
+ setMemory(rememberEffort(storageOf(), current.provider, current.model, current.reasoningEffort));
1286
+ }, [current]);
1287
+ if (!available) return null;
884
1288
  const raiseToast = (text) => {
885
1289
  toastSeq.current += 1;
886
1290
  setToast({
@@ -888,367 +1292,47 @@ window.__ModuleLoader__.load({
888
1292
  text
889
1293
  });
890
1294
  };
891
- const reload = () => {
892
- load();
893
- };
894
- const submit = (selection, onDone, onRejected) => {
1295
+ const submit = (selection) => {
895
1296
  select(selection).then((result) => {
896
1297
  if (result === void 0 || result.ok) {
897
- remember(selection.provider, selection.model, selection.reasoningEffort);
898
- onDone?.();
1298
+ setMemory(rememberEffort(storageOf(), selection.provider, selection.model, selection.reasoningEffort));
1299
+ picker.close();
899
1300
  return;
900
1301
  }
901
- onRejected?.();
902
1302
  raiseToast(result.error.code === "session/writer-held" ? t("error.sessionInUse") : t("error.action", { message: `${result.error.code}: ${result.error.message}` }));
903
1303
  });
904
1304
  };
905
- /** Close the menu. Closing writes nothing: every choice already committed on click. */
906
- const closeMenu = (0, react.useCallback)((restoreFocus) => {
907
- setOpen(false);
908
- if (restoreFocus) queueMicrotask(() => {
909
- triggerRef.current?.focus();
910
- });
911
- }, []);
912
- (0, react.useEffect)(() => {
913
- if (!open) return;
914
- const closeOutside = (event) => {
915
- if (rootRef.current?.contains(event.target) === true) return;
916
- if (menuRef.current?.contains(event.target) === true) return;
917
- closeMenu(false);
918
- };
919
- document.addEventListener("mousedown", closeOutside);
920
- return () => {
921
- document.removeEventListener("mousedown", closeOutside);
922
- };
923
- }, [open, closeMenu]);
924
- (0, react.useEffect)(() => {
925
- if (!open) return;
926
- const frame = requestAnimationFrame(() => {
927
- const checked = menuRef.current?.querySelector("[role=\"menuitemradio\"][aria-checked=\"true\"]:not([disabled])");
928
- const first = menuRef.current?.querySelector("[role=\"menuitemradio\"]:not([disabled])");
929
- const target = checked ?? first ?? triggerRef.current;
930
- target?.scrollIntoView({ block: "nearest" });
931
- target?.focus();
932
- });
933
- return () => {
934
- cancelAnimationFrame(frame);
935
- };
936
- }, [open]);
937
- (0, react.useLayoutEffect)(() => {
938
- if (!open) {
939
- setMenuPos(null);
940
- return;
941
- }
942
- const place = () => {
943
- const rect = triggerRef.current?.getBoundingClientRect();
944
- if (rect === void 0) return;
945
- const MARGIN = 12;
946
- const lw = menuRef.current?.offsetWidth ?? 0;
947
- const lh = menuRef.current?.offsetHeight ?? 0;
948
- let x = rect.right - lw;
949
- let y = rect.top - 8 - lh;
950
- if (lw > 0) x = Math.min(Math.max(x, MARGIN), window.innerWidth - lw - MARGIN);
951
- if (lh > 0) y = Math.min(Math.max(y, MARGIN), window.innerHeight - lh - MARGIN);
952
- setMenuPos({
953
- left: x,
954
- top: y
955
- });
956
- };
957
- place();
958
- window.addEventListener("scroll", place, true);
959
- window.addEventListener("resize", place);
960
- return () => {
961
- window.removeEventListener("scroll", place, true);
962
- window.removeEventListener("resize", place);
963
- };
964
- }, [open, state]);
965
- if (!available) return null;
966
- const show = () => {
967
- setOpen(true);
968
- reload();
969
- };
970
- /** The picker door: the level already landed on click, so only the surface changes. */
971
- const openPicker = () => {
972
- setOpen(false);
973
- setPickerOpen(true);
974
- };
975
- /** Record one model's level, so switching away and back restores it. */
976
- const remember = (provider, model, effort) => {
977
- setMemory(rememberEffort(storageOf(), provider, model, effort));
978
- };
979
- const chooseModel = (provider, model) => {
980
- const picked = planModelCommit(live, provider, model.id, reason, effortFor(memory, provider, model.id));
981
- if (picked === null) {
982
- setOpen(false);
983
- queueMicrotask(() => {
984
- triggerRef.current?.focus();
985
- });
986
- return;
987
- }
988
- setOpen(false);
989
- submit(picked, () => {
990
- queueMicrotask(() => {
991
- triggerRef.current?.focus();
992
- });
993
- }, () => {
994
- setOpen(true);
995
- });
996
- };
997
- const rowButtons = () => [...menuRef.current?.querySelectorAll("[role=\"menuitemradio\"]:not([disabled])") ?? []];
998
- const moveFocus = (offset) => {
999
- const items = rowButtons();
1000
- if (items.length === 0) return;
1001
- const active = items.findIndex((item) => item === document.activeElement);
1002
- items[active === -1 ? offset > 0 ? 0 : items.length - 1 : (active + offset + items.length) % items.length]?.focus();
1003
- };
1004
- const onEffortKeyDown = (event) => {
1005
- if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
1006
- if (choices.length === 0 || live === null) return;
1007
- event.preventDefault();
1008
- const index = choices.findIndex((choice) => choice.effort === effectiveEffort);
1009
- const step = event.key === "ArrowRight" ? 1 : -1;
1010
- const next = choices[((index === -1 ? 0 : index) + step + choices.length) % choices.length];
1011
- if (next === void 0) return;
1012
- const pending = planEffortCommit(live, next.effort);
1013
- if (pending !== null) submit(pending, null);
1014
- };
1015
- const onRootKeyDown = (event) => {
1016
- if (event.key === "Escape" && open) {
1017
- event.preventDefault();
1018
- closeMenu(true);
1019
- return;
1020
- }
1021
- if (!open) return;
1022
- if (event.key === "Tab") {
1023
- if (event.shiftKey) {
1024
- event.preventDefault();
1025
- closeMenu(true);
1026
- return;
1027
- }
1028
- const focused = document.activeElement;
1029
- const rows = rowButtons();
1030
- if (focused instanceof HTMLButtonElement && rows.includes(focused)) {
1031
- event.preventDefault();
1032
- focused.click();
1033
- return;
1034
- }
1035
- if (focused !== triggerRef.current) return;
1036
- event.preventDefault();
1037
- (menuRef.current?.querySelector("[role=\"menuitemradio\"][aria-checked=\"true\"]:not([disabled])") ?? rows[0])?.focus();
1038
- return;
1039
- }
1040
- if (event.key === "ArrowDown" || event.key === "ArrowUp") {
1041
- event.preventDefault();
1042
- moveFocus(event.key === "ArrowDown" ? 1 : -1);
1043
- }
1044
- };
1045
- /**
1046
- * The keyboard leaving the seat closes the menu — but that is not one of the
1047
- * four closing gestures that write a mark, so it closes without committing.
1048
- * Otherwise clicking the notice text or a divider inside the menu, which moves
1049
- * focus without unmounting it, would land a level nobody asked to commit.
1050
- */
1051
- const onBlur = (event) => {
1052
- if (pickerOpen) return;
1053
- if (event.relatedTarget instanceof Node && (rootRef.current?.contains(event.relatedTarget) === true || menuRef.current?.contains(event.relatedTarget) === true)) return;
1054
- setOpen(false);
1055
- };
1056
- const toggleFavoriteOf = (provider, model) => {
1057
- setFavorites(toggleFavorite(storageOf(), provider, model));
1058
- };
1059
- const waiting = current === null && state.status === "loading";
1060
- const modelLabel = waiting ? t("trigger.loading") : currentModel?.name ?? (current === null ? t("trigger.fallback") : `${current.provider}/${current.model}`);
1061
- const triggerAria = waiting ? t("trigger.loading") : current === null ? t("trigger.selectAria") : effortLabel === void 0 ? t("trigger.aria", { model: modelLabel }) : t("trigger.ariaEffort", {
1062
- model: modelLabel,
1063
- effort: effortLabel
1064
- });
1065
- const providerName = currentGroup?.name ?? state.failures.find((failure) => failure.id === current?.provider)?.name ?? current?.provider ?? "";
1066
- const providerFailure = current === null ? void 0 : state.failures.find((failure) => failure.id === current.provider);
1067
1305
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1068
1306
  ref: rootRef,
1069
- className: ModelSubmenu_module_css_default.root,
1070
- onKeyDown: onRootKeyDown,
1071
- onBlur,
1072
- children: [
1073
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1074
- ref: triggerRef,
1075
- type: "button",
1076
- className: ModelSubmenu_module_css_default.trigger,
1077
- "aria-label": triggerAria,
1078
- "aria-haspopup": "menu",
1079
- "aria-expanded": open,
1080
- "aria-controls": open ? `${id}-menu` : void 0,
1081
- title: effortLabel === void 0 ? modelLabel : `${modelLabel} · ${effortLabel}`,
1082
- disabled: locked,
1083
- onClick: () => {
1084
- if (open) closeMenu(false);
1085
- else show();
1086
- },
1087
- children: [
1088
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFlatListOutlineRegular, {
1089
- className: ModelSubmenu_module_css_default.triggerIcon,
1090
- size: 16
1091
- }),
1092
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1093
- className: ModelSubmenu_module_css_default.triggerLabel,
1094
- children: modelLabel
1095
- }),
1096
- effortLabel !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1097
- className: ModelSubmenu_module_css_default.triggerEffort,
1098
- children: effortLabel
1099
- }),
1100
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: cls(ModelSubmenu_module_css_default.chevron, open && ModelSubmenu_module_css_default.chevronOpen) })
1101
- ]
1102
- }),
1103
- open && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.MenuSurface, {
1104
- ref: menuRef,
1105
- id: `${id}-menu`,
1106
- className: ModelSubmenu_module_css_default.menu,
1107
- style: menuPos ?? MEASURE_STYLE,
1108
- role: "group",
1109
- "aria-label": t("menu.aria"),
1110
- "aria-busy": state.status === "loading" || busy,
1111
- children: [
1112
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1113
- className: ModelSubmenu_module_css_default.providerName,
1114
- title: providerName,
1115
- children: providerName
1116
- }),
1117
- state.status === "loading" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1118
- className: ModelSubmenu_module_css_default.notice,
1119
- children: t("status.loading")
1120
- }),
1121
- state.error !== null && state.status === "error" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1122
- className: ModelSubmenu_module_css_default.notice,
1123
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("status.failed", { message: state.error }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1124
- type: "button",
1125
- className: ModelSubmenu_module_css_default.retry,
1126
- onClick: reload,
1127
- children: t("action.reload")
1128
- })]
1129
- }),
1130
- providerFailure !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1131
- className: ModelSubmenu_module_css_default.notice,
1132
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("status.providerFailed", {
1133
- name: providerFailure.name,
1134
- message: providerFailure.message
1135
- }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1136
- type: "button",
1137
- className: ModelSubmenu_module_css_default.retry,
1138
- onClick: reload,
1139
- children: t("action.reload")
1140
- })]
1141
- }),
1142
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1143
- className: cls(ModelSubmenu_module_css_default.groups, "scrollable"),
1144
- role: "menu",
1145
- "aria-label": t("menu.aria"),
1146
- children: [menuRows.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelRow, {
1147
- name: model.name,
1148
- selected: current !== null && currentGroup !== void 0 && current.provider === currentGroup.id && current.model === model.id,
1149
- busy,
1150
- star: "static",
1151
- favorited: currentGroup !== void 0 && isFavorite(favorites, currentGroup.id, model.id),
1152
- onPick: () => {
1153
- if (currentGroup !== void 0) chooseModel(currentGroup.id, model);
1154
- }
1155
- }, model.id)), menuRows.length === 0 && state.status === "ready" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1156
- className: ModelSubmenu_module_css_default.notice,
1157
- children: t("status.empty")
1158
- })]
1159
- }),
1160
- choices.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1161
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: ModelSubmenu_module_css_default.divider }),
1162
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1163
- className: ModelSubmenu_module_css_default.effortRow,
1164
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1165
- className: ModelSubmenu_module_css_default.effortHeading,
1166
- children: t("menu.effort")
1167
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1168
- className: ModelSubmenu_module_css_default.effortTrack,
1169
- role: "radiogroup",
1170
- "aria-label": t("menu.effort"),
1171
- onKeyDown: onEffortKeyDown,
1172
- children: choices.map((choice) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1173
- type: "button",
1174
- role: "radio",
1175
- "aria-checked": choice.effort === effectiveEffort,
1176
- className: ModelSubmenu_module_css_default.effortSegment,
1177
- disabled: busy,
1178
- onClick: () => {
1179
- if (live !== null) {
1180
- const pending = planEffortCommit(live, choice.effort);
1181
- if (pending !== null) submit(pending, null);
1182
- }
1183
- },
1184
- children: choice.label
1185
- }, choice.key))
1186
- })]
1187
- }),
1188
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: ModelSubmenu_module_css_default.divider })
1189
- ] }),
1190
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1191
- className: ModelSubmenu_module_css_default.hint,
1192
- children: t("menu.hint")
1193
- }),
1194
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: ModelSubmenu_module_css_default.divider }),
1195
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1196
- type: "button",
1197
- role: "menuitem",
1198
- className: ModelSubmenu_module_css_default.moreRow,
1199
- onClick: openPicker,
1200
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFlatListOutlineRegular, {
1201
- className: ModelSubmenu_module_css_default.moreIcon,
1202
- size: 16
1203
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("menu.more") })]
1204
- })
1205
- ]
1206
- }), document.body),
1207
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelPicker, {
1208
- open: pickerOpen,
1209
- state,
1210
- live,
1211
- facts,
1212
- favorites,
1213
- busy,
1214
- t,
1215
- onPick: (selection) => {
1216
- submit(selection, () => {
1217
- setPickerOpen(false);
1218
- });
1219
- },
1220
- onToggleFavorite: toggleFavoriteOf,
1221
- onReload: reload,
1222
- onClose: () => {
1223
- setPickerOpen(false);
1224
- queueMicrotask(() => {
1225
- triggerRef.current?.focus();
1226
- });
1227
- }
1228
- }),
1229
- toast !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Toast, {
1230
- text: toast.text,
1231
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconWarningOutlineRegular, {}),
1232
- anchor: rootRef.current?.closest("[data-composer-card]") ?? null,
1233
- onDone: () => {
1234
- setToast(null);
1235
- }
1236
- }, toast.seq)
1237
- ]
1307
+ className: ModelOptions_module_css_default.pickerHost,
1308
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelPicker, {
1309
+ open,
1310
+ resolveText,
1311
+ state,
1312
+ live,
1313
+ facts,
1314
+ favorites,
1315
+ busy,
1316
+ t,
1317
+ rememberedOf: (provider, model) => effortFor(memory, provider, model),
1318
+ onPick: submit,
1319
+ onToggleFavorite: (provider, model) => {
1320
+ setFavorites(toggleFavorite(storageOf(), provider, model));
1321
+ },
1322
+ onReload: load,
1323
+ onClose: () => {
1324
+ picker.close();
1325
+ }
1326
+ }), toast !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Toast, {
1327
+ text: toast.text,
1328
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconWarningOutlineRegular, {}),
1329
+ anchor: rootRef.current?.closest("[data-composer-card]") ?? null,
1330
+ onDone: () => {
1331
+ setToast(null);
1332
+ }
1333
+ }, toast.seq)]
1238
1334
  });
1239
1335
  }
1240
- /** The selection in force, or null while the host has not resolved one. */
1241
- function selectionOf(state) {
1242
- if (state.current === null) return null;
1243
- return state.current.reasoningEffort === void 0 ? {
1244
- provider: state.current.provider,
1245
- model: state.current.model
1246
- } : {
1247
- provider: state.current.provider,
1248
- model: state.current.model,
1249
- reasoningEffort: state.current.reasoningEffort
1250
- };
1251
- }
1252
1336
  /** Isolate the browser's storage so a locked-down browser reads as "no favorites". */
1253
1337
  function storageOf() {
1254
1338
  try {
@@ -1258,6 +1342,35 @@ window.__ModuleLoader__.load({
1258
1342
  }
1259
1343
  }
1260
1344
  //#endregion
1345
+ //#region src/client/picker-state.ts
1346
+ /**
1347
+ * @returns a store that starts closed.
1348
+ */
1349
+ function createPickerStore() {
1350
+ let snapshot = { sessionId: null };
1351
+ const listeners = /* @__PURE__ */ new Set();
1352
+ const publish = (next) => {
1353
+ if (next.sessionId === snapshot.sessionId) return;
1354
+ snapshot = next;
1355
+ for (const listener of listeners) listener();
1356
+ };
1357
+ return {
1358
+ getSnapshot: () => snapshot,
1359
+ subscribe: (listener) => {
1360
+ listeners.add(listener);
1361
+ return () => {
1362
+ listeners.delete(listener);
1363
+ };
1364
+ },
1365
+ open: (sessionId) => {
1366
+ publish({ sessionId });
1367
+ },
1368
+ close: () => {
1369
+ publish({ sessionId: null });
1370
+ }
1371
+ };
1372
+ }
1373
+ //#endregion
1261
1374
  //#region src/client/facts-source.ts
1262
1375
  const EMPTY = () => ({
1263
1376
  contextWindow: void 0,
@@ -1305,21 +1418,11 @@ window.__ModuleLoader__.load({
1305
1418
  }
1306
1419
  //#endregion
1307
1420
  //#region src/client/locales.ts
1308
- /** Locale bundles for the composer model menu and the picker it opens. */
1421
+ /** Locale bundles for the all-provider model picker. */
1309
1422
  /** Dictionary namespace owned by this plugin. */
1310
1423
  const NS = "modelSubmenu";
1311
1424
  /** Simplified Chinese dictionary (the key-set source of truth). */
1312
1425
  const zh = {
1313
- "trigger.fallback": "选择模型",
1314
- "trigger.loading": "正在加载模型…",
1315
- "trigger.selectAria": "选择模型",
1316
- "trigger.aria": "选择模型,当前 {model}",
1317
- "trigger.ariaEffort": "选择模型,当前 {model},思考等级 {effort}",
1318
- "menu.aria": "模型与思考等级",
1319
- "menu.effort": "思考",
1320
- "menu.more": "更多模型…",
1321
- "menu.hint": "提示:切换模型或思考程度会使既有的提示词缓存失效。建议新建会话,避免额外的 token 消耗。",
1322
- "effort.providerDefault": "默认",
1323
1426
  "status.loading": "正在加载模型…",
1324
1427
  "status.empty": "没有可用的模型。",
1325
1428
  "status.failed": "模型列表加载失败:{message}",
@@ -1338,20 +1441,13 @@ window.__ModuleLoader__.load({
1338
1441
  "picker.hintSelect": "选择",
1339
1442
  "picker.hintClose": "关闭",
1340
1443
  "meta.effort": "思考",
1341
- "meta.images": "图片输入"
1444
+ "meta.images": "图片输入",
1445
+ "effort.label": "推理等级",
1446
+ "effort.providerDefault": "Default",
1447
+ "effort.useDefault": "切换为默认档位"
1342
1448
  };
1343
1449
  /** English dictionary, checked complete against the zh key set. */
1344
1450
  const en = {
1345
- "trigger.fallback": "Select model",
1346
- "trigger.loading": "Loading models…",
1347
- "trigger.selectAria": "Select model",
1348
- "trigger.aria": "Select model, current {model}",
1349
- "trigger.ariaEffort": "Select model, current {model}, effort {effort}",
1350
- "menu.aria": "Model and effort",
1351
- "menu.effort": "Effort",
1352
- "menu.more": "More models…",
1353
- "menu.hint": "Changing the model or the effort invalidates the existing prompt cache. Start a new session to avoid the extra token cost.",
1354
- "effort.providerDefault": "Default",
1355
1451
  "status.loading": "Loading models…",
1356
1452
  "status.empty": "No models available.",
1357
1453
  "status.failed": "Model list failed to load: {message}",
@@ -1370,14 +1466,17 @@ window.__ModuleLoader__.load({
1370
1466
  "picker.hintSelect": "select",
1371
1467
  "picker.hintClose": "close",
1372
1468
  "meta.effort": "Effort",
1373
- "meta.images": "Images"
1469
+ "meta.images": "Images",
1470
+ "effort.label": "Reasoning effort",
1471
+ "effort.providerDefault": "Default",
1472
+ "effort.useDefault": "Switch to the provider default"
1374
1473
  };
1375
1474
  //#endregion
1376
1475
  //#region src/client/index.ts
1377
1476
  /**
1378
1477
  * Cordis services required by the browser half. `slots` is the contribution
1379
1478
  * registry, `sessions` tells whether a session may select a model,
1380
- * `modelDirectories` owns the per-session catalog snapshot this seat renders
1479
+ * `modelDirectories` owns the per-session catalog snapshot both entries render
1381
1480
  * from, and `configForms` carries the settings provider's shared describe
1382
1481
  * mirror, which answers each model's context window and accepted modalities.
1383
1482
  * `remote`, `remote.llm`, and `remote.session` are declared because a service
@@ -1396,9 +1495,14 @@ window.__ModuleLoader__.load({
1396
1495
  "slots"
1397
1496
  ];
1398
1497
  /**
1399
- * Client plugin body: register this plugin's dictionaries, then take over the
1400
- * composer model seat below the built-in occupant's priority so this menu
1401
- * renders in its place.
1498
+ * Client plugin body: register this plugin's dictionaries, then contribute the
1499
+ * bridge that turns the Host's model cell into the picker's door and its effort
1500
+ * cell into the reasoning-level slider, the overlay that renders the picker, and
1501
+ * the tint that keeps the collapsed trigger's effort caption on the same color.
1502
+ *
1503
+ * The two are separate entries because the Host unmounts the model menu the
1504
+ * moment a row is clicked; the overlay slot stays mounted, so the dialog
1505
+ * survives the menu's teardown.
1402
1506
  * @param ctx - client root context.
1403
1507
  */
1404
1508
  function apply(ctx) {
@@ -1414,36 +1518,62 @@ window.__ModuleLoader__.load({
1414
1518
  ], (scope) => {
1415
1519
  const models = scope.modelDirectories;
1416
1520
  const sessions = scope.sessions;
1521
+ const picker = createPickerStore();
1417
1522
  const facts = createFactsSource({
1418
1523
  describe: scope.configForms.describe(),
1419
1524
  listProviders: () => scope.remote.llm.listConfigurableProviders()
1420
1525
  });
1421
1526
  ctx.effect(() => facts.start(), "dsh-model-submenu: model metadata");
1422
- scope.slots.inject("conversation.input.model", () => scope.slots.register({
1423
- name: "conversation.input.model",
1424
- priority: -100,
1527
+ /**
1528
+ * Build one entry's business face for a session.
1529
+ * @param sessionId - the slot's session scope.
1530
+ * @returns the catalog face plus the session and store both entries need.
1531
+ */
1532
+ const faceOf = (sessionId) => {
1533
+ const key = sessionId;
1534
+ const directory = models.directoryFor(key);
1535
+ const available = sessions.subagentAddress(key) === void 0;
1536
+ return {
1537
+ sessionId,
1538
+ picker,
1539
+ available,
1540
+ directory: directory.store,
1541
+ factsSource: facts.source,
1542
+ load: () => {
1543
+ if (available) directory.load().catch(() => {});
1544
+ },
1545
+ resolveText: (text) => ctx.locale.resolveText(text),
1546
+ select: (selection) => available ? directory.select(selection) : Promise.resolve(void 0)
1547
+ };
1548
+ };
1549
+ scope.slots.inject("conversation.input.model.options", () => scope.slots.register({
1550
+ name: "conversation.input.model.options",
1551
+ id: "model-menu-bridge",
1425
1552
  registrant: "@lynn123411/dsh-model-submenu",
1426
1553
  locale: NS,
1427
- inject: (sessionId) => {
1428
- const key = sessionId;
1429
- const directory = models.directoryFor(key);
1430
- const available = sessions.subagentAddress(key) === void 0;
1431
- return {
1432
- available,
1433
- directory: directory.store,
1434
- factsSource: facts.source,
1435
- load: () => {
1436
- if (available) directory.load().catch(() => {});
1437
- },
1438
- select: (selection) => available ? directory.select(selection) : Promise.resolve(void 0)
1439
- };
1440
- }
1441
- }, ModelSubmenu));
1554
+ inject: faceOf
1555
+ }, ModelMenuBridge));
1556
+ scope.slots.inject("conversation.input.overlay", () => scope.slots.register({
1557
+ name: "conversation.input.overlay",
1558
+ id: "model-picker",
1559
+ registrant: "@lynn123411/dsh-model-submenu",
1560
+ locale: NS,
1561
+ inject: faceOf
1562
+ }, ModelPickerHost));
1563
+ scope.slots.inject("conversation.input.overlay", () => scope.slots.register({
1564
+ name: "conversation.input.overlay",
1565
+ id: "model-effort-tint",
1566
+ registrant: "@lynn123411/dsh-model-submenu",
1567
+ inject: faceOf
1568
+ }, EffortTint));
1442
1569
  });
1443
1570
  }
1444
1571
  //#endregion
1445
- exports.ModelSubmenu = ModelSubmenu;
1572
+ exports.EffortTint = EffortTint;
1573
+ exports.ModelMenuBridge = ModelMenuBridge;
1574
+ exports.ModelPickerHost = ModelPickerHost;
1446
1575
  exports.apply = apply;
1576
+ exports.createPickerStore = createPickerStore;
1447
1577
  exports.inject = inject;
1448
1578
  return module.exports;
1449
1579
  }