dsh-codex-subscription 1.5.0 → 1.7.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.5.0` package below; do not install a moving branch.
9
+ - Use the exact `1.7.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.5.0
37
+ npx -y @deepseek-ai/dsh@0.1.1-rc.2 plugin --profile web add dsh-codex-subscription@1.7.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.5.0
43
+ dsh plugin --profile web add dsh-codex-subscription@1.7.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.5.0` appears exactly once.
61
+ 1. `dsh-codex-subscription` version `1.7.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 and expiring resets, web search, image preview/download/refinement, optional composer quota, optional Fast mode, secret-free support diagnostics and direct feedback
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
 
@@ -49,8 +49,8 @@ DSH-Portable exposes the same standard plugin command, so the command above also
49
49
  | **Composer quota** | Choose a compact percentage, progress bar, or no inline quota display |
50
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** | Preview or download the original, then describe refinements in the same conversation |
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 one selected image; preview, zoom, annotate regions, download the original, and continue in the same composer |
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,13 +139,13 @@ 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
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);
148
+ - Standard or Fast mode for supported Codex models directly in the composer;
149
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
 
@@ -167,10 +167,18 @@ decides whether a window needs resetting and may return **nothing to reset** wit
167
167
  action requires an acknowledgement checkbox and five-second cooldown. Cancel never consumes a reset, rapid repeated
168
168
  clicks are single-flight, and an uncertain network result is never retried automatically.
169
169
 
170
- Generated images open in a plugin-owned preview with **Download original**. To refine one, continue describing the
171
- change in the same conversation; the durable conversation image remains available to the next model turn.
170
+ ### Image generation and editing (Beta)
172
171
 
173
- ### Composer speed (Beta)
172
+ Generated images open in a DSH-native preview with zoom, fit, dimensions, **Download original**, and numbered region notes.
173
+ **Continue editing in composer** attaches exactly the image you opened and writes the region notes into the draft without
174
+ sending it. A new image request does not silently include earlier images. 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
+ <p align="center">
178
+ <img src="https://raw.githubusercontent.com/WSL043/dsh-codex-subscription/main/docs/assets/image-preview-annotations-en.png" width="900" alt="DSH-native generated-image preview with zoom, original download, and numbered region annotations">
179
+ </p>
180
+
181
+ ### Composer speed
174
182
 
175
183
  With a supported Codex model selected, open the composer's model menu to choose Standard or Fast.
176
184
  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
 
@@ -50,8 +50,8 @@ DSH-Portable 也提供相同的标准插件命令,因此同样使用上面的
50
50
  | **输入框额度** | 可选择紧凑百分比、进度条或完全关闭输入框额度显示 |
51
51
  | **安全额度重置** | 直接查看最早到期时间,并通过冷静期和知情确认主动尝试重置 |
52
52
  | **订阅搜索** | 可在 DSH 默认搜索与 Codex 订阅搜索之间明确切换 |
53
- | **Codex 图片生成** | 预览或下载原图,并在同一会话继续描述修改内容 |
54
- | **高速模式(Beta)** | 直接在输入框切换标准或高速,无需离开当前会话 |
53
+ | **Codex 图片生成与编辑(Beta)** | 可无参考图全新生成,也可明确选择会话图片继续编辑;支持预览、缩放和下载原图 |
54
+ | **高速模式** | 直接在输入框切换标准或高速,无需离开当前会话 |
55
55
 
56
56
  这些能力共用同一份本机 ChatGPT 登录。订阅路由失败时会明确报错,不会静默切换到其他付费路由。
57
57
 
@@ -139,13 +139,13 @@ 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
146
  - 显示重置卡数量与最早到期时间,也允许在额度未完全用尽时主动尝试,并经过分层确认且不会自动重试;
147
147
  - 输入框可用百分比或进度条显示当前 Codex 模型的剩余额度(默认关闭);
148
- - 输入框可为支持的 Codex 模型切换标准或高速模式(Beta);
148
+ - 输入框可为支持的 Codex 模型切换标准或高速模式;
149
149
  - 设置页可生成并复制无敏感信息的支持诊断,并直接打开反馈入口;报告不包含 OAuth 凭据、账号标识或授权时间;
150
150
  - 订阅路由不可用时明确报错,不会静默切换到其他付费路由。
151
151
 
@@ -164,9 +164,17 @@ ChatGPT 返回可用重置卡时,设置页会用紧凑的一行显示数量和
164
164
  也可以主动尝试使用,适合重置卡即将过期的情况;是否需要重置仍由 ChatGPT 判断,服务端可能返回“当前无需重置”且不扣次数。
165
165
  最终操作需要勾选知情确认并等待 5 秒。取消不会消耗,快速连续点击只允许一次请求,网络结果不确定时也不会自动重试。
166
166
 
167
- 生成的图片可在插件自有预览中下载原图。需要继续修改时,直接在同一会话描述改动;持久化的会话图片会继续提供给下一轮模型。
167
+ ### 图片生成与编辑(Beta)
168
168
 
169
- ### 输入框速度(Beta)
169
+ 生成图片会在 DSH 原生查看器中打开,可缩放、适合窗口、查看尺寸、下载原图,并对具体部位添加编号标注。
170
+ 点击“在输入框中继续编辑”时,只会附上当前打开的这一张图,并把部位标注写入草稿,不会自动发送。
171
+ 新的图片请求不会静默带入历史图片。GPT Image 2 的耗时可能明显长于文本回复,复杂文字、精确构图和连续角色一致性也可能需要再次调整。
172
+
173
+ <p align="center">
174
+ <img src="https://raw.githubusercontent.com/WSL043/dsh-codex-subscription/main/docs/assets/image-preview-annotations.png" width="900" alt="DSH 原生图片预览中的缩放、原图下载与编号部位标注">
175
+ </p>
176
+
177
+ ### 输入框速度
170
178
 
