dsh-codex-subscription 2.1.0-beta.4 → 2.1.0-beta.5

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.
Files changed (3) hide show
  1. package/lib/client.js +987 -898
  2. package/lib/index.js +49 -16
  3. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -31,937 +31,968 @@ window.__ModuleLoader__.load({
31
31
  let react_jsx_runtime = require("react/jsx-runtime");
32
32
  let react_dom = require("react-dom");
33
33
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
34
- //#region src/image-edit.js
35
- const isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
36
- const validCoordinate = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
37
- const cleanNote = (value) => typeof value === "string" ? value.trim() : "";
38
- const coordinateError = (number) => {
39
- const error = /* @__PURE__ */ new Error(`Annotation ${number} must have finite x and y coordinates between 0 and 1`);
40
- error.code = "ANNOTATION_INVALID";
41
- return error;
42
- };
43
- /**
44
- * Validate the annotation contract shared by the draft builder and the
45
- * reference-image renderer. Annotation numbers are their array positions so
46
- * that they stay aligned with the pins shown to the user.
47
- */
48
- function normalizeImageEditAnnotations(annotations, { requireNotes = false } = {}) {
49
- if (!Array.isArray(annotations)) throw new TypeError("annotations must be an array");
50
- return annotations.map((annotation, index) => {
51
- const number = index + 1;
52
- const note = cleanNote(annotation?.note);
53
- if (requireNotes && note === "") {
54
- const error = /* @__PURE__ */ new Error(`Annotation ${number} is missing a note; describe what should change`);
55
- error.code = "ANNOTATION_INVALID";
56
- throw error;
34
+ //#region src/sketch-curves.js
35
+ const cached = /* @__PURE__ */ new WeakMap();
36
+ const midpoint = (a, b) => ({
37
+ x: (a.x + b.x) / 2,
38
+ y: (a.y + b.y) / 2
39
+ });
40
+ function flattenSketchCurve(stroke, width, height) {
41
+ const previous = cached.get(stroke);
42
+ if (previous?.width === width && previous.height === height) return previous.points;
43
+ const controls = stroke.points.map((p) => ({
44
+ x: p.x * width,
45
+ y: p.y * height
46
+ })), points = [controls[0]];
47
+ const distance = (p, a, b) => {
48
+ const dx = b.x - a.x, dy = b.y - a.y, d = dx * dx + dy * dy;
49
+ const t = d ? Math.max(0, Math.min(1, ((p.x - a.x) * dx + (p.y - a.y) * dy) / d)) : 0;
50
+ return Math.hypot(p.x - a.x - t * dx, p.y - a.y - t * dy);
51
+ };
52
+ const split = (a, b, c, d, depth) => {
53
+ if (depth === 10 || Math.max(distance(b, a, d), distance(c, a, d)) <= .5) {
54
+ points.push(d);
55
+ return;
57
56
  }
58
- if (!isRecord(annotation) || !validCoordinate(annotation.x) || !validCoordinate(annotation.y)) throw coordinateError(number);
59
- return {
60
- number,
61
- x: annotation.x,
62
- y: annotation.y,
63
- note
64
- };
65
- });
66
- }
67
- const formatPercent = (value) => {
68
- return `${Number((value * 100).toFixed(2))}%`;
69
- };
70
- const formatPixel = (value) => {
71
- const rounded = Number(value.toFixed(2));
72
- return String(rounded);
73
- };
74
- const withNames = (value, sourceName, referenceName) => String(value).replaceAll("{sourceName}", sourceName).replaceAll("{referenceName}", referenceName);
75
- const positiveImageDimension = (value) => Number.isSafeInteger(value) && value > 0;
76
- function buildImageEditDraft({ prompt = "", annotations = [], translate, width, height, sourceName = "source.png", referenceName = "annotated-reference.png" }) {
77
- if (typeof translate !== "function") throw new TypeError("translate must be a function");
78
- const base = typeof prompt === "string" && prompt.trim() !== "" ? prompt.trim() : translate("imageEditDefault");
79
- if (!Array.isArray(annotations)) throw new TypeError("annotations must be an array");
80
- if (annotations.length === 0) return base;
81
- const hasWidth = width !== void 0;
82
- if (hasWidth !== (height !== void 0) || hasWidth && (!positiveImageDimension(width) || !positiveImageDimension(height))) throw new Error("width and height must be positive integers when provided");
83
- const normalized = normalizeImageEditAnnotations(annotations, { requireNotes: true });
84
- const source = typeof sourceName === "string" && sourceName.trim() !== "" ? sourceName.trim() : "source.png";
85
- const reference = typeof referenceName === "string" && referenceName.trim() !== "" ? referenceName.trim() : "annotated-reference.png";
86
- const guide = withNames(translate("imageEditReferenceGuide"), source, reference);
87
- const location = translate("imageEditLocation");
88
- const notes = normalized.map(({ number, x, y, note }) => {
89
- const pixels = hasWidth ? ` (pixel x=${formatPixel(x * Math.max(0, width - 1))} of ${width}, y=${formatPixel(y * Math.max(0, height - 1))} of ${height})` : "";
90
- return `${number}. ${location}: x=${formatPercent(x)} (normalized ${x}), y=${formatPercent(y)} (normalized ${y})${pixels}; ${note}`;
57
+ const ab = midpoint(a, b), bc = midpoint(b, c), cd = midpoint(c, d), abc = midpoint(ab, bc), bcd = midpoint(bc, cd), m = midpoint(abc, bcd);
58
+ split(a, ab, abc, m, depth + 1);
59
+ split(m, bcd, cd, d, depth + 1);
60
+ };
61
+ for (let i = 1; i < controls.length; i += 3) split(controls[i - 1], controls[i], controls[i + 1], controls[i + 2], 0);
62
+ const normalized = points.map((p) => ({
63
+ x: p.x / width,
64
+ y: p.y / height
65
+ }));
66
+ cached.set(stroke, {
67
+ width,
68
+ height,
69
+ points: normalized
91
70
  });
92
- return [
93
- base,
94
- "",
95
- guide,
96
- "",
97
- translate("imageRegionNotes"),
98
- ...notes
99
- ].join("\n");
71
+ return normalized;
100
72
  }
101
73
  //#endregion
102
- //#region src/client-image-previews.jsx
103
- function openPreview(props, item, opener, sourceInDraft = false) {
104
- const { service, preference, t, attachForEdit } = props;
105
- const settings = preference.getSnapshot();
106
- const referenceName = `annotated-${item.name}.png`;
107
- service.open({
108
- items: [{
109
- ...item,
110
- actions: settings.imageEditing ? [{
111
- id: "edit",
112
- label: t("imageEdit"),
113
- pendingLabel: t("imageEditPreparing"),
114
- errorLabel: t("imageEditFailed"),
115
- closeOnSuccess: true,
116
- onInvoke: ({ annotations }) => attachForEdit(item.src, item.name, buildImageEditDraft({
117
- annotations,
118
- translate: t,
119
- sourceName: item.name,
120
- referenceName
121
- }), annotations, referenceName, sourceInDraft)
122
- }, ...settings.imageSketch && props.openSketchImage ? [{
123
- id: "sketch",
124
- label: t("imageToSketch"),
125
- pendingLabel: t("imageEditPreparing"),
126
- errorLabel: t("imageEditFailed"),
127
- onInvoke: () => props.openSketchImage(item.src, item.name)
128
- }] : []] : []
129
- }],
130
- opener,
131
- source: sourceInDraft ? "codex-draft" : "codex-message",
132
- annotations: settings.imageAnnotations
133
- });
134
- }
135
- function ComposerImagePreviews(props) {
136
- const { attachments, service, nativeAttachments, watchNativeAttachments, nativeTranslate } = props;
137
- const entry = (0, react.useSyncExternalStore)(watchNativeAttachments, nativeAttachments);
138
- (0, react.useEffect)(() => {
139
- const current = service.getSnapshot();
140
- if (current?.source === "codex-draft" && !attachments.some((item) => item.id === current.items[0]?.id)) service.close();
141
- }, [attachments, service]);
142
- (0, react.useEffect)(() => () => {
143
- if (service.getSnapshot()?.source === "codex-draft") service.close();
144
- }, [service]);
145
- if (!entry) return null;
146
- const NativeAttachments = entry.component;
147
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
148
- style: { display: "contents" },
149
- onClickCapture: (event) => {
150
- const button = event.target.closest("button"), image = button?.querySelector("img");
151
- const item = image && attachments.find((item) => item.previewUrl === image.src);
152
- if (!item || event.button !== 0 || event.ctrlKey || event.metaKey || event.altKey) return;
153
- event.preventDefault();
154
- event.stopPropagation();
155
- openPreview(props, {
156
- id: item.id,
157
- src: item.previewUrl,
158
- name: item.file.name,
159
- width: item.width,
160
- height: item.height,
161
- bytes: item.file.size
162
- }, button, true);
163
- },
164
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NativeAttachments, {
165
- ...props,
166
- t: nativeTranslate
167
- })
168
- });
74
+ //#region src/sketch-document.js
75
+ const SKETCH_SIZE = 1024;
76
+ const MAX_SKETCH_STROKES = 2e3;
77
+ const MAX_STROKE_POINTS = 2e3;
78
+ function sketchPoint(clientX, clientY, rect) {
79
+ if (!(rect.width > 0 && rect.height > 0)) return void 0;
80
+ return {
81
+ x: Math.max(0, Math.min(1, (clientX - rect.left) / rect.width)),
82
+ y: Math.max(0, Math.min(1, (clientY - rect.top) / rect.height))
83
+ };
169
84
  }
170
- function MessageImagePreview({ image, ...props }) {
171
- const { loadImage, t } = props;
172
- const [src, setSrc] = (0, react.useState)(image.preview?.url);
173
- const [failed, setFailed] = (0, react.useState)(false);
174
- const [attempt, setAttempt] = (0, react.useState)(0);
175
- (0, react.useEffect)(() => {
176
- if (image.preview) {
177
- setSrc(image.preview.url);
178
- return;
179
- }
180
- let live = true;
181
- setSrc(void 0);
182
- setFailed(false);
183
- Promise.resolve().then(() => loadImage(image.attachment)).then((value) => {
184
- if (live) setSrc(value);
185
- }, () => {
186
- if (live) setFailed(true);
187
- });
188
- return () => {
189
- live = false;
190
- };
191
- }, [
192
- image,
193
- loadImage,
194
- attempt
195
- ]);
196
- const item = image.attachment ?? image.preview;
197
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
198
- type: "button",
199
- className: "codexImageThumb",
200
- "aria-label": `${t("imagePreview")} ${item.name ?? ""}`,
201
- disabled: !src && !failed,
202
- onClick: (event) => {
203
- if (failed) {
204
- setAttempt((value) => value + 1);
205
- return;
85
+ function paintSketch(context, strokes, size = SKETCH_SIZE, transparent = false, height = size, start = 0, end = strokes.length) {
86
+ context.globalCompositeOperation = "source-over";
87
+ context.globalAlpha = 1;
88
+ if (!transparent) {
89
+ context.fillStyle = "#ffffff";
90
+ context.fillRect(0, 0, size, height);
91
+ }
92
+ context.lineCap = "round";
93
+ context.lineJoin = "round";
94
+ for (let index = start; index < end; index++) {
95
+ const stroke = strokes[index];
96
+ const first = stroke.points[0];
97
+ if (!first) continue;
98
+ context.globalCompositeOperation = stroke.shape === "eraser" ? "destination-out" : "source-over";
99
+ context.globalAlpha = (stroke.opacity ?? 1) * (stroke.brush === "marker" ? .28 : stroke.brush === "pencil" ? .65 : 1);
100
+ context.strokeStyle = stroke.color;
101
+ context.fillStyle = stroke.color;
102
+ context.lineWidth = stroke.width * (stroke.brush === "pencil" ? .55 : 1) * (stroke.pressure ?? 1);
103
+ context.beginPath();
104
+ const last = stroke.points.at(-1);
105
+ if (stroke.shape === "text") {
106
+ const x = Math.min(first.x, last.x) * size, y = Math.min(first.y, last.y) * height, w = Math.abs(last.x - first.x) * size, h = Math.abs(last.y - first.y) * height;
107
+ const lines = stroke.text.split("\n"), fontSize = Math.min(stroke.width, h / Math.max(1, lines.length) / 1.2);
108
+ context.font = `${fontSize}px system-ui, sans-serif`;
109
+ context.textBaseline = "top";
110
+ lines.forEach((line, i) => context.fillText(line, x, y + i * fontSize * 1.2, w));
111
+ } else if (stroke.shape === "arrow") {
112
+ const x = last.x * size, y = last.y * height, a = Math.atan2(y - first.y * height, x - first.x * size), head = Math.min(Math.hypot(x - first.x * size, y - first.y * height) * .4, Math.max(12, stroke.width * 3));
113
+ context.moveTo(first.x * size, first.y * height);
114
+ context.lineTo(x, y);
115
+ context.stroke();
116
+ context.beginPath();
117
+ context.moveTo(x, y);
118
+ context.lineTo(x - head * Math.cos(a - .5), y - head * Math.sin(a - .5));
119
+ context.lineTo(x - head * Math.cos(a + .5), y - head * Math.sin(a + .5));
120
+ context.closePath();
121
+ context.fill();
122
+ } else if (stroke.shape === "bezier") {
123
+ context.moveTo(first.x * size, first.y * height);
124
+ for (let i = 1; i < stroke.points.length; i += 3) {
125
+ const [a, b, c] = stroke.points.slice(i, i + 3);
126
+ context.bezierCurveTo(a.x * size, a.y * height, b.x * size, b.y * height, c.x * size, c.y * height);
206
127
  }
207
- openPreview(props, {
208
- id: item.attachmentId ?? src,
209
- src,
210
- name: item.name ?? "image.png",
211
- width: item.width,
212
- height: item.height
213
- }, event.currentTarget);
214
- },
215
- children: src ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
216
- src,
217
- alt: item.name ?? "image"
218
- }) : failed ? t("accountRetry") : "…"
219
- });
220
- }
221
- function MessageImagePreviews({ images, align, ...props }) {
222
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
223
- className: "codexMessageImages",
224
- "data-align": align,
225
- "data-single": images.length === 1,
226
- children: images.map((image, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessageImagePreview, {
227
- image,
228
- ...props
229
- }, image.attachment?.attachmentId ?? image.preview?.url ?? index))
230
- });
128
+ if (stroke.fill) {
129
+ context.closePath();
130
+ context.fill();
131
+ } else context.stroke();
132
+ } else if (stroke.shape === "line") {
133
+ context.moveTo(first.x * size, first.y * height);
134
+ context.lineTo(last.x * size, last.y * height);
135
+ context.stroke();
136
+ } else if (stroke.shape === "rectangle") {
137
+ context.rect(first.x * size, first.y * height, (last.x - first.x) * size, (last.y - first.y) * height);
138
+ if (stroke.fill) context.fill();
139
+ else context.stroke();
140
+ } else if (stroke.shape === "circle") {
141
+ context.ellipse((first.x + last.x) * size / 2, (first.y + last.y) * height / 2, Math.abs(last.x - first.x) * size / 2, Math.abs(last.y - first.y) * height / 2, 0, 0, Math.PI * 2);
142
+ if (stroke.fill) context.fill();
143
+ else context.stroke();
144
+ } else if (stroke.shape === "polygon") {
145
+ context.moveTo(first.x * size, first.y * height);
146
+ for (const point of stroke.points.slice(1)) context.lineTo(point.x * size, point.y * height);
147
+ context.closePath();
148
+ if (stroke.fill) context.fill();
149
+ else context.stroke();
150
+ } else if (stroke.points.length === 1) {
151
+ context.arc(first.x * size, first.y * height, context.lineWidth / 2, 0, Math.PI * 2);
152
+ context.fill();
153
+ } else {
154
+ context.moveTo(first.x * size, first.y * height);
155
+ for (let i = 1; i < stroke.points.length - 1; i++) {
156
+ const point = stroke.points[i], next = stroke.points[i + 1];
157
+ if (context.quadraticCurveTo) context.quadraticCurveTo(point.x * size, point.y * height, (point.x + next.x) * size / 2, (point.y + next.y) * height / 2);
158
+ else context.lineTo(point.x * size, point.y * height);
159
+ }
160
+ context.lineTo(last.x * size, last.y * height);
161
+ context.stroke();
162
+ }
163
+ }
164
+ context.globalCompositeOperation = "source-over";
165
+ context.globalAlpha = 1;
231
166
  }
