dsh-codex-subscription 1.4.1 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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.4.1` package below; do not install a moving branch.
9
+ - Use the exact `1.6.0` 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.1-rc.2 plugin --profile web add dsh-codex-subscription@1.4.1
37
+ npx -y @deepseek-ai/dsh@0.1.1-rc.2 plugin --profile web add dsh-codex-subscription@1.6.0
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.4.1
43
+ dsh plugin --profile web add dsh-codex-subscription@1.6.0
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.1-rc.2` in place of `dsh`.
60
60
 
61
- 1. `dsh-codex-subscription` version `1.4.1` appears exactly once.
61
+ 1. `dsh-codex-subscription` version `1.6.0` 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/DIRECTORY.md CHANGED
@@ -4,11 +4,11 @@ Use this factual copy when listing `dsh-codex-subscription` in a DSH plugin dire
4
4
 
5
5
  ## Short description
6
6
 
7
- Use a ChatGPT subscription in DeepSeek Harness through independent OAuth sign-in, with Codex models, backend-reported quota, subscription web search, image generation, and no silent paid fallback.
7
+ Use a ChatGPT subscription in DeepSeek Harness through independent OAuth sign-in, with Codex models, backend-reported quota, subscription web search, Beta image generation/editing, and no silent paid fallback.
8
8
 
9
9
  ## 中文短描述
10
10
 
11
- 通过独立 OAuth 登录在 DeepSeek Harness 中使用 ChatGPT 订阅,提供 Codex 模型、服务端实际额度、订阅搜索和图片生成,不会静默切换其他付费路由。
11
+ 通过独立 OAuth 登录在 DeepSeek Harness 中使用 ChatGPT 订阅,提供 Codex 模型、服务端实际额度、订阅搜索和 Beta 图片生成与编辑,不会静默切换其他付费路由。
12
12
 
13
13
  ## Facts
14
14
 
@@ -18,7 +18,7 @@ Use a ChatGPT subscription in DeepSeek Harness through independent OAuth sign-in
18
18
  - Codex CLI required: no
19
19
  - OpenAI API key required: no
20
20
  - Platforms: official DSH plus DSH-Portable product forms for Windows, macOS, and Linux
21
- - Main capabilities: Codex models, quota, web search, image generation, optional composer quota, optional Fast mode, secret-free support diagnostics
21
+ - Main capabilities: Codex models, quota and expiring resets, web search, Beta image generation/editing with explicit references and native preview/download, optional composer quota, Fast mode, secret-free support diagnostics and direct feedback
22
22
  - Safety boundary: subscription failures remain visible and never silently fall back to another paid provider
23
23
 
24
24
  ## Install
package/README.md CHANGED
@@ -8,7 +8,7 @@ No OpenAI API key or Codex CLI. Models, search, quota, and image generation stay
8
8
 
