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