dsh-llm-fallbacks 0.6.13 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -146,26 +146,9 @@ window.__ModuleLoader__.load({
146
146
  return value;
147
147
  };
148
148
  }));
149
- //#endregion
150
- //#region src/client/use-snapshot.ts
151
- var import_with_selector = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
149
+ (/* @__PURE__ */ __commonJSMin(((exports, module) => {
152
150
  module.exports = require_with_selector_production();
153
151
  })))();
154
- /**
155
- * Bind a bare observable source to a typed uSES selector hook.
156
- * subscribe/getSnapshot are captured once per source into stable closures
157
- * (also re-binds `this` for method-based sources), so components never
158
- * resubscribe across renders. Equality defaults to Object.is.
159
- * @param w - snapshot source (engine store, Session object, store instance).
160
- * @returns the selector hook.
161
- */
162
- function bindSnapshotSelector(w) {
163
- const subscribe = (fn) => w.subscribe(fn);
164
- const getSnapshot = () => w.getSnapshot();
165
- return function useSelector(sel, eq) {
166
- return (0, import_with_selector.useSyncExternalStoreWithSelector)(subscribe, getSnapshot, void 0, sel, eq);
167
- };
168
- }
169
152
  //#endregion
170
153
  //#region src/selectors.ts
171
154
  /** Catchable error for illegal/unknown selectors (config-warning path). */
