dsh-codex-subscription 1.14.0 → 1.14.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
@@ -22,12 +22,11 @@ No OpenAI API key or Codex CLI. Models, search, quota, and image generation stay
22
22
  <img src="https://raw.githubusercontent.com/WSL043/dsh-codex-subscription/main/docs/assets/readme-hero-en.webp" width="900" alt="Your Codex subscription inside DSH: models, web search, quota and safe reset, image generation, and Fast mode">
23
23
  </p>
24
24
 
25
- ## What's included in 1.14.0
25
+ ## What's included in 1.14.1
26
26
 
27
- - Built-in image viewer: a bottom toolbar, wheel zoom, drag to pan, on-image notes, copy notes, and continue editing, without another plugin.
28
-
29
- - Fixes generated-image previews that could not open in DSH Web, so generated images can be viewed in the conversation again.
30
- - New and edited images now return the exact original path on the current DSH host in the tool result, so a model or Agent can read or copy the file; preview and download remain session-authorized.
27
+ - The composer shows both 5-hour and weekly quota windows when returned by the service, so neither window is hidden.
28
+ - Image edits validate attachment references, normalize bare SHA-256 digests, and resolve the actual attachment metadata from the current session. Paths produce an actionable error asking the model to obtain the correct reference with `read_image` and retry.
29
+ - Location edits attach the clean source, a numbered annotation image, and coordinate instructions so the model can match each requested change to its location.
31
30
 
32
31
  ## Three-step start
33
32
 
@@ -149,8 +148,8 @@ The plugin can follow an existing HTTPS proxy from the process environment or op
149
148
  </p>
150
149
 
151
150
  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.
152
- Standard Codex uses the lowest remaining window returned by the service; Spark uses its independent quota. The plugin does not hard-code a
153
- “5-hour + weekly” layout or invent Credits and spending caps that the service did not return.
151
+ 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
152
+ five-hour limit, Credits, or spending caps that the service did not return.
154
153
 
155
154
  ### Safe quota reset
156
155
 
@@ -162,11 +161,11 @@ clicks are single-flight, and an uncertain network result is never retried autom
162
161
 
163
162
  ### Image generation and editing (Beta)
164
163
 
165
- A basic viewer derived from `dsh-image-viewer` is now built in, with no extra installation required. If `dsh-image-viewer` is installed, its unified preview still takes priority; otherwise, or if it declines, the built-in viewer opens. You can zoom, pan, fit, add region notes, and download the image. The standard **Download** action retrieves the permission- and integrity-checked exact original by default; only legacy sessions without an exact original fall back to the conversation preview.
164
+ A basic viewer derived from `dsh-image-viewer` is now built in, with no extra installation required. Plugin-generated image cards use the built-in viewer to keep annotation and continue-editing actions available. You can zoom, pan, fit, add region notes, and download the image. The standard **Download** action retrieves the permission- and integrity-checked exact original by default; only legacy sessions without an exact original fall back to the conversation preview.
166
165
 
167
166
  New and edited images return the exact original path on the current DSH host in the tool result, so a model or Agent can read or copy the file. The path is on the host running DSH, not a browser download link; original downloads remain session-authorized. Uninstalling the plugin does not delete generated originals.
168
167
 
169
- **Continue editing in composer** attaches exactly the image you opened and writes the region notes into the draft without sending it. Press **Enter** to save and collapse a region note; use **Shift+Enter** for a new line. Notes remain available when the same image is reopened during the current DSH page session.
168
+ **Continue editing in composer** does not send automatically. With annotations, it attaches the clean source and a numbered location-reference image, and includes matching numbers, coordinates, notes, and instructions to exclude the markers from the result. Without annotations, it attaches only the opened image. Every marker needs a note; reference preparation failures stop the handoff. Press **Enter** to save and collapse a region note; use **Shift+Enter** for a new line. Notes remain available when the same image is reopened during the current DSH page session.
170
169
 
171
170
  A new image request does not silently include earlier images. GPT Image 2 can take longer than a normal text turn, and detailed text, exact composition, or repeated-character consistency may still need another pass.
172
171
 
package/README.md CHANGED
@@ -23,12 +23,11 @@
23
23
  <img src="https://raw.githubusercontent.com/WSL043/dsh-codex-subscription/main/docs/assets/readme-hero.webp" width="900" alt="Codex 订阅直接用在 DSH:订阅模型、联网搜索、额度与安全重置、图片生成和高速模式">
24
24
  </p>
25
25
 
26
- ## 1.14.0 重点变化
26
+ ## 1.14.1 重点变化
27
27
 
28
- - 内置图片查看器:无需额外安装插件,即可使用底部工具栏、滚轮缩放、拖动、图上备注、复制备注和继续编辑。
29
-
30
- - 修复 DSH Web 中生成图片预览无法打开的问题,生成图片可以继续在会话中查看。
31
- - 新生成或编辑的图片会在工具结果中返回当前 DSH 主机上的原图路径,方便模型或 Agent 读取或复制;预览和下载仍按会话权限处理。
28
+ - 输入框同时显示服务端返回的 5 小时与每周额度,避免其中一个额度窗口被隐藏。
29
+ - 图片编辑会检查附件引用,自动补全裸 SHA-256,并从当前会话取回真实附件元数据;误填路径时提示模型通过 `read_image` 获取正确引用并重试。
30
+ - 部位修改会同时附上干净原图、编号标注图和坐标说明,让模型能够对应每条修改要求。
32
31
 
33
32
  ## 三步开始
34
33
 
@@ -149,8 +148,8 @@ dsh --profile web --dump-config
149
148
  <img src="https://raw.githubusercontent.com/WSL043/dsh-codex-subscription/main/docs/assets/composer-quota.png" width="800" alt="中文 DSH 输入框内的 Codex 剩余额度进度条">
150
149
  </p>
151
150
 
