dsh-codex-subscription 2.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -42,7 +42,7 @@ DSH-Portable exposes the same standard plugin command, so the command above also
42
42
  | **Subscription models** | Sign in to ChatGPT and use Codex without an OpenAI API key or Codex CLI |
43
43
  | **Recoverable and diagnosable** | Sign-in state reconciles automatically; failed reads can be retried in place, while timeouts and stale account responses cannot overwrite current state; Settings can create a support report without credentials or account identifiers |
44
44
  | **Visible quota** | Keep backend-provided standard Codex, Spark, and other limits separate |
45
- | **Composer quota** | Choose a compact percentage, progress bar, or no inline quota display |
45
+ | **Composer quota** | Choose a compact percentage, progress bar, Beta runway forecast, or no inline display |
46
46
  | **Safe quota reset** | See each reset credit separately and deliberately try one with a cooldown and acknowledgement |
47
47
  | **Subscription search** | Explicitly route search globally through DSH default search or the signed-in Codex subscription |
48
48
  | **Codex image generation and editing (Beta)** | Generate without references, or explicitly edit one selected image; preview, zoom, annotate regions, download the original, and get the original host path for new or edited images |
@@ -55,10 +55,10 @@ These capabilities reuse the same local ChatGPT sign-in. Subscription routing fa
55
55
  ## Product screen
56
56
 
57
57
  <p align="center">
58
- <img src="https://raw.githubusercontent.com/WSL043/dsh-codex-subscription/main/docs/assets/context-settings-en.png" width="820" alt="Current Codex subscription settings in DeepSeek Harness with search, model-aware context, composer quota, and support diagnostics">
58
+ <img src="https://raw.githubusercontent.com/WSL043/dsh-codex-subscription/main/docs/assets/context-settings.png" width="820" alt="2.0.0 account and preference settings: sign-in, quota display and alerts">
59
59
  </p>
60
60
 
61
- The screenshot illustrates the settings layout; available options can vary by DSH and plugin version.
61
+ 2.0.0 groups settings into Account & preferences and Advanced & diagnostics. Screenshots use the Chinese UI, with the account label hidden and scrollable content expanded for readability. [View advanced settings](docs/assets/settings-advanced-2.0.png).
62
62
 
63
63
  ## Prepare DSH
64
64
 
@@ -92,9 +92,9 @@ dsh --profile headless "Reply with only the word: ok"
92
92
  The official `npx @deepseek-ai/dsh web` command does not create a global `dsh` command. Keep the full `npx` prefix when installing the plugin:
93
93
 
94
94
  ```sh
95
- npx -y @deepseek-ai/dsh@0.1.2-rc.1 plugin --profile web add dsh-codex-subscription
96
- npx -y @deepseek-ai/dsh@0.1.2-rc.1 plugin --profile web list dsh-codex-subscription --depth 0
97
- npx -y @deepseek-ai/dsh@0.1.2-rc.1 --profile web --dump-config
95
+ npx -y @deepseek-ai/dsh@0.1.5-rc.1 plugin --profile web add dsh-codex-subscription
96
+ npx -y @deepseek-ai/dsh@0.1.5-rc.1 plugin --profile web list dsh-codex-subscription --depth 0
97
+ npx -y @deepseek-ai/dsh@0.1.5-rc.1 --profile web --dump-config
98
98
  ```
99
99
 
100
100
  </details>
@@ -142,12 +142,13 @@ When the official model catalog exposes GPT-6 Astra, Standard preserves the cata
142
142
  ### Composer quota
143
143
 
144
144
  <p align="center">
145
- <img src="https://raw.githubusercontent.com/WSL043/dsh-codex-subscription/main/docs/assets/composer-quota-en.png" width="800" alt="Codex quota inside the composer">
145
+ <img src="https://raw.githubusercontent.com/WSL043/dsh-codex-subscription/main/docs/assets/composer-quota.png" width="800" alt="Codex quota inside the composer">
146
146
  </p>
147
147
 
148
- Choose Off, Percent, Progress bar, or Beta Runway in Settings. The compact display appears only for a selected Codex model. Runway is opt-in and estimates pace only from official remaining-percentage observations. It needs at least three samples; sustained high use usually produces a range in 5–10 minutes, while low use takes longer or reports a stable state. Non-sensitive observations from the last 24 hours are kept locally so calibration can continue after restart; a quota reset, account switch, or disabling the feature starts a new calibration period.
149
- The composer shows each returned quota window with its duration. When Plus returns both five-hour and weekly limits, both are visible. Spark keeps its independent quota. Accounts with only a weekly window still show only that window; the plugin does not invent a
150
- five-hour limit, Credits, or spending caps that the service did not return.
148
+ Choose Off, Percent, Progress bar, or Beta Runway under Account & preferences. When a five-hour window exists, the composer shows that window only; the popover retains all windows. A weekly-only display omits the week label and uses compact durations such as `36% · ≈10h–12h`.
149
+
150
+ Runway uses official observations from the recent two hours, including unchanged readings. Repeated boundary crossings can refine the rate interval when the assumptions hold; insufficient evidence or changing intensity falls back to a conservative estimate. It remains Beta and is not a guarantee of working time. History is bounded and stored locally; resets, long gaps or disabling the feature restart calibration.
151
+ Spark keeps its independent quota. The plugin does not invent five-hour limits, Credits, or spending caps that the service did not return.
151
152
 
152
153
  ### Safe quota reset
153
154
 
@@ -173,6 +174,24 @@ A new image request does not silently include earlier images. GPT Image 2 can ta
173
174
 
174
175
  The screenshot above illustrates image viewing and on-image notes; available buttons can vary with the image and installed viewer version.
175
176
 
