dsh-llm-fallbacks 0.7.0 → 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,50 +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
476
  presets: "bundled",
493
- timeSlots: [],
494
477
  tz: "Asia/Shanghai",
495
478
  recovery: "timer"
496
479
  };
497
- /**
498
- * Reserved role id: legal as a rule target (`roles.rules[].role`) and as
499
- * the no-rule-match fallback, but FORBIDDEN in `roles.list[].id`.
500
- */
501
- const INHERIT_ROLE_ID = "inherit";
502
- /**
503
- * The no-op gate (AC-8), re-keyed from the removed `enabled` switch to
504
- * CONTENT PRESENCE (plan fallbacks-web-ux-alignment T2): the plugin
505
- * intervenes exactly when something is configured — a non-empty `rootChain`
506
- * (the all-day chain), at least one time-slot row, or at least one declared
507
- * role. An inactive config is a pass-through exactly like an uninstalled
508
- * plugin; the Plugins-page row toggle remains the master switch (row
509
- * disable = fiber destroy). ONE helper for every runtime gate — the former
510
- * `config.enabled` sites (request waterfalls, picker-row registration, TUI
511
- * summary, General-row badge) all read through this.
512
- */
513
- function isFallbackActive(config) {
514
- return config.rootChain.length > 0 || (config.timeSlots?.length ?? 0) > 0 || config.roles.list.length > 0;
515
- }
516
480
  /** Role id format (aligned with yet-another-subagent `isValidProfileId`). */
517
481
  const ROLE_ID_PATTERN = /^[a-z0-9-]{1,32}$/;
518
482
  //#endregion
519
- //#region src/route-allowlist.ts
520
- /**
521
- * Whether the route is on the allowlist under exact route identity: both the
522
- * provider and the model strings must equal an allowed route exactly.
523
- */
524
- function routeOnAllowlist(route, allowedModels) {
525
- return allowedModels.some((allowed) => allowed.provider === route.provider && allowed.model === route.model);
526
- }
527
- //#endregion
528
483
  //#region src/client/fallbacks-store.ts
529
484
  /** The plugin's settings namespace on the host wire (settings/document-updated ns filter). */
530
485
  const FALLBACKS_SETTINGS_NS = "fallbacks";
@@ -580,56 +535,6 @@ window.__ModuleLoader__.load({
580
535
  }
581
536
  return entries;
582
537
  }
583
- function parsePolicyRoute(value) {
584
- if (!isRecord(value)) return void 0;
585
- if (typeof value.provider !== "string" || value.provider.length === 0) return void 0;
586
- if (typeof value.model !== "string" || value.model.length === 0) return void 0;
587
- return {
588
- provider: value.provider,
589
- model: value.model
590
- };
591
- }
592
- /**
593
- * Shape-guard the additive `subagentPolicy` wire field (T5). Malformed
594
- * payloads resolve to `undefined` (hide the status area) — the store never
595
- * throws on an old or misshapen field (old-payload tolerance).
596
- */
597
- function parseSubagentPolicyWire(value) {
598
- if (!isRecord(value) || typeof value.state !== "string") return void 0;
599
- if (value.state === "disabled" || value.state === "unprovable") return { state: value.state };
600
- if (value.state !== "enabled") return void 0;
601
- if (!Array.isArray(value.allowedModels)) return void 0;
602
- const allowedModels = [];
603
- for (const entry of value.allowedModels) {
604
- const route = parsePolicyRoute(entry);
605
- if (route === void 0) return void 0;
606
- allowedModels.push(route);
607
- }
608
- const view = {
609
- state: "enabled",
610
- allowedModels
611
- };
612
- if (isRecord(value.head) && (value.head.source === "authorized" || value.head.source === "injected")) {
613
- const route = parsePolicyRoute(value.head.route);
614
- if (route !== void 0) view.head = {
615
- route,
616
- source: value.head.source
617
- };
618
- }
619
- if (isRecord(value.blockedAttempt) && typeof value.blockedAttempt.at === "number") {
620
- const route = parsePolicyRoute(value.blockedAttempt.route);
621
- if (route !== void 0) view.blockedAttempt = {
622
- at: value.blockedAttempt.at,
623
- route,
624
- reason: typeof value.blockedAttempt.reason === "string" ? value.blockedAttempt.reason : ""
625
- };
626
- }
627
- return view;
628
- }
629
- function subagentPolicyFromWire(value, fallback) {
630
- if (!isRecord(value) || !("subagentPolicy" in value)) return fallback;
631
- return parseSubagentPolicyWire(value.subagentPolicy);
632
- }
633
538
  /**
634
539
  * The provider dropdown's offer set (spec §2.5 D-4): catalog providers whose
635
540
  * settings profile resolves in the describe namespaces — the Models page's
@@ -655,8 +560,29 @@ window.__ModuleLoader__.load({
655
560
  if (!isRecord(value)) throw new TypeError(`fallbacks descriptor value is not an object: ${String(value)}`);
656
561
  const triggerCodes = value.triggerCodes;
657
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");
658
- const rootChain = value.rootChain;
659
- 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 })");
660
586
  const roles = isRecord(value.roles) ? value.roles : {};
661
587
  const parsedList = (Array.isArray(roles.list) ? roles.list : []).map((role, index) => {
662
588
  if (!isRecord(role) || typeof role.id !== "string") throw new TypeError(`fallbacks descriptor roles.list[${String(index)}] must have a string id`);
@@ -666,32 +592,13 @@ window.__ModuleLoader__.load({
666
592
  if (prompt !== void 0 && typeof prompt !== "string") throw new TypeError(`fallbacks descriptor roles.list[${String(index)}].prompt must be a string`);
667
593
  const permissions = role.permissions;
668
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`);
669
- const chain = role.chain;
670
- 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`);
671
- const fallback = role.fallback;
672
- 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;
673
596
  return {
674
597
  id: role.id,
675
598
  persona: persona ?? "",
676
599
  ...prompt === void 0 ? {} : { prompt },
677
600
  ...permissions === void 0 ? {} : { permissions },
678
- chain: chain ?? [],
679
- fallback: fallback ?? "inherit-root"
680
- };
681
- });
682
- const parsedRules = (Array.isArray(roles.rules) ? roles.rules : []).map((rule, index) => {
683
- if (!isRecord(rule) || typeof rule.role !== "string") throw new TypeError(`fallbacks descriptor roles.rules[${String(index)}] must have a string role`);
684
- const origin = rule.origin;
685
- if (origin !== void 0 && origin !== "root" && origin !== "subagent") throw new TypeError(`fallbacks descriptor roles.rules[${String(index)}].origin must be root|subagent`);
686
- const provider = rule.provider;
687
- const model = rule.model;
688
- if (provider !== void 0 && typeof provider !== "string") throw new TypeError(`fallbacks descriptor roles.rules[${String(index)}].provider must be a string`);
689
- if (model !== void 0 && typeof model !== "string") throw new TypeError(`fallbacks descriptor roles.rules[${String(index)}].model must be a string`);
690
- return {
691
- ...origin === void 0 ? {} : { origin },
692
- ...provider === void 0 ? {} : { provider },
693
- ...model === void 0 ? {} : { model },
694
- role: rule.role
601
+ ...chain === void 0 || chain === "" ? {} : { chain }
695
602
  };
696
603
  });
697
604
  const cooldownMs = value.cooldownMs;
@@ -701,37 +608,21 @@ window.__ModuleLoader__.load({
701
608
  if (revertPolicy !== void 0 && revertPolicy !== "cooldown-expiry" && revertPolicy !== "never") throw new TypeError("fallbacks descriptor revertPolicy must be cooldown-expiry|never");
702
609
  const presets = value.presets;
703
610
  if (presets !== void 0 && presets !== "bundled" && presets !== "none") throw new TypeError("fallbacks descriptor presets must be bundled|none");
704
- const timeSlots = value.timeSlots;
705
- if (timeSlots !== void 0 && (!Array.isArray(timeSlots) || timeSlots.some((row) => {
706
- if (!isRecord(row)) return true;
707
- for (const field of [
708
- "kind",
709
- "preset",
710
- "start",
711
- "end"
712
- ]) if (row[field] !== void 0 && typeof row[field] !== "string") return true;
713
- const chain = row.chain;
714
- if (chain !== void 0 && (!Array.isArray(chain) || chain.some((entry) => typeof entry !== "string"))) return true;
715
- const days = row.days;
716
- if (days !== void 0 && (!Array.isArray(days) || days.some((day) => typeof day !== "number"))) return true;
717
- return false;
718
- }))) throw new TypeError("fallbacks descriptor timeSlots must be an array of slot rows (kind/preset/start/end strings, chain string array, days number array)");
719
611
  const tz = value.tz;
720
612
  if (tz !== void 0 && typeof tz !== "string") throw new TypeError("fallbacks descriptor tz must be a string");
721
613
  const recovery = value.recovery;
722
614
  if (recovery !== void 0 && recovery !== "timer" && recovery !== "half-open") throw new TypeError("fallbacks descriptor recovery must be timer|half-open");
723
615
  return {
724
616
  triggerCodes: triggerCodes ?? [...defaultFallbacksConfig.triggerCodes],
725
- rootChain: rootChain ?? [...defaultFallbacksConfig.rootChain],
726
- roles: {
727
- list: parsedList,
728
- rules: parsedRules
729
- },
617
+ chains: chains ?? defaultFallbacksConfig.chains.map((chain) => ({
618
+ ...chain,
619
+ timeSlots: [...chain.timeSlots ?? []]
620
+ })),
621
+ roles: { list: parsedList },
730
622
  cooldownMs: cooldownMs ?? defaultFallbacksConfig.cooldownMs,
731
623
  revertPolicy: revertPolicy ?? defaultFallbacksConfig.revertPolicy,
732
624
  maxSwitchesPerStep: maxSwitchesPerStep ?? defaultFallbacksConfig.maxSwitchesPerStep,
733
625
  presets: presets ?? defaultFallbacksConfig.presets,
734
- timeSlots: timeSlots ?? [...defaultFallbacksConfig.timeSlots ?? []],
735
626
  tz: tz ?? defaultFallbacksConfig.tz,
736
627
  recovery: recovery ?? defaultFallbacksConfig.recovery
737
628
  };
@@ -771,27 +662,6 @@ window.__ModuleLoader__.load({
771
662
  raw
772
663
  };
773
664
  }
774
- /**
775
- * Extract the most recent `fallbacks/switch` events from one history page
776
- * (spec §2.5 D-5): drop packed chunk runs, filter by event type, order by
777
- * `seq` descending, take at most `limit`. Single-page read — fewer than
778
- * `limit` events show as-is; no multi-page backfill (Non-Goal).
779
- */
780
- function extractRecentSwitches(entries, limit = 5) {
781
- const switches = [];
782
- for (const entry of entries) {
783
- if (entry.type !== "event") continue;
784
- const event = entry.event;
785
- if (event.type !== "fallbacks/switch") continue;
786
- switches.push({
787
- ...event.data,
788
- seq: event.seq,
789
- time: event.time
790
- });
791
- }
792
- switches.sort((a, b) => b.seq - a.seq);
793
- return switches.slice(0, limit);
794
- }
795
665
  /** Serialize one selector row to its wire string (`provider/model` | `provider/*`). */
796
666
  function selectorRowToRaw(row) {
797
667
  const provider = selectionToRaw(row.provider);
@@ -820,22 +690,6 @@ window.__ModuleLoader__.load({
820
690
  };
821
691
  }
822
692
  }
823
- /** Project the rootChain entries into editable rows (one flat chain row). */
824
- function rootChainToRows(rootChain, catalog) {
825
- return [{ selectors: rootChain.map((entry) => entryToSelectorRow(entry, catalog)) }];
826
- }
827
- /** Rebuild the rootChain from edited rows; rows with no usable selector drop out. */
828
- function rowsToRootChain(rows) {
829
- const entries = [];
830
- for (const row of rows) {
831
- if (row.selectors.length === 0) continue;
832
- for (const selector of row.selectors) {
833
- const raw = selectorRowToRaw(selector);
834
- if (raw !== "") entries.push(raw);
835
- }
836
- }
837
- return entries;
838
- }
839
693
  /** Project the time-slot rows into editable rows (chain selectors classified). */
