@mars-sea/dsh-commandcode-provider 0.10.1 → 0.10.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -66,8 +66,8 @@ window.__ModuleLoader__.load({
66
66
  exceededWarning: " ⚠️ 超限!",
67
67
  resetSuffix: "重置 {when}",
68
68
  partialFailures: "⚠️ 部分端点失败: {list}",
69
- noData: "(no data — check your API key)",
70
- errorText: "Could not fetch Command Code usage: {message}",
69
+ noData: "(无数据 — 请检查 API 密钥)",
70
+ errorText: "获取 Command Code 用量失败:{message}",
71
71
  imageGate: "当前会话已包含图片,而模型 {model} 不支持图片输入;请选择支持图片的模型,或先移除会话中的图片。"
72
72
  },
73
73
  en: {
@@ -343,9 +343,17 @@ window.__ModuleLoader__.load({
343
343
  addedRules = [];
344
344
  /** Staged edits to stored routing rules, by stored row id. */
345
345
  ruleDrafts = /* @__PURE__ */ new Map();
346
- /** Stored routing rule rows staged for removal. */
347
- removedRuleIds = /* @__PURE__ */ new Set();
348
- /** The catalog the rule editor offers (Host-side). */
346
+ /**
347
+ * Stored routing rule rows staged for removal, by stored row id with a
348
+ * content snapshot. The snapshot makes reconcile content-based: stored
349
+ * row ids are positional (`rule-N`) and shift after any write, so an
350
+ * id-only check would misread a landed removal as pending (and a retry
351
+ * would delete the wrong row).
352
+ */
353
+ removedRuleIds = /* @__PURE__ */ new Map();
354
+ /** Staged visible-model allowlist (undefined = no draft). */
355
+ visibleModelsDraft = void 0;
356
+ /** The catalog the model editors offer (Host-side). */
349
357
  catalogModels = [];
350
358
  catalogFailed = false;
351
359
  saving = false;
@@ -398,8 +406,8 @@ window.__ModuleLoader__.load({
398
406
  const snapshot = this.scope.getSnapshot();
399
407
  const named = typeof snapshot.value?.apiKeyEnv === "string" && snapshot.value.apiKeyEnv.length > 0 ? snapshot.value.apiKeyEnv : DEFAULT_API_KEY_REF;
400
408
  if (named === this.credentialRef) return;
409
+ this.credentialStates.delete(this.credentialRef);
401
410
  this.credentialRef = named;
402
- this.credentialStates.delete(named);
403
411
  }
404
412
  /** Subscribe to state projections. @returns the disposer. */
405
413
  subscribe(listener) {
@@ -437,9 +445,10 @@ window.__ModuleLoader__.load({
437
445
  accounts,
438
446
  accountsRemoving: [...this.removedRefs],
439
447
  rules: this.effectiveRules(),
448
+ visibleModels: this.effectiveVisibleModels(),
440
449
  catalogModels: this.catalogModels,
441
450
  catalogFailed: this.catalogFailed,
442
- dirty: plan.length > 0 || this.accountsDirty() || this.rulesDirty(),
451
+ dirty: plan.length > 0 || this.accountsDirty() || this.rulesDirty() || this.visibleModelsDirty(),
443
452
  invalid: plan.some((item) => item.run === void 0),
444
453
  saving: this.saving,
445
454
  failed: this.failed,
@@ -525,7 +534,16 @@ window.__ModuleLoader__.load({
525
534
  removeRule(id) {
526
535
  const addedIndex = this.addedRules.findIndex((_, index) => `new-${index}` === id);
527
536
  if (addedIndex >= 0) this.addedRules.splice(addedIndex, 1);
528
- else this.removedRuleIds.add(id);
537
+ else {
538
+ const stored = this.storedRules().find((rule) => rule.id === id);
539
+ this.removedRuleIds.set(id, stored === void 0 ? {
540
+ models: [],
541
+ account: ""
542
+ } : {
543
+ models: [...stored.models],
544
+ account: stored.account
545
+ });
546
+ }
529
547
  this.ruleDrafts.delete(id);
530
548
  this.failed = false;
531
549
  this.publish();
@@ -598,10 +616,11 @@ window.__ModuleLoader__.load({
598
616
  }
599
617
  /** Discard every staged edit. */
600
618
  discard() {
601
- if (this.staged.size === 0 && !this.accountsStaged() && !this.rulesStaged() && !this.failed) return;
619
+ if (this.staged.size === 0 && !this.accountsStaged() && !this.rulesStaged() && !this.visibleModelsStaged() && !this.failed) return;
602
620
  this.staged.clear();
603
621
  this.clearAccountStaging();
604
622
  this.clearRuleStaging();
623
+ this.clearVisibleModelsStaging();
605
624
  this.failed = false;
606
625
  this.publish();
607
626
  }
@@ -618,7 +637,8 @@ window.__ModuleLoader__.load({
618
637
  const plan = this.plan();
619
638
  const accountRuns = this.accountPlan();
620
639
  const ruleRuns = this.rulesPlan();
621
- if (plan.length === 0 && accountRuns.length === 0 && ruleRuns.length === 0 || this.saving) return;
640
+ const visibleRuns = this.visibleModelsPlan();
641
+ if (plan.length === 0 && accountRuns.length === 0 && ruleRuns.length === 0 && visibleRuns.length === 0 || this.saving) return;
622
642
  const runs = [];
623
643
  for (const item of plan) {
624
644
  if (item.run === void 0) return;
@@ -631,10 +651,19 @@ window.__ModuleLoader__.load({
631
651
  for (const run of [
632
652
  ...runs,
633
653
  ...accountRuns,
634
- ...ruleRuns
635
- ]) if (!await run()) {
636
- landed = false;
637
- break;
654
+ ...ruleRuns,
655
+ ...visibleRuns
656
+ ]) {
657
+ let ok = false;
658
+ try {
659
+ ok = await run();
660
+ } catch {
661
+ ok = false;
662
+ }
663
+ if (!ok) {
664
+ landed = false;
665
+ break;
666
+ }
638
667
  }
639
668
  this.saving = false;
640
669
  this.failed = !landed;
@@ -643,9 +672,11 @@ window.__ModuleLoader__.load({
643
672
  this.staged.clear();
644
673
  this.clearAccountStaging();
645
674
  this.clearRuleStaging();
675
+ this.clearVisibleModelsStaging();
646
676
  } else {
647
677
  this.reconcileAccountStaging();
648
678
  this.reconcileRuleStaging();
679
+ this.reconcileVisibleModelsStaging();
649
680
  }
650
681
  this.publish();
651
682
  }
@@ -798,10 +829,10 @@ window.__ModuleLoader__.load({
798
829
  if (changed) this.publish();
799
830
  }
800
831
  /**
801
- * Fetch the model catalog for the routing-rule editor through the Host
802
- * Remote. Runs once at construction; call again (e.g. from the client entry
803
- * once the Remote mount lands) to (re)try — a later success clears a prior
804
- * failure flag so the editor recovers without a page reload.
832
+ * Fetch the model catalog for the settings page's model editors through
833
+ * the Host Remote. Runs once at construction; call again (e.g. from the
834
+ * client entry once the Remote mount lands) to (re)try — a later success
835
+ * clears a prior failure flag so the editors recover without a page reload.
805
836
  */
806
837
  refreshCatalog() {
807
838
  const models = this.api.models;
@@ -812,7 +843,18 @@ window.__ModuleLoader__.load({
812
843
  }
813
844
  models().then((response) => {
814
845
  if (response.ok && Array.isArray(response.value?.models)) {
815
- this.catalogModels = response.value.models;
846
+ const shaped = [];
847
+ for (const model of response.value.models) {
848
+ if (typeof model !== "object" || model === null) continue;
849
+ const entry = model;
850
+ if (typeof entry.id !== "string" || typeof entry.name !== "string") continue;
851
+ shaped.push({
852
+ id: entry.id,
853
+ name: entry.name,
854
+ ...typeof entry.tier === "string" ? { tier: entry.tier } : {}
855
+ });
856
+ }
857
+ this.catalogModels = shaped;
816
858
  this.catalogFailed = false;
817
859
  } else this.catalogFailed = true;
818
860
  }, () => {
@@ -978,11 +1020,70 @@ window.__ModuleLoader__.load({
978
1020
  this.ruleDrafts.clear();
979
1021
  this.removedRuleIds.clear();
980
1022
  }
981
- /** Drop rule staging the stored section already reflects (partial-save retry). */
1023
+ /**
1024
+ * Drop rule staging the stored section already reflects (partial-save
1025
+ * retry). Stored row ids are positional (`rule-N`) and shift after any
1026
+ * write, so every check here is content-based, never id-based:
1027
+ * - landed additions (in `addedRules`, already in stored) are dropped, or
1028
+ * a retry would persist them twice;
1029
+ * - staged removals whose snapshot row is gone from stored have landed
1030
+ * (drop them); a removal whose snapshot still matches a stored row is
1031
+ * still pending (keep it).
1032
+ */
982
1033
  reconcileRuleStaging() {
983
- const storedIds = new Set(this.storedRules().map((rule) => rule.id));
984
- for (const id of [...this.removedRuleIds]) if (!storedIds.has(id)) this.removedRuleIds.delete(id);
985
- for (const id of [...this.ruleDrafts.keys()]) if (!storedIds.has(id)) this.ruleDrafts.delete(id);
1034
+ const stored = this.storedRules();
1035
+ this.addedRules = this.addedRules.filter((added) => !stored.some((rule) => sameModels(added.models, rule.models) && added.account === rule.account));
1036
+ for (const [id, snapshot] of [...this.removedRuleIds]) if (!stored.some((rule) => sameModels(snapshot.models, rule.models) && snapshot.account === rule.account)) this.removedRuleIds.delete(id);
1037
+ this.ruleDrafts.clear();
1038
+ }
1039
+ /** The stored visible-model allowlist (`visibleModels`); empty = show all. */
1040
+ storedVisibleModels() {
1041
+ const raw = this.scope.getSnapshot().value?.visibleModels;
1042
+ if (!Array.isArray(raw)) return [];
1043
+ return raw.filter((m) => typeof m === "string" && m !== "");
1044
+ }
1045
+ /** Effective visible-model allowlist: staged draft or stored value. */
1046
+ effectiveVisibleModels() {
1047
+ return this.visibleModelsDraft ?? this.storedVisibleModels();
1048
+ }
1049
+ /** Whether the staged visible-model selection differs from stored. */
1050
+ visibleModelsDirty() {
1051
+ return this.visibleModelsDraft !== void 0 && !sameModels(this.visibleModelsDraft, this.storedVisibleModels());
1052
+ }
1053
+ /** Whether any visible-model staging exists. */
1054
+ visibleModelsStaged() {
1055
+ return this.visibleModelsDraft !== void 0;
1056
+ }
1057
+ /** Reset the visible-model staged edit. */
1058
+ clearVisibleModelsStaging() {
1059
+ this.visibleModelsDraft = void 0;
1060
+ }
1061
+ /** Drop visible-model staging the stored section already reflects. */
1062
+ reconcileVisibleModelsStaging() {
1063
+ if (this.visibleModelsDraft !== void 0 && sameModels(this.visibleModelsDraft, this.storedVisibleModels())) this.visibleModelsDraft = void 0;
1064
+ }
1065
+ /** The visible-model writes a save performs (empty when nothing staged). */
1066
+ visibleModelsPlan() {
1067
+ if (!this.visibleModelsDirty()) return [];
1068
+ return [() => this.writeVisibleModels()];
1069
+ }
1070
+ /** Persist the staged visible-model allowlist into the settings section. */
1071
+ async writeVisibleModels() {
1072
+ const list = this.visibleModelsDraft ?? [];
1073
+ await this.scope.set("visibleModels", list);
1074
+ return sameModels(this.storedVisibleModels(), list);
1075
+ }
1076
+ /** Stage the visible-model allowlist (multi-select). */
1077
+ editVisibleModels(models) {
1078
+ this.visibleModelsDraft = [...models];
1079
+ this.failed = false;
1080
+ this.publish();
1081
+ }
1082
+ /** Stage "show all models" (clears the allowlist). */
1083
+ clearVisibleModels() {
1084
+ this.visibleModelsDraft = [];
1085
+ this.failed = false;
1086
+ this.publish();
986
1087
  }
987
1088
  /** The routing-rule writes a save performs (empty when nothing staged). */
988
1089
  rulesPlan() {
@@ -991,7 +1092,10 @@ window.__ModuleLoader__.load({
991
1092
  }
992
1093
  /** Persist the staged routing rules into the settings section. */
993
1094
  async writeRules() {
994
- const list = [...this.storedRules().filter((rule) => !this.removedRuleIds.has(rule.id)).map((rule) => {
1095
+ const list = [...this.storedRules().filter((rule) => {
1096
+ const snapshot = this.removedRuleIds.get(rule.id);
1097
+ return snapshot === void 0 || !sameModels(snapshot.models, rule.models) || snapshot.account !== rule.account;
1098
+ }).map((rule) => {
995
1099
  const draft = this.ruleDrafts.get(rule.id);
996
1100
  return {
997
1101
  models: draft !== void 0 && draft.models.length > 0 ? draft.models : rule.models,
@@ -1001,9 +1105,16 @@ window.__ModuleLoader__.load({
1001
1105
  models: rule.models,
1002
1106
  account: rule.account
1003
1107
  }))].filter((rule) => rule.models.length > 0);
1004
- await this.scope.set("modelAccountRules", list);
1108
+ const seen = /* @__PURE__ */ new Set();
1109
+ const deduped = list.filter((rule) => {
1110
+ const key = `${rule.account}\u0001${[...rule.models].sort().join("")}`;
1111
+ if (seen.has(key)) return false;
1112
+ seen.add(key);
1113
+ return true;
1114
+ });
1115
+ await this.scope.set("modelAccountRules", deduped);
1005
1116
  const after = this.storedRules();
1006
- return after.length === list.length && list.every((item, index) => after[index] !== void 0 && sameModels(after[index].models, item.models) && after[index].account === item.account);
1117
+ return after.length === deduped.length && deduped.every((item, index) => after[index] !== void 0 && sameModels(after[index].models, item.models) && after[index].account === item.account);
1007
1118
  }
1008
1119
  publish() {
1009
1120
  if (this.disposed) return;
@@ -1149,6 +1260,16 @@ window.__ModuleLoader__.load({
1149
1260
  if (value >= 1e3) return `${(value / 1e3).toFixed(1)}K`;
1150
1261
  return String(value);
1151
1262
  }
1263
+ /**
1264
+ * Format a success-rate percentage (already in percent units, e.g. 99.96):
1265
+ * at most two decimals, trailing zeros trimmed — `100` stays `100`, not
1266
+ * `100.00`, and the raw upstream float `99.965552876334` becomes `99.97`.
1267
+ * The `%` suffix is appended by the caller (the card and the dashboard line
1268
+ * both compose it).
1269
+ */
1270
+ function formatSuccessRate(value) {
1271
+ return String(Number(value.toFixed(2)));
1272
+ }
1152
1273
  /** One window's fill ratio in [0, 1]; 0 when uncapped. */
1153
1274
  function windowRatio(used, cap) {
1154
1275
  if (cap <= 0) return 0;
@@ -1437,7 +1558,7 @@ window.__ModuleLoader__.load({
1437
1558
  /** The wire namespace all Command Code endpoints share. */
1438
1559
  const REMOTE_NAMESPACE = "commandcode";
1439
1560
  /**
1440
- * Build the validator helpers one endpoint uses Sequence. `prefix` names the
1561
+ * Build the validator helpers one endpoint uses. `prefix` names the
1441
1562
  * endpoint in the rejection message (e.g. `commandcode/report result:`), so
1442
1563
  * each wire file keeps its own diagnostic phrasing while sharing the helper
1443
1564
  * bodies.
@@ -1493,14 +1614,13 @@ window.__ModuleLoader__.load({
1493
1614
  /** Canonical `<namespace>/<method>` endpoint of the usage report Remote. */
1494
1615
  const USAGE_REPORT_ENDPOINT = "commandcode/report";
1495
1616
  /**
1496
- * The shared read/validate helpers, prefixed with the report endpoint so
1497
- * rejection messages name the offending boundary (the catalog endpoint below
1498
- * shares this prefix, matching the historical behavior).
1617
+ * The shared read/validate helpers for the usage report endpoint, prefixed
1618
+ * so rejection messages name the offending boundary.
1499
1619
  */
1500
- const { reject: reject$1, record, stringField, numberField, booleanField } = makeBoundaryValidator("commandcode/report result:");
1620
+ const { reject: reject$1, record: record$1, stringField: stringField$1, numberField, booleanField } = makeBoundaryValidator("commandcode/report result:");
1501
1621
  /** Validate one window-limit block (`fiveHour` / `weekly`). */
1502
1622
  function windowLimit(value, field) {
1503
- const source = record(value, field);
1623
+ const source = record$1(value, field);
1504
1624
  return {
1505
1625
  used: numberField(source, "used", `${field}.used`),
1506
1626
  cap: numberField(source, "cap", `${field}.cap`),
@@ -1514,7 +1634,7 @@ window.__ModuleLoader__.load({
1514
1634
  * malformed frame fails the boundary instead of rendering garbage.
1515
1635
  */
1516
1636
  function parseUsageReport(value) {
1517
- const source = record(value, "report");
1637
+ const source = record$1(value, "report");
1518
1638
  const failures = source.failures;
1519
1639
  if (!Array.isArray(failures) || failures.some((entry) => typeof entry !== "string")) reject$1("failures");
1520
1640
  const report = { failures };
@@ -1524,15 +1644,15 @@ window.__ModuleLoader__.load({
1524
1644
  else reject$1("blocked");
1525
1645
  }
1526
1646
  if (source.account !== void 0) {
1527
- const account = record(source.account, "account");
1647
+ const account = record$1(source.account, "account");
1528
1648
  report.account = {
1529
- id: stringField(account, "id", "account.id"),
1530
- name: stringField(account, "name", "account.name"),
1531
- userName: stringField(account, "userName", "account.userName")
1649
+ id: stringField$1(account, "id", "account.id"),
1650
+ name: stringField$1(account, "name", "account.name"),
1651
+ userName: stringField$1(account, "userName", "account.userName")
1532
1652
  };
1533
1653
  }
1534
1654
  if (source.usage !== void 0) {
1535
- const usage = record(source.usage, "usage");
1655
+ const usage = record$1(source.usage, "usage");
1536
1656
  report.usage = {
1537
1657
  totalCount: numberField(usage, "totalCount", "usage.totalCount"),
1538
1658
  totalCost: numberField(usage, "totalCost", "usage.totalCost"),
@@ -1542,11 +1662,11 @@ window.__ModuleLoader__.load({
1542
1662
  totalTokensIn: numberField(usage, "totalTokensIn", "usage.totalTokensIn"),
1543
1663
  totalTokensOut: numberField(usage, "totalTokensOut", "usage.totalTokensOut"),
1544
1664
  totalCredits: numberField(usage, "totalCredits", "usage.totalCredits"),
1545
- periodBasis: stringField(usage, "periodBasis", "usage.periodBasis")
1665
+ periodBasis: stringField$1(usage, "periodBasis", "usage.periodBasis")
1546
1666
  };
1547
1667
  }
1548
1668
  if (source.credits !== void 0) {
1549
- const credits = record(source.credits, "credits");
1669
+ const credits = record$1(source.credits, "credits");
1550
1670
  report.credits = {
1551
1671
  monthlyCredits: numberField(credits, "monthlyCredits", "credits.monthlyCredits"),
1552
1672
  purchasedCredits: numberField(credits, "purchasedCredits", "credits.purchasedCredits"),
@@ -1556,13 +1676,13 @@ window.__ModuleLoader__.load({
1556
1676
  };
1557
1677
  }
1558
1678
  if (source.plan !== void 0) {
1559
- const plan = record(source.plan, "plan");
1679
+ const plan = record$1(source.plan, "plan");
1560
1680
  const monthly = plan.monthlyCredits;
1561
1681
  if (monthly !== null && (typeof monthly !== "number" || !Number.isFinite(monthly))) reject$1("plan.monthlyCredits");
1562
1682
  report.plan = {
1563
- planId: stringField(plan, "planId", "plan.planId"),
1564
- name: stringField(plan, "name", "plan.name"),
1565
- status: stringField(plan, "status", "plan.status"),
1683
+ planId: stringField$1(plan, "planId", "plan.planId"),
1684
+ name: stringField$1(plan, "name", "plan.name"),
1685
+ status: stringField$1(plan, "status", "plan.status"),
1566
1686
  monthlyCredits: monthly,
1567
1687
  currentPeriodEnd: numberField(plan, "currentPeriodEnd", "plan.currentPeriodEnd")
1568
1688
  };
@@ -1571,20 +1691,20 @@ window.__ModuleLoader__.load({
1571
1691
  }
1572
1692
  /** Parse one untrusted boundary value into a {@link CommandCodeAccountUsage}. */
1573
1693
  function parseAccountUsage(value) {
1574
- const source = record(value, "account");
1694
+ const source = record$1(value, "account");
1575
1695
  return {
1576
- id: stringField(source, "id", "account.id"),
1577
- label: stringField(source, "label", "account.label"),
1696
+ id: stringField$1(source, "id", "account.id"),
1697
+ label: stringField$1(source, "label", "account.label"),
1578
1698
  configured: booleanField(source, "configured", "account.configured"),
1579
1699
  active: booleanField(source, "active", "account.active"),
1580
- mark: stringField(source, "mark", "account.mark"),
1700
+ mark: stringField$1(source, "mark", "account.mark"),
1581
1701
  cooldownUntil: numberField(source, "cooldownUntil", "account.cooldownUntil"),
1582
1702
  report: parseUsageReport(source.report)
1583
1703
  };
1584
1704
  }
1585
1705
  /** Parse the wire result into a {@link CommandCodeAccountsReport}. */
1586
1706
  function parseAccountsReport(value) {
1587
- const accounts = record(value, "result").accounts;
1707
+ const accounts = record$1(value, "result").accounts;
1588
1708
  if (Array.isArray(accounts)) return { accounts: accounts.map(parseAccountUsage) };
1589
1709
  return reject$1("accounts");
1590
1710
  }
@@ -1595,19 +1715,27 @@ window.__ModuleLoader__.load({
1595
1715
  };
1596
1716
  /** Canonical `<namespace>/<method>` endpoint of the model-catalog Remote. */
1597
1717
  const MODELS_ENDPOINT = "commandcode/models";
1718
+ /**
1719
+ * The shared read/validate helpers for the model-catalog endpoint — a
1720
+ * separate instance so catalog boundary errors name `commandcode/models`,
1721
+ * not the report endpoint.
1722
+ */
1723
+ const { record: catalogRecord, stringField: catalogString } = makeBoundaryValidator("commandcode/models result:");
1598
1724
  /** Parse one untrusted boundary value into a {@link CommandCodeCatalogModel}. */
1599
1725
  function parseCatalogModel(value) {
1600
- const source = record(value, "model");
1601
- return {
1602
- id: stringField(source, "id", "model.id"),
1603
- name: stringField(source, "name", "model.name")
1726
+ const source = catalogRecord(value, "model");
1727
+ const model = {
1728
+ id: catalogString(source, "id", "model.id"),
1729
+ name: catalogString(source, "name", "model.name")
1604
1730
  };
1731
+ if (source.tier !== void 0) model.tier = catalogString(source, "tier", "model.tier");
1732
+ return model;
1605
1733
  }
1606
1734
  /** Parse the wire result into a {@link CommandCodeCatalog}. */
1607
1735
  function parseCatalog(value) {
1608
- const models = record(value, "result").models;
1736
+ const models = catalogRecord(value, "result").models;
1609
1737
  if (Array.isArray(models)) return { models: models.map(parseCatalogModel) };
1610
- return reject$1("models");
1738
+ throw new TypeError("commandcode/models result: invalid models");
1611
1739
  }
1612
1740
  /** The Client-face contribution for the model-catalog endpoint. */
1613
1741
  const MODELS_REMOTE_CONTRIBUTION = {
@@ -1631,45 +1759,29 @@ window.__ModuleLoader__.load({
1631
1759
  ];
1632
1760
  /** The shared read/validate helpers, prefixed with the login endpoint so
1633
1761
  * rejection messages name the offending boundary. */
1634
- /** Reject one boundary value with a field-naming error. A module-level
1635
- * function declaration (not the factory's destructured arrow) so TypeScript's
1636
- * control-flow analysis recognizes it as never-returning and narrows `state`
1637
- * after the guard below. */
1638
- function reject(field) {
1639
- throw new TypeError(`commandcode/login result: invalid ${field}`);
1640
- }
1762
+ const { reject, record, stringField } = makeBoundaryValidator("commandcode/login result:");
1641
1763
  /**
1642
1764
  * Parse one untrusted boundary value into a {@link CommandCodeLoginStatus}.
1643
1765
  * Every field is shape-checked so a malformed frame fails the boundary
1644
1766
  * instead of leaking into the page.
1645
1767
  */
1646
1768
  function parseLoginStatus(value) {
1647
- if (typeof value !== "object" || value === null || Array.isArray(value)) reject("status");
1648
- const source = value;
1769
+ const source = record(value, "status");
1649
1770
  const state = source.state;
1650
- if (state !== "idle" && state !== "waiting" && state !== "success" && state !== "failed") reject("state");
1651
- const status = { state };
1652
- if (source.authUrl !== void 0) {
1653
- if (typeof source.authUrl !== "string") reject("authUrl");
1654
- status.authUrl = source.authUrl;
1655
- }
1656
- if (source.userName !== void 0) {
1657
- if (typeof source.userName !== "string") reject("userName");
1658
- status.userName = source.userName;
1659
- }
1660
- if (source.keyName !== void 0) {
1661
- if (typeof source.keyName !== "string") reject("keyName");
1662
- status.keyName = source.keyName;
1663
- }
1664
- if (source.reason !== void 0) {
1665
- if (!REASONS.includes(source.reason)) reject("reason");
1666
- status.reason = source.reason;
1667
- }
1668
- if (source.message !== void 0) {
1669
- if (typeof source.message !== "string") reject("message");
1670
- status.message = source.message;
1771
+ if (state === "idle" || state === "waiting" || state === "success" || state === "failed") {
1772
+ const status = { state };
1773
+ if (source.authUrl !== void 0) status.authUrl = stringField(source, "authUrl", "authUrl");
1774
+ if (source.userName !== void 0) status.userName = stringField(source, "userName", "userName");
1775
+ if (source.keyName !== void 0) status.keyName = stringField(source, "keyName", "keyName");
1776
+ if (source.reason !== void 0) {
1777
+ const reason = source.reason;
1778
+ if (typeof reason === "string" && REASONS.includes(reason)) status.reason = reason;
1779
+ else reject("reason");
1780
+ }
1781
+ if (source.message !== void 0) status.message = stringField(source, "message", "message");
1782
+ return status;
1671
1783
  }
1672
- return status;
1784
+ return reject("state");
1673
1785
  }
1674
1786
  /** The strict result codec shared by all three login endpoints. */
1675
1787
  const loginStatusSchema = { parse: parseLoginStatus };
@@ -1735,8 +1847,128 @@ window.__ModuleLoader__.load({
1735
1847
  });
1736
1848
  }
1737
1849
  //#endregion
1850
+ //#region src/client/model-select.ts
1851
+ /**
1852
+ * Model-select helpers for the settings page's model editors (browser half).
1853
+ *
1854
+ * The routing-rule editor and the visible-models filter both pick catalog
1855
+ * models through the same checkbox multi-select dropdown (`ModelMultiSelect`
1856
+ * in section.tsx). The dropdown's data shaping — search filtering, stale-id
1857
+ * detection, tier grouping — lives here, React-free, so node tests can drive
1858
+ * it directly.
1859
+ *
1860
+ * Dependency-free by design: the client bundle may only import platform/seed
1861
+ * modules, so the plan snapshot below is a deliberately small vendored copy
1862
+ * (tier key → heading label) rather than an import of src/capabilities.ts.
1863
+ * When upstream adds a plan tier, extend BOTH tables.
1864
+ *
1865
+ * @module dsh-commandcode-provider/model-select
1866
+ */
1867
+ /**
1868
+ * Minimum plan tier → dropdown section heading. Mirrors the Host-side
1869
+ * `KNOWN_PLANS` values + `PLAN_LABELS` in src/capabilities.ts (kept as a
1870
+ * vendored copy because the client bundle cannot import host modules).
1871
+ * Covers every tier key `KNOWN_PLANS` uses today; an unknown tier key falls
1872
+ * back to the raw key rather than vanishing the row.
1873
+ */
1874
+ const TIER_HEADINGS = {
1875
+ go: "Go",
1876
+ goat: "GOAT",
1877
+ pro: "Pro",
1878
+ provider: "Provider",
1879
+ max: "Max"
1880
+ };
1881
+ /**
1882
+ * The dropdown section heading for a catalog model id, or undefined for
1883
+ * models outside the known plan tiers (unmapped models and stale ids render
1884
+ * unheaded). `knownPlans` is the Host-side `KNOWN_PLANS` table, threaded in
1885
+ * by the caller so this module stays dependency-free.
1886
+ */
1887
+ function tierHeadingFor(modelId, knownPlans) {
1888
+ const tier = knownPlans[modelId];
1889
+ if (tier === void 0) return void 0;
1890
+ return TIER_HEADINGS[tier] ?? tier;
1891
+ }
1892
+ /**
1893
+ * Whether `text` matches `query` as a case-insensitive substring over the
1894
+ * model id AND display name. An empty/blank query matches everything.
1895
+ */
1896
+ function matchesModelQuery(model, query) {
1897
+ const needle = query.trim().toLowerCase();
1898
+ if (needle === "") return true;
1899
+ return model.id.toLowerCase().includes(needle) || model.name.toLowerCase().includes(needle);
1900
+ }
1901
+ /**
1902
+ * Build the dropdown options: the catalog (already in picker order) plus
1903
+ * any selected ids the catalog no longer carries, flagged stale so the UI
1904
+ * can mark them — a saved selection never silently loses an entry, and the
1905
+ * user can see which ones went stale upstream.
1906
+ *
1907
+ * When `query` is non-blank, catalog rows are filtered by
1908
+ * {@link matchesModelQuery}; stale rows are kept only while they match too,
1909
+ * so a search for a live model does not surface unrelated stale ids.
1910
+ */
1911
+ function buildModelSelectOptions(catalog, selected, query = "") {
1912
+ const catalogIds = new Set(catalog.map((model) => model.id));
1913
+ const options = catalog.filter((model) => matchesModelQuery(model, query)).map((model) => ({
1914
+ value: model.id,
1915
+ label: model.name,
1916
+ stale: false
1917
+ }));
1918
+ const seen = new Set(catalogIds);
1919
+ for (const id of selected) {
1920
+ if (id === "" || seen.has(id)) continue;
1921
+ seen.add(id);
1922
+ if (catalogIds.has(id)) continue;
1923
+ if (!matchesModelQuery({
1924
+ id,
1925
+ name: id
1926
+ }, query)) continue;
1927
+ options.push({
1928
+ value: id,
1929
+ label: id,
1930
+ stale: true
1931
+ });
1932
+ }
1933
+ return options;
1934
+ }
1935
+ /**
1936
+ * Group dropdown options under plan-tier headings (`tierOf` maps a model id
1937
+ * to its tier heading, or undefined for unmapped models — see
1938
+ * {@link tierHeadingFor}). Live rows keep their relative order; stale ids
1939
+ * and unmapped live rows share one trailing unheaded group. Groups merge
1940
+ * repeats, so a catalog interleaving two tiers still renders one section
1941
+ * per tier.
1942
+ */
1943
+ function groupModelSelectOptions(options, tierOf) {
1944
+ const groups = [];
1945
+ const byHeading = /* @__PURE__ */ new Map();
1946
+ for (const option of options) {
1947
+ const heading = option.stale ? void 0 : tierOf(option.value);
1948
+ let group = byHeading.get(heading);
1949
+ if (group === void 0) {
1950
+ group = {
1951
+ heading,
1952
+ options: []
1953
+ };
1954
+ byHeading.set(heading, group);
1955
+ groups.push(group);
1956
+ }
1957
+ group.options.push(option);
1958
+ }
1959
+ return groups;
1960
+ }
1961
+ /**
1962
+ * Toggle one model id in a selection: remove it when present, append it
1963
+ * when absent (append keeps catalog order irrelevant — the picker re-sorts
1964
+ * by plan tier on render).
1965
+ */
1966
+ function toggleModelSelection(selected, modelId) {
1967
+ return selected.includes(modelId) ? selected.filter((value) => value !== modelId) : [...selected, modelId];
1968
+ }
1969
+ //#endregion
1738
1970
  //#region package.json
1739
- var version = "0.10.1";
1971
+ var version = "0.10.3";
1740
1972
  var repository = {
1741
1973
  "type": "git",
1742
1974
  "url": "git+https://github.com/Mars-Sea/dsh-commandcode-provider.git"
@@ -1759,8 +1991,13 @@ window.__ModuleLoader__.load({
1759
1991
  /**
1760
1992
  * This package's GitHub releases page, derived from the repository field so
1761
1993
  * the update hint's link target can never drift from the published home.
1994
+ * Tolerates both repository shapes (`{ url }` and the plain string form).
1762
1995
  */
1763
- const PLUGIN_RELEASES_URL = `${repository.url.replace(/^git\+/, "").replace(/\.git$/, "")}/releases`;
1996
+ const PLUGIN_RELEASES_URL = (() => {
1997
+ const repo = repository;
1998
+ const url = typeof repo === "string" ? repo : repo?.url;
1999
+ return `${typeof url === "string" ? url.replace(/^git\+/, "").replace(/\.git$/, "") : "https://github.com/Mars-Sea/dsh-commandcode-provider"}/releases`;
2000
+ })();
1764
2001
  /** Abort a hung registry request rather than keep the footer waiting. */
1765
2002
  const FETCH_TIMEOUT_MS = 5e3;
1766
2003
  /**
@@ -1812,7 +2049,10 @@ window.__ModuleLoader__.load({
1812
2049
  }
1813
2050
  /** Split a tolerant version string into numeric core + prerelease ids. */
1814
2051
  function splitVersion(value) {
1815
- const [coreText = "", preText] = value.trim().replace(/^v/i, "").split("-");
2052
+ const cleaned = value.trim().replace(/^v/i, "");
2053
+ const dash = cleaned.indexOf("-");
2054
+ const coreText = dash < 0 ? cleaned : cleaned.slice(0, dash);
2055
+ const preText = dash < 0 ? void 0 : cleaned.slice(dash + 1);
1816
2056
  return {
1817
2057
  core: coreText === "" ? [0] : coreText.split(".").map((part) => {
1818
2058
  const parsed = Number.parseInt(part, 10);
@@ -1951,6 +2191,7 @@ window.__ModuleLoader__.load({
1951
2191
  className: "cc-reset",
1952
2192
  disabled,
1953
2193
  onClick: onReset,
2194
+ "aria-label": `${label} — ${t("reset")}`,
1954
2195
  children: t("reset")
1955
2196
  })]
1956
2197
  })]
@@ -2005,7 +2246,11 @@ window.__ModuleLoader__.load({
2005
2246
  }),
2006
2247
  overridden > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2007
2248
  className: "cc-badge",
2008
- children: overridden === 1 ? t("advancedOverriddenOne") : t("advancedOverriddenMany").replace("{count}", String(overridden))
2249
+ children: overridden === 1 ? t("advancedOverriddenOne") : t("advancedOverriddenMany", { count: overridden })
2250
+ }) : null,
2251
+ !expanded && invalid ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2252
+ className: "cc-badge cc-badgeWarn",
2253
+ children: t("advancedInvalid")
2009
2254
  }) : null,
2010
2255
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "cc-advancedSpacer" }),
2011
2256
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -2108,9 +2353,9 @@ window.__ModuleLoader__.load({
2108
2353
  className: "cc-field",
2109
2354
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2110
2355
  className: "cc-fieldHead",
2111
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
2356
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2112
2357
  className: "cc-label",
2113
- htmlFor: id,
2358
+ "aria-hidden": "true",
2114
2359
  children: label
2115
2360
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2116
2361
  className: "cc-badges",
@@ -2122,6 +2367,7 @@ window.__ModuleLoader__.load({
2122
2367
  className: "cc-reset",
2123
2368
  disabled,
2124
2369
  onClick: onReset,
2370
+ "aria-label": `${label} — ${t("reset")}`,
2125
2371
  children: t("reset")
2126
2372
  })]
2127
2373
  })]
@@ -2251,6 +2497,7 @@ window.__ModuleLoader__.load({
2251
2497
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2252
2498
  className: "cc-usageBar",
2253
2499
  role: "progressbar",
2500
+ "aria-label": label,
2254
2501
  "aria-valuemin": 0,
2255
2502
  "aria-valuemax": 100,
2256
2503
  "aria-valuenow": Math.round(ratio * 100),
@@ -2368,7 +2615,7 @@ window.__ModuleLoader__.load({
2368
2615
  }),
2369
2616
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageStat, {
2370
2617
  label: t("usageSuccessRate"),
2371
- value: `${report.usage.successRate}%`
2618
+ value: `${formatSuccessRate(report.usage.successRate)}%`
2372
2619
  }),
2373
2620
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageStat, {
2374
2621
  label: t("usageCost"),
@@ -2456,7 +2703,10 @@ window.__ModuleLoader__.load({
2456
2703
  /** The status dot on an account tab: cooling/invalid warn, everything else ok. */
2457
2704
  function AccountTabDot({ entry }) {
2458
2705
  const cls = entry.mark === "invalid-credential" ? "cc-tabDot cc-tabDotError" : entry.mark !== "" || entry.cooldownUntil > 0 ? "cc-tabDot cc-tabDotWarn" : "cc-tabDot cc-tabDotOk";
2459
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: cls });
2706
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2707
+ className: cls,
2708
+ "aria-hidden": "true"
2709
+ });
2460
2710
  }
2461
2711
  /**
2462
2712
  * The account-usage card: the `/commandcode` dashboard's facts rendered as
@@ -2532,12 +2782,10 @@ window.__ModuleLoader__.load({
2532
2782
  }) : null,
2533
2783
  entries.length > 1 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2534
2784
  className: "cc-tabs",
2535
- role: "tablist",
2536
2785
  "aria-label": t("accountsTitle"),
2537
2786
  children: entries.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2538
2787
  type: "button",
2539
- role: "tab",
2540
- "aria-selected": selected?.id === entry.id,
2788
+ "aria-pressed": selected?.id === entry.id,
2541
2789
  className: selected?.id === entry.id ? "cc-tab cc-tabActive" : "cc-tab",
2542
2790
  onClick: () => setSelectedId(entry.id),
2543
2791
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountTabDot, { entry }), entry.label]
@@ -2780,6 +3028,7 @@ window.__ModuleLoader__.load({
2780
3028
  value: rule.account,
2781
3029
  disabled,
2782
3030
  onChange: (event) => onAccount(event.target.value),
3031
+ "aria-label": t("ruleAccount"),
2783
3032
  children: targets.map((target) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2784
3033
  value: target.value,
2785
3034
  children: target.label
@@ -2793,59 +3042,91 @@ window.__ModuleLoader__.load({
2793
3042
  });
2794
3043
  }
2795
3044
  /**
2796
- * A checkbox multi-select dropdown for routing-rule models. The trigger shows
2797
- * the selection count; the anchored Menu lists every catalog model with a
2798
- * checkbox, toggled by clicking the row. Selected ids the catalog no longer
2799
- * carries still render so a saved rule never silently loses a selection.
3045
+ * A checkbox multi-select dropdown for picking catalog models (the
3046
+ * routing-rule rows and the visible-models filter share it). The trigger
3047
+ * shows the selection count; the Menu lists every catalog model with a
3048
+ * checkbox, toggled by clicking the row. A search box under the trigger
3049
+ * (inside the Menu anchor, so focusing it never trips the outside-click
3050
+ * close) filters the list by id/display-name substring, and items group
3051
+ * under plan-tier headings in picker order. Selected ids the catalog no
3052
+ * longer carries still render — flagged stale — so a saved selection never
3053
+ * silently loses an entry, and the VisibleModelsCard offers a one-click
3054
+ * cleanup.
2800
3055
  */
2801
3056
  function ModelMultiSelect({ id, selected, catalog, disabled, t, onSelect }) {
2802
3057
  const [open, setOpen] = (0, react.useState)(false);
2803
- const options = [...catalog.map((model) => ({
2804
- value: model.id,
2805
- label: model.name
2806
- })), ...selected.filter((id) => !catalog.some((model) => model.id === id)).map((id) => ({
2807
- value: id,
2808
- label: id
2809
- }))];
2810
- const selectedSet = new Set(selected);
2811
- const items = options.map((option) => ({
3058
+ const [query, setQuery] = (0, react.useState)("");
3059
+ (0, react.useEffect)(() => {
3060
+ if (open) setQuery("");
3061
+ }, [open]);
3062
+ const tiers = (0, react.useMemo)(() => Object.fromEntries(catalog.flatMap((model) => model.tier === void 0 ? [] : [[model.id, model.tier]])), [catalog]);
3063
+ const options = buildModelSelectOptions(catalog, selected, query);
3064
+ const items = groupModelSelectOptions(options, (modelId) => tierHeadingFor(modelId, tiers)).flatMap((group) => [...group.heading === void 0 ? [] : [{
3065
+ type: "label",
3066
+ id: `cc-tier-${group.heading}`,
3067
+ text: group.heading
3068
+ }], ...group.options.map((option) => ({
2812
3069
  id: option.value,
2813
3070
  label: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2814
3071
  className: "cc-checkRow",
2815
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2816
- type: "checkbox",
2817
- className: "cc-check",
2818
- checked: selectedSet.has(option.value),
2819
- readOnly: true,
2820
- tabIndex: -1
2821
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2822
- className: "cc-checkName",
2823
- children: option.label
2824
- })]
3072
+ children: [
3073
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3074
+ type: "checkbox",
3075
+ className: "cc-check",
3076
+ checked: selected.includes(option.value),
3077
+ readOnly: true,
3078
+ tabIndex: -1
3079
+ }),
3080
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3081
+ className: "cc-checkName",
3082
+ children: option.label
3083
+ }),
3084
+ option.stale ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3085
+ className: "cc-badge",
3086
+ children: t("modelStale")
3087
+ }) : null
3088
+ ]
2825
3089
  })
2826
- }));
3090
+ }))]);
2827
3091
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
2828
3092
  open,
2829
3093
  onClose: () => setOpen(false),
2830
3094
  onSelect: (modelId) => {
2831
- onSelect(selectedSet.has(modelId) ? selected.filter((value) => value !== modelId) : [...selected, modelId]);
3095
+ onSelect(toggleModelSelection(selected, modelId));
2832
3096
  },
2833
3097
  selectedIds: selected,
2834
3098
  items,
3099
+ footer: options.length === 0 ? [{
3100
+ type: "label",
3101
+ id: "cc-model-search-empty",
3102
+ text: t("modelSearchEmpty")
3103
+ }] : [],
2835
3104
  portal: true,
2836
- anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2837
- id,
2838
- type: "button",
2839
- className: "cc-input cc-ruleTrigger",
2840
- disabled: disabled || catalog.length === 0,
2841
- onClick: () => setOpen((value) => !value),
2842
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2843
- className: "cc-ruleTriggerText",
2844
- children: selected.length === 0 ? t("ruleModelPick") : t("ruleModelCount", { count: selected.length })
2845
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2846
- className: "cc-ruleCaret",
2847
- "aria-hidden": "true"
2848
- })]
3105
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3106
+ className: "cc-modelSelectAnchor",
3107
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3108
+ id,
3109
+ type: "button",
3110
+ className: "cc-input cc-ruleTrigger",
3111
+ disabled: disabled || catalog.length === 0,
3112
+ onClick: () => setOpen((value) => !value),
3113
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3114
+ className: "cc-ruleTriggerText",
3115
+ children: selected.length === 0 ? t("ruleModelPick") : t("ruleModelCount", { count: selected.length })
3116
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3117
+ className: "cc-ruleCaret",
3118
+ "aria-hidden": "true"
3119
+ })]
3120
+ }), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3121
+ type: "search",
3122
+ className: "cc-input cc-modelSearch",
3123
+ placeholder: t("modelSearchPlaceholder"),
3124
+ "aria-label": t("modelSearchPlaceholder"),
3125
+ value: query,
3126
+ disabled,
3127
+ autoFocus: true,
3128
+ onChange: (event) => setQuery(event.target.value)
3129
+ }) : null]
2849
3130
  })
2850
3131
  });
2851
3132
  }
@@ -2901,6 +3182,68 @@ window.__ModuleLoader__.load({
2901
3182
  ]
2902
3183
  });
2903
3184
  }
3185
+ /** The visible-model filter card: an allowlist over the catalog. Empty = show all. */
3186
+ function VisibleModelsCard({ t, state, disabled, onSelect, onClear }) {
3187
+ const count = state.visibleModels.length;
3188
+ const pickT = (key, params) => {
3189
+ if (key === "ruleModelPick") return t("visibleModelsPick");
3190
+ if (key === "ruleModelCount") return t("visibleModelsCount", params);
3191
+ return t(key, params);
3192
+ };
3193
+ const catalogIds = new Set(state.catalogModels.map((model) => model.id));
3194
+ const staleIds = state.visibleModels.filter((id) => !catalogIds.has(id));
3195
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3196
+ className: "cc-card",
3197
+ "aria-label": t("visibleModelsTitle"),
3198
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3199
+ className: "cc-field",
3200
+ children: [
3201
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3202
+ className: "cc-fieldHead",
3203
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
3204
+ className: "cc-label",
3205
+ children: t("visibleModelsTitle")
3206
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3207
+ className: "cc-badges",
3208
+ children: [staleIds.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3209
+ type: "button",
3210
+ className: "cc-reset",
3211
+ disabled,
3212
+ onClick: () => onSelect(state.visibleModels.filter((id) => catalogIds.has(id))),
3213
+ children: t("visibleModelsCleanStale", { count: staleIds.length })
3214
+ }) : null, count > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3215
+ type: "button",
3216
+ className: "cc-reset",
3217
+ disabled,
3218
+ onClick: onClear,
3219
+ children: t("visibleModelsShowAll")
3220
+ }) : null]
3221
+ })]
3222
+ }),
3223
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3224
+ className: "cc-hint",
3225
+ children: t("visibleModelsHint")
3226
+ }),
3227
+ state.catalogFailed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3228
+ className: "cc-invalid",
3229
+ children: t("rulesCatalogFailed")
3230
+ }) : null,
3231
+ staleIds.length > 0 && !state.catalogFailed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3232
+ className: "cc-hint",
3233
+ children: t("visibleModelsStaleHint", { count: staleIds.length })
3234
+ }) : null,
3235
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMultiSelect, {
3236
+ id: "cc-visible-models",
3237
+ selected: state.visibleModels,
3238
+ catalog: state.catalogModels,
3239
+ disabled,
3240
+ t: pickT,
3241
+ onSelect
3242
+ })
3243
+ ]
3244
+ })
3245
+ });
3246
+ }
2904
3247
  /**
2905
3248
  * Show the "Saved ✓" affordance for a short window after each accepted save.
2906
3249
  * The controller only counts saves (`savedCount`); the flash timing lives
@@ -2997,6 +3340,13 @@ window.__ModuleLoader__.load({
2997
3340
  onModels: props.editRuleModels,
2998
3341
  onAccount: props.editRuleAccount
2999
3342
  }),
3343
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VisibleModelsCard, {
3344
+ t,
3345
+ state,
3346
+ disabled,
3347
+ onSelect: props.editVisibleModels,
3348
+ onClear: props.clearVisibleModels
3349
+ }),
3000
3350
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3001
3351
  className: "cc-card",
3002
3352
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SecretKeyField, {
@@ -3099,10 +3449,14 @@ window.__ModuleLoader__.load({
3099
3449
  }
3100
3450
  /** The closed-panel style: the outlet stays mounted as the detection anchor. */