232
- const IMAGE_PREVIEWS_CSS = `
233
- .codexMessageImages{display:flex;gap:10px;max-width:100%;padding:8px 0;overflow-x:auto}
234
- .codexImageThumb{display:grid;place-items:center;width:64px;height:64px;padding:0;border:1px solid var(--dsw-alias-border-l2-darkmode-thin);border-radius:14px;background:var(--dsw-alias-interactive-bg-hover);color:inherit;overflow:hidden;cursor:zoom-in;flex:none}
235
- .codexImageThumb img{width:100%;height:100%;object-fit:cover}
236
- .codexImageThumb:focus-visible{outline:2px solid #4598ed;outline-offset:2px}
237
- .codexMessageImages{flex-wrap:wrap}.codexMessageImages[data-align=end]{justify-content:flex-end}
238
- .codexMessageImages[data-single=true] .codexImageThumb{width:240px;height:auto;max-width:100%}
239
- .codexMessageImages[data-single=true] img{height:auto;max-height:320px;object-fit:contain}
240
- `;
241
- //#endregion
242
- //#region src/image-conversation-node.js
243
- const KIND = "codex-image-output";
244
- const resultsOf = (event) => event?.type === "tool/result" && event.data.meta?.kind === "codex-subscription-image" ? (event.data.message?.content ?? []).filter((block) => block.type === "tool-result" && !block.isError && block.content?.some((part) => part.type === "image" && part.attachment)) : [];
245
- const imageConversationNode = {
246
- kind: KIND,
247
- target: "chat",
248
- match(event) {
249
- if (event.type !== "turn/end" && resultsOf(event).length === 0) return null;
167
+ const createSketchLayers = () => ({
168
+ active: 1,
169
+ nextId: 2,
170
+ layers: [{
171
+ id: 1,
172
+ name: "",
173
+ visible: true,
174
+ strokes: []
175
+ }]
176
+ });
177
+ const strokeCount = (doc) => doc.layers.reduce((n, layer) => n + layer.strokes.length, 0);
178
+ function changeSketchLayer(doc, action, id = doc.active, value) {
179
+ const index = doc.layers.findIndex((layer) => layer.id === id);
180
+ if (index < 0) return doc;
181
+ const layers = doc.layers.slice(), layer = layers[index];
182
+ if (action === "select") return {
183
+ ...doc,
184
+ active: id
185
+ };
186
+ if (action === "add" || action === "duplicate") {
187
+ if (layers.length >= 8 || action === "duplicate" && strokeCount(doc) + layer.strokes.length > 2e3) return doc;
188
+ const next = action === "add" ? {
189
+ id: doc.nextId,
190
+ name: "",
191
+ visible: true,
192
+ strokes: []
193
+ } : {
194
+ ...layer,
195
+ id: doc.nextId,
196
+ strokes: layer.strokes.slice()
197
+ };
198
+ layers.splice(index + 1, 0, next);
250
199
  return {
251
- id: String(event.data.turn),
252
- role: "update"
200
+ ...doc,
201
+ layers,
202
+ active: next.id,
203
+ nextId: doc.nextId + 1
253
204
  };
254
- },
255
- start: () => void 0,
256
- update: (context) => context.state,
257
- buildViewNode(context) {
258
- const end = context.matches.find((match) => match.event.type === "turn/end");
259
- if (!end) return null;
260
- const blocks = context.matches.flatMap(({ event }) => resultsOf(event).map((block) => ({
261
- ...block,
262
- kind: "tool-result",
263
- meta: event.data.meta
264
- })));
265
- if (!blocks.length) return null;
266
- const answer = end.location.turn?.steps?.at(-1)?.data.get("assistant-step");
267
- const lastResultSeq = Math.max(...context.matches.filter((match) => resultsOf(match.event).length).map((match) => match.event.seq));
268
- const answerSeq = answer?.finalNode?.seq;
269
- const anchorSeq = answerSeq > lastResultSeq ? answerSeq + .025 : end.event.seq - .025;
205
+ }
206
+ if (action === "delete") {
207
+ if (layers.length === 1) return doc;
208
+ layers.splice(index, 1);
270
209
  return {
271
- key: context.key,
272
- id: context.id,
273
- kind: KIND,
274
- target: "chat",
275
- location: end.location,
276
- anchorSeq,
277
- visibility: "visible",
278
- data: { blocks }
210
+ ...doc,
211
+ layers,
212
+ active: doc.active === id ? layers[Math.min(index, layers.length - 1)].id : doc.active
279
213
  };
280
214
  }
215
+ if (action === "up" || action === "down") {
216
+ const target = index + (action === "up" ? 1 : -1);
217
+ if (!layers[target]) return doc;
218
+ [layers[index], layers[target]] = [layers[target], layer];
219
+ } else if (action === "visible") layers[index] = {
220
+ ...layer,
221
+ visible: !layer.visible
222
+ };
223
+ else if (action === "rename") layers[index] = {
224
+ ...layer,
225
+ name: String(value).trim().slice(0, 40)
226
+ };
227
+ else if (action === "clear") layers[index] = {
228
+ ...layer,
229
+ strokes: [],
230
+ image: void 0
231
+ };
232
+ else return doc;
233
+ return {
234
+ ...doc,
235
+ layers
236
+ };
237
+ }
238
+ const distanceToSegment = (p, a, b) => {
239
+ const dx = b.x - a.x, dy = b.y - a.y, length = dx * dx + dy * dy;
240
+ const k = length ? Math.max(0, Math.min(1, ((p.x - a.x) * dx + (p.y - a.y) * dy) / length)) : 0;
241
+ return Math.hypot(p.x - a.x - k * dx, p.y - a.y - k * dy);
281
242
  };
282
- //#endregion
283
- //#region node_modules/.pnpm/@heroicons+react@2.2.0_react@18.3.1/node_modules/@heroicons/react/24/outline/esm/ArrowPathIcon.js
284
- function ArrowPathIcon({ title, titleId, ...props }, svgRef) {
285
- return /*#__PURE__*/ react.createElement("svg", Object.assign({
286
- xmlns: "http://www.w3.org/2000/svg",
287
- fill: "none",
288
- viewBox: "0 0 24 24",
289
- strokeWidth: 1.5,
290
- stroke: "currentColor",
291
- "aria-hidden": "true",
292
- "data-slot": "icon",
293
- ref: svgRef,
294
- "aria-labelledby": titleId
295
- }, props), title ? /*#__PURE__*/ react.createElement("title", { id: titleId }, title) : null, /*#__PURE__*/ react.createElement("path", {
296
- strokeLinecap: "round",
297
- strokeLinejoin: "round",
298
- d: "M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99"
243
+ function strokeHit(stroke, point, radius, width = SKETCH_SIZE, height = width) {
244
+ let points = stroke.shape === "bezier" ? flattenSketchCurve(stroke, width, height) : stroke.points;
245
+ if (!points.length) return false;
246
+ const a = points[0], b = points.at(-1);
247
+ if ((stroke.shape === "text" || stroke.fill && stroke.shape === "rectangle") && point.x >= Math.min(a.x, b.x) && point.x <= Math.max(a.x, b.x) && point.y >= Math.min(a.y, b.y) && point.y <= Math.max(a.y, b.y)) return true;
248
+ if (stroke.fill && stroke.shape === "circle") {
249
+ const rx = Math.abs(b.x - a.x) / 2, ry = Math.abs(b.y - a.y) / 2;
250
+ if (rx && ry && ((point.x - (a.x + b.x) / 2) / rx) ** 2 + ((point.y - (a.y + b.y) / 2) / ry) ** 2 <= 1) return true;
251
+ }
252
+ if (stroke.shape === "polygon" || stroke.shape === "bezier" && stroke.fill) {
253
+ if (stroke.fill) {
254
+ let inside = false;
255
+ for (let i = 0, j = points.length - 1; i < points.length; j = i++) {
256
+ const p = points[i], q = points[j];
257
+ if (p.y > point.y !== q.y > point.y && point.x < (q.x - p.x) * (point.y - p.y) / (q.y - p.y) + p.x) inside = !inside;
258
+ }
259
+ if (inside) return true;
260
+ }
261
+ points = [...points, points[0]];
262
+ }
263
+ if (stroke.shape === "rectangle") points = [
264
+ a,
265
+ {
266
+ x: b.x,
267
+ y: a.y
268
+ },
269
+ b,
270
+ {
271
+ x: a.x,
272
+ y: b.y
273
+ },
274
+ a
275
+ ];
276
+ if (stroke.shape === "circle") points = Array.from({ length: 65 }, (_, i) => ({
277
+ x: (a.x + b.x) / 2 + Math.abs(b.x - a.x) / 2 * Math.cos(i * Math.PI / 32),
278
+ y: (a.y + b.y) / 2 + Math.abs(b.y - a.y) / 2 * Math.sin(i * Math.PI / 32)
299
279
  }));
300
- }
301
- const ForwardRef$3 = /*#__PURE__*/ react.forwardRef(ArrowPathIcon);
302
- //#endregion
303
- //#region node_modules/.pnpm/@heroicons+react@2.2.0_react@18.3.1/node_modules/@heroicons/react/24/outline/esm/ExclamationCircleIcon.js
304
- function ExclamationCircleIcon({ title, titleId, ...props }, svgRef) {
305
- return /*#__PURE__*/ react.createElement("svg", Object.assign({
306
- xmlns: "http://www.w3.org/2000/svg",
307
- fill: "none",
308
- viewBox: "0 0 24 24",
309
- strokeWidth: 1.5,
310
- stroke: "currentColor",
311
- "aria-hidden": "true",
312
- "data-slot": "icon",
313
- ref: svgRef,
314
- "aria-labelledby": titleId
315
- }, props), title ? /*#__PURE__*/ react.createElement("title", { id: titleId }, title) : null, /*#__PURE__*/ react.createElement("path", {
316
- strokeLinecap: "round",
317
- strokeLinejoin: "round",
318
- d: "M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z"
280
+ points = points.map((p) => ({
281
+ x: p.x * width,
282
+ y: p.y * height
319
283
  }));
284
+ point = {
285
+ x: point.x * width,
286
+ y: point.y * height
287
+ };
288
+ const tolerance = radius + stroke.width / 2;
289
+ return points.some((p, i) => distanceToSegment(point, i ? points[i - 1] : p, p) <= tolerance);
320
290
  }
321
- const ForwardRef$2 = /*#__PURE__*/ react.forwardRef(ExclamationCircleIcon);
322
- //#endregion
323
- //#region node_modules/.pnpm/@heroicons+react@2.2.0_react@18.3.1/node_modules/@heroicons/react/24/outline/esm/PhotoIcon.js
324
- function PhotoIcon({ title, titleId, ...props }, svgRef) {
325
- return /*#__PURE__*/ react.createElement("svg", Object.assign({
326
- xmlns: "http://www.w3.org/2000/svg",
327
- fill: "none",
328
- viewBox: "0 0 24 24",
329
- strokeWidth: 1.5,
330
- stroke: "currentColor",
331
- "aria-hidden": "true",
332
- "data-slot": "icon",
333
- ref: svgRef,
334
- "aria-labelledby": titleId
335
- }, props), title ? /*#__PURE__*/ react.createElement("title", { id: titleId }, title) : null, /*#__PURE__*/ react.createElement("path", {
336
- strokeLinecap: "round",
337
- strokeLinejoin: "round",
338
- d: "m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z"
339
- }));
340
- }
341
- const ForwardRef$1 = /*#__PURE__*/ react.forwardRef(PhotoIcon);
342
- const ORIGINAL_IMAGE_ID_PATTERN = /^img_[0-9a-f]{32}$/u;
343
- const positiveInteger = (value) => Number.isSafeInteger(value) && value > 0;
344
- function decodeOriginalImageRef(value) {
345
- if (value === null || typeof value !== "object" || Array.isArray(value) || typeof value.assetId !== "string" || !ORIGINAL_IMAGE_ID_PATTERN.test(value.assetId) || value.mediaType !== "image/png" || !positiveInteger(value.bytes) || value.bytes > 48 * 1024 * 1024 || !positiveInteger(value.width) || !positiveInteger(value.height) || typeof value.name !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(value.name) || typeof value.sha256 !== "string" || !/^[0-9a-f]{64}$/u.test(value.sha256)) return void 0;
291
+ const SKETCH_RATIOS = Object.freeze({
292
+ "1:1": [1024, 1024],
293
+ "4:3": [1024, 768],
294
+ "3:4": [768, 1024],
295
+ "16:9": [1024, 576],
296
+ "9:16": [576, 1024]
297
+ });
298
+ function resizeSketch(doc, ratio) {
299
+ if (!Object.hasOwn(SKETCH_RATIOS, ratio)) throw new Error("Invalid sketch ratio");
300
+ const [width, height] = SKETCH_RATIOS[ratio];
301
+ const oldWidth = doc.width ?? 1024, oldHeight = doc.height ?? 1024;
302
+ if (width === oldWidth && height === oldHeight) return doc;
303
+ const scale = Math.min(width / oldWidth, height / oldHeight);
304
+ const dx = (width - oldWidth * scale) / 2, dy = (height - oldHeight * scale) / 2;
346
305
  return {
347
- assetId: value.assetId,
348
- mediaType: value.mediaType,
349
- bytes: value.bytes,
350
- width: value.width,
351
- height: value.height,
352
- name: value.name,
353
- sha256: value.sha256
306
+ ...doc,
307
+ width,
308
+ height,
309
+ ratio,
310
+ layers: doc.layers.map((layer) => ({
311
+ ...layer,
312
+ ...layer.image ? { image: {
313
+ ...layer.image,
314
+ x: (layer.image.x * oldWidth * scale + dx) / width,
315
+ y: (layer.image.y * oldHeight * scale + dy) / height,
316
+ width: layer.image.width * oldWidth * scale / width,
317
+ height: layer.image.height * oldHeight * scale / height
318
+ } } : {},
319
+ strokes: layer.strokes.map((stroke) => ({
320
+ ...stroke,
321
+ width: stroke.width * scale,
322
+ points: stroke.points.map((p) => ({
323
+ x: (p.x * oldWidth * scale + dx) / width,
324
+ y: (p.y * oldHeight * scale + dy) / height
325
+ }))
326
+ }))
327
+ }))
354
328
  };
355
329
  }
356
- function decodeImagePresentation(value) {
357
- if (value === null || typeof value !== "object" || Array.isArray(value) || value.kind !== "codex-subscription-image" || value.schemaVersion !== 1) return void 0;
358
- const original = decodeOriginalImageRef(value.original);
359
- return original === void 0 ? void 0 : { original };
360
- }
361
- function originalImageRefsEqual(left, right) {
362
- const a = decodeOriginalImageRef(left);
363
- const b = decodeOriginalImageRef(right);
364
- return a !== void 0 && b !== void 0 && a.assetId === b.assetId && a.mediaType === b.mediaType && a.bytes === b.bytes && a.width === b.width && a.height === b.height && a.name === b.name && a.sha256 === b.sha256;
365
- }
366
330
  //#endregion
367
- //#region src/rpc-contract.js
368
- const CHANNEL = "/codex-subscription";
369
- const RPC_ENDPOINTS = Object.freeze([
370
- "status",
371
- "login/start",
372
- "login/status",
373
- "login/submit",
374
- "login/cancel",
375
- "logout",
376
- "account/select",
377
- "account/remove",
378
- "usage",
379
- "diagnostics",
380
- "preferences/status",
381
- "preferences/models",
382
- "preferences/update",
383
- "reset-credit/inspect",
384
- "reset-credit/prepare",
385
- "reset-credit/consume",
386
- "image/original/chunk",
387
- "sketch/connect",
388
- "sketch/poll",
389
- "sketch/claim",
390
- "sketch/result",
391
- "sketch/disconnect"
392
- ]);
393
- function createSubscriptionRpcClient(transport) {
394
- return Object.freeze({ call(channel, endpoint, payload, signal) {
395
- if (channel !== "/codex-subscription" || !RPC_ENDPOINTS.includes(endpoint)) throw new Error("Invalid subscription RPC target");
396
- return transport.call("/api", `codex-subscription/${endpoint}`, payload, signal);
397
- } });
331
+ //#region src/sketch-session-state.js
332
+ function createSketchSessionState() {
333
+ const ref = (current) => ({ current });
334
+ return {
335
+ doc: ref(createSketchLayers()),
336
+ undo: ref([]),
337
+ redo: ref([]),
338
+ images: ref(/* @__PURE__ */ new Map()),
339
+ saved: ref(null),
340
+ dirty: ref(false),
341
+ documentId: ref(crypto.randomUUID()),
342
+ documentRevision: ref(0),
343
+ agentAdapter: ref({}),
344
+ agentSession: ref(null),
345
+ agentRun: ref(null)
346
+ };
398
347
  }
399
- function unwrap(response) {
400
- if (!response?.ok) throw new Error(response?.error?.message ?? "Codex RPC failed");
401
- return response.value;
348
+ function createSketchSessionRegistry() {
349
+ const sessions = /* @__PURE__ */ new Map();
350
+ return {
351
+ get(id) {
352
+ if (!sessions.has(id)) sessions.set(id, createSketchSessionState());
353
+ return sessions.get(id);
354
+ },
355
+ dispose() {
356
+ for (const value of sessions.values()) value.agentRun.current?.dispose();
357
+ sessions.clear();
358
+ }
359
+ };
402
360
  }
403
361
  //#endregion
404
- //#region src/original-image-download.js
405
- function decodeBase64Chunk(value) {
406
- if (typeof value !== "string" || value.length === 0 || value.length > Math.ceil(4194304 / 3) * 4 + 8) throw new Error("Invalid original image chunk");
407
- let decoded;
408
- try {
409
- decoded = atob(value);
410
- } catch {
411
- throw new Error("Invalid original image chunk");
412
- }
413
- const bytes = new Uint8Array(decoded.length);
414
- for (let index = 0; index < decoded.length; index += 1) bytes[index] = decoded.charCodeAt(index);
415
- return bytes;
416
- }
417
- /** Keep only the destination and current chunk, while verifying every reply. */
418
- async function readOriginalImage(rpc, sessionId, original, { signal, onProgress } = {}) {
419
- signal?.throwIfAborted();
420
- original = decodeOriginalImageRef(original);
421
- if (original === void 0) throw new Error("Invalid original image reference");
422
- const data = new Uint8Array(original.bytes);
423
- let total = 0;
424
- let done = false;
425
- while (!done) {
426
- signal?.throwIfAborted();
427
- const response = await rpc.call(CHANNEL, "image/original/chunk", {
428
- sessionId,
429
- assetId: original.assetId,
430
- offset: total
431
- }, signal);
432
- signal?.throwIfAborted();
433
- if (!response?.ok) throw new Error(response?.error?.message ?? "Codex RPC failed");
434
- const chunk = response.value;
435
- if (!originalImageRefsEqual(chunk?.ref, original) || chunk.offset !== total || typeof chunk.done !== "boolean") throw new Error("Original image metadata changed");
436
- const bytes = decodeBase64Chunk(chunk.encoded);
437
- if (bytes.byteLength === 0 || total + bytes.byteLength > original.bytes) throw new Error("Original image download is incomplete");
438
- data.set(bytes, total);
439
- total += bytes.byteLength;
440
- done = chunk.done;
441
- onProgress?.({
442
- loaded: total,
443
- total: original.bytes
444
- });
445
- }
446
- if (total !== original.bytes) throw new Error("Original image download is incomplete");
447
- const digest = await crypto.subtle.digest("SHA-256", data);
448
- signal?.throwIfAborted();
449
- if ([...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("") !== original.sha256) throw new Error("Original image integrity check failed");
450
- return data;
362
+ //#region src/image-edit.js
363
+ const isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
364
+ const validCoordinate = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
365
+ const cleanNote = (value) => typeof value === "string" ? value.trim() : "";
366
+ const coordinateError = (number) => {
367
+ const error = /* @__PURE__ */ new Error(`Annotation ${number} must have finite x and y coordinates between 0 and 1`);
368
+ error.code = "ANNOTATION_INVALID";
369
+ return error;
370
+ };
371
+ /**
372
+ * Validate the annotation contract shared by the draft builder and the
373
+ * reference-image renderer. Annotation numbers are their array positions so
374
+ * that they stay aligned with the pins shown to the user.
375
+ */
376
+ function normalizeImageEditAnnotations(annotations, { requireNotes = false } = {}) {
377
+ if (!Array.isArray(annotations)) throw new TypeError("annotations must be an array");
378
+ return annotations.map((annotation, index) => {
379
+ const number = index + 1;
380
+ const note = cleanNote(annotation?.note);
381
+ if (requireNotes && note === "") {
382
+ const error = /* @__PURE__ */ new Error(`Annotation ${number} is missing a note; describe what should change`);
383
+ error.code = "ANNOTATION_INVALID";
384
+ throw error;
385
+ }
386
+ if (!isRecord(annotation) || !validCoordinate(annotation.x) || !validCoordinate(annotation.y)) throw coordinateError(number);
387
+ return {
388
+ number,
389
+ x: annotation.x,
390
+ y: annotation.y,
391
+ note
392
+ };
393
+ });
451
394
  }
452
- //#endregion
453
- //#region src/client-images.jsx
454
- const imageDownloadName = (attachment) => {
455
- const fallback = "codex-generated-image.png";
456
- if (typeof attachment?.name !== "string") return fallback;
457
- const cleaned = attachment.name.replace(/[<>:"/\\|?*\u0000-\u001f]/gu, "-").replace(/[. ]+$/u, "").trim();
458
- if (cleaned === "") return fallback;
459
- return cleaned.toLowerCase().endsWith(".png") ? cleaned : `${cleaned}.png`;
395
+ const formatPercent = (value) => {
396
+ return `${Number((value * 100).toFixed(2))}%`;
460
397
  };
461
- function triggerBlobDownload(data, mediaType, filename) {
462
- const url = URL.createObjectURL(new Blob([data], { type: mediaType }));
463
- const anchor = document.createElement("a");
464
- anchor.href = url;
465
- anchor.download = filename;
466
- anchor.rel = "noopener";
467
- document.body.append(anchor);
468
- try {
469
- anchor.click();
470
- } finally {
471
- anchor.remove();
472
- URL.revokeObjectURL(url);
473
- }
398
+ const formatPixel = (value) => {
399
+ const rounded = Number(value.toFixed(2));
400
+ return String(rounded);
401
+ };
402
+ const withNames = (value, sourceName, referenceName) => String(value).replaceAll("{sourceName}", sourceName).replaceAll("{referenceName}", referenceName);
403
+ const positiveImageDimension = (value) => Number.isSafeInteger(value) && value > 0;
404
+ function buildImageEditDraft({ prompt = "", annotations = [], translate, width, height, sourceName = "source.png", referenceName = "annotated-reference.png" }) {
405
+ if (typeof translate !== "function") throw new TypeError("translate must be a function");
406
+ const base = typeof prompt === "string" && prompt.trim() !== "" ? prompt.trim() : translate("imageEditDefault");
407
+ if (!Array.isArray(annotations)) throw new TypeError("annotations must be an array");
408
+ if (annotations.length === 0) return base;
409
+ const hasWidth = width !== void 0;
410
+ if (hasWidth !== (height !== void 0) || hasWidth && (!positiveImageDimension(width) || !positiveImageDimension(height))) throw new Error("width and height must be positive integers when provided");
411
+ const normalized = normalizeImageEditAnnotations(annotations, { requireNotes: true });
412
+ const source = typeof sourceName === "string" && sourceName.trim() !== "" ? sourceName.trim() : "source.png";
413
+ const reference = typeof referenceName === "string" && referenceName.trim() !== "" ? referenceName.trim() : "annotated-reference.png";
414
+ const guide = withNames(translate("imageEditReferenceGuide"), source, reference);
415
+ const location = translate("imageEditLocation");
416
+ const notes = normalized.map(({ number, x, y, note }) => {
417
+ const pixels = hasWidth ? ` (pixel x=${formatPixel(x * Math.max(0, width - 1))} of ${width}, y=${formatPixel(y * Math.max(0, height - 1))} of ${height})` : "";
418
+ return `${number}. ${location}: x=${formatPercent(x)} (normalized ${x}), y=${formatPercent(y)} (normalized ${y})${pixels}; ${note}`;
419
+ });
420
+ return [
421
+ base,
422
+ "",
423
+ guide,
424
+ "",
425
+ translate("imageRegionNotes"),
426
+ ...notes
427
+ ].join("\n");
474
428
  }
475
- function CodexGeneratedImage({ attachment, original, rpc, sessionId, loadImage, openSketchImage, attachForEdit, getImageViewer, getInternalImageViewer, t, features }) {
476
- const [attempt, setAttempt] = (0, react.useState)(0);
477
- const [error, setError] = (0, react.useState)(false);
478
- const [src, setSrc] = (0, react.useState)();
479
- const triggerRef = (0, react.useRef)(null);
429
+ //#endregion
430
+ //#region src/client-image-previews.jsx
431
+ function openPreview(props, item, opener, sourceInDraft = false) {
432
+ const { service, preference, t, attachForEdit } = props;
433
+ const settings = preference.getSnapshot();
434
+ const referenceName = `annotated-${item.name}.png`;
435
+ service.open({
436
+ items: [{
437
+ ...item,
438
+ actions: settings.imageEditing ? [{
439
+ id: "edit",
440
+ label: t("imageEdit"),
441
+ pendingLabel: t("imageEditPreparing"),
442
+ errorLabel: t("imageEditFailed"),
443
+ closeOnSuccess: true,
444
+ onInvoke: ({ annotations }) => attachForEdit(item.src, item.name, buildImageEditDraft({
445
+ annotations,
446
+ translate: t,
447
+ sourceName: item.name,
448
+ referenceName
449
+ }), annotations, referenceName, sourceInDraft)
450
+ }, ...settings.imageSketch && props.openSketchImage ? [{
451
+ id: "sketch",
452
+ label: t("imageToSketch"),
453
+ pendingLabel: t("imageEditPreparing"),
454
+ errorLabel: t("imageEditFailed"),
455
+ onInvoke: () => props.openSketchImage(item.src, item.name)
456
+ }] : []] : []
457
+ }],
458
+ opener,
459
+ source: sourceInDraft ? "codex-draft" : "codex-message",
460
+ annotations: settings.imageAnnotations
461
+ });
462
+ }
463
+ function ComposerImagePreviews(props) {
464
+ const { attachments, service, nativeAttachments, watchNativeAttachments, nativeTranslate } = props;
465
+ const entry = (0, react.useSyncExternalStore)(watchNativeAttachments, nativeAttachments);
466
+ (0, react.useEffect)(() => {
467
+ const current = service.getSnapshot();
468
+ if (current?.source === "codex-draft" && !attachments.some((item) => item.id === current.items[0]?.id)) service.close();
469
+ }, [attachments, service]);
470
+ (0, react.useEffect)(() => () => {
471
+ if (service.getSnapshot()?.source === "codex-draft") service.close();
472
+ }, [service]);
473
+ if (!entry) return null;
474
+ const NativeAttachments = entry.component;
475
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
476
+ style: { display: "contents" },
477
+ onClickCapture: (event) => {
478
+ const button = event.target.closest("button"), image = button?.querySelector("img");
479
+ const item = image && attachments.find((item) => item.previewUrl === image.src);
480
+ if (!item || event.button !== 0 || event.ctrlKey || event.metaKey || event.altKey) return;
481
+ event.preventDefault();
482
+ event.stopPropagation();
483
+ openPreview(props, {
484
+ id: item.id,
485
+ src: item.previewUrl,
486
+ name: item.file.name,
487
+ width: item.width,
488
+ height: item.height,
489
+ bytes: item.file.size
490
+ }, button, true);
491
+ },
492
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NativeAttachments, {
493
+ ...props,
494
+ t: nativeTranslate
495
+ })
496
+ });
497
+ }
498
+ function MessageImagePreview({ image, ...props }) {
499
+ const { loadImage, t } = props;
500
+ const [src, setSrc] = (0, react.useState)(image.preview?.url);
501
+ const [failed, setFailed] = (0, react.useState)(false);
502
+ const [attempt, setAttempt] = (0, react.useState)(0);
480
503
  (0, react.useEffect)(() => {
504
+ if (image.preview) {
505
+ setSrc(image.preview.url);
506
+ return;
507
+ }
481
508
  let live = true;
482
- setError(false);
483
509
  setSrc(void 0);
484
- Promise.resolve().then(() => loadImage(attachment)).then((value) => {
510
+ setFailed(false);
511
+ Promise.resolve().then(() => loadImage(image.attachment)).then((value) => {
485
512
  if (live) setSrc(value);
486
- }).catch(() => {
487
- if (live) setError(true);
513
+ }, () => {
514
+ if (live) setFailed(true);
488
515
  });
489
516
  return () => {
490
517
  live = false;
491
518
  };
492
519
  }, [
493
- attachment,
520
+ image,
494
521
  loadImage,
495
522
  attempt
496
523
  ]);
497
- const label = attachment.name ?? t("imageLabel");
498
- const downloadName = imageDownloadName(attachment);
499
- const downloadOriginal = async ({ signal, onProgress } = {}) => {
500
- if (original === void 0) return;
501
- triggerBlobDownload(await readOriginalImage(rpc, sessionId, original, {
502
- signal,
503
- onProgress
504
- }), original.mediaType, original.name);
505
- };
506
- const openImage = () => {
507
- if (src === void 0) return;
508
- const request = {
509
- items: [{
510
- id: attachment.attachmentId ?? downloadName,
511
- src,
512
- name: label,
513
- width: attachment.width,
514
- height: attachment.height,
515
- bytes: attachment.bytes,
516
- download: original === void 0 ? void 0 : {
517
- pendingLabel: t("imageDownloadPreparing"),
518
- errorLabel: t("imageDownloadFailed"),
519
- onInvoke: downloadOriginal
520
- },
521
- actions: !features.imageEditing ? [] : [{
522
- id: "continue-editing",
523
- label: t("imageEdit"),
524
- pendingLabel: t("imageEditPreparing"),
525
- errorLabel: t("imageEditFailed"),
526
- closeOnSuccess: true,
527
- onInvoke: ({ annotations = [] }) => {
528
- const imageKey = String(attachment.attachmentId ?? "image").replace(/[^a-zA-Z0-9_-]/g, "_");
529
- const sourceName = annotations.length === 0 ? downloadName : `codex-edit-${imageKey}-source.png`;
530
- const referenceName = `codex-edit-${imageKey}-annotations.png`;
531
- return attachForEdit(src, sourceName, buildImageEditDraft({
532
- annotations,
533
- translate: t,
534
- width: attachment.width,
535
- height: attachment.height,
536
- sourceName,
537
- referenceName
538
- }), annotations, referenceName);
539
- }
540
- }, ...features.imageSketch && openSketchImage ? [{
541
- id: "sketch",
542
- label: t("imageToSketch"),
543
- pendingLabel: t("imageEditPreparing"),
544
- errorLabel: t("imageEditFailed"),
545
- onInvoke: () => openSketchImage(src, downloadName)
546
- }] : []]
547
- }],
548
- opener: triggerRef.current,
549
- source: "codex-generated",
550
- annotations: features.imageViewer && features.imageAnnotations
551
- };
552
- if (features.imageViewer && getInternalImageViewer?.()?.open?.(request) === true) return;
553
- if ((getImageViewer?.())?.open?.(request) === true) return;
554
- if (features.imageViewer) getInternalImageViewer?.()?.open?.({
555
- ...request,
556
- annotations: false
557
- });
558
- else window.open(src, "_blank", "noopener,noreferrer");
559
- };
560
- if (error) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
561
- type: "button",
562
- className: "codexGeneratedImageRetry",
563
- onClick: () => setAttempt((value) => value + 1),
564
- children: t("imageLoadFailed")
565
- });
524
+ const item = image.attachment ?? image.preview;
566
525
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
567
- ref: triggerRef,
568
526
  type: "button",
569
- className: "codexGeneratedImageFrame",
570
- title: t("imageOpen"),
571
- "aria-label": t("imageOpenNamed").replace("{value}", String(label)),
572
- onClick: openImage,
573
- children: src === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("imageLoading") }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
527
+ className: "codexImageThumb",
528
+ "aria-label": `${t("imagePreview")} ${item.name ?? ""}`,
529
+ disabled: !src && !failed,
530
+ onClick: (event) => {
531
+ if (failed) {
532
+ setAttempt((value) => value + 1);
533
+ return;
534
+ }
535
+ openPreview(props, {
536
+ id: item.attachmentId ?? src,
537
+ src,
538
+ name: item.name ?? "image.png",
539
+ width: item.width,
540
+ height: item.height
541
+ }, event.currentTarget);
542
+ },
543
+ children: src ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
574
544
  src,
575
- alt: label
576
- })
545
+ alt: item.name ?? "image"
546
+ }) : failed ? t("accountRetry") : "…"
577
547
  });
578
548
  }
579
- function CodexImageToolRow({ presentation = "tool", block, sessionId, rpc, loadImage, openSketchImage, attachForEdit, getImageViewer, getInternalImageViewer, t, preference }) {
580
- const features = (0, react.useSyncExternalStore)(preference.subscribe, preference.getSnapshot);
581
- const settled = block?.kind === "tool-result";
582
- const image = settled ? block.content.find((item) => item?.type === "image" && item.attachment !== void 0) : void 0;
583
- const failed = settled && block.isError === true;
584
- const state = !settled ? "running" : failed ? "error" : "done";
585
- const status = !settled ? t("imageGenerating") : failed ? t("imageFailed") : t("imageGenerated");
586
- const error = failed ? block.content.find((item) => item?.type === "text" && typeof item.text === "string")?.text : void 0;
587
- const original = decodeImagePresentation(block?.meta)?.original;
588
- const showOutput = presentation === "output";
589
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
590
- className: "codexImageTool",
591
- "data-state": state,
592
- children: [
593
- showOutput ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
594
- className: "codexImageToolRow",
595
- children: [
596
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(!settled ? ForwardRef$3 : failed ? ForwardRef$2 : ForwardRef$1, {
597
- className: "codexImageToolIcon",
598
- "aria-hidden": "true"
599
- }),
600
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
601
- className: "codexImageToolTitle",
602
- children: t("imageGenerate")
603
- }),
604
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
605
- className: "codexImageBeta",
606
- children: t("imageBeta")
607
- }),
608
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
609
- className: "codexImageToolState",
610
- children: status
611
- })
612
- ]
613
- }),
614
- !showOutput || image === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
615
- className: "codexImageToolGallery",
616
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CodexGeneratedImage, {
617
- features,
618
- attachment: image.attachment,
619
- original,
620
- rpc,
621
- sessionId,
622
- loadImage,
623
- openSketchImage,
624
- attachForEdit,
625
- getImageViewer,
626
- getInternalImageViewer,
627
- t
628
- })
629
- }),
630
- !showOutput && typeof block?.meta?.requestedModel === "string" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
631
- className: "codexImageDetails",
632
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", { children: t("imageDetails") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [
633
- t("imageRequestedModel"),
634
- ": ",
635
- block.meta.requestedModel.slice(0, 100),
636
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("br", {}),
637
- t("imageReportedModel"),
638
- ": ",
639
- typeof block.meta.reportedModel === "string" ? block.meta.reportedModel.slice(0, 100) : t("imageModelUnreported"),
640
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("br", {}),
641
- t("imageRequestedSize"),
642
- ": ",
643
- String(block.meta.requestedSize ?? "auto").slice(0, 40),
644
- " · ",
645
- t("imageActualSize"),
646
- ": ",
647
- original?.width,
648
- " × ",
649
- original?.height
650
- ] })]
651
- }) : null,
652
- error === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
653
- className: "codexImageToolError",
654
- children: error
655
- })
656
- ]
657
- });
658
- }
659
- function CodexImageOutput({ node, ...props }) {
549
+ function MessageImagePreviews({ images, align, ...props }) {
660
550
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
661
- className: "codexImageOutput",
662
- children: node.data.blocks.map((block) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CodexImageToolRow, {
663
- block,
664
- ...props,
665
- presentation: "output"
666
- }, block.toolCallId))
551
+ className: "codexMessageImages",
552
+ "data-align": align,
553
+ "data-single": images.length === 1,
554
+ children: images.map((image, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessageImagePreview, {
555
+ image,
556
+ ...props
557
+ }, image.attachment?.attachmentId ?? image.preview?.url ?? index))
667
558
  });
