dsh-codex-subscription 2.1.0-beta.3 → 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 +1064 -924
  2. package/lib/index.js +50 -16
  3. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -29,939 +29,970 @@ window.__ModuleLoader__.load({
29
29
  let react = require("react");
30
30
  react = __toESM(react, 1);
31
31
  let react_jsx_runtime = require("react/jsx-runtime");
32
- let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
33
32
  let react_dom = require("react-dom");
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;
33
+ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
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);
127
+ }
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);
206
159
  }
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
- });
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
  }
281
- };
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"
299
- }));
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"
319
- }));
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
+ };
320
237
  }
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"
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);
242
+ };
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)
339
279
  }));
280
+ points = points.map((p) => ({
281
+ x: p.x * width,
282
+ y: p.y * height
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);
340
290
  }
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;
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
+ });
416
394
  }
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;
395
+ const formatPercent = (value) => {
396
+ return `${Number((value * 100).toFixed(2))}%`;
397
+ };
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");
451
428
  }
452
429
  //#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`;
460
- };
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
- }
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
+ });
474
462
  }
475
- function CodexGeneratedImage({ attachment, original, rpc, sessionId, loadImage, openSketchImage, attachForEdit, getImageViewer, getInternalImageViewer, t, features }) {
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);
476
502
  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);
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
- ]
549
+ function MessageImagePreviews({ images, align, ...props }) {
550
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
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))
657
558
  });
658
559
  }
