dsh-codex-subscription 1.12.2 → 1.13.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/AGENTS.md CHANGED
@@ -6,7 +6,7 @@ Use this guide when a user asks an Agent to install, update, verify, or remove
6
6
  ## Safety
7
7
 
8
8
  - Confirm the target DSH installation and profile. Use `web` only when it is the user's target.
9
- - Use the exact `1.12.2` package below; do not install a moving branch.
9
+ - Use the exact `1.13.1` package below; do not install a moving branch.
10
10
  - Never print OAuth credentials, account IDs, authorization callbacks, or credential-store contents.
11
11
  - Preserve the DSH profile, unrelated plugins, sessions, and saved sign-in.
12
12
  - Do not start, stop, or restart DSH without explicit permission.
@@ -34,13 +34,13 @@ For an official DSH installation run through npm, keep the complete `npx` prefix
34
34
  run command does not create a global `dsh` command:
35
35
 
36
36
  ```sh
37
- npx -y @deepseek-ai/dsh@0.1.2-rc.1 plugin --profile web add dsh-codex-subscription@1.12.2
37
+ npx -y @deepseek-ai/dsh@0.1.2-rc.1 plugin --profile web add dsh-codex-subscription@1.13.1
38
38
  ```
39
39
 
40
40
  With an existing global `dsh` command:
41
41
 
42
42
  ```sh
43
- dsh plugin --profile web add dsh-codex-subscription@1.12.2
43
+ dsh plugin --profile web add dsh-codex-subscription@1.13.1
44
44
  ```
45
45
 
46
46
  Use the same `add` command to update or repair. This is the complete package-changing operation.
@@ -58,7 +58,7 @@ dsh --profile web --dump-config
58
58
 
59
59
  For the official npm route, run the same checks with `npx -y @deepseek-ai/dsh@0.1.2-rc.1` in place of `dsh`.
60
60
 
61
- 1. `dsh-codex-subscription` version `1.12.2` appears exactly once.
61
+ 1. `dsh-codex-subscription` version `1.13.1` appears exactly once.
62
62
  2. `codex-subscription` appears exactly once in the composed config.
63
63
  3. No unrelated plugin or profile was changed and DSH was not restarted.
64
64
 
package/README.en.md CHANGED
@@ -43,7 +43,7 @@ DSH-Portable exposes the same standard plugin command, so the command above also
43
43
  | **Recoverable and diagnosable** | Sign-in state reconciles automatically; Settings can create a support report without credentials or account identifiers |
44
44
  | **Visible quota** | Keep backend-provided standard Codex, Spark, and other limits separate |
45
45
  | **Composer quota** | Choose a compact percentage, progress bar, or no inline quota display |
46
- | **Safe quota reset** | See the next expiry and deliberately try a reset with a cooldown and acknowledgement |
46
+ | **Safe quota reset** | See each reset credit separately and deliberately try one with a cooldown and acknowledgement |
47
47
  | **Subscription search** | Explicitly route search globally through DSH default search or the signed-in Codex subscription |
48
48
  | **Codex image generation and editing (Beta)** | Generate without references, or explicitly edit one selected image; preview, zoom, annotate regions, download the original, and continue in the same composer |
49
49
  | **Fast mode** | Switch between Standard and Fast directly in the composer |
@@ -133,12 +133,12 @@ Restart DSH manually after installation, then:
133
133
 
134
134
  ## Features
135
135
 
136
- - ChatGPT OAuth sign-in with credentials kept on the host; manually add, switch, and remove accounts without automatic rotation or quota pooling;
136
+ - ChatGPT OAuth sign-in with credentials kept on the host; accounts are identified by a privacy-masked email that can be clicked to reveal, and can be manually added, switched, or removed without automatic rotation or quota pooling;
137
137
  - Codex models and Beta image generation/editing directly inside DSH conversations;
138
138
  - A clear global choice between DSH default search and Codex subscription search; it applies to every model and session rather than following the selected model;
139
139
  - Actual backend-provided quota, reset time, and freshness;
140
140
  - Separate standard Codex, Codex-Spark, Credits, and other independent limits;
141
- - Visible quota-reset count and earliest expiry, with deliberate early redemption, layered confirmation, and no automatic retry;
141
+ - One row per available quota reset with its disclosed name and expiry, plus deliberate early redemption, layered confirmation, and no automatic retry;
142
142
  - Optional percentage, progress bar, or Beta runway estimate for the selected Codex model (off by default);
143
143
  - Standard or Fast mode for supported Codex models directly in the composer;
144
144
  - Standard, Extended, and per-model Custom context windows; Custom accepts a full numeric token count, stays within each audited model capacity, and feeds DSH's native agent compaction policy;
@@ -159,7 +159,7 @@ Standard Codex uses the lowest remaining window returned by the service; Spark u
159
159
 
160
160
  ### Safe quota reset
161
161
 
162
- If ChatGPT reports an available quota reset, Settings shows its count and earliest disclosed expiry in a compact row.
162
+ If ChatGPT reports available quota resets, Settings shows each one in its own compact row with its disclosed name and expiry.
163
163
  You may deliberately try it before a quota reaches 100%, which is useful for a reset nearing expiry. ChatGPT still
164
164
  decides whether a window needs resetting and may return **nothing to reset** without spending the reset. The final
165
165
  action requires an acknowledgement checkbox and five-second cooldown. Cancel never consumes a reset, rapid repeated
package/README.md CHANGED
@@ -44,7 +44,7 @@ DSH-Portable 也提供相同的标准插件命令,因此同样使用上面的
44
44
  | **可恢复、可诊断** | 登录状态会自动对账;设置页可生成不含凭据和账号标识的支持报告 |
45
45
  | **额度可见** | 普通 Codex、Spark 等服务端实际返回的额度分开显示 |
46
46
  | **输入框额度** | 可选择紧凑百分比、进度条或完全关闭输入框额度显示 |
47
- | **安全额度重置** | 直接查看最早到期时间,并通过冷静期和知情确认主动尝试重置 |
47
+ | **安全额度重置** | 每张重置卡单独显示,并通过冷静期和知情确认主动尝试重置 |
48
48
  | **订阅搜索** | 可将全部模型的搜索明确路由到 DSH 默认搜索或已登录的 Codex 订阅 |
49
49
  | **Codex 图片生成与编辑(Beta)** | 可无参考图全新生成,也可明确选择会话图片继续编辑;支持预览、缩放、区域备注、下载原图并衔接回当前输入框 |
50
50
  | **高速模式** | 直接在输入框切换标准或高速,无需离开当前会话 |
@@ -134,12 +134,12 @@ dsh --profile web --dump-config
134
134
 
135
135
  ## 功能
136
136
 
137
- - ChatGPT OAuth 登录,凭据保留在本机;可手动添加、切换和移除多个账号,不会自动轮换或合并额度;
137
+ - ChatGPT OAuth 登录,凭据保留在本机;账号以默认隐藏部分字符的邮箱区分,点击可切换完整显示;可手动添加、切换和移除多个账号,不会自动轮换或合并额度;
138
138
  - Codex 模型和 Beta 图片生成与编辑直接出现在 DSH 会话中;
139
139
  - 搜索来源是全局设置,可在 DSH 默认搜索与 Codex 订阅搜索之间切换;它对所有模型和会话生效,不会随当前模型自动切换;
140
140
  - 设置页显示服务端返回的额度、重置时间和更新时间;
141
141
  - 普通 Codex、Codex-Spark、Credits 等独立额度分开显示;
142
- - 显示重置卡数量与最早到期时间,也允许在额度未完全用尽时主动尝试,并经过分层确认且不会自动重试;
142
+ - 每张可用重置卡单独显示名称和到期时间,也允许在额度未完全用尽时主动尝试,并经过分层确认且不会自动重试;
143
143
  - 输入框可用百分比、进度条或可选的 Beta 续航预测显示当前 Codex 模型的剩余额度(默认关闭);
144
144
  - 输入框可为支持的 Codex 模型切换标准或高速模式;
145
145
  - 上下文窗口提供标准、扩展和逐模型自定义;自定义直接填写完整 Token 数值,并在已审核的模型容量内交给 DSH 原生 Agent 压缩策略处理;
@@ -159,7 +159,7 @@ Spark 使用独立额度。插件不会写死“5 小时 + 每周”,也不会
159
159
 
160
160
  ### 安全使用额度重置
161
161
 
162
- ChatGPT 返回可用重置卡时,设置页会用紧凑的一行显示数量和服务端提供的最早到期时间。即使额度尚未到 100%,
162
+ ChatGPT 返回可用重置卡时,设置页会把每张卡分别显示为紧凑的一行,并展示服务端提供的名称和到期时间。即使额度尚未到 100%,
163
163
  也可以主动尝试使用,适合重置卡即将过期的情况;是否需要重置仍由 ChatGPT 判断,服务端可能返回“当前无需重置”且不扣次数。
164
164
  最终操作需要勾选知情确认并等待 5 秒。取消不会消耗,快速连续点击只允许一次请求,网络结果不确定时也不会自动重试。
165
165
 
package/lib/client.js CHANGED
@@ -220,6 +220,148 @@ window.__ModuleLoader__.load({
220
220
  }
221
221
  }
222
222
  //#endregion
