dsh-codex-subscription 2.6.9 → 2.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -106,7 +106,7 @@ This plugin supports the latest DeepSeek Harness release recorded in its package
106
106
  2. Paste this package name, with its version, into the **Package name or address** field:
107
107
 
108
108
  ```text
109
- dsh-codex-subscription@2.6.9
109
+ dsh-codex-subscription@2.7.1
110
110
  ```
111
111
 
112
112
  3. Click **Install** and wait for completion. Follow the page instructions; save your work before restarting if requested.
@@ -118,7 +118,7 @@ Why the version is included: DSH's plugin page only installs versions that are a
118
118
  <summary>Terminal installation (with an existing dsh command)</summary>
119
119
 
120
120
  ```sh
121
- dsh plugin --profile web add dsh-codex-subscription@2.6.9
121
+ dsh plugin --profile web add dsh-codex-subscription@2.7.1
122
122
  ```
123
123
 
124
124
  Follow the restart instructions, then sign in under **Settings → Codex**. Both the Plugins page and the terminal use DSH's installation management.
@@ -131,7 +131,7 @@ Follow the restart instructions, then sign in under **Settings → Codex**. Both
131
131
  After signing in and selecting a Codex model in Web, install the same plugin in the Headless profile:
132
132
 
133
133
  ```sh
134
- dsh plugin --profile headless add dsh-codex-subscription@2.6.9
134
+ dsh plugin --profile headless add dsh-codex-subscription@2.7.1
135
135
  dsh --profile headless "Reply with only the word: ok"
136
136
  ```
137
137
 
@@ -266,7 +266,7 @@ Find this plugin on the DSH **Plugins** page and use its update or uninstall act
266
266
  <summary>Terminal commands</summary>
267
267
 
268
268
  ```sh
269
- dsh plugin --profile web update dsh-codex-subscription@2.6.9
269
+ dsh plugin --profile web update dsh-codex-subscription@2.7.1
270
270
  ```
271
271
 
272
272
  Run only when you want to uninstall:
@@ -282,7 +282,7 @@ dsh plugin --profile web remove dsh-codex-subscription
282
282
  <details>
283
283
  <summary>DSH 0.2.0-rc.2 reports the plugin as incompatible?</summary>
284
284
 
285
- DSH 0.2.0-rc.2 did not exist when any release before 2.2.10 was published, and a published package's compatibility declaration cannot be changed. Install 2.2.10 or later (a bare package name can resolve to an older release during the first 24 hours, so enter `dsh-codex-subscription@2.2.10`) from the **Plugins** page, or run `dsh plugin --profile web update dsh-codex-subscription@2.6.9`, then restart DSH.
285
+ DSH 0.2.0-rc.2 did not exist when any release before 2.2.10 was published, and a published package's compatibility declaration cannot be changed. Install 2.2.10 or later (a bare package name can resolve to an older release during the first 24 hours, so enter `dsh-codex-subscription@2.2.10`) from the **Plugins** page, or run `dsh plugin --profile web update dsh-codex-subscription@2.7.1`, then restart DSH.
286
286
 
287
287
  </details>
288
288
 
package/README.md CHANGED
@@ -106,7 +106,7 @@
106
106
  2. 在 **包名或地址** 输入框中粘贴下面的包名(已带版本号):
107
107
 
108
108
  ```text
109
- dsh-codex-subscription@2.6.9
109
+ dsh-codex-subscription@2.7.1
110
110
  ```
111
111
 
112
112
  3. 点击 **安装**,等待安装完成;按页面提示操作,需要重启时先保存工作。
@@ -118,7 +118,7 @@
118
118
  <summary>终端安装(已能运行 dsh 命令)</summary>
119
119
 
120
120
  ```sh
121
- dsh plugin --profile web add dsh-codex-subscription@2.6.9
121
+ dsh plugin --profile web add dsh-codex-subscription@2.7.1
122
122
  ```
123
123
 
124
124
  安装完成后按提示重启 DSH,再到 **设置 → Codex 订阅** 登录。插件页面和终端均由 DSH 管理安装。
@@ -131,7 +131,7 @@ dsh plugin --profile web add dsh-codex-subscription@2.6.9
131
131
  先在 Web 中完成登录并选择一次 Codex 模型,再把同一个插件安装到 Headless profile:
132
132
 
133
133
  ```sh
134
- dsh plugin --profile headless add dsh-codex-subscription@2.6.9
134
+ dsh plugin --profile headless add dsh-codex-subscription@2.7.1
135
135
  dsh --profile headless "只回复:ok"
136
136
  ```
137
137
 
@@ -295,7 +295,7 @@ DSH 实机设置截图。
295
295
  <summary>终端方式</summary>
296
296
 
297
297
  ```sh
298
- dsh plugin --profile web update dsh-codex-subscription@2.6.9
298
+ dsh plugin --profile web update dsh-codex-subscription@2.7.1
299
299
  ```
300
300
 
301
301
  仅在需要卸载时运行:
@@ -311,7 +311,7 @@ dsh plugin --profile web remove dsh-codex-subscription
311
311
  <details>
312
312
  <summary>DSH 0.2.0-rc.2 提示插件不兼容?</summary>
313
313
 
314
- 2.2.10 之前的所有版本发布时 DSH 0.2.0-rc.2 还不存在,其兼容声明无法事后修改。请安装 2.2.10 或更高版本(只填包名时,发布满 24 小时前可能装到旧版,请直接填 `dsh-codex-subscription@2.2.10`):在 **插件** 页面更新,或运行 `dsh plugin --profile web update dsh-codex-subscription@2.6.9`,然后重启 DSH。
314
+ 2.2.10 之前的所有版本发布时 DSH 0.2.0-rc.2 还不存在,其兼容声明无法事后修改。请安装 2.2.10 或更高版本(只填包名时,发布满 24 小时前可能装到旧版,请直接填 `dsh-codex-subscription@2.2.10`):在 **插件** 页面更新,或运行 `dsh plugin --profile web update dsh-codex-subscription@2.7.1`,然后重启 DSH。
315
315
 
316
316
  </details>
317
317
 