668
559
  }
560
+ const IMAGE_PREVIEWS_CSS = `
561
+ .codexMessageImages{display:flex;gap:10px;max-width:100%;padding:8px 0;overflow-x:auto}
562
+ .codexImageThumb{display:grid;place-items:center;width:64px;height:64px;padding:0;border:1px solid var(--dsw-alias-border-l2-darkmode-thin);border-radius:14px;background:var(--dsw-alias-interactive-bg-hover);color:inherit;overflow:hidden;cursor:zoom-in;flex:none}
563
+ .codexImageThumb img{width:100%;height:100%;object-fit:cover}
564
+ .codexImageThumb:focus-visible{outline:2px solid #4598ed;outline-offset:2px}
565
+ .codexMessageImages{flex-wrap:wrap}.codexMessageImages[data-align=end]{justify-content:flex-end}
566
+ .codexMessageImages[data-single=true] .codexImageThumb{width:240px;height:auto;max-width:100%}
567
+ .codexMessageImages[data-single=true] img{height:auto;max-height:320px;object-fit:contain}
568
+ `;
669
569
  //#endregion
670
- //#region src/sketch-document.js
671
- const SKETCH_SIZE = 1024;
672
- const MAX_SKETCH_STROKES = 2e3;
673
- const MAX_STROKE_POINTS = 2e3;
674
- function sketchPoint(clientX, clientY, rect) {
675
- if (!(rect.width > 0 && rect.height > 0)) return void 0;
570
+ //#region src/image-conversation-node.js
571
+ const KIND = "codex-image-output";
572
+ const resultsOf = (event) => event?.type === "tool/result" && event.data.meta?.kind === "codex-subscription-image" ? (event.data.message?.content ?? []).filter((block) => block.type === "tool-result" && !block.isError && block.content?.some((part) => part.type === "image" && part.attachment)) : [];
573
+ const imageConversationNode = {
574
+ kind: KIND,
575
+ target: "chat",
576
+ match(event) {
577
+ if (event.type !== "turn/end" && resultsOf(event).length === 0) return null;
578
+ return {
579
+ id: String(event.data.turn),
580
+ role: "update"
581
+ };
582
+ },
583
+ start: () => void 0,
584
+ update: (context) => context.state,
585
+ buildViewNode(context) {
586
+ const end = context.matches.find((match) => match.event.type === "turn/end");
587
+ if (!end) return null;
588
+ const blocks = context.matches.flatMap(({ event }) => resultsOf(event).map((block) => ({
589
+ ...block,
590
+ kind: "tool-result",
591
+ meta: event.data.meta
592
+ })));
593
+ if (!blocks.length) return null;
594
+ const answer = end.location.turn?.steps?.at(-1)?.data.get("assistant-step");
595
+ const lastResultSeq = Math.max(...context.matches.filter((match) => resultsOf(match.event).length).map((match) => match.event.seq));
596
+ const answerSeq = answer?.finalNode?.seq;
597
+ const anchorSeq = answerSeq > lastResultSeq ? answerSeq + .025 : end.event.seq - .025;
598
+ return {
599
+ key: context.key,
600
+ id: context.id,
601
+ kind: KIND,
602
+ target: "chat",
603
+ location: end.location,
604
+ anchorSeq,
605
+ visibility: "visible",
606
+ data: { blocks }
607
+ };
608
+ }
609
+ };
610
+ //#endregion
611
+ //#region node_modules/.pnpm/@heroicons+react@2.2.0_react@18.3.1/node_modules/@heroicons/react/24/outline/esm/ArrowPathIcon.js
612
+ function ArrowPathIcon({ title, titleId, ...props }, svgRef) {
613
+ return /*#__PURE__*/ react.createElement("svg", Object.assign({
614
+ xmlns: "http://www.w3.org/2000/svg",
615
+ fill: "none",
616
+ viewBox: "0 0 24 24",
617
+ strokeWidth: 1.5,
618
+ stroke: "currentColor",
619
+ "aria-hidden": "true",
620
+ "data-slot": "icon",
621
+ ref: svgRef,
622
+ "aria-labelledby": titleId
623
+ }, props), title ? /*#__PURE__*/ react.createElement("title", { id: titleId }, title) : null, /*#__PURE__*/ react.createElement("path", {
624
+ strokeLinecap: "round",
625
+ strokeLinejoin: "round",
626
+ d: "M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99"
627
+ }));
628
+ }
629
+ const ForwardRef$3 = /*#__PURE__*/ react.forwardRef(ArrowPathIcon);
630
+ //#endregion
631
+ //#region node_modules/.pnpm/@heroicons+react@2.2.0_react@18.3.1/node_modules/@heroicons/react/24/outline/esm/ExclamationCircleIcon.js
632
+ function ExclamationCircleIcon({ title, titleId, ...props }, svgRef) {
633
+ return /*#__PURE__*/ react.createElement("svg", Object.assign({
634
+ xmlns: "http://www.w3.org/2000/svg",
635
+ fill: "none",
636
+ viewBox: "0 0 24 24",
637
+ strokeWidth: 1.5,
638
+ stroke: "currentColor",
639
+ "aria-hidden": "true",
640
+ "data-slot": "icon",
641
+ ref: svgRef,
642
+ "aria-labelledby": titleId
643
+ }, props), title ? /*#__PURE__*/ react.createElement("title", { id: titleId }, title) : null, /*#__PURE__*/ react.createElement("path", {
644
+ strokeLinecap: "round",
645
+ strokeLinejoin: "round",
646
+ d: "M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z"
647
+ }));
648
+ }
649
+ const ForwardRef$2 = /*#__PURE__*/ react.forwardRef(ExclamationCircleIcon);
650
+ //#endregion
651
+ //#region node_modules/.pnpm/@heroicons+react@2.2.0_react@18.3.1/node_modules/@heroicons/react/24/outline/esm/PhotoIcon.js
652
+ function PhotoIcon({ title, titleId, ...props }, svgRef) {
653
+ return /*#__PURE__*/ react.createElement("svg", Object.assign({
654
+ xmlns: "http://www.w3.org/2000/svg",
655
+ fill: "none",
656
+ viewBox: "0 0 24 24",
657
+ strokeWidth: 1.5,
658
+ stroke: "currentColor",
659
+ "aria-hidden": "true",
660
+ "data-slot": "icon",
661
+ ref: svgRef,
662
+ "aria-labelledby": titleId
663
+ }, props), title ? /*#__PURE__*/ react.createElement("title", { id: titleId }, title) : null, /*#__PURE__*/ react.createElement("path", {
664
+ strokeLinecap: "round",
665
+ strokeLinejoin: "round",
666
+ d: "m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z"
667
+ }));
668
+ }
669
+ const ForwardRef$1 = /*#__PURE__*/ react.forwardRef(PhotoIcon);
670
+ const ORIGINAL_IMAGE_ID_PATTERN = /^img_[0-9a-f]{32}$/u;
671
+ const positiveInteger = (value) => Number.isSafeInteger(value) && value > 0;
672
+ function decodeOriginalImageRef(value) {
673
+ if (value === null || typeof value !== "object" || Array.isArray(value) || typeof value.assetId !== "string" || !ORIGINAL_IMAGE_ID_PATTERN.test(value.assetId) || value.mediaType !== "image/png" || !positiveInteger(value.bytes) || value.bytes > 48 * 1024 * 1024 || !positiveInteger(value.width) || !positiveInteger(value.height) || typeof value.name !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(value.name) || typeof value.sha256 !== "string" || !/^[0-9a-f]{64}$/u.test(value.sha256)) return void 0;
676
674
  return {
677
- x: Math.max(0, Math.min(1, (clientX - rect.left) / rect.width)),
678
- y: Math.max(0, Math.min(1, (clientY - rect.top) / rect.height))
675
+ assetId: value.assetId,
676
+ mediaType: value.mediaType,
677
+ bytes: value.bytes,
678
+ width: value.width,
679
+ height: value.height,
680
+ name: value.name,
681
+ sha256: value.sha256
679
682
  };
680
683
  }
681
- function paintSketch(context, strokes, size = SKETCH_SIZE, transparent = false, height = size, start = 0, end = strokes.length) {
682
- context.globalCompositeOperation = "source-over";
683
- context.globalAlpha = 1;
684
- if (!transparent) {
685
- context.fillStyle = "#ffffff";
686
- context.fillRect(0, 0, size, height);
684
+ function decodeImagePresentation(value) {
685
+ if (value === null || typeof value !== "object" || Array.isArray(value) || value.kind !== "codex-subscription-image" || value.schemaVersion !== 1) return void 0;
686
+ const original = decodeOriginalImageRef(value.original);
687
+ return original === void 0 ? void 0 : { original };
688
+ }
689
+ function originalImageRefsEqual(left, right) {
690
+ const a = decodeOriginalImageRef(left);
691
+ const b = decodeOriginalImageRef(right);
692
+ return a !== void 0 && b !== void 0 && a.assetId === b.assetId && a.mediaType === b.mediaType && a.bytes === b.bytes && a.width === b.width && a.height === b.height && a.name === b.name && a.sha256 === b.sha256;
693
+ }
694
+ //#endregion
695
+ //#region src/rpc-contract.js
696
+ const CHANNEL = "/codex-subscription";
697
+ const RPC_ENDPOINTS = Object.freeze([
698
+ "status",
699
+ "login/start",
700
+ "login/status",
701
+ "login/submit",
702
+ "login/cancel",
703
+ "logout",
704
+ "account/select",
705
+ "account/remove",
706
+ "usage",
707
+ "diagnostics",
708
+ "preferences/status",
709
+ "preferences/models",
710
+ "preferences/update",
711
+ "reset-credit/inspect",
712
+ "reset-credit/prepare",
713
+ "reset-credit/consume",
714
+ "image/original/chunk",
715
+ "sketch/connect",
716
+ "sketch/poll",
717
+ "sketch/claim",
718
+ "sketch/result",
719
+ "sketch/disconnect"
720
+ ]);
721
+ function createSubscriptionRpcClient(transport) {
722
+ return Object.freeze({ call(channel, endpoint, payload, signal) {
723
+ if (channel !== "/codex-subscription" || !RPC_ENDPOINTS.includes(endpoint)) throw new Error("Invalid subscription RPC target");
724
+ return transport.call("/api", `codex-subscription/${endpoint}`, payload, signal);
725
+ } });
726
+ }
727
+ function unwrap(response) {
728
+ if (!response?.ok) throw new Error(response?.error?.message ?? "Codex RPC failed");
729
+ return response.value;
730
+ }
731
+ //#endregion
732
+ //#region src/original-image-download.js
733
+ function decodeBase64Chunk(value) {
734
+ if (typeof value !== "string" || value.length === 0 || value.length > Math.ceil(4194304 / 3) * 4 + 8) throw new Error("Invalid original image chunk");
735
+ let decoded;
736
+ try {
737
+ decoded = atob(value);
738
+ } catch {
739
+ throw new Error("Invalid original image chunk");
687
740
  }
688
- context.lineCap = "round";
689
- context.lineJoin = "round";
690
- for (let index = start; index < end; index++) {
691
- const stroke = strokes[index];
692
- const first = stroke.points[0];
693
- if (!first) continue;
694
- context.globalCompositeOperation = stroke.shape === "eraser" ? "destination-out" : "source-over";
695
- context.globalAlpha = (stroke.opacity ?? 1) * (stroke.brush === "marker" ? .28 : stroke.brush === "pencil" ? .65 : 1);
696
- context.strokeStyle = stroke.color;
697
- context.fillStyle = stroke.color;
698
- context.lineWidth = stroke.width * (stroke.brush === "pencil" ? .55 : 1) * (stroke.pressure ?? 1);
699
- context.beginPath();
700
- const last = stroke.points.at(-1);
701
- if (stroke.shape === "text") {
702
- const x = Math.min(first.x, last.x) * size, y = Math.min(first.y, last.y) * height, w = Math.abs(last.x - first.x) * size, h = Math.abs(last.y - first.y) * height;
703
- const lines = stroke.text.split("\n"), fontSize = Math.min(stroke.width, h / Math.max(1, lines.length) / 1.2);
704
- context.font = `${fontSize}px system-ui, sans-serif`;
705
- context.textBaseline = "top";
706
- lines.forEach((line, i) => context.fillText(line, x, y + i * fontSize * 1.2, w));
707
- } else if (stroke.shape === "arrow") {
708
- const x = last.x * size, y = last.y * height, a = Math.atan2(y - first.y * height, x - first.x * size), head = Math.min(Math.hypot(x - first.x * size, y - first.y * height) * .4, Math.max(12, stroke.width * 3));
709
- context.moveTo(first.x * size, first.y * height);
710
- context.lineTo(x, y);
711
- context.stroke();
712
- context.beginPath();
713
- context.moveTo(x, y);
714
- context.lineTo(x - head * Math.cos(a - .5), y - head * Math.sin(a - .5));
715
- context.lineTo(x - head * Math.cos(a + .5), y - head * Math.sin(a + .5));
716
- context.closePath();
717
- context.fill();
718
- } else if (stroke.shape === "bezier") {
719
- context.moveTo(first.x * size, first.y * height);
720
- for (let i = 1; i < stroke.points.length; i += 3) {
721
- const [a, b, c] = stroke.points.slice(i, i + 3);
722
- context.bezierCurveTo(a.x * size, a.y * height, b.x * size, b.y * height, c.x * size, c.y * height);
723
- }
724
- if (stroke.fill) {
725
- context.closePath();
726
- context.fill();
727
- } else context.stroke();
728
- } else if (stroke.shape === "line") {
729
- context.moveTo(first.x * size, first.y * height);
730
- context.lineTo(last.x * size, last.y * height);
731
- context.stroke();
732
- } else if (stroke.shape === "rectangle") {
733
- context.rect(first.x * size, first.y * height, (last.x - first.x) * size, (last.y - first.y) * height);
734
- if (stroke.fill) context.fill();
735
- else context.stroke();
736
- } else if (stroke.shape === "circle") {
737
- context.ellipse((first.x + last.x) * size / 2, (first.y + last.y) * height / 2, Math.abs(last.x - first.x) * size / 2, Math.abs(last.y - first.y) * height / 2, 0, 0, Math.PI * 2);
738
- if (stroke.fill) context.fill();
739
- else context.stroke();
740
- } else if (stroke.shape === "polygon") {
741
- context.moveTo(first.x * size, first.y * height);
742
- for (const point of stroke.points.slice(1)) context.lineTo(point.x * size, point.y * height);
743
- context.closePath();
744
- if (stroke.fill) context.fill();
745
- else context.stroke();
746
- } else if (stroke.points.length === 1) {
747
- context.arc(first.x * size, first.y * height, context.lineWidth / 2, 0, Math.PI * 2);
748
- context.fill();
749
- } else {
750
- context.moveTo(first.x * size, first.y * height);
751
- for (let i = 1; i < stroke.points.length - 1; i++) {
752
- const point = stroke.points[i], next = stroke.points[i + 1];
753
- if (context.quadraticCurveTo) context.quadraticCurveTo(point.x * size, point.y * height, (point.x + next.x) * size / 2, (point.y + next.y) * height / 2);
754
- else context.lineTo(point.x * size, point.y * height);
755
- }
756
- context.lineTo(last.x * size, last.y * height);
757
- context.stroke();
758
- }
741
+ const bytes = new Uint8Array(decoded.length);
742
+ for (let index = 0; index < decoded.length; index += 1) bytes[index] = decoded.charCodeAt(index);
743
+ return bytes;
744
+ }
745
+ /** Keep only the destination and current chunk, while verifying every reply. */
746
+ async function readOriginalImage(rpc, sessionId, original, { signal, onProgress } = {}) {
747
+ signal?.throwIfAborted();
748
+ original = decodeOriginalImageRef(original);
749
+ if (original === void 0) throw new Error("Invalid original image reference");
750
+ const data = new Uint8Array(original.bytes);
751
+ let total = 0;
752
+ let done = false;
753
+ while (!done) {
754
+ signal?.throwIfAborted();
755
+ const response = await rpc.call(CHANNEL, "image/original/chunk", {
756
+ sessionId,
757
+ assetId: original.assetId,
758
+ offset: total
759
+ }, signal);
760
+ signal?.throwIfAborted();
761
+ if (!response?.ok) throw new Error(response?.error?.message ?? "Codex RPC failed");
762
+ const chunk = response.value;
763
+ if (!originalImageRefsEqual(chunk?.ref, original) || chunk.offset !== total || typeof chunk.done !== "boolean") throw new Error("Original image metadata changed");
764
+ const bytes = decodeBase64Chunk(chunk.encoded);
765
+ if (bytes.byteLength === 0 || total + bytes.byteLength > original.bytes) throw new Error("Original image download is incomplete");
766
+ data.set(bytes, total);
767
+ total += bytes.byteLength;
768
+ done = chunk.done;
769
+ onProgress?.({
770
+ loaded: total,
771
+ total: original.bytes
772
+ });
759
773
  }
760
- context.globalCompositeOperation = "source-over";
761
- context.globalAlpha = 1;
774
+ if (total !== original.bytes) throw new Error("Original image download is incomplete");
775
+ const digest = await crypto.subtle.digest("SHA-256", data);
776
+ signal?.throwIfAborted();
777
+ if ([...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("") !== original.sha256) throw new Error("Original image integrity check failed");
778
+ return data;
762
779
  }
763
780
  //#endregion
764
- //#region src/sketch-curves.js
765
- const cached = /* @__PURE__ */ new WeakMap();
766
- const midpoint = (a, b) => ({
767
- x: (a.x + b.x) / 2,
768
- y: (a.y + b.y) / 2
769
- });
770
- function flattenSketchCurve(stroke, width, height) {
771
- const previous = cached.get(stroke);
772
- if (previous?.width === width && previous.height === height) return previous.points;
773
- const controls = stroke.points.map((p) => ({
774
- x: p.x * width,
775
- y: p.y * height
776
- })), points = [controls[0]];
777
- const distance = (p, a, b) => {
778
- const dx = b.x - a.x, dy = b.y - a.y, d = dx * dx + dy * dy;
779
- const t = d ? Math.max(0, Math.min(1, ((p.x - a.x) * dx + (p.y - a.y) * dy) / d)) : 0;
780
- return Math.hypot(p.x - a.x - t * dx, p.y - a.y - t * dy);
781
- };
782
- const split = (a, b, c, d, depth) => {
783
- if (depth === 10 || Math.max(distance(b, a, d), distance(c, a, d)) <= .5) {
784
- points.push(d);
785
- return;
786
- }
787
- const ab = midpoint(a, b), bc = midpoint(b, c), cd = midpoint(c, d), abc = midpoint(ab, bc), bcd = midpoint(bc, cd), m = midpoint(abc, bcd);
788
- split(a, ab, abc, m, depth + 1);
789
- split(m, bcd, cd, d, depth + 1);
790
- };
791
- for (let i = 1; i < controls.length; i += 3) split(controls[i - 1], controls[i], controls[i + 1], controls[i + 2], 0);
792
- const normalized = points.map((p) => ({
793
- x: p.x / width,
794
- y: p.y / height
795
- }));
796
- cached.set(stroke, {
797
- width,
798
- height,
799
- points: normalized
800
- });
801
- return normalized;
781
+ //#region src/client-images.jsx
782
+ const imageDownloadName = (attachment) => {
783
+ const fallback = "codex-generated-image.png";
784
+ if (typeof attachment?.name !== "string") return fallback;
785
+ const cleaned = attachment.name.replace(/[<>:"/\\|?*\u0000-\u001f]/gu, "-").replace(/[. ]+$/u, "").trim();
786
+ if (cleaned === "") return fallback;
787
+ return cleaned.toLowerCase().endsWith(".png") ? cleaned : `${cleaned}.png`;
788
+ };
789
+ function triggerBlobDownload(data, mediaType, filename) {
790
+ const url = URL.createObjectURL(new Blob([data], { type: mediaType }));
791
+ const anchor = document.createElement("a");
792
+ anchor.href = url;
793
+ anchor.download = filename;
794
+ anchor.rel = "noopener";
795
+ document.body.append(anchor);
796
+ try {
797
+ anchor.click();
798
+ } finally {
799
+ anchor.remove();
800
+ URL.revokeObjectURL(url);
801
+ }
802
802
  }
803
- const createSketchLayers = () => ({
804
- active: 1,
805
- nextId: 2,
806
- layers: [{
807
- id: 1,
808
- name: "",
809
- visible: true,
810
- strokes: []
811
- }]
812
- });
813
- const strokeCount = (doc) => doc.layers.reduce((n, layer) => n + layer.strokes.length, 0);
814
- function changeSketchLayer(doc, action, id = doc.active, value) {
815
- const index = doc.layers.findIndex((layer) => layer.id === id);
816
- if (index < 0) return doc;
817
- const layers = doc.layers.slice(), layer = layers[index];
818
- if (action === "select") return {
819
- ...doc,
820
- active: id
821
- };
822
- if (action === "add" || action === "duplicate") {
823
- if (layers.length >= 8 || action === "duplicate" && strokeCount(doc) + layer.strokes.length > 2e3) return doc;
824
- const next = action === "add" ? {
825
- id: doc.nextId,
826
- name: "",
827
- visible: true,
828
- strokes: []
829
- } : {
830
- ...layer,
831
- id: doc.nextId,
832
- strokes: layer.strokes.slice()
833
- };
834
- layers.splice(index + 1, 0, next);
835
- return {
836
- ...doc,
837
- layers,
838
- active: next.id,
839
- nextId: doc.nextId + 1
803
+ function CodexGeneratedImage({ attachment, original, rpc, sessionId, loadImage, openSketchImage, attachForEdit, getImageViewer, getInternalImageViewer, t, features }) {
804
+ const [attempt, setAttempt] = (0, react.useState)(0);
805
+ const [error, setError] = (0, react.useState)(false);
806
+ const [src, setSrc] = (0, react.useState)();
807
+ const triggerRef = (0, react.useRef)(null);
808
+ (0, react.useEffect)(() => {
809
+ let live = true;
810
+ setError(false);
811
+ setSrc(void 0);
812
+ Promise.resolve().then(() => loadImage(attachment)).then((value) => {
813
+ if (live) setSrc(value);
814
+ }).catch(() => {
815
+ if (live) setError(true);
816
+ });
817
+ return () => {
818
+ live = false;
840
819
  };
841
- }
842
- if (action === "delete") {
843
- if (layers.length === 1) return doc;
844
- layers.splice(index, 1);
845
- return {
846
- ...doc,
847
- layers,
848
- active: doc.active === id ? layers[Math.min(index, layers.length - 1)].id : doc.active
820
+ }, [
821
+ attachment,
822
+ loadImage,
823
+ attempt
824
+ ]);
825
+ const label = attachment.name ?? t("imageLabel");
826
+ const downloadName = imageDownloadName(attachment);
827
+ const downloadOriginal = async ({ signal, onProgress } = {}) => {
828
+ if (original === void 0) return;
829
+ triggerBlobDownload(await readOriginalImage(rpc, sessionId, original, {
830
+ signal,
831
+ onProgress
832
+ }), original.mediaType, original.name);
833
+ };
834
+ const openImage = () => {
835
+ if (src === void 0) return;
836
+ const request = {
837
+ items: [{
838
+ id: attachment.attachmentId ?? downloadName,
839
+ src,
840
+ name: label,
841
+ width: attachment.width,
842
+ height: attachment.height,
843
+ bytes: attachment.bytes,
844
+ download: original === void 0 ? void 0 : {
845
+ pendingLabel: t("imageDownloadPreparing"),
846
+ errorLabel: t("imageDownloadFailed"),
847
+ onInvoke: downloadOriginal
848
+ },
849
+ actions: !features.imageEditing ? [] : [{
850
+ id: "continue-editing",
851
+ label: t("imageEdit"),
852
+ pendingLabel: t("imageEditPreparing"),
853
+ errorLabel: t("imageEditFailed"),
854
+ closeOnSuccess: true,
855
+ onInvoke: ({ annotations = [] }) => {
856
+ const imageKey = String(attachment.attachmentId ?? "image").replace(/[^a-zA-Z0-9_-]/g, "_");
857
+ const sourceName = annotations.length === 0 ? downloadName : `codex-edit-${imageKey}-source.png`;
858
+ const referenceName = `codex-edit-${imageKey}-annotations.png`;
859
+ return attachForEdit(src, sourceName, buildImageEditDraft({
860
+ annotations,
861
+ translate: t,
862
+ width: attachment.width,
863
+ height: attachment.height,
864
+ sourceName,
865
+ referenceName
866
+ }), annotations, referenceName);
867
+ }
868
+ }, ...features.imageSketch && openSketchImage ? [{
869
+ id: "sketch",
870
+ label: t("imageToSketch"),
871
+ pendingLabel: t("imageEditPreparing"),
872
+ errorLabel: t("imageEditFailed"),
873
+ onInvoke: () => openSketchImage(src, downloadName)
874
+ }] : []]
875
+ }],
876
+ opener: triggerRef.current,
877
+ source: "codex-generated",
878
+ annotations: features.imageViewer && features.imageAnnotations
849
879
  };
850
- }
851
- if (action === "up" || action === "down") {
852
- const target = index + (action === "up" ? 1 : -1);
853
- if (!layers[target]) return doc;
854
- [layers[index], layers[target]] = [layers[target], layer];
855
- } else if (action === "visible") layers[index] = {
856
- ...layer,
857
- visible: !layer.visible
858
- };
859
- else if (action === "rename") layers[index] = {
860
- ...layer,
861
- name: String(value).trim().slice(0, 40)
862
- };
863
- else if (action === "clear") layers[index] = {
864
- ...layer,
865
- strokes: [],
866
- image: void 0
867
- };
868
- else return doc;
869
- return {
870
- ...doc,
871
- layers
880
+ if (features.imageViewer && getInternalImageViewer?.()?.open?.(request) === true) return;
881
+ if ((getImageViewer?.())?.open?.(request) === true) return;
882
+ if (features.imageViewer) getInternalImageViewer?.()?.open?.({
883
+ ...request,
884
+ annotations: false
885
+ });
886
+ else window.open(src, "_blank", "noopener,noreferrer");
872
887
  };
888
+ if (error) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
889
+ type: "button",
890
+ className: "codexGeneratedImageRetry",
891
+ onClick: () => setAttempt((value) => value + 1),
892
+ children: t("imageLoadFailed")
893
+ });
894
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
895
+ ref: triggerRef,
896
+ type: "button",
897
+ className: "codexGeneratedImageFrame",
898
+ title: t("imageOpen"),
899
+ "aria-label": t("imageOpenNamed").replace("{value}", String(label)),
900
+ onClick: openImage,
901
+ children: src === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("imageLoading") }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
902
+ src,
903
+ alt: label
904
+ })
905
+ });
873
906
  }
874
- const distanceToSegment = (p, a, b) => {
875
- const dx = b.x - a.x, dy = b.y - a.y, length = dx * dx + dy * dy;
876
- const k = length ? Math.max(0, Math.min(1, ((p.x - a.x) * dx + (p.y - a.y) * dy) / length)) : 0;
877
- return Math.hypot(p.x - a.x - k * dx, p.y - a.y - k * dy);
878
- };
879
- function strokeHit(stroke, point, radius, width = SKETCH_SIZE, height = width) {
880
- let points = stroke.shape === "bezier" ? flattenSketchCurve(stroke, width, height) : stroke.points;
881
- if (!points.length) return false;
882
- const a = points[0], b = points.at(-1);
883
- if ((stroke.shape === "text" || stroke.fill && stroke.shape === "rectangle") && point.x >= Math.min(a.x, b.x) && point.x <= Math.max(a.x, b.x) && point.y >= Math.min(a.y, b.y) && point.y <= Math.max(a.y, b.y)) return true;
884
- if (stroke.fill && stroke.shape === "circle") {
885
- const rx = Math.abs(b.x - a.x) / 2, ry = Math.abs(b.y - a.y) / 2;
886
- if (rx && ry && ((point.x - (a.x + b.x) / 2) / rx) ** 2 + ((point.y - (a.y + b.y) / 2) / ry) ** 2 <= 1) return true;
887
- }
888
- if (stroke.shape === "polygon" || stroke.shape === "bezier" && stroke.fill) {
889
- if (stroke.fill) {
890
- let inside = false;
891
- for (let i = 0, j = points.length - 1; i < points.length; j = i++) {
892
- const p = points[i], q = points[j];
893
- if (p.y > point.y !== q.y > point.y && point.x < (q.x - p.x) * (point.y - p.y) / (q.y - p.y) + p.x) inside = !inside;
894
- }
895
- if (inside) return true;
896
- }
897
- points = [...points, points[0]];
898
- }
899
- if (stroke.shape === "rectangle") points = [
900
- a,
901
- {
902
- x: b.x,
903
- y: a.y
904
- },
905
- b,
906
- {
907
- x: a.x,
908
- y: b.y
909
- },
910
- a
911
- ];
912
- if (stroke.shape === "circle") points = Array.from({ length: 65 }, (_, i) => ({
913
- x: (a.x + b.x) / 2 + Math.abs(b.x - a.x) / 2 * Math.cos(i * Math.PI / 32),
914
- y: (a.y + b.y) / 2 + Math.abs(b.y - a.y) / 2 * Math.sin(i * Math.PI / 32)
915
- }));
916
- points = points.map((p) => ({
917
- x: p.x * width,
918
- y: p.y * height
919
- }));
920
- point = {
921
- x: point.x * width,
922
- y: point.y * height
923
- };
924
- const tolerance = radius + stroke.width / 2;
925
- return points.some((p, i) => distanceToSegment(point, i ? points[i - 1] : p, p) <= tolerance);
907
+ function CodexImageToolRow({ presentation = "tool", block, sessionId, rpc, loadImage, openSketchImage, attachForEdit, getImageViewer, getInternalImageViewer, t, preference }) {
908
+ const features = (0, react.useSyncExternalStore)(preference.subscribe, preference.getSnapshot);
909
+ const settled = block?.kind === "tool-result";
910
+ const image = settled ? block.content.find((item) => item?.type === "image" && item.attachment !== void 0) : void 0;
911
+ const failed = settled && block.isError === true;
912
+ const state = !settled ? "running" : failed ? "error" : "done";
913
+ const status = !settled ? t("imageGenerating") : failed ? t("imageFailed") : t("imageGenerated");
914
+ const error = failed ? block.content.find((item) => item?.type === "text" && typeof item.text === "string")?.text : void 0;
915
+ const original = decodeImagePresentation(block?.meta)?.original;
916
+ const showOutput = presentation === "output";
917
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
918
+ className: "codexImageTool",
919
+ "data-state": state,
920
+ children: [
921
+ showOutput ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
922
+ className: "codexImageToolRow",
923
+ children: [
924
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(!settled ? ForwardRef$3 : failed ? ForwardRef$2 : ForwardRef$1, {
925
+ className: "codexImageToolIcon",
926
+ "aria-hidden": "true"
927
+ }),
928
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
929
+ className: "codexImageToolTitle",
930
+ children: t("imageGenerate")
931
+ }),
932
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
933
+ className: "codexImageBeta",
934
+ children: t("imageBeta")
935
+ }),
936
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
937
+ className: "codexImageToolState",
938
+ children: status
939
+ })
940
+ ]
941
+ }),
942
+ !showOutput || image === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
943
+ className: "codexImageToolGallery",
944
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CodexGeneratedImage, {
945
+ features,
946
+ attachment: image.attachment,
947
+ original,
948
+ rpc,
949
+ sessionId,
950
+ loadImage,
951
+ openSketchImage,
952
+ attachForEdit,
953
+ getImageViewer,
954
+ getInternalImageViewer,
955
+ t
956
+ })
957
+ }),
958
+ !showOutput && typeof block?.meta?.requestedModel === "string" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
959
+ className: "codexImageDetails",
960
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", { children: t("imageDetails") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [
961
+ t("imageRequestedModel"),
962
+ ": ",
963
+ block.meta.requestedModel.slice(0, 100),
964
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("br", {}),
965
+ t("imageReportedModel"),
966
+ ": ",
967
+ typeof block.meta.reportedModel === "string" ? block.meta.reportedModel.slice(0, 100) : t("imageModelUnreported"),
968
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("br", {}),
969
+ t("imageRequestedSize"),
970
+ ": ",
971
+ String(block.meta.requestedSize ?? "auto").slice(0, 40),
972
+ " · ",
973
+ t("imageActualSize"),
974
+ ": ",
975
+ original?.width,
976
+ " × ",
977
+ original?.height
978
+ ] })]
979
+ }) : null,
980
+ error === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
981
+ className: "codexImageToolError",
982
+ children: error
983
+ })
984
+ ]
985
+ });
926
986
  }