223
+ //#region src/preference-controller.js
224
+ const CHANNEL$1 = "/codex-subscription";
225
+ const unwrap$1 = (response) => {
226
+ if (!response?.ok) throw new Error(response?.error?.message ?? "Codex RPC failed");
227
+ return response.value;
228
+ };
229
+ function createPreferenceController(scope, rpc) {
230
+ let updating = false;
231
+ let error = false;
232
+ let fallbackStatus = "loading";
233
+ let fallback;
234
+ let pendingPatch;
235
+ let failedPatch;
236
+ let generation = 0;
237
+ let contextModels = [];
238
+ let verbosityModels = [];
239
+ const nativeSnapshot = () => scope.getSnapshot();
240
+ const read = () => {
241
+ const native = nativeSnapshot();
242
+ const current = native.status === "ready" ? native : fallbackStatus === "ready" ? fallback : native;
243
+ const value = pendingPatch === void 0 ? current.value : {
244
+ ...current.value,
245
+ ...pendingPatch
246
+ };
247
+ return Object.freeze({
248
+ status: current.status,
249
+ quickQuotaMode: normalizeQuickQuotaMode(value?.[QUICK_QUOTA_MODE_FIELD], value?.[LEGACY_QUICK_QUOTA_FIELD]),
250
+ searchProvider: normalizeSearchProvider(value?.[SEARCH_PROVIDER_FIELD]),
251
+ speedMode: normalizeSpeedMode(value?.[SPEED_MODE_FIELD]),
252
+ outputVerbosity: normalizeOutputVerbosity(value?.[OUTPUT_VERBOSITY_FIELD]),
253
+ contextMode: normalizeContextMode(value?.[CONTEXT_MODE_FIELD]),
254
+ customContextWindow: normalizeCustomContextWindow(value?.[CUSTOM_CONTEXT_WINDOW_FIELD]),
255
+ customContextWindows: Object.fromEntries(Object.entries(CUSTOM_CONTEXT_MODEL_FIELDS).map(([modelKey, field]) => [modelKey, normalizeCustomContextWindow(value?.[field] ?? CUSTOM_CONTEXT_MODEL_DEFAULTS[modelKey], CUSTOM_CONTEXT_MODEL_CAPS[modelKey])])),
256
+ contextModels,
257
+ verbosityModels,
258
+ writable: !updating && current.status === "ready" && current.writable === true,
259
+ saving: updating,
260
+ error
261
+ });
262
+ };
263
+ let snapshot = read();
264
+ const listeners = /* @__PURE__ */ new Set();
265
+ const publish = () => {
266
+ snapshot = read();
267
+ for (const listener of listeners) listener();
268
+ };
269
+ const disposeScope = scope.subscribe(() => {
270
+ error = false;
271
+ if (!updating) failedPatch = void 0;
272
+ publish();
273
+ });
274
+ const acceptFallback = (value) => {
275
+ contextModels = Array.isArray(value?.contextModels) ? value.contextModels : [];
276
+ verbosityModels = Array.isArray(value?.verbosityModels) ? value.verbosityModels : [];
277
+ fallbackStatus = "ready";
278
+ fallback = {
279
+ status: "ready",
280
+ value: {
281
+ [QUICK_QUOTA_MODE_FIELD]: normalizeQuickQuotaMode(value?.[QUICK_QUOTA_MODE_FIELD], value?.[LEGACY_QUICK_QUOTA_FIELD]),
282
+ [SEARCH_PROVIDER_FIELD]: normalizeSearchProvider(value?.[SEARCH_PROVIDER_FIELD]),
283
+ [SPEED_MODE_FIELD]: normalizeSpeedMode(value?.[SPEED_MODE_FIELD]),
284
+ [OUTPUT_VERBOSITY_FIELD]: normalizeOutputVerbosity(value?.[OUTPUT_VERBOSITY_FIELD]),
285
+ [CONTEXT_MODE_FIELD]: normalizeContextMode(value?.[CONTEXT_MODE_FIELD]),
286
+ [CUSTOM_CONTEXT_WINDOW_FIELD]: normalizeCustomContextWindow(value?.[CUSTOM_CONTEXT_WINDOW_FIELD]),
287
+ ...Object.fromEntries(Object.entries(CUSTOM_CONTEXT_MODEL_FIELDS).map(([modelKey, field]) => [field, normalizeCustomContextWindow(value?.[field] ?? CUSTOM_CONTEXT_MODEL_DEFAULTS[modelKey], CUSTOM_CONTEXT_MODEL_CAPS[modelKey])]))
288
+ },
289
+ writable: value?.writable === true
290
+ };
291
+ };
292
+ const load = async () => {
293
+ const current = ++generation;
294
+ updating = false;
295
+ pendingPatch = void 0;
296
+ fallbackStatus = "loading";
297
+ fallback = void 0;
298
+ error = false;
299
+ publish();
300
+ try {
301
+ const value = unwrap$1(await rpc.call(CHANNEL$1, "preferences/status", {}));
302
+ if (current !== generation) return;
303
+ if (nativeSnapshot().status === "ready") {
304
+ contextModels = Array.isArray(value?.contextModels) ? value.contextModels : [];
305
+ verbosityModels = Array.isArray(value?.verbosityModels) ? value.verbosityModels : [];
306
+ } else acceptFallback(value);
307
+ publish();
308
+ } catch {
309
+ if (current !== generation || nativeSnapshot().status === "ready") return;
310
+ fallbackStatus = "unavailable";
311
+ publish();
312
+ }
313
+ };
314
+ const set = async (patch) => {
315
+ if (snapshot.status !== "ready" || snapshot.writable !== true) return;
316
+ const current = ++generation;
317
+ const entries = Object.entries(patch);
318
+ updating = true;
319
+ pendingPatch = patch;
320
+ error = false;
321
+ failedPatch = void 0;
322
+ publish();
323
+ try {
324
+ if (nativeSnapshot().status === "ready") {
325
+ for (const [field, value] of entries) {
326
+ if (current !== generation) return;
327
+ await scope.set(field, value);
328
+ }
329
+ if (current !== generation) return;
330
+ const accepted = nativeSnapshot().value;
331
+ error = entries.some(([field, value]) => accepted?.[field] !== value);
332
+ pendingPatch = void 0;
333
+ } else {
334
+ const value = unwrap$1(await rpc.call(CHANNEL$1, "preferences/update", patch));
335
+ if (current !== generation) return;
336
+ acceptFallback(value);
337
+ pendingPatch = void 0;
338
+ }
339
+ } catch {
340
+ if (current === generation) {
341
+ pendingPatch = void 0;
342
+ error = true;
343
+ failedPatch = patch;
344
+ }
345
+ } finally {
346
+ if (current === generation) {
347
+ updating = false;
348
+ publish();
349
+ }
350
+ }
351
+ };
352
+ return {
353
+ getSnapshot: () => snapshot,
354
+ subscribe: (listener) => {
355
+ listeners.add(listener);
356
+ return () => listeners.delete(listener);
357
+ },
358
+ load,
359
+ set,
360
+ retry: () => failedPatch === void 0 ? load() : set(failedPatch),
361
+ dispose: disposeScope
362
+ };
363
+ }
364
+ //#endregion
223
365
  //#region src/client.jsx