171
179
  选择支持的 Codex 模型后,可在输入框的模型菜单中切换标准与高速。标准模式不增加图标,
172
180
  只有高速模式会在模型名称左侧显示闪电;Spark 不显示速度入口。高速模式会提高速度,也会消耗更多 Credits;具体规则见
package/lib/client.js CHANGED
@@ -28,6 +28,7 @@ window.__ModuleLoader__.load({
28
28
  //#endregion
29
29
  let react = require("react");
30
30
  react = __toESM(react, 1);
31
+ let react_dom = require("react-dom");
31
32
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
32
33
  let react_jsx_runtime = require("react/jsx-runtime");
33
34
  //#region node_modules/.pnpm/@heroicons+react@2.2.0_react@18.3.1/node_modules/@heroicons/react/16/solid/esm/BoltIcon.js
@@ -185,14 +186,16 @@ window.__ModuleLoader__.load({
185
186
  monthlyCreditLimit: "Credits 月度消费上限",
186
187
  resetCredits: "可用额度重置次数",
187
188
  resetCreditsValue: "{count} 次",
188
- resetUse: "查看并使用",
189
+ resetUse: "使用重置",
189
190
  resetPreparing: "正在读取重置详情…",
190
191
  resetConfirmTitle: "确认使用额度重置",
191
192
  resetWarning: "若 ChatGPT 执行重置,将立即消耗 1 次且无法撤销。",
192
193
  resetEarlyWarning: "当前模型额度尚未用尽;ChatGPT 可能执行重置,也可能判定暂无需重置且不消耗次数。",
193
194
  resetAcknowledge: "我知道这次操作可能立即消耗 1 次重置",
194
- resetCreditExpires: "最早到期:{value}",
195
+ resetCreditExpires: "到期:{value}",
195
196
  resetCreditExpiryUnknown: "到期时间未提供",
197
+ resetCreditExpiryLoading: "正在读取到期时间…",
198
+ resetCreditExpiryFailed: "无法读取到期时间",
196
199
  resetWait: "请再等待 {count} 秒",
197
200
  resetFinal: "消耗 1 次并重置额度",
198
201
  resetUsing: "正在重置…",
@@ -233,6 +236,7 @@ window.__ModuleLoader__.load({
233
236
  modelFailed: "模型目录加载失败:{value}",
234
237
  groupFailed: "{name}:{value}",
235
238
  imageGenerate: "生成图片",
239
+ imageBeta: "Beta",
236
240
  imageGenerating: "正在生成…",
237
241
  imageGenerated: "已生成",
238
242
  imageFailed: "生成失败",
@@ -244,7 +248,20 @@ window.__ModuleLoader__.load({
244
248
  imagePreview: "图片预览",
245
249
  imageClosePreview: "关闭预览",
246
250
  imageDownload: "下载原图",
247
- imageRefineHint: "继续在当前会话描述修改内容,即可基于这张图片继续调整。"
251
+ imageZoomOut: "缩小",
252
+ imageZoomIn: "放大",
253
+ imageFit: "适合窗口",
254
+ imageAnnotate: "标注部位",
255
+ imageAnnotateHint: "点击图片添加编号标注",
256
+ imageAnnotation: "标注 {value}",
257
+ imageAnnotationPlaceholder: "描述这个部位要修改什么",
258
+ imageEditPrompt: "描述你想怎样修改这张图",
259
+ imageEditDefault: "编辑这张图片。",
260
+ imageRegionNotes: "部位修改:",
261
+ imageEdit: "在输入框中继续编辑",
262
+ imageEditPreparing: "正在添加到输入框…",
263
+ imageEditFailed: "无法把图片添加到输入框。",
264
+ imageRemoveAnnotation: "删除标注"
248
265
  };
249
266
  const en = {
250
267
  nav: "Codex",
@@ -301,14 +318,16 @@ window.__ModuleLoader__.load({
301
318
  monthlyCreditLimit: "Monthly Credits spending cap",
302
319
  resetCredits: "Available quota resets",
303
320
  resetCreditsValue: "{count} available",
304
- resetUse: "Review and use",
321
+ resetUse: "Use reset",
305
322
  resetPreparing: "Reading reset details…",
306
323
  resetConfirmTitle: "Confirm quota reset",
307
324
  resetWarning: "If ChatGPT performs the reset, one reset is consumed immediately and cannot be restored.",
308
325
  resetEarlyWarning: "No model quota is exhausted. ChatGPT may reset it, or report that nothing needs resetting without consuming a reset.",
309
326
  resetAcknowledge: "I understand this may consume one reset now",
310
- resetCreditExpires: "Earliest expiry: {value}",
327
+ resetCreditExpires: "Expires {value}",
311
328
  resetCreditExpiryUnknown: "Expiration time not provided",
329
+ resetCreditExpiryLoading: "Reading expiration…",
330
+ resetCreditExpiryFailed: "Could not read expiration",
312
331
  resetWait: "Wait {count} more seconds",
313
332
  resetFinal: "Consume one reset",
314
333
  resetUsing: "Resetting…",
@@ -349,6 +368,7 @@ window.__ModuleLoader__.load({
349
368
  modelFailed: "Could not load models: {value}",
350
369
  groupFailed: "{name}: {value}",
351
370
  imageGenerate: "Generate image",
371
+ imageBeta: "Beta",
352
372
  imageGenerating: "Generating…",
353
373
  imageGenerated: "Generated",
354
374
  imageFailed: "Generation failed",
@@ -360,7 +380,20 @@ window.__ModuleLoader__.load({
360
380
  imagePreview: "Image preview",
361
381
  imageClosePreview: "Close preview",
362
382
  imageDownload: "Download original",
363
- imageRefineHint: "Describe the changes in this conversation to continue refining this image."
383
+ imageZoomOut: "Zoom out",
384
+ imageZoomIn: "Zoom in",
385
+ imageFit: "Fit to window",
386
+ imageAnnotate: "Annotate",
387
+ imageAnnotateHint: "Click the image to add a numbered note",
388
+ imageAnnotation: "Note {value}",
389
+ imageAnnotationPlaceholder: "Describe what should change in this area",
390
+ imageEditPrompt: "Describe how you want to change this image",
391
+ imageEditDefault: "Edit this image.",
392
+ imageRegionNotes: "Region changes:",
393
+ imageEdit: "Continue editing in composer",
394
+ imageEditPreparing: "Adding to composer…",
395
+ imageEditFailed: "Could not add the image to the composer.",
396
+ imageRemoveAnnotation: "Remove note"
364
397
  };
365
398
  const STYLE = `
366
399
  .codexSubscription{display:flex;flex-direction:column;gap:10px;max-width:720px;color:var(--dsw-alias-label-primary);container-type:inline-size}
@@ -395,10 +428,24 @@ window.__ModuleLoader__.load({
395
428
  .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)}
396
429
  .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}
397
430
  .codexModelSelectMenu{overflow:visible}
398
- .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 - 112px);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}.codexGeneratedImageActions{position:absolute;left:50%;bottom:18px;transform:translateX(-50%);display:flex;align-items:center;gap:10px;max-width:min(90vw,720px);padding:6px 8px 6px 12px;border:1px solid rgba(255,255,255,.2);border-radius:999px;background:rgba(20,20,20,.82);color:rgba(255,255,255,.72);font-size:12px;line-height:18px;backdrop-filter:blur(12px)}.codexGeneratedImageDownload{flex:0 0 auto;padding:5px 10px;border-radius:999px;background:rgba(255,255,255,.12);color:#fff;text-decoration:none;white-space:nowrap}.codexGeneratedImageDownload:hover{background:rgba(255,255,255,.2)}.codexGeneratedImageDownload:focus-visible,.codexGeneratedImageClose:focus-visible{outline:2px solid #fff;outline-offset:2px}.codexGeneratedImageRefineHint{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}@keyframes codexImageSpin{to{transform:rotate(360deg)}}
431
+ .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%);height:240px;padding:0;overflow:hidden;border:1px solid var(--dsw-alias-border-l2);border-radius:16px;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-tertiary);cursor:pointer}.codexGeneratedImageFrame img{display:block;width:100%;height:100%;object-fit:cover}.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)}}
432
+ .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}
399
433
  @container (max-width:560px){.codexSubscriptionCreditRows{grid-template-columns:1fr}}
400
434
  @container (max-width:480px){.codexSubscriptionAccountRow,.codexSubscriptionSectionHead{align-items:flex-start;flex-direction:column}.codexSubscriptionActions{width:100%}.codexSubscriptionSearchChoices{grid-template-columns:1fr}}
401
435
  @media(max-width:640px){.codexSubscriptionCard{padding:14px}}
436
+ `;
437
+ const IMAGE_STYLE = String.raw`
438
+ .codexGeneratedImageLightbox{position:fixed;inset:0;z-index:1000;display:grid;grid-template-rows:auto minmax(0,1fr) auto;background:var(--dsw-alias-bg-base,#111);color:var(--dsw-alias-label-primary);outline:0}.codexGeneratedImageTopbar{display:flex;align-items:center;justify-content:space-between;gap:20px;min-height:58px;padding:8px 14px;border-bottom:1px solid var(--dsw-alias-border-l2);background:color-mix(in srgb,var(--dsw-alias-bg-base,#111) 92%,transparent);backdrop-filter:blur(18px)}.codexGeneratedImageTopbar>div:first-child{display:flex;min-width:0;flex-direction:column}.codexGeneratedImageTopbar strong{overflow:hidden;font-size:13px;font-weight:600;line-height:20px;text-overflow:ellipsis;white-space:nowrap}.codexGeneratedImageTopbar small{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:17px}.codexGeneratedImageActions{display:flex;align-items:center;gap:6px}.codexGeneratedImageActions button,.codexGeneratedImageActions a{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;gap:5px;height:32px;padding:0 10px;border:1px solid transparent;border-radius:16px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;text-decoration:none;cursor:pointer}.codexGeneratedImageActions button:hover,.codexGeneratedImageActions a:hover,.codexGeneratedImageActions button.is-active{background:var(--dsw-alias-interactive-bg-hover)}.codexGeneratedImageActions button:focus-visible,.codexGeneratedImageActions a:focus-visible{outline:2px solid var(--dsw-alias-border-l3);outline-offset:1px}.codexGeneratedImageActions button:disabled{opacity:.4;cursor:default}.codexGeneratedImageActions>span{min-width:38px;color:var(--dsw-alias-label-tertiary);font-size:11px;text-align:center}.codexGeneratedImageClose{font-size:20px!important}.codexGeneratedImageCanvas{position:relative;display:grid;place-items:center;min-height:0;overflow:auto;padding:24px;background:var(--dsw-alias-bg-base,#111)}.codexGeneratedImageSurface{position:relative;display:inline-flex;max-width:calc(100vw - 48px);max-height:calc(100vh - 210px);transform-origin:center;transition:transform 120ms ease}.codexGeneratedImageSurface img{display:block;max-width:100%;max-height:calc(100vh - 210px);border-radius:12px;object-fit:contain}.codexGeneratedImageCanvas.is-annotating .codexGeneratedImageSurface{cursor:crosshair}.codexGeneratedImagePin{position:absolute;display:grid;place-items:center;width:24px;height:24px;padding:0;border:2px solid white;border-radius:50%;background:var(--dsw-alias-label-primary);box-shadow:0 1px 4px rgba(0,0,0,.35);color:var(--dsw-alias-bg-base,#111);font:inherit;font-size:11px;font-weight:700;transform:translate(-50%,-50%);cursor:pointer}.codexGeneratedImagePin.is-active{background:var(--dsw-alias-state-business-primary,#3964fe);color:white}.codexGeneratedImageAnnotateHint{position:absolute;bottom:14px;left:50%;padding:6px 10px;border-radius:14px;background:color-mix(in srgb,var(--dsw-alias-bg-layer-3) 90%,transparent);box-shadow:var(--dsw-shadow-lv2);color:var(--dsw-alias-label-secondary);font-size:11px;transform:translateX(-50%)}.codexGeneratedImageEditDock{display:flex;flex-direction:column;gap:8px;padding:10px 14px 14px;border-top:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-base))}.codexGeneratedImagePrompt,.codexGeneratedImageAnnotation{display:flex;align-items:center;gap:8px;width:min(820px,100%);margin:0 auto}.codexGeneratedImagePrompt>*:first-child,.codexGeneratedImageAnnotation>*:nth-child(2){min-width:0;flex:1}.codexGeneratedImagePrompt button{flex:none;white-space:nowrap}.codexGeneratedImageAnnotation>span{flex:none;color:var(--dsw-alias-label-secondary);font-size:12px}.codexGeneratedImageAnnotation>button{flex:none;height:28px;padding:0 8px;border:0;border-radius:14px;background:transparent;color:var(--dsw-alias-label-tertiary);font:inherit;font-size:11px;cursor:pointer}.codexGeneratedImageAnnotation>button:hover{background:var(--dsw-alias-interactive-bg-hover)}.codexGeneratedImageEditError{width:min(820px,100%);margin:0 auto;color:var(--dsw-alias-state-error-primary);font-size:11px;line-height:17px}@media(max-width:760px){.codexGeneratedImageTopbar{align-items:flex-start;flex-direction:column}.codexGeneratedImageActions{width:100%;overflow-x:auto}.codexGeneratedImageActions>span{display:none}.codexGeneratedImageCanvas{padding:12px}.codexGeneratedImageSurface{max-width:calc(100vw - 24px);max-height:calc(100vh - 270px)}.codexGeneratedImageSurface img{max-height:calc(100vh - 270px)}.codexGeneratedImagePrompt,.codexGeneratedImageAnnotation{align-items:stretch;flex-direction:column}.codexGeneratedImagePrompt button{width:100%}}
439
+ `;
440
+ const IMAGE_LAYOUT_STYLE = String.raw`
441
+ .codexGeneratedImageWorkspace{display:grid;min-height:0;grid-template-columns:minmax(0,1fr)}
442
+ .codexGeneratedImageWorkspace.has-comments{grid-template-columns:minmax(0,1fr) 296px}
443
+ .codexGeneratedImageComments{display:flex;min-width:0;flex-direction:column;gap:12px;overflow:auto;padding:18px 14px;border-left:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-base))}
444
+ .codexGeneratedImageComments>header{display:flex;flex-direction:column;gap:2px}.codexGeneratedImageComments>header strong{font-size:13px;font-weight:600;line-height:20px}.codexGeneratedImageComments>header small{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:17px}
445
+ .codexGeneratedImageCommentList{display:flex;flex-direction:column;gap:8px}.codexGeneratedImageCommentList article{display:grid;grid-template-columns:24px minmax(0,1fr) 24px;align-items:start;gap:7px;padding:9px;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-3)}.codexGeneratedImageCommentList article.is-active{border-color:var(--dsw-alias-state-business-primary,#3964fe)}
446
+ .codexGeneratedImageCommentList article>span{display:grid;place-items:center;width:22px;height:22px;border-radius:50%;background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-base,#111);font-size:10px;font-weight:700}.codexGeneratedImageCommentList textarea{box-sizing:border-box;width:100%;min-height:64px;resize:vertical;border:0;outline:0;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;line-height:18px}.codexGeneratedImageCommentList textarea::placeholder{color:var(--dsw-alias-label-dimmed)}
447
+ .codexGeneratedImageCommentList article>button{display:grid;place-items:center;width:22px;height:22px;padding:0;border:0;border-radius:50%;background:transparent;color:var(--dsw-alias-label-tertiary);font:inherit;font-size:17px;cursor:pointer}.codexGeneratedImageCommentList article>button:hover{background:var(--dsw-alias-interactive-bg-hover)}
448
+ @media(max-width:760px){.codexGeneratedImageWorkspace.has-comments{grid-template-columns:minmax(0,1fr);grid-template-rows:minmax(0,1fr) minmax(118px,34vh)}.codexGeneratedImageComments{border-top:1px solid var(--dsw-alias-border-l2);border-left:0;padding:10px 12px}}
402
449
  `;
403
450
  const unwrap = (response) => {
404
451
  if (!response?.ok) throw new Error(response?.error?.message ?? "Codex RPC failed");
@@ -427,14 +474,29 @@ window.__ModuleLoader__.load({
427
474
  if (cleaned === "") return fallback;
428
475
  return cleaned.toLowerCase().endsWith(".png") ? cleaned : `${cleaned}.png`;
429
476
  };
430
- function CodexGeneratedImage({ attachment, loadImage, t }) {
477
+ 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`;
478
+ function editDraft(prompt, annotations, t) {
479
+ const notes = annotations.map((annotation, index) => ({
480
+ number: index + 1,
481
+ note: annotation.note.trim()
482
+ })).filter((annotation) => annotation.note !== "").map((annotation) => String(annotation.number) + ". " + annotation.note);
483
+ const base = prompt.trim() === "" ? t("imageEditDefault") : prompt.trim();
484
+ return notes.length === 0 ? base : base + "\n\n" + t("imageRegionNotes") + "\n" + notes.join("\n");
485
+ }
486
+ function CodexGeneratedImage({ attachment, loadImage, attachForEdit, t }) {
431
487
  const [attempt, setAttempt] = (0, react.useState)(0);
432
488
  const [error, setError] = (0, react.useState)(false);
433
489
  const [open, setOpen] = (0, react.useState)(false);
434
490
  const [src, setSrc] = (0, react.useState)();
435
- const closeRef = (0, react.useRef)(null);
436
- const downloadRef = (0, react.useRef)(null);
491
+ const [zoom, setZoom] = (0, react.useState)(1);
492
+ const [annotationMode, setAnnotationMode] = (0, react.useState)(false);
493
+ const [annotations, setAnnotations] = (0, react.useState)([]);
494
+ const [selectedAnnotation, setSelectedAnnotation] = (0, react.useState)();
495
+ const [prompt, setPrompt] = (0, react.useState)("");
496
+ const [editBusy, setEditBusy] = (0, react.useState)(false);
497
+ const [editError, setEditError] = (0, react.useState)(false);
437
498
  const triggerRef = (0, react.useRef)(null);
499
+ const dialogRef = (0, react.useRef)(null);
438
500
  (0, react.useEffect)(() => {
439
501
  let live = true;
440
502
  setError(false);
@@ -452,34 +514,228 @@ window.__ModuleLoader__.load({
452
514
  loadImage,
453
515
  attempt
454
516
  ]);
517
+ const close = () => {
518
+ setOpen(false);
519
+ window.requestAnimationFrame(() => triggerRef.current?.focus());
520
+ };
455
521
  (0, react.useEffect)(() => {
456
522
  if (!open) return void 0;
457
- const handleKeyDown = (event) => {
523
+ const previousOverflow = document.body.style.overflow;
524
+ document.body.style.overflow = "hidden";
525
+ dialogRef.current?.focus();
526
+ const keydown = (event) => {
458
527
  if (event.key === "Escape") {
459
528
  event.preventDefault();
460
- setOpen(false);
461
- } else if (event.key === "Tab") {
462
- event.preventDefault();
463
- const controls = [downloadRef.current, closeRef.current].filter(Boolean);
464
- const current = controls.indexOf(document.activeElement);
465
- controls[event.shiftKey ? current <= 0 ? controls.length - 1 : current - 1 : (current + 1) % controls.length]?.focus();
529
+ close();
530
+ return;
531
+ }
532
+ if (event.key === "Tab") {
533
+ const focusable = [...dialogRef.current.querySelectorAll("button:not(:disabled),a[href],input:not(:disabled),textarea:not(:disabled),[tabindex]:not([tabindex=\"-1\"])")];
534
+ if (focusable.length === 0) return;
535
+ const first = focusable[0];
536
+ const last = focusable.at(-1);
537
+ if (event.shiftKey && document.activeElement === first) {
538
+ event.preventDefault();
539
+ last.focus();
540
+ } else if (!event.shiftKey && document.activeElement === last) {
541
+ event.preventDefault();
542
+ first.focus();
543
+ }
466
544
  }
467
545
  };
468
- document.addEventListener("keydown", handleKeyDown);
469
- closeRef.current?.focus();
546
+ document.addEventListener("keydown", keydown);
470
547
  return () => {
471
- document.removeEventListener("keydown", handleKeyDown);
472
- triggerRef.current?.focus();
548
+ document.body.style.overflow = previousOverflow;
549
+ document.removeEventListener("keydown", keydown);
473
550
  };
474
551
  }, [open]);
475
552
  const label = attachment.name ?? t("imageLabel");
476
553
  const downloadName = imageDownloadName(attachment);
554
+ const imageMeta = attachment.width + " × " + attachment.height + " · " + imageByteSize(attachment.bytes);
555
+ const addAnnotation = (event) => {
556
+ if (!annotationMode || event.target.closest(".codexGeneratedImagePin")) return;
557
+ const bounds = event.currentTarget.getBoundingClientRect();
558
+ const annotation = {
559
+ id: crypto.randomUUID(),
560
+ x: Math.max(0, Math.min(1, (event.clientX - bounds.left) / bounds.width)),
561
+ y: Math.max(0, Math.min(1, (event.clientY - bounds.top) / bounds.height)),
562
+ note: ""
563
+ };
564
+ setAnnotations((value) => [...value, annotation]);
565
+ setSelectedAnnotation(annotation.id);
566
+ };
567
+ const continueEditing = async () => {
568
+ if (editBusy || src === void 0) return;
569
+ setEditBusy(true);
570
+ setEditError(false);
571
+ try {
572
+ await attachForEdit(src, downloadName, editDraft(prompt, annotations, t));
573
+ close();
574
+ } catch {
575
+ setEditError(true);
576
+ } finally {
577
+ setEditBusy(false);
578
+ }
579
+ };
477
580
  if (error) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
478
581
  type: "button",
479
582
  className: "codexGeneratedImageRetry",
480
583
  onClick: () => setAttempt((value) => value + 1),
481
584
  children: t("imageLoadFailed")
482
585
  });
586
+ const lightbox = !open || src === void 0 ? null : (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
587
+ ref: dialogRef,
588
+ className: "codexGeneratedImageLightbox",
589
+ role: "dialog",
590
+ "aria-modal": "true",
591
+ "aria-label": t("imagePreview"),
592
+ tabIndex: -1,
593
+ children: [
594
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
595
+ className: "codexGeneratedImageTopbar",
596
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: imageMeta })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
597
+ className: "codexGeneratedImageActions",
598
+ children: [
599
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
600
+ type: "button",
601
+ className: annotationMode ? "is-active" : "",
602
+ "aria-pressed": annotationMode,
603
+ onClick: () => setAnnotationMode((value) => !value),
604
+ children: t("imageAnnotate")
605
+ }),
606
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
607
+ type: "button",
608
+ "aria-label": t("imageZoomOut"),
609
+ disabled: zoom <= .5,
610
+ onClick: () => setZoom((value) => Math.max(.5, value - .25)),
611
+ children: "−"
612
+ }),
613
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [Math.round(zoom * 100), "%"] }),
614
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
615
+ type: "button",
616
+ "aria-label": t("imageZoomIn"),
617
+ disabled: zoom >= 3,
618
+ onClick: () => setZoom((value) => Math.min(3, value + .25)),
619
+ children: "+"
620
+ }),
621
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
622
+ type: "button",
623
+ onClick: () => setZoom(1),
624
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFullscreenOutline16, { "aria-hidden": "true" }), t("imageFit")]
625
+ }),
626
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("a", {
627
+ href: src,
628
+ download: downloadName,
629
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconDownloadOutline16, { "aria-hidden": "true" }), t("imageDownload")]
630
+ }),
631
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
632
+ type: "button",
633
+ className: "codexGeneratedImageClose",
634
+ "aria-label": t("imageClosePreview"),
635
+ onClick: close,
636
+ children: "×"
637
+ })
638
+ ]
639
+ })]
640
+ }),
641
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
642
+ className: "codexGeneratedImageWorkspace " + (annotationMode || annotations.length > 0 ? "has-comments" : ""),
643
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("main", {
644
+ className: "codexGeneratedImageCanvas " + (annotationMode ? "is-annotating" : ""),
645
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
646
+ className: "codexGeneratedImageSurface",
647
+ onClick: addAnnotation,
648
+ style: { transform: "scale(" + zoom + ")" },
649
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
650
+ src,
651
+ alt: label
652
+ }), annotations.map((annotation, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
653
+ type: "button",
654
+ className: "codexGeneratedImagePin " + (annotation.id === selectedAnnotation ? "is-active" : ""),
655
+ "aria-label": fill(t("imageAnnotation"), { value: index + 1 }),
656
+ style: {
657
+ left: annotation.x * 100 + "%",
658
+ top: annotation.y * 100 + "%"
659
+ },
660
+ onClick: (event) => {
661
+ event.stopPropagation();
662
+ setSelectedAnnotation(annotation.id);
663
+ },
664
+ children: index + 1
665
+ }, annotation.id))]
666
+ }), annotationMode ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
667
+ className: "codexGeneratedImageAnnotateHint",
668
+ children: t("imageAnnotateHint")
669
+ }) : null]
670
+ }), annotationMode || annotations.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("aside", {
671
+ className: "codexGeneratedImageComments",
672
+ "aria-label": t("imageAnnotate"),
673
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t("imageAnnotate") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: t("imageAnnotateHint") })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
674
+ className: "codexGeneratedImageCommentList",
675
+ children: annotations.map((annotation, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("article", {
676
+ className: annotation.id === selectedAnnotation ? "is-active" : "",
677
+ onClick: () => setSelectedAnnotation(annotation.id),
678
+ children: [
679
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: index + 1 }),
680
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
681
+ value: annotation.note,
682
+ rows: 3,
683
+ "aria-label": fill(t("imageAnnotation"), { value: index + 1 }),
684
+ placeholder: t("imageAnnotationPlaceholder"),
685
+ onFocus: () => setSelectedAnnotation(annotation.id),
686
+ onChange: (event) => {
687
+ const note = event.target.value;
688
+ setAnnotations((value) => value.map((item) => item.id === annotation.id ? {
689
+ ...item,
690
+ note
691
+ } : item));
692
+ }
693
+ }),
694
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
695
+ type: "button",
696
+ "aria-label": t("imageRemoveAnnotation"),
697
+ onClick: (event) => {
698
+ event.stopPropagation();
699
+ setAnnotations((value) => value.filter((item) => item.id !== annotation.id));
700
+ if (selectedAnnotation === annotation.id) setSelectedAnnotation(void 0);
701
+ },
702
+ children: "×"
703
+ })
704
+ ]
705
+ }, annotation.id))
706
+ })]
707
+ }) : null]
708
+ }),
709
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("footer", {
710
+ className: "codexGeneratedImageEditDock",
711
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
712
+ className: "codexGeneratedImagePrompt",
713
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
714
+ value: prompt,
715
+ placeholder: t("imageEditPrompt"),
716
+ onChange: (event) => setPrompt(event.target.value),
717
+ onKeyDown: (event) => {
718
+ if (event.key === "Enter" && !event.shiftKey) {
719
+ event.preventDefault();
720
+ continueEditing();
721
+ }
722
+ }
723
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
724
+ type: "button",
725
+ disabled: editBusy,
726
+ onClick: () => {
727
+ continueEditing();
728
+ },
729
+ children: editBusy ? t("imageEditPreparing") : t("imageEdit")
730
+ })]
731
+ }), editError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
732
+ className: "codexGeneratedImageEditError",
733
+ role: "alert",
734
+ children: t("imageEditFailed")
735
+ }) : null]
736
+ })
737
+ ]
738
+ }), document.body);
483
739
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
484
740
  ref: triggerRef,
485
741
  type: "button",
@@ -487,50 +743,18 @@ window.__ModuleLoader__.load({
487
743
  title: t("imageOpen"),
488
744
  "aria-label": fill(t("imageOpenNamed"), { value: label }),
489
745
  onClick: () => {
490
- if (src !== void 0) setOpen(true);
746
+ if (src !== void 0) {
747
+ setZoom(1);
748
+ setOpen(true);
749
+ }
491
750
  },
492
751
  children: src === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("imageLoading") }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
493
752
  src,
494
753
  alt: label
495
754
  })
496
- }), !open || src === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
497
- className: "codexGeneratedImageLightbox",
498
- role: "dialog",
499
- "aria-modal": "true",
500
- "aria-label": t("imagePreview"),
501
- onClick: () => setOpen(false),
502
- children: [
503
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
504
- src,
505
- alt: label,
506
- onClick: (event) => event.stopPropagation()
507
- }),
508
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
509
- className: "codexGeneratedImageActions",
510
- onClick: (event) => event.stopPropagation(),
511
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
512
- className: "codexGeneratedImageRefineHint",
513
- children: t("imageRefineHint")
514
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
515
- ref: downloadRef,
516
- className: "codexGeneratedImageDownload",
517
- href: src,
518
- download: downloadName,
519
- children: t("imageDownload")
520
- })]
521
- }),
522
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
523
- ref: closeRef,
524
- type: "button",
525
- className: "codexGeneratedImageClose",
526
- "aria-label": t("imageClosePreview"),
527
- onClick: () => setOpen(false),
528
- children: "×"
529
- })
530
- ]
531
- })] });
755
+ }), lightbox] });
532
756
  }
533
- function CodexImageToolRow({ block, loadImage, t }) {
757
+ function CodexImageToolRow({ block, loadImage, attachForEdit, t }) {
534
758
  const settled = block?.kind === "tool-result";
535
759
  const image = settled ? block.content.find((item) => item?.type === "image" && item.attachment !== void 0) : void 0;
536
760
  const failed = settled && block.isError === true;
@@ -552,6 +776,10 @@ window.__ModuleLoader__.load({
552
776
  className: "codexImageToolTitle",
553
777
  children: t("imageGenerate")
554
778
  }),
779
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
780
+ className: "codexImageBeta",
781
+ children: t("imageBeta")
782
+ }),
555
783
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
556
784
  className: "codexImageToolState",
557
785
  children: status
@@ -563,6 +791,7 @@ window.__ModuleLoader__.load({
563
791
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CodexGeneratedImage, {
564
792
  attachment: image.attachment,
565
793
  loadImage,
794
+ attachForEdit,
566
795
  t
567
796
  })
568
797
  }),
@@ -1412,6 +1641,31 @@ window.__ModuleLoader__.load({
1412
1641
  const [resetCountdown, setResetCountdown] = (0, react.useState)(0);
1413
1642
  const [resetError, setResetError] = (0, react.useState)();
1414
1643
  const [resetResult, setResetResult] = (0, react.useState)();
1644
+ const [inspectedExpiry, setInspectedExpiry] = (0, react.useState)(nextExpiresAt);
1645
+ const [expiryState, setExpiryState] = (0, react.useState)(nextExpiresAt === void 0 ? "loading" : "ready");
1646
+ (0, react.useEffect)(() => {
1647
+ if (nextExpiresAt !== void 0) {
1648
+ setInspectedExpiry(nextExpiresAt);
1649
+ setExpiryState("ready");
1650
+ return;
1651
+ }
1652
+ let live = true;
1653
+ setExpiryState("loading");
1654
+ rpc.call(CHANNEL, "reset-credit/inspect", {}).then(unwrap).then((value) => {
1655
+ if (!live) return;
1656
+ setInspectedExpiry(value.nextExpiresAt);
1657
+ setExpiryState("ready");
1658
+ }).catch(() => {
1659
+ if (live) setExpiryState("error");
1660
+ });
1661
+ return () => {
1662
+ live = false;
1663
+ };
1664
+ }, [
1665
+ rpc,
1666
+ count,
1667
+ nextExpiresAt
1668
+ ]);
1415
1669
  (0, react.useEffect)(() => {
1416
1670
  if (challenge === void 0) {
1417
1671
  setResetCountdown(0);
@@ -1463,8 +1717,15 @@ window.__ModuleLoader__.load({
1463
1717
  className: "codexSubscriptionResetSummary",
1464
1718
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1465
1719
  className: "codexSubscriptionResetMeta",
1466
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: fill(t("resetCreditsValue"), { count }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResetCreditExpiry, {
1467
- expiresAt: nextExpiresAt,
1720
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: fill(t("resetCreditsValue"), { count }) }), expiryState === "loading" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1721
+ className: "codexSubscriptionResetExpiry",
1722
+ role: "status",
1723
+ children: t("resetCreditExpiryLoading")
1724
+ }) : expiryState === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1725
+ className: "codexSubscriptionResetExpiry",
1726
+ children: t("resetCreditExpiryFailed")
1727
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResetCreditExpiry, {
1728
+ expiresAt: inspectedExpiry,
1468
1729
  t
1469
1730
  })]
1470
1731
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -1795,7 +2056,7 @@ window.__ModuleLoader__.load({
1795
2056
  ctx.effect(() => {
1796
2057
  const tag = document.createElement("style");
1797
2058
  tag.dataset.plugin = "dsh-codex-subscription";
1798
- tag.textContent = STYLE;
2059
+ tag.textContent = STYLE + IMAGE_STYLE + IMAGE_LAYOUT_STYLE;
1799
2060
  document.head.append(tag);
1800
2061
  return () => tag.remove();
1801
2062
  }, "codex-subscription: style");
@@ -1862,7 +2123,22 @@ window.__ModuleLoader__.load({
1862
2123
  locale: NS,
1863
2124
  inject: (sessionId) => ({
1864
2125
  t,
1865
- loadImage: (attachment) => conversation.resolveImage(sessionId, attachment)
2126
+ loadImage: (attachment) => conversation.resolveImage(sessionId, attachment),
2127
+ attachForEdit: async (src, filename, draft) => {
2128
+ const actx = sessions.scope(sessionId);
2129
+ if (actx === void 0 || typeof conversation.createDraftImages !== "function" || conversation.input?.for === void 0) throw new Error("This DSH version does not provide the image composer bridge");
2130
+ const response = await fetch(src);
2131
+ if (!response.ok) throw new Error("Could not read generated image");
2132
+ const blob = await response.blob();
2133
+ const created = conversation.createDraftImages([new File([blob], filename, { type: blob.type || "image/png" })]);
2134
+ const input = conversation.input.for(actx);
2135
+ if (!input.addImages(created.map((item) => item.id))) {
2136
+ conversation.releaseDraftImages(created);
2137
+ throw new Error("The composer is busy");
2138
+ }
2139
+ sessions.open(sessionId);
2140
+ input.setDraft(draft);
2141
+ }
1866
2142
  })
1867
2143
  }, CodexImageToolRow));
1868
2144
  }
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.5.0";
507
+ const PACKAGE_VERSION = "1.7.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);
@@ -1145,29 +1202,42 @@ function createCodexResetCreditService(options) {
1145
1202
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
1146
1203
  const challenges = /* @__PURE__ */ new Map();
1147
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
+ };
1148
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
+ },
1149
1239
  async prepare({ signal } = {}) {
1150
- const { access, accountId } = await resolveCredentials(signal);
1151
- const response = await fetchReset(CODEX_RESET_CREDITS_URL, {
1152
- method: "GET",
1153
- redirect: "error",
1154
- headers: {
1155
- authorization: `Bearer ${access}`,
1156
- "chatgpt-account-id": accountId,
1157
- accept: "application/json",
1158
- "cache-control": "no-store",
1159
- "user-agent": USER_AGENT
1160
- },
1161
- signal: requestSignal(signal, timeoutMs)
1162
- });
1163
- 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})`);
1164
- let raw;
1165
- try {
1166
- raw = await response.json();
1167
- } catch {
1168
- throw new Error("ChatGPT returned unreadable quota reset details");
1169
- }
1170
- const details = parseDetails(raw, now());
1240
+ const { accountId, details } = await readDetails(signal);
1171
1241
  const preparedAt = now();
1172
1242
  const readyAt = preparedAt + confirmDelayMs;
1173
1243
  const expiresAt = Math.min(preparedAt + challengeTtlMs, details.creditExpiresAt ?? Number.MAX_SAFE_INTEGER);
@@ -1329,11 +1399,11 @@ function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditSer
1329
1399
  const message = error instanceof Error && known.has(error.message) ? error.message : "Could not read ChatGPT usage";
1330
1400
  return publicError("internal", message);
1331
1401
  }
1332
- 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 {
1333
1403
  signal.throwIfAborted();
1334
1404
  return {
1335
1405
  ok: true,
1336
- 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({
1337
1407
  challengeId: payload?.challengeId,
1338
1408
  acknowledged: payload?.acknowledged,
1339
1409
  signal
@@ -1353,7 +1423,7 @@ function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditSer
1353
1423
  "You must acknowledge that one quota reset will be consumed",
1354
1424
  "The signed-in ChatGPT account changed"
1355
1425
  ]);
1356
- 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";
1357
1427
  const message = error instanceof Error && known.has(error.message) ? error.message : fallback;
1358
1428
  return publicError("internal", message);
1359
1429
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-codex-subscription",
3
- "version": "1.5.0",
3
+ "version": "1.7.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",
@@ -20,8 +20,7 @@
20
20
  "compatibility.json",
21
21
  "LICENSE",
22
22
  "SECURITY.md",
23
- "THIRD_PARTY_NOTICES.md",
24
- "docs/assets/*.png"
23
+ "THIRD_PARTY_NOTICES.md"
25
24
  ],
26
25
  "dsh": {
27
26
  "bundle": {
@@ -95,7 +94,8 @@
95
94
  "@deepseek-ai/dsh-web": "0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2",
96
95
  "@deepseek-ai/schemastery": "3.18.1",
97
96
  "@earendil-works/pi-ai": "0.82.1",
98
- "react": "^18.2.0"
97
+ "react": "^18.2.0",
98
+ "react-dom": "^18.2.0"
99
99
  },
100
100
  "devDependencies": {
101
101
  "@deepseek-ai/cordis": "4.0.1",
@@ -119,6 +119,7 @@
119
119
  "@earendil-works/pi-ai": "0.82.1",
120
120
  "@heroicons/react": "2.2.0",
121
121
  "react": "18.3.1",
122
+ "react-dom": "18.3.1",
122
123
  "tsdown": "0.22.2"
123
124
  },
124
125
  "scripts": {
Binary file
Binary file
Binary file
Binary file
Binary file