package/lib/client.js CHANGED
@@ -1074,6 +1074,7 @@ window.__ModuleLoader__.load({
1074
1074
  "account/select",
1075
1075
  "account/remove",
1076
1076
  "usage",
1077
+ "usage/accounts",
1077
1078
  "diagnostics",
1078
1079
  "preferences/status",
1079
1080
  "preferences/models",
@@ -5468,6 +5469,8 @@ window.__ModuleLoader__.load({
5468
5469
  const IconChevronDownOutline14 = _deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14 ?? _deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular;
5469
5470
  const IconChevronRightOutline14 = _deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutline14 ?? _deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutlineRegular;
5470
5471
  const Menu = _deepseek_ai_dsh_client_ui_primitives.Menu;
5472
+ const MenuSurface = _deepseek_ai_dsh_client_ui_primitives.MenuSurface;
5473
+ const MenuGroup = _deepseek_ai_dsh_client_ui_primitives.MenuGroup;
5471
5474
  const Tooltip = _deepseek_ai_dsh_client_ui_primitives.Tooltip;
5472
5475
  const IconChevronLeftOutline14 = _deepseek_ai_dsh_client_ui_primitives.IconChevronLeftOutline14 ?? _deepseek_ai_dsh_client_ui_primitives.IconChevronLeftOutlineRegular;
5473
5476
  const IconCloseOutline16 = _deepseek_ai_dsh_client_ui_primitives.IconCloseOutline16 ?? _deepseek_ai_dsh_client_ui_primitives.IconCloseOutlineRegular;
@@ -6037,7 +6040,13 @@ window.__ModuleLoader__.load({
6037
6040
  resetNoCredit: "没有可用的额度重置。",
6038
6041
  resetAlready: "这次重置请求已处理。",
6039
6042
  resetFailed: "无法使用额度重置。",
6043
+ accountQuotaLeft: "{label}剩余 {value}%",
6044
+ accountQuotaResets: "{value} 重置",
6045
+ accountQuotaLoading: "正在读取额度…",
6046
+ accountQuotaError: "暂时读不到这个账号的额度",
6047
+ accountQuotaNone: "没有额度窗口信息",
6040
6048
  resetRenewLogin: "登录状态已失效,请重新登录。",
6049
+ resetBlocked: "ChatGPT 的网络防护拦截了这次请求(HTTP 403),登录状态没有问题。请稍后再试,或检查网络、代理或 VPN。",
6041
6050
  resetExpired: "本次确认已失效,请重新开始。",
6042
6051
  resetInProgress: "额度重置正在处理中。",
6043
6052
  resetTooEarly: "请等待冷静期结束后再确认。",
@@ -6643,7 +6652,13 @@ window.__ModuleLoader__.load({
6643
6652
  resetNoCredit: "No quota reset is available.",
6644
6653
  resetAlready: "This reset request was already processed.",
6645
6654
  resetFailed: "Could not use the quota reset.",
6655
+ accountQuotaLeft: "{label} {value}% left",
6656
+ accountQuotaResets: "resets {value}",
6657
+ accountQuotaLoading: "Reading quota…",
6658
+ accountQuotaError: "The quota of this account could not be read right now",
6659
+ accountQuotaNone: "No quota window reported",
6646
6660
  resetRenewLogin: "Your sign-in expired. Sign in again.",
6661
+ resetBlocked: "ChatGPT network protection blocked this request (HTTP 403). Your sign-in is fine; try again later or check your network, proxy or VPN.",
6647
6662
  resetExpired: "This confirmation expired. Start again.",
6648
6663
  resetInProgress: "A quota reset is already in progress.",
6649
6664
  resetTooEarly: "Wait for the cooldown before confirming.",
@@ -6849,6 +6864,9 @@ window.__ModuleLoader__.load({
6849
6864
  .codexSubscriptionAccounts{display:flex;flex-direction:column;border-top:1px solid var(--dsw-alias-border-l2)}
6850
6865
  .codexSubscriptionAccount{display:flex;align-items:center;justify-content:space-between;gap:10px;min-height:42px;border-bottom:1px solid var(--dsw-alias-border-l2);font-size:13px}
6851
6866
  .codexSubscriptionAccount:last-child{border-bottom:0}
6867
+ .codexSubscriptionAccountMain{display:flex;flex-direction:column;gap:2px;padding:6px 0;min-width:0}
6868
+ .codexSubscriptionAccountQuota{display:flex;flex-wrap:wrap;gap:2px 12px;font-size:12px;color:var(--dsw-alias-text-l2)}
6869
+ .codexSubscriptionAccountQuota[data-state=error]{color:var(--dsw-alias-text-danger,#c0392b)}
6852
6870
  .codexSubscriptionAccount[data-active=true] .codexSubscriptionEmail,.codexSubscriptionAccount[data-active=true]>span{font-weight:600}
6853
6871
  .codexSubscriptionEmail{max-width:100%;overflow:hidden;padding:2px 4px;border:0;border-radius:5px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;text-align:left;text-overflow:ellipsis;white-space:nowrap;cursor:pointer}
6854
6872
  .codexSubscriptionEmail:hover{background:var(--dsw-alias-interactive-bg-hover)}
@@ -6969,7 +6987,7 @@ window.__ModuleLoader__.load({
6969
6987
  .codexComposerQuotaBar::-webkit-progress-value{background:var(--dsw-alias-label-secondary);border-radius:999px}
6970
6988
  .codexComposerQuotaBar::-moz-progress-bar{background:var(--dsw-alias-label-secondary);border-radius:999px}
6971
6989
  .codexModelSelect{position:relative;min-width:0}
6972
- .codexModelSelectTrigger{display:flex;align-items:center;gap:4px;min-width:0;max-width:min(360px,45cqw);height:28px;padding:0 4px 0 8px;border:0;border-radius:24px;outline:0;background:transparent;color:var(--dsw-alias-label-secondary);font-size:13px;font-weight:500;line-height:20px;cursor:pointer}
6990
+ .codexModelSelectTrigger{display:flex;align-items:center;gap:4px;min-width:0;max-width:min(360px,45cqw);height:28px;padding:0 4px 0 8px;border:0;border-radius:var(--dsw-radius-sm);outline:0;background:transparent;color:var(--dsw-alias-label-secondary);font-size:13px;font-weight:500;line-height:20px;cursor:pointer}
6973
6991
  .codexModelSelectTrigger:hover:not(:disabled),.codexModelSelectTrigger[aria-expanded=true]{background:var(--dsw-alias-interactive-bg-hover)}
6974
6992
  .codexModelSelectTrigger:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-border-l3)}
6975
6993
  .codexModelSelectTrigger:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}
@@ -6978,34 +6996,32 @@ window.__ModuleLoader__.load({
6978
6996
  .codexModelSelectEffort{flex:none;color:var(--dsw-alias-label-caption)}
6979
6997
  .codexModelSelectChevron{flex:none;color:var(--dsw-alias-label-caption);transition:transform 120ms}
6980
6998
  .codexModelSelectTrigger[aria-expanded=true] .codexModelSelectChevron{transform:rotate(180deg)}
6981
- .codexModelSelectMenu,.codexModelSelectSubmenu{position:absolute;z-index:30;box-sizing:border-box;width:max-content;min-width:min(240px,calc(100vw - 32px));max-width:min(420px,calc(100vw - 32px));max-height:min(360px,calc(100vh - 96px));padding:4px;border:1px solid var(--dsw-alias-border-inverted);border-radius:12px;background:var(--dsw-alias-bg-layer-2,#2c2c2e);box-shadow:var(--dsw-shadow-lv3);color:var(--dsw-alias-label-primary);overflow:hidden}
6982
- .codexModelSelectMenu{right:0;bottom:calc(100% + 8px)}
6983
- .codexModelSelectSubmenu{right:calc(100% + 8px);bottom:0;min-width:min(230px,calc(100vw - 32px))}
6984
- /* Keep search outside the scroll viewport; let the model list fit the bounded menu. */
6985
- .codexModelSelectModelMenu{display:flex;flex-direction:column}
6986
- .codexModelSelectCell{display:flex;align-items:center;gap:8px;width:100%;min-width:100%;height:40px;box-sizing:border-box;padding:0 10px;border:0;border-radius:10px;background:transparent;color:inherit;font-size:14px;line-height:22px;text-align:left;cursor:pointer}
6987
- .codexModelSelectCell:hover,.codexModelSelectCell:focus-visible,.codexModelSelectCell[data-open=true]{background:var(--dsw-alias-interactive-bg-hover);outline:0}
6988
- .codexModelSelectCell:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}
6999
+ /* The menu follows DSH's own model picker: one floating surface above the trigger, rows drill into the same surface. */
7000
+ .codexModelSelectMenu{z-index:1100;--dsw-elevation-stroke-color:var(--dsw-alias-border-l1);box-sizing:border-box;position:fixed;display:flex;flex-direction:column;width:max-content;min-width:min(240px,100vw - 32px);max-width:min(420px,100vw - 32px);max-height:min(360px,100vh - 96px);padding:4px;border:0;box-shadow:var(--dsw-elevation-prominent);color:var(--dsw-alias-label-primary);--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);overflow:hidden}
7001
+ .codexModelSelectCell{box-sizing:border-box;display:flex;align-items:center;gap:6px;width:auto;min-width:100%;height:34px;padding:0 8px;border:none;border-radius:var(--dsw-radius-md);outline:none;background:transparent;color:var(--dsw-alias-label-primary);font-size:13px;line-height:20px;text-align:left;cursor:pointer}
7002
+ .codexModelSelectCell:hover,.codexModelSelectCell:focus-visible{background:var(--dsw-alias-interactive-bg-hover)}
6989
7003
  .codexModelSelectCellLabel{flex:none;white-space:nowrap}
6990
7004
  .codexModelSelectCellValue{flex:auto;min-width:0;overflow:hidden;color:var(--dsw-alias-label-tertiary);text-align:right;text-overflow:ellipsis;white-space:nowrap}
6991
- .codexModelSelectCellChevron{flex:none;color:var(--dsw-alias-label-tertiary)}
6992
- .codexModelSelectSearch{display:block;flex-shrink:0;width:100%;box-sizing:border-box;height:32px;margin:0 0 4px;padding:0 10px;border:1px solid var(--dsw-alias-border-l3);border-radius:8px;outline:0;background:transparent;color:inherit;font:inherit;font-size:13px}
6993
- .codexModelSelectSearch:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-border-l3)}
7005
+ .codexModelSelectCellChevron{flex:none;width:12px;height:12px;color:var(--dsw-alias-menu-icon)}
7006
+ .codexModelSelectSearchRow{flex-shrink:0;position:relative;margin:2px 0 3px}
7007
+ .codexModelSelectSearchRow .codexModelSelectSearch{display:flex;height:auto;padding:5px 7px;border:0 solid transparent;border-radius:var(--dsw-radius-md);background:transparent}
7008
+ .codexModelSelectSearchRow .codexModelSelectSearch:focus-within{border-color:transparent}
7009
+ .codexModelSelectSearchRow .codexModelSelectSearch input{padding:0;font-size:12px;line-height:normal}
7010
+ .codexModelSelectSearchRow .codexModelSelectSearch input::placeholder{color:var(--dsw-alias-label-caption)}
6994
7011
  .codexModelSelectGroups{flex:1 1 auto;min-height:0;overflow-y:auto}
6995
- .codexModelSelectGroup+.codexModelSelectGroup{margin-top:4px}
6996
- .codexModelSelectGroupTitle{position:sticky;top:0;z-index:1;padding:5px 8px 3px;background:var(--dsw-alias-bg-layer-2,#2c2c2e);color:var(--dsw-alias-label-tertiary);font-size:12px;font-weight:500;line-height:18px}
6997
- .codexModelSelectOption{display:flex;align-items:center;gap:8px;width:100%;min-width:100%;min-height:38px;box-sizing:border-box;padding:6px 8px;border:0;border-radius:10px;outline:0;background:transparent;color:inherit;text-align:left;cursor:pointer}
7012
+ .codexModelSelectGroupTitle{padding:5px 8px 3px;color:var(--dsw-alias-label-tertiary);font-size:12px;font-weight:500;line-height:18px}
7013
+ .codexModelSelectOption{box-sizing:border-box;display:flex;align-items:center;gap:6px;width:auto;min-width:100%;min-height:34px;padding:5px 7px;border:none;border-radius:var(--dsw-radius-md);outline:none;background:transparent;color:inherit;text-align:left;cursor:pointer}
6998
7014
  .codexModelSelectOption:hover:not(:disabled),.codexModelSelectOption:focus-visible{background:var(--dsw-alias-interactive-bg-hover)}
6999
7015
  .codexModelSelectOption:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}
7000
- .codexModelSelectOptionCopy{display:flex;flex:1;min-width:0;flex-direction:column}
7001
- .codexModelSelectOptionName{overflow:hidden;color:inherit;font-size:14px;font-weight:500;line-height:20px;text-overflow:ellipsis;white-space:nowrap}
7002
- .codexModelSelectOptionDescription{overflow:hidden;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;text-overflow:ellipsis;white-space:nowrap}
7003
- .codexModelSelectCheck{display:grid;place-items:center;flex:0 0 18px;color:var(--dsw-alias-label-primary)}
7004
- .codexModelSelectStatus,.codexModelSelectEmpty{padding:10px;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:20px}
7005
- .codexModelSelectError,.codexModelSelectWarning{display:flex;align-items:flex-start;justify-content:space-between;gap:8px;margin-bottom:4px;padding:7px 8px;border-radius:8px;background:var(--dsw-alias-interactive-bg-hover-danger);color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px}
7016
+ .codexModelSelectOptionCopy{display:flex;flex:1;flex-direction:column;min-width:0}
7017
+ .codexModelSelectOptionName{overflow:hidden;color:inherit;font-size:13px;font-weight:400;line-height:18px;text-overflow:ellipsis;white-space:nowrap}
7018
+ .codexModelSelectOptionDescription{overflow:hidden;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:16px;text-overflow:ellipsis;white-space:nowrap}
7019
+ .codexModelSelectCheck{display:grid;place-items:center;flex:0 0 14px;color:var(--dsw-alias-label-primary)}
7020
+ .codexModelSelectCheck svg{width:14px;height:14px}
7021
+ .codexModelSelectStatus,.codexModelSelectEmpty{padding:8px;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}
7022
+ .codexModelSelectError,.codexModelSelectWarning{display:flex;align-items:flex-start;justify-content:space-between;gap:6px;margin-bottom:3px;padding:6px 7px;border-radius:var(--dsw-radius-md);background:var(--dsw-alias-interactive-bg-hover-danger);color:var(--dsw-alias-state-error-primary);font-size:11px;line-height:16px}
7006
7023
  .codexModelSelectWarning{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-state-warn-label)}
7007
- .codexModelSelectRetry{flex:none;padding:0;border:0;background:transparent;color:inherit;font:inherit;font-weight:600;cursor:pointer}
7008
- .codexModelSelectMenu{overflow:visible}
7024
+ .codexModelSelectRetry{flex:none;padding:0;border:none;background:transparent;color:inherit;font:inherit;font-weight:600;cursor:pointer}
7009
7025
  .codexImageTool{display:flex;flex-direction:column;gap:8px;margin:4px 0;color:var(--dsw-alias-label-primary)}
7010
7026
  .codexImageToolRow{display:flex;align-items:center;min-height:24px;gap:8px;font-size:13px;line-height:20px}
7011
7027
  .codexImageToolIcon{display:block;flex-shrink:0;width:16px;height:16px;color:var(--dsw-alias-label-secondary)}
@@ -9078,7 +9094,9 @@ window.__ModuleLoader__.load({
9078
9094
  return `${local[0]}•••${local.at(-1)}@${domain}`;
9079
9095
  };
9080
9096
  const hours = (seconds) => Math.round(seconds / 3600 * 10) / 10;
9081
- const percent = (value) => Number(value).toLocaleString(void 0, { maximumFractionDigits: 1 });
9097
+ /** The language DSH shows its interface in, so dates and numbers match the text around them rather than the OS. */
9098
+ const uiLocale = () => typeof document === "undefined" ? void 0 : document.documentElement.lang || void 0;
9099
+ const percent = (value) => Number(value).toLocaleString(uiLocale(), { maximumFractionDigits: 1 });
9082
9100
  const isApproximateWindow = (seconds, expected) => seconds >= expected * .95 && seconds <= expected * 1.05;
9083
9101
  const windowLabel = (seconds, t) => {
9084
9102
  if (isApproximateWindow(seconds, 18e3)) return t("windowFiveHours");
@@ -9155,7 +9173,7 @@ window.__ModuleLoader__.load({
9155
9173
  }
9156
9174
  //#endregion
9157
9175
  //#region src/version.js
9158
- const PACKAGE_VERSION = "2.6.9";
9176
+ const PACKAGE_VERSION = "2.7.1";
9159
9177
  //#endregion
9160
9178
  //#region src/client-recovery.js
9161
9179
  async function recoveryCall(rpc, endpoint, payload = {}, timeoutMs = 1e4) {
@@ -9383,7 +9401,7 @@ window.__ModuleLoader__.load({
9383
9401
  }
9384
9402
  function shortReset(quota, t) {
9385
9403
  if (!Number.isSafeInteger(quota.resetsAt)) return t("resetUnknown");
9386
- const value = (/* @__PURE__ */ new Date(quota.resetsAt * 1e3)).toLocaleString(void 0, {
9404
+ const value = (/* @__PURE__ */ new Date(quota.resetsAt * 1e3)).toLocaleString(uiLocale(), {
9387
9405
  month: "numeric",
9388
9406
  day: "numeric",
9389
9407
  hour: "2-digit",
@@ -9426,6 +9444,11 @@ window.__ModuleLoader__.load({
9426
9444
  const ForwardRef = /*#__PURE__*/ react.forwardRef(BoltIcon);
9427
9445
  //#endregion
9428
9446
  //#region src/client-model-select.jsx
9447
+ const MEASURE_STYLE = {
9448
+ visibility: "hidden",
9449
+ left: 0,
9450
+ top: 0
9451
+ };
9429
9452
  function CodexModelSelect({ locked, available, directory, load, select, preference, t }) {
9430
9453
  const state = (0, react.useSyncExternalStore)(directory.subscribe, directory.getSnapshot);
9431
9454
  const preferenceSnapshot = usePreferenceSnapshot(preference);
@@ -9433,6 +9456,8 @@ window.__ModuleLoader__.load({
9433
9456
  const [pane, setPane] = (0, react.useState)("root");
9434
9457
  const [query, setQuery] = (0, react.useState)("");
9435
9458
  const rootRef = (0, react.useRef)(null);
9459
+ const menuRef = (0, react.useRef)(null);
9460
+ const [menuPos, setMenuPos] = (0, react.useState)(null);
9436
9461
  const triggerRef = (0, react.useRef)(null);
9437
9462
  const id = (0, react.useId)();
9438
9463
  const choices = (0, react.useMemo)(() => state.groups.flatMap((group) => group.models.map((model) => ({
@@ -9516,7 +9541,7 @@ window.__ModuleLoader__.load({
9516
9541
  (0, react.useEffect)(() => {
9517
9542
  if (!open) return void 0;
9518
9543
  const closeOutside = (event) => {
9519
- if (!rootRef.current?.contains(event.target)) {
9544
+ if (!rootRef.current?.contains(event.target) && !menuRef.current?.contains(event.target)) {
9520
9545
  setOpen(false);
9521
9546
  setPane("root");
9522
9547
  setQuery("");
@@ -9525,6 +9550,39 @@ window.__ModuleLoader__.load({
9525
9550
  document.addEventListener("mousedown", closeOutside);
9526
9551
  return () => document.removeEventListener("mousedown", closeOutside);
9527
9552
  }, [open]);
9553
+ (0, react.useLayoutEffect)(() => {
9554
+ if (!open) {
9555
+ setMenuPos(null);
9556
+ return;
9557
+ }
9558
+ const place = () => {
9559
+ const rect = triggerRef.current?.getBoundingClientRect();
9560
+ if (rect === void 0) return;
9561
+ const margin = 12;
9562
+ const width = menuRef.current?.offsetWidth ?? 0;
9563
+ const height = menuRef.current?.offsetHeight ?? 0;
9564
+ let left = rect.right - width;
9565
+ let top = rect.top - 8 - height;
9566
+ if (width > 0) left = Math.min(Math.max(left, margin), window.innerWidth - width - margin);
9567
+ if (height > 0) top = Math.min(Math.max(top, margin), window.innerHeight - height - margin);
9568
+ setMenuPos({
9569
+ left,
9570
+ top
9571
+ });
9572
+ };
9573
+ place();
9574
+ window.addEventListener("scroll", place, true);
9575
+ window.addEventListener("resize", place);
9576
+ return () => {
9577
+ window.removeEventListener("scroll", place, true);
9578
+ window.removeEventListener("resize", place);
9579
+ };
9580
+ }, [
9581
+ open,
9582
+ pane,
9583
+ state,
9584
+ query
9585
+ ]);
9528
9586
  (0, react.useEffect)(() => {
9529
9587
  if (pane !== "model") setQuery("");
9530
9588
  }, [pane]);
@@ -9598,10 +9656,10 @@ window.__ModuleLoader__.load({
9598
9656
  type: "button",
9599
9657
  role: "menuitem",
9600
9658
  className: "codexModelSelectCell",
9601
- "data-open": pane === target,
9602
- "aria-haspopup": "menu",
9603
- "aria-expanded": pane === target,
9604
- onClick: () => setPane((current) => current === target ? "root" : target),
9659
+ onClick: () => {
9660
+ setQuery("");
9661
+ setPane(target);
9662
+ },
9605
9663
  children: [
9606
9664
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
9607
9665
  className: "codexModelSelectCellLabel",
@@ -9614,141 +9672,147 @@ window.__ModuleLoader__.load({
9614
9672
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconChevronRightOutline14, { className: "codexModelSelectCellChevron" })
9615
9673
  ]
9616
9674
  });
9617
- let submenu = null;
9618
- if (pane === "model") submenu = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
9619
- className: "codexModelSelectSubmenu codexModelSelectModelMenu",
9620
- role: "menu",
9621
- "aria-label": t("modelLabel"),
9622
- children: [
9623
- state.status === "loading" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9624
- className: "codexModelSelectStatus",
9625
- children: t("modelsLoading")
9626
- }) : null,
9627
- state.error === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
9628
- className: "codexModelSelectError",
9629
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: fill(t("modelFailed"), { value: state.error }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
9630
- className: "codexModelSelectRetry",
9631
- type: "button",
9632
- onClick: load,
9633
- children: t("modelRetry")
9634
- })]
9635
- }),
9636
- state.failures.map((failure) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9637
- className: "codexModelSelectWarning",
9638
- children: fill(t("groupFailed"), {
9639
- name: failure.name,
9640
- value: failure.message
9641
- })
9642
- }, failure.id)),
9643
- searchable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
9675
+ const group = (key, label, children) => MenuGroup === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
9676
+ role: "group",
9677
+ "aria-label": label,
9678
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9679
+ className: "codexModelSelectGroupTitle",
9680
+ children: label
9681
+ }), children]
9682
+ }, key) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MenuGroup, {
9683
+ label,
9684
+ children
9685
+ }, key);
9686
+ let content = null;
9687
+ if (pane === "root") content = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
9688
+ cell("model", t("modelLabel"), modelLabel),
9689
+ reasoning === void 0 ? null : cell("effort", t("effortLabel"), effortLabel),
9690
+ (speedSupported || ultrafastSupported) && cell("speed", t("speedTitle"), t(speedKey)),
9691
+ verbositySupported && cell("verbosity", t("verbosityTitle"), verbosityLabel)
9692
+ ] });
9693
+ else if (pane === "model") content = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
9694
+ searchable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9695
+ className: "codexModelSelectSearchRow",
9696
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Input, {
9644
9697
  className: "codexModelSelectSearch",
9645
- type: "search",
9698
+ type: "text",
9699
+ role: "searchbox",
9646
9700
  value: query,
9647
9701
  autoFocus: true,
9648
9702
  placeholder: t("modelSearch"),
9649
9703
  "aria-label": t("modelSearch"),
9650
9704
  onChange: (event) => setQuery(event.target.value)
9651
- }) : null,
9652
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9653
- className: "codexModelSelectGroups scrollable",
9654
- children: shownGroups.map((group) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
9655
- className: "codexModelSelectGroup",
9656
- role: "group",
9657
- "aria-labelledby": `${id}-${group.id}`,
9658
- children: [
9659
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9660
- className: "codexModelSelectGroupTitle",
9661
- id: `${id}-${group.id}`,
9662
- children: group.name
9663
- }),
9664
- group.allHidden === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9665
- className: "codexModelSelectEmpty",
9666
- children: t("modelsHidden")
9667
- }) : null,
9668
- group.models.map((model) => option({
9669
- key: model.id,
9670
- label: model.name,
9671
- description: model.description,
9672
- selected: state.current?.provider === group.id && state.current.model === model.id,
9673
- disabled: busy,
9674
- onClick: () => chooseModel({
9675
- provider: group.id,
9676
- model: model.id
9677
- })
9678
- }))
9679
- ]
9680
- }, group.id))
9681
- }),
9682
- state.status === "ready" && visibleGroups.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9683
- className: "codexModelSelectEmpty",
9684
- children: t(choices.length === 0 ? "modelsEmpty" : "modelsHidden")
9685
- }) : null,
9686
- state.status === "ready" && visibleGroups.length > 0 && shownGroups.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9705
+ })
9706
+ }) : null,
9707
+ state.status === "loading" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9708
+ className: "codexModelSelectStatus",
9709
+ children: t("modelsLoading")
9710
+ }) : null,
9711
+ state.error === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
9712
+ className: "codexModelSelectError",
9713
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: fill(t("modelFailed"), { value: state.error }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
9714
+ className: "codexModelSelectRetry",
9715
+ type: "button",
9716
+ onClick: load,
9717
+ children: t("modelRetry")
9718
+ })]
9719
+ }),
9720
+ state.failures.map((failure) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9721
+ className: "codexModelSelectWarning",
9722
+ children: fill(t("groupFailed"), {
9723
+ name: failure.name,
9724
+ value: failure.message
9725
+ })
9726
+ }, failure.id)),
9727
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9728
+ className: "codexModelSelectGroups scrollable",
9729
+ role: "menu",
9730
+ "aria-label": t("modelLabel"),
9731
+ children: shownGroups.map((entry) => group(entry.id, entry.name, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [entry.allHidden === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9687
9732
  className: "codexModelSelectEmpty",
9688
- children: t("modelNoMatch")
9689
- }) : null
9690
- ]
9691
- });
9692
- else if (pane === "effort") submenu = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9693
- className: "codexModelSelectSubmenu",
9694
- role: "menu",
9695
- "aria-label": t("effortLabel"),
9696
- children: effortChoices.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9733
+ children: t("modelsHidden")
9734
+ }) : null, entry.models.map((model) => option({
9735
+ key: model.id,
9736
+ label: model.name,
9737
+ selected: state.current?.provider === entry.id && state.current.model === model.id,
9738
+ disabled: busy,
9739
+ onClick: () => chooseModel({
9740
+ provider: entry.id,
9741
+ model: model.id
9742
+ })
9743
+ }))] })))
9744
+ }),
9745
+ state.status === "ready" && visibleGroups.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9697
9746
  className: "codexModelSelectEmpty",