@@ -423,25 +406,24 @@ window.__ModuleLoader__.load({
423
406
  return isOfficialAllDayId(tail);
424
407
  }
425
408
  /**
426
- * Slot winner + display label (P5): drives 分时切换 detection (per-root-agent
427
- * last-winner marker, in-process only) and the card / `/fallbacks` status
428
- * strip. `winner` is the matching row or `'all-day'`; `label` names the
429
- * slot (frozen preset label or `custom HH:mm-HH:mm`).
409
+ * Slot winner + display label for one named chain: drives 分时切换 detection
410
+ * (per-(agent, chain) last-winner marker, in-process only) and the card /
411
+ * `/fallbacks` status strip. `winner` is the matching row or `'all-day'`;
412
+ * `label` names the slot (frozen preset label or `custom HH:mm-HH:mm`).
430
413
  *
431
- * P6 gate (qc1 F-001): without a conforming all-day
432
- * (`isAllDayConforming(config.rootChain)`) the winner is ALWAYS `'all-day'`
433
- * — a legacy multi-model (or empty) chain earns no slot rows, so every
434
- * surface fed by this resolver reports the inert state and routing stays on
435
- * the raw `rootChain` (the legacy walk verbatim).
414
+ * Gate: without a conforming entries tail (`isAllDayConforming(chain.entries)`)
415
+ * the winner is ALWAYS `'all-day'` — a multi-model (or empty) chain earns no
416
+ * slot rows, so every surface fed by this resolver reports the inert state and
417
+ * routing stays on the raw `entries`.
436
418
  */
437
- function resolveSlotState(config, now, tz) {
438
- if (!isAllDayConforming(config.rootChain)) return {
419
+ function resolveSlotState(chain, now, tz) {
420
+ if (!isAllDayConforming(chain.entries)) return {
439
421
  winner: "all-day",
440
422
  label: "all-day"
441
423
  };
442
424
  const clock = wallClock(now, tz);
443
425
  const seenPresets = /* @__PURE__ */ new Set();
444
- const rows = Array.isArray(config.timeSlots) ? config.timeSlots : [];
426
+ const rows = Array.isArray(chain.timeSlots) ? chain.timeSlots : [];
445
427
  for (const row of rows) {
446
428
  if (typeof row !== "object" || row === null) {
447
429
  warnMalformed(row, "row is not an object");
@@ -468,12 +450,17 @@ window.__ModuleLoader__.load({
468
450
  }
469
451
  //#endregion
470
452
  //#region src/config.ts
453
+ /** Chain id format (v2): case-sensitive, uppercase allowed so the migrated
454
+ * default id is exactly `Auto`. Deliberately NOT {@link ROLE_ID_PATTERN},
455
+ * which keeps governing role ids. */
456
+ const CHAIN_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9-]{0,31}$/;
471
457
  /**
472
- * Spec §4 defaults — `Config({})` must equal this (no-op install).
473
- * There is no `enabled` default (the key is removed — the Plugins-page row
474
- * toggle is the master switch): an unconfigured install (empty rootChain,
475
- * no time slots, empty roles) behaves exactly like an uninstalled plugin
476
- * (AC-3 / no-op invariant, {@link isFallbackActive}).
458
+ * Spec §1 defaults — `Config({})` must equal this (no-op install).
459
+ * Empty `chains` is the ONLY first-run state: nothing pre-creates an
460
+ * `Auto` chain and nothing seeds `entries` — the picker row appears only
461
+ * after the user creates a chain in the card. The migrated/seeded default
462
+ * chain (`{ id: 'Auto', label: '默认链', entries, timeSlots }`) exists only
463
+ * in the migration table (docs) applied by hand — warn-only migration.
477
464
  */
478
465
  const defaultFallbacksConfig = {
479
466
  triggerCodes: [
@@ -481,52 +468,18 @@ window.__ModuleLoader__.load({
481
468
  "QUOTA",
482
469
  "RATE_LIMIT"
483
470
  ],
484
- rootChain: [],
485
- roles: {
486
- list: [],
487
- rules: []
488
- },
471
+ chains: [],
472
+ roles: { list: [] },
489
473
  cooldownMs: 3e5,
490
474
  revertPolicy: "cooldown-expiry",
491
475
  maxSwitchesPerStep: 8,
492
- alwaysModeRetryCap: 5,
493
476
  presets: "bundled",
494
- roleAutoMatch: true,
495
- timeSlots: [],
496
477
  tz: "Asia/Shanghai",
497
478
  recovery: "timer"
498
479
  };
499
- /**
500
- * Reserved role id: legal as a rule target (`roles.rules[].role`) and as
501
- * the no-rule-match fallback, but FORBIDDEN in `roles.list[].id`.
502
- */
503
- const INHERIT_ROLE_ID = "inherit";
504
- /**
505
- * The no-op gate (AC-8), re-keyed from the removed `enabled` switch to
506
- * CONTENT PRESENCE (plan fallbacks-web-ux-alignment T2): the plugin
507
- * intervenes exactly when something is configured — a non-empty `rootChain`
508
- * (the all-day chain), at least one time-slot row, or at least one declared
509
- * role. An inactive config is a pass-through exactly like an uninstalled
510
- * plugin; the Plugins-page row toggle remains the master switch (row
511
- * disable = fiber destroy). ONE helper for every runtime gate — the former
512
- * `config.enabled` sites (request waterfalls, picker-row registration, TUI
513
- * summary, General-row badge) all read through this.
514
- */
515
- function isFallbackActive(config) {
516
- return config.rootChain.length > 0 || (config.timeSlots?.length ?? 0) > 0 || config.roles.list.length > 0;
517
- }
518
480
  /** Role id format (aligned with yet-another-subagent `isValidProfileId`). */
519
481
  const ROLE_ID_PATTERN = /^[a-z0-9-]{1,32}$/;
520
482
  //#endregion
521
- //#region src/route-allowlist.ts
522
- /**
523
- * Whether the route is on the allowlist under exact route identity: both the
524
- * provider and the model strings must equal an allowed route exactly.
525
- */
526
- function routeOnAllowlist(route, allowedModels) {
527
- return allowedModels.some((allowed) => allowed.provider === route.provider && allowed.model === route.model);
528
- }
529
- //#endregion
530
483
  //#region src/client/fallbacks-store.ts
531
484
  /** The plugin's settings namespace on the host wire (settings/document-updated ns filter). */
532
485
  const FALLBACKS_SETTINGS_NS = "fallbacks";
@@ -582,56 +535,6 @@ window.__ModuleLoader__.load({
582
535
  }
583
536
  return entries;
584
537
  }
585
- function parsePolicyRoute(value) {
586
- if (!isRecord(value)) return void 0;
587
- if (typeof value.provider !== "string" || value.provider.length === 0) return void 0;
588
- if (typeof value.model !== "string" || value.model.length === 0) return void 0;
589
- return {
590
- provider: value.provider,
591
- model: value.model
592
- };
593
- }
594
- /**
595
- * Shape-guard the additive `subagentPolicy` wire field (T5). Malformed
596
- * payloads resolve to `undefined` (hide the status area) — the store never
597
- * throws on an old or misshapen field (old-payload tolerance).
598
- */
599
- function parseSubagentPolicyWire(value) {
600
- if (!isRecord(value) || typeof value.state !== "string") return void 0;
601
- if (value.state === "disabled" || value.state === "unprovable") return { state: value.state };
602
- if (value.state !== "enabled") return void 0;
603
- if (!Array.isArray(value.allowedModels)) return void 0;
604
- const allowedModels = [];
605
- for (const entry of value.allowedModels) {
606
- const route = parsePolicyRoute(entry);
607
- if (route === void 0) return void 0;
608
- allowedModels.push(route);
609
- }
610
- const view = {
611
- state: "enabled",
612
- allowedModels
613
- };
614
- if (isRecord(value.head) && (value.head.source === "authorized" || value.head.source === "injected")) {
615
- const route = parsePolicyRoute(value.head.route);
616
- if (route !== void 0) view.head = {
617
- route,
618
- source: value.head.source
619
- };
620
- }
621
- if (isRecord(value.blockedAttempt) && typeof value.blockedAttempt.at === "number") {
622
- const route = parsePolicyRoute(value.blockedAttempt.route);
623
- if (route !== void 0) view.blockedAttempt = {
624
- at: value.blockedAttempt.at,
625
- route,
626
- reason: typeof value.blockedAttempt.reason === "string" ? value.blockedAttempt.reason : ""
627
- };
628
- }
629
- return view;
630
- }
631
- function subagentPolicyFromWire(value, fallback) {
632
- if (!isRecord(value) || !("subagentPolicy" in value)) return fallback;
633
- return parseSubagentPolicyWire(value.subagentPolicy);
634
- }
635
538
  /**
636
539
  * The provider dropdown's offer set (spec §2.5 D-4): catalog providers whose
637
540
  * settings profile resolves in the describe namespaces — the Models page's
@@ -657,8 +560,29 @@ window.__ModuleLoader__.load({
657
560
  if (!isRecord(value)) throw new TypeError(`fallbacks descriptor value is not an object: ${String(value)}`);
658
561
  const triggerCodes = value.triggerCodes;
659
562
  if (triggerCodes !== void 0 && (!Array.isArray(triggerCodes) || triggerCodes.some((code) => typeof code !== "string"))) throw new TypeError("fallbacks descriptor triggerCodes must be a string array");
660
- const rootChain = value.rootChain;
661
- if (rootChain !== void 0 && (!Array.isArray(rootChain) || rootChain.some((entry) => typeof entry !== "string"))) throw new TypeError("fallbacks descriptor rootChain must be a string array");
563
+ const chains = value.chains;
564
+ if (chains !== void 0 && (!Array.isArray(chains) || chains.some((chain) => {
565
+ if (!isRecord(chain)) return true;
566
+ if (typeof chain.id !== "string" || typeof chain.label !== "string") return true;
567
+ const entries = chain.entries;
568
+ if (entries !== void 0 && (!Array.isArray(entries) || entries.some((entry) => typeof entry !== "string"))) return true;
569
+ const slots = chain.timeSlots;
570
+ if (slots !== void 0 && (!Array.isArray(slots) || slots.some((row) => {
571
+ if (!isRecord(row)) return true;
572
+ for (const field of [
573
+ "kind",
574
+ "preset",
575
+ "start",
576
+ "end"
577
+ ]) if (row[field] !== void 0 && typeof row[field] !== "string") return true;
578
+ const rowChain = row.chain;
579
+ if (rowChain !== void 0 && (!Array.isArray(rowChain) || rowChain.some((entry) => typeof entry !== "string"))) return true;
580
+ const days = row.days;
581
+ if (days !== void 0 && (!Array.isArray(days) || days.some((day) => typeof day !== "number"))) return true;
582
+ return false;
583
+ }))) return true;
584
+ return false;
585
+ }))) throw new TypeError("fallbacks descriptor chains must be an array of named chains ({ id, label, entries, timeSlots })");
662
586
  const roles = isRecord(value.roles) ? value.roles : {};
663
587
  const parsedList = (Array.isArray(roles.list) ? roles.list : []).map((role, index) => {
664
588
  if (!isRecord(role) || typeof role.id !== "string") throw new TypeError(`fallbacks descriptor roles.list[${String(index)}] must have a string id`);
@@ -668,81 +592,37 @@ window.__ModuleLoader__.load({
668
592
  if (prompt !== void 0 && typeof prompt !== "string") throw new TypeError(`fallbacks descriptor roles.list[${String(index)}].prompt must be a string`);
669
593
  const permissions = role.permissions;
670
594
  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`);
671
- const chain = role.chain;
672
- 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`);
673
- const fallback = role.fallback;
674
- if (fallback !== void 0 && fallback !== "inherit-root" && fallback !== "none") throw new TypeError(`fallbacks descriptor roles.list[${String(index)}].fallback must be inherit-root|none`);
595
+ const chain = typeof role.chain === "string" ? role.chain : void 0;
675
596
  return {
676
597
  id: role.id,
677
598
  persona: persona ?? "",
678
599
  ...prompt === void 0 ? {} : { prompt },
679
600
  ...permissions === void 0 ? {} : { permissions },
680
- chain: chain ?? [],
681
- fallback: fallback ?? "inherit-root"
682
- };
683
- });
684
- const parsedRules = (Array.isArray(roles.rules) ? roles.rules : []).map((rule, index) => {
685
- if (!isRecord(rule) || typeof rule.role !== "string") throw new TypeError(`fallbacks descriptor roles.rules[${String(index)}] must have a string role`);
686
- const origin = rule.origin;
687
- if (origin !== void 0 && origin !== "root" && origin !== "subagent") throw new TypeError(`fallbacks descriptor roles.rules[${String(index)}].origin must be root|subagent`);
688
- const provider = rule.provider;
689
- const model = rule.model;
690
- if (provider !== void 0 && typeof provider !== "string") throw new TypeError(`fallbacks descriptor roles.rules[${String(index)}].provider must be a string`);
691
- if (model !== void 0 && typeof model !== "string") throw new TypeError(`fallbacks descriptor roles.rules[${String(index)}].model must be a string`);
692
- return {
693
- ...origin === void 0 ? {} : { origin },
694
- ...provider === void 0 ? {} : { provider },
695
- ...model === void 0 ? {} : { model },
696
- role: rule.role
601
+ ...chain === void 0 || chain === "" ? {} : { chain }
697
602
  };
698
603
  });
699
604
  const cooldownMs = value.cooldownMs;
700
605
  const maxSwitchesPerStep = value.maxSwitchesPerStep;
701
- const alwaysModeRetryCap = value.alwaysModeRetryCap;
702
- for (const [field, raw] of [
703
- ["cooldownMs", cooldownMs],
704
- ["maxSwitchesPerStep", maxSwitchesPerStep],
705
- ["alwaysModeRetryCap", alwaysModeRetryCap]
706
- ]) if (raw !== void 0 && typeof raw !== "number") throw new TypeError(`fallbacks descriptor ${field} must be a number`);
606
+ for (const [field, raw] of [["cooldownMs", cooldownMs], ["maxSwitchesPerStep", maxSwitchesPerStep]]) if (raw !== void 0 && typeof raw !== "number") throw new TypeError(`fallbacks descriptor ${field} must be a number`);
707
607
  const revertPolicy = value.revertPolicy;
708
608
  if (revertPolicy !== void 0 && revertPolicy !== "cooldown-expiry" && revertPolicy !== "never") throw new TypeError("fallbacks descriptor revertPolicy must be cooldown-expiry|never");
709
609
  const presets = value.presets;
710
610
  if (presets !== void 0 && presets !== "bundled" && presets !== "none") throw new TypeError("fallbacks descriptor presets must be bundled|none");
711
- const roleAutoMatch = value.roleAutoMatch;
712
- if (roleAutoMatch !== void 0 && typeof roleAutoMatch !== "boolean") throw new TypeError("fallbacks descriptor roleAutoMatch must be a boolean");
713
- const timeSlots = value.timeSlots;
714
- if (timeSlots !== void 0 && (!Array.isArray(timeSlots) || timeSlots.some((row) => {
715
- if (!isRecord(row)) return true;
716
- for (const field of [
717
- "kind",
718
- "preset",
719
- "start",
720
- "end"
721
- ]) if (row[field] !== void 0 && typeof row[field] !== "string") return true;
722
- const chain = row.chain;
723
- if (chain !== void 0 && (!Array.isArray(chain) || chain.some((entry) => typeof entry !== "string"))) return true;
724
- const days = row.days;
725
- if (days !== void 0 && (!Array.isArray(days) || days.some((day) => typeof day !== "number"))) return true;
726
- return false;
727
- }))) throw new TypeError("fallbacks descriptor timeSlots must be an array of slot rows (kind/preset/start/end strings, chain string array, days number array)");
728
611
  const tz = value.tz;
729
612
  if (tz !== void 0 && typeof tz !== "string") throw new TypeError("fallbacks descriptor tz must be a string");
730
613
  const recovery = value.recovery;
731
614
  if (recovery !== void 0 && recovery !== "timer" && recovery !== "half-open") throw new TypeError("fallbacks descriptor recovery must be timer|half-open");
732
615
  return {
733
616
  triggerCodes: triggerCodes ?? [...defaultFallbacksConfig.triggerCodes],
734
- rootChain: rootChain ?? [...defaultFallbacksConfig.rootChain],
735
- roles: {
736
- list: parsedList,
737
- rules: parsedRules
738
- },
617
+ chains: chains ?? defaultFallbacksConfig.chains.map((chain) => ({
618
+ ...chain,
619
+ timeSlots: [...chain.timeSlots ?? []]
620
+ })),
621
+ roles: { list: parsedList },
739
622
  cooldownMs: cooldownMs ?? defaultFallbacksConfig.cooldownMs,
740
623
  revertPolicy: revertPolicy ?? defaultFallbacksConfig.revertPolicy,
741
624
  maxSwitchesPerStep: maxSwitchesPerStep ?? defaultFallbacksConfig.maxSwitchesPerStep,
742
- alwaysModeRetryCap: alwaysModeRetryCap ?? defaultFallbacksConfig.alwaysModeRetryCap,
743
625
  presets: presets ?? defaultFallbacksConfig.presets,
744
- roleAutoMatch: roleAutoMatch ?? defaultFallbacksConfig.roleAutoMatch,
745
- timeSlots: timeSlots ?? [...defaultFallbacksConfig.timeSlots ?? []],
746
626
  tz: tz ?? defaultFallbacksConfig.tz,
747
627
  recovery: recovery ?? defaultFallbacksConfig.recovery
748
628
  };
@@ -782,27 +662,6 @@ window.__ModuleLoader__.load({
782
662
  raw
783
663
  };
784
664
  }
785
- /**
786
- * Extract the most recent `fallbacks/switch` events from one history page
787
- * (spec §2.5 D-5): drop packed chunk runs, filter by event type, order by
788
- * `seq` descending, take at most `limit`. Single-page read — fewer than
789
- * `limit` events show as-is; no multi-page backfill (Non-Goal).
790
- */
791
- function extractRecentSwitches(entries, limit = 5) {
792
- const switches = [];
793
- for (const entry of entries) {
794
- if (entry.type !== "event") continue;
795
- const event = entry.event;
796
- if (event.type !== "fallbacks/switch") continue;
797
- switches.push({
798
- ...event.data,
799
- seq: event.seq,
800
- time: event.time
801
- });
802
- }
803
- switches.sort((a, b) => b.seq - a.seq);
804
- return switches.slice(0, limit);
805
- }
806
665
  /** Serialize one selector row to its wire string (`provider/model` | `provider/*`). */
807
666
  function selectorRowToRaw(row) {
808
667
  const provider = selectionToRaw(row.provider);
@@ -831,22 +690,6 @@ window.__ModuleLoader__.load({
831
690
  };
832
691
  }
833
692
  }
834
- /** Project the rootChain entries into editable rows (one flat chain row). */
835
- function rootChainToRows(rootChain, catalog) {
836
- return [{ selectors: rootChain.map((entry) => entryToSelectorRow(entry, catalog)) }];
837
- }
838
- /** Rebuild the rootChain from edited rows; rows with no usable selector drop out. */
839
- function rowsToRootChain(rows) {
840
- const entries = [];
841
- for (const row of rows) {
842
- if (row.selectors.length === 0) continue;
843
- for (const selector of row.selectors) {
844
- const raw = selectorRowToRaw(selector);
845
- if (raw !== "") entries.push(raw);
846
- }
847
- }
848
- return entries;
849
- }
850
693
  /** Project the time-slot rows into editable rows (chain selectors classified). */
851
694
  function timeSlotsToRows(timeSlots, catalog) {
852
695
  return timeSlots.map((row) => ({
@@ -862,11 +705,10 @@ window.__ModuleLoader__.load({
862
705
  }
863
706
  /** Rebuild the time-slot rows from edited rows; blank selectors drop out.
864
707
  * `kind` rides verbatim (a hand-written unknown kind reads back unchanged;
865
- * save validation rejects it) — the cast asserts the trusted editor shape.
866
- * `days` is ALWAYS serialized ([] included): schemastery composes absent
867
- * array fields as `[]`, so the composed config every card load accepts
868
- * carries `days` on every row — the draft must too, or a clean card would
869
- * read back dirty. */
708
+ * save validation rejects it). `days` is ALWAYS serialized ([] included):
709
+ * schemastery composes absent array fields as `[]`, so the composed config
710
+ * every card load accepts carries `days` on every row — the draft must too,
711
+ * or a clean card would read back dirty. */
870
712
  function rowsToTimeSlots(rows) {
871
713
  return rows.map((row) => {
872
714
  const chain = row.selectors.map(selectorRowToRaw).filter((entry) => entry !== "");
@@ -887,35 +729,63 @@ window.__ModuleLoader__.load({
887
729
  };
888
730
  });
889
731
  }
890
- /** Project the declared roles into editable rows (chain selectors classified). */
891
- function rolesToRows(roles, catalog) {
732
+ /** Project the named chains into editable drafts (selectors classified). */
733
+ function chainsToDrafts(chains, catalog) {
734
+ return chains.map((chain) => ({
735
+ id: chain.id,
736
+ label: chain.label,
737
+ selectors: chain.entries.map((entry) => entryToSelectorRow(entry, catalog)),
738
+ slots: timeSlotsToRows(chain.timeSlots ?? [], catalog)
739
+ }));
740
+ }
741
+ /**
742
+ * Generate a fresh unique chain id for the card's `+` affordance. Pass the
743
+ * DRAFT list, not the accepted config: two unsaved additions must not both
744
+ * claim `chain-N` (duplicate React keys, and a section the gateway rejects).
745
+ */
746
+ function newChainId(existing) {
747
+ let index = existing.length + 1;
748
+ let id = `chain-${index}`;
749
+ while (existing.some((chain) => chain.id === id)) {
750
+ index += 1;
751
+ id = `chain-${index}`;
752
+ }
753
+ return id;
754
+ }
755
+ /** Rebuild one named chain from its draft; blank selectors drop out. */
756
+ function draftToChain(draft) {
757
+ return {
758
+ id: draft.id,
759
+ label: draft.label,
760
+ entries: draft.selectors.map(selectorRowToRaw).filter((entry) => entry !== ""),
761
+ timeSlots: rowsToTimeSlots(draft.slots)
762
+ };
763
+ }
764
+ /** Project the declared roles into editable rows (chain pointer kept verbatim). */
765
+ function rolesToRows(roles) {
892
766
  return roles.map((role) => ({
893
767
  id: role.id,
894
768
  persona: role.persona,
895
- selectors: (role.chain ?? []).map((entry) => entryToSelectorRow(entry, catalog)),
896
- fallback: role.fallback ?? "inherit-root",
897
- collapsed: true,
769
+ chain: role.chain ?? "",
770
+ collapsed: false,
898
771
  personaOpen: false
899
772
  }));
900
773
  }
901
- /** Rebuild the declared roles from edited rows; empty selectors drop out. */
774
+ /** Rebuild the declared roles from edited rows; an empty pointer drops out. */
902
775
  function rowsToRoles(rows) {
903
776
  return rows.map((row) => ({
904
777
  id: row.id.trim(),
905
778
  persona: row.persona,
906
- chain: row.selectors.map(selectorRowToRaw).filter((entry) => entry !== ""),
907
- fallback: row.fallback
779
+ ...row.chain === "" ? {} : { chain: row.chain }
908
780
  }));
909
781
  }
910
782
  /**
911
783
  * Rebuild the declared roles from edited rows, re-attaching the
912
784
  * schema-reserved `prompt`/`permissions` fields from the last accepted
913
785
  * config by role id — they never round-trip through rows this round, so
914
- * without the merge a save would silently drop them (T2 reviewer minor
915
- * #2). The id trim matches {@link rowsToRoles}; a row whose id matches no
916
- * original role (a freshly added one) keeps no extras. Key order mirrors
917
- * `parseFallbacksConfig` so a clean draft's JSON dirty comparison never
918
- * flags it.
786
+ * without the merge a save would silently drop them. The id trim matches
787
+ * {@link rowsToRoles}; a row whose id matches no original role (a freshly
788
+ * added one) keeps no extras.
919
789
  */
920
790
  function mergeRoleExtras(rows, originalRoles) {
921
791
  const originalById = new Map(originalRoles.map((role) => [role.id, role]));
@@ -927,36 +797,10 @@ window.__ModuleLoader__.load({
927
797
  persona: role.persona,
928
798
  ...original.prompt === void 0 ? {} : { prompt: original.prompt },
929
799
  ...original.permissions === void 0 ? {} : { permissions: original.permissions },
930
- chain: role.chain,
931
- fallback: role.fallback
800
+ ...role.chain === void 0 ? {} : { chain: role.chain }
932
801
  };
933
802
  });
934
803
  }
935
- /**
936
- * The `roles.rules` role dropdown's offer set — the ONLY data source for the
937
- * rule rows' role selector: the built-in `'inherit'` target plus every
938
- * declared `roles.list` id, in declaration order (a role added/removed on
939
- * the same page is reflected immediately).
940
- */
941
- function ruleRoleOptions(roles) {
942
- return [INHERIT_ROLE_ID, ...new Set(roles.list.map((role) => role.id.trim()))];
943
- }
944
- /** Project the role rules into editable rows (provider/model classified). */
945
- function rulesToRows(rules, catalog) {
946
- return rules.map((rule) => ({
947
- provider: classifyProvider(rule.provider ?? "", catalog),
948
- model: classifyModel(rule.provider ?? "", rule.model ?? "", catalog),
949
- role: rule.role
950
- }));
951
- }
952
- /** Rebuild the role rules from edited rows; empty provider/model drop out. */
953
- function rowsToRules(rows) {
954
- return rows.map((row) => ({
955
- ...row.provider === null ? {} : { provider: selectionToRaw(row.provider) },
956
- ...row.model === null ? {} : { model: selectionToRaw(row.model) },
957
- role: row.role.trim()
958
- })).filter((rule) => rule.role !== "");
959
- }
960
804
  /** Controller joining Settings reads, writes, and pushed invalidations. */
961
805
  var FallbacksSettingsController = class {
962
806
  api;
@@ -976,10 +820,7 @@ window.__ModuleLoader__.load({
976
820
  configuredProviders: [],
977
821
  groups: [],
978
822
  catalogEpoch: 0,
979
- switchesStatus: "idle",
980
- switchesError: null,
981
- switches: [],
982
- subagentPolicy: void 0
823
+ currentSession: void 0
983
824
  });
984
825
  /** Read guard: a newer load() supersedes an older one's publish. */
985
826
  readGeneration = 0;
@@ -990,22 +831,12 @@ window.__ModuleLoader__.load({
990
831
  */
991
832
  writeGeneration = 0;
992
833
  catalogGeneration = 0;
993
- switchesGeneration = 0;
994
834
  /** Every settings namespace from the last describe, keyed by ns — the configured-provider join's other input. */
995
835
  namespaces = /* @__PURE__ */ new Map();
996
- currentSession;
997
- /**
998
- * The store's dispose signal (qc3 F-003): created alongside the store
999
- * lifecycle, threaded into every `session.follow` call, aborted once in
1000
- * {@link dispose} — a follow opening still mid-await at plugin unload
1001
- * (HMR / fiber dispose, or a wedged gateway stream) is cancelled promptly
1002
- * instead of staying open until the mount abort or socket failure.
1003
- */
1004
- followAbort = new AbortController();
1005
836
  /**
1006
837
  * @param api - the Remote namespace faces the store reads (settings
1007
838
  * describe `writable` + namespace directory, llm provider directory,
1008
- * session model catalog + follow tail page) — `ctx.remote` cast once at
839
+ * session model catalog) — `ctx.remote` cast once at
1009
840
  * the mount boundary.
1010
841
  * @param rpc - the connection's generic RPC caller for the host gateway
1011
842
  * channel (`/api`), injected from the connection handle.
@@ -1053,8 +884,7 @@ window.__ModuleLoader__.load({
1053
884
  }
1054
885
  if ("seeds" in getResult.value) seeds = parseSeedsWire(getResult.value.seeds);
1055
886
  }
1056
- const subagentPolicy = getResult !== void 0 && getResult.ok ? subagentPolicyFromWire(getResult.value, void 0) : void 0;
1057
- this.accept(config, writable, legacyKeys, seeds, subagentPolicy);
887
+ this.accept(config, writable, legacyKeys, seeds);
1058
888
  } catch (error) {
1059
889
  if (generation !== this.readGeneration) return;
1060
890
  if (writeGenerationAtStart !== this.writeGeneration) return;
@@ -1103,75 +933,13 @@ window.__ModuleLoader__.load({
1103
933
  }
1104
934
  }
1105
935
  /**
1106
- * Record the current session the status block reads (spec §2.5 D-5). Once
1107
- * the block has been read once, its summary follows session switches
1108
- * immediately; an idle block only records the id — the section's mount
1109
- * effect performs the first read.
1110
- * @param sessionId - the session whose history is summarized; undefined
1111
- * (no current session) resolves to the empty state.
936
+ * Record the current session the conversation switch strip follows
937
+ * (switch events ride the session stream — no client history read).
1112
938
  */
1113
939
  setCurrentSession(sessionId) {
1114
- if (sessionId === this.currentSession) return;
1115
- this.currentSession = sessionId;
1116
- if (this.store.getSnapshot().switchesStatus !== "idle") this.loadSwitches();
1117
- }
1118
- /**
1119
- * Read the recent-switch summary for the current session (spec §2.5 D-5):
1120
- * one tail page (`maxMessages` = {@link SWITCHES_HISTORY_PAGE}) read from
1121
- * the `session/follow` opening snapshot — the host has no unary tail-page
1122
- * read (`session/page` needs the follow opening frame's cursor), so the
1123
- * stream is cancelled right after its first frame —
1124
- * `fallbacks/switch` events extracted newest-first capped at
1125
- * {@link RECENT_SWITCH_LIMIT}. No current session → honest empty ready
1126
- * state (no RPC); a read failure lands `switchesStatus: 'error'` and never
1127
- * touches the settings state (the form keeps editing/saving normally).
1128
- * @returns nothing; {@link store} carries success or failure.
1129
- */
1130
- async loadSwitches() {
1131
- const generation = ++this.switchesGeneration;
1132
- const sessionId = this.currentSession;
1133
- if (sessionId === void 0) {
1134
- this.store.update((state) => {
1135
- state.switchesStatus = "ready";
1136
- state.switchesError = null;
1137
- state.switches = [];
1138
- });
1139
- return;
1140
- }
1141
940
  this.store.update((state) => {
1142
- state.switchesStatus = "loading";
1143
- state.switchesError = null;
941
+ state.currentSession = sessionId;
1144
942
  });
1145
- try {
1146
- const iterator = this.api.session.follow({
1147
- address: {
1148
- kind: "session",
1149
- sessionId
1150
- },
1151
- maxMessages: 50
1152
- }, this.followAbort.signal)[Symbol.asyncIterator]();
1153
- let first;
1154
- try {
1155
- first = await iterator.next();
1156
- } finally {
1157
- await iterator.return?.();
1158
- }
1159
- if (generation !== this.switchesGeneration) return;
1160
- if (first.done || first.value.type !== "snapshot") throw new Error("session follow closed before its opening snapshot");
1161
- const switches = extractRecentSwitches(first.value.records);
1162
- this.store.update((state) => {
1163
- state.switchesStatus = "ready";
1164
- state.switchesError = null;
1165
- state.switches = switches;
1166
- });
1167
- } catch (error) {
1168
- if (generation !== this.switchesGeneration) return;
1169
- const wire = error;
1170
- this.store.update((state) => {
1171
- state.switchesStatus = "error";
1172
- state.switchesError = typeof wire?.message === "string" ? wire.message : messageOf(error);
1173
- });
1174
- }
1175
943
  }
1176
944
  /**
1177
945
  * Persist the full edited configuration through the gateway channel
@@ -1203,30 +971,6 @@ window.__ModuleLoader__.load({
1203
971
  }
1204
972
  }
1205
973
  /**
1206
- * Explicit "sync to host allowlist" write (plan
1207
- * role-chain-allowlist-visibility T2): the gateway endpoint merges the
1208
- * CURRENT allowlist with this plugin's exact chain routes plus the virtual
1209
- * `FallbacksChain/Auto` route and writes only `allowedModels`. The button
1210
- * click is the ONLY trigger — the store never syncs on its own.
1211
- * @returns true when the write landed (the card then reads the fresh
1212
- * snapshot from the store); false on a refused/failed call, leaving the
1213
- * last accepted state untouched. A response discarded by the write
1214
- * generation guard is still a landed write — dispose() is the only
1215
- * invalidator, so it must not report failure.
1216
- */
1217
- async syncAllowlist() {
1218
- if (!this.store.getSnapshot().writable) return false;
1219
- const generation = ++this.writeGeneration;
1220
- try {
1221
- const result = await this.rpc.call("/api", "fallbacks/sync-allowlist", { args: {} });
1222
- if (!result.ok) return false;
1223
- if (generation === this.writeGeneration) this.acceptWire(result.value);
1224
- return true;
1225
- } catch {
1226
- return false;
1227
- }
1228
- }
1229
- /**
1230
974
  * Reset to composition defaults through the gateway channel
1231
975
  * (`/api/fallbacks/reset` — the fallbacks-specific third method; the host
1232
976
  * clears the user layer via `settings.replace(ns, {})`, the removal path a
@@ -1255,15 +999,13 @@ window.__ModuleLoader__.load({
1255
999
  this.readGeneration += 1;
1256
1000
  this.writeGeneration += 1;
1257
1001
  this.catalogGeneration += 1;
1258
- this.switchesGeneration += 1;
1259
1002
  this.namespaces = /* @__PURE__ */ new Map();
1260
- this.followAbort.abort();
1261
1003
  }
1262
1004
  /**
1263
- * Unpack one write response (set / reset / sync-allowlist) into the store
1264
- * snapshot. The additive fields (legacyKeys §9, seeds §9.4, subagentPolicy
1265
- * D4) follow the W-1/F-1 rule: absent on the wire keeps the last accepted
1266
- * value — only a `get` may settle server truth.
1005
+ * Unpack one write response (set / reset) into the store snapshot. The
1006
+ * additive fields (legacyKeys §9, seeds §9.4) follow the W-1/F-1 rule:
1007
+ * absent on the wire keeps the last accepted value — only a `get` may
1008
+ * settle server truth.
1267
1009
  */
1268
1010
  acceptWire(value) {
1269
1011
  const config = value !== null && typeof value === "object" && "config" in value ? value.config : void 0;
@@ -1277,8 +1019,7 @@ window.__ModuleLoader__.load({
1277
1019
  const wireSeeds = value.seeds;
1278
1020
  if (Array.isArray(wireSeeds)) seeds = parseSeedsWire(wireSeeds);
1279
1021
  }
1280
- const subagentPolicy = subagentPolicyFromWire(value, this.store.getSnapshot().subagentPolicy);
1281
- this.accept(config, true, legacyKeys, seeds, subagentPolicy);
1022
+ this.accept(config, true, legacyKeys, seeds);
1282
1023
  }
1283
1024
  /**
1284
1025
  * Publish a settled load: `status` ready, `writable` from describe, and —
@@ -1296,7 +1037,7 @@ window.__ModuleLoader__.load({
1296
1037
  * only when a real config resolved — a transient channel-down keeps the
1297
1038
  * last accepted badge state.
1298
1039
  */
1299
- accept(config, writable, legacyKeys, seeds, subagentPolicy) {
1040
+ accept(config, writable, legacyKeys, seeds) {
1300
1041
  const parsed = config === void 0 ? void 0 : parseFallbacksConfig(config);
1301
1042
  this.store.update((state) => {
1302
1043
  state.status = "ready";
@@ -1305,7 +1046,6 @@ window.__ModuleLoader__.load({
1305
1046
  state.present = parsed !== void 0;
1306
1047
  state.legacyKeys = parsed === void 0 ? state.legacyKeys : legacyKeys;
1307
1048
  state.seeds = parsed === void 0 ? state.seeds : seeds;
1308
- state.subagentPolicy = parsed === void 0 ? state.subagentPolicy : subagentPolicy;
1309
1049
  if (parsed !== void 0) state.config = parsed;
1310
1050
  state.configuredProviders = configuredProvidersOf(state.providers, this.namespaces);
1311
1051
  });
@@ -1336,17 +1076,6 @@ window.__ModuleLoader__.load({
1336
1076
  if (controller.store.getSnapshot().catalogStatus === "idle") return;
1337
1077
  controller.loadCatalog();
1338
1078
  }
1339
- /**
1340
- * Refetch the recent-switch summary after `settings/document-updated`
1341
- * (fallbacks ns) / `connection/reset` only when the status block has already
1342
- * been read once
1343
- * (the switches twin of {@link refreshFallbacksIfLoaded}).
1344
- * @param controller - the fallbacks settings controller.
1345
- */
1346
- function refreshSwitchesIfLoaded(controller) {
1347
- if (controller.store.getSnapshot().switchesStatus === "idle") return;
1348
- controller.loadSwitches();
1349
- }
1350
1079
  //#endregion
1351
1080
  //#region src/client/locales.ts
1352
1081
  /**
@@ -1378,18 +1107,13 @@ window.__ModuleLoader__.load({
1378
1107
  "cooldownMs.label": "冷却时长(毫秒)",
1379
1108
  "cooldownMs.hint": "冷却期内模型不再入选",
1380
1109
  "cooldownMs.tooltip": "被切离/失败的模型在冷却期内不再入选。",
1381
- "maxSwitchesPerStep.label": "单步最大切换次数",
1110
+ "maxSwitchesPerStep.label": "单次请求行走最大切换",
1382
1111
  "maxSwitchesPerStep.hint": "超过后停止切换",
1383
- "maxSwitchesPerStep.tooltip": "超过后停止切换,以原始错误语义结束当前步,防止链循环放大延迟。",
1384
- "alwaysModeRetryCap.label": "always 模式重试上限",
1385
- "alwaysModeRetryCap.hint": "达到上限次数后切换;0 表示禁用",
1386
- "alwaysModeRetryCap.tooltip": "retryPolicy 为 always 的模型在同一请求内重试达到该次数后切换;0 表示禁用。",
1112
+ "maxSwitchesPerStep.tooltip": "超过后不再切换,以原始错误语义结束本次请求,防止链循环放大延迟。",
1113
+ "virtualOnly.rule": "只有模型选择为 FallbacksChain/<链 id> 的会话(含继承该路由的子代理)会执行降级链;选择真实模型的会话没有插件降级,仅由 llm-retry 处理重试。",
1387
1114
  "advanced.label": "高级选项",
1388
1115
  "advanced.expand": "展开高级选项",
1389
1116
  "advanced.collapse": "收起高级选项",
1390
- "roleAutoMatch.label": "启用角色自动匹配",
1391
- "roleAutoMatch.hint": "规则未命中时,由模型自选最贴近的已声明角色",
1392
- "roleAutoMatch.tooltip": "规则未命中时,模型会自动从已声明角色(id + persona)中选择最匹配者并注入该角色的链;关闭后未命中规则时按现状回落(inherit / rootChain)。",
1393
1117
  "rootChain.label": "默认降级链",
1394
1118
  "rootChain.tooltip": "未命中任何分时槽时先走这条降级链;全部失败后落到下面的默认模型。",
1395
1119
  "defaultModel.label": "默认模型",
@@ -1401,21 +1125,26 @@ window.__ModuleLoader__.load({
1401
1125
  "chains.selector.modelPlaceholder": "选择 model",
1402
1126
  "chains.selector.wildcardLegacy": "该条目为通配(provider/*):选择具体模型后将转为精确条目",
1403
1127
  "chains.selector.noModelsStrict": "该 provider 暂无可用模型(目录查询失败),请改选其他 provider。",
1404
- "mainAgent.label": "主代理",
1405
1128
  "subagents.label": "子代理",
1406
- "subagents.policy.label": "宿主子代理模型策略",
1407
- "subagents.policy.allowlist": "允许的模型",
1408
- "subagents.policy.head": "当前链头",
1409
- "subagents.policy.source.authorized": "宿主授权",
1410
- "subagents.policy.source.injected": "插件注入",
1411
- "subagents.policy.blocked": "降级候选均不在允许列表中 — 未切换",
1412
- "subagents.policy.unprovable": "策略存在但无法读取 — 插件切换已禁用",
1413
- "subagents.policy.offAllowlist": "角色链中有 {count} 个精确条目不在允许列表 — 这些条目不会生效",
1414
- "subagents.policy.recommend": "建议将 FallbacksChain/Auto 加入允许列表(该虚拟路由不受宿主策略过滤)",
1415
- "subagents.policy.sync": "同步到宿主允许列表",
1416
- "subagents.policy.sync.done": "已同步:精确条目(角色链、默认降级链与 FallbacksChain/Auto)已写入允许列表;通配条目不展开、不同步",
1417
- "subagents.policy.sync.failed": "同步失败:宿主设置未写入(策略未启用、条目不可配置或版本冲突等宿主拒绝),请检查宿主设置",
1418
- "subagents.policy.sync.dirty": "角色链或默认降级链有未保存的修改 — 请先保存再同步(同步只写入已保存的配置)",
1129
+ "virtualChain.label": "虚拟链",
1130
+ "virtualChain.hint": "每条链是一张独立的虚拟路由,可各自配置分时、默认降级链与链尾。",
1131
+ "chains.tab.add": "新建链",
1132
+ "chains.tab.menu": "管理当前链:重命名 / 删除",
1133
+ "chains.name.label": "链名称",
1134
+ "chains.name.hint": "仅用于展示(tab 标题与模型选择器前缀);id「{id}」是模型选择器里的路由,创建后不可改。",
1135
+ "chains.name.tooltip": "给这条虚拟链起个可读的名字;实际路由是 FallbacksChain/<链 id>,改名不影响已选该路由的会话。",
1136
+ "chains.tab.renameLabel": "链名(仅展示名,id「{id}」不变)",
1137
+ "chains.tab.renameDone": "完成",
1138
+ "chains.tab.delete": "删除该链",
1139
+ "chains.tab.deleteDisabled": "删除该链(至少保留一条链)",
1140
+ "chains.tab.dirty": "(未保存修改)",
1141
+ "chains.tab.error": "(校验错误)",
1142
+ "chains.empty.title": "尚未配置任何降级链",
1143
+ "chains.empty.body": "未配置链时,模型选择器中不会出现任何 FallbacksChain/* 虚拟路由;已选 FallbacksChain 路由的会话将没有插件降级,仅由宿主 llm-retry 处理重试。",
1144
+ "chains.empty.create": "+ 新建第一条链",
1145
+ "chains.chainFoot.hint": "仅保存当前链:选择器 · 链尾 · 分时槽 · 链名",
1146
+ "chains.chainFoot.discard": "放弃修改",
1147
+ "chains.chainFoot.save": "保存",
1419
1148
  "timeSlots.label": "分时槽设置",
1420
1149
  "timeSlots.hint": "自上而下第一条命中生效;全时段行固定最后",
1421
1150
  "timeSlots.tz.label": "时区(UTC±)",
@@ -1423,7 +1152,7 @@ window.__ModuleLoader__.load({
1423
1152
  "timeSlots.name": "名称",
1424
1153
  "timeSlots.expand": "展开该行",
1425
1154
  "timeSlots.collapse": "收起该行",
1426
- "timeSlots.tooltip": "命中行的模型链成为 root 生效链(取代全时段链);未命中任何行时使用全时段链。分时切换是路由种子而非失败决策:不消耗冷却、不计入单步切换上限。",
1155
+ "timeSlots.tooltip": "命中行的模型链成为 root 生效链(取代全时段链);未命中任何行时使用全时段链。分时切换是路由种子而非失败决策:不消耗冷却、不计入单次请求行走的切换上限。",
1427
1156
  "timeSlots.addPreset": "添加预设",
1428
1157
  "timeSlots.addCustom": "添加自定义时段",
1429
1158
  "timeSlots.presetPlaceholder": "选择预设",
@@ -1463,8 +1192,6 @@ window.__ModuleLoader__.load({
1463
1192
  "allDay.pro": "官方 Pro(deepseek-official/deepseek-v4-pro)",
1464
1193
  "allDay.nonconforming": "当前默认模型不合法:请选择官方 Flash 或 Pro 后保存",
1465
1194
  "roles.list.label": "角色实体",
1466
- "roles.list.hint": "先声明角色,规则才能引用",
1467
- "roles.list.tooltip": "角色 id 须匹配 /^[a-z0-9-]{1,32}$/ 且唯一;\"inherit\" 为保留字,不能用作角色 id。",
1468
1195
  "roles.id": "id",
1469
1196
  "roles.id.hint": "小写字母/数字/连字符,1–32 字符",
1470
1197
  "roles.idPlaceholder": "例如 reviewer",
@@ -1476,35 +1203,24 @@ window.__ModuleLoader__.load({
1476
1203
  "roles.seedSource.bundled": "内置",
1477
1204
  "roles.seedSource.external": "外部",
1478
1205
  "roles.seedSource.user": "用户",
1479
- "roles.seedChainOptional": "角色 \"{id}\" 为 seed 角色:链可留空,保存不会被拦截",
1480
- "roles.fallback": "链拼接策略",
1481
- "roles.fallback.inherit-root": "继承 root(角色链后追加 rootChain)",
1482
- "roles.fallback.none": "仅角色链(不追加 rootChain)",
1206
+ "roles.list.hint": "角色是该角色的身份与人格;可在「指定降级链」中手动指定一条命名虚拟链",
1207
+ "roles.chain.label": "指定降级链",
1208
+ "roles.chain.unspecified": "未指定(沿用 dsh 默认)",
1209
+ "roles.chain.hint": "手动指定该角色使用的虚拟链;未指定时沿用 dsh 默认:继承委派它的代理所选链。",
1210
+ "roles.chain.tooltip": "手动指定该角色使用的命名虚拟链:派发到该角色的子代理会挂到 FallbacksChain/<链 id>,由那条链承接降级。未指定则插件不干预。",
1211
+ "roles.chainMeta.unspecified": "未指定降级链",
1483
1212
  "roles.add": "添加角色",
1484
1213
  "roles.remove": "删除该角色",
1485
1214
  "roles.expand": "展开该角色",
1486
1215
  "roles.collapse": "收起该角色",
1487
- "roles.selector.add": "添加选择器",
1488
- "roles.rules": "角色规则",
1489
- "roles.rules.hint": "仅对子代理生效:顺序匹配 provider/model,未命中 → inherit(root 链)",
1490
- "roles.rules.tooltip": "规则仅对子代理生效(root 请求不匹配规则):命中后走对应角色的链;未命中走内置 inherit(rootChain)。",
1491
- "roles.rule.provider": "provider",
1492
- "roles.rule.provider.any": "任意",
1493
- "roles.rule.model": "model",
1494
- "roles.rule.model.any": "任意",
1495
- "roles.rule.role": "角色",
1496
- "roles.rule.role.inherit": "inherit(内置:root 链)",
1497
- "roles.rule.roleSelectPlaceholder": "选择角色",
1498
- "roles.rule.roleUndeclared.short": "(未声明)",
1499
- "roles.addRule": "添加规则",
1500
- "roles.removeRule": "删除该规则",
1501
1216
  "validation.blocked": "配置校验未通过,保存被拦截:",
1502
1217
  "validation.roleIdFormat": "角色 id \"{id}\" 不符合格式 /^[a-z0-9-]{1,32}$/",
1503
1218
  "validation.roleIdReserved": "\"inherit\" 为保留角色 id,不能用于角色实体",
1504
1219
  "validation.roleIdDuplicate": "角色 id \"{id}\" 重复",
1505
- "validation.ruleRoleUndeclared": "规则引用了未声明的角色 \"{role}\"",
1506
- "validation.ruleRoleRequired": "规则未选择角色:请选择目标角色,或删除该行",
1507
- "validation.roleChainRequired": "角色 \"{id}\" 未配置模型:请至少添加一条链选择器(模型配置)",
1220
+ "validation.chainIdFormat": "链 id \"{id}\" 不符合格式 /^[A-Za-z0-9][A-Za-z0-9-]{0,31}$/",
1221
+ "validation.chainIdDuplicate": "链 id \"{id}\" 重复",
1222
+ "validation.chainLabelRequired": "链 \"{id}\" 缺少展示名(label)",
1223
+ "validation.chainEntriesRequired": "默认降级链不能为空:请至少添加一条选择器。",
1508
1224
  "validation.allDayRequired": "默认模型必须二选一:官方 Flash 或 Pro",
1509
1225
  "validation.slotChainRequired": "分时槽未配置模型:请至少添加一条链选择器",
1510
1226
  "validation.slotWindow": "分时槽开始/结束时间须为 HH:mm 格式",
@@ -1514,7 +1230,7 @@ window.__ModuleLoader__.load({
1514
1230
  "validation.slotPresetDuplicate": "预设 \"{preset}\" 已存在:每个预设只能添加一行",
1515
1231
  "validation.slotPresetFrozen": "预设窗口是冻结代码常量:预设行不能携带 start/end/days",
1516
1232
  "validation.selector": "选择器 \"{entry}\" 非法:{message}",
1517
- "legacy.banner": "检测到旧格式配置字段({keys}):已按新模型展示,请按 docs/configuration.md 迁移表手工改写;插件不会自动改写配置。",
1233
+ "legacy.banner": "检测到旧格式字段({keys}):已自动升级为命名链——原默认降级链成为第 0 条链「默认链」,角色与人格保持不变,旧的角色数组链已置为「未指定」(可在下方重新指定)。保存一次即可固化;插件不会主动改写你的配置文件。",
1518
1234
  "catalog.empty": "暂无可用模型:请先在模型页添加模型,添加后此处将自动可选。",
1519
1235
  "catalog.error": "模型目录读取失败:{message}",
1520
1236
  "catalog.partial": "部分 provider 模型查询失败:{message}",
@@ -1522,24 +1238,9 @@ window.__ModuleLoader__.load({
1522
1238
  "catalog.outside.tooltip": "不在当前模型目录,可保留原值并保存;新增条目仅可从目录选择。",
1523
1239
  "catalog.outside.short": " (目录外)",
1524
1240
  "catalog.unconfigured.short": " (未配置)",
1525
- "status.title": "运行状态(只读)",
1526
- "status.switches.label": "最近切换:",
1527
- "status.switches.empty": "本会话暂无 fallback 切换。",
1528
- "status.switches.error": "切换历史读取失败:{message}",
1529
- "status.switches.compact": "最近 {count} 次 · {from} → {to}({role} · {reason})",
1530
- "status.switches.compact.roleInject": "最近 {count} 次 · {role} → {to}({reason})",
1531
1241
  "status.switches.reason.trigger-code": "触发失败码",
1532
1242
  "status.switches.reason.always-cap": "always 模式上限",
1533
1243
  "status.switches.reason.role-inject": "角色注入",
1534
- "general.title": "模型故障降级",
1535
- "general.enabled": "已启用",
1536
- "general.disabled": "未启用",
1537
- "general.unknown": "未知",
1538
- "general.unavailable": "状态通道暂不可达",
1539
- "general.switch": "最近切换:{from} → {to}({role} · {reason})",
1540
- "general.switch.roleInject": "最近切换:{role} → {to}({reason})",
1541
- "general.switch.empty": "本会话暂无切换",
1542
- "general.error": "状态读取失败:{message}",
1543
1244
  "chat.switch.title": "模型已降级",
1544
1245
  "chat.switch.summary": "{from} → {to}({reason})",
1545
1246
  "chat.switch.summary.roleInject": "({reason})",
@@ -1574,18 +1275,13 @@ window.__ModuleLoader__.load({
1574
1275
  "cooldownMs.label": "Cooldown (milliseconds)",
1575
1276
  "cooldownMs.hint": "Models stay out of candidacy during cooldown",
1576
1277
  "cooldownMs.tooltip": "Switched-away or failed models stay out of candidacy during the cooldown window.",
1577
- "maxSwitchesPerStep.label": "Max switches per step",
1278
+ "maxSwitchesPerStep.label": "Max switches per request walk",
1578
1279
  "maxSwitchesPerStep.hint": "Stops switching beyond the cap",
1579
- "maxSwitchesPerStep.tooltip": "Beyond this the step stops switching and ends with the original error semantics, preventing chain loops from amplifying latency.",
1580
- "alwaysModeRetryCap.label": "Always-mode retry cap",
1581
- "alwaysModeRetryCap.hint": "Switches after the cap; 0 disables",
1582
- "alwaysModeRetryCap.tooltip": "Models whose retryPolicy is always switch after this many retries within one request; 0 disables.",
1280
+ "maxSwitchesPerStep.tooltip": "Beyond this no further switch happens and the request ends with the original error semantics, preventing chain loops from amplifying latency.",
1281
+ "virtualOnly.rule": "Only sessions whose model selection is FallbacksChain/<chain id> (including subagents that inherit that route) walk a chain; a real-model selection gets no plugin fallback — llm-retry alone handles its retries.",
1583
1282
  "advanced.label": "Advanced options",
1584
1283
  "advanced.expand": "Expand advanced options",
1585
1284
  "advanced.collapse": "Collapse advanced options",
1586
- "roleAutoMatch.label": "Enable role auto-match",
1587
- "roleAutoMatch.hint": "On rules-miss, the model picks the closest declared role",
1588
- "roleAutoMatch.tooltip": "When no rule matches, the model auto-selects the best-fit declared role (id + persona) and uses its chain; turn off to keep today's fallback (inherit / rootChain) on a rules-miss.",
1589
1285
  "rootChain.label": "Default fallback chain",
1590
1286
  "rootChain.tooltip": "Walked first whenever no time slot matches; if every entry fails, the default model below is the last fallback.",
1591
1287
  "defaultModel.label": "Default model",
@@ -1597,21 +1293,26 @@ window.__ModuleLoader__.load({
1597
1293
  "chains.selector.modelPlaceholder": "Select model",
1598
1294
  "chains.selector.wildcardLegacy": "This entry is a wildcard (provider/*): picking a model converts it to an exact entry",
1599
1295
  "chains.selector.noModelsStrict": "No models available for this provider (catalog lookup failed); pick another provider.",
1600
- "mainAgent.label": "Main agent",
1601
1296
  "subagents.label": "Subagents",
1602
- "subagents.policy.label": "Host subagent model policy",
1603
- "subagents.policy.allowlist": "Allowed models",
1604
- "subagents.policy.head": "Effective chain head",
1605
- "subagents.policy.source.authorized": "authorized",
1606
- "subagents.policy.source.injected": "injected",
1607
- "subagents.policy.blocked": "No in-allowlist fallback candidate — switch skipped",
1608
- "subagents.policy.unprovable": "Policy present but unreadable — plugin switches disabled",
1609
- "subagents.policy.offAllowlist": "{count} exact role-chain entries are outside the allowlist — they will not run",
1610
- "subagents.policy.recommend": "Add FallbacksChain/Auto to the allowlist (this virtual route is not filtered by the host policy)",
1611
- "subagents.policy.sync": "Sync to host allowlist",
1612
- "subagents.policy.sync.done": "Synced: exact entries (role chains, default fallback chain and FallbacksChain/Auto) are now allowed; wildcard entries are not expanded or synced",
1613
- "subagents.policy.sync.failed": "Sync failed: the host settings were not written (policy off, entry not configurable, revision conflict or another host refusal) — check the host settings",
1614
- "subagents.policy.sync.dirty": "Unsaved role-chain or default-chain edits — save first, then sync (the sync writes the saved configuration only)",
1297
+ "virtualChain.label": "Virtual chains",
1298
+ "virtualChain.hint": "Each chain is a standalone virtual route with its own time slots, default chain and chain tail.",
1299
+ "chains.tab.add": "New chain",
1300
+ "chains.tab.menu": "Manage this chain: rename / delete",
1301
+ "chains.name.label": "Chain name",
1302
+ "chains.name.hint": "Display only (tab title and picker prefix); the id \"{id}\" is the picker route and cannot change after creation.",
1303
+ "chains.name.tooltip": "Give this virtual chain a readable name; the actual route is FallbacksChain/<chain id>, so renaming does not affect sessions already on it.",
1304
+ "chains.tab.renameLabel": "Chain name (display only; id \"{id}\" stays)",
1305
+ "chains.tab.renameDone": "Done",
1306
+ "chains.tab.delete": "Delete this chain",
1307
+ "chains.tab.deleteDisabled": "Delete this chain (keep at least one)",
1308
+ "chains.tab.dirty": " (unsaved changes)",
1309
+ "chains.tab.error": " (validation error)",
1310
+ "chains.empty.title": "No chains configured",
1311
+ "chains.empty.body": "With no chain configured, no FallbacksChain/* virtual route appears in the model picker; sessions on a FallbacksChain route get no plugin fallback — the host llm-retry alone handles retries.",
1312
+ "chains.empty.create": "+ Create the first chain",
1313
+ "chains.chainFoot.hint": "Saves this chain only: selectors · tail · time slots · name",
1314
+ "chains.chainFoot.discard": "Discard",
1315
+ "chains.chainFoot.save": "Save",
1615
1316
  "timeSlots.label": "Time slots",
1616
1317
  "timeSlots.hint": "First match from top to bottom wins; the all-day row is always last",
1617
1318
  "timeSlots.tz.label": "Timezone (UTC±)",
@@ -1619,7 +1320,7 @@ window.__ModuleLoader__.load({
1619
1320
  "timeSlots.name": "Name",
1620
1321
  "timeSlots.expand": "Expand this row",
1621
1322
  "timeSlots.collapse": "Collapse this row",
1622
- "timeSlots.tooltip": "A matched row's model chain becomes the effective root chain (replacing the all-day chain); no match uses the all-day chain. A time-slot switch is a routing seed, not a failure decision: it consumes no cooldown and does not count against the per-step switch cap.",
1323
+ "timeSlots.tooltip": "A matched row's model chain becomes the effective root chain (replacing the all-day chain); no match uses the all-day chain. A time-slot switch is a routing seed, not a failure decision: it consumes no cooldown and does not count against the per-request-walk switch cap.",
1623
1324
  "timeSlots.addPreset": "Add preset",
1624
1325
  "timeSlots.addCustom": "Add custom time slot",
1625
1326
  "timeSlots.presetPlaceholder": "Select a preset",
@@ -1659,8 +1360,7 @@ window.__ModuleLoader__.load({
1659
1360
  "allDay.pro": "Official Pro (deepseek-official/deepseek-v4-pro)",
1660
1361
  "allDay.nonconforming": "The current default model is not valid: pick official Flash or Pro before saving",
1661
1362
  "roles.list.label": "Declared roles",
1662
- "roles.list.hint": "Declare roles before rules can reference them",
1663
- "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.",
1363
+ "roles.list.hint": "A role is the identity + persona; optionally point it at one named chain via 指定降级链",
1664
1364
  "roles.id": "ID",
1665
1365
  "roles.id.hint": "lowercase letters, digits, hyphens; 1–32 chars",
1666
1366
  "roles.idPlaceholder": "e.g. reviewer",
@@ -1672,35 +1372,23 @@ window.__ModuleLoader__.load({
1672
1372
  "roles.seedSource.bundled": "bundled",
1673
1373
  "roles.seedSource.external": "external",
1674
1374
  "roles.seedSource.user": "User",
1675
- "roles.seedChainOptional": "Role \"{id}\" is a seed role: the chain may stay empty",
1676
- "roles.fallback": "Chain append",
1677
- "roles.fallback.inherit-root": "Inherit root (append rootChain after the role chain)",
1678
- "roles.fallback.none": "Role chain only (no rootChain)",
1375
+ "roles.chain.label": "Specified chain",
1376
+ "roles.chain.unspecified": "Not specified (dsh default)",
1377
+ "roles.chain.hint": "Manually specify the named virtual chain this role uses; when unspecified the dsh default applies: the child inherits the delegating agent's chain.",
1378
+ "roles.chain.tooltip": "Manually specify the named virtual chain for this role: children dispatched under it mount on FallbacksChain/<chain id> and that chain owns the fallback. Unspecified ⇒ the plugin does not intervene.",
1379
+ "roles.chainMeta.unspecified": "No chain specified",
1679
1380
  "roles.add": "Add role",
1680
1381
  "roles.remove": "Remove this role",
1681
1382
  "roles.expand": "Expand this role",
1682
1383
  "roles.collapse": "Collapse this role",
1683
- "roles.selector.add": "Add selector",
1684
- "roles.rules": "Role rules",
1685
- "roles.rules.hint": "Subagents only: matches provider/model in order; no match → inherit (root chain)",
1686
- "roles.rules.tooltip": "Rules apply to subagents only (root requests never match): a matched rule uses that role's chain; no match uses the built-in inherit (rootChain).",
1687
- "roles.rule.provider": "provider",
1688
- "roles.rule.provider.any": "Any",
1689
- "roles.rule.model": "model",
1690
- "roles.rule.model.any": "Any",
1691
- "roles.rule.role": "role",
1692
- "roles.rule.role.inherit": "inherit (built-in: root chain)",
1693
- "roles.rule.roleSelectPlaceholder": "Select role",
1694
- "roles.rule.roleUndeclared.short": " (undeclared)",
1695
- "roles.addRule": "Add rule",
1696
- "roles.removeRule": "Remove this rule",
1697
1384
  "validation.blocked": "Configuration validation failed; save was blocked: ",
1698
1385
  "validation.roleIdFormat": "Role id \"{id}\" does not match /^[a-z0-9-]{1,32}$/",
1699
1386
  "validation.roleIdReserved": "\"inherit\" is a reserved role id and cannot be declared",
1700
1387
  "validation.roleIdDuplicate": "Duplicate role id \"{id}\"",
1701
- "validation.ruleRoleUndeclared": "Rule references undeclared role \"{role}\"",
1702
- "validation.ruleRoleRequired": "Rule has no role selected: pick a target role, or remove the row",
1703
- "validation.roleChainRequired": "Role \"{id}\" has no model config: add at least one chain entry",
1388
+ "validation.chainIdFormat": "Chain id \"{id}\" does not match /^[A-Za-z0-9][A-Za-z0-9-]{0,31}$/",
1389
+ "validation.chainIdDuplicate": "Duplicate chain id \"{id}\"",
1390
+ "validation.chainLabelRequired": "Chain \"{id}\" is missing a display label",
1391
+ "validation.chainEntriesRequired": "The default degradation chain cannot be empty: add at least one selector.",
1704
1392
  "validation.allDayRequired": "The default model must be exactly one official model (Flash or Pro)",
1705
1393
  "validation.slotChainRequired": "Time-slot row has no models: add at least one chain entry",
1706
1394
  "validation.slotWindow": "Time-slot start/end must use HH:mm format",
@@ -1710,7 +1398,7 @@ window.__ModuleLoader__.load({
1710
1398
  "validation.slotPresetDuplicate": "Preset \"{preset}\" already exists: at most one row per preset",
1711
1399
  "validation.slotPresetFrozen": "Preset windows are frozen code constants: a preset row cannot carry start/end/days",
1712
1400
  "validation.selector": "Invalid selector \"{entry}\": {message}",
1713
- "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).",
1401
+ "legacy.banner": "Legacy fields detected ({keys}): automatically upgraded to named chains — your previous default chain became chain #0 「默认链」, roles and personas are unchanged, and old per-role chain arrays now read as \"not specified\" (re-attach them below). Save once to persist this; the plugin never rewrites your config file on its own.",
1714
1402
  "catalog.empty": "No models yet: add a model on the Models page first; options will appear here automatically.",
1715
1403
  "catalog.error": "Model catalog read failed: {message}",
1716
1404
  "catalog.partial": "Some provider model lookups failed: {message}",
@@ -1718,24 +1406,9 @@ window.__ModuleLoader__.load({
1718
1406
  "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).",
1719
1407
  "catalog.outside.short": " (outside catalog)",
1720
1408
  "catalog.unconfigured.short": " (not configured)",
1721
- "status.title": "Runtime status (read-only)",
1722
- "status.switches.label": "Recent switches: ",
1723
- "status.switches.empty": "No fallback switches in this session yet.",
1724
- "status.switches.error": "Switch history read failed: {message}",
1725
- "status.switches.compact": "last {count} · {from} → {to} ({role} · {reason})",
1726
- "status.switches.compact.roleInject": "last {count} · {role} → {to} ({reason})",
1727
1409
  "status.switches.reason.trigger-code": "trigger code",
1728
1410
  "status.switches.reason.always-cap": "always-mode cap",
1729
1411
  "status.switches.reason.role-inject": "role inject",
1730
- "general.title": "Model failover",
1731
- "general.enabled": "Enabled",
1732
- "general.disabled": "Disabled",
1733
- "general.unknown": "Unknown",
1734
- "general.unavailable": "Status channel unavailable",
1735
- "general.switch": "Last switch: {from} → {to} ({role} · {reason})",
1736
- "general.switch.roleInject": "Last switch: {role} → {to} ({reason})",
1737
- "general.switch.empty": "No switches this session",
1738
- "general.error": "Status read failed: {message}",
1739
1412
  "chat.switch.title": "Model downgraded",
1740
1413
  "chat.switch.summary": "{from} → {to} ({reason})",
1741
1414
  "chat.switch.summary.roleInject": "({reason})",
@@ -1753,8 +1426,8 @@ window.__ModuleLoader__.load({
1753
1426
  /** The settings section's dictionary namespace. */
1754
1427
  const NS = "fallbacks";
1755
1428
  /**
1756
- * Reason → locale key map for switch summaries (S-c; shared by the card's
1757
- * status block, the General page status row, and the conversation node). All
1429
+ * Reason → locale key map for switch summaries (shared by the conversation
1430
+ * node). All
1758
1431
  * reasons resolve from ONE key family (`status.switches.reason.*`) — the
1759
1432
  * shared reason vocabulary must not mix dictionary families (qc1 F-004). The
1760
1433
  * session log is durable and forward-compatible: a reason value outside the
@@ -1788,13 +1461,13 @@ window.__ModuleLoader__.load({
1788
1461
  }
1789
1462
  //#endregion
1790
1463
  //#region \0dsh-css:/home/runner/work/dsh-llm-fallbacks/dsh-llm-fallbacks/src/client/FallbacksCard.module.css.mjs
1791
- const css$3 = "\n\n\n._20fd0e45_root {\n min-width: 0;\n}\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._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._94c91bfd_fieldset {\n margin: 0;\n padding: 0;\n border: none;\n display: flex;\n flex-direction: column;\n min-width: 0;\n}\n\n\n._ba240d82_sectionHeading {\n display: flex;\n align-items: center;\n gap: 8px;\n margin: 0;\n padding: 4px 0;\n font-size: 16px;\n line-height: 24px;\n font-weight: 600;\n color: var(--dsw-alias-label-primary);\n}\n\n._ddbc1b49_sectionHeadingText {\n display: block;\n min-width: 0;\n}\n\n\n._c41f9db7_sectionActions {\n display: flex;\n align-items: center;\n gap: 8px;\n margin-left: auto;\n}\n\n._c41f9db7_sectionActions ._f0864181_discard,\n._c41f9db7_sectionActions ._ccff7e48_save {\n padding: 3px 12px;\n font-size: 12px;\n line-height: 18px;\n}\n\n\n._b116c758_sectionToggle {\n appearance: none;\n border: 0;\n background: none;\n display: flex;\n align-items: center;\n gap: 4px;\n margin: 0;\n padding: 0;\n font: inherit;\n color: inherit;\n cursor: pointer;\n}\n\n._b116c758_sectionToggle:disabled {\n cursor: default;\n}\n\n._b116c758_sectionToggle:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n\n._80101203_advancedBody {\n display: flex;\n flex-direction: column;\n min-width: 0;\n}\n\n._94c91bfd_fieldset > ._80101203_advancedBody > ._67826267_field,\n._94c91bfd_fieldset > ._80101203_advancedBody > ._9998065c_checkboxRow,\n._94c91bfd_fieldset > ._80101203_advancedBody > ._3e95886d_numberFields {\n padding: 12px 0;\n}\n\n._94c91bfd_fieldset > ._80101203_advancedBody > ._67826267_field + ._67826267_field,\n._94c91bfd_fieldset > ._80101203_advancedBody > ._9998065c_checkboxRow + ._67826267_field,\n._94c91bfd_fieldset > ._80101203_advancedBody > ._67826267_field + ._3e95886d_numberFields {\n border-top: 0.5px solid var(--dsw-alias-border-l2);\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\n._94c91bfd_fieldset > ._67826267_field,\n._94c91bfd_fieldset > ._9998065c_checkboxRow,\n._94c91bfd_fieldset > ._3e95886d_numberFields {\n padding: 12px 0;\n}\n\n._94c91bfd_fieldset > ._67826267_field + ._67826267_field,\n._94c91bfd_fieldset > ._67826267_field + ._3e95886d_numberFields,\n._94c91bfd_fieldset > ._9998065c_checkboxRow + ._67826267_field {\n border-top: 0.5px solid var(--dsw-alias-border-l2);\n}\n\n\n._9998065c_checkboxRow {\n display: flex;\n align-items: flex-start;\n gap: 8px;\n}\n\n\n._9998065c_checkboxRow ._06389b18_checkbox {\n margin-top: 3px;\n}\n\n._13e68c3f_fieldLabel {\n \n display: inline-flex;\n align-items: center;\n gap: 10px;\n padding: 0;\n font-size: 13px;\n line-height: 1.5;\n font-weight: 500;\n color: var(--dsw-alias-label-primary);\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\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._b1962759_slotPresetName {\n font-size: 14px;\n line-height: 22px;\n font-weight: 500;\n color: var(--dsw-alias-label-primary);\n}\n\n\n._a6244318_infoHint {\n position: relative;\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._a6244318_infoHint::after {\n content: attr(data-tip);\n position: absolute;\n top: 50%;\n left: calc(100% + 8px);\n transform: translateY(-50%);\n z-index: 10;\n box-sizing: border-box;\n width: max-content;\n max-width: 280px;\n padding: 5px 10px;\n border-radius: 8px;\n background: var(--dsw-alias-label-primary);\n color: var(--dsw-alias-bg-layer-3);\n font-size: 11px;\n line-height: 17px;\n font-weight: 400;\n white-space: normal;\n text-align: left;\n pointer-events: none;\n opacity: 0;\n transition: opacity .12s;\n}\n\n._a6244318_infoHint:hover::after,\n._a6244318_infoHint:focus-visible::after {\n opacity: 1;\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 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 outline: none;\n box-shadow: 0 0 0 2px var(--dsw-alias-border-l3);\n}\n\n._a2521bb0_optionRow input:disabled {\n cursor: default;\n}\n\n\n._c4ecbdf3_dayRow {\n display: flex;\n flex-wrap: wrap;\n gap: 4px;\n}\n\n._0330c1ed_dayCell {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 2px 6px;\n border-radius: 6px;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-secondary);\n cursor: pointer;\n}\n\n._0330c1ed_dayCell:hover:has(input:not(:disabled)) {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n._0330c1ed_dayCell 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._0330c1ed_dayCell input:disabled {\n cursor: default;\n}\n\n._0330c1ed_dayCell input:focus-visible {\n outline: none;\n box-shadow: 0 0 0 2px var(--dsw-alias-border-l3);\n}\n\n\n._f9d86f7b_input {\n box-sizing: border-box;\n width: 100%;\n height: 34px;\n padding: 0 12px;\n border: 0.5px solid var(--dsw-alias-border-l4);\n border-radius: var(--dsw-radius-md);\n font: inherit;\n font-size: 13px;\n line-height: 1.5;\n background: var(--dsw-alias-bg-layer-3);\n color: var(--dsw-alias-label-primary);\n}\n\n._f9d86f7b_input:focus-visible {\n outline: none;\n border-color: var(--dsw-alias-state-business-primary);\n}\n\n._f9d86f7b_input::placeholder {\n color: var(--dsw-alias-label-dimmed);\n}\n\n._f9d86f7b_input:disabled {\n color: var(--dsw-alias-label-tertiary);\n cursor: default;\n}\n\n\n._3a9b42fd_inputTextarea {\n height: auto;\n min-height: 72px;\n padding: 8px 12px;\n resize: vertical;\n}\n\n\n._1285c19b_personaBriefRow {\n display: flex;\n align-items: center;\n gap: 4px;\n min-width: 0;\n}\n\n._4b8f7345_personaBrief {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 14px;\n line-height: 22px;\n color: var(--dsw-alias-label-primary);\n}\n\n\n._2686400e_personaBriefEmpty {\n color: var(--dsw-alias-label-tertiary);\n}\n\n\n._4e4a07be_personaFull {\n margin: 0;\n font-size: 14px;\n line-height: 22px;\n color: var(--dsw-alias-label-secondary);\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n}\n\n\n._7b60337a_inputInvalid,\n._7b60337a_inputInvalid:focus-visible {\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: 12px;\n}\n\n._3e95886d_numberFields ._67826267_field {\n padding: 0;\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._631094a0_chevron {\n flex: none;\n color: var(--dsw-alias-label-tertiary);\n transform: rotate(-90deg);\n transition: transform .16s;\n}\n\n._44836ce8_chevronOpen {\n transform: none;\n}\n\n\n._cf3961bb_chevronFlip {\n transform: rotate(180deg);\n}\n\n\n._86bc52d2_collapseRow {\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 0;\n}\n\n._a73efcc6_collapseToggle {\n appearance: none;\n border: 0;\n background: none;\n padding: 0;\n cursor: pointer;\n color: var(--dsw-alias-label-secondary);\n display: flex;\n align-items: center;\n gap: 8px;\n flex: 1;\n min-width: 0;\n font: inherit;\n text-align: left;\n}\n\n._a73efcc6_collapseToggle:disabled {\n cursor: default;\n}\n\n._af017718_collapseTitle {\n \n flex: none;\n max-width: 55%;\n min-width: 0;\n font-size: 12px;\n line-height: 18px;\n font-weight: 500;\n color: var(--dsw-alias-label-primary);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n._2cabe603_collapseMeta {\n \n flex: 0 1 auto;\n min-width: 0;\n max-width: 40%;\n margin-left: auto;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-secondary);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n\n._52d6df87_seedBadge {\n flex: none;\n max-width: 40%;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\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-layer-1);\n color: var(--dsw-alias-label-secondary);\n}\n\n\n._7b04f727_slotTag {\n flex: none;\n display: inline-flex;\n align-items: center;\n border-radius: 999px;\n padding: 0 6px;\n font-size: 11px;\n line-height: 17px;\n font-weight: 500;\n white-space: nowrap;\n border: 1px solid;\n}\n\n._c381e792_slotTagHighCost {\n color: var(--dsw-alias-state-error-primary);\n border-color: var(--dsw-alias-state-error-primary);\n background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);\n}\n\n._23bdb6fc_slotTagMultiplier {\n color: var(--dsw-alias-state-warn-primary);\n border-color: var(--dsw-alias-state-warn-primary);\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 10%, transparent);\n}\n\n._f000eed9_slotTagActive {\n color: var(--dsw-alias-state-success-primary);\n border-color: var(--dsw-alias-state-success-primary);\n background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 10%, transparent);\n}\n\n\n._cd905ce5_dragHandle {\n appearance: none;\n border: 0;\n background: none;\n padding: 0;\n cursor: grab;\n color: var(--dsw-alias-label-tertiary);\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 20px;\n height: 20px;\n border-radius: 6px;\n flex: none;\n}\n\n._cd905ce5_dragHandle:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-primary);\n}\n\n._cd905ce5_dragHandle:active {\n cursor: grabbing;\n}\n\n._cd905ce5_dragHandle:disabled {\n cursor: default;\n opacity: 0.4;\n}\n\n._cd905ce5_dragHandle:focus-visible {\n outline: none;\n box-shadow: 0 0 0 2px var(--dsw-alias-border-l3);\n}\n\n\n._fee63b7e_slotCardDragging {\n opacity: 0.45;\n}\n\n._6f108491_slotCardOver {\n outline: 1px dashed var(--dsw-alias-brand-primary);\n outline-offset: 2px;\n}\n\n\n._1a0fa2e1_selectorRowDragging {\n opacity: 0.45;\n}\n\n._688095f2_selectorRowOver {\n outline: 1px dashed var(--dsw-alias-brand-primary);\n outline-offset: 2px;\n border-radius: 6px;\n}\n\n\n._7489fed0_selectorGrip {\n flex: none;\n}\n\n._7489fed0_selectorGrip:disabled {\n display: none;\n}\n\n\n._28237b53_chainBlock {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n\n._15a12620_chainSelectors {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n\n._5694c1de_selectorLine {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: nowrap;\n \n min-height: 32px;\n}\n\n\n._2f156f38_selectorIndex {\n flex: none;\n width: 1.5em;\n text-align: right;\n font-size: 11px;\n line-height: 16px;\n color: var(--dsw-alias-label-tertiary);\n font-variant-numeric: tabular-nums;\n}\n\n\n._4843ae21_selectorProviderCell {\n flex: 0 1 clamp(140px, 20%, 220px);\n min-width: 0;\n}\n\n._df828181_selectorModelCell {\n flex: 1 1 auto;\n min-width: 0;\n}\n\n\n._df828181_selectorModelCell > select._f9d86f7b_input {\n max-width: 100%;\n}\n\n\n._fe6d05a5_selectorHint {\n display: flex;\n align-items: center;\n flex: 0 1 auto;\n min-width: 0;\n}\n\n\n._d04ee9fe_selectorHintText {\n flex: 0 1 auto;\n min-width: 0;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n font-size: 11px;\n line-height: 16px;\n color: var(--dsw-alias-label-secondary);\n}\n\n\n._fe6d05a5_selectorHint > ._a6244318_infoHint {\n flex: none;\n margin-left: 4px;\n}\n\n\n._ec7c2fac_selectorRemove {\n flex: none;\n opacity: 0;\n transition: opacity .12s;\n}\n\n\n._ec7c2fac_selectorRemove:disabled {\n display: none;\n}\n\n\n._240cbbf8_selectorRow:hover ._ec7c2fac_selectorRemove:not(:disabled),\n._240cbbf8_selectorRow:focus-within ._ec7c2fac_selectorRemove:not(:disabled) {\n opacity: 1;\n}\n\n@media (hover: none) {\n ._ec7c2fac_selectorRemove:not(:disabled) {\n opacity: 0.7;\n }\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._711248a9_outlineButton {\n appearance: none;\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 4px 10px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: var(--dsw-radius-md);\n background: none;\n font: inherit;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-secondary);\n cursor: pointer;\n}\n\n._711248a9_outlineButton:hover:not(:disabled) {\n color: var(--dsw-alias-label-primary);\n border-color: var(--dsw-alias-label-dimmed);\n}\n\n._711248a9_outlineButton:disabled {\n opacity: 0.4;\n cursor: default;\n}\n\n._711248a9_outlineButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n\n._28202c90_addButton {\n align-self: flex-start;\n}\n\n\n._28237b53_chainBlock > ._28202c90_addButton {\n box-sizing: border-box;\n align-self: flex-start;\n height: 26px;\n padding: 0 8px;\n border: 1px solid transparent;\n gap: 4px;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-secondary);\n}\n\n._28237b53_chainBlock > ._28202c90_addButton:hover:not(:disabled) {\n border-color: transparent;\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-primary);\n}\n\n\n._28237b53_chainBlock > ._28202c90_addButton:focus-visible {\n outline: none;\n box-shadow: 0 0 0 2px var(--dsw-alias-border-l3);\n}\n\n\n._6a2bd54c_slotAddRow {\n display: flex;\n align-items: center;\n gap: 8px;\n margin-top: 4px;\n flex-wrap: wrap;\n}\n\n\n._4e77523e_rowActions {\n display: flex;\n align-items: center;\n gap: 4px;\n}\n\n\n._f0864181_discard,\n._ccff7e48_save {\n appearance: none;\n border: 1px solid transparent;\n border-radius: var(--dsw-radius-md);\n padding: 5px 14px;\n font: inherit;\n font-size: 13px;\n line-height: 1.5;\n cursor: pointer;\n}\n\n._f0864181_discard {\n border-color: var(--dsw-alias-border-l2);\n background: none;\n color: var(--dsw-alias-label-secondary);\n}\n\n._f0864181_discard:hover:not(:disabled) {\n color: var(--dsw-alias-label-primary);\n border-color: var(--dsw-alias-label-dimmed);\n}\n\n._ccff7e48_save {\n background: var(--dsw-alias-label-primary);\n color: var(--dsw-alias-bg-layer-3);\n}\n\n._f0864181_discard:disabled,\n._ccff7e48_save:disabled {\n opacity: 0.4;\n cursor: default;\n}\n\n._f0864181_discard:focus-visible,\n._ccff7e48_save:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\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._700006c9_policyStatus {\n margin: 12px 0 0;\n padding: 10px 12px;\n border-radius: 8px;\n background: var(--dsw-alias-bg-module-platform);\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n\n._aa3fae4a_policyStatusText {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n._d90f6286_policyStatusSync {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 6px;\n}\n\n\n._579f87fb_policyStatusLive:empty {\n position: absolute;\n width: 1px;\n height: 1px;\n overflow: hidden;\n clip-path: inset(50%);\n white-space: nowrap;\n}\n\n._d4b67a81_policyStatusLabel {\n font-size: 12px;\n font-weight: 600;\n line-height: 18px;\n color: var(--dsw-alias-label-secondary);\n}\n\n._278bb4b3_policyStatusLine {\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-primary);\n}\n\n._da6387e5_policyStatusWarn {\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n@media (prefers-reduced-motion: reduce) {\n ._70effa64_iconButton::after,\n ._a6244318_infoHint::after {\n transition: none;\n }\n\n ._631094a0_chevron {\n transition: none;\n }\n}\n";
1792
- const tagId$3 = "dsh-llm-fallbacks/FallbacksCard.module.css";
1793
- if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$3) + "]") === null) {
1464
+ const css$2 = "\n\n\n._20fd0e45_root {\n min-width: 0;\n}\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._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._94c91bfd_fieldset {\n margin: 0;\n padding: 0;\n border: none;\n display: flex;\n flex-direction: column;\n min-width: 0;\n}\n\n\n._ba240d82_sectionHeading {\n display: flex;\n align-items: center;\n gap: 8px;\n margin: 0;\n padding: 4px 0;\n font-size: 16px;\n line-height: 24px;\n font-weight: 600;\n color: var(--dsw-alias-label-primary);\n}\n\n._ddbc1b49_sectionHeadingText {\n display: block;\n min-width: 0;\n}\n\n\n._c41f9db7_sectionActions {\n display: flex;\n align-items: center;\n gap: 8px;\n margin-left: auto;\n}\n\n._c41f9db7_sectionActions ._f0864181_discard,\n._c41f9db7_sectionActions ._ccff7e48_save {\n padding: 3px 12px;\n font-size: 12px;\n line-height: 18px;\n}\n\n\n._b116c758_sectionToggle {\n appearance: none;\n border: 0;\n background: none;\n display: flex;\n align-items: center;\n gap: 4px;\n margin: 0;\n padding: 0;\n font: inherit;\n color: inherit;\n cursor: pointer;\n}\n\n._b116c758_sectionToggle:disabled {\n cursor: default;\n}\n\n._b116c758_sectionToggle:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n\n._80101203_advancedBody {\n display: flex;\n flex-direction: column;\n min-width: 0;\n}\n\n._94c91bfd_fieldset > ._80101203_advancedBody > ._67826267_field,\n._94c91bfd_fieldset > ._80101203_advancedBody > ._3e95886d_numberFields {\n padding: 12px 0;\n}\n\n._94c91bfd_fieldset > ._80101203_advancedBody > ._67826267_field + ._67826267_field,\n._94c91bfd_fieldset > ._80101203_advancedBody > ._67826267_field + ._3e95886d_numberFields {\n border-top: 0.5px solid var(--dsw-alias-border-l2);\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\n._94c91bfd_fieldset > ._67826267_field,\n._94c91bfd_fieldset > ._3e95886d_numberFields {\n padding: 12px 0;\n}\n\n._94c91bfd_fieldset > ._67826267_field + ._67826267_field,\n._94c91bfd_fieldset > ._67826267_field + ._3e95886d_numberFields {\n border-top: 0.5px solid var(--dsw-alias-border-l2);\n}\n\n._13e68c3f_fieldLabel {\n \n display: inline-flex;\n align-items: center;\n gap: 10px;\n padding: 0;\n font-size: 13px;\n line-height: 1.5;\n font-weight: 500;\n color: var(--dsw-alias-label-primary);\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\n._b1962759_slotPresetName {\n font-size: 14px;\n line-height: 22px;\n font-weight: 500;\n color: var(--dsw-alias-label-primary);\n}\n\n\n._a6244318_infoHint {\n position: relative;\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._a6244318_infoHint::after {\n content: attr(data-tip);\n position: absolute;\n top: 50%;\n left: calc(100% + 8px);\n transform: translateY(-50%);\n z-index: 10;\n box-sizing: border-box;\n width: max-content;\n max-width: 280px;\n padding: 5px 10px;\n border-radius: 8px;\n background: var(--dsw-alias-label-primary);\n color: var(--dsw-alias-bg-layer-3);\n font-size: 11px;\n line-height: 17px;\n font-weight: 400;\n white-space: normal;\n text-align: left;\n pointer-events: none;\n opacity: 0;\n transition: opacity .12s;\n}\n\n._a6244318_infoHint:hover::after,\n._a6244318_infoHint:focus-visible::after {\n opacity: 1;\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 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 outline: none;\n box-shadow: 0 0 0 2px var(--dsw-alias-border-l3);\n}\n\n._a2521bb0_optionRow input:disabled {\n cursor: default;\n}\n\n\n._c4ecbdf3_dayRow {\n display: flex;\n flex-wrap: wrap;\n gap: 4px;\n}\n\n._0330c1ed_dayCell {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 2px 6px;\n border-radius: 6px;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-secondary);\n cursor: pointer;\n}\n\n._0330c1ed_dayCell:hover:has(input:not(:disabled)) {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n._0330c1ed_dayCell 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._0330c1ed_dayCell input:disabled {\n cursor: default;\n}\n\n._0330c1ed_dayCell input:focus-visible {\n outline: none;\n box-shadow: 0 0 0 2px var(--dsw-alias-border-l3);\n}\n\n\n._f9d86f7b_input {\n box-sizing: border-box;\n width: 100%;\n height: 34px;\n padding: 0 12px;\n border: 0.5px solid var(--dsw-alias-border-l4);\n border-radius: var(--dsw-radius-md);\n font: inherit;\n font-size: 13px;\n line-height: 1.5;\n background: var(--dsw-alias-bg-layer-3);\n color: var(--dsw-alias-label-primary);\n}\n\n._f9d86f7b_input:focus-visible {\n outline: none;\n border-color: var(--dsw-alias-state-business-primary);\n}\n\n._f9d86f7b_input::placeholder {\n color: var(--dsw-alias-label-dimmed);\n}\n\n._f9d86f7b_input:disabled {\n color: var(--dsw-alias-label-tertiary);\n cursor: default;\n}\n\n\n._3a9b42fd_inputTextarea {\n height: auto;\n min-height: 72px;\n padding: 8px 12px;\n resize: vertical;\n}\n\n\n._1285c19b_personaBriefRow {\n display: flex;\n align-items: center;\n gap: 4px;\n min-width: 0;\n}\n\n._4b8f7345_personaBrief {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 14px;\n line-height: 22px;\n color: var(--dsw-alias-label-primary);\n}\n\n\n._2686400e_personaBriefEmpty {\n color: var(--dsw-alias-label-tertiary);\n}\n\n\n._4e4a07be_personaFull {\n margin: 0;\n font-size: 14px;\n line-height: 22px;\n color: var(--dsw-alias-label-secondary);\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n}\n\n\n._7b60337a_inputInvalid,\n._7b60337a_inputInvalid:focus-visible {\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: 12px;\n}\n\n._3e95886d_numberFields ._67826267_field {\n padding: 0;\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._631094a0_chevron {\n flex: none;\n color: var(--dsw-alias-label-tertiary);\n transform: rotate(-90deg);\n transition: transform .16s;\n}\n\n._44836ce8_chevronOpen {\n transform: none;\n}\n\n\n._cf3961bb_chevronFlip {\n transform: rotate(180deg);\n}\n\n\n._86bc52d2_collapseRow {\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 0;\n}\n\n._a73efcc6_collapseToggle {\n appearance: none;\n border: 0;\n background: none;\n padding: 0;\n cursor: pointer;\n color: var(--dsw-alias-label-secondary);\n display: flex;\n align-items: center;\n gap: 8px;\n flex: 1;\n min-width: 0;\n font: inherit;\n text-align: left;\n}\n\n._a73efcc6_collapseToggle:disabled {\n cursor: default;\n}\n\n._af017718_collapseTitle {\n \n flex: none;\n max-width: 55%;\n min-width: 0;\n font-size: 12px;\n line-height: 18px;\n font-weight: 500;\n color: var(--dsw-alias-label-primary);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n._2cabe603_collapseMeta {\n \n flex: 0 1 auto;\n min-width: 0;\n max-width: 40%;\n margin-left: auto;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-secondary);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n\n._52d6df87_seedBadge {\n flex: none;\n max-width: 40%;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\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-layer-1);\n color: var(--dsw-alias-label-secondary);\n}\n\n\n._7b04f727_slotTag {\n flex: none;\n display: inline-flex;\n align-items: center;\n border-radius: 999px;\n padding: 0 6px;\n font-size: 11px;\n line-height: 17px;\n font-weight: 500;\n white-space: nowrap;\n border: 1px solid;\n}\n\n._c381e792_slotTagHighCost {\n color: var(--dsw-alias-state-error-primary);\n border-color: var(--dsw-alias-state-error-primary);\n background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);\n}\n\n._23bdb6fc_slotTagMultiplier {\n color: var(--dsw-alias-state-warn-primary);\n border-color: var(--dsw-alias-state-warn-primary);\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 10%, transparent);\n}\n\n._f000eed9_slotTagActive {\n color: var(--dsw-alias-state-success-primary);\n border-color: var(--dsw-alias-state-success-primary);\n background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 10%, transparent);\n}\n\n\n._cd905ce5_dragHandle {\n appearance: none;\n border: 0;\n background: none;\n padding: 0;\n cursor: grab;\n color: var(--dsw-alias-label-tertiary);\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 20px;\n height: 20px;\n border-radius: 6px;\n flex: none;\n}\n\n._cd905ce5_dragHandle:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-primary);\n}\n\n._cd905ce5_dragHandle:active {\n cursor: grabbing;\n}\n\n._cd905ce5_dragHandle:disabled {\n cursor: default;\n opacity: 0.4;\n}\n\n._cd905ce5_dragHandle:focus-visible {\n outline: none;\n box-shadow: 0 0 0 2px var(--dsw-alias-border-l3);\n}\n\n\n._fee63b7e_slotCardDragging {\n opacity: 0.45;\n}\n\n._6f108491_slotCardOver {\n outline: 1px dashed var(--dsw-alias-brand-primary);\n outline-offset: 2px;\n}\n\n\n._1a0fa2e1_selectorRowDragging {\n opacity: 0.45;\n}\n\n._688095f2_selectorRowOver {\n outline: 1px dashed var(--dsw-alias-brand-primary);\n outline-offset: 2px;\n border-radius: 6px;\n}\n\n\n._7489fed0_selectorGrip {\n flex: none;\n}\n\n._7489fed0_selectorGrip:disabled {\n display: none;\n}\n\n\n._28237b53_chainBlock {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n\n._15a12620_chainSelectors {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n\n._5694c1de_selectorLine {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: nowrap;\n \n min-height: 32px;\n}\n\n\n._2f156f38_selectorIndex {\n flex: none;\n width: 1.5em;\n text-align: right;\n font-size: 11px;\n line-height: 16px;\n color: var(--dsw-alias-label-tertiary);\n font-variant-numeric: tabular-nums;\n}\n\n\n._4843ae21_selectorProviderCell {\n flex: 0 1 clamp(140px, 20%, 220px);\n min-width: 0;\n}\n\n._df828181_selectorModelCell {\n flex: 1 1 auto;\n min-width: 0;\n}\n\n\n._df828181_selectorModelCell > select._f9d86f7b_input {\n max-width: 100%;\n}\n\n\n._fe6d05a5_selectorHint {\n display: flex;\n align-items: center;\n flex: 0 1 auto;\n min-width: 0;\n}\n\n\n._d04ee9fe_selectorHintText {\n flex: 0 1 auto;\n min-width: 0;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n font-size: 11px;\n line-height: 16px;\n color: var(--dsw-alias-label-secondary);\n}\n\n\n._fe6d05a5_selectorHint > ._a6244318_infoHint {\n flex: none;\n margin-left: 4px;\n}\n\n\n._ec7c2fac_selectorRemove {\n flex: none;\n opacity: 0;\n transition: opacity .12s;\n}\n\n\n._ec7c2fac_selectorRemove:disabled {\n display: none;\n}\n\n\n._240cbbf8_selectorRow:hover ._ec7c2fac_selectorRemove:not(:disabled),\n._240cbbf8_selectorRow:focus-within ._ec7c2fac_selectorRemove:not(:disabled) {\n opacity: 1;\n}\n\n@media (hover: none) {\n ._ec7c2fac_selectorRemove:not(:disabled) {\n opacity: 0.7;\n }\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._711248a9_outlineButton {\n appearance: none;\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 4px 10px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: var(--dsw-radius-md);\n background: none;\n font: inherit;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-secondary);\n cursor: pointer;\n}\n\n._711248a9_outlineButton:hover:not(:disabled) {\n color: var(--dsw-alias-label-primary);\n border-color: var(--dsw-alias-label-dimmed);\n}\n\n._711248a9_outlineButton:disabled {\n opacity: 0.4;\n cursor: default;\n}\n\n._711248a9_outlineButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n\n._28202c90_addButton {\n align-self: flex-start;\n}\n\n\n._28237b53_chainBlock > ._28202c90_addButton {\n box-sizing: border-box;\n align-self: flex-start;\n height: 26px;\n padding: 0 8px;\n border: 1px solid transparent;\n gap: 4px;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-secondary);\n}\n\n._28237b53_chainBlock > ._28202c90_addButton:hover:not(:disabled) {\n border-color: transparent;\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-primary);\n}\n\n\n._28237b53_chainBlock > ._28202c90_addButton:focus-visible {\n outline: none;\n box-shadow: 0 0 0 2px var(--dsw-alias-border-l3);\n}\n\n\n._6a2bd54c_slotAddRow {\n display: flex;\n align-items: center;\n gap: 8px;\n margin-top: 4px;\n flex-wrap: wrap;\n}\n\n\n._4e77523e_rowActions {\n display: flex;\n align-items: center;\n gap: 4px;\n}\n\n\n._f0864181_discard,\n._ccff7e48_save {\n appearance: none;\n border: 1px solid transparent;\n border-radius: var(--dsw-radius-md);\n padding: 5px 14px;\n font: inherit;\n font-size: 13px;\n line-height: 1.5;\n cursor: pointer;\n}\n\n._f0864181_discard {\n border-color: var(--dsw-alias-border-l2);\n background: none;\n color: var(--dsw-alias-label-secondary);\n}\n\n._f0864181_discard:hover:not(:disabled) {\n color: var(--dsw-alias-label-primary);\n border-color: var(--dsw-alias-label-dimmed);\n}\n\n._ccff7e48_save {\n background: var(--dsw-alias-label-primary);\n color: var(--dsw-alias-bg-layer-3);\n}\n\n._f0864181_discard:disabled,\n._ccff7e48_save:disabled {\n opacity: 0.4;\n cursor: default;\n}\n\n._f0864181_discard:focus-visible,\n._ccff7e48_save:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\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._700006c9_policyStatus {\n margin: 12px 0 0;\n padding: 10px 12px;\n border-radius: 8px;\n background: var(--dsw-alias-bg-module-platform);\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n\n._aa3fae4a_policyStatusText {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n._d90f6286_policyStatusSync {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 6px;\n}\n\n\n._579f87fb_policyStatusLive:empty {\n position: absolute;\n width: 1px;\n height: 1px;\n overflow: hidden;\n clip-path: inset(50%);\n white-space: nowrap;\n}\n\n._d4b67a81_policyStatusLabel {\n font-size: 12px;\n font-weight: 600;\n line-height: 18px;\n color: var(--dsw-alias-label-secondary);\n}\n\n._278bb4b3_policyStatusLine {\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-primary);\n}\n\n._da6387e5_policyStatusWarn {\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n@media (prefers-reduced-motion: reduce) {\n ._70effa64_iconButton::after,\n ._a6244318_infoHint::after {\n transition: none;\n }\n\n ._631094a0_chevron {\n transition: none;\n }\n}\n\n\n._71f663ad_tabsWrap {\n position: relative;\n border-bottom: 0.5px solid var(--dsw-alias-border-l2);\n max-width: 720px;\n}\n\n._0c1dcd2d_tabs {\n display: flex;\n align-items: center;\n gap: 4px;\n overflow-x: auto;\n scrollbar-width: none;\n padding: 0 2px;\n}\n._0c1dcd2d_tabs::-webkit-scrollbar { display: none; }\n\n._98f72e4c_tab {\n appearance: none;\n border: none;\n background: none;\n font: inherit;\n color: var(--dsw-alias-label-secondary);\n padding: 6px 10px;\n cursor: pointer;\n position: relative;\n white-space: nowrap;\n border-radius: 6px 6px 0 0;\n}\n._98f72e4c_tab:hover { background: var(--dsw-alias-interactive-bg-hover); }\n._98f72e4c_tab[aria-selected='true'] {\n color: var(--dsw-alias-label-primary);\n font-weight: 600;\n}\n._98f72e4c_tab[aria-selected='true']::after {\n content: '';\n position: absolute;\n left: 6px;\n right: 6px;\n bottom: -1px;\n height: 2px;\n border-radius: 2px 2px 0 0;\n background: var(--dsw-alias-brand-primary);\n}\n._98f72e4c_tab:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n._0ebd08c8_dirtyPip {\n display: inline-block;\n width: 6px;\n height: 6px;\n margin-left: 4px;\n border-radius: 50%;\n background: var(--dsw-alias-state-warn-primary);\n vertical-align: middle;\n}\n\n._52953be4_errorMark {\n display: inline-block;\n margin-left: 4px;\n color: var(--dsw-alias-state-error-primary);\n font-weight: 700;\n vertical-align: middle;\n}\n\n._f65332f7_tabAdd {\n appearance: none;\n border: none;\n background: none;\n font: inherit;\n color: var(--dsw-alias-label-tertiary);\n padding: 6px 8px;\n cursor: pointer;\n border-radius: 6px;\n}\n._f65332f7_tabAdd:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }\n._f65332f7_tabAdd:disabled { color: var(--dsw-alias-label-dimmed); cursor: default; }\n._f65332f7_tabAdd:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n._30e90023_tabMenu { position: relative; display: inline-flex; }\n\n._43fc138e_menuBtn {\n appearance: none;\n border: none;\n background: none;\n font: inherit;\n color: var(--dsw-alias-label-tertiary);\n padding: 6px 8px;\n cursor: pointer;\n border-radius: 6px;\n}\n._43fc138e_menuBtn:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }\n._43fc138e_menuBtn:disabled { color: var(--dsw-alias-label-dimmed); cursor: default; }\n\n._6674f868_menuPanel {\n position: absolute;\n top: calc(100% + 4px);\n right: 0;\n z-index: 20;\n min-width: 240px;\n background: var(--dsw-alias-bg-layer-3);\n border: 0.5px solid var(--dsw-alias-border-l3);\n border-radius: var(--dsw-radius-md, 8px);\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);\n padding: 10px;\n display: grid;\n gap: 8px;\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n}\n\n._a4112fce_menuInput { width: 100%; }\n\n._54106171_menuActions { display: flex; justify-content: flex-end; }\n\n._b76100ea_menuItemDanger {\n appearance: none;\n border: none;\n background: none;\n font: inherit;\n color: var(--dsw-alias-state-error-primary);\n text-align: left;\n padding: 6px 4px;\n cursor: pointer;\n border-radius: 6px;\n}\n._b76100ea_menuItemDanger:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover-danger); }\n._b76100ea_menuItemDanger:disabled { color: var(--dsw-alias-label-dimmed); cursor: default; }\n\n._6e67618d_fadeL,\n._50673253_fadeR {\n position: absolute;\n top: 0;\n bottom: 0;\n width: 24px;\n pointer-events: none;\n opacity: 0;\n transition: opacity 0.15s;\n}\n._6e67618d_fadeL { left: 0; background: linear-gradient(to right, var(--dsw-alias-bg-layer-1), transparent); }\n._50673253_fadeR { right: 0; background: linear-gradient(to left, var(--dsw-alias-bg-layer-1), transparent); }\n._71f663ad_tabsWrap:has([data-tablist][data-scroll-left='1']) ._6e67618d_fadeL,\n._71f663ad_tabsWrap:has([data-tablist][data-scroll-right='1']) ._50673253_fadeR { opacity: 1; }\n\n._a87096d3_emptyState {\n border: 0.5px solid var(--dsw-alias-border-l2);\n border-radius: var(--dsw-radius-md, 8px);\n padding: 24px;\n margin: 12px 0;\n display: grid;\n gap: 8px;\n justify-items: start;\n color: var(--dsw-alias-label-secondary);\n}\n._a87096d3_emptyState strong { color: var(--dsw-alias-label-primary); }\n\n._5f2c1c0a_chainBody { min-width: 0; }\n\n._142cbcb6_chainFoot {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 12px 0;\n border-top: 0.5px solid var(--dsw-alias-border-l2);\n flex-wrap: wrap;\n}\n\n._3fa8f843_chainFootHint {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n margin-left: auto;\n}\n";
1465
+ const tagId$2 = "dsh-llm-fallbacks/FallbacksCard.module.css";
1466
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]") === null) {
1794
1467
  const tag = document.createElement("style");
1795
1468
  tag.dataset.plugin = "dsh-llm-fallbacks";
1796
- tag.dataset.pluginCss = tagId$3;
1797
- tag.textContent = css$3;
1469
+ tag.dataset.pluginCss = tagId$2;
1470
+ tag.textContent = css$2;
1798
1471
  document.head.appendChild(tag);
1799
1472
  }
1800
1473
  var FallbacksCard_module_css_default = {
@@ -1813,18 +1486,14 @@ window.__ModuleLoader__.load({
1813
1486
  "sectionToggle": "_b116c758_sectionToggle",
1814
1487
  "advancedBody": "_80101203_advancedBody",
1815
1488
  "field": "_67826267_field",
1816
- "checkboxRow": "_9998065c_checkboxRow",
1817
1489
  "numberFields": "_3e95886d_numberFields",
1818
- "checkbox": "_06389b18_checkbox",
1819
1490
  "fieldLabel": "_13e68c3f_fieldLabel",
1820
1491
  "hint": "_4bc809b8_hint",
1821
- "checkLabel": "_02c2f6df_checkLabel",
1822
- "checkLabelTitle": "_f2d47237_checkLabelTitle",
1823
- "checkLabelDesc": "_c1bed8ea_checkLabelDesc",
1824
1492
  "slotPresetName": "_b1962759_slotPresetName",
1825
1493
  "infoHint": "_a6244318_infoHint",
1826
1494
  "infoHintDisabled": "_e655c840_infoHintDisabled",
1827
1495
  "optionRow": "_a2521bb0_optionRow",
1496
+ "checkbox": "_06389b18_checkbox",
1828
1497
  "dayRow": "_c4ecbdf3_dayRow",
1829
1498
  "dayCell": "_0330c1ed_dayCell",
1830
1499
  "input": "_f9d86f7b_input",
@@ -1885,92 +1554,71 @@ window.__ModuleLoader__.load({
1885
1554
  "policyStatusLive": "_579f87fb_policyStatusLive",
1886
1555
  "policyStatusLabel": "_d4b67a81_policyStatusLabel",
1887
1556
  "policyStatusLine": "_278bb4b3_policyStatusLine",
1888
- "policyStatusWarn": "_da6387e5_policyStatusWarn"
1557
+ "policyStatusWarn": "_da6387e5_policyStatusWarn",
1558
+ "tabsWrap": "_71f663ad_tabsWrap",
1559
+ "tabs": "_0c1dcd2d_tabs",
1560
+ "tab": "_98f72e4c_tab",
1561
+ "dirtyPip": "_0ebd08c8_dirtyPip",
1562
+ "errorMark": "_52953be4_errorMark",
1563
+ "tabAdd": "_f65332f7_tabAdd",
1564
+ "tabMenu": "_30e90023_tabMenu",
1565
+ "menuBtn": "_43fc138e_menuBtn",
1566
+ "menuPanel": "_6674f868_menuPanel",
1567
+ "menuInput": "_a4112fce_menuInput",
1568
+ "menuActions": "_54106171_menuActions",
1569
+ "menuItemDanger": "_b76100ea_menuItemDanger",
1570
+ "fadeL": "_6e67618d_fadeL",
1571
+ "fadeR": "_50673253_fadeR",
1572
+ "emptyState": "_a87096d3_emptyState",
1573
+ "chainBody": "_5f2c1c0a_chainBody",
1574
+ "chainFoot": "_142cbcb6_chainFoot",
1575
+ "chainFootHint": "_3fa8f843_chainFootHint"
1889
1576
  };
1890
1577
  //#endregion
1891
1578
  //#region src/client/FallbacksCard.tsx
1892
1579
  /**
1893
- * Fallbacks settings card — the `fallbacks` plugin card on the web
1894
- * Plugins page (spec §4). Registered into the `plugins.bundle.config` keyed
1895
- * slot (key `dsh-llm-fallbacks`, the bundle package name, alongside
1896
- * the upstream bash/agent-loop/web-search cards and the advisor card, in
1897
- * registration order); owner props are empty and all data flows
1898
- * through {@link FallbacksSettingsController}.
1580
+ * Fallbacks settings card — the `fallbacks` plugin card on the web Plugins
1581
+ * page (named virtual chains, plan named-virtual-chains T5; the FROZEN UI
1582
+ * mockup `.mstar/design/named-virtual-chains/settings-mockup.html`).
1899
1583
  *
1900
- * FLAT rebuild (plan fallbacks-web-ux-alignment T3, the advisor PR #98
1901
- * pattern): the collapsible card chrome is gone — no header button, no
1902
- * rotating chevron, no dirty "unsaved" pill, no card-local open state, no
1903
- * bordered `<li>` box. The page already renders the plugin title/description
1904
- * above the card (the `locale/*.json` meta files the host resolves per UI
1905
- * language), so the notices, the form, and the section actions directly
1906
- * beneath it in the official flat settings-form language (reference §3):
1907
- * fields at
1908
- * 12px vertical padding with 0.5px hairline separators, 34px inputs on the
1909
- * 0.5px l4 border, per-section Discard + Save pairs on the section
1910
- * headings. The card is ALWAYS open —
1911
- * nothing needs card-level disclosure state; the degraded /
1912
- * error / read-only / migration notices render flat atop the form (the
1913
- * unavailable notice stays always-visible per KD-G5 — the documented
1914
- * divergence from the advisor, whose unavailable card renders nothing: the
1915
- * fallbacks skeleton stays the USABLE last-known form and saves are
1916
- * attempted).
1584
+ * Flat form vocabulary unchanged: fields at 12px vertical padding with 0.5px
1585
+ * hairline separators, 34px inputs on the 0.5px l4 border, per-section
1586
+ * Discard + Save pairs. The 虚拟链 group is TAB-PER-CHAIN: underline tabs on
1587
+ * the 0.5px l2 hairline (active label = label-primary + 2px brand underline,
1588
+ * dirty pip, error "!" mark), a "+" add affordance and a "⋯" chain menu
1589
+ * (delete, ≥1 chain), WAI-ARIA tabs with roving tabindex.
1590
+ * Each tab body carries 链名称 (the single label editor — the ⋯ menu binds to
1591
+ * the same draft field) followed by the SHIPPED block order 分时槽设置 →
1592
+ * 默认降级链 → 默认模型(链尾) plus a per-tab Discard/Save writing ONLY that
1593
+ * chain (block ids per chain, error text under the owning block).
1917
1594
  *
1918
- * The config-level `enabled` switch is REMOVED (T2, breaking): there is no
1919
- * checkbox row and no `enabled.off` hiding — all fields render
1920
- * unconditionally; the Plugins-page row toggle is the master switch and the
1921
- * runtime gates read content presence (`isFallbackActive`).
1595
+ * The 子代理 section is role cards: id + persona + 指定降级链 (a MANUAL
1596
+ * named-chain pointer; 未指定(沿用 dsh 默认) = the plugin does not
1597
+ * intervene). No rules, no per-role chain entries, no host-policy block.
1598
+ * The 高级选项 group is unchanged; the 运行状态(只读) block and the
1599
+ * General settings row are REMOVED with the client switch-history read.
1922
1600
  *
1923
- * PER-SECTION Save/Discard return beside each section heading (plan
1924
- * fallbacks-card-section-ux, the maintainer's live-host feedback reversing
1925
- * the flat plan's one-document model): the scroll-to-save cost of ONE
1926
- * bottom Save outweighed it. Each section's Save writes ONLY that section's
1927
- * fields (patch = the last ACCEPTED config with the section's fields
1928
- * replaced — the ride-along protection, PR #62 UX round 3), gated by that
1929
- * section's dirty term and validation bucket only (KD-U1: save =
1930
- * `!sectionDirty || saving || !writable`, discard = `!sectionDirty ||
1931
- * saving`; Save additionally carries `!writable` — W-1, qc2 fix wave: the
1932
- * dirty-implies-writable assumption does not hold for this in-place-draft
1933
- * store). Violations render live under their OWNING section heading, and a
1934
- * store write failure renders under the section whose Save was last
1935
- * clicked (`lastSaveSection`). Discard is kept — our staged edits survive
1936
- * refresh, the advisor's documented divergence from the official
1937
- * no-discard form — a pure client-side revert of THAT section's editors to
1938
- * the last accepted config.
1601
+ * Validation blocks the owning save only: chain id format/duplicates,
1602
+ * missing label, per-chain slot guards, tail conformance, role id checks.
1603
+ * A non-empty `state.legacyKeys` renders the migration banner at the top.
1939
1604
  *
1940
- * The form body is the two-block editing surface (spec §8): the 6 top-level
1941
- * scalar fields (trigger codes / revert policy / three numeric fields), the
1942
- * `rootChain` block (block 1 — the root agent's single chain, no key input),
1943
- * and the roles block (block 2 — declared role entity cards from
1944
- * `roles.list` plus the rule rows from `roles.rules`, whose role field is a
1945
- * dropdown bound to the declared ids + the built-in `inherit`, same-page
1946
- * live). Row-level disclosures that are CONTENT keep their expand/collapse:
1947
- * the time-slot row cards and the role entity cards (with the seeded
1948
- * persona briefs) stay collapsible — a read-only view forces them open so
1949
- * the configs stay reachable. The 主代理 / 子代理 / 高级选项 group labels
1950
- * are semantic `<h2>` headings (T1); 高级选项 is a content-level disclosure
1951
- * collapsed by default (T3 — same expand/collapse contract as the rows, a
1952
- * read-only view forces it open).
1953
- *
1954
- * Validation: id format/reserved word/duplicates, undeclared rule role
1955
- * references, illegal selectors, and a role with no chain entries (no model
1956
- * config) block the write (the Save disabled gate) with per-section banners
1957
- * + inline red borders / hints (never touching the store error path); a
1958
- * non-empty `state.legacyKeys` renders the migration banner at the top.
1959
- * The reset-to-defaults affordance is GONE from the card (PR #62 UX
1960
- * round 3) — the gateway RPC `fallbacks/reset` and the store
1961
- * `resetToDefaults()` stay as host APIs, only the card UI was removed.
1962
- *
1963
- * The AC-7 read-only status block (recent-switch summary, D-5) stays at the
1964
- * bottom, flat.
1965
- *
1966
- * Presentation is ui-primitives-free (T4, advisor parity + reference §4
1967
- * risk): Button/Tooltip and the five icon imports are replaced by
1968
- * self-drawn styled elements — inline SVG icons (plus / trash / chevron /
1969
- * grip) and the CSS `data-tip` tooltip bubble (the card's existing
1970
- * icon-button pattern); every color resolves through a `--dsw-alias-*`
1971
- * token so the card adapts to the light/dark theme.
1605
+ * Presentation stays ui-primitives-free: inline SVG icons and the CSS
1606
+ * `data-tip` tooltip bubble; every color resolves through a
1607
+ * `--dsw-alias-*` token (light/dark parity).
1972
1608
  */
1973
- const ALL_DAY_LABEL_KEYS = ["allDay.flash", "allDay.pro"];
1609
+ function collectInvalidRoleIds(rows) {
1610
+ const counts = /* @__PURE__ */ new Map();
1611
+ for (const row of rows) {
1612
+ const id = row.id.trim();
1613
+ counts.set(id, (counts.get(id) ?? 0) + 1);
1614
+ }
1615
+ const invalid = /* @__PURE__ */ new Set();
1616
+ for (const row of rows) {
1617
+ const id = row.id.trim();
1618
+ if (!ROLE_ID_PATTERN.test(id) || id === "inherit" || (counts.get(id) ?? 0) > 1) invalid.add(id);
1619
+ }
1620
+ return invalid;
1621
+ }
1974
1622
  const SLOT_PRESET_IDS = [
1975
1623
  "liang-peak",
1976
1624
  "liang-valley",
@@ -2114,98 +1762,18 @@ window.__ModuleLoader__.load({
2114
1762
  const offset = tzUtcOffset(tz);
2115
1763
  return offset === "" ? tz : `${tz} (${offset})`;
2116
1764
  }
2117
- /** Persist tz: presets lock UTC+8; custom-only uses the host zone; else keep the accepted value. */
2118
- function resolvedSlotTz(rows, fallback) {
2119
- if (rows.some((row) => row.kind === "preset")) return "Asia/Shanghai";
2120
- if (rows.some((row) => row.kind === "custom")) return hostTimeZone();
2121
- return fallback === "" ? "Asia/Shanghai" : fallback;
2122
- }
2123
1765
  /** Strict 24h `HH:mm` — the resolver's HHMM_RE twin (drift-guarded by the gateway reject-on-save). */
2124
1766
  const HHMM_RE = /^([01]\d|2[0-3]):[0-5]\d$/;
2125
- /**
2126
- * The 默认模型 value for a chain: the official id when the chain TAIL
2127
- * is that model (Flash or Pro XOR — leading 默认降级链 entries allowed);
2128
- * `''` for an empty chain or a chain whose last entry is not official
2129
- * (the panel reads back unselected and save validation blocks the value).
2130
- */
2131
- function allDayModelOf(chain) {
2132
- const tail = chain.length >= 1 ? chain[chain.length - 1] : void 0;
2133
- return tail !== void 0 && isOfficialAllDayId(tail) ? tail : "";
2134
- }
2135
- /**
2136
- * The 默认降级链 editor row: the leading entries BEFORE the official tail
2137
- * (Flash or Pro), or the whole chain while the tail is not official (the
2138
- * draft rides the accepted value until a 默认模型 pick).
2139
- */
2140
- function allDayChainRowOf(chain, catalog) {
2141
- return rootChainToRows(allDayModelOf(chain) === "" ? chain : chain.slice(0, -1), catalog)[0];
2142
- }
2143
- /** Split scalars from the row editors (rootChain / role entities / role rules). */
2144
1767
  function scalarsOf(config) {
2145
1768
  return {
2146
1769
  triggerCodes: [...config.triggerCodes],
2147
1770
  cooldownMs: config.cooldownMs,
2148
1771
  revertPolicy: config.revertPolicy,
2149
1772
  maxSwitchesPerStep: config.maxSwitchesPerStep,
2150
- alwaysModeRetryCap: config.alwaysModeRetryCap,
2151
- roleAutoMatch: config.roleAutoMatch,
2152
1773
  tz: config.tz ?? "Asia/Shanghai"
2153
1774
  };
2154
1775
  }
2155
- /**
2156
- * Assemble the full config the row editors + scalars describe. The rebuilt
2157
- * `roles.list` comes from the rows, with the schema-reserved
2158
- * `prompt`/`permissions` merged back from the last accepted config by role
2159
- * id (see {@link mergeRoleExtras}) so a save never silently drops them
2160
- * (T2 reviewer minor #2). `presets` (spec §9.4) follows the same rule at
2161
- * the top level: no presets UI this iteration (R-001 re-defer), so the
2162
- * draft carries the accepted value through untouched — a clean draft stays
2163
- * equal to the accepted config and a save never drops the key.
2164
- * `roleAutoMatch` follows the same rule (config-model mirror of `presets`):
2165
- * the draft carries the scalar's value through untouched. The scalar is
2166
- * ALWAYS defined — the gateway composition resolves the schema default
2167
- * `true` even for a legacy config that never declared the key — so the
2168
- * toggle always renders (default on) and a save persists the resolved value
2169
- * (AC-7 re-scope, PM decision 2026-08-17 Option A).
2170
- *
2171
- * All-day: rootChain is composed from the 默认降级链 editor's leading
2172
- * selectors plus the 默认模型 tail (exactly one official model — Flash or
2173
- * Pro XOR). While no tail is selected the ACCEPTED chain rides through
2174
- * untouched. `timeSlots` is rebuilt from the slot rows every render. `tz`
2175
- * is a card scalar: preset rows lock it to Asia/Shanghai; custom rows
2176
- * follow the selected timezone.
2177
- */
2178
- function assembleConfig(scalars, allDayModel, acceptedRootChain, allDayChainRow, roleRows, ruleRows, originalRoles, presets, roleAutoMatch, timeSlotRows) {
2179
- const list = mergeRoleExtras(roleRows, originalRoles);
2180
- const trailingChain = rowsToRootChain([allDayChainRow]);
2181
- const tz = resolvedSlotTz(timeSlotRows, scalars.tz);
2182
- return {
2183
- triggerCodes: [...scalars.triggerCodes],
2184
- rootChain: allDayModel === "" ? [...acceptedRootChain] : [...trailingChain, allDayModel],
2185
- roles: {
2186
- list,
2187
- rules: rowsToRules(ruleRows)
2188
- },
2189
- cooldownMs: scalars.cooldownMs,
2190
- revertPolicy: scalars.revertPolicy,
2191
- maxSwitchesPerStep: scalars.maxSwitchesPerStep,
2192
- alwaysModeRetryCap: scalars.alwaysModeRetryCap,
2193
- ...presets === void 0 ? {} : { presets },
2194
- roleAutoMatch,
2195
- timeSlots: rowsToTimeSlots(timeSlotRows),
2196
- tz
2197
- };
2198
- }
2199
- /**
2200
- * Badge label for one row's provenance (plan role-card-seeded-ux): every
2201
- * row states where its seed state comes from. The three reserved labels
2202
- * localize — `bundled`, `external` (the shared unnamed-producer slice;
2203
- * plan seeds-source-and-persona-width), and the operator's own rows —
2204
- * while a live seed declared under a registered set name renders that
2205
- * name VERBATIM (case-preserved — no case-folding). `null` = no badge: a
2206
- * currently-seeded row whose wire entry lacks `source` (gateway version
2207
- * skew) — the degradation path, never an error.
2208
- */
1776
+ /** Badge label for one row's provenance; `null` = no badge (version skew). */
2209
1777
  function seedBadgeLabel(seed, t) {
2210
1778
  if (seed === void 0) return t("roles.seedSource.user");
2211
1779
  if (seed.source === void 0) return null;
@@ -2215,105 +1783,66 @@ window.__ModuleLoader__.load({
2215
1783
  return seed.source;
2216
1784
  }
2217
1785
  /**
2218
- * Pre-save validation of the assembled draft (spec §8 / plan Task 3):
2219
- * role id format/reserved word/duplicates, undeclared rule role references
2220
- * (only reachable through the synthetic outside option — the dropdown
2221
- * itself constrains normal edits), and illegal selector entries in
2222
- * rootChain and role chains. Returns one localized message per violation,
2223
- * bucketed by the section that owns the offending field (主代理: allDay /
2224
- * timeSlots / slot* / tz / default model / default chain; 子代理: role* /
2225
- * rule*; 高级选项: trigger / cooldown / revert / always / roleAutoMatch —
2226
- * the scalars are never validated, so the advanced bucket stays empty
2227
- * today). A non-empty bucket blocks THAT section's Save write only
2228
- * (per-section saves, plan fallbacks-card-section-ux) — a sibling section
2229
- * with a clean bucket still saves. `persona` is free text and never
2230
- * validated.
2231
- *
2232
- * `seedInfo` is the live trimmed-id → seed-entry map derived from
2233
- * `state.seeds` (spec §9.4): the empty-chain block relaxes for seeded ids
2234
- * only (spec §9.6 / AC-3 — a seeded role's chain is legitimately empty by
2235
- * design, R4, and the row must stay persistable); non-seeded behavior is
2236
- * byte-identical.
1786
+ * Per-chain validation (named virtual chains): chain id format/duplicates +
1787
+ * label presence, the 默认模型 tail conformance, selector legality across
1788
+ * entries and slot rows, and the per-chain slot-row guards. Roles: id
1789
+ * format/reserved/duplicates. Advanced scalars are never validated.
2237
1790
  */
2238
- function validateDraft(draft, t, seedInfo) {
2239
- const errors = {
2240
- main: [],
2241
- sub: [],
2242
- advanced: []
2243
- };
2244
- const declaredIds = /* @__PURE__ */ new Set();
2245
- for (const role of draft.roles.list) {
2246
- if (!ROLE_ID_PATTERN.test(role.id)) errors.sub.push(t("validation.roleIdFormat", { id: role.id }));
2247
- if (role.id === "inherit") errors.sub.push(t("validation.roleIdReserved"));
2248
- if (declaredIds.has(role.id)) errors.sub.push(t("validation.roleIdDuplicate", { id: role.id }));
2249
- declaredIds.add(role.id);
2250
- for (const entry of role.chain ?? []) try {
1791
+ function validateDraft(chains, roles, t) {
1792
+ const chainBuckets = chains.map(() => []);
1793
+ const seenIds = /* @__PURE__ */ new Set();
1794
+ chains.forEach((chain, index) => {
1795
+ const bucket = chainBuckets[index];
1796
+ if (!CHAIN_ID_PATTERN.test(chain.id)) bucket.push(t("validation.chainIdFormat", { id: chain.id }));
1797
+ if (seenIds.has(chain.id)) bucket.push(t("validation.chainIdDuplicate", { id: chain.id }));
1798
+ seenIds.add(chain.id);
1799
+ if (chain.label === "") bucket.push(t("validation.chainLabelRequired", { id: chain.id }));
1800
+ const tail = chain.entries.length >= 1 ? chain.entries[chain.entries.length - 1] : void 0;
1801
+ if (tail === void 0 || !isOfficialAllDayId(tail)) bucket.push(t("validation.allDayRequired"));
1802
+ for (const entry of chain.entries) try {
2251
1803
  parseSelector(entry);
2252
1804
  } catch (error) {
2253
- errors.sub.push(t("validation.selector", {
1805
+ bucket.push(t("validation.selector", {
2254
1806
  entry,
2255
1807
  message: error.message
2256
1808
  }));
2257
1809
  }
2258
- if ((role.chain ?? []).length === 0 && !seedInfo.has(role.id.trim())) errors.sub.push(t("validation.roleChainRequired", { id: role.id }));
2259
- }
2260
- const allDayTail = draft.rootChain.length >= 1 ? draft.rootChain[draft.rootChain.length - 1] : void 0;
2261
- if (allDayTail === void 0 || !isOfficialAllDayId(allDayTail)) errors.main.push(t("validation.allDayRequired"));
2262
- for (const entry of draft.rootChain) try {
2263
- parseSelector(entry);
2264
- } catch (error) {
2265
- errors.main.push(t("validation.selector", {
2266
- entry,
2267
- message: error.message
2268
- }));
2269
- }
2270
- const seenSlotPresets = /* @__PURE__ */ new Set();
2271
- for (const row of draft.timeSlots ?? []) {
2272
- if (row.kind !== "preset" && row.kind !== "custom") errors.main.push(t("validation.slotKind"));
2273
- if (row.kind === "preset") {
2274
- if (typeof row.preset !== "string" || !SLOT_PRESET_IDS.includes(row.preset)) errors.main.push(t("validation.slotPresetUnknown", { preset: row.preset }));
2275
- else if (seenSlotPresets.has(row.preset)) errors.main.push(t("validation.slotPresetDuplicate", { preset: row.preset }));
2276
- else seenSlotPresets.add(row.preset);
2277
- if (row.start !== void 0 || row.end !== void 0 || row.days !== void 0 && row.days.length > 0) errors.main.push(t("validation.slotPresetFrozen"));
2278
- } else if (row.kind === "custom") {
2279
- if (typeof row.start !== "string" || typeof row.end !== "string" || !HHMM_RE.test(row.start) || !HHMM_RE.test(row.end)) errors.main.push(t("validation.slotWindow"));
2280
- if (row.days !== void 0 && row.days.some((day) => !Number.isInteger(day) || day < 0 || day > 6)) errors.main.push(t("validation.slotDays"));
2281
- }
2282
- for (const entry of row.chain) try {
2283
- parseSelector(entry);
2284
- } catch (error) {
2285
- errors.main.push(t("validation.selector", {
2286
- entry,
2287
- message: error.message
2288
- }));
1810
+ const seenSlotPresets = /* @__PURE__ */ new Set();
1811
+ for (const row of chain.timeSlots ?? []) {
1812
+ if (row.kind !== "preset" && row.kind !== "custom") bucket.push(t("validation.slotKind"));
1813
+ if (row.kind === "preset") {
1814
+ if (typeof row.preset !== "string" || !SLOT_PRESET_IDS.includes(row.preset)) bucket.push(t("validation.slotPresetUnknown", { preset: row.preset }));
1815
+ else if (seenSlotPresets.has(row.preset)) bucket.push(t("validation.slotPresetDuplicate", { preset: row.preset }));
1816
+ else seenSlotPresets.add(row.preset);
1817
+ if (row.start !== void 0 || row.end !== void 0 || row.days !== void 0 && row.days.length > 0) bucket.push(t("validation.slotPresetFrozen"));
1818
+ } else if (row.kind === "custom") {
1819
+ if (typeof row.start !== "string" || typeof row.end !== "string" || !HHMM_RE.test(row.start) || !HHMM_RE.test(row.end)) bucket.push(t("validation.slotWindow"));
1820
+ if (row.days !== void 0 && row.days.some((day) => !Number.isInteger(day) || day < 0 || day > 6)) bucket.push(t("validation.slotDays"));
1821
+ }
1822
+ for (const entry of row.chain) try {
1823
+ parseSelector(entry);
1824
+ } catch (error) {
1825
+ bucket.push(t("validation.selector", {
1826
+ entry,
1827
+ message: error.message
1828
+ }));
1829
+ }
1830
+ if (row.chain.length === 0) bucket.push(t("validation.slotChainRequired"));
2289
1831
  }
2290
- if (row.chain.length === 0) errors.main.push(t("validation.slotChainRequired"));
2291
- }
2292
- const validTargets = /* @__PURE__ */ new Set([...declaredIds, INHERIT_ROLE_ID]);
2293
- for (const rule of draft.roles.rules) if (!validTargets.has(rule.role)) errors.sub.push(t("validation.ruleRoleUndeclared", { role: rule.role }));
2294
- return errors;
2295
- }
2296
- /**
2297
- * The trimmed role ids that are validation failures (format / reserved word
2298
- * / duplicate) — drives the inline red border on the offending id input
2299
- * (live: the border tracks the draft, matching the live Save disabled
2300
- * gate). Derived once per render into a Set (qc3 F-3): a duplicate scan
2301
- * inside the render loop would be O(N²) per row; here the whole derivation
2302
- * is O(N) and each row's check is a single Set lookup. Selector errors stay
2303
- * on the banner only (plan Task 3 inline-scope rule).
2304
- */
2305
- function collectInvalidRoleIds(rows) {
2306
- const counts = /* @__PURE__ */ new Map();
2307
- for (const row of rows) {
2308
- const id = row.id.trim();
2309
- counts.set(id, (counts.get(id) ?? 0) + 1);
2310
- }
2311
- const invalid = /* @__PURE__ */ new Set();
2312
- for (const row of rows) {
2313
- const id = row.id.trim();
2314
- if (!ROLE_ID_PATTERN.test(id) || id === "inherit" || (counts.get(id) ?? 0) > 1) invalid.add(id);
1832
+ });
1833
+ const roleErrors = [];
1834
+ const declaredIds = /* @__PURE__ */ new Set();
1835
+ for (const role of roles) {
1836
+ if (!ROLE_ID_PATTERN.test(role.id)) roleErrors.push(t("validation.roleIdFormat", { id: role.id }));
1837
+ if (role.id === "inherit") roleErrors.push(t("validation.roleIdReserved"));
1838
+ if (declaredIds.has(role.id)) roleErrors.push(t("validation.roleIdDuplicate", { id: role.id }));
1839
+ declaredIds.add(role.id);
2315
1840
  }
2316
- return invalid;
1841
+ return {
1842
+ chains: chainBuckets,
1843
+ roles: roleErrors,
1844
+ advanced: []
1845
+ };
2317
1846
  }
2318
1847
  /** Parse a number input, clamped to a non-negative integer. */
2319
1848
  function parseCount(raw) {
@@ -2370,60 +1899,6 @@ window.__ModuleLoader__.load({
2370
1899
  });
2371
1900
  }
2372
1901
  /**
2373
- * The virtual all-fallbacks route the host carve-out recognizes (features.md):
2374
- * a subagent spawned on it is not allowlist-filtered, so it is the
2375
- * recommendation when the allowlist does not already carry it.
2376
- */
2377
- const VIRTUAL_ALL_ROUTE = {
2378
- provider: "FallbacksChain",
2379
- model: "Auto"
2380
- };
2381
- /**
2382
- * Parse one raw selector into an exact route; wildcard and unparseable
2383
- * entries resolve to `undefined` (they are not exact routes — never expanded,
2384
- * never counted). The selector parser is the single grammar authority.
2385
- */
2386
- function exactRouteOf(raw) {
2387
- let parsed;
2388
- try {
2389
- parsed = parseSelector(raw);
2390
- } catch {
2391
- return;
2392
- }
2393
- return parsed.model === void 0 ? void 0 : {
2394
- provider: parsed.provider,
2395
- model: parsed.model
2396
- };
2397
- }
2398
- /**
2399
- * The whole chain as one line (plan fallbacks-chain-reorder T2, user request):
2400
- * every non-blank entry in walk order joined with `>`, or `undefined` for an
2401
- * empty/blank chain so the caller keeps its own summary (a role's raw strategy
2402
- * token). The card ellipsizes long summaries; the full string rides `title`.
2403
- *
2404
- * With `allowedModels` (host policy enabled) exact entries the policy cannot
2405
- * use are dropped, so the title tells the truth about what will run; wildcard
2406
- * entries stay (their resolved routes are unknown client-side) and an
2407
- * unparseable entry is kept rather than hidden.
2408
- */
2409
- function chainSummary(selectors, allowedModels) {
2410
- const entries = selectors.map(selectorRowToRaw).filter((entry) => entry !== "");
2411
- const visible = allowedModels === void 0 ? entries : entries.filter((entry) => {
2412
- const route = exactRouteOf(entry);
2413
- return route === void 0 || routeOnAllowlist(route, allowedModels);
2414
- });
2415
- return visible.length === 0 ? void 0 : visible.join(" > ");
2416
- }
2417
- /** How many selected role-chain exact entries the host allowlist does not carry. */
2418
- function offAllowlistEntryCount(roles, allowedModels) {
2419
- let count = 0;
2420
- for (const role of roles) for (const entry of role.chain ?? []) {
2421
- const route = exactRouteOf(entry);
2422
- if (route !== void 0 && !routeOnAllowlist(route, allowedModels)) count += 1;
2423
- }
2424
- return count;
2425
- }
2426
- /**
2427
1902
  * Pure chain reorder (plan fallbacks-chain-reorder T1): a copy of `list` with
2428
1903
  * the entry at `from` re-inserted at `to`; an out-of-range or same-position
2429
1904
  * move returns `list` untouched (the end-clamping the keyboard and the drop
@@ -2444,9 +1919,8 @@ window.__ModuleLoader__.load({
2444
1919
  * icon. The visible provider/model field labels are gone — the two selects
2445
1920
  * carry their accessible name on `aria-label` (the visible labels repeated on
2446
1921
  * every row and cost a whole line).
2447
- * The GUI never offers a `provider/*` wildcard (root agent and role chains
2448
- * alike; provider-any matching lives in the role rules) — but `provider/*`
2449
- * stays a legal YAML entry, so a wildcard row read back from the server
1922
+ * The GUI never offers a `provider/*` wildcard — but `provider/*` stays a
1923
+ * legal YAML entry in any chain, so a wildcard row read back from the server
2450
1924
  * renders with the legacy-conversion hint and an enabled model select:
2451
1925
  * picking a model converts the row to an exact entry (the patch carries
2452
1926
  * `wildcard: false`). The provider options are the catalog providers
@@ -2688,11 +2162,9 @@ window.__ModuleLoader__.load({
2688
2162
  });
2689
2163
  }
2690
2164
  /**
2691
- * Render the Fallbacks settings card inside the plugin-config section —
2692
- * flat: the notices, the form, and the section action pairs directly under
2693
- * the page's plugin title/description, with no chrome of our own.
2694
- * @param props - slot-delivered injected dependencies and the synthesized t seat.
2695
- * @returns the card.
2165
+ * Render the Fallbacks settings card inside the plugin-config section
2166
+ * (named virtual chains): the 虚拟链 tab group, the 子代理 role cards and
2167
+ * the 高级选项 group, flat under the page's plugin title/description.
2696
2168
  */
2697
2169
  function FallbacksCard({ controller, useSnapshot, t }) {
2698
2170
  const state = useSnapshot((snapshot) => snapshot);
@@ -2700,51 +2172,39 @@ window.__ModuleLoader__.load({
2700
2172
  const snapshot = controller.store.getSnapshot();
2701
2173
  if (snapshot.status === "idle") controller.load();
2702
2174
  if (snapshot.catalogStatus === "idle") controller.loadCatalog();
2703
- if (snapshot.switchesStatus === "idle") controller.loadSwitches();
2704
2175
  }, [controller]);
2705
2176
  const [scalars, setScalars] = (0, react.useState)(() => scalarsOf(defaultFallbacksConfig));
2706
- const [allDayModel, setAllDayModel] = (0, react.useState)(() => allDayModelOf(defaultFallbacksConfig.rootChain));
2707
- const [allDayChainRow, setAllDayChainRow] = (0, react.useState)(() => allDayChainRowOf(defaultFallbacksConfig.rootChain, void 0));
2708
- const [timeSlotRows, setTimeSlotRows] = (0, react.useState)(() => timeSlotsToRows(defaultFallbacksConfig.timeSlots ?? []));
2177
+ const [chainDrafts, setChainDrafts] = (0, react.useState)(() => chainsToDrafts(defaultFallbacksConfig.chains));
2178
+ const [activeChain, setActiveChain] = (0, react.useState)(0);
2709
2179
  const [roleRows, setRoleRows] = (0, react.useState)(() => rolesToRows(defaultFallbacksConfig.roles.list));
2710
- const [ruleRows, setRuleRows] = (0, react.useState)(() => rulesToRows(defaultFallbacksConfig.roles.rules));
2711
2180
  const [presetToAdd, setPresetToAdd] = (0, react.useState)("");
2712
2181
  const [lastSaveSection, setLastSaveSection] = (0, react.useState)(null);
2182
+ const [lastSaveChain, setLastSaveChain] = (0, react.useState)(null);
2713
2183
  const [advancedOpen, setAdvancedOpen] = (0, react.useState)(false);
2184
+ const [menuOpen, setMenuOpen] = (0, react.useState)(false);
2714
2185
  const seededConfigKey = (0, react.useRef)(null);
2715
- const draft = assembleConfig(scalars, allDayModel, state.config.rootChain, allDayChainRow, roleRows, ruleRows, state.config.roles.list, state.config.presets, scalars.roleAutoMatch, timeSlotRows);
2716
- const hasEmptyRuleRows = ruleRows.some((row) => row.role === "");
2717
- const mainDirty = JSON.stringify([...draft.rootChain, draft.timeSlots]) !== JSON.stringify([...state.config.rootChain, state.config.timeSlots ?? []]);
2718
- const subDirty = hasEmptyRuleRows || JSON.stringify(draft.roles) !== JSON.stringify(state.config.roles);
2186
+ const firstSeedDone = (0, react.useRef)(false);
2187
+ const chains = chainDrafts.map(draftToChain);
2188
+ const chainsDirty = JSON.stringify(chains) !== JSON.stringify(state.config.chains);
2189
+ const acceptedChainOf = (id) => state.config.chains.find((chain) => chain.id === id);
2190
+ const perChainDirty = chainDrafts.map((chainDraft) => {
2191
+ const accepted = acceptedChainOf(chainDraft.id);
2192
+ return accepted === void 0 || JSON.stringify(draftToChain(chainDraft)) !== JSON.stringify(accepted);
2193
+ });
2194
+ const structureDirty = chainDrafts.length !== state.config.chains.length || chainDrafts.some((draft, index) => state.config.chains[index]?.id !== draft.id);
2195
+ const rolesDraft = { list: mergeRoleExtras(roleRows, state.config.roles.list) };
2196
+ const rolesDirty = JSON.stringify(rolesDraft) !== JSON.stringify(state.config.roles);
2719
2197
  const advancedDirty = JSON.stringify([
2720
- draft.triggerCodes,
2721
- draft.cooldownMs,
2722
- draft.revertPolicy,
2723
- draft.maxSwitchesPerStep,
2724
- draft.alwaysModeRetryCap,
2725
- draft.roleAutoMatch
2198
+ scalars.triggerCodes,
2199
+ scalars.cooldownMs,
2200
+ scalars.revertPolicy,
2201
+ scalars.maxSwitchesPerStep
2726
2202
  ]) !== JSON.stringify([
2727
2203
  state.config.triggerCodes,
2728
2204
  state.config.cooldownMs,
2729
2205
  state.config.revertPolicy,
2730
- state.config.maxSwitchesPerStep,
2731
- state.config.alwaysModeRetryCap,
2732
- state.config.roleAutoMatch
2206
+ state.config.maxSwitchesPerStep
2733
2207
  ]);
2734
- const enabledPolicy = state.subagentPolicy?.state === "enabled" ? state.subagentPolicy : void 0;
2735
- const allowedModels = enabledPolicy?.allowedModels;
2736
- const offAllowlistCount = enabledPolicy === void 0 ? 0 : offAllowlistEntryCount(draft.roles.list, enabledPolicy.allowedModels);
2737
- const virtualRouteAllowed = enabledPolicy !== void 0 && routeOnAllowlist(VIRTUAL_ALL_ROUTE, enabledPolicy.allowedModels);
2738
- const [syncStatus, setSyncStatus] = (0, react.useState)("idle");
2739
- const syncDirtyHintId = `fallbacks-policy-sync-dirty-${(0, react.useId)().replace(/:/g, "")}`;
2740
- const syncAllowlist = () => {
2741
- if (syncStatus === "pending") return;
2742
- setSyncStatus("pending");
2743
- controller.syncAllowlist().then((ok) => {
2744
- setSyncStatus(ok ? "ok" : "error");
2745
- });
2746
- };
2747
- const firstSeedDone = (0, react.useRef)(false);
2748
2208
  (0, react.useEffect)(() => {
2749
2209
  if (state.status !== "ready") return;
2750
2210
  const key = JSON.stringify(state.config);
@@ -2753,35 +2213,21 @@ window.__ModuleLoader__.load({
2753
2213
  const firstSeed = !firstSeedDone.current;
2754
2214
  firstSeedDone.current = true;
2755
2215
  const catalog = catalogOf(state);
2756
- if (firstSeed || !mainDirty) {
2757
- setAllDayModel(allDayModelOf(state.config.rootChain));
2758
- setAllDayChainRow(allDayChainRowOf(state.config.rootChain, catalog));
2759
- setTimeSlotRows(timeSlotsToRows(state.config.timeSlots ?? [], catalog));
2760
- setScalars((prev) => ({
2761
- ...prev,
2762
- tz: state.config.tz ?? "Asia/Shanghai"
2763
- }));
2764
- }
2765
- if (firstSeed || !subDirty) {
2766
- setRoleRows(rolesToRows(state.config.roles.list, catalog));
2767
- setRuleRows(rulesToRows(state.config.roles.rules, catalog));
2216
+ if (firstSeed || !chainsDirty) {
2217
+ const drafts = chainsToDrafts(state.config.chains, catalog);
2218
+ setChainDrafts(drafts);
2219
+ setActiveChain((index) => Math.min(index, Math.max(0, drafts.length - 1)));
2768
2220
  }
2221
+ if (firstSeed || !rolesDirty) setRoleRows(rolesToRows(state.config.roles.list));
2769
2222
  if (firstSeed || !advancedDirty) setScalars((prev) => ({
2770
2223
  ...prev,
2771
2224
  triggerCodes: [...state.config.triggerCodes],
2772
2225
  cooldownMs: state.config.cooldownMs,
2773
2226
  revertPolicy: state.config.revertPolicy,
2774
2227
  maxSwitchesPerStep: state.config.maxSwitchesPerStep,
2775
- alwaysModeRetryCap: state.config.alwaysModeRetryCap,
2776
- roleAutoMatch: state.config.roleAutoMatch
2228
+ tz: state.config.tz ?? "Asia/Shanghai"
2777
2229
  }));
2778
- }, [
2779
- state.status,
2780
- state.config,
2781
- mainDirty,
2782
- subDirty,
2783
- advancedDirty
2784
- ]);
2230
+ }, [state.status, state.config]);
2785
2231
  const updateScalars = (mutator) => {
2786
2232
  setScalars((prev) => {
2787
2233
  const next = {
@@ -2792,139 +2238,195 @@ window.__ModuleLoader__.load({
2792
2238
  return next;
2793
2239
  });
2794
2240
  };
2241
+ const patchActiveDraft = (mutator) => {
2242
+ setChainDrafts((drafts) => drafts.map((draft, index) => index === activeChain ? mutator({
2243
+ ...draft,
2244
+ slots: draft.slots.map((row) => ({
2245
+ ...row,
2246
+ selectors: row.selectors.map((s) => ({ ...s }))
2247
+ })),
2248
+ selectors: draft.selectors.map((s) => ({ ...s }))
2249
+ }) : draft));
2250
+ };
2795
2251
  const updateTimeSlotRow = (index, patch) => {
2796
- setTimeSlotRows((rows) => {
2797
- const next = rows.map((row) => ({ ...row }));
2798
- next[index] = {
2799
- ...next[index],
2252
+ patchActiveDraft((draft) => {
2253
+ const slots = draft.slots.map((row) => ({ ...row }));
2254
+ slots[index] = {
2255
+ ...slots[index],
2800
2256
  ...patch
2801
2257
  };
2802
- return next;
2258
+ return {
2259
+ ...draft,
2260
+ slots
2261
+ };
2803
2262
  });
2804
2263
  };
2805
2264
  const updateTimeSlotSelector = (rowIndex, selectorIndex, patch) => {
2806
- setTimeSlotRows((rows) => {
2807
- const next = rows.map((row) => ({
2265
+ patchActiveDraft((draft) => {
2266
+ const slots = draft.slots.map((row) => ({
2808
2267
  ...row,
2809
2268
  selectors: row.selectors.map((selector) => ({ ...selector }))
2810
2269
  }));
2811
- const selectors = next[rowIndex].selectors;
2270
+ const selectors = slots[rowIndex].selectors;
2812
2271
  selectors[selectorIndex] = {
2813
2272
  ...selectors[selectorIndex],
2814
2273
  ...patch
2815
2274
  };
2816
- return next;
2275
+ return {
2276
+ ...draft,
2277
+ slots
2278
+ };
2817
2279
  });
2818
2280
  };
2819
2281
  const addTimeSlotSelector = (rowIndex) => {
2820
- setTimeSlotRows((rows) => rows.map((row, index) => index === rowIndex ? {
2821
- ...row,
2822
- selectors: [...row.selectors, {
2823
- wildcard: false,
2824
- provider: null,
2825
- model: null
2826
- }]
2827
- } : row));
2282
+ patchActiveDraft((draft) => ({
2283
+ ...draft,
2284
+ slots: draft.slots.map((row, index) => index === rowIndex ? {
2285
+ ...row,
2286
+ selectors: [...row.selectors, {
2287
+ wildcard: false,
2288
+ provider: null,
2289
+ model: null
2290
+ }]
2291
+ } : row)
2292
+ }));
2828
2293
  };
2829
2294
  const removeTimeSlotSelector = (rowIndex, selectorIndex) => {
2830
- setTimeSlotRows((rows) => rows.map((row, index) => index === rowIndex ? {
2831
- ...row,
2832
- selectors: row.selectors.filter((_, sIndex) => sIndex !== selectorIndex)
2833
- } : row));
2295
+ patchActiveDraft((draft) => ({
2296
+ ...draft,
2297
+ slots: draft.slots.map((row, index) => index === rowIndex ? {
2298
+ ...row,
2299
+ selectors: row.selectors.filter((_, sIndex) => sIndex !== selectorIndex)
2300
+ } : row)
2301
+ }));
2834
2302
  };
2835
2303
  const addPresetSlotRow = () => {
2836
2304
  if (presetToAdd === "") return;
2837
- if ((presetToAdd === "glm-peak" || presetToAdd === "glm-valley") && !glmConfigured) return;
2838
- setTimeSlotRows((rows) => [...rows, {
2839
- kind: "preset",
2840
- preset: presetToAdd,
2841
- start: "",
2842
- end: "",
2843
- days: [],
2844
- name: "",
2845
- collapsed: false,
2846
- selectors: []
2847
- }]);
2305
+ patchActiveDraft((draft) => ({
2306
+ ...draft,
2307
+ slots: [...draft.slots, {
2308
+ kind: "preset",
2309
+ preset: presetToAdd,
2310
+ start: "",
2311
+ end: "",
2312
+ days: [],
2313
+ name: "",
2314
+ collapsed: false,
2315
+ selectors: []
2316
+ }]
2317
+ }));
2848
2318
  setPresetToAdd("");
2849
2319
  };
2850
2320
  const addCustomSlotRow = () => {
2851
- setTimeSlotRows((rows) => [...rows, {
2852
- kind: "custom",
2853
- start: "",
2854
- end: "",
2855
- days: [],
2856
- name: "",
2857
- collapsed: false,
2858
- selectors: []
2859
- }]);
2321
+ patchActiveDraft((draft) => ({
2322
+ ...draft,
2323
+ slots: [...draft.slots, {
2324
+ kind: "custom",
2325
+ start: "",
2326
+ end: "",
2327
+ days: [],
2328
+ name: "",
2329
+ collapsed: false,
2330
+ selectors: []
2331
+ }]
2332
+ }));
2860
2333
  };
2861
2334
  const removeTimeSlotRow = (index) => {
2862
- setTimeSlotRows((rows) => rows.filter((_, rowIndex) => rowIndex !== index));
2335
+ patchActiveDraft((draft) => ({
2336
+ ...draft,
2337
+ slots: draft.slots.filter((_, rowIndex) => rowIndex !== index)
2338
+ }));
2863
2339
  };
2864
2340
  const moveTimeSlotRow = (index, delta) => {
2865
- setTimeSlotRows((rows) => {
2341
+ patchActiveDraft((draft) => {
2866
2342
  const target = index + delta;
2867
- if (target < 0 || target >= rows.length) return rows;
2868
- const next = rows.map((row) => ({ ...row }));
2869
- const moved = next[index];
2870
- next[index] = next[target];
2871
- next[target] = moved;
2872
- return next;
2343
+ if (target < 0 || target >= draft.slots.length) return draft;
2344
+ const slots = [...draft.slots];
2345
+ const moved = slots[index];
2346
+ slots[index] = slots[target];
2347
+ slots[target] = moved;
2348
+ return {
2349
+ ...draft,
2350
+ slots
2351
+ };
2873
2352
  });
2874
2353
  };
2875
2354
  const [draggedSlotIndex, setDraggedSlotIndex] = (0, react.useState)(null);
2876
2355
  const [overSlotIndex, setOverSlotIndex] = (0, react.useState)(null);
2877
2356
  const reorderTimeSlotRow = (from, to) => {
2878
- setTimeSlotRows((rows) => {
2879
- if (from === to || from < 0 || to < 0 || from >= rows.length || to >= rows.length) return rows;
2880
- const next = rows.map((row) => ({ ...row }));
2881
- const [moved] = next.splice(from, 1);
2882
- next.splice(to, 0, moved);
2883
- return next;
2357
+ patchActiveDraft((draft) => {
2358
+ const slots = [...draft.slots];
2359
+ if (from === to || from < 0 || to < 0 || from >= slots.length || to >= slots.length) return draft;
2360
+ const [moved] = slots.splice(from, 1);
2361
+ slots.splice(to, 0, moved);
2362
+ return {
2363
+ ...draft,
2364
+ slots
2365
+ };
2884
2366
  });
2885
2367
  };
2886
- const updateAllDayChainSelector = (selectorIndex, patch) => {
2887
- setAllDayChainRow((row) => ({
2888
- ...row,
2889
- selectors: row.selectors.map((selector, index) => index === selectorIndex ? {
2368
+ const moveSlotSelector = (rowIndex, from, to) => {
2369
+ patchActiveDraft((draft) => ({
2370
+ ...draft,
2371
+ slots: draft.slots.map((slot, slotIndex) => slotIndex === rowIndex ? {
2372
+ ...slot,
2373
+ selectors: moveItem(slot.selectors, from, to)
2374
+ } : slot)
2375
+ }));
2376
+ };
2377
+ const updateChainSelector = (selectorIndex, patch) => {
2378
+ patchActiveDraft((draft) => {
2379
+ const selectors = draft.selectors.map((selector, index) => index === selectorIndex ? {
2890
2380
  ...selector,
2891
2381
  ...patch
2892
- } : selector)
2893
- }));
2382
+ } : selector);
2383
+ return {
2384
+ ...draft,
2385
+ selectors
2386
+ };
2387
+ });
2894
2388
  };
2895
- const addAllDayChainSelector = () => {
2896
- setAllDayChainRow((row) => ({
2897
- ...row,
2898
- selectors: [...row.selectors, {
2389
+ const addChainSelector = () => {
2390
+ patchActiveDraft((draft) => ({
2391
+ ...draft,
2392
+ selectors: [...draft.selectors, {
2899
2393
  wildcard: false,
2900
2394
  provider: null,
2901
2395
  model: null
2902
2396
  }]
2903
2397
  }));
2904
2398
  };
2905
- const removeAllDayChainSelector = (selectorIndex) => {
2906
- setAllDayChainRow((row) => ({
2907
- ...row,
2908
- selectors: row.selectors.filter((_, index) => index !== selectorIndex)
2399
+ const removeChainSelector = (selectorIndex) => {
2400
+ patchActiveDraft((draft) => ({
2401
+ ...draft,
2402
+ selectors: draft.selectors.filter((_, index) => index !== selectorIndex)
2909
2403
  }));
2910
2404
  };
2911
- const reorderAllDayChainSelector = (from, to) => {
2912
- setAllDayChainRow((row) => ({
2913
- ...row,
2914
- selectors: moveItem(row.selectors, from, to)
2405
+ const reorderChainSelector = (from, to) => {
2406
+ patchActiveDraft((draft) => ({
2407
+ ...draft,
2408
+ selectors: moveItem(draft.selectors, from, to)
2915
2409
  }));
2916
2410
  };
2917
- const reorderTimeSlotSelector = (slotIndex, from, to) => {
2918
- setTimeSlotRows((rows) => rows.map((row, index) => index === slotIndex ? {
2919
- ...row,
2920
- selectors: moveItem(row.selectors, from, to)
2921
- } : row));
2411
+ const addChain = () => {
2412
+ let added = chainDrafts.length;
2413
+ setChainDrafts((drafts) => {
2414
+ added = drafts.length;
2415
+ return [...drafts, {
2416
+ id: newChainId(drafts),
2417
+ label: "",
2418
+ selectors: [],
2419
+ slots: []
2420
+ }];
2421
+ });
2422
+ setActiveChain(added);
2423
+ setMenuOpen(false);
2922
2424
  };
2923
- const reorderRoleSelector = (roleIndex, from, to) => {
2924
- setRoleRows((rows) => rows.map((row, index) => index === roleIndex ? {
2925
- ...row,
2926
- selectors: moveItem(row.selectors, from, to)
2927
- } : row));
2425
+ const deleteActiveChain = () => {
2426
+ if (chainDrafts.length <= 1) return;
2427
+ setChainDrafts((drafts) => drafts.filter((_, index) => index !== activeChain));
2428
+ setActiveChain((index) => Math.max(0, index - 1));
2429
+ setMenuOpen(false);
2928
2430
  };
2929
2431
  const updateRoleRow = (index, patch) => {
2930
2432
  setRoleRows((rows) => {
@@ -2936,42 +2438,11 @@ window.__ModuleLoader__.load({
2936
2438
  return next;
2937
2439
  });
2938
2440
  };
2939
- const updateRoleSelector = (roleIndex, selectorIndex, patch) => {
2940
- setRoleRows((rows) => {
2941
- const next = rows.map((row) => ({
2942
- ...row,
2943
- selectors: row.selectors.map((selector) => ({ ...selector }))
2944
- }));
2945
- const selectors = next[roleIndex].selectors;
2946
- selectors[selectorIndex] = {
2947
- ...selectors[selectorIndex],
2948
- ...patch
2949
- };
2950
- return next;
2951
- });
2952
- };
2953
- const addRoleSelector = (roleIndex) => {
2954
- setRoleRows((rows) => rows.map((row, index) => index === roleIndex ? {
2955
- ...row,
2956
- selectors: [...row.selectors, {
2957
- wildcard: false,
2958
- provider: null,
2959
- model: null
2960
- }]
2961
- } : row));
2962
- };
2963
- const removeRoleSelector = (roleIndex, selectorIndex) => {
2964
- setRoleRows((rows) => rows.map((row, index) => index === roleIndex ? {
2965
- ...row,
2966
- selectors: row.selectors.filter((_, sIndex) => sIndex !== selectorIndex)
2967
- } : row));
2968
- };
2969
2441
  const addRole = () => {
2970
2442
  setRoleRows((rows) => [...rows, {
2971
2443
  id: "",
2972
2444
  persona: "",
2973
- selectors: [],
2974
- fallback: "inherit-root",
2445
+ chain: "",
2975
2446
  collapsed: true,
2976
2447
  personaOpen: false
2977
2448
  }]);
@@ -2979,146 +2450,145 @@ window.__ModuleLoader__.load({
2979
2450
  const removeRole = (index) => {
2980
2451
  setRoleRows((rows) => rows.filter((_, rowIndex) => rowIndex !== index));
2981
2452
  };
2982
- const updateRuleRow = (index, patch) => {
2983
- setRuleRows((rows) => {
2984
- const next = rows.map((row) => ({ ...row }));
2985
- next[index] = {
2986
- ...next[index],
2987
- ...patch
2988
- };
2989
- return next;
2990
- });
2991
- };
2992
2453
  const saving = state.status === "saving";
2993
2454
  const writable = state.writable;
2994
2455
  const advancedVisible = advancedOpen || !writable;
2995
2456
  const cooldownMsTip = `${t("cooldownMs.tooltip")} ${t("defaults.prefix")}: ${defaultFallbacksConfig.cooldownMs}`;
2996
2457
  const maxSwitchesPerStepTip = `${t("maxSwitchesPerStep.tooltip")} ${t("defaults.prefix")}: ${defaultFallbacksConfig.maxSwitchesPerStep}`;
2997
- const alwaysModeRetryCapTip = `${t("alwaysModeRetryCap.tooltip")} ${t("defaults.prefix")}: ${defaultFallbacksConfig.alwaysModeRetryCap}`;
2998
2458
  const unknownCodes = scalars.triggerCodes.filter((code) => !KNOWN_TRIGGER_CODES.includes(code));
2999
- const presetsPresent = timeSlotRows.some((row) => row.kind === "preset");
2459
+ const presetsPresent = (chainDrafts[activeChain]?.slots ?? []).some((row) => row.kind === "preset");
3000
2460
  const glmConfigured = state.configuredProviders.some((entry) => entry.provider === "zai-coding-cn");
3001
- const slotState = resolveSlotState(state.config, /* @__PURE__ */ new Date(), scalars.tz);
3002
- const activeSlotIndex = slotState.winner === "all-day" ? -1 : state.config.timeSlots?.indexOf(slotState.winner) ?? -1;
3003
- const roleOptions = ruleRoleOptions({ list: roleRows });
2461
+ const acceptedChain = chainDrafts[activeChain] === void 0 ? void 0 : acceptedChainOf(chainDrafts[activeChain].id);
2462
+ const activeSlotIndex = (() => {
2463
+ if (acceptedChain === void 0) return -1;
2464
+ const winner = resolveSlotState(acceptedChain, /* @__PURE__ */ new Date(), scalars.tz);
2465
+ if (winner.winner === "all-day") return -1;
2466
+ return acceptedChain.timeSlots?.indexOf(winner.winner) ?? -1;
2467
+ })();
2468
+ const invalidSlotRows = collectInvalidSlotRows(chainDrafts[activeChain]?.slots ?? []);
3004
2469
  const invalidRoleIds = collectInvalidRoleIds(roleRows);
3005
- const invalidSlotRows = collectInvalidSlotRows(timeSlotRows);
3006
2470
  const seedInfo = /* @__PURE__ */ new Map();
3007
2471
  for (const seed of state.seeds) seedInfo.set(seed.id.trim(), {
3008
2472
  overridden: seed.overridden,
3009
2473
  source: seed.source
3010
2474
  });
3011
- const liveErrors = validateDraft(draft, t, seedInfo);
3012
- const latestSwitch = state.switches[0];
3013
- let switchesLine;
3014
- if (state.switchesStatus === "error") switchesLine = t("status.switches.error", { message: state.switchesError });
3015
- else if (state.switchesStatus === "loading") switchesLine = t("loading");
3016
- else if (latestSwitch === void 0) switchesLine = t("status.switches.empty");
3017
- else {
3018
- const reasonKey = SWITCH_REASON_KEYS[latestSwitch.reason];
3019
- const params = {
3020
- count: String(state.switches.length),
3021
- from: `${latestSwitch.from.provider}/${latestSwitch.from.model}`,
3022
- to: `${latestSwitch.to.provider}/${latestSwitch.to.model}`,
3023
- role: latestSwitch.role,
3024
- reason: reasonKey === void 0 ? latestSwitch.reason : t(reasonKey)
3025
- };
3026
- switchesLine = latestSwitch.reason === "role-inject" ? t("status.switches.compact.roleInject", params) : t("status.switches.compact", params);
3027
- }
2475
+ const liveErrors = validateDraft(chains, rolesDraft.list, t);
2476
+ const activeErrors = liveErrors.chains[activeChain] ?? [];
2477
+ const anyChainErrors = liveErrors.chains.some((bucket) => bucket.length > 0);
2478
+ const activeDirty = (perChainDirty[activeChain] ?? false) || structureDirty;
3028
2479
  const catalogSeededSections = (0, react.useRef)({
3029
2480
  epoch: null,
3030
- main: false,
3031
- sub: false
2481
+ chains: false,
2482
+ roles: false
3032
2483
  });
3033
2484
  (0, react.useEffect)(() => {
3034
2485
  if (state.catalogStatus !== "ready") return;
3035
2486
  const seed = catalogSeededSections.current;
3036
2487
  if (seed.epoch !== state.catalogEpoch) {
3037
2488
  seed.epoch = state.catalogEpoch;
3038
- seed.main = false;
3039
- seed.sub = false;
2489
+ seed.chains = false;
2490
+ seed.roles = false;
3040
2491
  }
3041
2492
  const catalog = catalogOf(state);
3042
- if (!mainDirty && !seed.main) {
3043
- seed.main = true;
3044
- setAllDayModel(allDayModelOf(state.config.rootChain));
3045
- setAllDayChainRow(allDayChainRowOf(state.config.rootChain, catalog));
3046
- setTimeSlotRows(timeSlotsToRows(state.config.timeSlots ?? [], catalog));
2493
+ if (!chainsDirty && !seed.chains) {
2494
+ seed.chains = true;
2495
+ setChainDrafts(chainsToDrafts(state.config.chains, catalog));
3047
2496
  }
3048
- if (!subDirty && !seed.sub) {
3049
- seed.sub = true;
3050
- setRoleRows(rolesToRows(state.config.roles.list, catalog));
3051
- setRuleRows(rulesToRows(state.config.roles.rules, catalog));
2497
+ if (!rolesDirty && !seed.roles) {
2498
+ seed.roles = true;
2499
+ setRoleRows(rolesToRows(state.config.roles.list));
3052
2500
  }
3053
2501
  }, [
3054
2502
  state.catalogStatus,
3055
2503
  state.catalogEpoch,
3056
- state.config,
3057
- mainDirty,
3058
- subDirty
2504
+ state.config
3059
2505
  ]);
3060
- const sectionPatch = (section) => {
3061
- const base = { ...state.config };
3062
- switch (section) {
3063
- case "main": return {
3064
- ...base,
3065
- rootChain: draft.rootChain,
3066
- timeSlots: draft.timeSlots,
3067
- tz: draft.tz
3068
- };
3069
- case "sub": return {
3070
- ...base,
3071
- roles: draft.roles
3072
- };
3073
- case "advanced": return {
3074
- ...base,
3075
- triggerCodes: draft.triggerCodes,
3076
- cooldownMs: draft.cooldownMs,
3077
- revertPolicy: draft.revertPolicy,
3078
- maxSwitchesPerStep: draft.maxSwitchesPerStep,
3079
- alwaysModeRetryCap: draft.alwaysModeRetryCap,
3080
- roleAutoMatch: draft.roleAutoMatch
3081
- };
3082
- }
2506
+ const chainsPatch = () => {
2507
+ const activeId = chainDrafts[activeChain]?.id;
2508
+ return chainDrafts.map((draft) => {
2509
+ if (draft.id === activeId) return draftToChain(draft);
2510
+ return acceptedChainOf(draft.id) ?? draftToChain(draft);
2511
+ });
3083
2512
  };
3084
2513
  const saveSection = (section) => {
3085
- if (liveErrors[section].length > 0 || section === "sub" && hasEmptyRuleRows) return;
3086
- setLastSaveSection(section);
3087
- controller.save(sectionPatch(section));
2514
+ if (section === "chains") {
2515
+ if (anyChainErrors) return;
2516
+ setLastSaveSection("chains");
2517
+ setLastSaveChain(activeChain);
2518
+ controller.save({
2519
+ ...state.config,
2520
+ chains: chainsPatch()
2521
+ });
2522
+ return;
2523
+ }
2524
+ if (section === "sub") {
2525
+ if (liveErrors.roles.length > 0) return;
2526
+ setLastSaveSection("sub");
2527
+ controller.save({
2528
+ ...state.config,
2529
+ roles: rolesDraft
2530
+ });
2531
+ return;
2532
+ }
2533
+ setLastSaveSection("advanced");
2534
+ controller.save({
2535
+ ...state.config,
2536
+ triggerCodes: scalars.triggerCodes,
2537
+ cooldownMs: scalars.cooldownMs,
2538
+ revertPolicy: scalars.revertPolicy,
2539
+ maxSwitchesPerStep: scalars.maxSwitchesPerStep
2540
+ });
3088
2541
  };
3089
- const discardSection = (section) => {
3090
- const catalog = catalogOf(state);
3091
- switch (section) {
3092
- case "main":
3093
- setAllDayModel(allDayModelOf(state.config.rootChain));
3094
- setAllDayChainRow(allDayChainRowOf(state.config.rootChain, catalog));
3095
- setTimeSlotRows(timeSlotsToRows(state.config.timeSlots ?? [], catalog));
3096
- setScalars((prev) => ({
3097
- ...prev,
3098
- tz: state.config.tz ?? "Asia/Shanghai"
3099
- }));
3100
- break;
3101
- case "sub":
3102
- setRoleRows(rolesToRows(state.config.roles.list, catalog));
3103
- setRuleRows(rulesToRows(state.config.roles.rules, catalog));
3104
- break;
3105
- case "advanced": setScalars((prev) => ({
3106
- ...prev,
3107
- triggerCodes: [...state.config.triggerCodes],
3108
- cooldownMs: state.config.cooldownMs,
3109
- revertPolicy: state.config.revertPolicy,
3110
- maxSwitchesPerStep: state.config.maxSwitchesPerStep,
3111
- alwaysModeRetryCap: state.config.alwaysModeRetryCap,
3112
- roleAutoMatch: state.config.roleAutoMatch
3113
- }));
2542
+ const discardChain = () => {
2543
+ const activeId = chainDrafts[activeChain]?.id;
2544
+ if (activeId === void 0) return;
2545
+ const accepted = acceptedChainOf(activeId);
2546
+ if (accepted === void 0) {
2547
+ setChainDrafts((drafts) => drafts.filter((_, index) => index !== activeChain));
2548
+ setActiveChain((index) => Math.max(0, index - 1));
2549
+ return;
3114
2550
  }
2551
+ const reverted = chainsToDrafts([accepted], catalogOf(state))[0];
2552
+ setChainDrafts((drafts) => drafts.map((draft, index) => index === activeChain ? reverted : draft));
2553
+ };
2554
+ const discardSection = (section) => {
2555
+ if (section === "sub") setRoleRows(rolesToRows(state.config.roles.list));
2556
+ else setScalars((prev) => ({
2557
+ ...prev,
2558
+ triggerCodes: [...state.config.triggerCodes],
2559
+ cooldownMs: state.config.cooldownMs,
2560
+ revertPolicy: state.config.revertPolicy,
2561
+ maxSwitchesPerStep: state.config.maxSwitchesPerStep
2562
+ }));
3115
2563
  };
3116
2564
  (0, react.useEffect)(() => {
3117
- if (state.status === "ready") setLastSaveSection(null);
2565
+ if (state.status === "ready") {
2566
+ setLastSaveSection(null);
2567
+ setLastSaveChain(null);
2568
+ }
3118
2569
  }, [state.status]);
3119
2570
  const degradedLatch = (0, react.useRef)(false);
3120
2571
  if (state.status === "ready") degradedLatch.current = !state.present;
3121
2572
  const degraded = state.status === "ready" ? !state.present : degradedLatch.current;
2573
+ const tabRefs = (0, react.useRef)([]);
2574
+ const onTabKeyDown = (event, index) => {
2575
+ const count = chainDrafts.length;
2576
+ if (count === 0) return;
2577
+ let target = null;
2578
+ if (event.key === "ArrowRight") target = (index + 1) % count;
2579
+ else if (event.key === "ArrowLeft") target = (index - 1 + count) % count;
2580
+ else if (event.key === "Home") target = 0;
2581
+ else if (event.key === "End") target = count - 1;
2582
+ if (target === null) return;
2583
+ event.preventDefault();
2584
+ setActiveChain(target);
2585
+ setMenuOpen(false);
2586
+ tabRefs.current[target].focus();
2587
+ };
2588
+ const activeDraft = chainDrafts[activeChain];
2589
+ const timeSlotRows = activeDraft?.slots ?? [];
2590
+ const tailError = activeErrors.includes(t("validation.allDayRequired")) ? t("validation.allDayRequired") : null;
2591
+ const slotErrors = activeErrors.filter((message) => message !== tailError);
3122
2592
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3123
2593
  className: FallbacksCard_module_css_default.root,
3124
2594
  children: [
@@ -3127,6 +2597,10 @@ window.__ModuleLoader__.load({
3127
2597
  role: "status",
3128
2598
  children: t("legacy.banner", { keys: state.legacyKeys.join(", ") })
3129
2599
  }),
2600
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2601
+ className: FallbacksCard_module_css_default.notice,
2602
+ children: t("virtualOnly.rule")
2603
+ }),
3130
2604
  state.status === "error" && state.error !== null && lastSaveSection === null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3131
2605
  className: FallbacksCard_module_css_default.noticeRow,
3132
2606
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
@@ -3158,471 +2632,695 @@ window.__ModuleLoader__.load({
3158
2632
  children: [
3159
2633
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h2", {
3160
2634
  className: FallbacksCard_module_css_default.sectionHeading,
3161
- id: "fallbacks-main-agent",
3162
- "aria-label": t("mainAgent.label"),
2635
+ id: "fallbacks-virtual-chains",
2636
+ "aria-label": t("virtualChain.label"),
3163
2637
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3164
2638
  className: FallbacksCard_module_css_default.sectionHeadingText,
3165
- children: t("mainAgent.label")
3166
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3167
- className: FallbacksCard_module_css_default.sectionActions,
3168
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3169
- type: "button",
3170
- className: FallbacksCard_module_css_default.discard,
3171
- disabled: !mainDirty || saving,
3172
- onClick: () => {
3173
- discardSection("main");
3174
- },
3175
- children: t("discard")
3176
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3177
- type: "button",
3178
- className: FallbacksCard_module_css_default.save,
3179
- disabled: !writable || saving || !mainDirty,
3180
- onClick: () => {
3181
- saveSection("main");
3182
- },
3183
- children: saving ? t("save.saving") : t("save")
3184
- })]
2639
+ children: t("virtualChain.label")
2640
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2641
+ className: FallbacksCard_module_css_default.hint,
2642
+ children: t("virtualChain.hint")
3185
2643
  })]
3186
2644
  }),
3187
- liveErrors.main.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3188
- className: FallbacksCard_module_css_default.error,
3189
- role: "alert",
3190
- children: `${t("validation.blocked")}${liveErrors.main.join("; ")}`
3191
- }),
3192
- lastSaveSection === "main" && state.status === "error" && state.error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3193
- className: FallbacksCard_module_css_default.error,
3194
- role: "alert",
3195
- children: t("error.generic", { message: state.error })
3196
- }),
3197
2645
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3198
- className: FallbacksCard_module_css_default.field,
3199
- role: "group",
3200
- "aria-labelledby": "fallbacks-time-slots",
2646
+ className: FallbacksCard_module_css_default.tabsWrap,
2647
+ "data-tabs-wrap": "",
3201
2648
  children: [
3202
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3203
- className: FallbacksCard_module_css_default.fieldLabel,
3204
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3205
- id: "fallbacks-time-slots",
3206
- children: t("timeSlots.label")
3207
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
3208
- label: t("timeSlots.tooltip"),
3209
- disabled: !writable
3210
- })]
2649
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2650
+ className: FallbacksCard_module_css_default.tabs,
2651
+ role: "tablist",
2652
+ "aria-label": t("virtualChain.label"),
2653
+ "data-tablist": "",
2654
+ children: [
2655
+ chainDrafts.map((chainDraft, index) => {
2656
+ const selected = index === activeChain;
2657
+ const dirty = perChainDirty[index] ?? false;
2658
+ const invalid = (liveErrors.chains[index] ?? []).length > 0;
2659
+ const name = (chainDraft.label === "" ? chainDraft.id : chainDraft.label) + (dirty ? t("chains.tab.dirty") : "") + (invalid ? t("chains.tab.error") : "");
2660
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2661
+ ref: (node) => {
2662
+ tabRefs.current[index] = node;
2663
+ },
2664
+ type: "button",
2665
+ role: "tab",
2666
+ "data-chain-tab": chainDraft.id,
2667
+ "aria-selected": selected,
2668
+ "aria-controls": `fallbacks-chain-panel-${chainDraft.id}`,
2669
+ tabIndex: selected ? 0 : -1,
2670
+ className: FallbacksCard_module_css_default.tab,
2671
+ "aria-label": name,
2672
+ onClick: () => {
2673
+ setActiveChain(index);
2674
+ setMenuOpen(false);
2675
+ },
2676
+ onKeyDown: (event) => {
2677
+ onTabKeyDown(event, index);
2678
+ },
2679
+ children: [
2680
+ chainDraft.label === "" ? chainDraft.id : chainDraft.label,
2681
+ dirty && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2682
+ className: FallbacksCard_module_css_default.dirtyPip,
2683
+ title: t("chains.tab.dirty"),
2684
+ "aria-hidden": "true",
2685
+ "data-dirty-pip": ""
2686
+ }),
2687
+ invalid && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2688
+ className: FallbacksCard_module_css_default.errorMark,
2689
+ "aria-hidden": "true",
2690
+ "data-error-mark": "",
2691
+ children: "!"
2692
+ })
2693
+ ]
2694
+ }, chainDraft.id);
2695
+ }),
2696
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2697
+ type: "button",
2698
+ className: FallbacksCard_module_css_default.tabAdd,
2699
+ title: t("chains.tab.add"),
2700
+ "aria-label": t("chains.tab.add"),
2701
+ disabled: !writable,
2702
+ onClick: addChain,
2703
+ "data-tab-add": "",
2704
+ children: "+"
2705
+ }),
2706
+ chainDrafts.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2707
+ className: FallbacksCard_module_css_default.tabMenu,
2708
+ "data-tab-menu": "",
2709
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2710
+ type: "button",
2711
+ className: FallbacksCard_module_css_default.menuBtn,
2712
+ "aria-haspopup": "true",
2713
+ "aria-expanded": menuOpen,
2714
+ "aria-label": t("chains.tab.menu"),
2715
+ "data-menu-btn": "",
2716
+ disabled: !writable,
2717
+ onClick: () => {
2718
+ setMenuOpen((open) => !open);
2719
+ },
2720
+ children: "⋯"
2721
+ }), menuOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2722
+ className: FallbacksCard_module_css_default.menuPanel,
2723
+ role: "menu",
2724
+ "aria-label": t("chains.tab.menu"),
2725
+ "data-menu-panel": "",
2726
+ children: [
2727
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [t("chains.tab.renameLabel", { id: activeDraft?.id ?? "" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2728
+ className: `${FallbacksCard_module_css_default.input} ${FallbacksCard_module_css_default.menuInput}`,
2729
+ value: activeDraft?.label ?? "",
2730
+ "data-menu-input": "",
2731
+ disabled: !writable,
2732
+ onChange: (event) => {
2733
+ patchActiveDraft((draft) => ({
2734
+ ...draft,
2735
+ label: event.target.value
2736
+ }));
2737
+ }
2738
+ })] }),
2739
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2740
+ className: FallbacksCard_module_css_default.menuActions,
2741
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2742
+ type: "button",
2743
+ className: FallbacksCard_module_css_default.outlineButton,
2744
+ onClick: () => {
2745
+ setMenuOpen(false);
2746
+ },
2747
+ "data-menu-apply": "",
2748
+ children: t("chains.tab.renameDone")
2749
+ })
2750
+ }),
2751
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2752
+ type: "button",
2753
+ className: FallbacksCard_module_css_default.menuItemDanger,
2754
+ disabled: chainDrafts.length <= 1,
2755
+ onClick: deleteActiveChain,
2756
+ "data-menu-delete": "",
2757
+ children: chainDrafts.length <= 1 ? t("chains.tab.deleteDisabled") : t("chains.tab.delete")
2758
+ })
2759
+ ]
2760
+ })]
2761
+ })
2762
+ ]
3211
2763
  }),
3212
2764
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3213
- className: FallbacksCard_module_css_default.hint,
3214
- children: t("timeSlots.hint")
2765
+ className: FallbacksCard_module_css_default.fadeL,
2766
+ "aria-hidden": "true"
3215
2767
  }),
3216
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3217
- className: FallbacksCard_module_css_default.list,
3218
- children: timeSlotRows.map((row, index) => {
3219
- const invalidWindow = invalidSlotRows.has(index);
3220
- const chainEmpty = row.selectors.every((selector) => selectorRowToRaw(selector) === "");
3221
- const firstModel = row.selectors.map(selectorRowToRaw).find((entry) => entry !== "");
3222
- const slotExpanded = !row.collapsed || !writable;
3223
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3224
- className: `${FallbacksCard_module_css_default.editorCard} ${draggedSlotIndex === index ? FallbacksCard_module_css_default.slotCardDragging : ""} ${overSlotIndex === index && draggedSlotIndex !== null && draggedSlotIndex !== index ? FallbacksCard_module_css_default.slotCardOver : ""}`,
3225
- onDragOver: (event) => {
3226
- if (draggedSlotIndex === null) return;
3227
- event.preventDefault();
3228
- if (overSlotIndex !== index) setOverSlotIndex(index);
3229
- },
3230
- onDrop: (event) => {
3231
- event.preventDefault();
3232
- const from = draggedSlotIndex;
3233
- setDraggedSlotIndex(null);
3234
- setOverSlotIndex(null);
3235
- if (from !== null && from !== index) reorderTimeSlotRow(from, index);
3236
- },
3237
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3238
- className: FallbacksCard_module_css_default.collapseRow,
3239
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3240
- type: "button",
3241
- className: FallbacksCard_module_css_default.collapseToggle,
3242
- "aria-expanded": slotExpanded,
3243
- "aria-label": t(slotExpanded ? "timeSlots.collapse" : "timeSlots.expand"),
3244
- disabled: !writable,
3245
- onClick: () => {
3246
- updateTimeSlotRow(index, { collapsed: !row.collapsed });
3247
- },
3248
- children: [
3249
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronDownIcon, { className: slotExpanded ? `${FallbacksCard_module_css_default.chevron} ${FallbacksCard_module_css_default.chevronOpen}` : FallbacksCard_module_css_default.chevron }),
3250
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3251
- className: FallbacksCard_module_css_default.collapseTitle,
3252
- children: row.kind === "preset" ? t(`timeSlots.preset.${row.preset}.label`) : row.name !== "" ? row.name : `custom ${row.start}-${row.end}`
3253
- }),
3254
- row.kind === "preset" && (row.preset === "liang-peak" || row.preset === "glm-peak") && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3255
- className: `${FallbacksCard_module_css_default.slotTag} ${FallbacksCard_module_css_default.slotTagHighCost}`,
3256
- children: t("timeSlots.preset.highCost")
3257
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3258
- className: `${FallbacksCard_module_css_default.slotTag} ${FallbacksCard_module_css_default.slotTagMultiplier}`,
3259
- children: t("timeSlots.preset.multiplier", { n: row.preset === "liang-peak" ? "2" : "3" })
3260
- })] }),
3261
- activeSlotIndex === index && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3262
- className: `${FallbacksCard_module_css_default.slotTag} ${FallbacksCard_module_css_default.slotTagActive}`,
3263
- children: t("timeSlots.active")
3264
- }),
3265
- firstModel !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3266
- className: FallbacksCard_module_css_default.collapseMeta,
3267
- children: firstModel
3268
- })
3269
- ]
3270
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3271
- type: "button",
3272
- className: FallbacksCard_module_css_default.dragHandle,
3273
- draggable: writable,
3274
- "data-tip": t("timeSlots.drag"),
3275
- "aria-label": t("timeSlots.drag"),
3276
- disabled: !writable,
3277
- onDragStart: () => {
3278
- if (!writable) return;
3279
- setDraggedSlotIndex(index);
3280
- setOverSlotIndex(index);
2768
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2769
+ className: FallbacksCard_module_css_default.fadeR,
2770
+ "aria-hidden": "true"
2771
+ })
2772
+ ]
2773
+ }),
2774
+ chainDrafts.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2775
+ className: FallbacksCard_module_css_default.emptyState,
2776
+ "data-empty-state": "",
2777
+ children: [
2778
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t("chains.empty.title") }),
2779
+ t("chains.empty.body"),
2780
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("br", {}),
2781
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("br", {}),
2782
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2783
+ type: "button",
2784
+ className: FallbacksCard_module_css_default.outlineButton,
2785
+ onClick: addChain,
2786
+ "data-empty-create": "",
2787
+ children: t("chains.empty.create")
2788
+ })
2789
+ ]
2790
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2791
+ className: FallbacksCard_module_css_default.chainBody,
2792
+ role: "tabpanel",
2793
+ id: `fallbacks-chain-panel-${activeDraft.id}`,
2794
+ "aria-labelledby": `fallbacks-chain-tab-${activeDraft.id}`,
2795
+ "data-chain-panel": "",
2796
+ children: [
2797
+ slotErrors.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2798
+ className: FallbacksCard_module_css_default.error,
2799
+ role: "alert",
2800
+ children: `${t("validation.blocked")}${slotErrors.join("; ")}`
2801
+ }),
2802
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2803
+ className: FallbacksCard_module_css_default.field,
2804
+ role: "group",
2805
+ "aria-labelledby": `fallbacks-chain-name-${activeDraft.id}`,
2806
+ children: [
2807
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2808
+ className: FallbacksCard_module_css_default.fieldLabel,
2809
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2810
+ id: `fallbacks-chain-name-${activeDraft.id}`,
2811
+ children: t("chains.name.label")
2812
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
2813
+ label: t("chains.name.tooltip"),
2814
+ disabled: !writable
2815
+ })]
2816
+ }),
2817
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2818
+ className: FallbacksCard_module_css_default.input,
2819
+ value: activeDraft.label,
2820
+ placeholder: activeDraft.id,
2821
+ "aria-label": t("chains.name.label"),
2822
+ "data-chain-name": "",
2823
+ disabled: !writable,
2824
+ onChange: (event) => {
2825
+ patchActiveDraft((draft) => ({
2826
+ ...draft,
2827
+ label: event.target.value
2828
+ }));
2829
+ }
2830
+ }),
2831
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2832
+ className: FallbacksCard_module_css_default.hint,
2833
+ children: t("chains.name.hint", { id: activeDraft.id })
2834
+ })
2835
+ ]
2836
+ }),
2837
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2838
+ className: FallbacksCard_module_css_default.field,
2839
+ role: "group",
2840
+ "aria-labelledby": `fallbacks-time-slots-${activeDraft.id}`,
2841
+ children: [
2842
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2843
+ className: FallbacksCard_module_css_default.fieldLabel,
2844
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2845
+ id: `fallbacks-time-slots-${activeDraft.id}`,
2846
+ children: t("timeSlots.label")
2847
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
2848
+ label: t("timeSlots.tooltip"),
2849
+ disabled: !writable
2850
+ })]
2851
+ }),
2852
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2853
+ className: FallbacksCard_module_css_default.hint,
2854
+ children: t("timeSlots.hint")
2855
+ }),
2856
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2857
+ className: FallbacksCard_module_css_default.list,
2858
+ children: timeSlotRows.map((row, index) => {
2859
+ const invalidWindow = invalidSlotRows.has(index);
2860
+ const chainEmpty = row.selectors.every((selector) => selectorRowToRaw(selector) === "");
2861
+ const firstModel = row.selectors.map(selectorRowToRaw).find((entry) => entry !== "");
2862
+ const slotExpanded = !row.collapsed || !writable;
2863
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2864
+ className: `${FallbacksCard_module_css_default.editorCard} ${draggedSlotIndex === index ? FallbacksCard_module_css_default.slotCardDragging : ""} ${overSlotIndex === index && draggedSlotIndex !== null && draggedSlotIndex !== index ? FallbacksCard_module_css_default.slotCardOver : ""}`,
2865
+ onDragOver: (event) => {
2866
+ if (draggedSlotIndex === null) return;
2867
+ event.preventDefault();
2868
+ if (overSlotIndex !== index) setOverSlotIndex(index);
3281
2869
  },
3282
- onDragEnd: () => {
2870
+ onDrop: (event) => {
2871
+ event.preventDefault();
2872
+ const from = draggedSlotIndex;
3283
2873
  setDraggedSlotIndex(null);
3284
2874
  setOverSlotIndex(null);
2875
+ if (from !== null && from !== index) reorderTimeSlotRow(from, index);
3285
2876
  },
3286
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GripIcon, {})
3287
- })]
3288
- }), slotExpanded && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
3289
- row.kind === "preset" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
3290
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3291
- className: FallbacksCard_module_css_default.ruleGrid,
3292
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3293
- className: FallbacksCard_module_css_default.ruleCell,
3294
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3295
- className: FallbacksCard_module_css_default.ruleCellLabel,
3296
- children: t("timeSlots.preset.name")
3297
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3298
- className: FallbacksCard_module_css_default.slotPresetName,
3299
- children: t(`timeSlots.preset.${row.preset}.label`)
3300
- })]
3301
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3302
- className: FallbacksCard_module_css_default.ruleCell,
3303
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3304
- className: FallbacksCard_module_css_default.ruleCellLabel,
3305
- children: t("timeSlots.preset.windowLabel")
3306
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3307
- className: FallbacksCard_module_css_default.hint,
3308
- children: t(`timeSlots.preset.${row.preset}.window`)
3309
- })]
3310
- })]
3311
- }),
3312
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3313
- className: FallbacksCard_module_css_default.hint,
3314
- children: t("timeSlots.preset.chainsOnly")
3315
- }),
3316
- (row.preset === "glm-peak" || row.preset === "glm-valley") && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3317
- className: FallbacksCard_module_css_default.hint,
3318
- children: t("timeSlots.preset.glm.note")
3319
- })
3320
- ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
3321
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3322
- className: FallbacksCard_module_css_default.field,
3323
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3324
- className: FallbacksCard_module_css_default.ruleCellLabel,
3325
- children: t("timeSlots.name")
3326
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3327
- className: FallbacksCard_module_css_default.input,
3328
- value: row.name,
3329
- placeholder: t("timeSlots.name"),
3330
- "aria-label": t("timeSlots.name"),
2877
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2878
+ className: FallbacksCard_module_css_default.collapseRow,
2879
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2880
+ type: "button",
2881
+ className: FallbacksCard_module_css_default.collapseToggle,
2882
+ "aria-expanded": slotExpanded,
2883
+ "aria-label": t(slotExpanded ? "timeSlots.collapse" : "timeSlots.expand"),
3331
2884
  disabled: !writable,