927
- const SKETCH_RATIOS = Object.freeze({
928
- "1:1": [1024, 1024],
929
- "4:3": [1024, 768],
930
- "3:4": [768, 1024],
931
- "16:9": [1024, 576],
932
- "9:16": [576, 1024]
933
- });
934
- function resizeSketch(doc, ratio) {
935
- if (!Object.hasOwn(SKETCH_RATIOS, ratio)) throw new Error("Invalid sketch ratio");
936
- const [width, height] = SKETCH_RATIOS[ratio];
937
- const oldWidth = doc.width ?? 1024, oldHeight = doc.height ?? 1024;
938
- if (width === oldWidth && height === oldHeight) return doc;
939
- const scale = Math.min(width / oldWidth, height / oldHeight);
940
- const dx = (width - oldWidth * scale) / 2, dy = (height - oldHeight * scale) / 2;
941
- return {
942
- ...doc,
943
- width,
944
- height,
945
- ratio,
946
- layers: doc.layers.map((layer) => ({
947
- ...layer,
948
- ...layer.image ? { image: {
949
- ...layer.image,
950
- x: (layer.image.x * oldWidth * scale + dx) / width,
951
- y: (layer.image.y * oldHeight * scale + dy) / height,
952
- width: layer.image.width * oldWidth * scale / width,
953
- height: layer.image.height * oldHeight * scale / height
954
- } } : {},
955
- strokes: layer.strokes.map((stroke) => ({
956
- ...stroke,
957
- width: stroke.width * scale,
958
- points: stroke.points.map((p) => ({
959
- x: (p.x * oldWidth * scale + dx) / width,
960
- y: (p.y * oldHeight * scale + dy) / height
961
- }))
962
- }))
963
- }))
964
- };
987
+ function CodexImageOutput({ node, ...props }) {
988
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
989
+ className: "codexImageOutput",
990
+ children: node.data.blocks.map((block) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CodexImageToolRow, {
991
+ block,
992
+ ...props,
993
+ presentation: "output"
994
+ }, block.toolCallId))
995
+ });
965
996
  }