9698
- children: t("effortsEmpty")
9699
- }) : effortChoices.map((level) => option({
9700
- key: level.key,
9701
- label: level.label,
9702
- description: level.description,
9703
- selected: effectiveEffort === level.effort,
9704
- disabled: busy,
9705
- onClick: () => chooseEffort(level.effort)
9706
- }))
9707
- });
9708
- else if (pane === "speed") submenu = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
9709
- className: "codexModelSelectSubmenu",
9710
- role: "menu",
9711
- "aria-label": t("speedTitle"),
9712
- children: [
9713
- option({
9714
- key: SPEED_MODE_STANDARD,
9715
- label: t("speedStandard"),
9716
- description: t("speedStandardHint"),
9717
- selected: !fast,
9718
- disabled: !speedWritable,
9719
- onClick: () => chooseSpeed(SPEED_MODE_STANDARD)
9720
- }),
9721
- speedSupported && option({
9722
- key: "fast",
9723
- label: t("speedFast"),
9724
- description: t(state.current?.model === "gpt-6-astra" ? "speedFastAstraHint" : "speedFastHint"),
9725
- selected: fast && !ultrafast,
9726
- disabled: !speedWritable,
9727
- onClick: () => chooseSpeed("fast")
9728
- }),
9729
- ultrafastSupported && option({
9730
- key: "ultrafast",
9731
- label: t("speedUltrafast"),
9732
- description: t("speedUltrafastHint"),
9733
- selected: ultrafast,
9734
- disabled: !speedWritable,
9735
- onClick: () => chooseSpeed("ultrafast")
9736
- })
9737
- ]
9738
- });
9739
- else if (pane === "verbosity") submenu = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9740
- className: "codexModelSelectSubmenu",
9741
- role: "menu",
9742
- "aria-label": t("verbosityTitle"),
9743
- children: verbosityItems.map((item) => option({
9744
- key: item.id,
9745
- label: item.label,
9746
- description: item.description,
9747
- selected: preferenceSnapshot.outputVerbosity === item.id,
9748
- disabled: !verbosityWritable,
9749
- onClick: () => chooseVerbosity(item.id)
9750
- }))
9751
- });
9747
+ children: t(choices.length === 0 ? "modelsEmpty" : "modelsHidden")
9748
+ }) : null,
9749
+ state.status === "ready" && visibleGroups.length > 0 && shownGroups.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9750
+ className: "codexModelSelectEmpty",
9751
+ children: t("modelNoMatch")
9752
+ }) : null
9753
+ ] });
9754
+ else if (pane === "effort") content = effortChoices.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9755
+ className: "codexModelSelectEmpty",
9756
+ children: t("effortsEmpty")
9757
+ }) : effortChoices.map((level) => option({
9758
+ key: level.key,
9759
+ label: level.label,
9760
+ description: level.description,
9761
+ selected: effectiveEffort === level.effort,
9762
+ disabled: busy,
9763
+ onClick: () => chooseEffort(level.effort)
9764
+ }));
9765
+ else if (pane === "speed") content = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
9766
+ option({
9767
+ key: SPEED_MODE_STANDARD,
9768
+ label: t("speedStandard"),
9769
+ description: t("speedStandardHint"),
9770
+ selected: !fast,
9771
+ disabled: !speedWritable,
9772
+ onClick: () => chooseSpeed(SPEED_MODE_STANDARD)
9773
+ }),
9774
+ speedSupported && option({
9775
+ key: "fast",
9776
+ label: t("speedFast"),
9777
+ description: t(state.current?.model === "gpt-6-astra" ? "speedFastAstraHint" : "speedFastHint"),
9778
+ selected: fast && !ultrafast,
9779
+ disabled: !speedWritable,
9780
+ onClick: () => chooseSpeed("fast")
9781
+ }),
9782
+ ultrafastSupported && option({
9783
+ key: "ultrafast",
9784
+ label: t("speedUltrafast"),
9785
+ description: t("speedUltrafastHint"),
9786
+ selected: ultrafast,
9787
+ disabled: !speedWritable,
9788
+ onClick: () => chooseSpeed("ultrafast")
9789
+ })
9790
+ ] });
9791
+ else if (pane === "verbosity") content = verbosityItems.map((item) => option({
9792
+ key: item.id,
9793
+ label: item.label,
9794
+ description: item.description,
9795
+ selected: preferenceSnapshot.outputVerbosity === item.id,
9796
+ disabled: !verbosityWritable,
9797
+ onClick: () => chooseVerbosity(item.id)
9798
+ }));
9799
+ const menu = open ? (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(MenuSurface ?? "div", {
9800
+ ref: menuRef,
9801
+ id: `${id}-menu`,
9802
+ className: "codexModelSelectMenu",
9803
+ style: menuPos ?? MEASURE_STYLE,
9804
+ role: pane === "model" ? "group" : "menu",
9805
+ "aria-label": t("modelMenuAria"),
9806
+ "aria-busy": state.status === "loading" || busy,
9807
+ onKeyDown: (event) => {
9808
+ if (event.key !== "Escape") return;
9809
+ event.preventDefault();
9810
+ event.stopPropagation();
9811
+ if (pane === "root") close(true);
9812
+ else setPane("root");
9813
+ },
9814
+ children: content
9815
+ }), document.body) : null;
9752
9816
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
9753
9817
  className: "codexModelSelect",