3332
- onChange: (event) => {
3333
- updateTimeSlotRow(index, { name: event.target.value });
3334
- }
3335
- })]
3336
- }),
3337
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3338
- className: FallbacksCard_module_css_default.field,
3339
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3340
- className: FallbacksCard_module_css_default.ruleCellLabel,
3341
- children: t("timeSlots.tz.label")
3342
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3343
- className: FallbacksCard_module_css_default.hint,
3344
- "aria-label": t("timeSlots.tz.label"),
3345
- children: tzDisplayLabel(presetsPresent ? "Asia/Shanghai" : hostTimeZone())
3346
- })]
3347
- }),
3348
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3349
- className: FallbacksCard_module_css_default.ruleGrid,
3350
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3351
- className: FallbacksCard_module_css_default.ruleCell,
3352
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3353
- className: FallbacksCard_module_css_default.ruleCellLabel,
3354
- children: t("timeSlots.start")
3355
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3356
- className: `${FallbacksCard_module_css_default.input} ${invalidWindow ? FallbacksCard_module_css_default.inputInvalid : ""}`,
3357
- value: row.start,
3358
- placeholder: "09:00",
3359
- "aria-label": t("timeSlots.start"),
3360
- disabled: !writable,
3361
- onChange: (event) => {
3362
- updateTimeSlotRow(index, { start: event.target.value });
3363
- }
3364
- })]
3365
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3366
- className: FallbacksCard_module_css_default.ruleCell,
3367
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3368
- className: FallbacksCard_module_css_default.ruleCellLabel,
3369
- children: t("timeSlots.end")
3370
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3371
- className: `${FallbacksCard_module_css_default.input} ${invalidWindow ? FallbacksCard_module_css_default.inputInvalid : ""}`,
3372
- value: row.end,
3373
- placeholder: "18:00",
3374
- "aria-label": t("timeSlots.end"),
3375
- disabled: !writable,
3376
- onChange: (event) => {
3377
- updateTimeSlotRow(index, { end: event.target.value });
3378
- }
3379
- })]
2885
+ onClick: () => {
2886
+ updateTimeSlotRow(index, { collapsed: !row.collapsed });
2887
+ },
2888
+ children: [
2889
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronDownIcon, { className: slotExpanded ? `${FallbacksCard_module_css_default.chevron} ${FallbacksCard_module_css_default.chevronOpen}` : FallbacksCard_module_css_default.chevron }),
2890
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2891
+ className: FallbacksCard_module_css_default.collapseTitle,
2892
+ children: row.kind === "preset" ? t(`timeSlots.preset.${row.preset}.label`) : row.name !== "" ? row.name : `custom ${row.start}-${row.end}`
2893
+ }),
2894
+ row.kind === "preset" && (row.preset === "liang-peak" || row.preset === "glm-peak") && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2895
+ className: `${FallbacksCard_module_css_default.slotTag} ${FallbacksCard_module_css_default.slotTagHighCost}`,
2896
+ children: t("timeSlots.preset.highCost")
2897
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2898
+ className: `${FallbacksCard_module_css_default.slotTag} ${FallbacksCard_module_css_default.slotTagMultiplier}`,
2899
+ children: t("timeSlots.preset.multiplier", { n: row.preset === "liang-peak" ? "2" : "3" })
2900
+ })] }),
2901
+ activeSlotIndex === index && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2902
+ className: `${FallbacksCard_module_css_default.slotTag} ${FallbacksCard_module_css_default.slotTagActive}`,
2903
+ children: t("timeSlots.active")
2904
+ }),
2905
+ firstModel !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2906
+ className: FallbacksCard_module_css_default.collapseMeta,
2907
+ children: firstModel
2908
+ })
2909
+ ]
2910
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2911
+ type: "button",
2912
+ className: FallbacksCard_module_css_default.dragHandle,
2913
+ draggable: writable,
2914
+ "data-tip": t("timeSlots.drag"),
2915
+ "aria-label": t("timeSlots.drag"),
2916
+ disabled: !writable,
2917
+ onDragStart: () => {
2918
+ if (!writable) return;
2919
+ setDraggedSlotIndex(index);
2920
+ setOverSlotIndex(index);
2921
+ },
2922
+ onDragEnd: () => {
2923
+ setDraggedSlotIndex(null);
2924
+ setOverSlotIndex(null);
2925
+ },
2926
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GripIcon, {})
3380
2927
  })]
