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

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;
@@ -1437,7 +1548,7 @@ window.__ModuleLoader__.load({
1437
1548
  /** The wire namespace all Command Code endpoints share. */
1438
1549
  const REMOTE_NAMESPACE = "commandcode";
1439
1550
  /**
1440
- * Build the validator helpers one endpoint uses Sequence. `prefix` names the
1551
+ * Build the validator helpers one endpoint uses. `prefix` names the
1441
1552
  * endpoint in the rejection message (e.g. `commandcode/report result:`), so
1442
1553
  * each wire file keeps its own diagnostic phrasing while sharing the helper
1443
1554
  * bodies.
@@ -1493,14 +1604,13 @@ window.__ModuleLoader__.load({
1493
1604
  /** Canonical `<namespace>/<method>` endpoint of the usage report Remote. */
1494
1605
  const USAGE_REPORT_ENDPOINT = "commandcode/report";
1495
1606
  /**
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).
1607
+ * The shared read/validate helpers for the usage report endpoint, prefixed
1608
+ * so rejection messages name the offending boundary.
1499
1609
  */
1500
- const { reject: reject$1, record, stringField, numberField, booleanField } = makeBoundaryValidator("commandcode/report result:");
1610
+ const { reject: reject$1, record: record$1, stringField: stringField$1, numberField, booleanField } = makeBoundaryValidator("commandcode/report result:");
1501
1611
  /** Validate one window-limit block (`fiveHour` / `weekly`). */
1502
1612
  function windowLimit(value, field) {
1503
- const source = record(value, field);
1613
+ const source = record$1(value, field);
1504
1614
  return {
1505
1615
  used: numberField(source, "used", `${field}.used`),
1506
1616
  cap: numberField(source, "cap", `${field}.cap`),
@@ -1514,7 +1624,7 @@ window.__ModuleLoader__.load({
1514
1624
  * malformed frame fails the boundary instead of rendering garbage.
1515
1625
  */
1516
1626
  function parseUsageReport(value) {
1517
- const source = record(value, "report");
1627
+ const source = record$1(value, "report");
1518
1628
  const failures = source.failures;
1519
1629
  if (!Array.isArray(failures) || failures.some((entry) => typeof entry !== "string")) reject$1("failures");
1520
1630
  const report = { failures };
@@ -1524,15 +1634,15 @@ window.__ModuleLoader__.load({
1524
1634
  else reject$1("blocked");
1525
1635
  }
1526
1636
  if (source.account !== void 0) {
1527
- const account = record(source.account, "account");
1637
+ const account = record$1(source.account, "account");
1528
1638
  report.account = {
1529
- id: stringField(account, "id", "account.id"),
1530
- name: stringField(account, "name", "account.name"),
1531
- userName: stringField(account, "userName", "account.userName")
1639
+ id: stringField$1(account, "id", "account.id"),
1640
+ name: stringField$1(account, "name", "account.name"),
1641
+ userName: stringField$1(account, "userName", "account.userName")
1532
1642
  };
1533
1643
  }
1534
1644
  if (source.usage !== void 0) {
1535
- const usage = record(source.usage, "usage");
1645
+ const usage = record$1(source.usage, "usage");
1536
1646
  report.usage = {
1537
1647
  totalCount: numberField(usage, "totalCount", "usage.totalCount"),
1538
1648
  totalCost: numberField(usage, "totalCost", "usage.totalCost"),
@@ -1542,11 +1652,11 @@ window.__ModuleLoader__.load({
1542
1652
  totalTokensIn: numberField(usage, "totalTokensIn", "usage.totalTokensIn"),
1543
1653
  totalTokensOut: numberField(usage, "totalTokensOut", "usage.totalTokensOut"),
1544
1654
  totalCredits: numberField(usage, "totalCredits", "usage.totalCredits"),
1545
- periodBasis: stringField(usage, "periodBasis", "usage.periodBasis")
1655
+ periodBasis: stringField$1(usage, "periodBasis", "usage.periodBasis")
1546
1656
  };
1547
1657
  }
1548
1658
  if (source.credits !== void 0) {
1549
- const credits = record(source.credits, "credits");
1659
+ const credits = record$1(source.credits, "credits");
1550
1660
  report.credits = {
1551
1661
  monthlyCredits: numberField(credits, "monthlyCredits", "credits.monthlyCredits"),
1552
1662
  purchasedCredits: numberField(credits, "purchasedCredits", "credits.purchasedCredits"),
@@ -1556,13 +1666,13 @@ window.__ModuleLoader__.load({
1556
1666
  };
1557
1667
  }
1558
1668
  if (source.plan !== void 0) {
1559
- const plan = record(source.plan, "plan");
1669
+ const plan = record$1(source.plan, "plan");
1560
1670
  const monthly = plan.monthlyCredits;
1561
1671
  if (monthly !== null && (typeof monthly !== "number" || !Number.isFinite(monthly))) reject$1("plan.monthlyCredits");
1562
1672
  report.plan = {
1563
- planId: stringField(plan, "planId", "plan.planId"),
1564
- name: stringField(plan, "name", "plan.name"),
1565
- status: stringField(plan, "status", "plan.status"),
1673
+ planId: stringField$1(plan, "planId", "plan.planId"),
1674
+ name: stringField$1(plan, "name", "plan.name"),
1675
+ status: stringField$1(plan, "status", "plan.status"),
1566
1676
  monthlyCredits: monthly,
1567
1677
  currentPeriodEnd: numberField(plan, "currentPeriodEnd", "plan.currentPeriodEnd")
1568
1678
  };
@@ -1571,20 +1681,20 @@ window.__ModuleLoader__.load({
1571
1681
  }
1572
1682
  /** Parse one untrusted boundary value into a {@link CommandCodeAccountUsage}. */
1573
1683
  function parseAccountUsage(value) {
1574
- const source = record(value, "account");
1684
+ const source = record$1(value, "account");
1575
1685
  return {
1576
- id: stringField(source, "id", "account.id"),
1577
- label: stringField(source, "label", "account.label"),
1686
+ id: stringField$1(source, "id", "account.id"),
1687
+ label: stringField$1(source, "label", "account.label"),
1578
1688
  configured: booleanField(source, "configured", "account.configured"),
1579
1689
  active: booleanField(source, "active", "account.active"),
1580
- mark: stringField(source, "mark", "account.mark"),
1690
+ mark: stringField$1(source, "mark", "account.mark"),
1581
1691
  cooldownUntil: numberField(source, "cooldownUntil", "account.cooldownUntil"),
1582
1692
  report: parseUsageReport(source.report)
1583
1693
  };
1584
1694
  }
1585
1695
  /** Parse the wire result into a {@link CommandCodeAccountsReport}. */
1586
1696
  function parseAccountsReport(value) {
1587
- const accounts = record(value, "result").accounts;
1697
+ const accounts = record$1(value, "result").accounts;
1588
1698
  if (Array.isArray(accounts)) return { accounts: accounts.map(parseAccountUsage) };
1589
1699
  return reject$1("accounts");
1590
1700
  }
@@ -1595,19 +1705,27 @@ window.__ModuleLoader__.load({
1595
1705
  };
1596
1706
  /** Canonical `<namespace>/<method>` endpoint of the model-catalog Remote. */
1597
1707
  const MODELS_ENDPOINT = "commandcode/models";
1708
+ /**
1709
+ * The shared read/validate helpers for the model-catalog endpoint — a
1710
+ * separate instance so catalog boundary errors name `commandcode/models`,
1711
+ * not the report endpoint.
1712
+ */
1713
+ const { record: catalogRecord, stringField: catalogString } = makeBoundaryValidator("commandcode/models result:");
1598
1714
  /** Parse one untrusted boundary value into a {@link CommandCodeCatalogModel}. */
1599
1715
  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")
1716
+ const source = catalogRecord(value, "model");
1717
+ const model = {
1718
+ id: catalogString(source, "id", "model.id"),
1719
+ name: catalogString(source, "name", "model.name")
1604
1720
  };
1721
+ if (source.tier !== void 0) model.tier = catalogString(source, "tier", "model.tier");
1722
+ return model;
1605
1723
  }
1606
1724
  /** Parse the wire result into a {@link CommandCodeCatalog}. */
1607
1725
  function parseCatalog(value) {
1608
- const models = record(value, "result").models;
1726
+ const models = catalogRecord(value, "result").models;
1609
1727
  if (Array.isArray(models)) return { models: models.map(parseCatalogModel) };
1610
- return reject$1("models");
1728
+ throw new TypeError("commandcode/models result: invalid models");
1611
1729
  }
1612
1730
  /** The Client-face contribution for the model-catalog endpoint. */
1613
1731
  const MODELS_REMOTE_CONTRIBUTION = {
@@ -1631,45 +1749,29 @@ window.__ModuleLoader__.load({
1631
1749
  ];
1632
1750
  /** The shared read/validate helpers, prefixed with the login endpoint so
1633
1751
  * 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
- }
1752
+ const { reject, record, stringField } = makeBoundaryValidator("commandcode/login result:");
1641
1753
  /**
1642
1754
  * Parse one untrusted boundary value into a {@link CommandCodeLoginStatus}.
1643
1755
  * Every field is shape-checked so a malformed frame fails the boundary
1644
1756
  * instead of leaking into the page.
1645
1757
  */
1646
1758
  function parseLoginStatus(value) {
1647
- if (typeof value !== "object" || value === null || Array.isArray(value)) reject("status");
1648
- const source = value;
1759
+ const source = record(value, "status");
1649
1760
  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;
1761
+ if (state === "idle" || state === "waiting" || state === "success" || state === "failed") {
1762
+ const status = { state };
1763
+ if (source.authUrl !== void 0) status.authUrl = stringField(source, "authUrl", "authUrl");
1764
+ if (source.userName !== void 0) status.userName = stringField(source, "userName", "userName");
1765
+ if (source.keyName !== void 0) status.keyName = stringField(source, "keyName", "keyName");
1766
+ if (source.reason !== void 0) {
1767
+ const reason = source.reason;
1768
+ if (typeof reason === "string" && REASONS.includes(reason)) status.reason = reason;
1769
+ else reject("reason");
1770
+ }
1771
+ if (source.message !== void 0) status.message = stringField(source, "message", "message");
1772
+ return status;
1671
1773
  }
1672
- return status;
1774
+ return reject("state");
1673
1775
  }
1674
1776
  /** The strict result codec shared by all three login endpoints. */
1675
1777
  const loginStatusSchema = { parse: parseLoginStatus };
@@ -1735,8 +1837,128 @@ window.__ModuleLoader__.load({
1735
1837
  });
1736
1838
  }
1737
1839
  //#endregion
1840
+ //#region src/client/model-select.ts
1841
+ /**
1842
+ * Model-select helpers for the settings page's model editors (browser half).
1843
+ *
1844
+ * The routing-rule editor and the visible-models filter both pick catalog
1845
+ * models through the same checkbox multi-select dropdown (`ModelMultiSelect`
1846
+ * in section.tsx). The dropdown's data shaping — search filtering, stale-id
1847
+ * detection, tier grouping — lives here, React-free, so node tests can drive
1848
+ * it directly.
1849
+ *
1850
+ * Dependency-free by design: the client bundle may only import platform/seed
1851
+ * modules, so the plan snapshot below is a deliberately small vendored copy
1852
+ * (tier key → heading label) rather than an import of src/capabilities.ts.
1853
+ * When upstream adds a plan tier, extend BOTH tables.
1854
+ *
1855
+ * @module dsh-commandcode-provider/model-select
1856
+ */
1857
+ /**
1858
+ * Minimum plan tier → dropdown section heading. Mirrors the Host-side
1859
+ * `KNOWN_PLANS` values + `PLAN_LABELS` in src/capabilities.ts (kept as a
1860
+ * vendored copy because the client bundle cannot import host modules).
1861
+ * Covers every tier key `KNOWN_PLANS` uses today; an unknown tier key falls
1862
+ * back to the raw key rather than vanishing the row.
1863
+ */
1864
+ const TIER_HEADINGS = {
1865
+ go: "Go",
1866
+ goat: "GOAT",
1867
+ pro: "Pro",
1868
+ provider: "Provider",
1869
+ max: "Max"
1870
+ };
1871
+ /**
1872
+ * The dropdown section heading for a catalog model id, or undefined for
1873
+ * models outside the known plan tiers (unmapped models and stale ids render
1874
+ * unheaded). `knownPlans` is the Host-side `KNOWN_PLANS` table, threaded in
1875
+ * by the caller so this module stays dependency-free.
1876
+ */
1877
+ function tierHeadingFor(modelId, knownPlans) {
1878
+ const tier = knownPlans[modelId];
1879
+ if (tier === void 0) return void 0;
1880
+ return TIER_HEADINGS[tier] ?? tier;
1881
+ }
1882
+ /**
1883
+ * Whether `text` matches `query` as a case-insensitive substring over the
1884
+ * model id AND display name. An empty/blank query matches everything.
1885
+ */
1886
+ function matchesModelQuery(model, query) {
1887
+ const needle = query.trim().toLowerCase();
1888
+ if (needle === "") return true;
1889
+ return model.id.toLowerCase().includes(needle) || model.name.toLowerCase().includes(needle);
1890
+ }
1891
+ /**
1892
+ * Build the dropdown options: the catalog (already in picker order) plus
1893
+ * any selected ids the catalog no longer carries, flagged stale so the UI
1894
+ * can mark them — a saved selection never silently loses an entry, and the
1895
+ * user can see which ones went stale upstream.
1896
+ *
1897
+ * When `query` is non-blank, catalog rows are filtered by
1898
+ * {@link matchesModelQuery}; stale rows are kept only while they match too,
1899
+ * so a search for a live model does not surface unrelated stale ids.
1900
+ */
1901
+ function buildModelSelectOptions(catalog, selected, query = "") {
1902
+ const catalogIds = new Set(catalog.map((model) => model.id));
1903
+ const options = catalog.filter((model) => matchesModelQuery(model, query)).map((model) => ({
1904
+ value: model.id,
1905
+ label: model.name,
1906
+ stale: false
1907
+ }));
1908
+ const seen = new Set(catalogIds);
1909
+ for (const id of selected) {
1910
+ if (id === "" || seen.has(id)) continue;
1911
+ seen.add(id);
1912
+ if (catalogIds.has(id)) continue;
1913
+ if (!matchesModelQuery({
1914
+ id,
1915
+ name: id
1916
+ }, query)) continue;
1917
+ options.push({
1918
+ value: id,
1919
+ label: id,
1920
+ stale: true
1921
+ });
1922
+ }
1923
+ return options;
1924
+ }
1925
+ /**
1926
+ * Group dropdown options under plan-tier headings (`tierOf` maps a model id
1927
+ * to its tier heading, or undefined for unmapped models — see
1928
+ * {@link tierHeadingFor}). Live rows keep their relative order; stale ids
1929
+ * and unmapped live rows share one trailing unheaded group. Groups merge
1930
+ * repeats, so a catalog interleaving two tiers still renders one section
1931
+ * per tier.
1932
+ */
1933
+ function groupModelSelectOptions(options, tierOf) {
1934
+ const groups = [];
1935
+ const byHeading = /* @__PURE__ */ new Map();
1936
+ for (const option of options) {
1937
+ const heading = option.stale ? void 0 : tierOf(option.value);
1938
+ let group = byHeading.get(heading);
1939
+ if (group === void 0) {
1940
+ group = {
1941
+ heading,
1942
+ options: []
1943
+ };
1944
+ byHeading.set(heading, group);
1945
+ groups.push(group);
1946
+ }
1947
+ group.options.push(option);
1948
+ }
1949
+ return groups;
1950
+ }
1951
+ /**
1952
+ * Toggle one model id in a selection: remove it when present, append it
1953
+ * when absent (append keeps catalog order irrelevant — the picker re-sorts
1954
+ * by plan tier on render).
1955
+ */
1956
+ function toggleModelSelection(selected, modelId) {
1957
+ return selected.includes(modelId) ? selected.filter((value) => value !== modelId) : [...selected, modelId];
1958
+ }
1959
+ //#endregion
1738
1960
  //#region package.json
1739
- var version = "0.10.1";
1961
+ var version = "0.10.2";
1740
1962
  var repository = {
1741
1963
  "type": "git",
1742
1964
  "url": "git+https://github.com/Mars-Sea/dsh-commandcode-provider.git"
@@ -1759,8 +1981,13 @@ window.__ModuleLoader__.load({
1759
1981
  /**
1760
1982
  * This package's GitHub releases page, derived from the repository field so
1761
1983
  * the update hint's link target can never drift from the published home.
1984
+ * Tolerates both repository shapes (`{ url }` and the plain string form).
1762
1985
  */
1763
- const PLUGIN_RELEASES_URL = `${repository.url.replace(/^git\+/, "").replace(/\.git$/, "")}/releases`;
1986
+ const PLUGIN_RELEASES_URL = (() => {
1987
+ const repo = repository;
1988
+ const url = typeof repo === "string" ? repo : repo?.url;
1989
+ return `${typeof url === "string" ? url.replace(/^git\+/, "").replace(/\.git$/, "") : "https://github.com/Mars-Sea/dsh-commandcode-provider"}/releases`;
1990
+ })();
1764
1991
  /** Abort a hung registry request rather than keep the footer waiting. */
1765
1992
  const FETCH_TIMEOUT_MS = 5e3;
1766
1993
  /**
@@ -1812,7 +2039,10 @@ window.__ModuleLoader__.load({
1812
2039
  }
1813
2040
  /** Split a tolerant version string into numeric core + prerelease ids. */
1814
2041
  function splitVersion(value) {
1815
- const [coreText = "", preText] = value.trim().replace(/^v/i, "").split("-");
2042
+ const cleaned = value.trim().replace(/^v/i, "");
2043
+ const dash = cleaned.indexOf("-");
2044
+ const coreText = dash < 0 ? cleaned : cleaned.slice(0, dash);
2045
+ const preText = dash < 0 ? void 0 : cleaned.slice(dash + 1);
1816
2046
  return {
1817
2047
  core: coreText === "" ? [0] : coreText.split(".").map((part) => {
1818
2048
  const parsed = Number.parseInt(part, 10);
@@ -1951,6 +2181,7 @@ window.__ModuleLoader__.load({
1951
2181
  className: "cc-reset",
1952
2182
  disabled,
1953
2183
  onClick: onReset,
2184
+ "aria-label": `${label} — ${t("reset")}`,
1954
2185
  children: t("reset")
1955
2186
  })]
1956
2187
  })]
@@ -2005,7 +2236,11 @@ window.__ModuleLoader__.load({
2005
2236
  }),
2006
2237
  overridden > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2007
2238
  className: "cc-badge",
2008
- children: overridden === 1 ? t("advancedOverriddenOne") : t("advancedOverriddenMany").replace("{count}", String(overridden))
2239
+ children: overridden === 1 ? t("advancedOverriddenOne") : t("advancedOverriddenMany", { count: overridden })
2240
+ }) : null,
2241
+ !expanded && invalid ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2242
+ className: "cc-badge cc-badgeWarn",
2243
+ children: t("advancedInvalid")
2009
2244
  }) : null,
2010
2245
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "cc-advancedSpacer" }),
2011
2246
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -2108,9 +2343,9 @@ window.__ModuleLoader__.load({
2108
2343
  className: "cc-field",
2109
2344
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2110
2345
  className: "cc-fieldHead",
2111
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
2346
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2112
2347
  className: "cc-label",
2113
- htmlFor: id,
2348
+ "aria-hidden": "true",
2114
2349
  children: label
2115
2350
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2116
2351
  className: "cc-badges",
@@ -2122,6 +2357,7 @@ window.__ModuleLoader__.load({
2122
2357
  className: "cc-reset",
2123
2358
  disabled,
2124
2359
  onClick: onReset,
2360
+ "aria-label": `${label} — ${t("reset")}`,
2125
2361
  children: t("reset")
2126
2362
  })]
2127
2363
  })]
@@ -2251,6 +2487,7 @@ window.__ModuleLoader__.load({
2251
2487
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2252
2488
  className: "cc-usageBar",
2253
2489
  role: "progressbar",
2490
+ "aria-label": label,
2254
2491
  "aria-valuemin": 0,
2255
2492
  "aria-valuemax": 100,
2256
2493
  "aria-valuenow": Math.round(ratio * 100),
@@ -2456,7 +2693,10 @@ window.__ModuleLoader__.load({
2456
2693
  /** The status dot on an account tab: cooling/invalid warn, everything else ok. */
2457
2694
  function AccountTabDot({ entry }) {
2458
2695
  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 });
2696
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2697
+ className: cls,
2698
+ "aria-hidden": "true"
2699
+ });
2460
2700
  }
2461
2701
  /**
2462
2702
  * The account-usage card: the `/commandcode` dashboard's facts rendered as
@@ -2532,12 +2772,10 @@ window.__ModuleLoader__.load({
2532
2772
  }) : null,
2533
2773
  entries.length > 1 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2534
2774
  className: "cc-tabs",
2535
- role: "tablist",
2536
2775
  "aria-label": t("accountsTitle"),
2537
2776
  children: entries.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2538
2777
  type: "button",
2539
- role: "tab",
2540
- "aria-selected": selected?.id === entry.id,
2778
+ "aria-pressed": selected?.id === entry.id,
2541
2779
  className: selected?.id === entry.id ? "cc-tab cc-tabActive" : "cc-tab",
2542
2780
  onClick: () => setSelectedId(entry.id),
2543
2781
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountTabDot, { entry }), entry.label]
@@ -2780,6 +3018,7 @@ window.__ModuleLoader__.load({
2780
3018
  value: rule.account,
2781
3019
  disabled,
2782
3020
  onChange: (event) => onAccount(event.target.value),
3021
+ "aria-label": t("ruleAccount"),
2783
3022
  children: targets.map((target) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2784
3023
  value: target.value,
2785
3024
  children: target.label
@@ -2793,59 +3032,91 @@ window.__ModuleLoader__.load({
2793
3032
  });
2794
3033
  }
2795
3034
  /**
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.
3035
+ * A checkbox multi-select dropdown for picking catalog models (the
3036
+ * routing-rule rows and the visible-models filter share it). The trigger
3037
+ * shows the selection count; the Menu lists every catalog model with a
3038
+ * checkbox, toggled by clicking the row. A search box under the trigger
3039
+ * (inside the Menu anchor, so focusing it never trips the outside-click
3040
+ * close) filters the list by id/display-name substring, and items group
3041
+ * under plan-tier headings in picker order. Selected ids the catalog no
3042
+ * longer carries still render — flagged stale — so a saved selection never
3043
+ * silently loses an entry, and the VisibleModelsCard offers a one-click
3044
+ * cleanup.
2800
3045
  */
2801
3046
  function ModelMultiSelect({ id, selected, catalog, disabled, t, onSelect }) {
2802
3047
  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) => ({
3048
+ const [query, setQuery] = (0, react.useState)("");
3049
+ (0, react.useEffect)(() => {
3050
+ if (open) setQuery("");
3051
+ }, [open]);
3052
+ const tiers = (0, react.useMemo)(() => Object.fromEntries(catalog.flatMap((model) => model.tier === void 0 ? [] : [[model.id, model.tier]])), [catalog]);
3053
+ const options = buildModelSelectOptions(catalog, selected, query);
3054
+ const items = groupModelSelectOptions(options, (modelId) => tierHeadingFor(modelId, tiers)).flatMap((group) => [...group.heading === void 0 ? [] : [{
3055
+ type: "label",
3056
+ id: `cc-tier-${group.heading}`,
3057
+ text: group.heading
3058
+ }], ...group.options.map((option) => ({
2812
3059
  id: option.value,
2813
3060
  label: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2814
3061
  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
- })]
3062
+ children: [
3063
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3064
+ type: "checkbox",
3065
+ className: "cc-check",
3066
+ checked: selected.includes(option.value),
3067
+ readOnly: true,
3068
+ tabIndex: -1
3069
+ }),
3070
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3071
+ className: "cc-checkName",
3072
+ children: option.label
3073
+ }),
3074
+ option.stale ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3075
+ className: "cc-badge",
3076
+ children: t("modelStale")
3077
+ }) : null
3078
+ ]
2825
3079
  })
2826
- }));
3080
+ }))]);
2827
3081
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
2828
3082
  open,