659
- function CodexImageOutput({ node, ...props }) {
660
- 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))
667
- });
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
+ `;
569
+ //#endregion
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
+ }));
668
628
  }
629
+ const ForwardRef$3 = /*#__PURE__*/ react.forwardRef(ArrowPathIcon);
669
630
  //#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;
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);
687
- }
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
- }
759
- }
760
- context.globalCompositeOperation = "source-over";
761
- context.globalAlpha = 1;
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;
762
730
  }
763
731
  //#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;
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");
740
+ }
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;
802
744
  }
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
840
- };
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
+ });
841
773
  }
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
849
- };
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;
779
+ }
780
+ //#endregion
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);
850
801
  }
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
802
+ }
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;
819
+ };
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);
867
833
  };
868
- else return doc;
869
- return {
870
- ...doc,
871
- layers
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
879
+ };
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
@@ -1351,8 +1382,39 @@ window.__ModuleLoader__.load({
1351
1382
  };
1352
1383
  }
1353
1384
  function SketchViewControls({ navigation, t }) {
1354
- const [open, setOpen] = (0, react.useState)(false), host = (0, react.useRef)(null);
1355
- useSketchDismiss(open, setOpen, host, [".codexSketchViewControls"]);
1385
+ const [open, setOpen] = (0, react.useState)(false), [placement, setPlacement] = (0, react.useState)(null);
1386
+ const host = (0, react.useRef)(null), trigger = (0, react.useRef)(null);
1387
+ const close = () => {
1388
+ setOpen(false);
1389
+ trigger.current?.focus({ preventScroll: true });
1390
+ };
1391
+ useSketchDismiss(open, setOpen, host, [".codexSketchViewControls", ".codexSketchKeyPanel"]);
1392
+ (0, react.useLayoutEffect)(() => {
1393
+ if (!open) return;
1394
+ const dialog = host.current.closest("dialog");
1395
+ const place = () => {
1396
+ const box = dialog.getBoundingClientRect(), anchor = trigger.current.getBoundingClientRect();
1397
+ const width = Math.min(360, box.width - 24);
1398
+ setPlacement({
1399
+ dialog,
1400
+ style: {
1401
+ width,
1402
+ left: Math.max(12, Math.min(anchor.left - box.left, box.width - width - 12)),
1403
+ bottom: box.bottom - anchor.top + 8,
1404
+ maxHeight: Math.max(80, anchor.top - box.top - 24)
1405
+ }
1406
+ });
1407
+ };
1408
+ place();
1409
+ const observer = new ResizeObserver(place);
1410
+ observer.observe(dialog);
1411
+ observer.observe(host.current);
1412
+ window.addEventListener("resize", place);
1413
+ return () => {
1414
+ observer.disconnect();
1415
+ window.removeEventListener("resize", place);
1416
+ };
1417
+ }, [open]);
1356
1418
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1357
1419
  ref: host,
1358
1420
  className: "codexSketchViewControls",
@@ -1376,40 +1438,56 @@ window.__ModuleLoader__.load({
1376
1438
  children: "+"
1377
1439
  }),
1378
1440
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1441
+ ref: trigger,
1379
1442
  type: "button",
1443
+ "aria-expanded": open,
1380
1444
  onClick: () => setOpen(!open),
1381
1445
  children: t("sketchKeys")
1382
1446
  }),
1383
- open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1447
+ open && placement ? (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1384
1448
  className: "codexSketchKeyPanel",
1449
+ "aria-label": t("sketchKeys"),
1450
+ style: placement.style,
1385
1451
  onKeyDown: (e) => {
1386
1452
  if (e.key === "Escape") {
1387
1453
  e.preventDefault();
1388
1454
  e.stopPropagation();
1389
- setOpen(false);
1455
+ close();
1390
1456
  }
1391
1457
  },
1392
1458
  children: [
1393
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1394
- type: "checkbox",
1395
- checked: navigation.shortcuts,
1396
- onChange: navigation.toggle
1397
- }), t("sketchKeysEnabled")] }),
1459
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t("sketchKeys") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1460
+ type: "button",
1461
+ "aria-label": t("sketchFileClose"),
1462
+ onClick: close,
1463
+ children: "×"
1464
+ })] }),
1465
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1466
+ className: "codexSketchKeysEnabled",
1467
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("sketchKeysEnabled") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1468
+ type: "checkbox",
1469
+ checked: navigation.shortcuts,
1470
+ onChange: navigation.toggle
1471
+ })]
1472
+ }),
1398
1473
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("sketchNavigationHint") }),
1399
- Object.entries(navigation.keys).map(([action, key]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [t(`sketchKey_${action}`), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1400
- "aria-label": t(`sketchKey_${action}`),
1401
- value: key === " " ? "Space" : key,
1402
- readOnly: true,
1403
- onKeyDown: (e) => {
1404
- if (e.key === "Tab" || e.key === "Escape") return;
1405
- e.preventDefault();
1406
- e.stopPropagation();
1407
- if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) navigation.setKey(action, e.key);
1408
- }
1409
- })] }, action)),
1474
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1475
+ className: "codexSketchKeyGrid",
1476
+ children: Object.entries(navigation.keys).map(([action, key]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [t(`sketchKey_${action}`), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1477
+ "aria-label": t(`sketchKey_${action}`),
1478
+ value: key === " " ? "Space" : key,
1479
+ readOnly: true,
1480
+ onKeyDown: (e) => {
1481
+ if (e.key === "Tab" || e.key === "Escape") return;
1482
+ e.preventDefault();
1483
+ e.stopPropagation();
1484
+ if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) navigation.setKey(action, e.key);
1485
+ }
1486
+ })] }, action))
1487
+ }),
1410
1488
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: t("sketchKeyHint") })
1411
1489
  ]
1412
- }) : null
1490
+ }), placement.dialog) : null
1413
1491
  ]
1414
1492
  });
1415
1493
  }
@@ -1465,10 +1543,11 @@ window.__ModuleLoader__.load({
1465
1543
  }
1466
1544
  const SKETCH_COMMAND_HELP = {
1467
1545
  coordinates: "Assign short meaningful stroke id values for later edits. Text uses two opposite box corners and text content; width is font size in pixels, automatic fitting within the box. Arrow uses two endpoints. Normalized x/y in [0,1]; width is canvas pixels. Read documentId and revision before editing.",
1468
- shapes: "line: exactly two endpoints; rectangle/circle: 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.",
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.",
1469
1547
  commands: {
1470
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},...]}",
1471
- 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.",
1472
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.",
1473
1552
  resize: "{op:\"resize\",ratio:\"1:1|4:3|3:4|16:9|9:16\"}"
1474
1553
  },
@@ -1501,6 +1580,23 @@ window.__ModuleLoader__.load({
1501
1580
  "delete",
1502
1581
  "clear"
1503
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
+ }
1504
1600
  const next = changeSketchLayer(doc, command.action, command.id ?? doc.active, command.value);
1505
1601
  if (next === doc) throw Error("Layer action unavailable; inspect the document first");
1506
1602
  doc = next;
@@ -1559,7 +1655,17 @@ window.__ModuleLoader__.load({
1559
1655
  continue;
1560
1656
  }
1561
1657
  if (command.op !== "stroke") throw Error("Unknown command");
1562
- const { points, color, shape = "pen", width = shape === "text" ? 24 : 2, opacity = 1, fill = false } = command;
1658
+ const shape = command.shape === "ellipse" ? "circle" : command.shape ?? "pen";
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
+ }
1563
1669
  if (![
1564
1670
  "pen",
1565
1671
  "line",
@@ -1578,7 +1684,7 @@ window.__ModuleLoader__.load({
1578
1684
  "rectangle",
1579
1685
  "circle"
1580
1686
  ].includes(shape) && points.length !== 2 || shape === "polygon" && points.length < 3) throw Error("Invalid shape points");
1581
- if (shape === "bezier" && (points.length < 4 || points.length > 193 || (points.length - 1) % 3 !== 0)) throw Error("Bezier needs a start point followed by groups of two controls and an endpoint (max 64 segments)");
1687
+ if (shape === "bezier" && (points.length < 4 || points.length > 193 || (points.length - 1) % 3 !== 0)) throw Error(`commands[${commands.indexOf(command)}]: Bezier has ${points.length} points; expected 4, 7, 10, ... 193 (start + control1/control2/end per segment). No commands in this batch were applied.`);
1582
1688
  if (fill && ![
1583
1689
  "rectangle",
1584
1690
  "circle",
@@ -1627,6 +1733,7 @@ window.__ModuleLoader__.load({
1627
1733
  if (!Number.isInteger(offset) || offset < 0) throw Error("offset must be a non-negative integer");
1628
1734
  return {
1629
1735
  ...current,
1736
+ protocolVersion: 2,
1630
1737
  objects: objects.slice(offset, offset + 50),
1631
1738
  objectCount: objects.length,
1632
1739
  ...offset + 50 < objects.length ? { nextOffset: offset + 50 } : {},
@@ -1655,7 +1762,15 @@ window.__ModuleLoader__.load({
1655
1762
  if (request.revision !== current.revision || adapter.busy()) throw Error("Sketch changed or is being edited; inspect again");
1656
1763
  let changedObjects;
1657
1764
  if (request.action === "apply") {
1658
- 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
+ }
1659
1774
  adapter.commit(next);
1660
1775
  const previous = new Map(before.layers.flatMap((l) => l.strokes.map((s) => [`${l.id}:${s.id}`, s])));
1661
1776
  changedObjects = next.layers.flatMap((l) => l.strokes.filter((s) => previous.get(`${l.id}:${s.id}`) !== s).map((s) => ({
@@ -2215,7 +2330,11 @@ window.__ModuleLoader__.load({
2215
2330
  update("finished");
2216
2331
  return completed.value;
2217
2332
  } catch (error) {
2218
- if (version === generation) update("failed");
2333
+ if (version === generation && error.code === "SKETCH_INVALID_BATCH") throw error;
2334
+ if (version === generation) {
2335
+ update("failed");
2336
+ throw new Error(`${error.message} Call inspect to obtain the current runId and revision before retrying.`, { cause: error });
2337
+ }
2219
2338
  throw error;
2220
2339
  } finally {
2221
2340
  pending = false;
@@ -2230,7 +2349,7 @@ window.__ModuleLoader__.load({
2230
2349
  //#endregion
2231
2350
  //#region src/sketch-agent-client.js
2232
2351
  function connectSketchAgent(rpc, sessionId, execute, report, pollDelay = () => 350) {
2233
- let stopped = false, token, timer, attempts = 0;
2352
+ let stopped = false, token, timer, attempts = 0, failures = 0;
2234
2353
  const call = (endpoint, payload) => rpc.call(CHANNEL, `sketch/${endpoint}`, {
2235
2354
  sessionId,
2236
2355
  token,
@@ -2260,13 +2379,24 @@ window.__ModuleLoader__.load({
2260
2379
  });
2261
2380
  }
2262
2381
  } catch (error) {
2263
- 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
+ }
2264
2392
  return;
2265
2393
  }
2394
+ failures = 0;
2266
2395
  if (!stopped) timer = setTimeout(poll, pollDelay());
2267
2396
  };
2268
2397
  const connect = () => void call("connect").then((value) => {
2269
2398
  token = value.token;
2399
+ attempts = 0;
2270
2400
  if (stopped) call("disconnect").catch(() => {});
2271
2401
  else poll();
2272
2402
  }, (error) => {
@@ -2293,12 +2423,13 @@ window.__ModuleLoader__.load({
2293
2423
  "#34c759",
2294
2424
  "#0088ff"
2295
2425
  ];
2296
- function SketchStudio({ open, agentEnabled, agentPreview, onOpen, onClose, attachSketch, enabled, t, incoming, sessionId, rpc }) {
2297
- const dialog = (0, react.useRef)(null), canvas = (0, react.useRef)(null), doc = (0, react.useRef)(createSketchLayers()), cache = (0, react.useRef)(/* @__PURE__ */ new Map());
2298
- const undo = (0, react.useRef)([]), redo = (0, react.useRef)([]), active = (0, react.useRef)(null), frame = (0, react.useRef)(null);
2299
- const images = (0, react.useRef)(/* @__PURE__ */ new Map()), saved = (0, react.useRef)(null), dirty = (0, react.useRef)(false), updateUi = (0, react.useRef)(false);
2300
- const documentId = (0, react.useRef)(crypto.randomUUID()), documentRevision = (0, react.useRef)(0), agentAdapter = (0, react.useRef)({}), agentSession = (0, react.useRef)(null);
2301
- 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");
2302
2433
  const agentLocked = agentState === "drawing";
2303
2434
  const [noticeHidden, setNoticeHidden] = (0, react.useState)(false);
2304
2435
  const [stability, setStability] = (0, react.useState)(0), [flow, setFlow] = (0, react.useState)(100), [picturesOpen, setPicturesOpen] = (0, react.useState)(false);
@@ -2386,10 +2517,10 @@ window.__ModuleLoader__.load({
2386
2517
  (0, react.useEffect)(() => () => {
2387
2518
  cancelAnimationFrame(frame.current);
2388
2519
  cache.current.clear();
2389
- agentRun.current?.dispose();
2390
2520
  }, []);
2391
2521
  const hasContent = () => doc.current.layers.some((layer) => layer.image || layer.strokes.length);
2392
2522
  const save = async (name) => {
2523
+ const savingDocument = documentId.current, savingRevision = documentRevision.current;
2393
2524
  const row = {
2394
2525
  id: saved.current?.id ?? crypto.randomUUID(),
2395
2526
  name: name?.trim() || saved.current?.name || `${t("sketchTitle")} ${(/* @__PURE__ */ new Date()).toLocaleString()}`,
@@ -2397,11 +2528,13 @@ window.__ModuleLoader__.load({
2397
2528
  doc: structuredClone(doc.current)
2398
2529
  };
2399
2530
  await sketchDrafts("save", row);
2400
- saved.current = {
2401
- id: row.id,
2402
- name: row.name
2403
- };
2404
- 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
+ }
2405
2538
  };
2406
2539
  const saveChanges = async () => {
2407
2540
  if (dirty.current && (hasContent() || saved.current)) await save();
@@ -2679,6 +2812,10 @@ window.__ModuleLoader__.load({
2679
2812
  schedule();
2680
2813
  };
2681
2814
  Object.assign(agentAdapter.current, {
2815
+ changed: (state) => {
2816
+ setAgentState(state);
2817
+ setNoticeHidden(false);
2818
+ },
2682
2819
  available: () => enabled && agentEnabled,
2683
2820
  previewEnabled: () => agentPreview,
2684
2821
  open: () => onOpen(),
@@ -2724,17 +2861,14 @@ window.__ModuleLoader__.load({
2724
2861
  agentRun.current ??= createSketchAgentRun({
2725
2862
  execute: (request) => agentSession.current(request),
2726
2863
  open: () => agentAdapter.current.open(),
2727
- changed: (state) => {
2728
- setAgentState(state);
2729
- setNoticeHidden(false);
2730
- },
2864
+ changed: (state) => agentAdapter.current.changed?.(state),
2731
2865
  busy: () => agentAdapter.current.busy(),
2732
2866
  previewEnabled: () => agentAdapter.current.previewEnabled()
2733
2867
  });
2734
2868
  (0, react.useEffect)(() => {
2735
2869
  if (!enabled || !agentEnabled) return;
2736
2870
  const api = Object.freeze({
2737
- version: 1,
2871
+ version: 2,
2738
2872
  sessionId,
2739
2873
  execute: (request) => agentRun.current.execute(request),
2740
2874
  export: async (format) => {
@@ -2761,9 +2895,12 @@ window.__ModuleLoader__.load({
2761
2895
  sessionId
2762
2896
  ]);
2763
2897
  (0, react.useEffect)(() => {
2764
- 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;
2765
2903
  let live = true;
2766
- agentRun.current.resume();
2767
2904
  const disconnect = connectSketchAgent(rpc, sessionId, (request) => {
2768
2905
  if (!live) throw Error("Sketch session disconnected");
2769
2906
  return agentRun.current.execute(request);
@@ -2773,7 +2910,6 @@ window.__ModuleLoader__.load({
2773
2910
  }, () => 350);
2774
2911
  return () => {
2775
2912
  live = false;
2776
- agentRun.current.stop();
2777
2913
  disconnect();
2778
2914
  };
2779
2915
  }, [
@@ -3650,13 +3786,13 @@ window.__ModuleLoader__.load({
3650
3786
  .codexLayerBody{overflow:hidden}.codexLayerStudio canvas{cursor:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24'%3E%3Ccircle cx='12' cy='12' r='7' fill='none' stroke='white' stroke-width='3'/%3E%3Ccircle cx='12' cy='12' r='7' fill='none' stroke='%23333' stroke-width='1'/%3E%3C/svg%3E") 12 12,crosshair}
3651
3787
  .codexSketchDialog .codexSketchSwatch{width:32px;height:32px;padding:0;border-radius:50%;corner-shape:round}.codexSketchSwatch::before{width:22px;height:22px;flex-shrink:0;border-radius:50%;corner-shape:round;clip-path:circle(50%)}.codexSketchSwatch[aria-pressed=true]::after{inset:1px;border:1.5px solid var(--sketch-fg);border-radius:50%;corner-shape:round}.codexSketchCustom{width:30px;height:30px;margin:1px 4px;corner-shape:round;clip-path:circle(50%)}.codexSketchCustom span{width:22px;height:22px;border-width:2px;corner-shape:round}
3652
3788
  .codexSketchPictures{position:absolute;left:0;top:0;z-index:3;background:var(--sketch-bg);border:1px solid var(--sketch-line);border-radius:12px;padding:10px;width:160px;max-height:100%;overflow:auto}.codexSketchPictures header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}.codexSketchPictures>div{display:flex;gap:5px;align-items:center;padding:4px;border:1px solid transparent;border-radius:8px}.codexSketchPictures>div[data-active=true]{border-color:#0a84ff}.codexSketchPictures img{width:90px;height:65px;object-fit:contain}.codexSketchPictures small{color:var(--sketch-muted)}
3653
- .codexSketchViewControls{display:flex;gap:2px;align-items:center}.codexSketchViewControls button{min-height:32px;padding:0 7px;border-radius:8px}.codexSketchKeyPanel{position:absolute;bottom:80px;left:12px;width:280px;max-height:65%;overflow:auto;padding:14px;border:1px solid var(--sketch-line);border-radius:12px;background:var(--sketch-bg);z-index:5;box-shadow:0 8px 30px #0003}.codexSketchKeyPanel label{display:flex;align-items:center;justify-content:space-between;min-height:32px;gap:12px}.codexSketchKeyPanel input:not([type=checkbox]){width:64px;border:1px solid var(--sketch-line);border-radius:6px;color:inherit;background:transparent;padding:4px;text-align:center}.codexSketchKeyPanel p,.codexSketchKeyPanel small{color:var(--sketch-muted);font-size:11px}.codexSketchControls{position:static}
3789
+ .codexSketchViewControls{display:flex;gap:2px;align-items:center}.codexSketchViewControls button{min-height:32px;padding:0 7px;border-radius:8px}.codexSketchKeyPanel{position:absolute;overflow:auto;overscroll-behavior:contain;padding:12px;border:1px solid var(--sketch-line);border-radius:14px;background:var(--sketch-bg);z-index:5;box-shadow:0 8px 30px #0003;scrollbar-width:thin}.codexSketchKeyPanel header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}.codexSketchKeyPanel header button{width:28px;height:28px;border-radius:8px;font-size:20px}.codexSketchKeyPanel label{display:flex;align-items:center;justify-content:space-between;min-height:32px;gap:8px}.codexSketchKeysEnabled{padding-bottom:8px;border-bottom:1px solid var(--sketch-line)}.codexSketchKeysEnabled input{margin:0;accent-color:#0a84ff}.codexSketchKeyGrid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:4px 16px;margin:10px 0}.codexSketchKeyPanel input:not([type=checkbox]){width:52px;min-width:0;border:1px solid var(--sketch-line);border-radius:6px;color:inherit;background:color-mix(in srgb,var(--sketch-fg) 4%,transparent);padding:4px;text-align:center;font:inherit}.codexSketchKeyPanel p,.codexSketchKeyPanel small{display:block;color:var(--sketch-muted);font-size:11px;line-height:1.5;margin:0}.codexSketchKeyPanel small{border-top:1px solid var(--sketch-line);padding-top:8px}@media(max-width:380px){.codexSketchKeyGrid{grid-template-columns:1fr}}.codexSketchControls{position:static}
3654
3790
 
3655
3791
  .codexSketchCursor{position:fixed;left:0;top:0;z-index:100;pointer-events:none;border:1px solid #222;border-radius:50%;corner-shape:round;box-shadow:0 0 0 1px #fff;box-sizing:border-box}.codexSketchCursor[hidden]{display:none}
3656
3792
  `;