3381
- }),
3382
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3383
- className: FallbacksCard_module_css_default.field,
3384
- children: [
2928
+ }), slotExpanded && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2929
+ row.kind === "preset" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2930
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2931
+ className: FallbacksCard_module_css_default.ruleGrid,
2932
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2933
+ className: FallbacksCard_module_css_default.ruleCell,
2934
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2935
+ className: FallbacksCard_module_css_default.ruleCellLabel,
2936
+ children: t("timeSlots.preset.name")
2937
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2938
+ className: FallbacksCard_module_css_default.slotPresetName,
2939
+ children: t(`timeSlots.preset.${row.preset}.label`)
2940
+ })]
2941
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2942
+ className: FallbacksCard_module_css_default.ruleCell,
2943
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2944
+ className: FallbacksCard_module_css_default.ruleCellLabel,
2945
+ children: t("timeSlots.preset.windowLabel")
2946
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2947
+ className: FallbacksCard_module_css_default.hint,
2948
+ children: t(`timeSlots.preset.${row.preset}.window`)
2949
+ })]
2950
+ })]
2951
+ }),
3385
2952
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3386
- className: FallbacksCard_module_css_default.ruleCellLabel,
3387
- children: t("timeSlots.days")
2953
+ className: FallbacksCard_module_css_default.hint,
2954
+ children: t("timeSlots.preset.chainsOnly")
2955
+ }),
2956
+ (row.preset === "glm-peak" || row.preset === "glm-valley") && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2957
+ className: FallbacksCard_module_css_default.hint,
2958
+ children: t("timeSlots.preset.glm.note")
2959
+ })
2960
+ ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2961
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2962
+ className: FallbacksCard_module_css_default.field,
2963
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2964
+ className: FallbacksCard_module_css_default.ruleCellLabel,
2965
+ children: t("timeSlots.name")
2966
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2967
+ className: FallbacksCard_module_css_default.input,
2968
+ value: row.name,
2969
+ placeholder: t("timeSlots.name"),
2970
+ "aria-label": t("timeSlots.name"),
2971
+ disabled: !writable,
2972
+ onChange: (event) => {
2973
+ updateTimeSlotRow(index, { name: event.target.value });
2974
+ }
2975
+ })]
2976
+ }),
2977
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2978
+ className: FallbacksCard_module_css_default.field,
2979
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2980
+ className: FallbacksCard_module_css_default.ruleCellLabel,
2981
+ children: t("timeSlots.tz.label")
2982
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2983
+ className: FallbacksCard_module_css_default.hint,
2984
+ "aria-label": t("timeSlots.tz.label"),
2985
+ children: tzDisplayLabel(presetsPresent ? "Asia/Shanghai" : hostTimeZone())
2986
+ })]
3388
2987
  }),