2829
3083
  onClose: () => setOpen(false),
2830
3084
  onSelect: (modelId) => {
2831
- onSelect(selectedSet.has(modelId) ? selected.filter((value) => value !== modelId) : [...selected, modelId]);
3085
+ onSelect(toggleModelSelection(selected, modelId));
2832
3086
  },
2833
3087
  selectedIds: selected,
2834
3088
  items,
3089
+ footer: options.length === 0 ? [{
3090
+ type: "label",
3091
+ id: "cc-model-search-empty",
3092
+ text: t("modelSearchEmpty")
3093
+ }] : [],
2835
3094
  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
- })]
3095
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3096
+ className: "cc-modelSelectAnchor",
3097
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3098
+ id,
3099
+ type: "button",
3100
+ className: "cc-input cc-ruleTrigger",
3101
+ disabled: disabled || catalog.length === 0,
3102
+ onClick: () => setOpen((value) => !value),
3103
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3104
+ className: "cc-ruleTriggerText",
3105
+ children: selected.length === 0 ? t("ruleModelPick") : t("ruleModelCount", { count: selected.length })
3106
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3107
+ className: "cc-ruleCaret",
3108
+ "aria-hidden": "true"
3109
+ })]
3110
+ }), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3111
+ type: "search",
3112
+ className: "cc-input cc-modelSearch",
3113
+ placeholder: t("modelSearchPlaceholder"),
3114
+ "aria-label": t("modelSearchPlaceholder"),
3115
+ value: query,
3116
+ disabled,
3117
+ autoFocus: true,
3118
+ onChange: (event) => setQuery(event.target.value)
3119
+ }) : null]
2849
3120
  })
