@eddyskywalker/dsh-chatgpt-subscription 0.12.6 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/CHANGELOG.md +637 -619
  2. package/lib/client.js +3975 -4521
  3. package/lib/client.js.map +1 -1
  4. package/lib/index.js +1146 -299
  5. package/lib/types/client/CodexComposerQuota.d.ts.map +1 -1
  6. package/lib/types/client/CodexSubscriptionSection.d.ts +1 -3
  7. package/lib/types/client/CodexSubscriptionSection.d.ts.map +1 -1
  8. package/lib/types/client/ProviderHubSection.d.ts +6 -5
  9. package/lib/types/client/ProviderHubSection.d.ts.map +1 -1
  10. package/lib/types/client/antigravity/AntigravityComposerQuota.d.ts +22 -1
  11. package/lib/types/client/antigravity/AntigravityComposerQuota.d.ts.map +1 -1
  12. package/lib/types/client/antigravity/AntigravitySection.d.ts +0 -2
  13. package/lib/types/client/antigravity/AntigravitySection.d.ts.map +1 -1
  14. package/lib/types/client/antigravity/locales.d.ts +33 -26
  15. package/lib/types/client/antigravity/locales.d.ts.map +1 -1
  16. package/lib/types/client/antigravity/styles.d.ts.map +1 -1
  17. package/lib/types/client/claude/ClaudeComposerQuota.d.ts +3 -7
  18. package/lib/types/client/claude/ClaudeComposerQuota.d.ts.map +1 -1
  19. package/lib/types/client/claude/ClaudeSection.d.ts +10 -9
  20. package/lib/types/client/claude/ClaudeSection.d.ts.map +1 -1
  21. package/lib/types/client/claude/locales.d.ts +29 -29
  22. package/lib/types/client/claude/locales.d.ts.map +1 -1
  23. package/lib/types/client/claude/useClaudeSection.d.ts +93 -0
  24. package/lib/types/client/claude/useClaudeSection.d.ts.map +1 -0
  25. package/lib/types/client/command-code/CommandCodeComposerQuota.d.ts +3 -7
  26. package/lib/types/client/command-code/CommandCodeComposerQuota.d.ts.map +1 -1
  27. package/lib/types/client/command-code/CommandCodeSection.d.ts +0 -3
  28. package/lib/types/client/command-code/CommandCodeSection.d.ts.map +1 -1
  29. package/lib/types/client/command-code/locales.d.ts +32 -29
  30. package/lib/types/client/command-code/locales.d.ts.map +1 -1
  31. package/lib/types/client/common/AccountPoolSection.d.ts +16 -4
  32. package/lib/types/client/common/AccountPoolSection.d.ts.map +1 -1
  33. package/lib/types/client/common/ComposerQuotaBadge.d.ts +35 -0
  34. package/lib/types/client/common/ComposerQuotaBadge.d.ts.map +1 -0
  35. package/lib/types/client/common/ContextWindowEditor.d.ts +69 -0
  36. package/lib/types/client/common/ContextWindowEditor.d.ts.map +1 -0
  37. package/lib/types/client/common/ModelChecklist.d.ts +29 -0
  38. package/lib/types/client/common/ModelChecklist.d.ts.map +1 -0
  39. package/lib/types/client/common/account-pool-labels.d.ts +18 -2
  40. package/lib/types/client/common/account-pool-labels.d.ts.map +1 -1
  41. package/lib/types/client/common/account-quota.d.ts +60 -0
  42. package/lib/types/client/common/account-quota.d.ts.map +1 -0
  43. package/lib/types/client/common/format.d.ts +30 -0
  44. package/lib/types/client/common/format.d.ts.map +1 -0
  45. package/lib/types/client/common/line-api.d.ts +8 -0
  46. package/lib/types/client/common/line-api.d.ts.map +1 -0
  47. package/lib/types/client/common/model-checklist-styles.d.ts +2 -0
  48. package/lib/types/client/common/model-checklist-styles.d.ts.map +1 -0
  49. package/lib/types/client/common/styles.d.ts.map +1 -1
  50. package/lib/types/client/common/use-store.d.ts +3 -0
  51. package/lib/types/client/common/use-store.d.ts.map +1 -0
  52. package/lib/types/client/hub/HubOverview.d.ts +14 -0
  53. package/lib/types/client/hub/HubOverview.d.ts.map +1 -0
  54. package/lib/types/client/hub/brand-icons.d.ts +46 -0
  55. package/lib/types/client/hub/brand-icons.d.ts.map +1 -0
  56. package/lib/types/client/hub/brand-svg.d.ts +51 -0
  57. package/lib/types/client/hub/brand-svg.d.ts.map +1 -0
  58. package/lib/types/client/hub/hub-styles.d.ts +2 -0
  59. package/lib/types/client/hub/hub-styles.d.ts.map +1 -0
  60. package/lib/types/client/hub/providers.d.ts +38 -0
  61. package/lib/types/client/hub/providers.d.ts.map +1 -0
  62. package/lib/types/client/index.d.ts +20 -6
  63. package/lib/types/client/index.d.ts.map +1 -1
  64. package/lib/types/client/kimi-code/KimiCodeComposerQuota.d.ts +3 -7
  65. package/lib/types/client/kimi-code/KimiCodeComposerQuota.d.ts.map +1 -1
  66. package/lib/types/client/kimi-code/KimiCodeSection.d.ts +0 -3
  67. package/lib/types/client/kimi-code/KimiCodeSection.d.ts.map +1 -1
  68. package/lib/types/client/kimi-code/locales.d.ts +32 -32
  69. package/lib/types/client/kimi-code/locales.d.ts.map +1 -1
  70. package/lib/types/client/kimi-code/styles.d.ts.map +1 -1
  71. package/lib/types/client/locales.d.ts +45 -35
  72. package/lib/types/client/locales.d.ts.map +1 -1
  73. package/lib/types/client/minimax-code/MinimaxCodeComposerQuota.d.ts +4 -8
  74. package/lib/types/client/minimax-code/MinimaxCodeComposerQuota.d.ts.map +1 -1
  75. package/lib/types/client/minimax-code/MinimaxCodeSection.d.ts +8 -37
  76. package/lib/types/client/minimax-code/MinimaxCodeSection.d.ts.map +1 -1
  77. package/lib/types/client/minimax-code/api.d.ts +3 -3
  78. package/lib/types/client/minimax-code/api.d.ts.map +1 -1
  79. package/lib/types/client/minimax-code/locales.d.ts +29 -56
  80. package/lib/types/client/minimax-code/locales.d.ts.map +1 -1
  81. package/lib/types/client/minimax-code/useMinimaxCodeSection.d.ts +115 -0
  82. package/lib/types/client/minimax-code/useMinimaxCodeSection.d.ts.map +1 -0
  83. package/lib/types/client/ollama/OllamaSection.d.ts +2 -2
  84. package/lib/types/client/ollama/OllamaSection.d.ts.map +1 -1
  85. package/lib/types/client/ollama/locales.d.ts +54 -24
  86. package/lib/types/client/ollama/locales.d.ts.map +1 -1
  87. package/lib/types/client/styles.d.ts.map +1 -1
  88. package/lib/types/client/workbuddy/WorkBuddyComposerQuota.d.ts +3 -7
  89. package/lib/types/client/workbuddy/WorkBuddyComposerQuota.d.ts.map +1 -1
  90. package/lib/types/client/workbuddy/WorkBuddySection.d.ts +10 -37
  91. package/lib/types/client/workbuddy/WorkBuddySection.d.ts.map +1 -1
  92. package/lib/types/client/workbuddy/locales.d.ts +0 -66
  93. package/lib/types/client/workbuddy/locales.d.ts.map +1 -1
  94. package/lib/types/client/workbuddy/useWorkBuddySection.d.ts +141 -0
  95. package/lib/types/client/workbuddy/useWorkBuddySection.d.ts.map +1 -0
  96. package/lib/types/host/antigravity/account-pool.d.ts +18 -1
  97. package/lib/types/host/antigravity/account-pool.d.ts.map +1 -1
  98. package/lib/types/host/antigravity/client.d.ts.map +1 -1
  99. package/lib/types/host/claude/account-pool.d.ts +22 -2
  100. package/lib/types/host/claude/account-pool.d.ts.map +1 -1
  101. package/lib/types/host/claude/client.d.ts +37 -5
  102. package/lib/types/host/claude/client.d.ts.map +1 -1
  103. package/lib/types/host/claude/routes.d.ts.map +1 -1
  104. package/lib/types/host/codex-account-pool.d.ts +26 -1
  105. package/lib/types/host/codex-account-pool.d.ts.map +1 -1
  106. package/lib/types/host/command-code/account-pool.d.ts +13 -1
  107. package/lib/types/host/command-code/account-pool.d.ts.map +1 -1
  108. package/lib/types/host/command-code/client.d.ts +2 -1
  109. package/lib/types/host/command-code/client.d.ts.map +1 -1
  110. package/lib/types/host/common/account-quota.d.ts +44 -0
  111. package/lib/types/host/common/account-quota.d.ts.map +1 -0
  112. package/lib/types/host/common/catalog-snapshot.d.ts +9 -0
  113. package/lib/types/host/common/catalog-snapshot.d.ts.map +1 -1
  114. package/lib/types/host/common/provider-route.d.ts +56 -0
  115. package/lib/types/host/common/provider-route.d.ts.map +1 -0
  116. package/lib/types/host/hub-overview.d.ts +26 -0
  117. package/lib/types/host/hub-overview.d.ts.map +1 -0
  118. package/lib/types/host/kimi-code/account-pool.d.ts +24 -1
  119. package/lib/types/host/kimi-code/account-pool.d.ts.map +1 -1
  120. package/lib/types/host/kimi-code/client.d.ts +23 -2
  121. package/lib/types/host/kimi-code/client.d.ts.map +1 -1
  122. package/lib/types/host/minimax-code/account-pool.d.ts +34 -2
  123. package/lib/types/host/minimax-code/account-pool.d.ts.map +1 -1
  124. package/lib/types/host/minimax-code/client.d.ts +23 -0
  125. package/lib/types/host/minimax-code/client.d.ts.map +1 -1
  126. package/lib/types/host/minimax-code/routes.d.ts.map +1 -1
  127. package/lib/types/host/oauth-service.d.ts +16 -0
  128. package/lib/types/host/oauth-service.d.ts.map +1 -1
  129. package/lib/types/host/usage-service.d.ts +27 -2
  130. package/lib/types/host/usage-service.d.ts.map +1 -1
  131. package/lib/types/host/workbuddy/account-pool.d.ts +13 -1
  132. package/lib/types/host/workbuddy/account-pool.d.ts.map +1 -1
  133. package/lib/types/host/workbuddy/client.d.ts +15 -0
  134. package/lib/types/host/workbuddy/client.d.ts.map +1 -1
  135. package/lib/types/host/workbuddy/routes.d.ts.map +1 -1
  136. package/lib/types/index.d.ts.map +1 -1
  137. package/lib/types/shared/account-pool-contracts.d.ts +44 -0
  138. package/lib/types/shared/account-pool-contracts.d.ts.map +1 -1
  139. package/lib/types/shared/antigravity-contracts.d.ts +9 -3
  140. package/lib/types/shared/antigravity-contracts.d.ts.map +1 -1
  141. package/lib/types/shared/hub-contracts.d.ts +37 -0
  142. package/lib/types/shared/hub-contracts.d.ts.map +1 -0
  143. package/package.json +1 -1
package/lib/index.js CHANGED
@@ -516,6 +516,17 @@ function catalogSnapshotPath(name) {
516
516
  return path.join(dshHomeDir(), "storages", `${name}-catalog.json`);
517
517
  }
518
518
  /**
519
+ * How many models one line's catalog holds, for the hub overview's "N/M models".
520
+ *
521
+ * An empty catalog is reported as `null` rather than `0`: "this line has synced
522
+ * nothing yet" and "this line offers no models" are different statements, and
523
+ * only the second one is a fact. The overview hides the model count for a null,
524
+ * which is what a line that has never synced should show.
525
+ */
526
+ function catalogTotal(count) {
527
+ return count === 0 ? null : count;
528
+ }
529
+ /**
519
530
  * Snapshot name for one catalog *and* one credential scope.
520
531
  *
521
532
  * A persisted snapshot answers before any credential is read — that is the whole
@@ -681,7 +692,7 @@ let snapshotRehydrated = null;
681
692
  * directly; returning it here under a different entry type would force every
682
693
  * one of them to convert it.
683
694
  */