3389
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3390
- className: FallbacksCard_module_css_default.dayRow,
3391
- children: SLOT_WEEKDAYS.map((day, dayIndex) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3392
- className: FallbacksCard_module_css_default.dayCell,
3393
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3394
- type: "checkbox",
3395
- checked: row.days.includes(dayIndex),
2988
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2989
+ className: FallbacksCard_module_css_default.ruleGrid,
2990
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2991
+ className: FallbacksCard_module_css_default.ruleCell,
2992
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2993
+ className: FallbacksCard_module_css_default.ruleCellLabel,
2994
+ children: t("timeSlots.start")
2995
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2996
+ className: `${FallbacksCard_module_css_default.input} ${invalidWindow ? FallbacksCard_module_css_default.inputInvalid : ""}`,
2997
+ value: row.start,
2998
+ placeholder: "09:00",
2999
+ "aria-label": t("timeSlots.start"),
3000
+ disabled: !writable,
3001
+ onChange: (event) => {
3002
+ updateTimeSlotRow(index, { start: event.target.value });
3003
+ }
3004
+ })]
3005
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3006
+ className: FallbacksCard_module_css_default.ruleCell,
3007
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3008
+ className: FallbacksCard_module_css_default.ruleCellLabel,
3009
+ children: t("timeSlots.end")
3010
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3011
+ className: `${FallbacksCard_module_css_default.input} ${invalidWindow ? FallbacksCard_module_css_default.inputInvalid : ""}`,
3012
+ value: row.end,
3013
+ placeholder: "18:00",
3014
+ "aria-label": t("timeSlots.end"),
3396
3015
  disabled: !writable,