9754
9818
  ref: rootRef,
@@ -9784,20 +9848,7 @@ window.__ModuleLoader__.load({
9784
9848
  }),
9785
9849
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconChevronDownOutline14, { className: "codexModelSelectChevron" })
9786
9850
  ]
9787
- }), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
9788
- className: "codexModelSelectMenu",
9789
- id: `${id}-menu`,
9790
- role: "menu",
9791
- "aria-label": t("modelMenuAria"),
9792
- "aria-busy": state.status === "loading" || busy,
9793
- children: [
9794
- cell("model", t("modelLabel"), modelLabel),
9795
- reasoning === void 0 ? null : cell("effort", t("effortLabel"), effortLabel),
9796
- (speedSupported || ultrafastSupported) && cell("speed", t("speedTitle"), t(speedKey)),
9797
- verbositySupported && cell("verbosity", t("verbosityTitle"), verbosityLabel),
9798
- submenu
9799
- ]
9800
- }) : null]
9851
+ }), menu]
9801
9852
  });
9802
9853
  }
9803
9854
  //#endregion
@@ -11152,6 +11203,73 @@ window.__ModuleLoader__.load({
11152
11203
  }
11153
11204
  }
11154
11205
  //#endregion
11206
+ //#region src/client-account-quota.jsx
11207
+ const when = (seconds) => (/* @__PURE__ */ new Date(seconds * 1e3)).toLocaleString(uiLocale(), {
11208
+ month: "numeric",
11209
+ day: "numeric",
11210
+ hour: "2-digit",
11211
+ minute: "2-digit"
11212
+ });
11213
+ /** Quota of every signed-in account (read-only; the active account does not change). */
11214
+ function useAccountQuotas(rpc, ids) {
11215
+ const [entries, setEntries] = (0, react.useState)({});
11216
+ const [busy, setBusy] = (0, react.useState)(false);
11217
+ const request = (0, react.useRef)(0);
11218
+ const key = ids.join(",");
11219
+ const load = (force) => {
11220
+ if (ids.length < 2) {
11221
+ setEntries({});
11222
+ return;
11223
+ }
11224
+ const id = ++request.current;
11225
+ setBusy(true);
11226
+ recoveryCall(rpc, "usage/accounts", { force }).then((value) => {
11227
+ if (request.current === id) setEntries(Object.fromEntries((value?.accounts ?? []).map((entry) => [entry.id, entry])));
11228
+ }).catch(() => {
11229
+ if (request.current === id) setEntries(Object.fromEntries(ids.map((account) => [account, {
11230
+ id: account,
11231
+ error: "Could not read ChatGPT usage"
11232
+ }])));
11233
+ }).finally(() => {
11234
+ if (request.current === id) setBusy(false);
11235
+ });
11236
+ };
11237
+ (0, react.useEffect)(() => {
11238
+ load(false);
11239
+ return () => {
11240
+ request.current += 1;
11241
+ };
11242
+ }, [key]);
11243
+ return {
11244
+ entries,
11245
+ busy,
11246
+ refresh: () => load(true)
11247
+ };
11248
+ }
11249
+ function AccountQuota({ entry, loading, t }) {
11250
+ if (entry === void 0) return loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", {
11251
+ className: "codexSubscriptionAccountQuota",
11252
+ children: t("accountQuotaLoading")
11253
+ }) : null;
11254
+ if (entry.error !== void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", {
11255
+ className: "codexSubscriptionAccountQuota",
11256
+ "data-state": "error",
11257
+ children: entry.error === "ChatGPT sign-in needs to be renewed" ? t("resetRenewLogin") : t("accountQuotaError")
11258
+ });
11259
+ const windows = entry.usage?.rateLimits?.find((limit) => limit.id === "codex")?.windows ?? [];
11260
+ if (windows.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", {
11261
+ className: "codexSubscriptionAccountQuota",
11262
+ children: t("accountQuotaNone")
11263
+ });
11264
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", {
11265
+ className: "codexSubscriptionAccountQuota",
11266
+ children: windows.map((window) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [fill(t("accountQuotaLeft"), {
11267
+ label: windowLabel(window.windowSeconds, t),
11268
+ value: percent(window.remainingPercent)
11269
+ }), window.resetsAt === void 0 ? "" : ` · ${fill(t("accountQuotaResets"), { value: when(window.resetsAt) })}`] }, window.windowSeconds))
11270
+ });
11271
+ }
11272
+ //#endregion
11155
11273
  //#region src/client-account.jsx
