dsh-llm-fallbacks 0.1.0-alpha.1

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.
@@ -0,0 +1,3916 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-llm-fallbacks",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ //#region \0rolldown/runtime.js
8
+ var __create = Object.create;
9
+ var __defProp = Object.defineProperty;
10
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
11
+ var __getOwnPropNames = Object.getOwnPropertyNames;
12
+ var __getProtoOf = Object.getPrototypeOf;
13
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
14
+ var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
15
+ var __copyProps = (to, from, except, desc) => {
16
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
17
+ key = keys[i];
18
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
19
+ get: ((k) => from[k]).bind(null, key),
20
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
21
+ });
22
+ }
23
+ return to;
24
+ };
25
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
26
+ value: mod,
27
+ enumerable: true
28
+ }) : target, mod));
29
+ //#endregion
30
+ let _deepseek_ai_dsh_client_web_react = require("@deepseek-ai/dsh-client-web-react");
31
+ let react = require("react");
32
+ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
33
+ let _deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-runtime/client");
34
+ let react_jsx_runtime = require("react/jsx-runtime");
35
+ //#region node_modules/cosmokit/lib/index.cjs
36
+ var require_lib$1 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
37
+ var __defProp = Object.defineProperty;
38
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
39
+ var __getOwnPropNames = Object.getOwnPropertyNames;
40
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
41
+ var __export = (target, all) => {
42
+ for (var name in all) __defProp(target, name, {
43
+ get: all[name],
44
+ enumerable: true
45
+ });
46
+ };
47
+ var __copyProps = (to, from, except, desc) => {
48
+ if (from && typeof from === "object" || typeof from === "function") {
49
+ for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
50
+ get: () => from[key],
51
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
52
+ });
53
+ }
54
+ return to;
55
+ };
56
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
57
+ var index_exports = {};
58
+ __export(index_exports, {
59
+ Binary: () => Binary,
60
+ Time: () => Time,
61
+ arrayBufferToBase64: () => arrayBufferToBase64,
62
+ arrayBufferToHex: () => arrayBufferToHex,
63
+ base64ToArrayBuffer: () => base64ToArrayBuffer,
64
+ camelCase: () => camelCase,
65
+ camelize: () => camelize,
66
+ capitalize: () => capitalize,
67
+ clone: () => clone,
68
+ contain: () => contain,
69
+ deduplicate: () => deduplicate,
70
+ deepEqual: () => deepEqual,
71
+ defineProperty: () => defineProperty,
72
+ difference: () => difference,
73
+ filterKeys: () => filterKeys,
74
+ formatProperty: () => formatProperty,
75
+ hexToArrayBuffer: () => hexToArrayBuffer,
76
+ hyphenate: () => hyphenate,
77
+ intersection: () => intersection,
78
+ is: () => is,
79
+ isNonNullable: () => isNonNullable,
80
+ isNullable: () => isNullable,
81
+ isPlainObject: () => isPlainObject,
82
+ makeArray: () => makeArray,
83
+ mapValues: () => mapValues,
84
+ noop: () => noop,
85
+ omit: () => omit,
86
+ paramCase: () => paramCase,
87
+ pick: () => pick,
88
+ remove: () => remove,
89
+ sanitize: () => sanitize,
90
+ snakeCase: () => snakeCase,
91
+ trimSlash: () => trimSlash,
92
+ uncapitalize: () => uncapitalize,
93
+ union: () => union,
94
+ valueMap: () => mapValues
95
+ });
96
+ module.exports = __toCommonJS(index_exports);
97
+ function noop() {}
98
+ function isNullable(value) {
99
+ return value === null || value === void 0;
100
+ }
101
+ function isNonNullable(value) {
102
+ return !isNullable(value);
103
+ }
104
+ function isPlainObject(data) {
105
+ return data && typeof data === "object" && !Array.isArray(data);
106
+ }
107
+ function filterKeys(object, filter) {
108
+ return Object.fromEntries(Object.entries(object).filter(([key, value]) => filter(key, value)));
109
+ }
110
+ function mapValues(object, transform) {
111
+ return Object.fromEntries(Object.entries(object).map(([key, value]) => [key, transform(value, key)]));
112
+ }
113
+ function pick(source, keys, forced) {
114
+ if (!keys) return { ...source };
115
+ const result = {};
116
+ for (const key of keys) if (forced || source[key] !== void 0) result[key] = source[key];
117
+ return result;
118
+ }
119
+ function omit(source, keys) {
120
+ if (!keys) return { ...source };
121
+ const result = { ...source };
122
+ for (const key of keys) Reflect.deleteProperty(result, key);
123
+ return result;
124
+ }
125
+ function defineProperty(object, key, value) {
126
+ return Object.defineProperty(object, key, {
127
+ writable: true,
128
+ value,
129
+ enumerable: false
130
+ });
131
+ }
132
+ function contain(array1, array2) {
133
+ return array2.every((item) => array1.includes(item));
134
+ }
135
+ function intersection(array1, array2) {
136
+ return array1.filter((item) => array2.includes(item));
137
+ }
138
+ function difference(array1, array2) {
139
+ return array1.filter((item) => !array2.includes(item));
140
+ }
141
+ function union(array1, array2) {
142
+ return Array.from(/* @__PURE__ */ new Set([...array1, ...array2]));
143
+ }
144
+ function deduplicate(array) {
145
+ return [...new Set(array)];
146
+ }
147
+ function remove(list, item) {
148
+ const index = list?.indexOf(item);
149
+ if (index >= 0) {
150
+ list.splice(index, 1);
151
+ return true;
152
+ } else return false;
153
+ }
154
+ function makeArray(source) {
155
+ return Array.isArray(source) ? source : isNullable(source) ? [] : [source];
156
+ }
157
+ function is(type, value) {
158
+ if (arguments.length === 1) return (value2) => is(type, value2);
159
+ return type in globalThis && value instanceof globalThis[type] || Object.prototype.toString.call(value).slice(8, -1) === type;
160
+ }
161
+ function isArrayBufferLike(value) {
162
+ return is("ArrayBuffer", value) || is("SharedArrayBuffer", value);
163
+ }
164
+ function isArrayBufferSource(value) {
165
+ return isArrayBufferLike(value) || ArrayBuffer.isView(value);
166
+ }
167
+ var Binary;
168
+ ((Binary2) => {
169
+ Binary2.is = isArrayBufferLike;
170
+ Binary2.isSource = isArrayBufferSource;
171
+ function fromSource(source) {
172
+ if (ArrayBuffer.isView(source)) return source.buffer.slice(source.byteOffset, source.byteOffset + source.byteLength);
173
+ else return source;
174
+ }
175
+ Binary2.fromSource = fromSource;
176
+ function toBase64(source) {
177
+ source = fromSource(source);
178
+ if (typeof Buffer !== "undefined") return Buffer.from(source).toString("base64");
179
+ let binary = "";
180
+ const bytes = new Uint8Array(source);
181
+ for (let i = 0; i < bytes.byteLength; i++) binary += String.fromCharCode(bytes[i]);
182
+ return btoa(binary);
183
+ }
184
+ Binary2.toBase64 = toBase64;
185
+ function fromBase64(source) {
186
+ if (typeof Buffer !== "undefined") return fromSource(Buffer.from(source, "base64"));
187
+ return Uint8Array.from(atob(source), (c) => c.charCodeAt(0));
188
+ }
189
+ Binary2.fromBase64 = fromBase64;
190
+ function toHex(source) {
191
+ source = fromSource(source);
192
+ if (typeof Buffer !== "undefined") return Buffer.from(source).toString("hex");
193
+ return Array.from(new Uint8Array(source), (byte) => byte.toString(16).padStart(2, "0")).join("");
194
+ }
195
+ Binary2.toHex = toHex;
196
+ function fromHex(source) {
197
+ if (typeof Buffer !== "undefined") return fromSource(Buffer.from(source, "hex"));
198
+ const hex = source.length % 2 === 0 ? source : source.slice(0, source.length - 1);
199
+ const buffer = [];
200
+ for (let i = 0; i < hex.length; i += 2) buffer.push(parseInt(`${hex[i]}${hex[i + 1]}`, 16));
201
+ return Uint8Array.from(buffer).buffer;
202
+ }
203
+ Binary2.fromHex = fromHex;
204
+ })(Binary || (Binary = {}));
205
+ var base64ToArrayBuffer = Binary.fromBase64;
206
+ var arrayBufferToBase64 = Binary.toBase64;
207
+ var hexToArrayBuffer = Binary.fromHex;
208
+ var arrayBufferToHex = Binary.toHex;
209
+ function clone(source, refs = /* @__PURE__ */ new Map()) {
210
+ if (!source || typeof source !== "object") return source;
211
+ if (is("Date", source)) return new Date(source.valueOf());
212
+ if (is("RegExp", source)) return new RegExp(source.source, source.flags);
213
+ if (isArrayBufferLike(source)) return source.slice(0);
214
+ if (ArrayBuffer.isView(source)) return source.buffer.slice(source.byteOffset, source.byteOffset + source.byteLength);
215
+ const cached = refs.get(source);
216
+ if (cached) return cached;
217
+ if (Array.isArray(source)) {
218
+ const result2 = [];
219
+ refs.set(source, result2);
220
+ source.forEach((value, index) => {
221
+ result2[index] = Reflect.apply(clone, null, [value, refs]);
222
+ });
223
+ return result2;
224
+ }
225
+ const result = Object.create(Object.getPrototypeOf(source));
226
+ refs.set(source, result);
227
+ for (const key of Reflect.ownKeys(source)) {
228
+ const descriptor = { ...Reflect.getOwnPropertyDescriptor(source, key) };
229
+ if ("value" in descriptor) descriptor.value = Reflect.apply(clone, null, [descriptor.value, refs]);
230
+ Reflect.defineProperty(result, key, descriptor);
231
+ }
232
+ return result;
233
+ }
234
+ function deepEqual(a, b, strict) {
235
+ if (a === b) return true;
236
+ if (!strict && isNullable(a) && isNullable(b)) return true;
237
+ if (typeof a !== typeof b) return false;
238
+ if (typeof a !== "object") return false;
239
+ if (!a || !b) return false;
240
+ function check(test, then) {
241
+ return test(a) ? test(b) ? then(a, b) : false : test(b) ? false : void 0;
242
+ }
243
+ return check(Array.isArray, (a2, b2) => a2.length === b2.length && a2.every((item, index) => deepEqual(item, b2[index]))) ?? check(is("Date"), (a2, b2) => a2.valueOf() === b2.valueOf()) ?? check(is("RegExp"), (a2, b2) => a2.source === b2.source && a2.flags === b2.flags) ?? check(isArrayBufferLike, (a2, b2) => {
244
+ if (a2.byteLength !== b2.byteLength) return false;
245
+ const viewA = new Uint8Array(a2);
246
+ const viewB = new Uint8Array(b2);
247
+ for (let i = 0; i < viewA.length; i++) if (viewA[i] !== viewB[i]) return false;
248
+ return true;
249
+ }) ?? Object.keys({
250
+ ...a,
251
+ ...b
252
+ }).every((key) => deepEqual(a[key], b[key], strict));
253
+ }
254
+ function capitalize(source) {
255
+ return source.charAt(0).toUpperCase() + source.slice(1);
256
+ }
257
+ function uncapitalize(source) {
258
+ return source.charAt(0).toLowerCase() + source.slice(1);
259
+ }
260
+ function camelCase(source) {
261
+ return source.replace(/[_-][a-z]/g, (str) => str.slice(1).toUpperCase());
262
+ }
263
+ function tokenize(source, delimiters, delimiter) {
264
+ const output = [];
265
+ let state = 0;
266
+ for (let i = 0; i < source.length; i++) {
267
+ const code = source.charCodeAt(i);
268
+ if (code >= 65 && code <= 90) {
269
+ if (state === 1) {
270
+ const next = source.charCodeAt(i + 1);
271
+ if (next >= 97 && next <= 122) output.push(delimiter);
272
+ output.push(code + 32);
273
+ } else {
274
+ if (state !== 0) output.push(delimiter);
275
+ output.push(code + 32);
276
+ }
277
+ state = 1;
278
+ } else if (code >= 97 && code <= 122) {
279
+ output.push(code);
280
+ state = 2;
281
+ } else if (delimiters.includes(code)) {
282
+ if (state !== 0) output.push(delimiter);
283
+ state = 0;
284
+ } else output.push(code);
285
+ }
286
+ return String.fromCharCode(...output);
287
+ }
288
+ function paramCase(source) {
289
+ return tokenize(source, [45, 95], 45);
290
+ }
291
+ function snakeCase(source) {
292
+ return tokenize(source, [45, 95], 95);
293
+ }
294
+ var camelize = camelCase;
295
+ var hyphenate = paramCase;
296
+ function formatProperty(key) {
297
+ if (typeof key !== "string") return `[${key.toString()}]`;
298
+ return /^[a-z_$][\w$]*$/i.test(key) ? `.${key}` : `[${JSON.stringify(key)}]`;
299
+ }
300
+ function trimSlash(source) {
301
+ return source.replace(/\/$/, "");
302
+ }
303
+ function sanitize(source) {
304
+ if (!source.startsWith("/")) source = "/" + source;
305
+ return trimSlash(source);
306
+ }
307
+ var Time;
308
+ ((Time2) => {
309
+ Time2.millisecond = 1;
310
+ Time2.second = 1e3;
311
+ Time2.minute = Time2.second * 60;
312
+ Time2.hour = Time2.minute * 60;
313
+ Time2.day = Time2.hour * 24;
314
+ Time2.week = Time2.day * 7;
315
+ let timezoneOffset = (/* @__PURE__ */ new Date()).getTimezoneOffset();
316
+ function setTimezoneOffset(offset) {
317
+ timezoneOffset = offset;
318
+ }
319
+ Time2.setTimezoneOffset = setTimezoneOffset;
320
+ function getTimezoneOffset() {
321
+ return timezoneOffset;
322
+ }
323
+ Time2.getTimezoneOffset = getTimezoneOffset;
324
+ function getDateNumber(date = /* @__PURE__ */ new Date(), offset) {
325
+ if (typeof date === "number") date = new Date(date);
326
+ if (offset === void 0) offset = timezoneOffset;
327
+ return Math.floor((date.valueOf() / Time2.minute - offset) / 1440);
328
+ }
329
+ Time2.getDateNumber = getDateNumber;
330
+ function fromDateNumber(value, offset) {
331
+ const date = new Date(value * Time2.day);
332
+ if (offset === void 0) offset = timezoneOffset;
333
+ return new Date(+date + offset * Time2.minute);
334
+ }
335
+ Time2.fromDateNumber = fromDateNumber;
336
+ const numeric = /\d+(?:\.\d+)?/.source;
337
+ const timeRegExp = new RegExp(`^${[
338
+ "w(?:eek(?:s)?)?",
339
+ "d(?:ay(?:s)?)?",
340
+ "h(?:our(?:s)?)?",
341
+ "m(?:in(?:ute)?(?:s)?)?",
342
+ "s(?:ec(?:ond)?(?:s)?)?"
343
+ ].map((unit) => `(${numeric}${unit})?`).join("")}$`);
344
+ function parseTime(source) {
345
+ const capture = timeRegExp.exec(source);
346
+ if (!capture) return 0;
347
+ return (parseFloat(capture[1]) * Time2.week || 0) + (parseFloat(capture[2]) * Time2.day || 0) + (parseFloat(capture[3]) * Time2.hour || 0) + (parseFloat(capture[4]) * Time2.minute || 0) + (parseFloat(capture[5]) * Time2.second || 0);
348
+ }
349
+ Time2.parseTime = parseTime;
350
+ function parseDate(date) {
351
+ const parsed = parseTime(date);
352
+ if (parsed) date = Date.now() + parsed;
353
+ else if (/^\d{1,2}(:\d{1,2}){1,2}$/.test(date)) date = `${(/* @__PURE__ */ new Date()).toLocaleDateString()}-${date}`;
354
+ else if (/^\d{1,2}-\d{1,2}-\d{1,2}(:\d{1,2}){1,2}$/.test(date)) date = `${(/* @__PURE__ */ new Date()).getFullYear()}-${date}`;
355
+ return date ? new Date(date) : /* @__PURE__ */ new Date();
356
+ }
357
+ Time2.parseDate = parseDate;
358
+ function format(ms) {
359
+ const abs = Math.abs(ms);
360
+ if (abs >= Time2.day - Time2.hour / 2) return Math.round(ms / Time2.day) + "d";
361
+ else if (abs >= Time2.hour - Time2.minute / 2) return Math.round(ms / Time2.hour) + "h";
362
+ else if (abs >= Time2.minute - Time2.second / 2) return Math.round(ms / Time2.minute) + "m";
363
+ else if (abs >= Time2.second) return Math.round(ms / Time2.second) + "s";
364
+ return ms + "ms";
365
+ }
366
+ Time2.format = format;
367
+ function toDigits(source, length = 2) {
368
+ return source.toString().padStart(length, "0");
369
+ }
370
+ Time2.toDigits = toDigits;
371
+ function template(template2, time = /* @__PURE__ */ new Date()) {
372
+ return template2.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));
373
+ }
374
+ Time2.template = template;
375
+ })(Time || (Time = {}));
376
+ 0 && (module.exports = {
377
+ Binary,
378
+ Time,
379
+ arrayBufferToBase64,
380
+ arrayBufferToHex,
381
+ base64ToArrayBuffer,
382
+ camelCase,
383
+ camelize,
384
+ capitalize,
385
+ clone,
386
+ contain,
387
+ deduplicate,
388
+ deepEqual,
389
+ defineProperty,
390
+ difference,
391
+ filterKeys,
392
+ formatProperty,
393
+ hexToArrayBuffer,
394
+ hyphenate,
395
+ intersection,
396
+ is,
397
+ isNonNullable,
398
+ isNullable,
399
+ isPlainObject,
400
+ makeArray,
401
+ mapValues,
402
+ noop,
403
+ omit,
404
+ paramCase,
405
+ pick,
406
+ remove,
407
+ sanitize,
408
+ snakeCase,
409
+ trimSlash,
410
+ uncapitalize,
411
+ union,
412
+ valueMap
413
+ });
414
+ }));
415
+ //#endregion
416
+ //#region src/selectors.ts
417
+ var import_lib = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
418
+ var __defProp = Object.defineProperty;
419
+ var __name = (target, value) => __defProp(target, "name", {
420
+ value,
421
+ configurable: true
422
+ });
423
+ var import_cosmokit = require_lib$1();
424
+ var kSchema = Symbol.for("schemastery");
425
+ var kValidationError = Symbol.for("ValidationError");
426
+ globalThis.__schemastery_index__ ??= 0;
427
+ globalThis.__schemastery_refs__ = void 0;
428
+ var ValidationError = class extends TypeError {
429
+ constructor(message, options) {
430
+ let prefix = "$";
431
+ for (const segment of options.path || []) if (typeof segment === "string") prefix += "." + segment;
432
+ else if (typeof segment === "number") prefix += "[" + segment + "]";
433
+ else if (typeof segment === "symbol") prefix += `[Symbol(${segment.toString()})]`;
434
+ if (prefix.startsWith(".")) prefix = prefix.slice(1);
435
+ super((prefix === "$" ? "" : `${prefix} `) + message);
436
+ this.options = options;
437
+ }
438
+ static {
439
+ __name(this, "ValidationError");
440
+ }
441
+ name = "ValidationError";
442
+ static is(error) {
443
+ return !!error?.[kValidationError];
444
+ }
445
+ };
446
+ Object.defineProperty(ValidationError.prototype, kValidationError, { value: true });
447
+ var Schema = /* @__PURE__ */ __name(function(options) {
448
+ const schema = /* @__PURE__ */ __name(function(data, options2 = {}) {
449
+ return Schema.resolve(data, schema, options2)[0];
450
+ }, "schema");
451
+ if (options.refs) {
452
+ const refs = (0, import_cosmokit.valueMap)(options.refs, (options2) => new Schema(options2));
453
+ const getRef = /* @__PURE__ */ __name((uid) => refs[uid], "getRef");
454
+ for (const key in refs) {
455
+ const options2 = refs[key];
456
+ options2.sKey = getRef(options2.sKey);
457
+ options2.inner = getRef(options2.inner);
458
+ options2.list = options2.list && options2.list.map(getRef);
459
+ options2.dict = options2.dict && (0, import_cosmokit.valueMap)(options2.dict, getRef);
460
+ }
461
+ return refs[options.uid];
462
+ }
463
+ Object.assign(schema, options);
464
+ if (typeof schema.callback === "string") try {
465
+ schema.callback = new Function("return " + schema.callback)();
466
+ } catch {}
467
+ Object.defineProperty(schema, "uid", { value: globalThis.__schemastery_index__++ });
468
+ Object.setPrototypeOf(schema, Schema.prototype);
469
+ schema.meta ||= {};
470
+ schema.toString = schema.toString.bind(schema);
471
+ return schema;
472
+ }, "Schema");
473
+ Schema.prototype = Object.create(Function.prototype);
474
+ Schema.prototype[kSchema] = true;
475
+ Object.defineProperty(Schema.prototype, "~standard", { get() {
476
+ return {
477
+ version: 1,
478
+ vendor: "schemastery",
479
+ validate: /* @__PURE__ */ __name((value) => {
480
+ try {
481
+ return { value: Schema.resolve(value, this, {})[0] };
482
+ } catch (error) {
483
+ if (ValidationError.is(error)) return { issues: [{
484
+ message: error.message,
485
+ path: error.options.path
486
+ }] };
487
+ throw error;
488
+ }
489
+ }, "validate")
490
+ };
491
+ } });
492
+ Schema.ValidationError = ValidationError;
493
+ Schema.prototype.toJSON = /* @__PURE__ */ __name(function toJSON() {
494
+ if (globalThis.__schemastery_refs__) {
495
+ globalThis.__schemastery_refs__[this.uid] ??= JSON.parse(JSON.stringify({ ...this }));
496
+ return this.uid;
497
+ }
498
+ globalThis.__schemastery_refs__ = { [this.uid]: { ...this } };
499
+ globalThis.__schemastery_refs__[this.uid] = JSON.parse(JSON.stringify({ ...this }));
500
+ const result = {
501
+ uid: this.uid,
502
+ refs: globalThis.__schemastery_refs__
503
+ };
504
+ globalThis.__schemastery_refs__ = void 0;
505
+ return result;
506
+ }, "toJSON");
507
+ Schema.prototype.set = /* @__PURE__ */ __name(function set(key, value) {
508
+ this.dict[key] = value;
509
+ return this;
510
+ }, "set");
511
+ Schema.prototype.push = /* @__PURE__ */ __name(function push(value) {
512
+ this.list.push(value);
513
+ return this;
514
+ }, "push");
515
+ function mergeDesc(original, messages) {
516
+ const result = typeof original === "string" ? { "": original } : { ...original };
517
+ for (const locale in messages) {
518
+ const value = messages[locale];
519
+ if (value?.$description || value?.$desc) result[locale] = value.$description || value.$desc;
520
+ else if (typeof value === "string") result[locale] = value;
521
+ }
522
+ return result;
523
+ }
524
+ __name(mergeDesc, "mergeDesc");
525
+ function getInner(value) {
526
+ return value?.$value ?? value?.$inner;
527
+ }
528
+ __name(getInner, "getInner");
529
+ function extractKeys(data) {
530
+ return (0, import_cosmokit.filterKeys)(data ?? {}, (key) => !key.startsWith("$"));
531
+ }
532
+ __name(extractKeys, "extractKeys");
533
+ Schema.prototype.i18n = /* @__PURE__ */ __name(function i18n(messages) {
534
+ const schema = Schema(this);
535
+ const desc = mergeDesc(schema.meta.description, messages);
536
+ if (Object.keys(desc).length) schema.meta.description = desc;
537
+ if (schema.dict) schema.dict = (0, import_cosmokit.valueMap)(schema.dict, (inner, key) => {
538
+ return inner.i18n((0, import_cosmokit.valueMap)(messages, (data) => getInner(data)?.[key] ?? data?.[key]));
539
+ });
540
+ if (schema.list) schema.list = schema.list.map((inner, index) => {
541
+ return inner.i18n((0, import_cosmokit.valueMap)(messages, (data = {}) => {
542
+ if (Array.isArray(getInner(data))) return getInner(data)[index];
543
+ if (Array.isArray(data)) return data[index];
544
+ return extractKeys(data);
545
+ }));
546
+ });
547
+ if (schema.inner) schema.inner = schema.inner.i18n((0, import_cosmokit.valueMap)(messages, (data) => {
548
+ if (getInner(data)) return getInner(data);
549
+ return extractKeys(data);
550
+ }));
551
+ if (schema.sKey) schema.sKey = schema.sKey.i18n((0, import_cosmokit.valueMap)(messages, (data) => data?.$key));
552
+ return schema;
553
+ }, "i18n");
554
+ Schema.prototype.extra = /* @__PURE__ */ __name(function extra(key, value) {
555
+ const schema = Schema(this);
556
+ schema.meta = {
557
+ ...schema.meta,
558
+ [key]: value
559
+ };
560
+ return schema;
561
+ }, "extra");
562
+ for (const key of [
563
+ "required",
564
+ "disabled",
565
+ "collapse",
566
+ "hidden",
567
+ "loose"
568
+ ]) Object.assign(Schema.prototype, { [key](value = true) {
569
+ const schema = Schema(this);
570
+ schema.meta = {
571
+ ...schema.meta,
572
+ [key]: value
573
+ };
574
+ return schema;
575
+ } });
576
+ Schema.prototype.deprecated = /* @__PURE__ */ __name(function deprecated() {
577
+ const schema = Schema(this);
578
+ schema.meta.badges ||= [];
579
+ schema.meta.badges.push({
580
+ text: "deprecated",
581
+ type: "danger"
582
+ });
583
+ return schema;
584
+ }, "deprecated");
585
+ Schema.prototype.experimental = /* @__PURE__ */ __name(function experimental() {
586
+ const schema = Schema(this);
587
+ schema.meta.badges ||= [];
588
+ schema.meta.badges.push({
589
+ text: "experimental",
590
+ type: "warning"
591
+ });
592
+ return schema;
593
+ }, "experimental");
594
+ Schema.prototype.pattern = /* @__PURE__ */ __name(function pattern(regexp) {
595
+ const schema = Schema(this);
596
+ const pattern2 = (0, import_cosmokit.pick)(regexp, ["source", "flags"]);
597
+ schema.meta = {
598
+ ...schema.meta,
599
+ pattern: pattern2
600
+ };
601
+ return schema;
602
+ }, "pattern");
603
+ Schema.prototype.simplify = /* @__PURE__ */ __name(function simplify(value) {
604
+ if ((0, import_cosmokit.deepEqual)(value, this.meta.default, this.type === "dict")) return null;
605
+ if ((0, import_cosmokit.isNullable)(value)) return value;
606
+ if (this.type === "object" || this.type === "dict") {
607
+ const result = {};
608
+ for (const key in value) {
609
+ const item = (this.type === "object" ? this.dict[key] : this.inner)?.simplify(value[key]);
610
+ if (this.type === "dict" || !(0, import_cosmokit.isNullable)(item)) result[key] = item;
611
+ }
612
+ if ((0, import_cosmokit.deepEqual)(result, this.meta.default, this.type === "dict")) return null;
613
+ return result;
614
+ } else if (this.type === "array" || this.type === "tuple") {
615
+ const result = [];
616
+ value.forEach((value2, index) => {
617
+ const schema = this.type === "array" ? this.inner : this.list[index];
618
+ const item = schema ? schema.simplify(value2) : value2;
619
+ result.push(item);
620
+ });
621
+ return result;
622
+ } else if (this.type === "intersect") {
623
+ const result = {};
624
+ for (const item of this.list) Object.assign(result, item.simplify(value));
625
+ return result;
626
+ } else if (this.type === "union") for (const schema of this.list) try {
627
+ Schema.resolve(value, schema, {});
628
+ return schema.simplify(value);
629
+ } catch {}
630
+ return value;
631
+ }, "simplify");
632
+ Schema.prototype.toString = /* @__PURE__ */ __name(function toString(inline) {
633
+ return formatters[this.type]?.(this, inline) ?? `Schema<${this.type}>`;
634
+ }, "toString");
635
+ Schema.prototype.role = /* @__PURE__ */ __name(function role(role, extra2) {
636
+ const schema = Schema(this);
637
+ schema.meta = {
638
+ ...schema.meta,
639
+ role,
640
+ extra: extra2
641
+ };
642
+ return schema;
643
+ }, "role");
644
+ for (const key of [
645
+ "default",
646
+ "link",
647
+ "comment",
648
+ "description",
649
+ "max",
650
+ "min",
651
+ "step"
652
+ ]) Object.assign(Schema.prototype, { [key](value) {
653
+ const schema = Schema(this);
654
+ schema.meta = {
655
+ ...schema.meta,
656
+ [key]: value
657
+ };
658
+ return schema;
659
+ } });
660
+ var resolvers = {};
661
+ Schema.extend = /* @__PURE__ */ __name(function extend(type, resolve2) {
662
+ resolvers[type] = resolve2;
663
+ }, "extend");
664
+ Schema.resolve = /* @__PURE__ */ __name(function resolve(data, schema, options = {}, strict = false) {
665
+ if (!schema) return [data];
666
+ if (options.ignore?.(data, schema)) return [data];
667
+ if ((0, import_cosmokit.isNullable)(data) && schema.type !== "lazy") {
668
+ if (schema.meta.required) throw new ValidationError(`missing required value`, options);
669
+ let current = schema;
670
+ let fallback = schema.meta.default;
671
+ while (current?.type === "intersect" && (0, import_cosmokit.isNullable)(fallback)) {
672
+ current = current.list[0];
673
+ fallback = current?.meta.default;
674
+ }
675
+ if ((0, import_cosmokit.isNullable)(fallback)) return [data];
676
+ data = (0, import_cosmokit.clone)(fallback);
677
+ }
678
+ const callback = resolvers[schema.type];
679
+ if (!callback) throw new ValidationError(`unsupported type "${schema.type}"`, options);
680
+ try {
681
+ return callback(data, schema, options, strict);
682
+ } catch (error) {
683
+ if (!schema.meta.loose) throw error;
684
+ return [schema.meta.default];
685
+ }
686
+ }, "resolve");
687
+ Schema.from = /* @__PURE__ */ __name(function from(source) {
688
+ if ((0, import_cosmokit.isNullable)(source)) return Schema.any();
689
+ else if ([
690
+ "string",
691
+ "number",
692
+ "boolean"
693
+ ].includes(typeof source)) return Schema.const(source).required();
694
+ else if (source[kSchema]) return source;
695
+ else if (typeof source === "function") switch (source) {
696
+ case String: return Schema.string().required();
697
+ case Number: return Schema.number().required();
698
+ case Boolean: return Schema.boolean().required();
699
+ case Function: return Schema.function().required();
700
+ default: return Schema.is(source).required();
701
+ }
702
+ else throw new TypeError(`cannot infer schema from ${source}`);
703
+ }, "from");
704
+ Schema.lazy = /* @__PURE__ */ __name(function lazy(builder) {
705
+ const schema = new Schema({
706
+ type: "lazy",
707
+ builder,
708
+ inner: { toJSON: /* @__PURE__ */ __name(() => {
709
+ if (!schema.inner[kSchema]) {
710
+ schema.inner = schema.builder();
711
+ schema.inner.meta = {
712
+ ...schema.meta,
713
+ ...schema.inner.meta
714
+ };
715
+ }
716
+ return schema.inner.toJSON();
717
+ }, "toJSON") }
718
+ });
719
+ return schema;
720
+ }, "lazy");
721
+ Schema.natural = /* @__PURE__ */ __name(function natural() {
722
+ return Schema.number().step(1).min(0);
723
+ }, "natural");
724
+ Schema.percent = /* @__PURE__ */ __name(function percent() {
725
+ return Schema.number().step(.01).min(0).max(1).role("slider");
726
+ }, "percent");
727
+ Schema.date = /* @__PURE__ */ __name(function date() {
728
+ return Schema.union([Schema.is(Date), Schema.transform(Schema.string().role("datetime"), (value, options) => {
729
+ const date2 = new Date(value);
730
+ if (isNaN(+date2)) throw new ValidationError(`invalid date "${value}"`, options);
731
+ return date2;
732
+ }, true)]);
733
+ }, "date");
734
+ Schema.regExp = /* @__PURE__ */ __name(function regExp(flag = "") {
735
+ return Schema.union([Schema.is(RegExp), Schema.transform(Schema.string().role("regexp", { flag }), (value, options) => {
736
+ try {
737
+ return new RegExp(value, flag);
738
+ } catch (e) {
739
+ throw new ValidationError(e.message, options);
740
+ }
741
+ }, true)]);
742
+ }, "regExp");
743
+ Schema.arrayBuffer = /* @__PURE__ */ __name(function arrayBuffer(encoding) {
744
+ return Schema.union([
745
+ Schema.is(ArrayBuffer),
746
+ Schema.is(SharedArrayBuffer),
747
+ Schema.transform(Schema.any(), (value, options) => {
748
+ if (import_cosmokit.Binary.isSource(value)) return import_cosmokit.Binary.fromSource(value);
749
+ throw new ValidationError(`expected ArrayBufferSource but got ${value}`, options);
750
+ }, true),
751
+ ...encoding ? [Schema.transform(Schema.string(), (value, options) => {
752
+ try {
753
+ return encoding === "base64" ? import_cosmokit.Binary.fromBase64(value) : import_cosmokit.Binary.fromHex(value);
754
+ } catch (e) {
755
+ throw new ValidationError(e.message, options);
756
+ }
757
+ }, true)] : []
758
+ ]);
759
+ }, "arrayBuffer");
760
+ Schema.extend("lazy", (data, schema, options, strict) => {
761
+ if (!schema.inner[kSchema]) {
762
+ schema.inner = schema.builder();
763
+ schema.inner.meta = {
764
+ ...schema.meta,
765
+ ...schema.inner.meta
766
+ };
767
+ }
768
+ return Schema.resolve(data, schema.inner, options, strict);
769
+ });
770
+ Schema.extend("any", (data) => {
771
+ return [data];
772
+ });
773
+ Schema.extend("never", (data, _, options) => {
774
+ throw new ValidationError(`expected nullable but got ${data}`, options);
775
+ });
776
+ Schema.extend("const", (data, { value }, options) => {
777
+ if ((0, import_cosmokit.deepEqual)(data, value)) return [value];
778
+ throw new ValidationError(`expected ${value} but got ${data}`, options);
779
+ });
780
+ function checkWithinRange(data, meta, description, options, skipMin = false) {
781
+ const { max = Infinity, min = -Infinity } = meta;
782
+ if (data > max) throw new ValidationError(`expected ${description} <= ${max} but got ${data}`, options);
783
+ if (data < min && !skipMin) throw new ValidationError(`expected ${description} >= ${min} but got ${data}`, options);
784
+ }
785
+ __name(checkWithinRange, "checkWithinRange");
786
+ Schema.extend("string", (data, { meta }, options) => {
787
+ if (typeof data !== "string") throw new ValidationError(`expected string but got ${data}`, options);
788
+ if (meta.pattern) {
789
+ const regexp = new RegExp(meta.pattern.source, meta.pattern.flags);
790
+ if (!regexp.test(data)) throw new ValidationError(`expect string to match regexp ${regexp}`, options);
791
+ }
792
+ checkWithinRange(data.length, meta, "string length", options);
793
+ return [data];
794
+ });
795
+ function decimalShift(data, digits) {
796
+ const str = data.toString();
797
+ if (str.includes("e")) return data * Math.pow(10, digits);
798
+ const index = str.indexOf(".");
799
+ if (index === -1) return data * Math.pow(10, digits);
800
+ const frac = str.slice(index + 1);
801
+ const integer = str.slice(0, index);
802
+ if (frac.length <= digits) return +(integer + frac.padEnd(digits, "0"));
803
+ return +(integer + frac.slice(0, digits) + "." + frac.slice(digits));
804
+ }
805
+ __name(decimalShift, "decimalShift");
806
+ function isMultipleOf(data, min, step) {
807
+ step = Math.abs(step);
808
+ if (!/^\d+\.\d+$/.test(step.toString())) return (data - min) % step === 0;
809
+ const index = step.toString().indexOf(".");
810
+ const digits = step.toString().slice(index + 1).length;
811
+ return Math.abs(decimalShift(data, digits) - decimalShift(min, digits)) % decimalShift(step, digits) === 0;
812
+ }
813
+ __name(isMultipleOf, "isMultipleOf");
814
+ Schema.extend("number", (data, { meta }, options) => {
815
+ if (typeof data !== "number") throw new ValidationError(`expected number but got ${data}`, options);
816
+ checkWithinRange(data, meta, "number", options);
817
+ const { step } = meta;
818
+ if (step && !isMultipleOf(data, meta.min ?? 0, step)) throw new ValidationError(`expected number multiple of ${step} but got ${data}`, options);
819
+ return [data];
820
+ });
821
+ Schema.extend("boolean", (data, _, options) => {
822
+ if (typeof data === "boolean") return [data];
823
+ throw new ValidationError(`expected boolean but got ${data}`, options);
824
+ });
825
+ Schema.extend("bitset", (data, { bits, meta }, options) => {
826
+ let value = 0, keys = [];
827
+ if (typeof data === "number") {
828
+ value = data;
829
+ for (const key in bits) if (data & bits[key]) keys.push(key);
830
+ } else if (Array.isArray(data)) {
831
+ keys = data;
832
+ for (const key of keys) {
833
+ if (typeof key !== "string") throw new ValidationError(`expected string but got ${key}`, options);
834
+ if (key in bits) value |= bits[key];
835
+ }
836
+ } else throw new ValidationError(`expected number or array but got ${data}`, options);
837
+ if (value === meta.default) return [value];
838
+ return [value, keys];
839
+ });
840
+ Schema.extend("function", (data, _, options) => {
841
+ if (typeof data === "function") return [data];
842
+ throw new ValidationError(`expected function but got ${data}`, options);
843
+ });
844
+ Schema.extend("is", (data, { constructor }, options) => {
845
+ if (typeof constructor === "function") {
846
+ if (data instanceof constructor) return [data];
847
+ throw new ValidationError(`expected ${constructor.name} but got ${data}`, options);
848
+ } else {
849
+ if ((0, import_cosmokit.isNullable)(data)) throw new ValidationError(`expected ${constructor} but got ${data}`, options);
850
+ let prototype = Object.getPrototypeOf(data);
851
+ while (prototype) {
852
+ if (prototype.constructor?.name === constructor) return [data];
853
+ prototype = Object.getPrototypeOf(prototype);
854
+ }
855
+ throw new ValidationError(`expected ${constructor} but got ${data}`, options);
856
+ }
857
+ });
858
+ function property(data, key, schema, options) {
859
+ try {
860
+ const [value, adapted] = Schema.resolve(data[key], schema, {
861
+ ...options,
862
+ path: [...options.path || [], key]
863
+ });
864
+ if (adapted !== void 0) data[key] = adapted;
865
+ return value;
866
+ } catch (e) {
867
+ if (!options?.autofix) throw e;
868
+ delete data[key];
869
+ return schema.meta.default;
870
+ }
871
+ }
872
+ __name(property, "property");
873
+ Schema.extend("array", (data, { inner, meta }, options) => {
874
+ if (!Array.isArray(data)) throw new ValidationError(`expected array but got ${data}`, options);
875
+ checkWithinRange(data.length, meta, "array length", options, !(0, import_cosmokit.isNullable)(inner.meta.default));
876
+ return [data.map((_, index) => property(data, index, inner, options))];
877
+ });
878
+ Schema.extend("dict", (data, { inner, sKey }, options, strict) => {
879
+ if (!(0, import_cosmokit.isPlainObject)(data)) throw new ValidationError(`expected object but got ${data}`, options);
880
+ const result = {};
881
+ for (const key in data) {
882
+ let rKey;
883
+ try {
884
+ rKey = Schema.resolve(key, sKey, options)[0];
885
+ } catch (error) {
886
+ if (strict) continue;
887
+ throw error;
888
+ }
889
+ result[rKey] = property(data, key, inner, options);
890
+ data[rKey] = data[key];
891
+ if (key !== rKey) delete data[key];
892
+ }
893
+ return [result];
894
+ });
895
+ Schema.extend("tuple", (data, { list }, options, strict) => {
896
+ if (!Array.isArray(data)) throw new ValidationError(`expected array but got ${data}`, options);
897
+ const result = list.map((inner, index) => property(data, index, inner, options));
898
+ if (strict) return [result];
899
+ result.push(...data.slice(list.length));
900
+ return [result];
901
+ });
902
+ function merge(result, data) {
903
+ for (const key in data) {
904
+ if (key in result) continue;
905
+ result[key] = data[key];
906
+ }
907
+ }
908
+ __name(merge, "merge");
909
+ Schema.extend("object", (data, { dict }, options, strict) => {
910
+ if (!(0, import_cosmokit.isPlainObject)(data)) throw new ValidationError(`expected object but got ${data}`, options);
911
+ const result = {};
912
+ for (const key in dict) {
913
+ const value = property(data, key, dict[key], options);
914
+ if (!(0, import_cosmokit.isNullable)(value) || key in data) result[key] = value;
915
+ }
916
+ if (!strict) merge(result, data);
917
+ return [result];
918
+ });
919
+ Schema.extend("union", (data, { list, toString: toString2 }, options, strict) => {
920
+ const messages = [];
921
+ for (const inner of list) try {
922
+ return Schema.resolve(data, inner, options, strict);
923
+ } catch (error) {
924
+ messages.push(error);
925
+ }
926
+ throw new ValidationError(`expected ${toString2()} but got ${JSON.stringify(data)}`, options);
927
+ });
928
+ Schema.extend("intersect", (data, { list, toString: toString2 }, options, strict) => {
929
+ if (!list.length) return [data];
930
+ let result;
931
+ for (const inner of list) {
932
+ const value = Schema.resolve(data, inner, options, true)[0];
933
+ if ((0, import_cosmokit.isNullable)(value)) continue;
934
+ if ((0, import_cosmokit.isNullable)(result)) result = value;
935
+ else if (typeof result !== typeof value) throw new ValidationError(`expected ${toString2()} but got ${JSON.stringify(data)}`, options);
936
+ else if (typeof value === "object") merge(result ??= {}, value);
937
+ else if (result !== value) throw new ValidationError(`expected ${toString2()} but got ${JSON.stringify(data)}`, options);
938
+ }
939
+ if (!strict && (0, import_cosmokit.isPlainObject)(data)) merge(result, data);
940
+ return [result];
941
+ });
942
+ Schema.extend("transform", (data, { inner, callback, preserve }, options) => {
943
+ const [result, adapted = data] = Schema.resolve(data, inner, options, true);
944
+ if (preserve) return [callback(result)];
945
+ else return [callback(result), callback(adapted)];
946
+ });
947
+ var formatters = {};
948
+ function defineMethod(name, keys, format) {
949
+ formatters[name] = format;
950
+ Object.assign(Schema, { [name](...args) {
951
+ const schema = new Schema({ type: name });
952
+ keys.forEach((key, index) => {
953
+ switch (key) {
954
+ case "sKey":
955
+ schema.sKey = args[index] ?? Schema.string();
956
+ break;
957
+ case "inner":
958
+ schema.inner = Schema.from(args[index]);
959
+ break;
960
+ case "list":
961
+ schema.list = args[index].map(Schema.from);
962
+ break;
963
+ case "dict":
964
+ schema.dict = (0, import_cosmokit.valueMap)(args[index], Schema.from);
965
+ break;
966
+ case "bits":
967
+ schema.bits = {};
968
+ for (const key2 in args[index]) {
969
+ if (typeof args[index][key2] !== "number") continue;
970
+ schema.bits[key2] = args[index][key2];
971
+ }
972
+ break;
973
+ case "callback": {
974
+ const callback = schema.callback = args[index];
975
+ callback["toJSON"] ||= () => callback.toString();
976
+ break;
977
+ }
978
+ case "constructor": {
979
+ const constructor = schema.constructor = args[index];
980
+ if (typeof constructor === "function") constructor["toJSON"] ||= () => constructor["name"];
981
+ break;
982
+ }
983
+ default: schema[key] = args[index];
984
+ }
985
+ });
986
+ if (name === "object" || name === "dict") schema.meta.default = {};
987
+ else if (name === "array" || name === "tuple") schema.meta.default = [];
988
+ else if (name === "bitset") schema.meta.default = 0;
989
+ return schema;
990
+ } });
991
+ }
992
+ __name(defineMethod, "defineMethod");
993
+ defineMethod("is", ["constructor"], ({ constructor }) => {
994
+ if (typeof constructor === "function") return constructor.name;
995
+ else return constructor;
996
+ });
997
+ defineMethod("any", [], () => "any");
998
+ defineMethod("never", [], () => "never");
999
+ defineMethod("const", ["value"], ({ value }) => typeof value === "string" ? JSON.stringify(value) : value);
1000
+ defineMethod("string", [], () => "string");
1001
+ defineMethod("number", [], () => "number");
1002
+ defineMethod("boolean", [], () => "boolean");
1003
+ defineMethod("bitset", ["bits"], () => "bitset");
1004
+ defineMethod("function", [], () => "function");
1005
+ defineMethod("array", ["inner"], ({ inner }) => `${inner.toString(true)}[]`);
1006
+ defineMethod("dict", ["inner", "sKey"], ({ inner, sKey }) => `{ [key: ${sKey.toString()}]: ${inner.toString()} }`);
1007
+ defineMethod("tuple", ["list"], ({ list }) => `[${list.map((inner) => inner.toString()).join(", ")}]`);
1008
+ defineMethod("object", ["dict"], ({ dict }) => {
1009
+ if (Object.keys(dict).length === 0) return "{}";
1010
+ return `{ ${Object.entries(dict).map(([key, inner]) => {
1011
+ return `${key}${inner.meta.required ? "" : "?"}: ${inner.toString()}`;
1012
+ }).join(", ")} }`;
1013
+ });
1014
+ defineMethod("union", ["list"], ({ list }, inline) => {
1015
+ const result = list.map(({ toString: format }) => format()).join(" | ");
1016
+ return inline ? `(${result})` : result;
1017
+ });
1018
+ defineMethod("intersect", ["list"], ({ list }) => {
1019
+ return `${list.map((inner) => inner.toString(true)).join(" & ")}`;
1020
+ });
1021
+ defineMethod("transform", [
1022
+ "inner",
1023
+ "callback",
1024
+ "preserve"
1025
+ ], ({ inner }, isInner) => inner.toString(isInner));
1026
+ module.exports = Schema;
1027
+ })))(), 1);
1028
+ /** Catchable error for illegal/unknown selectors (config-warning path). */
1029
+ var SelectorError = class extends Error {
1030
+ constructor(message) {
1031
+ super(message);
1032
+ this.name = "SelectorError";
1033
+ }
1034
+ };
1035
+ /**
1036
+ * Parse a chain key or entry selector.
1037
+ *
1038
+ * Accepts `provider/model` and `provider/*`; throws {@link SelectorError}
1039
+ * on anything else (missing separator, empty parts, extra separators).
1040
+ */
1041
+ function parseSelector(input) {
1042
+ if (typeof input !== "string") throw new SelectorError(`invalid selector ${String(input)}: expected "provider/model" or "provider/*"`);
1043
+ const trimmed = input.trim();
1044
+ const slash = trimmed.indexOf("/");
1045
+ if (slash <= 0 || slash === trimmed.length - 1) throw new SelectorError(`invalid selector "${input}": expected "provider/model" or "provider/*"`);
1046
+ const provider = trimmed.slice(0, slash).trim();
1047
+ const modelPart = trimmed.slice(slash + 1).trim();
1048
+ if (!provider || !modelPart) throw new SelectorError(`invalid selector "${input}": empty provider or model`);
1049
+ if (modelPart.includes("/")) throw new SelectorError(`invalid selector "${input}": unexpected extra separator`);
1050
+ return {
1051
+ provider,
1052
+ model: modelPart === "*" ? void 0 : modelPart,
1053
+ raw: trimmed
1054
+ };
1055
+ }
1056
+ //#endregion
1057
+ //#region src/config.ts
1058
+ /**
1059
+ * The `fallbacks` settings namespace: plugin config schema + defaults.
1060
+ *
1061
+ * Two-block config model (plan fallbacks-role-config-model): block 1
1062
+ * `rootChain` — the root agent's single fallback chain (empty = no
1063
+ * degradation) — plus block 2 declared role entities: `roles.list`
1064
+ * (id/label/description/prompt?/permissions?/chain?/fallback) and
1065
+ * `roles.rules` enum references into the declared ids (or the built-in
1066
+ * `'inherit'` role). The legacy `chains` / `roles.default` keys are gone
1067
+ * from the schema and type (zero residual, migration table excepted); the
1068
+ * runtime consumes the new shape directly and flags surviving legacy keys
1069
+ * at startup via `detectLegacyKeys` (see `src/index.ts` apply()).
1070
+ *
1071
+ * Spec §4 is authoritative for field names and default values — notably
1072
+ * `triggerCodes` defaults to dsh's stable failure codes `['AUTH', 'QUOTA',
1073
+ * 'RATE_LIMIT']` (there is no `QUOTA_EXCEEDED` code in dsh), and an
1074
+ * unconfigured install (`enabled: false`, empty `rootChain`, empty roles)
1075
+ * is a no-op pass-through exactly like an uninstalled plugin (AC-8).
1076
+ *
1077
+ * This module is pure logic: it must not import any `@deepseek-ai/*` package
1078
+ * (types included) — `FallbacksConfig` is the plugin's own type. Task 3
1079
+ * registers this schema with `installSettingsSection` under the `fallbacks`
1080
+ * settings namespace.
1081
+ *
1082
+ * @module dsh-llm-fallbacks/config
1083
+ */
1084
+ /**
1085
+ * Spec §4 defaults — `Config({})` must equal this (no-op install).
1086
+ * `enabled` defaults to `false` (readme-settings spec §1.2): the feature
1087
+ * switch is off until the user turns it on in the settings page; an
1088
+ * unconfigured install (`enabled: false`, empty rootChain, empty roles)
1089
+ * behaves exactly like an uninstalled plugin (AC-3 / no-op invariant).
1090
+ */
1091
+ const defaultFallbacksConfig = {
1092
+ enabled: false,
1093
+ triggerCodes: [
1094
+ "AUTH",
1095
+ "QUOTA",
1096
+ "RATE_LIMIT"
1097
+ ],
1098
+ rootChain: [],
1099
+ roles: {
1100
+ list: [],
1101
+ rules: []
1102
+ },
1103
+ cooldownMs: 3e5,
1104
+ revertPolicy: "cooldown-expiry",
1105
+ maxSwitchesPerStep: 8,
1106
+ alwaysModeRetryCap: 5
1107
+ };
1108
+ /**
1109
+ * Reserved role id: legal as a rule target (`roles.rules[].role`) and as
1110
+ * the no-rule-match fallback, but FORBIDDEN in `roles.list[].id`.
1111
+ */
1112
+ const INHERIT_ROLE_ID = "inherit";
1113
+ /** Role id format (aligned with yet-another-subagent `isValidProfileId`). */
1114
+ const ROLE_ID_PATTERN = /^[a-z0-9-]{1,32}$/;
1115
+ import_lib.default.object({
1116
+ enabled: import_lib.default.boolean().default(false),
1117
+ triggerCodes: import_lib.default.array(import_lib.default.string()).default([
1118
+ "AUTH",
1119
+ "QUOTA",
1120
+ "RATE_LIMIT"
1121
+ ]),
1122
+ rootChain: import_lib.default.array(import_lib.default.string()).default([]),
1123
+ roles: import_lib.default.object({
1124
+ list: import_lib.default.array(import_lib.default.object({
1125
+ id: import_lib.default.string().required(),
1126
+ label: import_lib.default.string().default(""),
1127
+ description: import_lib.default.string().default(""),
1128
+ prompt: import_lib.default.string(),
1129
+ permissions: import_lib.default.object({
1130
+ allow: import_lib.default.array(import_lib.default.string()),
1131
+ deny: import_lib.default.array(import_lib.default.string())
1132
+ }),
1133
+ chain: import_lib.default.array(import_lib.default.string()),
1134
+ fallback: import_lib.default.union([import_lib.default.const("inherit-root"), import_lib.default.const("none")]).default("inherit-root")
1135
+ })).default([]),
1136
+ rules: import_lib.default.array(import_lib.default.object({
1137
+ origin: import_lib.default.union([import_lib.default.const("root"), import_lib.default.const("subagent")]),
1138
+ provider: import_lib.default.string(),
1139
+ model: import_lib.default.string(),
1140
+ role: import_lib.default.string().required()
1141
+ })).default([])
1142
+ }).default({
1143
+ list: [],
1144
+ rules: []
1145
+ }),
1146
+ cooldownMs: import_lib.default.number().default(3e5),
1147
+ revertPolicy: import_lib.default.union([import_lib.default.const("cooldown-expiry"), import_lib.default.const("never")]).default("cooldown-expiry"),
1148
+ maxSwitchesPerStep: import_lib.default.number().default(8),
1149
+ alwaysModeRetryCap: import_lib.default.number().default(5)
1150
+ });
1151
+ //#endregion
1152
+ //#region src/client/fallbacks-store.ts
1153
+ /** The plugin's settings namespace on the host wire (settings/document-updated ns filter). */
1154
+ const FALLBACKS_SETTINGS_NS = "fallbacks";
1155
+ function messageOf(error) {
1156
+ return error instanceof Error ? error.message : String(error);
1157
+ }
1158
+ function isRecord(value) {
1159
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1160
+ }
1161
+ /**
1162
+ * Read a nested value by path — the `@deepseek-ai/dsh-client-schema-form`
1163
+ * `getPath` semantics, copied locally so the provider-configured join needs no
1164
+ * new dependency (array indexes as numeric keys, `undefined` along a missing
1165
+ * branch).
1166
+ */
1167
+ function getPath(value, path) {
1168
+ let current = value;
1169
+ for (const key of path) {
1170
+ if (Array.isArray(current)) {
1171
+ current = current[Number(key)];
1172
+ continue;
1173
+ }
1174
+ if (typeof current !== "object" || current === null) return void 0;
1175
+ current = current[key];
1176
+ }
1177
+ return current;
1178
+ }
1179
+ /**
1180
+ * The provider dropdown's offer set (spec §2.5 D-4): catalog providers whose
1181
+ * settings profile resolves in the describe namespaces — the Models page's
1182
+ * `configured` predicate (`ui-models` store.ts): a provider is configured
1183
+ * when its settings namespace exists AND either it addresses the whole
1184
+ * section (`settingsPath` empty) or its profile path resolves in the resolved
1185
+ * value. Directory-only (unconfigured) providers never become options; the
1186
+ * section still renders existing values for them (read-back + annotation) so
1187
+ * nothing is lost on save.
1188
+ */
1189
+ function configuredProvidersOf(providers, namespaces) {
1190
+ return providers.filter((entry) => {
1191
+ const namespace = namespaces.get(entry.settingsNs);
1192
+ return namespace !== void 0 && (entry.settingsPath.length === 0 || getPath(namespace.value, entry.settingsPath) !== void 0);
1193
+ });
1194
+ }
1195
+ /**
1196
+ * Fold the redacted descriptor value into a complete {@link FallbacksConfig}:
1197
+ * missing optional fields take spec §4 defaults; gross type mismatches throw
1198
+ * so the UI can surface a broken descriptor instead of mis-rendering.
1199
+ */
1200
+ function parseFallbacksConfig(value) {
1201
+ if (!isRecord(value)) throw new TypeError(`fallbacks descriptor value is not an object: ${String(value)}`);
1202
+ const triggerCodes = value.triggerCodes;
1203
+ if (triggerCodes !== void 0 && (!Array.isArray(triggerCodes) || triggerCodes.some((code) => typeof code !== "string"))) throw new TypeError("fallbacks descriptor triggerCodes must be a string array");
1204
+ const rootChain = value.rootChain;
1205
+ if (rootChain !== void 0 && (!Array.isArray(rootChain) || rootChain.some((entry) => typeof entry !== "string"))) throw new TypeError("fallbacks descriptor rootChain must be a string array");
1206
+ const roles = isRecord(value.roles) ? value.roles : {};
1207
+ const parsedList = (Array.isArray(roles.list) ? roles.list : []).map((role, index) => {
1208
+ if (!isRecord(role) || typeof role.id !== "string") throw new TypeError(`fallbacks descriptor roles.list[${String(index)}] must have a string id`);
1209
+ const label = role.label;
1210
+ if (label !== void 0 && typeof label !== "string") throw new TypeError(`fallbacks descriptor roles.list[${String(index)}].label must be a string`);
1211
+ const description = role.description;
1212
+ if (description !== void 0 && typeof description !== "string") throw new TypeError(`fallbacks descriptor roles.list[${String(index)}].description must be a string`);
1213
+ const prompt = role.prompt;
1214
+ if (prompt !== void 0 && typeof prompt !== "string") throw new TypeError(`fallbacks descriptor roles.list[${String(index)}].prompt must be a string`);
1215
+ const permissions = role.permissions;
1216
+ if (permissions !== void 0 && (!isRecord(permissions) || permissions.allow !== void 0 && (!Array.isArray(permissions.allow) || permissions.allow.some((item) => typeof item !== "string")) || permissions.deny !== void 0 && (!Array.isArray(permissions.deny) || permissions.deny.some((item) => typeof item !== "string")))) throw new TypeError(`fallbacks descriptor roles.list[${String(index)}].permissions must be an allow/deny string-array object`);
1217
+ const chain = role.chain;
1218
+ if (chain !== void 0 && (!Array.isArray(chain) || chain.some((entry) => typeof entry !== "string"))) throw new TypeError(`fallbacks descriptor roles.list[${String(index)}].chain must be a string array`);
1219
+ const fallback = role.fallback;
1220
+ if (fallback !== void 0 && fallback !== "inherit-root" && fallback !== "none") throw new TypeError(`fallbacks descriptor roles.list[${String(index)}].fallback must be inherit-root|none`);
1221
+ return {
1222
+ id: role.id,
1223
+ label: label ?? "",
1224
+ description: description ?? "",
1225
+ ...prompt === void 0 ? {} : { prompt },
1226
+ ...permissions === void 0 ? {} : { permissions },
1227
+ chain: chain ?? [],
1228
+ fallback: fallback ?? "inherit-root"
1229
+ };
1230
+ });
1231
+ const parsedRules = (Array.isArray(roles.rules) ? roles.rules : []).map((rule, index) => {
1232
+ if (!isRecord(rule) || typeof rule.role !== "string") throw new TypeError(`fallbacks descriptor roles.rules[${String(index)}] must have a string role`);
1233
+ const origin = rule.origin;
1234
+ if (origin !== void 0 && origin !== "root" && origin !== "subagent") throw new TypeError(`fallbacks descriptor roles.rules[${String(index)}].origin must be root|subagent`);
1235
+ const provider = rule.provider;
1236
+ const model = rule.model;
1237
+ if (provider !== void 0 && typeof provider !== "string") throw new TypeError(`fallbacks descriptor roles.rules[${String(index)}].provider must be a string`);
1238
+ if (model !== void 0 && typeof model !== "string") throw new TypeError(`fallbacks descriptor roles.rules[${String(index)}].model must be a string`);
1239
+ return {
1240
+ ...origin === void 0 ? {} : { origin },
1241
+ ...provider === void 0 ? {} : { provider },
1242
+ ...model === void 0 ? {} : { model },
1243
+ role: rule.role
1244
+ };
1245
+ });
1246
+ const cooldownMs = value.cooldownMs;
1247
+ const maxSwitchesPerStep = value.maxSwitchesPerStep;
1248
+ const alwaysModeRetryCap = value.alwaysModeRetryCap;
1249
+ for (const [field, raw] of [
1250
+ ["cooldownMs", cooldownMs],
1251
+ ["maxSwitchesPerStep", maxSwitchesPerStep],
1252
+ ["alwaysModeRetryCap", alwaysModeRetryCap]
1253
+ ]) if (raw !== void 0 && typeof raw !== "number") throw new TypeError(`fallbacks descriptor ${field} must be a number`);
1254
+ const revertPolicy = value.revertPolicy;
1255
+ if (revertPolicy !== void 0 && revertPolicy !== "cooldown-expiry" && revertPolicy !== "never") throw new TypeError("fallbacks descriptor revertPolicy must be cooldown-expiry|never");
1256
+ const enabled = value.enabled;
1257
+ if (enabled !== void 0 && typeof enabled !== "boolean") throw new TypeError("fallbacks descriptor enabled must be a boolean");
1258
+ return {
1259
+ enabled: enabled ?? defaultFallbacksConfig.enabled,
1260
+ triggerCodes: triggerCodes ?? [...defaultFallbacksConfig.triggerCodes],
1261
+ rootChain: rootChain ?? [...defaultFallbacksConfig.rootChain],
1262
+ roles: {
1263
+ list: parsedList,
1264
+ rules: parsedRules
1265
+ },
1266
+ cooldownMs: cooldownMs ?? defaultFallbacksConfig.cooldownMs,
1267
+ revertPolicy: revertPolicy ?? defaultFallbacksConfig.revertPolicy,
1268
+ maxSwitchesPerStep: maxSwitchesPerStep ?? defaultFallbacksConfig.maxSwitchesPerStep,
1269
+ alwaysModeRetryCap: alwaysModeRetryCap ?? defaultFallbacksConfig.alwaysModeRetryCap
1270
+ };
1271
+ }
1272
+ /** The raw selector string a selection serializes to ('' when empty). */
1273
+ function selectionToRaw(selection) {
1274
+ return selection === null ? "" : selection.kind === "catalog" ? selection.id : selection.raw;
1275
+ }
1276
+ /**
1277
+ * Classify a raw provider value against the catalog: a catalog route id is a
1278
+ * catalog selection, anything else is an outside value kept verbatim.
1279
+ */
1280
+ function classifyProvider(raw, catalog) {
1281
+ if (raw === "") return null;
1282
+ if (catalog !== void 0 && catalog.providers.some((entry) => entry.provider === raw)) return {
1283
+ kind: "catalog",
1284
+ id: raw
1285
+ };
1286
+ return {
1287
+ kind: "outside",
1288
+ raw
1289
+ };
1290
+ }
1291
+ /**
1292
+ * Classify a raw model value under its provider against the catalog: a model
1293
+ * id advertised by that provider is a catalog selection, anything else is an
1294
+ * outside value kept verbatim.
1295
+ */
1296
+ function classifyModel(provider, raw, catalog) {
1297
+ if (raw === "") return null;
1298
+ if (catalog !== void 0 && catalog.groups.some((group) => group.id === provider && group.models.some((model) => model.id === raw))) return {
1299
+ kind: "catalog",
1300
+ id: raw
1301
+ };
1302
+ return {
1303
+ kind: "outside",
1304
+ raw
1305
+ };
1306
+ }
1307
+ /**
1308
+ * Extract the most recent `fallbacks/switch` events from one history page
1309
+ * (spec §2.5 D-5): filter by event type, order by `seq` descending, take at
1310
+ * most `limit`. Single-page read — fewer than `limit` events show as-is; no
1311
+ * multi-page backfill (Non-Goal).
1312
+ */
1313
+ function extractRecentSwitches(entries, limit = 5) {
1314
+ const switches = [];
1315
+ for (const entry of entries) {
1316
+ const event = entry.event;
1317
+ if (event.type !== "fallbacks/switch") continue;
1318
+ switches.push({
1319
+ ...event.data,
1320
+ seq: event.seq,
1321
+ time: event.time
1322
+ });
1323
+ }
1324
+ switches.sort((a, b) => b.seq - a.seq);
1325
+ return switches.slice(0, limit);
1326
+ }
1327
+ /** The config's primary target: the rootChain's first entry (D-6 ③). */
1328
+ function configPrimaryTarget(config) {
1329
+ const firstEntry = config.rootChain[0];
1330
+ if (firstEntry === void 0) return null;
1331
+ try {
1332
+ const selector = parseSelector(firstEntry);
1333
+ return {
1334
+ provider: selector.provider,
1335
+ model: selector.model ?? "*"
1336
+ };
1337
+ } catch {
1338
+ return {
1339
+ provider: firstEntry,
1340
+ model: "*"
1341
+ };
1342
+ }
1343
+ }
1344
+ /**
1345
+ * Derive the status block's "current effective model" (spec §2.5 D-6): ①
1346
+ * disabled / empty rootChain → unavailable; ② a recent switch exists → the
1347
+ * latest one's `to`; ③ otherwise → the config's primary target. A **display
1348
+ * value** — never a live route probe (the section appends the non-probing
1349
+ * note inline right after the derived value, available case only; the
1350
+ * unavailable 空态 renders its own copy without the note).
1351
+ */
1352
+ function deriveEffectiveModel(config, switches) {
1353
+ if (!config.enabled || config.rootChain.length === 0) return { kind: "unavailable" };
1354
+ const latest = switches[0];
1355
+ if (latest !== void 0) return {
1356
+ kind: "switched",
1357
+ provider: latest.to.provider,
1358
+ model: latest.to.model
1359
+ };
1360
+ const target = configPrimaryTarget(config);
1361
+ if (target === null) return { kind: "unavailable" };
1362
+ return {
1363
+ kind: "config",
1364
+ ...target
1365
+ };
1366
+ }
1367
+ /** Serialize one selector row to its wire string (`provider/model` | `provider/*`). */
1368
+ function selectorRowToRaw(row) {
1369
+ const provider = selectionToRaw(row.provider);
1370
+ if (provider === "") return "";
1371
+ if (row.wildcard) return `${provider}/*`;
1372
+ const model = selectionToRaw(row.model);
1373
+ return model === "" ? provider : `${provider}/${model}`;
1374
+ }
1375
+ /** Parse one entry line into a selector row, classifying against the catalog. */
1376
+ function entryToSelectorRow(entry, catalog) {
1377
+ try {
1378
+ const selector = parseSelector(entry);
1379
+ return {
1380
+ wildcard: selector.model === void 0,
1381
+ provider: classifyProvider(selector.provider, catalog),
1382
+ model: selector.model === void 0 ? null : classifyModel(selector.provider, selector.model, catalog)
1383
+ };
1384
+ } catch {
1385
+ return {
1386
+ wildcard: false,
1387
+ provider: {
1388
+ kind: "outside",
1389
+ raw: entry.trim()
1390
+ },
1391
+ model: null
1392
+ };
1393
+ }
1394
+ }
1395
+ /** Project the rootChain entries into editable rows (one flat chain row). */
1396
+ function rootChainToRows(rootChain, catalog) {
1397
+ return [{ selectors: rootChain.map((entry) => entryToSelectorRow(entry, catalog)) }];
1398
+ }
1399
+ /** Rebuild the rootChain from edited rows; rows with no usable selector drop out. */
1400
+ function rowsToRootChain(rows) {
1401
+ const entries = [];
1402
+ for (const row of rows) {
1403
+ if (row.selectors.length === 0) continue;
1404
+ for (const selector of row.selectors) {
1405
+ const raw = selectorRowToRaw(selector);
1406
+ if (raw !== "") entries.push(raw);
1407
+ }
1408
+ }
1409
+ return entries;
1410
+ }
1411
+ /** Project the declared roles into editable rows (chain selectors classified). */
1412
+ function rolesToRows(roles, catalog) {
1413
+ return roles.map((role) => ({
1414
+ id: role.id,
1415
+ label: role.label,
1416
+ description: role.description,
1417
+ selectors: (role.chain ?? []).map((entry) => entryToSelectorRow(entry, catalog)),
1418
+ fallback: role.fallback ?? "inherit-root"
1419
+ }));
1420
+ }
1421
+ /** Rebuild the declared roles from edited rows; empty selectors drop out. */
1422
+ function rowsToRoles(rows) {
1423
+ return rows.map((row) => ({
1424
+ id: row.id.trim(),
1425
+ label: row.label,
1426
+ description: row.description,
1427
+ chain: row.selectors.map(selectorRowToRaw).filter((entry) => entry !== ""),
1428
+ fallback: row.fallback
1429
+ }));
1430
+ }
1431
+ /**
1432
+ * Rebuild the declared roles from edited rows, re-attaching the
1433
+ * schema-reserved `prompt`/`permissions` fields from the last accepted
1434
+ * config by role id — they never round-trip through rows this round, so
1435
+ * without the merge a save would silently drop them (T2 reviewer minor
1436
+ * #2). The id trim matches {@link rowsToRoles}; a row whose id matches no
1437
+ * original role (a freshly added one) keeps no extras. Key order mirrors
1438
+ * `parseFallbacksConfig` so a clean draft's JSON dirty comparison never
1439
+ * flags it.
1440
+ */
1441
+ function mergeRoleExtras(rows, originalRoles) {
1442
+ const originalById = new Map(originalRoles.map((role) => [role.id, role]));
1443
+ return rowsToRoles(rows).map((role) => {
1444
+ const original = originalById.get(role.id);
1445
+ if (original === void 0) return role;
1446
+ return {
1447
+ id: role.id,
1448
+ label: role.label,
1449
+ description: role.description,
1450
+ ...original.prompt === void 0 ? {} : { prompt: original.prompt },
1451
+ ...original.permissions === void 0 ? {} : { permissions: original.permissions },
1452
+ chain: role.chain,
1453
+ fallback: role.fallback
1454
+ };
1455
+ });
1456
+ }
1457
+ /**
1458
+ * The `roles.rules` role dropdown's offer set — the ONLY data source for the
1459
+ * rule rows' role selector: the built-in `'inherit'` target plus every
1460
+ * declared `roles.list` id, in declaration order (a role added/removed on
1461
+ * the same page is reflected immediately).
1462
+ */
1463
+ function ruleRoleOptions(roles) {
1464
+ return [INHERIT_ROLE_ID, ...new Set(roles.list.map((role) => role.id.trim()))];
1465
+ }
1466
+ /** Project the role rules into editable rows (provider/model classified). */
1467
+ function rulesToRows(rules, catalog) {
1468
+ return rules.map((rule) => ({
1469
+ origin: rule.origin ?? "",
1470
+ provider: classifyProvider(rule.provider ?? "", catalog),
1471
+ model: classifyModel(rule.provider ?? "", rule.model ?? "", catalog),
1472
+ role: rule.role
1473
+ }));
1474
+ }
1475
+ /** Rebuild the role rules from edited rows; empty origin/provider/model drop out. */
1476
+ function rowsToRules(rows) {
1477
+ return rows.map((row) => ({
1478
+ ...row.origin === "" ? {} : { origin: row.origin },
1479
+ ...row.provider === null ? {} : { provider: selectionToRaw(row.provider) },
1480
+ ...row.model === null ? {} : { model: selectionToRaw(row.model) },
1481
+ role: row.role.trim()
1482
+ })).filter((rule) => rule.role !== "");
1483
+ }
1484
+ /** Controller joining Settings reads, writes, and pushed invalidations. */
1485
+ var FallbacksSettingsController = class {
1486
+ api;
1487
+ rpc;
1488
+ /** Snapshot consumed by the section through `useSyncExternalStore`. */
1489
+ store = (0, _deepseek_ai_dsh_client_runtime_client.createSnapshotStore)({
1490
+ status: "idle",
1491
+ error: null,
1492
+ writable: false,
1493
+ config: defaultFallbacksConfig,
1494
+ present: false,
1495
+ legacyKeys: [],
1496
+ catalogStatus: "idle",
1497
+ catalogError: null,
1498
+ providers: [],
1499
+ configuredProviders: [],
1500
+ groups: [],
1501
+ catalogEpoch: 0,
1502
+ switchesStatus: "idle",
1503
+ switchesError: null,
1504
+ switches: []
1505
+ });
1506
+ generation = 0;
1507
+ catalogGeneration = 0;
1508
+ switchesGeneration = 0;
1509
+ /** Every settings namespace from the last describe, keyed by ns — the configured-provider join's other input. */
1510
+ namespaces = /* @__PURE__ */ new Map();
1511
+ currentSession;
1512
+ /**
1513
+ * @param api - Settings / Llm / Sessions wire faces (describe `writable` +
1514
+ * namespace directory, provider/model catalog, session history).
1515
+ * @param rpc - the connection's generic RPC caller for the host gateway
1516
+ * channel (`/api`), injected from the connection handle.
1517
+ */
1518
+ constructor(api, rpc) {
1519
+ this.api = api;
1520
+ this.rpc = rpc;
1521
+ }
1522
+ /**
1523
+ * Refresh the page snapshot. Latest request wins. `settings.describe`
1524
+ * still runs — it supplies the top-level `writable` flag (host read-only
1525
+ * mode) and the namespace directory (the configured-provider join's other
1526
+ * input) — but the fallbacks config itself rides the gateway channel:
1527
+ * `rpc.call('/api', 'fallbacks/get', { args: {} })`. The two reads are
1528
+ * independent and run in PARALLEL (Promise.all — one round trip per
1529
+ * refresh, not two). The `fallbacks` namespace is NOT expected in describe
1530
+ * anymore (it is off the apiproxy boundary post-patch); a describe failure
1531
+ * remains a hard `error` (the form cannot render provider/model options
1532
+ * without the directory), while a get failure is NOT a page error —
1533
+ * `present` goes false and the section keeps the usable skeleton (KD-G5).
1534
+ * @returns nothing; {@link store} carries success or failure.
1535
+ */
1536
+ async load() {
1537
+ const generation = ++this.generation;
1538
+ this.store.update((state) => {
1539
+ state.status = "loading";
1540
+ state.error = null;
1541
+ });
1542
+ try {
1543
+ const [describeResult, getResult] = await Promise.all([this.api.settings.describe({}), this.rpc.call("/api", "fallbacks/get", { args: {} }).catch(() => void 0)]);
1544
+ if (generation !== this.generation) return;
1545
+ if (!describeResult.result.ok) throw describeResult.result.error;
1546
+ this.namespaces = new Map(describeResult.result.value.namespaces.map((entry) => [entry.ns, entry]));
1547
+ const writable = describeResult.result.value.writable;
1548
+ let config;
1549
+ let legacyKeys = [];
1550
+ if (getResult !== void 0 && getResult.ok && getResult.value !== null && typeof getResult.value === "object") {
1551
+ if ("config" in getResult.value) config = getResult.value.config;
1552
+ if ("legacyKeys" in getResult.value) {
1553
+ const wireLegacyKeys = getResult.value.legacyKeys;
1554
+ if (Array.isArray(wireLegacyKeys)) legacyKeys = wireLegacyKeys.filter((key) => typeof key === "string");
1555
+ }
1556
+ }
1557
+ this.accept(config, writable, legacyKeys);
1558
+ } catch (error) {
1559
+ if (generation !== this.generation) return;
1560
+ this.fail(error);
1561
+ }
1562
+ }
1563
+ /**
1564
+ * Refresh the provider/model catalog (`llm.providers` + `llm.models`), an
1565
+ * independent read path with its own generation guard so it can run
1566
+ * parallel to {@link load} without clobbering it (spec §2.5 D-4).
1567
+ * Per-provider lookup failures ride `catalogError` as a diagnostic without
1568
+ * failing the sound groups; a whole-load failure lands `catalogStatus:
1569
+ * 'error'` and never blocks the rest of the form.
1570
+ * @returns nothing; {@link store} carries success or failure.
1571
+ */
1572
+ async loadCatalog() {
1573
+ const generation = ++this.catalogGeneration;
1574
+ this.store.update((state) => {
1575
+ state.catalogStatus = "loading";
1576
+ state.catalogError = null;
1577
+ });
1578
+ try {
1579
+ const [providersResponse, modelsResponse] = await Promise.all([this.api.llm.providers({}), this.api.llm.models({})]);
1580
+ if (generation !== this.catalogGeneration) return;
1581
+ if (!providersResponse.result.ok) throw providersResponse.result.error;
1582
+ if (!modelsResponse.result.ok) throw modelsResponse.result.error;
1583
+ const providers = providersResponse.result.value.providers;
1584
+ const groups = modelsResponse.result.value.groups;
1585
+ const failures = modelsResponse.result.value.failures;
1586
+ this.store.update((state) => {
1587
+ state.catalogStatus = "ready";
1588
+ state.catalogError = failures.length > 0 ? failures.map((failure) => `${failure.name}: ${failure.message}`).join("; ") : null;
1589
+ state.providers = providers;
1590
+ state.configuredProviders = configuredProvidersOf(providers, this.namespaces);
1591
+ state.groups = groups;
1592
+ state.catalogEpoch += 1;
1593
+ });
1594
+ } catch (error) {
1595
+ if (generation !== this.catalogGeneration) return;
1596
+ const wire = error;
1597
+ this.store.update((state) => {
1598
+ state.catalogStatus = "error";
1599
+ state.catalogError = typeof wire?.message === "string" ? wire.message : messageOf(error);
1600
+ });
1601
+ }
1602
+ }
1603
+ /**
1604
+ * Record the current session the status block reads (spec §2.5 D-5). Once
1605
+ * the block has been read once, its summary follows session switches
1606
+ * immediately; an idle block only records the id — the section's mount
1607
+ * effect performs the first read.
1608
+ * @param sessionId - the session whose history is summarized; undefined
1609
+ * (no current session) resolves to the empty state.
1610
+ */
1611
+ setCurrentSession(sessionId) {
1612
+ if (sessionId === this.currentSession) return;
1613
+ this.currentSession = sessionId;
1614
+ if (this.store.getSnapshot().switchesStatus !== "idle") this.loadSwitches();
1615
+ }
1616
+ /**
1617
+ * Read the recent-switch summary for the current session (spec §2.5 D-5):
1618
+ * one `sessions.history` page (`maxMessages` = {@link SWITCHES_HISTORY_PAGE}),
1619
+ * `fallbacks/switch` events extracted newest-first capped at
1620
+ * {@link RECENT_SWITCH_LIMIT}. No current session → honest empty ready
1621
+ * state (no RPC); a read failure lands `switchesStatus: 'error'` and never
1622
+ * touches the settings state (the form keeps editing/saving normally).
1623
+ * @returns nothing; {@link store} carries success or failure.
1624
+ */
1625
+ async loadSwitches() {
1626
+ const generation = ++this.switchesGeneration;
1627
+ const sessionId = this.currentSession;
1628
+ if (sessionId === void 0) {
1629
+ this.store.update((state) => {
1630
+ state.switchesStatus = "ready";
1631
+ state.switchesError = null;
1632
+ state.switches = [];
1633
+ });
1634
+ return;
1635
+ }
1636
+ this.store.update((state) => {
1637
+ state.switchesStatus = "loading";
1638
+ state.switchesError = null;
1639
+ });
1640
+ try {
1641
+ const response = await this.api.sessions.history({
1642
+ sessionId,
1643
+ maxMessages: 50
1644
+ });
1645
+ if (generation !== this.switchesGeneration) return;
1646
+ if (!response.result.ok) throw response.result.error;
1647
+ const switches = extractRecentSwitches(response.result.value.events);
1648
+ this.store.update((state) => {
1649
+ state.switchesStatus = "ready";
1650
+ state.switchesError = null;
1651
+ state.switches = switches;
1652
+ });
1653
+ } catch (error) {
1654
+ if (generation !== this.switchesGeneration) return;
1655
+ const wire = error;
1656
+ this.store.update((state) => {
1657
+ state.switchesStatus = "error";
1658
+ state.switchesError = typeof wire?.message === "string" ? wire.message : messageOf(error);
1659
+ });
1660
+ }
1661
+ }
1662
+ /**
1663
+ * Persist the full edited configuration through the gateway channel
1664
+ * (`/api/fallbacks/set`). The full config is sent as a MERGE patch (guide
1665
+ * §9) — keys the new schema cannot express (legacy `chains` /
1666
+ * `roles.default` in the user layer) survive the write, which is why the
1667
+ * gateway returns POST-WRITE `legacyKeys` and the banner stays honest
1668
+ * (W-1/F-1). The merge has no revision guard: any failure (business
1669
+ * rejection or transport) surfaces its message in `state.error` for the
1670
+ * section's error banner and the form stays editable for retry (KD-G3).
1671
+ * @param next - the complete edited configuration.
1672
+ */
1673
+ async save(next) {
1674
+ const state = this.store.getSnapshot();
1675
+ if (!state.writable || state.status === "saving") return;
1676
+ const generation = ++this.generation;
1677
+ this.store.update((draft) => {
1678
+ draft.status = "saving";
1679
+ draft.error = null;
1680
+ });
1681
+ try {
1682
+ const result = await this.rpc.call("/api", "fallbacks/set", { args: { patch: next } });
1683
+ if (generation !== this.generation) return;
1684
+ if (!result.ok) throw result.error;
1685
+ const value = result.value;
1686
+ const config = value !== null && typeof value === "object" && "config" in value ? value.config : void 0;
1687
+ let legacyKeys = this.store.getSnapshot().legacyKeys;
1688
+ if (value !== null && typeof value === "object" && "legacyKeys" in value) {
1689
+ const wireLegacyKeys = value.legacyKeys;
1690
+ if (Array.isArray(wireLegacyKeys)) legacyKeys = wireLegacyKeys.filter((key) => typeof key === "string");
1691
+ }
1692
+ this.accept(config, true, legacyKeys);
1693
+ } catch (error) {
1694
+ if (generation !== this.generation) return;
1695
+ this.fail(error);
1696
+ }
1697
+ }
1698
+ /**
1699
+ * Reset to composition defaults through the gateway channel
1700
+ * (`/api/fallbacks/reset` — the fallbacks-specific third method; the host
1701
+ * clears the user layer via `settings.replace(ns, {})`, the removal path a
1702
+ * merge cannot express). Same error handling as {@link save} (KD-G3).
1703
+ */
1704
+ async resetToDefaults() {
1705
+ const state = this.store.getSnapshot();
1706
+ if (!state.writable || state.status === "saving") return;
1707
+ const generation = ++this.generation;
1708
+ this.store.update((draft) => {
1709
+ draft.status = "saving";
1710
+ draft.error = null;
1711
+ });
1712
+ try {
1713
+ const result = await this.rpc.call("/api", "fallbacks/reset", { args: {} });
1714
+ if (generation !== this.generation) return;
1715
+ if (!result.ok) throw result.error;
1716
+ const value = result.value;
1717
+ const config = value !== null && typeof value === "object" && "config" in value ? value.config : void 0;
1718
+ let legacyKeys = this.store.getSnapshot().legacyKeys;
1719
+ if (value !== null && typeof value === "object" && "legacyKeys" in value) {
1720
+ const wireLegacyKeys = value.legacyKeys;
1721
+ if (Array.isArray(wireLegacyKeys)) legacyKeys = wireLegacyKeys.filter((key) => typeof key === "string");
1722
+ }
1723
+ this.accept(config, true, legacyKeys);
1724
+ } catch (error) {
1725
+ if (generation !== this.generation) return;
1726
+ this.fail(error);
1727
+ }
1728
+ }
1729
+ /** Stop in-flight responses from publishing after plugin disposal. */
1730
+ dispose() {
1731
+ this.generation += 1;
1732
+ this.catalogGeneration += 1;
1733
+ this.switchesGeneration += 1;
1734
+ this.namespaces = /* @__PURE__ */ new Map();
1735
+ }
1736
+ /**
1737
+ * Publish a settled load: `status` ready, `writable` from describe, and —
1738
+ * only when the gateway returned a REAL config — `present` true and
1739
+ * `state.config` replaced with the parsed value. A get that did not
1740
+ * resolve (`config === undefined`) lands `present` false and keeps the
1741
+ * last accepted config (the defaults skeleton on a first load) — the
1742
+ * draft seed invariant (I-1): a transient channel-down must never seed
1743
+ * the form with defaults over real server truth. `legacyKeys` rides the
1744
+ * same publish: the wire field drives the migration banner. save/reset
1745
+ * pass the POST-WRITE value (W-1/F-1) — or the previous value when the
1746
+ * response omits the field, so a write can never clear the banner
1747
+ * against server truth; only a real `get` may.
1748
+ */
1749
+ accept(config, writable, legacyKeys) {
1750
+ const parsed = config === void 0 ? void 0 : parseFallbacksConfig(config);
1751
+ this.store.update((state) => {
1752
+ state.status = "ready";
1753
+ state.error = null;
1754
+ state.writable = writable;
1755
+ state.present = parsed !== void 0;
1756
+ state.legacyKeys = parsed === void 0 ? state.legacyKeys : legacyKeys;
1757
+ if (parsed !== void 0) state.config = parsed;
1758
+ state.configuredProviders = configuredProvidersOf(state.providers, this.namespaces);
1759
+ });
1760
+ }
1761
+ fail(error) {
1762
+ const wire = error;
1763
+ this.store.update((state) => {
1764
+ state.status = "error";
1765
+ state.error = typeof wire?.message === "string" ? wire.message : messageOf(error);
1766
+ });
1767
+ }
1768
+ };
1769
+ /**
1770
+ * Refetch after reconnect / settings change only when the section has already
1771
+ * opened once.
1772
+ * @param controller - the fallbacks settings controller.
1773
+ */
1774
+ function refreshFallbacksIfLoaded(controller) {
1775
+ if (controller.store.getSnapshot().status === "idle") return;
1776
+ controller.load();
1777
+ }
1778
+ /**
1779
+ * Refetch the catalog after `llm/adapters-updated` only when it has already
1780
+ * been opened once (the catalog twin of {@link refreshFallbacksIfLoaded}).
1781
+ * @param controller - the fallbacks settings controller.
1782
+ */
1783
+ function refreshCatalogIfLoaded(controller) {
1784
+ if (controller.store.getSnapshot().catalogStatus === "idle") return;
1785
+ controller.loadCatalog();
1786
+ }
1787
+ /**
1788
+ * Refetch the recent-switch summary after `settings/document-updated`
1789
+ * (fallbacks ns) / `connection/reset` only when the status block has already
1790
+ * been read once
1791
+ * (the switches twin of {@link refreshFallbacksIfLoaded}).
1792
+ * @param controller - the fallbacks settings controller.
1793
+ */
1794
+ function refreshSwitchesIfLoaded(controller) {
1795
+ if (controller.store.getSnapshot().switchesStatus === "idle") return;
1796
+ controller.loadSwitches();
1797
+ }
1798
+ //#endregion
1799
+ //#region src/client/locales.ts
1800
+ /**
1801
+ * Fallbacks settings section dictionaries (zh source of truth) plus the
1802
+ * `fallbacks` LocaleNamespaceMap merge — the registration's `locale:` seat
1803
+ * (`PropsLocale<'fallbacks'>` puts the typed `t` on the section props).
1804
+ *
1805
+ * Label conventions follow spec §4 用户直观性: enumerable config values
1806
+ * (triggerCodes / revertPolicy) render readable labels, never raw enum
1807
+ * strings.
1808
+ */
1809
+ /** Simplified Chinese dictionary (the key-set source of truth). */
1810
+ const zh = {
1811
+ "title": "Fallbacks",
1812
+ "intro": "模型故障自动降级",
1813
+ "collapse": "收起设置",
1814
+ "expand": "展开设置",
1815
+ "unsaved": "未保存",
1816
+ "discard": "放弃修改",
1817
+ "retry": "重试",
1818
+ "readOnly": "当前环境中的设置为只读。",
1819
+ "enabled.label": "启用故障降级",
1820
+ "enabled.hint": "关闭后插件完全不介入",
1821
+ "enabled.tooltip": "关闭后插件完全不介入;开启但未配置 rootChain 时行为与未安装插件一致。",
1822
+ "enabled.off": "功能未开启:打开 enabled 开关以显示配置界面。",
1823
+ "triggerCodes.label": "触发失败码",
1824
+ "triggerCodes.hint": "命中这些失败码时进入降级决策",
1825
+ "triggerCodes.tooltip": "命中这些失败码时进入降级链决策;可重试型故障(如 5xx)由 llm-retry 先行退避,预算耗尽后同样进入决策。",
1826
+ "triggerCodes.RATE_LIMIT": "限流(429)",
1827
+ "triggerCodes.QUOTA": "配额超限",
1828
+ "triggerCodes.AUTH": "权限/认证失败",
1829
+ "triggerCodes.extra": "此外还保留了 {codes} 等自定义失败码。",
1830
+ "revertPolicy.label": "冷却结束后",
1831
+ "revertPolicy.cooldown-expiry": "冷却到期后回主模型",
1832
+ "revertPolicy.never": "保持备用模型(会话内不回)",
1833
+ "revertPolicy.hint": "冷却到期后是否回主模型",
1834
+ "revertPolicy.tooltip": "被切换离的模型在冷却期内不再入选;到期后按此策略决定是否回主。",
1835
+ "cooldownMs.label": "冷却时长(毫秒)",
1836
+ "cooldownMs.hint": "冷却期内模型不再入选",
1837
+ "cooldownMs.tooltip": "被切离/失败的模型在冷却期内不再入选。",
1838
+ "maxSwitchesPerStep.label": "单步最大切换次数",
1839
+ "maxSwitchesPerStep.hint": "超过后停止切换",
1840
+ "maxSwitchesPerStep.tooltip": "超过后停止切换,以原始错误语义结束当前步,防止链循环放大延迟。",
1841
+ "alwaysModeRetryCap.label": "always 模式重试上限",
1842
+ "alwaysModeRetryCap.hint": "达到上限次数后切换;0 表示禁用",
1843
+ "alwaysModeRetryCap.tooltip": "retryPolicy 为 always 的模型在同一请求内重试达到该次数后切换;0 表示禁用。",
1844
+ "rootChain.label": "root 主代理降级链",
1845
+ "rootChain.hint": "未配置 = root 不降级",
1846
+ "rootChain.tooltip": "root 主代理失败时按此有序选择器列表依次降级;未配置时行为与未安装插件一致。",
1847
+ "rootChain.selector.add": "添加选择器",
1848
+ "chains.selector.remove": "删除该选择器",
1849
+ "chains.selector.providerPlaceholder": "选择 provider",
1850
+ "chains.selector.modelPlaceholder": "选择 model",
1851
+ "chains.selector.wildcard": "通配该 provider(provider/*)",
1852
+ "chains.selector.noModels": "该 provider 暂无可用模型(目录查询失败),请使用通配或改选。",
1853
+ "roles.list.label": "角色实体",
1854
+ "roles.list.hint": "先声明角色,规则才能引用",
1855
+ "roles.list.tooltip": "角色 id 须匹配 /^[a-z0-9-]{1,32}$/ 且唯一;\"inherit\" 为保留字,不能用作角色 id。",
1856
+ "roles.id": "id",
1857
+ "roles.id.hint": "小写字母/数字/连字符,1–32 字符",
1858
+ "roles.idPlaceholder": "例如 reviewer",
1859
+ "roles.label": "名称",
1860
+ "roles.description": "描述",
1861
+ "roles.fallback": "链拼接策略",
1862
+ "roles.fallback.inherit-root": "继承 root(角色链后追加 rootChain)",
1863
+ "roles.fallback.none": "仅角色链(不追加 rootChain)",
1864
+ "roles.add": "添加角色",
1865
+ "roles.remove": "删除该角色",
1866
+ "roles.selector.add": "添加选择器",
1867
+ "roles.rules": "角色规则",
1868
+ "roles.rules.hint": "顺序匹配 origin/provider/model,未命中 → inherit(root 链)",
1869
+ "roles.rules.tooltip": "规则命中后走对应角色的链;未命中走内置 inherit(rootChain)。",
1870
+ "roles.rule.origin": "来源",
1871
+ "roles.rule.origin.any": "任意",
1872
+ "roles.rule.origin.root": "root",
1873
+ "roles.rule.origin.subagent": "subagent",
1874
+ "roles.rule.provider": "provider",
1875
+ "roles.rule.provider.any": "任意",
1876
+ "roles.rule.model": "model",
1877
+ "roles.rule.model.any": "任意",
1878
+ "roles.rule.role": "角色",
1879
+ "roles.rule.role.inherit": "inherit(内置:root 链)",
1880
+ "roles.rule.roleSelectPlaceholder": "选择角色",
1881
+ "roles.rule.roleUndeclared.short": "(未声明)",
1882
+ "roles.addRule": "添加规则",
1883
+ "roles.removeRule": "删除该规则",
1884
+ "validation.blocked": "配置校验未通过,保存被拦截:",
1885
+ "validation.roleIdFormat": "角色 id \"{id}\" 不符合格式 /^[a-z0-9-]{1,32}$/",
1886
+ "validation.roleIdReserved": "\"inherit\" 为保留角色 id,不能用于角色实体",
1887
+ "validation.roleIdDuplicate": "角色 id \"{id}\" 重复",
1888
+ "validation.ruleRoleUndeclared": "规则引用了未声明的角色 \"{role}\"",
1889
+ "validation.ruleRoleRequired": "规则未选择角色:请选择目标角色,或删除该行",
1890
+ "validation.selector": "选择器 \"{entry}\" 非法:{message}",
1891
+ "legacy.banner": "检测到旧格式配置字段({keys}):已按新模型展示,请按 docs/configuration.md 迁移表手工改写;插件不会自动改写配置。",
1892
+ "catalog.empty": "暂无可用模型:请先在模型页添加模型,添加后此处将自动可选。",
1893
+ "catalog.error": "模型目录读取失败:{message}",
1894
+ "catalog.partial": "部分 provider 模型查询失败:{message}",
1895
+ "catalog.outside.hint": "目录外,可保留原值",
1896
+ "catalog.outside.tooltip": "不在当前模型目录,可保留原值并保存;新增条目仅可从目录选择。",
1897
+ "catalog.outside.short": " (目录外)",
1898
+ "catalog.unconfigured.short": " (未配置)",
1899
+ "status.title": "运行状态(只读)",
1900
+ "status.effectiveModel.label": "当前生效模型:",
1901
+ "status.effectiveModel.unavailable": "fallbacks 未启用(或 rootChain 未配置)",
1902
+ "status.effectiveModel.note": "配置 + 最近切换推导,非实时路由探测",
1903
+ "status.switches.label": "最近切换:",
1904
+ "status.switches.empty": "本会话暂无 fallback 切换。",
1905
+ "status.switches.error": "切换历史读取失败:{message}",
1906
+ "status.switches.compact": "最近 {count} 次 · {from} → {to}({role} · {reason})",
1907
+ "status.switches.reason.trigger-code": "触发失败码",
1908
+ "status.switches.reason.always-cap": "always 模式上限",
1909
+ "status.selectionNote": "说明:web 前端手动选择的模型可能在切换后重新套用(标记载体已随本地 patch 移除)。",
1910
+ "general.title": "模型故障降级",
1911
+ "general.enabled": "已启用",
1912
+ "general.disabled": "未启用",
1913
+ "general.unknown": "未知",
1914
+ "general.unavailable": "状态通道暂不可达",
1915
+ "general.switch": "最近切换:{from} → {to}({role} · {reason})",
1916
+ "general.switch.empty": "本会话暂无切换",
1917
+ "general.error": "状态读取失败:{message}",
1918
+ "chat.switch.title": "模型切换",
1919
+ "chat.switch.summary": "{from} → {to}({role} · {reason})",
1920
+ "defaults.prefix": "默认值",
1921
+ "save": "保存",
1922
+ "save.saving": "保存中…",
1923
+ "save.error": "保存失败:{message}",
1924
+ "close": "关闭",
1925
+ "reset": "恢复默认",
1926
+ "reset.confirmTitle": "恢复默认配置",
1927
+ "reset.confirm": "恢复后 fallbacks 配置将回到插件默认值,当前编辑内容会丢失。",
1928
+ "reset.confirm.cancel": "取消",
1929
+ "reset.confirm.action": "恢复默认",
1930
+ "reset.saving": "恢复中…",
1931
+ "loading": "加载中…",
1932
+ "unavailable": "fallbacks 配置通道暂不可达:以下显示默认配置(或上次读取值),可尝试保存;保存失败会在此处如实提示。",
1933
+ "error.generic": "出错:{message}"
1934
+ };
1935
+ /** English dictionary, checked complete against the zh key set. */
1936
+ const en = {
1937
+ "title": "Fallbacks",
1938
+ "intro": "Automatic fallback on model failures",
1939
+ "collapse": "Hide settings",
1940
+ "expand": "Show settings",
1941
+ "unsaved": "Unsaved",
1942
+ "discard": "Discard",
1943
+ "retry": "Retry",
1944
+ "readOnly": "Settings are read-only in this environment.",
1945
+ "enabled.label": "Enable failure fallback",
1946
+ "enabled.hint": "Plugin never intervenes when off",
1947
+ "enabled.tooltip": "When off the plugin never intervenes; when on with no rootChain configured behavior is identical to an uninstalled plugin.",
1948
+ "enabled.off": "Feature disabled: turn on the enabled switch to show the configuration interface.",
1949
+ "triggerCodes.label": "Trigger failure codes",
1950
+ "triggerCodes.hint": "Failures with these codes enter fallback decision",
1951
+ "triggerCodes.tooltip": "Failures with these codes enter chain decision; retryable failures (e.g. 5xx) back off via llm-retry first and reach the decision only when its budget is exhausted.",
1952
+ "triggerCodes.RATE_LIMIT": "Rate limit (429)",
1953
+ "triggerCodes.QUOTA": "Quota exceeded",
1954
+ "triggerCodes.AUTH": "Auth / permission failure",
1955
+ "triggerCodes.extra": "Custom codes are preserved: {codes}.",
1956
+ "revertPolicy.label": "After cooldown",
1957
+ "revertPolicy.cooldown-expiry": "Return to the primary model",
1958
+ "revertPolicy.never": "Keep the fallback model (until session end)",
1959
+ "revertPolicy.hint": "Whether to return to the primary model after cooldown",
1960
+ "revertPolicy.tooltip": "A model switched away from stays out of candidacy during its cooldown; this policy decides whether it returns afterwards.",
1961
+ "cooldownMs.label": "Cooldown (milliseconds)",
1962
+ "cooldownMs.hint": "Models stay out of candidacy during cooldown",
1963
+ "cooldownMs.tooltip": "Switched-away or failed models stay out of candidacy during the cooldown window.",
1964
+ "maxSwitchesPerStep.label": "Max switches per step",
1965
+ "maxSwitchesPerStep.hint": "Stops switching beyond the cap",
1966
+ "maxSwitchesPerStep.tooltip": "Beyond this the step stops switching and ends with the original error semantics, preventing chain loops from amplifying latency.",
1967
+ "alwaysModeRetryCap.label": "Always-mode retry cap",
1968
+ "alwaysModeRetryCap.hint": "Switches after the cap; 0 disables",
1969
+ "alwaysModeRetryCap.tooltip": "Models whose retryPolicy is always switch after this many retries within one request; 0 disables.",
1970
+ "rootChain.label": "Root agent fallback chain",
1971
+ "rootChain.hint": "Unset = root does not fall back",
1972
+ "rootChain.tooltip": "When the root agent fails it falls back down this ordered selector list; unset behaves like an uninstalled plugin.",
1973
+ "rootChain.selector.add": "Add selector",
1974
+ "chains.selector.remove": "Remove this selector",
1975
+ "chains.selector.providerPlaceholder": "Select provider",
1976
+ "chains.selector.modelPlaceholder": "Select model",
1977
+ "chains.selector.wildcard": "Wildcard this provider (provider/*)",
1978
+ "chains.selector.noModels": "No models available for this provider (catalog lookup failed); use the wildcard or pick another provider.",
1979
+ "roles.list.label": "Declared roles",
1980
+ "roles.list.hint": "Declare roles before rules can reference them",
1981
+ "roles.list.tooltip": "Role ids must match /^[a-z0-9-]{1,32}$/ and be unique; \"inherit\" is reserved and cannot be used as a role id.",
1982
+ "roles.id": "ID",
1983
+ "roles.id.hint": "lowercase letters, digits, hyphens; 1–32 chars",
1984
+ "roles.idPlaceholder": "e.g. reviewer",
1985
+ "roles.label": "Label",
1986
+ "roles.description": "Description",
1987
+ "roles.fallback": "Chain append",
1988
+ "roles.fallback.inherit-root": "Inherit root (append rootChain after the role chain)",
1989
+ "roles.fallback.none": "Role chain only (no rootChain)",
1990
+ "roles.add": "Add role",
1991
+ "roles.remove": "Remove this role",
1992
+ "roles.selector.add": "Add selector",
1993
+ "roles.rules": "Role rules",
1994
+ "roles.rules.hint": "Matches origin/provider/model in order; no match → inherit (root chain)",
1995
+ "roles.rules.tooltip": "A matched rule uses that role's chain; no match uses the built-in inherit (rootChain).",
1996
+ "roles.rule.origin": "Origin",
1997
+ "roles.rule.origin.any": "Any",
1998
+ "roles.rule.origin.root": "root",
1999
+ "roles.rule.origin.subagent": "subagent",
2000
+ "roles.rule.provider": "provider",
2001
+ "roles.rule.provider.any": "Any",
2002
+ "roles.rule.model": "model",
2003
+ "roles.rule.model.any": "Any",
2004
+ "roles.rule.role": "role",
2005
+ "roles.rule.role.inherit": "inherit (built-in: root chain)",
2006
+ "roles.rule.roleSelectPlaceholder": "Select role",
2007
+ "roles.rule.roleUndeclared.short": " (undeclared)",
2008
+ "roles.addRule": "Add rule",
2009
+ "roles.removeRule": "Remove this rule",
2010
+ "validation.blocked": "Configuration validation failed; save was blocked: ",
2011
+ "validation.roleIdFormat": "Role id \"{id}\" does not match /^[a-z0-9-]{1,32}$/",
2012
+ "validation.roleIdReserved": "\"inherit\" is a reserved role id and cannot be declared",
2013
+ "validation.roleIdDuplicate": "Duplicate role id \"{id}\"",
2014
+ "validation.ruleRoleUndeclared": "Rule references undeclared role \"{role}\"",
2015
+ "validation.ruleRoleRequired": "Rule has no role selected: pick a target role, or remove the row",
2016
+ "validation.selector": "Invalid selector \"{entry}\": {message}",
2017
+ "legacy.banner": "Legacy config fields detected ({keys}): now shown in the new model — rewrite them manually following the migration table in docs/configuration.md (the plugin will not rewrite them automatically).",
2018
+ "catalog.empty": "No models yet: add a model on the Models page first; options will appear here automatically.",
2019
+ "catalog.error": "Model catalog read failed: {message}",
2020
+ "catalog.partial": "Some provider model lookups failed: {message}",
2021
+ "catalog.outside.hint": "Outside catalog; the value can be kept",
2022
+ "catalog.outside.tooltip": "Not in the current model catalog; you can keep the original value and save it (new entries are restricted to the catalog).",
2023
+ "catalog.outside.short": " (outside catalog)",
2024
+ "catalog.unconfigured.short": " (not configured)",
2025
+ "status.title": "Runtime status (read-only)",
2026
+ "status.effectiveModel.label": "Current effective model: ",
2027
+ "status.effectiveModel.unavailable": "Fallbacks disabled (or rootChain not configured)",
2028
+ "status.effectiveModel.note": "Derived from configuration and recent switches; not real-time route probing",
2029
+ "status.switches.label": "Recent switches: ",
2030
+ "status.switches.empty": "No fallback switches in this session yet.",
2031
+ "status.switches.error": "Switch history read failed: {message}",
2032
+ "status.switches.compact": "last {count} · {from} → {to} ({role} · {reason})",
2033
+ "status.switches.reason.trigger-code": "trigger code",
2034
+ "status.switches.reason.always-cap": "always-mode cap",
2035
+ "status.selectionNote": "Note: a model manually selected in the web front end may be re-applied after a switch (the marker coordination shipped with the local patch has been removed).",
2036
+ "general.title": "Model failover",
2037
+ "general.enabled": "Enabled",
2038
+ "general.disabled": "Disabled",
2039
+ "general.unknown": "Unknown",
2040
+ "general.unavailable": "Status channel unavailable",
2041
+ "general.switch": "Last switch: {from} → {to} ({role} · {reason})",
2042
+ "general.switch.empty": "No switches this session",
2043
+ "general.error": "Status read failed: {message}",
2044
+ "chat.switch.title": "Model switch",
2045
+ "chat.switch.summary": "{from} → {to} ({role} · {reason})",
2046
+ "defaults.prefix": "Default",
2047
+ "save": "Save",
2048
+ "save.saving": "Saving…",
2049
+ "save.error": "Save failed: {message}",
2050
+ "close": "Close",
2051
+ "reset": "Reset to defaults",
2052
+ "reset.confirmTitle": "Reset to defaults",
2053
+ "reset.confirm": "Resetting restores the fallbacks configuration to plugin defaults; your current edits will be lost.",
2054
+ "reset.confirm.cancel": "Cancel",
2055
+ "reset.confirm.action": "Reset",
2056
+ "reset.saving": "Resetting…",
2057
+ "loading": "Loading…",
2058
+ "unavailable": "The fallbacks config channel is unreachable: showing the default configuration (or the last read value). You can try to save; failures will be reported here.",
2059
+ "error.generic": "Error: {message}"
2060
+ };
2061
+ /** The settings section's dictionary namespace. */
2062
+ const NS = "fallbacks";
2063
+ /**
2064
+ * Reason → locale key map for switch summaries (S-c; shared by the card's
2065
+ * status block and the General page status row). The session log is durable
2066
+ * and forward-compatible: a reason value outside the current union (a newer
2067
+ * plugin wrote it) renders raw instead of falling into a binary else branch.
2068
+ */
2069
+ const SWITCH_REASON_KEYS = {
2070
+ "trigger-code": "status.switches.reason.trigger-code",
2071
+ "always-cap": "status.switches.reason.always-cap"
2072
+ };
2073
+ /** Human-readable trigger-code labels (spec §4 用户直观性). */
2074
+ const TRIGGER_CODE_LABELS = {
2075
+ RATE_LIMIT: "triggerCodes.RATE_LIMIT",
2076
+ QUOTA: "triggerCodes.QUOTA",
2077
+ AUTH: "triggerCodes.AUTH"
2078
+ };
2079
+ /**
2080
+ * The known trigger codes the form toggles; unknown codes are preserved.
2081
+ * M-04: derived from the host defaults so the toggle set can never drift from
2082
+ * the decision set (`defaultFallbacksConfig.triggerCodes` is the single
2083
+ * source of truth; the labels mapping above stays keyed by code).
2084
+ */
2085
+ const KNOWN_TRIGGER_CODES = [...defaultFallbacksConfig.triggerCodes];
2086
+ /** Toggle one known code's membership in `codes` (used by the form; pure). */
2087
+ function withTriggerCode(codes, code, present) {
2088
+ const next = new Set(codes);
2089
+ if (present) next.add(code);
2090
+ else next.delete(code);
2091
+ return [...next];
2092
+ }
2093
+ //#endregion
2094
+ //#region \0dsh-css:/Users/bibi/workspace/ai/deepseek/dsh-llm-fallbacks/src/client/FallbacksCard.module.css.mjs
2095
+ const css$2 = "\n\n\n._8827595f_card {\n list-style: none;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 12px;\n background: var(--dsw-alias-bg-layer-3);\n transition: border-color .16s, background .16s;\n}\n\n._8827595f_card:hover {\n border-color: var(--dsw-alias-label-dimmed);\n}\n\n\n._06704203_cardOpen {\n background: var(--dsw-alias-bg-layer-2);\n border-color: var(--dsw-alias-label-dimmed);\n}\n\n\n._e488d460_header {\n width: 100%;\n appearance: none;\n border: 0;\n background: none;\n font: inherit;\n color: inherit;\n text-align: left;\n cursor: pointer;\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 14px 16px;\n border-radius: 12px;\n}\n\n._e488d460_header:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: -2px;\n}\n\n\n._f5dfe084_headText {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n._8d39bde6_name {\n font-size: 15px;\n font-weight: 600;\n line-height: 1.4;\n color: var(--dsw-alias-label-primary);\n}\n\n._346f3b69_description {\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n\n._631094a0_chevron {\n flex: none;\n color: var(--dsw-alias-label-tertiary);\n transition: transform .16s;\n}\n\n._44836ce8_chevronOpen {\n transform: rotate(180deg);\n}\n\n\n._dbaa7975_body {\n border-top: 1px solid var(--dsw-alias-border-l2);\n margin: 0 16px;\n padding-bottom: 8px;\n}\n\n._4e39cf17_readOnly {\n margin: 12px 0 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n\n\n._99bf7f3c_pending {\n flex: none;\n border-radius: 999px;\n padding: 1px 8px;\n font-size: 11px;\n line-height: 17px;\n font-weight: 500;\n white-space: nowrap;\n background: var(--dsw-alias-bg-module-platform);\n color: var(--dsw-alias-label-secondary);\n}\n\n._114985b2_footer {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 8px;\n padding: 12px 0 4px;\n border-top: 1px solid var(--dsw-alias-border-l2);\n}\n\n\n._9860a5b1_notice {\n margin: 12px 0 0;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-state-business-primary);\n}\n\n\n._5006d43e_legacyNotice {\n margin: 12px 0 0;\n padding: 10px 12px;\n border-radius: 8px;\n background: var(--dsw-alias-bg-module-platform);\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-state-business-primary);\n}\n\n._21918751_error {\n margin: 12px 0 0;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-state-error-primary);\n}\n\n\n._7cea4b2f_noticeRow {\n display: flex;\n align-items: center;\n gap: 8px;\n margin: 12px 0 0;\n}\n\n._7cea4b2f_noticeRow ._21918751_error {\n flex: 1;\n min-width: 0;\n margin: 0;\n}\n\n\n._4058c747_form {\n display: flex;\n flex-direction: column;\n gap: 12px;\n padding: 12px 0 0;\n}\n\n\n._9998065c_checkboxRow {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n._02c2f6df_checkLabel {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 4px;\n font-size: 14px;\n line-height: 22px;\n font-weight: 400;\n color: var(--dsw-alias-label-primary);\n cursor: pointer;\n}\n\n._f2d47237_checkLabelTitle {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-weight: 400;\n}\n\n._c1bed8ea_checkLabelDesc {\n font-size: 12px;\n line-height: 18px;\n font-weight: 400;\n color: var(--dsw-alias-label-tertiary);\n}\n\n\n._06389b18_checkbox {\n flex: none;\n width: 16px;\n height: 16px;\n margin: 0;\n accent-color: var(--dsw-alias-brand-primary);\n cursor: pointer;\n}\n\n._06389b18_checkbox:disabled {\n opacity: 0.4;\n cursor: default;\n}\n\n\n._94c91bfd_fieldset {\n margin: 0;\n padding: 0;\n border: none;\n display: flex;\n flex-direction: column;\n gap: 12px;\n}\n\n\n._67826267_field {\n display: flex;\n flex-direction: column;\n gap: 6px;\n margin: 0;\n padding: 0;\n border: none;\n min-width: 0;\n}\n\n._13e68c3f_fieldLabel {\n \n display: inline-flex;\n align-items: center;\n gap: 10px;\n padding: 0;\n font-size: 12px;\n line-height: 18px;\n font-weight: 500;\n color: var(--dsw-alias-label-secondary);\n}\n\n._4bc809b8_hint {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n flex-wrap: wrap;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n._579f813a_defaultNote {\n font-size: 12px;\n line-height: 18px;\n font-weight: 400;\n color: var(--dsw-alias-label-tertiary);\n}\n\n\n._a6244318_infoHint {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: none;\n width: 16px;\n height: 16px;\n border-radius: 50%;\n font-size: 14px;\n line-height: 1;\n color: var(--dsw-alias-label-tertiary);\n background: var(--dsw-alias-interactive-bg-hover);\n cursor: help;\n user-select: none;\n}\n\n._a6244318_infoHint:focus-visible {\n outline: none;\n box-shadow: 0 0 0 2px var(--dsw-alias-border-l3);\n}\n\n\n._e655c840_infoHintDisabled {\n opacity: 0.4;\n cursor: default;\n}\n\n\n._a2521bb0_optionRow {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 6px 8px;\n border-radius: 6px;\n font-size: 14px;\n line-height: 22px;\n color: var(--dsw-alias-label-primary);\n cursor: pointer;\n}\n\n._a2521bb0_optionRow:hover:has(input:not(:disabled)) {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n\n._06389b18_checkbox,\n._a2521bb0_optionRow input,\n._929f3ef3_wildcardCell input {\n flex: none;\n width: 16px;\n height: 16px;\n margin: 0;\n accent-color: var(--dsw-alias-brand-primary);\n cursor: pointer;\n}\n\n._06389b18_checkbox:focus-visible,\n._a2521bb0_optionRow input:focus-visible,\n._929f3ef3_wildcardCell input:focus-visible {\n outline: none;\n box-shadow: 0 0 0 2px var(--dsw-alias-border-l3);\n}\n\n._a2521bb0_optionRow input:disabled,\n._929f3ef3_wildcardCell input:disabled {\n cursor: default;\n}\n\n\n._f9d86f7b_input {\n box-sizing: border-box;\n width: 100%;\n height: 32px;\n padding: 0 10px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 8px;\n font: inherit;\n font-size: 14px;\n line-height: 22px;\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-primary);\n}\n\n._f9d86f7b_input:focus {\n outline: none;\n border-color: var(--dsw-alias-brand-primary);\n}\n\n._f9d86f7b_input::placeholder {\n color: var(--dsw-alias-label-dimmed);\n}\n\n._f9d86f7b_input:disabled {\n opacity: 0.6;\n cursor: default;\n}\n\n\n._7b60337a_inputInvalid,\n._7b60337a_inputInvalid:focus {\n border-color: var(--dsw-alias-state-error-primary);\n}\n\n\nselect._f9d86f7b_input {\n max-width: 240px;\n cursor: pointer;\n}\n\n\n._16eb7153_selectInput {\n appearance: none;\n padding-right: 32px;\n \n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='none'%3E%3Cpath d='M3 4.5L6 7.5L9 4.5' stroke='%2381858C' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E\");\n background-repeat: no-repeat;\n background-position: right 12px center;\n background-size: 12px 12px;\n}\n\n\n._3e95886d_numberFields {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));\n gap: 8px;\n}\n\n._0cfb5881_list {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 4px;\n}\n\n\n._f5c620d6_editorCard {\n display: flex;\n flex-direction: column;\n gap: 14px;\n padding: 14px 16px;\n border-radius: 12px;\n background: var(--dsw-alias-bg-module-platform);\n}\n\n._224b1387_cardFoot {\n display: flex;\n justify-content: flex-end;\n}\n\n._8e220e07_ruleGrid {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n}\n\n._24a12a2f_ruleCell {\n display: flex;\n flex-direction: column;\n gap: 4px;\n min-width: 120px;\n flex: 1;\n}\n\n._1014c097_ruleCellLabel {\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-secondary);\n}\n\n\n._15a12620_chainSelectors {\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n._240cbbf8_selectorRow {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n\n._929f3ef3_wildcardCell {\n flex-direction: row;\n align-items: center;\n gap: 8px;\n min-width: 200px;\n align-self: center;\n padding: 6px 8px;\n border-radius: 6px;\n cursor: pointer;\n}\n\n._929f3ef3_wildcardCell:hover:has(input:not(:disabled)) {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n\n._70effa64_iconButton {\n position: relative;\n box-sizing: border-box;\n appearance: none;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n padding: 0;\n border: 0;\n border-radius: 6px;\n background: none;\n color: var(--dsw-alias-label-tertiary);\n cursor: pointer;\n}\n\n._70effa64_iconButton:disabled {\n opacity: 0.4;\n cursor: default;\n}\n\n._70effa64_iconButton:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-primary);\n}\n\n._70effa64_iconButton:focus-visible {\n outline: none;\n box-shadow: 0 0 0 2px var(--dsw-alias-border-l3);\n}\n\n._70effa64_iconButton::after {\n content: attr(data-tip);\n position: absolute;\n bottom: calc(100% + 6px);\n left: 50%;\n transform: translateX(-50%);\n padding: 3px 8px;\n border-radius: 6px;\n background: var(--dsw-alias-label-primary);\n color: var(--dsw-alias-bg-layer-3);\n font-size: 11px;\n line-height: 17px;\n white-space: nowrap;\n opacity: 0;\n pointer-events: none;\n transition: opacity .12s;\n}\n\n._70effa64_iconButton:hover::after,\n._70effa64_iconButton:focus-visible::after {\n opacity: 1;\n}\n\n\n._a0968257_iconButtonDanger:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover-danger);\n color: var(--dsw-alias-state-error-primary);\n}\n\n\n._28202c90_addButton {\n align-self: flex-start;\n}\n\n\n._747fd56d_primaryButton,\n._7e855445_secondaryButton {\n box-sizing: border-box;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 4px;\n height: 36px;\n padding: 0 14px;\n border: none;\n border-radius: 18px;\n font: inherit;\n font-size: 14px;\n line-height: 22px;\n cursor: pointer;\n}\n\n._747fd56d_primaryButton {\n background: var(--dsw-alias-button-primary-fill);\n color: var(--dsw-alias-label-primary-foreground);\n}\n\n._747fd56d_primaryButton:hover:not(:disabled) {\n background: var(--dsw-alias-button-primary-hover);\n}\n\n._7e855445_secondaryButton {\n border: 1px solid var(--dsw-alias-border-l2);\n background: transparent;\n color: var(--dsw-alias-label-primary);\n}\n\n._7e855445_secondaryButton:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover-solid);\n}\n\n._747fd56d_primaryButton:disabled,\n._7e855445_secondaryButton:disabled {\n opacity: 0.4;\n cursor: default;\n}\n\n._747fd56d_primaryButton:focus-visible,\n._7e855445_secondaryButton:focus-visible {\n outline: none;\n box-shadow: 0 0 0 2px var(--dsw-alias-border-l3);\n}\n\n\n._fbd045e0_statusBlock {\n display: flex;\n flex-direction: column;\n gap: 12px;\n padding: 12px 14px;\n margin-top: 12px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 12px;\n}\n\n._d4f70367_statusTitle {\n font-size: 12px;\n line-height: 18px;\n font-weight: 500;\n color: var(--dsw-alias-label-secondary);\n}\n\n\n._8ab300ed_statusLine {\n margin: 0;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-secondary);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n._e8cedbf5_statusLineLabel {\n font-weight: 500;\n color: var(--dsw-alias-label-secondary);\n}\n\n\n._b48dfcba_offNotice {\n margin: 0;\n padding: 12px;\n border: 1px dashed var(--dsw-alias-border-l3);\n border-radius: 8px;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-tertiary);\n text-align: center;\n}\n\n\n._ec8bd970_resetDialog {\n width: min(480px, 100%);\n}\n\n._8677165c_confirmDanger:not(:disabled) {\n border-color: var(--dsw-alias-state-error-primary);\n color: var(--dsw-alias-state-error-primary);\n}\n\n._8677165c_confirmDanger:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover-danger);\n}\n\n@media (prefers-reduced-motion: reduce) {\n ._70effa64_iconButton::after {\n transition: none;\n }\n}\n";
2096
+ const tagId$2 = "dsh-llm-fallbacks/FallbacksCard.module.css";
2097
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]") === null) {
2098
+ const tag = document.createElement("style");
2099
+ tag.dataset.plugin = "dsh-llm-fallbacks";
2100
+ tag.dataset.pluginCss = tagId$2;
2101
+ tag.textContent = css$2;
2102
+ document.head.appendChild(tag);
2103
+ }
2104
+ var FallbacksCard_module_css_default = {
2105
+ "card": "_8827595f_card",
2106
+ "cardOpen": "_06704203_cardOpen",
2107
+ "header": "_e488d460_header",
2108
+ "headText": "_f5dfe084_headText",
2109
+ "name": "_8d39bde6_name",
2110
+ "description": "_346f3b69_description",
2111
+ "chevron": "_631094a0_chevron",
2112
+ "chevronOpen": "_44836ce8_chevronOpen",
2113
+ "body": "_dbaa7975_body",
2114
+ "readOnly": "_4e39cf17_readOnly",
2115
+ "pending": "_99bf7f3c_pending",
2116
+ "footer": "_114985b2_footer",
2117
+ "notice": "_9860a5b1_notice",
2118
+ "legacyNotice": "_5006d43e_legacyNotice",
2119
+ "error": "_21918751_error",
2120
+ "noticeRow": "_7cea4b2f_noticeRow",
2121
+ "form": "_4058c747_form",
2122
+ "checkboxRow": "_9998065c_checkboxRow",
2123
+ "checkLabel": "_02c2f6df_checkLabel",
2124
+ "checkLabelTitle": "_f2d47237_checkLabelTitle",
2125
+ "checkLabelDesc": "_c1bed8ea_checkLabelDesc",
2126
+ "checkbox": "_06389b18_checkbox",
2127
+ "fieldset": "_94c91bfd_fieldset",
2128
+ "field": "_67826267_field",
2129
+ "fieldLabel": "_13e68c3f_fieldLabel",
2130
+ "hint": "_4bc809b8_hint",
2131
+ "defaultNote": "_579f813a_defaultNote",
2132
+ "infoHint": "_a6244318_infoHint",
2133
+ "infoHintDisabled": "_e655c840_infoHintDisabled",
2134
+ "optionRow": "_a2521bb0_optionRow",
2135
+ "wildcardCell": "_929f3ef3_wildcardCell",
2136
+ "input": "_f9d86f7b_input",
2137
+ "inputInvalid": "_7b60337a_inputInvalid",
2138
+ "selectInput": "_16eb7153_selectInput",
2139
+ "numberFields": "_3e95886d_numberFields",
2140
+ "list": "_0cfb5881_list",
2141
+ "editorCard": "_f5c620d6_editorCard",
2142
+ "cardFoot": "_224b1387_cardFoot",
2143
+ "ruleGrid": "_8e220e07_ruleGrid",
2144
+ "ruleCell": "_24a12a2f_ruleCell",
2145
+ "ruleCellLabel": "_1014c097_ruleCellLabel",
2146
+ "chainSelectors": "_15a12620_chainSelectors",
2147
+ "selectorRow": "_240cbbf8_selectorRow",
2148
+ "iconButton": "_70effa64_iconButton",
2149
+ "iconButtonDanger": "_a0968257_iconButtonDanger",
2150
+ "addButton": "_28202c90_addButton",
2151
+ "primaryButton": "_747fd56d_primaryButton",
2152
+ "secondaryButton": "_7e855445_secondaryButton",
2153
+ "statusBlock": "_fbd045e0_statusBlock",
2154
+ "statusTitle": "_d4f70367_statusTitle",
2155
+ "statusLine": "_8ab300ed_statusLine",
2156
+ "statusLineLabel": "_e8cedbf5_statusLineLabel",
2157
+ "offNotice": "_b48dfcba_offNotice",
2158
+ "resetDialog": "_ec8bd970_resetDialog",
2159
+ "confirmDanger": "_8677165c_confirmDanger"
2160
+ };
2161
+ //#endregion
2162
+ //#region src/client/FallbacksCard.tsx
2163
+ /**
2164
+ * Fallbacks settings card — the `fallbacks` plugin card on the web settings
2165
+ * "插件配置" page (spec §4). Registered into the `settings.plugin.item` slot
2166
+ * (id `fallbacks`, order 30, alongside the upstream bash/agent-loop/web-search
2167
+ * cards and the advisor card); owner props are empty and all data flows
2168
+ * through {@link FallbacksSettingsController}.
2169
+ *
2170
+ * The card chrome replicates the upstream `PluginCard` contract (self-drawn:
2171
+ * the upstream client value face exports no reusable card): a collapsible
2172
+ * `<li>` whose header is a button stacking the plugin name over its
2173
+ * description, with a dirty "unsaved" pill and a rotating chevron
2174
+ * (`IconChevronDownOutline14` from ui-primitives — a CLIENT_EXTERNALS value
2175
+ * import), `aria-expanded`/`aria-label` like the upstream header; a divider
2176
+ * under the header; then the form content; then a footer with
2177
+ * Discard / Reset / Save carrying the upstream disabled semantics — save =
2178
+ * `!dirty || saving || !writable`, discard = `!dirty || saving` (KD-U1).
2179
+ * Disclosure is card-local state: which card a user has open is a reading
2180
+ * gesture, and staged edits outlive collapsing — the pill rides the header
2181
+ * (upstream rationale).
2182
+ *
2183
+ * The form body is the two-block editing surface (spec §8): the `enabled`
2184
+ * checkbox row, the 6 top-level scalar fields (trigger codes / revert
2185
+ * policy / three numeric fields), the `rootChain` block (block 1 — the
2186
+ * root agent's single chain, no key input), and the roles block (block 2 —
2187
+ * declared role entity cards from `roles.list` plus the rule rows from
2188
+ * `roles.rules`, whose role field is a dropdown bound to the declared ids
2189
+ * + the built-in `inherit`, same-page live). Saving runs `validateDraft`
2190
+ * first — id format/reserved word/duplicates, undeclared rule role
2191
+ * references, and illegal selectors block the write with a validation
2192
+ * banner + inline red borders (never touching the store error path); a
2193
+ * non-empty `state.legacyKeys` renders the migration banner at the top of
2194
+ * the card body. The row editors keep their filled editorCard surface
2195
+ * inside the card, with `--dsw-alias-*` tokens throughout. The reset-
2196
+ * to-defaults confirmation stays a `Modal` (the delete-confirm pattern of
2197
+ * the Models page) — no `window.confirm`.
2198
+ *
2199
+ * The page-only chrome is gone (720px column wrapper, title/intro banners,
2200
+ * page-bottom status block): the AC-7 read-only status (derived effective
2201
+ * model + recent-switch summary) is folded into the card body above the
2202
+ * footer, and the plugin-config section owns the column width.
2203
+ *
2204
+ * Degraded/error/loading states keep the same card chrome (KD-U3): the
2205
+ * header always renders title+description+chevron, and the body carries the
2206
+ * config-channel notice or the load error. A card that cannot reach the
2207
+ * `fallbacks/get` gateway channel (`ready && !present`) keeps the USABLE
2208
+ * skeleton — the form stays writable and saves are attempted (KD-G5) — with
2209
+ * the `unavailable` notice ALWAYS visible (derived open — the header cannot
2210
+ * collapse it away), while a healthy card is collapsed until the user
2211
+ * expands it (AC-1, the documented divergence from upstream whose
2212
+ * unavailable card renders nothing). A hard load failure (`status ===
2213
+ * 'error'`) also forces the body open with an error notice and — when the
2214
+ * form is inert (`!writable`, i.e. the load never landed) — a Retry button;
2215
+ * a save failure keeps the editable form so the Save action itself is the
2216
+ * retry (the single `state.error` surface covers both, unlike the advisor's
2217
+ * separate apply-failure hints).
2218
+ *
2219
+ * The degraded derivation is latched in the card (the store stays untouched):
2220
+ * `present` only ever changes inside the store's `accept()`, so the settled
2221
+ * `ready` read is authoritative, and a card-local latch carries that value
2222
+ * through refresh/save windows (`loading`/`saving`) so the notice body can
2223
+ * never collapse mid-refresh (the advisor's latched `degraded` field,
2224
+ * implemented without a store change); on a first mount the latch is false,
2225
+ * so the healthy card starts (and stays) collapsed through its first load.
2226
+ */
2227
+ /** Split scalars from the row editors (rootChain / role entities / role rules). */
2228
+ function scalarsOf(config) {
2229
+ return {
2230
+ enabled: config.enabled,
2231
+ triggerCodes: [...config.triggerCodes],
2232
+ cooldownMs: config.cooldownMs,
2233
+ revertPolicy: config.revertPolicy,
2234
+ maxSwitchesPerStep: config.maxSwitchesPerStep,
2235
+ alwaysModeRetryCap: config.alwaysModeRetryCap
2236
+ };
2237
+ }
2238
+ /**
2239
+ * Assemble the full config the row editors + scalars describe. The rebuilt
2240
+ * `roles.list` comes from the rows, with the schema-reserved
2241
+ * `prompt`/`permissions` merged back from the last accepted config by role
2242
+ * id (see {@link mergeRoleExtras}) so a save never silently drops them
2243
+ * (T2 reviewer minor #2).
2244
+ */
2245
+ function assembleConfig(scalars, rootChainRows, roleRows, ruleRows, originalRoles) {
2246
+ const list = mergeRoleExtras(roleRows, originalRoles);
2247
+ return {
2248
+ enabled: scalars.enabled,
2249
+ triggerCodes: [...scalars.triggerCodes],
2250
+ rootChain: rowsToRootChain(rootChainRows),
2251
+ roles: {
2252
+ list,
2253
+ rules: rowsToRules(ruleRows)
2254
+ },
2255
+ cooldownMs: scalars.cooldownMs,
2256
+ revertPolicy: scalars.revertPolicy,
2257
+ maxSwitchesPerStep: scalars.maxSwitchesPerStep,
2258
+ alwaysModeRetryCap: scalars.alwaysModeRetryCap
2259
+ };
2260
+ }
2261
+ /**
2262
+ * Pre-save validation of the assembled draft (spec §8 / plan Task 3):
2263
+ * role id format/reserved word/duplicates, undeclared rule role references
2264
+ * (only reachable through the synthetic outside option — the dropdown
2265
+ * itself constrains normal edits), and illegal selector entries in
2266
+ * rootChain and role chains. Returns one localized message per violation;
2267
+ * a non-empty result blocks {@link save} — the draft is never written.
2268
+ * `label`/`description` are free text and never validated.
2269
+ */
2270
+ function validateDraft(draft, t) {
2271
+ const errors = [];
2272
+ const declaredIds = /* @__PURE__ */ new Set();
2273
+ for (const role of draft.roles.list) {
2274
+ if (!ROLE_ID_PATTERN.test(role.id)) errors.push(t("validation.roleIdFormat", { id: role.id }));
2275
+ if (role.id === "inherit") errors.push(t("validation.roleIdReserved"));
2276
+ if (declaredIds.has(role.id)) errors.push(t("validation.roleIdDuplicate", { id: role.id }));
2277
+ declaredIds.add(role.id);
2278
+ for (const entry of role.chain ?? []) try {
2279
+ parseSelector(entry);
2280
+ } catch (error) {
2281
+ errors.push(t("validation.selector", {
2282
+ entry,
2283
+ message: error.message
2284
+ }));
2285
+ }
2286
+ }
2287
+ for (const entry of draft.rootChain) try {
2288
+ parseSelector(entry);
2289
+ } catch (error) {
2290
+ errors.push(t("validation.selector", {
2291
+ entry,
2292
+ message: error.message
2293
+ }));
2294
+ }
2295
+ const validTargets = /* @__PURE__ */ new Set([...declaredIds, INHERIT_ROLE_ID]);
2296
+ for (const rule of draft.roles.rules) if (!validTargets.has(rule.role)) errors.push(t("validation.ruleRoleUndeclared", { role: rule.role }));
2297
+ return errors;
2298
+ }
2299
+ /**
2300
+ * The trimmed role ids that are validation failures (format / reserved word
2301
+ * / duplicate) — drives the inline red border after a blocked save attempt.
2302
+ * Derived once per render into a Set (qc3 F-3): a duplicate scan inside the
2303
+ * render loop would be O(N²) per row; here the whole derivation is O(N) and
2304
+ * each row's check is a single Set lookup. Selector errors stay on the
2305
+ * banner only (plan Task 3 inline-scope rule).
2306
+ */
2307
+ function collectInvalidRoleIds(rows) {
2308
+ const counts = /* @__PURE__ */ new Map();
2309
+ for (const row of rows) {
2310
+ const id = row.id.trim();
2311
+ counts.set(id, (counts.get(id) ?? 0) + 1);
2312
+ }
2313
+ const invalid = /* @__PURE__ */ new Set();
2314
+ for (const row of rows) {
2315
+ const id = row.id.trim();
2316
+ if (!ROLE_ID_PATTERN.test(id) || id === "inherit" || (counts.get(id) ?? 0) > 1) invalid.add(id);
2317
+ }
2318
+ return invalid;
2319
+ }
2320
+ /** Parse a number input, clamped to a non-negative integer. */
2321
+ function parseCount(raw) {
2322
+ const parsed = Number.parseInt(raw, 10);
2323
+ return Number.isNaN(parsed) ? 0 : Math.max(0, parsed);
2324
+ }
2325
+ /** The catalog faces the dropdowns classify against; undefined while unready. */
2326
+ function catalogOf(state) {
2327
+ return state.catalogStatus === "ready" ? {
2328
+ providers: state.providers,
2329
+ groups: state.groups
2330
+ } : void 0;
2331
+ }
2332
+ /**
2333
+ * Inline "!" info badge (T3): the detailed explanation rides a primitives
2334
+ * Tooltip bubble (side "right", ~300ms hover delay, immediate on keyboard
2335
+ * focus) while the short inline hint stays on the row. The badge is an
2336
+ * exposed, focusable image — the Models page credential-status pattern
2337
+ * (role="img" + aria-label) — so the accessible name is always available;
2338
+ * the tooltip is a progressive enhancement on top.
2339
+ *
2340
+ * `disabled` mirrors the read-only/loading suppression of the surrounding
2341
+ * controls: the bubble is suppressed, the badge drops out of the tab order
2342
+ * (and its `:disabled` style dims it).
2343
+ *
2344
+ * Placement contract (QC W-2 fix): the badge is always a **sibling** of the
2345
+ * label-text element — never nested inside a `<label>` or an
2346
+ * `aria-labelledby`-referenced node — so its aria-label can never leak into
2347
+ * a control/group accessible name. A click on the badge therefore has no
2348
+ * label-activation default action to cancel.
2349
+ */
2350
+ function InfoHint({ label, disabled = false }) {
2351
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
2352
+ label,
2353
+ side: "right",
2354
+ delayMs: 300,
2355
+ disabled,
2356
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2357
+ className: disabled ? `${FallbacksCard_module_css_default.infoHint} ${FallbacksCard_module_css_default.infoHintDisabled}` : FallbacksCard_module_css_default.infoHint,
2358
+ role: "img",
2359
+ "aria-label": label,
2360
+ tabIndex: disabled ? -1 : 0,
2361
+ children: "!"
2362
+ })
2363
+ });
2364
+ }
2365
+ /**
2366
+ * One chain entry selector row: provider select + model select (cascade) +
2367
+ * wildcard checkbox (spec §2.5 D-3). The provider options are the catalog
2368
+ * providers **configured on the Models page** (`configuredProviders`, the
2369
+ * Models-page `configured` join) — unconfigured directory providers never
2370
+ * become offerable. Out-of-catalog values read back from the server render as
2371
+ * a synthetic option with the short "outside catalog" annotation and stay
2372
+ * selected — keeping them saves verbatim; picking a catalog option is an
2373
+ * intentional change. A directory provider that is not configured is offered
2374
+ * the same read-back treatment (short "not configured" annotation) so an
2375
+ * existing value is never hidden or dropped. New rows only offer configured
2376
+ * options.
2377
+ */
2378
+ function ChainSelectorEditor({ selector, catalog, configuredProviders, disabled, t, onChange, onRemove }) {
2379
+ const providerRaw = selectionToRaw(selector.provider);
2380
+ const providerOutside = selector.provider?.kind === "outside";
2381
+ const providerUnconfigured = !providerOutside && providerRaw !== "" && (catalog?.providers.some((entry) => entry.provider === providerRaw) ?? false) && !configuredProviders.some((entry) => entry.provider === providerRaw);
2382
+ const modelRaw = selectionToRaw(selector.model);
2383
+ const modelOutside = selector.model?.kind === "outside";
2384
+ const group = catalog?.groups.find((entry) => entry.id === providerRaw);
2385
+ const groupMissing = providerRaw !== "" && !providerOutside && !selector.wildcard && group === void 0;
2386
+ const modelDisabled = disabled || selector.wildcard || providerRaw === "" || groupMissing || providerOutside && modelRaw === "";
2387
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2388
+ className: FallbacksCard_module_css_default.selectorRow,
2389
+ children: [
2390
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2391
+ className: FallbacksCard_module_css_default.ruleGrid,
2392
+ children: [
2393
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2394
+ className: FallbacksCard_module_css_default.ruleCell,
2395
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2396
+ className: FallbacksCard_module_css_default.ruleCellLabel,
2397
+ children: t("roles.rule.provider")
2398
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
2399
+ className: `${FallbacksCard_module_css_default.input} ${FallbacksCard_module_css_default.selectInput}`,
2400
+ value: providerRaw,
2401
+ disabled,
2402
+ onChange: (event) => {
2403
+ if (event.target.value === providerRaw) return;
2404
+ onChange({
2405
+ provider: classifyProvider(event.target.value, catalog),
2406
+ model: null
2407
+ });
2408
+ },
2409
+ children: [
2410
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2411
+ value: "",
2412
+ children: t("chains.selector.providerPlaceholder")
2413
+ }),
2414
+ configuredProviders.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2415
+ value: entry.provider,
2416
+ children: entry.displayName
2417
+ }, entry.provider)),
2418
+ providerUnconfigured && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2419
+ value: providerRaw,
2420
+ children: `${providerRaw}${t("catalog.unconfigured.short")}`
2421
+ }),
2422
+ providerOutside && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2423
+ value: providerRaw,
2424
+ children: `${providerRaw}${t("catalog.outside.short")}`
2425
+ })
2426
+ ]
2427
+ })]
2428
+ }),
2429
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2430
+ className: FallbacksCard_module_css_default.ruleCell,
2431
+ children: [
2432
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2433
+ className: FallbacksCard_module_css_default.ruleCellLabel,
2434
+ children: t("roles.rule.model")
2435
+ }),
2436
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
2437
+ className: `${FallbacksCard_module_css_default.input} ${FallbacksCard_module_css_default.selectInput}`,
2438
+ value: selector.wildcard ? "" : modelRaw,
2439
+ disabled: modelDisabled,
2440
+ onChange: (event) => {
2441
+ onChange({ model: classifyModel(providerRaw, event.target.value, catalog) });
2442
+ },
2443
+ children: [
2444
+ modelRaw === "" && !providerOutside && !selector.wildcard && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2445
+ value: "",
2446
+ children: t("chains.selector.modelPlaceholder")
2447
+ }),
2448
+ (group?.models ?? []).map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2449
+ value: model.id,
2450
+ children: model.name
2451
+ }, model.id)),
2452
+ modelOutside && !selector.wildcard && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2453
+ value: modelRaw,
2454
+ children: `${modelRaw}${t("catalog.outside.short")}`
2455
+ })
2456
+ ]
2457
+ }),
2458
+ groupMissing && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2459
+ className: FallbacksCard_module_css_default.hint,
2460
+ children: t("chains.selector.noModels")
2461
+ })
2462
+ ]
2463
+ }),
2464
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2465
+ className: `${FallbacksCard_module_css_default.ruleCell} ${FallbacksCard_module_css_default.wildcardCell}`,
2466
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2467
+ type: "checkbox",
2468
+ checked: selector.wildcard,
2469
+ disabled: disabled || providerRaw === "",
2470
+ onChange: (event) => {
2471
+ onChange({
2472
+ wildcard: event.target.checked,
2473
+ ...event.target.checked ? { model: null } : {}
2474
+ });
2475
+ }
2476
+ }), t("chains.selector.wildcard")]
2477
+ })
2478
+ ]
2479
+ }),
2480
+ (providerOutside || modelOutside) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2481
+ className: FallbacksCard_module_css_default.hint,
2482
+ children: [t("catalog.outside.hint"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
2483
+ label: t("catalog.outside.tooltip"),
2484
+ disabled
2485
+ })]
2486
+ }),
2487
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2488
+ className: FallbacksCard_module_css_default.cardFoot,
2489
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2490
+ type: "button",
2491
+ className: `${FallbacksCard_module_css_default.iconButton} ${FallbacksCard_module_css_default.iconButtonDanger}`,
2492
+ "data-tip": t("chains.selector.remove"),
2493
+ "aria-label": t("chains.selector.remove"),
2494
+ onClick: onRemove,
2495
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, {})
2496
+ })
2497
+ })
2498
+ ]
2499
+ });
2500
+ }
2501
+ /**
2502
+ * Render the Fallbacks settings card inside the plugin-config section,
2503
+ * replicating the upstream PluginCard chrome (KD-U1). The body carries the
2504
+ * existing form content unchanged plus the folded-in status block and the
2505
+ * footer actions (Discard / Reset / Save).
2506
+ * @param props - slot-delivered injected dependencies and the synthesized t seat.
2507
+ * @returns the card.
2508
+ */
2509
+ function FallbacksCard({ controller, useSnapshot, t }) {
2510
+ const state = useSnapshot((snapshot) => snapshot);
2511
+ (0, react.useEffect)(() => {
2512
+ const snapshot = controller.store.getSnapshot();
2513
+ if (snapshot.status === "idle") controller.load();
2514
+ if (snapshot.catalogStatus === "idle") controller.loadCatalog();
2515
+ if (snapshot.switchesStatus === "idle") controller.loadSwitches();
2516
+ }, [controller]);
2517
+ const [scalars, setScalars] = (0, react.useState)(() => scalarsOf(defaultFallbacksConfig));
2518
+ const [rootChainRows, setRootChainRows] = (0, react.useState)(() => rootChainToRows(defaultFallbacksConfig.rootChain));
2519
+ const [roleRows, setRoleRows] = (0, react.useState)(() => rolesToRows(defaultFallbacksConfig.roles.list));
2520
+ const [ruleRows, setRuleRows] = (0, react.useState)(() => rulesToRows(defaultFallbacksConfig.roles.rules));
2521
+ const [validationErrors, setValidationErrors] = (0, react.useState)([]);
2522
+ const [validationAttempted, setValidationAttempted] = (0, react.useState)(false);
2523
+ const seededConfigKey = (0, react.useRef)(null);
2524
+ (0, react.useEffect)(() => {
2525
+ if (state.status !== "ready") return;
2526
+ const key = JSON.stringify(state.config);
2527
+ if (seededConfigKey.current === key) return;
2528
+ seededConfigKey.current = key;
2529
+ setScalars(scalarsOf(state.config));
2530
+ setRootChainRows(rootChainToRows(state.config.rootChain, catalogOf(state)));
2531
+ setRoleRows(rolesToRows(state.config.roles.list, catalogOf(state)));
2532
+ setRuleRows(rulesToRows(state.config.roles.rules, catalogOf(state)));
2533
+ }, [state.status, state.config]);
2534
+ const [confirmingReset, setConfirmingReset] = (0, react.useState)(false);
2535
+ const [resetting, setResetting] = (0, react.useState)(false);
2536
+ const updateScalars = (mutator) => {
2537
+ setScalars((prev) => {
2538
+ const next = {
2539
+ ...prev,
2540
+ triggerCodes: [...prev.triggerCodes]
2541
+ };
2542
+ mutator(next);
2543
+ return next;
2544
+ });
2545
+ };
2546
+ const updateRootChainSelector = (selectorIndex, patch) => {
2547
+ setRootChainRows((rows) => rows.map((row, index) => index === 0 ? {
2548
+ ...row,
2549
+ selectors: row.selectors.map((selector, sIndex) => sIndex === selectorIndex ? {
2550
+ ...selector,
2551
+ ...patch
2552
+ } : selector)
2553
+ } : row));
2554
+ };
2555
+ const addRootChainSelector = () => {
2556
+ setRootChainRows((rows) => rows.map((row, index) => index === 0 ? {
2557
+ ...row,
2558
+ selectors: [...row.selectors, {
2559
+ wildcard: false,
2560
+ provider: null,
2561
+ model: null
2562
+ }]
2563
+ } : row));
2564
+ };
2565
+ const removeRootChainSelector = (selectorIndex) => {
2566
+ setRootChainRows((rows) => rows.map((row, index) => index === 0 ? {
2567
+ ...row,
2568
+ selectors: row.selectors.filter((_, sIndex) => sIndex !== selectorIndex)
2569
+ } : row));
2570
+ };
2571
+ const updateRoleRow = (index, patch) => {
2572
+ setRoleRows((rows) => {
2573
+ const next = rows.map((row) => ({ ...row }));
2574
+ next[index] = {
2575
+ ...next[index],
2576
+ ...patch
2577
+ };
2578
+ return next;
2579
+ });
2580
+ };
2581
+ const updateRoleSelector = (roleIndex, selectorIndex, patch) => {
2582
+ setRoleRows((rows) => {
2583
+ const next = rows.map((row) => ({
2584
+ ...row,
2585
+ selectors: row.selectors.map((selector) => ({ ...selector }))
2586
+ }));
2587
+ const selectors = next[roleIndex].selectors;
2588
+ selectors[selectorIndex] = {
2589
+ ...selectors[selectorIndex],
2590
+ ...patch
2591
+ };
2592
+ return next;
2593
+ });
2594
+ };
2595
+ const addRoleSelector = (roleIndex) => {
2596
+ setRoleRows((rows) => rows.map((row, index) => index === roleIndex ? {
2597
+ ...row,
2598
+ selectors: [...row.selectors, {
2599
+ wildcard: false,
2600
+ provider: null,
2601
+ model: null
2602
+ }]
2603
+ } : row));
2604
+ };
2605
+ const removeRoleSelector = (roleIndex, selectorIndex) => {
2606
+ setRoleRows((rows) => rows.map((row, index) => index === roleIndex ? {
2607
+ ...row,
2608
+ selectors: row.selectors.filter((_, sIndex) => sIndex !== selectorIndex)
2609
+ } : row));
2610
+ };
2611
+ const addRole = () => {
2612
+ setRoleRows((rows) => [...rows, {
2613
+ id: "",
2614
+ label: "",
2615
+ description: "",
2616
+ selectors: [],
2617
+ fallback: "inherit-root"
2618
+ }]);
2619
+ };
2620
+ const removeRole = (index) => {
2621
+ setRoleRows((rows) => rows.filter((_, rowIndex) => rowIndex !== index));
2622
+ };
2623
+ const updateRuleRow = (index, patch) => {
2624
+ setRuleRows((rows) => {
2625
+ const next = rows.map((row) => ({ ...row }));
2626
+ next[index] = {
2627
+ ...next[index],
2628
+ ...patch
2629
+ };
2630
+ return next;
2631
+ });
2632
+ };
2633
+ const draft = assembleConfig(scalars, rootChainRows, roleRows, ruleRows, state.config.roles.list);
2634
+ const hasEmptyRuleRows = ruleRows.some((row) => row.role === "");
2635
+ const dirty = JSON.stringify(draft) !== JSON.stringify(state.config) || hasEmptyRuleRows;
2636
+ const saving = state.status === "saving";
2637
+ const writable = state.writable;
2638
+ const unknownCodes = scalars.triggerCodes.filter((code) => !KNOWN_TRIGGER_CODES.includes(code));
2639
+ const roleOptions = ruleRoleOptions({ list: roleRows });
2640
+ const invalidRoleIds = validationAttempted ? collectInvalidRoleIds(roleRows) : null;
2641
+ const effectiveModel = deriveEffectiveModel(state.config, state.switches);
2642
+ const effectiveModelLine = effectiveModel.kind === "unavailable" ? t("status.effectiveModel.unavailable") : `${effectiveModel.provider}/${effectiveModel.model} · ${t("status.effectiveModel.note")}`;
2643
+ const latestSwitch = state.switches[0];
2644
+ let switchesLine;
2645
+ if (state.switchesStatus === "error") switchesLine = t("status.switches.error", { message: state.switchesError });
2646
+ else if (state.switchesStatus === "loading") switchesLine = t("loading");
2647
+ else if (latestSwitch === void 0) switchesLine = t("status.switches.empty");
2648
+ else {
2649
+ const reasonKey = SWITCH_REASON_KEYS[latestSwitch.reason];
2650
+ switchesLine = t("status.switches.compact", {
2651
+ count: String(state.switches.length),
2652
+ from: `${latestSwitch.from.provider}/${latestSwitch.from.model}`,
2653
+ to: `${latestSwitch.to.provider}/${latestSwitch.to.model}`,
2654
+ role: latestSwitch.role,
2655
+ reason: reasonKey === void 0 ? latestSwitch.reason : t(reasonKey)
2656
+ });
2657
+ }
2658
+ const catalogSeededEpoch = (0, react.useRef)(null);
2659
+ (0, react.useEffect)(() => {
2660
+ if (state.catalogStatus !== "ready") return;
2661
+ if (catalogSeededEpoch.current === state.catalogEpoch) return;
2662
+ if (dirty) return;
2663
+ catalogSeededEpoch.current = state.catalogEpoch;
2664
+ setRootChainRows(rootChainToRows(state.config.rootChain, catalogOf(state)));
2665
+ setRoleRows(rolesToRows(state.config.roles.list, catalogOf(state)));
2666
+ setRuleRows(rulesToRows(state.config.roles.rules, catalogOf(state)));
2667
+ }, [
2668
+ state.catalogStatus,
2669
+ state.catalogEpoch,
2670
+ state.config,
2671
+ dirty
2672
+ ]);
2673
+ const save = () => {
2674
+ const errors = validateDraft(draft, t);
2675
+ if (hasEmptyRuleRows) errors.push(t("validation.ruleRoleRequired"));
2676
+ if (errors.length > 0) {
2677
+ setValidationErrors(errors);
2678
+ setValidationAttempted(true);
2679
+ return;
2680
+ }
2681
+ setValidationErrors([]);
2682
+ setValidationAttempted(false);
2683
+ controller.save(draft);
2684
+ };
2685
+ const discard = () => {
2686
+ setScalars(scalarsOf(state.config));
2687
+ setRootChainRows(rootChainToRows(state.config.rootChain, catalogOf(state)));
2688
+ setRoleRows(rolesToRows(state.config.roles.list, catalogOf(state)));
2689
+ setRuleRows(rulesToRows(state.config.roles.rules, catalogOf(state)));
2690
+ setValidationErrors([]);
2691
+ setValidationAttempted(false);
2692
+ };
2693
+ (0, react.useEffect)(() => {
2694
+ if (!validationAttempted) return;
2695
+ if (validateDraft(draft, t).length === 0 && !ruleRows.some((row) => row.role === "")) {
2696
+ setValidationErrors([]);
2697
+ setValidationAttempted(false);
2698
+ }
2699
+ }, [
2700
+ validationAttempted,
2701
+ draft,
2702
+ ruleRows,
2703
+ t
2704
+ ]);
2705
+ const confirmReset = () => {
2706
+ setResetting(true);
2707
+ controller.resetToDefaults().finally(() => {
2708
+ setResetting(false);
2709
+ setConfirmingReset(false);
2710
+ });
2711
+ };
2712
+ const [userOpen, setUserOpen] = (0, react.useState)(false);
2713
+ const degradedLatch = (0, react.useRef)(false);
2714
+ const errorLatch = (0, react.useRef)(false);
2715
+ if (state.status === "ready") {
2716
+ degradedLatch.current = !state.present;
2717
+ errorLatch.current = false;
2718
+ } else if (state.status === "error") errorLatch.current = true;
2719
+ const degraded = state.status === "ready" ? !state.present : degradedLatch.current;
2720
+ const open = userOpen || errorLatch.current || degraded;
2721
+ const title = t("title");
2722
+ const header = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2723
+ type: "button",
2724
+ className: FallbacksCard_module_css_default.header,
2725
+ "aria-expanded": open,
2726
+ "aria-label": `${t(open ? "collapse" : "expand")}: ${title}`,
2727
+ onClick: () => {
2728
+ if (!degraded && state.status !== "error") setUserOpen(!userOpen);
2729
+ },
2730
+ children: [
2731
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2732
+ className: FallbacksCard_module_css_default.headText,
2733
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2734
+ className: FallbacksCard_module_css_default.name,
2735
+ children: title
2736
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2737
+ className: FallbacksCard_module_css_default.description,
2738
+ children: t("intro")
2739
+ })]
2740
+ }),
2741
+ dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2742
+ className: FallbacksCard_module_css_default.pending,
2743
+ children: t("unsaved")
2744
+ }) : null,
2745
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, { className: open ? `${FallbacksCard_module_css_default.chevron} ${FallbacksCard_module_css_default.chevronOpen}` : FallbacksCard_module_css_default.chevron })
2746
+ ]
2747
+ });
2748
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
2749
+ className: open ? `${FallbacksCard_module_css_default.card} ${FallbacksCard_module_css_default.cardOpen}` : FallbacksCard_module_css_default.card,
2750
+ children: [
2751
+ header,
2752
+ open && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2753
+ className: FallbacksCard_module_css_default.body,
2754
+ children: [
2755
+ state.legacyKeys.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2756
+ className: FallbacksCard_module_css_default.legacyNotice,
2757
+ role: "status",
2758
+ children: t("legacy.banner", { keys: state.legacyKeys.join(", ") })
2759
+ }),
2760
+ state.status === "error" && state.error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2761
+ className: FallbacksCard_module_css_default.noticeRow,
2762
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2763
+ className: FallbacksCard_module_css_default.error,
2764
+ role: "alert",
2765
+ children: t("error.generic", { message: state.error })
2766
+ }), !state.writable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2767
+ variant: "outline",
2768
+ size: "sm",
2769
+ onClick: () => {
2770
+ controller.load();
2771
+ },
2772
+ children: t("retry")
2773
+ })]
2774
+ }),
2775
+ validationErrors.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2776
+ className: FallbacksCard_module_css_default.error,
2777
+ role: "alert",
2778
+ children: `${t("validation.blocked")}${validationErrors.join("; ")}`
2779
+ }),
2780
+ degraded && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2781
+ className: FallbacksCard_module_css_default.notice,
2782
+ role: "status",
2783
+ children: t("unavailable")
2784
+ }),
2785
+ state.status === "ready" && !state.writable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2786
+ className: FallbacksCard_module_css_default.readOnly,
2787
+ role: "status",
2788
+ children: t("readOnly")
2789
+ }),
2790
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2791
+ className: FallbacksCard_module_css_default.form,
2792
+ children: [
2793
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2794
+ className: FallbacksCard_module_css_default.checkboxRow,
2795
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2796
+ className: FallbacksCard_module_css_default.checkLabel,
2797
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2798
+ className: FallbacksCard_module_css_default.checkLabelTitle,
2799
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
2800
+ htmlFor: "fallbacks-enabled",
2801
+ children: t("enabled.label")
2802
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
2803
+ label: t("enabled.tooltip"),
2804
+ disabled: !writable
2805
+ })]
2806
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2807
+ className: FallbacksCard_module_css_default.checkLabelDesc,
2808
+ children: t("enabled.hint")
2809
+ })]
2810
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2811
+ id: "fallbacks-enabled",
2812
+ type: "checkbox",
2813
+ className: FallbacksCard_module_css_default.checkbox,
2814
+ checked: scalars.enabled,
2815
+ disabled: !writable,
2816
+ onChange: (event) => {
2817
+ updateScalars((draft) => {
2818
+ draft.enabled = event.target.checked;
2819
+ });
2820
+ }
2821
+ })]
2822
+ }),
2823
+ !scalars.enabled && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2824
+ className: FallbacksCard_module_css_default.offNotice,
2825
+ children: t("enabled.off")
2826
+ }),
2827
+ scalars.enabled && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
2828
+ className: FallbacksCard_module_css_default.fieldset,
2829
+ disabled: !writable,
2830
+ children: [
2831
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2832
+ className: FallbacksCard_module_css_default.field,
2833
+ role: "group",
2834
+ "aria-labelledby": "fallbacks-trigger-codes",
2835
+ children: [
2836
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2837
+ className: FallbacksCard_module_css_default.fieldLabel,
2838
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2839
+ id: "fallbacks-trigger-codes",
2840
+ children: t("triggerCodes.label")
2841
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
2842
+ label: t("triggerCodes.tooltip"),
2843
+ disabled: !writable
2844
+ })]
2845
+ }),
2846
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2847
+ className: FallbacksCard_module_css_default.hint,
2848
+ children: t("triggerCodes.hint")
2849
+ }),
2850
+ KNOWN_TRIGGER_CODES.map((code) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2851
+ className: FallbacksCard_module_css_default.optionRow,
2852
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2853
+ type: "checkbox",
2854
+ checked: scalars.triggerCodes.includes(code),
2855
+ onChange: (event) => {
2856
+ updateScalars((draft) => {
2857
+ draft.triggerCodes = withTriggerCode(draft.triggerCodes, code, event.target.checked);
2858
+ });
2859
+ }
2860
+ }), t(TRIGGER_CODE_LABELS[code])]
2861
+ }, code)),
2862
+ unknownCodes.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2863
+ className: FallbacksCard_module_css_default.hint,
2864
+ children: t("triggerCodes.extra", { codes: unknownCodes.join(", ") })
2865
+ })
2866
+ ]
2867
+ }),
2868
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2869
+ className: FallbacksCard_module_css_default.field,
2870
+ role: "group",
2871
+ "aria-labelledby": "fallbacks-revert-policy",
2872
+ children: [
2873
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2874
+ className: FallbacksCard_module_css_default.fieldLabel,
2875
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2876
+ id: "fallbacks-revert-policy",
2877
+ children: t("revertPolicy.label")
2878
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
2879
+ label: t("revertPolicy.tooltip"),
2880
+ disabled: !writable
2881
+ })]
2882
+ }),
2883
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2884
+ className: FallbacksCard_module_css_default.hint,
2885
+ children: t("revertPolicy.hint")
2886
+ }),
2887
+ ["cooldown-expiry", "never"].map((policy) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2888
+ className: FallbacksCard_module_css_default.optionRow,
2889
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2890
+ type: "radio",
2891
+ name: "fallbacks-revert-policy",
2892
+ checked: scalars.revertPolicy === policy,
2893
+ onChange: () => {
2894
+ updateScalars((draft) => {
2895
+ draft.revertPolicy = policy;
2896
+ });
2897
+ }
2898
+ }), t(`revertPolicy.${policy}`)]
2899
+ }, policy))
2900
+ ]
2901
+ }),
2902
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2903
+ className: FallbacksCard_module_css_default.numberFields,
2904
+ children: [
2905
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2906
+ className: FallbacksCard_module_css_default.field,
2907
+ children: [
2908
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2909
+ className: FallbacksCard_module_css_default.fieldLabel,
2910
+ children: [
2911
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
2912
+ htmlFor: "fallbacks-cooldown-ms",
2913
+ children: t("cooldownMs.label")
2914
+ }),
2915
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
2916
+ label: t("cooldownMs.tooltip"),
2917
+ disabled: !writable
2918
+ }),
2919
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2920
+ className: FallbacksCard_module_css_default.defaultNote,
2921
+ children: [
2922
+ t("defaults.prefix"),
2923
+ ": ",
2924
+ state.config.cooldownMs
2925
+ ]
2926
+ })
2927
+ ]
2928
+ }),
2929
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2930
+ id: "fallbacks-cooldown-ms",
2931
+ className: FallbacksCard_module_css_default.input,
2932
+ type: "number",
2933
+ min: 0,
2934
+ value: String(scalars.cooldownMs),
2935
+ disabled: !writable,
2936
+ onChange: (event) => {
2937
+ updateScalars((draft) => {
2938
+ draft.cooldownMs = parseCount(event.target.value);
2939
+ });
2940
+ }
2941
+ }),
2942
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2943
+ className: FallbacksCard_module_css_default.hint,
2944
+ children: t("cooldownMs.hint")
2945
+ })
2946
+ ]
2947
+ }),
2948
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2949
+ className: FallbacksCard_module_css_default.field,
2950
+ children: [
2951
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2952
+ className: FallbacksCard_module_css_default.fieldLabel,
2953
+ children: [
2954
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
2955
+ htmlFor: "fallbacks-max-switches",
2956
+ children: t("maxSwitchesPerStep.label")
2957
+ }),
2958
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
2959
+ label: t("maxSwitchesPerStep.tooltip"),
2960
+ disabled: !writable
2961
+ }),
2962
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2963
+ className: FallbacksCard_module_css_default.defaultNote,
2964
+ children: [
2965
+ t("defaults.prefix"),
2966
+ ": ",
2967
+ state.config.maxSwitchesPerStep
2968
+ ]
2969
+ })
2970
+ ]
2971
+ }),
2972
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2973
+ id: "fallbacks-max-switches",
2974
+ className: FallbacksCard_module_css_default.input,
2975
+ type: "number",
2976
+ min: 0,
2977
+ value: String(scalars.maxSwitchesPerStep),
2978
+ disabled: !writable,
2979
+ onChange: (event) => {
2980
+ updateScalars((draft) => {
2981
+ draft.maxSwitchesPerStep = parseCount(event.target.value);
2982
+ });
2983
+ }
2984
+ }),
2985
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2986
+ className: FallbacksCard_module_css_default.hint,
2987
+ children: t("maxSwitchesPerStep.hint")
2988
+ })
2989
+ ]
2990
+ }),
2991
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2992
+ className: FallbacksCard_module_css_default.field,
2993
+ children: [
2994
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2995
+ className: FallbacksCard_module_css_default.fieldLabel,
2996
+ children: [
2997
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
2998
+ htmlFor: "fallbacks-always-cap",
2999
+ children: t("alwaysModeRetryCap.label")
3000
+ }),
3001
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
3002
+ label: t("alwaysModeRetryCap.tooltip"),
3003
+ disabled: !writable
3004
+ }),
3005
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3006
+ className: FallbacksCard_module_css_default.defaultNote,
3007
+ children: [
3008
+ t("defaults.prefix"),
3009
+ ": ",
3010
+ state.config.alwaysModeRetryCap
3011
+ ]
3012
+ })
3013
+ ]
3014
+ }),
3015
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3016
+ id: "fallbacks-always-cap",
3017
+ className: FallbacksCard_module_css_default.input,
3018
+ type: "number",
3019
+ min: 0,
3020
+ value: String(scalars.alwaysModeRetryCap),
3021
+ disabled: !writable,
3022
+ onChange: (event) => {
3023
+ updateScalars((draft) => {
3024
+ draft.alwaysModeRetryCap = parseCount(event.target.value);
3025
+ });
3026
+ }
3027
+ }),
3028
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3029
+ className: FallbacksCard_module_css_default.hint,
3030
+ children: t("alwaysModeRetryCap.hint")
3031
+ })
3032
+ ]
3033
+ })
3034
+ ]
3035
+ }),
3036
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3037
+ className: FallbacksCard_module_css_default.field,
3038
+ role: "group",
3039
+ "aria-labelledby": "fallbacks-root-chain",
3040
+ children: [
3041
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3042
+ className: FallbacksCard_module_css_default.fieldLabel,
3043
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3044
+ id: "fallbacks-root-chain",
3045
+ children: t("rootChain.label")
3046
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
3047
+ label: t("rootChain.tooltip"),
3048
+ disabled: !writable
3049
+ })]
3050
+ }),
3051
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3052
+ className: FallbacksCard_module_css_default.hint,
3053
+ children: t("rootChain.hint")
3054
+ }),
3055
+ state.catalogStatus === "error" && state.catalogError !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3056
+ className: FallbacksCard_module_css_default.hint,
3057
+ children: t("catalog.error", { message: state.catalogError })
3058
+ }),
3059
+ state.catalogStatus === "ready" && state.catalogError !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3060
+ className: FallbacksCard_module_css_default.hint,
3061
+ children: t("catalog.partial", { message: state.catalogError })
3062
+ }),
3063
+ state.catalogStatus === "ready" && (state.groups.length === 0 || state.configuredProviders.length === 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3064
+ className: FallbacksCard_module_css_default.hint,
3065
+ children: t("catalog.empty")
3066
+ }),
3067
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3068
+ className: FallbacksCard_module_css_default.list,
3069
+ children: rootChainRows.map((row, rowIndex) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3070
+ className: FallbacksCard_module_css_default.editorCard,
3071
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3072
+ className: FallbacksCard_module_css_default.chainSelectors,
3073
+ children: row.selectors.map((selector, selectorIndex) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChainSelectorEditor, {
3074
+ selector,
3075
+ catalog: catalogOf(state),
3076
+ configuredProviders: state.configuredProviders,
3077
+ disabled: !writable,
3078
+ t,
3079
+ onChange: (patch) => {
3080
+ updateRootChainSelector(selectorIndex, patch);
3081
+ },
3082
+ onRemove: () => {
3083
+ removeRootChainSelector(selectorIndex);
3084
+ }
3085
+ }, selectorIndex))
3086
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
3087
+ variant: "outline",
3088
+ size: "sm",
3089
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPlusOutline16, { size: 14 }),
3090
+ className: FallbacksCard_module_css_default.addButton,
3091
+ onClick: addRootChainSelector,
3092
+ children: t("rootChain.selector.add")
3093
+ })]
3094
+ }, rowIndex))
3095
+ })
3096
+ ]
3097
+ }),
3098
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3099
+ className: FallbacksCard_module_css_default.field,
3100
+ role: "group",
3101
+ "aria-labelledby": "fallbacks-roles-list",
3102
+ children: [
3103
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3104
+ className: FallbacksCard_module_css_default.fieldLabel,
3105
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3106
+ id: "fallbacks-roles-list",
3107
+ children: t("roles.list.label")
3108
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
3109
+ label: t("roles.list.tooltip"),
3110
+ disabled: !writable
3111
+ })]
3112
+ }),
3113
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3114
+ className: FallbacksCard_module_css_default.hint,
3115
+ children: t("roles.list.hint")
3116
+ }),
3117
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3118
+ className: FallbacksCard_module_css_default.list,
3119
+ children: roleRows.map((row, index) => {
3120
+ const invalid = invalidRoleIds?.has(row.id.trim()) ?? false;
3121
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3122
+ className: FallbacksCard_module_css_default.editorCard,
3123
+ children: [
3124
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3125
+ className: FallbacksCard_module_css_default.ruleGrid,
3126
+ children: [
3127
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3128
+ className: FallbacksCard_module_css_default.ruleCell,
3129
+ children: [
3130
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3131
+ className: FallbacksCard_module_css_default.ruleCellLabel,
3132
+ children: t("roles.id")
3133
+ }),
3134
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3135
+ className: `${FallbacksCard_module_css_default.input} ${invalid ? FallbacksCard_module_css_default.inputInvalid : ""}`,
3136
+ value: row.id,
3137
+ placeholder: t("roles.idPlaceholder"),
3138
+ "aria-label": t("roles.id"),
3139
+ "aria-invalid": invalid ? true : void 0,
3140
+ disabled: !writable,
3141
+ onChange: (event) => {
3142
+ updateRoleRow(index, { id: event.target.value });
3143
+ }
3144
+ }),
3145
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3146
+ className: FallbacksCard_module_css_default.hint,
3147
+ children: t("roles.id.hint")
3148
+ })
3149
+ ]
3150
+ }),
3151
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3152
+ className: FallbacksCard_module_css_default.ruleCell,
3153
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3154
+ className: FallbacksCard_module_css_default.ruleCellLabel,
3155
+ children: t("roles.label")
3156
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3157
+ className: FallbacksCard_module_css_default.input,
3158
+ value: row.label,
3159
+ "aria-label": t("roles.label"),
3160
+ disabled: !writable,
3161
+ onChange: (event) => {
3162
+ updateRoleRow(index, { label: event.target.value });
3163
+ }
3164
+ })]
3165
+ }),
3166
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3167
+ className: FallbacksCard_module_css_default.ruleCell,
3168
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3169
+ className: FallbacksCard_module_css_default.ruleCellLabel,
3170
+ children: t("roles.description")
3171
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3172
+ className: FallbacksCard_module_css_default.input,
3173
+ value: row.description,
3174
+ "aria-label": t("roles.description"),
3175
+ disabled: !writable,
3176
+ onChange: (event) => {
3177
+ updateRoleRow(index, { description: event.target.value });
3178
+ }
3179
+ })]
3180
+ })
3181
+ ]
3182
+ }),
3183
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3184
+ className: FallbacksCard_module_css_default.chainSelectors,
3185
+ children: row.selectors.map((selector, selectorIndex) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChainSelectorEditor, {
3186
+ selector,
3187
+ catalog: catalogOf(state),
3188
+ configuredProviders: state.configuredProviders,
3189
+ disabled: !writable,
3190
+ t,
3191
+ onChange: (patch) => {
3192
+ updateRoleSelector(index, selectorIndex, patch);
3193
+ },
3194
+ onRemove: () => {
3195
+ removeRoleSelector(index, selectorIndex);
3196
+ }
3197
+ }, selectorIndex))
3198
+ }),
3199
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3200
+ className: FallbacksCard_module_css_default.ruleGrid,
3201
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3202
+ className: FallbacksCard_module_css_default.ruleCell,
3203
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3204
+ className: FallbacksCard_module_css_default.ruleCellLabel,
3205
+ children: t("roles.fallback")
3206
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
3207
+ className: `${FallbacksCard_module_css_default.input} ${FallbacksCard_module_css_default.selectInput}`,
3208
+ value: row.fallback,
3209
+ "aria-label": t("roles.fallback"),
3210
+ disabled: !writable,
3211
+ onChange: (event) => {
3212
+ updateRoleRow(index, { fallback: event.target.value });
3213
+ },
3214
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3215
+ value: "inherit-root",
3216
+ children: t("roles.fallback.inherit-root")
3217
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3218
+ value: "none",
3219
+ children: t("roles.fallback.none")
3220
+ })]
3221
+ })]
3222
+ })
3223
+ }),
3224
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
3225
+ variant: "outline",
3226
+ size: "sm",
3227
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPlusOutline16, { size: 14 }),
3228
+ className: FallbacksCard_module_css_default.addButton,
3229
+ onClick: () => {
3230
+ addRoleSelector(index);
3231
+ },
3232
+ children: t("roles.selector.add")
3233
+ }),
3234
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3235
+ className: FallbacksCard_module_css_default.cardFoot,
3236
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3237
+ type: "button",
3238
+ className: `${FallbacksCard_module_css_default.iconButton} ${FallbacksCard_module_css_default.iconButtonDanger}`,
3239
+ "data-tip": t("roles.remove"),
3240
+ "aria-label": t("roles.remove"),
3241
+ onClick: () => {
3242
+ removeRole(index);
3243
+ },
3244
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, {})
3245
+ })
3246
+ })
3247
+ ]
3248
+ }, index);
3249
+ })
3250
+ }),
3251
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
3252
+ variant: "outline",
3253
+ size: "sm",
3254
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPlusOutline16, { size: 14 }),
3255
+ className: FallbacksCard_module_css_default.addButton,
3256
+ onClick: addRole,
3257
+ children: t("roles.add")
3258
+ })
3259
+ ]
3260
+ }),
3261
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3262
+ className: FallbacksCard_module_css_default.field,
3263
+ role: "group",
3264
+ "aria-labelledby": "fallbacks-roles-rules",
3265
+ children: [
3266
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3267
+ className: FallbacksCard_module_css_default.fieldLabel,
3268
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3269
+ id: "fallbacks-roles-rules",
3270
+ children: t("roles.rules")
3271
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
3272
+ label: t("roles.rules.tooltip"),
3273
+ disabled: !writable
3274
+ })]
3275
+ }),
3276
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3277
+ className: FallbacksCard_module_css_default.hint,
3278
+ children: t("roles.rules.hint")
3279
+ }),
3280
+ state.catalogStatus === "error" && state.catalogError !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3281
+ className: FallbacksCard_module_css_default.hint,
3282
+ children: t("catalog.error", { message: state.catalogError })
3283
+ }),
3284
+ state.catalogStatus === "ready" && state.catalogError !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3285
+ className: FallbacksCard_module_css_default.hint,
3286
+ children: t("catalog.partial", { message: state.catalogError })
3287
+ }),
3288
+ state.catalogStatus === "ready" && (state.groups.length === 0 || state.configuredProviders.length === 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3289
+ className: FallbacksCard_module_css_default.hint,
3290
+ children: t("catalog.empty")
3291
+ }),
3292
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3293
+ className: FallbacksCard_module_css_default.list,
3294
+ children: ruleRows.map((row, index) => {
3295
+ const catalog = catalogOf(state);
3296
+ const providerRaw = selectionToRaw(row.provider);
3297
+ const group = catalog?.groups.find((entry) => entry.id === providerRaw);
3298
+ const providerOutside = row.provider?.kind === "outside";
3299
+ const providerUnconfigured = !providerOutside && providerRaw !== "" && (catalog?.providers.some((entry) => entry.provider === providerRaw) ?? false) && !state.configuredProviders.some((entry) => entry.provider === providerRaw);
3300
+ const modelOutside = row.model?.kind === "outside";
3301
+ const roleOutside = row.role !== "" && !roleOptions.includes(row.role);
3302
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3303
+ className: FallbacksCard_module_css_default.editorCard,
3304
+ children: [
3305
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3306
+ className: FallbacksCard_module_css_default.ruleGrid,
3307
+ children: [
3308
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3309
+ className: FallbacksCard_module_css_default.ruleCell,
3310
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3311
+ className: FallbacksCard_module_css_default.ruleCellLabel,
3312
+ children: t("roles.rule.origin")
3313
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
3314
+ className: `${FallbacksCard_module_css_default.input} ${FallbacksCard_module_css_default.selectInput}`,
3315
+ value: row.origin,
3316
+ onChange: (event) => {
3317
+ updateRuleRow(index, { origin: event.target.value });
3318
+ },
3319
+ children: [
3320
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3321
+ value: "",
3322
+ children: t("roles.rule.origin.any")
3323
+ }),
3324
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3325
+ value: "root",
3326
+ children: t("roles.rule.origin.root")
3327
+ }),
3328
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3329
+ value: "subagent",
3330
+ children: t("roles.rule.origin.subagent")
3331
+ })
3332
+ ]
3333
+ })]
3334
+ }),
3335
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3336
+ className: FallbacksCard_module_css_default.ruleCell,
3337
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3338
+ className: FallbacksCard_module_css_default.ruleCellLabel,
3339
+ children: t("roles.rule.provider")
3340
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
3341
+ className: `${FallbacksCard_module_css_default.input} ${FallbacksCard_module_css_default.selectInput}`,
3342
+ value: providerRaw,
3343
+ onChange: (event) => {
3344
+ if (event.target.value === providerRaw) return;
3345
+ updateRuleRow(index, {
3346
+ provider: classifyProvider(event.target.value, catalog),
3347
+ model: null
3348
+ });
3349
+ },
3350
+ children: [
3351
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3352
+ value: "",
3353
+ children: t("roles.rule.provider.any")
3354
+ }),
3355
+ state.configuredProviders.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3356
+ value: entry.provider,
3357
+ children: entry.displayName
3358
+ }, entry.provider)),
3359
+ providerUnconfigured && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3360
+ value: providerRaw,
3361
+ children: `${providerRaw}${t("catalog.unconfigured.short")}`
3362
+ }),
3363
+ providerOutside && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3364
+ value: providerRaw,
3365
+ children: `${providerRaw}${t("catalog.outside.short")}`
3366
+ })
3367
+ ]
3368
+ })]
3369
+ }),
3370
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3371
+ className: FallbacksCard_module_css_default.ruleCell,
3372
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3373
+ className: FallbacksCard_module_css_default.ruleCellLabel,
3374
+ children: t("roles.rule.model")
3375
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
3376
+ className: `${FallbacksCard_module_css_default.input} ${FallbacksCard_module_css_default.selectInput}`,
3377
+ value: selectionToRaw(row.model),
3378
+ onChange: (event) => {
3379
+ updateRuleRow(index, { model: classifyModel(providerRaw, event.target.value, catalog) });
3380
+ },
3381
+ children: [
3382
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3383
+ value: "",
3384
+ children: t("roles.rule.model.any")
3385
+ }),
3386
+ (group?.models ?? []).map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3387
+ value: model.id,
3388
+ children: model.name
3389
+ }, model.id)),
3390
+ modelOutside && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3391
+ value: selectionToRaw(row.model),
3392
+ children: `${selectionToRaw(row.model)}${t("catalog.outside.short")}`
3393
+ })
3394
+ ]
3395
+ })]
3396
+ }),
3397
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3398
+ className: FallbacksCard_module_css_default.ruleCell,
3399
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3400
+ className: FallbacksCard_module_css_default.ruleCellLabel,
3401
+ children: t("roles.rule.role")
3402
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
3403
+ className: `${FallbacksCard_module_css_default.input} ${FallbacksCard_module_css_default.selectInput}`,
3404
+ value: row.role,
3405
+ disabled: !writable,
3406
+ onChange: (event) => {
3407
+ updateRuleRow(index, { role: event.target.value });
3408
+ },
3409
+ children: [
3410
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3411
+ value: "",
3412
+ children: t("roles.rule.roleSelectPlaceholder")
3413
+ }),
3414
+ roleOptions.map((id) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3415
+ value: id,
3416
+ children: id === "inherit" ? t("roles.rule.role.inherit") : id
3417
+ }, id)),
3418
+ roleOutside && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3419
+ value: row.role,
3420
+ children: `${row.role}${t("roles.rule.roleUndeclared.short")}`
3421
+ })
3422
+ ]
3423
+ })]
3424
+ })
3425
+ ]
3426
+ }),
3427
+ (providerOutside || modelOutside) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3428
+ className: FallbacksCard_module_css_default.hint,
3429
+ children: [t("catalog.outside.hint"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
3430
+ label: t("catalog.outside.tooltip"),
3431
+ disabled: !writable
3432
+ })]
3433
+ }),
3434
+ row.role === "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3435
+ className: FallbacksCard_module_css_default.hint,
3436
+ children: t("validation.ruleRoleRequired")
3437
+ }),
3438
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3439
+ className: FallbacksCard_module_css_default.cardFoot,
3440
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3441
+ type: "button",
3442
+ className: `${FallbacksCard_module_css_default.iconButton} ${FallbacksCard_module_css_default.iconButtonDanger}`,
3443
+ "data-tip": t("roles.removeRule"),
3444
+ "aria-label": t("roles.removeRule"),
3445
+ onClick: () => {
3446
+ setRuleRows((rows) => rows.filter((_, rowIndex) => rowIndex !== index));
3447
+ },
3448
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, {})
3449
+ })
3450
+ })
3451
+ ]
3452
+ }, index);
3453
+ })
3454
+ }),
3455
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
3456
+ variant: "outline",
3457
+ size: "sm",
3458
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPlusOutline16, { size: 14 }),
3459
+ className: FallbacksCard_module_css_default.addButton,
3460
+ onClick: () => {
3461
+ setRuleRows((rows) => [...rows, {
3462
+ origin: "",
3463
+ provider: null,
3464
+ model: null,
3465
+ role: ""
3466
+ }]);
3467
+ },
3468
+ children: t("roles.addRule")
3469
+ })
3470
+ ]
3471
+ })
3472
+ ]
3473
+ })
3474
+ ]
3475
+ }),
3476
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3477
+ className: FallbacksCard_module_css_default.statusBlock,
3478
+ children: [
3479
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3480
+ className: FallbacksCard_module_css_default.statusTitle,
3481
+ children: t("status.title")
3482
+ }),
3483
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
3484
+ className: FallbacksCard_module_css_default.statusLine,
3485
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3486
+ className: FallbacksCard_module_css_default.statusLineLabel,
3487
+ children: t("status.effectiveModel.label")
3488
+ }), effectiveModelLine]
3489
+ }),
3490
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
3491
+ className: FallbacksCard_module_css_default.statusLine,
3492
+ role: state.switchesStatus === "error" ? "alert" : void 0,
3493
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3494
+ className: FallbacksCard_module_css_default.statusLineLabel,
3495
+ children: t("status.switches.label")
3496
+ }), switchesLine]
3497
+ }),
3498
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3499
+ className: FallbacksCard_module_css_default.statusLine,
3500
+ children: t("status.selectionNote")
3501
+ })
3502
+ ]
3503
+ }),
3504
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3505
+ className: FallbacksCard_module_css_default.footer,
3506
+ children: [
3507
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3508
+ type: "button",
3509
+ className: FallbacksCard_module_css_default.secondaryButton,
3510
+ disabled: !dirty || saving,
3511
+ onClick: discard,
3512
+ children: t("discard")
3513
+ }),
3514
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3515
+ type: "button",
3516
+ className: FallbacksCard_module_css_default.secondaryButton,
3517
+ disabled: !writable || saving,
3518
+ onClick: () => {
3519
+ setConfirmingReset(true);
3520
+ },
3521
+ children: t("reset")
3522
+ }),
3523
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3524
+ type: "button",
3525
+ className: FallbacksCard_module_css_default.primaryButton,
3526
+ disabled: !writable || saving || !dirty,
3527
+ onClick: save,
3528
+ children: saving ? t("save.saving") : t("save")
3529
+ })
3530
+ ]
3531
+ })
3532
+ ]
3533
+ }),
3534
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
3535
+ open: confirmingReset,
3536
+ onClose: () => {
3537
+ if (!resetting) setConfirmingReset(false);
3538
+ },
3539
+ title: t("reset.confirmTitle"),
3540
+ closeLabel: t("close"),
3541
+ description: t("reset.confirm"),
3542
+ className: FallbacksCard_module_css_default.resetDialog,
3543
+ footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
3544
+ variant: "outline",
3545
+ autoFocus: true,
3546
+ disabled: resetting,
3547
+ onClick: () => {
3548
+ setConfirmingReset(false);
3549
+ },
3550
+ children: t("reset.confirm.cancel")
3551
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
3552
+ variant: "outline",
3553
+ className: FallbacksCard_module_css_default.confirmDanger,
3554
+ disabled: resetting,
3555
+ onClick: confirmReset,
3556
+ children: resetting ? t("reset.saving") : t("reset.confirm.action")
3557
+ })] })
3558
+ })
3559
+ ]
3560
+ });
3561
+ }
3562
+ //#endregion
3563
+ //#region \0dsh-css:/Users/bibi/workspace/ai/deepseek/dsh-llm-fallbacks/src/client/GeneralFallbacksRow.module.css.mjs
3564
+ const css$1 = "\n\n._440e1d7b_row {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 16px 0;\n border-bottom: 1px solid var(--dsw-alias-border-l2);\n}\n\n._a4f2ceec_rowText {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 4px;\n padding-right: 48px;\n}\n\n._9865b509_title {\n font-size: 14px;\n font-weight: 400;\n line-height: 22px;\n color: var(--dsw-alias-label-primary);\n}\n\n\n._10a44713_summary {\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-tertiary);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n\n._35648278_badge {\n flex: none;\n padding: 2px 10px;\n border-radius: 9px;\n font-size: 12px;\n line-height: 18px;\n background: var(--dsw-alias-bg-module-platform);\n color: var(--dsw-alias-label-tertiary);\n}\n\n._3b9e8609_badgeEnabled {\n color: var(--dsw-alias-state-success-primary);\n}\n";
3565
+ const tagId$1 = "dsh-llm-fallbacks/GeneralFallbacksRow.module.css";
3566
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
3567
+ const tag = document.createElement("style");
3568
+ tag.dataset.plugin = "dsh-llm-fallbacks";
3569
+ tag.dataset.pluginCss = tagId$1;
3570
+ tag.textContent = css$1;
3571
+ document.head.appendChild(tag);
3572
+ }
3573
+ var GeneralFallbacksRow_module_css_default = {
3574
+ "row": "_440e1d7b_row",
3575
+ "rowText": "_a4f2ceec_rowText",
3576
+ "title": "_9865b509_title",
3577
+ "summary": "_10a44713_summary",
3578
+ "badge": "_35648278_badge",
3579
+ "badgeEnabled": "_3b9e8609_badgeEnabled"
3580
+ };
3581
+ //#endregion
3582
+ //#region src/client/GeneralFallbacksRow.tsx
3583
+ /**
3584
+ * Fallbacks status row — the `fallbacks` read-only row on the dsh General
3585
+ * settings page (plan fallbacks-aux-seams, task 1). Registered into the
3586
+ * `settings.general.item` slot (id `fallbacks`, order 100 — after every
3587
+ * upstream preference row: agent-preset -25 / permission -20 / language 0 /
3588
+ * appearance 10 / composer-enter 20, so the informational row renders at the
3589
+ * column end). Owner props are intentionally empty (`children?: never`,
3590
+ * dsh-private ui-settings slots.ts:81-84 — the section column only stacks),
3591
+ * so all data flows through the shared {@link FallbacksSettingsController}
3592
+ * (the same instance the plugin-config card consumes): the row triggers the
3593
+ * first read on mount when the store is still idle, and the pushed
3594
+ * invalidations wired in `apply` (`settings/document-updated` fallbacks-ns +
3595
+ * `connection/reset`, which refresh only already-read stores) keep it fresh
3596
+ * afterwards — no new data path, no store API change.
3597
+ *
3598
+ * The row is read-only by design (偏好位语义: a General preference row is not
3599
+ * a control surface): an enabled badge + a compact last-switch summary.
3600
+ * Honest degraded states: a hard load error or an unreachable gateway
3601
+ * channel (`ready && !present`) render the neutral 'unknown' badge — a
3602
+ * channel-down read must never masquerade as 'disabled' (KD-G5); the
3603
+ * switches face keeps its own error/empty states (D-5 semantics unchanged).
3604
+ *
3605
+ * Geometry follows the upstream Setting-Cell (figma 501:30011 — gap 8,
3606
+ * pad 16/0, hairline separator, title over subtitle in the text column, a
3607
+ * small non-interactive pill on the right); every color resolves through a
3608
+ * `--dsw-alias-*` token (light/dark adaptive).
3609
+ */
3610
+ /**
3611
+ * Render the Fallbacks status row.
3612
+ * @param props - composed slot props.
3613
+ * @returns the row element tree.
3614
+ */
3615
+ function GeneralFallbacksRow({ controller, useSnapshot, t }) {
3616
+ const state = useSnapshot((snapshot) => snapshot);
3617
+ (0, react.useEffect)(() => {
3618
+ const snapshot = controller.store.getSnapshot();
3619
+ if (snapshot.status === "idle") controller.load();
3620
+ if (snapshot.switchesStatus === "idle") controller.loadSwitches();
3621
+ }, [controller]);
3622
+ const settled = state.status === "ready";
3623
+ const badgeKey = settled && state.present ? state.config.enabled ? "general.enabled" : "general.disabled" : "general.unknown";
3624
+ const latestSwitch = state.switches[0];
3625
+ let summary;
3626
+ if (state.status === "error") summary = t("general.error", { message: state.error ?? "" });
3627
+ else if (!settled) summary = t("loading");
3628
+ else if (!state.present) summary = t("general.unavailable");
3629
+ else if (state.switchesStatus === "error") summary = t("status.switches.error", { message: state.switchesError ?? "" });
3630
+ else if (state.switchesStatus === "loading") summary = t("loading");
3631
+ else if (latestSwitch === void 0) summary = t("general.switch.empty");
3632
+ else {
3633
+ const reasonKey = SWITCH_REASON_KEYS[latestSwitch.reason];
3634
+ summary = t("general.switch", {
3635
+ from: `${latestSwitch.from.provider}/${latestSwitch.from.model}`,
3636
+ to: `${latestSwitch.to.provider}/${latestSwitch.to.model}`,
3637
+ role: latestSwitch.role,
3638
+ reason: reasonKey === void 0 ? latestSwitch.reason : t(reasonKey)
3639
+ });
3640
+ }
3641
+ const alert = state.status === "error" || state.switchesStatus === "error";
3642
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3643
+ className: GeneralFallbacksRow_module_css_default.row,
3644
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3645
+ className: GeneralFallbacksRow_module_css_default.rowText,
3646
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3647
+ className: GeneralFallbacksRow_module_css_default.title,
3648
+ children: t("general.title")
3649
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3650
+ className: GeneralFallbacksRow_module_css_default.summary,
3651
+ role: alert ? "alert" : void 0,
3652
+ children: summary
3653
+ })]
3654
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3655
+ className: `${GeneralFallbacksRow_module_css_default.badge} ${badgeKey === "general.enabled" ? GeneralFallbacksRow_module_css_default.badgeEnabled : ""}`,
3656
+ children: t(badgeKey)
3657
+ })]
3658
+ });
3659
+ }
3660
+ //#endregion
3661
+ //#region src/client/switch-guard.ts
3662
+ /**
3663
+ * True when `value` is a well-formed `fallbacks/switch` payload — the ONE
3664
+ * client-side shape guard, shared by the conversation node definition's
3665
+ * `match`/`start` and the renderer's degrade check (both in
3666
+ * `src/client/ConversationFallbackSwitch.tsx`).
3667
+ *
3668
+ * The durable session log is append-only and survives plugin/host upgrades,
3669
+ * so a `fallbacks/switch` event or node payload may carry a stale or
3670
+ * corrupted shape — version skew must degrade the transcript line (a
3671
+ * title-only notice), never crash the session assembly or the renderer.
3672
+ *
3673
+ * The HOST-side mirror (`src/commands.ts` `isFallbacksSwitchData`) lives in
3674
+ * a DIFFERENT bundle (host vs client) — it intentionally stays separate; do
3675
+ * not merge the two guards across the bundle boundary.
3676
+ */
3677
+ function isFallbacksSwitchData(value) {
3678
+ if (typeof value !== "object" || value === null) return false;
3679
+ const payload = value;
3680
+ if (typeof payload.turn !== "number" || typeof payload.step !== "number") return false;
3681
+ if (typeof payload.role !== "string" || typeof payload.reason !== "string") return false;
3682
+ const from = payload.from;
3683
+ const to = payload.to;
3684
+ return typeof from?.provider === "string" && typeof from?.model === "string" && typeof to?.provider === "string" && typeof to?.model === "string";
3685
+ }
3686
+ //#endregion
3687
+ //#region \0dsh-css:/Users/bibi/workspace/ai/deepseek/dsh-llm-fallbacks/src/client/ConversationFallbackSwitch.module.css.mjs
3688
+ const css = "\n\n._ea99bbef_switchRow {\n display: flex;\n align-items: center;\n padding: 2px 0;\n font-size: 14px;\n line-height: 24px;\n}\n\n\n._02580bfd_switchTitle {\n flex: none;\n color: var(--dsw-alias-label-primary-dimmed);\n}\n\n._88b2d18b_switchSep {\n flex: none;\n width: 2px;\n height: 2px;\n margin: 0 8px;\n border-radius: 1px;\n background: var(--dsw-alias-label-caption);\n}\n\n\n._7c8c1b6f_switchSummary {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n color: var(--dsw-alias-label-tertiary);\n}\n";
3689
+ const tagId = "dsh-llm-fallbacks/ConversationFallbackSwitch.module.css";
3690
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
3691
+ const tag = document.createElement("style");
3692
+ tag.dataset.plugin = "dsh-llm-fallbacks";
3693
+ tag.dataset.pluginCss = tagId;
3694
+ tag.textContent = css;
3695
+ document.head.appendChild(tag);
3696
+ }
3697
+ var ConversationFallbackSwitch_module_css_default = {
3698
+ "switchRow": "_ea99bbef_switchRow",
3699
+ "switchTitle": "_02580bfd_switchTitle",
3700
+ "switchSep": "_88b2d18b_switchSep",
3701
+ "switchSummary": "_7c8c1b6f_switchSummary"
3702
+ };
3703
+ //#endregion
3704
+ //#region src/client/ConversationFallbackSwitch.tsx
3705
+ /**
3706
+ * One switch event → one chat node. Each `fallbacks/switch` event is its own
3707
+ * Context (id = event seq — the durable unique key), so every match is a
3708
+ * `start`; `update` is a passthrough (no aggregation — D3's per-Turn
3709
+ * counting is a separate, unselected seam).
3710
+ */
3711
+ const fallbackSwitchDefinition = {
3712
+ kind: "fallbacks-switch",
3713
+ target: "chat",
3714
+ match: (event) => event.type === "fallbacks/switch" && Number.isInteger(event.seq) && isFallbacksSwitchData(event.data) ? {
3715
+ id: String(event.seq),
3716
+ role: "start"
3717
+ } : null,
3718
+ start: (_context, match) => {
3719
+ if (match.event.type !== "fallbacks/switch") throw new Error("fallbacks-switch start requires a fallbacks/switch event");
3720
+ const { seq, time } = match.event;
3721
+ if (!Number.isInteger(seq) || !isFallbacksSwitchData(match.event.data)) return {
3722
+ seq,
3723
+ time
3724
+ };
3725
+ const { turn, step, from, to, role, reason } = match.event.data;
3726
+ return {
3727
+ seq,
3728
+ time,
3729
+ turn,
3730
+ step,
3731
+ from,
3732
+ to,
3733
+ role,
3734
+ reason
3735
+ };
3736
+ },
3737
+ update: (context) => context.state,
3738
+ buildViewNode: (context) => {
3739
+ if (context.start === void 0 || context.state === void 0) return null;
3740
+ return {
3741
+ key: context.key,
3742
+ kind: "fallbacks-switch",
3743
+ id: context.id,
3744
+ target: "chat",
3745
+ anchorSeq: context.start.event.seq,
3746
+ location: context.start.location,
3747
+ visibility: "visible",
3748
+ data: context.state
3749
+ };
3750
+ }
3751
+ };
3752
+ /**
3753
+ * Render one fallback switch as a compact system-style transcript line.
3754
+ *
3755
+ * Geometry follows the upstream chat system rows (the compaction boundary
3756
+ * notice: dim title + separator + ellipsized summary — `chat/MessageItem
3757
+ * .module.css:38-122`); every color resolves through a `--dsw-alias-*`
3758
+ * token. A reason outside the current union renders raw (forward-compatible
3759
+ * durable log, same rule as the card/general row summaries). A malformed or
3760
+ * partial payload (version skew) degrades to the title-only line instead of
3761
+ * throwing during interpolation — the transcript slot stays visible with a
3762
+ * truthful "a switch happened" notice and no summary details.
3763
+ * @param props - composed keyed seat props.
3764
+ * @returns the switch line element tree.
3765
+ */
3766
+ function ConversationFallbackSwitch({ node, t }) {
3767
+ const data = node.data;
3768
+ if (!isFallbacksSwitchData(data)) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3769
+ className: ConversationFallbackSwitch_module_css_default.switchRow,
3770
+ role: "status",
3771
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3772
+ className: ConversationFallbackSwitch_module_css_default.switchTitle,
3773
+ children: t("chat.switch.title")
3774
+ })
3775
+ });
3776
+ const reasonKey = SWITCH_REASON_KEYS[data.reason];
3777
+ const summary = t("chat.switch.summary", {
3778
+ from: `${data.from.provider}/${data.from.model}`,
3779
+ to: `${data.to.provider}/${data.to.model}`,
3780
+ role: data.role,
3781
+ reason: reasonKey === void 0 ? data.reason : t(reasonKey)
3782
+ });
3783
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3784
+ className: ConversationFallbackSwitch_module_css_default.switchRow,
3785
+ role: "status",
3786
+ children: [
3787
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3788
+ className: ConversationFallbackSwitch_module_css_default.switchTitle,
3789
+ children: t("chat.switch.title")
3790
+ }),
3791
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3792
+ className: ConversationFallbackSwitch_module_css_default.switchSep,
3793
+ "aria-hidden": "true"
3794
+ }),
3795
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3796
+ className: ConversationFallbackSwitch_module_css_default.switchSummary,
3797
+ children: summary
3798
+ })
3799
+ ]
3800
+ });
3801
+ }
3802
+ //#endregion
3803
+ //#region src/client/index.ts
3804
+ /**
3805
+ * Required services (cordis fiber inject); registrations wait on the slot
3806
+ * declaration. `conversationEvents` is declared because the D1 Definition
3807
+ * registration reads the service directly (`ctx.conversationEvents.register`
3808
+ * at the bottom of `apply` — explicit fiber-ordering parity with the
3809
+ * ui-workflow-run precedent, whose inject list includes it for the same
3810
+ * direct read). The runtime would still provide the service synchronously
3811
+ * on apply, but the declaration makes the dependency honest. `sessions` is
3812
+ * deliberately NOT injected (S-g): a non-web host without the dsh-session
3813
+ * client service must not hang the fiber waiting for it — the wiring reads
3814
+ * it reflectively and degrades to the switches empty state when absent
3815
+ * (`setCurrentSession` never called, `loadSwitches` ready with an empty
3816
+ * array, which the store already supports).
3817
+ */
3818
+ const inject = [
3819
+ "slots",
3820
+ "locale",
3821
+ "connection",
3822
+ "remote",
3823
+ "conversationEvents"
3824
+ ];
3825
+ /**
3826
+ * Register the `fallbacks` dictionaries and the plugin-config card once the
3827
+ * `settings.plugin.item` declaration is on the ledger.
3828
+ * @param ctx - client root context.
3829
+ */
3830
+ function apply(ctx) {
3831
+ ctx.effect(() => ctx.locale.register(NS, {
3832
+ zh,
3833
+ en
3834
+ }), "llm-fallbacks: dictionaries");
3835
+ const connection = ctx.get("connection");
3836
+ const sessions = ctx.get("sessions");
3837
+ const controller = new FallbacksSettingsController(connection.api, connection.rpc);
3838
+ const useSnapshot = (0, _deepseek_ai_dsh_client_web_react.bindSnapshotSelector)(controller.store);
3839
+ ctx.effect(() => {
3840
+ const syncSession = () => {
3841
+ controller.setCurrentSession(sessions?.list.getSnapshot().current);
3842
+ };
3843
+ if (sessions !== void 0) syncSession();
3844
+ const refresh = (ns) => {
3845
+ if (ns !== void 0 && ns !== "fallbacks") return;
3846
+ refreshFallbacksIfLoaded(controller);
3847
+ refreshSwitchesIfLoaded(controller);
3848
+ };
3849
+ const refreshCatalog = () => {
3850
+ refreshCatalogIfLoaded(controller);
3851
+ };
3852
+ let pendingReset = false;
3853
+ let disposed = false;
3854
+ const refreshAll = () => {
3855
+ if (pendingReset) return;
3856
+ pendingReset = true;
3857
+ queueMicrotask(() => {
3858
+ pendingReset = false;
3859
+ if (disposed) return;
3860
+ refresh();
3861
+ refreshCatalog();
3862
+ });
3863
+ };
3864
+ const disposers = [
3865
+ ctx.remote.$on("settings/document-updated", refresh),
3866
+ ctx.remote.$on("llm/adapters-updated", refreshCatalog),
3867
+ ctx.on("connection/reset", refreshAll),
3868
+ ...sessions === void 0 ? [] : [sessions.list.subscribe(syncSession)]
3869
+ ];
3870
+ return () => {
3871
+ disposed = true;
3872
+ for (const dispose of disposers) dispose();
3873
+ controller.dispose();
3874
+ };
3875
+ }, "llm-fallbacks: pushed invalidations");
3876
+ ctx.slots.inject("settings.plugin.item", function* () {
3877
+ yield ctx.slots.register({
3878
+ name: "settings.plugin.item",
3879
+ id: "fallbacks",
3880
+ order: 30,
3881
+ locale: NS,
3882
+ inject: () => ({
3883
+ controller,
3884
+ useSnapshot
3885
+ })
3886
+ }, FallbacksCard);
3887
+ });
3888
+ ctx.slots.inject("settings.general.item", function* () {
3889
+ yield ctx.slots.register({
3890
+ name: "settings.general.item",
3891
+ id: "fallbacks",
3892
+ order: 100,
3893
+ locale: NS,
3894
+ inject: () => ({
3895
+ controller,
3896
+ useSnapshot
3897
+ })
3898
+ }, GeneralFallbacksRow);
3899
+ });
3900
+ ctx.effect(() => ctx.conversationEvents.register(fallbackSwitchDefinition), "llm-fallbacks: conversation node definition");
3901
+ ctx.slots.inject("conversation.chat.node", function* () {
3902
+ yield ctx.slots.register({
3903
+ name: "conversation.chat.node",
3904
+ key: "fallbacks-switch",
3905
+ locale: NS
3906
+ }, ConversationFallbackSwitch);
3907
+ });
3908
+ }
3909
+ //#endregion
3910
+ exports.FALLBACKS_SETTINGS_NS = FALLBACKS_SETTINGS_NS;
3911
+ exports.FallbacksSettingsController = FallbacksSettingsController;
3912
+ exports.apply = apply;
3913
+ exports.inject = inject;
3914
+ return module.exports;
3915
+ }
3916
+ });