152
- 可在设置中选择关闭、百分比、进度条或 Beta 续航预测;紧凑额度只在选择 Codex 模型时显示。续航预测仅在用户主动选择后,根据官方剩余百分比估算当前消耗速度。它至少需要 3 个样本;持续高消耗时通常 5–10 分钟即可给出范围,消耗较低时会延长观察或显示稳定。最近 24 小时的无敏感信息观测会保存在本机,重启后可以继续校准;额度重置、账号切换或关闭功能会开启新的校准周期。普通 Codex 使用服务端返回窗口中剩余最少的一项,
153
- Spark 使用独立额度。插件不会写死“5 小时 + 每周”,也不会虚构服务端没有返回的 Credits 或消费上限。
151
+ 可在设置中选择关闭、百分比、进度条或 Beta 续航预测;紧凑额度只在选择 Codex 模型时显示。续航预测仅在用户主动选择后,根据官方剩余百分比估算当前消耗速度。它至少需要 3 个样本;持续高消耗时通常 5–10 分钟即可给出范围,消耗较低时会延长观察或显示稳定。最近 24 小时的无敏感信息观测会保存在本机,重启后可以继续校准;额度重置、账号切换或关闭功能会开启新的校准周期。输入框分别显示服务端返回的各额度窗口,并标明窗口时长:Plus 返回 5 小时和每周额度时,两项都会显示。
152
+ Spark 使用独立额度。只返回每周额度的账号仍只显示每周,不会虚构 5 小时窗口、Credits 或消费上限。
154
153
 
155
154
  ### 安全使用额度重置
156
155
 
@@ -160,11 +159,11 @@ ChatGPT 返回可用重置卡时,设置页会把每张卡分别显示为紧凑
160
159
 
161
160
  ### 图片生成与编辑(Beta)
162
161
 
163
- 订阅插件已内置基于 `dsh-image-viewer` 的基础查看器,无需额外安装。若已安装 `dsh-image-viewer`,仍优先使用它的统一预览;没有该服务或其拒绝打开时,自动使用内置查看器。你可以缩放、拖动、适合窗口、添加区域备注并下载图片。标准“下载”默认获取经过权限与完整性校验的精确原图;旧会话没有精确原图时才下载会话预览图。
162
+ 订阅插件已内置基于 `dsh-image-viewer` 的基础查看器,无需额外安装。插件生成图片的工具卡片使用内置查看器,确保标注和继续编辑功能可用。你可以缩放、拖动、适合窗口、添加区域备注并下载图片。标准“下载”默认获取经过权限与完整性校验的精确原图;旧会话没有精确原图时才下载会话预览图。
164
163
 
165
164
  新生成或编辑的图片会在工具结果中返回当前 DSH 主机上的原图路径,模型或 Agent 可以读取或复制该文件。该路径位于运行 DSH 的主机,并非浏览器下载链接;原图下载仍按会话授权。卸载插件不会删除已生成的原图。
166
165
 
167
- 点击“在输入框中继续编辑”只附上当前打开的图片,并把部位标注写入草稿,不会自动发送。按 Enter 保存并收起备注,Shift+Enter 换行;在当前 DSH 页面重新打开同一张图片时,备注仍会保留。
166
+ 点击“在输入框中继续编辑”不会自动发送。有标注时会附上干净源图和带编号标记的定位参考图,草稿包含对应编号、坐标、备注,以及不得把标记绘入成品的说明;没有标注时只附上当前图片。每个标记必须填写备注,参考图准备失败时会中止回填。按 Enter 保存并收起备注,Shift+Enter 换行;在当前 DSH 页面重新打开同一张图片时,备注仍会保留。
168
167
 
169
168
  新的图片请求不会静默带入历史图片。GPT Image 2 可能比文本回复耗时更长,复杂文字、精确构图和连续角色一致性也可能需要再次调整。
170
169
 