11156
11274
  const LOGIN_FAILURES = [
11157
11275
  "account-claim",
@@ -11173,6 +11291,7 @@ window.__ModuleLoader__.load({
11173
11291
  children: emailVisible ? candidate.email : maskEmail(candidate.email)
11174
11292
  });
11175
11293
  }
11294
+ const signedInIds = (account) => account?.authenticated === true ? (account.accounts ?? []).map((candidate) => candidate.id).filter((id) => typeof id === "string") : [];
11176
11295
  function AccountCard({ rpc, t, account, setAccount, onSignedOut }) {
11177
11296
  const [flow, setFlow] = (0, react.useState)();
11178
11297
  const flowGeneration = (0, react.useRef)(0);
@@ -11185,6 +11304,7 @@ window.__ModuleLoader__.load({
11185
11304
  const [emailVisibilityKey, setEmailVisibilityKey] = (0, react.useState)(accountVisibilityKey);
11186
11305
  const [busy, setBusy] = (0, react.useState)(false);
11187
11306
  const [error, setError] = (0, react.useState)();
11307
+ const quotas = useAccountQuotas(rpc, signedInIds(account));
11188
11308
  const call = (endpoint, payload = {}) => recoveryCall(rpc, endpoint, payload);
11189
11309
  (0, react.useEffect)(() => {
11190
11310
  if (emailVisibilityKey === accountVisibilityKey) return;
@@ -11380,12 +11500,19 @@ window.__ModuleLoader__.load({
11380
11500
  children: accounts.map((candidate) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
11381
11501
  className: "codexSubscriptionAccount",
11382
11502
  "data-active": candidate.active,
11383
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountEmail, {
11384
- candidate,
11385
- fallback: candidate.label,
11386
- t,
11387
- emailVisible: emailVisibleForAccount,
11388
- onClick: toggleEmail
11503
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
11504
+ className: "codexSubscriptionAccountMain",
11505
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountEmail, {
11506
+ candidate,
11507
+ fallback: candidate.label,
11508
+ t,
11509
+ emailVisible: emailVisibleForAccount,
11510
+ onClick: toggleEmail
11511
+ }), accounts.length > 1 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountQuota, {
11512
+ entry: quotas.entries[candidate.id],
11513
+ loading: quotas.busy,
11514
+ t
11515
+ }) : null]
11389
11516
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
11390
11517
  className: "codexSubscriptionActions",
11391
11518
  children: [
@@ -11789,7 +11916,7 @@ window.__ModuleLoader__.load({
11789
11916
  children: [
11790
11917
  t("diagnosticsSnapshotTime"),
11791
11918
  " ",
11792
- new Date(report.generatedAt).toLocaleString()
11919
+ new Date(report.generatedAt).toLocaleString(uiLocale())
11793
11920
  ]
11794
11921
  }) : null,
11795
11922
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -11807,7 +11934,7 @@ window.__ModuleLoader__.load({
11807
11934
  " · ",
11808
11935
  t(`diagnosticSource_${event.source}`),
11809
11936
  " · ",
11810
- new Date(event.observedAt).toLocaleTimeString()
11937
+ new Date(event.observedAt).toLocaleTimeString(uiLocale())
11811
11938
  ] }, `${event.source}:${event.action}`)) }) : null
11812
11939
  ] }, item.code)) }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("diagnosticsNoRecordedFailure") })
11813
11940
  }),
@@ -11825,7 +11952,7 @@ window.__ModuleLoader__.load({
11825
11952
  ] }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: (report.inspection?.capabilities ?? []).map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
11826
11953
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: t(`diagnosticCapability_${item.id}`) }),
11827
11954
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("td", { children: [t(`diagnosticReason_${item.reason}`), item.blockedBy ? ` · ${t(`diagnosticCapability_${item.blockedBy}`)}` : ""] }),
11828
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("td", { children: [item.latest ? `${item.latest.action} · ${t(`diagnosticSource_${item.latest.source}`)} · ${t(`diagnosticOutcome_${item.latest.status}`)} · ${new Date(item.latest.observedAt).toLocaleTimeString()}` : t("diagnosticsUnverified"), item.unresolved?.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("diagnosticsUnresolved") }) : null] })
11955
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("td", { children: [item.latest ? `${item.latest.action} · ${t(`diagnosticSource_${item.latest.source}`)} · ${t(`diagnosticOutcome_${item.latest.status}`)} · ${new Date(item.latest.observedAt).toLocaleTimeString(uiLocale())}` : t("diagnosticsUnverified"), item.unresolved?.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("diagnosticsUnresolved") }) : null] })
11829
11956
  ] }, item.id)) })]
11830
11957
  }),
