dsh-model-organizer 0.3.0

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 ADDED
@@ -0,0 +1,1516 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-model-organizer",
3
+ factory: (require) => {
4
+ const module = { exports: {} };
5
+ const exports = module.exports;
6
+
7
+ const React = require("react");
8
+ const ReactDOM = require("react-dom");
9
+ const h = React.createElement;
10
+ const useState = React.useState;
11
+ const useEffect = React.useEffect;
12
+ const useMemo = React.useMemo;
13
+ const useRef = React.useRef;
14
+ const useLayoutEffect = React.useLayoutEffect;
15
+ const useSyncExternalStore = React.useSyncExternalStore;
16
+
17
+ let P = null;
18
+ try { P = require("@deepseek-ai/dsh-client-ui-primitives"); } catch (e) { P = null; }
19
+
20
+ const NS = "model-organizer";
21
+ const PI_AI_NS = "llm-pi-ai";
22
+
23
+ /* ------------------------------ dictionaries ------------------------------ */
24
+ const zh = {
25
+ "composer.title": "选择模型",
26
+ "composer.none": "未选择模型",
27
+ "composer.empty": "暂无可用模型",
28
+ "composer.locked": "当前不可切换模型",
29
+ "composer.count": "个模型",
30
+ "composer.aria": "选择模型,当前 {model}",
31
+ "composer.ariaEffort": "选择模型,当前 {model},推理等级 {effort}",
32
+ "composer.effort": "推理等级",
33
+ "composer.effortDefault": "Default",
34
+ "composer.effortEmpty": "当前模型未提供推理等级。",
35
+ "composer.effortBack": "模型",
36
+ "composer.refreshing": "正在刷新模型列表…",
37
+ "order.title": "模型顺序",
38
+ "order.hint": "拖动 ⠿ 调整,松手即保存",
39
+ "order.saving": "保存中…",
40
+ "order.saved": "已保存 ✓",
41
+ "order.unsaved": "有未保存改动",
42
+ "order.readonly": "当前环境不可写入该设置",
43
+ "providers.title": "供应商与模型顺序",
44
+ "providers.hint": "拖动行即可调整顺序",
45
+ "providers.expand": "展开顺序面板",
46
+ "providers.collapse": "收起顺序面板",
47
+ "providers.saved": "已保存 ✓",
48
+ };
49
+ const en = {
50
+ "composer.title": "Select model",
51
+ "composer.none": "No model",
52
+ "composer.empty": "No models available",
53
+ "composer.locked": "Model switching unavailable",
54
+ "composer.count": "models",
55
+ "composer.aria": "Select model, currently {model}",
56
+ "composer.ariaEffort": "Select model, currently {model}, reasoning effort {effort}",
57
+ "composer.effort": "Effort",
58
+ "composer.effortDefault": "Default",
59
+ "composer.effortEmpty": "This model provides no reasoning effort levels.",
60
+ "composer.effortBack": "Model",
61
+ "composer.refreshing": "Refreshing model list…",
62
+ "order.title": "Model order",
63
+ "order.hint": "Drag ⠿ to reorder, then save",
64
+ "order.saving": "Saving…",
65
+ "order.saved": "Saved ✓",
66
+ "order.unsaved": "Unsaved changes",
67
+ "order.readonly": "This settings document is read-only here",
68
+ "providers.title": "Provider and model order",
69
+ "providers.hint": "Drag a row to reorder",
70
+ "providers.expand": "Expand the order panel",
71
+ "providers.collapse": "Collapse the order panel",
72
+ "providers.saved": "Saved ✓",
73
+ };
74
+
75
+ /* --------------------------- official CSS classes -------------------------- */
76
+ /* The shipped client-ui-model-selection bundle injects its CSS-module sheet and
77
+ STILL loads (this plugin only shadows its seat), so reusing its class names
78
+ reproduces the shipped look exactly. The hash is resolved from the live
79
+ stylesheet so a dsh upgrade cannot silently drop the styling. */
80
+ const CLASS_SUFFIXES = ["root", "trigger", "triggerLabel", "triggerEffort", "triggerIcon", "chevron", "chevronOpen",
81
+ "menu", "status", "empty", "error", "warning", "retry", "groups", "group", "groupTitle",
82
+ "option", "selected", "optionCopy", "modelName", "check", "cell", "cellLabel", "cellValue", "cellChevron"];
83
+
84
+ /* Resolution is cached against the live sheet count so a late-injected sheet is
85
+ still picked up, and a resolved prefix is VALIDATED against a second class
86
+ only this module owns — an unrelated module shipping a `_trigger` rule must
87
+ not win (the updater module does). */
88
+ /* Bump on every deploy. The host serves the plugin bundle as
89
+ `immutable, max-age=31536000` under a URL that does not change when a plugin's
90
+ code changes, so a browser can keep running an old build until a hard reload.
91
+ Kept OFF the UI (it is not user-facing): read it as
92
+ `window.__dshModelOrganizerBuild` when a stale build is suspected. */
93
+ const BUILD = "b23";
94
+
95
+ let officialClasses = null;
96
+ let officialClassesSheets = -1;
97
+ let officialClassesValidated = false;
98
+
99
+ function sheetRules() {
100
+ const out = [];
101
+ let sheets;
102
+ try { sheets = document.styleSheets; } catch (e) { return out; }
103
+ for (let i = 0; i < sheets.length; i++) {
104
+ let rules = null;
105
+ try { rules = sheets[i].cssRules; } catch (e) { continue; }
106
+ if (rules === null) continue;
107
+ for (let j = 0; j < rules.length; j++) {
108
+ const sel = rules[j].selectorText;
109
+ if (typeof sel === "string") out.push(sel);
110
+ }
111
+ }
112
+ return out;
113
+ }
114
+
115
+ function prefixIsValid(prefix, selectors) {
116
+ const marker = "." + prefix + "_optionCopy";
117
+ for (let i = 0; i < selectors.length; i++) if (selectors[i].indexOf(marker) !== -1) return true;
118
+ return false;
119
+ }
120
+
121
+ function resolveOfficialClasses() {
122
+ let sheetCount = 0;
123
+ try { sheetCount = document.styleSheets.length; } catch (e) { sheetCount = 0; }
124
+ if (officialClassesValidated && officialClassesSheets === sheetCount) return officialClasses;
125
+ /* Anchored on classes only the model selector owns — a bare "_trigger" probe
126
+ also matches unrelated modules (the updater module ships one too). */
127
+ const ANCHORS = ["_triggerEffort", "_optionCopy", "_chevronOpen", "_cellValue"];
128
+ const selectors = sheetRules();
129
+ let prefix = null;
130
+ for (let a = 0; a < ANCHORS.length && prefix === null; a++) {
131
+ for (let i = 0; i < selectors.length && prefix === null; i++) {
132
+ const at = selectors[i].indexOf(ANCHORS[a]);
133
+ if (at === -1) continue;
134
+ const found = /[.]([A-Za-z0-9_-]+)$/.exec(selectors[i].slice(0, at));
135
+ if (found === null) continue;
136
+ if (!prefixIsValid(found[1], selectors)) continue;
137
+ prefix = found[1];
138
+ }
139
+ }
140
+ officialClassesSheets = sheetCount;
141
+ officialClassesValidated = true;
142
+ if (prefix === null) { officialClasses = null; return null; }
143
+ const out = {};
144
+ for (let i = 0; i < CLASS_SUFFIXES.length; i++) out[CLASS_SUFFIXES[i]] = prefix + "_" + CLASS_SUFFIXES[i];
145
+ officialClasses = out;
146
+ return out;
147
+ }
148
+
149
+ /* The shipped sheet styles a static group heading; the collapsible variant needs
150
+ a hover affordance and a chevron that flips. Injected once per document. */
151
+ const OWN_STYLE_ID = "dsh-model-organizer-style";
152
+ function ensureOwnStyles() {
153
+ try {
154
+ if (document.getElementById(OWN_STYLE_ID) !== null) return;
155
+ const style = document.createElement("style");
156
+ style.id = OWN_STYLE_ID;
157
+ style.textContent = [
158
+ ".dsh-mo-head{transition:background .12s ease;}",
159
+ ".dsh-mo-head:hover{background:var(--dsw-alias-interactive-bg-hover);}",
160
+ /* A drag leaves the source row FOCUSED in Chromium, and both the shipped
161
+ \`option\` and this sheet paint a :focus-visible background — that is the grey
162
+ block that used to stay behind after a drag. Rows are mouse-only
163
+ (tabIndex -1), so they get no focus paint at all; the panel header stays
164
+ keyboard-reachable and keeps its own. */
165
+ ".dsh-mo-head:focus-visible{outline:none;}",
166
+ ".dsh-mo-head.dsh-mo-head:focus-visible,.dsh-mo-model.dsh-mo-model:focus-visible,.dsh-mo-row.dsh-mo-row:focus-visible{background:transparent;}",
167
+ ".dsh-mo-panelhead.dsh-mo-panelhead:focus-visible{background:var(--dsw-alias-interactive-bg-hover);}",
168
+ ".dsh-mo-chev{transition:transform .12s ease;}",
169
+ ".dsh-mo-head[aria-expanded='true'] .dsh-mo-chev{transform:rotate(90deg);}",
170
+ ".dsh-mo-row{border-width:1px;border-style:solid;border-color:transparent;}",
171
+ /* No row may paint a hover tint while a drag owns the pointer. */
172
+ ".dsh-mo-nohover.dsh-mo-nohover .dsh-mo-head:hover,.dsh-mo-nohover.dsh-mo-nohover .dsh-mo-row:hover,.dsh-mo-nohover.dsh-mo-nohover .dsh-mo-model:hover{background:transparent;}",
173
+
174
+ /* The row IS the handle now: no grip glyph, the cursor says "drag me".
175
+ Scoped to the settings panel's own list — the composer menu reuses the
176
+ shipped cell/option classes together with dsh-mo-head / dsh-mo-model for
177
+ rows that are NOT draggable, and those must keep the shipped cursor. */
178
+ ".dsh-mo-list .dsh-mo-head{cursor:grab;}",
179
+ ".dsh-mo-list .dsh-mo-model{cursor:grab;}",
180
+ ".dsh-mo-list .dsh-mo-row{cursor:grab;}",
181
+ ".dsh-mo-list .dsh-mo-head:active,.dsh-mo-list .dsh-mo-model:active{cursor:grabbing;}",
182
+ /* The panel header is a drag handle, not a button: keep the cursor, drop the
183
+ hover tint. Doubled selectors outrank the shipped `cell` :hover rule. */
184
+ ".dsh-mo-panelhead.dsh-mo-panelhead{cursor:grab;}",
185
+ ".dsh-mo-panelhead.dsh-mo-panelhead:hover{background:transparent;}",
186
+ ".dsh-mo-list{display:flex;flex-direction:column;gap:2px;margin-top:2px;}",
187
+ ".dsh-mo-head{touch-action:none;user-select:none;-webkit-user-select:none;}",
188
+ /* Borrowed from the sidebar rows: without -webkit-user-drag the browser treats
189
+ a press-and-move over the row's TEXT as a NATIVE TEXT DRAG (the "release to
190
+ search" ghost) instead of our reorder drag. It must NOT go on the row itself
191
+ — that disables the row's own draggable behaviour — so it goes on the row's
192
+ contents, leaving the row draggable. */
193
+ ".dsh-mo-list .dsh-mo-head,.dsh-mo-list .dsh-mo-model,.dsh-mo-list .dsh-mo-row{user-select:none;-webkit-user-select:none;}",
194
+ ".dsh-mo-list .dsh-mo-head *,.dsh-mo-list .dsh-mo-model *,.dsh-mo-list .dsh-mo-row *{-webkit-user-drag:none;user-select:none;-webkit-user-select:none;}",
195
+ ".dsh-mo-panelhead,.dsh-mo-panelhead *{-webkit-user-drag:none;user-select:none;-webkit-user-select:none;}",
196
+ ".dsh-mo-moving,.dsh-mo-moving:hover{cursor:grabbing;background:transparent;}",
197
+ /* (the header's own arrow button is gone — the row toggles on click) */
198
+
199
+ ".dsh-mo-model{padding-left:28px;position:relative;}",
200
+ ".dsh-mo-model::before{content:'';position:absolute;left:15px;top:50%;width:5px;height:5px;margin-top:-2.5px;border-radius:50%;background:var(--dsw-alias-label-tertiary);opacity:.5;}",
201
+ ".dsh-mo-model[aria-checked='true']::before{background:var(--dsw-alias-label-primary);opacity:.9;}"
202
+ ].join("");
203
+ document.head.appendChild(style);
204
+ } catch (e) {}
205
+ }
206
+
207
+ function cx() {
208
+ let out = "";
209
+ for (let i = 0; i < arguments.length; i++) {
210
+ const v = arguments[i];
211
+ if (typeof v === "string" && v.length > 0) out = out.length === 0 ? v : out + " " + v;
212
+ }
213
+ return out;
214
+ }
215
+
216
+ function icon(name, props) {
217
+ if (P === null || typeof P[name] !== "function") return null;
218
+ return h(P[name], props ? Object.assign({}, props) : {});
219
+ }
220
+
221
+ /* --------------------------------- styles --------------------------------- */
222
+ const S = {
223
+ chip: {
224
+ display: "inline-flex", alignItems: "center", gap: 6, maxWidth: 260,
225
+ padding: "3px 8px", borderRadius: 8, cursor: "pointer", fontSize: 12, lineHeight: "18px",
226
+ border: "1px solid var(--dsw-alias-border-l1)",
227
+ background: "var(--dsw-alias-interactive-bg, rgba(127,127,127,.10))",
228
+ color: "var(--dsw-alias-label-primary)"
229
+ },
230
+ chipDisabled: { opacity: 0.5, cursor: "not-allowed" },
231
+ chipLabel: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
232
+ chipEffort: { color: "var(--dsw-alias-label-caption)", flexShrink: 1000, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", minWidth: 0 },
233
+ caret: { fontSize: 9, color: "var(--dsw-alias-label-tertiary)" },
234
+ menuEmpty: { padding: "10px 8px", fontSize: 12, color: "var(--dsw-alias-label-tertiary)" },
235
+ /* fallbacks used only when the shipped stylesheet could not be located */
236
+ fallbackMenu: {
237
+ position: "fixed", zIndex: 1100, maxHeight: "min(360px, 100vh - 96px)", overflow: "hidden",
238
+ display: "flex", flexDirection: "column", padding: 4,
239
+ background: "var(--dsw-specific-menu, var(--dsw-alias-bg-module, #222))",
240
+ color: "var(--dsw-alias-label-primary)", borderRadius: 20, boxShadow: "0 8px 28px rgba(0,0,0,.32)"
241
+ },
242
+ fallbackGroups: { overflowY: "auto", minHeight: 0, display: "flex", flexDirection: "column", gap: 4 },
243
+ fallbackGroup: { display: "flex", flexDirection: "column" },
244
+ fallbackGroupTitle: {
245
+ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 6, width: "100%",
246
+ textAlign: "left", padding: "5px 8px 3px", border: "none", cursor: "pointer",
247
+ background: "transparent", color: "var(--dsw-alias-label-tertiary)", fontSize: 12, fontWeight: 500
248
+ },
249
+ fallbackModel: {
250
+ display: "flex", alignItems: "center", gap: 8, width: "100%", minHeight: 38,
251
+ textAlign: "left", padding: "6px 8px", border: "none", borderRadius: 10, cursor: "pointer",
252
+ background: "transparent", color: "inherit"
253
+ },
254
+ fallbackCopy: { display: "flex", flexDirection: "column", flex: 1, minWidth: 0 },
255
+ fallbackCheck: { flex: "0 0 18px", display: "grid", placeItems: "center" },
256
+ /* the shipped groupTitle is a plain heading; the clickable variant only adds
257
+ layout + affordance so the visuals keep coming from the shipped sheet. */
258
+ groupChevron: { color: "var(--dsw-alias-label-tertiary)", flex: "none", transition: "transform .12s" },
259
+ measureStyle: { visibility: "hidden", left: 0, top: 0 },
260
+ groupCount: { fontSize: 10, color: "var(--dsw-alias-label-tertiary)", flex: "none" },
261
+ failure: { display: "flex", flexDirection: "column", gap: 2, padding: "5px 8px", fontSize: 11, color: "var(--dsw-alias-state-warn-label, #d29922)" },
262
+ box: { marginTop: 8, padding: 8, border: "1px solid var(--dsw-alias-border-l1)", borderRadius: 10, background: "var(--dsw-alias-bg-module-secondary, transparent)" },
263
+ providerGroup: { display: "flex", flexDirection: "column" },
264
+
265
+ /* Floating presentation: the panel sits in the corner of the Models page so
266
+ provider/API configuration and reordering are visible at the same time. */
267
+ floatPanel: {
268
+ position: "fixed", right: 20, bottom: 20, zIndex: 900, width: 380, maxWidth: "calc(100vw - 40px)",
269
+ display: "flex", flexDirection: "column", padding: 4,
270
+ border: "1px solid var(--dsw-alias-border-l1)", borderRadius: 14,
271
+ background: "var(--dsw-specific-menu, var(--dsw-alias-bg-module, #222))",
272
+ boxShadow: "0 10px 30px rgba(0,0,0,.34)", color: "var(--dsw-alias-label-primary)"
273
+ },
274
+ /* The shipped `cell` sets min-width:100%; the head must stay inside the panel. */
275
+ panelHeadCell: { display: "flex", alignItems: "center", gap: 6, flex: "1 1 auto", minWidth: 0 },
276
+ floatBody: { display: "flex", flexDirection: "column", gap: 2, overflowY: "auto", maxHeight: "52vh", padding: "2px 4px 4px" },
277
+
278
+ boxHead: { display: "flex", alignItems: "center", gap: 8, marginBottom: 6, flexWrap: "wrap" },
279
+ boxTitle: { fontSize: 12, fontWeight: 600, color: "var(--dsw-alias-label-primary)" },
280
+ boxHint: { fontSize: 10, color: "var(--dsw-alias-label-tertiary)" },
281
+ list: { display: "flex", flexDirection: "column", gap: 2 },
282
+ row: {
283
+ display: "flex", alignItems: "center", gap: 8, padding: "5px 6px", borderRadius: 7,
284
+ fontSize: 12, color: "var(--dsw-alias-label-secondary)", outline: "none",
285
+ borderWidth: 1, borderStyle: "solid", borderColor: "transparent",
286
+ background: "var(--dsw-alias-interactive-bg, rgba(127,127,127,.06))",
287
+ transition: "background .12s ease, border-color .12s ease, opacity .12s ease, transform .12s ease",
288
+ cursor: "grab"
289
+ },
290
+ /* Only border-color changes while dragging: keeping the border as longhands
291
+ everywhere stops React from expanding the shorthand and then dropping
292
+ border-color, which would fall back to currentColor and look like a
293
+ highlight that never clears. */
294
+ /* Only the drag affordances ride inline when the shipped classes are in use. */
295
+ rowDraggingOnly: {
296
+ opacity: 0.55, cursor: "grabbing", borderColor: "var(--dsw-alias-state-accent, #4f8cff)", transform: "scale(1.01)"
297
+ },
298
+ rowDragging: {
299
+ opacity: 0.55, cursor: "grabbing",
300
+ borderColor: "var(--dsw-alias-state-accent, #4f8cff)",
301
+ background: "var(--dsw-alias-interactive-bg, rgba(79,140,255,.14))",
302
+ transform: "scale(1.01)"
303
+ },
304
+ rowName: { flex: "1 1 auto", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
305
+ /* Right-aligned "N models" count on a provider row (fallback styling only). */
306
+ rowMeta: { flex: "none", fontSize: 10, color: "var(--dsw-alias-label-tertiary)" },
307
+ dirty: { fontSize: 11, color: "var(--dsw-alias-state-warn-label, #d29922)" },
308
+ ok: { fontSize: 11, color: "var(--dsw-alias-state-success, #3fb950)" }
309
+ };
310
+
311
+ /* --------------------------- drag reorder helper -------------------------- */
312
+ /* Live preview: the list re-renders in the would-be order while dragging, so
313
+ neighbouring rows visibly move out of the way before the drop lands. */
314
+ const MEASURE_STYLE = S.measureStyle;
315
+
316
+ function moved(ids, from, to) {
317
+ const next = ids.slice();
318
+ const item = next.splice(from, 1)[0];
319
+ next.splice(to, 0, item);
320
+ return next;
321
+ }
322
+
323
+ /* A transient status note. The timer is kept per instance so a second save cannot
324
+ be cut short by the first save's timer, and nothing fires after unmount. */
325
+ function useNote() {
326
+ const state = useState(null);
327
+ const timer = useRef(null);
328
+ useEffect(function () {
329
+ return function () {
330
+ if (timer.current !== null) clearTimeout(timer.current);
331
+ };
332
+ }, []);
333
+ const set = function (value, ttl) {
334
+ if (timer.current !== null) { clearTimeout(timer.current); timer.current = null; }
335
+ state[1](value);
336
+ if (value !== null && typeof ttl === "number") {
337
+ timer.current = setTimeout(function () {
338
+ timer.current = null;
339
+ state[1](null);
340
+ }, ttl);
341
+ }
342
+ };
343
+ return [state[0], set];
344
+ }
345
+
346
+ function DragList(props) {
347
+ const ids = props.ids;
348
+ const render = props.render;
349
+ const onCommit = props.onCommit;
350
+ const disabled = props.disabled;
351
+ const dragState = useState(null);
352
+ const drag = dragState[0];
353
+ const setDrag = dragState[1];
354
+ /* The live value lives in a ref as well as in state: dragover fires many times
355
+ per frame, and a handler reading the rendered state would compute from a
356
+ stale list — that is what let the highlighted row drift away from the row
357
+ actually being dragged. */
358
+ const dragRef = useRef(null);
359
+ const handled = useRef(false);
360
+ /* While an HTML5 drag session runs, Chromium owns the pointer and stops
361
+ updating :hover — so the row that was under the cursor when the press
362
+ started keeps its hover tint, frozen at the position it was pressed at,
363
+ while the dragged row travels with the pointer. Hover paint is therefore
364
+ suppressed for the whole gesture (and until the pointer moves again, so a
365
+ frozen tint can never survive the drop). The dragged row keeps its own
366
+ accent border + opacity, so the feedback is not lost. */
367
+ const lockState = useState(false);
368
+ const hoverLocked = lockState[0];
369
+ const setHoverLocked = lockState[1];
370
+
371
+ useEffect(function () {
372
+ if (!hoverLocked) return undefined;
373
+ const release = function () { if (dragRef.current === null) setHoverLocked(false); };
374
+ window.addEventListener("pointermove", release, true);
375
+ window.addEventListener("mousemove", release, true);
376
+ window.addEventListener("pointerdown", release, true);
377
+ const timer = setTimeout(release, 400);
378
+ return function () {
379
+ window.removeEventListener("pointermove", release, true);
380
+ window.removeEventListener("mousemove", release, true);
381
+ window.removeEventListener("pointerdown", release, true);
382
+ clearTimeout(timer);
383
+ };
384
+ }, [hoverLocked]);
385
+
386
+
387
+ const shown = drag !== null ? drag.preview : ids;
388
+
389
+ function put(next) {
390
+ dragRef.current = next;
391
+ setDrag(next);
392
+ }
393
+
394
+ /* Identity, not position: the dragged row is remembered by its id, so the
395
+ highlight follows the row itself wherever the preview moves it. */
396
+ function start(id, event) {
397
+ if (disabled) return;
398
+ handled.current = false;
399
+ setHoverLocked(true);
400
+ /* Chromium focuses the mousedown target; a native drag then keeps it focused
401
+ for the rest of the gesture, which paints a focus background on the row. */
402
+ try { if (event && event.currentTarget && event.currentTarget.blur) event.currentTarget.blur(); } catch (e) {}
403
+ if (event.dataTransfer) {
404
+ event.dataTransfer.effectAllowed = "move";
405
+ try { event.dataTransfer.setData("text/plain", String(id)); } catch (e) {}
406
+ }
407
+ put({ id: id, preview: ids });
408
+ }
409
+
410
+ function over(rowId, event) {
411
+ const current = dragRef.current;
412
+ if (disabled || current === null) return;
413
+ event.preventDefault();
414
+ if (event.dataTransfer) event.dataTransfer.dropEffect = "move";
415
+ const list = current.preview;
416
+ const from = list.indexOf(current.id);
417
+ const index = list.indexOf(rowId);
418
+ if (from < 0 || index < 0) return;
419
+ const rect = event.currentTarget.getBoundingClientRect();
420
+ const below = event.clientY > rect.top + rect.height / 2;
421
+ let to = below ? index + 1 : index;
422
+ if (from < to) to -= 1;
423
+ to = Math.max(0, Math.min(list.length - 1, to));
424
+ if (to === from) return;
425
+ put({ id: current.id, preview: moved(list, from, to) });
426
+ }
427
+
428
+ /* Safety net: if a drag ends without reaching our drop handler (dropped on the
429
+ page background, cancelled, window blurred, source node re-rendered), the
430
+ highlight must not stay behind. Bubble phase, so a real drop still commits
431
+ first. */
432
+ useEffect(function () {
433
+ if (drag === null) return undefined;
434
+ const clear = function () {
435
+ dragRef.current = null;
436
+ setDrag(null);
437
+ handled.current = false;
438
+ };
439
+ window.addEventListener("dragend", clear);
440
+ window.addEventListener("drop", clear);
441
+ window.addEventListener("mouseup", clear);
442
+ window.addEventListener("blur", clear);
443
+ return function () {
444
+ window.removeEventListener("dragend", clear);
445
+ window.removeEventListener("drop", clear);
446
+ window.removeEventListener("mouseup", clear);
447
+ window.removeEventListener("blur", clear);
448
+ };
449
+ }, [drag === null]);
450
+
451
+ function clearFocus(node) {
452
+ try {
453
+ if (node && typeof node.blur === "function") node.blur();
454
+ const active = document.activeElement;
455
+ if (active && typeof active.blur === "function" && active !== document.body) active.blur();
456
+ } catch (e) {}
457
+ }
458
+
459
+ function drop(event) {
460
+ const current = dragRef.current;
461
+ if (current === null || handled.current) return;
462
+ handled.current = true;
463
+ event.preventDefault();
464
+ put(null);
465
+ clearFocus(event.currentTarget);
466
+ onCommit(current.preview);
467
+ }
468
+
469
+ function end(event) {
470
+ put(null);
471
+ handled.current = false;
472
+ clearFocus(event ? event.currentTarget : null);
473
+ }
474
+
475
+ return h("div", {
476
+ className: hoverLocked ? "dsh-mo-list dsh-mo-nohover" : "dsh-mo-list",
477
+ style: S.list,
478
+ onDragEnd: end,
479
+ onDrop: drop
480
+ },
481
+ shown.map(function (id, index) {
482
+ const dragging = drag !== null && drag.id === id;
483
+ return render(id, index, {
484
+ draggable: !disabled,
485
+ dragging: dragging,
486
+ onDragStart: function (event) { start(id, event); },
487
+ onDragOver: function (event) { over(id, event); },
488
+ onDragEnd: end,
489
+ onDrop: drop
490
+ });
491
+ })
492
+ );
493
+ }
494
+
495
+ /* ---------------- A. composer model selector, grouped by provider ---------- */
496
+ function ProviderGroupedModelSelect(props) {
497
+ const locked = props.locked;
498
+ const available = props.available;
499
+ const directory = props.directory;
500
+ const load = props.load;
501
+ const select = props.select;
502
+ const t = props.t;
503
+
504
+ const C = resolveOfficialClasses();
505
+ ensureOwnStyles();
506
+ const state = useSyncExternalStore(
507
+ function (fn) { return directory.subscribe(fn); },
508
+ function () { return directory.getSnapshot(); }
509
+ );
510
+ const openState = useState(false);
511
+ const open = openState[0];
512
+ const setOpen = openState[1];
513
+ const expandedState = useState({});
514
+ const expanded = expandedState[0];
515
+ const setExpanded = expandedState[1];
516
+ const posState = useState(null);
517
+ const menuPos = posState[0];
518
+ const setMenuPos = posState[1];
519
+ const paneState = useState("model");
520
+ const pane = paneState[0];
521
+ const setPane = paneState[1];
522
+ const rootRef = useRef(null);
523
+ const triggerRef = useRef(null);
524
+ const menuRef = useRef(null);
525
+ const idRef = useRef(null);
526
+ if (idRef.current === null) idRef.current = "mo-" + Math.random().toString(36).slice(2, 9);
527
+
528
+ const groups = (state && state.groups) || [];
529
+ const failures = (state && state.failures) || [];
530
+ const current = (state && state.current) || null;
531
+ const busy = !!state && state.status === "selecting";
532
+ const loading = !!state && state.status === "loading";
533
+
534
+ /* Providers follow the plugin's own display preference (see below). Computed
535
+ per render — not memoised — so a reorder made in Settings shows up here the
536
+ next time the menu opens, without a page reload. */
537
+ const groupsOrdered = (function () {
538
+ const ids = [];
539
+ const byId = {};
540
+ for (let i = 0; i < groups.length; i++) { ids.push(groups[i].id); byId[groups[i].id] = groups[i]; }
541
+ const ordered = applyProviderOrder(ids, readProviderOrder());
542
+ const out = [];
543
+ for (let i = 0; i < ordered.length; i++) if (byId[ordered[i]] !== undefined) out.push(byId[ordered[i]]);
544
+ return out;
545
+ })();
546
+
547
+ const layoutKey = useMemo(function () {
548
+ const parts = [];
549
+ for (let i = 0; i < groupsOrdered.length; i++) parts.push(groupsOrdered[i].id + (expanded[groupsOrdered[i].id] === true ? "+" : "-"));
550
+ return parts.join("|") + "|" + String(current === null ? "" : current.provider);
551
+ }, [groupsOrdered, expanded, current]);
552
+
553
+ useEffect(function () { load(); }, []);
554
+
555
+ useEffect(function () {
556
+ if (!open) return undefined;
557
+ const onDown = function (event) {
558
+ if (triggerRef.current && triggerRef.current.contains(event.target)) return;
559
+ if (menuRef.current && menuRef.current.contains(event.target)) return;
560
+ setOpen(false);
561
+ };
562
+ const onKey = function (event) {
563
+ if (event.key !== "Escape") return;
564
+ setPane(function (currentPane) {
565
+ if (currentPane === "effort") return "model";
566
+ setOpen(false);
567
+ return currentPane;
568
+ });
569
+ };
570
+ document.addEventListener("mousedown", onDown);
571
+ document.addEventListener("keydown", onKey);
572
+ return function () {
573
+ document.removeEventListener("mousedown", onDown);
574
+ document.removeEventListener("keydown", onKey);
575
+ };
576
+ }, [open]);
577
+
578
+ const currentInfo = useMemo(function () {
579
+ if (current === null) return null;
580
+ for (let i = 0; i < groups.length; i++) {
581
+ const group = groups[i];
582
+ if (group.id !== current.provider) continue;
583
+ for (let j = 0; j < group.models.length; j++) {
584
+ if (group.models[j].id === current.model) return { group: group, model: group.models[j] };
585
+ }
586
+ }
587
+ return null;
588
+ }, [groups, current]);
589
+
590
+ /* Reasoning effort, mirroring the shipped ModelSelect: exact-model metadata
591
+ comes from the Host, and the effective level is the session's explicit
592
+ choice or the model's own default. */
593
+ const reasoning = currentInfo !== null && currentInfo.model.reasoning ? currentInfo.model.reasoning : undefined;
594
+ const effectiveEffort = current !== null && current.reasoningEffort !== undefined
595
+ ? current.reasoningEffort
596
+ : (reasoning !== undefined ? reasoning.defaultEffort : undefined);
597
+ let effortLabel;
598
+ if (reasoning !== undefined) {
599
+ if (effectiveEffort === undefined) effortLabel = t("composer.effortDefault");
600
+ else {
601
+ const levels = reasoning.efforts || [];
602
+ let found;
603
+ for (let i = 0; i < levels.length; i++) if (levels[i].id === effectiveEffort) found = levels[i];
604
+ effortLabel = found !== undefined ? found.name : effectiveEffort;
605
+ }
606
+ }
607
+ const effortChoices = useMemo(function () {
608
+ if (reasoning === undefined) return [];
609
+ const out = [];
610
+ if (reasoning.defaultEffort === undefined) out.push({ key: "provider-default", effort: undefined, label: t("composer.effortDefault") });
611
+ const levels = reasoning.efforts || [];
612
+ for (let i = 0; i < levels.length; i++) out.push({ key: "effort:" + levels[i].id, effort: levels[i].id, label: levels[i].name });
613
+ return out;
614
+ }, [reasoning, t]);
615
+
616
+ /* Official placement: right-aligned above the trigger, clamped to the
617
+ viewport, re-measured on scroll/resize (mirrors the shipped algorithm). */
618
+ useLayoutEffect(function () {
619
+ if (!open) { setMenuPos(null); return undefined; }
620
+ const place = function () {
621
+ const rect = triggerRef.current ? triggerRef.current.getBoundingClientRect() : undefined;
622
+ if (rect === undefined) return;
623
+ const MARGIN = 12;
624
+ const lw = menuRef.current ? menuRef.current.offsetWidth : 0;
625
+ const lh = menuRef.current ? menuRef.current.offsetHeight : 0;
626
+ let x = rect.right - lw;
627
+ let y = rect.top - 8 - lh;
628
+ if (lw > 0) x = Math.min(Math.max(x, MARGIN), window.innerWidth - lw - MARGIN);
629
+ if (lh > 0) y = Math.min(Math.max(y, MARGIN), window.innerHeight - lh - MARGIN);
630
+ setMenuPos({ left: x, top: y });
631
+ };
632
+ place();
633
+ window.addEventListener("scroll", place, true);
634
+ window.addEventListener("resize", place);
635
+ return function () {
636
+ window.removeEventListener("scroll", place, true);
637
+ window.removeEventListener("resize", place);
638
+ };
639
+ }, [open, layoutKey]);
640
+
641
+ if (available === false) return null;
642
+
643
+ const disabled = !!locked;
644
+
645
+ function toggle() {
646
+ if (disabled) return;
647
+ if (!open) {
648
+ load();
649
+ if (Object.keys(expanded).length === 0 && current !== null) {
650
+ const seed = {};
651
+ seed[current.provider] = true;
652
+ setExpanded(seed);
653
+ }
654
+ }
655
+ setOpen(!open);
656
+ }
657
+
658
+ function toggleGroup(id) {
659
+ const next = {};
660
+ for (const key in expanded) next[key] = expanded[key];
661
+ next[id] = !next[id];
662
+ setExpanded(next);
663
+ }
664
+
665
+ function pick(group, model) {
666
+ const sameModel = current !== null && current.provider === group.id && current.model === model.id;
667
+ let effort = model.reasoning ? model.reasoning.defaultEffort : undefined;
668
+ if (sameModel && current.reasoningEffort !== undefined) effort = current.reasoningEffort;
669
+ const selection = { provider: group.id, model: model.id };
670
+ if (effort !== undefined) selection.reasoningEffort = effort;
671
+ Promise.resolve(select(selection)).then(function (ok) { if (ok) setOpen(false); });
672
+ }
673
+
674
+ /* Same model, new effort — the shipped semantics: an unchanged level just
675
+ closes, otherwise the whole selection is resubmitted. */
676
+ function chooseEffort(effort) {
677
+ if (current === null) return;
678
+ if (effectiveEffort === effort) { setOpen(false); return; }
679
+ const selection = { provider: current.provider, model: current.model };
680
+ if (effort !== undefined) selection.reasoningEffort = effort;
681
+ Promise.resolve(select(selection)).then(function (ok) { if (ok) setOpen(false); });
682
+ }
683
+
684
+ const label = currentInfo !== null ? currentInfo.model.name : (current === null ? t("composer.none") : current.provider + "/" + current.model);
685
+ const triggerTitle = effortLabel === undefined ? label : label + " · " + effortLabel;
686
+ const aria = (effortLabel === undefined ? t("composer.aria") : t("composer.ariaEffort"))
687
+ .split("{model}").join(label)
688
+ .split("{effort}").join(effortLabel === undefined ? "" : effortLabel);
689
+
690
+ const chevronIcon = icon("IconChevronDownOutline14", {
691
+ className: C !== null ? cx(C.chevron, open && C.chevronOpen) : undefined,
692
+ style: C === null ? S.caret : undefined
693
+ });
694
+ const triggerChildren = [];
695
+ const triggerGlyph = icon("IconDataOutline16", { key: "g", className: C !== null ? C.triggerIcon : undefined, size: 16 });
696
+ if (triggerGlyph !== null) triggerChildren.push(triggerGlyph);
697
+ triggerChildren.push(h("span", { key: "l", className: C !== null ? C.triggerLabel : undefined, style: C === null ? S.chipLabel : undefined }, label));
698
+ if (effortLabel !== undefined) {
699
+ triggerChildren.push(h("span", { key: "e", className: C !== null ? C.triggerEffort : undefined, style: C === null ? S.chipEffort : undefined }, effortLabel));
700
+ }
701
+ triggerChildren.push(chevronIcon !== null ? chevronIcon : h("span", { key: "c", style: S.caret }, open ? "▾" : "▴"));
702
+
703
+ const trigger = h("button", {
704
+ ref: triggerRef,
705
+ type: "button",
706
+ className: C !== null ? C.trigger : undefined,
707
+ style: C === null ? (disabled ? Object.assign({}, S.chip, S.chipDisabled) : S.chip) : undefined,
708
+ disabled: disabled,
709
+ title: disabled ? t("composer.locked") : triggerTitle,
710
+ "aria-label": aria,
711
+ "aria-haspopup": "menu",
712
+ "aria-expanded": open,
713
+ "aria-controls": open ? idRef.current + "-menu" : undefined,
714
+ onClick: toggle
715
+ }, triggerChildren);
716
+
717
+ const rootBox = h("div", {
718
+ ref: rootRef,
719
+ className: C !== null ? C.root : undefined,
720
+ style: C === null ? { minWidth: 0, position: "relative" } : undefined
721
+ }, trigger);
722
+
723
+ if (!open) return rootBox;
724
+
725
+ const groupNodes = groupsOrdered.map(function (group) {
726
+ const isOpen = expanded[group.id] === true;
727
+ const headingId = idRef.current + "-" + group.id;
728
+ /* A provider row reuses the shipped `cell` drill-in row (label + right-aligned
729
+ value + chevron), so both menu levels are drawn by the shipped sheet. */
730
+ const headChildren = [
731
+ h("span", { key: "n", className: C !== null ? C.cellLabel : undefined, style: C === null ? undefined : undefined }, group.name || group.id),
732
+ h("span", { key: "v", className: C !== null ? C.cellValue : undefined, style: C === null ? S.groupCount : undefined }, group.models.length + " " + t("composer.count"))
733
+ ];
734
+ const groupChevron = icon("IconChevronRightOutline14", {
735
+ key: "i",
736
+ className: C !== null ? cx(C.cellChevron, "dsh-mo-chev") : "dsh-mo-chev",
737
+ style: C === null ? S.groupChevron : undefined
738
+ });
739
+ if (groupChevron !== null) headChildren.push(groupChevron);
740
+ const head = h("button", {
741
+ key: "h",
742
+ type: "button",
743
+ id: headingId,
744
+ className: C !== null ? cx(C.cell, "dsh-mo-head") : "dsh-mo-head",
745
+ style: C === null ? S.fallbackGroupTitle : undefined,
746
+ "aria-expanded": isOpen,
747
+ onClick: function () { toggleGroup(group.id); }
748
+ }, headChildren);
749
+
750
+ const items = !isOpen ? [] : group.models.map(function (model) {
751
+ const selected = current !== null && current.provider === group.id && current.model === model.id;
752
+ const check = selected ? icon("IconCheckOutline16", { key: "k" }) : null;
753
+ return h("button", {
754
+ key: model.id,
755
+ type: "button",
756
+ role: "menuitemradio",
757
+ "aria-checked": selected,
758
+ className: C !== null ? cx(C.option, "dsh-mo-model", selected && C.selected) : "dsh-mo-model",
759
+ style: C === null ? S.fallbackModel : undefined,
760
+ title: model.name,
761
+ disabled: busy,
762
+ onClick: function () { pick(group, model); }
763
+ },
764
+ h("span", { className: C !== null ? C.optionCopy : undefined, style: C === null ? S.fallbackCopy : undefined },
765
+ h("span", { className: C !== null ? C.modelName : undefined, style: C === null ? S.chipLabel : undefined }, model.name || model.id)),
766
+ h("span", { className: C !== null ? C.check : undefined, style: C === null ? S.fallbackCheck : undefined }, check)
767
+ );
768
+ });
769
+
770
+ return h("section", {
771
+ key: group.id,
772
+ role: "group",
773
+ "aria-labelledby": headingId,
774
+ className: C !== null ? C.group : S.fallbackGroup
775
+ }, [head].concat(items));
776
+ });
777
+
778
+ const bodyChildren = [];
779
+ if (loading) bodyChildren.push(h("div", { key: "s", className: C !== null ? C.status : undefined, style: C === null ? S.boxHint : undefined }, t("composer.refreshing")));
780
+
781
+ if (pane === "effort") {
782
+ /* Back row, styled as the shipped drill-in cell. */
783
+ bodyChildren.push(h("button", {
784
+ key: "back",
785
+ type: "button",
786
+ role: "menuitem",
787
+ className: C !== null ? C.cell : undefined,
788
+ style: C === null ? S.fallbackGroupTitle : undefined,
789
+ onClick: function () { setPane("model"); }
790
+ },
791
+ h("span", { key: "l", className: C !== null ? C.cellLabel : undefined }, t("composer.effortBack")),
792
+ h("span", { key: "v", className: C !== null ? C.cellValue : undefined }, label)
793
+ ));
794
+ if (effortChoices.length === 0) {
795
+ bodyChildren.push(h("div", { key: "ee", className: C !== null ? C.empty : undefined, style: C === null ? S.menuEmpty : undefined }, t("composer.effortEmpty")));
796
+ } else {
797
+ bodyChildren.push(h("div", {
798
+ key: "eg",
799
+ className: C !== null ? cx(C.groups, "scrollable") : undefined,
800
+ style: C === null ? S.fallbackGroups : undefined
801
+ }, effortChoices.map(function (level) {
802
+ const selected = effectiveEffort === level.effort;
803
+ const check = selected ? icon("IconCheckOutline16", { key: "k" }) : null;
804
+ return h("button", {
805
+ key: level.key,
806
+ type: "button",
807
+ role: "menuitemradio",
808
+ "aria-checked": selected,
809
+ className: C !== null ? cx(C.option, selected && C.selected) : undefined,
810
+ style: C === null ? S.fallbackModel : undefined,
811
+ disabled: busy,
812
+ onClick: function () { chooseEffort(level.effort); }
813
+ },
814
+ h("span", { className: C !== null ? C.optionCopy : undefined, style: C === null ? S.fallbackCopy : undefined },
815
+ h("span", { className: C !== null ? C.modelName : undefined, style: C === null ? S.chipLabel : undefined }, level.label)),
816
+ h("span", { className: C !== null ? C.check : undefined, style: C === null ? S.fallbackCheck : undefined }, check)
817
+ );
818
+ })));
819
+ }
820
+ } else {
821
+ if (failures.length > 0) {
822
+ bodyChildren.push(h("div", { key: "w", className: C !== null ? C.warning : undefined, style: C === null ? S.failure : undefined },
823
+ h("span", null, failures.map(function (f) { return f.name + ": " + f.message; }).join(" · "))));
824
+ }
825
+ if (groupsOrdered.length === 0) {
826
+ if (!loading) bodyChildren.push(h("div", { key: "e", className: C !== null ? C.empty : undefined, style: C === null ? S.menuEmpty : undefined }, t("composer.empty")));
827
+ } else {
828
+ bodyChildren.push(h("div", {
829
+ key: "g",
830
+ className: C !== null ? cx(C.groups, "scrollable") : undefined,
831
+ style: C === null ? S.fallbackGroups : undefined
832
+ }, groupNodes));
833
+ }
834
+ /* The effort entry sits last, as a shipped drill-in cell, so the provider
835
+ list stays the default view. */
836
+ if (effortChoices.length > 0) {
837
+ bodyChildren.push(h("button", {
838
+ key: "eff",
839
+ type: "button",
840
+ role: "menuitem",
841
+ className: C !== null ? cx(C.cell, "dsh-mo-head") : "dsh-mo-head",
842
+ style: C === null ? S.fallbackGroupTitle : undefined,
843
+ onClick: function () { setPane("effort"); }
844
+ },
845
+ h("span", { key: "l", className: C !== null ? C.cellLabel : undefined }, t("composer.effort")),
846
+ h("span", { key: "v", className: C !== null ? C.cellValue : undefined }, effortLabel),
847
+ icon("IconChevronRightOutline14", { key: "c", className: C !== null ? C.cellChevron : undefined })
848
+ ));
849
+ }
850
+ }
851
+
852
+ const menu = h("div", {
853
+ ref: menuRef,
854
+ id: idRef.current + "-menu",
855
+ className: C !== null ? C.menu : undefined,
856
+ style: C !== null
857
+ ? (menuPos !== null ? menuPos : MEASURE_STYLE)
858
+ : Object.assign({}, S.fallbackMenu, menuPos !== null ? menuPos : MEASURE_STYLE),
859
+ role: "menu",
860
+ "aria-label": t("composer.title"),
861
+ "aria-busy": loading || busy
862
+ }, bodyChildren);
863
+
864
+ return h(React.Fragment, null, rootBox, ReactDOM.createPortal(menu, document.body));
865
+ }
866
+
867
+ /* ------------- B. one provider card's model order editor ------------------ */
868
+ /* Owner props: { provider, configured, keyConfigured }. Injected: store, namespace, mutate, t. */
869
+ function ProviderModelOrder(props) {
870
+ const C = resolveOfficialClasses();
871
+ ensureOwnStyles();
872
+ const entry = props.provider;
873
+ const store = props.store;
874
+ const namespace = props.namespace;
875
+ const mutate = props.mutate;
876
+ const t = props.t;
877
+
878
+ const snap = useSyncExternalStore(
879
+ function (fn) { return store.subscribe(fn); },
880
+ function () { return store.getSnapshot(); }
881
+ );
882
+ const view = snap ? snap.view : undefined;
883
+ const writable = !!(view && view.writable);
884
+ const nsView = namespaceView(view, namespace);
885
+ const providerId = entry ? entry.provider : undefined;
886
+ const layer = providerLayer(nsView);
887
+ const resolved = (nsView && nsView.value && nsView.value.providers) || {};
888
+ const row = providerId !== undefined ? (layer[providerId] || resolved[providerId]) : undefined;
889
+ const models = (row && row.models) || [];
890
+ const baseIds = models.map(function (m) { return m.id; });
891
+ const sourceKey = baseIds.join("|");
892
+ const byId = {};
893
+ for (let i = 0; i < models.length; i++) byId[models[i].id] = models[i];
894
+
895
+ const orderState = useState(null);
896
+ const order = orderState[0];
897
+ const setOrder = orderState[1];
898
+ const savingState = useState(false);
899
+ const saving = savingState[0];
900
+ const setSaving = savingState[1];
901
+ const notePair = useNote();
902
+ const note = notePair[0];
903
+ const setNote = notePair[1];
904
+ const expandState = useState(false);
905
+ const expanded = expandState[0];
906
+ const setExpanded = expandState[1];
907
+
908
+ useEffect(function () { setOrder(null); }, [providerId, sourceKey]);
909
+
910
+ const ids = order === null ? baseIds : order;
911
+ const dirty = order !== null && order.join("|") !== sourceKey;
912
+
913
+ function commit(next) {
914
+ if (next.join("|") === baseIds.join("|")) { setOrder(null); return; }
915
+ setOrder(next);
916
+ if (!writable || saving) return;
917
+ setSaving(true);
918
+ const basePath = entry && entry.settingsPath ? entry.settingsPath.slice() : ["providers", providerId];
919
+ const path = basePath.concat(["models"]);
920
+ const revision = nsView ? nsView.revision : undefined;
921
+ let pending;
922
+ try {
923
+ pending = mutate([{ op: "set", path: path, value: next.map(function (id) { return byId[id]; }) }], revision);
924
+ } catch (e) {
925
+ setSaving(false);
926
+ setNote(String((e && e.message) || e));
927
+ setTimeout(function () { setNote(null); }, 4000);
928
+ return;
929
+ }
930
+ Promise.resolve(pending).then(function (res) {
931
+ setSaving(false);
932
+ if (res && res.ok) { setOrder(null); setNote("ok"); }
933
+ else setNote((res && res.error && res.error.message) || "save failed");
934
+ setTimeout(function () { setNote(null); }, 2600);
935
+ }, function (err) {
936
+ setSaving(false);
937
+ setNote(String((err && err.message) || err));
938
+ setTimeout(function () { setNote(null); }, 2600);
939
+ });
940
+ }
941
+
942
+ /* The embedded list also renders a lone model, so expanding a single-model
943
+ provider shows what it holds instead of nothing. */
944
+ const minModels = props.embedded === true ? 1 : 2;
945
+ if (!entry || models.length < minModels) return null;
946
+
947
+ /* Embedded mode: the footer panel owns the surrounding provider row and its
948
+ expand state, so only the draggable model list is rendered here. */
949
+ if (props.embedded === true) {
950
+ return h(DragList, {
951
+ ids: ids,
952
+ disabled: !writable || saving,
953
+ onCommit: commit,
954
+ render: function (id, index, dragProps) {
955
+ const model = byId[id];
956
+ return h("div", {
957
+ key: id,
958
+ draggable: dragProps.draggable,
959
+ "data-mo": "model-row",
960
+ "data-mo-id": id,
961
+ "data-mo-provider": String(providerId),
962
+ tabIndex: -1,
963
+ onDragStart: dragProps.onDragStart,
964
+ onDragOver: dragProps.onDragOver,
965
+ onDragEnd: dragProps.onDragEnd,
966
+ onDrop: dragProps.onDrop,
967
+ className: C !== null ? cx(C.option, "dsh-mo-model") : "dsh-mo-model",
968
+ style: dragProps.dragging
969
+ ? (C !== null ? S.rowDraggingOnly : Object.assign({}, S.row, S.rowDragging))
970
+ : (C !== null ? undefined : S.row)
971
+ },
972
+
973
+ h("span", { key: "n", className: C !== null ? C.optionCopy : undefined, style: C === null ? { flex: "1 1 auto", minWidth: 0, display: "flex" } : undefined },
974
+ h("span", { className: C !== null ? C.modelName : undefined, style: C === null ? S.rowName : undefined }, (model && (model.name || model.id)) || id))
975
+ );
976
+ }
977
+ });
978
+ }
979
+
980
+ /* Header doubles as the collapse control, drawn by the shipped `cell` row.
981
+ Status notes ride the header's right edge so the list never reflows. */
982
+ const headChildren = [
983
+ h("span", { key: "l", className: C !== null ? C.cellLabel : undefined, style: C === null ? S.boxTitle : undefined }, t("order.title")),
984
+ h("span", { key: "v", className: C !== null ? C.cellValue : undefined, style: C === null ? S.boxHint : undefined }, saving ? t("order.saving") : t("order.hint"))
985
+ ];
986
+ const notes = [];
987
+ if (!writable) notes.push(h("span", { key: "ro", style: S.dirty }, t("order.readonly")));
988
+ if (dirty) notes.push(h("span", { key: "d", style: S.dirty }, t("order.unsaved")));
989
+ if (note === "ok") notes.push(h("span", { key: "ok", style: S.ok }, t("order.saved")));
990
+ else if (note !== null) notes.push(h("span", { key: "n", style: S.dirty }, note));
991
+ for (let i = 0; i < notes.length; i++) headChildren.push(notes[i]);
992
+ const headChevron = icon("IconChevronRightOutline14", {
993
+ key: "c",
994
+ className: C !== null ? cx(C.cellChevron, "dsh-mo-chev") : "dsh-mo-chev",
995
+ style: C === null ? S.groupChevron : undefined
996
+ });
997
+ if (headChevron !== null) headChildren.push(headChevron);
998
+
999
+ const head = h("button", {
1000
+ type: "button",
1001
+ className: C !== null ? cx(C.cell, "dsh-mo-head") : "dsh-mo-head",
1002
+ style: C === null ? S.boxHead : undefined,
1003
+ "aria-expanded": expanded,
1004
+ onClick: function () { setExpanded(!expanded); }
1005
+ }, headChildren);
1006
+
1007
+ const list = h(DragList, {
1008
+ ids: ids,
1009
+ disabled: !writable || saving,
1010
+ onCommit: commit,
1011
+ render: function (id, index, dragProps) {
1012
+ const model = byId[id];
1013
+ return h("div", {
1014
+ key: id,
1015
+ draggable: dragProps.draggable,
1016
+ "data-mo": "model-row",
1017
+ "data-mo-id": id,
1018
+ "data-mo-provider": String(providerId),
1019
+ tabIndex: -1,
1020
+ onDragStart: dragProps.onDragStart,
1021
+ onDragOver: dragProps.onDragOver,
1022
+ onDragEnd: dragProps.onDragEnd,
1023
+ onDrop: dragProps.onDrop,
1024
+ className: C !== null ? cx(C.option, "dsh-mo-row") : undefined,
1025
+ style: dragProps.dragging
1026
+ ? (C !== null ? S.rowDraggingOnly : Object.assign({}, S.row, S.rowDragging))
1027
+ : (C !== null ? undefined : S.row)
1028
+ },
1029
+
1030
+ h("span", { key: "n", className: C !== null ? C.optionCopy : undefined, style: C === null ? undefined : { flex: "1 1 auto", minWidth: 0, display: "flex" } },
1031
+ h("span", { className: C !== null ? C.modelName : undefined, style: C === null ? S.rowName : undefined }, (model && (model.name || model.id)) || id))
1032
+ );
1033
+ }
1034
+ });
1035
+
1036
+ return h("div", { style: S.box }, head, expanded ? list : null);
1037
+ }
1038
+
1039
+ /* ------------- C. provider order panel (models page footer) --------------- */
1040
+ /* Provider order is a DISPLAY preference owned by this plugin: the settings
1041
+ document stores `providers` as a record whose key order the host normalises
1042
+ away (verified with both a whole-record `set` and an `unset`+`set` pair), so
1043
+ the order cannot live there. It is kept per browser and applied to the
1044
+ composer menu this plugin renders. */
1045
+ const ORDER_STORAGE_KEY = "dsh-model-organizer.providerOrder";
1046
+
1047
+ function readProviderOrder() {
1048
+ try {
1049
+ const raw = window.localStorage.getItem(ORDER_STORAGE_KEY);
1050
+ if (raw === null) return [];
1051
+ const parsed = JSON.parse(raw);
1052
+ if (!Array.isArray(parsed)) return [];
1053
+ const out = [];
1054
+ for (let i = 0; i < parsed.length; i++) if (typeof parsed[i] === "string") out.push(parsed[i]);
1055
+ return out;
1056
+ } catch (e) { return []; }
1057
+ }
1058
+
1059
+ function writeProviderOrder(ids) {
1060
+ try { window.localStorage.setItem(ORDER_STORAGE_KEY, JSON.stringify(ids)); } catch (e) {}
1061
+ }
1062
+
1063
+ const PANEL_STORAGE_KEY = "dsh-model-organizer.panelOpen";
1064
+ const PANEL_POS_KEY = "dsh-model-organizer.panelPos";
1065
+
1066
+
1067
+ function readPanelPos() {
1068
+ try {
1069
+ const raw = window.localStorage.getItem(PANEL_POS_KEY);
1070
+ if (raw === null) return null;
1071
+ const parsed = JSON.parse(raw);
1072
+ if (parsed === null || typeof parsed !== "object") return null;
1073
+ if (typeof parsed.left !== "number" || typeof parsed.top !== "number") return null;
1074
+ return { left: parsed.left, top: parsed.top };
1075
+ } catch (e) { return null; }
1076
+ }
1077
+
1078
+ function writePanelPos(pos) {
1079
+ try { window.localStorage.setItem(PANEL_POS_KEY, JSON.stringify(pos)); } catch (e) {}
1080
+ }
1081
+
1082
+ /* ALWAYS expanded on mount. A remembered collapse is indistinguishable from
1083
+ "the default is collapsed" the next time the page opens, which is not what
1084
+ this panel is for. */
1085
+ function readPanelOpen() {
1086
+ return true;
1087
+ }
1088
+
1089
+ function writePanelOpen(open) {
1090
+ try { window.localStorage.setItem(PANEL_STORAGE_KEY, open ? "1" : "0"); } catch (e) {}
1091
+ }
1092
+
1093
+ /* Stored order first, then everything else in document order. */
1094
+ function applyProviderOrder(ids, preferred) {
1095
+ if (preferred.length === 0) return ids;
1096
+ const rank = {};
1097
+ for (let i = 0; i < preferred.length; i++) rank[preferred[i]] = i;
1098
+ const known = [];
1099
+ const rest = [];
1100
+ for (let i = 0; i < ids.length; i++) {
1101
+ if (rank[ids[i]] === undefined) rest.push(ids[i]);
1102
+ else known.push(ids[i]);
1103
+ }
1104
+ known.sort(function (a, b) { return rank[a] - rank[b]; });
1105
+ return known.concat(rest);
1106
+ }
1107
+
1108
+ function ProviderOrderPanel(props) {
1109
+ const C = resolveOfficialClasses();
1110
+ ensureOwnStyles();
1111
+ const store = props.store;
1112
+ const namespace = props.namespace;
1113
+ const mutate = props.mutate;
1114
+ const t = props.t;
1115
+
1116
+ const snap = useSyncExternalStore(
1117
+ function (fn) { return store.subscribe(fn); },
1118
+ function () { return store.getSnapshot(); }
1119
+ );
1120
+ const view = snap ? snap.view : undefined;
1121
+ const nsView = namespaceView(view, namespace);
1122
+ const display = orderLayer(nsView) || {};
1123
+ const documentIds = Object.keys(display);
1124
+ const storedState = useState(readProviderOrder);
1125
+ const stored = storedState[0];
1126
+ const setStored = storedState[1];
1127
+ const openState = useState({});
1128
+ const openMap = openState[0];
1129
+ const setOpenMap = openState[1];
1130
+ const panelState = useState(readPanelOpen);
1131
+ const panelOpen = panelState[0];
1132
+ const setPanelOpen = function (next) { writePanelOpen(next); panelState[1](next); };
1133
+ const posState = useState(readPanelPos);
1134
+ const panelPos = posState[0];
1135
+ const setPanelPos = posState[1];
1136
+ const panelRef = useRef(null);
1137
+ const pressRef = useRef(null);
1138
+ const suppressClick = useRef(false);
1139
+ const movingState = useState(false);
1140
+ const moving = movingState[0];
1141
+ const setMoving = movingState[1];
1142
+ const baseIds = applyProviderOrder(documentIds, stored);
1143
+ const sourceKey = baseIds.join("|");
1144
+
1145
+ const orderState = useState(null);
1146
+ const order = orderState[0];
1147
+ const setOrder = orderState[1];
1148
+ const savingState = useState(false);
1149
+ const saving = savingState[0];
1150
+ const setSaving = savingState[1];
1151
+ const notePair = useNote();
1152
+ const note = notePair[0];
1153
+ const setNote = notePair[1];
1154
+
1155
+ useEffect(function () { setOrder(null); }, [sourceKey]);
1156
+
1157
+ useLayoutEffect(function () {
1158
+ if (panelPos !== null || panelRef.current === null) return;
1159
+ const rect = panelRef.current.getBoundingClientRect();
1160
+ setPanelPos({ left: Math.round(rect.left), top: Math.round(rect.top) });
1161
+ }, [panelPos, panelOpen]);
1162
+
1163
+ const ids = order === null ? baseIds : order;
1164
+ const idsKey = ids.join("|");
1165
+ const dirty = order !== null && order.join("|") !== sourceKey;
1166
+
1167
+ /* Mirror the stored order onto the shipped provider cards (CSS `order` only —
1168
+ the DOM React rendered is never touched). The cards may be (re)rendered after
1169
+ this effect runs, which drops the inline styles, so it is applied again on the
1170
+ next frame and once more shortly after. */
1171
+ useEffect(function () {
1172
+ const list = idsKey.length === 0 ? [] : idsKey.split("|");
1173
+ applyCardOrder(display, list);
1174
+ const frame = requestAnimationFrame(function () { applyCardOrder(display, list); });
1175
+ const timer = setTimeout(function () { applyCardOrder(display, list); }, 400);
1176
+ return function () {
1177
+ cancelAnimationFrame(frame);
1178
+ clearTimeout(timer);
1179
+ };
1180
+ }, [sourceKey, idsKey]);
1181
+
1182
+ function commit(next) {
1183
+ if (next.join("|") === baseIds.join("|")) { setOrder(null); return; }
1184
+ writeProviderOrder(next);
1185
+ setStored(next);
1186
+ setOrder(null);
1187
+ setNote("ok");
1188
+ setTimeout(function () { setNote(null); }, 2200);
1189
+ }
1190
+
1191
+ if (baseIds.length < 2) return null;
1192
+
1193
+ const notes = [];
1194
+ if (note === "ok") notes.push(h("span", { key: "ok", style: S.ok }, t("providers.saved")));
1195
+ else if (note !== null) notes.push(h("span", { key: "n", style: S.dirty }, note));
1196
+
1197
+ /* Same shipped-cell header as every other row in this page: the title, the
1198
+ hint, then any status note on the right edge. */
1199
+ const headChildren = [
1200
+ h("span", { key: "l", className: C !== null ? C.cellLabel : undefined, style: C === null ? S.boxTitle : undefined }, t("providers.title"))
1201
+ ];
1202
+ /* Status notes sit immediately after the title; the hint keeps the right edge. */
1203
+ for (let i = 0; i < notes.length; i++) headChildren.push(notes[i]);
1204
+ headChildren.push(h("span", { key: "v", className: C !== null ? C.cellValue : undefined, style: C === null ? S.boxHint : undefined }, t("providers.hint")));
1205
+ /* One row, two gestures — exactly like the rows below: a press that does not
1206
+ travel is a CLICK (toggle), a press that travels is a DRAG (move the panel).
1207
+ Arming on MOVEMENT only means a slow click still toggles; pinning the measured
1208
+ top-left on arming keeps the first movement from jumping. */
1209
+ function armMove(token) {
1210
+ if (token.armed) return;
1211
+ token.armed = true;
1212
+ setMoving(true);
1213
+ setPanelPos(token.base);
1214
+ }
1215
+
1216
+ function moveDown(event) {
1217
+ if (event.button !== undefined && event.button !== 0) return;
1218
+ const rect = panelRef.current ? panelRef.current.getBoundingClientRect() : null;
1219
+ const base = rect === null ? { left: 0, top: 0 } : { left: rect.left, top: rect.top };
1220
+ const token = { armed: false, moved: false, base: base, start: { x: event.clientX, y: event.clientY } };
1221
+ pressRef.current = token;
1222
+ try { if (event.currentTarget.setPointerCapture) event.currentTarget.setPointerCapture(event.pointerId); } catch (e) {}
1223
+ try { if (event.currentTarget.blur) event.currentTarget.blur(); } catch (e) {}
1224
+ }
1225
+
1226
+ function moveMove(event) {
1227
+ const token = pressRef.current;
1228
+ if (token === null) return;
1229
+ if (!token.armed) {
1230
+ const travelled = Math.abs(event.clientX - token.start.x) + Math.abs(event.clientY - token.start.y);
1231
+ if (travelled <= 6) return;
1232
+ armMove(token);
1233
+ }
1234
+ event.preventDefault();
1235
+ token.moved = true;
1236
+ const width = panelRef.current ? panelRef.current.offsetWidth : 380;
1237
+ const height = panelRef.current ? panelRef.current.offsetHeight : 120;
1238
+ const left = Math.min(Math.max(token.base.left + (event.clientX - token.start.x), 4), Math.max(4, window.innerWidth - width - 4));
1239
+ const top = Math.min(Math.max(token.base.top + (event.clientY - token.start.y), 4), Math.max(4, window.innerHeight - height - 4));
1240
+ setPanelPos({ left: left, top: top });
1241
+ }
1242
+
1243
+ function moveUp() {
1244
+ const token = pressRef.current;
1245
+ if (token === null) return;
1246
+ if (token.armed) {
1247
+ if (panelRef.current) {
1248
+ const rect = panelRef.current.getBoundingClientRect();
1249
+ writePanelPos({ left: Math.round(rect.left), top: Math.round(rect.top) });
1250
+ }
1251
+ /* The release after a move also emits a click; swallow exactly that one so
1252
+ dragging the panel never collapses it. */
1253
+ suppressClick.current = token.moved === true;
1254
+ } else {
1255
+ /* No travel and no hold: a plain click toggles. */
1256
+ setPanelOpen(!panelOpen);
1257
+ }
1258
+ setMoving(false);
1259
+ pressRef.current = null;
1260
+ }
1261
+
1262
+
1263
+
1264
+ /* The whole header row is the collapse control (no separate arrow): hover,
1265
+ pointer cursor and keyboard activation all come from this one element. */
1266
+ const head = h("div", {
1267
+ role: "button",
1268
+ tabIndex: 0,
1269
+ className: cx(C !== null ? C.cell : undefined, "dsh-mo-head", "dsh-mo-panelhead", moving ? "dsh-mo-moving" : undefined),
1270
+ style: C !== null ? S.panelHeadCell : S.boxHead,
1271
+ title: panelOpen ? t("providers.collapse") : t("providers.expand"),
1272
+ onPointerDown: moveDown,
1273
+ onPointerMove: moveMove,
1274
+ onPointerUp: moveUp,
1275
+ onPointerCancel: moveUp,
1276
+ onClick: function (event) {
1277
+ if (suppressClick.current) { suppressClick.current = false; return; }
1278
+ if (event && event.stopPropagation) event.stopPropagation();
1279
+ },
1280
+ onKeyDown: function (event) {
1281
+ if (event.key !== "Enter" && event.key !== " ") return;
1282
+ event.preventDefault();
1283
+ setPanelOpen(!panelOpen);
1284
+ }
1285
+ }, headChildren);
1286
+
1287
+
1288
+ const list = h(DragList, {
1289
+ ids: ids,
1290
+ disabled: false,
1291
+ onCommit: commit,
1292
+ render: function (id, index, dragProps) {
1293
+ const profile = display[id] || {};
1294
+ const count = (profile.models || []).length;
1295
+ const isOpen = openMap[id] === true;
1296
+ const rowChildren = [
1297
+
1298
+ h("span", { key: "n", className: C !== null ? C.cellLabel : undefined, style: C === null ? S.rowName : undefined }, profile.displayName || id),
1299
+ h("span", { key: "c", className: C !== null ? C.cellValue : undefined, style: C === null ? S.rowMeta : undefined }, count + " " + t("composer.count"))
1300
+ ];
1301
+ const rowChevron = icon("IconChevronRightOutline14", {
1302
+ key: "v",
1303
+ className: C !== null ? cx(C.cellChevron, "dsh-mo-chev") : "dsh-mo-chev",
1304
+ style: C === null ? S.groupChevron : undefined
1305
+ });
1306
+ if (rowChevron !== null) rowChildren.push(rowChevron);
1307
+
1308
+ const row = h("div", {
1309
+ key: "row",
1310
+ draggable: dragProps.draggable,
1311
+ "data-mo": "provider-row",
1312
+ "data-mo-id": id,
1313
+ tabIndex: -1,
1314
+ role: "button",
1315
+ "aria-expanded": isOpen,
1316
+ onDragStart: dragProps.onDragStart,
1317
+ onDragOver: dragProps.onDragOver,
1318
+ onDragEnd: dragProps.onDragEnd,
1319
+ onDrop: dragProps.onDrop,
1320
+ onClick: function () {
1321
+ const next = {};
1322
+ for (const key in openMap) next[key] = openMap[key];
1323
+ next[id] = !(next[id] === true);
1324
+ setOpenMap(next);
1325
+ },
1326
+ className: C !== null ? cx(C.cell, "dsh-mo-head") : "dsh-mo-head",
1327
+ style: dragProps.dragging
1328
+ ? (C !== null ? S.rowDraggingOnly : Object.assign({}, S.row, S.rowDragging))
1329
+ : (C === null ? S.row : undefined)
1330
+ }, rowChildren);
1331
+
1332
+ if (!isOpen || count < 1) return h("div", { key: id, style: S.providerGroup }, row);
1333
+ return h("div", { key: id, style: S.providerGroup }, row,
1334
+ h(ProviderModelOrder, {
1335
+ key: "models",
1336
+ provider: { provider: id, settingsPath: ["providers", id], settingsNs: namespace },
1337
+ store: store,
1338
+ namespace: namespace,
1339
+ mutate: mutate,
1340
+ t: t,
1341
+ embedded: true
1342
+ })
1343
+ );
1344
+ }
1345
+ });
1346
+
1347
+ /* The resting place is the bottom-right corner; pinning its measured top-left
1348
+ once keeps that spot while making the panel top-anchored, so collapsing
1349
+ raises the bottom edge instead of dragging the header down. */
1350
+ const panelStyle = panelPos === null
1351
+ ? S.floatPanel
1352
+ : Object.assign({}, S.floatPanel, { left: panelPos.left, top: panelPos.top, right: "auto", bottom: "auto" });
1353
+ /* Original layout (header first, list below) with a TOP-anchored panel: the
1354
+ header never moves and collapsing pulls the bottom edge up. */
1355
+ return h("div", { ref: panelRef, style: panelStyle },
1356
+ head,
1357
+ panelOpen ? h("div", { style: S.floatBody }, list) : null
1358
+ );
1359
+ }
1360
+
1361
+ /* -------------------------------- helpers --------------------------------- */
1362
+ /* The Models page renders one <li> per provider inside its own <ul>. The host
1363
+ owns that order (the settings document cannot express it — verified), so the
1364
+ stored preference is applied as CSS `order` on a flex column: the DOM is left
1365
+ exactly as React rendered it, only the visual sequence changes. Cards that do
1366
+ not correspond to a settings provider (catalog-only rows) stay pinned first. */
1367
+ function applyCardOrder(providers, ids) {
1368
+ try {
1369
+ const names = [];
1370
+ for (const id in providers) {
1371
+ const dn = providers[id] ? providers[id].displayName : undefined;
1372
+ if (typeof dn === "string" && dn.length > 0) names.push([id, dn]);
1373
+ }
1374
+ if (names.length === 0) return;
1375
+ names.sort(function (a, b) { return b[1].length - a[1].length; });
1376
+
1377
+ let list = null;
1378
+ let best = 0;
1379
+ const lists = document.querySelectorAll("ul");
1380
+ for (let i = 0; i < lists.length; i++) {
1381
+ const items = lists[i].children;
1382
+ let hits = 0;
1383
+ for (let j = 0; j < items.length; j++) {
1384
+ const text = items[j].textContent || "";
1385
+ for (let k = 0; k < names.length; k++) if (text.indexOf(names[k][1]) !== -1) { hits++; break; }
1386
+ }
1387
+ if (hits >= 2 && hits > best) { best = hits; list = lists[i]; }
1388
+ }
1389
+ if (list === null) return;
1390
+
1391
+ list.style.display = "flex";
1392
+ list.style.flexDirection = "column";
1393
+ const rank = {};
1394
+ for (let i = 0; i < ids.length; i++) rank[ids[i]] = i;
1395
+ const items = list.children;
1396
+ for (let i = 0; i < items.length; i++) {
1397
+ const text = items[i].textContent || "";
1398
+ let matched;
1399
+ for (let k = 0; k < names.length; k++) if (text.indexOf(names[k][1]) !== -1) { matched = names[k][0]; break; }
1400
+ const r = matched === undefined ? undefined : rank[matched];
1401
+ items[i].style.order = String(r === undefined ? -1 : r);
1402
+ }
1403
+ } catch (e) {}
1404
+ }
1405
+
1406
+ function namespaceView(view, namespace) {
1407
+ if (!view || !view.namespaces) return null;
1408
+ for (let i = 0; i < view.namespaces.length; i++) {
1409
+ if (view.namespaces[i].ns === namespace) return view.namespaces[i];
1410
+ }
1411
+ return null;
1412
+ }
1413
+
1414
+ /* The user layer carries the values to write back; the resolved value is the
1415
+ fallback for providers configured only by a lower layer. */
1416
+ function providerLayer(nsView) {
1417
+ if (!nsView) return null;
1418
+ return (nsView.user && nsView.user.providers) || (nsView.value && nsView.value.providers) || null;
1419
+ }
1420
+
1421
+ /* The resolved value keeps the document's real key order; the user section is
1422
+ re-projected by the host and loses it, so ordering reads from `value`. */
1423
+ function orderLayer(nsView) {
1424
+ if (!nsView) return null;
1425
+ return (nsView.value && nsView.value.providers) || (nsView.user && nsView.user.providers) || null;
1426
+ }
1427
+
1428
+ /* -------------------------------- plugin body ----------------------------- */
1429
+ /* The seat contract resolves its standard props (sessionId, useSession, …) through
1430
+ THIS plugin's own context, so every service those props read must be declared
1431
+ here — otherwise the entry throws during render and is silently abdicated. */
1432
+ const inject = ["locale", "slots", "sessions", "remote", "remote.session", "remote.settings"];
1433
+
1434
+ /* Seat registration is isolated per seat: a future seat rename, a contract change,
1435
+ or another plugin taking the same single/keyed cell must not take the whole
1436
+ plugin down with it (a swallowed error here would also hide the cause, so it
1437
+ is reported through the client logger when one is available). */
1438
+ function guard(ctx, what, fn) {
1439
+ try {
1440
+ return fn();
1441
+ } catch (error) {
1442
+ const message = "dsh-model-organizer: could not register " + what + " — " + String((error && error.message) || error);
1443
+ try {
1444
+ if (ctx.logger && typeof ctx.logger.warn === "function") ctx.logger.warn(message);
1445
+ else console.warn(message);
1446
+ } catch (e) {}
1447
+ return function () {};
1448
+ }
1449
+ }
1450
+
1451
+ function apply(ctx) {
1452
+ try { window.__dshModelOrganizerBuild = BUILD; } catch (e) {}
1453
+ ctx.effect(function () { return ctx.locale.register(NS, { zh: zh, en: en }); }, "dsh-model-organizer: dictionaries");
1454
+ const t = ctx.locale.bind(NS);
1455
+
1456
+ ctx.inject(["slots", "modelDirectories", "sessions"], function (scope) {
1457
+ const models = scope.modelDirectories;
1458
+ const sessions = scope.sessions;
1459
+ scope.slots.inject("conversation.input.model", function () {
1460
+ return guard(scope, "conversation.input.model", function () { return scope.slots.register({
1461
+ name: "conversation.input.model",
1462
+ locale: NS,
1463
+ priority: -1,
1464
+ inject: function () {
1465
+ const first = arguments.length > 0 ? arguments[0] : undefined;
1466
+ const id = first && typeof first === "object" ? first.sessionId : first;
1467
+ const directory = models.directoryFor(id);
1468
+ const available = sessions.subagentAddress(id) === undefined;
1469
+ return {
1470
+ available: available,
1471
+ directory: directory.store,
1472
+ load: function () { if (available) directory.load().catch(function () {}); },
1473
+ select: function (selection) {
1474
+ if (!available) return Promise.resolve(false);
1475
+ return directory.select(selection).then(function () { return true; }, function () { return false; });
1476
+ },
1477
+ t: t
1478
+ };
1479
+ }
1480
+ }, ProviderGroupedModelSelect); });
1481
+ });
1482
+ });
1483
+
1484
+ ctx.inject(["slots", "settingsScope", "remote", "remote.settings"], function (scope) {
1485
+ const mirror = scope.settingsScope.describe();
1486
+ mirror.ensure();
1487
+ const mutate = function (ops, revision) { return scope.remote.settings.mutate(PI_AI_NS, ops, revision); };
1488
+
1489
+ const face = function () {
1490
+ return { store: mirror, namespace: PI_AI_NS, mutate: mutate, t: t };
1491
+ };
1492
+
1493
+ /* NOTE: `settings.models.provider-card` is deliberately NOT taken. The owner
1494
+ dispatches it keyed by the provider's settings namespace, and
1495
+ @linxin666/dsh-client-ui-model-capabilities already registers that exact
1496
+ key; a keyed cell holds ONE occupant, so registering here throws and
1497
+ silently suppresses their "模型能力" panel. This plugin stays in the
1498
+ footer seat it owns instead. */
1499
+
1500
+ scope.slots.inject("settings.models.footer", function () {
1501
+ return guard(scope, "settings.models.footer", function () {
1502
+ return scope.slots.register({
1503
+ name: "settings.models.footer",
1504
+ id: "model-organizer-provider-order",
1505
+ order: 100,
1506
+ inject: face
1507
+ }, ProviderOrderPanel);
1508
+ });
1509
+ });
1510
+ });
1511
+ }
1512
+
1513
+ module.exports = { name: "model-organizer", apply: apply, inject: inject };
1514
+ return module.exports;
1515
+ }
1516
+ });