224
366
  const inject = [
225
367
  "slots",
@@ -246,7 +388,6 @@ window.__ModuleLoader__.load({
246
388
  deviceLogin: "设备代码登录",
247
389
  logout: "退出登录",
248
390
  addAccount: "添加账号",
249
- accountLabel: "账号名称",
250
391
  switchAccount: "切换",
251
392
  removeAccount: "移除",
252
393
  removeConfirm: "确认移除",
@@ -262,12 +403,15 @@ window.__ModuleLoader__.load({
262
403
  loadFailed: "无法读取账户状态。",
263
404
  accountRetry: "重试",
264
405
  diagnostics: "支持诊断",
265
- diagnosticsHint: "生成不含凭据、账号标识和授权时间的诊断信息。",
266
406
  diagnosticsLoad: "生成诊断",
407
+ diagnosticsLoading: "生成中…",
267
408
  diagnosticsCopy: "复制诊断",
268
409
  diagnosticsCopied: "已复制",
269
410
  diagnosticsFailed: "无法生成诊断信息。",
270
411
  feedbackOpen: "反馈问题",
412
+ showEmail: "显示完整邮箱",
413
+ hideEmail: "隐藏邮箱",
414
+ emailUnavailable: "邮箱不可用",
271
415
  searchTitle: "搜索来源",
272
416
  searchScope: "自动按当前会话模型分流;手动选择会覆盖所有模型和会话。",
273
417
  searchAuto: "自动",
@@ -299,21 +443,21 @@ window.__ModuleLoader__.load({
299
443
  creditsUnit: "credits",
300
444
  unlimited: "不限额",
301
445
  monthlyCreditLimit: "Credits 月度消费上限",
302
- resetCredits: "可用额度重置次数",
303
- resetCreditsValue: "{count} 次",
304
- resetUse: "使用重置",
305
- resetPreparing: "正在读取重置详情…",
446
+ resetCredits: "额度重置",
447
+ resetCreditDefaultName: "额度重置",
448
+ resetUse: "使用",
449
+ resetPreparing: "准备中…",
306
450
  resetConfirmTitle: "确认使用额度重置",
307
- resetWarning: " ChatGPT 执行重置,将立即消耗 1 次且无法撤销。",
308
- resetEarlyWarning: "当前模型额度尚未用尽;ChatGPT 可能执行重置,也可能判定暂无需重置且不消耗次数。",
451
+ resetWarning: "执行后会消耗 1 次,且无法撤销。",
452
+ resetEarlyWarning: "当前额度未用尽,服务可能不执行重置。",
309
453
  resetAcknowledge: "我知道这次操作可能立即消耗 1 次重置",
310
454
  resetCreditExpires: "到期:{value}",
311
455
  resetCreditExpiryUnknown: "到期时间未提供",
312
456
  resetCreditExpiryLoading: "正在读取到期时间…",
313
457
  resetCreditExpiryFailed: "无法读取到期时间",
314
- resetWait: "请再等待 {count} 秒",
315
- resetFinal: "消耗 1 次并重置额度",
316
- resetUsing: "正在重置…",
458
+ resetWait: "请等待 {count} 秒",
459
+ resetFinal: "确认使用",
460
+ resetUsing: "使用中…",
317
461
  resetSuccess: "额度重置已完成。",
318
462
  resetNothing: "当前没有可重置的额度,未消耗新的重置次数。",
319
463
  resetNoCredit: "没有可用的额度重置。",
@@ -326,7 +470,7 @@ window.__ModuleLoader__.load({
326
470
  resetAcknowledgeRequired: "请先确认已了解这次操作可能消耗重置次数。",
327
471
  resetAccountChanged: "登录账号已变更,请重新开始。",
328
472
  resetUncertain: "服务端返回结果不确定。请再次确认,插件会复用同一个请求,不会另外发起一次重置。",
329
- creditsNote: "仅显示 Codex 为此账户或工作区实际返回的额外 Credits、消费上限或额度重置次数;三者不是同一项。",
473
+ creditsNote: "额外 Credits、消费上限、重置次数分别显示。",
330
474
  creditsUsed: "已用 {used} / {limit} credits",
331
475
  spendReached: "Credits 月度消费上限已用尽。",
332
476
  unavailable: "暂无数据",
@@ -429,7 +573,6 @@ window.__ModuleLoader__.load({
429
573
  deviceLogin: "Device-code sign-in",
430
574
  logout: "Sign out",
431
575
  addAccount: "Add account",
432
- accountLabel: "Account name",
433
576
  switchAccount: "Switch",
434
577
  removeAccount: "Remove",
435
578
  removeConfirm: "Confirm remove",
@@ -445,12 +588,15 @@ window.__ModuleLoader__.load({
445
588
  loadFailed: "Could not read account status.",
446
589
  accountRetry: "Retry",
447
590
  diagnostics: "Support diagnostics",
448
- diagnosticsHint: "Create a report without credentials, account identifiers, or authorization timestamps.",
449
591
  diagnosticsLoad: "Create report",
592
+ diagnosticsLoading: "Creating…",
450
593
  diagnosticsCopy: "Copy report",
451
594
  diagnosticsCopied: "Copied",
452
595
  diagnosticsFailed: "Could not create diagnostics.",
453
596
  feedbackOpen: "Report a problem",
597
+ showEmail: "Show full email",
598
+ hideEmail: "Hide email",
599
+ emailUnavailable: "Email unavailable",
454
600
  searchTitle: "Search source",
455
601
  searchScope: "Auto follows the current session model; an explicit choice overrides every model and session.",
456
602
  searchAuto: "Auto",
@@ -482,21 +628,21 @@ window.__ModuleLoader__.load({
482
628
  creditsUnit: "credits",
483
629
  unlimited: "Unlimited",
484
630
  monthlyCreditLimit: "Monthly Credits spending cap",
485
- resetCredits: "Available quota resets",
486
- resetCreditsValue: "{count} available",
487
- resetUse: "Use reset",
488
- resetPreparing: "Reading reset details…",
631
+ resetCredits: "Quota resets",
632
+ resetCreditDefaultName: "Quota reset",
633
+ resetUse: "Use",
634
+ resetPreparing: "Preparing…",
489
635
  resetConfirmTitle: "Confirm quota reset",
490
- resetWarning: "If ChatGPT performs the reset, one reset is consumed immediately and cannot be restored.",
491
- resetEarlyWarning: "No model quota is exhausted. ChatGPT may reset it, or report that nothing needs resetting without consuming a reset.",
636
+ resetWarning: "This consumes one reset and cannot be undone.",
637
+ resetEarlyWarning: "Quota remains. The service may decline the reset.",
492
638
  resetAcknowledge: "I understand this may consume one reset now",
493
639
  resetCreditExpires: "Expires {value}",
494
640
  resetCreditExpiryUnknown: "Expiration time not provided",
495
641
  resetCreditExpiryLoading: "Reading expiration…",
496
642
  resetCreditExpiryFailed: "Could not read expiration",
497
- resetWait: "Wait {count} more seconds",
498
- resetFinal: "Consume one reset",
499
- resetUsing: "Resetting…",
643
+ resetWait: "Wait {count} seconds",
644
+ resetFinal: "Confirm use",
645
+ resetUsing: "Using…",
500
646
  resetSuccess: "Quota reset completed.",
501
647
  resetNothing: "There is currently nothing to reset; no new reset was consumed.",
502
648
  resetNoCredit: "No quota reset is available.",
@@ -509,7 +655,7 @@ window.__ModuleLoader__.load({
509
655
  resetAcknowledgeRequired: "Confirm that you understand this may consume a reset.",
510
656
  resetAccountChanged: "The signed-in account changed. Start again.",
511
657
  resetUncertain: "The server result is uncertain. Confirm again to check the same request; the plugin will not start a separate reset.",
512
- creditsNote: "Shows only extra Credits, spending caps, or quota resets returned for this account or workspace; these are separate items.",
658
+ creditsNote: "Extra Credits, spending caps, and resets are separate items.",
513
659
  creditsUsed: "{used} / {limit} credits used",
514
660
  spendReached: "The monthly Credits spending cap has been reached.",
515
661
  unavailable: "No data yet",
@@ -615,14 +761,15 @@ window.__ModuleLoader__.load({
615
761
  .codexSubscriptionContext{display:flex;flex-direction:column;gap:8px}.codexSubscriptionContextHead{display:flex;align-items:flex-start;justify-content:space-between;gap:12px}.codexSubscriptionContextCopy{display:flex;min-width:0;flex:1;flex-direction:column;gap:2px}.codexSubscriptionContextHint{font-size:11px;line-height:17px;color:var(--dsw-alias-label-tertiary)}.codexSubscriptionContextTrigger{height:32px;min-width:108px;display:inline-flex;align-items:center;justify-content:space-between;gap:10px;padding:0 10px 0 12px;border:0;border-radius:999px;outline:0;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;cursor:pointer}.codexSubscriptionContextTrigger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.codexSubscriptionContextTrigger:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-border-l3)}.codexSubscriptionContextTrigger:disabled{color:var(--dsw-alias-label-dimmed);cursor:not-allowed}.codexSubscriptionContextTrigger svg{color:var(--dsw-alias-label-tertiary);transition:transform 120ms var(--ds-ease-in-out)}.codexSubscriptionContextTrigger[aria-expanded=true] svg{transform:rotate(180deg)}.codexSubscriptionContextModels{display:flex;flex-direction:column;border-top:1px solid var(--dsw-alias-border-l2)}.codexSubscriptionContextModel{min-height:42px;display:flex;align-items:center;justify-content:space-between;gap:12px;border-bottom:1px solid var(--dsw-alias-border-l2)}.codexSubscriptionContextModel:last-child{border-bottom:0}.codexSubscriptionContextModelCopy{display:flex;min-width:0;flex-direction:column}.codexSubscriptionContextModelCopy strong{font-size:12px;line-height:18px;font-weight:500}.codexSubscriptionContextModelCopy span{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary)}.codexSubscriptionContextInput{width:116px}
616
762
  .codexSubscriptionSwitch{position:relative;flex:0 0 auto;width:32px;height:18px;padding:0;border:1px solid var(--dsw-alias-border-l3);border-radius:999px;background:var(--dsw-alias-bg-module-platform);cursor:pointer}.codexSubscriptionSwitch:disabled{cursor:not-allowed;opacity:.5}.codexSubscriptionSwitch[aria-checked=true]{background:var(--dsw-alias-label-secondary);border-color:var(--dsw-alias-label-secondary)}.codexSubscriptionSwitchKnob{position:absolute;top:2px;left:2px;width:12px;height:12px;border-radius:50%;background:var(--dsw-alias-bg-layer-1);transition:transform 120ms var(--ds-ease-in-out)}.codexSubscriptionSwitch[aria-checked=true] .codexSubscriptionSwitchKnob{transform:translateX(14px)}
617
763
  .codexSubscriptionSearch{display:flex;flex-direction:column;gap:7px}.codexSubscriptionSearchChoices{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:6px}.codexSubscriptionSearchChoice{display:grid;grid-template-columns:14px minmax(0,1fr);align-items:center;column-gap:8px;min-width:0;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-primary);padding:9px 10px;text-align:left;cursor:pointer}.codexSubscriptionSearchChoice:has(input:disabled){cursor:not-allowed;opacity:.5}.codexSubscriptionSearchChoice:has(input:checked){border-color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2)}.codexSubscriptionSearchChoice:has(input:focus-visible){outline:2px solid var(--dsw-alias-border-l3);outline-offset:2px}.codexSubscriptionSearchInput{width:14px;height:14px;margin:0;accent-color:var(--dsw-alias-label-primary);cursor:inherit}.codexSubscriptionSearchCopy{display:block;min-width:0;pointer-events:none}.codexSubscriptionSearchCopy strong,.codexSubscriptionSearchCopy span{display:block}.codexSubscriptionSearchCopy strong{font-size:12px;line-height:18px;font-weight:500;color:var(--dsw-alias-label-secondary)}.codexSubscriptionSearchChoice:has(input:checked) strong{color:var(--dsw-alias-label-primary)}.codexSubscriptionSearchCopy span{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary)}.codexSubscriptionDivider{height:1px;background:var(--dsw-alias-border-l2)}
764
+ .codexSubscriptionQuotaModes[data-saving=true] .codexSubscriptionQuotaMode:has(input:disabled){cursor:wait;opacity:1}.codexSubscriptionSearchChoices[data-saving=true] .codexSubscriptionSearchChoice:has(input:disabled){cursor:wait;opacity:1}
618
765
  .codexSubscriptionAccountRow,.codexSubscriptionSectionHead{display:flex;align-items:center;justify-content:space-between;gap:12px}.codexSubscriptionStatus{display:flex;align-items:center;gap:8px;font-size:14px;line-height:22px;font-weight:500}
619
- .codexSubscriptionAccounts{display:flex;flex-direction:column;border-top:1px solid var(--dsw-alias-border-l2)}.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}.codexSubscriptionAccount:last-child{border-bottom:0}.codexSubscriptionAccount[data-active=true]>span{font-weight:600}.codexSubscriptionFlow label{display:flex;flex-direction:column;gap:6px;font-size:12px;color:var(--dsw-alias-label-secondary)}
766
+ .codexSubscriptionAccounts{display:flex;flex-direction:column;border-top:1px solid var(--dsw-alias-border-l2)}.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}.codexSubscriptionAccount:last-child{border-bottom:0}.codexSubscriptionAccount[data-active=true] .codexSubscriptionEmail,.codexSubscriptionAccount[data-active=true]>span{font-weight:600}.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}.codexSubscriptionEmail:hover{background:var(--dsw-alias-interactive-bg-hover)}.codexSubscriptionEmail:focus-visible{outline:2px solid var(--dsw-alias-border-l3);outline-offset:1px}.codexSubscriptionFlow label{display:flex;flex-direction:column;gap:6px;font-size:12px;color:var(--dsw-alias-label-secondary)}
620
767
  .codexSubscriptionDot{width:8px;height:8px;border-radius:50%;background:var(--dsw-alias-label-dimmed)}.codexSubscriptionDot[data-state=connected]{background:var(--dsw-alias-state-success-primary)}.codexSubscriptionDot[data-state=disconnected]{background:var(--dsw-alias-state-error-primary)}
621
768
  .codexSubscriptionActions{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.codexSubscriptionFlow{display:flex;flex-direction:column;gap:10px;padding:12px 14px;border-radius:10px;background:var(--dsw-alias-bg-module-platform)}
622
769
  .codexSubscriptionFlow p{font-size:13px;line-height:20px;color:var(--dsw-alias-label-secondary)}.codexSubscriptionCode{width:max-content;max-width:100%;font:600 16px/22px ui-monospace,SFMono-Regular,Consolas,monospace;letter-spacing:.08em;overflow-wrap:anywhere}
623
770
  .codexSubscriptionError{font-size:13px;line-height:20px;color:var(--dsw-alias-state-error-primary)}.codexSubscriptionInput{width:100%;box-sizing:border-box}
624
771
  .codexSubscriptionRecover{display:flex;align-items:center;justify-content:space-between;gap:12px}.codexSubscriptionRecover .codexSubscriptionError{flex:1}.codexSubscriptionRecover button{flex:0 0 auto}
625
- .codexSubscriptionDiagnostics pre{max-height:240px;margin:0;padding:10px 12px;border-radius:8px;background:var(--dsw-alias-bg-module-platform);overflow:auto;white-space:pre-wrap;overflow-wrap:anywhere;font:11px/17px ui-monospace,SFMono-Regular,Consolas,monospace;color:var(--dsw-alias-label-secondary)}.codexSubscriptionLink{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;min-height:32px;padding:0 13px;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;background:transparent;color:var(--dsw-alias-label-primary);font-size:13px;line-height:20px;text-decoration:none;white-space:nowrap}.codexSubscriptionLink:hover{background:var(--dsw-alias-bg-module-platform)}.codexSubscriptionLink:focus-visible{outline:2px solid var(--dsw-alias-border-l3);outline-offset:2px}
772
+ .codexSubscriptionDiagnostics{padding:8px 12px;gap:8px;background:transparent;color:var(--dsw-alias-label-secondary)}.codexSubscriptionDiagnostics pre{max-height:240px;margin:0;padding:10px 12px;border-radius:8px;background:var(--dsw-alias-bg-module-platform);overflow:auto;white-space:pre-wrap;overflow-wrap:anywhere;font:11px/17px ui-monospace,SFMono-Regular,Consolas,monospace;color:var(--dsw-alias-label-secondary)}.codexSubscriptionLink{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;min-height:32px;padding:0 13px;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;background:transparent;color:var(--dsw-alias-label-primary);font-size:13px;line-height:20px;text-decoration:none;white-space:nowrap}.codexSubscriptionLink:hover{background:var(--dsw-alias-bg-module-platform)}.codexSubscriptionLink:focus-visible{outline:2px solid var(--dsw-alias-border-l3);outline-offset:2px}
626
773
  .codexSubscriptionSectionTitle{display:flex;flex:1;min-width:0;flex-direction:column;gap:2px}.codexSubscriptionFreshness{font-size:11px;line-height:17px;color:var(--dsw-alias-label-tertiary)}
627
774
  .codexSubscriptionRefresh{flex:0 0 auto;min-width:72px;width:max-content;white-space:nowrap!important;word-break:keep-all!important;overflow-wrap:normal!important;writing-mode:horizontal-tb!important}.codexSubscriptionRefresh *{white-space:nowrap!important;word-break:keep-all!important;writing-mode:horizontal-tb!important}
628
775
  .codexSubscriptionEmpty{padding:18px;border:1px dashed var(--dsw-alias-border-l3);border-radius:10px;text-align:center;font-size:13px;line-height:20px;color:var(--dsw-alias-label-tertiary)}
@@ -632,7 +779,7 @@ window.__ModuleLoader__.load({
632
779
  .codexSubscriptionLimit progress::-webkit-progress-bar{background:var(--dsw-alias-border-l3);border-radius:999px}.codexSubscriptionLimit progress::-webkit-progress-value{background:var(--dsw-alias-brand-primary,#3964fe);border-radius:999px}.codexSubscriptionLimit progress::-moz-progress-bar{background:var(--dsw-alias-brand-primary,#3964fe);border-radius:999px}.codexSubscriptionLimitMeta{display:flex;justify-content:space-between;gap:10px;flex-wrap:wrap;font-size:11px;line-height:17px;color:var(--dsw-alias-label-tertiary)}
633
780
  .codexSubscriptionCreditSection{display:flex;flex-direction:column;gap:7px}.codexSubscriptionCreditNote{font-size:11px;line-height:17px;color:var(--dsw-alias-label-tertiary)}.codexSubscriptionCreditRows{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px}.codexSubscriptionCreditBalance,.codexSubscriptionSpendLimit{min-width:0;border-radius:10px;padding:12px 14px;background:var(--dsw-alias-bg-module-platform)}
634
781
  .codexSubscriptionCreditBalance{display:flex;flex-direction:column;gap:6px}.codexSubscriptionCreditBalance span,.codexSubscriptionCreditLabel{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}.codexSubscriptionCreditBalance strong{font:600 18px/24px ui-monospace,SFMono-Regular,Consolas,monospace;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}
635
- .codexSubscriptionResetSummary{display:flex;align-items:center;justify-content:space-between;gap:10px}.codexSubscriptionResetMeta{display:flex;min-width:0;flex-direction:column;gap:1px}.codexSubscriptionResetBalance{display:flex;flex-direction:column;gap:8px}.codexSubscriptionResetBalance .codexSubscriptionActions{justify-content:flex-start}.codexSubscriptionResetFlow{display:flex;flex-direction:column;gap:10px;border-top:1px solid var(--dsw-alias-border-l2);padding-top:10px}.codexSubscriptionResetFlow h4{margin:0;font-size:13px;line-height:20px;font-weight:500}.codexSubscriptionResetWarning{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}.codexSubscriptionResetExpiry{font-size:11px;line-height:17px;color:var(--dsw-alias-label-tertiary)}.codexSubscriptionResetCheck{display:flex;align-items:flex-start;gap:8px;padding:9px 10px;border-radius:8px;background:var(--dsw-alias-bg-module-platform);font-size:12px;line-height:18px;color:var(--dsw-alias-label-primary);cursor:pointer}.codexSubscriptionResetCheck input{margin:3px 0 0;accent-color:var(--dsw-alias-label-primary)}.codexSubscriptionResetFinal{border-color:var(--dsw-alias-state-error-primary)!important;color:var(--dsw-alias-state-error-primary)!important}.codexSubscriptionResetResult{font-size:12px;line-height:18px;color:var(--dsw-alias-state-success-primary)}
782
+ .codexSubscriptionCreditRows{display:flex;flex-direction:column;gap:6px}.codexSubscriptionResetMeta{display:flex;min-width:0;flex-direction:column;gap:1px}.codexSubscriptionResetBalance{display:flex;flex-direction:column;gap:8px}.codexSubscriptionResetCard{display:flex;align-items:center;justify-content:space-between;gap:10px;min-width:0;padding:9px 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:9px;background:var(--dsw-alias-bg-module-platform)}.codexSubscriptionResetCard .codexSubscriptionResetMeta{flex:1}.codexSubscriptionResetCard strong{overflow:hidden;font-size:12px;line-height:18px;font-weight:500;text-overflow:ellipsis;white-space:nowrap}.codexSubscriptionResetCard .codexSubscriptionActions{flex:0 0 auto}.codexSubscriptionResetCard .codexSubscriptionResetUse{min-height:28px;padding:0 10px}.codexSubscriptionResetBalance .codexSubscriptionActions{justify-content:flex-start}.codexSubscriptionResetFlow{display:flex;flex-direction:column;gap:10px;border-top:1px solid var(--dsw-alias-border-l2);padding-top:10px}.codexSubscriptionResetFlow h4{margin:0;font-size:13px;line-height:20px;font-weight:500}.codexSubscriptionResetWarning{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}.codexSubscriptionResetExpiry{font-size:11px;line-height:17px;color:var(--dsw-alias-label-tertiary)}.codexSubscriptionResetCheck{display:flex;align-items:flex-start;gap:8px;padding:9px 10px;border-radius:8px;background:var(--dsw-alias-bg-module-platform);font-size:12px;line-height:18px;color:var(--dsw-alias-label-primary);cursor:pointer}.codexSubscriptionResetCheck input{margin:3px 0 0;accent-color:var(--dsw-alias-label-primary)}.codexSubscriptionResetFinal{border-color:var(--dsw-alias-state-error-primary)!important;color:var(--dsw-alias-state-error-primary)!important}.codexSubscriptionResetResult{font-size:12px;line-height:18px;color:var(--dsw-alias-state-success-primary)}
636
783
  .codexSubscriptionResetUse:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.codexSubscriptionResetUse:focus-visible{outline:2px solid var(--dsw-alias-border-l3);outline-offset:1px}
637
784
  .codexSubscriptionSpendLimit{display:flex;flex-direction:column;gap:8px}.codexSubscriptionSpendTop{display:flex;align-items:baseline;justify-content:space-between;gap:12px}.codexSubscriptionSpendTop strong{font:600 16px/22px ui-monospace,SFMono-Regular,Consolas,monospace;font-variant-numeric:tabular-nums}.codexSubscriptionSpendLimit progress{width:100%;height:6px;border:0;border-radius:999px;overflow:hidden;background:var(--dsw-alias-border-l3);accent-color:var(--dsw-alias-brand-primary,#3964fe);-webkit-appearance:none;appearance:none}.codexSubscriptionSpendLimit progress::-webkit-progress-bar{background:var(--dsw-alias-border-l3);border-radius:999px}.codexSubscriptionSpendLimit progress::-webkit-progress-value{background:var(--dsw-alias-brand-primary,#3964fe);border-radius:999px}.codexSubscriptionSpendLimit progress::-moz-progress-bar{background:var(--dsw-alias-brand-primary,#3964fe);border-radius:999px}
638
785
  .codexComposerQuota{display:inline-flex;align-items:center;flex:0 0 auto;height:28px;box-sizing:border-box;padding:0;color:var(--dsw-alias-label-secondary);font-family:inherit;font-size:12px;line-height:20px;font-weight:500;font-variant-numeric:tabular-nums;white-space:nowrap;user-select:none}.codexComposerQuotaBar{display:block;width:40px;height:4px;border:0;border-radius:999px;overflow:hidden;background:var(--dsw-alias-border-l3);accent-color:var(--dsw-alias-label-secondary);-webkit-appearance:none;appearance:none}.codexComposerQuotaBar::-webkit-progress-bar{background:var(--dsw-alias-border-l3);border-radius:999px}.codexComposerQuotaBar::-webkit-progress-value{background:var(--dsw-alias-label-secondary);border-radius:999px}.codexComposerQuotaBar::-moz-progress-bar{background:var(--dsw-alias-label-secondary);border-radius:999px}
@@ -664,6 +811,12 @@ window.__ModuleLoader__.load({
664
811
  return response.value;
665
812
  };
666
813
  const fill = (text, values) => Object.entries(values).reduce((next, [key, value]) => next.replace(`{${key}}`, String(value)), text);
814
+ const maskEmail = (value) => {
815
+ if (typeof value !== "string" || !value.includes("@")) return "••••";
816
+ const [local, domain] = value.split("@", 2);
817
+ if (local.length <= 2) return `${local.slice(0, 1)}••@${domain}`;
818
+ return `${local[0]}•••${local.at(-1)}@${domain}`;
819
+ };
667
820
  const hours = (seconds) => Math.round(seconds / 3600 * 10) / 10;
668
821
  const percent = (value) => Number(value).toLocaleString(void 0, { maximumFractionDigits: 1 });
669
822
  const isApproximateWindow = (seconds, expected) => seconds >= expected * .95 && seconds <= expected * 1.05;
@@ -1108,131 +1261,6 @@ window.__ModuleLoader__.load({
1108
1261
  ]
1109
1262
  });
1110
1263
  }
1111
- function createPreferenceController(scope, rpc) {
1112
- let updating = false;
1113
- let error = false;
1114
- let fallbackStatus = "loading";
1115
- let fallback;
1116
- let failedPatch;
1117
- let generation = 0;
1118
- let contextModels = [];
1119
- let verbosityModels = [];
1120
- const nativeSnapshot = () => scope.getSnapshot();
1121
- const read = () => {
1122
- const native = nativeSnapshot();
1123
- const current = native.status === "ready" ? native : fallbackStatus === "ready" ? fallback : native;
1124
- return Object.freeze({
1125
- status: updating ? "updating" : current.status,
1126
- quickQuotaMode: normalizeQuickQuotaMode(current.value?.[QUICK_QUOTA_MODE_FIELD], current.value?.[LEGACY_QUICK_QUOTA_FIELD]),
1127
- searchProvider: normalizeSearchProvider(current.value?.[SEARCH_PROVIDER_FIELD]),
1128
- speedMode: normalizeSpeedMode(current.value?.[SPEED_MODE_FIELD]),
1129
- outputVerbosity: normalizeOutputVerbosity(current.value?.[OUTPUT_VERBOSITY_FIELD]),
1130
- contextMode: normalizeContextMode(current.value?.[CONTEXT_MODE_FIELD]),
1131
- customContextWindow: normalizeCustomContextWindow(current.value?.[CUSTOM_CONTEXT_WINDOW_FIELD]),
1132
- customContextWindows: Object.fromEntries(Object.entries(CUSTOM_CONTEXT_MODEL_FIELDS).map(([modelKey, field]) => [modelKey, normalizeCustomContextWindow(current.value?.[field] ?? CUSTOM_CONTEXT_MODEL_DEFAULTS[modelKey], CUSTOM_CONTEXT_MODEL_CAPS[modelKey])])),
1133
- contextModels,
1134
- verbosityModels,
1135
- writable: !updating && current.status === "ready" && current.writable === true,
1136
- error
1137
- });
1138
- };
1139
- let snapshot = read();
1140
- const listeners = /* @__PURE__ */ new Set();
1141
- const publish = () => {
1142
- snapshot = read();
1143
- for (const listener of listeners) listener();
1144
- };
1145
- const disposeScope = scope.subscribe(() => {
1146
- error = false;
1147
- failedPatch = void 0;
1148
- publish();
1149
- });
1150
- const acceptFallback = (value) => {
1151
- contextModels = Array.isArray(value?.contextModels) ? value.contextModels : [];
1152
- verbosityModels = Array.isArray(value?.verbosityModels) ? value.verbosityModels : [];
1153
- fallbackStatus = "ready";
1154
- fallback = {
1155
- status: "ready",
1156
- value: {
1157
- [QUICK_QUOTA_MODE_FIELD]: normalizeQuickQuotaMode(value?.[QUICK_QUOTA_MODE_FIELD], value?.[LEGACY_QUICK_QUOTA_FIELD]),
1158
- [SEARCH_PROVIDER_FIELD]: normalizeSearchProvider(value?.[SEARCH_PROVIDER_FIELD]),
1159
- [SPEED_MODE_FIELD]: normalizeSpeedMode(value?.[SPEED_MODE_FIELD]),
1160
- [OUTPUT_VERBOSITY_FIELD]: normalizeOutputVerbosity(value?.[OUTPUT_VERBOSITY_FIELD]),
1161
- [CONTEXT_MODE_FIELD]: normalizeContextMode(value?.[CONTEXT_MODE_FIELD]),
1162
- [CUSTOM_CONTEXT_WINDOW_FIELD]: normalizeCustomContextWindow(value?.[CUSTOM_CONTEXT_WINDOW_FIELD]),
1163
- ...Object.fromEntries(Object.entries(CUSTOM_CONTEXT_MODEL_FIELDS).map(([modelKey, field]) => [field, normalizeCustomContextWindow(value?.[field] ?? CUSTOM_CONTEXT_MODEL_DEFAULTS[modelKey], CUSTOM_CONTEXT_MODEL_CAPS[modelKey])]))
1164
- },
1165
- writable: value?.writable === true
1166
- };
1167
- };
1168
- const load = async () => {
1169
- const current = ++generation;
1170
- updating = false;
1171
- fallbackStatus = "loading";
1172
- fallback = void 0;
1173
- error = false;
1174
- publish();
1175
- nativeSnapshot();
1176
- try {
1177
- const value = unwrap(await rpc.call(CHANNEL, "preferences/status", {}));
1178
- if (current !== generation) return;
1179
- if (nativeSnapshot().status === "ready") {
1180
- contextModels = Array.isArray(value?.contextModels) ? value.contextModels : [];
1181
- verbosityModels = Array.isArray(value?.verbosityModels) ? value.verbosityModels : [];
1182
- } else acceptFallback(value);
1183
- publish();
1184
- } catch {
1185
- if (current !== generation || nativeSnapshot().status === "ready") return;
1186
- fallbackStatus = "unavailable";
1187
- publish();
1188
- }
1189
- };
1190
- const set = async (patch) => {
1191
- if (snapshot.status !== "ready" || snapshot.writable !== true) return;
1192
- const current = ++generation;
1193
- const entries = Object.entries(patch);
1194
- updating = true;
1195
- error = false;
1196
- failedPatch = void 0;
1197
- publish();
1198
- try {
1199
- if (nativeSnapshot().status === "ready") {
1200
- for (const [field, value] of entries) {
1201
- if (current !== generation) return;
1202
- await scope.set(field, value);
1203
- }
1204
- if (current !== generation) return;
1205
- const accepted = nativeSnapshot().value;
1206
- error = entries.some(([field, value]) => accepted?.[field] !== value);
1207
- } else {
1208
- const value = unwrap(await rpc.call(CHANNEL, "preferences/update", patch));
1209
- if (current !== generation) return;
1210
- acceptFallback(value);
1211
- }
1212
- } catch {
1213
- if (current === generation) {
1214
- error = true;
1215
- failedPatch = patch;
1216
- }
1217
- } finally {
1218
- if (current === generation) {
1219
- updating = false;
1220
- publish();
1221
- }
1222
- }
1223
- };
1224
- return {
1225
- getSnapshot: () => snapshot,
1226
- subscribe: (listener) => {
1227
- listeners.add(listener);
1228
- return () => listeners.delete(listener);
1229
- },
1230
- load,
1231
- set,
1232
- retry: () => failedPatch === void 0 ? load() : set(failedPatch),
1233
- dispose: disposeScope
1234
- };
1235
- }
1236
1264
  const usePreferenceSnapshot = (preference) => (0, react.useSyncExternalStore)(preference.subscribe, preference.getSnapshot);
1237
1265
  const notifyQuickQuota = () => window.dispatchEvent(new Event(QUICK_QUOTA_REFRESH_EVENT));
1238
1266
  const formatRunway = (seconds, t) => {
@@ -1331,6 +1359,8 @@ window.__ModuleLoader__.load({
1331
1359
  }) : null]
1332
1360
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1333
1361
  className: "codexSubscriptionQuotaModes",
1362
+ "data-saving": snapshot.saving || void 0,
1363
+ "aria-busy": snapshot.saving || void 0,
1334
1364
  role: "radiogroup",
1335
1365
  "aria-label": t("quickQuotaSetting"),
1336
1366
  children: [
@@ -1375,6 +1405,8 @@ window.__ModuleLoader__.load({
1375
1405
  })]
1376
1406
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1377
1407
  className: "codexSubscriptionSearchChoices",
1408
+ "data-saving": snapshot.saving || void 0,
1409
+ "aria-busy": snapshot.saving || void 0,
1378
1410
  role: "radiogroup",
1379
1411
  "aria-label": t("searchTitle"),
1380
1412
  children: [
@@ -1885,15 +1917,37 @@ window.__ModuleLoader__.load({
1885
1917
  }) : null]
1886
1918
  });
1887
1919
  }
1920
+ function AccountEmail({ candidate, fallback, t, emailVisible, onClick }) {
1921
+ if (typeof candidate?.email !== "string" || candidate.email.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1922
+ title: t("emailUnavailable"),
1923
+ children: fallback ?? candidate?.label ?? t("emailUnavailable")
1924
+ });
1925
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1926
+ type: "button",
1927
+ className: "codexSubscriptionEmail",
1928
+ "aria-label": t(emailVisible ? "hideEmail" : "showEmail"),
1929
+ "aria-pressed": emailVisible,
1930
+ onClick,
1931
+ children: emailVisible ? candidate.email : maskEmail(candidate.email)
1932
+ });
1933
+ }
1888
1934
  function AccountCard({ rpc, t, account, setAccount, onSignedOut }) {
1889
1935
  const [flow, setFlow] = (0, react.useState)();
1890
1936
  const [manualCode, setManualCode] = (0, react.useState)("");
1891
1937
  const [adding, setAdding] = (0, react.useState)(false);
1892
- const [accountLabel, setAccountLabel] = (0, react.useState)("");
1893
1938
  const [removeId, setRemoveId] = (0, react.useState)();
1939
+ const [emailVisible, setEmailVisible] = (0, react.useState)(false);
1940
+ const accounts = account?.accounts ?? [];
1941
+ const accountVisibilityKey = `${account?.authenticated === true ? "signed-in" : "signed-out"}:${accounts.map((candidate) => `${candidate.id ?? ""}:${candidate.active === true}:${candidate.email ?? ""}`).join("|")}`;
1942
+ const [emailVisibilityKey, setEmailVisibilityKey] = (0, react.useState)(accountVisibilityKey);
1894
1943
  const [busy, setBusy] = (0, react.useState)(false);
1895
1944
  const [error, setError] = (0, react.useState)();
1896
1945
  const call = (endpoint, payload = {}) => rpc.call(CHANNEL, endpoint, payload).then(unwrap);
1946
+ (0, react.useEffect)(() => {
1947
+ if (emailVisibilityKey === accountVisibilityKey) return;
1948
+ setEmailVisible(false);
1949
+ setEmailVisibilityKey(accountVisibilityKey);
1950
+ }, [accountVisibilityKey, emailVisibilityKey]);
1897
1951
  (0, react.useEffect)(() => {
1898
1952
  if (flow?.id === void 0 || [
1899
1953
  "authenticated",
@@ -1915,7 +1969,6 @@ window.__ModuleLoader__.load({
1915
1969
  setAccount(next.account);
1916
1970
  onSignedOut();
1917
1971
  setAdding(false);
1918
- setAccountLabel("");
1919
1972
  setFlow(void 0);
1920
1973
  notifyQuickQuota();
1921
1974
  }
@@ -1931,10 +1984,11 @@ window.__ModuleLoader__.load({
1931
1984
  setFlow(void 0);
1932
1985
  setBusy(true);
1933
1986
  setError(void 0);
1987
+ const loginLabel = adding && label === void 0 ? `Account ${accounts.length + 1}` : label;
1934
1988
  call("login/start", {
1935
1989
  method,
1936
1990
  openExternal: true,
1937
- ...label === void 0 ? {} : { label }
1991
+ ...loginLabel === void 0 ? {} : { label: loginLabel }
1938
1992
  }).then(setFlow).catch(() => setError(t("failed"))).finally(() => setBusy(false));
1939
1993
  };
1940
1994
  const cancel = () => {
@@ -1942,10 +1996,7 @@ window.__ModuleLoader__.load({
1942
1996
  setBusy(true);
1943
1997
  call("login/cancel", { id: flow.id }).then((next) => {
1944
1998
  setFlow(adding ? void 0 : next);
1945
- if (adding) {
1946
- setAdding(false);
1947
- setAccountLabel("");
1948
- }
1999
+ if (adding) setAdding(false);
1949
2000
  if (adding) return void 0;
1950
2001
  return call("status").then((account) => {
1951
2002
  if (account.authenticated === true) {
@@ -2008,13 +2059,16 @@ window.__ModuleLoader__.load({
2008
2059
  };
2009
2060
  const signedIn = account?.authenticated === true;
2010
2061
  const accountReady = account !== void 0;
2011
- const accounts = account?.accounts ?? [];
2012
- const activeAccount = accounts.find((candidate) => candidate.active);
2013
2062
  const loginVisible = flow !== void 0 && ![
2014
2063
  "authenticated",
2015
2064
  "failed",
2016
2065
  "cancelled"
2017
2066
  ].includes(flow.phase);
2067
+ const toggleEmail = () => {
2068
+ setEmailVisibilityKey(accountVisibilityKey);
2069
+ setEmailVisible((value) => emailVisibilityKey === accountVisibilityKey ? !value : true);
2070
+ };
2071
+ const emailVisibleForAccount = emailVisible && emailVisibilityKey === accountVisibilityKey;
2018
2072
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2019
2073
  className: "codexSubscriptionCard",
2020
2074
  children: [
@@ -2028,7 +2082,7 @@ window.__ModuleLoader__.load({
2028
2082
  className: "codexSubscriptionDot",
2029
2083
  "data-state": accountReady ? signedIn ? "connected" : "disconnected" : "loading",
2030
2084
  "aria-hidden": "true"
2031
- }), accountReady ? signedIn ? activeAccount?.label ?? t("connected") : t("disconnected") : t("accountLoading")]
2085
+ }), accountReady ? signedIn ? t("connected") : t("disconnected") : t("accountLoading")]
2032
2086
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2033
2087
  className: "codexSubscriptionActions",
2034
2088
  children: signedIn ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
@@ -2038,7 +2092,6 @@ window.__ModuleLoader__.load({
2038
2092
  onClick: () => {
2039
2093
  setFlow(void 0);
2040
2094
  setAdding(true);
2041
- setAccountLabel(`Account ${accounts.length + 1}`);
2042
2095
  },
2043
2096
  children: t("addAccount")
2044
2097
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
@@ -2067,7 +2120,13 @@ window.__ModuleLoader__.load({
2067
2120
  children: accounts.map((candidate) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2068
2121
  className: "codexSubscriptionAccount",
2069
2122
  "data-active": candidate.active,
2070
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: candidate.label }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2123
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountEmail, {
2124
+ candidate,
2125
+ fallback: candidate.label,
2126
+ t,
2127
+ emailVisible: emailVisibleForAccount,
2128
+ onClick: toggleEmail
2129
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2071
2130
  className: "codexSubscriptionActions",
2072
2131
  children: [
2073
2132
  candidate.active ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
@@ -2095,42 +2154,34 @@ window.__ModuleLoader__.load({
2095
2154
  })]
2096
2155
  }, candidate.id))
2097
2156
  }) : null,
2098
- signedIn && adding && flow === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2157
+ signedIn && adding && flow === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2099
2158
  className: "codexSubscriptionFlow",
2100
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [t("accountLabel"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
2101
- className: "codexSubscriptionInput",
2102
- value: accountLabel,
2103
- maxLength: 48,
2104
- onChange: (event) => setAccountLabel(event.currentTarget.value)
2105
- })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2159
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2106
2160
  className: "codexSubscriptionActions",
2107
2161
  children: [
2108
2162
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2109
2163
  type: "button",
2110
2164
  variant: "primary",
2111
- disabled: busy || accountLabel.trim() === "",
2112
- onClick: () => begin("browser", accountLabel.trim()),
2165
+ disabled: busy,
2166
+ onClick: () => begin("browser"),
2113
2167
  children: t("browserLogin")
2114
2168
  }),
2115
2169
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2116
2170
  type: "button",
2117
2171
  variant: "outline",
2118
- disabled: busy || accountLabel.trim() === "",
2119
- onClick: () => begin("device_code", accountLabel.trim()),
2172
+ disabled: busy,
2173
+ onClick: () => begin("device_code"),
2120
2174
  children: t("deviceLogin")
2121
2175
  }),
2122
2176
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2123
2177
  type: "button",
2124
2178
  variant: "outline",
2125
2179
  disabled: busy,
2126
- onClick: () => {
2127
- setAdding(false);
2128
- setAccountLabel("");
2129
- },
2180
+ onClick: () => setAdding(false),
2130
2181
  children: t("cancel")
2131
2182
  })
2132
2183
  ]
2133
- })]
2184
+ })
2134
2185
  }) : null,
2135
2186
  flow?.phase === "waiting_device" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2136
2187
  className: "codexSubscriptionFlow",
@@ -2240,12 +2291,9 @@ window.__ModuleLoader__.load({
2240
2291
  children: [
2241
2292
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2242
2293
  className: "codexSubscriptionSectionHead",
2243
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2294
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2244
2295
  className: "codexSubscriptionSectionTitle",
2245
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t("diagnostics") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2246
- className: "codexSubscriptionNote",
2247
- children: t("diagnosticsHint")
2248
- })]
2296
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t("diagnostics") })
2249
2297
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2250
2298
  className: "codexSubscriptionActions",
2251
2299
  children: [
@@ -2254,7 +2302,7 @@ window.__ModuleLoader__.load({
2254
2302
  variant: "outline",
2255
2303
  disabled: busy,
2256
2304
  onClick: load,
2257
- children: t("diagnosticsLoad")
2305
+ children: busy ? t("diagnosticsLoading") : t("diagnosticsLoad")
2258
2306
  }),
2259
2307
  report === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2260
2308
  type: "button",
@@ -2307,29 +2355,19 @@ window.__ModuleLoader__.load({
2307
2355
  })
2308
2356
  });
2309
2357
  }
2310
- function ResetCreditControl({ rpc, t, count, nextExpiresAt, hasExhaustedQuota, onConsumed }) {
2311
- const [challenge, setChallenge] = (0, react.useState)();
2312
- const [resetBusy, setResetBusy] = (0, react.useState)(false);
2313
- const [resetAcknowledged, setResetAcknowledged] = (0, react.useState)(false);
2314
- const [resetCountdown, setResetCountdown] = (0, react.useState)(0);
2315
- const [resetError, setResetError] = (0, react.useState)();
2316
- const [resetResult, setResetResult] = (0, react.useState)();
2317
- const [inspectedExpiry, setInspectedExpiry] = (0, react.useState)(nextExpiresAt);
2318
- const [expiryState, setExpiryState] = (0, react.useState)(nextExpiresAt === void 0 ? "loading" : "ready");
2358
+ function ResetCreditList({ rpc, t, count, nextExpiresAt, initialCredits, refreshKey, hasExhaustedQuota, onConsumed }) {
2359
+ const [credits, setCredits] = (0, react.useState)(initialCredits ?? (nextExpiresAt === void 0 ? [] : [{ expiresAt: nextExpiresAt }]));
2360
+ const [state, setState] = (0, react.useState)("loading");
2319
2361
  (0, react.useEffect)(() => {
2320
- if (nextExpiresAt !== void 0) {
2321
- setInspectedExpiry(nextExpiresAt);
2322
- setExpiryState("ready");
2323
- return;
2324
- }
2325
2362
  let live = true;
2326
- setExpiryState("loading");
2363
+ setState("loading");
2364
+ setCredits([]);
2327
2365
  rpc.call(CHANNEL, "reset-credit/inspect", {}).then(unwrap).then((value) => {
2328
2366
  if (!live) return;
2329
- setInspectedExpiry(value.nextExpiresAt);
2330
- setExpiryState("ready");
2367
+ setCredits(Array.isArray(value.credits) ? value.credits : []);
2368
+ setState("ready");
2331
2369
  }).catch(() => {
2332
- if (live) setExpiryState("error");
2370
+ if (live) setState("error");
2333
2371
  });
2334
2372
  return () => {
2335
2373
  live = false;
@@ -2337,8 +2375,35 @@ window.__ModuleLoader__.load({
2337
2375
  }, [
2338
2376
  rpc,
2339
2377
  count,
2340
- nextExpiresAt
2378
+ refreshKey
2341
2379
  ]);
2380
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2381
+ className: "codexSubscriptionResetBalance",
2382
+ "aria-label": t("resetCredits"),
2383
+ children: [credits.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2384
+ className: "codexSubscriptionCreditNote",
2385
+ role: "status",
2386
+ children: state === "loading" ? t("resetCreditExpiryLoading") : t("resetCreditExpiryFailed")
2387
+ }) : credits.map((credit, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResetCreditControl, {
2388
+ rpc,
2389
+ t,
2390
+ credit,
2391
+ hasExhaustedQuota,
2392
+ onConsumed
2393
+ }, credit.ref ?? `pending-${index}`)), state === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2394
+ className: "codexSubscriptionCreditNote",
2395
+ role: "status",
2396
+ children: t("resetCreditExpiryFailed")
2397
+ }) : null]
2398
+ });
2399
+ }
2400
+ function ResetCreditControl({ rpc, t, credit, hasExhaustedQuota, onConsumed }) {
2401
+ const [challenge, setChallenge] = (0, react.useState)();
2402
+ const [resetBusy, setResetBusy] = (0, react.useState)(false);
2403
+ const [resetAcknowledged, setResetAcknowledged] = (0, react.useState)(false);
2404
+ const [resetCountdown, setResetCountdown] = (0, react.useState)(0);
2405
+ const [resetError, setResetError] = (0, react.useState)();
2406
+ const [resetResult, setResetResult] = (0, react.useState)();
2342
2407
  (0, react.useEffect)(() => {
2343
2408
  if (challenge === void 0) {
2344
2409
  setResetCountdown(0);
@@ -2350,11 +2415,11 @@ window.__ModuleLoader__.load({
2350
2415
  return () => window.clearInterval(timer);
2351
2416
  }, [challenge]);
2352
2417
  const prepareReset = () => {
2353
- if (resetBusy) return;
2418
+ if (resetBusy || typeof credit.ref !== "string") return;
2354
2419
  setResetBusy(true);
2355
2420
  setResetError(void 0);
2356
2421
  setResetResult(void 0);
2357
- rpc.call(CHANNEL, "reset-credit/prepare", {}).then(unwrap).then((next) => {
2422
+ rpc.call(CHANNEL, "reset-credit/prepare", { creditRef: credit.ref }).then(unwrap).then((next) => {
2358
2423
  setChallenge(next);
2359
2424
  setResetAcknowledged(false);
2360
2425
  }).catch((error) => setResetError(resetCreditErrorText(error, t))).finally(() => setResetBusy(false));
@@ -2384,36 +2449,26 @@ window.__ModuleLoader__.load({
2384
2449
  }).catch((error) => setResetError(resetCreditErrorText(error, t))).finally(() => setResetBusy(false));
2385
2450
  };
2386
2451
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2387
- className: "codexSubscriptionResetBalance",
2452
+ className: "codexSubscriptionResetCard",
2388
2453
  children: [
2389
- challenge === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2390
- className: "codexSubscriptionResetSummary",
2391
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2392
- className: "codexSubscriptionResetMeta",
2393
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: fill(t("resetCreditsValue"), { count }) }), expiryState === "loading" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2394
- className: "codexSubscriptionResetExpiry",
2395
- role: "status",
2396
- children: t("resetCreditExpiryLoading")
2397
- }) : expiryState === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2398
- className: "codexSubscriptionResetExpiry",
2399
- children: t("resetCreditExpiryFailed")
2400
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResetCreditExpiry, {
2401
- expiresAt: inspectedExpiry,
2402
- t
2403
- })]
2404
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2405
- className: "codexSubscriptionActions",
2406
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2407
- className: "codexSubscriptionResetUse",
2408
- type: "button",
2409
- variant: "outline",
2410
- disabled: resetBusy,
2411
- "aria-busy": resetBusy,
2412
- onClick: prepareReset,
2413
- children: resetBusy ? t("resetPreparing") : t("resetUse")
2414
- })
2454
+ challenge === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2455
+ className: "codexSubscriptionResetMeta",
2456
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: credit.name ?? t("resetCreditDefaultName") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResetCreditExpiry, {
2457
+ expiresAt: credit.expiresAt,
2458
+ t
2415
2459
  })]
2416
- }) }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2460
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2461
+ className: "codexSubscriptionActions",
2462
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2463
+ className: "codexSubscriptionResetUse",
2464
+ type: "button",
2465
+ variant: "outline",
2466
+ disabled: resetBusy || typeof credit.ref !== "string",
2467
+ "aria-busy": resetBusy,
2468
+ onClick: prepareReset,
2469
+ children: resetBusy ? t("resetPreparing") : t("resetUse")
2470
+ })
2471
+ })] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2417
2472
  className: "codexSubscriptionResetFlow",