11831
11958
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
@@ -11972,8 +12099,8 @@ window.__ModuleLoader__.load({
11972
12099
  }
11973
12100
  //#endregion
11974
12101
  //#region src/client-usage.jsx
11975
- const count = (value) => Number(value).toLocaleString();
11976
- const creditAmount = (value) => Number.isFinite(Number(value)) && String(value).trim() !== "" ? Number(value).toLocaleString(void 0, { maximumFractionDigits: 2 }) : value;
12102
+ const count = (value) => Number(value).toLocaleString(uiLocale());
12103
+ const creditAmount = (value) => Number.isFinite(Number(value)) && String(value).trim() !== "" ? Number(value).toLocaleString(uiLocale(), { maximumFractionDigits: 2 }) : value;
11977
12104
  function CreditExpiry({ credits, t }) {
11978
12105
  const date = validDate(credits.expiresAt);
11979
12106
  if (date === void 0 || credits.unlimited) return null;
@@ -11984,7 +12111,7 @@ window.__ModuleLoader__.load({
11984
12111
  "data-soon": status.soon,
11985
12112
  role: "status",
11986
12113
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [fill(t(status.daysLeft === 0 ? "creditsExpiredToday" : "creditsExpires"), {
11987
- date: date.toLocaleDateString(),
12114
+ date: date.toLocaleDateString(uiLocale()),
11988
12115
  days: status.daysLeft
11989
12116
  }), estimate ? ` ${t("creditsExpiryEstimate")}` : ""] })
11990
12117
  });