684
- function getCachedCatalog() {
695
+ function getCachedCatalog$2() {
685
696
  return catalogCache$2?.models;
686
697
  }
687
698
  /**
@@ -740,7 +751,7 @@ async function performCatalogLoad$1(credentials, options) {
740
751
  live = true;
741
752
  writeCatalogSnapshot(catalogSnapshotName(SNAPSHOT_SCOPE, key), [...models], Date.now());
742
753
  } catch {
743
- models = getCachedCatalog() ?? [];
754
+ models = getCachedCatalog$2() ?? [];
744
755
  live = false;
745
756
  }
746
757
  catalogCache$2 = {
@@ -2521,13 +2532,31 @@ var OAuthService = class {
2521
2532
  * "credentials are required" on every web search.
2522
2533
  */
2523
2534
  async credentials(forceRefresh = false, access = {}) {
2535
+ return (await this.credentialSelection(forceRefresh, access)).credentials;
2536
+ }
2537
+ /**
2538
+ * The credential to use next, together with the pool row that owns it.
2539
+ *
2540
+ * The settings card keys each account's quota snapshot by the row the reading
2541
+ * was made for, and the credential cannot do that job: a refresh rotates its
2542
+ * tokens, and an account may state no id of its own. Callers that care about
2543
+ * identity ask here; every other caller keeps using {@link credentials} and
2544
+ * sees no difference.
2545
+ *
2546
+ * @param forceRefresh - refresh even a token that is not close to expiry.
2547
+ * @param access - who is asking; see {@link credentials}.
2548
+ */
2549
+ async credentialSelection(forceRefresh = false, access = {}) {
2524
2550
  if (this.pool !== null) {
2525
- if (access.purpose === "tool") return (await this.pool.getCredentialAccount(this.fetchFn, forceRefresh)).credentials;
2526
- return (await this.pool.getEffectiveAccount(void 0, this.fetchFn, forceRefresh)).credentials;
2551
+ const picked = access.purpose === "tool" ? await this.pool.getCredentialAccount(this.fetchFn, forceRefresh) : await this.pool.getEffectiveAccount(void 0, this.fetchFn, forceRefresh);
2552
+ return {
2553
+ accountId: picked.account.id,
2554
+ credentials: picked.credentials
2555
+ };
2527
2556
  }
2528
2557
  const stored = await this.loadAuthenticated();
2529
- if (forceRefresh || stored.expiresAt - this.now() <= 6e4) return this.refreshCredentials(stored);
2530
- return stored;
2558
+ if (forceRefresh || stored.expiresAt - this.now() <= 6e4) return { credentials: await this.refreshCredentials(stored) };
2559
+ return { credentials: stored };
2531
2560
  }
2532
2561
  dispose() {
2533
2562
  if (this.disposed) return;
@@ -2823,6 +2852,49 @@ async function oauthErrorIdentifier(response) {
2823
2852
  return null;
2824
2853
  }
2825
2854
  //#endregion
2855
+ //#region src/host/common/account-quota.ts
2856
+ /**
2857
+ * A reset instant as Unix milliseconds.
2858
+ *
2859
+ * Providers state this in seconds, in milliseconds, or not at all, and the two
2860
+ * numeric forms are told apart by magnitude rather than by a flag: 10^10
2861
+ * seconds is the year 2286, so anything larger is already milliseconds.
2862
+ */
2863
+ function quotaResetMs(value) {
2864
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return null;
2865
+ return value > 1e10 ? value : value * 1e3;
2866
+ }
2867
+ /**
2868
+ * One window, or null when its consumed share was never measured.
2869
+ *
2870
+ * `label` may be empty: the card names an unlabeled window from its length, and
2871
+ * only falls back to a generic name when the line stated neither.
2872
+ */
2873
+ function quotaWindow$1(label, usedPercent, options = {}) {
2874
+ if (typeof usedPercent !== "number" || !Number.isFinite(usedPercent)) return null;
2875
+ const windowDurationMins = typeof options.windowDurationMins === "number" && Number.isFinite(options.windowDurationMins) ? options.windowDurationMins : null;
2876
+ return {
2877
+ label,
2878
+ usedPercent: Math.min(100, Math.max(0, usedPercent)),
2879
+ windowDurationMins,
2880
+ resetsAt: quotaResetMs(options.resetsAt)
2881
+ };
2882
+ }
2883
+ /**
2884
+ * The snapshot to publish for one account, or undefined when there is none.
2885
+ *
2886
+ * Undefined is the honest answer for "never read" and for "read before this
2887
+ * process started": both mean the card has nothing to show, and neither means
2888
+ * the account is unspent.
2889
+ */
2890
+ function poolQuota(fetchedAt, windows) {
2891
+ if (typeof fetchedAt !== "number" || !Number.isFinite(fetchedAt) || fetchedAt <= 0) return void 0;
2892
+ return {
2893
+ windows: windows.filter((window) => window !== null),
2894
+ fetchedAt
2895
+ };
2896
+ }
2897
+ //#endregion
2826
2898
  //#region src/host/codex-account-pool.ts
2827
2899
  /** Encrypted pool file the ChatGPT line owns; the single-credential store stays the mirror. */
2828
2900
  function codexPoolPath() {
@@ -2891,13 +2963,32 @@ const concurrency = new ConcurrencyGate();
2891
2963
  function chatGPTConcurrency() {
2892
2964
  return concurrency;
2893
2965
  }
2966
+ /**
2967
+ * One ChatGPT usage snapshot as the account card's per-account quota.
2968
+ *
2969
+ * A bucket lists its windows either in `windows` or, on older payloads, only as
2970
+ * the `primary`/`secondary` pair — the same fallback the quota card itself
2971
+ * applies, so the account row cannot show fewer windows than the card does. The
2972
+ * label stays empty on purpose: a ChatGPT window is named by its length, and
2973
+ * the card localizes that length in the reader's own language.
2974
+ */
2975
+ function codexAccountQuota(snapshot) {
2976
+ return poolQuota(snapshot.fetchedAt, snapshot.usage.buckets.flatMap((bucket) => {
2977
+ return (bucket.windows.length > 0 ? bucket.windows : [bucket.primary, bucket.secondary].filter((window) => window !== null)).map((window) => quotaWindow$1("", window.usedPercent, {
2978
+ windowDurationMins: window.windowDurationMins,
2979
+ resetsAt: window.resetsAt
2980
+ }));
2981
+ }));
2982
+ }
2894
2983
  var CodexAccountPool = class extends AccountPoolCore {
2895
2984
  refresherRef;
2896
2985
  store;
2986
+ quotaSnapshotRef;
2897
2987
  quotaBlockedUntil;
2898
2988
  constructor(options = {}) {
2899
2989
  const store = options.store ?? createPlatformTokenStore();
2900
2990
  const ref = {};
2991
+ const quotaSnapshotRef = {};
2901
2992
  const hooks = {
2902
2993
  providerId: "codex-chatgpt",
2903
2994
  displayName: "ChatGPT",
@@ -2948,7 +3039,11 @@ var CodexAccountPool = class extends AccountPoolCore {
2948
3039
  extendSummary: (account, base) => ({
2949
3040
  ...base,
2950
3041
  ...account.email === void 0 ? {} : { email: account.email },
2951
- ...account.planType === void 0 ? {} : { planLabel: account.planType }
3042
+ ...account.planType === void 0 ? {} : { planLabel: account.planType },
3043
+ ...(() => {
3044
+ const quota = quotaSnapshotRef.current?.(account);
3045
+ return quota === void 0 ? {} : { quota };
3046
+ })()
2952
3047
  }),
2953
3048
  emptyMessage: "未登录 ChatGPT 账号,请在「设置 → 订阅服务 → ChatGPT」中添加并登录账号。",
2954
3049
  ...options.backend === void 0 ? {} : { backend: options.backend },
@@ -2956,6 +3051,7 @@ var CodexAccountPool = class extends AccountPoolCore {
2956
3051
  };
2957
3052
  super(hooks);
2958
3053
  this.refresherRef = ref;
3054
+ this.quotaSnapshotRef = quotaSnapshotRef;
2959
3055
  this.store = store;
2960
3056
  }
2961
3057
  /** Hand the pool the OAuth service's per-account refresh implementation. */
@@ -2973,6 +3069,18 @@ var CodexAccountPool = class extends AccountPoolCore {
2973
3069
  setQuotaBlockedUntil(lookup) {
2974
3070
  this.quotaBlockedUntil = lookup;
2975
3071
  }
3072
+ /**
3073
+ * Wire in the account's own newest quota snapshot, for display only.
3074
+ *
3075
+ * The same map that already spares an exhausted account a wasted request also
3076
+ * answers "what did this account's window look like last time we read it".
3077
+ * The lookup must be a memory read: it runs while the settings card builds
3078
+ * its account list, and that page must never fan out an upstream request per
3079
+ * pooled account.
3080
+ */
3081
+ setQuotaSnapshot(lookup) {
3082
+ this.quotaSnapshotRef.current = lookup;
3083
+ }
2976
3084
  isEligible(account, now, triedAccountIds) {
2977
3085
  if (!super.isEligible(account, now, triedAccountIds)) return false;
2978
3086
  const blockedUntil = this.quotaBlockedUntil?.(account, now);
@@ -4088,6 +4196,56 @@ function createControlledModelFetch(baseFetch, options) {
4088
4196
  };
4089
4197
  }
4090
4198
  //#endregion
4199
+ //#region src/host/common/provider-route.ts
4200
+ /**
4201
+ * Claim one provider route for this plugin and keep claiming it.
4202
+ *
4203
+ * The route is taken immediately, and every `llm/adapters-updated` reports the
4204
+ * claim again — it is a no-op while the registration is held, and takes the
4205
+ * route back once another owner releases it. A rejected claim is remembered
4206
+ * rather than thrown, because the line whose route table asks through
4207
+ * {@link ProviderRouteClaim.conflict} still has to render.
4208
+ * @param ctx - The plugin context owning the `llm` service and the event seam.
4209
+ * @param options - The route id, the display label the two log lines name, and the adapter serving it.
4210
+ * @returns The claim's state and its disposer.
4211
+ */
4212
+ function claimProviderRoute(ctx, options) {
4213
+ let registration;
4214
+ let conflict = null;
4215
+ const claim = () => {
4216
+ if (registration !== void 0) return;
4217
+ try {
4218
+ registration = ctx.llm.registerAdapter([options.providerId], options.adapter);
4219
+ if (conflict !== null) ctx.logger.info(`[dsh-chatgpt-subscription] ${options.label} route "${options.providerId}" is now served by this plugin`);
4220
+ conflict = null;
4221
+ } catch (error) {
4222
+ conflict = error instanceof Error ? error.message : String(error);
4223
+ ctx.logger.warn(`[dsh-chatgpt-subscription] provider route "${options.providerId}" is already owned by another adapter; ${options.label} models keep being served by that one until its configuration is removed (${conflict})`);
4224
+ }
4225
+ };
4226
+ claim();
4227
+ const watch = options.watch === false || typeof ctx.on !== "function" ? void 0 : ctx.on("llm/adapters-updated", () => {
4228
+ claim();
4229
+ });
4230
+ return {
4231
+ serving: () => registration !== void 0,
4232
+ conflict: () => conflict,
4233
+ dispose: () => {
4234
+ releaseHandle(watch);
4235
+ registration?.();
4236
+ registration = void 0;
4237
+ }
4238
+ };
4239
+ }
4240
+ /** Cordis event handles are either a disposer function or a disposable object. */
4241
+ function releaseHandle(handle) {
4242
+ if (typeof handle === "function") {
4243
+ handle();
4244
+ return;
4245
+ }
4246
+ handle?.dispose?.();
4247
+ }
4248
+ //#endregion
4091
4249
  //#region src/host/common/request-diagnostics.ts
4092
4250
  /**
4093
4251
  * Reusable request diagnostics wrapper for model provider calls.
@@ -6239,6 +6397,72 @@ function acceptIdentity(current, incoming) {
6239
6397
  return typeof incoming === "string" && incoming.length > 0 ? incoming : current;
6240
6398
  }
6241
6399
  //#endregion
6400
+ //#region src/shared/hub-contracts.ts
6401
+ /**
6402
+ * Wire contracts for the subscription hub's overview route.
6403
+ *
6404
+ * The overview is the settings page's opening screen: one card per provider
6405
+ * line, annotated with how many accounts it holds and whether it is enabled.
6406
+ * Everything here is a non-secret fact a settings card may render; no
6407
+ * credential, quota figure, or upstream call ever crosses this boundary.
6408
+ */
6409
+ /** The aggregated, read-only overview the hub's opening screen renders. */
6410
+ const HUB_OVERVIEW_PATH = `${ROUTE_PREFIX$5}/hub/overview`;
6411
+ //#endregion
6412
+ //#region src/host/hub-overview.ts
6413
+ function registerHubOverviewRoutes(ctx, sources) {
6414
+ const handler = async (request, response) => {
6415
+ if (request.method !== "GET") {
6416
+ response.writeHead(405, {
6417
+ allow: "GET",
6418
+ "content-type": "application/json; charset=utf-8"
6419
+ });
6420
+ response.end(JSON.stringify({
6421
+ ok: false,
6422
+ error: {
6423
+ code: "bad-request",
6424
+ message: "Method not allowed."
6425
+ }
6426
+ }));
6427
+ return;
6428
+ }
6429
+ const envelope = {
6430
+ ok: true,
6431
+ value: { providers: (await Promise.allSettled(sources.map(async (source) => ({
6432
+ id: source.id,
6433
+ providerId: source.providerId,
6434
+ canToggle: source.canToggle,
6435
+ ...await source.read()
6436
+ })))).map((result, index) => {
6437
+ if (result.status === "fulfilled") return result.value;
6438
+ const source = sources[index];
6439
+ return {
6440
+ id: source.id,
6441
+ providerId: source.providerId,
6442
+ canToggle: source.canToggle,
6443
+ enabled: !source.canToggle,
6444
+ accountCount: 0,
6445
+ authenticated: false,
6446
+ enabledModelCount: null,
6447
+ totalModelCount: null,
6448
+ error: true
6449
+ };
6450
+ }) }
6451
+ };
6452
+ response.writeHead(200, {
6453
+ "content-type": "application/json; charset=utf-8",
6454
+ "cache-control": "no-store",
6455
+ "x-content-type-options": "nosniff"
6456
+ });
6457
+ response.end(JSON.stringify(envelope));
6458
+ };
6459
+ return ctx.webServer.register({
6460
+ kind: "exact",
6461
+ path: HUB_OVERVIEW_PATH,
6462
+ handler
6463
+ });
6464
+ }
6465
+ //#endregion
6242
6466
  //#region src/host/common/same-origin.ts
6243
6467
  /**
6244
6468
  * Whether one mutation request comes from this application's own document.
@@ -6346,9 +6570,9 @@ var UsageService = class {
6346
6570
  };
6347
6571
  const now = this.now();
6348
6572
  if (!force && !this.invalidated && this.cache !== null && now - this.cache.fetchedAt < 6e4 && this.cacheIdentityRevision === this.oauth.currentIdentityRevision()) return this.fromCache(false);
6349
- let credentials;
6573
+ let selection;
6350
6574
  try {
6351
- credentials = await this.oauth.credentials(false, USAGE_CREDENTIAL_ACCESS);
6575
+ selection = await this.oauth.credentialSelection(false, USAGE_CREDENTIAL_ACCESS);
6352
6576
  } catch (error) {
6353
6577
  const reason = error instanceof Error && error.message !== "" ? ` (${error.message})` : "";
6354
6578
  return this.failure({
@@ -6356,7 +6580,8 @@ var UsageService = class {
6356
6580
  message: `ChatGPT credentials could not be refreshed.${reason}`
6357
6581
  });
6358
6582
  }
6359
- const accountKey = identityKey(credentials);
6583
+ const credentials = selection.credentials;
6584
+ const accountKey = this.snapshotKeyFor(selection.accountId, credentials);
6360
6585
  if (this.cache !== null && this.cache.accountKey !== accountKey) this.clear();
6361
6586
  if (!force && !this.invalidated && this.cache !== null && now - this.cache.fetchedAt < 6e4) return this.fromCache(false);
6362
6587
  if (this.cache !== null && now - this.lastUpstreamAt < 15e3) return this.fromCache(this.invalidated || now - this.cache.fetchedAt >= 6e4);
@@ -6384,9 +6609,9 @@ var UsageService = class {
6384
6609
  * The rejection is absorbed here: this refresh has no caller to receive it,
6385
6610
  * and {@link fromCache} keeps serving the snapshot it failed to replace.
6386
6611
  */
6387
- startBackgroundRefresh(credentials, accountKey) {
6612
+ startBackgroundRefresh(credentials, snapshotKey) {
6388
6613
  if (this.inFlight !== null) return;
6389
- const task = this.refreshUpstream(credentials, accountKey).finally(() => {
6614
+ const task = this.refreshUpstream(credentials, snapshotKey).finally(() => {
6390
6615
  if (this.inFlight === task) this.inFlight = null;
6391
6616
  });
6392
6617
  this.inFlight = task;
@@ -6403,8 +6628,8 @@ var UsageService = class {
6403
6628
  * cooldown rather than forever, so a missing upstream field cannot strand an
6404
6629
  * account permanently.
6405
6630
  */
6406
- blockedUntilFor(credentials, now) {
6407
- const snapshot = this.snapshots.get(identityKey(credentials));
6631
+ blockedUntilFor(accountId, credentials, now) {
6632
+ const snapshot = this.snapshots.get(this.snapshotKeyFor(accountId, credentials));
6408
6633
  if (snapshot === void 0) return void 0;
6409
6634
  let reopen;
6410
6635
  for (const bucket of snapshot.usage.buckets) {
@@ -6417,6 +6642,32 @@ var UsageService = class {
6417
6642
  }
6418
6643
  return reopen !== void 0 && reopen > now ? reopen : void 0;
6419
6644
  }
6645
+ /**
6646
+ * The newest quota snapshot held for one account, if any.
6647
+ *
6648
+ * Quota follows the account, so the settings card shows each account its own
6649
+ * progress. The pool asks here with the row id it addresses the account by —
6650
+ * the identity that survives a token refresh — while its credentials answer
6651
+ * for a composition without a pool. This reads the remembered map only, never
6652
+ * the network, because opening the settings page must not cost one upstream
6653
+ * request per pooled account.
6654
+ */
6655
+ snapshotFor(accountId, credentials) {
6656
+ return this.snapshots.get(this.snapshotKeyFor(accountId, credentials));
6657
+ }
6658
+ /**
6659
+ * Key one account's remembered snapshot is filed under.
6660
+ *
6661
+ * A pool row id when the caller names one, prefixed so it can never be
6662
+ * confused with the credential-derived key, and otherwise the credential
6663
+ * identity — which is all a composition without a pool has. Two pooled accounts
6664
+ * that state neither an account id nor an email would otherwise collapse onto
6665
+ * one key and show each other's numbers, so the row id is the answer wherever
6666
+ * a pool is in charge.
6667
+ */
6668
+ snapshotKeyFor(accountId, credentials) {
6669
+ return accountId === void 0 || accountId === "" ? identityKey(credentials) : `row:${accountId}`;
6670
+ }
6420
6671
  /** Remember the newest snapshot for one account, bounded to the accounts in use. */
6421
6672
  rememberSnapshot(accountKey, usage, fetchedAt) {
6422
6673
  this.snapshots.set(accountKey, {
@@ -6436,8 +6687,8 @@ var UsageService = class {
6436
6687
  this.invalidated = false;
6437
6688
  }
6438
6689
  /** Drop the remembered snapshot of one account, after its window is known to have reset. */
6439
- forgetSnapshot(credentials) {
6440
- this.snapshots.delete(identityKey(credentials));
6690
+ forgetSnapshot(accountId, credentials) {
6691
+ this.snapshots.delete(this.snapshotKeyFor(accountId, credentials));
6441
6692
  }
6442
6693
  async consumeResetCredit() {
6443
6694
  if (this.resetConsumeInFlight !== null) return this.resetConsumeInFlight;
@@ -6448,12 +6699,17 @@ var UsageService = class {
6448
6699
  }
6449
6700
  async consumeResetCreditUpstream() {
6450
6701
  let credentials;
6702
+ let accountId;
6451
6703
  try {
6452
- credentials = await this.oauth.credentials(false, USAGE_CREDENTIAL_ACCESS);
6704
+ const selection = await this.oauth.credentialSelection(false, USAGE_CREDENTIAL_ACCESS);
6705
+ credentials = selection.credentials;
6706
+ accountId = selection.accountId;
6453
6707
  let creditsResponse = await this.fetchResetCredits(credentials);
6454
6708
  if (creditsResponse.status === 401) {
6455
6709
  await creditsResponse.body?.cancel().catch(() => void 0);
6456
- credentials = await this.oauth.credentials(true, USAGE_CREDENTIAL_ACCESS);
6710
+ const renewed = await this.oauth.credentialSelection(true, USAGE_CREDENTIAL_ACCESS);
6711
+ credentials = renewed.credentials;
6712
+ accountId = renewed.accountId;
6457
6713
  creditsResponse = await this.fetchResetCredits(credentials);
6458
6714
  }
6459
6715
  if (!creditsResponse.ok) {
@@ -6484,7 +6740,9 @@ var UsageService = class {
6484
6740
  });
6485
6741
  if (consumeResponse.status === 401) {
6486
6742
  await consumeResponse.body?.cancel().catch(() => void 0);
6487
- credentials = await this.oauth.credentials(true, USAGE_CREDENTIAL_ACCESS);
6743
+ const renewed = await this.oauth.credentialSelection(true, USAGE_CREDENTIAL_ACCESS);
6744
+ credentials = renewed.credentials;
6745
+ accountId = renewed.accountId;
6488
6746
  consumeResponse = await this.fetchFn(CODEX_RESET_CREDITS_CONSUME_URL, {
6489
6747
  method: "POST",
6490
6748
  headers: {
@@ -6508,7 +6766,7 @@ var UsageService = class {
6508
6766
  }
6509
6767
  await consumeResponse.body?.cancel().catch(() => void 0);
6510
6768
  this.clear();
6511
- const refreshed = await this.refreshUpstream(credentials, identityKey(credentials));
6769
+ const refreshed = await this.refreshUpstream(credentials, this.snapshotKeyFor(accountId, credentials));
6512
6770
  if (refreshed.error !== void 0) throw new UsageServiceError({
6513
6771
  code: "quota-failed",
6514
6772
  message: "The reset credit was used, but usage could not be refreshed."
@@ -6535,16 +6793,17 @@ var UsageService = class {
6535
6793
  checkedAt: Math.floor(this.now() / 1e3)
6536
6794
  };
6537
6795
  }
6538
- async refreshUpstream(initialCredentials, initialAccountKey) {
6796
+ async refreshUpstream(initialCredentials, initialSnapshotKey) {
6539
6797
  this.lastUpstreamAt = this.now();
6540
6798
  try {
6541
6799
  let credentials = initialCredentials;
6542
- let accountKey = initialAccountKey;
6800
+ let snapshotKey = initialSnapshotKey;
6543
6801
  let response = await this.fetch(credentials);
6544
6802
  if (response.status === 401) {
6545
6803
  await response.body?.cancel().catch(() => void 0);
6546
- credentials = await this.oauth.credentials(true, USAGE_CREDENTIAL_ACCESS);
6547
- accountKey = identityKey(credentials);
6804
+ const renewed = await this.oauth.credentialSelection(true, USAGE_CREDENTIAL_ACCESS);
6805
+ credentials = renewed.credentials;
6806
+ snapshotKey = this.snapshotKeyFor(renewed.accountId, credentials);
6548
6807
  response = await this.fetch(credentials);
6549
6808
  }
6550
6809
  if (response.status === 429) {
@@ -6577,10 +6836,10 @@ var UsageService = class {
6577
6836
  this.cache = {
6578
6837
  usage,
6579
6838
  fetchedAt: this.now(),
6580
- accountKey
6839
+ accountKey: snapshotKey
6581
6840
  };
6582
6841
  this.cacheIdentityRevision = this.oauth.currentIdentityRevision();
6583
- this.rememberSnapshot(accountKey, usage, this.cache.fetchedAt);
6842
+ this.rememberSnapshot(snapshotKey, usage, this.cache.fetchedAt);
6584
6843
  this.invalidated = false;
6585
6844
  return this.fromCache(false);
6586
6845
  } catch (error) {
@@ -7919,7 +8178,7 @@ var FileModelSettingsStore$2 = class {
7919
8178
  */
7920
8179
  /** Default loopback port the Antigravity CLI's own flow uses. */
7921
8180
  const DEFAULT_CALLBACK_PORT$2 = 51121;
7922
- let cachedQuota$1;
8181
+ let cachedQuota;
7923
8182
  let quotaFetchInFlight = null;
7924
8183
  /** Bumped by every cache clear so an in-flight fetch cannot publish stale state. */
7925
8184
  let quotaCacheEpoch$2 = 0;
@@ -8096,7 +8355,7 @@ function parseQuotaSummary(data) {
8096
8355
  for (const bucket of rawBuckets) {
8097
8356
  if (typeof bucket !== "object" || bucket === null) continue;
8098
8357
  const bRec = bucket;
8099
- const remaining = typeof bRec.remainingFraction === "number" ? Math.max(0, Math.min(1, bRec.remainingFraction)) : 0;
8358
+ const remaining = typeof bRec.remainingFraction === "number" && Number.isFinite(bRec.remainingFraction) ? Math.max(0, Math.min(1, bRec.remainingFraction)) : null;
8100
8359
  buckets.push({
8101
8360
  bucketId: String(bRec.bucketId || bRec.displayName || "limit"),
8102
8361
  displayName: String(bRec.displayName || bRec.bucketId || "Limit"),
@@ -8159,7 +8418,7 @@ function mergeCatalogSelection(stored, storedCatalog) {
8159
8418
  return stored.filter((id) => known.has(id));
8160
8419
  }
8161
8420
  async function fetchAccountQuota(store = new FileCredentialStore$2(), modelSettings, fetchFn = fetch, force = false) {
8162
- if (!force && cachedQuota$1 && Date.now() - (cachedQuota$1.fetchedAt || 0) < 12e4) return cachedQuota$1;
8421
+ if (!force && cachedQuota && Date.now() - (cachedQuota.fetchedAt || 0) < 12e4) return cachedQuota;
8163
8422
  if (quotaFetchInFlight) return quotaFetchInFlight;
8164
8423
  const epoch = quotaCacheEpoch$2;
8165
8424
  const request = (async () => {
@@ -8195,7 +8454,7 @@ async function fetchAccountQuota(store = new FileCredentialStore$2(), modelSetti
8195
8454
  fetchedAt: Date.now()
8196
8455
  };
8197
8456
  if (epoch !== quotaCacheEpoch$2) return snapshot;
8198
- cachedQuota$1 = snapshot;
8457
+ cachedQuota = snapshot;
8199
8458
  if (modelSettings && catalogModels.length > 0) {
8200
8459
  const current = await modelSettings.read();
8201
8460
  const mergedEnabled = mergeCatalogSelection(current.enabledModelIds, current.catalogModels);
@@ -8211,11 +8470,11 @@ async function fetchAccountQuota(store = new FileCredentialStore$2(), modelSetti
8211
8470
  }
8212
8471
  }
8213
8472
  function getCachedQuota() {
8214
- return cachedQuota$1;
8473
+ return cachedQuota;
8215
8474
  }
8216
8475
  function clearCachedQuota() {
8217
8476
  quotaCacheEpoch$2 += 1;
8218
- cachedQuota$1 = void 0;
8477
+ cachedQuota = void 0;
8219
8478
  quotaFetchInFlight = null;
8220
8479
  }
8221
8480
  //#endregion
@@ -9595,6 +9854,54 @@ function createPoolCredentialBackend(filePath) {
9595
9854
  throw new Error("Antigravity pool encrypted storage requires Windows, macOS, or Linux.");
9596
9855
  }
9597
9856
  /**
9857
+ * One bucket's reset moment as Unix milliseconds, when it parses as a date.
9858
+ *
9859
+ * The service states it as an ISO-ish string; anything `Date.parse` refuses is
9860
+ * dropped rather than guessed at, because a wrong instant on the card is worse
9861
+ * than none.
9862
+ */
9863
+ function resetTimeMs(resetTime) {
9864
+ if (resetTime === void 0 || resetTime === "") return null;
9865
+ const parsed = Date.parse(resetTime);
9866
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
9867
+ }
9868
+ /**
9869
+ * One cached Antigravity snapshot as the shared account card renders it.
9870
+ *
9871
+ * One window per bucket. The service states what is LEFT, and the card draws what
9872
+ * is SPENT, so the used share is the complement. A bucket whose fraction is
9873
+ * `null` (never measured) or outside 0-1 is dropped rather than drawn as 0% or
9874
+ * 100%: "not stated" and "nothing used" are different claims and only one of
9875
+ * them is true. A stated 0 is a real measurement — the allowance is exhausted —
9876
+ * and is published as 100% used.
9877
+ *
9878
+ * `windowDurationMins` is deliberately absent. The bucket's `window` field is
9879
+ * carried through this line's cache verbatim and nothing in the line interprets
9880
+ * it as a duration, so naming a window from it would be a guess; the card names
9881
+ * each window from the label below instead.
9882
+ */
9883
+ function antigravityPoolQuota(quota) {
9884
+ if (quota === void 0) return void 0;
9885
+ const nameCounts = /* @__PURE__ */ new Map();
9886
+ for (const group of quota.groups) for (const bucket of group.buckets) {
9887
+ const name = bucket.displayName.trim();
9888
+ if (name === "") continue;
9889
+ nameCounts.set(name, (nameCounts.get(name) ?? 0) + 1);
9890
+ }
9891
+ const windows = [];
9892
+ for (const group of quota.groups) {
9893
+ const groupName = group.displayName.trim();
9894
+ for (const bucket of group.buckets) {
9895
+ const remaining = bucket.remainingFraction;
9896
+ if (remaining === null || !Number.isFinite(remaining) || remaining < 0 || remaining > 1) continue;
9897
+ const name = bucket.displayName.trim();
9898
+ const label = (name === "" || (nameCounts.get(name) ?? 0) > 1) && groupName !== "" ? name === "" ? groupName : groupName + " · " + name : name;
9899
+ windows.push(quotaWindow$1(label, (1 - remaining) * 100, { resetsAt: resetTimeMs(bucket.resetTime) }));
9900
+ }
9901
+ }
9902
+ return poolQuota(quota.fetchedAt, windows);
9903
+ }
9904
+ /**
9598
9905
  * Antigravity's account pool.
9599
9906
  *
9600
9907
  * The storage, eligibility, rotation and cooldown rules live in the shared
@@ -9658,7 +9965,11 @@ var AccountPoolStore = class {
9658
9965
  ...base,
9659
9966
  ...projectId === void 0 ? {} : { projectId },
9660
9967
  ...email === void 0 ? {} : { email },
9661
- ...account.planLabel === void 0 ? {} : { planLabel: account.planLabel }
9968
+ ...account.planLabel === void 0 ? {} : { planLabel: account.planLabel },
9969
+ ...account.isPrimary === true ? (() => {
9970
+ const quota = antigravityPoolQuota(getCachedQuota());
9971
+ return quota === void 0 ? {} : { quota };
9972
+ })() : {}
9662
9973
  };
9663
9974
  },
9664
9975
  emptyMessage: "未登录 Antigravity 账号,请在「设置 → Antigravity」中添加并登录账号。",
@@ -12569,7 +12880,41 @@ function whoami(apiKey, options = {}) {
12569
12880
  async function verifyApiKey(apiKey, options = {}) {
12570
12881
  return parseWhoami(await whoami(apiKey, options), { authenticatedAt: Date.now() });
12571
12882
  }
12572
- let cachedQuotaEntry;
12883
+ /**
12884
+ * One newest snapshot PER ACCOUNT.
12885
+ *
12886
+ * Quota follows the account: rotation decides which key spends the next
12887
+ * request, so a single slot only ever described whichever key was read last.
12888
+ * The pool's card draws every account its own reading, so each account keeps
12889
+ * its own entry; past the cap the oldest read is evicted.
12890
+ *
12891
+ * This map is a memory read only. Nothing here fetches — the settings card
12892
+ * builds its account list from it, and that must never cost one upstream
12893
+ * request per pooled key.
12894
+ */
12895
+ const cachedQuotaEntries$1 = /* @__PURE__ */ new Map();
12896
+ /** How many accounts' snapshots are remembered; mirrors the Codex usage service. */
12897
+ const QUOTA_CACHE_LIMIT$1 = 20;
12898
+ /** Key of a snapshot whose caller named no account. */
12899
+ const UNNAMED_ACCOUNT_KEY$1 = "";
12900
+ function quotaCacheKey(accountId) {
12901
+ return accountId ?? UNNAMED_ACCOUNT_KEY$1;
12902
+ }
12903
+ /** The entry read most recently, whichever account it belongs to. */
12904
+ function newestQuotaEntry$2() {
12905
+ let newest;
12906
+ for (const entry of cachedQuotaEntries$1.values()) if (newest === void 0 || entry.fetchedAt > newest.fetchedAt) newest = entry;
12907
+ return newest;
12908
+ }
12909
+ /** Remember one account's newest snapshot, bounded to the accounts in use. */
12910
+ function rememberQuotaEntry(entry) {
12911
+ cachedQuotaEntries$1.set(quotaCacheKey(entry.accountId), entry);
12912
+ while (cachedQuotaEntries$1.size > QUOTA_CACHE_LIMIT$1) {
12913
+ const oldest = [...cachedQuotaEntries$1.entries()].sort((left, right) => left[1].fetchedAt - right[1].fetchedAt)[0];
12914
+ if (oldest === void 0) break;
12915
+ cachedQuotaEntries$1.delete(oldest[0]);
12916
+ }
12917
+ }
12573
12918
  let quotaInFlight$2 = null;
12574
12919
  let quotaInFlightAccountId$1;
12575
12920
  let quotaCacheEpoch$1 = 0;
@@ -12578,21 +12923,22 @@ let quotaCacheEpoch$1 = 0;
12578
12923
  * Kept with a no-argument signature for backward compatibility.
12579
12924
  */
12580
12925
  function getCachedQuota$2() {
12581
- return cachedQuotaEntry?.quota;
12926
+ return newestQuotaEntry$2()?.quota;
12582
12927
  }
12583
12928
  /**
12584
12929
  * The cached snapshot only when it belongs to this account.
12585
12930
  *
12586
12931
  * Passing no account keeps the pre-pool behavior of returning whatever was
12587
- * fetched last.
12932
+ * fetched last. This is the lookup the account pool calls while it builds its
12933
+ * summaries: a pure memory read, never a fetch.
12588
12934
  */
12589
- function getCachedQuotaFor$1(accountId) {
12590
- if (accountId === void 0 || accountId === null) return cachedQuotaEntry?.quota;
12591
- return cachedQuotaEntry?.accountId === accountId ? cachedQuotaEntry.quota : void 0;
12935
+ function getCachedQuotaFor$3(accountId) {
12936
+ if (accountId === void 0 || accountId === null) return newestQuotaEntry$2()?.quota;
12937
+ return cachedQuotaEntries$1.get(accountId)?.quota;
12592
12938
  }
12593
12939
  function clearCachedQuota$2() {
12594
12940
  quotaCacheEpoch$1 += 1;
12595
- cachedQuotaEntry = void 0;
12941
+ cachedQuotaEntries$1.clear();
12596
12942
  quotaInFlight$2 = null;
12597
12943
  quotaInFlightAccountId$1 = void 0;
12598
12944
  }
@@ -12607,8 +12953,8 @@ async function fetchAccountQuota$2(store = new FileCredentialStore$1(), fetchFnO
12607
12953
  const fetchFn = typeof fetchFnOrOptions === "function" ? fetchFnOrOptions : fetchFnOrOptions?.fetchFn ?? fetch;
12608
12954
  const effectiveForce = isOptionsObj ? fetchFnOrOptions.force ?? force : force;
12609
12955
  const effectiveAccountId = isOptionsObj ? fetchFnOrOptions.accountId ?? accountId : accountId;
12610
- const sameAccount = effectiveAccountId === void 0 || cachedQuotaEntry?.accountId === effectiveAccountId;
12611
- if (!effectiveForce && sameAccount && cachedQuotaEntry && Date.now() - cachedQuotaEntry.fetchedAt < 12e4) return cachedQuotaEntry.quota;
12956
+ const cached = getCachedQuotaFor$3(effectiveAccountId);
12957
+ if (!effectiveForce && cached !== void 0 && Date.now() - (cached.fetchedAt || 0) < 12e4) return cached;
12612
12958
  if (quotaInFlight$2 !== null && (effectiveAccountId === void 0 || quotaInFlightAccountId$1 === effectiveAccountId)) return quotaInFlight$2;
12613
12959
  const epoch = quotaCacheEpoch$1;
12614
12960
  const request = (async () => {
@@ -12664,11 +13010,11 @@ async function fetchAccountQuota$2(store = new FileCredentialStore$1(), fetchFnO
12664
13010
  sources
12665
13011
  };
12666
13012
  if (epoch !== quotaCacheEpoch$1) return snapshot;
12667
- cachedQuotaEntry = {
13013
+ rememberQuotaEntry({
12668
13014
  accountId: effectiveAccountId,
12669
13015
  quota: snapshot,
12670
13016
  fetchedAt: snapshot.fetchedAt
12671
- };
13017
+ });
12672
13018
  return snapshot;
12673
13019
  })();
12674
13020
  quotaInFlight$2 = request;
@@ -14758,6 +15104,34 @@ function processLine$1(line, state, wire) {
14758
15104
  }
14759
15105
  //#endregion
14760
15106
  //#region src/host/command-code/account-pool.ts
15107
+ /**
15108
+ * One cached snapshot as the shared account card renders it.
15109
+ *
15110
+ * A usage window states its own consumed share, so it maps straight across; a
15111
+ * window whose share nobody measured is dropped rather than drawn at 0, which
15112
+ * would claim an unspent window no reading supports.
15113
+ *
15114
+ * Exported for the pool's regression tests; the card only reaches it through
15115
+ * the summary hook.
15116
+ */
15117
+ function commandCodePoolQuota(cached) {
15118
+ return poolQuota(cached.fetchedAt, cached.windows.map((window) => quotaWindow$1(window.label, window.usedPercent, {
15119
+ windowDurationMins: window.windowDurationMins,
15120
+ resetsAt: window.resetsAt
15121
+ })));
15122
+ }
15123
+ /**
15124
+ * This key's own newest snapshot, ready for the account card.
15125
+ *
15126
+ * Quota follows the account, so the pool reports what the line already
15127
+ * remembered for THIS account id. The lookup is a pure memory read: the card
15128
+ * builds its account list here, and that must never cost one upstream request
15129
+ * per pooled key.
15130
+ */
15131
+ function quotaFor$1(accountId) {
15132
+ const cached = getCachedQuotaFor$3(accountId);
15133
+ return cached === void 0 ? void 0 : commandCodePoolQuota(cached);
15134
+ }
14761
15135
  /** Encrypted pool file this line owns; the pre-pool credential file stays the mirror. */
14762
15136
  function commandCodePoolPath() {
14763
15137
  return path.join(dshHomeDir(), "storages", "command-code-pool.json");
@@ -14895,7 +15269,11 @@ var CommandCodeAccountPool = class extends AccountPoolCore {
14895
15269
  ...account.userName === void 0 ? {} : { userName: account.userName },
14896
15270
  ...account.organizationName === void 0 ? {} : { organizationName: account.organizationName },
14897
15271
  ...account.planId === void 0 ? {} : { planId: account.planId },
14898
- ...account.userId === void 0 ? {} : { userId: account.userId }
15272
+ ...account.userId === void 0 ? {} : { userId: account.userId },
15273
+ ...(() => {
15274
+ const quota = quotaFor$1(account.id);
15275
+ return quota === void 0 ? {} : { quota };
15276
+ })()
14899
15277
  };
14900
15278
  },
14901
15279
  emptyMessage: "Not signed in to Command Code. 请在「设置 → 订阅服务 → Command Code」中添加账号或粘贴 API Key。",
@@ -15450,7 +15828,7 @@ async function getCommandCodeWebStatus(store, modelSettings, preferences, option
15450
15828
  const catalog = live.length > 0 ? live : fallbackCatalog$1();
15451
15829
  const enabled = settings.enabled !== false;
15452
15830
  const models = buildModelOptions$2(catalog, resolveEnabledModelIds$4(settings.enabledModelIds, catalog, enabled), settings.contextWindowOverrides);
15453
- const quota = getCachedQuotaFor$1(activeAccount?.id ?? null);
15831
+ const quota = getCachedQuotaFor$3(activeAccount?.id ?? null);
15454
15832
  const activeAccountView = activePoolAccount === void 0 ? null : {
15455
15833
  userId: activePoolAccount.userId ?? activePoolAccount.credentials.userId ?? null,
15456
15834
  userName: activePoolAccount.userName ?? activePoolAccount.credentials.userName ?? null,
@@ -15523,7 +15901,7 @@ function registerCommandCodeRoutes(ctx, store, modelSettings, preferences, optio
15523
15901
  if (method !== "GET") return sendMethodNotAllowed$5(response);
15524
15902
  const credentials = await store.read();
15525
15903
  const targetId = await activeAccountId();
15526
- const cached = getCachedQuotaFor$1(targetId);
15904
+ const cached = getCachedQuotaFor$3(targetId);
15527
15905
  if ((credentials !== null || targetId !== void 0) && (cached === void 0 || Date.now() - (cached.fetchedAt || 0) > 12e4)) {
15528
15906
  const refresh = () => fetchAccountQuota$2(quotaStore, fetchFn, false, targetId);
15529
15907
  if (cached === void 0) await quotaRefresh.run(refresh);
@@ -19336,19 +19714,89 @@ function buildModelOptions$1(catalog, enabledModelIds, contextWindowOverrides) {
19336
19714
  };
19337
19715
  });
19338
19716
  }
19339
- let quotaCache$1 = null;
19340
- let quotaAccountId;
19717
+ /**
19718
+ * How many accounts' snapshots are remembered.
19719
+ *
19720
+ * Mirrors the bound the ChatGPT line's usage service applies to its own
19721
+ * per-account map: room for every account a user is likely to pool, and no way
19722
+ * for a long-lived process to accumulate one snapshot per account it has seen.
19723
+ */
19724
+ const QUOTA_ACCOUNT_LIMIT$1 = 20;
19725
+ /** Key of a snapshot whose caller named no account: the single-credential read. */
19726
+ const UNNAMED_ACCOUNT_KEY = "";
19727
+ /**
19728
+ * One newest snapshot PER ACCOUNT, keyed by the account id the caller stated.
19729
+ *
19730
+ * Quota follows the account: rotation decides which account spends the next
19731
+ * request, so a single slot could only ever describe whichever account was read
19732
+ * last. The pool's card draws every account its own reading, so each account
19733
+ * keeps its own entry; past the cap the oldest read is evicted.
19734
+ *
19735
+ * This map is a memory read only. Nothing here fetches — the settings card
19736
+ * builds its account list from it, and that must never cost one upstream
19737
+ * request per pooled account.
19738
+ */
19739
+ const quotaCache$1 = /* @__PURE__ */ new Map();
19740
+ /**
19741
+ * Key of the entry written most recently.
19742
+ *
19743
+ * The pre-pool card asks "whatever was fetched last"; the last WRITE answers
19744
+ * that exactly, where a scan by `fetchedAt` would break a tie between two
19745
+ * accounts read inside the same millisecond by map order.
19746
+ */
19747
+ let quotaLastKey$1 = UNNAMED_ACCOUNT_KEY;
19748
+ function quotaCacheKeyFor(accountId) {
19749
+ return accountId ?? UNNAMED_ACCOUNT_KEY;
19750
+ }
19751
+ /** The entry read most recently, whichever account it belongs to. */
19752
+ function newestQuotaEntry$1() {
19753
+ return quotaCache$1.get(quotaLastKey$1);
19754
+ }
19755
+ /** Remember one account's newest snapshot, bounded to the accounts in use. */
19756
+ function rememberQuota$1(accountId, snapshot) {
19757
+ const key = quotaCacheKeyFor(accountId);
19758
+ quotaCache$1.set(key, snapshot);
19759
+ quotaLastKey$1 = key;
19760
+ while (quotaCache$1.size > QUOTA_ACCOUNT_LIMIT$1) {
19761
+ const oldest = [...quotaCache$1.entries()].sort((left, right) => left[1].fetchedAt - right[1].fetchedAt)[0];
19762
+ if (oldest === void 0) break;
19763
+ quotaCache$1.delete(oldest[0]);
19764
+ if (quotaLastKey$1 === oldest[0]) quotaLastKey$1 = UNNAMED_ACCOUNT_KEY;
19765
+ }
19766
+ }
19767
+ /**
19768
+ * The snapshot read most recently, whichever account it belongs to.
19769
+ *
19770
+ * Kept for the pre-pool single-credential card, which has no account id to ask
19771
+ * with. A pooled caller asks {@link getCachedQuotaFor} instead.
19772
+ */
19341
19773
  function getCachedQuota$3() {
19342
- return quotaCache$1;
19774
+ return newestQuotaEntry$1() ?? null;
19343
19775
  }
19344
- /** The cached snapshot only when it belongs to this account. */
19345
- function getCachedQuotaFor(accountId) {
19346
- if (accountId === void 0 || accountId === null) return quotaCache$1;
19347
- return quotaAccountId === accountId ? quotaCache$1 : null;
19776
+ /**
19777
+ * The cached snapshot only when it belongs to this account.
19778
+ *
19779
+ * Passing no account keeps the pre-pool behavior of returning whatever was
19780
+ * fetched last. Passing one answers that account's own newest snapshot, or null
19781
+ * when this line has never read it — null is "never read", which is a different
19782
+ * statement from a snapshot whose windows are all at zero.
19783
+ */
19784
+ function getCachedQuotaFor$2(accountId) {
19785
+ if (accountId === void 0 || accountId === null) return newestQuotaEntry$1() ?? null;
19786
+ return cachedQuotaFor$1(accountId);
19787
+ }
19788
+ /**
19789
+ * The newest snapshot held for one account, for display only.
19790
+ *
19791
+ * The pool asks here while it builds the account list for the settings card, so
19792
+ * this is a pure memory read: it never fetches.
19793
+ */
19794
+ function cachedQuotaFor$1(accountId) {
19795
+ return quotaCache$1.get(accountId) ?? null;
19348
19796
  }
19349
19797
  function clearCachedQuota$3() {
19350
- quotaCache$1 = null;
19351
- quotaAccountId = void 0;
19798
+ quotaCache$1.clear();
19799
+ quotaLastKey$1 = UNNAMED_ACCOUNT_KEY;
19352
19800
  }
19353
19801
  /**
19354
19802
  * Display name and nominal length for each window key the service reports.
@@ -19595,8 +20043,8 @@ function parsePlanLevel(payload) {
19595
20043
  * account's real usage.
19596
20044
  */
19597
20045
  async function fetchAccountQuota$3(store, options = {}) {
19598
- const sameAccount = options.accountId === void 0 || quotaAccountId === options.accountId;
19599
- if (options.force !== true && sameAccount && quotaCache$1 !== null && Date.now() - quotaCache$1.fetchedAt < 12e4) return quotaCache$1;
20046
+ const cached = quotaCache$1.get(quotaCacheKeyFor(options.accountId));
20047
+ if (options.force !== true && cached !== void 0 && Date.now() - cached.fetchedAt < 12e4) return cached;
19600
20048
  const fetchFn = options.fetchFn ?? fetch;
19601
20049
  const pooled = options.credentials !== void 0;
19602
20050
  let credentials;
@@ -19645,8 +20093,7 @@ async function fetchAccountQuota$3(store, options = {}) {
19645
20093
  fetchedAt: Date.now(),
19646
20094
  sources: [openAIUrl(USAGES_PATH, credentials.region)]
19647
20095
  };
19648
- quotaCache$1 = snapshot;
19649
- quotaAccountId = options.accountId;
20096
+ rememberQuota$1(options.accountId, snapshot);
19650
20097
  return snapshot;
19651
20098
  }
19652
20099
  /**
@@ -22547,6 +22994,34 @@ function parseKimiCodePoolData(value) {
22547
22994
  return result;
22548
22995
  }
22549
22996
  /**
22997
+ * One account's own newest quota snapshot, in the shape the account card draws.
22998
+ *
22999
+ * The snapshot comes from 'client.ts''s per-account cache keyed by the pool
23000
+ * account id, and this is where this line's own numbers are translated into the
23001
+ * shared shape. Every judgement below trades a possibly-nice-looking bar for an
23002
+ * honest one:
23003
+ *
23004
+ * - a window whose consumed share nobody measured is DROPPED (`quotaWindow`
23005
+ * returns null), because "0% used" and "not stated" are different claims. This
23006
+ * parser only mints a window once it has a ratio, so a window that states none
23007
+ * never reaches here at all — the drop is the second line of defence;
23008
+ * - `resetsAt` is already Unix MILLISECONDS on this line, so it is passed
23009
+ * through and normalized rather than multiplied;
23010
+ * - the read time is the snapshot's own `fetchedAt`, which this line always
23011
+ * sets. A snapshot with no read time is not a snapshot, and `poolQuota`
23012
+ * reports that as undefined.
23013
+ *
23014
+ * A null snapshot means "this account was never read", which the card renders as
23015
+ * exactly that instead of drawing a zero.
23016
+ */
23017
+ function kimiCodePoolQuota(snapshot) {
23018
+ if (snapshot === null) return void 0;
23019
+ return poolQuota(snapshot.fetchedAt, snapshot.windows.map((window) => quotaWindow$1(window.label, window.usedPercent, {
23020
+ windowDurationMins: window.windowDurationMins,
23021
+ resetsAt: window.resetsAt
23022
+ })));
23023
+ }
23024
+ /**
22550
23025
  * Kimi Code's account pool.
22551
23026
  *
22552
23027
  * The credential is an OAuth pair whose refresh token rotates, so the pool owns
@@ -22625,6 +23100,7 @@ var KimiCodeAccountPool = class extends AccountPoolCore {
22625
23100
  const nickname = account.nickname ?? account.credentials.nickname;
22626
23101
  const planName = account.planName ?? account.credentials.planName;
22627
23102
  const region = account.region ?? account.credentials.region;
23103
+ const quota = kimiCodePoolQuota(cachedQuotaFor$1(account.id));
22628
23104
  return {
22629
23105
  ...base,
22630
23106
  ...email === void 0 ? {} : { email },
@@ -22634,7 +23110,8 @@ var KimiCodeAccountPool = class extends AccountPoolCore {
22634
23110
  planLabel: planName
22635
23111
  },
22636
23112
  ...region === void 0 ? {} : { region },
22637
- ...account.userId === void 0 ? {} : { userId: account.userId }
23113
+ ...account.userId === void 0 ? {} : { userId: account.userId },
23114
+ ...quota === void 0 ? {} : { quota }
22638
23115
  };
22639
23116
  },
22640
23117
  emptyMessage: "Not signed in to Kimi Code. Sign in from Settings > Kimi Code.",
@@ -22748,7 +23225,7 @@ async function getKimiCodeWebStatus(store, modelSettings, preferences, options =
22748
23225
  const catalog = live.length > 0 ? live : fallbackCatalog();
22749
23226
  const enabled = settings.enabled !== false;
22750
23227
  const models = buildModelOptions$1(catalog, resolveEnabledModelIds$3(settings.enabledModelIds, catalog, enabled), settings.contextWindowOverrides);
22751
- const quota = getCachedQuotaFor(activeAccount?.id ?? null);
23228
+ const quota = getCachedQuotaFor$2(activeAccount?.id ?? null);
22752
23229
  const account = quota?.account ?? (active === null || active === void 0 ? null : accountFromCredentials$2(active));
22753
23230
  return {
22754
23231
  enabled,
@@ -22882,7 +23359,7 @@ function registerKimiCodeRoutes(ctx, store, modelSettings, preferences, options
22882
23359
  if (path === "" || path === "status") {
22883
23360
  if (method !== "GET") return sendMethodNotAllowed$3(response);
22884
23361
  const poolActive = await activeAccount();
22885
- const cached = getCachedQuotaFor(poolActive.id ?? null);
23362
+ const cached = getCachedQuotaFor$2(poolActive.id ?? null);
22886
23363
  if (poolActive.credentials !== void 0 && (cached === null || Date.now() - (cached.fetchedAt || 0) > 12e4)) {
22887
23364
  const refresh = async () => {
22888
23365
  const credentials = await effectiveCredentials(poolActive);
@@ -26725,29 +27202,75 @@ const QUOTA_FAILURE_CACHE_MS = 10 * 6e4;
26725
27202
  const QUOTA_TIMEOUT_MS = 8e3;
26726
27203
  let quotaHostInForce = null;
26727
27204
  /**
26728
- * The last usage snapshot, plus the last failure to have one.
27205
+ * How many accounts' snapshots are remembered.
27206
+ *
27207
+ * Bounded like the ChatGPT line's own per-account map: the pool accepts a
27208
+ * handful of accounts, and a long-lived process must not accumulate one entry
27209
+ * per id it has ever seen. The oldest WRITE is evicted, which is also the read
27210
+ * least likely to be looked at again.
27211
+ */
27212
+ const QUOTA_SNAPSHOT_ACCOUNT_LIMIT = 20;
27213
+ /**
27214
+ * The newest read, whichever account it belonged to.
26729
27215
  *
26730
- * `value` and `reason` are tracked SEPARATELY from the last GOOD value on
26731
- * purpose. A read that fails carries no information about the previous one, so
26732
- * replacing a good snapshot with `null` turned every transient refusal into a
26733
- * visible disappearance of the quota box; `quotaLastGood` lets the card keep
26734
- * showing the last known numbers while saying they are old.
27216
+ * Kept beside the per-account map because the page-level block and the
27217
+ * "is a read even worth starting" test are about the line's latest reading,
27218
+ * not about one account.
26735
27219
  */
26736
27220
  let quotaSnapshot = null;
26737
- /** The last snapshot that actually parsed, kept across failures. */
26738
- let quotaLastGood = null;
27221
+ /** Per-account snapshots, so an account is never shown the numbers read for another. */
27222
+ const quotaByAccount = /* @__PURE__ */ new Map();
26739
27223
  /** How long a refused credential is remembered before asking again. */
26740
27224
  const QUOTA_REJECTED_CACHE_MS = 30 * 6e4;
26741
27225
  /** Forget the cached usage snapshot (a sign-out, a sign-in, a test). */
26742
27226
  function clearCachedQuota$4() {
26743
27227
  quotaSnapshot = null;
26744
- quotaLastGood = null;
27228
+ quotaByAccount.clear();
26745
27229
  quotaHostInForce = null;
26746
27230
  }
26747
27231
  /** The last usage snapshot without touching the network. */
26748
27232
  function getCachedQuota$4() {
26749
27233
  return quotaSnapshot?.value ?? null;
26750
27234
  }
27235
+ /**
27236
+ * The cached snapshot only when it belongs to this account, without touching the
27237
+ * network.
27238
+ *
27239
+ * Quota follows the account: a rotation decides whose credential is spent next,
27240
+ * so the snapshot a read produced describes the row it was made for. The pool
27241
+ * asks here while it builds the settings card's account list, which must never
27242
+ * wait on — or trigger — an upstream read.
27243
+ *
27244
+ * An absent, empty or null id keeps the pre-pool behaviour and answers with the
27245
+ * newest read, which is what the page-level block wants.
27246
+ */
27247
+ function getCachedQuotaFor$1(accountId) {
27248
+ if (accountId === void 0 || accountId === null || accountId === "") return getCachedQuota$4();
27249
+ return quotaByAccount.get(accountId)?.value ?? null;
27250
+ }
27251
+ /** The entry a read for this account should consult, when there is one. */
27252
+ function quotaEntryFor(accountId) {
27253
+ if (accountId === void 0 || accountId === "") return quotaSnapshot;
27254
+ return quotaByAccount.get(accountId) ?? null;
27255
+ }
27256
+ /**
27257
+ * Remember one read against the account it was made for.
27258
+ *
27259
+ * Re-inserting an id moves it to the newest position, so the map's first key is
27260
+ * always the least recently written entry and eviction is a single delete.
27261
+ */
27262
+ function rememberQuotaSnapshot(entry) {
27263
+ quotaSnapshot = entry;
27264
+ const accountId = entry.accountId;
27265
+ if (accountId === void 0 || accountId === "") return;
27266
+ quotaByAccount.delete(accountId);
27267
+ quotaByAccount.set(accountId, entry);
27268
+ while (quotaByAccount.size > QUOTA_SNAPSHOT_ACCOUNT_LIMIT) {
27269
+ const oldest = quotaByAccount.keys().next().value;
27270
+ if (oldest === void 0) break;
27271
+ quotaByAccount.delete(oldest);
27272
+ }
27273
+ }
26751
27274
  /** Why the last read produced no snapshot, or null when it produced one. */
26752
27275
  function getQuotaUnavailable() {
26753
27276
  return quotaSnapshot?.reason ?? null;
@@ -26869,9 +27392,10 @@ function parseTokenPlanQuota(payload, now = Date.now()) {
26869
27392
  async function fetchTokenPlanQuota(credentials, options = {}) {
26870
27393
  const fetchFn = options.fetchFn ?? fetch;
26871
27394
  const now = Date.now();
26872
- if (options.force !== true && quotaSnapshot !== null) {
26873
- const ttl = quotaSnapshot.reason === "credential-not-accepted" ? QUOTA_REJECTED_CACHE_MS : quotaSnapshot.reason === "token-expired" && quotaSnapshot.value === null ? QUOTA_FAILURE_CACHE_MS : quotaSnapshot.value === null ? QUOTA_FAILURE_CACHE_MS : QUOTA_CACHE_MS;
26874
- if (now - quotaSnapshot.at < ttl) return quotaSnapshot.value;
27395
+ const cached = quotaEntryFor(options.accountId);
27396
+ if (options.force !== true && cached !== null) {
27397
+ const ttl = cached.reason === "credential-not-accepted" ? QUOTA_REJECTED_CACHE_MS : cached.reason === "token-expired" && cached.value === null ? QUOTA_FAILURE_CACHE_MS : cached.value === null ? QUOTA_FAILURE_CACHE_MS : QUOTA_CACHE_MS;
27398
+ if (now - cached.at < ttl) return cached.value;
26875
27399
  }
26876
27400
  const candidates = quotaHostCandidates(credentials.region);
26877
27401
  const ordered = quotaHostInForce === null ? candidates : [quotaHostInForce, ...candidates.filter((host) => host !== quotaHostInForce)];
@@ -26911,19 +27435,19 @@ async function fetchTokenPlanQuota(credentials, options = {}) {
26911
27435
  });
26912
27436
  }
26913
27437
  if (value === null && reason === null) reason = authRefused ? "token-expired" : "unreachable";
26914
- if (value !== null) quotaLastGood = {
26915
- value,
26916
- at: Date.now()
26917
- };
26918
- else if (quotaLastGood !== null) {
26919
- value = quotaLastGood.value;
26920
- reason = "stale";
27438
+ if (value === null) {
27439
+ const previous = quotaEntryFor(options.accountId);
27440
+ if (previous !== null && previous.value !== null) {
27441
+ value = previous.value;
27442
+ reason = "stale";
27443
+ }
26921
27444
  }
26922
- quotaSnapshot = {
27445
+ rememberQuotaSnapshot({
27446
+ accountId: options.accountId,
26923
27447
  at: Date.now(),
26924
27448
  value,
26925
27449
  reason
26926
- };
27450
+ });
26927
27451
  return value;
26928
27452
  }
26929
27453
  /**
@@ -27961,6 +28485,38 @@ function parseMinimaxCodePoolData(value) {
27961
28485
  return result;
27962
28486
  }
27963
28487
  /**
28488
+ * Nominal length of the two windows the Token Plan reports, in minutes.
28489
+ *
28490
+ * The line's own card names these two keys (`quotaWindowInterval` = 5 小时窗口,
28491
+ * `quotaWindowWeekly` = 每周窗口), and 5 hours / 7 days is what those words mean,
28492
+ * so the shared account card can name an unlabeled window from its length
28493
+ * instead of showing a generic one. The same two numbers label the equivalent
28494
+ * windows on the Command Code and Kimi Code lines.
28495
+ */
28496
+ const WINDOW_MINUTES = {
28497
+ interval: 300,
28498
+ weekly: 10080
28499
+ };
28500
+ /**
28501
+ * One cached read as the shared account card renders it.
28502
+ *
28503
+ * Every window is labeled through its LENGTH rather than with a string: the
28504
+ * canonical window names live in the client's dictionary, and the host has no
28505
+ * business shipping display text. A window whose consumed share nobody measured
28506
+ * is dropped by {@link quotaWindow} — a null percentage is not 0%, it is
28507
+ * "not stated", and a bar would lie about it. An `unlimited` window states no
28508
+ * consumed share at all and the per-account shape has no way to say
28509
+ * "unlimited", so it is dropped too rather than drawn as an empty bar.
28510
+ */
28511
+ function minimaxCodePoolQuota(quota) {
28512
+ if (quota === null || quota === void 0) return void 0;
28513
+ const windows = quota.windows === void 0 || quota.windows.length === 0 ? [quotaWindow$1("", quota.usedPercent, { resetsAt: quota.resetsAtMs })] : quota.windows.map((window) => quotaWindow$1("", window.usedPercent, {
28514
+ windowDurationMins: WINDOW_MINUTES[window.key],
28515
+ resetsAt: window.resetsAtMs
28516
+ }));
28517
+ return poolQuota(quota.fetchedAtMs, windows);
28518
+ }
28519
+ /**
27964
28520
  * MiniMax Code's account pool.
27965
28521
  *
27966
28522
  * It owns rotation and the write-back, exactly like the sibling lines, with one
@@ -28041,7 +28597,11 @@ var MinimaxCodeAccountPool = class extends AccountPoolCore {
28041
28597
  ...planName === void 0 ? {} : {
28042
28598
  planName,
28043
28599
  planLabel: planName
28044
- }
28600
+ },
28601
+ ...(() => {
28602
+ const quota = minimaxCodePoolQuota(getCachedQuotaFor$1(account.id));
28603
+ return quota === void 0 ? {} : { quota };
28604
+ })()
28045
28605
  };
28046
28606
  },
28047
28607
  liveCredentialsFor: (account) => this.nativeAuthority(account),
@@ -28128,6 +28688,34 @@ var MinimaxCodeAccountPool = class extends AccountPoolCore {
28128
28688
  });
28129
28689
  }
28130
28690
  /**
28691
+ * The account a usage read belongs to, together with the credential to read it
28692
+ * with.
28693
+ *
28694
+ * Authored here rather than in the routes because it is provider-shaped: the
28695
+ * routes need a credential and the ACCOUNT it was read for, so a snapshot can
28696
+ * be remembered against the row whose numbers it describes. The selection is
28697
+ * the core's own rule for a caller that names no account — the active one, then
28698
+ * the primary, then the first — repeated in these three lines because the core
28699
+ * exposes it only through `getFreshCredential`, which returns the credential
28700
+ * alone; the credential (and its single-flight refresh, write-back and
28701
+ * bookkeeping) still comes from the core, so no rotation rule lives here.
28702
+ *
28703
+ * No upstream request is made for the selection itself, and none of this moves
28704
+ * the rotation: reading quota is not serving a request.
28705
+ */
28706
+ async getQuotaReadTarget(fetchFn = fetch) {
28707
+ const data = await this.read();
28708
+ const target = data.accounts.find((account) => account.id === data.activeAccountId) ?? data.accounts.find((account) => account.isPrimary) ?? data.accounts[0];
28709
+ if (target === void 0) {
28710
+ await this.getFreshCredential(void 0, fetchFn);
28711
+ throw new Error("MiniMax Code 号池中没有账号。");
28712
+ }
28713
+ return {
28714
+ accountId: target.id,
28715
+ credentials: await this.getFreshCredential(target.id, fetchFn)
28716
+ };
28717
+ }
28718
+ /**
28131
28719
  * Pick the account for the next request.
28132
28720
  *
28133
28721
  * The returned credential carries its own region and record key, so a caller
@@ -28461,8 +29049,12 @@ function registerMinimaxCodeRoutes(ctx, store = new MinimaxCodeCredentialStore()
28461
29049
  * network, and the status route reads the snapshot through the cache instead.
28462
29050
  */
28463
29051
  const refreshQuota = async (force) => {
28464
- const credentials = await (options.accountPool === void 0 ? ensureAccessToken$2(store, { fetchFn }).catch(() => null) : options.accountPool.getFreshCredential(void 0, fetchFn).catch(() => null));
28465
- if (credentials === null) return null;
29052
+ const target = await (options.accountPool === void 0 ? ensureAccessToken$2(store, { fetchFn }).then((credentials) => ({
29053
+ accountId: void 0,
29054
+ credentials
29055
+ })).catch(() => null) : options.accountPool.getQuotaReadTarget(fetchFn).catch(() => null));
29056
+ if (target === null) return null;
29057
+ const credentials = target.credentials;
28466
29058
  const readToken = credentials.refreshToken;
28467
29059
  const isCredentialStale = async () => {
28468
29060
  const live = await store.readFresh().catch(() => null);
@@ -28472,10 +29064,11 @@ function registerMinimaxCodeRoutes(ctx, store = new MinimaxCodeCredentialStore()
28472
29064
  fetchFn,
28473
29065
  force,
28474
29066
  isCredentialStale,
29067
+ ...target.accountId === void 0 ? {} : { accountId: target.accountId },
28475
29068
  renewCredential: async () => await (options.accountPool === void 0 ? ensureAccessToken$2(store, {
28476
29069
  fetchFn,
28477
29070
  force: true
28478
- }) : options.accountPool.renewCredential(void 0, fetchFn)).catch(() => null)
29071
+ }) : options.accountPool.renewCredential(target.accountId, fetchFn)).catch(() => null)
28479
29072
  }).catch(() => null);
28480
29073
  };
28481
29074
  /**
@@ -30540,6 +31133,9 @@ const CATALOG_CACHE_TTL_MS$1 = 1800 * 1e3;
30540
31133
  * every cache miss.
30541
31134
  */
30542
31135
  const catalogSnapshotScopesLoaded = /* @__PURE__ */ new Set();
31136
+ function getCachedCatalog$1() {
31137
+ return cachedCatalog?.models ?? [];
31138
+ }
30543
31139
  function clearCachedCatalog$1() {
30544
31140
  cachedCatalog = void 0;
30545
31141
  catalogCacheEpoch += 1;
@@ -30731,16 +31327,64 @@ function billingMeters(billing) {
30731
31327
  }
30732
31328
  return meters;
30733
31329
  }
30734
- let cachedQuota;
31330
+ /**
31331
+ * One newest snapshot PER ACCOUNT, keyed by the snapshot's own `account.id` —
31332
+ * the same public key the pool addresses an account with.
31333
+ *
31334
+ * Quota follows the account: rotation decides which account spends the next
31335
+ * request, so a single slot only ever described whichever account was read
31336
+ * last. The pool's card draws every account its own reading, so each account
31337
+ * keeps its own entry; past the cap the oldest read is evicted.
31338
+ *
31339
+ * This map is a memory read only. Nothing here fetches — the settings card
31340
+ * builds its account list from it, and that must never cost one upstream
31341
+ * request per pooled account.
31342
+ */
31343
+ const cachedQuotaEntries = /* @__PURE__ */ new Map();
31344
+ /** How many accounts' snapshots are remembered; mirrors the Codex usage service. */
31345
+ const QUOTA_CACHE_LIMIT = 20;
31346
+ /** The entry read most recently, whichever account it belongs to. */
31347
+ function newestCachedQuota() {
31348
+ let newest;
31349
+ for (const snapshot of cachedQuotaEntries.values()) if (newest === void 0 || (snapshot.fetchedAt || 0) > (newest.fetchedAt || 0)) newest = snapshot;
31350
+ return newest;
31351
+ }
31352
+ /** Remember one account's newest snapshot, bounded to the accounts in use. */
31353
+ function rememberCachedQuota(snapshot) {
31354
+ cachedQuotaEntries.set(snapshot.account.id, snapshot);
31355
+ while (cachedQuotaEntries.size > QUOTA_CACHE_LIMIT) {
31356
+ const oldest = [...cachedQuotaEntries.entries()].sort((left, right) => (left[1].fetchedAt || 0) - (right[1].fetchedAt || 0))[0];
31357
+ if (oldest === void 0) break;
31358
+ cachedQuotaEntries.delete(oldest[0]);
31359
+ }
31360
+ }
30735
31361
  let quotaInFlight$1 = null;
30736
31362
  let quotaInFlightAccountId = null;
30737
31363
  let quotaCacheEpoch = 0;
31364
+ /**
31365
+ * The snapshot read most recently, whichever account it belongs to.
31366
+ *
31367
+ * Kept with a no-argument signature for backward compatibility; the page-level
31368
+ * facts use {@link getCachedQuotaFor} instead, because they describe one
31369
+ * account and must never be another account's numbers.
31370
+ */
30738
31371
  function getCachedQuota$5() {
30739
- return cachedQuota;
31372
+ return newestCachedQuota();
31373
+ }
31374
+ /**
31375
+ * The snapshot remembered for one account, if it was read before.
31376
+ *
31377
+ * This is the lookup the account pool calls while it builds its summaries: a
31378
+ * pure memory read, never a fetch. Passing no account keeps the pre-pool
31379
+ * behavior of returning whatever was fetched last.
31380
+ */
31381
+ function getCachedQuotaFor(accountId) {
31382
+ if (accountId === void 0 || accountId === null) return newestCachedQuota();
31383
+ return cachedQuotaEntries.get(accountId);
30740
31384
  }
30741
31385
  function clearCachedQuota$5() {
30742
31386
  quotaCacheEpoch += 1;
30743
- cachedQuota = void 0;
31387
+ cachedQuotaEntries.clear();
30744
31388
  quotaInFlight$1 = null;
30745
31389
  quotaInFlightAccountId = null;
30746
31390
  }
@@ -30757,7 +31401,8 @@ async function fetchAccountQuota$4(store = new FileCredentialStore$4(), fetchFn
30757
31401
  });
30758
31402
  if (credentials === null) throw new Error(`Not signed in to ${PROVIDER_NAME$1}.`);
30759
31403
  const requestedAccountId = workBuddyAccountId(credentials);
30760
- if (!force && cachedQuota && cachedQuota.account.id === requestedAccountId && Date.now() - (cachedQuota.fetchedAt || 0) < 12e4) return cachedQuota;
31404
+ const cached = cachedQuotaEntries.get(requestedAccountId);
31405
+ if (!force && cached !== void 0 && Date.now() - (cached.fetchedAt || 0) < 12e4) return cached;
30761
31406
  if (quotaInFlight$1 && quotaInFlightAccountId === requestedAccountId) return quotaInFlight$1;
30762
31407
  const epoch = quotaCacheEpoch;
30763
31408
  const request = (async () => {
@@ -30793,7 +31438,7 @@ async function fetchAccountQuota$4(store = new FileCredentialStore$4(), fetchFn
30793
31438
  sources: ["billing/meter/get-user-resource"]
30794
31439
  };
30795
31440
  if (epoch !== quotaCacheEpoch) return snapshot;
30796
- cachedQuota = snapshot;
31441
+ rememberCachedQuota(snapshot);
30797
31442
  return snapshot;
30798
31443
  })();
30799
31444
  quotaInFlight$1 = request;
@@ -31108,7 +31753,7 @@ async function getWorkBuddyWebStatus(store, modelSettings, preferences, options
31108
31753
  const catalog = live.length > 0 ? live : FALLBACK_MODELS$1;
31109
31754
  const available = modelsForRegion(credentials?.region ?? "intl", catalog).map((model) => model.id);
31110
31755
  const models = buildModelOptions(catalog, available, resolveEnabledModelIds$2(settings.enabledModelIds, available, enabled), settings.contextWindowOverrides);
31111
- const quota = getCachedQuota$5();
31756
+ const quota = getCachedQuotaFor(credentials === null ? null : accountFromCredentials$1(credentials).id);
31112
31757
  const poolData = options.accountPool === void 0 ? null : await options.accountPool.read().catch(() => null);
31113
31758
  const poolAccounts = options.accountPool === void 0 ? [] : await options.accountPool.listAccounts().catch(() => []);
31114
31759
  const hiddenIds = new Set(settings.hiddenAccountIds);
@@ -31167,12 +31812,10 @@ function registerWorkBuddyRoutes(ctx, store, modelSettings, preferences, options
31167
31812
  accountId: settings.selectedAccountId,
31168
31813
  hiddenAccountIds: settings.hiddenAccountIds
31169
31814
  });
31170
- const cached = getCachedQuota$5();
31171
- const quotaMatches = cached?.account.id === (credentials === null ? void 0 : accountFromCredentials$1(credentials).id);
31172
- if (credentials !== null && (!quotaMatches || cached === void 0 || Date.now() - (cached.fetchedAt || 0) > 12e4)) {
31173
- if (!quotaMatches) clearCachedQuota$5();
31815
+ const cached = getCachedQuotaFor(credentials === null ? null : accountFromCredentials$1(credentials).id);
31816
+ if (credentials !== null && (cached === void 0 || Date.now() - (cached.fetchedAt || 0) > 12e4)) {
31174
31817
  const refresh = () => fetchAccountQuota$4(store, fetchFn, false, settings.selectedAccountId, settings.hiddenAccountIds);
31175
- if (cached === void 0 || !quotaMatches) await quotaRefresh.run(refresh);
31818
+ if (cached === void 0) await quotaRefresh.run(refresh);
31176
31819
  else quotaRefresh.start(refresh);
31177
31820
  }
31178
31821
  return sendJson$1(response, 200, {
@@ -32658,6 +33301,47 @@ function defaultAliasFor(credentials, position) {
32658
33301
  return credentials.nickname || credentials.uin || credentials.uid || `账号 ${position}`;
32659
33302
  }
32660
33303
  /**
33304
+ * A meter's consumed share, from whichever fraction this line measured.
33305
+ *
33306
+ * `usedFraction` is the DTO's own "share consumed"; `remainingFraction` is the
33307
+ * same statement read from the other end, and is the one the rest of the card
33308
+ * renders. A meter that states neither is dropped (null) rather than drawn at
33309
+ * 0, which would claim a full window no reading supports.
33310
+ */
33311
+ function meterUsedPercent(meter) {
33312
+ if (typeof meter.usedFraction === "number" && Number.isFinite(meter.usedFraction)) return meter.usedFraction * 100;
33313
+ if (typeof meter.remainingFraction === "number" && Number.isFinite(meter.remainingFraction)) return (1 - meter.remainingFraction) * 100;
33314
+ return null;
33315
+ }
33316
+ /**
33317
+ * One cached snapshot as the shared account card renders it.
33318
+ *
33319
+ * Only what the DTO states is published: every meter that measured a share
33320
+ * becomes a window, and a meter that measured none is dropped rather than drawn
33321
+ * at 0 — which would claim an unspent allowance no reading supports.
33322
+ *
33323
+ * Exported for the pool's regression tests; the card only reaches it through
33324
+ * the summary hook.
33325
+ */
33326
+ function workBuddyPoolQuota(snapshot) {
33327
+ const windows = snapshot.meters.map((meter) => quotaWindow$1(meter.label, meterUsedPercent(meter), { resetsAt: meter.resetsAt }));
33328
+ if (!snapshot.meters.some((meter) => meter.id === "cycle") && snapshot.cycleCredits !== null && snapshot.cycleCredits > 0 && snapshot.cycleUsedCredits !== null) windows.push(quotaWindow$1(snapshot.packageName ?? "Billing cycle", snapshot.cycleUsedCredits / snapshot.cycleCredits * 100, { resetsAt: snapshot.cycleEndsAt }));
33329
+ return poolQuota(snapshot.fetchedAt, windows);
33330
+ }
33331
+ /**
33332
+ * This account's own newest snapshot, ready for the account card.
33333
+ *
33334
+ * Quota follows the account, so the pool reports the snapshot the line already
33335
+ * remembered for THIS account id. The lookup is a pure memory read: the card
33336
+ * builds its account list here, and that must never cost one upstream request
33337
+ * per pooled account. Undefined means exactly that — never read — which is not
33338
+ * the same statement as "nothing used".
33339
+ */
33340
+ function quotaFor(accountId) {
33341
+ const snapshot = getCachedQuotaFor(accountId);
33342
+ return snapshot === void 0 ? void 0 : workBuddyPoolQuota(snapshot);
33343
+ }
33344
+ /**
32661
33345
  * WorkBuddy's account pool.
32662
33346
  *
32663
33347
  * Desktop accounts are adopted into the pool so they can be scheduled and
@@ -32718,7 +33402,11 @@ var WorkBuddyAccountPool = class extends AccountPoolCore {
32718
33402
  ...account.credentials.domain === "" ? {} : { domain: account.credentials.domain },
32719
33403
  ...account.credentials.backend === "" ? {} : { backend: account.credentials.backend },
32720
33404
  ...account.credentials.accountType === void 0 ? {} : { accountType: account.credentials.accountType },
32721
- ...account.credentials.expiresAt > 0 ? {} : { expiresAt: void 0 }
33405
+ ...account.credentials.expiresAt > 0 ? {} : { expiresAt: void 0 },
33406
+ ...(() => {
33407
+ const quota = quotaFor(account.id);
33408
+ return quota === void 0 ? {} : { quota };
33409
+ })()
32722
33410
  }),
32723
33411
  preferAccountId: () => this.resolveSelectionId(options.selection?.().selectedAccountId ?? null),
32724
33412
  emptyMessage: `Not signed in to ${PROVIDER_NAME$1}. Sign in with the CodeBuddy desktop client, or add an account from Settings > WorkBuddy.`,
@@ -34727,6 +35415,10 @@ const catalogInFlight = /* @__PURE__ */ new Map();
34727
35415
  function clearCachedCatalog() {
34728
35416
  catalogCache = null;
34729
35417
  }
35418
+ /** The listing currently held, or an empty array before the first load. */
35419
+ function getCachedCatalog() {
35420
+ return catalogCache?.models ?? [];
35421
+ }
34730
35422
  /**
34731
35423
  * Read one entry of the live listing.
34732
35424
  *
@@ -35022,22 +35714,108 @@ function parseQuotaHeaders(headers) {
35022
35714
  }
35023
35715
  };
35024
35716
  }
35025
- let quotaCache = null;
35717
+ /**
35718
+ * Cache key of one account's snapshot.
35719
+ *
35720
+ * A pool row id when the caller names one — that is the identity that survives a
35721
+ * token refresh and the one the account card asks with — and otherwise the
35722
+ * credential's token tail, which is all a single-credential process has. The
35723
+ * `acc:` prefix keeps the two apart, so no row id can ever be answered with an
35724
+ * entry written under a token tail.
35725
+ */
35726
+ function quotaEntryKey(accountId, credentials) {
35727
+ return accountId === void 0 || accountId === "" ? accountKeyFor(credentials) : `acc:${accountId}`;
35728
+ }
35729
+ /**
35730
+ * How many accounts' snapshots are remembered.
35731
+ *
35732
+ * Mirrors the bound the ChatGPT line's usage service applies to its own
35733
+ * per-account map: room for every account a user is likely to pool, and no way
35734
+ * for a long-lived process to accumulate one snapshot per token it has seen.
35735
+ */
35736
+ const QUOTA_ACCOUNT_LIMIT = 20;
35737
+ /**
35738
+ * The newest snapshot of each account, keyed by {@link quotaEntryKey}.
35739
+ *
35740
+ * ONE ENTRY PER ACCOUNT rather than one slot for the line: quota follows the
35741
+ * account, and rotation decides which account spends the next request, so a
35742
+ * single slot could only ever describe whichever account was read last. This is
35743
+ * a memory read only — the settings card builds its account list from it, and
35744
+ * that must never cost one upstream request per pooled account.
35745
+ */
35746
+ const quotaCache = /* @__PURE__ */ new Map();
35747
+ /**
35748
+ * Key of the entry written most recently.
35749
+ *
35750
+ * The pre-pool card asks "whatever was read last", and a header reading that
35751
+ * names no account may only merge into the snapshot it was read beside. Both
35752
+ * need the last WRITE rather than a scan: two accounts can be written inside one
35753
+ * millisecond, and a tie would otherwise be broken by map order.
35754
+ */
35755
+ let quotaLastKey = null;
35026
35756
  const quotaInFlight = /* @__PURE__ */ new Map();
35027
- /** Drop the cached snapshot. Called on sign-out and after a credential change. */
35757
+ /** Drop every cached snapshot. Called on sign-out and after a credential change. */
35028
35758
  function clearCachedQuota$1() {
35029
- quotaCache = null;
35759
+ quotaCache.clear();
35760
+ quotaLastKey = null;
35761
+ }
35762
+ /** The entry written most recently, whichever account it belongs to. */
35763
+ function newestQuotaEntry() {
35764
+ return quotaLastKey === null ? null : quotaCache.get(quotaLastKey) ?? null;
35765
+ }
35766
+ /**
35767
+ * Remember one account's newest snapshot, bounded to the accounts in use.
35768
+ *
35769
+ * The oldest entry is measured by {@link QuotaCacheEntry.observedAt} rather than
35770
+ * by `fetchedAt`: a snapshot kept warm by response headers has no full read
35771
+ * behind it at all, and it is still the newest thing known about that account.
35772
+ */
35773
+ function rememberQuota(entry) {
35774
+ quotaCache.set(entry.key, entry);
35775
+ quotaLastKey = entry.key;
35776
+ while (quotaCache.size > QUOTA_ACCOUNT_LIMIT) {
35777
+ const oldest = [...quotaCache.entries()].sort((left, right) => left[1].observedAt - right[1].observedAt)[0];
35778
+ if (oldest === void 0) break;
35779
+ quotaCache.delete(oldest[0]);
35780
+ if (quotaLastKey === oldest[0]) quotaLastKey = null;
35781
+ }
35030
35782
  }
35031
35783
  /**
35032
35784
  * The cached snapshot, optionally only when it belongs to this credential.
35033
35785
  *
35034
35786
  * With one stored account the argument is unnecessary; it exists so a later
35035
35787
  * pool chunk cannot render one account's usage under another account's name.
35788
+ * Given a credential the answer is THAT account's own newest snapshot, which is
35789
+ * what keeps two pooled accounts apart: a later read of a different account
35790
+ * cannot displace it. Without one the pre-pool answer is kept — whatever was
35791
+ * written last — because the single-account card has no identity to ask with.
35036
35792
  */
35037
35793
  function getCachedQuota$1(credentials) {
35038
- if (quotaCache === null) return null;
35039
- if (credentials === void 0) return quotaCache.quota;
35040
- return quotaCache.key === accountKeyFor(credentials) ? quotaCache.quota : null;
35794
+ if (credentials === void 0) return newestQuotaEntry()?.quota ?? null;
35795
+ return cachedQuotaFor(credentials);
35796
+ }
35797
+ /**
35798
+ * The newest snapshot held for one credential's account, for display only.
35799
+ *
35800
+ * The token-tail form, kept for a composition with a single stored credential.
35801
+ * A pooled caller asks {@link cachedQuotaForPool} with the row id instead: a
35802
+ * token is rotated by every refresh, so an entry filed under a tail is lost the
35803
+ * moment the account renews.
35804
+ */
35805
+ function cachedQuotaFor(credentials) {
35806
+ return quotaCache.get(accountKeyFor(credentials))?.quota ?? null;
35807
+ }
35808
+ /**
35809
+ * The newest snapshot held for one pool row, for display only.
35810
+ *
35811
+ * The pool asks here while it builds the account list for the settings card, so
35812
+ * this is a pure memory read: it never fetches. Null means nothing was ever read
35813
+ * for this account, which is a different statement from "nothing used". A row id
35814
+ * that is undefined falls back to the credential's own tail, which is what a
35815
+ * composition without a pool has.
35816
+ */
35817
+ function cachedQuotaForPool(accountId, credentials) {
35818
+ return quotaCache.get(quotaEntryKey(accountId, credentials))?.quota ?? null;
35041
35819
  }
35042
35820
  /**
35043
35821
  * Read the account's usage meters.
@@ -35055,12 +35833,13 @@ function getCachedQuota$1(credentials) {
35055
35833
  * up staring at a card that claims they are fine.
35056
35834
  */
35057
35835
  function fetchAccountQuota$1(credentials, options = {}) {
35058
- const key = accountKeyFor(credentials);
35836
+ const key = quotaEntryKey(options.accountId, credentials);
35059
35837
  const at = now();
35060
- if (options.force !== true && quotaCache !== null && quotaCache.key === key) {
35061
- const warm = at - quotaCache.observedAt < QUOTA_CACHE_TTL_MS;
35062
- const freshFullRead = quotaCache.fullReadAt !== null && at - quotaCache.fullReadAt < 3e5;
35063
- if (warm && freshFullRead) return Promise.resolve(quotaCache.quota);
35838
+ const cached = quotaCache.get(key);
35839
+ if (options.force !== true && cached !== void 0) {
35840
+ const warm = at - cached.observedAt < QUOTA_CACHE_TTL_MS;
35841
+ const freshFullRead = cached.fullReadAt !== null && at - cached.fullReadAt < 3e5;
35842
+ if (warm && freshFullRead) return Promise.resolve(cached.quota);
35064
35843
  }
35065
35844
  const pending = quotaInFlight.get(key);
35066
35845
  if (pending !== void 0) return pending;
@@ -35075,9 +35854,9 @@ function fetchAccountQuota$1(credentials, options = {}) {
35075
35854
  return run;
35076
35855
  }
35077
35856
  async function performQuotaFetch(credentials, options) {
35078
- const key = accountKeyFor(credentials);
35857
+ const key = quotaEntryKey(options.accountId, credentials);
35079
35858
  const fetchFn = options.fetchFn ?? fetch;
35080
- const previous = quotaCache !== null && quotaCache.key === key ? quotaCache.quota : null;
35859
+ const previous = quotaCache.get(key)?.quota ?? null;
35081
35860
  let response;
35082
35861
  try {
35083
35862
  response = await fetchFn("https://api.anthropic.com/api/oauth/usage", {
@@ -35115,12 +35894,12 @@ async function performQuotaFetch(credentials, options) {
35115
35894
  status: null,
35116
35895
  representativeClaim: null
35117
35896
  };
35118
- quotaCache = {
35897
+ rememberQuota({
35119
35898
  quota,
35120
35899
  key,
35121
35900
  fullReadAt: at,
35122
35901
  observedAt: at
35123
- };
35902
+ });
35124
35903
  return quota;
35125
35904
  }
35126
35905
  /**
@@ -39747,6 +40526,32 @@ async function refreshClaudeCredential(credentials, fetchFn) {
39747
40526
  };
39748
40527
  }
39749
40528
  /**
40529
+ * One account's own newest quota snapshot, in the shape the account card draws.
40530
+ *
40531
+ * The snapshot comes from 'client.ts''s per-account cache, and this is where
40532
+ * this line's own numbers are translated into the shared shape. Every judgement
40533
+ * below trades a possibly-nice-looking bar for an honest one:
40534
+ *
40535
+ * - a window whose consumed share nobody measured is DROPPED (`quotaWindow`
40536
+ * returns null), because "0% used" and "not stated" are different claims;
40537
+ * - the share falls back to `100 - remainingPercent` only when that field was
40538
+ * stated; when the payload stated NEITHER share there is nothing to draw;
40539
+ * - the read time is the full read's own `fetchedAt`, or — for a snapshot that
40540
+ * only response headers ever kept warm, which performed no full read — the
40541
+ * `observedAt` instant those numbers arrived. Either way it is when this line
40542
+ * saw the numbers, never when upstream served them.
40543
+ *
40544
+ * A null snapshot means "this account was never read", which `poolQuota` reports
40545
+ * as undefined so the card can say exactly that instead of drawing a zero.
40546
+ */
40547
+ function claudePoolQuota(snapshot) {
40548
+ if (snapshot === null) return void 0;
40549
+ return poolQuota(snapshot.fetchedAt ?? snapshot.observedAt, snapshot.windows.map((window) => quotaWindow$1(window.label, window.usedPercent ?? (window.remainingPercent === null ? null : 100 - window.remainingPercent), {
40550
+ windowDurationMins: window.windowMinutes,
40551
+ resetsAt: typeof window.resetsAt === "string" ? Date.parse(window.resetsAt) : null
40552
+ })));
40553
+ }
40554
+ /**
39750
40555
  * The Claude subscription account pool.
39751
40556
  *
39752
40557
  * Read the module comment first: it carries the two rules this class exists to
@@ -39844,6 +40649,7 @@ var ClaudeAccountPool = class extends AccountPoolCore {
39844
40649
  const subscriptionType = account.subscriptionType ?? credentials.subscriptionType;
39845
40650
  const adopted = account.adopted === true;
39846
40651
  const expired = adopted && adoptedCredentialExpired(credentials);
40652
+ const quota = claudePoolQuota(cachedQuotaForPool(account.id, credentials));
39847
40653
  return {
39848
40654
  ...base,
39849
40655
  ...email === void 0 ? {} : { email },
@@ -39855,6 +40661,7 @@ var ClaudeAccountPool = class extends AccountPoolCore {
39855
40661
  source: account.source,
39856
40662
  removable: !adopted,
39857
40663
  ...account.sourcePath === void 0 ? {} : { sourcePath: account.sourcePath },
40664
+ ...quota === void 0 ? {} : { quota },
39858
40665
  ...expired ? {
39859
40666
  authStatus: "expired",
39860
40667
  authFailedReason: ADOPTED_CREDENTIAL_EXPIRED_HINT
@@ -40421,7 +41228,7 @@ async function getClaudeWebStatus(store, modelSettings, preferences, options = {
40421
41228
  const catalog = live.length > 0 ? live : FALLBACK_MODELS;
40422
41229
  const available = catalog.map((model) => model.id);
40423
41230
  const models = buildClaudeModelOptions(catalog, available, resolveEnabledModelIds(settings.enabledModelIds, available, enabled), settings.contextWindowOverrides);
40424
- const quota = credential === void 0 ? getCachedQuota$1() : getCachedQuota$1(credential);
41231
+ const quota = credential === void 0 ? getCachedQuota$1() : cachedQuotaForPool(active.id, credential);
40425
41232
  const poolData = options.accountPool === void 0 ? null : await options.accountPool.read().catch(() => null);
40426
41233
  const poolAccounts = options.accountPool === void 0 ? [] : await options.accountPool.listAccounts().catch(() => []);
40427
41234
  const presence = await claudeCodeCredentialPresence(options.adoptPaths ?? claudeCodeCredentialPaths()).catch(() => ({
@@ -40555,13 +41362,14 @@ function registerClaudeRoutes(ctx, store, modelSettings, preferences, options =
40555
41362
  if (path === "" || path === "status") {
40556
41363
  if (method !== "GET") return sendMethodNotAllowed(response);
40557
41364
  const active = await activeAccount();
40558
- const cached = active.credentials === void 0 ? null : getCachedQuota$1(active.credentials);
41365
+ const cached = active.credentials === void 0 ? null : cachedQuotaForPool(active.id, active.credentials);
40559
41366
  const stale = cached === null || Date.now() - (cached.fetchedAt || 0) > 12e4;
40560
41367
  if (active.credentials !== void 0 && stale) {
40561
41368
  const credentials = active.credentials;
40562
41369
  const refresh = () => fetchAccountQuota$1(credentials, {
40563
41370
  fetchFn,
40564
- signal: quotaAbort.signal
41371
+ signal: quotaAbort.signal,
41372
+ accountId: active.id
40565
41373
  });
40566
41374
  if (cached === null) await quotaRefresh.run(refresh);
40567
41375
  else quotaRefresh.start(refresh);
@@ -40683,7 +41491,8 @@ function registerClaudeRoutes(ctx, store, modelSettings, preferences, options =
40683
41491
  await fetchAccountQuota$1(credentials, {
40684
41492
  fetchFn,
40685
41493
  force: method === "POST",
40686
- signal: quotaAbort.signal
41494
+ signal: quotaAbort.signal,
41495
+ accountId: active.id
40687
41496
  });
40688
41497
  } catch (error) {
40689
41498
  quotaError = error instanceof Error ? error.message : String(error);
@@ -44299,39 +45108,24 @@ function apply(ctx, pluginConfig = {}) {
44299
45108
  provider,
44300
45109
  limits: providerLimits
44301
45110
  })]));
44302
- const antigravityAdapter = new AntigravityAdapter(antigravityStore, antigravityModelSettings, antigravityPreferences, {
44303
- fetchFn: modelFetch.antigravity,
44304
- attachments: ctx.attachments
44305
- }, antigravityAccountPool);
44306
- let antigravityRegistration;
44307
- let antigravityConflict = null;
44308
- const claimAntigravityRoute = () => {
44309
- if (antigravityRegistration !== void 0) return;
44310
- try {
44311
- antigravityRegistration = ctx.llm.registerAdapter([PROVIDER_ID$6], antigravityAdapter);
44312
- if (antigravityConflict !== null) ctx.logger.info(`[dsh-chatgpt-subscription] Antigravity route "${PROVIDER_ID$6}" is now served by this plugin`);
44313
- antigravityConflict = null;
44314
- } catch (error) {
44315
- antigravityConflict = error instanceof Error ? error.message : String(error);
44316
- ctx.logger.warn(`[dsh-chatgpt-subscription] provider route "${PROVIDER_ID$6}" is already owned by another adapter; Antigravity models keep being served by that one until its configuration is removed (${antigravityConflict})`);
44317
- }
44318
- };
44319
- claimAntigravityRoute();
44320
- const antigravityRouteWatch = typeof ctx.on === "function" ? ctx.on("llm/adapters-updated", () => {
44321
- claimAntigravityRoute();
44322
- }) : void 0;
45111
+ const antigravityClaim = claimProviderRoute(ctx, {
45112
+ providerId: PROVIDER_ID$6,
45113
+ label: PROVIDER_NAME$6,
45114
+ adapter: new AntigravityAdapter(antigravityStore, antigravityModelSettings, antigravityPreferences, {
45115
+ fetchFn: modelFetch.antigravity,
45116
+ attachments: ctx.attachments
45117
+ }, antigravityAccountPool)
45118
+ });
44323
45119
  const disposeAntigravityRoutes = registerAntigravityRoutes(ctx, antigravityStore, antigravityModelSettings, antigravityPreferences, proxyFetch, antigravityAccountPool);
44324
45120
  const disposeCommandCodeRoutes = registerCommandCodeRoutes(ctx, commandCodeStore, commandCodeModelSettings, commandCodePreferences, {
44325
45121
  fetchFn: proxyFetch,
44326
- serving: () => commandCodeRegistration !== void 0,
44327
- conflict: () => commandCodeConflict
45122
+ serving: () => commandCodeClaim.serving(),
45123
+ conflict: () => commandCodeClaim.conflict()
44328
45124
  }, commandCodeAccountPool);
44329
45125
  const commandCodeAdapter = new CommandCodeAdapter(commandCodeStore, commandCodeModelSettings, commandCodePreferences, {
44330
45126
  fetchFn: modelFetch["command-code"],
44331
45127
  attachments: ctx.attachments
44332
45128
  }, commandCodeAccountPool);
44333
- let commandCodeRegistration;
44334
- let commandCodeConflict = null;
44335
45129
  const ollamaStore = new FileCredentialStore$5();
44336
45130
  const ollamaModelSettings = new FileModelSettingsStore$5();
44337
45131
  const ollamaAccountPool = new OllamaAccountPool({ store: ollamaStore });
@@ -44339,92 +45133,48 @@ function apply(ctx, pluginConfig = {}) {
44339
45133
  fetchFn: modelFetch.ollama,
44340
45134
  attachments: ctx.attachments
44341
45135
  }, ollamaAccountPool);
44342
- let ollamaRegistration;
44343
- let ollamaConflict = null;
44344
- const kimiCodeAdapter = new KimiCodeAdapter(kimiCodeStore, kimiCodeModelSettings, kimiCodePreferences, {
44345
- fetchFn: modelFetch["kimi-code"],
44346
- attachments: ctx.attachments,
44347
- videos: { readVideo: async (ref) => ({
44348
- data: await readVideoBytes(ref),
44349
- mediaType: ref.mediaType
44350
- }) }
44351
- }, kimiCodeAccountPool);
44352
- let kimiCodeRegistration;
44353
- let kimiCodeConflict = null;
44354
- const claimKimiCodeRoute = () => {
44355
- if (kimiCodeRegistration !== void 0) return;
44356
- try {
44357
- kimiCodeRegistration = ctx.llm.registerAdapter([PROVIDER_ID$3], kimiCodeAdapter);
44358
- if (kimiCodeConflict !== null) ctx.logger.info(`[dsh-chatgpt-subscription] ${PROVIDER_NAME$3} route "${PROVIDER_ID$3}" is now served by this plugin`);
44359
- kimiCodeConflict = null;
44360
- } catch (error) {
44361
- kimiCodeConflict = error instanceof Error ? error.message : String(error);
44362
- ctx.logger.warn(`[dsh-chatgpt-subscription] provider route "${PROVIDER_ID$3}" is already owned by another adapter; ${PROVIDER_NAME$3} models keep being served by that one until its configuration is removed (${kimiCodeConflict})`);
44363
- }
44364
- };
44365
- claimKimiCodeRoute();
44366
- const kimiCodeRouteWatch = typeof ctx.on === "function" ? ctx.on("llm/adapters-updated", () => {
44367
- claimKimiCodeRoute();
44368
- }) : void 0;
44369
- const claimOllamaRoute = () => {
44370
- if (ollamaRegistration !== void 0) return;
44371
- try {
44372
- ollamaRegistration = ctx.llm.registerAdapter([PROVIDER_ID$4], ollamaAdapter);
44373
- if (ollamaConflict !== null) ctx.logger.info(`[dsh-chatgpt-subscription] ${PROVIDER_NAME$4} route "${PROVIDER_ID$4}" is now served by this plugin`);
44374
- ollamaConflict = null;
44375
- } catch (error) {
44376
- ollamaConflict = error instanceof Error ? error.message : String(error);
44377
- ctx.logger.warn(`[dsh-chatgpt-subscription] provider route "${PROVIDER_ID$4}" is already owned by another adapter; ${PROVIDER_NAME$4} models keep being served by that one until its configuration is removed (${ollamaConflict})`);
44378
- }
44379
- };
44380
- claimOllamaRoute();
45136
+ const kimiCodeClaim = claimProviderRoute(ctx, {
45137
+ providerId: PROVIDER_ID$3,
45138
+ label: PROVIDER_NAME$3,
45139
+ adapter: new KimiCodeAdapter(kimiCodeStore, kimiCodeModelSettings, kimiCodePreferences, {
45140
+ fetchFn: modelFetch["kimi-code"],
45141
+ attachments: ctx.attachments,
45142
+ videos: { readVideo: async (ref) => ({
45143
+ data: await readVideoBytes(ref),
45144
+ mediaType: ref.mediaType
45145
+ }) }
45146
+ }, kimiCodeAccountPool)
45147
+ });
45148
+ const ollamaClaim = claimProviderRoute(ctx, {
45149
+ providerId: PROVIDER_ID$4,
45150
+ label: PROVIDER_NAME$4,
45151
+ adapter: ollamaAdapter,
45152
+ watch: false
45153
+ });
44381
45154
  const disposeOllamaRoutes = registerOllamaRoutes(ctx, {
44382
45155
  accountPool: ollamaAccountPool,
44383
45156
  modelSettings: ollamaModelSettings,
44384
45157
  fetchFn: proxyFetch
44385
45158
  });
44386
- const claimCommandCodeRoute = () => {
44387
- if (commandCodeRegistration !== void 0) return;
44388
- try {
44389
- commandCodeRegistration = ctx.llm.registerAdapter([PROVIDER_ID$5], commandCodeAdapter);
44390
- if (commandCodeConflict !== null) ctx.logger.info(`[dsh-chatgpt-subscription] ${PROVIDER_NAME$5} route "${PROVIDER_ID$5}" is now served by this plugin`);
44391
- commandCodeConflict = null;
44392
- } catch (error) {
44393
- commandCodeConflict = error instanceof Error ? error.message : String(error);
44394
- ctx.logger.warn(`[dsh-chatgpt-subscription] provider route "${PROVIDER_ID$5}" is already owned by another adapter; ${PROVIDER_NAME$5} models keep being served by that one until its configuration is removed (${commandCodeConflict})`);
44395
- }
44396
- };
44397
- claimCommandCodeRoute();
44398
- const commandCodeRouteWatch = typeof ctx.on === "function" ? ctx.on("llm/adapters-updated", () => {
44399
- claimCommandCodeRoute();
44400
- }) : void 0;
45159
+ const commandCodeClaim = claimProviderRoute(ctx, {
45160
+ providerId: PROVIDER_ID$5,
45161
+ label: PROVIDER_NAME$5,
45162
+ adapter: commandCodeAdapter
45163
+ });
44401
45164
  const disposeKimiCodeRoutes = registerKimiCodeRoutes(ctx, kimiCodeStore, kimiCodeModelSettings, kimiCodePreferences, {
44402
45165
  fetchFn: proxyFetch,
44403
- serving: () => kimiCodeRegistration !== void 0,
44404
- conflict: () => kimiCodeConflict
45166
+ serving: () => kimiCodeClaim.serving(),
45167
+ conflict: () => kimiCodeClaim.conflict()
44405
45168
  }, kimiCodeAccountPool);
44406
- const minimaxCodeAdapter = new MinimaxCodeAdapter(minimaxCodeStore, {
44407
- fetchFn: modelFetch["minimax-code"],
44408
- attachments: ctx.attachments,
44409
- accountPool: minimaxCodeAccountPool
44410
- }, minimaxCodeModelSettings, minimaxCodePreferences);
44411
- let minimaxCodeRegistration;
44412
- let minimaxCodeConflict = null;
44413
- const claimMinimaxCodeRoute = () => {
44414
- if (minimaxCodeRegistration !== void 0) return;
44415
- try {
44416
- minimaxCodeRegistration = ctx.llm.registerAdapter([PROVIDER_ID$2], minimaxCodeAdapter);
44417
- if (minimaxCodeConflict !== null) ctx.logger.info(`[dsh-chatgpt-subscription] ${PROVIDER_NAME$2} route "${PROVIDER_ID$2}" is now served by this plugin`);
44418
- minimaxCodeConflict = null;
44419
- } catch (error) {
44420
- minimaxCodeConflict = error instanceof Error ? error.message : String(error);
44421
- ctx.logger.warn(`[dsh-chatgpt-subscription] provider route "${PROVIDER_ID$2}" is already owned by another adapter; ${PROVIDER_NAME$2} models keep being served by that one until its configuration is removed (${minimaxCodeConflict})`);
44422
- }
44423
- };
44424
- claimMinimaxCodeRoute();
44425
- const minimaxCodeRouteWatch = typeof ctx.on === "function" ? ctx.on("llm/adapters-updated", () => {
44426
- claimMinimaxCodeRoute();
44427
- }) : void 0;
45169
+ const minimaxCodeClaim = claimProviderRoute(ctx, {
45170
+ providerId: PROVIDER_ID$2,
45171
+ label: PROVIDER_NAME$2,
45172
+ adapter: new MinimaxCodeAdapter(minimaxCodeStore, {
45173
+ fetchFn: modelFetch["minimax-code"],
45174
+ attachments: ctx.attachments,
45175
+ accountPool: minimaxCodeAccountPool
45176
+ }, minimaxCodeModelSettings, minimaxCodePreferences)
45177
+ });
44428
45178
  let minimaxCodeAppVersion;
44429
45179
  try {
44430
45180
  const manifest = createRequire(import.meta.url)("../package.json");
@@ -44448,33 +45198,20 @@ function apply(ctx, pluginConfig = {}) {
44448
45198
  minimaxCodeCheckinTick();
44449
45199
  const disposeMinimaxCodeRoutes = registerMinimaxCodeRoutes(ctx, minimaxCodeStore, {
44450
45200
  fetchFn: proxyFetch,
44451
- serving: () => minimaxCodeRegistration !== void 0,
44452
- conflict: () => minimaxCodeConflict,
45201
+ serving: () => minimaxCodeClaim.serving(),
45202
+ conflict: () => minimaxCodeClaim.conflict(),
44453
45203
  accountPool: minimaxCodeAccountPool,
44454
45204
  checkin: minimaxCodeCheckin
44455
45205
  }, minimaxCodeModelSettings, minimaxCodePreferences);
44456
- const workBuddyAdapter = new WorkBuddyAdapter(workBuddyStore, workBuddyModelSettings, workBuddyPreferences, {
44457
- fetchFn: modelFetch.workbuddy,
44458
- attachments: ctx.attachments,
44459
- accountPool: workBuddyAccountPool
45206
+ const workBuddyClaim = claimProviderRoute(ctx, {
45207
+ providerId: PROVIDER_ID$1,
45208
+ label: PROVIDER_NAME$1,
45209
+ adapter: new WorkBuddyAdapter(workBuddyStore, workBuddyModelSettings, workBuddyPreferences, {
45210
+ fetchFn: modelFetch.workbuddy,
45211
+ attachments: ctx.attachments,
45212
+ accountPool: workBuddyAccountPool
45213
+ })
44460
45214
  });
44461
- let workBuddyRegistration;
44462
- let workBuddyConflict = null;
44463
- const claimWorkBuddyRoute = () => {
44464
- if (workBuddyRegistration !== void 0) return;
44465
- try {
44466
- workBuddyRegistration = ctx.llm.registerAdapter([PROVIDER_ID$1], workBuddyAdapter);
44467
- if (workBuddyConflict !== null) ctx.logger.info(`[dsh-chatgpt-subscription] ${PROVIDER_NAME$1} route "${PROVIDER_ID$1}" is now served by this plugin`);
44468
- workBuddyConflict = null;
44469
- } catch (error) {
44470
- workBuddyConflict = error instanceof Error ? error.message : String(error);
44471
- ctx.logger.warn(`[dsh-chatgpt-subscription] provider route "${PROVIDER_ID$1}" is already owned by another adapter; ${PROVIDER_NAME$1} models keep being served by that one until its configuration is removed (${workBuddyConflict})`);
44472
- }
44473
- };
44474
- claimWorkBuddyRoute();
44475
- const workBuddyRouteWatch = typeof ctx.on === "function" ? ctx.on("llm/adapters-updated", () => {
44476
- claimWorkBuddyRoute();
44477
- }) : void 0;
44478
45215
  const workBuddyCheckin = new WorkBuddyCheckinService(workBuddyStore, {
44479
45216
  fetchFn: proxyFetch,
44480
45217
  settings: () => workBuddyPreferences.status().checkin,
@@ -44490,37 +45227,24 @@ function apply(ctx, pluginConfig = {}) {
44490
45227
  checkinTick();
44491
45228
  const disposeWorkBuddyRoutes = registerWorkBuddyRoutes(ctx, workBuddyStore, workBuddyModelSettings, workBuddyPreferences, {
44492
45229
  fetchFn: proxyFetch,
44493
- serving: () => workBuddyRegistration !== void 0,
44494
- conflict: () => workBuddyConflict,
45230
+ serving: () => workBuddyClaim.serving(),
45231
+ conflict: () => workBuddyClaim.conflict(),
44495
45232
  accountPool: workBuddyAccountPool,
44496
45233
  checkin: workBuddyCheckin
44497
45234
  });
44498
- const claudeAdapter = new ClaudeAdapter(claudeStore, claudeModelSettings, claudePreferences, {
44499
- fetchFn: modelFetch.claude,
44500
- attachments: ctx.attachments,
44501
- accountPool: claudeAccountPool
45235
+ const claudeClaim = claimProviderRoute(ctx, {
45236
+ providerId: PROVIDER_ID,
45237
+ label: PROVIDER_NAME,
45238
+ adapter: new ClaudeAdapter(claudeStore, claudeModelSettings, claudePreferences, {
45239
+ fetchFn: modelFetch.claude,
45240
+ attachments: ctx.attachments,
45241
+ accountPool: claudeAccountPool
45242
+ })
44502
45243
  });
44503
- let claudeRegistration;
44504
- let claudeConflict = null;
44505
- const claimClaudeRoute = () => {
44506
- if (claudeRegistration !== void 0) return;
44507
- try {
44508
- claudeRegistration = ctx.llm.registerAdapter([PROVIDER_ID], claudeAdapter);
44509
- if (claudeConflict !== null) ctx.logger.info(`[dsh-chatgpt-subscription] ${PROVIDER_NAME} route "${PROVIDER_ID}" is now served by this plugin`);
44510
- claudeConflict = null;
44511
- } catch (error) {
44512
- claudeConflict = error instanceof Error ? error.message : String(error);
44513
- ctx.logger.warn(`[dsh-chatgpt-subscription] provider route "${PROVIDER_ID}" is already owned by another adapter; ${PROVIDER_NAME} models keep being served by that one until its configuration is removed (${claudeConflict})`);
44514
- }
44515
- };
44516
- claimClaudeRoute();
44517
- const claudeRouteWatch = typeof ctx.on === "function" ? ctx.on("llm/adapters-updated", () => {
44518
- claimClaudeRoute();
44519
- }) : void 0;
44520
45244
  const disposeClaudeRoutes = registerClaudeRoutes(ctx, claudeStore, claudeModelSettings, claudePreferences, {
44521
45245
  fetchFn: proxyFetch,
44522
- serving: () => claudeRegistration !== void 0,
44523
- conflict: () => claudeConflict,
45246
+ serving: () => claudeClaim.serving(),
45247
+ conflict: () => claudeClaim.conflict(),
44524
45248
  accountPool: claudeAccountPool
44525
45249
  });
44526
45250
  workBuddyAccountPool.syncDesktopAccounts().catch(() => void 0);
@@ -44531,7 +45255,11 @@ function apply(ctx, pluginConfig = {}) {
44531
45255
  pool: codexAccountPool
44532
45256
  });
44533
45257
  const usage = new UsageService(oauth, { fetchFn: proxyFetch });
44534
- codexAccountPool.setQuotaBlockedUntil((account, now) => usage.blockedUntilFor(account.credentials, now));
45258
+ codexAccountPool.setQuotaBlockedUntil((account, now) => usage.blockedUntilFor(account.id, account.credentials, now));
45259
+ codexAccountPool.setQuotaSnapshot((account) => {
45260
+ const snapshot = usage.snapshotFor(account.id, account.credentials);
45261
+ return snapshot === void 0 ? void 0 : codexAccountQuota(snapshot);
45262
+ });
44535
45263
  const adapter = new CodexChatGptAdapter(new ResponsesClient(oauth, ctx.attachments, {
44536
45264
  fetchFn: modelFetch["codex-chatgpt"],
44537
45265
  accountPool: codexAccountPool,
@@ -44587,6 +45315,145 @@ function apply(ctx, pluginConfig = {}) {
44587
45315
  });
44588
45316
  };
44589
45317
  const disposeRoutes = registerRoutes(ctx, oauth, usage, preferences, proxyManager, searchSwitcher, readRouteAudit, codexAccountPool, fetchConfiguration);
45318
+ const routable = (accounts) => accounts.some((account) => account.authStatus === void 0 || account.authStatus === "ok");
45319
+ const disposeHubOverview = registerHubOverviewRoutes(ctx, [
45320
+ {
45321
+ id: "chatgpt",
45322
+ providerId: PROVIDER_ID$7,
45323
+ canToggle: true,
45324
+ read: async () => {
45325
+ const current = preferences.status();
45326
+ const accounts = await codexAccountPool.listAccounts().catch(() => []);
45327
+ return {
45328
+ enabled: current.enabled !== false,
45329
+ accountCount: accounts.length,
45330
+ authenticated: routable(accounts),
45331
+ enabledModelCount: current.visibleModelIds.length,
45332
+ totalModelCount: CODEX_MODEL_CATALOG.length
45333
+ };
45334
+ }
45335
+ },
45336
+ {
45337
+ id: "antigravity",
45338
+ providerId: PROVIDER_ID$6,
45339
+ canToggle: true,
45340
+ read: async () => {
45341
+ const current = antigravityPreferences.status();
45342
+ const accounts = await antigravityAccountPool.listAccounts().catch(() => []);
45343
+ const total = catalogTotal((await antigravityModelSettings.read()).catalogModels.length);
45344
+ return {
45345
+ enabled: current.enabled !== false,
45346
+ accountCount: accounts.length,
45347
+ authenticated: routable(accounts),
45348
+ enabledModelCount: total === null ? null : Math.min(current.enabledModelIds?.length ?? 0, total),
45349
+ totalModelCount: total
45350
+ };
45351
+ }
45352
+ },
45353
+ {
45354
+ id: "command-code",
45355
+ providerId: PROVIDER_ID$5,
45356
+ canToggle: true,
45357
+ read: async () => {
45358
+ const current = commandCodePreferences.status();
45359
+ const accounts = await commandCodeAccountPool.listAccounts().catch(() => []);
45360
+ const total = catalogTotal((await commandCodeModelSettings.read()).catalogModels.length);
45361
+ return {
45362
+ enabled: current.enabled !== false,
45363
+ accountCount: accounts.length,
45364
+ authenticated: routable(accounts),
45365
+ enabledModelCount: total === null ? null : Math.min(current.enabledModelIds?.length ?? 0, total),
45366
+ totalModelCount: total
45367
+ };
45368
+ }
45369
+ },
45370
+ {
45371
+ id: "kimi-code",
45372
+ providerId: PROVIDER_ID$3,
45373
+ canToggle: true,
45374
+ read: async () => {
45375
+ const current = kimiCodePreferences.status();
45376
+ const accounts = await kimiCodeAccountPool.listAccounts().catch(() => []);
45377
+ const total = catalogTotal((await kimiCodeModelSettings.read()).catalogModels.length);
45378
+ return {
45379
+ enabled: current.enabled !== false,
45380
+ accountCount: accounts.length,
45381
+ authenticated: routable(accounts),
45382
+ enabledModelCount: total === null ? null : Math.min(current.enabledModelIds?.length ?? 0, total),
45383
+ totalModelCount: total
45384
+ };
45385
+ }
45386
+ },
45387
+ {
45388
+ id: "workbuddy",
45389
+ providerId: PROVIDER_ID$1,
45390
+ canToggle: true,
45391
+ read: async () => {
45392
+ const current = workBuddyPreferences.status();
45393
+ const accounts = await workBuddyAccountPool.listAccounts().catch(() => []);
45394
+ const total = catalogTotal(getCachedCatalog$1().length);
45395
+ return {
45396
+ enabled: current.enabled !== false,
45397
+ accountCount: accounts.length,
45398
+ authenticated: routable(accounts),
45399
+ enabledModelCount: total === null ? null : Math.min(current.enabledModelIds?.length ?? 0, total),
45400
+ totalModelCount: total
45401
+ };
45402
+ }
45403
+ },
45404
+ {
45405
+ id: "minimax-code",
45406
+ providerId: PROVIDER_ID$2,
45407
+ canToggle: true,
45408
+ read: async () => {
45409
+ const current = minimaxCodePreferences.status();
45410
+ const accounts = await minimaxCodeAccountPool.listAccounts().catch(() => []);
45411
+ const total = catalogTotal(catalogSize());
45412
+ return {
45413
+ enabled: current.enabled !== false,
45414
+ accountCount: accounts.length,
45415
+ authenticated: routable(accounts),
45416
+ enabledModelCount: total === null ? null : Math.min(current.enabledModelIds?.length ?? 0, total),
45417
+ totalModelCount: total
45418
+ };
45419
+ }
45420
+ },
45421
+ {
45422
+ id: "claude",
45423
+ providerId: PROVIDER_ID,
45424
+ canToggle: true,
45425
+ read: async () => {
45426
+ const current = claudePreferences.status();
45427
+ const accounts = await claudeAccountPool.listAccounts().catch(() => []);
45428
+ const total = catalogTotal(getCachedCatalog().length);
45429
+ return {
45430
+ enabled: current.enabled !== false,
45431
+ accountCount: accounts.length,
45432
+ authenticated: routable(accounts),
45433
+ enabledModelCount: total === null ? null : Math.min(current.enabledModelIds?.length ?? 0, total),
45434
+ totalModelCount: total
45435
+ };
45436
+ }
45437
+ },
45438
+ {
45439
+ id: "ollama",
45440
+ providerId: PROVIDER_ID$4,
45441
+ canToggle: false,
45442
+ read: async () => {
45443
+ const accounts = await ollamaAccountPool.listAccounts().catch(() => []);
45444
+ const current = ollamaModelSettings.status();
45445
+ const total = catalogTotal(current.catalogModels.length);
45446
+ const enabled = current.enabledModelIds.length === 0 ? total : current.enabledModelIds.length;
45447
+ return {
45448
+ enabled: true,
45449
+ accountCount: accounts.length,
45450
+ authenticated: routable(accounts),
45451
+ enabledModelCount: total === null ? null : Math.min(enabled ?? 0, total),
45452
+ totalModelCount: total
45453
+ };
45454
+ }
45455
+ }
45456
+ ]);
44590
45457
  const disposeAdapter = ctx.llm.registerAdapter([PROVIDER_ID$7], adapter);
44591
45458
  const disposeImageTool = ctx.tools.register(createCodexImageTool(oauth, ctx.attachments, { fetchFn: proxyFetch }));
44592
45459
  const disposeVideoTool = ctx.tools.register(createKimiVideoTool(ctx, { fetchFn: proxyFetch }));
@@ -44613,48 +45480,28 @@ function apply(ctx, pluginConfig = {}) {
44613
45480
  disposeVideoTool();
44614
45481
  disposeAdapter();
44615
45482
  disposeRoutes();
45483
+ disposeHubOverview();
44616
45484
  disposeAntigravityRoutes();
44617
- releaseHandle(antigravityRouteWatch);
44618
- antigravityRegistration?.();
44619
- antigravityRegistration = void 0;
45485
+ antigravityClaim.dispose();
44620
45486
  disposeOllamaRoutes();
44621
- ollamaRegistration?.();
44622
- ollamaRegistration = void 0;
45487
+ ollamaClaim.dispose();
44623
45488
  disposeCommandCodeRoutes();
44624
- releaseHandle(commandCodeRouteWatch);
44625
- commandCodeRegistration?.();
44626
- commandCodeRegistration = void 0;
45489
+ commandCodeClaim.dispose();
44627
45490
  disposeKimiCodeRoutes();
44628
- releaseHandle(kimiCodeRouteWatch);
44629
- kimiCodeRegistration?.();
44630
- kimiCodeRegistration = void 0;
45491
+ kimiCodeClaim.dispose();
44631
45492
  disposeMinimaxCodeRoutes();
44632
- releaseHandle(minimaxCodeRouteWatch);
44633
- minimaxCodeRegistration?.();
44634
- minimaxCodeRegistration = void 0;
45493
+ minimaxCodeClaim.dispose();
44635
45494
  clearInterval(minimaxCodeCheckinTimer);
44636
45495
  clearInterval(checkinTimer);
44637
45496
  disposeWorkBuddyRoutes();
44638
- releaseHandle(workBuddyRouteWatch);
44639
- workBuddyRegistration?.();
44640
- workBuddyRegistration = void 0;
45497
+ workBuddyClaim.dispose();
44641
45498
  disposeClaudeRoutes();
44642
- releaseHandle(claudeRouteWatch);
44643
- claudeRegistration?.();
44644
- claudeRegistration = void 0;
45499
+ claudeClaim.dispose();
44645
45500
  oauth.dispose();
44646
45501
  proxyManager.dispose();
44647
45502
  };
44648
45503
  }, "dsh-chatgpt-subscription: adapter, routes, and lifecycle");
44649
45504
  }
44650
- /** Cordis event handles are either a disposer function or a disposable object. */
44651
- function releaseHandle(handle) {
44652
- if (typeof handle === "function") {
44653
- handle();
44654
- return;
44655
- }
44656
- handle?.dispose?.();
44657
- }
44658
45505
  function localWebServerBaseUrl(host, port) {
44659
45506
  return `http://${host === "0.0.0.0" ? "127.0.0.1" : host}:${port}`;
44660
45507
  }