3101
3451
  const HIDDEN_STYLE = { display: "none" };
3102
- /** Decide the card's posture from the injected face and the owner facts. */
3103
- function cardMode(props) {
3104
- if (props.useCommandCodeSettings === void 0) return { kind: "registration" };
3105
- const snapshot = props.useCommandCodeSettings((state) => state);
3452
+ /**
3453
+ * Decide the card's posture from one settings snapshot. Pure: the component
3454
+ * subscribes once and passes the snapshot in, so hook order never depends
3455
+ * on the registration→live transition. (The owner facts stay on the
3456
+ * component — only the snapshot decides the posture.)
3457
+ */
3458
+ function cardMode(snapshot) {
3459
+ if (snapshot === void 0) return { kind: "registration" };
3106
3460
  return {
3107
3461
  kind: "live",
3108
3462
  ready: snapshot.available,
@@ -3173,8 +3527,8 @@ window.__ModuleLoader__.load({
3173
3527
  */
3174
3528
  function CommandCodeProviderCard(props) {
3175
3529
  const { t } = props;
3176
- const mode = cardMode(props);
3177
3530
  const state = props.useCommandCodeSettings !== void 0 ? props.useCommandCodeSettings((snapshot) => snapshot) : void 0;
3531
+ const mode = cardMode(state);
3178
3532
  const login = props.useCommandCodeLogin !== void 0 ? props.useCommandCodeLogin((snapshot) => snapshot) : void 0;
3179
3533
  const dirty = state?.dirty ?? false;
3180
3534
  const saving = state?.saving ?? false;
@@ -3313,7 +3667,7 @@ window.__ModuleLoader__.load({
3313
3667
  filterModelsByPlan: "隐藏套餐外模型",
3314
3668
  filterModelsByPlanHint: "开启后,模型选择器只列出当前套餐可用的模型;账户持有按需余额时会显示全部。",
3315
3669
  webSearch: "用 Command Code 承载联网搜索",
3316
- webSearchHint: "开启后,dsh 的 web_search 工具由 Command Code 承担(复用同一个 API key 与地址);关闭则回退到 dsh 自带的 DeepSeek 搜索。",
3670
+ webSearchHint: "开启后,dsh 的 web_search 工具由 Command Code 承担(复用同一个 API key 与地址),并优先于其他搜索后端;关闭则把选择权交还给之前的后端(如 modsearch),而不是强制回退到 DeepSeek 搜索。",
3317
3671
  accountsTitle: "多账户轮换",
3318
3672
  accountsHint: "当前账户达到用量限额(429)或密钥失效(401)时,请求自动切换到下一个账户;全部耗尽时会提示最早的重置时间。",
3319
3673
  accountAdd: "添加账户",
@@ -3336,6 +3690,16 @@ window.__ModuleLoader__.load({
3336
3690
  ruleModelCount: "已选 {count} 个模型",
3337
3691
  ruleAccount: "目标账户",
3338
3692
  ruleHint: "从下拉列表勾选要路由的模型(可多选),再选择目标账户。",
3693
+ modelSearchPlaceholder: "搜索模型…",
3694
+ modelSearchEmpty: "没有匹配的模型。",
3695
+ modelStale: "已下架",
3696
+ visibleModelsTitle: "模型白名单",
3697
+ visibleModelsHint: "勾选要保留的模型,模型选择器就只列出这些;一个都不勾选时则显示全部模型。保存后,下次打开模型选择器生效。",
3698
+ visibleModelsPick: "选择要保留的模型…",
3699
+ visibleModelsCount: "已选 {count} 个模型",
3700
+ visibleModelsShowAll: "显示全部",
3701
+ visibleModelsStaleHint: "有 {count} 个已选模型在目录中找不到了(可能已下架),不影响其他模型;可清理或保留。",
3702
+ visibleModelsCleanStale: "清理失效({count})",
3339
3703
  overridden: "已覆盖",
3340
3704
  reset: "重置",
3341
3705
  invalidNumber: "无效数字",
@@ -3435,7 +3799,7 @@ window.__ModuleLoader__.load({
3435
3799
  filterModelsByPlan: "Hide out-of-plan models",
3436
3800
  filterModelsByPlanHint: "When on, the model picker lists only models your subscription includes; any on-demand credit balance shows the full catalog.",
3437
3801
  webSearch: "Serve dsh web search with Command Code",
3438
- webSearchHint: "When on, the model-facing web_search tool is backed by Command Code (same API key and base URL as chat). Off falls back to the shipped DeepSeek search.",
3802
+ webSearchHint: "When on, the model-facing web_search tool is backed by Command Code (same API key and base URL as chat), winning over other search backends. Off hands the selection back to the previous backend (e.g. modsearch) instead of forcing the shipped DeepSeek search.",
3439
3803
  accountsTitle: "Account rotation",
3440
3804
  accountsHint: "When the active account hits its usage limit (429) or its key fails (401), requests switch to the next account; when every account is exhausted the error names the earliest window reset.",
3441
3805
  accountAdd: "Add account",
@@ -3458,6 +3822,16 @@ window.__ModuleLoader__.load({
3458
3822
  ruleModelCount: "{count} model(s) selected",
3459
3823
  ruleAccount: "Target account",
3460
3824
  ruleHint: "Check the models to route from the dropdown (multi-select), then pick the target account.",
3825
+ modelSearchPlaceholder: "Search models…",
3826
+ modelSearchEmpty: "No matching models.",
3827
+ modelStale: "Retired",
3828
+ visibleModelsTitle: "Model allowlist",
3829
+ visibleModelsHint: "Check the models you want to keep, and model pickers will list only those. If nothing is checked, every model is shown. After saving, the change applies the next time you open a model picker.",
3830
+ visibleModelsPick: "Select models to keep…",
3831
+ visibleModelsCount: "{count} model(s) selected",
3832
+ visibleModelsShowAll: "Show all",
3833
+ visibleModelsStaleHint: "{count} selected model(s) are no longer in the catalog (possibly retired); other models are unaffected. Clean them up or keep them.",
3834
+ visibleModelsCleanStale: "Clean stale ({count})",
3461
3835
  overridden: "Overridden",
3462
3836
  reset: "Reset",
3463
3837
  invalidNumber: "Invalid number",
@@ -3567,6 +3941,13 @@ window.__ModuleLoader__.load({
3567
3941
  .cc-check:checked{background:var(--dsw-alias-brand-primary);border-color:var(--dsw-alias-brand-primary)}
3568
3942
  .cc-check:checked::after{content:'';position:absolute;top:2px;left:5px;width:3px;height:7px;border:solid #fff;border-width:0 1.5px 1.5px 0;transform:rotate(45deg)}
3569
3943
  .cc-checkName{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
3944
+ /* The model multi-select search box: stacked under the trigger while the
3945
+ * dropdown is open, same input sizing so the pair reads as one control. The
3946
+ * box lives inside the Menu anchor (which renders inside the Menu's root
3947
+ * span), so focusing/typing it never trips the Menu's outside-click close. */
3948
+ .cc-modelSelectAnchor{flex-direction:column;gap:6px;display:flex;width:100%}
3949
+ .cc-modelSearch{width:100%}
3950
+ .cc-modelSearch::-webkit-search-cancel-button{cursor:pointer}
3570
3951
  /* Selects need their own treatment to sit flush with the text inputs:
3571
3952
  * the UA stylesheet renders <select> border-box (34px total vs the inputs'
3572
3953
  * 36px) and forces its own menulist text metrics, so drop the native
@@ -3621,7 +4002,7 @@ select.cc-input{appearance:none;-webkit-appearance:none;-moz-appearance:none;box
3621
4002
  .cc-usageBar{overflow:hidden;background:var(--dsw-alias-bg-layer-1);border-radius:999px;height:6px}
3622
4003
  .cc-usageBarFill{background:var(--dsw-alias-brand-primary);border-radius:999px;height:100%;transition:width .3s ease}
3623
4004
  .cc-usageBarFillWarn{background:var(--dsw-alias-label-error)}
3624
- .cc-usageWindowReset{color:var(--dsw-alias-label-tertiary);margin:0;font-size:11px;line-height:1.5}
4005
+ @media (prefers-reduced-motion:reduce){.cc-chevron,.cc-toggle,.cc-toggle::after,.cc-usageBarFill{transition:none}}.cc-usageWindowReset{color:var(--dsw-alias-label-tertiary);margin:0;font-size:11px;line-height:1.5}
3625
4006
  .cc-usageMeta{align-items:center;gap:8px;display:flex}
3626
4007
  .cc-accountReport{flex-direction:column;gap:12px;display:flex}
3627
4008
  .cc-tabs{flex-wrap:wrap;gap:6px;display:flex}
@@ -3638,6 +4019,9 @@ select.cc-input{appearance:none;-webkit-appearance:none;-moz-appearance:none;box
3638
4019
  .cc-usageBlockedTitle{color:var(--dsw-alias-label-error);margin:0;font-size:13px;font-weight:600;line-height:1.5}
3639
4020
  .cc-usageBlockedHint{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px;line-height:1.5}
3640
4021
  .cc-version{margin:4px 0 0;text-align:center;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5}
4022
+ /* The Models-page provider panel root: unstyled by design — the row card the
4023
+ * Models page owns supplies the surface, the panel only stacks its controls. */
4024
+ .cc-providerCard{flex-direction:column;display:flex}
3641
4025
  /* The update hint rides the footer version line: warning-tinted (with a
3642
4026
  * muted fallback for themes without the alias), quiet until hovered. */
3643
4027
  .cc-versionLink{color:var(--dsw-alias-state-warning-primary,var(--dsw-alias-label-secondary));text-decoration:none}
@@ -3646,7 +4030,6 @@ select.cc-input{appearance:none;-webkit-appearance:none;-moz-appearance:none;box
3646
4030
  * authorization link is the only branded element on it. */
3647
4031
  .cc-loginLink{color:var(--dsw-alias-brand-primary);text-decoration:none;font-size:12px;line-height:1.5}
3648
4032
  .cc-loginLink:hover{text-decoration:underline;text-underline-position:under}
3649
- .cc-loginBusy{color:var(--dsw-alias-label-tertiary)}
3650
4033
  .cc-loginDone{color:var(--dsw-alias-state-success-primary,var(--dsw-alias-label-secondary))}
3651
4034
  .cc-loginError{color:var(--dsw-alias-label-error)}
3652
4035
  .cc-saved{color:var(--dsw-alias-state-success-primary,var(--dsw-alias-label-secondary));margin:0;font-size:12px;font-weight:500;line-height:1.5}
@@ -3822,7 +4205,9 @@ select.cc-input{appearance:none;-webkit-appearance:none;-moz-appearance:none;box
3822
4205
  addRule: () => controller.addRule(),
3823
4206
  removeRule: (id) => controller.removeRule(id),
3824
4207
  editRuleModels: (id, ids) => controller.editRuleModels(id, ids),
3825
- editRuleAccount: (id, text) => controller.editRuleAccount(id, text)
4208
+ editRuleAccount: (id, text) => controller.editRuleAccount(id, text),
4209
+ editVisibleModels: (ids) => controller.editVisibleModels(ids),
4210
+ clearVisibleModels: () => controller.clearVisibleModels()
3826
4211
  });
3827
4212
  ctx.slots.inject("settings.section", () => ctx.slots.register({
3828
4213
  name: "settings.section",