@@ -11992,7 +12119,7 @@ window.__ModuleLoader__.load({
11992
12119
  function ResetTime({ resetsAt, t }) {
11993
12120
  const date = Number.isSafeInteger(resetsAt) ? validDate(resetsAt * 1e3) : void 0;
11994
12121
  if (date === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("resetUnknown") });
11995
- const value = date.toLocaleString(void 0, {
12122
+ const value = date.toLocaleString(uiLocale(), {
11996
12123
  month: "numeric",
11997
12124
  day: "numeric",
11998
12125
  hour: "2-digit",
@@ -12000,7 +12127,7 @@ window.__ModuleLoader__.load({
12000
12127
  });
12001
12128
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("time", {
12002
12129
  dateTime: date.toISOString(),
12003
- title: date.toLocaleString(),
12130
+ title: date.toLocaleString(uiLocale()),
12004
12131
  children: fill(t("resets"), { value })
12005
12132
  });
12006
12133
  }
@@ -12010,8 +12137,8 @@ window.__ModuleLoader__.load({
12010
12137
  className: "codexSubscriptionResetExpiry",
12011
12138
  children: date === void 0 ? t("resetCreditExpiryUnknown") : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("time", {
12012
12139
  dateTime: date.toISOString(),
12013
- title: date.toLocaleString(),
12014
- children: fill(t("resetCreditExpires"), { value: date.toLocaleDateString(void 0, {
12140
+ title: date.toLocaleString(uiLocale()),
12141
+ children: fill(t("resetCreditExpires"), { value: date.toLocaleDateString(uiLocale(), {
12015
12142
  month: "long",
12016
12143
  day: "numeric"
12017
12144
  }) })
@@ -12203,6 +12330,7 @@ window.__ModuleLoader__.load({
12203
12330
  return t((/* @__PURE__ */ new Map([
12204
12331
  ["ChatGPT subscription is not signed in", "resetRenewLogin"],
12205
12332
  ["ChatGPT sign-in needs to be renewed", "resetRenewLogin"],
12333
+ ["ChatGPT blocked this request before it reached your account (HTTP 403); your sign-in is fine, try again later or check your network, proxy or VPN", "resetBlocked"],
12206
12334
  ["No quota reset is available", "resetNoCredit"],
12207
12335
  ["No usable quota reset is available", "resetNoCredit"],
12208
12336
  ["The available quota reset expires too soon", "resetExpired"],
@@ -12265,7 +12393,7 @@ window.__ModuleLoader__.load({
12265
12393
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t("usage") }), fetchedAt === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("time", {
12266
12394
  className: "codexSubscriptionFreshness",
12267
12395
  dateTime: fetchedAt.toISOString(),
12268
- children: fill(t("usageUpdated"), { value: fetchedAt.toLocaleString() })
12396
+ children: fill(t("usageUpdated"), { value: fetchedAt.toLocaleString(uiLocale()) })
12269
12397
  })]
12270
12398
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
12271
12399
  className: "codexSubscriptionRefresh",
package/lib/index.js CHANGED
@@ -1135,6 +1135,7 @@ const RPC_ENDPOINTS = Object.freeze([
1135
1135
  "account/select",
1136
1136
  "account/remove",
1137
1137
  "usage",
1138
+ "usage/accounts",
1138
1139
  "diagnostics",
1139
1140
  "preferences/status",
1140
1141
  "preferences/models",
@@ -2087,9 +2088,36 @@ function inheritedOriginalImageRef(session, assetId) {
2087
2088
  }
2088
2089
  //#endregion
2089
2090
  //#region src/version.js
2090
- const PACKAGE_VERSION = "2.6.9";
2091
+ const PACKAGE_VERSION = "2.7.1";
2091
2092
  const USER_AGENT = `dsh-codex-subscription/${PACKAGE_VERSION}`;
2092
2093
  //#endregion
2094
+ //#region src/http-failure.js
2095
+ const SIGN_IN_RENEW = "ChatGPT sign-in needs to be renewed";
2096
+ const REQUEST_BLOCKED = "ChatGPT blocked this request before it reached your account (HTTP 403); your sign-in is fine, try again later or check your network, proxy or VPN";
2097
+ const CHALLENGE_MARKERS = /just a moment|cf-chl|_cf_chl_opt|attention required|enable javascript and cookies|challenge-platform/iu;
2098
+ /** Whether error text is a Cloudflare / network-protection page (pi-ai keeps only the body of a failed request, not its status). */
2099
+ const isChallengePage = (text) => typeof text === "string" && /^\s*<(?:!doctype|html)/iu.test(text) && CHALLENGE_MARKERS.test(text);
2100
+ /** Whether a 403 response is a Cloudflare / network-protection page rather than an answer from the ChatGPT backend. */
2101
+ async function isBlockedByNetwork(response) {
2102
+ if (response?.status !== 403) return false;
2103
+ try {
2104
+ const header = (name) => String(response.headers?.get?.(name) ?? "");
2105
+ if (/challenge/iu.test(header("cf-mitigated"))) return true;
2106
+ if (!/html/iu.test(header("content-type"))) return false;
2107
+ if (/cloudflare/iu.test(header("server"))) return true;
2108
+ const text = typeof response.clone === "function" ? (await response.clone().text()).slice(0, 8192) : "";
2109
+ return CHALLENGE_MARKERS.test(text);
2110
+ } catch {
2111
+ return false;
2112
+ }
2113
+ }
2114
+ /** The error message for a 401 or 403 answer, or undefined for any other status. */
2115
+ async function authFailureMessage(response) {
2116
+ if (response?.status === 401) return SIGN_IN_RENEW;
2117
+ if (response?.status !== 403) return void 0;
2118
+ return await isBlockedByNetwork(response) ? REQUEST_BLOCKED : SIGN_IN_RENEW;
2119
+ }
2120
+ //#endregion
2093
2121
  //#region src/codex-images.js
2094
2122
  const CODEX_IMAGE_TOOL_NAME = "codex_image_generate";
2095
2123
  const CODEX_IMAGE_GENERATION_URL = "https://chatgpt.com/backend-api/codex/images/generations";
@@ -2592,7 +2620,8 @@ function createCodexImageTool(options) {
2592
2620
  throw new Error(`Codex image ${editing ? "edit" : "generation"} request failed${requestFailureReason(error)}`, { cause: error });
2593
2621
  }
2594
2622
  if (!response.ok) {
2595
- if (response.status === 401 || response.status === 403) throw new Error("ChatGPT sign-in needs to be renewed");
2623
+ const authFailure = await authFailureMessage(response);
2624
+ if (authFailure !== void 0) throw new Error(authFailure);
2596
2625
  const requestId = supportId(response);
2597
2626
  if (response.status === 429) throw new Error(`${await limitMessage(response)}${requestId}`);
2598
2627
  throw new Error(`Codex image ${editing ? "edit" : "generation"} failed (HTTP ${response.status})${requestId}`);
@@ -3000,6 +3029,42 @@ var DshOAuthAccountVault = class {
3000
3029
  return clone$1(payload?.accounts.find((account) => account.id === payload.activeId)?.credential);
3001
3030
  });
3002
3031
  }
3032
+ /** One account's credential, whether or not it is the active one. */
3033
+ readAccount(id) {
3034
+ return this.#enqueue(async () => {
3035
+ const payload = await this.#ensurePayload();
3036
+ return clone$1(payload?.accounts.find((account) => account.id === id)?.credential);
3037
+ });
3038
+ }
3039
+ /** Update one account's credential (a token refresh) without changing which account is active. */
3040
+ modifyAccount(id, update) {
3041
+ return this.#enqueue(async () => {
3042
+ let result;
3043
+ await this.#modifyPayload(async (current) => {
3044
+ const index = current.accounts.findIndex((account) => account.id === id);
3045
+ if (index < 0) throw new Error("Unknown Codex account");
3046
+ const previous = clone$1(current.accounts[index].credential);
3047
+ const next = await update(previous);
3048
+ if (next === void 0) {
3049
+ result = previous;
3050
+ return current;
3051
+ }
3052
+ const credential = sanitizeOAuthCredential(next);
3053
+ if (credential.email === void 0 && previous.email !== void 0) credential.email = previous.email;
3054
+ const accounts = [...current.accounts];
3055
+ accounts[index] = {
3056
+ ...accounts[index],
3057
+ credential
3058
+ };
3059
+ result = clone$1(credential);
3060
+ return {
3061
+ ...current,
3062
+ accounts
3063
+ };
3064
+ });
3065
+ return result;
3066
+ });
3067
+ }
3003
3068
  activeId() {
3004
3069
  return this.#enqueue(async () => (await this.#ensurePayload())?.activeId);
3005
3070
  }
@@ -3099,11 +3164,11 @@ var DshOAuthAccountVault = class {
3099
3164
  };
3100
3165
  //#endregion
3101
3166
  //#region src/credential-store.js
3102
- const PROVIDER$1 = "openai-codex";
3167
+ const PROVIDER$2 = "openai-codex";
3103
3168
  const abortIfNeeded = (options) => options?.signal?.throwIfAborted();
3104
3169
  const clone = (value) => value === void 0 ? void 0 : structuredClone(value);
3105
- function assertProvider(providerId) {
3106
- if (providerId !== PROVIDER$1) throw new Error(`Codex credential store does not own provider ${JSON.stringify(providerId)}`);
3170
+ function assertProvider$1(providerId) {
3171
+ if (providerId !== PROVIDER$2) throw new Error(`Codex credential store does not own provider ${JSON.stringify(providerId)}`);
3107
3172
  }
3108
3173
  function assertOAuthCredential(value) {
3109
3174
  if (value === void 0) return void 0;
@@ -3136,7 +3201,7 @@ var DshOAuthCredentialStore = class {
3136
3201
  this.vault = options.vault;
3137
3202
  }
3138
3203
  #enqueue(providerId, operation, options) {
3139
- assertProvider(providerId);
3204
+ assertProvider$1(providerId);
3140
3205
  const current = (this.#chains.get(providerId) ?? Promise.resolve()).catch(() => void 0).then(async () => {
3141
3206
  abortIfNeeded(options);
3142
3207
  return operation();
@@ -3149,7 +3214,7 @@ var DshOAuthCredentialStore = class {
3149
3214
  return current;
3150
3215
  }
3151
3216
  async #read(providerId, options) {
3152
- assertProvider(providerId);
3217
+ assertProvider$1(providerId);
3153
3218
  abortIfNeeded(options);
3154
3219
  if (this.vault !== void 0) {
3155
3220
  const current = await this.vault.readActive();
@@ -3182,8 +3247,8 @@ var DshOAuthCredentialStore = class {
3182
3247
  }
3183
3248
  async list(options) {
3184
3249
  abortIfNeeded(options);
3185
- return await this.read(PROVIDER$1, options) === void 0 ? [] : [{
3186
- providerId: PROVIDER$1,
3250
+ return await this.read(PROVIDER$2, options) === void 0 ? [] : [{
3251
+ providerId: PROVIDER$2,
3187
3252
  type: "oauth"
3188
3253
  }];
3189
3254
  }
@@ -3233,16 +3298,16 @@ function createCodexAuthService(models, store, options = {}) {
3233
3298
  const createPendingStore = options.createPendingStore ?? (() => new PendingOAuthCredentialStore());
3234
3299
  return Object.freeze({
3235
3300
  async status(options) {
3236
- const current = await store.read(PROVIDER$1, options);
3301
+ const current = await store.read(PROVIDER$2, options);
3237
3302
  const accounts = await accountVault?.list();
3238
3303
  if (current === void 0) return {
3239
3304
  authenticated: false,
3240
- provider: PROVIDER$1,
3305
+ provider: PROVIDER$2,
3241
3306
  ...accounts === void 0 ? {} : { accounts }
3242
3307
  };
3243
3308
  return {
3244
3309
  authenticated: true,
3245
- provider: PROVIDER$1,
3310
+ provider: PROVIDER$2,
3246
3311
  type: "oauth",
3247
3312
  expiresAt: current.expires,
3248
3313
  ...accounts === void 0 ? {} : { accounts }
@@ -3253,13 +3318,13 @@ function createCodexAuthService(models, store, options = {}) {
3253
3318
  if (accountVault === void 0 || createLoginModels === void 0) throw new Error("Codex multi-account is unavailable");
3254
3319
  return runLogin(async () => {
3255
3320
  const pending = createPendingStore();
3256
- await createLoginModels(pending).login(PROVIDER$1, "oauth", interaction);
3321
+ await createLoginModels(pending).login(PROVIDER$2, "oauth", interaction);
3257
3322
  const credential = pending.credential();
3258
3323
  if (credential === void 0) throw new Error("Codex login did not return credentials");
3259
3324
  await accountVault.add(input.label, credential);
3260
3325
  });
3261
3326
  }
3262
- return runLogin(() => models.login(PROVIDER$1, "oauth", interaction));
3327
+ return runLogin(() => models.login(PROVIDER$2, "oauth", interaction));
3263
3328
  },
3264
3329
  async select(id) {
3265
3330
  if (accountVault === void 0) throw new Error("Codex multi-account is unavailable");
@@ -3272,7 +3337,7 @@ function createCodexAuthService(models, store, options = {}) {
3272
3337
  return this.status();
3273
3338
  },
3274
3339
  logout(options) {
3275
- return models.logout(PROVIDER$1, options);
3340
+ return models.logout(PROVIDER$2, options);
3276
3341
  }
3277
3342
  });
3278
3343
  }
@@ -3808,6 +3873,13 @@ function normalizeTransportEvent(event, signal) {
3808
3873
  if (signal?.aborted || event?.type !== "error" || event.reason !== "error") return event;
3809
3874
  const message = event.error?.errorMessage;
3810
3875
  if (typeof message !== "string") return event;
3876
+ if (isChallengePage(message)) return {
3877
+ ...event,
3878
+ error: {
3879
+ ...event.error,
3880
+ errorMessage: REQUEST_BLOCKED
3881
+ }
3882
+ };
3811
3883
  if (!TRANSIENT_WEBSOCKET_CLOSE.test(message) && !GATEWAY_REPLAY_BUFFER_EXCEEDED.test(message)) return event;
3812
3884
  return {
3813
3885
  ...event,
@@ -4532,7 +4604,10 @@ function createCodexSearchProvider(options) {
4532
4604
  if (signal?.aborted || error?.name === "AbortError") throw new WebError("Codex search aborted", "WEB_ABORTED", { cause: error });
4533
4605
  throw new WebError("Codex search request failed", "WEB_PROVIDER_ERROR", { cause: error });
4534
4606
  }
4535
- if (!response.ok) throw response.status === 401 || response.status === 403 ? new WebError("ChatGPT sign-in needs to be renewed", "WEB_PROVIDER_CREDENTIAL_MISSING") : new WebError(`Codex search request failed (HTTP ${response.status})`, "WEB_PROVIDER_ERROR");
4607
+ if (!response.ok) {
4608
+ const authFailure = await authFailureMessage(response);
4609
+ throw authFailure === "ChatGPT sign-in needs to be renewed" ? new WebError(authFailure, "WEB_PROVIDER_CREDENTIAL_MISSING") : authFailure !== void 0 ? new WebError(authFailure, "WEB_PROVIDER_ERROR") : new WebError(`Codex search request failed (HTTP ${response.status})`, "WEB_PROVIDER_ERROR");
4610
+ }
4536
4611
  let value;
4537
4612
  try {
4538
4613
  value = await response.json();
@@ -5531,7 +5606,7 @@ function createCodexUsageReader(options) {
5531
5606
  signal: requestSignal$1(signal, timeoutMs)
5532
5607
  });
5533
5608
  if (!response.ok) {
5534
- const error = /* @__PURE__ */ new Error(response.status === 401 || response.status === 403 ? "ChatGPT sign-in needs to be renewed" : `ChatGPT usage request failed (HTTP ${response.status})`);
5609
+ const error = new Error(await authFailureMessage(response) ?? `ChatGPT usage request failed (HTTP ${response.status})`);
5535
5610
  Object.defineProperty(error, "retryAfterMs", { value: retryAfterMs(response, now, maxRetryAfterMs) });
5536
5611
  throw error;
5537
5612
  }
@@ -5582,6 +5657,102 @@ function createCodexUsageReader(options) {
5582
5657
  });
5583
5658
  }
5584
5659
  //#endregion
5660
+ //#region src/account-usage.js
5661
+ const PROVIDER$1 = "openai-codex";
5662
+ const MAX_PARALLEL = 4;
5663
+ const assertProvider = (providerId) => {
5664
+ if (providerId !== PROVIDER$1) throw new Error("Codex account usage received an unknown provider");
5665
+ };
5666
+ /** pi-ai's OAuth credential store, bound to one account of the vault. */
5667
+ function accountCredentialStore(vault, id, expirySkewMs = 0) {
5668
+ const shift = (credential) => credential === void 0 || expirySkewMs === 0 ? credential : {
5669
+ ...credential,
5670
+ expires: credential.expires - expirySkewMs
5671
+ };
5672
+ return {
5673
+ async read(providerId) {
5674
+ assertProvider(providerId);
5675
+ return shift(await vault.readAccount(id));
5676
+ },
5677
+ async list() {
5678
+ return await vault.readAccount(id) === void 0 ? [] : [{
5679
+ providerId: PROVIDER$1,
5680
+ type: "oauth"
5681
+ }];
5682
+ },
5683
+ async modify(providerId, update) {
5684
+ assertProvider(providerId);
5685
+ return vault.modifyAccount(id, async (current) => update(shift(current)));
5686
+ },
5687
+ async delete() {
5688
+ throw new Error("An account usage read cannot remove a sign-in");
5689
+ }
5690
+ };
5691
+ }
5692
+ /**
5693
+ * @param {object} options
5694
+ * @param {{ list(): Promise<object[]>, readAccount(id: string): Promise<object|undefined>, modifyAccount: Function }} options.vault
5695
+ * @param {(credentials: object) => object} options.createModels builds a pi-ai Models over a credential store, with the Codex provider set
5696
+ * @param {typeof fetch} options.fetch
5697
+ */
5698
+ function createAccountUsageReader({ vault, createModels, fetch, expirySkewMs = 0, now = Date.now, activeReader }) {
5699
+ const readers = /* @__PURE__ */ new Map();
5700
+ const readerFor = (id) => {
5701
+ let reader = readers.get(id);
5702
+ if (reader === void 0) {
5703
+ const models = createModels(accountCredentialStore(vault, id, expirySkewMs));
5704
+ reader = createCodexUsageReader({
5705
+ getAuth: () => models.getAuth(PROVIDER$1),
5706
+ readCredential: () => vault.readAccount(id).then((credential) => credential),
5707
+ fetch,
5708
+ now
5709
+ });
5710
+ readers.set(id, reader);
5711
+ }
5712
+ return reader;
5713
+ };
5714
+ const summarize = (error, signal) => {
5715
+ if (signal?.aborted) throw error;
5716
+ const message = error instanceof Error ? error.message : "";
5717
+ return ["ChatGPT subscription is not signed in", "ChatGPT sign-in needs to be renewed"].includes(message) || message.includes("blocked this request") ? message : "Could not read ChatGPT usage";
5718
+ };
5719
+ return Object.freeze({
5720
+ /** One entry per account: its usage reading, or the reason it could not be read. */
5721
+ async readAll({ force = false, signal } = {}) {
5722
+ const accounts = await vault.list();
5723
+ for (const id of [...readers.keys()]) if (!accounts.some((account) => account.id === id)) readers.delete(id);
5724
+ const entries = new Array(accounts.length);
5725
+ let next = 0;
5726
+ const worker = async () => {
5727
+ while (next < accounts.length) {
5728
+ const index = next++;
5729
+ const account = accounts[index];
5730
+ try {
5731
+ const reader = account.active && activeReader !== void 0 ? activeReader : readerFor(account.id);
5732
+ entries[index] = {
5733
+ id: account.id,
5734
+ usage: await reader.read({
5735
+ force,
5736
+ signal
5737
+ })
5738
+ };
5739
+ } catch (error) {
5740
+ entries[index] = {
5741
+ id: account.id,
5742
+ error: summarize(error, signal)
5743
+ };
5744
+ }
5745
+ }
5746
+ };
5747
+ await Promise.all(Array.from({ length: Math.min(MAX_PARALLEL, accounts.length) }, worker));
5748
+ return entries;
5749
+ },
5750
+ clear() {
5751
+ readers.clear();
5752
+ }
5753
+ });
5754
+ }
5755
+ //#endregion
5585
5756
  //#region src/quota-retry.js
5586
5757
  const DEFAULT_MAX_WAIT_MS = 360 * 60 * 1e3;
5587
5758
  const DEFAULT_RESET_MARGIN_MS = 1e4;
@@ -6302,7 +6473,7 @@ function createCodexResetCreditService(options) {
6302
6473
  },
6303
6474
  signal: requestSignal(signal, timeoutMs)
6304
6475
  });
6305
- if (!response.ok) throw new Error(response.status === 401 || response.status === 403 ? "ChatGPT sign-in needs to be renewed" : `ChatGPT quota reset request failed (HTTP ${response.status})`);
6476
+ if (!response.ok) throw new Error(await authFailureMessage(response) ?? `ChatGPT quota reset request failed (HTTP ${response.status})`);
6306
6477
  let raw;
6307
6478
  try {
6308
6479
  raw = await response.json();
@@ -6436,7 +6607,7 @@ function createCodexResetCreditService(options) {
6436
6607
  throw new Error(UNCERTAIN_RESET_RESULT);
6437
6608
  }
6438
6609
  if (response.status !== 401 && response.status !== 403) retryable = false;
6439
- throw new Error(response.status === 401 || response.status === 403 ? "ChatGPT sign-in needs to be renewed" : `ChatGPT quota reset request failed (HTTP ${response.status})`);
6610
+ throw new Error(await authFailureMessage(response) ?? `ChatGPT quota reset request failed (HTTP ${response.status})`);
6440
6611
  }
6441
6612
  let raw;
6442
6613
  try {
@@ -6478,7 +6649,7 @@ const publicError = (code, message) => ({
6478
6649
  details: { issues: [] }
6479
6650
  }
6480
6651
  });
6481
- function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditService, preferences, defaultModel, runtimeManagement, diagnosticsReader, modelCatalog, originalImages, resolveInheritedOriginal, closeConnections, onAccountChanged, onCleanupFailure }) {
6652
+ function createSubscriptionRpcHandler({ authHandler, usageReader, accountUsageReader, resetCreditService, preferences, defaultModel, runtimeManagement, diagnosticsReader, modelCatalog, originalImages, resolveInheritedOriginal, closeConnections, onAccountChanged, onCleanupFailure }) {
6482
6653
  return async (endpoint, payload, signal) => {
6483
6654
  if (endpoint === "default-model/status" || endpoint === "default-model/select") try {
6484
6655
  signal.throwIfAborted();
@@ -6625,10 +6796,27 @@ function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditSer
6625
6796
  };
6626
6797
  } catch (error) {
6627
6798
  if (signal.aborted) throw error;
6628
- const known = /* @__PURE__ */ new Set(["ChatGPT subscription is not signed in", "ChatGPT sign-in needs to be renewed"]);
6799
+ const known = /* @__PURE__ */ new Set([
6800
+ "ChatGPT subscription is not signed in",
6801
+ "ChatGPT sign-in needs to be renewed",
6802
+ REQUEST_BLOCKED
6803
+ ]);
6629
6804
  const message = error instanceof Error && known.has(error.message) ? error.message : "Could not read ChatGPT usage";
6630
6805
  return publicError("internal", message);
6631
6806
  }
6807
+ if (endpoint === "usage/accounts") try {
6808
+ signal.throwIfAborted();
6809
+ return {
6810
+ ok: true,
6811
+ value: { accounts: await accountUsageReader.readAll({
6812
+ force: payload?.force === true,
6813
+ signal
6814
+ }) }
6815
+ };
6816
+ } catch (error) {
6817
+ if (signal.aborted) throw error;
6818
+ return publicError("internal", "Could not read the usage of the signed-in accounts");
6819
+ }
6632
6820
  if (endpoint === "reset-credit/inspect" || endpoint === "reset-credit/prepare" || endpoint === "reset-credit/consume") try {
6633
6821
  signal.throwIfAborted();
6634
6822
  return {
@@ -6647,6 +6835,7 @@ function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditSer
6647
6835
  const known = /* @__PURE__ */ new Set([
6648
6836
  "ChatGPT subscription is not signed in",
6649
6837
  "ChatGPT sign-in needs to be renewed",
6838
+ REQUEST_BLOCKED,
6650
6839
  "No quota reset is available",
6651
6840
  "No usable quota reset is available",
6652
6841
  "The available quota reset expires too soon",
@@ -6669,7 +6858,7 @@ function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditSer
6669
6858
  () => modelCatalog?.clear(),
6670
6859
  () => onAccountChanged?.()
6671
6860
  ];
6672
- if (endpoint === "logout") tasks.push(() => usageReader.clear());
6861
+ if (endpoint === "logout") tasks.push(() => usageReader.clear(), () => accountUsageReader?.clear());
6673
6862
  if (endpoint === "account/remove" && typeof payload?.id === "string") tasks.push(() => usageReader.clearScope(payload.id));
6674
6863
  if ((await Promise.allSettled(tasks.map((task) => Promise.resolve().then(task)))).some((value) => value.status === "rejected")) try {
6675
6864
  onCleanupFailure?.();
@@ -7030,12 +7219,13 @@ function apply(ctx, config = {}) {
7030
7219
  }
7031
7220
  });
7032
7221
  const coordinator = new CodexLoginCoordinator(auth);
7222
+ const baseUsageReader = createCodexUsageReader({
7223
+ getAuth: resolveAuth,
7224
+ readCredential: (options) => store.read(PROVIDER, options),
7225
+ fetch: (input, init) => network.fetch("quota", input, init)
7226
+ });
7033
7227
  const usageReader = createQuotaForecastReader({
7034
- reader: createCodexUsageReader({
7035
- getAuth: resolveAuth,
7036
- readCredential: (options) => store.read(PROVIDER, options),
7037
- fetch: (input, init) => network.fetch("quota", input, init)
7038
- }),
7228
+ reader: baseUsageReader,
7039
7229
  enabled: () => normalizeQuickQuotaMode(settings.get()[QUICK_QUOTA_MODE_FIELD], settings.get()[LEGACY_QUICK_QUOTA_FIELD]) === QUICK_QUOTA_MODE_FORECAST,
7040
7230
  scope: async () => await accountVault?.activeId() ?? "legacy",
7041
7231
  stateStore: new QuotaForecastStateStore({ filename: dshHomePath("state", "codex-subscription", "quota-forecast.json") })
@@ -7109,9 +7299,21 @@ function apply(ctx, config = {}) {
7109
7299
  sketchBridge.dispose();
7110
7300
  };
7111
7301
  }, "codex-subscription: native sketch tool");
7302
+ const accountUsageReader = createAccountUsageReader({
7303
+ vault: accountVault,
7304
+ createModels: (credentials) => {
7305
+ const accountModels = createModels({ credentials });
7306
+ accountModels.setProvider(provider);
7307
+ return accountModels;
7308
+ },
7309
+ fetch: (input, init) => network.fetch("quota", input, init),
7310
+ expirySkewMs: OAUTH_EXPIRY_SKEW_MS,
7311
+ activeReader: baseUsageReader
7312
+ });
7112
7313
  const subscriptionHandler = createSubscriptionRpcHandler({
7113
7314
  authHandler: createCodexRpcHandler(coordinator, { openExternal: openCodexAuthUrl }),
7114
7315
  usageReader,
7316
+ accountUsageReader,
7115
7317
  resetCreditService,
7116
7318
  preferences,
7117
7319
  defaultModel,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-codex-subscription",
3
- "version": "2.6.9",
3
+ "version": "2.7.1",
4
4
  "description": "Use ChatGPT and Codex subscriptions in DeepSeek Harness with OAuth, quota, safe resets, web search, images, and Fast mode",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",