840
694
  function timeSlotsToRows(timeSlots, catalog) {
841
695
  return timeSlots.map((row) => ({
@@ -851,11 +705,10 @@ window.__ModuleLoader__.load({
851
705
  }
852
706
  /** Rebuild the time-slot rows from edited rows; blank selectors drop out.
853
707
  * `kind` rides verbatim (a hand-written unknown kind reads back unchanged;
854
- * save validation rejects it) — the cast asserts the trusted editor shape.
855
- * `days` is ALWAYS serialized ([] included): schemastery composes absent
856
- * array fields as `[]`, so the composed config every card load accepts
857
- * carries `days` on every row — the draft must too, or a clean card would
858
- * 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. */
859
712
  function rowsToTimeSlots(rows) {
860
713
  return rows.map((row) => {
861
714
  const chain = row.selectors.map(selectorRowToRaw).filter((entry) => entry !== "");
@@ -876,35 +729,63 @@ window.__ModuleLoader__.load({
876
729
  };
877
730
  });
878
731
  }
879
- /** Project the declared roles into editable rows (chain selectors classified). */
880
- 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) {
881
766
  return roles.map((role) => ({
882
767
  id: role.id,
883
768
  persona: role.persona,
884
- selectors: (role.chain ?? []).map((entry) => entryToSelectorRow(entry, catalog)),
885
- fallback: role.fallback ?? "inherit-root",
886
- collapsed: true,
769
+ chain: role.chain ?? "",
770
+ collapsed: false,
887
771
  personaOpen: false
888
772
  }));
889
773
  }
890
- /** Rebuild the declared roles from edited rows; empty selectors drop out. */
774
+ /** Rebuild the declared roles from edited rows; an empty pointer drops out. */
891
775
  function rowsToRoles(rows) {
892
776
  return rows.map((row) => ({
893
777
  id: row.id.trim(),
894
778
  persona: row.persona,
895
- chain: row.selectors.map(selectorRowToRaw).filter((entry) => entry !== ""),
896
- fallback: row.fallback
779
+ ...row.chain === "" ? {} : { chain: row.chain }
897
780
  }));
898
781
  }
899
782
  /**
900
783
  * Rebuild the declared roles from edited rows, re-attaching the
901
784
  * schema-reserved `prompt`/`permissions` fields from the last accepted
902
785
  * config by role id — they never round-trip through rows this round, so
903
- * without the merge a save would silently drop them (T2 reviewer minor
904
- * #2). The id trim matches {@link rowsToRoles}; a row whose id matches no
905
- * original role (a freshly added one) keeps no extras. Key order mirrors
906
- * `parseFallbacksConfig` so a clean draft's JSON dirty comparison never
907
- * 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.
908
789
  */
909
790
  function mergeRoleExtras(rows, originalRoles) {
910
791
  const originalById = new Map(originalRoles.map((role) => [role.id, role]));
@@ -916,36 +797,10 @@ window.__ModuleLoader__.load({
916
797
  persona: role.persona,
917
798
  ...original.prompt === void 0 ? {} : { prompt: original.prompt },
918
799
  ...original.permissions === void 0 ? {} : { permissions: original.permissions },
919
- chain: role.chain,
920
- fallback: role.fallback
800
+ ...role.chain === void 0 ? {} : { chain: role.chain }
921
801
  };
922
802
  });
923
803
  }
924
- /**
925
- * The `roles.rules` role dropdown's offer set — the ONLY data source for the
926
- * rule rows' role selector: the built-in `'inherit'` target plus every
927
- * declared `roles.list` id, in declaration order (a role added/removed on
928
- * the same page is reflected immediately).
929
- */
930
- function ruleRoleOptions(roles) {
931
- return [INHERIT_ROLE_ID, ...new Set(roles.list.map((role) => role.id.trim()))];
932
- }
933
- /** Project the role rules into editable rows (provider/model classified). */
934
- function rulesToRows(rules, catalog) {
935
- return rules.map((rule) => ({
936
- provider: classifyProvider(rule.provider ?? "", catalog),
937
- model: classifyModel(rule.provider ?? "", rule.model ?? "", catalog),
938
- role: rule.role
939
- }));
940
- }
941
- /** Rebuild the role rules from edited rows; empty provider/model drop out. */
942
- function rowsToRules(rows) {
943
- return rows.map((row) => ({
944
- ...row.provider === null ? {} : { provider: selectionToRaw(row.provider) },
945
- ...row.model === null ? {} : { model: selectionToRaw(row.model) },
946
- role: row.role.trim()
947
- })).filter((rule) => rule.role !== "");
948
- }
949
804
  /** Controller joining Settings reads, writes, and pushed invalidations. */
950
805
  var FallbacksSettingsController = class {
951
806
  api;
@@ -965,10 +820,7 @@ window.__ModuleLoader__.load({
965
820
  configuredProviders: [],
966
821
  groups: [],
967
822
  catalogEpoch: 0,
968
- switchesStatus: "idle",
969
- switchesError: null,
970
- switches: [],
971
- subagentPolicy: void 0
823
+ currentSession: void 0
972
824
  });
973
825
  /** Read guard: a newer load() supersedes an older one's publish. */
974
826
  readGeneration = 0;
@@ -979,22 +831,12 @@ window.__ModuleLoader__.load({
979
831
  */
980
832
  writeGeneration = 0;
981
833
  catalogGeneration = 0;
982
- switchesGeneration = 0;
983
834
  /** Every settings namespace from the last describe, keyed by ns — the configured-provider join's other input. */
984
835
  namespaces = /* @__PURE__ */ new Map();
985
- currentSession;
986
- /**
987
- * The store's dispose signal (qc3 F-003): created alongside the store
988
- * lifecycle, threaded into every `session.follow` call, aborted once in
989
- * {@link dispose} — a follow opening still mid-await at plugin unload
990
- * (HMR / fiber dispose, or a wedged gateway stream) is cancelled promptly
991
- * instead of staying open until the mount abort or socket failure.
992
- */
993
- followAbort = new AbortController();
994
836
  /**
995
837
  * @param api - the Remote namespace faces the store reads (settings
996
838
  * describe `writable` + namespace directory, llm provider directory,
997
- * session model catalog + follow tail page) — `ctx.remote` cast once at
839
+ * session model catalog) — `ctx.remote` cast once at
998
840
  * the mount boundary.
999
841
  * @param rpc - the connection's generic RPC caller for the host gateway
1000
842
  * channel (`/api`), injected from the connection handle.
@@ -1042,8 +884,7 @@ window.__ModuleLoader__.load({
1042
884
  }
1043
885
  if ("seeds" in getResult.value) seeds = parseSeedsWire(getResult.value.seeds);
1044
886
  }
1045
- const subagentPolicy = getResult !== void 0 && getResult.ok ? subagentPolicyFromWire(getResult.value, void 0) : void 0;
1046
- this.accept(config, writable, legacyKeys, seeds, subagentPolicy);
887
+ this.accept(config, writable, legacyKeys, seeds);
1047
888
  } catch (error) {
1048
889
  if (generation !== this.readGeneration) return;
1049
890
  if (writeGenerationAtStart !== this.writeGeneration) return;
@@ -1092,75 +933,13 @@ window.__ModuleLoader__.load({
1092
933
  }
1093
934
  }
1094
935
  /**
1095
- * Record the current session the status block reads (spec §2.5 D-5). Once
1096
- * the block has been read once, its summary follows session switches
1097
- * immediately; an idle block only records the id — the section's mount
1098
- * effect performs the first read.
1099
- * @param sessionId - the session whose history is summarized; undefined
1100
- * (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).
1101
938
  */
1102
939
  setCurrentSession(sessionId) {
1103
- if (sessionId === this.currentSession) return;
1104
- this.currentSession = sessionId;
1105
- if (this.store.getSnapshot().switchesStatus !== "idle") this.loadSwitches();
1106
- }
1107
- /**
1108
- * Read the recent-switch summary for the current session (spec §2.5 D-5):
1109
- * one tail page (`maxMessages` = {@link SWITCHES_HISTORY_PAGE}) read from
1110
- * the `session/follow` opening snapshot — the host has no unary tail-page
1111
- * read (`session/page` needs the follow opening frame's cursor), so the
1112
- * stream is cancelled right after its first frame —
1113
- * `fallbacks/switch` events extracted newest-first capped at
1114
- * {@link RECENT_SWITCH_LIMIT}. No current session → honest empty ready
1115
- * state (no RPC); a read failure lands `switchesStatus: 'error'` and never
1116
- * touches the settings state (the form keeps editing/saving normally).
1117
- * @returns nothing; {@link store} carries success or failure.
1118
- */
1119
- async loadSwitches() {
1120
- const generation = ++this.switchesGeneration;
1121
- const sessionId = this.currentSession;
1122
- if (sessionId === void 0) {
1123
- this.store.update((state) => {
1124
- state.switchesStatus = "ready";
1125
- state.switchesError = null;
1126
- state.switches = [];
1127
- });
1128
- return;
1129
- }
1130
940
  this.store.update((state) => {
1131
- state.switchesStatus = "loading";
1132
- state.switchesError = null;
941
+ state.currentSession = sessionId;
1133
942
  });
1134
- try {
1135
- const iterator = this.api.session.follow({
1136
- address: {
1137
- kind: "session",
1138
- sessionId
1139
- },
1140
- maxMessages: 50
1141
- }, this.followAbort.signal)[Symbol.asyncIterator]();
1142
- let first;
1143
- try {
1144
- first = await iterator.next();
1145
- } finally {
1146
- await iterator.return?.();
1147
- }
1148
- if (generation !== this.switchesGeneration) return;
1149
- if (first.done || first.value.type !== "snapshot") throw new Error("session follow closed before its opening snapshot");
1150
- const switches = extractRecentSwitches(first.value.records);
1151
- this.store.update((state) => {
1152
- state.switchesStatus = "ready";
1153
- state.switchesError = null;
1154
- state.switches = switches;
1155
- });
1156
- } catch (error) {
1157
- if (generation !== this.switchesGeneration) return;
1158
- const wire = error;
1159
- this.store.update((state) => {
1160
- state.switchesStatus = "error";
1161
- state.switchesError = typeof wire?.message === "string" ? wire.message : messageOf(error);
1162
- });
1163
- }
1164
943
  }
1165
944
  /**
1166
945
  * Persist the full edited configuration through the gateway channel
@@ -1192,30 +971,6 @@ window.__ModuleLoader__.load({
1192
971
  }
1193
972
  }
1194
973
  /**
1195
- * Explicit "sync to host allowlist" write (plan
1196
- * role-chain-allowlist-visibility T2): the gateway endpoint merges the
1197
- * CURRENT allowlist with this plugin's exact chain routes plus the virtual
1198
- * `FallbacksChain/Auto` route and writes only `allowedModels`. The button
1199
- * click is the ONLY trigger — the store never syncs on its own.
1200
- * @returns true when the write landed (the card then reads the fresh
1201
- * snapshot from the store); false on a refused/failed call, leaving the
1202
- * last accepted state untouched. A response discarded by the write
1203
- * generation guard is still a landed write — dispose() is the only
1204
- * invalidator, so it must not report failure.
1205
- */
1206
- async syncAllowlist() {
1207
- if (!this.store.getSnapshot().writable) return false;
1208
- const generation = ++this.writeGeneration;
1209
- try {
1210
- const result = await this.rpc.call("/api", "fallbacks/sync-allowlist", { args: {} });
1211
- if (!result.ok) return false;
1212
- if (generation === this.writeGeneration) this.acceptWire(result.value);
1213
- return true;
1214
- } catch {
1215
- return false;
1216
- }
1217
- }
1218
- /**
1219
974
  * Reset to composition defaults through the gateway channel
1220
975
  * (`/api/fallbacks/reset` — the fallbacks-specific third method; the host
1221
976
  * clears the user layer via `settings.replace(ns, {})`, the removal path a
@@ -1244,15 +999,13 @@ window.__ModuleLoader__.load({
1244
999
  this.readGeneration += 1;
1245
1000
  this.writeGeneration += 1;
1246
1001
  this.catalogGeneration += 1;
1247
- this.switchesGeneration += 1;
1248
1002
  this.namespaces = /* @__PURE__ */ new Map();
1249
- this.followAbort.abort();
1250
1003
  }
1251
1004
  /**
1252
- * Unpack one write response (set / reset / sync-allowlist) into the store
1253
- * snapshot. The additive fields (legacyKeys §9, seeds §9.4, subagentPolicy
1254
- * D4) follow the W-1/F-1 rule: absent on the wire keeps the last accepted
1255
- * 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.
1256
1009
  */
1257
1010
  acceptWire(value) {
1258
1011
  const config = value !== null && typeof value === "object" && "config" in value ? value.config : void 0;
@@ -1266,8 +1019,7 @@ window.__ModuleLoader__.load({
1266
1019
  const wireSeeds = value.seeds;
1267
1020
  if (Array.isArray(wireSeeds)) seeds = parseSeedsWire(wireSeeds);
1268
1021
  }
1269
- const subagentPolicy = subagentPolicyFromWire(value, this.store.getSnapshot().subagentPolicy);
1270
- this.accept(config, true, legacyKeys, seeds, subagentPolicy);
1022
+ this.accept(config, true, legacyKeys, seeds);
1271
1023
  }
1272
1024
  /**
1273
1025
  * Publish a settled load: `status` ready, `writable` from describe, and —
@@ -1285,7 +1037,7 @@ window.__ModuleLoader__.load({
1285
1037
  * only when a real config resolved — a transient channel-down keeps the
1286
1038
  * last accepted badge state.
1287
1039
  */
1288
- accept(config, writable, legacyKeys, seeds, subagentPolicy) {
1040
+ accept(config, writable, legacyKeys, seeds) {
1289
1041
  const parsed = config === void 0 ? void 0 : parseFallbacksConfig(config);
1290
1042
  this.store.update((state) => {
1291
1043
  state.status = "ready";
@@ -1294,7 +1046,6 @@ window.__ModuleLoader__.load({
1294
1046
  state.present = parsed !== void 0;
1295
1047
  state.legacyKeys = parsed === void 0 ? state.legacyKeys : legacyKeys;
1296
1048
  state.seeds = parsed === void 0 ? state.seeds : seeds;
1297
- state.subagentPolicy = parsed === void 0 ? state.subagentPolicy : subagentPolicy;
1298
1049
  if (parsed !== void 0) state.config = parsed;
1299
1050
  state.configuredProviders = configuredProvidersOf(state.providers, this.namespaces);
1300
1051
  });
@@ -1325,17 +1076,6 @@ window.__ModuleLoader__.load({
1325
1076
  if (controller.store.getSnapshot().catalogStatus === "idle") return;
1326
1077
  controller.loadCatalog();
1327
1078
  }
1328
- /**
1329
- * Refetch the recent-switch summary after `settings/document-updated`
1330
- * (fallbacks ns) / `connection/reset` only when the status block has already
1331
- * been read once
1332
- * (the switches twin of {@link refreshFallbacksIfLoaded}).
1333
- * @param controller - the fallbacks settings controller.
1334
- */
1335
- function refreshSwitchesIfLoaded(controller) {
1336
- if (controller.store.getSnapshot().switchesStatus === "idle") return;
1337
- controller.loadSwitches();
1338
- }
1339
1079
  //#endregion
1340
1080
  //#region src/client/locales.ts
1341
1081
  /**
@@ -1370,7 +1110,7 @@ window.__ModuleLoader__.load({
1370
1110
  "maxSwitchesPerStep.label": "单次请求行走最大切换",
1371
1111
  "maxSwitchesPerStep.hint": "超过后停止切换",
1372
1112
  "maxSwitchesPerStep.tooltip": "超过后不再切换,以原始错误语义结束本次请求,防止链循环放大延迟。",
1373
- "virtualOnly.rule": "只有模型选择为 FallbacksChain/Auto 的会话(含插件注入的子代理)会执行降级链;选择真实模型的会话没有插件降级,仅由 llm-retry 处理重试。",
1113
+ "virtualOnly.rule": "只有模型选择为 FallbacksChain/<链 id> 的会话(含继承该路由的子代理)会执行降级链;选择真实模型的会话没有插件降级,仅由 llm-retry 处理重试。",
1374
1114
  "advanced.label": "高级选项",
1375
1115
  "advanced.expand": "展开高级选项",
1376
1116
  "advanced.collapse": "收起高级选项",
@@ -1385,21 +1125,26 @@ window.__ModuleLoader__.load({
1385
1125
  "chains.selector.modelPlaceholder": "选择 model",
1386
1126
  "chains.selector.wildcardLegacy": "该条目为通配(provider/*):选择具体模型后将转为精确条目",
1387
1127
  "chains.selector.noModelsStrict": "该 provider 暂无可用模型(目录查询失败),请改选其他 provider。",
1388
- "mainAgent.label": "主代理",
1389
1128
  "subagents.label": "子代理",
1390
- "subagents.policy.label": "宿主子代理模型策略",
1391
- "subagents.policy.allowlist": "允许的模型",
1392
- "subagents.policy.head": "当前链头",
1393
- "subagents.policy.source.authorized": "宿主授权",
1394
- "subagents.policy.source.injected": "插件注入",
1395
- "subagents.policy.blocked": "降级候选均不在允许列表中 — 未切换",
1396
- "subagents.policy.unprovable": "策略存在但无法读取 — 插件切换已禁用",
1397
- "subagents.policy.offAllowlist": "角色链中有 {count} 个精确条目不在允许列表 — 这些条目不会生效",
1398
- "subagents.policy.recommend": "建议将 FallbacksChain/Auto 加入允许列表(该虚拟路由不受宿主策略过滤)",
1399
- "subagents.policy.sync": "同步到宿主允许列表",
1400
- "subagents.policy.sync.done": "已同步:精确条目(角色链、默认降级链与 FallbacksChain/Auto)已写入允许列表;通配条目不展开、不同步",
1401
- "subagents.policy.sync.failed": "同步失败:宿主设置未写入(策略未启用、条目不可配置或版本冲突等宿主拒绝),请检查宿主设置",
1402
- "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": "保存",
1403
1148
  "timeSlots.label": "分时槽设置",
1404
1149
  "timeSlots.hint": "自上而下第一条命中生效;全时段行固定最后",
1405
1150
  "timeSlots.tz.label": "时区(UTC±)",
@@ -1447,8 +1192,6 @@ window.__ModuleLoader__.load({
1447
1192
  "allDay.pro": "官方 Pro(deepseek-official/deepseek-v4-pro)",
1448
1193
  "allDay.nonconforming": "当前默认模型不合法:请选择官方 Flash 或 Pro 后保存",
1449
1194
  "roles.list.label": "角色实体",
1450
- "roles.list.hint": "先声明角色,规则才能引用",
1451
- "roles.list.tooltip": "角色 id 须匹配 /^[a-z0-9-]{1,32}$/ 且唯一;\"inherit\" 为保留字,不能用作角色 id。",
1452
1195
  "roles.id": "id",
1453
1196
  "roles.id.hint": "小写字母/数字/连字符,1–32 字符",
1454
1197
  "roles.idPlaceholder": "例如 reviewer",
@@ -1460,35 +1203,24 @@ window.__ModuleLoader__.load({
1460
1203
  "roles.seedSource.bundled": "内置",
1461
1204
  "roles.seedSource.external": "外部",
1462
1205
  "roles.seedSource.user": "用户",
1463
- "roles.seedChainOptional": "角色 \"{id}\" 为 seed 角色:链可留空,保存不会被拦截",
1464
- "roles.fallback": "链拼接策略",
1465
- "roles.fallback.inherit-root": "继承 root(角色链后追加 rootChain)",
1466
- "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": "未指定降级链",
1467
1212
  "roles.add": "添加角色",
1468
1213
  "roles.remove": "删除该角色",
1469
1214
  "roles.expand": "展开该角色",
1470
1215
  "roles.collapse": "收起该角色",
1471
- "roles.selector.add": "添加选择器",
1472
- "roles.rules": "角色规则",
1473
- "roles.rules.hint": "仅对子代理生效:顺序匹配 provider/model,未命中 → inherit(root 链)",
1474
- "roles.rules.tooltip": "规则仅对子代理生效(root 请求不匹配规则):命中后走对应角色的链;未命中走内置 inherit(rootChain)。",
1475
- "roles.rule.provider": "provider",
1476
- "roles.rule.provider.any": "任意",
1477
- "roles.rule.model": "model",
1478
- "roles.rule.model.any": "任意",
1479
- "roles.rule.role": "角色",
1480
- "roles.rule.role.inherit": "inherit(内置:root 链)",
1481
- "roles.rule.roleSelectPlaceholder": "选择角色",
1482
- "roles.rule.roleUndeclared.short": "(未声明)",
1483
- "roles.addRule": "添加规则",
1484
- "roles.removeRule": "删除该规则",
1485
1216
  "validation.blocked": "配置校验未通过,保存被拦截:",
1486
1217
  "validation.roleIdFormat": "角色 id \"{id}\" 不符合格式 /^[a-z0-9-]{1,32}$/",
1487
1218
  "validation.roleIdReserved": "\"inherit\" 为保留角色 id,不能用于角色实体",
1488
1219
  "validation.roleIdDuplicate": "角色 id \"{id}\" 重复",
1489
- "validation.ruleRoleUndeclared": "规则引用了未声明的角色 \"{role}\"",
1490
- "validation.ruleRoleRequired": "规则未选择角色:请选择目标角色,或删除该行",
1491
- "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": "默认降级链不能为空:请至少添加一条选择器。",
1492
1224
  "validation.allDayRequired": "默认模型必须二选一:官方 Flash 或 Pro",
1493
1225
  "validation.slotChainRequired": "分时槽未配置模型:请至少添加一条链选择器",
1494
1226
  "validation.slotWindow": "分时槽开始/结束时间须为 HH:mm 格式",
@@ -1498,7 +1230,7 @@ window.__ModuleLoader__.load({
1498
1230
  "validation.slotPresetDuplicate": "预设 \"{preset}\" 已存在:每个预设只能添加一行",
1499
1231
  "validation.slotPresetFrozen": "预设窗口是冻结代码常量:预设行不能携带 start/end/days",
1500
1232
  "validation.selector": "选择器 \"{entry}\" 非法:{message}",
1501
- "legacy.banner": "检测到旧格式配置字段({keys}):已按新模型展示,请按 docs/configuration.md 迁移表手工改写;插件不会自动改写配置。",
1233
+ "legacy.banner": "检测到旧格式字段({keys}):已自动升级为命名链——原默认降级链成为第 0 条链「默认链」,角色与人格保持不变,旧的角色数组链已置为「未指定」(可在下方重新指定)。保存一次即可固化;插件不会主动改写你的配置文件。",
1502
1234
  "catalog.empty": "暂无可用模型:请先在模型页添加模型,添加后此处将自动可选。",
1503
1235
  "catalog.error": "模型目录读取失败:{message}",
1504
1236
  "catalog.partial": "部分 provider 模型查询失败:{message}",
@@ -1506,24 +1238,9 @@ window.__ModuleLoader__.load({
1506
1238
  "catalog.outside.tooltip": "不在当前模型目录,可保留原值并保存;新增条目仅可从目录选择。",
1507
1239
  "catalog.outside.short": " (目录外)",
1508
1240
  "catalog.unconfigured.short": " (未配置)",
1509
- "status.title": "运行状态(只读)",
1510
- "status.switches.label": "最近切换:",
1511
- "status.switches.empty": "本会话暂无 fallback 切换。",
1512
- "status.switches.error": "切换历史读取失败:{message}",
1513
- "status.switches.compact": "最近 {count} 次 · {from} → {to}({role} · {reason})",
1514
- "status.switches.compact.roleInject": "最近 {count} 次 · {role} → {to}({reason})",
1515
1241
  "status.switches.reason.trigger-code": "触发失败码",
1516
1242
  "status.switches.reason.always-cap": "always 模式上限",
1517
1243
  "status.switches.reason.role-inject": "角色注入",
1518
- "general.title": "模型故障降级",
1519
- "general.enabled": "已启用",
1520
- "general.disabled": "未启用",
1521
- "general.unknown": "未知",
1522
- "general.unavailable": "状态通道暂不可达",
1523
- "general.switch": "最近切换:{from} → {to}({role} · {reason})",
1524
- "general.switch.roleInject": "最近切换:{role} → {to}({reason})",
1525
- "general.switch.empty": "本会话暂无切换",
1526
- "general.error": "状态读取失败:{message}",
1527
1244
  "chat.switch.title": "模型已降级",
1528
1245
  "chat.switch.summary": "{from} → {to}({reason})",
1529
1246
  "chat.switch.summary.roleInject": "({reason})",
@@ -1561,7 +1278,7 @@ window.__ModuleLoader__.load({
1561
1278
  "maxSwitchesPerStep.label": "Max switches per request walk",
1562
1279
  "maxSwitchesPerStep.hint": "Stops switching beyond the cap",
1563
1280
  "maxSwitchesPerStep.tooltip": "Beyond this no further switch happens and the request ends with the original error semantics, preventing chain loops from amplifying latency.",
1564
- "virtualOnly.rule": "Only sessions whose model selection is FallbacksChain/Auto (including plugin-injected subagents) walk a chain; a real-model selection gets no plugin fallback — llm-retry alone handles its retries.",
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.",
1565
1282
  "advanced.label": "Advanced options",
1566
1283
  "advanced.expand": "Expand advanced options",
1567
1284
  "advanced.collapse": "Collapse advanced options",
@@ -1576,21 +1293,26 @@ window.__ModuleLoader__.load({
1576
1293
  "chains.selector.modelPlaceholder": "Select model",
1577
1294
  "chains.selector.wildcardLegacy": "This entry is a wildcard (provider/*): picking a model converts it to an exact entry",
1578
1295
  "chains.selector.noModelsStrict": "No models available for this provider (catalog lookup failed); pick another provider.",
1579
- "mainAgent.label": "Main agent",
1580
1296
  "subagents.label": "Subagents",
1581
- "subagents.policy.label": "Host subagent model policy",
1582
- "subagents.policy.allowlist": "Allowed models",
1583
- "subagents.policy.head": "Effective chain head",
1584
- "subagents.policy.source.authorized": "authorized",
1585
- "subagents.policy.source.injected": "injected",
1586
- "subagents.policy.blocked": "No in-allowlist fallback candidate — switch skipped",
1587
- "subagents.policy.unprovable": "Policy present but unreadable — plugin switches disabled",
1588
- "subagents.policy.offAllowlist": "{count} exact role-chain entries are outside the allowlist — they will not run",
1589
- "subagents.policy.recommend": "Add FallbacksChain/Auto to the allowlist (this virtual route is not filtered by the host policy)",
1590
- "subagents.policy.sync": "Sync to host allowlist",
1591
- "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",
1592
- "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",
1593
- "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",
1594
1316
  "timeSlots.label": "Time slots",
1595
1317
  "timeSlots.hint": "First match from top to bottom wins; the all-day row is always last",
1596
1318
  "timeSlots.tz.label": "Timezone (UTC±)",
@@ -1638,8 +1360,7 @@ window.__ModuleLoader__.load({
1638
1360
  "allDay.pro": "Official Pro (deepseek-official/deepseek-v4-pro)",
1639
1361
  "allDay.nonconforming": "The current default model is not valid: pick official Flash or Pro before saving",
1640
1362
  "roles.list.label": "Declared roles",
1641
- "roles.list.hint": "Declare roles before rules can reference them",
1642
- "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 指定降级链",
1643
1364
  "roles.id": "ID",
1644
1365
  "roles.id.hint": "lowercase letters, digits, hyphens; 1–32 chars",
1645
1366
  "roles.idPlaceholder": "e.g. reviewer",
@@ -1651,35 +1372,23 @@ window.__ModuleLoader__.load({
1651
1372
  "roles.seedSource.bundled": "bundled",
1652
1373
  "roles.seedSource.external": "external",
1653
1374
  "roles.seedSource.user": "User",
1654
- "roles.seedChainOptional": "Role \"{id}\" is a seed role: the chain may stay empty",
1655
- "roles.fallback": "Chain append",
1656
- "roles.fallback.inherit-root": "Inherit root (append rootChain after the role chain)",
1657
- "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",
1658
1380
  "roles.add": "Add role",
1659
1381
  "roles.remove": "Remove this role",
1660
1382
  "roles.expand": "Expand this role",
1661
1383
  "roles.collapse": "Collapse this role",
1662
- "roles.selector.add": "Add selector",
1663
- "roles.rules": "Role rules",
1664
- "roles.rules.hint": "Subagents only: matches provider/model in order; no match → inherit (root chain)",
1665
- "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).",
1666
- "roles.rule.provider": "provider",
1667
- "roles.rule.provider.any": "Any",
1668
- "roles.rule.model": "model",
1669
- "roles.rule.model.any": "Any",
1670
- "roles.rule.role": "role",
1671
- "roles.rule.role.inherit": "inherit (built-in: root chain)",
1672
- "roles.rule.roleSelectPlaceholder": "Select role",
1673
- "roles.rule.roleUndeclared.short": " (undeclared)",
1674
- "roles.addRule": "Add rule",
1675
- "roles.removeRule": "Remove this rule",
1676
1384
  "validation.blocked": "Configuration validation failed; save was blocked: ",
1677
1385
  "validation.roleIdFormat": "Role id \"{id}\" does not match /^[a-z0-9-]{1,32}$/",
1678
1386
  "validation.roleIdReserved": "\"inherit\" is a reserved role id and cannot be declared",
1679
1387
  "validation.roleIdDuplicate": "Duplicate role id \"{id}\"",
1680
- "validation.ruleRoleUndeclared": "Rule references undeclared role \"{role}\"",
1681
- "validation.ruleRoleRequired": "Rule has no role selected: pick a target role, or remove the row",
1682
- "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.",
1683
1392
  "validation.allDayRequired": "The default model must be exactly one official model (Flash or Pro)",
1684
1393
  "validation.slotChainRequired": "Time-slot row has no models: add at least one chain entry",
1685
1394
  "validation.slotWindow": "Time-slot start/end must use HH:mm format",
@@ -1689,7 +1398,7 @@ window.__ModuleLoader__.load({
1689
1398
  "validation.slotPresetDuplicate": "Preset \"{preset}\" already exists: at most one row per preset",
1690
1399
  "validation.slotPresetFrozen": "Preset windows are frozen code constants: a preset row cannot carry start/end/days",
1691
1400
  "validation.selector": "Invalid selector \"{entry}\": {message}",
1692
- "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.",
1693
1402
  "catalog.empty": "No models yet: add a model on the Models page first; options will appear here automatically.",
1694
1403
  "catalog.error": "Model catalog read failed: {message}",
1695
1404
  "catalog.partial": "Some provider model lookups failed: {message}",
@@ -1697,24 +1406,9 @@ window.__ModuleLoader__.load({
1697
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).",
1698
1407
  "catalog.outside.short": " (outside catalog)",
1699
1408
  "catalog.unconfigured.short": " (not configured)",
1700
- "status.title": "Runtime status (read-only)",
1701
- "status.switches.label": "Recent switches: ",
1702
- "status.switches.empty": "No fallback switches in this session yet.",
1703
- "status.switches.error": "Switch history read failed: {message}",
1704
- "status.switches.compact": "last {count} · {from} → {to} ({role} · {reason})",
1705
- "status.switches.compact.roleInject": "last {count} · {role} → {to} ({reason})",
1706
1409
  "status.switches.reason.trigger-code": "trigger code",
1707
1410
  "status.switches.reason.always-cap": "always-mode cap",
1708
1411
  "status.switches.reason.role-inject": "role inject",
1709
- "general.title": "Model failover",
1710
- "general.enabled": "Enabled",
1711
- "general.disabled": "Disabled",
1712
- "general.unknown": "Unknown",
1713
- "general.unavailable": "Status channel unavailable",
1714
- "general.switch": "Last switch: {from} → {to} ({role} · {reason})",
1715
- "general.switch.roleInject": "Last switch: {role} → {to} ({reason})",
1716
- "general.switch.empty": "No switches this session",
1717
- "general.error": "Status read failed: {message}",
1718
1412
  "chat.switch.title": "Model downgraded",
1719
1413
  "chat.switch.summary": "{from} → {to} ({reason})",
1720
1414
  "chat.switch.summary.roleInject": "({reason})",
@@ -1732,8 +1426,8 @@ window.__ModuleLoader__.load({
1732
1426
  /** The settings section's dictionary namespace. */
1733
1427
  const NS = "fallbacks";
1734
1428
  /**
1735
- * Reason → locale key map for switch summaries (S-c; shared by the card's
1736
- * 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
1737
1431
  * reasons resolve from ONE key family (`status.switches.reason.*`) — the
1738
1432
  * shared reason vocabulary must not mix dictionary families (qc1 F-004). The
1739
1433
  * session log is durable and forward-compatible: a reason value outside the
@@ -1767,13 +1461,13 @@ window.__ModuleLoader__.load({
1767
1461
  }
1768
1462
  //#endregion
1769
1463
  //#region \0dsh-css:/home/runner/work/dsh-llm-fallbacks/dsh-llm-fallbacks/src/client/FallbacksCard.module.css.mjs
1770
- 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 > ._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";
1771
- const tagId$3 = "dsh-llm-fallbacks/FallbacksCard.module.css";
1772
- 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) {
1773
1467
  const tag = document.createElement("style");
1774
1468
  tag.dataset.plugin = "dsh-llm-fallbacks";
1775
- tag.dataset.pluginCss = tagId$3;
1776
- tag.textContent = css$3;
1469
+ tag.dataset.pluginCss = tagId$2;
1470
+ tag.textContent = css$2;
1777
1471
  document.head.appendChild(tag);
1778
1472
  }
1779
1473
  var FallbacksCard_module_css_default = {
@@ -1860,92 +1554,71 @@ window.__ModuleLoader__.load({
1860
1554
  "policyStatusLive": "_579f87fb_policyStatusLive",
1861
1555
  "policyStatusLabel": "_d4b67a81_policyStatusLabel",
1862
1556
  "policyStatusLine": "_278bb4b3_policyStatusLine",
1863
- "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"
1864
1576
  };
1865
1577
  //#endregion
1866
1578
  //#region src/client/FallbacksCard.tsx
1867
1579
  /**
1868
- * Fallbacks settings card — the `fallbacks` plugin card on the web
1869
- * Plugins page (spec §4). Registered into the `plugins.bundle.config` keyed
1870
- * slot (key `dsh-llm-fallbacks`, the bundle package name, alongside
1871
- * the upstream bash/agent-loop/web-search cards and the advisor card, in
1872
- * registration order); owner props are empty and all data flows
1873
- * through {@link FallbacksSettingsController}.
1874
- *
1875
- * FLAT rebuild (plan fallbacks-web-ux-alignment T3, the advisor PR #98
1876
- * pattern): the collapsible card chrome is gone — no header button, no
1877
- * rotating chevron, no dirty "unsaved" pill, no card-local open state, no
1878
- * bordered `<li>` box. The page already renders the plugin title/description
1879
- * above the card (the `locale/*.json` meta files the host resolves per UI
1880
- * language), so the notices, the form, and the section actions directly
1881
- * beneath it in the official flat settings-form language (reference §3):
1882
- * fields at
1883
- * 12px vertical padding with 0.5px hairline separators, 34px inputs on the
1884
- * 0.5px l4 border, per-section Discard + Save pairs on the section
1885
- * headings. The card is ALWAYS open —
1886
- * nothing needs card-level disclosure state; the degraded /
1887
- * error / read-only / migration notices render flat atop the form (the
1888
- * unavailable notice stays always-visible per KD-G5 — the documented
1889
- * divergence from the advisor, whose unavailable card renders nothing: the
1890
- * fallbacks skeleton stays the USABLE last-known form and saves are
1891
- * attempted).
1892
- *
1893
- * The config-level `enabled` switch is REMOVED (T2, breaking): there is no
1894
- * checkbox row and no `enabled.off` hiding — all fields render
1895
- * unconditionally; the Plugins-page row toggle is the master switch and the
1896
- * runtime gates read content presence (`isFallbackActive`).
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`).
1897
1583
  *
1898
- * PER-SECTION Save/Discard return beside each section heading (plan
1899
- * fallbacks-card-section-ux, the maintainer's live-host feedback reversing
1900
- * the flat plan's one-document model): the scroll-to-save cost of ONE
1901
- * bottom Save outweighed it. Each section's Save writes ONLY that section's
1902
- * fields (patch = the last ACCEPTED config with the section's fields
1903
- * replaced — the ride-along protection, PR #62 UX round 3), gated by that
1904
- * section's dirty term and validation bucket only (KD-U1: save =
1905
- * `!sectionDirty || saving || !writable`, discard = `!sectionDirty ||
1906
- * saving`; Save additionally carries `!writable` — W-1, qc2 fix wave: the
1907
- * dirty-implies-writable assumption does not hold for this in-place-draft
1908
- * store). Violations render live under their OWNING section heading, and a
1909
- * store write failure renders under the section whose Save was last
1910
- * clicked (`lastSaveSection`). Discard is kept — our staged edits survive
1911
- * refresh, the advisor's documented divergence from the official
1912
- * no-discard form — a pure client-side revert of THAT section's editors to
1913
- * the last accepted config.
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).
1914
1594
  *
1915
- * The form body is the two-block editing surface (spec §8): the 6 top-level
1916
- * scalar fields (trigger codes / revert policy / three numeric fields), the
1917
- * `rootChain` block (block 1 — the root agent's single chain, no key input),
1918
- * and the roles block (block 2 — declared role entity cards from
1919
- * `roles.list` plus the rule rows from `roles.rules`, whose role field is a
1920
- * dropdown bound to the declared ids + the built-in `inherit`, same-page
1921
- * live). Row-level disclosures that are CONTENT keep their expand/collapse:
1922
- * the time-slot row cards and the role entity cards (with the seeded
1923
- * persona briefs) stay collapsible — a read-only view forces them open so
1924
- * the configs stay reachable. The 主代理 / 子代理 / 高级选项 group labels
1925
- * are semantic `<h2>` headings (T1); 高级选项 is a content-level disclosure
1926
- * collapsed by default (T3 — same expand/collapse contract as the rows, a
1927
- * read-only view forces it open).
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.
1928
1600
  *
1929
- * Validation: id format/reserved word/duplicates, undeclared rule role
1930
- * references, illegal selectors, and a role with no chain entries (no model
1931
- * config) block the write (the Save disabled gate) with per-section banners
1932
- * + inline red borders / hints (never touching the store error path); a
1933
- * non-empty `state.legacyKeys` renders the migration banner at the top.
1934
- * The reset-to-defaults affordance is GONE from the card (PR #62 UX
1935
- * round 3) — the gateway RPC `fallbacks/reset` and the store
1936
- * `resetToDefaults()` stay as host APIs, only the card UI was removed.
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.
1937
1604
  *
1938
- * The AC-7 read-only status block (recent-switch summary, D-5) stays at the
1939
- * bottom, flat.
1940
- *
1941
- * Presentation is ui-primitives-free (T4, advisor parity + reference §4
1942
- * risk): Button/Tooltip and the five icon imports are replaced by
1943
- * self-drawn styled elements — inline SVG icons (plus / trash / chevron /
1944
- * grip) and the CSS `data-tip` tooltip bubble (the card's existing
1945
- * icon-button pattern); every color resolves through a `--dsw-alias-*`
1946
- * 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).
1947
1608
  */
1948
- 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
+ }
1949
1622
  const SLOT_PRESET_IDS = [
1950
1623
  "liang-peak",
1951
1624
  "liang-valley",
@@ -2089,33 +1762,8 @@ window.__ModuleLoader__.load({
2089
1762
  const offset = tzUtcOffset(tz);
2090
1763
  return offset === "" ? tz : `${tz} (${offset})`;
2091
1764
  }
2092
- /** Persist tz: presets lock UTC+8; custom-only uses the host zone; else keep the accepted value. */
2093
- function resolvedSlotTz(rows, fallback) {
2094
- if (rows.some((row) => row.kind === "preset")) return "Asia/Shanghai";
2095
- if (rows.some((row) => row.kind === "custom")) return hostTimeZone();
2096
- return fallback === "" ? "Asia/Shanghai" : fallback;
2097
- }
2098
1765
  /** Strict 24h `HH:mm` — the resolver's HHMM_RE twin (drift-guarded by the gateway reject-on-save). */
2099
1766
  const HHMM_RE = /^([01]\d|2[0-3]):[0-5]\d$/;
2100
- /**
2101
- * The 默认模型 value for a chain: the official id when the chain TAIL
2102
- * is that model (Flash or Pro XOR — leading 默认降级链 entries allowed);
2103
- * `''` for an empty chain or a chain whose last entry is not official
2104
- * (the panel reads back unselected and save validation blocks the value).
2105
- */
2106
- function allDayModelOf(chain) {
2107
- const tail = chain.length >= 1 ? chain[chain.length - 1] : void 0;
2108
- return tail !== void 0 && isOfficialAllDayId(tail) ? tail : "";
2109
- }
2110
- /**
2111
- * The 默认降级链 editor row: the leading entries BEFORE the official tail
2112
- * (Flash or Pro), or the whole chain while the tail is not official (the
2113
- * draft rides the accepted value until a 默认模型 pick).
2114
- */
2115
- function allDayChainRowOf(chain, catalog) {
2116
- return rootChainToRows(allDayModelOf(chain) === "" ? chain : chain.slice(0, -1), catalog)[0];
2117
- }
2118
- /** Split scalars from the row editors (rootChain / role entities / role rules). */
2119
1767
  function scalarsOf(config) {
2120
1768
  return {
2121
1769
  triggerCodes: [...config.triggerCodes],
@@ -2125,52 +1773,7 @@ window.__ModuleLoader__.load({
2125
1773
  tz: config.tz ?? "Asia/Shanghai"
2126
1774
  };
2127
1775
  }
2128
- /**
2129
- * Assemble the full config the row editors + scalars describe. The rebuilt
2130
- * `roles.list` comes from the rows, with the schema-reserved
2131
- * `prompt`/`permissions` merged back from the last accepted config by role
2132
- * id (see {@link mergeRoleExtras}) so a save never silently drops them
2133
- * (T2 reviewer minor #2). `presets` (spec §9.4) follows the same rule at
2134
- * the top level: no presets UI this iteration (R-001 re-defer), so the
2135
- * draft carries the accepted value through untouched — a clean draft stays
2136
- * equal to the accepted config and a save never drops the key.
2137
- *
2138
- * All-day: rootChain is composed from the 默认降级链 editor's leading
2139
- * selectors plus the 默认模型 tail (exactly one official model — Flash or
2140
- * Pro XOR). While no tail is selected the ACCEPTED chain rides through
2141
- * untouched. `timeSlots` is rebuilt from the slot rows every render. `tz`
2142
- * is a card scalar: preset rows lock it to Asia/Shanghai; custom rows
2143
- * follow the selected timezone.
2144
- */
2145
- function assembleConfig(scalars, allDayModel, acceptedRootChain, allDayChainRow, roleRows, ruleRows, originalRoles, presets, timeSlotRows) {
2146
- const list = mergeRoleExtras(roleRows, originalRoles);
2147
- const trailingChain = rowsToRootChain([allDayChainRow]);
2148
- const tz = resolvedSlotTz(timeSlotRows, scalars.tz);
2149
- return {
2150
- triggerCodes: [...scalars.triggerCodes],
2151
- rootChain: allDayModel === "" ? [...acceptedRootChain] : [...trailingChain, allDayModel],
2152
- roles: {
2153
- list,
2154
- rules: rowsToRules(ruleRows)
2155
- },
2156
- cooldownMs: scalars.cooldownMs,
2157
- revertPolicy: scalars.revertPolicy,
2158
- maxSwitchesPerStep: scalars.maxSwitchesPerStep,
2159
- ...presets === void 0 ? {} : { presets },
2160
- timeSlots: rowsToTimeSlots(timeSlotRows),
2161
- tz
2162
- };
2163
- }
2164
- /**
2165
- * Badge label for one row's provenance (plan role-card-seeded-ux): every
2166
- * row states where its seed state comes from. The three reserved labels
2167
- * localize — `bundled`, `external` (the shared unnamed-producer slice;
2168
- * plan seeds-source-and-persona-width), and the operator's own rows —
2169
- * while a live seed declared under a registered set name renders that
2170
- * name VERBATIM (case-preserved — no case-folding). `null` = no badge: a
2171
- * currently-seeded row whose wire entry lacks `source` (gateway version
2172
- * skew) — the degradation path, never an error.
2173
- */
1776
+ /** Badge label for one row's provenance; `null` = no badge (version skew). */
2174
1777
  function seedBadgeLabel(seed, t) {
2175
1778
  if (seed === void 0) return t("roles.seedSource.user");
2176
1779
  if (seed.source === void 0) return null;
@@ -2180,105 +1783,66 @@ window.__ModuleLoader__.load({
2180
1783
  return seed.source;
2181
1784
  }
2182
1785
  /**
2183
- * Pre-save validation of the assembled draft (spec §8 / plan Task 3):
2184
- * role id format/reserved word/duplicates, undeclared rule role references
2185
- * (only reachable through the synthetic outside option — the dropdown
2186
- * itself constrains normal edits), and illegal selector entries in
2187
- * rootChain and role chains. Returns one localized message per violation,
2188
- * bucketed by the section that owns the offending field (主代理: allDay /
2189
- * timeSlots / slot* / tz / default model / default chain; 子代理: role* /
2190
- * rule*; 高级选项: trigger / cooldown / revert —
2191
- * the scalars are never validated, so the advanced bucket stays empty
2192
- * today). A non-empty bucket blocks THAT section's Save write only
2193
- * (per-section saves, plan fallbacks-card-section-ux) — a sibling section
2194
- * with a clean bucket still saves. `persona` is free text and never
2195
- * validated.
2196
- *
2197
- * `seedInfo` is the live trimmed-id → seed-entry map derived from
2198
- * `state.seeds` (spec §9.4): the empty-chain block relaxes for seeded ids
2199
- * only (spec §9.6 / AC-3 — a seeded role's chain is legitimately empty by
2200
- * design, R4, and the row must stay persistable); non-seeded behavior is
2201
- * 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.
2202
1790
  */
2203
- function validateDraft(draft, t, seedInfo) {
2204
- const errors = {
2205
- main: [],
2206
- sub: [],
2207
- advanced: []
2208
- };
2209
- const declaredIds = /* @__PURE__ */ new Set();
2210
- for (const role of draft.roles.list) {
2211
- if (!ROLE_ID_PATTERN.test(role.id)) errors.sub.push(t("validation.roleIdFormat", { id: role.id }));
2212
- if (role.id === "inherit") errors.sub.push(t("validation.roleIdReserved"));
2213
- if (declaredIds.has(role.id)) errors.sub.push(t("validation.roleIdDuplicate", { id: role.id }));
2214
- declaredIds.add(role.id);
2215
- 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 {
2216
1803
  parseSelector(entry);
2217
1804
  } catch (error) {
2218
- errors.sub.push(t("validation.selector", {
1805
+ bucket.push(t("validation.selector", {
2219
1806
  entry,
2220
1807
  message: error.message
2221
1808
  }));
2222
1809
  }
2223
- if ((role.chain ?? []).length === 0 && !seedInfo.has(role.id.trim())) errors.sub.push(t("validation.roleChainRequired", { id: role.id }));
2224
- }
2225
- const allDayTail = draft.rootChain.length >= 1 ? draft.rootChain[draft.rootChain.length - 1] : void 0;
2226
- if (allDayTail === void 0 || !isOfficialAllDayId(allDayTail)) errors.main.push(t("validation.allDayRequired"));
2227
- for (const entry of draft.rootChain) try {
2228
- parseSelector(entry);
2229
- } catch (error) {
2230
- errors.main.push(t("validation.selector", {
2231
- entry,
2232
- message: error.message
2233
- }));
2234
- }
2235
- const seenSlotPresets = /* @__PURE__ */ new Set();
2236
- for (const row of draft.timeSlots ?? []) {
2237
- if (row.kind !== "preset" && row.kind !== "custom") errors.main.push(t("validation.slotKind"));
2238
- if (row.kind === "preset") {
2239
- if (typeof row.preset !== "string" || !SLOT_PRESET_IDS.includes(row.preset)) errors.main.push(t("validation.slotPresetUnknown", { preset: row.preset }));
2240
- else if (seenSlotPresets.has(row.preset)) errors.main.push(t("validation.slotPresetDuplicate", { preset: row.preset }));
2241
- else seenSlotPresets.add(row.preset);
2242
- if (row.start !== void 0 || row.end !== void 0 || row.days !== void 0 && row.days.length > 0) errors.main.push(t("validation.slotPresetFrozen"));
2243
- } else if (row.kind === "custom") {
2244
- 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"));
2245
- if (row.days !== void 0 && row.days.some((day) => !Number.isInteger(day) || day < 0 || day > 6)) errors.main.push(t("validation.slotDays"));
2246
- }
2247
- for (const entry of row.chain) try {
2248
- parseSelector(entry);
2249
- } catch (error) {
2250
- errors.main.push(t("validation.selector", {
2251
- entry,
2252
- message: error.message
2253
- }));
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"));
2254
1831
  }
2255
- if (row.chain.length === 0) errors.main.push(t("validation.slotChainRequired"));
2256
- }
2257
- const validTargets = /* @__PURE__ */ new Set([...declaredIds, INHERIT_ROLE_ID]);
2258
- for (const rule of draft.roles.rules) if (!validTargets.has(rule.role)) errors.sub.push(t("validation.ruleRoleUndeclared", { role: rule.role }));
2259
- return errors;
2260
- }
2261
- /**
2262
- * The trimmed role ids that are validation failures (format / reserved word
2263
- * / duplicate) — drives the inline red border on the offending id input
2264
- * (live: the border tracks the draft, matching the live Save disabled
2265
- * gate). Derived once per render into a Set (qc3 F-3): a duplicate scan
2266
- * inside the render loop would be O(N²) per row; here the whole derivation
2267
- * is O(N) and each row's check is a single Set lookup. Selector errors stay
2268
- * on the banner only (plan Task 3 inline-scope rule).
2269
- */
2270
- function collectInvalidRoleIds(rows) {
2271
- const counts = /* @__PURE__ */ new Map();
2272
- for (const row of rows) {
2273
- const id = row.id.trim();
2274
- counts.set(id, (counts.get(id) ?? 0) + 1);
2275
- }
2276
- const invalid = /* @__PURE__ */ new Set();
2277
- for (const row of rows) {
2278
- const id = row.id.trim();
2279
- 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);
2280
1840
  }
2281
- return invalid;
1841
+ return {
1842
+ chains: chainBuckets,
1843
+ roles: roleErrors,
1844
+ advanced: []
1845
+ };
2282
1846
  }
2283
1847
  /** Parse a number input, clamped to a non-negative integer. */
2284
1848
  function parseCount(raw) {
@@ -2335,60 +1899,6 @@ window.__ModuleLoader__.load({
2335
1899
  });
2336
1900
  }
2337
1901
  /**
2338
- * The virtual all-fallbacks route the host carve-out recognizes (features.md):
2339
- * a subagent spawned on it is not allowlist-filtered, so it is the
2340
- * recommendation when the allowlist does not already carry it.
2341
- */
2342
- const VIRTUAL_ALL_ROUTE = {
2343
- provider: "FallbacksChain",
2344
- model: "Auto"
2345
- };
2346
- /**
2347
- * Parse one raw selector into an exact route; wildcard and unparseable
2348
- * entries resolve to `undefined` (they are not exact routes — never expanded,
2349
- * never counted). The selector parser is the single grammar authority.
2350
- */
2351
- function exactRouteOf(raw) {
2352
- let parsed;
2353
- try {
2354
- parsed = parseSelector(raw);
2355
- } catch {
2356
- return;
2357
- }
2358
- return parsed.model === void 0 ? void 0 : {
2359
- provider: parsed.provider,
2360
- model: parsed.model
2361
- };
2362
- }
2363
- /**
2364
- * The whole chain as one line (plan fallbacks-chain-reorder T2, user request):
2365
- * every non-blank entry in walk order joined with `>`, or `undefined` for an
2366
- * empty/blank chain so the caller keeps its own summary (a role's raw strategy
2367
- * token). The card ellipsizes long summaries; the full string rides `title`.
2368
- *
2369
- * With `allowedModels` (host policy enabled) exact entries the policy cannot
2370
- * use are dropped, so the title tells the truth about what will run; wildcard
2371
- * entries stay (their resolved routes are unknown client-side) and an
2372
- * unparseable entry is kept rather than hidden.
2373
- */
2374
- function chainSummary(selectors, allowedModels) {
2375
- const entries = selectors.map(selectorRowToRaw).filter((entry) => entry !== "");
2376
- const visible = allowedModels === void 0 ? entries : entries.filter((entry) => {
2377
- const route = exactRouteOf(entry);
2378
- return route === void 0 || routeOnAllowlist(route, allowedModels);
2379
- });
2380
- return visible.length === 0 ? void 0 : visible.join(" > ");
2381
- }
2382
- /** How many selected role-chain exact entries the host allowlist does not carry. */
2383
- function offAllowlistEntryCount(roles, allowedModels) {
2384
- let count = 0;
2385
- for (const role of roles) for (const entry of role.chain ?? []) {
2386
- const route = exactRouteOf(entry);
2387
- if (route !== void 0 && !routeOnAllowlist(route, allowedModels)) count += 1;
2388
- }
2389
- return count;
2390
- }
2391
- /**
2392
1902
  * Pure chain reorder (plan fallbacks-chain-reorder T1): a copy of `list` with
2393
1903
  * the entry at `from` re-inserted at `to`; an out-of-range or same-position
2394
1904
  * move returns `list` untouched (the end-clamping the keyboard and the drop
@@ -2409,9 +1919,8 @@ window.__ModuleLoader__.load({
2409
1919
  * icon. The visible provider/model field labels are gone — the two selects
2410
1920
  * carry their accessible name on `aria-label` (the visible labels repeated on
2411
1921
  * every row and cost a whole line).
2412
- * The GUI never offers a `provider/*` wildcard (root agent and role chains
2413
- * alike; provider-any matching lives in the role rules) — but `provider/*`
2414
- * 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
2415
1924
  * renders with the legacy-conversion hint and an enabled model select:
2416
1925
  * picking a model converts the row to an exact entry (the patch carries
2417
1926
  * `wildcard: false`). The provider options are the catalog providers
@@ -2653,11 +2162,9 @@ window.__ModuleLoader__.load({
2653
2162
  });
2654
2163
  }
2655
2164
  /**
2656
- * Render the Fallbacks settings card inside the plugin-config section —
2657
- * flat: the notices, the form, and the section action pairs directly under
2658
- * the page's plugin title/description, with no chrome of our own.
2659
- * @param props - slot-delivered injected dependencies and the synthesized t seat.
2660
- * @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.
2661
2168
  */
2662
2169
  function FallbacksCard({ controller, useSnapshot, t }) {
2663
2170
  const state = useSnapshot((snapshot) => snapshot);
@@ -2665,47 +2172,39 @@ window.__ModuleLoader__.load({
2665
2172
  const snapshot = controller.store.getSnapshot();
2666
2173
  if (snapshot.status === "idle") controller.load();
2667
2174
  if (snapshot.catalogStatus === "idle") controller.loadCatalog();
2668
- if (snapshot.switchesStatus === "idle") controller.loadSwitches();
2669
2175
  }, [controller]);
2670
2176
  const [scalars, setScalars] = (0, react.useState)(() => scalarsOf(defaultFallbacksConfig));
2671
- const [allDayModel, setAllDayModel] = (0, react.useState)(() => allDayModelOf(defaultFallbacksConfig.rootChain));
2672
- const [allDayChainRow, setAllDayChainRow] = (0, react.useState)(() => allDayChainRowOf(defaultFallbacksConfig.rootChain, void 0));
2673
- 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);
2674
2179
  const [roleRows, setRoleRows] = (0, react.useState)(() => rolesToRows(defaultFallbacksConfig.roles.list));
2675
- const [ruleRows, setRuleRows] = (0, react.useState)(() => rulesToRows(defaultFallbacksConfig.roles.rules));
2676
2180
  const [presetToAdd, setPresetToAdd] = (0, react.useState)("");
2677
2181
  const [lastSaveSection, setLastSaveSection] = (0, react.useState)(null);
2182
+ const [lastSaveChain, setLastSaveChain] = (0, react.useState)(null);
2678
2183
  const [advancedOpen, setAdvancedOpen] = (0, react.useState)(false);
2184
+ const [menuOpen, setMenuOpen] = (0, react.useState)(false);
2679
2185
  const seededConfigKey = (0, react.useRef)(null);
2680
- const draft = assembleConfig(scalars, allDayModel, state.config.rootChain, allDayChainRow, roleRows, ruleRows, state.config.roles.list, state.config.presets, timeSlotRows);
2681
- const hasEmptyRuleRows = ruleRows.some((row) => row.role === "");
2682
- const mainDirty = JSON.stringify([...draft.rootChain, draft.timeSlots]) !== JSON.stringify([...state.config.rootChain, state.config.timeSlots ?? []]);
2683
- 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);
2684
2197
  const advancedDirty = JSON.stringify([
2685
- draft.triggerCodes,
2686
- draft.cooldownMs,
2687
- draft.revertPolicy,
2688
- draft.maxSwitchesPerStep
2198
+ scalars.triggerCodes,
2199
+ scalars.cooldownMs,
2200
+ scalars.revertPolicy,
2201
+ scalars.maxSwitchesPerStep
2689
2202
  ]) !== JSON.stringify([
2690
2203
  state.config.triggerCodes,
2691
2204
  state.config.cooldownMs,
2692
2205
  state.config.revertPolicy,
2693
2206
  state.config.maxSwitchesPerStep
2694
2207
  ]);
2695
- const enabledPolicy = state.subagentPolicy?.state === "enabled" ? state.subagentPolicy : void 0;
2696
- const allowedModels = enabledPolicy?.allowedModels;
2697
- const offAllowlistCount = enabledPolicy === void 0 ? 0 : offAllowlistEntryCount(draft.roles.list, enabledPolicy.allowedModels);
2698
- const virtualRouteAllowed = enabledPolicy !== void 0 && routeOnAllowlist(VIRTUAL_ALL_ROUTE, enabledPolicy.allowedModels);
2699
- const [syncStatus, setSyncStatus] = (0, react.useState)("idle");
2700
- const syncDirtyHintId = `fallbacks-policy-sync-dirty-${(0, react.useId)().replace(/:/g, "")}`;
2701
- const syncAllowlist = () => {
2702
- if (syncStatus === "pending") return;
2703
- setSyncStatus("pending");
2704
- controller.syncAllowlist().then((ok) => {
2705
- setSyncStatus(ok ? "ok" : "error");
2706
- });
2707
- };
2708
- const firstSeedDone = (0, react.useRef)(false);
2709
2208
  (0, react.useEffect)(() => {
2710
2209
  if (state.status !== "ready") return;
2711
2210
  const key = JSON.stringify(state.config);
@@ -2714,33 +2213,21 @@ window.__ModuleLoader__.load({
2714
2213
  const firstSeed = !firstSeedDone.current;
2715
2214
  firstSeedDone.current = true;
2716
2215
  const catalog = catalogOf(state);
2717
- if (firstSeed || !mainDirty) {
2718
- setAllDayModel(allDayModelOf(state.config.rootChain));
2719
- setAllDayChainRow(allDayChainRowOf(state.config.rootChain, catalog));
2720
- setTimeSlotRows(timeSlotsToRows(state.config.timeSlots ?? [], catalog));
2721
- setScalars((prev) => ({
2722
- ...prev,
2723
- tz: state.config.tz ?? "Asia/Shanghai"
2724
- }));
2725
- }
2726
- if (firstSeed || !subDirty) {
2727
- setRoleRows(rolesToRows(state.config.roles.list, catalog));
2728
- 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)));
2729
2220
  }
2221
+ if (firstSeed || !rolesDirty) setRoleRows(rolesToRows(state.config.roles.list));
2730
2222
  if (firstSeed || !advancedDirty) setScalars((prev) => ({
2731
2223
  ...prev,
2732
2224
  triggerCodes: [...state.config.triggerCodes],
2733
2225
  cooldownMs: state.config.cooldownMs,
2734
2226
  revertPolicy: state.config.revertPolicy,
2735
- maxSwitchesPerStep: state.config.maxSwitchesPerStep
2227
+ maxSwitchesPerStep: state.config.maxSwitchesPerStep,
2228
+ tz: state.config.tz ?? "Asia/Shanghai"
2736
2229
  }));
2737
- }, [
2738
- state.status,
2739
- state.config,
2740
- mainDirty,
2741
- subDirty,
2742
- advancedDirty
2743
- ]);
2230
+ }, [state.status, state.config]);
2744
2231
  const updateScalars = (mutator) => {
2745
2232
  setScalars((prev) => {
2746
2233
  const next = {
@@ -2751,139 +2238,195 @@ window.__ModuleLoader__.load({
2751
2238
  return next;
2752
2239
  });
2753
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
+ };
2754
2251
  const updateTimeSlotRow = (index, patch) => {
2755
- setTimeSlotRows((rows) => {
2756
- const next = rows.map((row) => ({ ...row }));
2757
- next[index] = {
2758
- ...next[index],
2252
+ patchActiveDraft((draft) => {
2253
+ const slots = draft.slots.map((row) => ({ ...row }));
2254
+ slots[index] = {
2255
+ ...slots[index],
2759
2256
  ...patch
2760
2257
  };
2761
- return next;
2258
+ return {
2259
+ ...draft,
2260
+ slots
2261
+ };
2762
2262
  });
2763
2263
  };
2764
2264
  const updateTimeSlotSelector = (rowIndex, selectorIndex, patch) => {
2765
- setTimeSlotRows((rows) => {
2766
- const next = rows.map((row) => ({
2265
+ patchActiveDraft((draft) => {
2266
+ const slots = draft.slots.map((row) => ({
2767
2267
  ...row,
2768
2268
  selectors: row.selectors.map((selector) => ({ ...selector }))
2769
2269
  }));
2770
- const selectors = next[rowIndex].selectors;
2270
+ const selectors = slots[rowIndex].selectors;
2771
2271
  selectors[selectorIndex] = {
2772
2272
  ...selectors[selectorIndex],
2773
2273
  ...patch
2774
2274
  };
2775
- return next;
2275
+ return {
2276
+ ...draft,
2277
+ slots
2278
+ };
2776
2279
  });
2777
2280
  };
2778
2281
  const addTimeSlotSelector = (rowIndex) => {
2779
- setTimeSlotRows((rows) => rows.map((row, index) => index === rowIndex ? {
2780
- ...row,
2781
- selectors: [...row.selectors, {
2782
- wildcard: false,
2783
- provider: null,
2784
- model: null
2785
- }]
2786
- } : 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
+ }));
2787
2293
  };
2788
2294
  const removeTimeSlotSelector = (rowIndex, selectorIndex) => {
2789
- setTimeSlotRows((rows) => rows.map((row, index) => index === rowIndex ? {
2790
- ...row,
2791
- selectors: row.selectors.filter((_, sIndex) => sIndex !== selectorIndex)
2792
- } : 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
+ }));
2793
2302
  };
2794
2303
  const addPresetSlotRow = () => {
2795
2304
  if (presetToAdd === "") return;
2796
- if ((presetToAdd === "glm-peak" || presetToAdd === "glm-valley") && !glmConfigured) return;
2797
- setTimeSlotRows((rows) => [...rows, {
2798
- kind: "preset",
2799
- preset: presetToAdd,
2800
- start: "",
2801
- end: "",
2802
- days: [],
2803
- name: "",
2804
- collapsed: false,
2805
- selectors: []
2806
- }]);
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
+ }));
2807
2318
  setPresetToAdd("");
2808
2319
  };
2809
2320
  const addCustomSlotRow = () => {
2810
- setTimeSlotRows((rows) => [...rows, {
2811
- kind: "custom",
2812
- start: "",
2813
- end: "",
2814
- days: [],
2815
- name: "",
2816
- collapsed: false,
2817
- selectors: []
2818
- }]);
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
+ }));
2819
2333
  };
2820
2334
  const removeTimeSlotRow = (index) => {
2821
- setTimeSlotRows((rows) => rows.filter((_, rowIndex) => rowIndex !== index));
2335
+ patchActiveDraft((draft) => ({
2336
+ ...draft,
2337
+ slots: draft.slots.filter((_, rowIndex) => rowIndex !== index)
2338
+ }));
2822
2339
  };
2823
2340
  const moveTimeSlotRow = (index, delta) => {
2824
- setTimeSlotRows((rows) => {
2341
+ patchActiveDraft((draft) => {
2825
2342
  const target = index + delta;
2826
- if (target < 0 || target >= rows.length) return rows;
2827
- const next = rows.map((row) => ({ ...row }));
2828
- const moved = next[index];
2829
- next[index] = next[target];
2830
- next[target] = moved;
2831
- 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
+ };
2832
2352
  });
2833
2353
  };
2834
2354
  const [draggedSlotIndex, setDraggedSlotIndex] = (0, react.useState)(null);
2835
2355
  const [overSlotIndex, setOverSlotIndex] = (0, react.useState)(null);
2836
2356
  const reorderTimeSlotRow = (from, to) => {
2837
- setTimeSlotRows((rows) => {
2838
- if (from === to || from < 0 || to < 0 || from >= rows.length || to >= rows.length) return rows;
2839
- const next = rows.map((row) => ({ ...row }));
2840
- const [moved] = next.splice(from, 1);
2841
- next.splice(to, 0, moved);
2842
- 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
+ };
2843
2366
  });
2844
2367
  };
2845
- const updateAllDayChainSelector = (selectorIndex, patch) => {
2846
- setAllDayChainRow((row) => ({
2847
- ...row,
2848
- 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 ? {
2849
2380
  ...selector,
2850
2381
  ...patch
2851
- } : selector)
2852
- }));
2382
+ } : selector);
2383
+ return {
2384
+ ...draft,
2385
+ selectors
2386
+ };
2387
+ });
2853
2388
  };
2854
- const addAllDayChainSelector = () => {
2855
- setAllDayChainRow((row) => ({
2856
- ...row,
2857
- selectors: [...row.selectors, {
2389
+ const addChainSelector = () => {
2390
+ patchActiveDraft((draft) => ({
2391
+ ...draft,
2392
+ selectors: [...draft.selectors, {
2858
2393
  wildcard: false,
2859
2394
  provider: null,
2860
2395
  model: null
2861
2396
  }]
2862
2397
  }));
2863
2398
  };
2864
- const removeAllDayChainSelector = (selectorIndex) => {
2865
- setAllDayChainRow((row) => ({
2866
- ...row,
2867
- selectors: row.selectors.filter((_, index) => index !== selectorIndex)
2399
+ const removeChainSelector = (selectorIndex) => {
2400
+ patchActiveDraft((draft) => ({
2401
+ ...draft,
2402
+ selectors: draft.selectors.filter((_, index) => index !== selectorIndex)
2868
2403
  }));
2869
2404
  };
2870
- const reorderAllDayChainSelector = (from, to) => {
2871
- setAllDayChainRow((row) => ({
2872
- ...row,
2873
- selectors: moveItem(row.selectors, from, to)
2405
+ const reorderChainSelector = (from, to) => {
2406
+ patchActiveDraft((draft) => ({
2407
+ ...draft,
2408
+ selectors: moveItem(draft.selectors, from, to)
2874
2409
  }));
2875
2410
  };
2876
- const reorderTimeSlotSelector = (slotIndex, from, to) => {
2877
- setTimeSlotRows((rows) => rows.map((row, index) => index === slotIndex ? {
2878
- ...row,
2879
- selectors: moveItem(row.selectors, from, to)
2880
- } : 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);
2881
2424
  };
2882
- const reorderRoleSelector = (roleIndex, from, to) => {
2883
- setRoleRows((rows) => rows.map((row, index) => index === roleIndex ? {
2884
- ...row,
2885
- selectors: moveItem(row.selectors, from, to)
2886
- } : 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);
2887
2430
  };
2888
2431
  const updateRoleRow = (index, patch) => {
2889
2432
  setRoleRows((rows) => {
@@ -2895,42 +2438,11 @@ window.__ModuleLoader__.load({
2895
2438
  return next;
2896
2439
  });
2897
2440
  };
2898
- const updateRoleSelector = (roleIndex, selectorIndex, patch) => {
2899
- setRoleRows((rows) => {
2900
- const next = rows.map((row) => ({
2901
- ...row,
2902
- selectors: row.selectors.map((selector) => ({ ...selector }))
2903
- }));
2904
- const selectors = next[roleIndex].selectors;
2905
- selectors[selectorIndex] = {
2906
- ...selectors[selectorIndex],
2907
- ...patch
2908
- };
2909
- return next;
2910
- });
2911
- };
2912
- const addRoleSelector = (roleIndex) => {
2913
- setRoleRows((rows) => rows.map((row, index) => index === roleIndex ? {
2914
- ...row,
2915
- selectors: [...row.selectors, {
2916
- wildcard: false,
2917
- provider: null,
2918
- model: null
2919
- }]
2920
- } : row));
2921
- };
2922
- const removeRoleSelector = (roleIndex, selectorIndex) => {
2923
- setRoleRows((rows) => rows.map((row, index) => index === roleIndex ? {
2924
- ...row,
2925
- selectors: row.selectors.filter((_, sIndex) => sIndex !== selectorIndex)
2926
- } : row));
2927
- };
2928
2441
  const addRole = () => {
2929
2442
  setRoleRows((rows) => [...rows, {
2930
2443
  id: "",
2931
2444
  persona: "",
2932
- selectors: [],
2933
- fallback: "inherit-root",
2445
+ chain: "",
2934
2446
  collapsed: true,
2935
2447
  personaOpen: false
2936
2448
  }]);
@@ -2938,141 +2450,145 @@ window.__ModuleLoader__.load({
2938
2450
  const removeRole = (index) => {
2939
2451
  setRoleRows((rows) => rows.filter((_, rowIndex) => rowIndex !== index));
2940
2452
  };
2941
- const updateRuleRow = (index, patch) => {
2942
- setRuleRows((rows) => {
2943
- const next = rows.map((row) => ({ ...row }));
2944
- next[index] = {
2945
- ...next[index],
2946
- ...patch
2947
- };
2948
- return next;
2949
- });
2950
- };
2951
2453
  const saving = state.status === "saving";
2952
2454
  const writable = state.writable;
2953
2455
  const advancedVisible = advancedOpen || !writable;
2954
2456
  const cooldownMsTip = `${t("cooldownMs.tooltip")} ${t("defaults.prefix")}: ${defaultFallbacksConfig.cooldownMs}`;
2955
2457
  const maxSwitchesPerStepTip = `${t("maxSwitchesPerStep.tooltip")} ${t("defaults.prefix")}: ${defaultFallbacksConfig.maxSwitchesPerStep}`;
2956
2458
  const unknownCodes = scalars.triggerCodes.filter((code) => !KNOWN_TRIGGER_CODES.includes(code));
2957
- const presetsPresent = timeSlotRows.some((row) => row.kind === "preset");
2459
+ const presetsPresent = (chainDrafts[activeChain]?.slots ?? []).some((row) => row.kind === "preset");
2958
2460
  const glmConfigured = state.configuredProviders.some((entry) => entry.provider === "zai-coding-cn");
2959
- const slotState = resolveSlotState(state.config, /* @__PURE__ */ new Date(), scalars.tz);
2960
- const activeSlotIndex = slotState.winner === "all-day" ? -1 : state.config.timeSlots?.indexOf(slotState.winner) ?? -1;
2961
- 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 ?? []);
2962
2469
  const invalidRoleIds = collectInvalidRoleIds(roleRows);
2963
- const invalidSlotRows = collectInvalidSlotRows(timeSlotRows);
2964
2470
  const seedInfo = /* @__PURE__ */ new Map();
2965
2471
  for (const seed of state.seeds) seedInfo.set(seed.id.trim(), {
2966
2472
  overridden: seed.overridden,
2967
2473
  source: seed.source
2968
2474
  });
2969
- const liveErrors = validateDraft(draft, t, seedInfo);
2970
- const latestSwitch = state.switches[0];
2971
- let switchesLine;
2972
- if (state.switchesStatus === "error") switchesLine = t("status.switches.error", { message: state.switchesError });
2973
- else if (state.switchesStatus === "loading") switchesLine = t("loading");
2974
- else if (latestSwitch === void 0) switchesLine = t("status.switches.empty");
2975
- else {
2976
- const reasonKey = SWITCH_REASON_KEYS[latestSwitch.reason];
2977
- const params = {
2978
- count: String(state.switches.length),
2979
- from: `${latestSwitch.from.provider}/${latestSwitch.from.model}`,
2980
- to: `${latestSwitch.to.provider}/${latestSwitch.to.model}`,
2981
- role: latestSwitch.role,
2982
- reason: reasonKey === void 0 ? latestSwitch.reason : t(reasonKey)
2983
- };
2984
- switchesLine = latestSwitch.reason === "role-inject" ? t("status.switches.compact.roleInject", params) : t("status.switches.compact", params);
2985
- }
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;
2986
2479
  const catalogSeededSections = (0, react.useRef)({
2987
2480
  epoch: null,
2988
- main: false,
2989
- sub: false
2481
+ chains: false,
2482
+ roles: false
2990
2483
  });
2991
2484
  (0, react.useEffect)(() => {
2992
2485
  if (state.catalogStatus !== "ready") return;
2993
2486
  const seed = catalogSeededSections.current;
2994
2487
  if (seed.epoch !== state.catalogEpoch) {
2995
2488
  seed.epoch = state.catalogEpoch;
2996
- seed.main = false;
2997
- seed.sub = false;
2489
+ seed.chains = false;
2490
+ seed.roles = false;
2998
2491
  }
2999
2492
  const catalog = catalogOf(state);
3000
- if (!mainDirty && !seed.main) {
3001
- seed.main = true;
3002
- setAllDayModel(allDayModelOf(state.config.rootChain));
3003
- setAllDayChainRow(allDayChainRowOf(state.config.rootChain, catalog));
3004
- setTimeSlotRows(timeSlotsToRows(state.config.timeSlots ?? [], catalog));
2493
+ if (!chainsDirty && !seed.chains) {
2494
+ seed.chains = true;
2495
+ setChainDrafts(chainsToDrafts(state.config.chains, catalog));
3005
2496
  }
3006
- if (!subDirty && !seed.sub) {
3007
- seed.sub = true;
3008
- setRoleRows(rolesToRows(state.config.roles.list, catalog));
3009
- setRuleRows(rulesToRows(state.config.roles.rules, catalog));
2497
+ if (!rolesDirty && !seed.roles) {
2498
+ seed.roles = true;
2499
+ setRoleRows(rolesToRows(state.config.roles.list));
3010
2500
  }
3011
2501
  }, [
3012
2502
  state.catalogStatus,
3013
2503
  state.catalogEpoch,
3014
- state.config,
3015
- mainDirty,
3016
- subDirty
2504
+ state.config
3017
2505
  ]);
3018
- const sectionPatch = (section) => {
3019
- const base = { ...state.config };
3020
- switch (section) {
3021
- case "main": return {
3022
- ...base,
3023
- rootChain: draft.rootChain,
3024
- timeSlots: draft.timeSlots,
3025
- tz: draft.tz
3026
- };
3027
- case "sub": return {
3028
- ...base,
3029
- roles: draft.roles
3030
- };
3031
- case "advanced": return {
3032
- ...base,
3033
- triggerCodes: draft.triggerCodes,
3034
- cooldownMs: draft.cooldownMs,
3035
- revertPolicy: draft.revertPolicy,
3036
- maxSwitchesPerStep: draft.maxSwitchesPerStep
3037
- };
3038
- }
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
+ });
3039
2512
  };
3040
2513
  const saveSection = (section) => {
3041
- if (liveErrors[section].length > 0 || section === "sub" && hasEmptyRuleRows) return;
3042
- setLastSaveSection(section);
3043
- 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
+ });
3044
2541
  };
3045
- const discardSection = (section) => {
3046
- const catalog = catalogOf(state);
3047
- switch (section) {
3048
- case "main":
3049
- setAllDayModel(allDayModelOf(state.config.rootChain));
3050
- setAllDayChainRow(allDayChainRowOf(state.config.rootChain, catalog));
3051
- setTimeSlotRows(timeSlotsToRows(state.config.timeSlots ?? [], catalog));
3052
- setScalars((prev) => ({
3053
- ...prev,
3054
- tz: state.config.tz ?? "Asia/Shanghai"
3055
- }));
3056
- break;
3057
- case "sub":
3058
- setRoleRows(rolesToRows(state.config.roles.list, catalog));
3059
- setRuleRows(rulesToRows(state.config.roles.rules, catalog));
3060
- break;
3061
- case "advanced": setScalars((prev) => ({
3062
- ...prev,
3063
- triggerCodes: [...state.config.triggerCodes],
3064
- cooldownMs: state.config.cooldownMs,
3065
- revertPolicy: state.config.revertPolicy,
3066
- maxSwitchesPerStep: state.config.maxSwitchesPerStep
3067
- }));
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;
3068
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
+ }));
3069
2563
  };
3070
2564
  (0, react.useEffect)(() => {
3071
- if (state.status === "ready") setLastSaveSection(null);
2565
+ if (state.status === "ready") {
2566
+ setLastSaveSection(null);
2567
+ setLastSaveChain(null);
2568
+ }
3072
2569
  }, [state.status]);
3073
2570
  const degradedLatch = (0, react.useRef)(false);
3074
2571
  if (state.status === "ready") degradedLatch.current = !state.present;
3075
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);
3076
2592
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3077
2593
  className: FallbacksCard_module_css_default.root,
3078
2594
  children: [
@@ -3116,474 +2632,698 @@ window.__ModuleLoader__.load({
3116
2632
  children: [
3117
2633
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h2", {
3118
2634
  className: FallbacksCard_module_css_default.sectionHeading,
3119
- id: "fallbacks-main-agent",
3120
- "aria-label": t("mainAgent.label"),
2635
+ id: "fallbacks-virtual-chains",
2636
+ "aria-label": t("virtualChain.label"),
3121
2637
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3122
2638
  className: FallbacksCard_module_css_default.sectionHeadingText,
3123
- children: t("mainAgent.label")
3124
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3125
- className: FallbacksCard_module_css_default.sectionActions,
3126
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3127
- type: "button",
3128
- className: FallbacksCard_module_css_default.discard,
3129
- disabled: !mainDirty || saving,
3130
- onClick: () => {
3131
- discardSection("main");
3132
- },
3133
- children: t("discard")
3134
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3135
- type: "button",
3136
- className: FallbacksCard_module_css_default.save,
3137
- disabled: !writable || saving || !mainDirty,
3138
- onClick: () => {
3139
- saveSection("main");
3140
- },
3141
- children: saving ? t("save.saving") : t("save")
3142
- })]
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")
3143
2643
  })]
3144
2644
  }),
3145
- liveErrors.main.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3146
- className: FallbacksCard_module_css_default.error,
3147
- role: "alert",
3148
- children: `${t("validation.blocked")}${liveErrors.main.join("; ")}`
3149
- }),
3150
- lastSaveSection === "main" && state.status === "error" && state.error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3151
- className: FallbacksCard_module_css_default.error,
3152
- role: "alert",
3153
- children: t("error.generic", { message: state.error })
3154
- }),
3155
2645
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3156
- className: FallbacksCard_module_css_default.field,
3157
- role: "group",
3158
- "aria-labelledby": "fallbacks-time-slots",
2646
+ className: FallbacksCard_module_css_default.tabsWrap,
2647
+ "data-tabs-wrap": "",
3159
2648
  children: [
3160
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3161
- className: FallbacksCard_module_css_default.fieldLabel,
3162
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3163
- id: "fallbacks-time-slots",
3164
- children: t("timeSlots.label")
3165
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
3166
- label: t("timeSlots.tooltip"),
3167
- disabled: !writable
3168
- })]
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
+ ]
3169
2763
  }),
3170
2764
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3171
- className: FallbacksCard_module_css_default.hint,
3172
- children: t("timeSlots.hint")
2765
+ className: FallbacksCard_module_css_default.fadeL,
2766
+ "aria-hidden": "true"
3173
2767
  }),
3174
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3175
- className: FallbacksCard_module_css_default.list,
3176
- children: timeSlotRows.map((row, index) => {
3177
- const invalidWindow = invalidSlotRows.has(index);
3178
- const chainEmpty = row.selectors.every((selector) => selectorRowToRaw(selector) === "");
3179
- const firstModel = row.selectors.map(selectorRowToRaw).find((entry) => entry !== "");
3180
- const slotExpanded = !row.collapsed || !writable;
3181
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3182
- className: `${FallbacksCard_module_css_default.editorCard} ${draggedSlotIndex === index ? FallbacksCard_module_css_default.slotCardDragging : ""} ${overSlotIndex === index && draggedSlotIndex !== null && draggedSlotIndex !== index ? FallbacksCard_module_css_default.slotCardOver : ""}`,
3183
- onDragOver: (event) => {
3184
- if (draggedSlotIndex === null) return;
3185
- event.preventDefault();
3186
- if (overSlotIndex !== index) setOverSlotIndex(index);
3187
- },
3188
- onDrop: (event) => {
3189
- event.preventDefault();
3190
- const from = draggedSlotIndex;
3191
- setDraggedSlotIndex(null);
3192
- setOverSlotIndex(null);
3193
- if (from !== null && from !== index) reorderTimeSlotRow(from, index);
3194
- },
3195
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3196
- className: FallbacksCard_module_css_default.collapseRow,
3197
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3198
- type: "button",
3199
- className: FallbacksCard_module_css_default.collapseToggle,
3200
- "aria-expanded": slotExpanded,
3201
- "aria-label": t(slotExpanded ? "timeSlots.collapse" : "timeSlots.expand"),
3202
- disabled: !writable,
3203
- onClick: () => {
3204
- updateTimeSlotRow(index, { collapsed: !row.collapsed });
3205
- },
3206
- children: [
3207
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronDownIcon, { className: slotExpanded ? `${FallbacksCard_module_css_default.chevron} ${FallbacksCard_module_css_default.chevronOpen}` : FallbacksCard_module_css_default.chevron }),
3208
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3209
- className: FallbacksCard_module_css_default.collapseTitle,
3210
- children: row.kind === "preset" ? t(`timeSlots.preset.${row.preset}.label`) : row.name !== "" ? row.name : `custom ${row.start}-${row.end}`
3211
- }),
3212
- 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", {
3213
- className: `${FallbacksCard_module_css_default.slotTag} ${FallbacksCard_module_css_default.slotTagHighCost}`,
3214
- children: t("timeSlots.preset.highCost")
3215
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3216
- className: `${FallbacksCard_module_css_default.slotTag} ${FallbacksCard_module_css_default.slotTagMultiplier}`,
3217
- children: t("timeSlots.preset.multiplier", { n: row.preset === "liang-peak" ? "2" : "3" })
3218
- })] }),
3219
- activeSlotIndex === index && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3220
- className: `${FallbacksCard_module_css_default.slotTag} ${FallbacksCard_module_css_default.slotTagActive}`,
3221
- children: t("timeSlots.active")
3222
- }),
3223
- firstModel !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3224
- className: FallbacksCard_module_css_default.collapseMeta,
3225
- children: firstModel
3226
- })
3227
- ]
3228
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3229
- type: "button",
3230
- className: FallbacksCard_module_css_default.dragHandle,
3231
- draggable: writable,
3232
- "data-tip": t("timeSlots.drag"),
3233
- "aria-label": t("timeSlots.drag"),
3234
- disabled: !writable,
3235
- onDragStart: () => {
3236
- if (!writable) return;
3237
- setDraggedSlotIndex(index);
3238
- 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);
3239
2869
  },
3240
- onDragEnd: () => {
2870
+ onDrop: (event) => {
2871
+ event.preventDefault();
2872
+ const from = draggedSlotIndex;
3241
2873
  setDraggedSlotIndex(null);
3242
2874
  setOverSlotIndex(null);
2875
+ if (from !== null && from !== index) reorderTimeSlotRow(from, index);
3243
2876
  },
3244
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GripIcon, {})
3245
- })]
3246
- }), slotExpanded && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
3247
- row.kind === "preset" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
3248
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3249
- className: FallbacksCard_module_css_default.ruleGrid,
3250
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3251
- className: FallbacksCard_module_css_default.ruleCell,
3252
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3253
- className: FallbacksCard_module_css_default.ruleCellLabel,
3254
- children: t("timeSlots.preset.name")
3255
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3256
- className: FallbacksCard_module_css_default.slotPresetName,
3257
- children: t(`timeSlots.preset.${row.preset}.label`)
3258
- })]
3259
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3260
- className: FallbacksCard_module_css_default.ruleCell,
3261
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3262
- className: FallbacksCard_module_css_default.ruleCellLabel,
3263
- children: t("timeSlots.preset.windowLabel")
3264
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3265
- className: FallbacksCard_module_css_default.hint,
3266
- children: t(`timeSlots.preset.${row.preset}.window`)
3267
- })]
3268
- })]
3269
- }),
3270
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3271
- className: FallbacksCard_module_css_default.hint,
3272
- children: t("timeSlots.preset.chainsOnly")
3273
- }),
3274
- (row.preset === "glm-peak" || row.preset === "glm-valley") && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3275
- className: FallbacksCard_module_css_default.hint,
3276
- children: t("timeSlots.preset.glm.note")
3277
- })
3278
- ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
3279
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3280
- className: FallbacksCard_module_css_default.field,
3281
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3282
- className: FallbacksCard_module_css_default.ruleCellLabel,
3283
- children: t("timeSlots.name")
3284
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3285
- className: FallbacksCard_module_css_default.input,
3286
- value: row.name,
3287
- placeholder: t("timeSlots.name"),
3288
- "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"),
3289
2884
  disabled: !writable,