2850
3121
  });
2851
3122
  }
@@ -2901,6 +3172,68 @@ window.__ModuleLoader__.load({
2901
3172
  ]
2902
3173
  });
2903
3174
  }
3175
+ /** The visible-model filter card: an allowlist over the catalog. Empty = show all. */
3176
+ function VisibleModelsCard({ t, state, disabled, onSelect, onClear }) {
3177
+ const count = state.visibleModels.length;
3178
+ const pickT = (key, params) => {
3179
+ if (key === "ruleModelPick") return t("visibleModelsPick");
3180
+ if (key === "ruleModelCount") return t("visibleModelsCount", params);
3181
+ return t(key, params);
3182
+ };
3183
+ const catalogIds = new Set(state.catalogModels.map((model) => model.id));
3184
+ const staleIds = state.visibleModels.filter((id) => !catalogIds.has(id));
3185
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3186
+ className: "cc-card",
3187
+ "aria-label": t("visibleModelsTitle"),
3188
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3189
+ className: "cc-field",
3190
+ children: [
3191
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3192
+ className: "cc-fieldHead",
3193
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
3194
+ className: "cc-label",
3195
+ children: t("visibleModelsTitle")
3196
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3197
+ className: "cc-badges",
3198
+ children: [staleIds.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3199
+ type: "button",
3200
+ className: "cc-reset",
3201
+ disabled,
3202
+ onClick: () => onSelect(state.visibleModels.filter((id) => catalogIds.has(id))),
3203
+ children: t("visibleModelsCleanStale", { count: staleIds.length })
3204
+ }) : null, count > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3205
+ type: "button",
3206
+ className: "cc-reset",
3207
+ disabled,
3208
+ onClick: onClear,
3209
+ children: t("visibleModelsShowAll")
3210
+ }) : null]
3211
+ })]
3212
+ }),
3213
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3214
+ className: "cc-hint",
3215
+ children: t("visibleModelsHint")
3216
+ }),
3217
+ state.catalogFailed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3218
+ className: "cc-invalid",
3219
+ children: t("rulesCatalogFailed")
3220
+ }) : null,
3221
+ staleIds.length > 0 && !state.catalogFailed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3222
+ className: "cc-hint",
3223
+ children: t("visibleModelsStaleHint", { count: staleIds.length })
3224
+ }) : null,
3225
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMultiSelect, {
3226
+ id: "cc-visible-models",
3227
+ selected: state.visibleModels,
3228
+ catalog: state.catalogModels,
3229
+ disabled,
3230
+ t: pickT,
3231
+ onSelect
3232
+ })
3233
+ ]
3234
+ })
3235
+ });
3236
+ }
2904
3237
  /**
2905
3238
  * Show the "Saved ✓" affordance for a short window after each accepted save.
2906
3239
  * The controller only counts saves (`savedCount`); the flash timing lives
@@ -2997,6 +3330,13 @@ window.__ModuleLoader__.load({
2997
3330
  onModels: props.editRuleModels,
2998
3331
  onAccount: props.editRuleAccount
2999
3332
  }),
3333
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VisibleModelsCard, {
3334
+ t,
3335
+ state,
3336
+ disabled,
3337
+ onSelect: props.editVisibleModels,
3338
+ onClear: props.clearVisibleModels
3339
+ }),
3000
3340
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3001
3341
  className: "cc-card",
3002
3342
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SecretKeyField, {
@@ -3099,10 +3439,14 @@ window.__ModuleLoader__.load({
3099
3439
  }
3100
3440
  /** The closed-panel style: the outlet stays mounted as the detection anchor. */