3657
3793
  //#endregion
3658
3794
  //#region src/sketch-workspace.jsx
3659
- function SketchWorkspace({ preference, attachSketch, registerOpen, t, sessionId, rpc }) {
3795
+ function SketchWorkspace({ preference, attachSketch, registerOpen, t, sessionId, rpc, sessionState }) {
3660
3796
  const settings = (0, react.useSyncExternalStore)(preference.subscribe, preference.getSnapshot);
3661
3797
  const [open, setOpen] = (0, react.useState)(false);
3662
3798
  const [incoming, setIncoming] = (0, react.useState)(null);
@@ -3669,6 +3805,7 @@ window.__ModuleLoader__.load({
3669
3805
  }
3670
3806
  }), [registerOpen]);
3671
3807
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SketchStudio, {
3808
+ sessionState,
3672
3809
  agentPreview: settings.imageSketchAgentPreview,
3673
3810
  agentEnabled: settings.imageSketchAgent,
3674
3811
  onOpen: () => {
@@ -6410,7 +6547,7 @@ window.__ModuleLoader__.load({
6410
6547
  }
6411
6548
  //#endregion
6412
6549
  //#region src/version.js
6413
- const PACKAGE_VERSION = "2.1.0-beta.3";
6550
+ const PACKAGE_VERSION = "2.1.0-beta.5";
6414
6551
  //#endregion
6415
6552
  //#region src/client-recovery.js
6416
6553
  async function recoveryCall(rpc, endpoint, payload = {}, timeoutMs = 1e4) {
@@ -8727,6 +8864,8 @@ window.__ModuleLoader__.load({
8727
8864
  const conversation = ctx.get("conversation");
8728
8865
  const uiConversation = ctx.get("uiConversation");
8729
8866
  const sketchOpeners = /* @__PURE__ */ new Map();
8867
+ const sketchSessions = createSketchSessionRegistry();
8868
+ ctx.effect(() => () => sketchSessions.dispose(), "codex-subscription: sketch sessions");
8730
8869
  ctx.inject(["inputTriggers"], (triggerContext) => triggerContext.effect(() => triggerContext.get("inputTriggers").registerSource(createSketchTrigger({
8731
8870
  enabled: () => {
8732
8871
  const value = preference.getSnapshot();
@@ -8835,6 +8974,7 @@ window.__ModuleLoader__.load({
8835
8974
  t,
8836
8975
  sessionId,
8837
8976
  rpc,
8977
+ sessionState: sketchSessions.get(sessionId),
8838
8978
  registerOpen: (callback) => {
8839
8979
  sketchOpeners.set(sessionId, callback);
8840
8980
  return () => {