3290
- onChange: (event) => {
3291
- updateTimeSlotRow(index, { name: event.target.value });
3292
- }
3293
- })]
3294
- }),
3295
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3296
- className: FallbacksCard_module_css_default.field,
3297
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3298
- className: FallbacksCard_module_css_default.ruleCellLabel,
3299
- children: t("timeSlots.tz.label")
3300
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3301
- className: FallbacksCard_module_css_default.hint,
3302
- "aria-label": t("timeSlots.tz.label"),
3303
- children: tzDisplayLabel(presetsPresent ? "Asia/Shanghai" : hostTimeZone())
3304
- })]
3305
- }),
3306
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3307
- className: FallbacksCard_module_css_default.ruleGrid,
3308
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3309
- className: FallbacksCard_module_css_default.ruleCell,
3310
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3311
- className: FallbacksCard_module_css_default.ruleCellLabel,
3312
- children: t("timeSlots.start")
3313
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3314
- className: `${FallbacksCard_module_css_default.input} ${invalidWindow ? FallbacksCard_module_css_default.inputInvalid : ""}`,
3315
- value: row.start,
3316
- placeholder: "09:00",
3317
- "aria-label": t("timeSlots.start"),
3318
- disabled: !writable,
3319
- onChange: (event) => {
3320
- updateTimeSlotRow(index, { start: event.target.value });
3321
- }
3322
- })]
3323
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3324
- className: FallbacksCard_module_css_default.ruleCell,
3325
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3326
- className: FallbacksCard_module_css_default.ruleCellLabel,
3327
- children: t("timeSlots.end")
3328
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3329
- className: `${FallbacksCard_module_css_default.input} ${invalidWindow ? FallbacksCard_module_css_default.inputInvalid : ""}`,
3330
- value: row.end,
3331
- placeholder: "18:00",
3332
- "aria-label": t("timeSlots.end"),
3333
- disabled: !writable,
3334
- onChange: (event) => {
3335
- updateTimeSlotRow(index, { end: event.target.value });
3336
- }
3337
- })]
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, {})
3338
2927
  })]
