@x1a0f3n9/dsh-failover-queue 0.1.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 ADDED
@@ -0,0 +1,2261 @@
1
+ window.__ModuleLoader__.load({ id: "@x1a0f3n9/dsh-failover-queue", factory: (require) => {
2
+ var module = { exports: {} }; var exports = module.exports;
3
+ let react = require("react");
4
+ let react_dom = require("react-dom");
5
+ let react_jsx_runtime = require("react/jsx-runtime");
6
+
7
+ //#region src/types.ts
8
+ /** Marker the `/failover __candidates` handler prefixes onto JSON. */
9
+ const CANDIDATES_MARKER = "FAILOVER_CANDIDATES_V1";
10
+
11
+ //#endregion
12
+ //#region src/candidates.ts
13
+ /**
14
+ * Parse `/failover __candidates` command text into catalog rows.
15
+ * @param text - command success text, possibly including the marker prefix.
16
+ * @returns recognized provider+model rows; unknown text is empty.
17
+ */
18
+ function candidatesFromText(text) {
19
+ const marker = text.indexOf(CANDIDATES_MARKER);
20
+ if (marker < 0) return [];
21
+ const json = text.slice(marker + CANDIDATES_MARKER.length).trim();
22
+ try {
23
+ const parsed = JSON.parse(json);
24
+ if (!Array.isArray(parsed)) return [];
25
+ return parsed.flatMap((row) => {
26
+ if (typeof row !== "object" || row === null) return [];
27
+ const item = row;
28
+ if (typeof item.provider !== "string" || typeof item.model !== "string") return [];
29
+ return [{
30
+ provider: item.provider,
31
+ providerName: typeof item.providerName === "string" ? item.providerName : item.provider,
32
+ model: item.model,
33
+ name: typeof item.name === "string" ? item.name : item.model
34
+ }];
35
+ });
36
+ } catch {
37
+ return [];
38
+ }
39
+ }
40
+ /**
41
+ * Flatten `session/modelCatalog` into the same provider+model rows.
42
+ * @param value - RPC value, or undefined when the call failed.
43
+ * @returns one row per model in successful groups.
44
+ */
45
+ function candidatesFromCatalog(value) {
46
+ if (typeof value !== "object" || value === null) return [];
47
+ const groups = value.groups;
48
+ if (!Array.isArray(groups)) return [];
49
+ return groups.flatMap((group) => {
50
+ if (typeof group !== "object" || group === null) return [];
51
+ const row = group;
52
+ if (typeof row.id !== "string" || !Array.isArray(row.models)) return [];
53
+ const providerName = typeof row.name === "string" ? row.name : row.id;
54
+ return row.models.flatMap((model) => {
55
+ if (typeof model !== "object" || model === null) return [];
56
+ const item = model;
57
+ if (typeof item.id !== "string") return [];
58
+ return [{
59
+ provider: row.id,
60
+ providerName,
61
+ model: item.id,
62
+ name: typeof item.name === "string" ? item.name : item.id
63
+ }];
64
+ });
65
+ });
66
+ }
67
+ /**
68
+ * Group flattened catalog rows by provider, keeping first-seen order.
69
+ * @param rows - available provider+model rows.
70
+ * @returns one group per provider.
71
+ */
72
+ function groupCandidatesByProvider(rows) {
73
+ const groups = [];
74
+ const index = /* @__PURE__ */ new Map();
75
+ for (const row of rows) {
76
+ const at = index.get(row.provider);
77
+ if (at === void 0) {
78
+ index.set(row.provider, groups.length);
79
+ groups.push({
80
+ provider: row.provider,
81
+ providerName: row.providerName,
82
+ models: [row]
83
+ });
84
+ continue;
85
+ }
86
+ const current = groups[at];
87
+ if (current === void 0) continue;
88
+ groups[at] = {
89
+ ...current,
90
+ models: [...current.models, row]
91
+ };
92
+ }
93
+ return groups;
94
+ }
95
+
96
+ //#endregion
97
+ //#region src/queue.ts
98
+ /** Stable identity for cooldown and de-dupe. */
99
+ function routeKey(route) {
100
+ return `${route.provider}\0${route.model}`;
101
+ }
102
+ /**
103
+ * Clamp a queue index into `[0, length)` (or `0` when empty).
104
+ * @param index - requested index.
105
+ * @param length - queue length.
106
+ * @returns a usable index.
107
+ */
108
+ function clampIndex(index, length) {
109
+ if (length <= 0) return 0;
110
+ if (!Number.isFinite(index)) return 0;
111
+ const whole = Math.trunc(index);
112
+ if (whole < 0) return 0;
113
+ if (whole >= length) return length - 1;
114
+ return whole;
115
+ }
116
+ /**
117
+ * Move one queue item from `from` to `to`.
118
+ * @param queue - current ordered routes.
119
+ * @param from - source index.
120
+ * @param to - destination index.
121
+ * @returns a new array.
122
+ */
123
+ function reorderQueue(queue, from, to) {
124
+ if (from === to) return [...queue];
125
+ if (from < 0 || from >= queue.length) return [...queue];
126
+ if (to < 0 || to >= queue.length) return [...queue];
127
+ const next = [...queue];
128
+ const [item] = next.splice(from, 1);
129
+ if (item === void 0) return next;
130
+ next.splice(to, 0, item);
131
+ return next;
132
+ }
133
+ /**
134
+ * Follow the same route after a drag, not the same slot.
135
+ * @param currentIndex - active index before the move.
136
+ * @param from - dragged index.
137
+ * @param to - drop index.
138
+ * @returns the index of the previously active route.
139
+ */
140
+ function indexAfterReorder(currentIndex, from, to) {
141
+ if (currentIndex === from) return to;
142
+ if (from < currentIndex && to >= currentIndex) return currentIndex - 1;
143
+ if (from > currentIndex && to <= currentIndex) return currentIndex + 1;
144
+ return currentIndex;
145
+ }
146
+ /**
147
+ * Drop duplicate provider+model pairs, keeping the first occurrence.
148
+ * @param queue - possibly messy user input.
149
+ */
150
+ function dedupeQueue(queue) {
151
+ const seen = /* @__PURE__ */ new Set();
152
+ const next = [];
153
+ for (const route of queue) {
154
+ const provider = route.provider.trim();
155
+ const model = route.model.trim();
156
+ if (provider === "" || model === "") continue;
157
+ const key = routeKey({
158
+ provider,
159
+ model
160
+ });
161
+ if (seen.has(key)) continue;
162
+ seen.add(key);
163
+ next.push({
164
+ provider,
165
+ model,
166
+ ...route.label === void 0 || route.label.trim() === "" ? {} : { label: route.label.trim() }
167
+ });
168
+ }
169
+ return next;
170
+ }
171
+ /**
172
+ * Names the chip and queue rows show for one route.
173
+ * Prefers the live catalog's provider/model titles, then the stored label.
174
+ * @param route - queue slot.
175
+ * @param candidates - advertised catalog, possibly empty.
176
+ */
177
+ function routeDisplay(route, candidates = []) {
178
+ const hit = candidates.find((row) => row.provider === route.provider && row.model === route.model);
179
+ return {
180
+ provider: hit?.providerName.trim() || route.provider,
181
+ model: route.label?.trim() || hit?.name.trim() || route.model
182
+ };
183
+ }
184
+
185
+ //#endregion
186
+ //#region src/client/place.ts
187
+ const COMPOSER_PANEL_MARGIN = 8;
188
+ const COMPOSER_PANEL_GAP = 8;
189
+ const COMPOSER_PANEL_MAX_HEIGHT = 420;
190
+ function clamp(value, min, max) {
191
+ return Math.min(Math.max(value, min), max);
192
+ }
193
+ /**
194
+ * Cap the composer popover so it can sit inside the viewport with 8px gutters.
195
+ * @param viewportHeight - `window.innerHeight`.
196
+ * @returns CSS max-height in pixels.
197
+ */
198
+ function composerPanelMaxHeight(viewportHeight) {
199
+ return Math.min(COMPOSER_PANEL_MAX_HEIGHT, Math.max(0, viewportHeight - COMPOSER_PANEL_MARGIN * 2));
200
+ }
201
+ /**
202
+ * Prefer above the chip; go below only when the full panel fits; otherwise clamp.
203
+ * @param input - trigger box, measured size, and viewport.
204
+ * @returns fixed `left`/`top` plus the height cap to apply.
205
+ */
206
+ function placeComposerPanel(input) {
207
+ const maxHeight = composerPanelMaxHeight(input.viewportHeight);
208
+ const height = Math.min(Math.max(0, input.height), maxHeight);
209
+ const maxWidth = Math.max(0, input.viewportWidth - COMPOSER_PANEL_MARGIN * 2);
210
+ const width = Math.min(Math.max(0, input.width), maxWidth);
211
+ const left = clamp(input.trigger.right - width, COMPOSER_PANEL_MARGIN, Math.max(COMPOSER_PANEL_MARGIN, input.viewportWidth - width - COMPOSER_PANEL_MARGIN));
212
+ const minTop = COMPOSER_PANEL_MARGIN;
213
+ const maxTop = Math.max(minTop, input.viewportHeight - height - COMPOSER_PANEL_MARGIN);
214
+ const above = input.trigger.top - COMPOSER_PANEL_GAP - height;
215
+ const below = input.trigger.bottom + COMPOSER_PANEL_GAP;
216
+ let top;
217
+ if (above >= minTop) top = above;
218
+ else if (below + height <= input.viewportHeight - COMPOSER_PANEL_MARGIN) top = below;
219
+ else top = clamp(above, minTop, maxTop);
220
+ return {
221
+ left,
222
+ top,
223
+ maxHeight
224
+ };
225
+ }
226
+
227
+ //#endregion
228
+ //#region src/circuit.ts
229
+ /**
230
+ * Badge tone for one queue row.
231
+ * @param health - host snapshot for this route, if any.
232
+ */
233
+ function circuitTone(health) {
234
+ if (health === void 0) return "ok";
235
+ if (health.state === "open") return "open";
236
+ if (health.state === "half_open") return "probe";
237
+ return health.failures > 0 ? "probe" : "ok";
238
+ }
239
+ /**
240
+ * Match a queue route to a health row.
241
+ * @param route - queue slot.
242
+ * @param circuits - host snapshot.
243
+ */
244
+ function healthFor(route, circuits) {
245
+ return circuits.find((row) => row.provider === route.provider && row.model === route.model);
246
+ }
247
+
248
+ //#endregion
249
+ //#region src/client/styles.ts
250
+ /** Class names shared between the injected DOM and the stylesheet. */
251
+ const CLASS = {
252
+ chip: "dsh-fq-chip",
253
+ chipOn: "dsh-fq-chip-on",
254
+ chipKicker: "dsh-fq-chip-kicker",
255
+ chipPriority: "dsh-fq-chip-priority",
256
+ chipRoute: "dsh-fq-chip-route",
257
+ page: "dsh-fq-page",
258
+ pageIntro: "dsh-fq-page-intro",
259
+ panel: "dsh-fq-panel",
260
+ panelPage: "dsh-fq-panel-page",
261
+ head: "dsh-fq-head",
262
+ title: "dsh-fq-title",
263
+ switchRow: "dsh-fq-switch-row",
264
+ switchCopy: "dsh-fq-switch-copy",
265
+ hint: "dsh-fq-hint",
266
+ list: "dsh-fq-list",
267
+ row: "dsh-fq-row",
268
+ rowActive: "dsh-fq-row-active",
269
+ handle: "dsh-fq-handle",
270
+ health: "dsh-fq-health",
271
+ healthOk: "dsh-fq-health-ok",
272
+ healthProbe: "dsh-fq-health-probe",
273
+ healthOpen: "dsh-fq-health-open",
274
+ badge: "dsh-fq-badge",
275
+ meta: "dsh-fq-meta",
276
+ name: "dsh-fq-name",
277
+ sub: "dsh-fq-sub",
278
+ remove: "dsh-fq-remove",
279
+ picker: "dsh-fq-picker",
280
+ pickerBtn: "dsh-fq-picker-btn",
281
+ menu: "dsh-fq-menu",
282
+ search: "dsh-fq-search",
283
+ catalog: "dsh-fq-catalog",
284
+ catalogItem: "dsh-fq-catalog-item",
285
+ catalogProvider: "dsh-fq-catalog-provider",
286
+ chevron: "dsh-fq-chevron",
287
+ back: "dsh-fq-back",
288
+ empty: "dsh-fq-empty",
289
+ toggle: "dsh-fq-toggle",
290
+ toggleOn: "dsh-fq-toggle-on",
291
+ toggleThumb: "dsh-fq-toggle-thumb"
292
+ };
293
+ /** One injected stylesheet scoped under `.dsh-fq-*`. */
294
+ const STYLE = `
295
+ .dsh-fq-chip {
296
+ display: inline-flex;
297
+ align-items: center;
298
+ gap: 4px;
299
+ max-width: min(280px, 42vw);
300
+ height: 22px;
301
+ padding: 0 8px;
302
+ border: 0;
303
+ border-radius: 6px;
304
+ background: var(--dsw-alias-fill-tsp-secondary, rgba(255,255,255,0.06));
305
+ color: var(--dsw-alias-label-secondary, inherit);
306
+ font-size: 12px;
307
+ line-height: 22px;
308
+ font-weight: 500;
309
+ letter-spacing: 0.01em;
310
+ cursor: pointer;
311
+ overflow: hidden;
312
+ white-space: nowrap;
313
+ }
314
+ .dsh-fq-chip:hover {
315
+ background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.10));
316
+ }
317
+ .dsh-fq-chip-on {
318
+ background: color-mix(in srgb, var(--dsw-alias-label-accent, #34d399) 16%, transparent);
319
+ color: var(--dsw-alias-label-accent, #34d399);
320
+ }
321
+ .dsh-fq-chip-kicker {
322
+ flex-shrink: 0;
323
+ font-weight: 600;
324
+ }
325
+ .dsh-fq-chip-priority {
326
+ flex-shrink: 0;
327
+ font-weight: 700;
328
+ }
329
+ .dsh-fq-chip-route {
330
+ min-width: 0;
331
+ overflow: hidden;
332
+ text-overflow: ellipsis;
333
+ white-space: nowrap;
334
+ }
335
+ .dsh-fq-panel {
336
+ position: relative;
337
+ z-index: auto;
338
+ width: 320px;
339
+ max-height: min(420px, calc(100vh - 16px));
340
+ overflow: auto;
341
+ padding: 12px;
342
+ border: 1px solid var(--dsw-alias-border-l2, rgba(255,255,255,0.12));
343
+ border-radius: 12px;
344
+ background: var(--dsw-specific-menu, var(--dsw-alias-bg-layer-3, #1c1c1e));
345
+ box-shadow: var(--dsw-shadow-lv3, 0 12px 40px rgba(0,0,0,0.35));
346
+ color: var(--dsw-alias-label-primary, inherit);
347
+ font-size: 13px;
348
+ line-height: 18px;
349
+ }
350
+ .dsh-fq-page {
351
+ max-width: 560px;
352
+ padding: 4px 0 24px;
353
+ }
354
+ .dsh-fq-page-intro {
355
+ margin: 0 0 12px;
356
+ color: var(--dsw-alias-label-tertiary, #8e8e93);
357
+ font-size: 13px;
358
+ line-height: 18px;
359
+ }
360
+ .dsh-fq-page .dsh-fq-panel,
361
+ .dsh-fq-panel.dsh-fq-panel-page {
362
+ position: static;
363
+ z-index: auto;
364
+ width: auto;
365
+ max-height: none;
366
+ overflow: visible;
367
+ box-shadow: none;
368
+ }
369
+ .dsh-fq-head {
370
+ display: flex;
371
+ align-items: center;
372
+ justify-content: space-between;
373
+ gap: 8px;
374
+ margin-bottom: 8px;
375
+ }
376
+ .dsh-fq-title {
377
+ font-size: 14px;
378
+ font-weight: 600;
379
+ }
380
+ .dsh-fq-switch-row {
381
+ display: flex;
382
+ align-items: center;
383
+ justify-content: space-between;
384
+ gap: 12px;
385
+ margin-bottom: 8px;
386
+ }
387
+ .dsh-fq-switch-copy {
388
+ display: flex;
389
+ flex-direction: column;
390
+ gap: 2px;
391
+ min-width: 0;
392
+ }
393
+ .dsh-fq-switch-copy small {
394
+ color: var(--dsw-alias-label-tertiary, #8e8e93);
395
+ font-size: 11px;
396
+ }
397
+ .dsh-fq-hint, .dsh-fq-empty {
398
+ color: var(--dsw-alias-label-tertiary, #8e8e93);
399
+ font-size: 12px;
400
+ margin: 0 0 8px;
401
+ }
402
+ .dsh-fq-list {
403
+ display: flex;
404
+ flex-direction: column;
405
+ gap: 4px;
406
+ margin: 0 0 8px;
407
+ padding: 0;
408
+ list-style: none;
409
+ }
410
+ .dsh-fq-row {
411
+ display: grid;
412
+ grid-template-columns: 16px 8px 32px minmax(0, 1fr) 28px;
413
+ align-items: center;
414
+ gap: 6px;
415
+ padding: 6px 8px;
416
+ border-radius: 8px;
417
+ background: var(--dsw-alias-fill-tsp-secondary, rgba(255,255,255,0.04));
418
+ cursor: pointer;
419
+ }
420
+ .dsh-fq-row:hover {
421
+ background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.08));
422
+ }
423
+ .dsh-fq-row-active {
424
+ outline: 1px solid color-mix(in srgb, var(--dsw-alias-label-accent, #34d399) 50%, transparent);
425
+ }
426
+ .dsh-fq-handle {
427
+ display: inline-flex;
428
+ align-items: center;
429
+ justify-content: center;
430
+ width: 16px;
431
+ height: 16px;
432
+ color: var(--dsw-alias-label-tertiary, #8e8e93);
433
+ cursor: grab;
434
+ }
435
+ .dsh-fq-handle:active { cursor: grabbing; }
436
+ .dsh-fq-health {
437
+ width: 8px;
438
+ height: 8px;
439
+ border-radius: 50%;
440
+ justify-self: center;
441
+ }
442
+ .dsh-fq-health-ok { background: #34d399; }
443
+ .dsh-fq-health-probe { background: #fbbf24; }
444
+ .dsh-fq-health-open { background: #f87171; }
445
+ .dsh-fq-badge {
446
+ font-size: 11px;
447
+ font-weight: 700;
448
+ color: var(--dsw-alias-label-accent, #34d399);
449
+ }
450
+ .dsh-fq-meta {
451
+ min-width: 0;
452
+ display: flex;
453
+ flex-direction: column;
454
+ }
455
+ .dsh-fq-name {
456
+ overflow: hidden;
457
+ text-overflow: ellipsis;
458
+ white-space: nowrap;
459
+ font-weight: 500;
460
+ }
461
+ .dsh-fq-sub {
462
+ overflow: hidden;
463
+ text-overflow: ellipsis;
464
+ white-space: nowrap;
465
+ color: var(--dsw-alias-label-tertiary, #8e8e93);
466
+ font-size: 11px;
467
+ }
468
+ .dsh-fq-remove {
469
+ border: 0;
470
+ background: transparent;
471
+ color: var(--dsw-alias-label-tertiary, #8e8e93);
472
+ cursor: pointer;
473
+ border-radius: 6px;
474
+ height: 24px;
475
+ padding: 0 6px;
476
+ }
477
+ .dsh-fq-remove:hover {
478
+ color: var(--dsw-alias-label-primary, inherit);
479
+ background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.08));
480
+ }
481
+ .dsh-fq-picker {
482
+ position: relative;
483
+ margin-top: 8px;
484
+ }
485
+ .dsh-fq-picker-btn {
486
+ width: 100%;
487
+ height: 28px;
488
+ padding: 0 8px;
489
+ border-radius: 6px;
490
+ border: 1px solid var(--dsw-alias-border-l2, rgba(255,255,255,0.12));
491
+ background: var(--dsw-alias-bg-layer-2, #141416);
492
+ color: var(--dsw-alias-label-primary, #f5f5f7);
493
+ font-size: 12px;
494
+ text-align: left;
495
+ cursor: pointer;
496
+ }
497
+ .dsh-fq-picker-btn:disabled {
498
+ cursor: default;
499
+ opacity: 0.65;
500
+ }
501
+ .dsh-fq-menu {
502
+ display: flex;
503
+ flex-direction: column;
504
+ gap: 6px;
505
+ padding: 8px;
506
+ overflow: hidden;
507
+ border: 1px solid var(--dsw-alias-border-l2, rgba(255,255,255,0.12));
508
+ border-radius: 8px;
509
+ background: var(--dsw-specific-menu, var(--dsw-alias-bg-layer-3, #1c1c1e));
510
+ box-shadow: var(--dsw-shadow-lv3, 0 12px 40px rgba(0,0,0,0.45));
511
+ color: var(--dsw-alias-label-primary, #f5f5f7);
512
+ box-sizing: border-box;
513
+ }
514
+ .dsh-fq-search {
515
+ width: 100%;
516
+ box-sizing: border-box;
517
+ flex-shrink: 0;
518
+ height: 28px;
519
+ border-radius: 6px;
520
+ border: 1px solid var(--dsw-alias-border-l2, rgba(255,255,255,0.12));
521
+ background: var(--dsw-alias-bg-layer-2, #141416);
522
+ color: var(--dsw-alias-label-primary, #f5f5f7);
523
+ font-size: 12px;
524
+ padding: 0 8px;
525
+ }
526
+ .dsh-fq-search::placeholder {
527
+ color: var(--dsw-alias-label-tertiary, #8e8e93);
528
+ }
529
+ .dsh-fq-catalog {
530
+ display: flex;
531
+ flex-direction: column;
532
+ flex: 1;
533
+ margin: 0;
534
+ padding: 0;
535
+ list-style: none;
536
+ min-height: 0;
537
+ overflow: auto;
538
+ }
539
+ .dsh-fq-catalog-item {
540
+ display: flex;
541
+ flex-direction: column;
542
+ align-items: flex-start;
543
+ gap: 1px;
544
+ width: 100%;
545
+ margin: 0;
546
+ padding: 5px 10px;
547
+ border: 0;
548
+ background: transparent;
549
+ color: var(--dsw-alias-label-primary, #f5f5f7);
550
+ text-align: left;
551
+ cursor: pointer;
552
+ }
553
+ .dsh-fq-catalog-item:hover:not(:disabled) {
554
+ background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.08));
555
+ }
556
+ .dsh-fq-catalog-item:disabled {
557
+ cursor: default;
558
+ opacity: 0.6;
559
+ }
560
+
561
+ .dsh-fq-catalog-provider {
562
+ flex-direction: row;
563
+ align-items: center;
564
+ justify-content: space-between;
565
+ gap: 8px;
566
+ }
567
+ .dsh-fq-chevron {
568
+ flex-shrink: 0;
569
+ color: var(--dsw-alias-label-tertiary, #8e8e93);
570
+ font-size: 16px;
571
+ line-height: 1;
572
+ }
573
+ .dsh-fq-back {
574
+ flex-shrink: 0;
575
+ align-self: flex-start;
576
+ height: 24px;
577
+ padding: 0 6px;
578
+ border: 0;
579
+ border-radius: 6px;
580
+ background: transparent;
581
+ color: var(--dsw-alias-label-secondary, #d1d1d6);
582
+ font-size: 12px;
583
+ cursor: pointer;
584
+ }
585
+ .dsh-fq-back:hover {
586
+ background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.08));
587
+ color: var(--dsw-alias-label-primary, #f5f5f7);
588
+ }
589
+ .dsh-fq-toggle {
590
+ position: relative;
591
+ width: 36px;
592
+ height: 20px;
593
+ flex-shrink: 0;
594
+ border: 0;
595
+ border-radius: 10px;
596
+ background: var(--dsw-alias-fill-tsp-secondary, rgba(255,255,255,0.18));
597
+ cursor: pointer;
598
+ padding: 0;
599
+ }
600
+ .dsh-fq-toggle-on {
601
+ background: var(--dsw-alias-label-accent, #34d399);
602
+ }
603
+ .dsh-fq-toggle-thumb {
604
+ position: absolute;
605
+ top: 2px;
606
+ left: 2px;
607
+ width: 16px;
608
+ height: 16px;
609
+ border-radius: 50%;
610
+ background: #fff;
611
+ transition: transform 120ms ease;
612
+ }
613
+ .dsh-fq-toggle-on .dsh-fq-toggle-thumb {
614
+ transform: translateX(16px);
615
+ }
616
+ `;
617
+
618
+ //#endregion
619
+ //#region src/client/QueuePanel.tsx
620
+ function candidateMatches(row, query) {
621
+ const needle = query.trim().toLowerCase();
622
+ if (needle === "") return true;
623
+ return [
624
+ row.provider,
625
+ row.providerName,
626
+ row.model,
627
+ row.name
628
+ ].some((part) => part.toLowerCase().includes(needle));
629
+ }
630
+ /**
631
+ * Drag-reorder P1/P2/P3 list plus the auto-failover switch.
632
+ * @param props - live snapshot, settings writes, locale.
633
+ */
634
+ function QueuePanel({ state, api, t, onClose, embedded = false }) {
635
+ const [pending, setPending] = (0, react.useState)(false);
636
+ const [query, setQuery] = (0, react.useState)("");
637
+ const [menuOpen, setMenuOpen] = (0, react.useState)(false);
638
+ const [selectedProvider, setSelectedProvider] = (0, react.useState)(null);
639
+ const pickerRef = (0, react.useRef)(null);
640
+ const menuRef = (0, react.useRef)(null);
641
+ const [menuPos, setMenuPos] = (0, react.useState)(null);
642
+ const available = (0, react.useMemo)(() => {
643
+ const taken = new Set(state.queue.map(routeKey));
644
+ return state.candidates.filter((row) => !taken.has(routeKey({
645
+ provider: row.provider,
646
+ model: row.model
647
+ })));
648
+ }, [state.candidates, state.queue]);
649
+ const groups = (0, react.useMemo)(() => groupCandidatesByProvider(available), [available]);
650
+ const needle = query.trim().toLowerCase();
651
+ const visibleGroups = (0, react.useMemo)(() => {
652
+ if (needle === "") return groups;
653
+ return groups.filter((group) => group.provider.toLowerCase().includes(needle) || group.providerName.toLowerCase().includes(needle) || group.models.some((row) => candidateMatches(row, query)));
654
+ }, [
655
+ groups,
656
+ needle,
657
+ query
658
+ ]);
659
+ const selectedGroup = selectedProvider === null ? void 0 : groups.find((group) => group.provider === selectedProvider);
660
+ const visibleModels = (0, react.useMemo)(() => {
661
+ if (selectedGroup === void 0) return [];
662
+ if (needle === "") return [...selectedGroup.models];
663
+ return selectedGroup.models.filter((row) => candidateMatches(row, query));
664
+ }, [
665
+ selectedGroup,
666
+ needle,
667
+ query
668
+ ]);
669
+ const closeMenu = () => {
670
+ setMenuOpen(false);
671
+ setSelectedProvider(null);
672
+ setQuery("");
673
+ };
674
+ (0, react.useEffect)(() => {
675
+ if (!menuOpen) return;
676
+ const onDoc = (event) => {
677
+ const target = event.target;
678
+ if (pickerRef.current?.contains(target) === true) return;
679
+ if (menuRef.current?.contains(target) === true) return;
680
+ closeMenu();
681
+ };
682
+ const onKey = (event) => {
683
+ if (event.key !== "Escape") return;
684
+ if (selectedProvider !== null) {
685
+ setSelectedProvider(null);
686
+ return;
687
+ }
688
+ closeMenu();
689
+ };
690
+ document.addEventListener("mousedown", onDoc);
691
+ document.addEventListener("keydown", onKey);
692
+ return () => {
693
+ document.removeEventListener("mousedown", onDoc);
694
+ document.removeEventListener("keydown", onKey);
695
+ };
696
+ }, [menuOpen, selectedProvider]);
697
+ (0, react.useLayoutEffect)(() => {
698
+ if (!menuOpen) {
699
+ setMenuPos(null);
700
+ return;
701
+ }
702
+ const place = () => {
703
+ const trigger = pickerRef.current;
704
+ if (trigger === null) return;
705
+ const rect = trigger.getBoundingClientRect();
706
+ const width = Math.max(rect.width, 280);
707
+ const maxHeight = Math.min(320, Math.floor(window.innerHeight * .5));
708
+ const roomAbove = rect.top - 8;
709
+ const roomBelow = window.innerHeight - rect.bottom - 8;
710
+ const top = roomAbove >= 160 || roomAbove >= roomBelow ? Math.max(8, rect.top - 6 - maxHeight) : Math.min(rect.bottom + 6, window.innerHeight - maxHeight - 8);
711
+ setMenuPos({
712
+ position: "fixed",
713
+ left: Math.min(rect.left, window.innerWidth - width - 8),
714
+ width,
715
+ top,
716
+ maxHeight,
717
+ zIndex: 2100
718
+ });
719
+ };
720
+ place();
721
+ window.addEventListener("resize", place);
722
+ window.addEventListener("scroll", place, true);
723
+ return () => {
724
+ window.removeEventListener("resize", place);
725
+ window.removeEventListener("scroll", place, true);
726
+ };
727
+ }, [
728
+ menuOpen,
729
+ selectedProvider,
730
+ visibleGroups.length,
731
+ visibleModels.length,
732
+ query
733
+ ]);
734
+ const run = (work) => {
735
+ if (pending) return;
736
+ setPending(true);
737
+ work().finally(() => {
738
+ setPending(false);
739
+ });
740
+ };
741
+ const onDrop = (to, event) => {
742
+ event.preventDefault();
743
+ const from = Number(event.dataTransfer.getData("text/plain"));
744
+ if (!Number.isInteger(from) || from === to) return;
745
+ const queue = reorderQueue(state.queue, from, to);
746
+ const currentIndex = indexAfterReorder(state.currentIndex, from, to);
747
+ run(() => api.setQueue(queue, currentIndex));
748
+ };
749
+ const addCandidate = (row) => {
750
+ const route = {
751
+ provider: row.provider,
752
+ model: row.model,
753
+ label: row.name
754
+ };
755
+ closeMenu();
756
+ run(() => api.setQueue([...state.queue, route], state.currentIndex));
757
+ };
758
+ const pickerLabel = !state.candidatesLoaded ? t("panel.add.loading") : groups.length === 0 ? t("panel.add.none") : t("panel.add.placeholderCount", { count: groups.length });
759
+ const emptyMenu = selectedProvider === null ? visibleGroups.length === 0 : visibleModels.length === 0;
760
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
761
+ className: embedded ? `${CLASS.panel} ${CLASS.panelPage}` : CLASS.panel,
762
+ role: embedded ? "region" : "dialog",
763
+ "aria-label": t("panel.title"),
764
+ children: [
765
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
766
+ className: CLASS.head,
767
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
768
+ className: CLASS.title,
769
+ children: t("panel.title")
770
+ }), onClose === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
771
+ type: "button",
772
+ className: CLASS.remove,
773
+ onClick: onClose,
774
+ children: t("panel.close")
775
+ })]
776
+ }),
777
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
778
+ className: CLASS.switchRow,
779
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
780
+ className: CLASS.switchCopy,
781
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("panel.switch") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: state.enabled ? t("panel.switch.on") : t("panel.switch.off") })]
782
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
783
+ type: "button",
784
+ role: "switch",
785
+ className: `${CLASS.toggle}${state.enabled ? ` ${CLASS.toggleOn}` : ""}`,
786
+ "aria-checked": state.enabled,
787
+ "aria-label": t("panel.switch"),
788
+ disabled: pending,
789
+ onClick: () => {
790
+ run(() => api.setEnabled(!state.enabled));
791
+ },
792
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: CLASS.toggleThumb })
793
+ })]
794
+ }),
795
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
796
+ className: CLASS.hint,
797
+ children: t("panel.hint")
798
+ }),
799
+ state.queue.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
800
+ className: CLASS.empty,
801
+ children: t("panel.empty")
802
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
803
+ className: CLASS.list,
804
+ children: state.queue.map((route, index) => {
805
+ const active = index === clampIndex(state.currentIndex, state.queue.length);
806
+ const display = routeDisplay(route, state.candidates);
807
+ const title = `${display.provider}/${display.model}`;
808
+ const tone = circuitTone(healthFor(route, state.circuits ?? []));
809
+ const healthLabel = t(tone === "ok" ? "panel.health.ok" : tone === "probe" ? "panel.health.probe" : "panel.health.open");
810
+ const healthClass = tone === "ok" ? CLASS.healthOk : tone === "probe" ? CLASS.healthProbe : CLASS.healthOpen;
811
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
812
+ className: `${CLASS.row}${active ? ` ${CLASS.rowActive}` : ""}`,
813
+ onDragOver: (event) => {
814
+ event.preventDefault();
815
+ },
816
+ onDrop: (event) => {
817
+ onDrop(index, event);
818
+ },
819
+ onClick: () => {
820
+ if (index === state.currentIndex) return;
821
+ run(() => api.setQueue(state.queue, index));
822
+ },
823
+ children: [
824
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
825
+ className: CLASS.handle,
826
+ draggable: true,
827
+ title: t("panel.drag"),
828
+ "aria-label": t("panel.drag"),
829
+ onDragStart: (event) => {
830
+ event.dataTransfer.setData("text/plain", String(index));
831
+ event.dataTransfer.effectAllowed = "move";
832
+ },
833
+ onClick: (event) => {
834
+ event.stopPropagation();
835
+ },
836
+ children: "⋮⋮"
837
+ }),
838
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
839
+ className: `${CLASS.health} ${healthClass}`,
840
+ title: healthLabel,
841
+ "aria-label": healthLabel
842
+ }),
843
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
844
+ className: CLASS.badge,
845
+ children: ["P", index + 1]
846
+ }),
847
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
848
+ className: CLASS.meta,
849
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
850
+ className: CLASS.name,
851
+ children: [title, active ? ` · ${t("panel.current")}` : ""]
852
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
853
+ className: CLASS.sub,
854
+ children: [
855
+ route.provider,
856
+ " / ",
857
+ route.model
858
+ ]
859
+ })]
860
+ }),
861
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
862
+ type: "button",
863
+ className: CLASS.remove,
864
+ "aria-label": t("panel.remove"),
865
+ onClick: (event) => {
866
+ event.stopPropagation();
867
+ const queue = state.queue.filter((_, item) => item !== index);
868
+ const currentIndex = index < state.currentIndex ? state.currentIndex - 1 : index === state.currentIndex ? clampIndex(state.currentIndex, queue.length) : state.currentIndex;
869
+ run(() => api.setQueue(queue, currentIndex));
870
+ },
871
+ children: "×"
872
+ })
873
+ ]
874
+ }, routeKey(route));
875
+ })
876
+ }),
877
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
878
+ className: CLASS.picker,
879
+ ref: pickerRef,
880
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
881
+ type: "button",
882
+ className: CLASS.pickerBtn,
883
+ disabled: pending || !state.candidatesLoaded || groups.length === 0,
884
+ "aria-expanded": menuOpen,
885
+ "aria-haspopup": "listbox",
886
+ "aria-label": t("panel.add"),
887
+ onClick: () => {
888
+ if (menuOpen) {
889
+ closeMenu();
890
+ return;
891
+ }
892
+ setMenuOpen(true);
893
+ },
894
+ children: pickerLabel
895
+ }), menuOpen && state.candidatesLoaded && groups.length > 0 && menuPos !== null ? (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
896
+ className: CLASS.menu,
897
+ ref: menuRef,
898
+ style: menuPos,
899
+ children: [
900
+ selectedProvider === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
901
+ type: "button",
902
+ className: CLASS.back,
903
+ onClick: () => {
904
+ setSelectedProvider(null);
905
+ },
906
+ children: ["← ", t("panel.add.back")]
907
+ }),
908
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
909
+ className: CLASS.search,
910
+ value: query,
911
+ autoFocus: true,
912
+ disabled: pending,
913
+ placeholder: selectedProvider === null ? t("panel.add.searchProvider") : t("panel.add.searchModel"),
914
+ "aria-label": selectedProvider === null ? t("panel.add.searchProvider") : t("panel.add.searchModel"),
915
+ onChange: (event) => {
916
+ setQuery(event.target.value);
917
+ }
918
+ }),
919
+ emptyMenu ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
920
+ className: CLASS.empty,
921
+ children: t("panel.add.nomatch")
922
+ }) : selectedProvider === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
923
+ className: CLASS.catalog,
924
+ role: "listbox",
925
+ "aria-label": t("panel.add"),
926
+ children: visibleGroups.map((group) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
927
+ type: "button",
928
+ className: `${CLASS.catalogItem} ${CLASS.catalogProvider}`,
929
+ disabled: pending,
930
+ "aria-label": `${group.providerName}, ${t("panel.add.modelCount", { count: group.models.length })}`,
931
+ onClick: () => {
932
+ setSelectedProvider(group.provider);
933
+ },
934
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
935
+ className: CLASS.meta,
936
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
937
+ className: CLASS.name,
938
+ children: group.providerName
939
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
940
+ className: CLASS.sub,
941
+ children: [
942
+ group.provider,
943
+ " · ",
944
+ t("panel.add.modelCount", { count: group.models.length })
945
+ ]
946
+ })]
947
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
948
+ className: CLASS.chevron,
949
+ "aria-hidden": "true",
950
+ children: "›"
951
+ })]
952
+ }) }, group.provider))
953
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
954
+ className: CLASS.catalog,
955
+ role: "listbox",
956
+ "aria-label": selectedGroup?.providerName ?? t("panel.add"),
957
+ children: visibleModels.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
958
+ type: "button",
959
+ className: CLASS.catalogItem,
960
+ role: "option",
961
+ disabled: pending,
962
+ "aria-label": `${t("panel.add")}: ${row.name}`,
963
+ onClick: () => {
964
+ addCandidate(row);
965
+ },
966
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
967
+ className: CLASS.name,
968
+ children: row.name
969
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
970
+ className: CLASS.sub,
971
+ children: row.model
972
+ })]
973
+ }) }, routeKey({
974
+ provider: row.provider,
975
+ model: row.model
976
+ })))
977
+ })
978
+ ]
979
+ }), document.body) : null]
980
+ })
981
+ ]
982
+ });
983
+ }
984
+
985
+ //#endregion
986
+ //#region src/client/FailoverChip.tsx
987
+ function sessionOf(props) {
988
+ return props.sessionId ?? props.getSessionId?.() ?? "";
989
+ }
990
+ function chipCopy(state, t) {
991
+ if (!state.enabled) return {
992
+ title: t("chip.title.off"),
993
+ aria: t("chip.aria.off"),
994
+ slot: void 0,
995
+ route: void 0
996
+ };
997
+ if (state.queue.length === 0) return {
998
+ title: t("chip.empty"),
999
+ aria: t("chip.empty"),
1000
+ slot: void 0,
1001
+ route: t("chip.empty")
1002
+ };
1003
+ const slot = clampIndex(state.currentIndex, state.queue.length) + 1;
1004
+ const current = state.queue[slot - 1];
1005
+ if (current === void 0) return {
1006
+ title: t("chip.empty"),
1007
+ aria: t("chip.empty"),
1008
+ slot: void 0,
1009
+ route: t("chip.empty")
1010
+ };
1011
+ const display = routeDisplay(current, state.candidates);
1012
+ const params = {
1013
+ slot,
1014
+ provider: display.provider,
1015
+ model: display.model
1016
+ };
1017
+ return {
1018
+ title: t("chip.on", params),
1019
+ aria: t("chip.aria.on", params),
1020
+ slot,
1021
+ route: `${display.provider}/${display.model}`
1022
+ };
1023
+ }
1024
+ /**
1025
+ * Composer chip: shows failover status and opens the drag panel.
1026
+ * @param props - composed slot props.
1027
+ */
1028
+ function FailoverChip(props) {
1029
+ const { useFailover, api, loadCandidates, t } = props;
1030
+ const state = useFailover((snapshot$1) => snapshot$1);
1031
+ const sessionId = sessionOf(props);
1032
+ const [open, setOpen] = (0, react.useState)(false);
1033
+ const rootRef = (0, react.useRef)(null);
1034
+ const panelRef = (0, react.useRef)(null);
1035
+ const [pos, setPos] = (0, react.useState)(null);
1036
+ const copy = chipCopy(state, t);
1037
+ (0, react.useEffect)(() => {
1038
+ loadCandidates(sessionId);
1039
+ }, [loadCandidates, sessionId]);
1040
+ (0, react.useLayoutEffect)(() => {
1041
+ if (!open) return;
1042
+ const place = () => {
1043
+ const trigger = rootRef.current;
1044
+ const panel = panelRef.current;
1045
+ if (trigger === null || panel === null) return;
1046
+ const placed = placeComposerPanel({
1047
+ trigger: trigger.getBoundingClientRect(),
1048
+ width: panel.offsetWidth,
1049
+ height: panel.offsetHeight,
1050
+ viewportWidth: window.innerWidth,
1051
+ viewportHeight: window.innerHeight
1052
+ });
1053
+ setPos({
1054
+ left: placed.left,
1055
+ top: placed.top,
1056
+ maxHeight: placed.maxHeight
1057
+ });
1058
+ };
1059
+ place();
1060
+ const observer = new ResizeObserver(place);
1061
+ if (panelRef.current !== null) observer.observe(panelRef.current);
1062
+ window.addEventListener("resize", place);
1063
+ window.addEventListener("scroll", place, true);
1064
+ return () => {
1065
+ observer.disconnect();
1066
+ window.removeEventListener("resize", place);
1067
+ window.removeEventListener("scroll", place, true);
1068
+ };
1069
+ }, [
1070
+ open,
1071
+ state.queue.length,
1072
+ state.enabled,
1073
+ copy.route
1074
+ ]);
1075
+ (0, react.useEffect)(() => {
1076
+ if (!open) return;
1077
+ const close = (event) => {
1078
+ const target = event.target;
1079
+ if (!(target instanceof Node)) return;
1080
+ if (rootRef.current?.contains(target) === true) return;
1081
+ if (panelRef.current?.contains(target) === true) return;
1082
+ if (target instanceof Element && target.closest(".dsh-fq-menu") !== null) return;
1083
+ setOpen(false);
1084
+ };
1085
+ const onKey = (event) => {
1086
+ if (event.key === "Escape") setOpen(false);
1087
+ };
1088
+ document.addEventListener("mousedown", close);
1089
+ document.addEventListener("keydown", onKey);
1090
+ return () => {
1091
+ document.removeEventListener("mousedown", close);
1092
+ document.removeEventListener("keydown", onKey);
1093
+ };
1094
+ }, [open]);
1095
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1096
+ ref: rootRef,
1097
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1098
+ type: "button",
1099
+ className: `${CLASS.chip}${state.enabled ? ` ${CLASS.chipOn}` : ""}`,
1100
+ "aria-pressed": state.enabled,
1101
+ "aria-expanded": open,
1102
+ "aria-label": copy.aria,
1103
+ title: copy.title,
1104
+ onMouseDown: (event) => {
1105
+ event.preventDefault();
1106
+ },
1107
+ onClick: () => {
1108
+ setOpen((value) => !value);
1109
+ },
1110
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1111
+ className: CLASS.chipKicker,
1112
+ children: t("chip.kicker")
1113
+ }), copy.slot === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1114
+ className: CLASS.chipRoute,
1115
+ children: copy.route ?? t("chip.off")
1116
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1117
+ className: CLASS.chipPriority,
1118
+ children: ["P", copy.slot]
1119
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1120
+ className: CLASS.chipRoute,
1121
+ children: copy.route
1122
+ })] })]
1123
+ }), open && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1124
+ ref: panelRef,
1125
+ style: {
1126
+ ...pos,
1127
+ position: "fixed",
1128
+ zIndex: 2e3,
1129
+ overflow: "auto"
1130
+ },
1131
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(QueuePanel, {
1132
+ state,
1133
+ api,
1134
+ t,
1135
+ onClose: () => {
1136
+ setOpen(false);
1137
+ }
1138
+ })
1139
+ }), document.body)]
1140
+ });
1141
+ }
1142
+ /**
1143
+ * Settings left-nav page: the same queue editor, in document flow.
1144
+ * @param props - composed slot props.
1145
+ */
1146
+ function FailoverSettingsCard(props) {
1147
+ const { useFailover, api, loadCandidates, t } = props;
1148
+ const state = useFailover((snapshot$1) => snapshot$1);
1149
+ const sessionId = sessionOf(props);
1150
+ (0, react.useEffect)(() => {
1151
+ loadCandidates(sessionId);
1152
+ }, [loadCandidates, sessionId]);
1153
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1154
+ className: CLASS.page,
1155
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1156
+ className: CLASS.pageIntro,
1157
+ children: t("settings.intro")
1158
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(QueuePanel, {
1159
+ state,
1160
+ api,
1161
+ t,
1162
+ embedded: true
1163
+ })]
1164
+ });
1165
+ }
1166
+
1167
+ //#endregion
1168
+ //#region src/client/locales.ts
1169
+ /** Locale bundles for the failover chip and drag panel. */
1170
+ const NS = "failover";
1171
+ const zh = {
1172
+ "chip.kicker": "故障转移:",
1173
+ "chip.off": "关",
1174
+ "chip.empty": "无队列",
1175
+ "chip.on": "故障转移:P{slot} {provider}/{model}",
1176
+ "chip.aria.on": "故障转移已开启,当前 P{slot} {provider}/{model},点击打开队列",
1177
+ "chip.aria.off": "故障转移已关闭,点击打开队列",
1178
+ "chip.title.off": "自动故障转移关闭。点进去开启并编排队列。",
1179
+ "panel.title": "故障转移队列",
1180
+ "panel.switch": "自动故障转移",
1181
+ "panel.switch.on": "失败按 P1 → P2 → P3 切换;P1 恢复后探活切回",
1182
+ "panel.switch.off": "只用当前会话模型,不跨路由",
1183
+ "panel.empty": "队列是空的。从下面点一条路由,P1 就是主供应商。",
1184
+ "panel.add": "加入队列",
1185
+ "panel.add.placeholderCount": "选择供应商({count})",
1186
+ "panel.add.searchProvider": "搜索供应商…",
1187
+ "panel.add.searchModel": "搜索模型…",
1188
+ "panel.add.back": "返回供应商",
1189
+ "panel.add.modelCount": "{count} 个模型",
1190
+ "panel.add.loading": "正在读取已配置的模型…",
1191
+ "panel.add.none": "没有可加的路由。先在「模型」里配供应商。",
1192
+ "panel.add.nomatch": "没有匹配的路由。",
1193
+ "panel.remove": "移除",
1194
+ "panel.drag": "拖动改优先级",
1195
+ "panel.current": "当前",
1196
+ "panel.close": "关闭",
1197
+ "panel.hint": "拖动左侧手柄调整 P1/P2/P3。失败切下一档;P1 恢复后会探活切回,不会粘在 P2。",
1198
+ "panel.health.ok": "健康",
1199
+ "panel.health.probe": "探活中",
1200
+ "panel.health.open": "已熔断",
1201
+ "settings.tab": "故障转移",
1202
+ "settings.intro": "编排 P1 / P2 / P3。开启后请求优先 P1;失败熔断后切备用档,P1 恢复后探活切回。"
1203
+ };
1204
+ const en = {
1205
+ "chip.kicker": "Failover: ",
1206
+ "chip.off": "Off",
1207
+ "chip.empty": "empty",
1208
+ "chip.on": "Failover: P{slot} {provider}/{model}",
1209
+ "chip.aria.on": "Failover on, current P{slot} {provider}/{model}, click to open the queue",
1210
+ "chip.aria.off": "Failover off, click to open the queue",
1211
+ "chip.title.off": "Auto failover is off. Click to enable and edit the queue.",
1212
+ "panel.title": "Failover queue",
1213
+ "panel.switch": "Auto failover",
1214
+ "panel.switch.on": "On failure, switch P1 → P2 → P3; recovered P1 is probed again",
1215
+ "panel.switch.off": "Use the session model only",
1216
+ "panel.empty": "Queue is empty. Click a route below. P1 is the primary.",
1217
+ "panel.add": "Add to queue",
1218
+ "panel.add.placeholderCount": "Pick a provider ({count})",
1219
+ "panel.add.searchProvider": "Search providers…",
1220
+ "panel.add.searchModel": "Search models…",
1221
+ "panel.add.back": "Back to providers",
1222
+ "panel.add.modelCount": "{count} models",
1223
+ "panel.add.loading": "Loading configured models…",
1224
+ "panel.add.none": "No routes to add. Configure a provider under Models first.",
1225
+ "panel.add.nomatch": "No matching routes.",
1226
+ "panel.remove": "Remove",
1227
+ "panel.drag": "Drag to reorder",
1228
+ "panel.current": "current",
1229
+ "panel.close": "Close",
1230
+ "panel.hint": "Drag the handle to change P1/P2/P3. Failover is not sticky: P1 is probed again after it recovers.",
1231
+ "panel.health.ok": "healthy",
1232
+ "panel.health.probe": "probing",
1233
+ "panel.health.open": "open",
1234
+ "settings.tab": "Failover",
1235
+ "settings.intro": "Arrange P1 / P2 / P3. While on, requests prefer P1. A recovered P1 is probed and selected again."
1236
+ };
1237
+
1238
+ //#endregion
1239
+ //#region ../deepseek-harness/vendor/cosmokit/src/misc.ts
1240
+ /** Return true when a value is `null` or `undefined`. */
1241
+ function isNullable(value) {
1242
+ return value === null || value === void 0;
1243
+ }
1244
+ /** Return true for non-array object values. */
1245
+ function isPlainObject(data) {
1246
+ return data && typeof data === "object" && !Array.isArray(data);
1247
+ }
1248
+ /** Filter object entries and return a new object. */
1249
+ function filterKeys(object, filter) {
1250
+ return Object.fromEntries(Object.entries(object).filter(([key, value]) => filter(key, value)));
1251
+ }
1252
+ /** Map object values while preserving the original key set. */
1253
+ function mapValues(object, transform) {
1254
+ return Object.fromEntries(Object.entries(object).map(([key, value]) => [key, transform(value, key)]));
1255
+ }
1256
+ /** Pick selected keys from an object, optionally including `undefined` values. */
1257
+ function pick(source, keys, forced) {
1258
+ if (!keys) return { ...source };
1259
+ const result = {};
1260
+ for (const key of keys) if (forced || source[key] !== void 0) result[key] = source[key];
1261
+ return result;
1262
+ }
1263
+
1264
+ //#endregion
1265
+ //#region ../deepseek-harness/vendor/cosmokit/src/types.ts
1266
+ /** Test values using `instanceof` with a `toStringTag` fallback. */
1267
+ function is(type, value) {
1268
+ if (arguments.length === 1) return (value$1) => is(type, value$1);
1269
+ return type in globalThis && value instanceof globalThis[type] || Object.prototype.toString.call(value).slice(8, -1) === type;
1270
+ }
1271
+ function isArrayBufferLike(value) {
1272
+ return is("ArrayBuffer", value) || is("SharedArrayBuffer", value);
1273
+ }
1274
+ function isArrayBufferSource(value) {
1275
+ return isArrayBufferLike(value) || ArrayBuffer.isView(value);
1276
+ }
1277
+ let Binary;
1278
+ (function(_Binary) {
1279
+ _Binary.is = isArrayBufferLike;
1280
+ _Binary.isSource = isArrayBufferSource;
1281
+ function fromSource(source) {
1282
+ if (ArrayBuffer.isView(source)) return source.buffer.slice(source.byteOffset, source.byteOffset + source.byteLength);
1283
+ else return source;
1284
+ }
1285
+ _Binary.fromSource = fromSource;
1286
+ function toBase64(source) {
1287
+ source = fromSource(source);
1288
+ if (typeof Buffer !== "undefined") return Buffer.from(source).toString("base64");
1289
+ let binary = "";
1290
+ const bytes = new Uint8Array(source);
1291
+ for (let i = 0; i < bytes.byteLength; i++) binary += String.fromCharCode(bytes[i]);
1292
+ return btoa(binary);
1293
+ }
1294
+ _Binary.toBase64 = toBase64;
1295
+ function fromBase64(source) {
1296
+ if (typeof Buffer !== "undefined") return fromSource(Buffer.from(source, "base64"));
1297
+ return Uint8Array.from(atob(source), (c) => c.charCodeAt(0));
1298
+ }
1299
+ _Binary.fromBase64 = fromBase64;
1300
+ function toHex(source) {
1301
+ source = fromSource(source);
1302
+ if (typeof Buffer !== "undefined") return Buffer.from(source).toString("hex");
1303
+ return Array.from(new Uint8Array(source), (byte) => byte.toString(16).padStart(2, "0")).join("");
1304
+ }
1305
+ _Binary.toHex = toHex;
1306
+ function fromHex(source) {
1307
+ if (typeof Buffer !== "undefined") return fromSource(Buffer.from(source, "hex"));
1308
+ const hex = source.length % 2 === 0 ? source : source.slice(0, source.length - 1);
1309
+ const buffer = [];
1310
+ for (let i = 0; i < hex.length; i += 2) buffer.push(parseInt(`${hex[i]}${hex[i + 1]}`, 16));
1311
+ return Uint8Array.from(buffer).buffer;
1312
+ }
1313
+ _Binary.fromHex = fromHex;
1314
+ })(Binary || (Binary = {}));
1315
+ /** Decode a base64 string into binary data. */
1316
+ const base64ToArrayBuffer = Binary.fromBase64;
1317
+ /** Encode binary data as base64. */
1318
+ const arrayBufferToBase64 = Binary.toBase64;
1319
+ /** Decode a hex string into binary data. */
1320
+ const hexToArrayBuffer = Binary.fromHex;
1321
+ /** Encode binary data as hex. */
1322
+ const arrayBufferToHex = Binary.toHex;
1323
+ /** Deep-clone common JavaScript values while preserving prototypes and cycles. */
1324
+ function clone(source, refs = /* @__PURE__ */ new Map()) {
1325
+ if (!source || typeof source !== "object") return source;
1326
+ if (is("Date", source)) return new Date(source.valueOf());
1327
+ if (is("RegExp", source)) return new RegExp(source.source, source.flags);
1328
+ if (isArrayBufferLike(source)) return source.slice(0);
1329
+ if (ArrayBuffer.isView(source)) return source.buffer.slice(source.byteOffset, source.byteOffset + source.byteLength);
1330
+ const cached = refs.get(source);
1331
+ if (cached) return cached;
1332
+ if (Array.isArray(source)) {
1333
+ const result$1 = [];
1334
+ refs.set(source, result$1);
1335
+ source.forEach((value, index) => {
1336
+ result$1[index] = Reflect.apply(clone, null, [value, refs]);
1337
+ });
1338
+ return result$1;
1339
+ }
1340
+ const result = Object.create(Object.getPrototypeOf(source));
1341
+ refs.set(source, result);
1342
+ for (const key of Reflect.ownKeys(source)) {
1343
+ const descriptor = { ...Reflect.getOwnPropertyDescriptor(source, key) };
1344
+ if ("value" in descriptor) descriptor.value = Reflect.apply(clone, null, [descriptor.value, refs]);
1345
+ Reflect.defineProperty(result, key, descriptor);
1346
+ }
1347
+ return result;
1348
+ }
1349
+ /** Deeply compare arrays, dates, regexps, buffers, and plain object fields. */
1350
+ function deepEqual(a, b, strict) {
1351
+ if (a === b) return true;
1352
+ if (!strict && isNullable(a) && isNullable(b)) return true;
1353
+ if (typeof a !== typeof b) return false;
1354
+ if (typeof a !== "object") return false;
1355
+ if (!a || !b) return false;
1356
+ function check(test, then) {
1357
+ return test(a) ? test(b) ? then(a, b) : false : test(b) ? false : void 0;
1358
+ }
1359
+ return check(Array.isArray, (a$1, b$1) => a$1.length === b$1.length && a$1.every((item, index) => deepEqual(item, b$1[index]))) ?? check(is("Date"), (a$1, b$1) => a$1.valueOf() === b$1.valueOf()) ?? check(is("RegExp"), (a$1, b$1) => a$1.source === b$1.source && a$1.flags === b$1.flags) ?? check(isArrayBufferLike, (a$1, b$1) => {
1360
+ if (a$1.byteLength !== b$1.byteLength) return false;
1361
+ const viewA = new Uint8Array(a$1);
1362
+ const viewB = new Uint8Array(b$1);
1363
+ for (let i = 0; i < viewA.length; i++) if (viewA[i] !== viewB[i]) return false;
1364
+ return true;
1365
+ }) ?? Object.keys({
1366
+ ...a,
1367
+ ...b
1368
+ }).every((key) => deepEqual(a[key], b[key], strict));
1369
+ }
1370
+
1371
+ //#endregion
1372
+ //#region ../deepseek-harness/vendor/cosmokit/src/time.ts
1373
+ let Time;
1374
+ (function(_Time) {
1375
+ _Time.millisecond = 1;
1376
+ const second = _Time.second = 1e3;
1377
+ const minute = _Time.minute = second * 60;
1378
+ const hour = _Time.hour = minute * 60;
1379
+ const day = _Time.day = hour * 24;
1380
+ const week = _Time.week = day * 7;
1381
+ let timezoneOffset = (/* @__PURE__ */ new Date()).getTimezoneOffset();
1382
+ function setTimezoneOffset(offset) {
1383
+ timezoneOffset = offset;
1384
+ }
1385
+ _Time.setTimezoneOffset = setTimezoneOffset;
1386
+ function getTimezoneOffset() {
1387
+ return timezoneOffset;
1388
+ }
1389
+ _Time.getTimezoneOffset = getTimezoneOffset;
1390
+ function getDateNumber(date = /* @__PURE__ */ new Date(), offset) {
1391
+ if (typeof date === "number") date = new Date(date);
1392
+ if (offset === void 0) offset = timezoneOffset;
1393
+ return Math.floor((date.valueOf() / minute - offset) / 1440);
1394
+ }
1395
+ _Time.getDateNumber = getDateNumber;
1396
+ function fromDateNumber(value, offset) {
1397
+ const date = new Date(value * day);
1398
+ if (offset === void 0) offset = timezoneOffset;
1399
+ return new Date(+date + offset * minute);
1400
+ }
1401
+ _Time.fromDateNumber = fromDateNumber;
1402
+ const numeric = /\d+(?:\.\d+)?/.source;
1403
+ const timeRegExp = /* @__PURE__ */ new RegExp(`^${[
1404
+ "w(?:eek(?:s)?)?",
1405
+ "d(?:ay(?:s)?)?",
1406
+ "h(?:our(?:s)?)?",
1407
+ "m(?:in(?:ute)?(?:s)?)?",
1408
+ "s(?:ec(?:ond)?(?:s)?)?"
1409
+ ].map((unit) => `(${numeric}${unit})?`).join("")}$`);
1410
+ function parseTime(source) {
1411
+ const capture = timeRegExp.exec(source);
1412
+ if (!capture) return 0;
1413
+ return (parseFloat(capture[1]) * week || 0) + (parseFloat(capture[2]) * day || 0) + (parseFloat(capture[3]) * hour || 0) + (parseFloat(capture[4]) * minute || 0) + (parseFloat(capture[5]) * second || 0);
1414
+ }
1415
+ _Time.parseTime = parseTime;
1416
+ function parseDate(date) {
1417
+ const parsed = parseTime(date);
1418
+ if (parsed) date = Date.now() + parsed;
1419
+ else if (/^\d{1,2}(:\d{1,2}){1,2}$/.test(date)) date = `${(/* @__PURE__ */ new Date()).toLocaleDateString()}-${date}`;
1420
+ else if (/^\d{1,2}-\d{1,2}-\d{1,2}(:\d{1,2}){1,2}$/.test(date)) date = `${(/* @__PURE__ */ new Date()).getFullYear()}-${date}`;
1421
+ return date ? new Date(date) : /* @__PURE__ */ new Date();
1422
+ }
1423
+ _Time.parseDate = parseDate;
1424
+ function format(ms) {
1425
+ const abs = Math.abs(ms);
1426
+ if (abs >= day - hour / 2) return Math.round(ms / day) + "d";
1427
+ else if (abs >= hour - minute / 2) return Math.round(ms / hour) + "h";
1428
+ else if (abs >= minute - second / 2) return Math.round(ms / minute) + "m";
1429
+ else if (abs >= second) return Math.round(ms / second) + "s";
1430
+ return ms + "ms";
1431
+ }
1432
+ _Time.format = format;
1433
+ function toDigits(source, length = 2) {
1434
+ return source.toString().padStart(length, "0");
1435
+ }
1436
+ _Time.toDigits = toDigits;
1437
+ function template(template$1, time = /* @__PURE__ */ new Date()) {
1438
+ return template$1.replace("yyyy", time.getFullYear().toString()).replace("yy", time.getFullYear().toString().slice(2)).replace("MM", toDigits(time.getMonth() + 1)).replace("dd", toDigits(time.getDate())).replace("hh", toDigits(time.getHours())).replace("mm", toDigits(time.getMinutes())).replace("ss", toDigits(time.getSeconds())).replace("SSS", toDigits(time.getMilliseconds(), 3));
1439
+ }
1440
+ _Time.template = template;
1441
+ })(Time || (Time = {}));
1442
+
1443
+ //#endregion
1444
+ //#region ../deepseek-harness/vendor/schemastery/src/index.ts
1445
+ const kSchema = Symbol.for("schemastery");
1446
+ const kValidationError = Symbol.for("ValidationError");
1447
+ globalThis.__schemastery_index__ ??= 0;
1448
+ globalThis.__schemastery_refs__ = void 0;
1449
+ var ValidationError = class extends TypeError {
1450
+ name = "ValidationError";
1451
+ constructor(message, options) {
1452
+ let prefix = "$";
1453
+ for (const segment of options.path || []) if (typeof segment === "string") prefix += "." + segment;
1454
+ else if (typeof segment === "number") prefix += "[" + segment + "]";
1455
+ else if (typeof segment === "symbol") prefix += `[Symbol(${segment.toString()})]`;
1456
+ if (prefix.startsWith(".")) prefix = prefix.slice(1);
1457
+ super((prefix === "$" ? "" : `${prefix} `) + message);
1458
+ this.options = options;
1459
+ }
1460
+ static is(error) {
1461
+ return !!error?.[kValidationError];
1462
+ }
1463
+ };
1464
+ Object.defineProperty(ValidationError.prototype, kValidationError, { value: true });
1465
+ const Schema = function(options) {
1466
+ const schema = function(data, options$1 = {}) {
1467
+ return Schema.resolve(data, schema, options$1)[0];
1468
+ };
1469
+ if (options.refs) {
1470
+ const refs = mapValues(options.refs, (options$1) => new Schema(options$1));
1471
+ const getRef = (uid) => refs[uid];
1472
+ for (const key in refs) {
1473
+ const options$1 = refs[key];
1474
+ options$1.sKey = getRef(options$1.sKey);
1475
+ options$1.inner = getRef(options$1.inner);
1476
+ options$1.list = options$1.list && options$1.list.map(getRef);
1477
+ options$1.dict = options$1.dict && mapValues(options$1.dict, getRef);
1478
+ }
1479
+ return refs[options.uid];
1480
+ }
1481
+ Object.assign(schema, options);
1482
+ if (typeof schema.callback === "string") try {
1483
+ schema.callback = new Function("return " + schema.callback)();
1484
+ } catch {}
1485
+ Object.defineProperty(schema, "uid", { value: globalThis.__schemastery_index__++ });
1486
+ Object.setPrototypeOf(schema, Schema.prototype);
1487
+ schema.meta ||= {};
1488
+ schema.toString = schema.toString.bind(schema);
1489
+ return schema;
1490
+ };
1491
+ Schema.prototype = Object.create(Function.prototype);
1492
+ Schema.prototype[kSchema] = true;
1493
+ Object.defineProperty(Schema.prototype, "~standard", { get() {
1494
+ return {
1495
+ version: 1,
1496
+ vendor: "schemastery",
1497
+ validate: (value) => {
1498
+ try {
1499
+ return { value: Schema.resolve(value, this, {})[0] };
1500
+ } catch (error) {
1501
+ if (ValidationError.is(error)) return { issues: [{
1502
+ message: error.message,
1503
+ path: error.options.path
1504
+ }] };
1505
+ throw error;
1506
+ }
1507
+ }
1508
+ };
1509
+ } });
1510
+ Schema.ValidationError = ValidationError;
1511
+ Schema.prototype.toJSON = function toJSON() {
1512
+ if (globalThis.__schemastery_refs__) {
1513
+ globalThis.__schemastery_refs__[this.uid] ??= JSON.parse(JSON.stringify({ ...this }));
1514
+ return this.uid;
1515
+ }
1516
+ globalThis.__schemastery_refs__ = { [this.uid]: { ...this } };
1517
+ globalThis.__schemastery_refs__[this.uid] = JSON.parse(JSON.stringify({ ...this }));
1518
+ const result = {
1519
+ uid: this.uid,
1520
+ refs: globalThis.__schemastery_refs__
1521
+ };
1522
+ globalThis.__schemastery_refs__ = void 0;
1523
+ return result;
1524
+ };
1525
+ Schema.prototype.set = function set(key, value) {
1526
+ this.dict[key] = value;
1527
+ return this;
1528
+ };
1529
+ Schema.prototype.push = function push(value) {
1530
+ this.list.push(value);
1531
+ return this;
1532
+ };
1533
+ function mergeDesc(original, messages) {
1534
+ const result = typeof original === "string" ? { "": original } : { ...original };
1535
+ for (const locale in messages) {
1536
+ const value = messages[locale];
1537
+ if (value?.$description || value?.$desc) result[locale] = value.$description || value.$desc;
1538
+ else if (typeof value === "string") result[locale] = value;
1539
+ }
1540
+ return result;
1541
+ }
1542
+ function getInner(value) {
1543
+ return value?.$value ?? value?.$inner;
1544
+ }
1545
+ function extractKeys(data) {
1546
+ return filterKeys(data ?? {}, (key) => !key.startsWith("$"));
1547
+ }
1548
+ Schema.prototype.i18n = function i18n(messages) {
1549
+ const schema = Schema(this);
1550
+ const desc = mergeDesc(schema.meta.description, messages);
1551
+ if (Object.keys(desc).length) schema.meta.description = desc;
1552
+ if (schema.dict) schema.dict = mapValues(schema.dict, (inner, key) => {
1553
+ return inner.i18n(mapValues(messages, (data) => getInner(data)?.[key] ?? data?.[key]));
1554
+ });
1555
+ if (schema.list) schema.list = schema.list.map((inner, index) => {
1556
+ return inner.i18n(mapValues(messages, (data = {}) => {
1557
+ if (Array.isArray(getInner(data))) return getInner(data)[index];
1558
+ if (Array.isArray(data)) return data[index];
1559
+ return extractKeys(data);
1560
+ }));
1561
+ });
1562
+ if (schema.inner) schema.inner = schema.inner.i18n(mapValues(messages, (data) => {
1563
+ if (getInner(data)) return getInner(data);
1564
+ return extractKeys(data);
1565
+ }));
1566
+ if (schema.sKey) schema.sKey = schema.sKey.i18n(mapValues(messages, (data) => data?.$key));
1567
+ return schema;
1568
+ };
1569
+ Schema.prototype.extra = function extra(key, value) {
1570
+ const schema = Schema(this);
1571
+ schema.meta = {
1572
+ ...schema.meta,
1573
+ [key]: value
1574
+ };
1575
+ return schema;
1576
+ };
1577
+ for (const key of [
1578
+ "required",
1579
+ "disabled",
1580
+ "collapse",
1581
+ "hidden",
1582
+ "loose"
1583
+ ]) Object.assign(Schema.prototype, { [key](value = true) {
1584
+ const schema = Schema(this);
1585
+ schema.meta = {
1586
+ ...schema.meta,
1587
+ [key]: value
1588
+ };
1589
+ return schema;
1590
+ } });
1591
+ Schema.prototype.deprecated = function deprecated() {
1592
+ const schema = Schema(this);
1593
+ schema.meta.badges ||= [];
1594
+ schema.meta.badges.push({
1595
+ text: "deprecated",
1596
+ type: "danger"
1597
+ });
1598
+ return schema;
1599
+ };
1600
+ Schema.prototype.experimental = function experimental() {
1601
+ const schema = Schema(this);
1602
+ schema.meta.badges ||= [];
1603
+ schema.meta.badges.push({
1604
+ text: "experimental",
1605
+ type: "warning"
1606
+ });
1607
+ return schema;
1608
+ };
1609
+ Schema.prototype.pattern = function pattern(regexp) {
1610
+ const schema = Schema(this);
1611
+ const pattern$1 = pick(regexp, ["source", "flags"]);
1612
+ schema.meta = {
1613
+ ...schema.meta,
1614
+ pattern: pattern$1
1615
+ };
1616
+ return schema;
1617
+ };
1618
+ Schema.prototype.simplify = function simplify(value) {
1619
+ if (deepEqual(value, this.meta.default, this.type === "dict")) return null;
1620
+ if (isNullable(value)) return value;
1621
+ if (this.type === "object" || this.type === "dict") {
1622
+ const result = {};
1623
+ for (const key in value) {
1624
+ const item = (this.type === "object" ? this.dict[key] : this.inner)?.simplify(value[key]);
1625
+ if (this.type === "dict" || !isNullable(item)) result[key] = item;
1626
+ }
1627
+ if (deepEqual(result, this.meta.default, this.type === "dict")) return null;
1628
+ return result;
1629
+ } else if (this.type === "array" || this.type === "tuple") {
1630
+ const result = [];
1631
+ value.forEach((value$1, index) => {
1632
+ const schema = this.type === "array" ? this.inner : this.list[index];
1633
+ const item = schema ? schema.simplify(value$1) : value$1;
1634
+ result.push(item);
1635
+ });
1636
+ return result;
1637
+ } else if (this.type === "intersect") {
1638
+ const result = {};
1639
+ for (const item of this.list) Object.assign(result, item.simplify(value));
1640
+ return result;
1641
+ } else if (this.type === "union") for (const schema of this.list) try {
1642
+ Schema.resolve(value, schema, {});
1643
+ return schema.simplify(value);
1644
+ } catch {}
1645
+ return value;
1646
+ };
1647
+ Schema.prototype.toString = function toString(inline) {
1648
+ return formatters[this.type]?.(this, inline) ?? `Schema<${this.type}>`;
1649
+ };
1650
+ Schema.prototype.role = function role(role$1, extra) {
1651
+ const schema = Schema(this);
1652
+ schema.meta = {
1653
+ ...schema.meta,
1654
+ role: role$1,
1655
+ extra
1656
+ };
1657
+ return schema;
1658
+ };
1659
+ for (const key of [
1660
+ "default",
1661
+ "link",
1662
+ "comment",
1663
+ "description",
1664
+ "max",
1665
+ "min",
1666
+ "step"
1667
+ ]) Object.assign(Schema.prototype, { [key](value) {
1668
+ const schema = Schema(this);
1669
+ schema.meta = {
1670
+ ...schema.meta,
1671
+ [key]: value
1672
+ };
1673
+ return schema;
1674
+ } });
1675
+ const resolvers = {};
1676
+ Schema.extend = function extend(type, resolve) {
1677
+ resolvers[type] = resolve;
1678
+ };
1679
+ Schema.resolve = function resolve(data, schema, options = {}, strict = false) {
1680
+ if (!schema) return [data];
1681
+ if (options.ignore?.(data, schema)) return [data];
1682
+ if (isNullable(data) && schema.type !== "lazy") {
1683
+ if (schema.meta.required) throw new ValidationError(`missing required value`, options);
1684
+ let current = schema;
1685
+ let fallback = schema.meta.default;
1686
+ while (current?.type === "intersect" && isNullable(fallback)) {
1687
+ current = current.list[0];
1688
+ fallback = current?.meta.default;
1689
+ }
1690
+ if (isNullable(fallback)) return [data];
1691
+ data = clone(fallback);
1692
+ }
1693
+ const callback = resolvers[schema.type];
1694
+ if (!callback) throw new ValidationError(`unsupported type "${schema.type}"`, options);
1695
+ try {
1696
+ return callback(data, schema, options, strict);
1697
+ } catch (error) {
1698
+ if (!schema.meta.loose) throw error;
1699
+ return [schema.meta.default];
1700
+ }
1701
+ };
1702
+ Schema.from = function from(source) {
1703
+ if (isNullable(source)) return Schema.any();
1704
+ else if ([
1705
+ "string",
1706
+ "number",
1707
+ "boolean"
1708
+ ].includes(typeof source)) return Schema.const(source).required();
1709
+ else if (source[kSchema]) return source;
1710
+ else if (typeof source === "function") switch (source) {
1711
+ case String: return Schema.string().required();
1712
+ case Number: return Schema.number().required();
1713
+ case Boolean: return Schema.boolean().required();
1714
+ case Function: return Schema.function().required();
1715
+ default: return Schema.is(source).required();
1716
+ }
1717
+ else throw new TypeError(`cannot infer schema from ${source}`);
1718
+ };
1719
+ Schema.lazy = function lazy(builder) {
1720
+ const toJSON = () => {
1721
+ if (!schema.inner[kSchema]) {
1722
+ schema.inner = schema.builder();
1723
+ schema.inner.meta = {
1724
+ ...schema.meta,
1725
+ ...schema.inner.meta
1726
+ };
1727
+ }
1728
+ return schema.inner.toJSON();
1729
+ };
1730
+ const schema = new Schema({
1731
+ type: "lazy",
1732
+ builder,
1733
+ inner: { toJSON }
1734
+ });
1735
+ return schema;
1736
+ };
1737
+ Schema.natural = function natural() {
1738
+ return Schema.number().step(1).min(0);
1739
+ };
1740
+ Schema.percent = function percent() {
1741
+ return Schema.number().step(.01).min(0).max(1).role("slider");
1742
+ };
1743
+ Schema.date = function date() {
1744
+ return Schema.union([Schema.is(Date), Schema.transform(Schema.string().role("datetime"), (value, options) => {
1745
+ const date$1 = new Date(value);
1746
+ if (isNaN(+date$1)) throw new ValidationError(`invalid date "${value}"`, options);
1747
+ return date$1;
1748
+ }, true)]);
1749
+ };
1750
+ Schema.regExp = function regExp(flag = "") {
1751
+ return Schema.union([Schema.is(RegExp), Schema.transform(Schema.string().role("regexp", { flag }), (value, options) => {
1752
+ try {
1753
+ return new RegExp(value, flag);
1754
+ } catch (e) {
1755
+ throw new ValidationError(e.message, options);
1756
+ }
1757
+ }, true)]);
1758
+ };
1759
+ Schema.arrayBuffer = function arrayBuffer(encoding) {
1760
+ return Schema.union([
1761
+ Schema.is(ArrayBuffer),
1762
+ Schema.is(SharedArrayBuffer),
1763
+ Schema.transform(Schema.any(), (value, options) => {
1764
+ if (Binary.isSource(value)) return Binary.fromSource(value);
1765
+ throw new ValidationError(`expected ArrayBufferSource but got ${value}`, options);
1766
+ }, true),
1767
+ ...encoding ? [Schema.transform(Schema.string(), (value, options) => {
1768
+ try {
1769
+ return encoding === "base64" ? Binary.fromBase64(value) : Binary.fromHex(value);
1770
+ } catch (e) {
1771
+ throw new ValidationError(e.message, options);
1772
+ }
1773
+ }, true)] : []
1774
+ ]);
1775
+ };
1776
+ Schema.extend("lazy", (data, schema, options, strict) => {
1777
+ if (!schema.inner[kSchema]) {
1778
+ schema.inner = schema.builder();
1779
+ schema.inner.meta = {
1780
+ ...schema.meta,
1781
+ ...schema.inner.meta
1782
+ };
1783
+ }
1784
+ return Schema.resolve(data, schema.inner, options, strict);
1785
+ });
1786
+ Schema.extend("any", (data) => {
1787
+ return [data];
1788
+ });
1789
+ Schema.extend("never", (data, _, options) => {
1790
+ throw new ValidationError(`expected nullable but got ${data}`, options);
1791
+ });
1792
+ Schema.extend("const", (data, { value }, options) => {
1793
+ if (deepEqual(data, value)) return [value];
1794
+ throw new ValidationError(`expected ${value} but got ${data}`, options);
1795
+ });
1796
+ function checkWithinRange(data, meta, description, options, skipMin = false) {
1797
+ const { max = Infinity, min = -Infinity } = meta;
1798
+ if (data > max) throw new ValidationError(`expected ${description} <= ${max} but got ${data}`, options);
1799
+ if (data < min && !skipMin) throw new ValidationError(`expected ${description} >= ${min} but got ${data}`, options);
1800
+ }
1801
+ Schema.extend("string", (data, { meta }, options) => {
1802
+ if (typeof data !== "string") throw new ValidationError(`expected string but got ${data}`, options);
1803
+ if (meta.pattern) {
1804
+ const regexp = new RegExp(meta.pattern.source, meta.pattern.flags);
1805
+ if (!regexp.test(data)) throw new ValidationError(`expect string to match regexp ${regexp}`, options);
1806
+ }
1807
+ checkWithinRange(data.length, meta, "string length", options);
1808
+ return [data];
1809
+ });
1810
+ function decimalShift(data, digits) {
1811
+ const str = data.toString();
1812
+ if (str.includes("e")) return data * Math.pow(10, digits);
1813
+ const index = str.indexOf(".");
1814
+ if (index === -1) return data * Math.pow(10, digits);
1815
+ const frac = str.slice(index + 1);
1816
+ const integer = str.slice(0, index);
1817
+ if (frac.length <= digits) return +(integer + frac.padEnd(digits, "0"));
1818
+ return +(integer + frac.slice(0, digits) + "." + frac.slice(digits));
1819
+ }
1820
+ function isMultipleOf(data, min, step) {
1821
+ step = Math.abs(step);
1822
+ if (!/^\d+\.\d+$/.test(step.toString())) return (data - min) % step === 0;
1823
+ const index = step.toString().indexOf(".");
1824
+ const digits = step.toString().slice(index + 1).length;
1825
+ return Math.abs(decimalShift(data, digits) - decimalShift(min, digits)) % decimalShift(step, digits) === 0;
1826
+ }
1827
+ Schema.extend("number", (data, { meta }, options) => {
1828
+ if (typeof data !== "number") throw new ValidationError(`expected number but got ${data}`, options);
1829
+ checkWithinRange(data, meta, "number", options);
1830
+ const { step } = meta;
1831
+ if (step && !isMultipleOf(data, meta.min ?? 0, step)) throw new ValidationError(`expected number multiple of ${step} but got ${data}`, options);
1832
+ return [data];
1833
+ });
1834
+ Schema.extend("boolean", (data, _, options) => {
1835
+ if (typeof data === "boolean") return [data];
1836
+ throw new ValidationError(`expected boolean but got ${data}`, options);
1837
+ });
1838
+ Schema.extend("bitset", (data, { bits, meta }, options) => {
1839
+ let value = 0, keys = [];
1840
+ if (typeof data === "number") {
1841
+ value = data;
1842
+ for (const key in bits) if (data & bits[key]) keys.push(key);
1843
+ } else if (Array.isArray(data)) {
1844
+ keys = data;
1845
+ for (const key of keys) {
1846
+ if (typeof key !== "string") throw new ValidationError(`expected string but got ${key}`, options);
1847
+ if (key in bits) value |= bits[key];
1848
+ }
1849
+ } else throw new ValidationError(`expected number or array but got ${data}`, options);
1850
+ if (value === meta.default) return [value];
1851
+ return [value, keys];
1852
+ });
1853
+ Schema.extend("function", (data, _, options) => {
1854
+ if (typeof data === "function") return [data];
1855
+ throw new ValidationError(`expected function but got ${data}`, options);
1856
+ });
1857
+ Schema.extend("is", (data, { constructor }, options) => {
1858
+ if (typeof constructor === "function") {
1859
+ if (data instanceof constructor) return [data];
1860
+ throw new ValidationError(`expected ${constructor.name} but got ${data}`, options);
1861
+ } else {
1862
+ if (isNullable(data)) throw new ValidationError(`expected ${constructor} but got ${data}`, options);
1863
+ let prototype = Object.getPrototypeOf(data);
1864
+ while (prototype) {
1865
+ if (prototype.constructor?.name === constructor) return [data];
1866
+ prototype = Object.getPrototypeOf(prototype);
1867
+ }
1868
+ throw new ValidationError(`expected ${constructor} but got ${data}`, options);
1869
+ }
1870
+ });
1871
+ function property(data, key, schema, options) {
1872
+ try {
1873
+ const [value, adapted] = Schema.resolve(data[key], schema, {
1874
+ ...options,
1875
+ path: [...options.path || [], key]
1876
+ });
1877
+ if (adapted !== void 0) data[key] = adapted;
1878
+ return value;
1879
+ } catch (e) {
1880
+ if (!options?.autofix) throw e;
1881
+ delete data[key];
1882
+ return schema.meta.default;
1883
+ }
1884
+ }
1885
+ Schema.extend("array", (data, { inner, meta }, options) => {
1886
+ if (!Array.isArray(data)) throw new ValidationError(`expected array but got ${data}`, options);
1887
+ checkWithinRange(data.length, meta, "array length", options, !isNullable(inner.meta.default));
1888
+ return [data.map((_, index) => property(data, index, inner, options))];
1889
+ });
1890
+ Schema.extend("dict", (data, { inner, sKey }, options, strict) => {
1891
+ if (!isPlainObject(data)) throw new ValidationError(`expected object but got ${data}`, options);
1892
+ const result = {};
1893
+ for (const key in data) {
1894
+ let rKey;
1895
+ try {
1896
+ rKey = Schema.resolve(key, sKey, options)[0];
1897
+ } catch (error) {
1898
+ if (strict) continue;
1899
+ throw error;
1900
+ }
1901
+ result[rKey] = property(data, key, inner, options);
1902
+ data[rKey] = data[key];
1903
+ if (key !== rKey) delete data[key];
1904
+ }
1905
+ return [result];
1906
+ });
1907
+ Schema.extend("tuple", (data, { list }, options, strict) => {
1908
+ if (!Array.isArray(data)) throw new ValidationError(`expected array but got ${data}`, options);
1909
+ const result = list.map((inner, index) => property(data, index, inner, options));
1910
+ if (strict) return [result];
1911
+ result.push(...data.slice(list.length));
1912
+ return [result];
1913
+ });
1914
+ function merge(result, data) {
1915
+ for (const key in data) {
1916
+ if (key in result) continue;
1917
+ result[key] = data[key];
1918
+ }
1919
+ }
1920
+ Schema.extend("object", (data, { dict }, options, strict) => {
1921
+ if (!isPlainObject(data)) throw new ValidationError(`expected object but got ${data}`, options);
1922
+ const result = {};
1923
+ for (const key in dict) {
1924
+ const value = property(data, key, dict[key], options);
1925
+ if (!isNullable(value) || key in data) result[key] = value;
1926
+ }
1927
+ if (!strict) merge(result, data);
1928
+ return [result];
1929
+ });
1930
+ Schema.extend("union", (data, { list, toString }, options, strict) => {
1931
+ const messages = [];
1932
+ for (const inner of list) try {
1933
+ return Schema.resolve(data, inner, options, strict);
1934
+ } catch (error) {
1935
+ messages.push(error);
1936
+ }
1937
+ throw new ValidationError(`expected ${toString()} but got ${JSON.stringify(data)}`, options);
1938
+ });
1939
+ Schema.extend("intersect", (data, { list, toString }, options, strict) => {
1940
+ if (!list.length) return [data];
1941
+ let result;
1942
+ for (const inner of list) {
1943
+ const value = Schema.resolve(data, inner, options, true)[0];
1944
+ if (isNullable(value)) continue;
1945
+ if (isNullable(result)) result = value;
1946
+ else if (typeof result !== typeof value) throw new ValidationError(`expected ${toString()} but got ${JSON.stringify(data)}`, options);
1947
+ else if (typeof value === "object") merge(result ??= {}, value);
1948
+ else if (result !== value) throw new ValidationError(`expected ${toString()} but got ${JSON.stringify(data)}`, options);
1949
+ }
1950
+ if (!strict && isPlainObject(data)) merge(result, data);
1951
+ return [result];
1952
+ });
1953
+ Schema.extend("transform", (data, { inner, callback, preserve }, options) => {
1954
+ const [result, adapted = data] = Schema.resolve(data, inner, options, true);
1955
+ if (preserve) return [callback(result)];
1956
+ else return [callback(result), callback(adapted)];
1957
+ });
1958
+ const formatters = {};
1959
+ function defineMethod(name, keys, format) {
1960
+ formatters[name] = format;
1961
+ Object.assign(Schema, { [name](...args) {
1962
+ const schema = new Schema({ type: name });
1963
+ keys.forEach((key, index) => {
1964
+ switch (key) {
1965
+ case "sKey":
1966
+ schema.sKey = args[index] ?? Schema.string();
1967
+ break;
1968
+ case "inner":
1969
+ schema.inner = Schema.from(args[index]);
1970
+ break;
1971
+ case "list":
1972
+ schema.list = args[index].map(Schema.from);
1973
+ break;
1974
+ case "dict":
1975
+ schema.dict = mapValues(args[index], Schema.from);
1976
+ break;
1977
+ case "bits":
1978
+ schema.bits = {};
1979
+ for (const key$1 in args[index]) {
1980
+ if (typeof args[index][key$1] !== "number") continue;
1981
+ schema.bits[key$1] = args[index][key$1];
1982
+ }
1983
+ break;
1984
+ case "callback": {
1985
+ const callback = schema.callback = args[index];
1986
+ callback["toJSON"] ||= () => callback.toString();
1987
+ break;
1988
+ }
1989
+ case "constructor": {
1990
+ const constructor = schema.constructor = args[index];
1991
+ if (typeof constructor === "function") constructor["toJSON"] ||= () => constructor["name"];
1992
+ break;
1993
+ }
1994
+ default: schema[key] = args[index];
1995
+ }
1996
+ });
1997
+ if (name === "object" || name === "dict") schema.meta.default = {};
1998
+ else if (name === "array" || name === "tuple") schema.meta.default = [];
1999
+ else if (name === "bitset") schema.meta.default = 0;
2000
+ return schema;
2001
+ } });
2002
+ }
2003
+ defineMethod("is", ["constructor"], ({ constructor }) => {
2004
+ if (typeof constructor === "function") return constructor.name;
2005
+ else return constructor;
2006
+ });
2007
+ defineMethod("any", [], () => "any");
2008
+ defineMethod("never", [], () => "never");
2009
+ defineMethod("const", ["value"], ({ value }) => typeof value === "string" ? JSON.stringify(value) : value);
2010
+ defineMethod("string", [], () => "string");
2011
+ defineMethod("number", [], () => "number");
2012
+ defineMethod("boolean", [], () => "boolean");
2013
+ defineMethod("bitset", ["bits"], () => "bitset");
2014
+ defineMethod("function", [], () => "function");
2015
+ defineMethod("array", ["inner"], ({ inner }) => `${inner.toString(true)}[]`);
2016
+ defineMethod("dict", ["inner", "sKey"], ({ inner, sKey }) => `{ [key: ${sKey.toString()}]: ${inner.toString()} }`);
2017
+ defineMethod("tuple", ["list"], ({ list }) => `[${list.map((inner) => inner.toString()).join(", ")}]`);
2018
+ defineMethod("object", ["dict"], ({ dict }) => {
2019
+ if (Object.keys(dict).length === 0) return "{}";
2020
+ return `{ ${Object.entries(dict).map(([key, inner]) => {
2021
+ return `${key}${inner.meta.required ? "" : "?"}: ${inner.toString()}`;
2022
+ }).join(", ")} }`;
2023
+ });
2024
+ defineMethod("union", ["list"], ({ list }, inline) => {
2025
+ const result = list.map(({ toString: format }) => format()).join(" | ");
2026
+ return inline ? `(${result})` : result;
2027
+ });
2028
+ defineMethod("intersect", ["list"], ({ list }) => {
2029
+ return `${list.map((inner) => inner.toString(true)).join(" & ")}`;
2030
+ });
2031
+ defineMethod("transform", [
2032
+ "inner",
2033
+ "callback",
2034
+ "preserve"
2035
+ ], ({ inner }, isInner) => inner.toString(isInner));
2036
+ var src_default = Schema;
2037
+
2038
+ //#endregion
2039
+ //#region src/config.ts
2040
+ /** Runtime schema. */
2041
+ const Config = src_default.object({
2042
+ cooldownMs: src_default.number().min(0).default(6e4),
2043
+ failureThreshold: src_default.number().min(1).default(2),
2044
+ successThreshold: src_default.number().min(1).default(2),
2045
+ immediateCodes: src_default.array(src_default.string()).default([
2046
+ "AUTH",
2047
+ "RATE_LIMIT",
2048
+ "NO_ADAPTER"
2049
+ ])
2050
+ });
2051
+ const QueueRouteSchema = src_default.object({
2052
+ provider: src_default.string(),
2053
+ model: src_default.string(),
2054
+ label: src_default.string().default("")
2055
+ });
2056
+ const CircuitHealthSchema = src_default.object({
2057
+ provider: src_default.string(),
2058
+ model: src_default.string(),
2059
+ state: src_default.string().default("closed"),
2060
+ failures: src_default.number().min(0).default(0)
2061
+ });
2062
+ /** Persisted user document. */
2063
+ const FailoverSettingsSchema = src_default.object({
2064
+ enabled: src_default.boolean().default(false),
2065
+ currentIndex: src_default.number().step(1).min(0).default(0),
2066
+ queue: src_default.array(QueueRouteSchema).default([]),
2067
+ circuits: src_default.array(CircuitHealthSchema).default([])
2068
+ });
2069
+ /** Empty queue, failover off. */
2070
+ const DEFAULT_SETTINGS = {
2071
+ enabled: false,
2072
+ currentIndex: 0,
2073
+ queue: [],
2074
+ circuits: []
2075
+ };
2076
+
2077
+ //#endregion
2078
+ //#region src/client/state.ts
2079
+ const listeners = /* @__PURE__ */ new Set();
2080
+ let snapshot = {
2081
+ ...DEFAULT_SETTINGS,
2082
+ circuits: [],
2083
+ candidates: [],
2084
+ candidatesLoaded: false
2085
+ };
2086
+ function publish(next) {
2087
+ snapshot = next;
2088
+ for (const listener of [...listeners]) listener();
2089
+ }
2090
+ /** Bare observable the slot renderer binds to `useFailover`. */
2091
+ const failoverSource = {
2092
+ subscribe(listener) {
2093
+ listeners.add(listener);
2094
+ return () => {
2095
+ listeners.delete(listener);
2096
+ };
2097
+ },
2098
+ getSnapshot() {
2099
+ return snapshot;
2100
+ }
2101
+ };
2102
+ /**
2103
+ * Replace settings fields, keep candidates.
2104
+ * @param settings - persisted document.
2105
+ */
2106
+ function replaceSettings(settings) {
2107
+ const queue = dedupeQueue(settings.queue);
2108
+ publish({
2109
+ enabled: settings.enabled,
2110
+ queue,
2111
+ currentIndex: clampIndex(settings.currentIndex, queue.length),
2112
+ circuits: settings.circuits ?? [],
2113
+ candidates: snapshot.candidates,
2114
+ candidatesLoaded: snapshot.candidatesLoaded
2115
+ });
2116
+ }
2117
+ /**
2118
+ * Replace the add-list catalog.
2119
+ * @param candidates - advertised provider+model rows.
2120
+ */
2121
+ function replaceCandidates(candidates) {
2122
+ publish({
2123
+ ...snapshot,
2124
+ candidates,
2125
+ candidatesLoaded: true
2126
+ });
2127
+ }
2128
+
2129
+ //#endregion
2130
+ //#region src/client/index.ts
2131
+ /** Required services for the composer chip and settings page. */
2132
+ const inject = [
2133
+ "slots",
2134
+ "locale",
2135
+ "settingsScope",
2136
+ "remote",
2137
+ "remote.commands",
2138
+ "remote.session"
2139
+ ];
2140
+ function currentSessionId(ctx, sessionId) {
2141
+ if (typeof sessionId === "string" && sessionId !== "") return sessionId;
2142
+ const current = ctx.get("sessions")?.list?.getSnapshot?.().current;
2143
+ return typeof current === "string" ? current : "";
2144
+ }
2145
+ function circuitState(value) {
2146
+ if (value === "open" || value === "half_open" || value === "closed") return value;
2147
+ return "closed";
2148
+ }
2149
+ function settingsFromUnknown(value) {
2150
+ if (typeof value !== "object" || value === null) return void 0;
2151
+ const record = value;
2152
+ if (typeof record.enabled !== "boolean") return void 0;
2153
+ if (typeof record.currentIndex !== "number") return void 0;
2154
+ if (!Array.isArray(record.queue)) return void 0;
2155
+ const queue = record.queue.flatMap((row) => {
2156
+ if (typeof row !== "object" || row === null) return [];
2157
+ const item = row;
2158
+ if (typeof item.provider !== "string" || typeof item.model !== "string") return [];
2159
+ return [{
2160
+ provider: item.provider,
2161
+ model: item.model,
2162
+ ...typeof item.label === "string" ? { label: item.label } : {}
2163
+ }];
2164
+ });
2165
+ const circuits = Array.isArray(record.circuits) ? record.circuits.flatMap((row) => {
2166
+ if (typeof row !== "object" || row === null) return [];
2167
+ const item = row;
2168
+ if (typeof item.provider !== "string" || typeof item.model !== "string") return [];
2169
+ return [{
2170
+ provider: item.provider,
2171
+ model: item.model,
2172
+ state: circuitState(item.state),
2173
+ failures: typeof item.failures === "number" && item.failures > 0 ? item.failures : 0
2174
+ }];
2175
+ }) : [];
2176
+ return {
2177
+ enabled: record.enabled,
2178
+ currentIndex: record.currentIndex,
2179
+ queue,
2180
+ circuits
2181
+ };
2182
+ }
2183
+ /**
2184
+ * Register dictionaries, the composer chip, and the Settings left-nav page.
2185
+ * @param ctx - browser plugin context.
2186
+ */
2187
+ function apply(ctx) {
2188
+ const client = ctx;
2189
+ const scope = client.settingsScope.bind({ namespace: "dsh-failover-queue" });
2190
+ const pull = () => {
2191
+ const value = settingsFromUnknown(scope.getSnapshot().value);
2192
+ if (value !== void 0) replaceSettings(value);
2193
+ };
2194
+ pull();
2195
+ const api = {
2196
+ setEnabled: async (enabled) => {
2197
+ await scope.set("enabled", enabled);
2198
+ },
2199
+ setQueue: async (queue, currentIndex) => {
2200
+ await scope.set("queue", queue);
2201
+ await scope.set("currentIndex", currentIndex);
2202
+ }
2203
+ };
2204
+ const loadCandidates = async (sessionId) => {
2205
+ const catalog = await client.remote.session?.modelCatalog();
2206
+ if (catalog?.ok) {
2207
+ const rows = candidatesFromCatalog(catalog.value);
2208
+ if (rows.length > 0) {
2209
+ replaceCandidates(rows);
2210
+ return;
2211
+ }
2212
+ }
2213
+ if (sessionId === "") return;
2214
+ const result = await client.remote.commands.execute(sessionId, "/failover __candidates", []);
2215
+ if (!result.ok) return;
2216
+ const text = typeof result.value === "string" ? result.value : typeof result.value?.text === "string" ? result.value.text : "";
2217
+ if (text === "") return;
2218
+ replaceCandidates(candidatesFromText(text));
2219
+ };
2220
+ const injected = (sessionId) => ({
2221
+ hooks: { failover: failoverSource },
2222
+ api,
2223
+ loadCandidates,
2224
+ getSessionId: () => currentSessionId(ctx, sessionId)
2225
+ });
2226
+ client.effect(() => {
2227
+ const style = document.createElement("style");
2228
+ style.dataset.plugin = "dsh-failover-queue";
2229
+ style.textContent = STYLE;
2230
+ document.head.appendChild(style);
2231
+ return () => {
2232
+ style.remove();
2233
+ };
2234
+ }, "dsh-failover-queue: styles");
2235
+ client.effect(() => client.locale.register(NS, {
2236
+ zh,
2237
+ en
2238
+ }), "dsh-failover-queue: dictionaries");
2239
+ client.effect(() => scope.subscribe(pull), "dsh-failover-queue: settings");
2240
+ client.slots.inject("conversation.input.right", () => client.slots.register({
2241
+ name: "conversation.input.right",
2242
+ id: "dsh-failover-queue",
2243
+ order: 40,
2244
+ locale: NS,
2245
+ inject: injected
2246
+ }, FailoverChip));
2247
+ client.slots.inject("settings.section", () => client.slots.register({
2248
+ name: "settings.section",
2249
+ id: "failover",
2250
+ order: 17,
2251
+ locale: NS,
2252
+ label: () => client.locale.bind(NS)("settings.tab"),
2253
+ inject: injected
2254
+ }, FailoverSettingsCard));
2255
+ }
2256
+
2257
+ //#endregion
2258
+ exports.apply = apply;
2259
+ exports.inject = inject;
2260
+ return module.exports; } });
2261
+ //# sourceMappingURL=client.js.map