package/lib/client.js CHANGED
@@ -49,14 +49,193 @@ window.__ModuleLoader__.load({
49
49
  const ForwardRef = /*#__PURE__*/ react.forwardRef(BoltIcon);
50
50
  //#endregion
51
51
  //#region src/image-edit.js
52
- function buildImageEditDraft({ prompt = "", annotations = [], translate }) {
52
+ const isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
53
+ const validCoordinate = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
54
+ const cleanNote = (value) => typeof value === "string" ? value.trim() : "";
55
+ const coordinateError = (number) => {
56
+ const error = /* @__PURE__ */ new Error(`Annotation ${number} must have finite x and y coordinates between 0 and 1`);
57
+ error.code = "ANNOTATION_INVALID";
58
+ return error;
59
+ };
60
+ /**
61
+ * Validate the annotation contract shared by the draft builder and the
62
+ * reference-image renderer. Annotation numbers are their array positions so
63
+ * that they stay aligned with the pins shown to the user.
64
+ */
65
+ function normalizeImageEditAnnotations(annotations, { requireNotes = false } = {}) {
66
+ if (!Array.isArray(annotations)) throw new TypeError("annotations must be an array");
67
+ return annotations.map((annotation, index) => {
68
+ const number = index + 1;
69
+ const note = cleanNote(annotation?.note);
70
+ if (requireNotes && note === "") {
71
+ const error = /* @__PURE__ */ new Error(`Annotation ${number} is missing a note; describe what should change`);
72
+ error.code = "ANNOTATION_INVALID";
73
+ throw error;
74
+ }
75
+ if (!isRecord(annotation) || !validCoordinate(annotation.x) || !validCoordinate(annotation.y)) throw coordinateError(number);
76
+ return {
77
+ number,
78
+ x: annotation.x,
79
+ y: annotation.y,
80
+ note
81
+ };
82
+ });
83
+ }
84
+ const formatPercent = (value) => {
85
+ return `${Number((value * 100).toFixed(2))}%`;
86
+ };
87
+ const formatPixel = (value) => {
88
+ const rounded = Number(value.toFixed(2));
89
+ return String(rounded);
90
+ };
91
+ const withNames = (value, sourceName, referenceName) => String(value).replaceAll("{sourceName}", sourceName).replaceAll("{referenceName}", referenceName);
92
+ const positiveImageDimension = (value) => Number.isSafeInteger(value) && value > 0;
93
+ function buildImageEditDraft({ prompt = "", annotations = [], translate, width, height, sourceName = "source.png", referenceName = "annotated-reference.png" }) {
53
94
  if (typeof translate !== "function") throw new TypeError("translate must be a function");
54
- const notes = annotations.map((annotation, index) => ({
55
- number: index + 1,
56
- note: typeof annotation?.note === "string" ? annotation.note.trim() : ""
57
- })).filter((annotation) => annotation.note !== "").map((annotation) => String(annotation.number) + ". " + annotation.note);
58
- const base = prompt.trim() === "" ? translate("imageEditDefault") : prompt.trim();
59
- return notes.length === 0 ? base : base + "\n\n" + translate("imageRegionNotes") + "\n" + notes.join("\n");
95
+ const base = typeof prompt === "string" && prompt.trim() !== "" ? prompt.trim() : translate("imageEditDefault");
96
+ if (!Array.isArray(annotations)) throw new TypeError("annotations must be an array");
97
+ if (annotations.length === 0) return base;
98
+ const hasWidth = width !== void 0;
99
+ if (hasWidth !== (height !== void 0) || hasWidth && (!positiveImageDimension(width) || !positiveImageDimension(height))) throw new Error("width and height must be positive integers when provided");
100
+ const normalized = normalizeImageEditAnnotations(annotations, { requireNotes: true });
101
+ const source = typeof sourceName === "string" && sourceName.trim() !== "" ? sourceName.trim() : "source.png";
102
+ const reference = typeof referenceName === "string" && referenceName.trim() !== "" ? referenceName.trim() : "annotated-reference.png";
103
+ const guide = withNames(translate("imageEditReferenceGuide"), source, reference);
104
+ const location = translate("imageEditLocation");
105
+ const notes = normalized.map(({ number, x, y, note }) => {
106
+ const pixels = hasWidth ? ` (pixel x=${formatPixel(x * Math.max(0, width - 1))} of ${width}, y=${formatPixel(y * Math.max(0, height - 1))} of ${height})` : "";
107
+ return `${number}. ${location}: x=${formatPercent(x)} (normalized ${x}), y=${formatPercent(y)} (normalized ${y})${pixels}; ${note}`;
108
+ });
109
+ return [
110
+ base,
111
+ "",
112
+ guide,
113
+ "",
114
+ translate("imageRegionNotes"),
115
+ ...notes
116
+ ].join("\n");
117
+ }
118
+ //#endregion
119
+ //#region src/image-edit-reference.js
120
+ const TWO_PI = Math.PI * 2;
121
+ const PIN_FILL = "#e11d48";
122
+ const PIN_OUTER_STROKE = "#111827";
123
+ const PIN_INNER_STROKE = "#ffffff";
124
+ const finiteDimension = (value) => Number.isSafeInteger(value) && value > 0;
125
+ const clamp$1 = (value, min, max) => Math.min(max, Math.max(min, value));
126
+ const clampCenter = (value, size, edge) => {
127
+ const margin = Math.min(edge, size / 2);
128
+ return clamp$1(value, margin, size - margin);
129
+ };
130
+ function getBitmapFactory(options) {
131
+ const factory = options?.createImageBitmap ?? options?.bitmapFactory ?? globalThis.createImageBitmap;
132
+ if (typeof factory !== "function") throw new Error("createImageBitmap is not available");
133
+ return factory;
134
+ }
135
+ function getCanvasFactory(options) {
136
+ const injected = options?.createCanvas ?? options?.canvasFactory;
137
+ if (typeof injected === "function") return injected;
138
+ const document = globalThis.document;
139
+ if (document !== void 0 && typeof document.createElement === "function") return (width, height) => {
140
+ const canvas = document.createElement("canvas");
141
+ canvas.width = width;
142
+ canvas.height = height;
143
+ return canvas;
144
+ };
145
+ throw new Error("A canvas factory is not available");
146
+ }
147
+ function drawPin(context, number, x, y, width, height) {
148
+ const label = String(number);
149
+ const fontSize = Math.max(12, Math.min(40, Math.round(Math.min(width, height) * .03)));
150
+ const radius = Math.max(12, fontSize * .8, fontSize * (.3 * label.length + .35));
151
+ const outerStroke = Math.max(2, Math.min(4, radius * .25));
152
+ const innerStroke = Math.max(1, Math.min(2, radius * .13));
153
+ const edge = radius + outerStroke + 2;
154
+ const targetX = x * Math.max(0, width - 1);
155
+ const targetY = y * Math.max(0, height - 1);
156
+ const centerX = clampCenter(targetX, width, edge);
157
+ const centerY = clampCenter(targetY, height, edge);
158
+ context.save?.();
159
+ if ((centerX !== targetX || centerY !== targetY) && typeof context.moveTo === "function" && typeof context.lineTo === "function") {
160
+ context.beginPath();
161
+ context.moveTo(targetX, targetY);
162
+ context.lineTo(centerX, centerY);
163
+ context.lineWidth = outerStroke * 2;
164
+ context.strokeStyle = PIN_OUTER_STROKE;
165
+ context.stroke();
166
+ context.beginPath();
167
+ context.moveTo(targetX, targetY);
168
+ context.lineTo(centerX, centerY);
169
+ context.lineWidth = innerStroke;
170
+ context.strokeStyle = PIN_INNER_STROKE;
171
+ context.stroke();
172
+ }
173
+ context.beginPath();
174
+ context.arc(centerX, centerY, radius, 0, TWO_PI);
175
+ context.fillStyle = PIN_FILL;
176
+ context.fill();
177
+ context.lineWidth = outerStroke;
178
+ context.strokeStyle = PIN_OUTER_STROKE;
179
+ context.stroke();
180
+ context.lineWidth = innerStroke;
181
+ context.strokeStyle = PIN_INNER_STROKE;
182
+ context.stroke();
183
+ context.font = `700 ${fontSize}px sans-serif`;
184
+ context.fillStyle = PIN_INNER_STROKE;
185
+ context.textAlign = "center";
186
+ context.textBaseline = "middle";
187
+ context.fillText(label, centerX, centerY);
188
+ context.restore?.();
189
+ }
190
+ function encodePng(canvas) {
191
+ if (typeof canvas?.toBlob !== "function") throw new Error("Canvas PNG encoding is not available");
192
+ return new Promise((resolve, reject) => {
193
+ let settled = false;
194
+ const finish = (callback, value) => {
195
+ if (settled) return;
196
+ settled = true;
197
+ callback(value);
198
+ };
199
+ try {
200
+ canvas.toBlob((value) => {
201
+ if (value === null || value === void 0) finish(reject, /* @__PURE__ */ new Error("Canvas failed to encode the annotation reference as PNG"));
202
+ else finish(resolve, value);
203
+ }, "image/png");
204
+ } catch (error) {
205
+ finish(reject, error);
206
+ }
207
+ });
208
+ }
209
+ /**
210
+ * Create the second image sent with an annotated edit. It contains the clean
211
+ * source plus numbered pins, with no note text. `options` is intentionally
212
+ * injectable so the rendering path can be exercised without a browser:
213
+ * `{ createImageBitmap, createCanvas }`.
214
+ */
215
+ async function createAnnotatedImageReference(blob, annotations, options = {}) {
216
+ const normalized = normalizeImageEditAnnotations(annotations);
217
+ const createImageBitmap = getBitmapFactory(options);
218
+ const createCanvas = getCanvasFactory(options);
219
+ const bitmap = await createImageBitmap(blob);
220
+ try {
221
+ const width = bitmap?.width;
222
+ const height = bitmap?.height;
223
+ if (!finiteDimension(width) || !finiteDimension(height)) throw new Error("The source image has invalid dimensions");
224
+ if (Math.min(width, height) < 64) throw new Error("The source image is too small for readable annotation pins");
225
+ const canvas = await createCanvas(width, height);
226
+ if (canvas === null || canvas === void 0) throw new Error("Canvas factory returned no canvas");
227
+ canvas.width = width;
228
+ canvas.height = height;
229
+ const context = canvas.getContext?.("2d");
230
+ if (context === null || context === void 0) throw new Error("Canvas 2D context is not available");
231
+ context.clearRect?.(0, 0, width, height);
232
+ context.drawImage?.(bitmap, 0, 0, width, height);
233
+ if (typeof context.drawImage !== "function") throw new Error("Canvas 2D context cannot draw the source image");
234
+ for (const annotation of normalized) drawPin(context, annotation.number, annotation.x, annotation.y, width, height);
235
+ return await encodePng(canvas);
236
+ } finally {
237
+ if (typeof bitmap?.close === "function") bitmap.close();
238
+ }
60
239
  }
61
240
  const ORIGINAL_IMAGE_ID_PATTERN = /^img_[0-9a-f]{32}$/u;
62
241
  const positiveInteger = (value) => Number.isSafeInteger(value) && value > 0;
@@ -723,9 +902,8 @@ window.__ModuleLoader__.load({
723
902
  /**
724
903
  * Local image viewer state for subscription-generated images.
725
904
  *
726
- * This deliberately stays private to the subscription client. An installed
727
- * native image viewer can take the request first, while this service provides
728
- * the same baseline experience when that optional plugin is absent.
905
+ * This stays private to subscription image cards, which need annotation and
906
+ * edit actions that a host's generic native viewer may not implement.
729
907
  */
730
908
  var SubscriptionImageViewerService = class {
731
909
  #listeners = /* @__PURE__ */ new Set();
@@ -865,16 +1043,13 @@ window.__ModuleLoader__.load({
865
1043
  if (/\bspark\b/u.test(normalized(model))) return /\bspark\b/u.test(normalized(`${limit?.id ?? ""} ${limit?.name ?? ""}`));
866
1044
  return limit?.id === "codex";
867
1045
  };
868
- function selectModelQuota(usage, model) {
869
- const windows = Array.isArray(usage?.rateLimits) ? usage.rateLimits.filter((limit) => limitMatchesModel(limit, model) && Array.isArray(limit.windows)).flatMap((limit) => limit.windows).filter(isDisplayableWindow) : [];
870
- if (windows.length === 0) return void 0;
871
- const selected = windows.reduce((lowest, candidate) => candidate.remainingPercent < lowest.remainingPercent ? candidate : lowest);
872
- return {
1046
+ function selectModelQuotaWindows(usage, model) {
1047
+ return (Array.isArray(usage?.rateLimits) ? usage.rateLimits.filter((limit) => limitMatchesModel(limit, model) && Array.isArray(limit.windows)).flatMap((limit) => limit.windows).filter(isDisplayableWindow) : []).map((selected) => ({
873
1048
  remainingPercent: selected.remainingPercent,
874
1049
  windowSeconds: selected.windowSeconds,
875
1050
  ...Number.isSafeInteger(selected.resetsAt) ? { resetsAt: selected.resetsAt } : {},
876
1051
  ...selected.forecast === void 0 ? {} : { forecast: selected.forecast }
877
- };
1052
+ })).sort((a, b) => a.windowSeconds - b.windowSeconds);
878
1053
  }
879
1054
  //#endregion
880
1055
  //#region src/login-progress.js
@@ -1078,6 +1253,8 @@ window.__ModuleLoader__.load({
1078
1253
  const QUICK_QUOTA_REFRESH_EVENT = "dsh-codex-subscription:refresh-quick-quota";
1079
1254
  const QUICK_QUOTA_REFRESH_MS = 6e4;
1080
1255
  const zh = {
1256
+ imageEditLocation: "位置",
1257
+ imageEditReferenceGuide: "本次编辑的干净源图为「{sourceName}」,编号定位参考图为「{referenceName}」。坐标以图片左上角为原点,x 向右、y 向下,百分比相对于整张图片。请查看这两张图片,将它们同时作为编辑工具的参考图,并在工具提示词中完整保留下方编号、位置和修改要求。只修改源图中对应位置的内容;定位参考图上的编号、圆点和引线仅用于定位,不得绘入最终结果。若无法读取两张图片或确定位置,请说明问题,不要猜测或忽略标注。",
1081
1258
  nav: "Codex 订阅",
1082
1259
  title: "Codex 订阅",
1083
1260
  connected: "已登录",
@@ -1267,10 +1444,12 @@ window.__ModuleLoader__.load({
1267
1444
  imageRegionNotes: "部位修改:",
1268
1445
  imageEdit: "在输入框中继续编辑",
1269
1446
  imageEditPreparing: "正在添加到输入框…",
1270
- imageEditFailed: "无法把图片添加到输入框。",
1447
+ imageEditFailed: "回填失败:请填写每个标记的备注,并确认输入框可接收图片后重试。",
1271
1448
  imageRemoveAnnotation: "删除标注"
1272
1449
  };
1273
1450
  const en = {
1451
+ imageEditLocation: "Location",
1452
+ imageEditReferenceGuide: "The clean source for this edit is \"{sourceName}\"; \"{referenceName}\" is the numbered location reference. Coordinates start at the top-left, x increases rightward and y downward; percentages refer to the whole image. Inspect both images and pass both as references to the image-editing tool. Preserve every number, position and requested change below in the tool prompt. Edit the corresponding content in the clean source; numbers, dots and leader lines on the location reference are guidance only and must not appear in the final result. If either image cannot be read or a location is unclear, explain the problem instead of guessing or ignoring annotations.",
1274
1453
  nav: "Codex",
1275
1454
  title: "Codex subscription",
1276
1455
  connected: "Signed in",
@@ -1460,7 +1639,7 @@ window.__ModuleLoader__.load({
1460
1639
  imageRegionNotes: "Region changes:",
1461
1640
  imageEdit: "Continue editing in composer",
1462
1641
  imageEditPreparing: "Adding to composer…",
1463
- imageEditFailed: "Could not add the image to the composer.",
1642
+ imageEditFailed: "Handoff failed. Add a note to every marker and ensure the composer accepts images, then retry.",
1464
1643
  imageRemoveAnnotation: "Remove note"
1465
1644
  };
1466
1645
  const STYLE = `
@@ -1497,7 +1676,7 @@ window.__ModuleLoader__.load({
1497
1676
  .codexSubscriptionCreditRows{display:flex;flex-direction:column;gap:6px}.codexSubscriptionResetMeta{display:flex;min-width:0;flex-direction:column;gap:1px}.codexSubscriptionResetBalance{display:flex;flex-direction:column;gap:8px}.codexSubscriptionResetCard{display:flex;align-items:center;justify-content:space-between;gap:10px;min-width:0;padding:9px 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:9px;background:var(--dsw-alias-bg-module-platform)}.codexSubscriptionResetCard .codexSubscriptionResetMeta{flex:1}.codexSubscriptionResetCard strong{overflow:hidden;font-size:12px;line-height:18px;font-weight:500;text-overflow:ellipsis;white-space:nowrap}.codexSubscriptionResetCard .codexSubscriptionActions{flex:0 0 auto}.codexSubscriptionResetCard .codexSubscriptionResetUse{min-height:28px;padding:0 10px}.codexSubscriptionResetBalance .codexSubscriptionActions{justify-content:flex-start}.codexSubscriptionResetFlow{display:flex;flex-direction:column;gap:10px;border-top:1px solid var(--dsw-alias-border-l2);padding-top:10px}.codexSubscriptionResetFlow h4{margin:0;font-size:13px;line-height:20px;font-weight:500}.codexSubscriptionResetWarning{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}.codexSubscriptionResetExpiry{font-size:11px;line-height:17px;color:var(--dsw-alias-label-tertiary)}.codexSubscriptionResetCheck{display:flex;align-items:flex-start;gap:8px;padding:9px 10px;border-radius:8px;background:var(--dsw-alias-bg-module-platform);font-size:12px;line-height:18px;color:var(--dsw-alias-label-primary);cursor:pointer}.codexSubscriptionResetCheck input{margin:3px 0 0;accent-color:var(--dsw-alias-label-primary)}.codexSubscriptionResetFinal{border-color:var(--dsw-alias-state-error-primary)!important;color:var(--dsw-alias-state-error-primary)!important}.codexSubscriptionResetResult{font-size:12px;line-height:18px;color:var(--dsw-alias-state-success-primary)}
1498
1677
  .codexSubscriptionResetUse:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.codexSubscriptionResetUse:focus-visible{outline:2px solid var(--dsw-alias-border-l3);outline-offset:1px}
1499
1678
  .codexSubscriptionSpendLimit{display:flex;flex-direction:column;gap:8px}.codexSubscriptionSpendTop{display:flex;align-items:baseline;justify-content:space-between;gap:12px}.codexSubscriptionSpendTop strong{font:600 16px/22px ui-monospace,SFMono-Regular,Consolas,monospace;font-variant-numeric:tabular-nums}.codexSubscriptionSpendLimit progress{width:100%;height:6px;border:0;border-radius:999px;overflow:hidden;background:var(--dsw-alias-border-l3);accent-color:var(--dsw-alias-brand-primary,#3964fe);-webkit-appearance:none;appearance:none}.codexSubscriptionSpendLimit progress::-webkit-progress-bar{background:var(--dsw-alias-border-l3);border-radius:999px}.codexSubscriptionSpendLimit progress::-webkit-progress-value{background:var(--dsw-alias-brand-primary,#3964fe);border-radius:999px}.codexSubscriptionSpendLimit progress::-moz-progress-bar{background:var(--dsw-alias-brand-primary,#3964fe);border-radius:999px}
1500
- .codexComposerQuota{display:inline-flex;align-items:center;flex:0 0 auto;height:28px;box-sizing:border-box;padding:0;color:var(--dsw-alias-label-secondary);font-family:inherit;font-size:12px;line-height:20px;font-weight:500;font-variant-numeric:tabular-nums;white-space:nowrap;user-select:none}.codexComposerQuotaBar{display:block;width:40px;height:4px;border:0;border-radius:999px;overflow:hidden;background:var(--dsw-alias-border-l3);accent-color:var(--dsw-alias-label-secondary);-webkit-appearance:none;appearance:none}.codexComposerQuotaBar::-webkit-progress-bar{background:var(--dsw-alias-border-l3);border-radius:999px}.codexComposerQuotaBar::-webkit-progress-value{background:var(--dsw-alias-label-secondary);border-radius:999px}.codexComposerQuotaBar::-moz-progress-bar{background:var(--dsw-alias-label-secondary);border-radius:999px}
1679
+ .codexComposerQuotaWindows{display:inline-flex;align-items:center;gap:10px;flex-wrap:wrap}.codexComposerQuota{display:inline-flex;align-items:center;gap:5px;flex:0 0 auto;height:28px;box-sizing:border-box;padding:0;color:var(--dsw-alias-label-secondary);font-family:inherit;font-size:12px;line-height:20px;font-weight:500;font-variant-numeric:tabular-nums;white-space:nowrap;user-select:none}.codexComposerQuotaBar{display:block;width:40px;height:4px;border:0;border-radius:999px;overflow:hidden;background:var(--dsw-alias-border-l3);accent-color:var(--dsw-alias-label-secondary);-webkit-appearance:none;appearance:none}.codexComposerQuotaBar::-webkit-progress-bar{background:var(--dsw-alias-border-l3);border-radius:999px}.codexComposerQuotaBar::-webkit-progress-value{background:var(--dsw-alias-label-secondary);border-radius:999px}.codexComposerQuotaBar::-moz-progress-bar{background:var(--dsw-alias-label-secondary);border-radius:999px}
1501
1680
  .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)}
1502
1681
  .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}
1503
1682
  .codexModelSelectMenu{overflow:visible}
@@ -1649,18 +1828,27 @@ window.__ModuleLoader__.load({
1649
1828
  pendingLabel: t("imageEditPreparing"),
1650
1829
  errorLabel: t("imageEditFailed"),
1651
1830
  closeOnSuccess: true,
1652
- onInvoke: ({ annotations }) => attachForEdit(src, downloadName, buildImageEditDraft({
1653
- annotations,
1654
- translate: t
1655
- }))
1831
+ onInvoke: ({ annotations = [] }) => {
1832
+ const imageKey = String(attachment.attachmentId ?? "image").replace(/[^a-zA-Z0-9_-]/g, "_");
1833
+ const sourceName = annotations.length === 0 ? downloadName : `codex-edit-${imageKey}-source.png`;
1834
+ const referenceName = `codex-edit-${imageKey}-annotations.png`;
1835
+ return attachForEdit(src, sourceName, buildImageEditDraft({
1836
+ annotations,
1837
+ translate: t,
1838
+ width: attachment.width,
1839
+ height: attachment.height,
1840
+ sourceName,
1841
+ referenceName
1842
+ }), annotations, referenceName);
1843
+ }
1656
1844
  }]
1657
1845
  }],
1658
1846
  opener: triggerRef.current,
1659
1847
  source: "codex-generated",
1660
1848
  annotations: true
1661
1849
  };
1662
- if ((getImageViewer?.())?.open?.(request) === true) return;
1663
- getInternalImageViewer?.()?.open?.(request);
1850
+ if (getInternalImageViewer?.()?.open?.(request) === true) return;
1851
+ (getImageViewer?.())?.open?.(request);
1664
1852
  };
1665
1853
  if (error) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1666
1854
  type: "button",
@@ -1780,7 +1968,7 @@ window.__ModuleLoader__.load({
1780
1968
  return;
1781
1969
  }
1782
1970
  const usage = unwrap(await rpc.call(CHANNEL, "usage", { force: false }));
1783
- if (live) setQuota(selectModelQuota(usage, model));
1971
+ if (live) setQuota(selectModelQuotaWindows(usage, model));
1784
1972
  } catch {
1785
1973
  if (live) setQuota(void 0);
1786
1974
  } finally {
@@ -2053,8 +2241,19 @@ window.__ModuleLoader__.load({
2053
2241
  const codex = current?.provider === "openai-codex";
2054
2242
  const quotaEnabled = preferenceSnapshot.status === "ready" && preferenceSnapshot.quickQuotaMode !== "off" && codex;
2055
2243
  const forecastMode = preferenceSnapshot.quickQuotaMode === QUICK_QUOTA_MODE_FORECAST;
2056
- const quota = useQuickQuota(rpc, quotaEnabled, current?.model);
2057
- if (!quotaEnabled || quota === void 0) return null;
2244
+ const quotas = useQuickQuota(rpc, quotaEnabled, current?.model);
2245
+ if (!quotaEnabled || quotas === void 0 || quotas.length === 0) return null;
2246
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2247
+ className: "codexComposerQuotaWindows",
2248
+ children: quotas.map((quota, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CodexComposerQuotaWindow, {
2249
+ quota,
2250
+ mode: preferenceSnapshot.quickQuotaMode,
2251
+ forecastMode,
2252
+ t
2253
+ }, `${quota.windowSeconds}-${index}`))
2254
+ });
2255
+ }
2256
+ function CodexComposerQuotaWindow({ quota, mode, forecastMode, t }) {
2058
2257
  const value = Math.round(Number(quota.remainingPercent) * 10) / 10;
2059
2258
  const display = percent(value);
2060
2259
  const forecast = forecastMode ? quota.forecast : void 0;
@@ -2063,17 +2262,20 @@ window.__ModuleLoader__.load({
2063
2262
  value: display,
2064
2263
  duration
2065
2264
  });
2066
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2265
+ const content = mode === "bar" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("progress", {
2266
+ className: "codexComposerQuotaBar",
2267
+ max: 100,
2268
+ value,
2269
+ "aria-hidden": "true"
2270
+ }) : forecast?.status === "calibrating" ? `${display}% · ${t("quickQuotaForecastCalibrating")}` : forecast?.status === "idle" ? `${display}% · ${t("quickQuotaForecastIdle")}` : forecast?.status === "ready" && forecast.survivesReset ? `${display}% · ${t("quickQuotaForecastUntilReset")}` : forecastMode && duration !== void 0 ? `${display}% · ≈${duration}` : `${display}%`;
2271
+ const durationLabel = windowLabel(quota.windowSeconds, t);
2272
+ const accessibleLabel = `${durationLabel}: ${label}`;
2273
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2067
2274
  className: "codexComposerQuota",
2068
2275
  role: "status",
2069
- "aria-label": label,
2070
- title: label,
2071
- children: preferenceSnapshot.quickQuotaMode === "bar" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("progress", {
2072
- className: "codexComposerQuotaBar",
2073
- max: 100,
2074
- value,
2075
- "aria-hidden": "true"
2076
- }) : forecast?.status === "calibrating" ? `${display}% · ${t("quickQuotaForecastCalibrating")}` : forecast?.status === "idle" ? `${display}% · ${t("quickQuotaForecastIdle")}` : forecast?.status === "ready" && forecast.survivesReset ? `${display}% · ${t("quickQuotaForecastUntilReset")}` : forecastMode && duration !== void 0 ? `${display}% · ≈${duration}` : `${display}%`
2276
+ "aria-label": accessibleLabel,
2277
+ title: accessibleLabel,
2278
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: durationLabel }), content]
2077
2279
  });
2078
2280
  }
2079
2281
  function CodexModelSelect({ locked, available, directory, load, select, preference, t }) {
@@ -3358,13 +3560,18 @@ window.__ModuleLoader__.load({
3358
3560
  }
3359
3561
  },
3360
3562
  getInternalImageViewer: () => imageViewer,
3361
- attachForEdit: async (src, filename, draft) => {
3563
+ attachForEdit: async (src, filename, draft, annotations = [], referenceName) => {
3362
3564
  const actx = sessions.scope(sessionId);
3363
3565
  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");
3364
3566
  const response = await fetch(src);
3365
3567
  if (!response.ok) throw new Error("Could not read generated image");
3366
3568
  const blob = await response.blob();
3367
- const created = conversation.createDraftImages([new File([blob], filename, { type: blob.type || "image/png" })]);
3569
+ const files = [new File([blob], filename, { type: blob.type || "image/png" })];
3570
+ if (annotations.length > 0) {
3571
+ const reference = await createAnnotatedImageReference(blob, annotations);
3572
+ files.push(new File([reference], referenceName, { type: "image/png" }));
3573
+ }
3574
+ const created = conversation.createDraftImages(files);
3368
3575
  const input = conversation.input.for(actx);
3369
3576
  if (!input.addImages(created.map((item) => item.id))) {
3370
3577
  conversation.releaseDraftImages(created);
package/lib/index.js CHANGED
@@ -1303,7 +1303,7 @@ function openaiCodexSubscriptionProvider({ resolveSpeedMode = () => void 0, reso
1303
1303
  }
1304
1304
  //#endregion
1305
1305
  //#region src/version.js
1306
- const PACKAGE_VERSION = "1.14.0";
1306
+ const PACKAGE_VERSION = "1.14.1";
1307
1307
  const USER_AGENT = `dsh-codex-subscription/${PACKAGE_VERSION}`;
1308
1308
  //#endregion
1309
1309
  //#region src/model-catalog.js
@@ -1637,6 +1637,10 @@ const PNG_SIGNATURE = Buffer.from([
1637
1637
  26,
1638
1638
  10
1639
1639
  ]);
1640
+ const FULL_ATTACHMENT_ID = /^sha256:[0-9a-f]{64}$/u;
1641
+ const BARE_ATTACHMENT_DIGEST = /^[0-9a-f]{64}$/iu;
1642
+ const PATH_LIKE_ATTACHMENT_ID = /[\\/]/u;
1643
+ const FILE_NAME_ATTACHMENT_ID = /\.[A-Za-z0-9]{1,16}$/u;
1640
1644
  const record$2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
1641
1645
  const nonEmpty = (value) => typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
1642
1646
  function normalizeImageOptions(args) {
@@ -1706,22 +1710,63 @@ function decodeCodexPng(value, maximumBytes) {
1706
1710
  return new Uint8Array(data);
1707
1711
  }
1708
1712
  function imageReference(value) {
1713
+ const originalDimensions = record$2(value.originalDimensions) ? {
1714
+ width: value.originalDimensions.width,
1715
+ height: value.originalDimensions.height
1716
+ } : void 0;
1709
1717
  return {
1710
1718
  attachmentId: value.attachmentId,
1711
1719
  mediaType: value.mediaType,
1712
1720
  bytes: value.bytes,
1713
1721
  width: value.width,
1714
1722
  height: value.height,
1715
- ...value.name === void 0 ? {} : { name: value.name }
1723
+ ...value.name === void 0 ? {} : { name: value.name },
1724
+ ...originalDimensions === void 0 ? {} : { originalDimensions }
1716
1725
  };
1717
1726
  }
1727
+ function normalizeAttachmentId(value) {
1728
+ if (typeof value !== "string") return void 0;
1729
+ if (FULL_ATTACHMENT_ID.test(value)) return value;
1730
+ const bare = BARE_ATTACHMENT_DIGEST.exec(value);
1731
+ return bare === null ? void 0 : `sha256:${bare[0].toLowerCase()}`;
1732
+ }
1733
+ function invalidAttachmentId(value) {
1734
+ const attachmentId = value?.attachmentId;
1735
+ if (typeof attachmentId === "string" && (PATH_LIKE_ATTACHMENT_ID.test(attachmentId) || FILE_NAME_ATTACHMENT_ID.test(attachmentId))) throw new Error("referenceImages attachmentId is a file path or filename; call read_image on that file and retry with its complete sha256:<64 lowercase hex> attachment reference. Do not omit referenceImages or fall back to new image generation.");
1736
+ throw new Error("referenceImages attachmentId must be sha256:<64 lowercase hex> copied from an image block or read_image result. Do not omit referenceImages or fall back to new image generation.");
1737
+ }
1718
1738
  function referenceOf(value, attachments) {
1719
- 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");
1720
- return imageReference(value);
1739
+ const attachmentId = normalizeAttachmentId(value?.attachmentId);
1740
+ if (attachmentId === void 0) invalidAttachmentId(value);
1741
+ if (!record$2(value) || !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) || value.originalDimensions !== void 0 && (!record$2(value.originalDimensions) || !Number.isSafeInteger(value.originalDimensions.width) || value.originalDimensions.width <= 0 || !Number.isSafeInteger(value.originalDimensions.height) || value.originalDimensions.height <= 0)) throw new Error("referenceImages contains an invalid image reference");
1742
+ return imageReference({
1743
+ ...value,
1744
+ attachmentId
1745
+ });
1721
1746
  }
1722
- async function editImages(values, attachments, signal) {
1747
+ function sessionImageReferences(messages) {
1748
+ const references = /* @__PURE__ */ new Map();
1749
+ const visit = (content) => {
1750
+ if (!Array.isArray(content)) return;
1751
+ for (const block of content) if (block?.type === "image" && record$2(block.attachment)) {
1752
+ const id = normalizeAttachmentId(block.attachment.attachmentId);
1753
+ if (id !== void 0) references.set(id, block.attachment);
1754
+ } else if (block?.type === "tool-result") visit(block.content);
1755
+ };
1756
+ for (const message of messages ?? []) visit(message?.content);
1757
+ return references;
1758
+ }
1759
+ async function editImages(values, attachments, signal, messages) {
1723
1760
  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`);
1724
- const references = values.map((value) => referenceOf(value, attachments));
1761
+ const available = messages === void 0 ? void 0 : sessionImageReferences(messages);
1762
+ const references = values.map((value) => {
1763
+ const id = normalizeAttachmentId(value?.attachmentId);
1764
+ if (id === void 0) invalidAttachmentId(value);
1765
+ if (available === void 0) return referenceOf(value, attachments);
1766
+ const selected = available.get(id);
1767
+ if (selected === void 0) throw new Error("The selected image attachment cannot be found in the current session. Call read_image on the intended image and retry with its returned reference. Do not omit referenceImages or substitute another image.");
1768
+ return referenceOf(selected, attachments);
1769
+ });
1725
1770
  if (new Set(references.map((value) => value.attachmentId)).size !== references.length) throw new Error("referenceImages must not contain duplicates");
1726
1771
  const images = [];
1727
1772
  let totalBytes = 0;
@@ -1774,7 +1819,21 @@ function imageOutputSchema() {
1774
1819
  type: "integer",
1775
1820
  required: true
1776
1821
  },
1777
- name: { type: "string" }
1822
+ name: { type: "string" },
1823
+ originalDimensions: {
1824
+ type: "object",
1825
+ additionalProperties: false,
1826
+ properties: {
1827
+ width: {
1828
+ type: "integer",
1829
+ required: true
1830
+ },
1831
+ height: {
1832
+ type: "integer",
1833
+ required: true
1834
+ }
1835
+ }
1836
+ }
1778
1837
  }
1779
1838
  },
1780
1839
  original: {
@@ -1841,7 +1900,7 @@ function createCodexImageTool(options) {
1841
1900
  const attachments = options.attachments;
1842
1901
  return defineTool({
1843
1902
  name: CODEX_IMAGE_TOOL_NAME,
1844
- 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.",
1903
+ description: "Create a new image or explicitly edit selected prior images using the signed-in Codex subscription. Omit referenceImages only for a completely new image. For an edit, copy each complete image reference from the session image block or read_image result, including attachmentId in the exact form sha256:<64 lowercase hex>; never use a workspace path, absolute path, or filename, and do not omit the references to turn an edit into text-to-image generation. A bare 64-character hex digest is accepted and normalized to sha256:<64 lowercase hex>. If a reference is unavailable, call read_image and retry with its returned reference. Include only the exact prior image references the user asked to edit; never assume every image in the conversation is a reference. For annotation-guided edits, include both the named clean source and its numbered location reference, and preserve the numbered coordinates and requested changes in the prompt. The location-reference markers are guidance only and must not appear in the result. If those references cannot be identified, do not substitute unrelated images or silently ignore the annotations.",
1845
1904
  parameters: {
1846
1905
  prompt: {
1847
1906
  type: "string",
@@ -1873,14 +1932,15 @@ function createCodexImageTool(options) {
1873
1932
  },
1874
1933
  referenceImages: {
1875
1934
  type: "array",
1876
- description: "Optional explicit references to 1-5 prior images to edit. Omit for a new image.",
1935
+ description: "Optional explicit references to 1-5 prior images to edit. Copy each complete reference from the session image block or read_image result. Omit only for a new image; an invalid reference must be fixed and retried rather than omitted.",
1877
1936
  items: {
1878
1937
  type: "object",
1879
1938
  additionalProperties: false,
1880
1939
  properties: {
1881
1940
  attachmentId: {
1882
1941
  type: "string",
1883
- required: true
1942
+ required: true,
1943
+ description: "Copy the complete attachmentId from the image block or read_image result: sha256:<64 lowercase hex>. Never pass a workspace path, absolute path, or filename. A bare 64-character hex digest is accepted and normalized to sha256:<64 lowercase hex>."
1884
1944
  },
1885
1945
  mediaType: {
1886
1946
  type: "string",
@@ -1898,7 +1958,21 @@ function createCodexImageTool(options) {
1898
1958
  type: "integer",
1899
1959
  required: true
1900
1960
  },
1901
- name: { type: "string" }
1961
+ name: { type: "string" },
1962
+ originalDimensions: {
1963
+ type: "object",
1964
+ additionalProperties: false,
1965
+ properties: {
1966
+ width: {
1967
+ type: "integer",
1968
+ required: true
1969
+ },
1970
+ height: {
1971
+ type: "integer",
1972
+ required: true
1973
+ }
1974
+ }
1975
+ }
1902
1976
  }
1903
1977
  }
1904
1978
  }
@@ -1926,7 +2000,8 @@ function createCodexImageTool(options) {
1926
2000
  if (!attachments.imageLimits.mediaTypes.includes("image/png")) throw new Error("This DSH installation does not accept PNG image attachments");
1927
2001
  const maximumBytes = Math.min(attachments.imageLimits.maxImageBytes, attachments.imageLimits.maxMessageImageBytes);
1928
2002
  const editing = args.referenceImages !== void 0;
1929
- const images = editing ? await editImages(args.referenceImages, attachments, exec.signal) : void 0;
2003
+ const sessionMessages = editing && typeof options.getSessionMessages === "function" ? await options.getSessionMessages(exec.agent?.id) : void 0;
2004
+ const images = editing ? await editImages(args.referenceImages, attachments, exec.signal, sessionMessages ?? (options.getSessionMessages ? [] : void 0)) : void 0;
1930
2005
  let response;
1931
2006
  try {
1932
2007
  response = await fetchImage(editing ? CODEX_IMAGE_EDIT_URL : CODEX_IMAGE_GENERATION_URL, {
@@ -3304,6 +3379,7 @@ function apply(ctx) {
3304
3379
  getAuth: resolveAuth,
3305
3380
  readCredential: (options) => store.read(PROVIDER, options),
3306
3381
  attachments: ctx.attachments,
3382
+ getSessionMessages: (sessionId) => ctx.get?.("sessions")?.get?.(sessionId)?.deriveMessages?.() ?? [],
3307
3383
  originalImages,
3308
3384
  fetch: (input, init) => network.fetch("image", input, init)
3309
3385
  }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-codex-subscription",
3
- "version": "1.14.0",
3
+ "version": "1.14.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",