3339
- }),
3340
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3341
- className: FallbacksCard_module_css_default.field,
3342
- 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
+ }),
3343
2952
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3344
- className: FallbacksCard_module_css_default.ruleCellLabel,
3345
- children: t("timeSlots.days")
2953
+ className: FallbacksCard_module_css_default.hint,
2954
+ children: t("timeSlots.preset.chainsOnly")
3346
2955
  }),
3347
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3348
- className: FallbacksCard_module_css_default.dayRow,
3349
- children: SLOT_WEEKDAYS.map((day, dayIndex) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3350
- className: FallbacksCard_module_css_default.dayCell,
3351
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3352
- type: "checkbox",
3353
- checked: row.days.includes(dayIndex),
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
+ })]
2987
+ }),
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"),
3354
3000
  disabled: !writable,
3355
- onChange: () => {
3356
- updateTimeSlotRow(index, { days: row.days.includes(dayIndex) ? row.days.filter((existing) => existing !== dayIndex) : [...row.days, dayIndex] });
3001
+ onChange: (event) => {
3002
+ updateTimeSlotRow(index, { start: event.target.value });
3357
3003
  }
3358
- }), t(`timeSlots.day.${day}`)]
3359
- }, day))
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"),
3015
+ disabled: !writable,
3016
+ onChange: (event) => {
3017
+ updateTimeSlotRow(index, { end: event.target.value });
3018
+ }
3019
+ })]
3020
+ })]
3360
3021
  }),