3397
- onChange: () => {
3398
- updateTimeSlotRow(index, { days: row.days.includes(dayIndex) ? row.days.filter((existing) => existing !== dayIndex) : [...row.days, dayIndex] });
3016
+ onChange: (event) => {
3017
+ updateTimeSlotRow(index, { end: event.target.value });
3399
3018
  }
3400
- }), t(`timeSlots.day.${day}`)]
3401
- }, day))
3019
+ })]
3020
+ })]
3402
3021
  }),
3403
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3022
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3023
+ className: FallbacksCard_module_css_default.field,
3024
+ children: [
3025
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3026
+ className: FallbacksCard_module_css_default.ruleCellLabel,
3027
+ children: t("timeSlots.days")
3028
+ }),
3029
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3030
+ className: FallbacksCard_module_css_default.dayRow,
3031
+ children: SLOT_WEEKDAYS.map((day, dayIndex) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3032
+ className: FallbacksCard_module_css_default.dayCell,
3033
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3034
+ type: "checkbox",
3035
+ checked: row.days.includes(dayIndex),
3036
+ disabled: !writable,
3037
+ onChange: () => {
3038
+ updateTimeSlotRow(index, { days: row.days.includes(dayIndex) ? row.days.filter((existing) => existing !== dayIndex) : [...row.days, dayIndex] });
3039
+ }
3040
+ }), t(`timeSlots.day.${day}`)]
3041
+ }, day))
3042
+ }),
3043
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3044
+ className: FallbacksCard_module_css_default.hint,
3045
+ children: t("timeSlots.days.hint")
3046
+ })
3047
+ ]
3048
+ }),
3049
+ (row.start !== "" || row.end !== "") && !(HHMM_RE.test(row.start) && HHMM_RE.test(row.end)) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3404
3050
  className: FallbacksCard_module_css_default.hint,