2418
2473
  role: "group",
2419
2474
  "aria-labelledby": "codex-reset-confirm-title",
@@ -2498,6 +2553,7 @@ window.__ModuleLoader__.load({
2498
2553
  }
2499
2554
  function UsageCard({ rpc, t, signedIn, resetKey }) {
2500
2555
  const [usage, setUsage] = (0, react.useState)();
2556
+ const [usageRefreshGeneration, setUsageRefreshGeneration] = (0, react.useState)(0);
2501
2557
  const [busy, setBusy] = (0, react.useState)(false);
2502
2558
  const [error, setError] = (0, react.useState)();
2503
2559
  const request = (0, react.useRef)(0);
@@ -2509,6 +2565,7 @@ window.__ModuleLoader__.load({
2509
2565
  rpc.call(CHANNEL, "usage", { force }).then(unwrap).then((next) => {
2510
2566
  if (request.current === id) {
2511
2567
  setUsage(next);
2568
+ setUsageRefreshGeneration((value) => value + 1);
2512
2569
  if (force) notifyQuickQuota();
2513
2570
  }
2514
2571
  }).catch((error) => {
@@ -2626,11 +2683,13 @@ window.__ModuleLoader__.load({
2626
2683
  }) : null,
2627
2684
  visibleUsage?.resetCredits?.availableCount > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2628
2685
  className: "codexSubscriptionCreditBalance",
2629
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("resetCredits") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResetCreditControl, {
2686
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("resetCredits") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResetCreditList, {
2630
2687
  rpc,
2631
2688
  t,
2632
2689
  count: visibleUsage.resetCredits.availableCount,
2633
2690
  nextExpiresAt: visibleUsage.resetCredits.nextExpiresAt,
2691
+ initialCredits: visibleUsage.resetCredits.credits,
2692
+ refreshKey: `${resetKey}:${usageRefreshGeneration}`,
2634
2693
  hasExhaustedQuota: exhausted,
2635
2694
  onConsumed: () => load(true)
2636
2695
  })]
package/lib/index.js CHANGED
@@ -21,6 +21,41 @@ import { dirname, join, resolve } from "node:path";
21
21
  const VERSION = 1;
22
22
  const DEFAULT_LABEL = "Account 1";
23
23
  const clone$1 = (value) => value === void 0 ? void 0 : structuredClone(value);
24
+ const EMAIL_MAX_LENGTH = 254;
25
+ const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/u;
26
+ /** Keep only a bounded, display-safe email address from a trusted OAuth result. */
27
+ function normalizeAccountEmail(value) {
28
+ if (typeof value !== "string") return void 0;
29
+ const email = value.trim();
30
+ return email.length > 0 && email.length <= EMAIL_MAX_LENGTH && EMAIL_PATTERN.test(email) ? email : void 0;
31
+ }
32
+ function decodeJwtPayload(access) {
33
+ if (typeof access !== "string") return void 0;
34
+ const encoded = access.split(".")[1];
35
+ if (typeof encoded !== "string" || encoded.length === 0) return void 0;
36
+ try {
37
+ return JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
38
+ } catch {
39
+ return;
40
+ }
41
+ }
42
+ function emailFromAccessToken(access) {
43
+ const payload = decodeJwtPayload(access);
44
+ return normalizeAccountEmail(payload?.["https://api.openai.com/profile"]?.email ?? payload?.email);
45
+ }
46
+ /** Normalize the one non-secret account attribute that may cross the UI boundary. */
47
+ function sanitizeOAuthCredential(value) {
48
+ const credential = assertOAuthCredential$1(value);
49
+ const email = emailFromAccessToken(credential.access) ?? normalizeAccountEmail(credential.email);
50
+ if (email === void 0) {
51
+ delete credential.email;
52
+ return credential;
53
+ }
54
+ return {
55
+ ...credential,
56
+ email
57
+ };
58
+ }
24
59
  function assertOAuthCredential$1(value) {
25
60
  if (value === null || typeof value !== "object" || value.type !== "oauth" || typeof value.access !== "string" || value.access.length === 0 || typeof value.refresh !== "string" || value.refresh.length === 0 || typeof value.expires !== "number" || !Number.isFinite(value.expires)) throw new Error("Codex account vault received a malformed OAuth credential");
26
61
  return clone$1(value);
@@ -48,7 +83,7 @@ function assertVaultRecord(record) {
48
83
  return {
49
84
  id: account.id,
50
85
  label: normalizeLabel(account.label),
51
- credential: assertOAuthCredential$1(account.credential)
86
+ credential: sanitizeOAuthCredential(account.credential)
52
87
  };
53
88
  });
54
89
  if (!ids.has(record.payload.activeId)) throw new Error("Codex account vault active account is missing");
@@ -80,7 +115,7 @@ var PendingOAuthCredentialStore = class {
80
115
  async modify(providerId, update) {
81
116
  if (providerId !== "openai-codex") throw new Error("Pending Codex login received an unknown provider");
82
117
  const next = await update(clone$1(this.#credential));
83
- if (next !== void 0) this.#credential = assertOAuthCredential$1(next);
118
+ if (next !== void 0) this.#credential = sanitizeOAuthCredential(next);
84
119
  return clone$1(this.#credential);
85
120
  }
86
121
  async delete(providerId) {
@@ -170,7 +205,8 @@ var DshOAuthAccountVault = class {
170
205
  id: account.id,
171
206
  label: account.label,
172
207
  active: account.id === payload.activeId,
173
- expiresAt: account.credential.expires
208
+ expiresAt: account.credential.expires,
209
+ ...account.credential.email === void 0 ? {} : { email: account.credential.email }
174
210
  }));
175
211
  });
176
212
  }
@@ -186,7 +222,7 @@ var DshOAuthAccountVault = class {
186
222
  add(label, credential) {
187
223
  return this.#enqueue(async () => {
188
224
  const normalizedLabel = normalizeLabel(label);
189
- const validated = assertOAuthCredential$1(credential);
225
+ const validated = sanitizeOAuthCredential(credential);
190
226
  await this.#ensurePayload();
191
227
  const id = this.createId();
192
228
  const account = (await this.#modifyPayload((current) => ({
@@ -202,7 +238,8 @@ var DshOAuthAccountVault = class {
202
238
  id,
203
239
  label: account.label,
204
240
  active: true,
205
- expiresAt: account.credential.expires
241
+ expiresAt: account.credential.expires,
242
+ ...account.credential.email === void 0 ? {} : { email: account.credential.email }
206
243
  };
207
244
  });
208
245
  }
@@ -222,7 +259,7 @@ var DshOAuthAccountVault = class {
222
259
  if (await this.#ensurePayload() === void 0) {
223
260
  const initial = await update(void 0);
224
261
  if (initial === void 0) return void 0;
225
- const credential = assertOAuthCredential$1(initial);
262
+ const credential = sanitizeOAuthCredential(initial);
226
263
  await this.credentials.set(this.legacyRef, JSON.stringify(credential));
227
264
  await this.#ensurePayload();
228
265
  return clone$1(credential);
@@ -236,7 +273,8 @@ var DshOAuthAccountVault = class {
236
273
  result = previous;
237
274
  return current;
238
275
  }
239
- const credential = assertOAuthCredential$1(next);
276
+ const credential = sanitizeOAuthCredential(next);
277
+ if (credential.email === void 0 && previous.email !== void 0) credential.email = previous.email;
240
278
  const accounts = [...current.accounts];
241
279
  accounts[index] = {
242
280
  ...accounts[index],
@@ -1265,7 +1303,7 @@ function openaiCodexSubscriptionProvider({ resolveSpeedMode = () => void 0, reso
1265
1303
  }
1266
1304
  //#endregion
1267
1305
  //#region src/version.js
1268
- const PACKAGE_VERSION = "1.12.2";
1306
+ const PACKAGE_VERSION = "1.13.1";
1269
1307
  const USER_AGENT = `dsh-codex-subscription/${PACKAGE_VERSION}`;
1270
1308
  //#endregion
1271
1309
  //#region src/model-catalog.js
@@ -2244,15 +2282,23 @@ function resetCreditsOf(value) {
2244
2282
  if (value === void 0 || value === null) return void 0;
2245
2283
  if (!record$1(value) || !Number.isSafeInteger(value.available_count) || value.available_count < 0) throw new Error("Codex returned malformed reset credit details");
2246
2284
  if (value.credits !== void 0 && value.credits !== null && !Array.isArray(value.credits)) throw new Error("Codex returned malformed reset credit details");
2247
- const expirations = (value.credits ?? []).filter((credit) => record$1(credit) && credit.status?.toLowerCase?.() === "available").map((credit) => {
2248
- if (Number.isSafeInteger(credit.expires_at) && credit.expires_at > 0) return credit.expires_at * 1e3;
2249
- if (typeof credit.expires_at === "string" && credit.expires_at.length <= 64) {
2285
+ const available = (value.credits ?? []).filter((credit) => record$1(credit) && credit.status?.toLowerCase?.() === "available").map((credit) => {
2286
+ const name = typeof credit.title === "string" && credit.title.trim().length > 0 ? credit.title.trim().slice(0, 120) : void 0;
2287
+ let expiresAt;
2288
+ if (Number.isSafeInteger(credit.expires_at) && credit.expires_at > 0) expiresAt = credit.expires_at * 1e3;
2289
+ else if (typeof credit.expires_at === "string" && credit.expires_at.length <= 64) {
2250
2290
  const parsed = Date.parse(credit.expires_at);
2251
- if (Number.isFinite(parsed) && parsed > 0) return parsed;
2291
+ if (Number.isFinite(parsed) && parsed > 0) expiresAt = parsed;
2252
2292
  }
2253
- }).filter((expiration) => expiration !== void 0);
2293
+ return {
2294
+ ...name === void 0 ? {} : { name },
2295
+ ...expiresAt === void 0 ? {} : { expiresAt }
2296
+ };
2297
+ });
2298
+ const expirations = available.map((credit) => credit.expiresAt).filter((expiration) => expiration !== void 0);
2254
2299
  return {
2255
2300
  availableCount: value.available_count,
2301
+ ...available.length === 0 ? {} : { credits: available },
2256
2302
  ...expirations.length === 0 ? {} : { nextExpiresAt: Math.min(...expirations) }
2257
2303
  };
2258
2304
  }
@@ -2691,10 +2737,13 @@ function parseDetails(value, now) {
2691
2737
  if (available.length === 0) throw new Error("No usable quota reset is available");
2692
2738
  return {
2693
2739
  availableCount: value.available_count,
2694
- creditId: available[0].credit.id,
2695
- title: safeCopy(available[0].credit.title),
2696
- description: safeCopy(available[0].credit.description),
2697
- creditExpiresAt: available[0].expiresAt
2740
+ credits: available.map(({ credit, expiresAt }, index) => ({
2741
+ creditId: credit.id,
2742
+ title: safeCopy(credit.title),
2743
+ description: safeCopy(credit.description),
2744
+ creditExpiresAt: expiresAt,
2745
+ index
2746
+ }))
2698
2747
  };
2699
2748
  }
2700
2749
  function parseConsumeResult(value) {
@@ -2728,6 +2777,23 @@ function createCodexResetCreditService(options) {
2728
2777
  const challengeTtlMs = options.challengeTtlMs ?? DEFAULT_CHALLENGE_TTL_MS;
2729
2778
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
2730
2779
  const challenges = /* @__PURE__ */ new Map();
2780
+ const creditRefs = /* @__PURE__ */ new Map();
2781
+ let creditRefSequence = 0;
2782
+ const rememberCreditRef = (accountId, credit) => {
2783
+ const ref = `${randomUUID$1()}-${++creditRefSequence}`;
2784
+ creditRefs.set(ref, {
2785
+ accountId,
2786
+ creditId: credit.creditId,
2787
+ index: credit.index
2788
+ });
2789
+ while (creditRefs.size > 64) creditRefs.delete(creditRefs.keys().next().value);
2790
+ return ref;
2791
+ };
2792
+ const publicCredit = (accountId, credit) => ({
2793
+ ref: rememberCreditRef(accountId, credit),
2794
+ ...credit.title === void 0 ? {} : { name: credit.title },
2795
+ ...credit.creditExpiresAt === void 0 ? {} : { expiresAt: credit.creditExpiresAt }
2796
+ });
2731
2797
  const resolveCredentials = async (signal) => credentialsOf(await getAuth({ signal }), await readCredential({ signal }));
2732
2798
  const readDetails = async (signal, credentials) => {
2733
2799
  const { access, accountId } = credentials ?? await resolveCredentials(signal);
@@ -2757,16 +2823,27 @@ function createCodexResetCreditService(options) {
2757
2823
  };
2758
2824
  return Object.freeze({
2759
2825
  async inspect({ signal } = {}) {
2760
- const { details } = await readDetails(signal);
2826
+ const { accountId, details } = await readDetails(signal);
2761
2827
  return {
2762
2828
  availableCount: details.availableCount,
2763
- ...details.creditExpiresAt === void 0 ? {} : { nextExpiresAt: details.creditExpiresAt }
2829
+ credits: details.credits.map((credit) => publicCredit(accountId, credit)),
2830
+ ...details.credits[0]?.creditExpiresAt === void 0 ? {} : { nextExpiresAt: details.credits[0].creditExpiresAt }
2764
2831
  };
2765
2832
  },
2766
- async prepare({ signal } = {}) {
2833
+ async prepare({ creditRef, signal } = {}) {
2767
2834
  const credentials = await resolveCredentials(signal);
2768
2835
  for (const [challengeId, challenge] of challenges) {
2769
- if (challenge.accountId !== credentials.accountId || challenge.uncertain !== true) continue;
2836
+ if (challenge.accountId === credentials.accountId && challenge.uncertain === false && challenge.creditRef === creditRef) if (now() > challenge.expiresAt) challenges.delete(challengeId);
2837
+ else return {
2838
+ challengeId,
2839
+ availableCount: challenge.availableCount,
2840
+ readyAt: challenge.readyAt,
2841
+ expiresAt: challenge.expiresAt,
2842
+ ...challenge.creditExpiresAt === void 0 ? {} : { creditExpiresAt: challenge.creditExpiresAt },
2843
+ ...challenge.title === void 0 ? {} : { title: challenge.title },
2844
+ ...challenge.description === void 0 ? {} : { description: challenge.description }
2845
+ };
2846
+ if (challenge.accountId !== credentials.accountId || challenge.uncertain !== true || creditRef !== void 0 && challenge.creditRef !== creditRef) continue;
2770
2847
  if (now() > challenge.expiresAt) {
2771
2848
  challenges.delete(challengeId);
2772
2849
  continue;
@@ -2782,22 +2859,31 @@ function createCodexResetCreditService(options) {
2782
2859
  };
2783
2860
  }
2784
2861
  const { accountId, details } = await readDetails(signal, credentials);
2862
+ let selected = details.credits[0];
2863
+ if (creditRef !== void 0) {
2864
+ const reference = typeof creditRef === "string" ? creditRefs.get(creditRef) : void 0;
2865
+ if (reference === void 0 || reference.accountId !== accountId) throw new Error("This quota reset confirmation is no longer valid");
2866
+ selected = details.credits.find((credit) => credit.creditId === reference.creditId && credit.index === reference.index);
2867
+ if (selected === void 0) throw new Error("This quota reset confirmation is no longer valid");
2868
+ }
2869
+ if (selected === void 0) throw new Error("No usable quota reset is available");
2785
2870
  const preparedAt = now();
2786
2871
  const readyAt = preparedAt + confirmDelayMs;
2787
- const expiresAt = Math.min(preparedAt + challengeTtlMs, details.creditExpiresAt ?? Number.MAX_SAFE_INTEGER);
2872
+ const expiresAt = Math.min(preparedAt + challengeTtlMs, selected.creditExpiresAt ?? Number.MAX_SAFE_INTEGER);
2788
2873
  if (expiresAt <= readyAt) throw new Error("The available quota reset expires too soon");
2789
2874
  const challengeId = randomUUID$1();
2790
2875
  challenges.set(challengeId, {
2791
2876
  state: "prepared",
2792
2877
  accountId,
2793
- creditId: details.creditId,
2878
+ creditRef,
2879
+ creditId: selected.creditId,
2794
2880
  redeemRequestId: randomUUID$1(),
2795
2881
  readyAt,
2796
2882
  expiresAt,
2797
2883
  availableCount: details.availableCount,
2798
- creditExpiresAt: details.creditExpiresAt,
2799
- title: details.title,
2800
- description: details.description,
2884
+ creditExpiresAt: selected.creditExpiresAt,
2885
+ title: selected.title,
2886
+ description: selected.description,
2801
2887
  uncertain: false
2802
2888
  });
2803
2889
  return {
@@ -2805,9 +2891,9 @@ function createCodexResetCreditService(options) {
2805
2891
  availableCount: details.availableCount,
2806
2892
  readyAt,
2807
2893
  expiresAt,
2808
- ...details.creditExpiresAt === void 0 ? {} : { creditExpiresAt: details.creditExpiresAt },
2809
- ...details.title === void 0 ? {} : { title: details.title },
2810
- ...details.description === void 0 ? {} : { description: details.description }
2894
+ ...selected.creditExpiresAt === void 0 ? {} : { creditExpiresAt: selected.creditExpiresAt },
2895
+ ...selected.title === void 0 ? {} : { title: selected.title },
2896
+ ...selected.description === void 0 ? {} : { description: selected.description }
2811
2897
  };
2812
2898
  },
2813
2899
  async consume({ challengeId, acknowledged, signal } = {}) {
@@ -2885,6 +2971,7 @@ function createCodexResetCreditService(options) {
2885
2971
  },
2886
2972
  clear() {
2887
2973
  challenges.clear();
2974
+ creditRefs.clear();
2888
2975
  }
2889
2976
  });
2890
2977
  }
@@ -3026,7 +3113,10 @@ function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditSer
3026
3113
  signal.throwIfAborted();
3027
3114
  return {
3028
3115
  ok: true,
3029
- value: endpoint === "reset-credit/inspect" ? await resetCreditService.inspect({ signal }) : endpoint === "reset-credit/prepare" ? await resetCreditService.prepare({ signal }) : await resetCreditService.consume({
3116
+ value: endpoint === "reset-credit/inspect" ? await resetCreditService.inspect({ signal }) : endpoint === "reset-credit/prepare" ? await resetCreditService.prepare({
3117
+ creditRef: payload?.creditRef,
3118
+ signal
3119
+ }) : await resetCreditService.consume({
3030
3120
  challengeId: payload?.challengeId,
3031
3121
  acknowledged: payload?.acknowledged,
3032
3122
  signal
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-codex-subscription",
3
- "version": "1.12.2",
3
+ "version": "1.13.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",