3361
- /* @__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", {
3362
3050
  className: FallbacksCard_module_css_default.hint,
3363
- children: t("timeSlots.days.hint")
3051
+ children: t("validation.slotWindow")
3364
3052
  })
3365
- ]
3366
- }),
3367
- (row.start !== "" || row.end !== "") && !(HHMM_RE.test(row.start) && HHMM_RE.test(row.end)) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3368
- className: FallbacksCard_module_css_default.hint,
3369
- children: t("validation.slotWindow")
3370
- })
3371
- ] }),
3372
- chainEmpty && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3373
- className: FallbacksCard_module_css_default.hint,
3374
- 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
+ })]
3375
3152
  }),
3376
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
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")]
3159
+ }),
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", {
3377
3209
  className: FallbacksCard_module_css_default.chainBlock,
3378
3210
  "data-chain-block": "",
3379
3211
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChainSelectorList, {
3380
- selectors: row.selectors,
3212
+ selectors: activeDraft.selectors,
3381
3213
  catalog: catalogOf(state),
3382
3214
  configuredProviders: state.configuredProviders,
3383
3215
  disabled: !writable,
3384
3216
  t,
3385
3217
  onChange: (selectorIndex, patch) => {
3386
- updateTimeSlotSelector(index, selectorIndex, patch);
3218
+ updateChainSelector(selectorIndex, patch);
3387
3219
  },
3388
3220
  onRemove: (selectorIndex) => {
3389
- removeTimeSlotSelector(index, selectorIndex);
3221
+ removeChainSelector(selectorIndex);
3390
3222
  },
3391
- onMove: (from, to) => {
3392
- reorderTimeSlotSelector(index, from, to);
3393
- }
3223
+ onMove: reorderChainSelector
3394
3224
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3395
3225
  type: "button",
3396
3226
  className: `${FallbacksCard_module_css_default.outlineButton} ${FallbacksCard_module_css_default.addButton}`,
3397
3227
  disabled: !writable,
3398
- onClick: () => {
3399
- addTimeSlotSelector(index);
3400
- },
3228
+ onClick: addChainSelector,
3401
3229
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlusIcon, {}), t("timeSlots.selector.add")]
3402
3230
  })]
3403
- }),
3404
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3405
- className: FallbacksCard_module_css_default.cardFoot,
3406
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3407
- className: FallbacksCard_module_css_default.rowActions,
3408
- children: [
3409
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3410
- type: "button",
3411
- className: FallbacksCard_module_css_default.iconButton,
3412
- "data-tip": t("timeSlots.moveUp"),
3413
- "aria-label": t("timeSlots.moveUp"),
3414
- disabled: !writable || index === 0,
3415
- onClick: () => {
3416
- moveTimeSlotRow(index, -1);
3417
- },
3418
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronDownIcon, { className: FallbacksCard_module_css_default.chevronFlip })
3419
- }),
3420
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3421
- type: "button",
3422
- className: FallbacksCard_module_css_default.iconButton,
3423
- "data-tip": t("timeSlots.moveDown"),
3424
- "aria-label": t("timeSlots.moveDown"),
3425
- disabled: !writable || index === timeSlotRows.length - 1,
3426
- onClick: () => {
3427
- moveTimeSlotRow(index, 1);
3428
- },
3429
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronDownIcon, {})
3430
- }),
3431
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3432
- type: "button",
3433
- className: `${FallbacksCard_module_css_default.iconButton} ${FallbacksCard_module_css_default.iconButtonDanger}`,
3434
- "data-tip": t("timeSlots.remove"),
3435
- "aria-label": t("timeSlots.remove"),
3436
- onClick: () => {
3437
- removeTimeSlotRow(index);
3438
- },
3439
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TrashIcon, {})
3440
- })
3441
- ]
3442
- })
3443
3231
  })
3444
- ] })]
3445
- }, index);
3446
- })
3232
+ })
3233
+ })
3234
+ ]
3447
3235
  }),
3448
3236
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3449
- className: FallbacksCard_module_css_default.slotAddRow,
3237
+ className: FallbacksCard_module_css_default.field,
3238
+ role: "group",
3239
+ "aria-labelledby": `fallbacks-default-model-${activeDraft.id}`,
3450
3240
  children: [
3451
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
3452
- className: `${FallbacksCard_module_css_default.input} ${FallbacksCard_module_css_default.selectInput}`,
3453
- value: presetToAdd,
3454
- "aria-label": t("timeSlots.presetPlaceholder"),
3455
- disabled: !writable,
3456
- onChange: (event) => {
3457
- setPresetToAdd(event.target.value);
3458
- },
3459
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3460
- value: "",
3461
- children: t("timeSlots.presetPlaceholder")
3462
- }), SLOT_PRESET_IDS.filter((id) => !timeSlotRows.some((row) => row.kind === "preset" && row.preset === id)).map((id) => {
3463
- const glmUnconfigured = !glmConfigured && (id === "glm-peak" || id === "glm-valley");
3464
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
3465
- value: id,
3466
- disabled: glmUnconfigured,
3467
- children: [t(`timeSlots.preset.${id}.label`), glmUnconfigured ? t("timeSlots.preset.glm.unconfigured") : null]
3468
- }, id);
3469
- })]
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
+ })
3247
+ }),
3248
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3249
+ className: FallbacksCard_module_css_default.hint,
3250
+ children: t("allDay.hint")
3251
+ }),
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}`
3470
3257
  }),
3471
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
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
+ })
3291
+ })
3292
+ ]
3293
+ }),
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", {
3472
3299
  type: "button",
3473
- className: `${FallbacksCard_module_css_default.outlineButton} ${FallbacksCard_module_css_default.addButton}`,
3474
- disabled: !writable || presetToAdd === "",
3475
- onClick: addPresetSlotRow,
3476
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlusIcon, {}), t("timeSlots.addPreset")]
3300
+ className: FallbacksCard_module_css_default.discard,
3301
+ disabled: !activeDirty || saving,
3302
+ onClick: discardChain,
3303
+ children: t("discard")
3477
3304
  }),
3478
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3305
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3479
3306
  type: "button",
3480
- className: `${FallbacksCard_module_css_default.outlineButton} ${FallbacksCard_module_css_default.addButton}`,
3481
- disabled: !writable,
3482
- onClick: addCustomSlotRow,
3483
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlusIcon, {}), t("timeSlots.addCustom")]
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 })
3484
3322
  })
3485
3323
  ]
3486
3324
  })
3487
3325
  ]
3488
3326
  }),
3489
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3490
- className: FallbacksCard_module_css_default.field,
3491
- role: "group",
3492
- "aria-labelledby": "fallbacks-root-chain",
3493
- children: [
3494
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3495
- className: FallbacksCard_module_css_default.fieldLabel,
3496
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3497
- id: "fallbacks-root-chain",
3498
- children: t("rootChain.label")
3499
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
3500
- label: t("rootChain.tooltip"),
3501
- disabled: !writable
3502
- })]
3503
- }),
3504
- state.catalogStatus === "error" && state.catalogError !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3505
- className: FallbacksCard_module_css_default.hint,
3506
- children: t("catalog.error", { message: state.catalogError })
3507
- }),
3508
- state.catalogStatus === "ready" && state.catalogError !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3509
- className: FallbacksCard_module_css_default.hint,
3510
- children: t("catalog.partial", { message: state.catalogError })
3511
- }),
3512
- state.catalogStatus === "ready" && (state.groups.length === 0 || state.configuredProviders.length === 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3513
- className: FallbacksCard_module_css_default.hint,
3514
- children: t("catalog.empty")
3515
- }),
3516
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3517
- className: FallbacksCard_module_css_default.list,
3518
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3519
- className: FallbacksCard_module_css_default.editorCard,
3520
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3521
- className: FallbacksCard_module_css_default.chainBlock,
3522
- "data-chain-block": "",
3523
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChainSelectorList, {
3524
- selectors: allDayChainRow.selectors,
3525
- catalog: catalogOf(state),
3526
- configuredProviders: state.configuredProviders,
3527
- disabled: !writable,
3528
- t,
3529
- onChange: (selectorIndex, patch) => {
3530
- updateAllDayChainSelector(selectorIndex, patch);
3531
- },
3532
- onRemove: (selectorIndex) => {
3533
- removeAllDayChainSelector(selectorIndex);
3534
- },
3535
- onMove: reorderAllDayChainSelector
3536
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3537
- type: "button",
3538
- className: `${FallbacksCard_module_css_default.outlineButton} ${FallbacksCard_module_css_default.addButton}`,
3539
- disabled: !writable,
3540
- onClick: addAllDayChainSelector,
3541
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlusIcon, {}), t("timeSlots.selector.add")]
3542
- })]
3543
- })
3544
- })
3545
- })
3546
- ]
3547
- }),
3548
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3549
- className: FallbacksCard_module_css_default.field,
3550
- role: "group",
3551
- "aria-labelledby": "fallbacks-default-model",
3552
- children: [
3553
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3554
- className: FallbacksCard_module_css_default.fieldLabel,
3555
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3556
- id: "fallbacks-default-model",
3557
- children: t("defaultModel.label")
3558
- })
3559
- }),
3560
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3561
- className: FallbacksCard_module_css_default.hint,
3562
- children: t("allDay.hint")
3563
- }),
3564
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3565
- className: FallbacksCard_module_css_default.list,
3566
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3567
- className: FallbacksCard_module_css_default.editorCard,
3568
- children: [OFFICIAL_ALL_DAY_IDS.map((id, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3569
- className: FallbacksCard_module_css_default.optionRow,
3570
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3571
- type: "radio",
3572
- name: "fallbacks-all-day",
3573
- checked: allDayModel === id,
3574
- disabled: !writable,
3575
- onChange: () => {
3576
- setAllDayModel(id);
3577
- }
3578
- }), t(ALL_DAY_LABEL_KEYS[index])]
3579
- }, id)), allDayModel === "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3580
- className: FallbacksCard_module_css_default.hint,
3581
- children: t("allDay.nonconforming")
3582
- })]
3583
- })
3584
- })
3585
- ]
3586
- }),
3587
3327
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h2", {
3588
3328
  className: FallbacksCard_module_css_default.sectionHeading,
3589
3329
  id: "fallbacks-subagents",
@@ -3596,7 +3336,7 @@ window.__ModuleLoader__.load({
3596
3336
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3597
3337
  type: "button",
3598
3338
  className: FallbacksCard_module_css_default.discard,
3599
- disabled: !subDirty || saving,
3339
+ disabled: !rolesDirty || saving,
3600
3340
  onClick: () => {
3601
3341
  discardSection("sub");
3602
3342
  },
@@ -3604,7 +3344,7 @@ window.__ModuleLoader__.load({
3604
3344
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3605
3345
  type: "button",
3606
3346
  className: FallbacksCard_module_css_default.save,
3607
- disabled: !writable || saving || !subDirty,
3347
+ disabled: !writable || saving || !rolesDirty || liveErrors.roles.length > 0,
3608
3348
  onClick: () => {
3609
3349
  saveSection("sub");
3610
3350
  },
@@ -3612,101 +3352,27 @@ window.__ModuleLoader__.load({
3612
3352
  })]
3613
3353
  })]
3614
3354
  }),
3615
- liveErrors.sub.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3355
+ liveErrors.roles.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3616
3356
  className: FallbacksCard_module_css_default.error,
3617
3357
  role: "alert",
3618
- children: `${t("validation.blocked")}${liveErrors.sub.join("; ")}`
3358
+ children: `${t("validation.blocked")}${liveErrors.roles.join("; ")}`
3619
3359
  }),
3620
3360
  lastSaveSection === "sub" && state.status === "error" && state.error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3621
3361
  className: FallbacksCard_module_css_default.error,
3622
3362
  role: "alert",
3623
3363
  children: t("error.generic", { message: state.error })
3624
3364
  }),
3625
- state.subagentPolicy?.state === "enabled" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3626
- className: FallbacksCard_module_css_default.policyStatus,
3627
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3628
- className: FallbacksCard_module_css_default.policyStatusText,
3629
- role: "status",
3630
- children: [
3631
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3632
- className: FallbacksCard_module_css_default.policyStatusLabel,
3633
- children: t("subagents.policy.label")
3634
- }),
3635
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3636
- className: FallbacksCard_module_css_default.policyStatusLine,
3637
- children: `${t("subagents.policy.allowlist")}: ${state.subagentPolicy.allowedModels.map((route) => `${route.provider}/${route.model}`).join(", ")}`
3638
- }),
3639
- state.subagentPolicy.head !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3640
- className: FallbacksCard_module_css_default.policyStatusLine,
3641
- 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")})`
3642
- }),
3643
- state.subagentPolicy.blockedAttempt !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3644
- className: FallbacksCard_module_css_default.policyStatusWarn,
3645
- role: "alert",
3646
- children: t("subagents.policy.blocked")
3647
- }),
3648
- offAllowlistCount > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3649
- className: FallbacksCard_module_css_default.policyStatusWarn,
3650
- children: t("subagents.policy.offAllowlist", { count: String(offAllowlistCount) })
3651
- }),
3652
- !virtualRouteAllowed && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3653
- className: FallbacksCard_module_css_default.policyStatusLine,
3654
- children: t("subagents.policy.recommend")
3655
- })
3656
- ]
3657
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3658
- className: FallbacksCard_module_css_default.policyStatusSync,
3659
- role: "group",
3660
- "aria-label": t("subagents.policy.sync"),
3661
- children: [
3662
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3663
- type: "button",
3664
- className: FallbacksCard_module_css_default.outlineButton,
3665
- disabled: !writable || subDirty || mainDirty,
3666
- "aria-busy": syncStatus === "pending" || void 0,
3667
- "aria-describedby": subDirty || mainDirty ? syncDirtyHintId : void 0,
3668
- onClick: syncAllowlist,
3669
- children: t("subagents.policy.sync")
3670
- }),
3671
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3672
- id: syncDirtyHintId,
3673
- role: "status",
3674
- "data-sync-hint": "dirty",
3675
- className: `${FallbacksCard_module_css_default.policyStatusWarn} ${FallbacksCard_module_css_default.policyStatusLive}`,
3676
- children: subDirty || mainDirty ? t("subagents.policy.sync.dirty") : ""
3677
- }),
3678
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3679
- role: "status",
3680
- "data-sync-hint": "outcome",
3681
- className: `${syncStatus === "error" ? FallbacksCard_module_css_default.policyStatusWarn : FallbacksCard_module_css_default.policyStatusLine} ${FallbacksCard_module_css_default.policyStatusLive}`,
3682
- children: syncStatus === "ok" ? t("subagents.policy.sync.done") : syncStatus === "error" ? t("subagents.policy.sync.failed") : ""
3683
- })
3684
- ]
3685
- })]
3686
- }),
3687
- state.subagentPolicy?.state === "unprovable" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3688
- className: FallbacksCard_module_css_default.policyStatus,
3689
- role: "status",
3690
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3691
- className: FallbacksCard_module_css_default.policyStatusWarn,
3692
- role: "alert",
3693
- children: t("subagents.policy.unprovable")
3694
- })
3695
- }),
3696
3365
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3697
3366
  className: FallbacksCard_module_css_default.field,
3698
3367
  role: "group",
3699
3368
  "aria-labelledby": "fallbacks-roles-list",
3700
3369
  children: [
3701
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3370
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3702
3371
  className: FallbacksCard_module_css_default.fieldLabel,
3703
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3372
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3704
3373
  id: "fallbacks-roles-list",
3705
3374
  children: t("roles.list.label")
3706
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
3707
- label: t("roles.list.tooltip"),
3708
- disabled: !writable
3709
- })]
3375
+ })
3710
3376
  }),
3711
3377
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3712
3378
  className: FallbacksCard_module_css_default.hint,
@@ -3719,11 +3385,12 @@ window.__ModuleLoader__.load({
3719
3385
  const seed = seedInfo.get(row.id.trim());
3720
3386
  const seedBadge = seedBadgeLabel(seed, t);
3721
3387
  const personaBlank = row.persona.trim() === "";
3722
- const roleChainSummary = chainSummary(row.selectors, allowedModels);
3723
- const roleSummary = roleChainSummary ?? row.fallback;
3388
+ const chainRoute = row.chain === "" ? t("roles.chainMeta.unspecified") : `FallbacksChain/${row.chain}`;
3724
3389
  const roleExpanded = !row.collapsed || !writable;
3390
+ const chainOutside = row.chain !== "" && !chains.some((chain) => chain.id === row.chain);
3725
3391
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3726
3392
  className: FallbacksCard_module_css_default.editorCard,
3393
+ "data-role-card": row.id,
3727
3394
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3728
3395
  className: FallbacksCard_module_css_default.collapseRow,
3729
3396
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
@@ -3748,8 +3415,8 @@ window.__ModuleLoader__.load({
3748
3415
  }),
3749
3416
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3750
3417
  className: FallbacksCard_module_css_default.collapseMeta,
3751
- title: roleChainSummary,
3752
- children: roleSummary
3418
+ title: chainRoute,
3419
+ children: chainRoute
3753
3420
  })
3754
3421
  ]
3755
3422
  })
@@ -3781,6 +3448,49 @@ window.__ModuleLoader__.load({
3781
3448
  ]
3782
3449
  })
3783
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
+ }),
3784
3494
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3785
3495
  className: FallbacksCard_module_css_default.ruleGrid,
3786
3496
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -3827,66 +3537,6 @@ window.__ModuleLoader__.load({
3827
3537
  })] })]
3828
3538
  })
3829
3539
  }),
3830
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3831
- className: FallbacksCard_module_css_default.chainBlock,
3832
- "data-chain-block": "",
3833
- children: [
3834
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChainSelectorList, {
3835
- selectors: row.selectors,
3836
- catalog: catalogOf(state),
3837
- configuredProviders: state.configuredProviders,
3838
- disabled: !writable,
3839
- t,
3840
- onChange: (selectorIndex, patch) => {
3841
- updateRoleSelector(index, selectorIndex, patch);
3842
- },
3843
- onRemove: (selectorIndex) => {
3844
- removeRoleSelector(index, selectorIndex);
3845
- },
3846
- onMove: (from, to) => {
3847
- reorderRoleSelector(index, from, to);
3848
- }
3849
- }),
3850
- row.selectors.every((selector) => selectorRowToRaw(selector) === "") && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3851
- className: FallbacksCard_module_css_default.hint,
3852
- children: seed !== void 0 ? t("roles.seedChainOptional", { id: row.id }) : t("validation.roleChainRequired", { id: row.id })
3853
- }),
3854
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3855
- type: "button",
3856
- className: `${FallbacksCard_module_css_default.outlineButton} ${FallbacksCard_module_css_default.addButton}`,
3857
- disabled: !writable,
3858
- onClick: () => {
3859
- addRoleSelector(index);
3860
- },
3861
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlusIcon, {}), t("roles.selector.add")]
3862
- })
3863
- ]
3864
- }),
3865
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3866
- className: FallbacksCard_module_css_default.ruleGrid,
3867
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3868
- className: FallbacksCard_module_css_default.ruleCell,
3869
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3870
- className: FallbacksCard_module_css_default.ruleCellLabel,
3871
- children: t("roles.fallback")
3872
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
3873
- className: `${FallbacksCard_module_css_default.input} ${FallbacksCard_module_css_default.selectInput}`,
3874
- value: row.fallback,
3875
- "aria-label": t("roles.fallback"),
3876
- disabled: !writable,
3877
- onChange: (event) => {
3878
- updateRoleRow(index, { fallback: event.target.value });
3879
- },
3880
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3881
- value: "inherit-root",
3882
- children: t("roles.fallback.inherit-root")
3883
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3884
- value: "none",
3885
- children: t("roles.fallback.none")
3886
- })]
3887
- })]
3888
- })
3889
- }),
3890
3540
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3891
3541
  className: FallbacksCard_module_css_default.cardFoot,
3892
3542
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
@@ -3913,188 +3563,6 @@ window.__ModuleLoader__.load({
3913
3563
  })
3914
3564
  ]
3915
3565
  }),
3916
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3917
- className: FallbacksCard_module_css_default.field,
3918
- role: "group",
3919
- "aria-labelledby": "fallbacks-roles-rules",
3920
- children: [
3921
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3922
- className: FallbacksCard_module_css_default.fieldLabel,
3923
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3924
- id: "fallbacks-roles-rules",
3925
- children: t("roles.rules")
3926
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
3927
- label: t("roles.rules.tooltip"),
3928
- disabled: !writable
3929
- })]
3930
- }),
3931
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3932
- className: FallbacksCard_module_css_default.hint,
3933
- children: t("roles.rules.hint")
3934
- }),
3935
- state.catalogStatus === "error" && state.catalogError !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3936
- className: FallbacksCard_module_css_default.hint,
3937
- children: t("catalog.error", { message: state.catalogError })
3938
- }),
3939
- state.catalogStatus === "ready" && state.catalogError !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3940
- className: FallbacksCard_module_css_default.hint,
3941
- children: t("catalog.partial", { message: state.catalogError })
3942
- }),
3943
- state.catalogStatus === "ready" && (state.groups.length === 0 || state.configuredProviders.length === 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3944
- className: FallbacksCard_module_css_default.hint,
3945
- children: t("catalog.empty")
3946
- }),
3947
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3948
- className: FallbacksCard_module_css_default.list,
3949
- children: ruleRows.map((row, index) => {
3950
- const catalog = catalogOf(state);
3951
- const providerRaw = selectionToRaw(row.provider);
3952
- const group = catalog?.groups.find((entry) => entry.id === providerRaw);
3953
- const providerOutside = row.provider?.kind === "outside";
3954
- const providerUnconfigured = !providerOutside && providerRaw !== "" && (catalog?.providers.some((entry) => entry.provider === providerRaw) ?? false) && !state.configuredProviders.some((entry) => entry.provider === providerRaw);
3955
- const modelOutside = row.model?.kind === "outside";
3956
- const roleOutside = row.role !== "" && !roleOptions.includes(row.role);
3957
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3958
- className: FallbacksCard_module_css_default.editorCard,
3959
- children: [
3960
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3961
- className: FallbacksCard_module_css_default.ruleGrid,
3962
- children: [
3963
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3964
- className: FallbacksCard_module_css_default.ruleCell,
3965
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3966
- className: FallbacksCard_module_css_default.ruleCellLabel,
3967
- children: t("roles.rule.provider")
3968
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
3969
- className: `${FallbacksCard_module_css_default.input} ${FallbacksCard_module_css_default.selectInput}`,
3970
- value: providerRaw,
3971
- onChange: (event) => {
3972
- if (event.target.value === providerRaw) return;
3973
- updateRuleRow(index, {
3974
- provider: classifyProvider(event.target.value, catalog),
3975
- model: null
3976
- });
3977
- },
3978
- children: [
3979
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3980
- value: "",
3981
- children: t("roles.rule.provider.any")
3982
- }),
3983
- state.configuredProviders.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3984
- value: entry.provider,
3985
- children: entry.displayName
3986
- }, entry.provider)),
3987
- providerUnconfigured && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3988
- value: providerRaw,
3989
- children: `${providerRaw}${t("catalog.unconfigured.short")}`
3990
- }),
3991
- providerOutside && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3992
- value: providerRaw,
3993
- children: `${providerRaw}${t("catalog.outside.short")}`
3994
- })
3995
- ]
3996
- })]
3997
- }),
3998
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3999
- className: FallbacksCard_module_css_default.ruleCell,
4000
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4001
- className: FallbacksCard_module_css_default.ruleCellLabel,
4002
- children: t("roles.rule.model")
4003
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
4004
- className: `${FallbacksCard_module_css_default.input} ${FallbacksCard_module_css_default.selectInput}`,
4005
- value: selectionToRaw(row.model),
4006
- onChange: (event) => {
4007
- updateRuleRow(index, { model: classifyModel(providerRaw, event.target.value, catalog) });
4008
- },
4009
- children: [
4010
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
4011
- value: "",
4012
- children: t("roles.rule.model.any")
4013
- }),
4014
- (group?.models ?? []).map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
4015
- value: model.id,
4016
- children: model.name
4017
- }, model.id)),
4018
- modelOutside && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
4019
- value: selectionToRaw(row.model),
4020
- children: `${selectionToRaw(row.model)}${t("catalog.outside.short")}`
4021
- })
4022
- ]
4023
- })]
4024
- }),
4025
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
4026
- className: FallbacksCard_module_css_default.ruleCell,
4027
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4028
- className: FallbacksCard_module_css_default.ruleCellLabel,
4029
- children: t("roles.rule.role")
4030
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
4031
- className: `${FallbacksCard_module_css_default.input} ${FallbacksCard_module_css_default.selectInput}`,
4032
- value: row.role,
4033
- disabled: !writable,
4034
- onChange: (event) => {
4035
- updateRuleRow(index, { role: event.target.value });
4036
- },
4037
- children: [
4038
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
4039
- value: "",
4040
- children: t("roles.rule.roleSelectPlaceholder")
4041
- }),
4042
- roleOptions.map((id) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
4043
- value: id,
4044
- children: id === "inherit" ? t("roles.rule.role.inherit") : id
4045
- }, id)),
4046
- roleOutside && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
4047
- value: row.role,
4048
- children: `${row.role}${t("roles.rule.roleUndeclared.short")}`
4049
- })
4050
- ]
4051
- })]
4052
- })
4053
- ]
4054
- }),
4055
- (providerOutside || modelOutside) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4056
- className: FallbacksCard_module_css_default.hint,
4057
- children: [t("catalog.outside.hint"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
4058
- label: t("catalog.outside.tooltip"),
4059
- disabled: !writable
4060
- })]
4061
- }),
4062
- row.role === "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4063
- className: FallbacksCard_module_css_default.hint,
4064
- children: t("validation.ruleRoleRequired")
4065
- }),
4066
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4067
- className: FallbacksCard_module_css_default.cardFoot,
4068
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4069
- type: "button",
4070
- className: `${FallbacksCard_module_css_default.iconButton} ${FallbacksCard_module_css_default.iconButtonDanger}`,
4071
- "data-tip": t("roles.removeRule"),
4072
- "aria-label": t("roles.removeRule"),
4073
- onClick: () => {
4074
- setRuleRows((rows) => rows.filter((_, rowIndex) => rowIndex !== index));
4075
- },
4076
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TrashIcon, {})
4077
- })
4078
- })
4079
- ]
4080
- }, index);
4081
- })
4082
- }),
4083
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4084
- type: "button",
4085
- className: `${FallbacksCard_module_css_default.outlineButton} ${FallbacksCard_module_css_default.addButton}`,
4086
- disabled: !writable,
4087
- onClick: () => {
4088
- setRuleRows((rows) => [...rows, {
4089
- provider: null,
4090
- model: null,
4091
- role: ""
4092
- }]);
4093
- },
4094
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlusIcon, {}), t("roles.addRule")]
4095
- })
4096
- ]
4097
- }),
4098
3566
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
4099
3567
  className: FallbacksCard_module_css_default.sectionHeading,
4100
3568
  id: "fallbacks-advanced",
@@ -4119,16 +3587,6 @@ window.__ModuleLoader__.load({
4119
3587
  id: "fallbacks-advanced-body",
4120
3588
  className: FallbacksCard_module_css_default.advancedBody,
4121
3589
  children: [
4122
- liveErrors.advanced.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4123
- className: FallbacksCard_module_css_default.error,
4124
- role: "alert",
4125
- children: `${t("validation.blocked")}${liveErrors.advanced.join("; ")}`
4126
- }),
4127
- lastSaveSection === "advanced" && state.status === "error" && state.error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4128
- className: FallbacksCard_module_css_default.error,
4129
- role: "alert",
4130
- children: t("error.generic", { message: state.error })
4131
- }),
4132
3590
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4133
3591
  className: FallbacksCard_module_css_default.field,
4134
3592
  role: "group",
@@ -4289,126 +3747,11 @@ window.__ModuleLoader__.load({
4289
3747
  ]
4290
3748
  })
4291
3749
  ]
4292
- }),
4293
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4294
- className: FallbacksCard_module_css_default.statusBlock,
4295
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4296
- className: FallbacksCard_module_css_default.statusTitle,
4297
- children: t("status.title")
4298
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
4299
- className: FallbacksCard_module_css_default.statusLine,
4300
- role: state.switchesStatus === "error" ? "alert" : void 0,
4301
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4302
- className: FallbacksCard_module_css_default.statusLineLabel,
4303
- children: t("status.switches.label")
4304
- }), switchesLine]
4305
- })]
4306
3750
  })
4307
3751
  ]
4308
3752
  });
4309
3753
  }
4310
3754
  //#endregion
4311
- //#region \0dsh-css:/home/runner/work/dsh-llm-fallbacks/dsh-llm-fallbacks/src/client/GeneralFallbacksRow.module.css.mjs
4312
- 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";
4313
- const tagId$2 = "dsh-llm-fallbacks/GeneralFallbacksRow.module.css";
4314
- if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]") === null) {
4315
- const tag = document.createElement("style");
4316
- tag.dataset.plugin = "dsh-llm-fallbacks";
4317
- tag.dataset.pluginCss = tagId$2;
4318
- tag.textContent = css$2;
4319
- document.head.appendChild(tag);
4320
- }
4321
- var GeneralFallbacksRow_module_css_default = {
4322
- "row": "_440e1d7b_row",
4323
- "rowText": "_a4f2ceec_rowText",
4324
- "title": "_9865b509_title",
4325
- "summary": "_10a44713_summary",
4326
- "badge": "_35648278_badge",
4327
- "badgeEnabled": "_3b9e8609_badgeEnabled"
4328
- };
4329
- //#endregion
4330
- //#region src/client/GeneralFallbacksRow.tsx
4331
- /**
4332
- * Fallbacks status row — the `fallbacks` read-only row on the dsh General
4333
- * settings page (plan fallbacks-aux-seams, task 1). Registered into the
4334
- * `settings.general.item` slot (id `fallbacks`, order 100 — after every
4335
- * upstream preference row: agent-preset -25 / permission -20 / language 0 /
4336
- * appearance 10 / composer-enter 20, so the informational row renders at the
4337
- * column end). Owner props are intentionally empty (`children?: never`,
4338
- * dsh-private ui-settings slots.ts:81-84 — the section column only stacks),
4339
- * so all data flows through the shared {@link FallbacksSettingsController}
4340
- * (the same instance the plugin-config card consumes): the row triggers the
4341
- * first read on mount when the store is still idle, and the pushed
4342
- * invalidations wired in `apply` (`settings/document-updated` fallbacks-ns +
4343
- * `connection/reset`, which refresh only already-read stores) keep it fresh
4344
- * afterwards — no new data path, no store API change.
4345
- *
4346
- * The row is read-only by design (偏好位语义: a General preference row is not
4347
- * a control surface): an active/inactive badge (content presence — the
4348
- * removed config-level `enabled` switch is re-keyed to `isFallbackActive`,
4349
- * plan fallbacks-web-ux-alignment T2) + a compact last-switch summary.
4350
- * Honest degraded states: a hard load error or an unreachable gateway
4351
- * channel (`ready && !present`) render the neutral 'unknown' badge — a
4352
- * channel-down read must never masquerade as 'disabled' (KD-G5); the
4353
- * switches face keeps its own error/empty states (D-5 semantics unchanged).
4354
- *
4355
- * Geometry follows the upstream Setting-Cell (figma 501:30011 — gap 8,
4356
- * pad 16/0, hairline separator, title over subtitle in the text column, a
4357
- * small non-interactive pill on the right); every color resolves through a
4358
- * `--dsw-alias-*` token (light/dark adaptive).
4359
- */
4360
- /**
4361
- * Render the Fallbacks status row.
4362
- * @param props - composed slot props.
4363
- * @returns the row element tree.
4364
- */
4365
- function GeneralFallbacksRow({ controller, useSnapshot, t }) {
4366
- const state = useSnapshot((snapshot) => snapshot);
4367
- (0, react.useEffect)(() => {
4368
- const snapshot = controller.store.getSnapshot();
4369
- if (snapshot.status === "idle") controller.load();
4370
- if (snapshot.switchesStatus === "idle") controller.loadSwitches();
4371
- }, [controller]);
4372
- const settled = state.status === "ready";
4373
- const badgeKey = settled && state.present ? isFallbackActive(state.config) ? "general.enabled" : "general.disabled" : "general.unknown";
4374
- const latestSwitch = state.switches[0];
4375
- let summary;
4376
- if (state.status === "error") summary = t("general.error", { message: state.error ?? "" });
4377
- else if (!settled) summary = t("loading");
4378
- else if (!state.present) summary = t("general.unavailable");
4379
- else if (state.switchesStatus === "error") summary = t("status.switches.error", { message: state.switchesError ?? "" });
4380
- else if (state.switchesStatus === "loading") summary = t("loading");
4381
- else if (latestSwitch === void 0) summary = t("general.switch.empty");
4382
- else {
4383
- const reasonKey = SWITCH_REASON_KEYS[latestSwitch.reason];
4384
- const params = {
4385
- from: `${latestSwitch.from.provider}/${latestSwitch.from.model}`,
4386
- to: `${latestSwitch.to.provider}/${latestSwitch.to.model}`,
4387
- role: latestSwitch.role,
4388
- reason: reasonKey === void 0 ? latestSwitch.reason : t(reasonKey)
4389
- };
4390
- summary = latestSwitch.reason === "role-inject" ? t("general.switch.roleInject", params) : t("general.switch", params);
4391
- }
4392
- const alert = state.status === "error" || state.switchesStatus === "error";
4393
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4394
- className: GeneralFallbacksRow_module_css_default.row,
4395
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4396
- className: GeneralFallbacksRow_module_css_default.rowText,
4397
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4398
- className: GeneralFallbacksRow_module_css_default.title,
4399
- children: t("general.title")
4400
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4401
- className: GeneralFallbacksRow_module_css_default.summary,
4402
- role: alert ? "alert" : void 0,
4403
- children: summary
4404
- })]
4405
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4406
- className: `${GeneralFallbacksRow_module_css_default.badge} ${badgeKey === "general.enabled" ? GeneralFallbacksRow_module_css_default.badgeEnabled : ""}`,
4407
- children: t(badgeKey)
4408
- })]
4409
- });
4410
- }
4411
- //#endregion
4412
3755
  //#region src/client/switch-guard.ts