3405
- children: t("timeSlots.days.hint")
3051
+ children: t("validation.slotWindow")
3406
3052
  })
3407
- ]
3408
- }),
3409
- (row.start !== "" || row.end !== "") && !(HHMM_RE.test(row.start) && HHMM_RE.test(row.end)) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3410
- className: FallbacksCard_module_css_default.hint,
3411
- children: t("validation.slotWindow")
3412
- })
3413
- ] }),
3414
- chainEmpty && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3415
- className: FallbacksCard_module_css_default.hint,
3416
- children: t("validation.slotChainRequired")
3053
+ ] }),
3054
+ chainEmpty && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3055
+ className: FallbacksCard_module_css_default.hint,
3056
+ children: t("validation.slotChainRequired")
3057
+ }),
3058
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3059
+ className: FallbacksCard_module_css_default.chainBlock,
3060
+ "data-chain-block": "",
3061
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChainSelectorList, {
3062
+ selectors: row.selectors,
3063
+ catalog: catalogOf(state),
3064
+ configuredProviders: state.configuredProviders,
3065
+ disabled: !writable,
3066
+ t,
3067
+ onChange: (selectorIndex, patch) => {
3068
+ updateTimeSlotSelector(index, selectorIndex, patch);
3069
+ },
3070
+ onRemove: (selectorIndex) => {
3071
+ removeTimeSlotSelector(index, selectorIndex);
3072
+ },
3073
+ onMove: (from, to) => {
3074
+ moveSlotSelector(index, from, to);
3075
+ }
3076
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3077
+ type: "button",
3078
+ className: `${FallbacksCard_module_css_default.outlineButton} ${FallbacksCard_module_css_default.addButton}`,
3079
+ disabled: !writable,
3080
+ onClick: () => {
3081
+ addTimeSlotSelector(index);
3082
+ },
3083
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlusIcon, {}), t("timeSlots.selector.add")]
3084
+ })]
3085
+ }),
3086
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3087
+ className: FallbacksCard_module_css_default.cardFoot,
3088
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3089
+ className: FallbacksCard_module_css_default.rowActions,
3090
+ children: [
3091
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3092
+ type: "button",
3093
+ className: FallbacksCard_module_css_default.iconButton,
3094
+ "data-tip": t("timeSlots.moveUp"),
3095
+ "aria-label": t("timeSlots.moveUp"),
3096
+ disabled: !writable || index === 0,
3097
+ onClick: () => {
3098
+ moveTimeSlotRow(index, -1);
3099
+ },
3100
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronDownIcon, { className: FallbacksCard_module_css_default.chevronFlip })
3101
+ }),
3102
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3103
+ type: "button",
3104
+ className: FallbacksCard_module_css_default.iconButton,
3105
+ "data-tip": t("timeSlots.moveDown"),
3106
+ "aria-label": t("timeSlots.moveDown"),
3107
+ disabled: !writable || index === timeSlotRows.length - 1,
3108
+ onClick: () => {
3109
+ moveTimeSlotRow(index, 1);
3110
+ },
3111
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronDownIcon, {})
3112
+ }),
3113
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3114
+ type: "button",
3115
+ className: `${FallbacksCard_module_css_default.iconButton} ${FallbacksCard_module_css_default.iconButtonDanger}`,
3116
+ "data-tip": t("timeSlots.remove"),
3117
+ "aria-label": t("timeSlots.remove"),
3118
+ onClick: () => {
3119
+ removeTimeSlotRow(index);
3120
+ },
3121
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TrashIcon, {})
3122
+ })
3123
+ ]
3124
+ })
3125
+ })
3126
+ ] })]
3127
+ }, index);
3128
+ })
3129
+ }),
3130
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3131
+ className: FallbacksCard_module_css_default.slotAddRow,
3132
+ children: [
3133
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
3134
+ className: `${FallbacksCard_module_css_default.input} ${FallbacksCard_module_css_default.selectInput}`,
3135
+ value: presetToAdd,
3136
+ "aria-label": t("timeSlots.presetPlaceholder"),
3137
+ disabled: !writable,
3138
+ onChange: (event) => {
3139
+ setPresetToAdd(event.target.value);
3140
+ },
3141
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3142
+ value: "",
3143
+ children: t("timeSlots.presetPlaceholder")
3144
+ }), SLOT_PRESET_IDS.filter((id) => !timeSlotRows.some((row) => row.kind === "preset" && row.preset === id)).map((id) => {
3145
+ const glmUnconfigured = !glmConfigured && (id === "glm-peak" || id === "glm-valley");
3146
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
3147
+ value: id,
3148
+ disabled: glmUnconfigured,
3149
+ children: [t(`timeSlots.preset.${id}.label`), glmUnconfigured ? t("timeSlots.preset.glm.unconfigured") : null]
3150
+ }, id);
3151
+ })]
3152
+ }),
3153
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3154
+ type: "button",
3155
+ className: `${FallbacksCard_module_css_default.outlineButton} ${FallbacksCard_module_css_default.addButton}`,
3156
+ disabled: !writable || presetToAdd === "",
3157
+ onClick: addPresetSlotRow,
3158
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlusIcon, {}), t("timeSlots.addPreset")]
3417
3159
  }),
3418
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3160
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3161
+ type: "button",
3162
+ className: `${FallbacksCard_module_css_default.outlineButton} ${FallbacksCard_module_css_default.addButton}`,
3163
+ disabled: !writable,
3164
+ onClick: addCustomSlotRow,
3165
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlusIcon, {}), t("timeSlots.addCustom")]
3166
+ })
3167
+ ]
3168
+ })
3169
+ ]
3170
+ }),
3171
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3172
+ className: FallbacksCard_module_css_default.field,
3173
+ role: "group",
3174
+ "aria-labelledby": `fallbacks-root-chain-${activeDraft.id}`,
3175
+ children: [
3176
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3177
+ className: FallbacksCard_module_css_default.fieldLabel,
3178
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3179
+ id: `fallbacks-root-chain-${activeDraft.id}`,
3180
+ children: t("rootChain.label")
3181
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
3182
+ label: t("rootChain.tooltip"),
3183
+ disabled: !writable
3184
+ })]
3185
+ }),
3186
+ state.catalogStatus === "error" && state.catalogError !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3187
+ className: FallbacksCard_module_css_default.hint,
3188
+ children: t("catalog.error", { message: state.catalogError })
3189
+ }),
3190
+ state.catalogStatus === "ready" && state.catalogError !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3191
+ className: FallbacksCard_module_css_default.hint,
3192
+ children: t("catalog.partial", { message: state.catalogError })
3193
+ }),
3194
+ state.catalogStatus === "ready" && (state.groups.length === 0 || state.configuredProviders.length === 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3195
+ className: FallbacksCard_module_css_default.hint,
3196
+ children: t("catalog.empty")
3197
+ }),
3198
+ slotErrors.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3199
+ className: FallbacksCard_module_css_default.error,
3200
+ role: "alert",
3201
+ "data-chain-error": "",
3202
+ children: `${t("validation.blocked")}${slotErrors.join("; ")}`
3203
+ }),
3204
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3205
+ className: FallbacksCard_module_css_default.list,
3206
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3207
+ className: FallbacksCard_module_css_default.editorCard,
3208
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3419
3209
  className: FallbacksCard_module_css_default.chainBlock,
3420
3210
  "data-chain-block": "",
3421
3211
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChainSelectorList, {
3422
- selectors: row.selectors,
3212
+ selectors: activeDraft.selectors,
3423
3213
  catalog: catalogOf(state),
3424
3214
  configuredProviders: state.configuredProviders,
3425
3215
  disabled: !writable,
3426
3216
  t,
3427
3217
  onChange: (selectorIndex, patch) => {
3428
- updateTimeSlotSelector(index, selectorIndex, patch);
3218
+ updateChainSelector(selectorIndex, patch);
3429
3219
  },
3430
3220
  onRemove: (selectorIndex) => {
3431
- removeTimeSlotSelector(index, selectorIndex);
3221
+ removeChainSelector(selectorIndex);
3432
3222
  },
3433
- onMove: (from, to) => {
3434
- reorderTimeSlotSelector(index, from, to);
3435
- }
3223
+ onMove: reorderChainSelector
3436
3224
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3437
3225
  type: "button",
3438
3226
  className: `${FallbacksCard_module_css_default.outlineButton} ${FallbacksCard_module_css_default.addButton}`,
3439
3227
  disabled: !writable,
3440
- onClick: () => {
3441
- addTimeSlotSelector(index);
3442
- },
3228
+ onClick: addChainSelector,
3443
3229
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlusIcon, {}), t("timeSlots.selector.add")]
3444
3230
  })]
3445
- }),
3446
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3447
- className: FallbacksCard_module_css_default.cardFoot,
3448
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3449
- className: FallbacksCard_module_css_default.rowActions,
3450
- children: [
3451
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3452
- type: "button",
3453
- className: FallbacksCard_module_css_default.iconButton,
3454
- "data-tip": t("timeSlots.moveUp"),
3455
- "aria-label": t("timeSlots.moveUp"),
3456
- disabled: !writable || index === 0,
3457
- onClick: () => {
3458
- moveTimeSlotRow(index, -1);
3459
- },
3460
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronDownIcon, { className: FallbacksCard_module_css_default.chevronFlip })
3461
- }),
3462
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3463
- type: "button",
3464
- className: FallbacksCard_module_css_default.iconButton,
3465
- "data-tip": t("timeSlots.moveDown"),
3466
- "aria-label": t("timeSlots.moveDown"),
3467
- disabled: !writable || index === timeSlotRows.length - 1,
3468
- onClick: () => {
3469
- moveTimeSlotRow(index, 1);
3470
- },
3471
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronDownIcon, {})
3472
- }),
3473
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3474
- type: "button",
3475
- className: `${FallbacksCard_module_css_default.iconButton} ${FallbacksCard_module_css_default.iconButtonDanger}`,
3476
- "data-tip": t("timeSlots.remove"),
3477
- "aria-label": t("timeSlots.remove"),
3478
- onClick: () => {
3479
- removeTimeSlotRow(index);
3480
- },
3481
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TrashIcon, {})
3482
- })
3483
- ]
3484
- })
3485
3231
  })
3486
- ] })]
3487
- }, index);
3488
- })
3232
+ })
3233
+ })
3234
+ ]
3489
3235
  }),
3490
3236
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3491
- className: FallbacksCard_module_css_default.slotAddRow,
3237
+ className: FallbacksCard_module_css_default.field,
3238
+ role: "group",
3239
+ "aria-labelledby": `fallbacks-default-model-${activeDraft.id}`,
3492
3240
  children: [
3493
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
3494
- className: `${FallbacksCard_module_css_default.input} ${FallbacksCard_module_css_default.selectInput}`,
3495
- value: presetToAdd,
3496
- "aria-label": t("timeSlots.presetPlaceholder"),
3497
- disabled: !writable,
3498
- onChange: (event) => {
3499
- setPresetToAdd(event.target.value);
3500
- },
3501
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3502
- value: "",
3503
- children: t("timeSlots.presetPlaceholder")
3504
- }), SLOT_PRESET_IDS.filter((id) => !timeSlotRows.some((row) => row.kind === "preset" && row.preset === id)).map((id) => {
3505
- const glmUnconfigured = !glmConfigured && (id === "glm-peak" || id === "glm-valley");
3506
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
3507
- value: id,
3508
- disabled: glmUnconfigured,
3509
- children: [t(`timeSlots.preset.${id}.label`), glmUnconfigured ? t("timeSlots.preset.glm.unconfigured") : null]
3510
- }, id);
3511
- })]
3241
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3242
+ className: FallbacksCard_module_css_default.fieldLabel,
3243
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3244
+ id: `fallbacks-default-model-${activeDraft.id}`,
3245
+ children: t("defaultModel.label")
3246
+ })
3512
3247
  }),
3513
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3514
- type: "button",
3515
- className: `${FallbacksCard_module_css_default.outlineButton} ${FallbacksCard_module_css_default.addButton}`,
3516
- disabled: !writable || presetToAdd === "",
3517
- onClick: addPresetSlotRow,
3518
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlusIcon, {}), t("timeSlots.addPreset")]
3248
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3249
+ className: FallbacksCard_module_css_default.hint,
3250
+ children: t("allDay.hint")
3519
3251
  }),
3520
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3521
- type: "button",
3522
- className: `${FallbacksCard_module_css_default.outlineButton} ${FallbacksCard_module_css_default.addButton}`,
3523
- disabled: !writable,
3524
- onClick: addCustomSlotRow,
3525
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlusIcon, {}), t("timeSlots.addCustom")]
3252
+ tailError !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3253
+ className: FallbacksCard_module_css_default.error,
3254
+ role: "alert",
3255
+ "data-tail-error": "",
3256
+ children: `${t("validation.blocked")}${tailError}`
3257
+ }),
3258
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3259
+ className: FallbacksCard_module_css_default.list,
3260
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3261
+ className: FallbacksCard_module_css_default.editorCard,
3262
+ children: OFFICIAL_ALL_DAY_IDS.map((id) => {
3263
+ const chain = chains[activeChain];
3264
+ const tail = chain !== void 0 && chain.entries.length >= 1 ? chain.entries[chain.entries.length - 1] : void 0;
3265
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3266
+ className: FallbacksCard_module_css_default.optionRow,
3267
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3268
+ type: "radio",
3269
+ name: `fallbacks-chain-tail-${activeDraft.id}`,
3270
+ checked: tail === id,
3271
+ disabled: !writable,
3272
+ onChange: () => {
3273
+ patchActiveDraft((draft) => {
3274
+ const selector = entryToSelectorRow(id, catalogOf(state));
3275
+ if (draft.selectors.length === 0) return {
3276
+ ...draft,
3277
+ selectors: [selector]
3278
+ };
3279
+ const selectors = draft.selectors.map((row) => ({ ...row }));
3280
+ selectors[selectors.length - 1] = selector;
3281
+ return {
3282
+ ...draft,
3283
+ selectors
3284
+ };
3285
+ });
3286
+ }
3287
+ }), t(id === OFFICIAL_ALL_DAY_IDS[0] ? "allDay.flash" : "allDay.pro")]
3288
+ }, id);
3289
+ })
3290
+ })
3526
3291
  })
3527
3292
  ]
3528
- })
3529
- ]
3530
- }),
3531
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3532
- className: FallbacksCard_module_css_default.field,
3533
- role: "group",
3534
- "aria-labelledby": "fallbacks-root-chain",
3535
- children: [
3536
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3537
- className: FallbacksCard_module_css_default.fieldLabel,
3538
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3539
- id: "fallbacks-root-chain",
3540
- children: t("rootChain.label")
3541
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
3542
- label: t("rootChain.tooltip"),
3543
- disabled: !writable
3544
- })]
3545
- }),
3546
- state.catalogStatus === "error" && state.catalogError !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3547
- className: FallbacksCard_module_css_default.hint,
3548
- children: t("catalog.error", { message: state.catalogError })
3549
- }),
3550
- state.catalogStatus === "ready" && state.catalogError !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3551
- className: FallbacksCard_module_css_default.hint,
3552
- children: t("catalog.partial", { message: state.catalogError })
3553
3293
  }),
3554
- state.catalogStatus === "ready" && (state.groups.length === 0 || state.configuredProviders.length === 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3555
- className: FallbacksCard_module_css_default.hint,
3556
- children: t("catalog.empty")
3557
- }),
3558
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3559
- className: FallbacksCard_module_css_default.list,
3560
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3561
- className: FallbacksCard_module_css_default.editorCard,
3562
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3563
- className: FallbacksCard_module_css_default.chainBlock,
3564
- "data-chain-block": "",
3565
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChainSelectorList, {
3566
- selectors: allDayChainRow.selectors,
3567
- catalog: catalogOf(state),
3568
- configuredProviders: state.configuredProviders,
3569
- disabled: !writable,
3570
- t,
3571
- onChange: (selectorIndex, patch) => {
3572
- updateAllDayChainSelector(selectorIndex, patch);
3573
- },
3574
- onRemove: (selectorIndex) => {
3575
- removeAllDayChainSelector(selectorIndex);
3576
- },
3577
- onMove: reorderAllDayChainSelector
3578
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3579
- type: "button",
3580
- className: `${FallbacksCard_module_css_default.outlineButton} ${FallbacksCard_module_css_default.addButton}`,
3581
- disabled: !writable,
3582
- onClick: addAllDayChainSelector,
3583
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlusIcon, {}), t("timeSlots.selector.add")]
3584
- })]
3585
- })
3586
- })
3587
- })
3588
- ]
3589
- }),
3590
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3591
- className: FallbacksCard_module_css_default.field,
3592
- role: "group",
3593
- "aria-labelledby": "fallbacks-default-model",
3594
- children: [
3595
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3596
- className: FallbacksCard_module_css_default.fieldLabel,
3597
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3598
- id: "fallbacks-default-model",
3599
- children: t("defaultModel.label")
3600
- })
3601
- }),
3602
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3603
- className: FallbacksCard_module_css_default.hint,
3604
- children: t("allDay.hint")
3605
- }),
3606
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3607
- className: FallbacksCard_module_css_default.list,
3608
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3609
- className: FallbacksCard_module_css_default.editorCard,
3610
- children: [OFFICIAL_ALL_DAY_IDS.map((id, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3611
- className: FallbacksCard_module_css_default.optionRow,
3612
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3613
- type: "radio",
3614
- name: "fallbacks-all-day",
3615
- checked: allDayModel === id,
3616
- disabled: !writable,
3617
- onChange: () => {
3618
- setAllDayModel(id);
3619
- }
3620
- }), t(ALL_DAY_LABEL_KEYS[index])]
3621
- }, id)), allDayModel === "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3622
- className: FallbacksCard_module_css_default.hint,
3623
- children: t("allDay.nonconforming")
3624
- })]
3625
- })
3294
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3295
+ className: FallbacksCard_module_css_default.chainFoot,
3296
+ "data-chain-foot": "",
3297
+ children: [
3298
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3299
+ type: "button",
3300
+ className: FallbacksCard_module_css_default.discard,
3301
+ disabled: !activeDirty || saving,
3302
+ onClick: discardChain,
3303
+ children: t("discard")
3304
+ }),
3305
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3306
+ type: "button",
3307
+ className: FallbacksCard_module_css_default.save,
3308
+ disabled: !writable || saving || !activeDirty || anyChainErrors,
3309
+ onClick: () => {
3310
+ saveSection("chains");
3311
+ },
3312
+ children: saving ? t("save.saving") : t("save")
3313
+ }),
3314
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3315
+ className: FallbacksCard_module_css_default.chainFootHint,
3316
+ children: t("chains.chainFoot.hint")
3317
+ }),
3318
+ lastSaveSection === "chains" && lastSaveChain === activeChain && state.status === "error" && state.error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3319
+ className: FallbacksCard_module_css_default.error,
3320
+ role: "alert",
3321
+ children: t("error.generic", { message: state.error })
3322
+ })
3323
+ ]
3626
3324
  })
3627
3325
  ]
3628
3326
  }),
@@ -3638,7 +3336,7 @@ window.__ModuleLoader__.load({
3638
3336
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3639
3337
  type: "button",
3640
3338
  className: FallbacksCard_module_css_default.discard,
3641
- disabled: !subDirty || saving,
3339
+ disabled: !rolesDirty || saving,
3642
3340
  onClick: () => {
3643
3341
  discardSection("sub");
3644
3342
  },
@@ -3646,7 +3344,7 @@ window.__ModuleLoader__.load({
3646
3344
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3647
3345
  type: "button",
3648
3346
  className: FallbacksCard_module_css_default.save,
3649
- disabled: !writable || saving || !subDirty,
3347
+ disabled: !writable || saving || !rolesDirty || liveErrors.roles.length > 0,
3650
3348
  onClick: () => {
3651
3349
  saveSection("sub");
3652
3350
  },
@@ -3654,101 +3352,27 @@ window.__ModuleLoader__.load({
3654
3352
  })]
3655
3353
  })]
3656
3354
  }),
3657
- liveErrors.sub.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3355
+ liveErrors.roles.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3658
3356
  className: FallbacksCard_module_css_default.error,
3659
3357
  role: "alert",
3660
- children: `${t("validation.blocked")}${liveErrors.sub.join("; ")}`
3358
+ children: `${t("validation.blocked")}${liveErrors.roles.join("; ")}`
3661
3359
  }),
3662
3360
  lastSaveSection === "sub" && state.status === "error" && state.error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3663
3361
  className: FallbacksCard_module_css_default.error,
3664
3362
  role: "alert",
3665
3363
  children: t("error.generic", { message: state.error })
3666
3364
  }),