3101
3441
  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);
3442
+ /**
3443
+ * Decide the card's posture from one settings snapshot. Pure: the component
3444
+ * subscribes once and passes the snapshot in, so hook order never depends
3445
+ * on the registration→live transition. (The owner facts stay on the
3446
+ * component — only the snapshot decides the posture.)
3447
+ */
3448
+ function cardMode(snapshot) {
3449
+ if (snapshot === void 0) return { kind: "registration" };
3106
3450
  return {
3107
3451
  kind: "live",
3108
3452
  ready: snapshot.available,
@@ -3173,8 +3517,8 @@ window.__ModuleLoader__.load({
3173
3517
  */
3174
3518
  function CommandCodeProviderCard(props) {
3175
3519
  const { t } = props;
3176
- const mode = cardMode(props);
3177
3520
  const state = props.useCommandCodeSettings !== void 0 ? props.useCommandCodeSettings((snapshot) => snapshot) : void 0;
3521
+ const mode = cardMode(state);
3178
3522
  const login = props.useCommandCodeLogin !== void 0 ? props.useCommandCodeLogin((snapshot) => snapshot) : void 0;
3179
3523
  const dirty = state?.dirty ?? false;
3180
3524
  const saving = state?.saving ?? false;
@@ -3313,7 +3657,7 @@ window.__ModuleLoader__.load({
3313
3657
  filterModelsByPlan: "隐藏套餐外模型",
3314
3658
  filterModelsByPlanHint: "开启后,模型选择器只列出当前套餐可用的模型;账户持有按需余额时会显示全部。",
3315
3659
  webSearch: "用 Command Code 承载联网搜索",
3316
- webSearchHint: "开启后,dsh 的 web_search 工具由 Command Code 承担(复用同一个 API key 与地址);关闭则回退到 dsh 自带的 DeepSeek 搜索。",
3660
+ webSearchHint: "开启后,dsh 的 web_search 工具由 Command Code 承担(复用同一个 API key 与地址),并优先于其他搜索后端;关闭则把选择权交还给之前的后端(如 modsearch),而不是强制回退到 DeepSeek 搜索。",
3317
3661
  accountsTitle: "多账户轮换",
3318
3662
  accountsHint: "当前账户达到用量限额(429)或密钥失效(401)时,请求自动切换到下一个账户;全部耗尽时会提示最早的重置时间。",
3319
3663
  accountAdd: "添加账户",
@@ -3336,6 +3680,16 @@ window.__ModuleLoader__.load({
3336
3680
  ruleModelCount: "已选 {count} 个模型",
3337
3681
  ruleAccount: "目标账户",
3338
3682
  ruleHint: "从下拉列表勾选要路由的模型(可多选),再选择目标账户。",
3683
+ modelSearchPlaceholder: "搜索模型…",
3684
+ modelSearchEmpty: "没有匹配的模型。",
3685
+ modelStale: "已下架",
3686
+ visibleModelsTitle: "显示的模型",
3687
+ visibleModelsHint: "只在模型选择器中显示勾选的模型;不勾选则显示全部。保存后下次打开选择器即生效。",
3688
+ visibleModelsPick: "选择要显示的模型…",
3689
+ visibleModelsCount: "已选 {count} 个模型",
3690
+ visibleModelsShowAll: "显示全部",
3691
+ visibleModelsStaleHint: "有 {count} 个已选模型在目录中找不到了(可能已下架),不影响其他模型;可清理或保留。",
3692
+ visibleModelsCleanStale: "清理失效({count})",
3339
3693
  overridden: "已覆盖",
3340
3694
  reset: "重置",
3341
3695
  invalidNumber: "无效数字",
@@ -3435,7 +3789,7 @@ window.__ModuleLoader__.load({
3435
3789
  filterModelsByPlan: "Hide out-of-plan models",
3436
3790
  filterModelsByPlanHint: "When on, the model picker lists only models your subscription includes; any on-demand credit balance shows the full catalog.",
3437
3791
  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.",
3792
+ 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
3793
  accountsTitle: "Account rotation",
3440
3794
  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
3795
  accountAdd: "Add account",
@@ -3458,6 +3812,16 @@ window.__ModuleLoader__.load({
3458
3812
  ruleModelCount: "{count} model(s) selected",
3459
3813
  ruleAccount: "Target account",
3460
3814
  ruleHint: "Check the models to route from the dropdown (multi-select), then pick the target account.",
3815
+ modelSearchPlaceholder: "Search models…",
3816
+ modelSearchEmpty: "No matching models.",
3817
+ modelStale: "Retired",
3818
+ visibleModelsTitle: "Visible models",
3819
+ visibleModelsHint: "Show only the checked models in model pickers; unchecked shows all. Applies the next time a picker opens after saving.",
3820
+ visibleModelsPick: "Select models to show…",
3821
+ visibleModelsCount: "{count} model(s) selected",
3822
+ visibleModelsShowAll: "Show all",
3823
+ visibleModelsStaleHint: "{count} selected model(s) are no longer in the catalog (possibly retired); other models are unaffected. Clean them up or keep them.",
3824
+ visibleModelsCleanStale: "Clean stale ({count})",
3461
3825
  overridden: "Overridden",
3462
3826
  reset: "Reset",
3463
3827
  invalidNumber: "Invalid number",
@@ -3567,6 +3931,13 @@ window.__ModuleLoader__.load({
3567
3931
  .cc-check:checked{background:var(--dsw-alias-brand-primary);border-color:var(--dsw-alias-brand-primary)}
3568
3932
  .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
3933
  .cc-checkName{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
3934
+ /* The model multi-select search box: stacked under the trigger while the
3935
+ * dropdown is open, same input sizing so the pair reads as one control. The
3936
+ * box lives inside the Menu anchor (which renders inside the Menu's root
3937
+ * span), so focusing/typing it never trips the Menu's outside-click close. */
3938
+ .cc-modelSelectAnchor{flex-direction:column;gap:6px;display:flex;width:100%}
3939
+ .cc-modelSearch{width:100%}
3940
+ .cc-modelSearch::-webkit-search-cancel-button{cursor:pointer}
3570
3941
  /* Selects need their own treatment to sit flush with the text inputs:
3571
3942
  * the UA stylesheet renders <select> border-box (34px total vs the inputs'
3572
3943
  * 36px) and forces its own menulist text metrics, so drop the native
@@ -3621,7 +3992,7 @@ select.cc-input{appearance:none;-webkit-appearance:none;-moz-appearance:none;box
3621
3992
  .cc-usageBar{overflow:hidden;background:var(--dsw-alias-bg-layer-1);border-radius:999px;height:6px}
3622
3993
  .cc-usageBarFill{background:var(--dsw-alias-brand-primary);border-radius:999px;height:100%;transition:width .3s ease}
3623
3994
  .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}
3995
+ @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
3996
  .cc-usageMeta{align-items:center;gap:8px;display:flex}
3626
3997
  .cc-accountReport{flex-direction:column;gap:12px;display:flex}
3627
3998
  .cc-tabs{flex-wrap:wrap;gap:6px;display:flex}
@@ -3638,6 +4009,9 @@ select.cc-input{appearance:none;-webkit-appearance:none;-moz-appearance:none;box
3638
4009
  .cc-usageBlockedTitle{color:var(--dsw-alias-label-error);margin:0;font-size:13px;font-weight:600;line-height:1.5}
3639
4010
  .cc-usageBlockedHint{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px;line-height:1.5}
3640
4011
  .cc-version{margin:4px 0 0;text-align:center;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5}
4012
+ /* The Models-page provider panel root: unstyled by design — the row card the
4013
+ * Models page owns supplies the surface, the panel only stacks its controls. */
4014
+ .cc-providerCard{flex-direction:column;display:flex}
3641
4015
  /* The update hint rides the footer version line: warning-tinted (with a
3642
4016
  * muted fallback for themes without the alias), quiet until hovered. */
3643
4017
  .cc-versionLink{color:var(--dsw-alias-state-warning-primary,var(--dsw-alias-label-secondary));text-decoration:none}
@@ -3646,7 +4020,6 @@ select.cc-input{appearance:none;-webkit-appearance:none;-moz-appearance:none;box
3646
4020
  * authorization link is the only branded element on it. */
3647
4021
  .cc-loginLink{color:var(--dsw-alias-brand-primary);text-decoration:none;font-size:12px;line-height:1.5}
3648
4022
  .cc-loginLink:hover{text-decoration:underline;text-underline-position:under}
3649
- .cc-loginBusy{color:var(--dsw-alias-label-tertiary)}
3650
4023
  .cc-loginDone{color:var(--dsw-alias-state-success-primary,var(--dsw-alias-label-secondary))}
3651
4024
  .cc-loginError{color:var(--dsw-alias-label-error)}
3652
4025
  .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 +4195,9 @@ select.cc-input{appearance:none;-webkit-appearance:none;-moz-appearance:none;box
3822
4195
  addRule: () => controller.addRule(),
3823
4196
  removeRule: (id) => controller.removeRule(id),
3824
4197
  editRuleModels: (id, ids) => controller.editRuleModels(id, ids),
3825
- editRuleAccount: (id, text) => controller.editRuleAccount(id, text)
4198
+ editRuleAccount: (id, text) => controller.editRuleAccount(id, text),
4199
+ editVisibleModels: (ids) => controller.editVisibleModels(ids),
4200
+ clearVisibleModels: () => controller.clearVisibleModels()
3826
4201
  });
3827
4202
  ctx.slots.inject("settings.section", () => ctx.slots.register({
3828
4203
  name: "settings.section",