4413
3756
  /**
4414
3757
  * True when `value` is a well-formed `fallbacks/switch` payload — the ONE
@@ -4762,7 +4105,7 @@ window.__ModuleLoader__.load({
4762
4105
  const connection = ctx.get("connection");
4763
4106
  const sessions = ctx.get("sessions");
4764
4107
  const controller = new FallbacksSettingsController(ctx.remote, connection.rpc);
4765
- const useSnapshot = bindSnapshotSelector(controller.store);
4108
+ controller.store;
4766
4109
  ctx.effect(() => {
4767
4110
  const syncSession = () => {
4768
4111
  controller.setCurrentSession(sessions === void 0 ? void 0 : viewedSessionId(sessions.list.getSnapshot()));
@@ -4771,7 +4114,6 @@ window.__ModuleLoader__.load({
4771
4114
  const refresh = (ns) => {
4772
4115
  if (ns !== void 0 && ns !== "fallbacks") return;
4773
4116
  refreshFallbacksIfLoaded(controller);
4774
- refreshSwitchesIfLoaded(controller);
4775
4117
  };
4776
4118
  const refreshCatalog = () => {
4777
4119
  refreshCatalogIfLoaded(controller);
@@ -4812,18 +4154,6 @@ window.__ModuleLoader__.load({
4812
4154
  })
4813
4155
  }, FallbacksCard);
4814
4156
  });
4815
- ctx.slots.inject("settings.general.item", function* () {
4816
- yield ctx.slots.register({
4817
- name: "settings.general.item",
4818
- id: "fallbacks",
4819
- order: 100,
4820
- locale: NS,
4821
- inject: () => ({
4822
- controller,
4823
- useSnapshot
4824
- })
4825
- }, GeneralFallbacksRow);
4826
- });
4827
4157
  ctx.effect(() => ctx.uiConversation.events.register(fallbackSwitchDefinition), "llm-fallbacks: conversation node definition");
4828
4158
  ctx.slots.inject("conversation.chat.node", function* () {
4829
4159
  yield ctx.slots.register({