177
+ ### Sketch canvas (Beta)
178
+
179
+ ![Sketch canvas in the Chinese UI: aspect ratio, brushes, shapes, layers and zoom](https://raw.githubusercontent.com/WSL043/dsh-codex-subscription/main/docs/assets/sketch-canvas.png)
180
+
181
+ Open Sketch from the composer button or `@Sketch`, or choose Open in sketch from an enhanced image preview. Attachment intake and removal use the native DSH component.
182
+
183
+ Sketch supports local drafts, image layers, aspect ratios, brushes, lines and shapes, two erasers, undo/redo, pan/zoom and configurable shortcuts. Smoothing processes a completed stroke only after release. Up to 20 drafts stay in the current browser; attaching a sketch never sends it automatically. Its image panel manages only the current sketch, not the conversation library.
184
+
185
+ **Sketch-to-image example**: draw, click Attach, describe the desired result in the composer, then send.
186
+
187
+ | Original sketch | Actual plugin output |
188
+ | --- | --- |
189
+ | ![Mountains and cabin sketch](https://raw.githubusercontent.com/WSL043/dsh-codex-subscription/main/docs/assets/sketch-demo-source.png) | ![Watercolor mountain cabin generated from the sketch](https://raw.githubusercontent.com/WSL043/dsh-codex-subscription/main/docs/assets/sketch-demo-result.png) |
190
+
191
+ The request preserves the mountain and cabin composition while creating a warm watercolor travel illustration with green peaks, an orange roof, a meadow stream and morning light, without the blue outlines. GPT-5.6-Luna made one image-tool call requesting low quality. Luna is the conversation model; the subscription backend determines the actual image model.
192
+
193
+ Flare / Sunburst request overrides remain experimental: successful generation does not confirm which image engine or quality the subscription backend used.
194
+
176
195
  ### Composer speed
177
196
 
178
197
  With a supported Codex model selected, open the composer's model menu to choose Standard or Fast.
@@ -205,22 +224,22 @@ These operations preserve the DSH profile, other plugins, and saved sign-in.
205
224
  ### Update and verify
206
225
 
207
226
  ```sh
208
- npx -y @deepseek-ai/dsh@0.1.2-rc.1 plugin --profile web update dsh-codex-subscription
209
- npx -y @deepseek-ai/dsh@0.1.2-rc.1 plugin --profile web list dsh-codex-subscription --depth 0
210
- npx -y @deepseek-ai/dsh@0.1.2-rc.1 --profile web --dump-config
227
+ npx -y @deepseek-ai/dsh@0.1.5-rc.1 plugin --profile web update dsh-codex-subscription
228
+ npx -y @deepseek-ai/dsh@0.1.5-rc.1 plugin --profile web list dsh-codex-subscription --depth 0
229
+ npx -y @deepseek-ai/dsh@0.1.5-rc.1 --profile web --dump-config
211
230
  ```
212
231
 
213
232
  ### Uninstall
214
233
 
215
234
  ```sh
216
- npx -y @deepseek-ai/dsh@0.1.2-rc.1 plugin --profile web remove dsh-codex-subscription
235
+ npx -y @deepseek-ai/dsh@0.1.5-rc.1 plugin --profile web remove dsh-codex-subscription
217
236
  ```
218
237
 
219
238
  </details>
220
239
 
221
240
  ## Troubleshooting
222
241
 
223
- - **`dsh` is not recognized:** the official npm route does not create a global `dsh` command; use the complete `npx -y @deepseek-ai/dsh@0.1.2-rc.1 ...` command above;
242
+ - **`dsh` is not recognized:** the official npm route does not create a global `dsh` command; use the complete `npx -y @deepseek-ai/dsh@0.1.5-rc.1 ...` command above;
224
243
  - **More than one DSH exists:** run the standard command from the intended DSH environment so that product selects the corresponding profile;
225
244
  - **Setup still fails:** confirm the command is running in the intended DSH environment. Do not delete the profile or change the system PATH to force an install.
226
245
  - **Need to report a problem:** generate a **Support diagnostics** report at the bottom of Settings, then open the [bug report form](https://github.com/WSL043/dsh-codex-subscription/issues/new?template=install-problem.yml). The report includes the OS/runtime, bounded sign-in phase, and safe request-failure categories, but excludes credentials, account identifiers, proxy addresses, raw responses, and full logs. Paste it into the required diagnostics field; never attach sign-in URLs, authorization codes, or browser callback addresses.
package/README.md CHANGED
@@ -43,7 +43,7 @@ DSH-Portable 也提供相同的标准插件命令,因此同样使用上面的
43
43
  | **订阅模型直连** | 登录 ChatGPT 后直接使用 Codex,不需要 OpenAI API Key 或 Codex CLI |
44
44
  | **可恢复、可诊断** | 登录状态会自动对账;读取失败时可在原处重试,超时和旧账号响应不会覆盖当前状态;设置页可生成不含凭据和账号标识的支持报告 |
45
45
  | **额度可见** | 普通 Codex、Spark 等服务端实际返回的额度分开显示 |
46
- | **输入框额度** | 可选择紧凑百分比、进度条或完全关闭输入框额度显示 |
46
+ | **输入框额度** | 可选择紧凑百分比、进度条、Beta 续航预测或关闭显示 |
47
47
  | **安全额度重置** | 每张重置卡单独显示,并通过冷静期和知情确认主动尝试重置 |
48
48
  | **订阅搜索** | 可将全部模型的搜索明确路由到 DSH 默认搜索或已登录的 Codex 订阅 |
49
49
  | **Codex 图片生成与编辑(Beta)** | 可无参考图全新生成,也可明确选择会话图片继续编辑;支持预览、缩放、区域备注、下载原图,并为新生成或编辑的图片提供 DSH 主机上的原图路径 |
@@ -56,10 +56,10 @@ DSH-Portable 也提供相同的标准插件命令,因此同样使用上面的
56
56
  ## 实际界面
57
57
 
58
58
  <p align="center">
59
- <img src="https://raw.githubusercontent.com/WSL043/dsh-codex-subscription/main/docs/assets/context-settings.png" width="820" alt="当前 DeepSeek Harness Codex 订阅设置,包含搜索来源、模型感知上下文、输入框额度和支持诊断">
59
+ <img src="https://raw.githubusercontent.com/WSL043/dsh-codex-subscription/main/docs/assets/context-settings.png" width="820" alt="2.0.0 账号与偏好:登录、额度、显示模式与提醒">
60
60
  </p>
61
61
 
62
- 截图用于说明设置页布局;可用选项会随 DSH 与插件版本变化。
62
+ 2.0.0 设置分为“账号与偏好”和“高级与诊断”。截图已隐藏账号信息,并展开滚动内容便于阅读;[查看高级页](docs/assets/settings-advanced-2.0.png)。
63
63
 
64
64
  ## 准备 DSH
65
65
 
@@ -93,9 +93,9 @@ dsh --profile headless "只回复:ok"
93
93
  官方的 `npx @deepseek-ai/dsh web` 不会创建全局 `dsh` 命令,因此安装插件时也要保留完整的 `npx` 前缀:
94
94
 
95
95
  ```sh
96
- npx -y @deepseek-ai/dsh@0.1.2-rc.1 plugin --profile web add dsh-codex-subscription
97
- npx -y @deepseek-ai/dsh@0.1.2-rc.1 plugin --profile web list dsh-codex-subscription --depth 0
98
- npx -y @deepseek-ai/dsh@0.1.2-rc.1 --profile web --dump-config
96
+ npx -y @deepseek-ai/dsh@0.1.5-rc.1 plugin --profile web add dsh-codex-subscription
97
+ npx -y @deepseek-ai/dsh@0.1.5-rc.1 plugin --profile web list dsh-codex-subscription --depth 0
98
+ npx -y @deepseek-ai/dsh@0.1.5-rc.1 --profile web --dump-config
99
99
  ```
100
100
 
101
101
  </details>
@@ -143,10 +143,12 @@ dsh --profile web --dump-config
143
143
  ### 输入框额度
144
144
 
145
145
  <p align="center">
146
- <img src="https://raw.githubusercontent.com/WSL043/dsh-codex-subscription/main/docs/assets/composer-quota.png" width="800" alt="中文 DSH 输入框内的 Codex 剩余额度进度条">
146
+ <img src="https://raw.githubusercontent.com/WSL043/dsh-codex-subscription/main/docs/assets/composer-quota.png" width="800" alt="中文 DSH 输入框内的 Codex 剩余额度和紧凑续航预测">
147
147
  </p>
148
148
 
149
- 可在设置中选择关闭、百分比、进度条或 Beta 续航预测;紧凑额度只在选择 Codex 模型时显示。续航预测仅在用户主动选择后,根据官方剩余百分比估算当前消耗速度。它至少需要 3 个样本;持续高消耗时通常 510 分钟即可给出范围,消耗较低时会延长观察或显示稳定。最近 24 小时的无敏感信息观测会保存在本机,重启后可以继续校准;额度重置、账号切换或关闭功能会开启新的校准周期。输入框分别显示服务端返回的各额度窗口,并标明窗口时长:Plus 返回 5 小时和每周额度时,两项都会显示。
149
+ 可在“账号与偏好”选择关闭、百分比、进度条或 Beta 续航预测。存在 5 小时额度时,输入框只显示短窗口;每周额度等完整信息留在悬浮详情。仅有周额度时省略“周”字,预测采用 `36% · ≈10h12h` 这样的紧凑格式。
150
+
151
+ 续航预测根据最近两小时的官方读数估算,额度不变的采样同样参与计算。满足条件时利用连续跳格约束速度,读数不足或使用强度变化时会退回保守估计。范围不是实际可工作时间的保证,仍标记 Beta。历史在本机有限保存;重置、长时间离线或关闭预测会重新校准。
150
152
  Spark 使用独立额度。只返回每周额度的账号仍只显示每周,不会虚构 5 小时窗口、Credits 或消费上限。
151
153
 
152
154
  ### 安全使用额度重置
@@ -171,6 +173,24 @@ ChatGPT 返回可用重置卡时,设置页会把每张卡分别显示为紧凑
171
173
 
172
174
  上图展示图片查看与图上备注的基本交互;具体按钮会随图片和所安装的查看器版本变化。
173
175
 
176
+ ### 草图画板(Beta)
177
+
178
+ ![草图画板实机界面:画布比例、笔刷、形状、图层与缩放](https://raw.githubusercontent.com/WSL043/dsh-codex-subscription/main/docs/assets/sketch-canvas.png)
179
+
180
+ 点击输入框的草图按钮或输入 `@Sketch` 打开。图片预览中可选择“进入草图”;上传、粘贴和移除附件仍由 DSH 原生组件处理。
181
+
182
+ 草图支持多份本地草稿、图片图层、画布比例、三种笔刷、直线和形状、两种橡皮、撤销重做、拖动缩放与可配置快捷键。平滑仅在抬笔后处理整笔路径,不拖慢绘画光标。草稿最多 20 份,仅保存在当前浏览器;附加草图不会自动发送。左侧图片面板只管理当前草图的图片,不是会话图片库。
183
+
184
+ **草图生成图片实测**:画好后点击“附加”,在输入框说明想要的效果,再发送。
185
+
186
+ | 画板原草图 | 插件实际生成结果 |
187
+ | --- | --- |
188
+ | ![山峰与小屋草图](https://raw.githubusercontent.com/WSL043/dsh-codex-subscription/main/docs/assets/sketch-demo-source.png) | ![根据草图生成的水彩山间小屋](https://raw.githubusercontent.com/WSL043/dsh-codex-subscription/main/docs/assets/sketch-demo-result.png) |
189
+
190
+ 示例要求:保留山峰与小屋的构图,生成温暖的水彩旅行插画,青绿山峰、橙色屋顶、草地小溪与柔和晨光,不保留蓝色线条。此例通过 GPT-5.6-Luna 发起一次图片工具调用,请求低质量;Luna 是对话模型,实际出图型号由订阅后端决定。
191
+
192
+ 图片请求中的 Flare / Sunburst 型号选项仍属实验性功能。成功生成不代表订阅后端确认采用指定型号或质量;不会把请求参数当成实际返回型号。
193
+
174
194
  ### 输入框速度
175
195
 
176
196
  选择支持的 Codex 模型后,可在输入框的模型菜单中切换标准与高速。标准模式不增加图标,
@@ -203,22 +223,22 @@ dsh plugin --profile web remove dsh-codex-subscription
203
223
  ### 更新并检查
204
224
 
205
225
  ```sh
206
- npx -y @deepseek-ai/dsh@0.1.2-rc.1 plugin --profile web update dsh-codex-subscription
207
- npx -y @deepseek-ai/dsh@0.1.2-rc.1 plugin --profile web list dsh-codex-subscription --depth 0
208
- npx -y @deepseek-ai/dsh@0.1.2-rc.1 --profile web --dump-config
226
+ npx -y @deepseek-ai/dsh@0.1.5-rc.1 plugin --profile web update dsh-codex-subscription
227
+ npx -y @deepseek-ai/dsh@0.1.5-rc.1 plugin --profile web list dsh-codex-subscription --depth 0
228
+ npx -y @deepseek-ai/dsh@0.1.5-rc.1 --profile web --dump-config
209
229
  ```
210
230
 
211
231
  ### 卸载
212
232
 
213
233
  ```sh
214
- npx -y @deepseek-ai/dsh@0.1.2-rc.1 plugin --profile web remove dsh-codex-subscription
234
+ npx -y @deepseek-ai/dsh@0.1.5-rc.1 plugin --profile web remove dsh-codex-subscription
215
235
  ```
216
236
 
217
237
  </details>
218
238
 
219
239
  ## 常见问题
220
240
 
221
- - **`dsh` 无法识别**:官方 npm 方式本来就不会创建全局 `dsh` 命令,请使用上面的完整 `npx -y @deepseek-ai/dsh@0.1.2-rc.1 ...` 命令;
241
+ - **`dsh` 无法识别**:官方 npm 方式本来就不会创建全局 `dsh` 命令,请使用上面的完整 `npx -y @deepseek-ai/dsh@0.1.5-rc.1 ...` 命令;
222
242
  - **电脑上有多个 DSH**:请从目标 DSH 环境运行标准命令,由该产品自身选择对应 profile;
223
243
  - **安装仍然失败**:确认命令是在目标 DSH 环境中运行,不要删除 profile 或随意修改系统 PATH。
224
244
  - **需要提交问题**:在设置页底部生成“支持诊断”,然后打开[使用问题表单](https://github.com/WSL043/dsh-codex-subscription/issues/new?template=install-problem.yml)。报告包含系统/运行时、有限的登录阶段和安全的请求失败分类,但不含凭据、账号标识、代理地址、原始响应或完整日志;请粘贴到必填诊断栏,且不要附上登录链接、授权码或浏览器回调地址。
@@ -7,7 +7,7 @@ This project depends on software distributed under its own terms. The dependency
7
7
  | DeepSeek Harness packages | Plugin host, client slots, credential and LLM interfaces | MIT | https://github.com/deepseek-ai/deepseek-harness |
8
8
  | `@earendil-works/pi-ai` 0.82.1 | OpenAI Codex OAuth, model catalog, Responses transport, and WebSocket continuation | MIT | https://github.com/earendil-works/pi |
9
9
  | React | DSH settings component runtime | MIT | https://github.com/facebook/react |
10
- | dsh-image-viewer 0.1.0-beta.9 (3d121d3) | Adapted built-in image viewer, interaction state, and styles | MIT, Copyright (c) 2026 WSL043; full license included in LICENSE | https://github.com/WSL043/dsh-image-viewer |
10
+ | dsh-image-viewer 0.1.0-beta.9 (3d121d3), with 0.1.0 (7f3be2c) zoom and annotation improvements | Adapted built-in image viewer, interaction state, and styles | MIT, Copyright (c) 2026 WSL043; full license included in LICENSE | https://github.com/WSL043/dsh-image-viewer |
11
11
  | Heroicons | Composer speed icon | MIT | https://github.com/tailwindlabs/heroicons |
12
12
  | `https-proxy-agent` 7.0.6 | HTTPS proxy transport for Codex OAuth token requests | MIT | https://github.com/TooTallNate/proxy-agents |
13
13
  | QuotaPin for Codex | Design basis for the opt-in local quota runway forecast | MIT | https://github.com/WSL043/QuotaPin-for-Codex |
@@ -1,5 +1,11 @@
1
1
  {
2
- "latestTested": "0.1.2-rc.1",
3
- "supported": ["0.1.2-rc.1"],
4
- "previews": ["0.1.5-alpha.1"]
2
+ "latestTested": "0.1.5-rc.1",
3
+ "supported": [
4
+ "0.1.2-rc.1",
5
+ "0.1.5-rc.1"
6
+ ],
7
+ "previews": [
8
+ "0.1.5-alpha.1",
9
+ "0.1.5-alpha.2"
10
+ ]
5
11
  }
package/lib/client.js CHANGED
@@ -2311,12 +2311,17 @@ window.__ModuleLoader__.load({
2311
2311
  }
2312
2312
  //#endregion
2313
2313
  //#region src/image-composer.js
2314
- function attachImageFiles(conversation, input, files) {
2315
- const created = conversation.createDraftImages(files);
2314
+ function attachImageFiles(conversation, input, files, sessionId) {
2315
+ const modern = typeof conversation.createDrafts === "function";
2316
+ const create = modern ? () => conversation.createDrafts(sessionId, files) : () => conversation.createDraftImages(files);
2317
+ const release = modern ? (items) => conversation.releaseDraftAttachments(items) : (items) => conversation.releaseDraftImages(items);
2318
+ const add = modern ? input.addAttachments : input.addImages;
2319
+ if (typeof add !== "function" || modern && !sessionId) throw new Error("Image composer is unavailable");
2320
+ const created = create();
2316
2321
  try {
2317
- if (!input.addImages(created.map((item) => item.id))) throw new Error("The composer is busy");
2322
+ if (!add.call(input, created.map((item) => item.id))) throw new Error("The composer is busy");
2318
2323
  } catch (error) {
2319
- conversation.releaseDraftImages(created);
2324
+ release(created);
2320
2325
  throw error;
2321
2326
  }
2322
2327
  return created;
@@ -3335,10 +3340,10 @@ window.__ModuleLoader__.load({
3335
3340
  const PIN_OUTER_STROKE = "#111827";
3336
3341
  const PIN_INNER_STROKE = "#ffffff";
3337
3342
  const finiteDimension = (value) => Number.isSafeInteger(value) && value > 0;
3338
- const clamp$1 = (value, min, max) => Math.min(max, Math.max(min, value));
3343
+ const clamp$2 = (value, min, max) => Math.min(max, Math.max(min, value));
3339
3344
  const clampCenter = (value, size, edge) => {
3340
3345
  const margin = Math.min(edge, size / 2);
3341
- return clamp$1(value, margin, size - margin);
3346
+ return clamp$2(value, margin, size - margin);
3342
3347
  };
3343
3348
  function getBitmapFactory(options) {
3344
3349
  const factory = options?.createImageBitmap ?? options?.bitmapFactory ?? globalThis.createImageBitmap;
@@ -3470,6 +3475,182 @@ window.__ModuleLoader__.load({
3470
3475
  @media(max-width:760px){.dcsiv-actions{flex-wrap:wrap;justify-content:center}.dcsiv-edit-short{display:inline;white-space:normal;max-width:min(280px,calc(100vw - 80px))}.dcsiv-edit-action{height:auto;min-height:32px}.dcsiv-close-floating{top:12px;right:12px;width:40px;height:40px}.dcsiv-workspace{padding:60px 12px 68px}.dcsiv-image{max-width:calc(100vw - 24px);max-height:calc(100vh - 136px)}.dcsiv-topbar{bottom:12px;max-width:calc(100vw - 24px)}.dcsiv-button{padding:0 8px}.dcsiv-button span.dcsiv-label{display:none}.dcsiv-inline-note{width:min(260px,calc(100vw - 40px))}.dcsiv-copy-notes{display:none}}
3471
3476
  @media(prefers-reduced-motion:reduce){.dcsiv-surface{transition:none}}
3472
3477
  `;
3478
+ //#endregion
3479
+ //#region src/subscription-image-transform.js
3480
+ const clamp$1 = (value, min, max) => Math.min(max, Math.max(min, value));
3481
+ const fitted = () => ({
3482
+ zoom: 1,
3483
+ x: 0,
3484
+ y: 0
3485
+ });
3486
+ function useImageTransform(identity) {
3487
+ const [transform, setTransform] = (0, react.useState)(fitted);
3488
+ const [dragging, setDragging] = (0, react.useState)(false);
3489
+ const [pixelScale, setPixelScale] = (0, react.useState)(1);
3490
+ const [geometry, setGeometry] = (0, react.useState)({
3491
+ width: 0,
3492
+ height: 0,
3493
+ stageWidth: 0,
3494
+ stageHeight: 0
3495
+ });
3496
+ const stageRef = (0, react.useRef)(null), surfaceRef = (0, react.useRef)(null), imageRef = (0, react.useRef)(null);
3497
+ const transformRef = (0, react.useRef)(transform);
3498
+ const pointers = (0, react.useRef)(/* @__PURE__ */ new Map()), gesture = (0, react.useRef)();
3499
+ transformRef.current = transform;
3500
+ const resetGesture = (0, react.useCallback)(() => {
3501
+ pointers.current.clear();
3502
+ gesture.current = void 0;
3503
+ setDragging(false);
3504
+ }, []);
3505
+ const fit = (0, react.useCallback)(() => {
3506
+ resetGesture();
3507
+ setTransform(fitted());
3508
+ }, [resetGesture]);
3509
+ (0, react.useEffect)(() => {
3510
+ fit();
3511
+ }, [identity, fit]);
3512
+ const actualScale = () => imageRef.current?.naturalWidth / Math.max(1, surfaceRef.current?.offsetWidth || 1) || 1;
3513
+ const boundedPan = (0, react.useCallback)((zoom, x, y) => {
3514
+ const stage = stageRef.current, surface = surfaceRef.current;
3515
+ if (!stage || !surface || zoom <= 1) return {
3516
+ x: 0,
3517
+ y: 0
3518
+ };
3519
+ const limitX = Math.max(0, (surface.offsetWidth * zoom - stage.clientWidth) / 2);
3520
+ const limitY = Math.max(0, (surface.offsetHeight * zoom - stage.clientHeight) / 2);
3521
+ return {
3522
+ x: clamp$1(x, -limitX, limitX),
3523
+ y: clamp$1(y, -limitY, limitY)
3524
+ };
3525
+ }, []);
3526
+ const setZoomAt = (0, react.useCallback)((zoom, clientX, clientY) => {
3527
+ const stage = stageRef.current;
3528
+ if (!stage) return;
3529
+ setTransform((current) => {
3530
+ const next = clamp$1(zoom, .5, Math.max(8, actualScale()));
3531
+ const box = stage.getBoundingClientRect();
3532
+ const px = clientX - box.left - box.width / 2, py = clientY - box.top - box.height / 2;
3533
+ const ratio = next / current.zoom;
3534
+ return {
3535
+ zoom: next,
3536
+ ...boundedPan(next, px - (px - current.x) * ratio, py - (py - current.y) * ratio)
3537
+ };
3538
+ });
3539
+ }, [boundedPan]);
3540
+ const actual = (0, react.useCallback)(() => {
3541
+ if (!imageRef.current?.naturalWidth) return;
3542
+ resetGesture();
3543
+ setTransform({
3544
+ zoom: actualScale(),
3545
+ x: 0,
3546
+ y: 0
3547
+ });
3548
+ }, [resetGesture]);
3549
+ const measure = (0, react.useCallback)(() => {
3550
+ const next = {
3551
+ width: surfaceRef.current?.offsetWidth ?? 0,
3552
+ height: surfaceRef.current?.offsetHeight ?? 0,
3553
+ stageWidth: stageRef.current?.clientWidth ?? 0,
3554
+ stageHeight: stageRef.current?.clientHeight ?? 0
3555
+ };
3556
+ setGeometry((current) => Object.keys(next).every((key) => current[key] === next[key]) ? current : next);
3557
+ setPixelScale(1 / actualScale());
3558
+ setTransform((current) => ({
3559
+ ...current,
3560
+ ...boundedPan(current.zoom, current.x, current.y)
3561
+ }));
3562
+ }, [boundedPan]);
3563
+ (0, react.useEffect)(() => {
3564
+ if (!stageRef.current || !surfaceRef.current) return;
3565
+ const observer = new ResizeObserver(measure);
3566
+ observer.observe(stageRef.current);
3567
+ observer.observe(surfaceRef.current);
3568
+ measure();
3569
+ return () => observer.disconnect();
3570
+ }, [identity, measure]);
3571
+ (0, react.useEffect)(() => {
3572
+ const stage = stageRef.current;
3573
+ if (!stage) return;
3574
+ const wheel = (event) => {
3575
+ event.preventDefault();
3576
+ const delta = event.deltaY * (event.deltaMode === 1 ? 16 : event.deltaMode === 2 ? stage.clientHeight : 1);
3577
+ setZoomAt(transformRef.current.zoom * Math.exp(-delta * .0015), event.clientX, event.clientY);
3578
+ };
3579
+ stage.addEventListener("wheel", wheel, { passive: false });
3580
+ return () => stage.removeEventListener("wheel", wheel);
3581
+ }, [identity, setZoomAt]);
3582
+ const onPointerDown = (event) => {
3583
+ if (event.button !== 0 || event.target.closest("button,a,input,textarea,select")) return;
3584
+ const current = transformRef.current;
3585
+ if (event.pointerType === "mouse" && current.zoom <= 1) return;
3586
+ event.currentTarget.setPointerCapture(event.pointerId);
3587
+ pointers.current.set(event.pointerId, {
3588
+ x: event.clientX,
3589
+ y: event.clientY
3590
+ });
3591
+ if (pointers.current.size === 2) {
3592
+ const [a, b] = [...pointers.current.values()];
3593
+ gesture.current = {
3594
+ kind: "pinch",
3595
+ distance: Math.hypot(a.x - b.x, a.y - b.y),
3596
+ transform: current
3597
+ };
3598
+ } else gesture.current = {
3599
+ kind: "pan",
3600
+ x: event.clientX,
3601
+ y: event.clientY,
3602
+ transform: current
3603
+ };
3604
+ setDragging(true);
3605
+ };
3606
+ const onPointerMove = (event) => {
3607
+ if (!pointers.current.has(event.pointerId)) return;
3608
+ pointers.current.set(event.pointerId, {
3609
+ x: event.clientX,
3610
+ y: event.clientY
3611
+ });
3612
+ const start = gesture.current;
3613
+ if (start?.kind === "pinch" && pointers.current.size >= 2) {
3614
+ const [a, b] = [...pointers.current.values()];
3615
+ setZoomAt(start.transform.zoom * Math.hypot(a.x - b.x, a.y - b.y) / Math.max(1, start.distance), (a.x + b.x) / 2, (a.y + b.y) / 2);
3616
+ } else if (start?.kind === "pan") setTransform({
3617
+ zoom: start.transform.zoom,
3618
+ ...boundedPan(start.transform.zoom, start.transform.x + event.clientX - start.x, start.transform.y + event.clientY - start.y)
3619
+ });
3620
+ };
3621
+ const endPointer = (event) => {
3622
+ pointers.current.delete(event.pointerId);
3623
+ const remaining = [...pointers.current.values()][0];
3624
+ if (remaining) gesture.current = {
3625
+ kind: "pan",
3626
+ ...remaining,
3627
+ transform: transformRef.current
3628
+ };
3629
+ else resetGesture();
3630
+ };
3631
+ return {
3632
+ transform,
3633
+ transformRef,
3634
+ dragging,
3635
+ pixelScale,
3636
+ geometry,
3637
+ stageRef,
3638
+ surfaceRef,
3639
+ imageRef,
3640
+ fit,
3641
+ actual,
3642
+ measure,
3643
+ setZoomAt,
3644
+ resetGesture,
3645
+ pointerHandlers: {
3646
+ onPointerDown,
3647
+ onPointerMove,
3648
+ onPointerUp: endPointer,
3649
+ onPointerCancel: endPointer,
3650
+ onLostPointerCapture: endPointer
3651
+ }
3652
+ };
3653
+ }
3473
3654
  //#endregion
3474
3655
  //#region src/subscription-image-viewer.jsx
3475
3656
  const fill$1 = (value, variables) => Object.entries(variables).reduce((text, [key, replacement]) => text.replaceAll(`{${key}}`, String(replacement)), value);
@@ -3482,8 +3663,15 @@ window.__ModuleLoader__.load({
3482
3663
  const noteText = (annotations, t) => annotations.map((annotation, index) => {
3483
3664
  return `${fill$1(t("imageAnnotation"), { value: index + 1 })} (${Math.round(annotation.x * 100)}%, ${Math.round(annotation.y * 100)}%): ${annotation.note.trim()}`;
3484
3665
  }).filter((line) => !line.endsWith(": ")).join("\n");
3485
- function ViewerAction({ action, annotations, item, service, t }) {
3666
+ function ViewerAction({ action, annotations, item, service, revision, t }) {
3486
3667
  const [state, setState] = (0, react.useState)("idle");
3668
+ const active = (0, react.useRef)(true);
3669
+ (0, react.useEffect)(() => {
3670
+ active.current = true;
3671
+ return () => {
3672
+ active.current = false;
3673
+ };
3674
+ }, []);
3487
3675
  const invoke = async () => {
3488
3676
  if (state === "pending") return;
3489
3677
  setState("pending");
@@ -3493,10 +3681,11 @@ window.__ModuleLoader__.load({
3493
3681
  item,
3494
3682
  src: item.src
3495
3683
  });
3684
+ if (!active.current || service.getSnapshot()?.revision !== revision) return;
3496
3685
  setState("idle");
3497
3686
  if (action.closeOnSuccess) service.close();
3498
3687
  } catch {
3499
- setState("failed");
3688
+ if (active.current) setState("failed");
3500
3689
  }
3501
3690
  };
3502
3691
  const label = state === "pending" ? action.pendingLabel : state === "failed" ? action.errorLabel : action.label;
@@ -3571,13 +3760,15 @@ window.__ModuleLoader__.load({
3571
3760
  }
3572
3761
  function SubscriptionImageViewerOverlay({ service, t }) {
3573
3762
  const request = (0, react.useSyncExternalStore)(service.subscribe, service.getSnapshot);
3574
- const [index, setIndex] = (0, react.useState)(0);
3575
- const [transform, setTransform] = (0, react.useState)({
3576
- zoom: 1,
3577
- x: 0,
3578
- y: 0
3763
+ const [cursor, setCursor] = (0, react.useState)({
3764
+ revision: 0,
3765
+ index: 0
3579
3766
  });
3580
- const [dragging, setDragging] = (0, react.useState)(false);
3767
+ const index = cursor.revision === request?.revision ? cursor.index : request?.index ?? 0;
3768
+ const setIndex = (update) => setCursor((current) => ({
3769
+ revision: request.revision,
3770
+ index: typeof update === "function" ? update(current.revision === request.revision ? current.index : request.index) : update
3771
+ }));
3581
3772
  const [annotating, setAnnotating] = (0, react.useState)(false);
3582
3773
  const [annotationsByImage, setAnnotationsByImage] = (0, react.useState)(service.getAnnotationsSnapshot);
3583
3774
  const annotationsByImageRef = (0, react.useRef)(annotationsByImage);
@@ -3585,28 +3776,15 @@ window.__ModuleLoader__.load({
3585
3776
  const [focusNote, setFocusNote] = (0, react.useState)();
3586
3777
  const [copied, setCopied] = (0, react.useState)(false);
3587
3778
  const rootRef = (0, react.useRef)(null);
3588
- const stageRef = (0, react.useRef)(null);
3589
- const surfaceRef = (0, react.useRef)(null);
3590
- const imageRef = (0, react.useRef)(null);
3591
- const pointersRef = (0, react.useRef)(/* @__PURE__ */ new Map());
3592
- const gestureRef = (0, react.useRef)();
3593
- const transformRef = (0, react.useRef)(transform);
3594
- transformRef.current = transform;
3595
3779
  annotationsByImageRef.current = annotationsByImage;
3596
3780
  (0, react.useEffect)(() => {
3597
3781
  if (request === void 0) return;
3598
- setIndex(request.index);
3599
- setTransform({
3600
- zoom: 1,
3601
- x: 0,
3602
- y: 0
3603
- });
3604
- setDragging(false);
3605
3782
  setAnnotating(false);
3606
3783
  setSelected(void 0);
3607
3784
  setCopied(false);
3608
3785
  }, [request?.revision]);
3609
3786
  const item = request?.items[index];
3787
+ const { transform, transformRef, dragging, pixelScale, geometry, stageRef, surfaceRef, imageRef, fit, actual, measure, setZoomAt, resetGesture, pointerHandlers } = useImageTransform(`${request?.revision}:${item?.id}:${item?.src}`);
3610
3788
  const annotations = item === void 0 ? [] : annotationsByImage[item.id] ?? [];
3611
3789
  const setAnnotations = (0, react.useCallback)((update) => {
3612
3790
  if (item === void 0) return;
@@ -3620,53 +3798,6 @@ window.__ModuleLoader__.load({
3620
3798
  service.setAnnotations(item.id, next);
3621
3799
  setAnnotationsByImage(snapshot);
3622
3800
  }, [item?.id, service]);
3623
- const boundedPan = (0, react.useCallback)((zoom, x, y) => {
3624
- const stage = stageRef.current;
3625
- const surface = surfaceRef.current;
3626
- if (stage === null || surface === null || zoom <= 1) return {
3627
- x: 0,
3628
- y: 0
3629
- };
3630
- const limitX = Math.max(0, (surface.offsetWidth * zoom - stage.clientWidth) / 2) + 28;
3631
- const limitY = Math.max(0, (surface.offsetHeight * zoom - stage.clientHeight) / 2) + 28;
3632
- return {
3633
- x: clamp(x, -limitX, limitX),
3634
- y: clamp(y, -limitY, limitY)
3635
- };
3636
- }, []);
3637
- const setZoomAt = (0, react.useCallback)((nextZoom, clientX, clientY) => {
3638
- const stage = stageRef.current;
3639
- if (stage === null) return;
3640
- setTransform((current) => {
3641
- const next = clamp(nextZoom, .5, 8);
3642
- const box = stage.getBoundingClientRect();
3643
- const px = clientX - box.left - box.width / 2;
3644
- const py = clientY - box.top - box.height / 2;
3645
- const ratio = next / current.zoom;
3646
- return {
3647
- zoom: next,
3648
- ...boundedPan(next, px - (px - current.x) * ratio, py - (py - current.y) * ratio)
3649
- };
3650
- });
3651
- }, [boundedPan]);
3652
- const fit = (0, react.useCallback)(() => {
3653
- setTransform({
3654
- zoom: 1,
3655
- x: 0,
3656
- y: 0
3657
- });
3658
- }, []);
3659
- const actual = (0, react.useCallback)(() => {
3660
- const image = imageRef.current;
3661
- const surface = surfaceRef.current;
3662
- if (image === null || surface === null || image.naturalWidth === 0) return;
3663
- const zoom = clamp(image.naturalWidth / Math.max(1, surface.offsetWidth), 1, 8);
3664
- setTransform({
3665
- zoom,
3666
- x: 0,
3667
- y: 0
3668
- });
3669
- }, []);
3670
3801
  (0, react.useEffect)(() => {
3671
3802
  if (request === void 0) return void 0;
3672
3803
  const previousOverflow = document.body.style.overflow;
@@ -3734,77 +3865,13 @@ window.__ModuleLoader__.load({
3734
3865
  annotations.length
3735
3866
  ]);
3736
3867
  (0, react.useEffect)(() => {
3737
- setTransform({
3738
- zoom: 1,
3739
- x: 0,
3740
- y: 0
3741
- });
3742
- setDragging(false);
3743
3868
  setAnnotating(false);
3744
3869
  setSelected(void 0);
3745
- }, [item?.id]);
3746
- const onWheel = (0, react.useCallback)((event) => {
3747
- event.preventDefault();
3748
- setZoomAt(transformRef.current.zoom * Math.exp(-event.deltaY * .0015), event.clientX, event.clientY);
3749
- }, [setZoomAt]);
3750
- (0, react.useEffect)(() => {
3751
- const stage = stageRef.current;
3752
- if (stage === null || request === void 0) return void 0;
3753
- stage.addEventListener("wheel", onWheel, { passive: false });
3754
- return () => stage.removeEventListener("wheel", onWheel);
3755
- }, [onWheel, request]);
3756
- const onPointerDown = (event) => {
3757
- const target = event.target;
3758
- if (event.button !== 0 || annotating || target instanceof Element && target.closest("button,textarea,input,a,select,[contenteditable=true]") !== null) return;
3759
- pointersRef.current.set(event.pointerId, {
3760
- x: event.clientX,
3761
- y: event.clientY
3762
- });
3763
- if (pointersRef.current.size === 2) {
3764
- event.currentTarget.setPointerCapture(event.pointerId);
3765
- const [a, b] = [...pointersRef.current.values()];
3766
- gestureRef.current = {
3767
- kind: "pinch",
3768
- distance: Math.hypot(a.x - b.x, a.y - b.y),
3769
- transform
3770
- };
3771
- } else if (!annotating && transform.zoom > 1) {
3772
- event.currentTarget.setPointerCapture(event.pointerId);
3773
- gestureRef.current = {
3774
- kind: "pan",
3775
- x: event.clientX,
3776
- y: event.clientY,
3777
- transform
3778
- };
3779
- setDragging(true);
3780
- }
3781
- };
3782
- const onPointerMove = (event) => {
3783
- if (!pointersRef.current.has(event.pointerId)) return;
3784
- pointersRef.current.set(event.pointerId, {
3785
- x: event.clientX,
3786
- y: event.clientY
3787
- });
3788
- const gesture = gestureRef.current;
3789
- if (gesture?.kind === "pinch" && pointersRef.current.size >= 2) {
3790
- const [a, b] = [...pointersRef.current.values()];
3791
- const distance = Math.max(1, Math.hypot(a.x - b.x, a.y - b.y));
3792
- setZoomAt(gesture.transform.zoom * distance / Math.max(1, gesture.distance), (a.x + b.x) / 2, (a.y + b.y) / 2);
3793
- } else if (gesture?.kind === "pan") {
3794
- const pan = boundedPan(gesture.transform.zoom, gesture.transform.x + event.clientX - gesture.x, gesture.transform.y + event.clientY - gesture.y);
3795
- setTransform({
3796
- zoom: gesture.transform.zoom,
3797
- ...pan
3798
- });
3799
- }
3800
- };
3801
- const endPointer = (event) => {
3802
- pointersRef.current.delete(event.pointerId);
3803
- if (pointersRef.current.size === 0) {
3804
- gestureRef.current = void 0;
3805
- setDragging(false);
3806
- }
3807
- };
3870
+ }, [
3871
+ item?.id,
3872
+ item?.src,
3873
+ request?.revision
3874
+ ]);
3808
3875
  const addAnnotation = (event) => {
3809
3876
  if (!annotating || event.target.closest(".dcsiv-annotation")) return;
3810
3877
  const bounds = surfaceRef.current?.getBoundingClientRect();
@@ -3836,6 +3903,15 @@ window.__ModuleLoader__.load({
3836
3903
  if (request === void 0 || item === void 0) return null;
3837
3904
  const meta = [item.width && item.height ? `${item.width} × ${item.height}` : void 0, bytesLabel(item.bytes)].filter(Boolean).join(" · ");
3838
3905
  const showCounter = request.items.length > 1;
3906
+ const annotationPosition = (annotation) => {
3907
+ const ratio = globalThis.devicePixelRatio || 1;
3908
+ const align = (value) => Math.round(value * ratio) / ratio;
3909
+ return {
3910
+ left: align(geometry.stageWidth / 2 + transform.x + (annotation.x - .5) * geometry.width * transform.zoom - 12),
3911
+ top: align(geometry.stageHeight / 2 + transform.y + (annotation.y - .5) * geometry.height * transform.zoom - 12),
3912
+ visibility: geometry.width > 0 ? "visible" : "hidden"
3913
+ };
3914
+ };
3839
3915
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3840
3916
  ref: rootRef,
3841
3917
  className: "dcsiv-root",
@@ -3861,7 +3937,10 @@ window.__ModuleLoader__.load({
3861
3937
  "data-active": annotating,
3862
3938
  "aria-label": annotating ? t("imageAnnotateCancel") : t("imageAnnotate"),
3863
3939
  "aria-pressed": annotating,
3864
- onClick: () => setAnnotating((value) => !value),
3940
+ onClick: () => {
3941
+ resetGesture();
3942
+ setAnnotating((value) => !value);
3943
+ },
3865
3944
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconEditOutline16, {}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3866
3945
  className: "dcsiv-label",
3867
3946
  children: annotating ? t("imageAnnotateCancel") : t("imageAnnotate")
@@ -3903,7 +3982,7 @@ window.__ModuleLoader__.load({
3903
3982
  }),
3904
3983
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3905
3984
  className: "dcsiv-zoom",
3906
- children: [Math.round(transform.zoom * 100), "%"]
3985
+ children: [Math.round(transform.zoom * pixelScale * 100), "%"]
3907
3986
  }),
3908
3987
  item.download === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("a", {
3909
3988
  className: "dcsiv-download",
@@ -3923,8 +4002,9 @@ window.__ModuleLoader__.load({
3923
4002
  annotations,
3924
4003
  item,
3925
4004
  service,
4005
+ revision: request.revision,
3926
4006
  t
3927
- }, action.id))
4007
+ }, `${request.revision}:${index}:${action.id}`))
3928
4008
  ]
3929
4009
  })
3930
4010
  }),
@@ -3945,34 +4025,33 @@ window.__ModuleLoader__.load({
3945
4025
  onClick: (event) => {
3946
4026
  if (event.target === event.currentTarget && !annotating && transform.zoom === 1) service.close();
3947
4027
  },
3948
- onPointerDown,
3949
- onPointerMove,
3950
- onPointerUp: endPointer,
3951
- onPointerCancel: endPointer,
4028
+ ...!annotating ? pointerHandlers : {},
3952
4029
  onDoubleClick: () => {
3953
4030
  if (transform.zoom === 1) actual();
3954
4031
  else fit();
3955
4032
  },
3956
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3957
- ref: surfaceRef,
3958
- className: "dcsiv-surface",
3959
- onClick: addAnnotation,
3960
- style: { transform: `translate3d(${transform.x}px,${transform.y}px,0) scale(${transform.zoom})` },
3961
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
3962
- ref: imageRef,
3963
- className: "dcsiv-image",
3964
- src: item.src,
3965
- alt: item.name,
3966
- draggable: "false"
3967
- }), annotations.map((annotation, position) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4033
+ children: [
4034
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4035
+ ref: surfaceRef,
4036
+ className: "dcsiv-surface",
4037
+ onClick: addAnnotation,
4038
+ style: { transform: `translate3d(${transform.x}px,${transform.y}px,0) scale(${transform.zoom})` },
4039
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
4040
+ ref: imageRef,
4041
+ className: "dcsiv-image",
4042
+ src: item.src,
4043
+ alt: item.name,
4044
+ draggable: "false",
4045
+ onLoad: () => {
4046
+ measure();
4047
+ }
4048
+ }, `${request.revision}:${item.id}`)
4049
+ }),
4050
+ annotations.map((annotation, position) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3968
4051
  className: "dcsiv-annotation",
3969
4052
  "data-x": annotation.x < .38 ? "right" : annotation.x > .62 ? "left" : "center",
3970
4053
  "data-y": annotation.y < .28 ? "down" : "up",
3971
- style: {
3972
- left: `${annotation.x * 100}%`,
3973
- top: `${annotation.y * 100}%`,
3974
- transform: `translate(-50%,-50%) scale(${1 / transform.zoom})`
3975
- },
4054
+ style: annotationPosition(annotation),
3976
4055
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3977
4056
  type: "button",
3978
4057
  className: "dcsiv-pin",
@@ -4028,43 +4107,44 @@ window.__ModuleLoader__.load({
4028
4107
  })
4029
4108
  ]
4030
4109
  }) : null]
4031
- }, annotation.id))]
4032
- }), showCounter ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
4033
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4034
- type: "button",
4035
- className: "dcsiv-button dcsiv-icon-only dcsiv-nav dcsiv-prev",
4036
- "aria-label": t("imagePrevious"),
4037
- onClick: (event) => {
4038
- event.stopPropagation();
4039
- setIndex((value) => (value - 1 + request.items.length) % request.items.length);
4040
- },
4041
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronLeftOutline14, {})
4042
- }),
4043
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4044
- type: "button",
4045
- className: "dcsiv-button dcsiv-icon-only dcsiv-nav dcsiv-next",
4046
- "aria-label": t("imageNext"),
4047
- onClick: (event) => {
4048
- event.stopPropagation();
4049
- setIndex((value) => (value + 1) % request.items.length);
4050
- },
4051
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutline14, {})
4052
- }),
4053
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4054
- className: "dcsiv-counter",
4055
- children: [
4056
- index + 1,
4057
- " / ",
4058
- request.items.length
4059
- ]
4060
- })
4061
- ] }) : annotating ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4062
- className: "dcsiv-hint",
4063
- children: t("imageAnnotateHint")
4064
- }) : transform.zoom === 1 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4065
- className: "dcsiv-hint",
4066
- children: t("imageZoomHint")
4067
- }) : null]
4110
+ }, annotation.id)),
4111
+ showCounter ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
4112
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4113
+ type: "button",
4114
+ className: "dcsiv-button dcsiv-icon-only dcsiv-nav dcsiv-prev",
4115
+ "aria-label": t("imagePrevious"),
4116
+ onClick: (event) => {
4117
+ event.stopPropagation();
4118
+ setIndex((value) => (value - 1 + request.items.length) % request.items.length);
4119
+ },
4120
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronLeftOutline14, {})
4121
+ }),
4122
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4123
+ type: "button",
4124
+ className: "dcsiv-button dcsiv-icon-only dcsiv-nav dcsiv-next",
4125
+ "aria-label": t("imageNext"),
4126
+ onClick: (event) => {
4127
+ event.stopPropagation();
4128
+ setIndex((value) => (value + 1) % request.items.length);
4129
+ },
4130
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutline14, {})
4131
+ }),
4132
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4133
+ className: "dcsiv-counter",
4134
+ children: [
4135
+ index + 1,
4136
+ " / ",
4137
+ request.items.length
4138
+ ]
4139
+ })
4140
+ ] }) : annotating ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4141
+ className: "dcsiv-hint",
4142
+ children: t("imageAnnotateHint")
4143
+ }) : transform.zoom === 1 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4144
+ className: "dcsiv-hint",
4145
+ children: t("imageZoomHint")
4146
+ }) : null
4147
+ ]
4068
4148
  }), annotations.some((annotation) => annotation.note.trim() !== "") ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4069
4149
  type: "button",
4070
4150
  className: "dcsiv-copy-notes",
@@ -4100,7 +4180,7 @@ window.__ModuleLoader__.load({
4100
4180
  const items = (Array.isArray(request?.items) ? request.items : []).flatMap((item, position) => {
4101
4181
  if (typeof item?.src !== "string" || item.src === "") return [];
4102
4182
  return [{
4103
- id: typeof item.id === "string" && item.id !== "" ? item.id : `image-${position + 1}`,
4183
+ id: typeof item.id === "string" && item.id !== "" ? item.id : item.src,
4104
4184
  src: item.src,
4105
4185
  name: typeof item.name === "string" && item.name !== "" ? item.name : `Image ${position + 1}`,
4106
4186
  width: boundedNumber(item.width, void 0),
@@ -4849,7 +4929,7 @@ window.__ModuleLoader__.load({
4849
4929
  }
4850
4930
  //#endregion
4851
4931
  //#region src/version.js
4852
- const PACKAGE_VERSION = "2.0.0";
4932
+ const PACKAGE_VERSION = "2.0.1";
4853
4933
  //#endregion
4854
4934
  //#region src/client-recovery.js
4855
4935
  async function recoveryCall(rpc, endpoint, payload = {}, timeoutMs = 1e4) {
@@ -7193,7 +7273,7 @@ window.__ModuleLoader__.load({
7193
7273
  })), "codex-subscription: Image trigger"));
7194
7274
  const sessionInput = (sessionId) => {
7195
7275
  const actx = sessions.scope(sessionId);
7196
- if (!actx || typeof conversation.createDraftImages !== "function" || !conversation.input?.for) throw new Error("Image composer is unavailable");
7276
+ if (!actx || !conversation.input?.for) throw new Error("Image composer is unavailable");
7197
7277
  return conversation.input.for(actx);
7198
7278
  };
7199
7279
  const openSketchImage = (sessionId) => async (src, name) => {
@@ -7209,7 +7289,7 @@ window.__ModuleLoader__.load({
7209
7289
  const attachForEdit = (sessionId) => async (src, filename, draft, annotations = [], referenceName, sourceInDraft = false) => {
7210
7290
  if (!preference.getSnapshot().imageEditing) throw new Error("Image editing is disabled");
7211
7291
  const actx = sessions.scope(sessionId);
7212
- 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");
7292
+ if (actx === void 0 || conversation.input?.for === void 0) throw new Error("This DSH version does not provide the image composer bridge");
7213
7293
  const response = await fetch(src);
7214
7294
  if (!response.ok) throw new Error("Could not read generated image");
7215
7295
  const blob = await response.blob();
@@ -7220,7 +7300,7 @@ window.__ModuleLoader__.load({
7220
7300
  }
7221
7301
  const input = conversation.input.for(actx);
7222
7302
  if (!preference.getSnapshot().imageEditing) throw new Error("Image editing is disabled");
7223
- if (files.length) attachImageFiles(conversation, input, files);
7303
+ if (files.length) attachImageFiles(conversation, input, files, sessionId);
7224
7304
  sessions.open(sessionId);
7225
7305
  if (sourceInDraft && !annotations.length) return;
7226
7306
  if (!input.state.getSnapshot().draft.trim()) input.setDraft(draft);
@@ -7279,7 +7359,7 @@ window.__ModuleLoader__.load({
7279
7359
  attachSketch: (blob) => {
7280
7360
  const current = preference.getSnapshot();
7281
7361
  if (!current.imageSketch || !current.imageEditing) throw new Error("Sketch editing is disabled");
7282
- attachImageFiles(conversation, sessionInput(sessionId), [new File([blob], "sketch-reference.png", { type: "image/png" })]);
7362
+ attachImageFiles(conversation, sessionInput(sessionId), [new File([blob], "sketch-reference.png", { type: "image/png" })], sessionId);
7283
7363
  }
7284
7364
  })
7285
7365
  }, ImageWorkspace));
package/lib/index.js CHANGED
@@ -1615,7 +1615,7 @@ function openaiCodexSubscriptionProvider({ resolveSpeedMode = () => void 0, reso
1615
1615
  }
1616
1616
  //#endregion
1617
1617
  //#region src/version.js
1618
- const PACKAGE_VERSION = "2.0.0";
1618
+ const PACKAGE_VERSION = "2.0.1";
1619
1619
  const USER_AGENT = `dsh-codex-subscription/${PACKAGE_VERSION}`;
1620
1620
  //#endregion
1621
1621
  //#region src/model-catalog.js
@@ -3882,6 +3882,7 @@ function apply(ctx) {
3882
3882
  displayName: "ChatGPT subscription",
3883
3883
  piProvider: provider,
3884
3884
  configuredMaxTokens: /* @__PURE__ */ new Map(),
3885
+ modelErrors: /* @__PURE__ */ new Map(),
3885
3886
  streamIdleTimeoutMs: 600 * 1e3,
3886
3887
  maxRequestImageBytes: MAX_REQUEST_IMAGE_BYTES,
3887
3888
  requestImagePixelBudget: REQUEST_IMAGE_PIXEL_BUDGET,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-codex-subscription",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "Use ChatGPT and Codex subscriptions in DeepSeek Harness with OAuth, quota, safe resets, web search, images, and Fast mode",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -15,7 +15,6 @@
15
15
  "cordis.patch.yml",
16
16
  "README.md",
17
17
  "README.en.md",
18
- "README.zh-CN.md",
19
18
  "DIRECTORY.md",
20
19
  "compatibility.json",
21
20
  "LICENSE",
@@ -75,21 +74,21 @@
75
74
  },
76
75
  "peerDependencies": {
77
76
  "@deepseek-ai/cordis": "4.0.1 || 4.0.2",
78
- "@deepseek-ai/dsh-api-remotes": "0.1.2-rc.1 || 0.1.5-alpha.1",
79
- "@deepseek-ai/dsh-client-connection": "0.1.2-rc.1 || 0.1.5-alpha.1",
80
- "@deepseek-ai/dsh-client-locale": "0.1.2-rc.1 || 0.1.5-alpha.1",
81
- "@deepseek-ai/dsh-client-ui-conversation": "0.1.2-rc.1 || 0.1.5-alpha.1",
82
- "@deepseek-ai/dsh-client-ui-model-selection": "0.1.2-rc.1 || 0.1.5-alpha.1",
83
- "@deepseek-ai/dsh-client-ui-primitives": "0.1.2-rc.1 || 0.1.5-alpha.1",
84
- "@deepseek-ai/dsh-client-ui-settings": "0.1.2-rc.1 || 0.1.5-alpha.1",
85
- "@deepseek-ai/dsh-client-ui-slots": "0.1.2-rc.1 || 0.1.5-alpha.1",
86
- "@deepseek-ai/dsh-client-ui-tool": "0.1.2-rc.1 || 0.1.5-alpha.1",
87
- "@deepseek-ai/dsh-credentials": "0.1.2-rc.1 || 0.1.5-alpha.1",
88
- "@deepseek-ai/dsh-llm": "0.1.2-rc.1 || 0.1.5-alpha.1",
89
- "@deepseek-ai/dsh-llm-pi-ai": "0.1.2-rc.1 || 0.1.5-alpha.1",
90
- "@deepseek-ai/dsh-settings": "0.1.2-rc.1 || 0.1.5-alpha.1",
91
- "@deepseek-ai/dsh-tools": "0.1.2-rc.1 || 0.1.5-alpha.1",
92
- "@deepseek-ai/dsh-web": "0.1.2-rc.1 || 0.1.5-alpha.1",
77
+ "@deepseek-ai/dsh-api-remotes": "0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-alpha.1 || 0.1.5-alpha.2",
78
+ "@deepseek-ai/dsh-client-connection": "0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-alpha.1 || 0.1.5-alpha.2",
79
+ "@deepseek-ai/dsh-client-locale": "0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-alpha.1 || 0.1.5-alpha.2",
80
+ "@deepseek-ai/dsh-client-ui-conversation": "0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-alpha.1 || 0.1.5-alpha.2",
81
+ "@deepseek-ai/dsh-client-ui-model-selection": "0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-alpha.1 || 0.1.5-alpha.2",
82
+ "@deepseek-ai/dsh-client-ui-primitives": "0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-alpha.1 || 0.1.5-alpha.2",
83
+ "@deepseek-ai/dsh-client-ui-settings": "0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-alpha.1 || 0.1.5-alpha.2",
84
+ "@deepseek-ai/dsh-client-ui-slots": "0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-alpha.1 || 0.1.5-alpha.2",
85
+ "@deepseek-ai/dsh-client-ui-tool": "0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-alpha.1 || 0.1.5-alpha.2",
86
+ "@deepseek-ai/dsh-credentials": "0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-alpha.1 || 0.1.5-alpha.2",
87
+ "@deepseek-ai/dsh-llm": "0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-alpha.1 || 0.1.5-alpha.2",
88
+ "@deepseek-ai/dsh-llm-pi-ai": "0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-alpha.1 || 0.1.5-alpha.2",
89
+ "@deepseek-ai/dsh-settings": "0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-alpha.1 || 0.1.5-alpha.2",
90
+ "@deepseek-ai/dsh-tools": "0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-alpha.1 || 0.1.5-alpha.2",
91
+ "@deepseek-ai/dsh-web": "0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-alpha.1 || 0.1.5-alpha.2",
93
92
  "@deepseek-ai/schemastery": "3.18.1 || ^3.18.2",
94
93
  "@earendil-works/pi-ai": "0.82.1",
95
94
  "react": "^18.2.0",
@@ -97,23 +96,23 @@
97
96
  },
98
97
  "devDependencies": {
99
98
  "@deepseek-ai/cordis": "4.0.2",
100
- "@deepseek-ai/dsh-api-remotes": "0.1.2-rc.1",
101
- "@deepseek-ai/dsh-client-connection": "0.1.2-rc.1",
102
- "@deepseek-ai/dsh-client-locale": "0.1.2-rc.1",
103
- "@deepseek-ai/dsh-client-ui-conversation": "0.1.2-rc.1",
104
- "@deepseek-ai/dsh-client-ui-model-selection": "0.1.2-rc.1",
105
- "@deepseek-ai/dsh-client-ui-primitives": "0.1.2-rc.1",
106
- "@deepseek-ai/dsh-client-ui-settings": "0.1.2-rc.1",
107
- "@deepseek-ai/dsh-client-ui-slots": "0.1.2-rc.1",
108
- "@deepseek-ai/dsh-client-ui-tool": "0.1.2-rc.1",
109
- "@deepseek-ai/dsh-credentials": "0.1.2-rc.1",
110
- "@deepseek-ai/dsh-invariants": "0.1.2-rc.1",
111
- "@deepseek-ai/dsh-llm": "0.1.2-rc.1",
112
- "@deepseek-ai/dsh-llm-pi-ai": "0.1.2-rc.1",
113
- "@deepseek-ai/dsh-settings": "0.1.2-rc.1",
114
- "@deepseek-ai/dsh-session": "0.1.2-rc.1",
115
- "@deepseek-ai/dsh-tools": "0.1.2-rc.1",
116
- "@deepseek-ai/dsh-web": "0.1.2-rc.1",
99
+ "@deepseek-ai/dsh-api-remotes": "0.1.5-rc.1",
100
+ "@deepseek-ai/dsh-client-connection": "0.1.5-rc.1",
101
+ "@deepseek-ai/dsh-client-locale": "0.1.5-rc.1",
102
+ "@deepseek-ai/dsh-client-ui-conversation": "0.1.5-rc.1",
103
+ "@deepseek-ai/dsh-client-ui-model-selection": "0.1.5-rc.1",
104
+ "@deepseek-ai/dsh-client-ui-primitives": "0.1.5-rc.1",
105
+ "@deepseek-ai/dsh-client-ui-settings": "0.1.5-rc.1",
106
+ "@deepseek-ai/dsh-client-ui-slots": "0.1.5-rc.1",
107
+ "@deepseek-ai/dsh-client-ui-tool": "0.1.5-rc.1",
108
+ "@deepseek-ai/dsh-credentials": "0.1.5-rc.1",
109
+ "@deepseek-ai/dsh-invariants": "0.1.5-rc.1",
110
+ "@deepseek-ai/dsh-llm": "0.1.5-rc.1",
111
+ "@deepseek-ai/dsh-llm-pi-ai": "0.1.5-rc.1",
112
+ "@deepseek-ai/dsh-settings": "0.1.5-rc.1",
113
+ "@deepseek-ai/dsh-session": "0.1.5-rc.1",
114
+ "@deepseek-ai/dsh-tools": "0.1.5-rc.1",
115
+ "@deepseek-ai/dsh-web": "0.1.5-rc.1",
117
116
  "@deepseek-ai/schemastery": "3.18.2",
118
117
  "@earendil-works/pi-ai": "0.82.1",
119
118
  "@heroicons/react": "2.2.0",
package/README.zh-CN.md DELETED
@@ -1,16 +0,0 @@
1
- # DSH Codex Subscription
2
-
3
- 本项目现在默认使用简体中文 README:
4
-
5
- - [打开完整中文说明](README.md)
6
- - [Open the complete English README](README.en.md)
7
-
8
- 旧的 `README.zh-CN.md` 地址会继续保留,已有书签和外部链接无需修改。
9
-
10
- 安装:
11
-
12
- ```powershell
13
- dsh plugin --profile web add dsh-codex-subscription
14
- ```
15
-
16
- 遇到问题请使用[使用问题表单](https://github.com/WSL043/dsh-codex-subscription/issues/new?template=install-problem.yml)。