9
9
  [![CI](https://github.com/WSL043/dsh-codex-subscription/actions/workflows/ci.yml/badge.svg)](https://github.com/WSL043/dsh-codex-subscription/actions/workflows/ci.yml)
10
10
  [![npm](https://img.shields.io/npm/v/dsh-codex-subscription?logo=npm&label=npm)](https://www.npmjs.com/package/dsh-codex-subscription)
11
- [![npm downloads](https://img.shields.io/npm/dt/dsh-codex-subscription?logo=npm&label=downloads)](https://www.npmjs.com/package/dsh-codex-subscription)
11
+ [![total npm downloads](https://img.shields.io/npm/dt/dsh-codex-subscription?logo=npm&label=total%20downloads)](https://www.npmjs.com/package/dsh-codex-subscription)
12
12
  [![MIT](https://img.shields.io/badge/license-MIT-111111.svg)](LICENSE)
13
13
  [![Star](https://img.shields.io/github/stars/WSL043/dsh-codex-subscription?style=flat&logo=github&label=Star)](https://github.com/WSL043/dsh-codex-subscription/stargazers)
14
14
 
@@ -47,10 +47,10 @@ DSH-Portable exposes the same standard plugin command, so the command above also
47
47
  | **Recoverable and diagnosable** | Sign-in state reconciles automatically; Settings can create a support report without credentials or account identifiers |
48
48
  | **Visible quota** | Keep backend-provided standard Codex, Spark, and other limits separate |
49
49
  | **Composer quota** | Choose a compact percentage, progress bar, or no inline quota display |
50
- | **Safe quota reset** | Use a reset only for an exhausted Codex quota, with a cooldown, checkbox, and typed confirmation |
50
+ | **Safe quota reset** | See the next expiry and deliberately try a reset with a cooldown and acknowledgement |
51
51
  | **Subscription search** | Explicitly choose DSH default search or Codex subscription search |
52
- | **Codex image generation** | Describe an image in a DSH conversation and view the generated result in that session |
53
- | **Fast mode (Beta)** | Switch between Standard and Fast directly in the composer |
52
+ | **Codex image generation and editing (Beta)** | Generate without references, or explicitly edit selected conversation images; preview, zoom, and download the original |
53
+ | **Fast mode** | Switch between Standard and Fast directly in the composer |
54
54
 
55
55
  These capabilities reuse the same local ChatGPT sign-in. Subscription routing failures stay visible and never silently switch to another paid route.
56
56
 
@@ -139,14 +139,14 @@ Restart DSH manually after installation, then:
139
139
  ## Features
140
140
 
141
141
  - ChatGPT OAuth sign-in with credentials kept on the host;
142
- - Codex models and image generation directly inside DSH conversations;
142
+ - Codex models and Beta image generation/editing directly inside DSH conversations;
143
143
  - A clear choice between DSH default search and Codex subscription search;
144
144
  - Actual backend-provided quota, reset time, and freshness;
145
145
  - Separate standard Codex, Codex-Spark, Credits, and other independent limits;
146
- - Deliberate quota-reset redemption for exhausted Codex limits, with layered confirmation and no automatic retry;
146
+ - Visible quota-reset count and earliest expiry, with deliberate early redemption, layered confirmation, and no automatic retry;
147
147
  - Optional percentage or progress bar for the selected Codex model (off by default);
148
- - Standard or Fast mode for supported Codex models directly in the composer (Beta);
149
- - A copyable support report in Settings that excludes OAuth credentials, account identifiers, and authorization timestamps;
148
+ - Standard or Fast mode for supported Codex models directly in the composer;
149
+ - A copyable support report and direct feedback link in Settings; the report excludes OAuth credentials, account identifiers, and authorization timestamps;
150
150
  - Visible errors when subscription routing is unavailable, with no silent paid fallback.
151
151
 
152
152
  ### Composer quota
@@ -161,12 +161,20 @@ Standard Codex uses the lowest remaining window returned by the service; Spark u
161
161
 
162
162
  ### Safe quota reset
163
163
 
164
- If ChatGPT reports an available quota reset, Settings shows it in a compact row. Redemption stays disabled
165
- until a Codex model quota is exhausted. Using one requires a five-second cooldown, an acknowledgement checkbox,
166
- and the exact confirmation phrase. Cancel never consumes a reset, rapid repeated clicks are single-flight, and an
167
- uncertain network result is never retried automatically.
164
+ If ChatGPT reports an available quota reset, Settings shows its count and earliest disclosed expiry in a compact row.
165
+ You may deliberately try it before a quota reaches 100%, which is useful for a reset nearing expiry. ChatGPT still
166
+ decides whether a window needs resetting and may return **nothing to reset** without spending the reset. The final
167
+ action requires an acknowledgement checkbox and five-second cooldown. Cancel never consumes a reset, rapid repeated
168
+ clicks are single-flight, and an uncertain network result is never retried automatically.
168
169
 
169
- ### Composer speed (Beta)
170
+ ### Image generation and editing (Beta)
171
+
172
+ Generated images open in a DSH-native preview with zoom, fit, dimensions, and **Download original**. A new image request
173
+ does not silently include earlier images. To refine a result, explicitly ask to edit that image in the same conversation;
174
+ the plugin sends only the selected durable image references, up to five. GPT Image 2 can take longer than a normal text
175
+ turn, and detailed text, exact composition, or repeated-character consistency may still need another pass.
176
+
177
+ ### Composer speed
170
178
 
171
179
  With a supported Codex model selected, open the composer's model menu to choose Standard or Fast.
172
180
  Standard adds no icon; only Fast shows a lightning icon before the model name. Spark does not show the speed entry. Fast mode increases speed and uses more Credits;
package/README.zh-CN.md CHANGED
@@ -9,7 +9,7 @@
9
9
 
10
10
  [![CI](https://github.com/WSL043/dsh-codex-subscription/actions/workflows/ci.yml/badge.svg)](https://github.com/WSL043/dsh-codex-subscription/actions/workflows/ci.yml)
11
11
  [![npm](https://img.shields.io/npm/v/dsh-codex-subscription?logo=npm&label=npm)](https://www.npmjs.com/package/dsh-codex-subscription)
12
- [![npm downloads](https://img.shields.io/npm/dt/dsh-codex-subscription?logo=npm&label=downloads)](https://www.npmjs.com/package/dsh-codex-subscription)
12
+ [![npm 总下载量](https://img.shields.io/npm/dt/dsh-codex-subscription?logo=npm&label=%E6%80%BB%E4%B8%8B%E8%BD%BD%E9%87%8F)](https://www.npmjs.com/package/dsh-codex-subscription)
13
13
  [![MIT](https://img.shields.io/badge/license-MIT-111111.svg)](LICENSE)
14
14
  [![Star](https://img.shields.io/github/stars/WSL043/dsh-codex-subscription?style=flat&logo=github&label=Star)](https://github.com/WSL043/dsh-codex-subscription/stargazers)
15
15
 
@@ -48,10 +48,10 @@ DSH-Portable 也提供相同的标准插件命令,因此同样使用上面的
48
48
  | **可恢复、可诊断** | 登录状态会自动对账;设置页可生成不含凭据和账号标识的支持报告 |
49
49
  | **额度可见** | 普通 Codex、Spark 等服务端实际返回的额度分开显示 |
50
50
  | **输入框额度** | 可选择紧凑百分比、进度条或完全关闭输入框额度显示 |
51
- | **安全额度重置** | 仅在 Codex 额度用尽后使用重置,并经过冷静期、勾选和输入确认 |
51
+ | **安全额度重置** | 直接查看最早到期时间,并通过冷静期和知情确认主动尝试重置 |
52
52
  | **订阅搜索** | 可在 DSH 默认搜索与 Codex 订阅搜索之间明确切换 |
53
- | **Codex 图片生成** | DSH 对话里直接描述画面,生成结果会显示在当前会话中 |
54
- | **高速模式(Beta)** | 直接在输入框切换标准或高速,无需离开当前会话 |
53
+ | **Codex 图片生成与编辑(Beta)** | 可无参考图全新生成,也可明确选择会话图片继续编辑;支持预览、缩放和下载原图 |
54
+ | **高速模式** | 直接在输入框切换标准或高速,无需离开当前会话 |
55
55
 
56
56
  这些能力共用同一份本机 ChatGPT 登录。订阅路由失败时会明确报错,不会静默切换到其他付费路由。
57
57
 
@@ -139,14 +139,14 @@ dsh --profile web --dump-config
139
139
  ## 功能
140
140
 
141
141
  - ChatGPT OAuth 登录,凭据保留在本机;
142
- - Codex 模型和图片生成直接出现在 DSH 会话中;
142
+ - Codex 模型和 Beta 图片生成与编辑直接出现在 DSH 会话中;
143
143
  - DSH 默认搜索与 Codex 订阅搜索可随时切换;
144
144
  - 设置页显示服务端返回的额度、重置时间和更新时间;
145
145
  - 普通 Codex、Codex-Spark、Credits 等独立额度分开显示;
146
- - 已用尽的 Codex 额度可主动使用重置卡,并经过分层确认且不会自动重试;
146
+ - 显示重置卡数量与最早到期时间,也允许在额度未完全用尽时主动尝试,并经过分层确认且不会自动重试;
147
147
  - 输入框可用百分比或进度条显示当前 Codex 模型的剩余额度(默认关闭);
148
- - 输入框可为支持的 Codex 模型切换标准或高速模式(Beta);
149
- - 设置页可生成并复制无敏感信息的支持诊断,报告不包含 OAuth 凭据、账号标识或授权时间;
148
+ - 输入框可为支持的 Codex 模型切换标准或高速模式;
149
+ - 设置页可生成并复制无敏感信息的支持诊断,并直接打开反馈入口;报告不包含 OAuth 凭据、账号标识或授权时间;
150
150
  - 订阅路由不可用时明确报错,不会静默切换到其他付费路由。
151
151
 
152
152
  ### 输入框额度
@@ -160,11 +160,17 @@ Spark 使用独立额度。插件不会写死“5 小时 + 每周”,也不会
160
160
 
161
161
  ### 安全使用额度重置
162
162
 
163
- ChatGPT 返回可用重置卡时,设置页会用紧凑的一行显示。只有至少一个 Codex 模型额度已用尽后才能使用;
164
- 最终操作还需要等待 5 秒、勾选知情确认,并输入完全一致的确认短语。取消不会消耗,快速连续点击只允许一次请求,
165
- 网络结果不确定时也不会自动重试。
163
+ ChatGPT 返回可用重置卡时,设置页会用紧凑的一行显示数量和服务端提供的最早到期时间。即使额度尚未到 100%,
164
+ 也可以主动尝试使用,适合重置卡即将过期的情况;是否需要重置仍由 ChatGPT 判断,服务端可能返回“当前无需重置”且不扣次数。
165
+ 最终操作需要勾选知情确认并等待 5 秒。取消不会消耗,快速连续点击只允许一次请求,网络结果不确定时也不会自动重试。
166
166
 
167
- ### 输入框速度(Beta)
167
+ ### 图片生成与编辑(Beta)
168
+
169
+ 生成的图片会在 DSH 原生风格的查看器中打开,支持缩放、适合窗口、查看尺寸和下载原图。全新生成不会静默带入历史图片;
170
+ 需要继续修改时,要在同一会话明确说明编辑这张图,插件只会发送明确选中的持久图片引用,最多 5 张。GPT Image 2 的耗时可能明显长于
171
+ 文本回复,复杂文字、精确构图和连续角色一致性也可能需要再次调整。
172
+
173
+ ### 输入框速度
168
174
 
169
175
  选择支持的 Codex 模型后,可在输入框的模型菜单中切换标准与高速。标准模式不增加图标,
170
176
  只有高速模式会在模型名称左侧显示闪电;Spark 不显示速度入口。高速模式会提高速度,也会消耗更多 Credits;具体规则见
package/lib/client.js CHANGED
@@ -127,6 +127,7 @@ window.__ModuleLoader__.load({
127
127
  ];
128
128
  const NS = "settings.codexSubscription";
129
129
  const CHANNEL = "/codex-subscription";
130
+ const SUPPORT_ISSUE_URL = "https://github.com/WSL043/dsh-codex-subscription/issues/new?template=install-problem.yml";
130
131
  const QUICK_QUOTA_REFRESH_EVENT = "dsh-codex-subscription:refresh-quick-quota";
131
132
  const QUICK_QUOTA_REFRESH_MS = 6e4;
132
133
  const zh = {
@@ -153,6 +154,7 @@ window.__ModuleLoader__.load({
153
154
  diagnosticsCopy: "复制诊断",
154
155
  diagnosticsCopied: "已复制",
155
156
  diagnosticsFailed: "无法生成诊断信息。",
157
+ feedbackOpen: "反馈问题",
156
158
  searchTitle: "搜索来源",
157
159
  searchDsh: "DSH 默认",
158
160
  searchDshHint: "当前搜索服务",
@@ -183,13 +185,16 @@ window.__ModuleLoader__.load({
183
185
  monthlyCreditLimit: "Credits 月度消费上限",
184
186
  resetCredits: "可用额度重置次数",
185
187
  resetCreditsValue: "{count} 次",
186
- resetUse: "使用一次重置",
187
- resetUnavailable: "仅当至少一个 Codex 模型额度已用尽时可使用。",
188
+ resetUse: "使用重置",
188
189
  resetPreparing: "正在读取重置详情…",
189
190
  resetConfirmTitle: "确认使用额度重置",
190
- resetWarning: "此操作会立即消耗 1 次额度重置,且无法撤销。请确认你确实需要恢复已用尽的模型额度。",
191
- resetAcknowledge: "我知道这会消耗 1 次额度重置",
192
- resetTypeHint: "输入 {phrase} 以继续",
191
+ resetWarning: " ChatGPT 执行重置,将立即消耗 1 次且无法撤销。",
192
+ resetEarlyWarning: "当前模型额度尚未用尽;ChatGPT 可能执行重置,也可能判定暂无需重置且不消耗次数。",
193
+ resetAcknowledge: "我知道这次操作可能立即消耗 1 次重置",
194
+ resetCreditExpires: "到期:{value}",
195
+ resetCreditExpiryUnknown: "到期时间未提供",
196
+ resetCreditExpiryLoading: "正在读取到期时间…",
197
+ resetCreditExpiryFailed: "无法读取到期时间",
193
198
  resetWait: "请再等待 {count} 秒",
194
199
  resetFinal: "消耗 1 次并重置额度",
195
200
  resetUsing: "正在重置…",
@@ -202,8 +207,7 @@ window.__ModuleLoader__.load({
202
207
  resetExpired: "本次确认已失效,请重新开始。",
203
208
  resetInProgress: "额度重置正在处理中。",
204
209
  resetTooEarly: "请等待冷静期结束后再确认。",
205
- resetPhraseMismatch: "确认短语不一致。",
206
- resetNotExhausted: "当前 Codex 模型额度尚未用尽。",
210
+ resetAcknowledgeRequired: "请先确认已了解这次操作可能消耗重置次数。",
207
211
  resetAccountChanged: "登录账号已变更,请重新开始。",
208
212
  creditsNote: "仅显示 Codex 为此账户或工作区实际返回的额外 Credits、消费上限或额度重置次数;三者不是同一项。",
209
213
  creditsUsed: "已用 {used} / {limit} credits",
@@ -231,6 +235,7 @@ window.__ModuleLoader__.load({
231
235
  modelFailed: "模型目录加载失败:{value}",
232
236
  groupFailed: "{name}:{value}",
233
237
  imageGenerate: "生成图片",
238
+ imageBeta: "Beta",
234
239
  imageGenerating: "正在生成…",
235
240
  imageGenerated: "已生成",
236
241
  imageFailed: "生成失败",
@@ -240,7 +245,13 @@ window.__ModuleLoader__.load({
240
245
  imageLoading: "正在加载图片…",
241
246
  imageLoadFailed: "图片加载失败,点击重试",
242
247
  imagePreview: "图片预览",
243
- imageClosePreview: "关闭预览"
248
+ imageClosePreview: "关闭预览",
249
+ imageDownload: "下载原图",
250
+ imageZoomOut: "缩小",
251
+ imageZoomIn: "放大",
252
+ imageFit: "适合窗口",
253
+ imageRefineHint: "要修改这张图,请在当前会话明确说“编辑这张图片……”;插件只会把这张图作为参考。",
254
+ imageNewHint: "要重新生成,请直接描述一张新图片;不会自动带入历史图片。"
244
255
  };
245
256
  const en = {
246
257
  nav: "Codex",
@@ -266,6 +277,7 @@ window.__ModuleLoader__.load({
266
277
  diagnosticsCopy: "Copy report",
267
278
  diagnosticsCopied: "Copied",
268
279
  diagnosticsFailed: "Could not create diagnostics.",
280
+ feedbackOpen: "Report a problem",
269
281
  searchTitle: "Search source",
270
282
  searchDsh: "DSH default",
271
283
  searchDshHint: "Current search service",
@@ -296,13 +308,16 @@ window.__ModuleLoader__.load({
296
308
  monthlyCreditLimit: "Monthly Credits spending cap",
297
309
  resetCredits: "Available quota resets",
298
310
  resetCreditsValue: "{count} available",
299
- resetUse: "Use one reset",
300
- resetUnavailable: "Available only when at least one Codex model quota is exhausted.",
311
+ resetUse: "Use reset",
301
312
  resetPreparing: "Reading reset details…",
302
313
  resetConfirmTitle: "Confirm quota reset",
303
- resetWarning: "This immediately consumes one quota reset and cannot be undone. Continue only if you intend to restore an exhausted model quota.",
304
- resetAcknowledge: "I understand this consumes one quota reset",
305
- resetTypeHint: "Type {phrase} to continue",
314
+ resetWarning: "If ChatGPT performs the reset, one reset is consumed immediately and cannot be restored.",
315
+ resetEarlyWarning: "No model quota is exhausted. ChatGPT may reset it, or report that nothing needs resetting without consuming a reset.",
316
+ resetAcknowledge: "I understand this may consume one reset now",
317
+ resetCreditExpires: "Expires {value}",
318
+ resetCreditExpiryUnknown: "Expiration time not provided",
319
+ resetCreditExpiryLoading: "Reading expiration…",
320
+ resetCreditExpiryFailed: "Could not read expiration",
306
321
  resetWait: "Wait {count} more seconds",
307
322
  resetFinal: "Consume one reset",
308
323
  resetUsing: "Resetting…",
@@ -315,8 +330,7 @@ window.__ModuleLoader__.load({
315
330
  resetExpired: "This confirmation expired. Start again.",
316
331
  resetInProgress: "A quota reset is already in progress.",
317
332
  resetTooEarly: "Wait for the cooldown before confirming.",
318
- resetPhraseMismatch: "The confirmation phrase does not match.",
319
- resetNotExhausted: "The current Codex model quota is not exhausted.",
333
+ resetAcknowledgeRequired: "Confirm that you understand this may consume a reset.",
320
334
  resetAccountChanged: "The signed-in account changed. Start again.",
321
335
  creditsNote: "Shows only extra Credits, spending caps, or quota resets returned for this account or workspace; these are separate items.",
322
336
  creditsUsed: "{used} / {limit} credits used",
@@ -344,6 +358,7 @@ window.__ModuleLoader__.load({
344
358
  modelFailed: "Could not load models: {value}",
345
359
  groupFailed: "{name}: {value}",
346
360
  imageGenerate: "Generate image",
361
+ imageBeta: "Beta",
347
362
  imageGenerating: "Generating…",
348
363
  imageGenerated: "Generated",
349
364
  imageFailed: "Generation failed",
@@ -353,7 +368,13 @@ window.__ModuleLoader__.load({
353
368
  imageLoading: "Loading image…",
354
369
  imageLoadFailed: "Image failed to load. Click to retry",
355
370
  imagePreview: "Image preview",
356
- imageClosePreview: "Close preview"
371
+ imageClosePreview: "Close preview",
372
+ imageDownload: "Download original",
373
+ imageZoomOut: "Zoom out",
374
+ imageZoomIn: "Zoom in",
375
+ imageFit: "Fit to window",
376
+ imageRefineHint: "To change this image, say “Edit this image…” in this conversation; only this image is sent as a reference.",
377
+ imageNewHint: "To start over, describe a new image; prior images are not included automatically."
357
378
  };
358
379
  const STYLE = `
359
380
  .codexSubscription{display:flex;flex-direction:column;gap:10px;max-width:720px;color:var(--dsw-alias-label-primary);container-type:inline-size}
@@ -372,7 +393,7 @@ window.__ModuleLoader__.load({
372
393
  .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}
373
394
  .codexSubscriptionError{font-size:13px;line-height:20px;color:var(--dsw-alias-state-error-primary)}.codexSubscriptionInput{width:100%;box-sizing:border-box}
374
395
  .codexSubscriptionRecover{display:flex;align-items:center;justify-content:space-between;gap:12px}.codexSubscriptionRecover .codexSubscriptionError{flex:1}.codexSubscriptionRecover button{flex:0 0 auto}
375
- .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)}
396
+ .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}
376
397
  .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)}
377
398
  .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}
378
399
  .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)}
@@ -382,13 +403,14 @@ window.__ModuleLoader__.load({
382
403
  .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)}
383
404
  .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)}
384
405
  .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}
385
- .codexSubscriptionResetSummary{display:flex;align-items:center;justify-content:space-between;gap:10px}.codexSubscriptionResetBalance{display:flex;flex-direction:column;gap:8px}.codexSubscriptionResetBalance .codexSubscriptionActions{justify-content:flex-start}.codexSubscriptionResetFlow{display:flex;flex-direction:column;gap:9px;border:1px solid var(--dsw-alias-state-warning-border,var(--dsw-alias-border-l2));border-radius:10px;padding:11px 12px;background:var(--dsw-alias-bg-module-platform)}.codexSubscriptionResetFlow h4{margin:0;font-size:13px;line-height:20px;font-weight:600}.codexSubscriptionResetWarning{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}.codexSubscriptionResetCheck{display:flex;align-items:flex-start;gap:8px;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)}.codexSubscriptionResetPhrase{font:600 12px/18px ui-monospace,SFMono-Regular,Consolas,monospace}.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)}
406
+ .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)}
386
407
  .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}
387
408
  .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}
388
409
  .codexModelSelect{position:relative;min-width:0}.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}.codexModelSelectTrigger:hover:not(:disabled),.codexModelSelectTrigger[aria-expanded=true]{background:var(--dsw-alias-interactive-bg-hover)}.codexModelSelectTrigger:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-border-l3)}.codexModelSelectTrigger:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}.codexModelSelectBolt{display:block;flex:none;width:14px;height:14px;color:var(--dsw-alias-label-primary)}.codexModelSelectLabel{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.codexModelSelectEffort{flex:none;color:var(--dsw-alias-label-caption)}.codexModelSelectChevron{flex:none;color:var(--dsw-alias-label-caption);transition:transform 120ms}.codexModelSelectTrigger[aria-expanded=true] .codexModelSelectChevron{transform:rotate(180deg)}
389
410
  .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-specific-menu);box-shadow:var(--dsw-shadow-lv3);color:var(--dsw-alias-label-primary);overflow:hidden}.codexModelSelectMenu{right:0;bottom:calc(100% + 8px)}.codexModelSelectSubmenu{right:calc(100% + 8px);bottom:0;min-width:min(230px,calc(100vw - 32px))}.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}.codexModelSelectCell:hover,.codexModelSelectCell:focus-visible,.codexModelSelectCell[data-open=true]{background:var(--dsw-alias-interactive-bg-hover);outline:0}.codexModelSelectCell:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}.codexModelSelectCellLabel{flex:none;white-space:nowrap}.codexModelSelectCellValue{flex:auto;min-width:0;overflow:hidden;color:var(--dsw-alias-label-tertiary);text-align:right;text-overflow:ellipsis;white-space:nowrap}.codexModelSelectCellChevron{flex:none;color:var(--dsw-alias-label-tertiary)}.codexModelSelectGroups{min-height:0;max-height:352px;overflow-y:auto}.codexModelSelectGroup+.codexModelSelectGroup{margin-top:4px}.codexModelSelectGroupTitle{position:sticky;top:0;z-index:1;padding:5px 8px 3px;background:var(--dsw-specific-menu);color:var(--dsw-alias-label-tertiary);font-size:12px;font-weight:500;line-height:18px}.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}.codexModelSelectOption:hover:not(:disabled),.codexModelSelectOption:focus-visible{background:var(--dsw-alias-interactive-bg-hover)}.codexModelSelectOption:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}.codexModelSelectOptionCopy{display:flex;flex:1;min-width:0;flex-direction:column}.codexModelSelectOptionName{overflow:hidden;color:inherit;font-size:14px;font-weight:500;line-height:20px;text-overflow:ellipsis;white-space:nowrap}.codexModelSelectOptionDescription{overflow:hidden;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;text-overflow:ellipsis;white-space:nowrap}.codexModelSelectCheck{display:grid;place-items:center;flex:0 0 18px;color:var(--dsw-alias-label-primary)}.codexModelSelectStatus,.codexModelSelectEmpty{padding:10px;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:20px}.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}.codexModelSelectWarning{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-state-warn-label)}.codexModelSelectRetry{flex:none;padding:0;border:0;background:transparent;color:inherit;font:inherit;font-weight:600;cursor:pointer}
390
411
  .codexModelSelectMenu{overflow:visible}
391
- .codexImageTool{display:flex;flex-direction:column;gap:8px;margin:4px 0;color:var(--dsw-alias-label-primary)}.codexImageToolRow{display:flex;align-items:center;min-height:24px;gap:8px;font-size:13px;line-height:20px}.codexImageToolIcon{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;color:var(--dsw-alias-label-secondary)}.codexImageToolIcon::before{content:'';width:8px;height:8px;border:1.5px solid currentColor;border-radius:3px}.codexImageTool[data-state=running] .codexImageToolIcon::before{border-radius:50%;border-right-color:transparent;animation:codexImageSpin 800ms linear infinite}.codexImageTool[data-state=error] .codexImageToolIcon::before{border-color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-state-error-primary)}.codexImageToolTitle{font-weight:500}.codexImageToolState{color:var(--dsw-alias-label-tertiary)}.codexImageToolError{margin:0 0 0 24px;font-size:12px;line-height:18px;color:var(--dsw-alias-state-error-primary)}.codexImageToolGallery{margin-left:24px}.codexGeneratedImageFrame{display:flex;align-items:center;justify-content:center;width:min(240px,100%);min-height:120px;padding:0;overflow:hidden;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-tertiary);cursor:pointer}.codexGeneratedImageFrame img{display:block;width:100%;max-height:240px;object-fit:contain}.codexGeneratedImageRetry{min-height:36px;padding:0 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);cursor:pointer}.codexGeneratedImageLightbox{position:fixed;inset:0;z-index:1000;display:flex;align-items:center;justify-content:center;padding:32px;border:0;background:rgba(0,0,0,.72)}.codexGeneratedImageLightbox img{display:block;max-width:min(1100px,calc(100vw - 64px));max-height:calc(100vh - 64px);object-fit:contain}.codexGeneratedImageClose{position:absolute;top:16px;right:16px;width:36px;height:36px;border:1px solid rgba(255,255,255,.35);border-radius:50%;background:rgba(0,0,0,.48);color:#fff;font-size:20px;line-height:1;cursor:pointer}@keyframes codexImageSpin{to{transform:rotate(360deg)}}
412
+ .codexImageTool{display:flex;flex-direction:column;gap:8px;margin:4px 0;color:var(--dsw-alias-label-primary)}.codexImageToolRow{display:flex;align-items:center;min-height:24px;gap:8px;font-size:13px;line-height:20px}.codexImageToolIcon{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;color:var(--dsw-alias-label-secondary)}.codexImageToolIcon::before{content:'';width:8px;height:8px;border:1.5px solid currentColor;border-radius:3px}.codexImageTool[data-state=running] .codexImageToolIcon::before{border-radius:50%;border-right-color:transparent;animation:codexImageSpin 800ms linear infinite}.codexImageTool[data-state=error] .codexImageToolIcon::before{border-color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-state-error-primary)}.codexImageToolTitle{font-weight:500}.codexImageToolState{color:var(--dsw-alias-label-tertiary)}.codexImageToolError{margin:0 0 0 24px;font-size:12px;line-height:18px;color:var(--dsw-alias-state-error-primary)}.codexImageToolGallery{margin-left:24px}.codexGeneratedImageFrame{display:flex;align-items:center;justify-content:center;width:min(240px,100%);min-height:120px;padding:0;overflow:hidden;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-tertiary);cursor:pointer}.codexGeneratedImageFrame img{display:block;width:100%;max-height:240px;object-fit:contain}.codexGeneratedImageRetry{min-height:36px;padding:0 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);cursor:pointer}.codexGeneratedImageModal{width:min(920px,calc(100vw - 32px));max-height:calc(100vh - 32px)}.codexGeneratedImageModalContent{min-height:0;overflow:hidden}.codexGeneratedImageViewer{display:flex;min-width:0;flex-direction:column;gap:12px}.codexGeneratedImageStage{display:grid;place-items:center;min-height:280px;max-height:calc(100vh - 260px);overflow:auto;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-module-platform)}.codexGeneratedImageStage img{display:block;max-width:100%;max-height:calc(100vh - 280px);object-fit:contain;transform-origin:center;transition:transform 120ms ease}.codexGeneratedImageMeta{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}.codexGeneratedImageToolbar{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap}.codexGeneratedImageZoom{display:flex;align-items:center;gap:6px}.codexGeneratedImageZoomValue{min-width:44px;color:var(--dsw-alias-label-secondary);font-size:12px;text-align:center}.codexGeneratedImageDownload{display:inline-flex;align-items:center;gap:6px;min-height:32px;box-sizing:border-box;padding:0 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;background:transparent;color:var(--dsw-alias-label-primary);font-size:13px;text-decoration:none}.codexGeneratedImageDownload:hover{background:var(--dsw-alias-interactive-bg-hover)}.codexGeneratedImageGuidance{display:flex;flex-direction:column;gap:2px;padding-top:2px;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}@keyframes codexImageSpin{to{transform:rotate(360deg)}}
413
+ .codexImageBeta{padding:0 5px;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;color:var(--dsw-alias-label-tertiary);font-size:10px;line-height:16px}
392
414
  @container (max-width:560px){.codexSubscriptionCreditRows{grid-template-columns:1fr}}
393
415
  @container (max-width:480px){.codexSubscriptionAccountRow,.codexSubscriptionSectionHead{align-items:flex-start;flex-direction:column}.codexSubscriptionActions{width:100%}.codexSubscriptionSearchChoices{grid-template-columns:1fr}}
394
416
  @media(max-width:640px){.codexSubscriptionCard{padding:14px}}
@@ -413,12 +435,20 @@ window.__ModuleLoader__.load({
413
435
  const date = new Date(value);
414
436
  return Number.isFinite(date.getTime()) ? date : void 0;
415
437
  };
438
+ const imageDownloadName = (attachment) => {
439
+ const fallback = "codex-generated-image.png";
440
+ if (typeof attachment?.name !== "string") return fallback;
441
+ const cleaned = attachment.name.replace(/[<>:"/\\|?*\u0000-\u001f]/gu, "-").replace(/[. ]+$/u, "").trim();
442
+ if (cleaned === "") return fallback;
443
+ return cleaned.toLowerCase().endsWith(".png") ? cleaned : `${cleaned}.png`;
444
+ };
445
+ const imageByteSize = (bytes) => bytes < 1024 * 1024 ? `${Math.max(.1, bytes / 1024).toLocaleString(void 0, { maximumFractionDigits: 1 })} KB` : `${(bytes / 1024 / 1024).toLocaleString(void 0, { maximumFractionDigits: 1 })} MB`;
416
446
  function CodexGeneratedImage({ attachment, loadImage, t }) {
417
447
  const [attempt, setAttempt] = (0, react.useState)(0);
418
448
  const [error, setError] = (0, react.useState)(false);
419
449
  const [open, setOpen] = (0, react.useState)(false);
420
450
  const [src, setSrc] = (0, react.useState)();
421
- const closeRef = (0, react.useRef)(null);
451
+ const [zoom, setZoom] = (0, react.useState)(1);
422
452
  const triggerRef = (0, react.useRef)(null);
423
453
  (0, react.useEffect)(() => {
424
454
  let live = true;
@@ -437,25 +467,13 @@ window.__ModuleLoader__.load({
437
467
  loadImage,
438
468
  attempt
439
469
  ]);
440
- (0, react.useEffect)(() => {
441
- if (!open) return void 0;
442
- const handleKeyDown = (event) => {
443
- if (event.key === "Escape") {
444
- event.preventDefault();
445
- setOpen(false);
446
- } else if (event.key === "Tab") {
447
- event.preventDefault();
448
- closeRef.current?.focus();
449
- }
450
- };
451
- document.addEventListener("keydown", handleKeyDown);
452
- closeRef.current?.focus();
453
- return () => {
454
- document.removeEventListener("keydown", handleKeyDown);
455
- triggerRef.current?.focus();
456
- };
457
- }, [open]);
470
+ const close = () => {
471
+ setOpen(false);
472
+ window.requestAnimationFrame(() => triggerRef.current?.focus());
473
+ };
458
474
  const label = attachment.name ?? t("imageLabel");
475
+ const downloadName = imageDownloadName(attachment);
476
+ const imageMeta = `${attachment.width} × ${attachment.height} · ${imageByteSize(attachment.bytes)}`;
459
477
  if (error) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
460
478
  type: "button",
461
479
  className: "codexGeneratedImageRetry",
@@ -469,30 +487,84 @@ window.__ModuleLoader__.load({
469
487
  title: t("imageOpen"),
470
488
  "aria-label": fill(t("imageOpenNamed"), { value: label }),
471
489
  onClick: () => {
472
- if (src !== void 0) setOpen(true);
490
+ if (src !== void 0) {
491
+ setZoom(1);
492
+ setOpen(true);
493
+ }
473
494
  },
474
495
  children: src === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("imageLoading") }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
475
496
  src,
476
497
  alt: label
477
498
  })
478
- }), !open || src === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
479
- className: "codexGeneratedImageLightbox",
480
- role: "dialog",
481
- "aria-modal": "true",
482
- "aria-label": t("imagePreview"),
483
- onClick: () => setOpen(false),
484
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
485
- src,
486
- alt: label,
487
- onClick: (event) => event.stopPropagation()
488
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
489
- ref: closeRef,
490
- type: "button",
491
- className: "codexGeneratedImageClose",
492
- "aria-label": t("imageClosePreview"),
493
- onClick: () => setOpen(false),
494
- children: "×"
495
- })]
499
+ }), src === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
500
+ open,
501
+ onClose: close,
502
+ title: t("imagePreview"),
503
+ closeLabel: t("imageClosePreview"),
504
+ className: "codexGeneratedImageModal",
505
+ contentClassName: "codexGeneratedImageModalContent",
506
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
507
+ className: "codexGeneratedImageViewer",
508
+ children: [
509
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
510
+ className: "codexGeneratedImageStage",
511
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
512
+ src,
513
+ alt: label,
514
+ style: { transform: `scale(${zoom})` }
515
+ })
516
+ }),
517
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
518
+ className: "codexGeneratedImageMeta",
519
+ children: imageMeta
520
+ }),
521
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
522
+ className: "codexGeneratedImageToolbar",
523
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
524
+ className: "codexGeneratedImageZoom",
525
+ children: [
526
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
527
+ type: "button",
528
+ variant: "outline",
529
+ "aria-label": t("imageZoomOut"),
530
+ title: t("imageZoomOut"),
531
+ disabled: zoom <= .5,
532
+ onClick: () => setZoom((value) => Math.max(.5, value - .25)),
533
+ children: "−"
534
+ }),
535
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
536
+ className: "codexGeneratedImageZoomValue",
537
+ children: [Math.round(zoom * 100), "%"]
538
+ }),
539
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
540
+ type: "button",
541
+ variant: "outline",
542
+ "aria-label": t("imageZoomIn"),
543
+ title: t("imageZoomIn"),
544
+ disabled: zoom >= 3,
545
+ onClick: () => setZoom((value) => Math.min(3, value + .25)),
546
+ children: "+"
547
+ }),
548
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Button, {
549
+ type: "button",
550
+ variant: "outline",
551
+ onClick: () => setZoom(1),
552
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFullscreenOutline16, { "aria-hidden": "true" }), t("imageFit")]
553
+ })
554
+ ]
555
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("a", {
556
+ className: "codexGeneratedImageDownload",
557
+ href: src,
558
+ download: downloadName,
559
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconDownloadOutline16, { "aria-hidden": "true" }), t("imageDownload")]
560
+ })]
561
+ }),
562
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
563
+ className: "codexGeneratedImageGuidance",
564
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("imageRefineHint") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("imageNewHint") })]
565
+ })
566
+ ]
567
+ })
496
568
  })] });
497
569
  }
498
570
  function CodexImageToolRow({ block, loadImage, t }) {
@@ -517,6 +589,10 @@ window.__ModuleLoader__.load({
517
589
  className: "codexImageToolTitle",
518
590
  children: t("imageGenerate")
519
591
  }),
592
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
593
+ className: "codexImageBeta",
594
+ children: t("imageBeta")
595
+ }),
520
596
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
521
597
  className: "codexImageToolState",
522
598
  children: status
@@ -1311,18 +1387,28 @@ window.__ModuleLoader__.load({
1311
1387
  })]
1312
1388
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1313
1389
  className: "codexSubscriptionActions",
1314
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1315
- type: "button",
1316
- variant: "outline",
1317
- disabled: busy,
1318
- onClick: load,
1319
- children: t("diagnosticsLoad")
1320
- }), report === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1321
- type: "button",
1322
- variant: "outline",
1323
- onClick: copy,
1324
- children: copied ? t("diagnosticsCopied") : t("diagnosticsCopy")
1325
- })]
1390
+ children: [
1391
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1392
+ type: "button",
1393
+ variant: "outline",
1394
+ disabled: busy,
1395
+ onClick: load,
1396
+ children: t("diagnosticsLoad")
1397
+ }),
1398
+ report === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1399
+ type: "button",
1400
+ variant: "outline",
1401
+ onClick: copy,
1402
+ children: copied ? t("diagnosticsCopied") : t("diagnosticsCopy")
1403
+ }),
1404
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
1405
+ className: "codexSubscriptionLink",
1406
+ href: SUPPORT_ISSUE_URL,
1407
+ target: "_blank",
1408
+ rel: "noreferrer",
1409
+ children: t("feedbackOpen")
1410
+ })
1411
+ ]
1326
1412
  })]
1327
1413
  }),
1328
1414
  report === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", { children: JSON.stringify(report, null, 2) }),
@@ -1349,14 +1435,49 @@ window.__ModuleLoader__.load({
1349
1435
  children: fill(t("resets"), { value })
1350
1436
  });
1351
1437
  }
1352
- function ResetCreditControl({ rpc, t, count, exhausted, onConsumed }) {
1438
+ function ResetCreditExpiry({ expiresAt, t }) {
1439
+ const date = validDate(expiresAt);
1440
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1441
+ className: "codexSubscriptionResetExpiry",
1442
+ children: date === void 0 ? t("resetCreditExpiryUnknown") : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("time", {
1443
+ dateTime: date.toISOString(),
1444
+ title: date.toLocaleString(),
1445
+ children: fill(t("resetCreditExpires"), { value: date.toLocaleString() })
1446
+ })
1447
+ });
1448
+ }
1449
+ function ResetCreditControl({ rpc, t, count, nextExpiresAt, hasExhaustedQuota, onConsumed }) {
1353
1450
  const [challenge, setChallenge] = (0, react.useState)();
1354
1451
  const [resetBusy, setResetBusy] = (0, react.useState)(false);
1355
1452
  const [resetAcknowledged, setResetAcknowledged] = (0, react.useState)(false);
1356
- const [resetPhrase, setResetPhrase] = (0, react.useState)("");
1357
1453
  const [resetCountdown, setResetCountdown] = (0, react.useState)(0);
1358
1454
  const [resetError, setResetError] = (0, react.useState)();
1359
1455
  const [resetResult, setResetResult] = (0, react.useState)();
1456
+ const [inspectedExpiry, setInspectedExpiry] = (0, react.useState)(nextExpiresAt);
1457
+ const [expiryState, setExpiryState] = (0, react.useState)(nextExpiresAt === void 0 ? "loading" : "ready");
1458
+ (0, react.useEffect)(() => {
1459
+ if (nextExpiresAt !== void 0) {
1460
+ setInspectedExpiry(nextExpiresAt);
1461
+ setExpiryState("ready");
1462
+ return;
1463
+ }
1464
+ let live = true;
1465
+ setExpiryState("loading");
1466
+ rpc.call(CHANNEL, "reset-credit/inspect", {}).then(unwrap).then((value) => {
1467
+ if (!live) return;
1468
+ setInspectedExpiry(value.nextExpiresAt);
1469
+ setExpiryState("ready");
1470
+ }).catch(() => {
1471
+ if (live) setExpiryState("error");
1472
+ });
1473
+ return () => {
1474
+ live = false;
1475
+ };
1476
+ }, [
1477
+ rpc,
1478
+ count,
1479
+ nextExpiresAt
1480
+ ]);
1360
1481
  (0, react.useEffect)(() => {
1361
1482
  if (challenge === void 0) {
1362
1483
  setResetCountdown(0);
@@ -1368,24 +1489,22 @@ window.__ModuleLoader__.load({
1368
1489
  return () => window.clearInterval(timer);
1369
1490
  }, [challenge]);
1370
1491
  const prepareReset = () => {
1371
- if (resetBusy || !exhausted) return;
1492
+ if (resetBusy) return;
1372
1493
  setResetBusy(true);
1373
1494
  setResetError(void 0);
1374
1495
  setResetResult(void 0);
1375
1496
  rpc.call(CHANNEL, "reset-credit/prepare", {}).then(unwrap).then((next) => {
1376
1497
  setChallenge(next);
1377
1498
  setResetAcknowledged(false);
1378
- setResetPhrase("");
1379
1499
  }).catch((error) => setResetError(resetCreditErrorText(error, t))).finally(() => setResetBusy(false));
1380
1500
  };
1381
1501
  const cancelReset = () => {
1382
1502
  if (resetBusy) return;
1383
1503
  setChallenge(void 0);
1384
1504
  setResetAcknowledged(false);
1385
- setResetPhrase("");
1386
1505
  setResetError(void 0);
1387
1506
  };
1388
- const resetReady = challenge !== void 0 && resetAcknowledged && resetPhrase === challenge.confirmPhrase && resetCountdown === 0;
1507
+ const resetReady = challenge !== void 0 && resetAcknowledged && resetCountdown === 0;
1389
1508
  const consumeReset = () => {
1390
1509
  if (resetBusy) return;
1391
1510
  if (!resetReady) return;
@@ -1394,11 +1513,10 @@ window.__ModuleLoader__.load({
1394
1513
  setResetResult(void 0);
1395
1514
  rpc.call(CHANNEL, "reset-credit/consume", {
1396
1515
  challengeId: challenge.challengeId,
1397
- phrase: resetPhrase
1516
+ acknowledged: resetAcknowledged
1398
1517
  }).then(unwrap).then((result) => {
1399
1518
  setChallenge(void 0);
1400
1519
  setResetAcknowledged(false);
1401
- setResetPhrase("");
1402
1520
  const message = result.code === "reset" ? t("resetSuccess") : result.code === "nothing_to_reset" ? t("resetNothing") : result.code === "no_credit" ? t("resetNoCredit") : t("resetAlready");
1403
1521
  setResetResult(message);
1404
1522
  onConsumed();
@@ -1409,14 +1527,26 @@ window.__ModuleLoader__.load({
1409
1527
  children: [
1410
1528
  challenge === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1411
1529
  className: "codexSubscriptionResetSummary",
1412
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: fill(t("resetCreditsValue"), { count }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1530
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1531
+ className: "codexSubscriptionResetMeta",
1532
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: fill(t("resetCreditsValue"), { count }) }), expiryState === "loading" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1533
+ className: "codexSubscriptionResetExpiry",
1534
+ role: "status",
1535
+ children: t("resetCreditExpiryLoading")
1536
+ }) : expiryState === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1537
+ className: "codexSubscriptionResetExpiry",
1538
+ children: t("resetCreditExpiryFailed")
1539
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResetCreditExpiry, {
1540
+ expiresAt: inspectedExpiry,
1541
+ t
1542
+ })]
1543
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1413
1544
  className: "codexSubscriptionActions",
1414
1545
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1415
1546
  type: "button",
1416
1547
  variant: "outline",
1417
- disabled: !exhausted || resetBusy,
1548
+ disabled: resetBusy,
1418
1549
  "aria-busy": resetBusy,
1419
- title: !exhausted ? t("resetUnavailable") : void 0,
1420
1550
  onClick: prepareReset,
1421
1551
  children: resetBusy ? t("resetPreparing") : t("resetUse")
1422
1552
  })
@@ -1434,9 +1564,13 @@ window.__ModuleLoader__.load({
1434
1564
  className: "codexSubscriptionResetWarning",
1435
1565
  children: challenge.description
1436
1566
  }) : null,
1567
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResetCreditExpiry, {
1568
+ expiresAt: challenge.creditExpiresAt,
1569
+ t
1570
+ }),
1437
1571
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1438
1572
  className: "codexSubscriptionResetWarning",
1439
- children: t("resetWarning")
1573
+ children: t(hasExhaustedQuota ? "resetWarning" : "resetEarlyWarning")
1440
1574
  }),
1441
1575
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1442
1576
  className: "codexSubscriptionResetCheck",
@@ -1447,20 +1581,6 @@ window.__ModuleLoader__.load({
1447
1581
  onChange: (event) => setResetAcknowledged(event.target.checked)
1448
1582
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("resetAcknowledge") })]
1449
1583
  }),
1450
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1451
- className: "codexSubscriptionPreferenceCopy",
1452
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1453
- className: "codexSubscriptionCreditLabel",
1454
- children: fill(t("resetTypeHint"), { phrase: challenge.confirmPhrase })
1455
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
1456
- className: "codexSubscriptionInput codexSubscriptionResetPhrase",
1457
- value: resetPhrase,
1458
- disabled: resetBusy,
1459
- autoComplete: "off",
1460
- spellCheck: false,
1461
- onChange: (event) => setResetPhrase(event.target.value)
1462
- })]
1463
- }),
1464
1584
  resetCountdown > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1465
1585
  className: "codexSubscriptionCreditNote",
1466
1586
  role: "status",
@@ -1509,8 +1629,7 @@ window.__ModuleLoader__.load({
1509
1629
  ["This quota reset confirmation is no longer valid", "resetExpired"],
1510
1630
  ["This quota reset is already in progress", "resetInProgress"],
1511
1631
  ["Wait before confirming this quota reset", "resetTooEarly"],
1512
- ["The quota reset confirmation phrase does not match", "resetPhraseMismatch"],
1513
- ["The current Codex quota is not exhausted", "resetNotExhausted"],
1632
+ ["You must acknowledge that this may consume one quota reset", "resetAcknowledgeRequired"],
1514
1633
  ["The signed-in ChatGPT account changed", "resetAccountChanged"]
1515
1634
  ])).get(error instanceof Error ? error.message : "") ?? "resetFailed");
1516
1635
  }
@@ -1648,7 +1767,8 @@ window.__ModuleLoader__.load({
1648
1767
  rpc,
1649
1768
  t,
1650
1769
  count: visibleUsage.resetCredits.availableCount,
1651
- exhausted,
1770
+ nextExpiresAt: visibleUsage.resetCredits.nextExpiresAt,
1771
+ hasExhaustedQuota: exhausted,
1652
1772
  onConsumed: () => load(true)
1653
1773
  })]
1654
1774
  }) : null,
package/lib/index.js CHANGED
@@ -448,6 +448,7 @@ const normalizeQuickQuotaMode = (value, legacyVisible = false) => [
448
448
  const supportsCodexFastMode = (modelId) => typeof modelId === "string" && (/^gpt-5\.(?:5|6)(?:$|-)/u.test(modelId) || modelId === "gpt-5.4");
449
449
  //#endregion
450
450
  //#region src/pi-ai-runtime.js
451
+ const FAST_SERVICE_TIER = "priority";
451
452
  /**
452
453
  * Preserve pi-ai's native Codex OAuth provider while allowing DSH's generic
453
454
  * PiAiAdapter to pass the access token resolved by the host credential store.
@@ -478,15 +479,15 @@ function openaiCodexSubscriptionProvider({ resolveSpeedMode = () => void 0 } = {
478
479
  const onPayload = options.onPayload;
479
480
  return {
480
481
  ...options,
481
- serviceTier: "fast",
482
+ serviceTier: FAST_SERVICE_TIER,
482
483
  async onPayload(payload, requestModel) {
483
484
  const fastPayload = {
484
485
  ...payload,
485
- service_tier: "fast"
486
+ service_tier: FAST_SERVICE_TIER
486
487
  };
487
488
  return {
488
489
  ...await onPayload?.(fastPayload, requestModel) ?? fastPayload,
489
- service_tier: "fast"
490
+ service_tier: FAST_SERVICE_TIER
490
491
  };
491
492
  }
492
493
  };
@@ -503,7 +504,7 @@ function openaiCodexSubscriptionProvider({ resolveSpeedMode = () => void 0 } = {
503
504
  }
504
505
  //#endregion
505
506
  //#region src/version.js
506
- const PACKAGE_VERSION = "1.4.1";
507
+ const PACKAGE_VERSION = "1.6.0";
507
508
  const USER_AGENT = `dsh-codex-subscription/${PACKAGE_VERSION}`;
508
509
  //#endregion
509
510
  //#region src/codex-search.js
@@ -626,7 +627,9 @@ function createCodexSearchProvider(options) {
626
627
  //#region src/codex-images.js
627
628
  const CODEX_IMAGE_TOOL_NAME = "codex_image_generate";
628
629
  const CODEX_IMAGE_GENERATION_URL = "https://chatgpt.com/backend-api/codex/images/generations";
630
+ const CODEX_IMAGE_EDIT_URL = "https://chatgpt.com/backend-api/codex/images/edits";
629
631
  const IMAGE_MODEL = "gpt-image-2";
632
+ const MAX_REFERENCE_IMAGES = 5;
630
633
  const RESPONSE_ENVELOPE_BYTES = 1024 * 1024;
631
634
  const PNG_SIGNATURE = Buffer.from([
632
635
  137,
@@ -695,6 +698,24 @@ function imageReference(value) {
695
698
  ...value.name === void 0 ? {} : { name: value.name }
696
699
  };
697
700
  }
701
+ function referenceOf(value, attachments) {
702
+ if (!record$2(value) || typeof value.attachmentId !== "string" || value.attachmentId.length === 0 || value.attachmentId.length > 256 || !attachments.imageLimits.mediaTypes.includes(value.mediaType) || !Number.isSafeInteger(value.bytes) || value.bytes <= 0 || !Number.isSafeInteger(value.width) || value.width <= 0 || !Number.isSafeInteger(value.height) || value.height <= 0 || value.name !== void 0 && (typeof value.name !== "string" || value.name.length > 256)) throw new Error("referenceImages contains an invalid image reference");
703
+ return imageReference(value);
704
+ }
705
+ async function editImages(values, attachments, signal) {
706
+ if (!Array.isArray(values) || values.length === 0 || values.length > MAX_REFERENCE_IMAGES) throw new Error(`referenceImages must contain between 1 and ${MAX_REFERENCE_IMAGES} images`);
707
+ const references = values.map((value) => referenceOf(value, attachments));
708
+ if (new Set(references.map((value) => value.attachmentId)).size !== references.length) throw new Error("referenceImages must not contain duplicates");
709
+ const images = [];
710
+ let totalBytes = 0;
711
+ for (const reference of references) {
712
+ const stored = await attachments.readImage(reference, signal);
713
+ totalBytes += stored.data.byteLength;
714
+ if (totalBytes > attachments.imageLimits.maxMessageImageBytes) throw new Error("referenceImages exceed the DSH message image limit");
715
+ images.push({ image_url: `data:${stored.ref.mediaType};base64,${Buffer.from(stored.data).toString("base64")}` });
716
+ }
717
+ return images;
718
+ }
698
719
  function imageContent(value) {
699
720
  return [{
700
721
  type: "text",
@@ -761,12 +782,45 @@ function createCodexImageTool(options) {
761
782
  const attachments = options.attachments;
762
783
  return defineTool({
763
784
  name: CODEX_IMAGE_TOOL_NAME,
764
- description: "Generate an image from a detailed description using the signed-in Codex subscription. Use this whenever the user asks to create an image.",
765
- parameters: { prompt: {
766
- type: "string",
767
- required: true,
768
- description: "A complete, production-ready description of the image to generate."
769
- } },
785
+ description: "Create a new image or explicitly edit selected prior images using the signed-in Codex subscription. Omit referenceImages for a completely new image. Include only the exact prior image references the user asked to edit; never assume every image in the conversation is a reference.",
786
+ parameters: {
787
+ prompt: {
788
+ type: "string",
789
+ required: true,
790
+ description: "A complete, production-ready description of the image to generate."
791
+ },
792
+ referenceImages: {
793
+ type: "array",
794
+ description: "Optional explicit references to 1-5 prior images to edit. Omit for a new image.",
795
+ items: {
796
+ type: "object",
797
+ additionalProperties: false,
798
+ properties: {
799
+ attachmentId: {
800
+ type: "string",
801
+ required: true
802
+ },
803
+ mediaType: {
804
+ type: "string",
805
+ required: true
806
+ },
807
+ bytes: {
808
+ type: "integer",
809
+ required: true
810
+ },
811
+ width: {
812
+ type: "integer",
813
+ required: true
814
+ },
815
+ height: {
816
+ type: "integer",
817
+ required: true
818
+ },
819
+ name: { type: "string" }
820
+ }
821
+ }
822
+ }
823
+ },
770
824
  output: {
771
825
  schema: imageOutputSchema(),
772
826
  render: (_args, value) => imageContent(value)
@@ -783,9 +837,11 @@ function createCodexImageTool(options) {
783
837
  if (typeof access !== "string" || access.length === 0 || typeof accountId !== "string" || accountId.length === 0) throw new Error("ChatGPT subscription is not signed in");
784
838
  if (!attachments.imageLimits.mediaTypes.includes("image/png")) throw new Error("This DSH installation does not accept PNG image attachments");
785
839
  const maximumBytes = Math.min(attachments.imageLimits.maxImageBytes, attachments.imageLimits.maxMessageImageBytes);
840
+ const editing = args.referenceImages !== void 0;
841
+ const images = editing ? await editImages(args.referenceImages, attachments, exec.signal) : void 0;
786
842
  let response;
787
843
  try {
788
- response = await fetchImage(CODEX_IMAGE_GENERATION_URL, {
844
+ response = await fetchImage(editing ? CODEX_IMAGE_EDIT_URL : CODEX_IMAGE_GENERATION_URL, {
789
845
  method: "POST",
790
846
  redirect: "error",
791
847
  headers: {
@@ -798,6 +854,7 @@ function createCodexImageTool(options) {
798
854
  "user-agent": USER_AGENT
799
855
  },
800
856
  body: JSON.stringify({
857
+ ...images === void 0 ? {} : { images },
801
858
  prompt,
802
859
  background: "auto",
803
860
  model: IMAGE_MODEL,
@@ -808,12 +865,12 @@ function createCodexImageTool(options) {
808
865
  });
809
866
  } catch (error) {
810
867
  if (exec.signal.aborted) throw exec.signal.reason;
811
- throw new Error("Codex image generation request failed", { cause: error });
868
+ throw new Error(`Codex image ${editing ? "edit" : "generation"} request failed`, { cause: error });
812
869
  }
813
870
  if (!response.ok) {
814
871
  if (response.status === 401 || response.status === 403) throw new Error("ChatGPT sign-in needs to be renewed");
815
872
  if (response.status === 429) throw new Error("Codex image generation quota is unavailable");
816
- throw new Error(`Codex image generation failed (HTTP ${response.status})`);
873
+ throw new Error(`Codex image ${editing ? "edit" : "generation"} failed (HTTP ${response.status})`);
817
874
  }
818
875
  const metadata = responseMetadata(await readJsonWithin(response, encodedLimit(maximumBytes) + RESPONSE_ENVELOPE_BYTES));
819
876
  const data = decodeCodexPng(metadata.encoded, maximumBytes);
@@ -948,7 +1005,18 @@ function spendControlReachedOf(value) {
948
1005
  function resetCreditsOf(value) {
949
1006
  if (value === void 0 || value === null) return void 0;
950
1007
  if (!record$1(value) || !Number.isSafeInteger(value.available_count) || value.available_count < 0) throw new Error("Codex returned malformed reset credit details");
951
- return { availableCount: value.available_count };
1008
+ if (value.credits !== void 0 && value.credits !== null && !Array.isArray(value.credits)) throw new Error("Codex returned malformed reset credit details");
1009
+ const expirations = (value.credits ?? []).filter((credit) => record$1(credit) && credit.status?.toLowerCase?.() === "available").map((credit) => {
1010
+ if (Number.isSafeInteger(credit.expires_at) && credit.expires_at > 0) return credit.expires_at * 1e3;
1011
+ if (typeof credit.expires_at === "string" && credit.expires_at.length <= 64) {
1012
+ const parsed = Date.parse(credit.expires_at);
1013
+ if (Number.isFinite(parsed) && parsed > 0) return parsed;
1014
+ }
1015
+ }).filter((expiration) => expiration !== void 0);
1016
+ return {
1017
+ availableCount: value.available_count,
1018
+ ...expirations.length === 0 ? {} : { nextExpiresAt: Math.min(...expirations) }
1019
+ };
952
1020
  }
953
1021
  /** Reduce the provider payload to a browser-safe quota projection. */
954
1022
  function parseCodexUsage(value) {
@@ -1054,7 +1122,6 @@ function createCodexUsageReader(options) {
1054
1122
  //#region src/reset-credits.js
1055
1123
  const CODEX_RESET_CREDITS_URL = "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits";
1056
1124
  const CODEX_RESET_CONSUME_URL = `${CODEX_RESET_CREDITS_URL}/consume`;
1057
- const RESET_CONFIRM_PHRASE = "USE RESET";
1058
1125
  const DEFAULT_CONFIRM_DELAY_MS = 5e3;
1059
1126
  const DEFAULT_CHALLENGE_TTL_MS = 6e4;
1060
1127
  const DEFAULT_TIMEOUT_MS = 15e3;
@@ -1076,15 +1143,22 @@ function credentialsOf(auth, credential) {
1076
1143
  accountId
1077
1144
  };
1078
1145
  }
1146
+ function expirationOf(value) {
1147
+ if (value === void 0 || value === null) return void 0;
1148
+ if (Number.isSafeInteger(value) && value > 0) return value * 1e3;
1149
+ if (typeof value === "string" && value.length > 0 && value.length <= 64) {
1150
+ const parsed = Date.parse(value);
1151
+ if (Number.isFinite(parsed) && parsed > 0) return parsed;
1152
+ }
1153
+ throw new Error("ChatGPT returned malformed quota reset details");
1154
+ }
1079
1155
  function parseDetails(value, now) {
1080
1156
  if (!record(value) || !Number.isSafeInteger(value.available_count) || value.available_count < 0 || !Array.isArray(value.credits)) throw new Error("ChatGPT returned malformed quota reset details");
1081
1157
  if (value.available_count === 0) throw new Error("No quota reset is available");
1082
1158
  const available = value.credits.filter((credit) => record(credit) && typeof credit.id === "string" && credit.id.length > 0 && credit.id.length <= 256 && typeof credit.status === "string" && credit.status.toLowerCase() === "available").map((credit) => {
1083
- const expiresAt = credit.expires_at === void 0 || credit.expires_at === null ? void 0 : credit.expires_at * 1e3;
1084
- if (expiresAt !== void 0 && (!Number.isSafeInteger(credit.expires_at) || credit.expires_at <= 0)) throw new Error("ChatGPT returned malformed quota reset details");
1085
1159
  return {
1086
1160
  credit,
1087
- expiresAt
1161
+ expiresAt: expirationOf(credit.expires_at)
1088
1162
  };
1089
1163
  }).filter(({ expiresAt }) => expiresAt === void 0 || expiresAt > now).sort((a, b) => (a.expiresAt ?? Number.MAX_SAFE_INTEGER) - (b.expiresAt ?? Number.MAX_SAFE_INTEGER));
1090
1164
  if (available.length === 0) throw new Error("No usable quota reset is available");
@@ -1104,14 +1178,13 @@ function parseConsumeResult(value) {
1104
1178
  "already_redeemed"
1105
1179
  ].includes(value.code)) throw new Error("ChatGPT returned an unreadable quota reset response");
1106
1180
  const windowsReset = Array.isArray(value.windows_reset) ? value.windows_reset.filter((item) => typeof item === "string").slice(0, 16) : [];
1181
+ const windowsResetCount = Number.isSafeInteger(value.windows_reset) && value.windows_reset >= 0 && value.windows_reset <= 16 ? value.windows_reset : void 0;
1107
1182
  return {
1108
1183
  code: value.code,
1109
- windowsReset
1184
+ windowsReset,
1185
+ ...windowsResetCount === void 0 ? {} : { windowsResetCount }
1110
1186
  };
1111
1187
  }
1112
- function quotaIsExhausted(usage) {
1113
- return Array.isArray(usage?.rateLimits) && usage.rateLimits.some((limit) => limit?.id !== "code_review" && Array.isArray(limit?.windows) && limit.windows.some((window) => Number.isFinite(window?.usedPercent) && window.usedPercent >= 100));
1114
- }
1115
1188
  /**
1116
1189
  * Host-only reset redemption. The browser receives an opaque, short-lived
1117
1190
  * challenge; account ids, bearer tokens, credit ids, and idempotency keys stay
@@ -1129,29 +1202,42 @@ function createCodexResetCreditService(options) {
1129
1202
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
1130
1203
  const challenges = /* @__PURE__ */ new Map();
1131
1204
  const resolveCredentials = async (signal) => credentialsOf(await getAuth({ signal }), await readCredential({ signal }));
1205
+ const readDetails = async (signal) => {
1206
+ const { access, accountId } = await resolveCredentials(signal);
1207
+ const response = await fetchReset(CODEX_RESET_CREDITS_URL, {
1208
+ method: "GET",
1209
+ redirect: "error",
1210
+ headers: {
1211
+ authorization: `Bearer ${access}`,
1212
+ "chatgpt-account-id": accountId,
1213
+ accept: "application/json",
1214
+ "cache-control": "no-store",
1215
+ "user-agent": USER_AGENT
1216
+ },
1217
+ signal: requestSignal(signal, timeoutMs)
1218
+ });
1219
+ 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})`);
1220
+ let raw;
1221
+ try {
1222
+ raw = await response.json();
1223
+ } catch {
1224
+ throw new Error("ChatGPT returned unreadable quota reset details");
1225
+ }
1226
+ return {
1227
+ accountId,
1228
+ details: parseDetails(raw, now())
1229
+ };
1230
+ };
1132
1231
  return Object.freeze({
1232
+ async inspect({ signal } = {}) {
1233
+ const { details } = await readDetails(signal);
1234
+ return {
1235
+ availableCount: details.availableCount,
1236
+ ...details.creditExpiresAt === void 0 ? {} : { nextExpiresAt: details.creditExpiresAt }
1237
+ };
1238
+ },
1133
1239
  async prepare({ signal } = {}) {
1134
- const { access, accountId } = await resolveCredentials(signal);
1135
- const response = await fetchReset(CODEX_RESET_CREDITS_URL, {
1136
- method: "GET",
1137
- redirect: "error",
1138
- headers: {
1139
- authorization: `Bearer ${access}`,
1140
- "chatgpt-account-id": accountId,
1141
- accept: "application/json",
1142
- "cache-control": "no-store",
1143
- "user-agent": USER_AGENT
1144
- },
1145
- signal: requestSignal(signal, timeoutMs)
1146
- });
1147
- 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})`);
1148
- let raw;
1149
- try {
1150
- raw = await response.json();
1151
- } catch {
1152
- throw new Error("ChatGPT returned unreadable quota reset details");
1153
- }
1154
- const details = parseDetails(raw, now());
1240
+ const { accountId, details } = await readDetails(signal);
1155
1241
  const preparedAt = now();
1156
1242
  const readyAt = preparedAt + confirmDelayMs;
1157
1243
  const expiresAt = Math.min(preparedAt + challengeTtlMs, details.creditExpiresAt ?? Number.MAX_SAFE_INTEGER);
@@ -1168,14 +1254,14 @@ function createCodexResetCreditService(options) {
1168
1254
  return {
1169
1255
  challengeId,
1170
1256
  availableCount: details.availableCount,
1171
- confirmPhrase: RESET_CONFIRM_PHRASE,
1172
1257
  readyAt,
1173
1258
  expiresAt,
1259
+ ...details.creditExpiresAt === void 0 ? {} : { creditExpiresAt: details.creditExpiresAt },
1174
1260
  ...details.title === void 0 ? {} : { title: details.title },
1175
1261
  ...details.description === void 0 ? {} : { description: details.description }
1176
1262
  };
1177
1263
  },
1178
- async consume({ challengeId, phrase, signal } = {}) {
1264
+ async consume({ challengeId, acknowledged, signal } = {}) {
1179
1265
  const challenge = typeof challengeId === "string" ? challenges.get(challengeId) : void 0;
1180
1266
  if (challenge === void 0) throw new Error("This quota reset confirmation is no longer valid");
1181
1267
  if (challenge.state === "pending") throw new Error("This quota reset is already in progress");
@@ -1184,13 +1270,9 @@ function createCodexResetCreditService(options) {
1184
1270
  challenges.delete(challengeId);
1185
1271
  throw new Error("This quota reset confirmation is no longer valid");
1186
1272
  }
1187
- if (phrase !== "USE RESET") throw new Error("The quota reset confirmation phrase does not match");
1273
+ if (acknowledged !== true) throw new Error("You must acknowledge that this may consume one quota reset");
1188
1274
  challenge.state = "pending";
1189
1275
  try {
1190
- if (!quotaIsExhausted(await usageReader.read({
1191
- force: true,
1192
- signal
1193
- }))) throw new Error("The current Codex quota is not exhausted");
1194
1276
  const { access, accountId } = await resolveCredentials(signal);
1195
1277
  if (accountId !== challenge.accountId) throw new Error("The signed-in ChatGPT account changed");
1196
1278
  const response = await fetchReset(CODEX_RESET_CONSUME_URL, {
@@ -1317,13 +1399,13 @@ function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditSer
1317
1399
  const message = error instanceof Error && known.has(error.message) ? error.message : "Could not read ChatGPT usage";
1318
1400
  return publicError("internal", message);
1319
1401
  }
1320
- if (endpoint === "reset-credit/prepare" || endpoint === "reset-credit/consume") try {
1402
+ if (endpoint === "reset-credit/inspect" || endpoint === "reset-credit/prepare" || endpoint === "reset-credit/consume") try {
1321
1403
  signal.throwIfAborted();
1322
1404
  return {
1323
1405
  ok: true,
1324
- value: endpoint === "reset-credit/prepare" ? await resetCreditService.prepare({ signal }) : await resetCreditService.consume({
1406
+ value: endpoint === "reset-credit/inspect" ? await resetCreditService.inspect({ signal }) : endpoint === "reset-credit/prepare" ? await resetCreditService.prepare({ signal }) : await resetCreditService.consume({
1325
1407
  challengeId: payload?.challengeId,
1326
- phrase: payload?.phrase,
1408
+ acknowledged: payload?.acknowledged,
1327
1409
  signal
1328
1410
  })
1329
1411
  };
@@ -1338,11 +1420,10 @@ function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditSer
1338
1420
  "This quota reset confirmation is no longer valid",
1339
1421
  "This quota reset is already in progress",
1340
1422
  "Wait before confirming this quota reset",
1341
- "The quota reset confirmation phrase does not match",
1342
- "The current Codex quota is not exhausted",
1423
+ "You must acknowledge that one quota reset will be consumed",
1343
1424
  "The signed-in ChatGPT account changed"
1344
1425
  ]);
1345
- const fallback = endpoint === "reset-credit/prepare" ? "Could not prepare a quota reset" : "Could not use the quota reset";
1426
+ const fallback = endpoint === "reset-credit/inspect" ? "Could not read quota reset details" : endpoint === "reset-credit/prepare" ? "Could not prepare a quota reset" : "Could not use the quota reset";
1346
1427
  const message = error instanceof Error && known.has(error.message) ? error.message : fallback;
1347
1428
  return publicError("internal", message);
1348
1429
  }
@@ -1483,4 +1564,4 @@ function apply(ctx) {
1483
1564
  ctx.effect(() => ctx.connection.rpc.handle(CHANNEL, handler, { authority: "loopback" }), "codex-subscription: loopback account RPC");
1484
1565
  }
1485
1566
  //#endregion
1486
- export { CODEX_IMAGE_GENERATION_URL, CODEX_IMAGE_TOOL_NAME, CODEX_RESET_CONSUME_URL, CODEX_RESET_CREDITS_URL, CODEX_USAGE_URL, CodexLoginCoordinator, DshOAuthCredentialStore, RESET_CONFIRM_PHRASE, apply, assertCodexAuthUrl, commandForCodexAuthUrl, createCodexAuthService, createCodexImageTool, createCodexResetCreditService, createCodexRpcHandler, createCodexUsageReader, createSearchProviderSwitcher, createSubscriptionDiagnostics, createSubscriptionRpcHandler, decodeCodexPng, inject, name, openCodexAuthUrl, parseCodexUsage };
1567
+ export { CODEX_IMAGE_GENERATION_URL, CODEX_IMAGE_TOOL_NAME, CODEX_RESET_CONSUME_URL, CODEX_RESET_CREDITS_URL, CODEX_USAGE_URL, CodexLoginCoordinator, DshOAuthCredentialStore, apply, assertCodexAuthUrl, commandForCodexAuthUrl, createCodexAuthService, createCodexImageTool, createCodexResetCreditService, createCodexRpcHandler, createCodexUsageReader, createSearchProviderSwitcher, createSubscriptionDiagnostics, createSubscriptionRpcHandler, decodeCodexPng, inject, name, openCodexAuthUrl, parseCodexUsage };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-codex-subscription",
3
- "version": "1.4.1",
3
+ "version": "1.6.0",
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",