3667
- state.subagentPolicy?.state === "enabled" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3668
- className: FallbacksCard_module_css_default.policyStatus,
3669
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3670
- className: FallbacksCard_module_css_default.policyStatusText,
3671
- role: "status",
3672
- children: [
3673
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3674
- className: FallbacksCard_module_css_default.policyStatusLabel,
3675
- children: t("subagents.policy.label")
3676
- }),
3677
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3678
- className: FallbacksCard_module_css_default.policyStatusLine,
3679
- children: `${t("subagents.policy.allowlist")}: ${state.subagentPolicy.allowedModels.map((route) => `${route.provider}/${route.model}`).join(", ")}`
3680
- }),
3681
- state.subagentPolicy.head !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3682
- className: FallbacksCard_module_css_default.policyStatusLine,
3683
- children: `${t("subagents.policy.head")}: ${state.subagentPolicy.head.route.provider}/${state.subagentPolicy.head.route.model} (${t(state.subagentPolicy.head.source === "authorized" ? "subagents.policy.source.authorized" : "subagents.policy.source.injected")})`
3684
- }),
3685
- state.subagentPolicy.blockedAttempt !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3686
- className: FallbacksCard_module_css_default.policyStatusWarn,
3687
- role: "alert",
3688
- children: t("subagents.policy.blocked")
3689
- }),
3690
- offAllowlistCount > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3691
- className: FallbacksCard_module_css_default.policyStatusWarn,
3692
- children: t("subagents.policy.offAllowlist", { count: String(offAllowlistCount) })
3693
- }),
3694
- !virtualRouteAllowed && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3695
- className: FallbacksCard_module_css_default.policyStatusLine,
3696
- children: t("subagents.policy.recommend")
3697
- })
3698
- ]
3699
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3700
- className: FallbacksCard_module_css_default.policyStatusSync,
3701
- role: "group",
3702
- "aria-label": t("subagents.policy.sync"),
3703
- children: [
3704
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3705
- type: "button",
3706
- className: FallbacksCard_module_css_default.outlineButton,
3707
- disabled: !writable || subDirty || mainDirty,
3708
- "aria-busy": syncStatus === "pending" || void 0,
3709
- "aria-describedby": subDirty || mainDirty ? syncDirtyHintId : void 0,
3710
- onClick: syncAllowlist,
3711
- children: t("subagents.policy.sync")
3712
- }),
3713
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3714
- id: syncDirtyHintId,
3715
- role: "status",
3716
- "data-sync-hint": "dirty",
3717
- className: `${FallbacksCard_module_css_default.policyStatusWarn} ${FallbacksCard_module_css_default.policyStatusLive}`,
3718
- children: subDirty || mainDirty ? t("subagents.policy.sync.dirty") : ""
3719
- }),
3720
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3721
- role: "status",
3722
- "data-sync-hint": "outcome",
3723
- className: `${syncStatus === "error" ? FallbacksCard_module_css_default.policyStatusWarn : FallbacksCard_module_css_default.policyStatusLine} ${FallbacksCard_module_css_default.policyStatusLive}`,
3724
- children: syncStatus === "ok" ? t("subagents.policy.sync.done") : syncStatus === "error" ? t("subagents.policy.sync.failed") : ""
3725
- })
3726
- ]
3727
- })]
3728
- }),
3729
- state.subagentPolicy?.state === "unprovable" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3730
- className: FallbacksCard_module_css_default.policyStatus,
3731
- role: "status",
3732
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3733
- className: FallbacksCard_module_css_default.policyStatusWarn,
3734
- role: "alert",
3735
- children: t("subagents.policy.unprovable")
3736
- })
3737
- }),
3738
3365
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3739
3366
  className: FallbacksCard_module_css_default.field,
3740
3367
  role: "group",
3741
3368
  "aria-labelledby": "fallbacks-roles-list",
3742
3369
  children: [
3743
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3370
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3744
3371
  className: FallbacksCard_module_css_default.fieldLabel,
3745
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3372
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3746
3373
  id: "fallbacks-roles-list",
3747
3374
  children: t("roles.list.label")
3748
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
3749
- label: t("roles.list.tooltip"),
3750
- disabled: !writable
3751
- })]
3375
+ })
3752
3376
  }),
3753
3377
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3754
3378
  className: FallbacksCard_module_css_default.hint,
@@ -3761,11 +3385,12 @@ window.__ModuleLoader__.load({
3761
3385
  const seed = seedInfo.get(row.id.trim());
3762
3386
  const seedBadge = seedBadgeLabel(seed, t);
3763
3387
  const personaBlank = row.persona.trim() === "";
3764
- const roleChainSummary = chainSummary(row.selectors, allowedModels);
3765
- const roleSummary = roleChainSummary ?? row.fallback;
3388
+ const chainRoute = row.chain === "" ? t("roles.chainMeta.unspecified") : `FallbacksChain/${row.chain}`;
3766
3389
  const roleExpanded = !row.collapsed || !writable;
3390
+ const chainOutside = row.chain !== "" && !chains.some((chain) => chain.id === row.chain);
3767
3391
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3768
3392
  className: FallbacksCard_module_css_default.editorCard,
3393
+ "data-role-card": row.id,
3769
3394
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3770
3395
  className: FallbacksCard_module_css_default.collapseRow,
3771
3396
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
@@ -3790,8 +3415,8 @@ window.__ModuleLoader__.load({
3790
3415
  }),
3791
3416
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3792
3417
  className: FallbacksCard_module_css_default.collapseMeta,
3793
- title: roleChainSummary,
3794
- children: roleSummary
3418
+ title: chainRoute,
3419
+ children: chainRoute
3795
3420
  })
3796
3421
  ]
3797
3422
  })
@@ -3823,6 +3448,49 @@ window.__ModuleLoader__.load({
3823
3448
  ]
3824
3449
  })
3825
3450
  }),
3451
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3452
+ className: FallbacksCard_module_css_default.ruleGrid,
3453
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3454
+ className: FallbacksCard_module_css_default.ruleCell,
3455
+ children: [
3456
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
3457
+ className: FallbacksCard_module_css_default.ruleCellLabel,
3458
+ htmlFor: `fallbacks-role-chain-${index}`,
3459
+ children: t("roles.chain.label")
3460
+ }),
3461
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
3462
+ id: `fallbacks-role-chain-${index}`,
3463
+ className: `${FallbacksCard_module_css_default.input} ${FallbacksCard_module_css_default.selectInput}`,
3464
+ value: row.chain,
3465
+ "aria-describedby": `fallbacks-role-chain-hint-${index}`,
3466
+ disabled: !writable,
3467
+ onChange: (event) => {
3468
+ updateRoleRow(index, { chain: event.target.value });
3469
+ },
3470
+ "data-role-chain-select": "",
3471
+ children: [
3472
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3473
+ value: "",
3474
+ children: t("roles.chain.unspecified")
3475
+ }),
3476
+ chains.map((chain) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3477
+ value: chain.id,
3478
+ children: `${chain.label === "" ? chain.id : chain.label}(FallbacksChain/${chain.id})`
3479
+ }, chain.id)),
3480
+ chainOutside && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3481
+ value: row.chain,
3482
+ children: `${row.chain}${t("roles.chainMeta.unspecified")}`
3483
+ })
3484
+ ]
3485
+ }),
3486
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3487
+ className: FallbacksCard_module_css_default.hint,
3488
+ id: `fallbacks-role-chain-hint-${index}`,
3489
+ children: t("roles.chain.hint")
3490
+ })
3491
+ ]
3492
+ })
3493
+ }),
3826
3494
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3827
3495
  className: FallbacksCard_module_css_default.ruleGrid,
3828
3496
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -3869,66 +3537,6 @@ window.__ModuleLoader__.load({
3869
3537
  })] })]
3870
3538
  })
3871
3539
  }),
3872
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3873
- className: FallbacksCard_module_css_default.chainBlock,
3874
- "data-chain-block": "",
3875
- children: [
3876
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChainSelectorList, {
3877
- selectors: row.selectors,
3878
- catalog: catalogOf(state),
3879
- configuredProviders: state.configuredProviders,
3880
- disabled: !writable,
3881
- t,
3882
- onChange: (selectorIndex, patch) => {
3883
- updateRoleSelector(index, selectorIndex, patch);
3884
- },
3885
- onRemove: (selectorIndex) => {
3886
- removeRoleSelector(index, selectorIndex);
3887
- },
3888
- onMove: (from, to) => {
3889
- reorderRoleSelector(index, from, to);
3890
- }
3891
- }),
3892
- row.selectors.every((selector) => selectorRowToRaw(selector) === "") && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3893
- className: FallbacksCard_module_css_default.hint,
3894
- children: seed !== void 0 ? t("roles.seedChainOptional", { id: row.id }) : t("validation.roleChainRequired", { id: row.id })
3895
- }),
3896
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3897
- type: "button",
3898
- className: `${FallbacksCard_module_css_default.outlineButton} ${FallbacksCard_module_css_default.addButton}`,
3899
- disabled: !writable,
3900
- onClick: () => {
3901
- addRoleSelector(index);
3902
- },
3903
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlusIcon, {}), t("roles.selector.add")]
3904
- })
3905
- ]
3906
- }),
3907
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3908
- className: FallbacksCard_module_css_default.ruleGrid,
3909
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3910
- className: FallbacksCard_module_css_default.ruleCell,
3911
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3912
- className: FallbacksCard_module_css_default.ruleCellLabel,
3913
- children: t("roles.fallback")
3914
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
3915
- className: `${FallbacksCard_module_css_default.input} ${FallbacksCard_module_css_default.selectInput}`,
3916
- value: row.fallback,
3917
- "aria-label": t("roles.fallback"),
3918
- disabled: !writable,
3919
- onChange: (event) => {
3920
- updateRoleRow(index, { fallback: event.target.value });
3921
- },
3922
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3923
- value: "inherit-root",
3924
- children: t("roles.fallback.inherit-root")
3925
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3926
- value: "none",
3927
- children: t("roles.fallback.none")
3928
- })]
3929
- })]
3930
- })
3931
- }),
3932
3540
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3933
3541
  className: FallbacksCard_module_css_default.cardFoot,
3934
3542
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
@@ -3955,188 +3563,6 @@ window.__ModuleLoader__.load({
3955
3563
  })
3956
3564
  ]
3957
3565
  }),
3958
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3959
- className: FallbacksCard_module_css_default.field,
3960
- role: "group",
3961
- "aria-labelledby": "fallbacks-roles-rules",
3962
- children: [
3963
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3964
- className: FallbacksCard_module_css_default.fieldLabel,
3965
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3966
- id: "fallbacks-roles-rules",
3967
- children: t("roles.rules")
3968
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
3969
- label: t("roles.rules.tooltip"),
3970
- disabled: !writable
3971
- })]
3972
- }),
3973
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3974
- className: FallbacksCard_module_css_default.hint,
3975
- children: t("roles.rules.hint")
3976
- }),
3977
- state.catalogStatus === "error" && state.catalogError !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3978
- className: FallbacksCard_module_css_default.hint,
3979
- children: t("catalog.error", { message: state.catalogError })
3980
- }),
3981
- state.catalogStatus === "ready" && state.catalogError !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3982
- className: FallbacksCard_module_css_default.hint,
3983
- children: t("catalog.partial", { message: state.catalogError })
3984
- }),
3985
- state.catalogStatus === "ready" && (state.groups.length === 0 || state.configuredProviders.length === 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3986
- className: FallbacksCard_module_css_default.hint,
3987
- children: t("catalog.empty")
3988
- }),
3989
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3990
- className: FallbacksCard_module_css_default.list,
3991
- children: ruleRows.map((row, index) => {
3992
- const catalog = catalogOf(state);
3993
- const providerRaw = selectionToRaw(row.provider);
3994
- const group = catalog?.groups.find((entry) => entry.id === providerRaw);
3995
- const providerOutside = row.provider?.kind === "outside";
3996
- const providerUnconfigured = !providerOutside && providerRaw !== "" && (catalog?.providers.some((entry) => entry.provider === providerRaw) ?? false) && !state.configuredProviders.some((entry) => entry.provider === providerRaw);
3997
- const modelOutside = row.model?.kind === "outside";
3998
- const roleOutside = row.role !== "" && !roleOptions.includes(row.role);
3999
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4000
- className: FallbacksCard_module_css_default.editorCard,
4001
- children: [
4002
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4003
- className: FallbacksCard_module_css_default.ruleGrid,
4004
- children: [
4005
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
4006
- className: FallbacksCard_module_css_default.ruleCell,
4007
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4008
- className: FallbacksCard_module_css_default.ruleCellLabel,
4009
- children: t("roles.rule.provider")
4010
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
4011
- className: `${FallbacksCard_module_css_default.input} ${FallbacksCard_module_css_default.selectInput}`,
4012
- value: providerRaw,
4013
- onChange: (event) => {
4014
- if (event.target.value === providerRaw) return;
4015
- updateRuleRow(index, {
4016
- provider: classifyProvider(event.target.value, catalog),
4017
- model: null
4018
- });
4019
- },
4020
- children: [
4021
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
4022
- value: "",
4023
- children: t("roles.rule.provider.any")
4024
- }),
4025
- state.configuredProviders.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
4026
- value: entry.provider,
4027
- children: entry.displayName
4028
- }, entry.provider)),
4029
- providerUnconfigured && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
4030
- value: providerRaw,
4031
- children: `${providerRaw}${t("catalog.unconfigured.short")}`
4032
- }),
4033
- providerOutside && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
4034
- value: providerRaw,
4035
- children: `${providerRaw}${t("catalog.outside.short")}`
4036
- })
4037
- ]
4038
- })]
4039
- }),
4040
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
4041
- className: FallbacksCard_module_css_default.ruleCell,
4042
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4043
- className: FallbacksCard_module_css_default.ruleCellLabel,
4044
- children: t("roles.rule.model")
4045
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
4046
- className: `${FallbacksCard_module_css_default.input} ${FallbacksCard_module_css_default.selectInput}`,
4047
- value: selectionToRaw(row.model),
4048
- onChange: (event) => {
4049
- updateRuleRow(index, { model: classifyModel(providerRaw, event.target.value, catalog) });
4050
- },
4051
- children: [
4052
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
4053
- value: "",
4054
- children: t("roles.rule.model.any")
4055
- }),
4056
- (group?.models ?? []).map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
4057
- value: model.id,
4058
- children: model.name
4059
- }, model.id)),
4060
- modelOutside && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
4061
- value: selectionToRaw(row.model),
4062
- children: `${selectionToRaw(row.model)}${t("catalog.outside.short")}`
4063
- })
4064
- ]
4065
- })]
4066
- }),
4067
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
4068
- className: FallbacksCard_module_css_default.ruleCell,
4069
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4070
- className: FallbacksCard_module_css_default.ruleCellLabel,
4071
- children: t("roles.rule.role")
4072
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
4073
- className: `${FallbacksCard_module_css_default.input} ${FallbacksCard_module_css_default.selectInput}`,
4074
- value: row.role,
4075
- disabled: !writable,
4076
- onChange: (event) => {
4077
- updateRuleRow(index, { role: event.target.value });
4078
- },
4079
- children: [
4080
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
4081
- value: "",
4082
- children: t("roles.rule.roleSelectPlaceholder")
4083
- }),
4084
- roleOptions.map((id) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
4085
- value: id,
4086
- children: id === "inherit" ? t("roles.rule.role.inherit") : id
4087
- }, id)),
4088
- roleOutside && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
4089
- value: row.role,
4090
- children: `${row.role}${t("roles.rule.roleUndeclared.short")}`
4091
- })
4092
- ]
4093
- })]
4094
- })
4095
- ]
4096
- }),
4097
- (providerOutside || modelOutside) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4098
- className: FallbacksCard_module_css_default.hint,
4099
- children: [t("catalog.outside.hint"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
4100
- label: t("catalog.outside.tooltip"),
4101
- disabled: !writable
4102
- })]
4103
- }),
4104
- row.role === "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4105
- className: FallbacksCard_module_css_default.hint,
4106
- children: t("validation.ruleRoleRequired")
4107
- }),
4108
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4109
- className: FallbacksCard_module_css_default.cardFoot,
4110
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4111
- type: "button",
4112
- className: `${FallbacksCard_module_css_default.iconButton} ${FallbacksCard_module_css_default.iconButtonDanger}`,
4113
- "data-tip": t("roles.removeRule"),
4114
- "aria-label": t("roles.removeRule"),
4115
- onClick: () => {
4116
- setRuleRows((rows) => rows.filter((_, rowIndex) => rowIndex !== index));
4117
- },
4118
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TrashIcon, {})
4119
- })
4120
- })
4121
- ]
4122
- }, index);
4123
- })
4124
- }),
4125
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4126
- type: "button",
4127
- className: `${FallbacksCard_module_css_default.outlineButton} ${FallbacksCard_module_css_default.addButton}`,
4128
- disabled: !writable,
4129
- onClick: () => {
4130
- setRuleRows((rows) => [...rows, {
4131
- provider: null,
4132
- model: null,
4133
- role: ""
4134
- }]);
4135
- },
4136
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlusIcon, {}), t("roles.addRule")]
4137
- })
4138
- ]
4139
- }),
4140
3566
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
4141
3567
  className: FallbacksCard_module_css_default.sectionHeading,
4142
3568
  id: "fallbacks-advanced",
@@ -4161,47 +3587,6 @@ window.__ModuleLoader__.load({
4161
3587
  id: "fallbacks-advanced-body",
4162
3588
  className: FallbacksCard_module_css_default.advancedBody,
4163
3589
  children: [
4164
- liveErrors.advanced.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4165
- className: FallbacksCard_module_css_default.error,
4166
- role: "alert",
4167
- children: `${t("validation.blocked")}${liveErrors.advanced.join("; ")}`
4168
- }),
4169
- lastSaveSection === "advanced" && state.status === "error" && state.error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4170
- className: FallbacksCard_module_css_default.error,
4171
- role: "alert",
4172
- children: t("error.generic", { message: state.error })
4173
- }),
4174
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4175
- className: FallbacksCard_module_css_default.checkboxRow,
4176
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4177
- id: "fallbacks-role-automatch",
4178
- type: "checkbox",
4179
- className: FallbacksCard_module_css_default.checkbox,
4180
- checked: scalars.roleAutoMatch,
4181
- disabled: !writable,
4182
- onChange: (event) => {
4183
- updateScalars((draft) => {
4184
- draft.roleAutoMatch = event.target.checked;
4185
- });
4186
- }
4187
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4188
- className: FallbacksCard_module_css_default.checkLabel,
4189
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4190
- className: FallbacksCard_module_css_default.checkLabelTitle,
4191
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
4192
- htmlFor: "fallbacks-role-automatch",
4193
- children: t("roleAutoMatch.label")
4194
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
4195
- label: t("roleAutoMatch.tooltip"),
4196
- disabled: !writable
4197
- })]
4198
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
4199
- className: FallbacksCard_module_css_default.checkLabelDesc,
4200
- htmlFor: "fallbacks-role-automatch",
4201
- children: t("roleAutoMatch.hint")
4202
- })]
4203
- })]
4204
- }),
4205
3590
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4206
3591
  className: FallbacksCard_module_css_default.field,
4207
3592
  role: "group",
@@ -4275,104 +3660,69 @@ window.__ModuleLoader__.load({
4275
3660
  }),
4276
3661
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4277
3662
  className: FallbacksCard_module_css_default.numberFields,
4278
- children: [
4279
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4280
- className: FallbacksCard_module_css_default.field,
4281
- children: [
4282
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4283
- className: FallbacksCard_module_css_default.fieldLabel,
4284
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
4285
- htmlFor: "fallbacks-cooldown-ms",
4286
- children: t("cooldownMs.label")
4287
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
4288
- label: cooldownMsTip,
4289
- disabled: !writable
4290
- })]
4291
- }),
4292
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4293
- id: "fallbacks-cooldown-ms",
4294
- className: FallbacksCard_module_css_default.input,
4295
- type: "number",
4296
- min: 0,
4297
- value: String(scalars.cooldownMs),
4298
- disabled: !writable,
4299
- onChange: (event) => {
4300
- updateScalars((draft) => {
4301
- draft.cooldownMs = parseCount(event.target.value);
4302
- });
4303
- }
4304
- }),
4305
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4306
- className: FallbacksCard_module_css_default.hint,
4307
- children: t("cooldownMs.hint")
4308
- })
4309
- ]
4310
- }),
4311
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4312
- className: FallbacksCard_module_css_default.field,
4313
- children: [
4314
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4315
- className: FallbacksCard_module_css_default.fieldLabel,
4316
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
4317
- htmlFor: "fallbacks-max-switches",
4318
- children: t("maxSwitchesPerStep.label")
4319
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
4320
- label: maxSwitchesPerStepTip,
4321
- disabled: !writable
4322
- })]
4323
- }),
4324
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4325
- id: "fallbacks-max-switches",
4326
- className: FallbacksCard_module_css_default.input,
4327
- type: "number",
4328
- min: 0,
4329
- value: String(scalars.maxSwitchesPerStep),
4330
- disabled: !writable,
4331
- onChange: (event) => {
4332
- updateScalars((draft) => {
4333
- draft.maxSwitchesPerStep = parseCount(event.target.value);
4334
- });
4335
- }
4336
- }),
4337
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4338
- className: FallbacksCard_module_css_default.hint,
4339
- children: t("maxSwitchesPerStep.hint")
4340
- })
4341
- ]
4342
- }),
4343
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4344
- className: FallbacksCard_module_css_default.field,
4345
- children: [
4346
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4347
- className: FallbacksCard_module_css_default.fieldLabel,
4348
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
4349
- htmlFor: "fallbacks-always-cap",
4350
- children: t("alwaysModeRetryCap.label")
4351
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
4352
- label: alwaysModeRetryCapTip,
4353
- disabled: !writable
4354
- })]
4355
- }),
4356
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4357
- id: "fallbacks-always-cap",
4358
- className: FallbacksCard_module_css_default.input,
4359
- type: "number",
4360
- min: 0,
4361
- value: String(scalars.alwaysModeRetryCap),
4362
- disabled: !writable,
4363
- onChange: (event) => {
4364
- updateScalars((draft) => {
4365
- draft.alwaysModeRetryCap = parseCount(event.target.value);
4366
- });
4367
- }
4368
- }),
4369
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4370
- className: FallbacksCard_module_css_default.hint,
4371
- children: t("alwaysModeRetryCap.hint")
4372
- })
4373
- ]
4374
- })
4375
- ]
3663
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3664
+ className: FallbacksCard_module_css_default.field,
3665
+ children: [
3666
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3667
+ className: FallbacksCard_module_css_default.fieldLabel,
3668
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
3669
+ htmlFor: "fallbacks-cooldown-ms",
3670
+ children: t("cooldownMs.label")
3671
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
3672
+ label: cooldownMsTip,
3673
+ disabled: !writable
3674
+ })]
3675
+ }),
3676
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3677
+ id: "fallbacks-cooldown-ms",
3678
+ className: FallbacksCard_module_css_default.input,
3679
+ type: "number",
3680
+ min: 0,
3681
+ value: String(scalars.cooldownMs),
3682
+ disabled: !writable,
3683
+ onChange: (event) => {
3684
+ updateScalars((draft) => {
3685
+ draft.cooldownMs = parseCount(event.target.value);
3686
+ });
3687
+ }
3688
+ }),
3689
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3690
+ className: FallbacksCard_module_css_default.hint,
3691
+ children: t("cooldownMs.hint")
3692
+ })
3693
+ ]
3694
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3695
+ className: FallbacksCard_module_css_default.field,
3696
+ children: [
3697
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3698
+ className: FallbacksCard_module_css_default.fieldLabel,
3699
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
3700
+ htmlFor: "fallbacks-max-switches",
3701
+ children: t("maxSwitchesPerStep.label")
3702
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
3703
+ label: maxSwitchesPerStepTip,
3704
+ disabled: !writable
3705
+ })]
3706
+ }),
3707
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3708
+ id: "fallbacks-max-switches",
3709
+ className: FallbacksCard_module_css_default.input,
3710
+ type: "number",
3711
+ min: 0,
3712
+ value: String(scalars.maxSwitchesPerStep),
3713
+ disabled: !writable,
3714
+ onChange: (event) => {
3715
+ updateScalars((draft) => {
3716
+ draft.maxSwitchesPerStep = parseCount(event.target.value);
3717
+ });
3718
+ }
3719
+ }),
3720
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3721
+ className: FallbacksCard_module_css_default.hint,
3722
+ children: t("maxSwitchesPerStep.hint")
3723
+ })
3724
+ ]
3725
+ })]
4376
3726
  }),
4377
3727
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4378
3728
  className: FallbacksCard_module_css_default.sectionActions,
@@ -4397,126 +3747,11 @@ window.__ModuleLoader__.load({
4397
3747
  ]
4398
3748
  })
4399
3749
  ]
4400
- }),
4401
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4402
- className: FallbacksCard_module_css_default.statusBlock,
4403
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4404
- className: FallbacksCard_module_css_default.statusTitle,
4405
- children: t("status.title")
4406
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
4407
- className: FallbacksCard_module_css_default.statusLine,
4408
- role: state.switchesStatus === "error" ? "alert" : void 0,
4409
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4410
- className: FallbacksCard_module_css_default.statusLineLabel,
4411
- children: t("status.switches.label")
4412
- }), switchesLine]
4413
- })]
4414
3750
  })
4415
3751
  ]
4416
3752
  });
4417
3753
  }
4418
3754
  //#endregion
4419
- //#region \0dsh-css:/home/runner/work/dsh-llm-fallbacks/dsh-llm-fallbacks/src/client/GeneralFallbacksRow.module.css.mjs
4420
- const css$2 = "\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";
4421
- const tagId$2 = "dsh-llm-fallbacks/GeneralFallbacksRow.module.css";
4422
- if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]") === null) {
4423
- const tag = document.createElement("style");
4424
- tag.dataset.plugin = "dsh-llm-fallbacks";
4425
- tag.dataset.pluginCss = tagId$2;
4426
- tag.textContent = css$2;
4427
- document.head.appendChild(tag);
4428
- }
4429
- var GeneralFallbacksRow_module_css_default = {
4430
- "row": "_440e1d7b_row",
4431
- "rowText": "_a4f2ceec_rowText",
4432
- "title": "_9865b509_title",
4433
- "summary": "_10a44713_summary",
4434
- "badge": "_35648278_badge",
4435
- "badgeEnabled": "_3b9e8609_badgeEnabled"
4436
- };
4437
- //#endregion
4438
- //#region src/client/GeneralFallbacksRow.tsx
4439
- /**
4440
- * Fallbacks status row — the `fallbacks` read-only row on the dsh General
4441
- * settings page (plan fallbacks-aux-seams, task 1). Registered into the
4442
- * `settings.general.item` slot (id `fallbacks`, order 100 — after every
4443
- * upstream preference row: agent-preset -25 / permission -20 / language 0 /
4444
- * appearance 10 / composer-enter 20, so the informational row renders at the
4445
- * column end). Owner props are intentionally empty (`children?: never`,
4446
- * dsh-private ui-settings slots.ts:81-84 — the section column only stacks),
4447
- * so all data flows through the shared {@link FallbacksSettingsController}
4448
- * (the same instance the plugin-config card consumes): the row triggers the
4449
- * first read on mount when the store is still idle, and the pushed
4450
- * invalidations wired in `apply` (`settings/document-updated` fallbacks-ns +
4451
- * `connection/reset`, which refresh only already-read stores) keep it fresh
4452
- * afterwards — no new data path, no store API change.
4453
- *
4454
- * The row is read-only by design (偏好位语义: a General preference row is not
4455
- * a control surface): an active/inactive badge (content presence — the
4456
- * removed config-level `enabled` switch is re-keyed to `isFallbackActive`,
4457
- * plan fallbacks-web-ux-alignment T2) + a compact last-switch summary.
4458
- * Honest degraded states: a hard load error or an unreachable gateway
4459
- * channel (`ready && !present`) render the neutral 'unknown' badge — a
4460
- * channel-down read must never masquerade as 'disabled' (KD-G5); the
4461
- * switches face keeps its own error/empty states (D-5 semantics unchanged).
4462
- *
4463
- * Geometry follows the upstream Setting-Cell (figma 501:30011 — gap 8,
4464
- * pad 16/0, hairline separator, title over subtitle in the text column, a
4465
- * small non-interactive pill on the right); every color resolves through a
4466
- * `--dsw-alias-*` token (light/dark adaptive).
4467
- */
4468
- /**
4469
- * Render the Fallbacks status row.
4470
- * @param props - composed slot props.
4471
- * @returns the row element tree.
4472
- */
4473
- function GeneralFallbacksRow({ controller, useSnapshot, t }) {
4474
- const state = useSnapshot((snapshot) => snapshot);
4475
- (0, react.useEffect)(() => {
4476
- const snapshot = controller.store.getSnapshot();
4477
- if (snapshot.status === "idle") controller.load();
4478
- if (snapshot.switchesStatus === "idle") controller.loadSwitches();
4479
- }, [controller]);
4480
- const settled = state.status === "ready";
4481
- const badgeKey = settled && state.present ? isFallbackActive(state.config) ? "general.enabled" : "general.disabled" : "general.unknown";
4482
- const latestSwitch = state.switches[0];
4483
- let summary;
4484
- if (state.status === "error") summary = t("general.error", { message: state.error ?? "" });
4485
- else if (!settled) summary = t("loading");
4486
- else if (!state.present) summary = t("general.unavailable");
4487
- else if (state.switchesStatus === "error") summary = t("status.switches.error", { message: state.switchesError ?? "" });
4488
- else if (state.switchesStatus === "loading") summary = t("loading");
4489
- else if (latestSwitch === void 0) summary = t("general.switch.empty");
4490
- else {
4491
- const reasonKey = SWITCH_REASON_KEYS[latestSwitch.reason];
4492
- const params = {
4493
- from: `${latestSwitch.from.provider}/${latestSwitch.from.model}`,
4494
- to: `${latestSwitch.to.provider}/${latestSwitch.to.model}`,
4495
- role: latestSwitch.role,
4496
- reason: reasonKey === void 0 ? latestSwitch.reason : t(reasonKey)
4497
- };
4498
- summary = latestSwitch.reason === "role-inject" ? t("general.switch.roleInject", params) : t("general.switch", params);
4499
- }
4500
- const alert = state.status === "error" || state.switchesStatus === "error";
4501
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4502
- className: GeneralFallbacksRow_module_css_default.row,
4503
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4504
- className: GeneralFallbacksRow_module_css_default.rowText,
4505
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4506
- className: GeneralFallbacksRow_module_css_default.title,
4507
- children: t("general.title")
4508
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4509
- className: GeneralFallbacksRow_module_css_default.summary,
4510
- role: alert ? "alert" : void 0,
4511
- children: summary
4512
- })]
4513
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4514
- className: `${GeneralFallbacksRow_module_css_default.badge} ${badgeKey === "general.enabled" ? GeneralFallbacksRow_module_css_default.badgeEnabled : ""}`,
4515
- children: t(badgeKey)
4516
- })]
4517
- });
4518
- }
4519
- //#endregion
4520
3755
  //#region src/client/switch-guard.ts
4521
3756
  /**
4522
3757
  * True when `value` is a well-formed `fallbacks/switch` payload — the ONE
@@ -4870,7 +4105,7 @@ window.__ModuleLoader__.load({
4870
4105
  const connection = ctx.get("connection");
4871
4106
  const sessions = ctx.get("sessions");
4872
4107
  const controller = new FallbacksSettingsController(ctx.remote, connection.rpc);
4873
- const useSnapshot = bindSnapshotSelector(controller.store);
4108
+ controller.store;
4874
4109
  ctx.effect(() => {
4875
4110
  const syncSession = () => {
4876
4111
  controller.setCurrentSession(sessions === void 0 ? void 0 : viewedSessionId(sessions.list.getSnapshot()));
@@ -4879,7 +4114,6 @@ window.__ModuleLoader__.load({
4879
4114
  const refresh = (ns) => {
4880
4115
  if (ns !== void 0 && ns !== "fallbacks") return;
4881
4116
  refreshFallbacksIfLoaded(controller);
4882
- refreshSwitchesIfLoaded(controller);
4883
4117
  };
4884
4118
  const refreshCatalog = () => {
4885
4119
  refreshCatalogIfLoaded(controller);
@@ -4920,18 +4154,6 @@ window.__ModuleLoader__.load({
4920
4154
  })
4921
4155
  }, FallbacksCard);
4922
4156
  });
4923
- ctx.slots.inject("settings.general.item", function* () {
4924
- yield ctx.slots.register({
4925
- name: "settings.general.item",
4926
- id: "fallbacks",
4927
- order: 100,
4928
- locale: NS,
4929
- inject: () => ({
4930
- controller,
4931
- useSnapshot
4932
- })
4933
- }, GeneralFallbacksRow);
4934
- });
4935
4157
  ctx.effect(() => ctx.uiConversation.events.register(fallbackSwitchDefinition), "llm-fallbacks: conversation node definition");
4936
4158
  ctx.slots.inject("conversation.chat.node", function* () {
4937
4159
  yield ctx.slots.register({