966
997
  //#endregion
967
998
  //#region src/sketch-layer-renderer.js
@@ -1515,7 +1546,8 @@ window.__ModuleLoader__.load({
1515
1546
  shapes: "line: exactly two endpoints; rectangle/circle (ellipse alias accepted): exactly two opposite bounding-box corners (circle draws an ellipse within that box); polygon: three or more vertices, closed automatically; pen: ordered path points. bezier: start point, then groups of control1/control2/end; use 4 points for one cubic curve, max 64 segments. Prefer bezier for smooth designed curves instead of many pen samples. fill:true closes and fills the curve. fill:true fills rectangle/circle/polygon. Layers and strokes paint in list order, later ones on top. All commands needed for drawing are described here; no source-code search is required.",
1516
1547
  commands: {
1517
1548
  stroke: "{op:\"stroke\",layer:1,shape:\"pen|line|arrow|text|rectangle|circle|polygon|bezier\",color:\"#rrggbb\",width:2,opacity:1,fill:false,points:[{x:0.1,y:0.1},...]}",
1518
- layer: "{op:\"layer\",action:\"add|select|rename|visible|duplicate|up|down|delete|clear\",id:1,value:\"name\"}",
1549
+ layer: "Add: {op:\"layer\",action:\"add\",value:\"name\"}; optional id is the NEW unique integer ID, otherwise allocated automatically. after is the existing insertion anchor, defaults to active layer. Other actions: {op:\"layer\",action:\"select|rename|visible|duplicate|up|down|delete|clear\",id:1,value:\"name\"}; id targets an existing layer.",
1550
+ curve: "Prefer {op:\"stroke\",shape:\"bezier\",start:{x:0,y:0},segments:[{control1:{x:0.2,y:0},control2:{x:0.8,y:1},end:{x:1,y:1}}],color:\"#123456\"}. Each segment has exactly two controls and an endpoint; no point counting required. Legacy points arrays still accepted. Do not provide both forms.",
1519
1551
  object: "{op:\"object\",layer:1,id:\"title\",action:\"update|duplicate|delete\",patch:{color:\"#0088ff\",text:\"Title\"},transform:{dx:0.05,dy:0,scaleX:1,scaleY:1}}. All patch and transform fields optional. Inspect returns object IDs and bounds. Prefer targeted edits over redrawing layers.",
1520
1552
  resize: "{op:\"resize\",ratio:\"1:1|4:3|3:4|16:9|9:16\"}"
1521
1553
  },
@@ -1548,6 +1580,23 @@ window.__ModuleLoader__.load({
1548
1580
  "delete",
1549
1581
  "clear"
1550
1582
  ].includes(command.action)) throw Error("Unknown layer action");
1583
+ if (command.action === "add") {
1584
+ const id = command.id ?? doc.nextId, after = command.after ?? doc.active;
1585
+ if (!Number.isSafeInteger(id) || id < 1 || id === Number.MAX_SAFE_INTEGER || doc.layers.some((l) => l.id === id)) throw Error("New layer id must be a unique positive integer; omit id to allocate automatically");
1586
+ const next = changeSketchLayer(doc, "add", after);
1587
+ if (next === doc) throw Error("Cannot add layer: check the existing after layer and the 8-layer limit");
1588
+ doc = {
1589
+ ...next,
1590
+ active: id,
1591
+ nextId: Math.max(next.nextId, id + 1),
1592
+ layers: next.layers.map((l) => l.id === next.active ? {
1593
+ ...l,
1594
+ id,
1595
+ name: String(command.value ?? "").trim().slice(0, 40)
1596
+ } : l)
1597
+ };
1598
+ continue;
1599
+ }
1551
1600
  const next = changeSketchLayer(doc, command.action, command.id ?? doc.active, command.value);
1552
1601
  if (next === doc) throw Error("Layer action unavailable; inspect the document first");
1553
1602
  doc = next;
@@ -1607,7 +1656,16 @@ window.__ModuleLoader__.load({
1607
1656
  }
1608
1657
  if (command.op !== "stroke") throw Error("Unknown command");
1609
1658
  const shape = command.shape === "ellipse" ? "circle" : command.shape ?? "pen";
1610
- const { points, color, width = shape === "text" ? 24 : 2, opacity = 1, fill = false } = command;
1659
+ const { color, width = shape === "text" ? 24 : 2, opacity = 1, fill = false } = command;
1660
+ let points = command.points;
1661
+ if (command.start !== void 0 || command.segments !== void 0) {
1662
+ if (shape !== "bezier" || points !== void 0 || !command.start || !Array.isArray(command.segments) || !command.segments.length || command.segments.length > 64) throw Error("Bezier requires start and 1–64 segments, without points");
1663
+ points = [command.start, ...command.segments.flatMap((s) => [
1664
+ s?.control1,
1665
+ s?.control2,
1666
+ s?.end
1667
+ ])];
1668
+ }
1611
1669
  if (![
1612
1670
  "pen",
1613
1671
  "line",
@@ -1675,6 +1733,7 @@ window.__ModuleLoader__.load({
1675
1733
  if (!Number.isInteger(offset) || offset < 0) throw Error("offset must be a non-negative integer");
1676
1734
  return {
1677
1735
  ...current,
1736
+ protocolVersion: 2,
1678
1737
  objects: objects.slice(offset, offset + 50),
1679
1738
  objectCount: objects.length,
1680
1739
  ...offset + 50 < objects.length ? { nextOffset: offset + 50 } : {},
@@ -1703,7 +1762,15 @@ window.__ModuleLoader__.load({
1703
1762
  if (request.revision !== current.revision || adapter.busy()) throw Error("Sketch changed or is being edited; inspect again");
1704
1763
  let changedObjects;
1705
1764
  if (request.action === "apply") {
1706
- const before = adapter.document(), next = applySketchCommands(before, request.commands);
1765
+ const before = adapter.document();
1766
+ let next;
1767
+ try {
1768
+ next = applySketchCommands(before, request.commands);
1769
+ } catch (cause) {
1770
+ const error = new Error(`${cause.message} Correct the batch and retry with the same runId and revision; nothing was applied.`, { cause });
1771
+ error.code = "SKETCH_INVALID_BATCH";
1772
+ throw error;
1773
+ }
1707
1774
  adapter.commit(next);
1708
1775
  const previous = new Map(before.layers.flatMap((l) => l.strokes.map((s) => [`${l.id}:${s.id}`, s])));
1709
1776
  changedObjects = next.layers.flatMap((l) => l.strokes.filter((s) => previous.get(`${l.id}:${s.id}`) !== s).map((s) => ({
@@ -2263,6 +2330,7 @@ window.__ModuleLoader__.load({
2263
2330
  update("finished");
2264
2331
  return completed.value;
2265
2332
  } catch (error) {
2333
+ if (version === generation && error.code === "SKETCH_INVALID_BATCH") throw error;
2266
2334
  if (version === generation) {
2267
2335
  update("failed");
2268
2336
  throw new Error(`${error.message} Call inspect to obtain the current runId and revision before retrying.`, { cause: error });
@@ -2281,7 +2349,7 @@ window.__ModuleLoader__.load({
2281
2349
  //#endregion
2282
2350
  //#region src/sketch-agent-client.js
2283
2351
  function connectSketchAgent(rpc, sessionId, execute, report, pollDelay = () => 350) {
2284
- let stopped = false, token, timer, attempts = 0;
2352
+ let stopped = false, token, timer, attempts = 0, failures = 0;
2285
2353
  const call = (endpoint, payload) => rpc.call(CHANNEL, `sketch/${endpoint}`, {
2286
2354
  sessionId,
2287
2355
  token,
@@ -2311,13 +2379,24 @@ window.__ModuleLoader__.load({
2311
2379
  });
2312
2380
  }
2313
2381
  } catch (error) {
2314
- if (!stopped) report(error.message);
2382
+ if (!stopped) {
2383
+ if (++failures > 5) {
2384
+ report(`${error.message}; reconnect failed. Reopen this session to retry.`);
2385
+ return;
2386
+ }
2387
+ report(`${error.message}; reconnecting. Inspect recentRequests before retrying a write.`);
2388
+ if (token) call("disconnect").catch(() => {});
2389
+ token = void 0;
2390
+ timer = setTimeout(connect, Math.min(1e4, 1e3 * 2 ** (failures - 1)));
2391
+ }
2315
2392
  return;
2316
2393
  }
2394
+ failures = 0;
2317
2395
  if (!stopped) timer = setTimeout(poll, pollDelay());
2318
2396
  };
2319
2397
  const connect = () => void call("connect").then((value) => {
2320
2398
  token = value.token;
2399
+ attempts = 0;
2321
2400
  if (stopped) call("disconnect").catch(() => {});
2322
2401
  else poll();
2323
2402
  }, (error) => {
@@ -2344,12 +2423,13 @@ window.__ModuleLoader__.load({
2344
2423
  "#34c759",
2345
2424
  "#0088ff"
2346
2425
  ];
2347
- function SketchStudio({ open, agentEnabled, agentPreview, onOpen, onClose, attachSketch, enabled, t, incoming, sessionId, rpc }) {
2348
- const dialog = (0, react.useRef)(null), canvas = (0, react.useRef)(null), doc = (0, react.useRef)(createSketchLayers()), cache = (0, react.useRef)(/* @__PURE__ */ new Map());
2349
- const undo = (0, react.useRef)([]), redo = (0, react.useRef)([]), active = (0, react.useRef)(null), frame = (0, react.useRef)(null);
2350
- const images = (0, react.useRef)(/* @__PURE__ */ new Map()), saved = (0, react.useRef)(null), dirty = (0, react.useRef)(false), updateUi = (0, react.useRef)(false);
2351
- const documentId = (0, react.useRef)(crypto.randomUUID()), documentRevision = (0, react.useRef)(0), agentAdapter = (0, react.useRef)({}), agentSession = (0, react.useRef)(null);
2352
- const [agentState, setAgentState] = (0, react.useState)("idle"), agentRun = (0, react.useRef)(null);
2426
+ function SketchStudio({ open, agentEnabled, agentPreview, onOpen, onClose, attachSketch, enabled, t, incoming, sessionId, rpc, sessionState }) {
2427
+ const localSession = (0, react.useRef)(null);
2428
+ localSession.current ??= sessionState ?? createSketchSessionState();
2429
+ const { doc, undo, redo, images, saved, dirty, documentId, documentRevision, agentAdapter, agentSession, agentRun } = localSession.current;
2430
+ const dialog = (0, react.useRef)(null), canvas = (0, react.useRef)(null), cache = (0, react.useRef)(/* @__PURE__ */ new Map());
2431
+ const active = (0, react.useRef)(null), frame = (0, react.useRef)(null), updateUi = (0, react.useRef)(false);
2432
+ const [agentState, setAgentState] = (0, react.useState)(agentRun.current?.state ?? "idle");
2353
2433
  const agentLocked = agentState === "drawing";
2354
2434
  const [noticeHidden, setNoticeHidden] = (0, react.useState)(false);
2355
2435
  const [stability, setStability] = (0, react.useState)(0), [flow, setFlow] = (0, react.useState)(100), [picturesOpen, setPicturesOpen] = (0, react.useState)(false);
@@ -2437,10 +2517,10 @@ window.__ModuleLoader__.load({
2437
2517
  (0, react.useEffect)(() => () => {
2438
2518
  cancelAnimationFrame(frame.current);
2439
2519
  cache.current.clear();
2440
- agentRun.current?.dispose();
2441
2520
  }, []);
2442
2521
  const hasContent = () => doc.current.layers.some((layer) => layer.image || layer.strokes.length);
2443
2522
  const save = async (name) => {
2523
+ const savingDocument = documentId.current, savingRevision = documentRevision.current;
2444
2524
  const row = {
2445
2525
  id: saved.current?.id ?? crypto.randomUUID(),
2446
2526
  name: name?.trim() || saved.current?.name || `${t("sketchTitle")} ${(/* @__PURE__ */ new Date()).toLocaleString()}`,
@@ -2448,11 +2528,13 @@ window.__ModuleLoader__.load({
2448
2528
  doc: structuredClone(doc.current)
2449
2529
  };
2450
2530
  await sketchDrafts("save", row);
2451
- saved.current = {
2452
- id: row.id,
2453
- name: row.name
2454
- };
2455
- dirty.current = false;
2531
+ if (documentId.current === savingDocument) {
2532
+ saved.current = {
2533
+ id: row.id,
2534
+ name: row.name
2535
+ };
2536
+ if (documentRevision.current === savingRevision) dirty.current = false;
2537
+ }
2456
2538
  };
2457
2539
  const saveChanges = async () => {
2458
2540
  if (dirty.current && (hasContent() || saved.current)) await save();
@@ -2730,6 +2812,10 @@ window.__ModuleLoader__.load({
2730
2812
  schedule();
2731
2813
  };
2732
2814
  Object.assign(agentAdapter.current, {
2815
+ changed: (state) => {
2816
+ setAgentState(state);
2817
+ setNoticeHidden(false);
2818
+ },
2733
2819
  available: () => enabled && agentEnabled,
2734
2820
  previewEnabled: () => agentPreview,
2735
2821
  open: () => onOpen(),
@@ -2775,17 +2861,14 @@ window.__ModuleLoader__.load({
2775
2861
  agentRun.current ??= createSketchAgentRun({
2776
2862
  execute: (request) => agentSession.current(request),
2777
2863
  open: () => agentAdapter.current.open(),
2778
- changed: (state) => {
2779
- setAgentState(state);
2780
- setNoticeHidden(false);
2781
- },
2864
+ changed: (state) => agentAdapter.current.changed?.(state),
2782
2865
  busy: () => agentAdapter.current.busy(),
2783
2866
  previewEnabled: () => agentAdapter.current.previewEnabled()
2784
2867
  });
2785
2868
  (0, react.useEffect)(() => {
2786
2869
  if (!enabled || !agentEnabled) return;
2787
2870
  const api = Object.freeze({
2788
- version: 1,
2871
+ version: 2,
2789
2872
  sessionId,
2790
2873
  execute: (request) => agentRun.current.execute(request),
2791
2874
  export: async (format) => {
@@ -2812,9 +2895,12 @@ window.__ModuleLoader__.load({
2812
2895
  sessionId
2813
2896
  ]);
2814
2897
  (0, react.useEffect)(() => {
2815
- if (!enabled || !agentEnabled || !rpc || !sessionId) return;
2898
+ if (!enabled || !agentEnabled) {
2899
+ if (agentRun.current.locked) agentRun.current.stop();
2900
+ return;
2901
+ }
2902
+ if (!rpc || !sessionId) return;
2816
2903
  let live = true;
2817
- agentRun.current.resume();
2818
2904
  const disconnect = connectSketchAgent(rpc, sessionId, (request) => {
2819
2905
  if (!live) throw Error("Sketch session disconnected");
2820
2906
  return agentRun.current.execute(request);
@@ -2824,7 +2910,6 @@ window.__ModuleLoader__.load({
2824
2910
  }, () => 350);
2825
2911
  return () => {
2826
2912
  live = false;
2827
- agentRun.current.stop();
2828
2913
  disconnect();
2829
2914
  };
2830
2915
  }, [
@@ -3707,7 +3792,7 @@ window.__ModuleLoader__.load({
3707
3792
  `;
3708
3793
  //#endregion
3709
3794
  //#region src/sketch-workspace.jsx
3710
- function SketchWorkspace({ preference, attachSketch, registerOpen, t, sessionId, rpc }) {
3795
+ function SketchWorkspace({ preference, attachSketch, registerOpen, t, sessionId, rpc, sessionState }) {
3711
3796
  const settings = (0, react.useSyncExternalStore)(preference.subscribe, preference.getSnapshot);
3712
3797
  const [open, setOpen] = (0, react.useState)(false);
3713
3798
  const [incoming, setIncoming] = (0, react.useState)(null);
@@ -3720,6 +3805,7 @@ window.__ModuleLoader__.load({
3720
3805
  }
3721
3806
  }), [registerOpen]);
3722
3807
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SketchStudio, {
3808
+ sessionState,
3723
3809
  agentPreview: settings.imageSketchAgentPreview,
3724
3810
  agentEnabled: settings.imageSketchAgent,
3725
3811
  onOpen: () => {
@@ -6461,7 +6547,7 @@ window.__ModuleLoader__.load({
6461
6547
  }
6462
6548
  //#endregion
6463
6549
  //#region src/version.js
6464
- const PACKAGE_VERSION = "2.1.0-beta.4";
6550
+ const PACKAGE_VERSION = "2.1.0-beta.5";
6465
6551
  //#endregion
6466
6552
  //#region src/client-recovery.js
6467
6553
  async function recoveryCall(rpc, endpoint, payload = {}, timeoutMs = 1e4) {
@@ -8778,6 +8864,8 @@ window.__ModuleLoader__.load({
8778
8864
  const conversation = ctx.get("conversation");
8779
8865
  const uiConversation = ctx.get("uiConversation");
8780
8866
  const sketchOpeners = /* @__PURE__ */ new Map();
8867
+ const sketchSessions = createSketchSessionRegistry();
8868
+ ctx.effect(() => () => sketchSessions.dispose(), "codex-subscription: sketch sessions");
8781
8869
  ctx.inject(["inputTriggers"], (triggerContext) => triggerContext.effect(() => triggerContext.get("inputTriggers").registerSource(createSketchTrigger({
8782
8870
  enabled: () => {
8783
8871
  const value = preference.getSnapshot();
@@ -8886,6 +8974,7 @@ window.__ModuleLoader__.load({
8886
8974
  t,
8887
8975
  sessionId,
8888
8976
  rpc,
8977
+ sessionState: sketchSessions.get(sessionId),
8889
8978
  registerOpen: (callback) => {
8890
8979
  sketchOpeners.set(sessionId, callback);
8891
8980
  return () => {