dsh-image-gen 0.1.7 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -2,9 +2,9 @@ import { credentialRef } from "@deepseek-ai/dsh-credentials";
2
2
  import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
3
3
  import { defineTool } from "@deepseek-ai/dsh-tools";
4
4
  import z from "@deepseek-ai/schemastery";
5
- import { randomUUID } from "node:crypto";
6
- import { mkdir, realpath, rename, unlink, writeFile } from "node:fs/promises";
5
+ import { mkdir, readFile, realpath, rename, stat, unlink, writeFile } from "node:fs/promises";
7
6
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
7
+ import { randomUUID } from "node:crypto";
8
8
  //#region lib/types/shared.js
9
9
  /** Values shared by the Host and browser Bundle faces. */
10
10
  /** Browser route used by the generated-image card. */
@@ -27,7 +27,7 @@ const DEFAULT_DASHSCOPE_ENDPOINT = "https://dashscope.aliyuncs.com/api/v1";
27
27
  const DEFAULT_GOOGLE_MODEL = "gemini-3.1-flash-image";
28
28
  const DEFAULT_OPENAI_MODEL = "gpt-image-2";
29
29
  const DEFAULT_SEEDREAM_MODEL = "doubao-seedream-5-0-260128";
30
- const DEFAULT_DASHSCOPE_MODEL = "wanx2.1-t2i-turbo";
30
+ const DEFAULT_DASHSCOPE_MODEL = "qwen-image-3.0";
31
31
  //#endregion
32
32
  //#region lib/types/config.js
33
33
  /** User-facing configuration for supported image providers. */
@@ -75,7 +75,7 @@ function resolveProvider(config) {
75
75
  case "dashscope": return {
76
76
  provider: "dashscope",
77
77
  apiKeyEnv: DASHSCOPE_API_KEY_ENV,
78
- model: config.dashscopeModel ?? "wanx2.1-t2i-turbo",
78
+ model: config.dashscopeModel ?? "qwen-image-3.0",
79
79
  endpoint: config.dashscopeEndpoint ?? "https://dashscope.aliyuncs.com/api/v1",
80
80
  imageSize: "1024*1024"
81
81
  };
@@ -91,83 +91,75 @@ function resolveProvider(config) {
91
91
  }
92
92
  //#endregion
93
93
  //#region lib/types/dashscope.js
94
- /** Call DashScope image synthesis/generation API and handle both synchronous output & async task polling. */
95
94
  async function generateDashScopeImage(options) {
95
+ assertQwenImageModel(options.model);
96
+ const formattedSize = formatSize(options.size);
97
+ return requestQwenImage({
98
+ ...options,
99
+ requestBody: {
100
+ model: options.model,
101
+ input: { messages: [{
102
+ role: "user",
103
+ content: [{ text: options.prompt }]
104
+ }] },
105
+ parameters: { ...formattedSize === void 0 ? {} : { size: formattedSize } }
106
+ },
107
+ operation: "generation"
108
+ });
109
+ }
110
+ async function editDashScopeImage(options) {
111
+ assertQwenImageModel(options.model);
112
+ const formattedSize = formatSize(options.size);
113
+ return requestQwenImage({
114
+ ...options,
115
+ requestBody: {
116
+ model: options.model,
117
+ input: { messages: [{
118
+ role: "user",
119
+ content: [{ image: toDataUrl$1(options.sourceImage) }, { text: options.prompt }]
120
+ }] },
121
+ parameters: {
122
+ prompt_extend: true,
123
+ ...formattedSize === void 0 ? {} : { size: formattedSize }
124
+ }
125
+ },
126
+ operation: "editing"
127
+ });
128
+ }
129
+ function assertQwenImageModel(model) {
130
+ if (!model.toLowerCase().startsWith("qwen-image")) throw new Error(`Unsupported DashScope image model ${model}. Configure a qwen-image model.`);
131
+ }
132
+ function formatSize(size) {
133
+ if (size === void 0 || size.length === 0) return void 0;
134
+ return size.replace("x", "*");
135
+ }
136
+ function toDataUrl$1(image) {
137
+ return `data:${image.mediaType};base64,${Buffer.from(image.data).toString("base64")}`;
138
+ }
139
+ async function requestQwenImage(options) {
96
140
  const base = options.endpoint.replace(/\/+$/, "");
97
- const formattedSize = (options.size ?? "1024*1024").replace("x", "*");
98
- const isQwenImage = options.model.toLowerCase().startsWith("qwen-image");
99
- const submitUrl = isQwenImage ? `${base}/services/aigc/multimodal-generation/generation` : `${base}/services/aigc/text2image/image-synthesis`;
100
- const requestBody = isQwenImage ? {
101
- model: options.model,
102
- input: { messages: [{
103
- role: "user",
104
- content: [{ text: options.prompt }]
105
- }] },
106
- parameters: { size: formattedSize }
107
- } : {
108
- model: options.model,
109
- input: { prompt: options.prompt },
110
- parameters: {
111
- size: formattedSize,
112
- n: 1
113
- }
114
- };
115
- const submitResponse = await fetch(submitUrl, {
141
+ const response = await fetch(`${base}/services/aigc/multimodal-generation/generation`, {
116
142
  method: "POST",
117
143
  ...options.signal ? { signal: options.signal } : {},
118
144
  headers: {
119
145
  "content-type": "application/json",
120
- authorization: `Bearer ${options.apiKey}`,
121
- "X-DashScope-Async": "enable"
146
+ authorization: `Bearer ${options.apiKey}`
122
147
  },
123
- body: JSON.stringify(requestBody)
148
+ body: JSON.stringify(options.requestBody)
124
149
  });
125
- if (!submitResponse.ok) {
126
- const errorText = await submitResponse.text();
127
- throw new Error(`DashScope image generation failed (${String(submitResponse.status)}): ${errorText}`);
128
- }
129
- const submitResult = await submitResponse.json();
130
- const directImageUrl = extractImageUrl(submitResult);
131
- if (directImageUrl !== void 0) return downloadImageBlob(directImageUrl, options);
132
- const taskId = submitResult.output?.task_id;
133
- if (taskId === void 0 || taskId.length === 0) throw new Error(`DashScope did not return an image URL or task_id: ${submitResult.message ?? JSON.stringify(submitResult)}`);
134
- const taskQueryUrl = `${base}/tasks/${taskId}`;
135
- const startTime = Date.now();
136
- const timeoutMs = 6e4;
137
- while (Date.now() - startTime < timeoutMs) {
138
- if (options.signal?.aborted) throw new Error("DashScope image task polling aborted");
139
- await new Promise((resolve, reject) => {
140
- const timer = setTimeout(resolve, 1500);
141
- if (options.signal !== void 0) options.signal.addEventListener("abort", () => {
142
- clearTimeout(timer);
143
- reject(/* @__PURE__ */ new Error("DashScope image generation aborted"));
144
- }, { once: true });
145
- });
146
- const taskResponse = await fetch(taskQueryUrl, {
147
- method: "GET",
148
- ...options.signal ? { signal: options.signal } : {},
149
- headers: { authorization: `Bearer ${options.apiKey}` }
150
- });
151
- if (!taskResponse.ok) {
152
- const errorText = await taskResponse.text();
153
- throw new Error(`DashScope task query failed (${String(taskResponse.status)}): ${errorText}`);
154
- }
155
- const taskResult = await taskResponse.json();
156
- const status = taskResult.output?.task_status;
157
- if (status === "SUCCEEDED") {
158
- const imageUrl = extractImageUrl(taskResult);
159
- if (imageUrl === void 0 || imageUrl.length === 0) throw new Error("DashScope task succeeded but returned no image URL");
160
- return downloadImageBlob(imageUrl, options);
161
- }
162
- if (status === "FAILED") throw new Error(`DashScope image generation failed: ${taskResult.output?.message ?? taskResult.message ?? "Unknown error"}`);
150
+ if (!response.ok) {
151
+ const errorText = await response.text();
152
+ throw new Error(`DashScope image ${options.operation} failed (${String(response.status)}): ${errorText}`);
163
153
  }
164
- throw new Error(`DashScope image generation timed out after ${String(timeoutMs / 1e3)} seconds`);
154
+ const payload = await response.json();
155
+ const imageUrl = extractImageUrl(payload);
156
+ if (imageUrl === void 0) throw new Error(`DashScope image ${options.operation} returned no image URL: ${payload.message ?? JSON.stringify(payload)}`);
157
+ return downloadImageBlob(imageUrl, options);
165
158
  }
166
159
  function extractImageUrl(response) {
167
- const resultUrl = response.output?.results?.[0]?.url;
168
- if (resultUrl !== void 0 && resultUrl.length > 0) return resultUrl;
169
160
  const contents = response.output?.choices?.[0]?.message?.content;
170
- if (Array.isArray(contents)) for (const item of contents) {
161
+ if (!Array.isArray(contents)) return void 0;
162
+ for (const item of contents) {
171
163
  if (item.image !== void 0 && item.image.length > 0) return item.image;
172
164
  if (item.image_url !== void 0 && item.image_url.length > 0) return item.image_url;
173
165
  if (item.url !== void 0 && item.url.length > 0) return item.url;
@@ -187,10 +179,34 @@ async function downloadImageBlob(imageUrl, options) {
187
179
  }
188
180
  //#endregion
189
181
  //#region lib/types/google.js
190
- const ERROR_LIMIT$1 = 4096;
182
+ const ERROR_LIMIT$2 = 4096;
191
183
  const REQUESTED_MEDIA_TYPE = "image/jpeg";
192
- /** Send one native Google image request and validate its inline image response. */
193
- async function generateGoogleImage(input) {
184
+ /** Send one native Google text-to-image request. */
185
+ function generateGoogleImage(input) {
186
+ return requestGoogleImage({
187
+ ...input,
188
+ operation: "generation",
189
+ interactionInput: input.prompt
190
+ });
191
+ }
192
+ /** Send one native Google image-editing request using already-resolved bytes. */
193
+ function editGoogleImage(input) {
194
+ return requestGoogleImage({
195
+ ...input,
196
+ operation: "editing",
197
+ interactionInput: [{
198
+ type: "text",
199
+ text: input.prompt
200
+ }, {
201
+ type: "image",
202
+ mime_type: input.sourceImage.mediaType,
203
+ data: Buffer.from(input.sourceImage.data).toString("base64")
204
+ }]
205
+ });
206
+ }
207
+ /** Shared Google request, response parsing, decoding, and size enforcement. */
208
+ async function requestGoogleImage(input) {
209
+ const label = `Google image ${input.operation}`;
194
210
  const response = await fetch(input.endpoint, {
195
211
  method: "POST",
196
212
  redirect: "error",
@@ -201,7 +217,7 @@ async function generateGoogleImage(input) {
201
217
  },
202
218
  body: JSON.stringify({
203
219
  model: input.model,
204
- input: input.prompt,
220
+ input: input.interactionInput,
205
221
  response_format: {
206
222
  type: "image",
207
223
  mime_type: REQUESTED_MEDIA_TYPE,
@@ -210,33 +226,33 @@ async function generateGoogleImage(input) {
210
226
  }
211
227
  })
212
228
  });
213
- const text = await readBoundedText$1(response, Math.ceil(input.maxBytes * 1.4) + ERROR_LIMIT$1);
214
- if (!response.ok) throw new Error(`Google image generation failed (${response.status}): ${text.slice(0, ERROR_LIMIT$1)}`);
229
+ const text = await readBoundedText$2(response, Math.ceil(input.maxBytes * 1.4) + ERROR_LIMIT$2, label);
230
+ if (!response.ok) throw new Error(`${label} failed (${response.status}): ${text.slice(0, ERROR_LIMIT$2)}`);
215
231
  let payload;
216
232
  try {
217
233
  payload = JSON.parse(text);
218
234
  } catch {
219
- throw new Error("Google image generation returned invalid JSON");
235
+ throw new Error(`${label} returned invalid JSON`);
220
236
  }
221
237
  const image = outputImage(payload);
222
- if (image === void 0) throw new Error(`Google image generation returned no image: ${text.slice(0, ERROR_LIMIT$1)}`);
238
+ if (image === void 0) throw new Error(`${label} returned no image: ${text.slice(0, ERROR_LIMIT$2)}`);
223
239
  const mediaType = mediaTypeOf(image.mime_type ?? REQUESTED_MEDIA_TYPE);
224
- if (mediaType === void 0) throw new Error(`Google image generation returned unsupported media type ${JSON.stringify(image.mime_type)}`);
225
- const data = decodeBase64$1(image.data);
226
- if (data.byteLength > input.maxBytes) throw new Error(`Google image generation exceeded the ${String(input.maxBytes)} byte image limit`);
240
+ if (mediaType === void 0) throw new Error(`${label} returned unsupported media type ${JSON.stringify(image.mime_type)}`);
241
+ const data = decodeBase64$2(image.data, label);
242
+ if (data.byteLength > input.maxBytes) throw new Error(`${label} exceeded the ${String(input.maxBytes)} byte image limit`);
227
243
  return {
228
244
  data,
229
245
  mediaType
230
246
  };
231
247
  }
232
248
  function outputImage(value) {
233
- const interaction = record$1(value);
249
+ const interaction = record$2(value);
234
250
  if (interaction === void 0) return void 0;
235
251
  const direct = imageContent(interaction.output_image, false);
236
252
  if (direct !== void 0) return direct;
237
253
  if (!Array.isArray(interaction.steps)) return void 0;
238
254
  for (const step of interaction.steps) {
239
- const modelOutput = record$1(step);
255
+ const modelOutput = record$2(step);
240
256
  if (modelOutput?.type !== "model_output" || !Array.isArray(modelOutput.content)) continue;
241
257
  for (const content of modelOutput.content) {
242
258
  const image = imageContent(content, true);
@@ -245,27 +261,27 @@ function outputImage(value) {
245
261
  }
246
262
  }
247
263
  function imageContent(value, requiresImageType) {
248
- const image = record$1(value);
264
+ const image = record$2(value);
249
265
  if (image === void 0 || requiresImageType && image.type !== "image" || typeof image.data !== "string") return void 0;
250
266
  return {
251
267
  data: image.data,
252
268
  mime_type: image.mime_type
253
269
  };
254
270
  }
255
- function record$1(value) {
271
+ function record$2(value) {
256
272
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
257
273
  }
258
274
  function mediaTypeOf(value) {
259
275
  return value === "image/png" || value === "image/jpeg" || value === "image/webp" || value === "image/gif" ? value : void 0;
260
276
  }
261
- function decodeBase64$1(data) {
277
+ function decodeBase64$2(data, label) {
262
278
  const clean = data.replace(/\s+/g, "");
263
- if (clean.length === 0) throw new Error("Google image generation returned invalid base64 image data");
279
+ if (clean.length === 0) throw new Error(`${label} returned invalid base64 image data`);
264
280
  const decoded = Buffer.from(clean, "base64");
265
- if (decoded.length === 0) throw new Error("Google image generation returned invalid base64 image data");
281
+ if (decoded.length === 0) throw new Error(`${label} returned invalid base64 image data`);
266
282
  return new Uint8Array(decoded);
267
283
  }
268
- async function readBoundedText$1(response, maxBytes) {
284
+ async function readBoundedText$2(response, maxBytes, label) {
269
285
  if (response.body === null) return "";
270
286
  const reader = response.body.getReader();
271
287
  const chunks = [];
@@ -275,7 +291,7 @@ async function readBoundedText$1(response, maxBytes) {
275
291
  const next = await reader.read();
276
292
  if (next.done) break;
277
293
  bytes += next.value.byteLength;
278
- if (bytes > maxBytes) throw new Error(`Google image generation response exceeded the ${String(maxBytes)} byte limit`);
294
+ if (bytes > maxBytes) throw new Error(`${label} response exceeded the ${String(maxBytes)} byte limit`);
279
295
  chunks.push(next.value);
280
296
  }
281
297
  } finally {
@@ -290,6 +306,156 @@ async function readBoundedText$1(response, maxBytes) {
290
306
  return new TextDecoder().decode(joined);
291
307
  }
292
308
  //#endregion
309
+ //#region lib/types/reference-image.js
310
+ /** Latest-DSH compatibility boundary for resolving conversation image references. */
311
+ /**
312
+ * Resolve and read the image to edit from the current effective conversation.
313
+ *
314
+ * This is the only module allowed to know how DSH sessions expose effective
315
+ * messages or how nested model-facing content stores durable image refs.
316
+ */
317
+ async function resolveReferenceImage(input) {
318
+ if (input.sourceAttachmentId !== void 0 && input.sourcePath !== void 0) throw new Error("edit_image accepts either source_attachment_id or source_path, not both");
319
+ if (input.sourcePath !== void 0) return readWorkspaceReferenceImage({
320
+ sourcePath: input.sourcePath,
321
+ ...input.agent?.session.header?.cwd === void 0 ? {} : { workspaceRoot: input.agent.session.header.cwd },
322
+ ...input.maxBytes === void 0 ? {} : { maxBytes: input.maxBytes },
323
+ signal: input.signal
324
+ });
325
+ if (input.agent === void 0) throw new Error("edit_image requires an active DSH agent session to resolve a reference image");
326
+ const ref = findReferenceImage(input.agent.session.deriveMessages(), input.sourceAttachmentId);
327
+ if (ref === void 0) {
328
+ if (input.sourceAttachmentId !== void 0) throw new Error(`edit_image could not find image attachment ${input.sourceAttachmentId} in the current conversation`);
329
+ throw new Error("edit_image requires an image in the current conversation; upload or generate an image first");
330
+ }
331
+ const stored = await input.attachments.readImage(ref, input.signal);
332
+ return {
333
+ data: stored.data,
334
+ mediaType: stored.ref.mediaType
335
+ };
336
+ }
337
+ /**
338
+ * Read an explicitly named workspace image without exposing filesystem or DSH
339
+ * details to provider adapters. Both lexical and real-path containment are
340
+ * enforced so absolute paths, parent traversal, and symlink escapes fail.
341
+ */
342
+ async function readWorkspaceReferenceImage(input) {
343
+ const requested = input.sourcePath.trim();
344
+ if (requested.length === 0) throw new Error("edit_image source_path must not be empty");
345
+ if (input.workspaceRoot === void 0) throw new Error("edit_image source_path requires an active DSH session workspace");
346
+ const root = resolve(input.workspaceRoot);
347
+ const candidate = isAbsolute(requested) ? resolve(requested) : resolve(root, requested);
348
+ if (!containsPath$1(root, candidate)) throw new Error("edit_image source_path must stay inside the session workspace: " + requested);
349
+ let realRoot;
350
+ let realCandidate;
351
+ try {
352
+ [realRoot, realCandidate] = await Promise.all([realpath(root), realpath(candidate)]);
353
+ } catch (error) {
354
+ if (error.code === "ENOENT") throw new Error("edit_image could not find workspace image: " + requested);
355
+ throw error;
356
+ }
357
+ if (!containsPath$1(realRoot, realCandidate)) throw new Error("edit_image source_path resolves outside the session workspace: " + requested);
358
+ const file = await stat(realCandidate);
359
+ if (!file.isFile()) throw new Error("edit_image source_path is not a file: " + requested);
360
+ if (input.maxBytes !== void 0 && file.size > input.maxBytes) throw new Error("edit_image source image is too large (" + file.size + " bytes; maximum " + input.maxBytes + ")");
361
+ const data = await readFile(realCandidate, { signal: input.signal });
362
+ const mediaType = detectImageMediaType(data);
363
+ if (mediaType === void 0) throw new Error("edit_image source_path is not a supported PNG, JPEG, WebP, or GIF image: " + requested);
364
+ return {
365
+ data: new Uint8Array(data),
366
+ mediaType
367
+ };
368
+ }
369
+ /** Find the newest matching image in the effective, replacement-aware history. */
370
+ function findReferenceImage(messages, sourceAttachmentId) {
371
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
372
+ const found = findInBlocks(messages[index]?.content ?? [], sourceAttachmentId);
373
+ if (found !== void 0) return found;
374
+ }
375
+ }
376
+ /** Validate an untrusted serialized image reference at an HTTP/UI boundary. */
377
+ function parseImageAttachmentRef(value) {
378
+ const ref = record$1(value);
379
+ if (ref === void 0) return void 0;
380
+ if (typeof ref.attachmentId !== "string" || !imageMediaType$2(ref.mediaType)) return void 0;
381
+ if (!nonNegativeInteger(ref.bytes) || !positiveInteger(ref.width) || !positiveInteger(ref.height)) return void 0;
382
+ if (ref.name !== void 0 && typeof ref.name !== "string") return void 0;
383
+ const originalDimensions = ref.originalDimensions === void 0 ? void 0 : parseDimensions(ref.originalDimensions);
384
+ if (ref.originalDimensions !== void 0 && originalDimensions === void 0) return void 0;
385
+ return {
386
+ attachmentId: ref.attachmentId,
387
+ mediaType: ref.mediaType,
388
+ bytes: ref.bytes,
389
+ width: ref.width,
390
+ height: ref.height,
391
+ ...typeof ref.name === "string" ? { name: ref.name } : {},
392
+ ...originalDimensions === void 0 ? {} : { originalDimensions }
393
+ };
394
+ }
395
+ function findInBlocks(blocks, sourceAttachmentId) {
396
+ for (let index = blocks.length - 1; index >= 0; index -= 1) {
397
+ const block = blocks[index];
398
+ if (block === void 0) continue;
399
+ if (block.type === "image") {
400
+ if (sourceAttachmentId === void 0 || String(block.attachment.attachmentId) === sourceAttachmentId) return block.attachment;
401
+ continue;
402
+ }
403
+ if (block.type === "tool-result") {
404
+ const nested = findInBlocks(block.content, sourceAttachmentId);
405
+ if (nested !== void 0) return nested;
406
+ }
407
+ }
408
+ }
409
+ function parseDimensions(value) {
410
+ const dimensions = record$1(value);
411
+ if (dimensions === void 0 || !positiveInteger(dimensions.width) || !positiveInteger(dimensions.height)) return void 0;
412
+ return {
413
+ width: dimensions.width,
414
+ height: dimensions.height
415
+ };
416
+ }
417
+ function imageMediaType$2(value) {
418
+ return value === "image/png" || value === "image/jpeg" || value === "image/webp" || value === "image/gif";
419
+ }
420
+ function detectImageMediaType(data) {
421
+ if (startsWith(data, [
422
+ 137,
423
+ 80,
424
+ 78,
425
+ 71,
426
+ 13,
427
+ 10,
428
+ 26,
429
+ 10
430
+ ])) return "image/png";
431
+ if (startsWith(data, [
432
+ 255,
433
+ 216,
434
+ 255
435
+ ])) return "image/jpeg";
436
+ if (ascii(data, 0, 6) === "GIF87a" || ascii(data, 0, 6) === "GIF89a") return "image/gif";
437
+ if (ascii(data, 0, 4) === "RIFF" && ascii(data, 8, 4) === "WEBP") return "image/webp";
438
+ }
439
+ function startsWith(data, signature) {
440
+ return signature.every((byte, index) => data[index] === byte);
441
+ }
442
+ function ascii(data, offset, length) {
443
+ return String.fromCharCode(...data.subarray(offset, offset + length));
444
+ }
445
+ function containsPath$1(parent, child) {
446
+ const rel = relative(parent, child);
447
+ return rel === "" || rel !== ".." && !rel.startsWith(".." + sep) && !isAbsolute(rel);
448
+ }
449
+ function positiveInteger(value) {
450
+ return typeof value === "number" && Number.isInteger(value) && value > 0;
451
+ }
452
+ function nonNegativeInteger(value) {
453
+ return typeof value === "number" && Number.isInteger(value) && value >= 0;
454
+ }
455
+ function record$1(value) {
456
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
457
+ }
458
+ //#endregion
293
459
  //#region lib/types/image-route.js
294
460
  const MAX_BODY_BYTES = 4096;
295
461
  /** Serve one verified durable image reference to a same-origin browser request. */
@@ -324,20 +490,10 @@ async function serveImage(req, res, deps) {
324
490
  function imageAttachmentFromMeta(meta) {
325
491
  const value = record(meta);
326
492
  if (value?.kind !== "dsh-image-gen") return void 0;
327
- return imageAttachment(value.attachment);
493
+ return parseImageAttachmentRef(value.attachment);
328
494
  }
329
495
  function attachmentFromRequest(value) {
330
- return imageAttachment(record(value)?.attachment);
331
- }
332
- function imageAttachment(value) {
333
- const ref = record(value);
334
- if (ref === void 0) return void 0;
335
- if (typeof ref.attachmentId !== "string" || !mediaType(ref.mediaType) || typeof ref.bytes !== "number" || typeof ref.width !== "number" || typeof ref.height !== "number") return void 0;
336
- if (ref.name !== void 0 && typeof ref.name !== "string") return void 0;
337
- return ref;
338
- }
339
- function mediaType(value) {
340
- return value === "image/png" || value === "image/jpeg" || value === "image/webp" || value === "image/gif";
496
+ return parseImageAttachmentRef(record(value)?.attachment);
341
497
  }
342
498
  function record(value) {
343
499
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
@@ -362,10 +518,9 @@ function jsonError(res, status, code) {
362
518
  }
363
519
  //#endregion
364
520
  //#region lib/types/openai-compatible.js
365
- const ERROR_LIMIT = 4096;
366
- /** Generate an image from an OpenAI-compatible endpoint. */
521
+ const ERROR_LIMIT$1 = 4096;
367
522
  async function generateOpenAICompatibleImage(input) {
368
- const response = await fetch(imageEndpoint(input.baseURL), {
523
+ return parseImageResponse(await fetch(imageEndpoint$1(input.baseURL, "generations"), {
369
524
  method: "POST",
370
525
  redirect: "error",
371
526
  signal: input.signal,
@@ -379,20 +534,174 @@ async function generateOpenAICompatibleImage(input) {
379
534
  size: input.size,
380
535
  ...input.provider === "seedream" ? { response_format: "url" } : {}
381
536
  })
537
+ }), input.provider, input);
538
+ }
539
+ async function editOpenAICompatibleImage(input) {
540
+ const form = new FormData();
541
+ const uploadBytes = new Uint8Array(input.sourceImage.data);
542
+ form.append("image", new Blob([uploadBytes], { type: input.sourceImage.mediaType }), `reference.${extensionOf(input.sourceImage.mediaType)}`);
543
+ form.append("prompt", input.prompt);
544
+ form.append("model", input.model);
545
+ if (input.size !== void 0 && input.size.length > 0) form.append("size", input.size);
546
+ return parseImageResponse(await fetch(imageEndpoint$1(input.baseURL, "edits"), {
547
+ method: "POST",
548
+ redirect: "error",
549
+ signal: input.signal,
550
+ headers: { authorization: `Bearer ${input.apiKey}` },
551
+ body: form
552
+ }), "openai", input);
553
+ }
554
+ async function parseImageResponse(response, provider, input) {
555
+ const text = await readBoundedText$1(response, Math.ceil(input.maxBytes * 1.4) + ERROR_LIMIT$1);
556
+ if (!response.ok) throw new Error(`${provider} image request failed (${response.status}): ${text.slice(0, ERROR_LIMIT$1)}`);
557
+ let payload;
558
+ try {
559
+ payload = JSON.parse(text);
560
+ } catch {
561
+ throw new Error(`${provider} image request returned invalid JSON`);
562
+ }
563
+ const image = firstImage$1(payload);
564
+ if (image === void 0) throw new Error(`${provider} image request returned no image: ${text.slice(0, ERROR_LIMIT$1)}`);
565
+ if (image.b64_json !== void 0) return {
566
+ data: decodeBase64$1(image.b64_json, provider),
567
+ mediaType: imageMediaType$1(image.mime_type) ?? "image/png"
568
+ };
569
+ return downloadImage$1(image.url, provider, input);
570
+ }
571
+ function imageEndpoint$1(baseURL, operation) {
572
+ try {
573
+ return new URL(`images/${operation}`, baseURL.endsWith("/") ? baseURL : `${baseURL}/`).toString();
574
+ } catch {
575
+ throw new Error("Image endpoint must be an absolute URL");
576
+ }
577
+ }
578
+ function firstImage$1(value) {
579
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
580
+ const record = value;
581
+ const data = Array.isArray(record.data) ? record.data : Array.isArray(record.images) ? record.images : Array.isArray(record.output) ? record.output : void 0;
582
+ if (data === void 0 || data.length === 0) return void 0;
583
+ const candidate = data[0];
584
+ if (typeof candidate !== "object" || candidate === null || Array.isArray(candidate)) return void 0;
585
+ const item = candidate;
586
+ const mime = typeof item.mime_type === "string" ? item.mime_type : typeof item.mime === "string" ? item.mime : void 0;
587
+ return typeof item.b64_json === "string" ? {
588
+ b64_json: item.b64_json,
589
+ ...mime === void 0 ? {} : { mime_type: mime }
590
+ } : typeof item.url === "string" ? {
591
+ url: item.url,
592
+ ...mime === void 0 ? {} : { mime_type: mime }
593
+ } : void 0;
594
+ }
595
+ async function downloadImage$1(url, provider, input) {
596
+ if (url === void 0) throw new Error(`${provider} image request returned no image data`);
597
+ if (url.startsWith("data:")) {
598
+ const parsed = parseDataUrl(url);
599
+ if (parsed === void 0) throw new Error(`${provider} image request returned invalid data URL`);
600
+ return {
601
+ data: decodeBase64$1(parsed.base64, provider),
602
+ mediaType: imageMediaType$1(parsed.mediaType) ?? "image/png"
603
+ };
604
+ }
605
+ const response = await fetch(url, {
606
+ redirect: "follow",
607
+ signal: input.signal,
608
+ ...input.apiKey === void 0 ? {} : { headers: { authorization: `Bearer ${input.apiKey}` } }
609
+ });
610
+ if (!response.ok) throw new Error(`${provider} image download failed (${response.status})`);
611
+ const mediaType = imageMediaType$1(response.headers.get("content-type"));
612
+ if (mediaType === void 0) throw new Error(`${provider} image download returned unsupported content type`);
613
+ return {
614
+ data: await readBoundedBytes$1(response, input.maxBytes),
615
+ mediaType
616
+ };
617
+ }
618
+ function parseDataUrl(value) {
619
+ const match = /^data:([^;,]+);base64,(.*)$/s.exec(value.trim());
620
+ return match?.[1] !== void 0 && match[2] !== void 0 ? {
621
+ mediaType: match[1],
622
+ base64: match[2]
623
+ } : void 0;
624
+ }
625
+ function extensionOf(mediaType) {
626
+ switch (mediaType) {
627
+ case "image/jpeg": return "jpg";
628
+ case "image/webp": return "webp";
629
+ case "image/gif": return "gif";
630
+ case "image/png": return "png";
631
+ }
632
+ }
633
+ function decodeBase64$1(value, provider) {
634
+ const clean = (parseDataUrl(value)?.base64 ?? value).replace(/\s+/g, "");
635
+ if (clean.length === 0) throw new Error(`${provider} image request returned invalid base64 image data`);
636
+ const decoded = Buffer.from(clean, "base64");
637
+ if (decoded.length === 0) throw new Error(`${provider} image request returned invalid base64 image data`);
638
+ return new Uint8Array(decoded);
639
+ }
640
+ function imageMediaType$1(value) {
641
+ const mediaType = value?.split(";", 1)[0]?.trim().toLowerCase();
642
+ return mediaType === "image/png" || mediaType === "image/jpeg" || mediaType === "image/webp" || mediaType === "image/gif" ? mediaType : void 0;
643
+ }
644
+ async function readBoundedText$1(response, maxBytes) {
645
+ return new TextDecoder().decode(await readBoundedBytes$1(response, maxBytes));
646
+ }
647
+ async function readBoundedBytes$1(response, maxBytes) {
648
+ if (response.body === null) return /* @__PURE__ */ new Uint8Array();
649
+ const reader = response.body.getReader();
650
+ const chunks = [];
651
+ let bytes = 0;
652
+ try {
653
+ for (;;) {
654
+ const next = await reader.read();
655
+ if (next.done) break;
656
+ bytes += next.value.byteLength;
657
+ if (bytes > maxBytes) throw new Error(`Image response exceeded the ${String(maxBytes)} byte limit`);
658
+ chunks.push(next.value);
659
+ }
660
+ } finally {
661
+ reader.releaseLock();
662
+ }
663
+ const joined = new Uint8Array(bytes);
664
+ let offset = 0;
665
+ for (const chunk of chunks) {
666
+ joined.set(chunk, offset);
667
+ offset += chunk.byteLength;
668
+ }
669
+ return joined;
670
+ }
671
+ //#endregion
672
+ //#region lib/types/seedream.js
673
+ const ERROR_LIMIT = 4096;
674
+ /** Edit one image through Ark ImageGenerations using a data-URL reference. */
675
+ async function editSeedreamImage(input) {
676
+ const response = await fetch(imageEndpoint(input.baseURL), {
677
+ method: "POST",
678
+ redirect: "error",
679
+ signal: input.signal,
680
+ headers: {
681
+ authorization: `Bearer ${input.apiKey}`,
682
+ "content-type": "application/json"
683
+ },
684
+ body: JSON.stringify({
685
+ model: input.model,
686
+ prompt: input.prompt,
687
+ image: [toDataUrl(input.sourceImage)],
688
+ ...input.size === void 0 || input.size.length === 0 ? {} : { size: input.size },
689
+ response_format: "b64_json"
690
+ })
382
691
  });
383
692
  const text = await readBoundedText(response, Math.ceil(input.maxBytes * 1.4) + ERROR_LIMIT);
384
- if (!response.ok) throw new Error(`${input.provider} image generation failed (${response.status}): ${text.slice(0, ERROR_LIMIT)}`);
693
+ if (!response.ok) throw new Error(`seedream image editing failed (${response.status}): ${text.slice(0, ERROR_LIMIT)}`);
385
694
  let payload;
386
695
  try {
387
696
  payload = JSON.parse(text);
388
697
  } catch {
389
- throw new Error(`${input.provider} image generation returned invalid JSON`);
698
+ throw new Error("seedream image editing returned invalid JSON");
390
699
  }
391
700
  const image = firstImage(payload);
392
- if (image === void 0) throw new Error(`${input.provider} image generation returned no image: ${text.slice(0, ERROR_LIMIT)}`);
701
+ if (image === void 0) throw new Error(`seedream image editing returned no image: ${text.slice(0, ERROR_LIMIT)}`);
393
702
  if (image.b64_json !== void 0) return {
394
- data: decodeBase64(image.b64_json, input.provider),
395
- mediaType: "image/png"
703
+ data: decodeBase64(image.b64_json),
704
+ mediaType: imageMediaType(image.mime_type) ?? "image/png"
396
705
  };
397
706
  return downloadImage(image.url, input);
398
707
  }
@@ -400,37 +709,48 @@ function imageEndpoint(baseURL) {
400
709
  try {
401
710
  return new URL("images/generations", baseURL.endsWith("/") ? baseURL : `${baseURL}/`).toString();
402
711
  } catch {
403
- throw new Error("Image generation endpoint must be an absolute URL");
712
+ throw new Error("Seedream image endpoint must be an absolute URL");
404
713
  }
405
714
  }
715
+ function toDataUrl(image) {
716
+ return `data:${image.mediaType};base64,${Buffer.from(image.data).toString("base64")}`;
717
+ }
406
718
  function firstImage(value) {
407
719
  if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
408
- const data = value.data;
409
- if (!Array.isArray(data) || data.length === 0) return void 0;
720
+ const record = value;
721
+ const data = Array.isArray(record.data) ? record.data : Array.isArray(record.images) ? record.images : Array.isArray(record.output) ? record.output : void 0;
722
+ if (data === void 0 || data.length === 0) return void 0;
410
723
  const candidate = data[0];
411
724
  if (typeof candidate !== "object" || candidate === null || Array.isArray(candidate)) return void 0;
412
725
  const item = candidate;
413
- return typeof item.b64_json === "string" ? { b64_json: item.b64_json } : typeof item.url === "string" ? { url: item.url } : void 0;
726
+ const mime = typeof item.mime_type === "string" ? item.mime_type : typeof item.mime === "string" ? item.mime : void 0;
727
+ return typeof item.b64_json === "string" ? {
728
+ b64_json: item.b64_json,
729
+ ...mime === void 0 ? {} : { mime_type: mime }
730
+ } : typeof item.url === "string" ? {
731
+ url: item.url,
732
+ ...mime === void 0 ? {} : { mime_type: mime }
733
+ } : void 0;
414
734
  }
415
735
  async function downloadImage(url, input) {
416
- if (url === void 0) throw new Error(`${input.provider} image generation returned no image data`);
736
+ if (url === void 0) throw new Error("seedream image editing returned no image data");
417
737
  const response = await fetch(url, {
418
738
  redirect: "follow",
419
739
  signal: input.signal
420
740
  });
421
- if (!response.ok) throw new Error(`${input.provider} image download failed (${response.status})`);
741
+ if (!response.ok) throw new Error(`seedream image download failed (${response.status})`);
422
742
  const mediaType = imageMediaType(response.headers.get("content-type"));
423
- if (mediaType === void 0) throw new Error(`${input.provider} image download returned unsupported content type`);
743
+ if (mediaType === void 0) throw new Error("seedream image download returned unsupported content type");
424
744
  return {
425
745
  data: await readBoundedBytes(response, input.maxBytes),
426
746
  mediaType
427
747
  };
428
748
  }
429
- function decodeBase64(value, provider) {
749
+ function decodeBase64(value) {
430
750
  const clean = value.replace(/\s+/g, "");
431
- if (clean.length === 0) throw new Error(`${provider} image generation returned invalid base64 image data`);
751
+ if (clean.length === 0) throw new Error("seedream image editing returned invalid base64 image data");
432
752
  const decoded = Buffer.from(clean, "base64");
433
- if (decoded.length === 0) throw new Error(`${provider} image generation returned invalid base64 image data`);
753
+ if (decoded.length === 0) throw new Error("seedream image editing returned invalid base64 image data");
434
754
  return new Uint8Array(decoded);
435
755
  }
436
756
  function imageMediaType(value) {
@@ -582,16 +902,13 @@ async function saveImageToWorkspace(options) {
582
902
  }
583
903
  //#endregion
584
904
  //#region lib/types/index.js
585
- /** Cordis plugin name. */
586
905
  const name = "dsh-image-gen";
587
- /** Host services required by the Bundle. */
588
906
  const inject = [
589
907
  "tools",
590
908
  "attachments",
591
909
  "credentials",
592
910
  "webServer"
593
911
  ];
594
- /** Register settings, the image route, and the model-callable tool. */
595
912
  function apply(ctx, config = {}) {
596
913
  let current = () => config;
597
914
  installSettingsSection(ctx, settingsNamespace(IMAGE_GENERATION_NAMESPACE), Config, config, {
@@ -607,7 +924,7 @@ function apply(ctx, config = {}) {
607
924
  }), "dsh-image-gen: image route");
608
925
  ctx.tools.register(defineTool({
609
926
  name: "generate_image",
610
- description: "Generate one image with the configured image provider. Use when the user explicitly asks to create or draw an image. Give a complete visual prompt including subject, composition, style, lighting, and any exact text that should appear. A successful image is already attached directly to the conversation; with workspace saving enabled (the default) it is also written as a file under the session workspace, and the result's savedTo field carries that absolute file path. Do not call read, glob, or other tools to locate or verify the image.",
927
+ description: "Generate a new image with the configured provider. Use when the user asks to create or draw a new image; use edit_image instead when they want to change an existing image. Give a complete visual prompt including subject, composition, style, lighting, and any exact text that should appear. A successful image is attached directly to the conversation and may also be saved under the session workspace. Do not call read, glob, or other tools to locate or verify the image.",
611
928
  parameters: {
612
929
  prompt: {
613
930
  type: "string",
@@ -641,72 +958,7 @@ function apply(ctx, config = {}) {
641
958
  description: "Optional dimensions or size tier for OpenAI, Seedream, or DashScope."
642
959
  }
643
960
  },
644
- output: {
645
- schema: {
646
- type: "object",
647
- additionalProperties: false,
648
- properties: {
649
- attachment: {
650
- type: "object",
651
- required: true,
652
- additionalProperties: false,
653
- properties: {
654
- attachmentId: {
655
- type: "string",
656
- required: true
657
- },
658
- mediaType: {
659
- type: "string",
660
- required: true
661
- },
662
- bytes: {
663
- type: "integer",
664
- required: true
665
- },
666
- width: {
667
- type: "integer",
668
- required: true
669
- },
670
- height: {
671
- type: "integer",
672
- required: true
673
- },
674
- name: { type: "string" }
675
- }
676
- },
677
- provider: {
678
- type: "string",
679
- required: true
680
- },
681
- model: {
682
- type: "string",
683
- required: true
684
- },
685
- output: {
686
- type: "string",
687
- required: true
688
- },
689
- savedTo: { type: "string" },
690
- saveError: { type: "string" }
691
- }
692
- },
693
- render: (_args, value) => {
694
- const saved = typeof value.savedTo === "string" ? ` It was also saved to the workspace as ${value.savedTo}.` : typeof value.saveError === "string" ? ` Saving it to the workspace failed: ${value.saveError}.` : " It has no local file path.";
695
- return [{
696
- type: "text",
697
- text: `Generated one image with ${value.provider}/${value.model} (${value.output}). It is already attached to the conversation.${saved} Respond to the user without reading or searching for the image.`
698
- }];
699
- },
700
- presentationMeta: (args, value) => ({
701
- kind: "dsh-image-gen",
702
- attachment: value.attachment,
703
- provider: value.provider,
704
- model: value.model,
705
- output: value.output,
706
- ...typeof value.savedTo === "string" ? { savedTo: value.savedTo } : {},
707
- prompt: args.prompt
708
- })
709
- },
961
+ output: imageOutput("Generated"),
710
962
  async execute(args, exec) {
711
963
  const active = resolveProvider(current());
712
964
  const credential = await ctx.credentials.resolve(credentialRef(active.apiKeyEnv));
@@ -751,13 +1003,214 @@ function apply(ctx, config = {}) {
751
1003
  },
752
1004
  presentResult: (_args, result) => imagePresentation(result)
753
1005
  }));
1006
+ ctx.tools.register(defineTool({
1007
+ name: "edit_image",
1008
+ description: "Edit or restyle an existing image with the configured provider. For a named workspace file, pass its exact path as source_path. For a specific image still attached to the current conversation, pass source_attachment_id. Never provide both. Omit both only when the user clearly means the newest conversation image. If the user identifies an older image but its exact path or attachment id is unknown, do not silently edit the newest image: first use an image-reading tool to put the intended file into the conversation, then call edit_image without a source.",
1009
+ parameters: {
1010
+ prompt: {
1011
+ type: "string",
1012
+ required: true,
1013
+ description: "Describe the changes to make while preserving everything else that should remain."
1014
+ },
1015
+ source_attachment_id: {
1016
+ type: "string",
1017
+ description: "Optional attachment id of a specific image already present in the current conversation."
1018
+ },
1019
+ source_path: {
1020
+ type: "string",
1021
+ description: "Optional absolute or workspace-relative path of a specific image file inside the active session workspace. Prefer this when the user names a saved file."
1022
+ },
1023
+ aspect_ratio: {
1024
+ type: "string",
1025
+ enum: [
1026
+ "1:1",
1027
+ "3:2",
1028
+ "2:3",
1029
+ "4:3",
1030
+ "3:4",
1031
+ "16:9",
1032
+ "9:16"
1033
+ ],
1034
+ description: "Optional output aspect ratio for Google Gemini."
1035
+ },
1036
+ image_size: {
1037
+ type: "string",
1038
+ enum: [
1039
+ "1K",
1040
+ "2K",
1041
+ "4K"
1042
+ ],
1043
+ description: "Optional output resolution for Google Gemini."
1044
+ },
1045
+ size: {
1046
+ type: "string",
1047
+ description: "Optional output size for OpenAI, Seedream, or DashScope."
1048
+ }
1049
+ },
1050
+ output: imageOutput("Edited"),
1051
+ async execute(args, exec) {
1052
+ const active = resolveProvider(current());
1053
+ const credential = await ctx.credentials.resolve(credentialRef(active.apiKeyEnv));
1054
+ if (credential === void 0 || credential.value.length === 0) throw new Error(`edit_image requires the ${active.apiKeyEnv} credential; configure it in Settings > Plugins > Image generation.`);
1055
+ const sourceImage = await resolveReferenceImage({
1056
+ ...exec.agent === void 0 ? {} : { agent: exec.agent },
1057
+ attachments: ctx.attachments,
1058
+ ...typeof args.source_attachment_id === "string" ? { sourceAttachmentId: args.source_attachment_id } : {},
1059
+ ...typeof args.source_path === "string" ? { sourcePath: args.source_path } : {},
1060
+ maxBytes: ctx.attachments.imageLimits.maxImageBytes,
1061
+ signal: exec.signal
1062
+ });
1063
+ if (active.provider === "google") {
1064
+ const aspectRatio = args.aspect_ratio ?? active.aspectRatio;
1065
+ const imageSize = args.image_size ?? active.imageSize;
1066
+ return saveGenerated(ctx, await editGoogleImage({
1067
+ apiKey: credential.value,
1068
+ endpoint: active.endpoint,
1069
+ model: active.model,
1070
+ prompt: args.prompt,
1071
+ sourceImage,
1072
+ aspectRatio,
1073
+ imageSize,
1074
+ maxBytes: ctx.attachments.imageLimits.maxImageBytes,
1075
+ signal: exec.signal
1076
+ }), active.provider, active.model, `${aspectRatio}, ${imageSize}`, current(), exec);
1077
+ }
1078
+ const size = args.size ?? active.imageSize;
1079
+ if (active.provider === "openai") return saveGenerated(ctx, await editOpenAICompatibleImage({
1080
+ apiKey: credential.value,
1081
+ baseURL: active.baseURL,
1082
+ model: active.model,
1083
+ prompt: args.prompt,
1084
+ sourceImage,
1085
+ size,
1086
+ maxBytes: ctx.attachments.imageLimits.maxImageBytes,
1087
+ signal: exec.signal
1088
+ }), active.provider, active.model, size, current(), exec);
1089
+ if (active.provider === "seedream") return saveGenerated(ctx, await editSeedreamImage({
1090
+ apiKey: credential.value,
1091
+ baseURL: active.baseURL,
1092
+ model: active.model,
1093
+ prompt: args.prompt,
1094
+ sourceImage,
1095
+ size,
1096
+ maxBytes: ctx.attachments.imageLimits.maxImageBytes,
1097
+ signal: exec.signal
1098
+ }), active.provider, active.model, size, current(), exec);
1099
+ return saveGenerated(ctx, await editDashScopeImage({
1100
+ apiKey: credential.value,
1101
+ endpoint: active.endpoint,
1102
+ model: active.model,
1103
+ prompt: args.prompt,
1104
+ sourceImage,
1105
+ size,
1106
+ maxBytes: ctx.attachments.imageLimits.maxImageBytes,
1107
+ signal: exec.signal
1108
+ }), active.provider, active.model, size, current(), exec);
1109
+ },
1110
+ presentResult: (_args, result) => imagePresentation(result)
1111
+ }));
1112
+ }
1113
+ function imageOutput(verb) {
1114
+ return {
1115
+ schema: {
1116
+ type: "object",
1117
+ additionalProperties: false,
1118
+ properties: {
1119
+ attachment: {
1120
+ type: "object",
1121
+ required: true,
1122
+ additionalProperties: false,
1123
+ properties: {
1124
+ attachmentId: {
1125
+ type: "string",
1126
+ required: true
1127
+ },
1128
+ mediaType: {
1129
+ type: "string",
1130
+ required: true
1131
+ },
1132
+ bytes: {
1133
+ type: "integer",
1134
+ required: true
1135
+ },
1136
+ width: {
1137
+ type: "integer",
1138
+ required: true
1139
+ },
1140
+ height: {
1141
+ type: "integer",
1142
+ required: true
1143
+ },
1144
+ name: { type: "string" },
1145
+ originalDimensions: {
1146
+ type: "object",
1147
+ additionalProperties: false,
1148
+ properties: {
1149
+ width: {
1150
+ type: "integer",
1151
+ required: true
1152
+ },
1153
+ height: {
1154
+ type: "integer",
1155
+ required: true
1156
+ }
1157
+ }
1158
+ }
1159
+ }
1160
+ },
1161
+ provider: {
1162
+ type: "string",
1163
+ required: true
1164
+ },
1165
+ model: {
1166
+ type: "string",
1167
+ required: true
1168
+ },
1169
+ output: {
1170
+ type: "string",
1171
+ required: true
1172
+ },
1173
+ savedTo: { type: "string" },
1174
+ saveError: { type: "string" }
1175
+ }
1176
+ },
1177
+ render: (_args, value) => {
1178
+ const saved = typeof value.savedTo === "string" ? ` It was also saved to the workspace as ${value.savedTo}.` : typeof value.saveError === "string" ? ` Saving it to the workspace failed: ${value.saveError}.` : " It has no local file path.";
1179
+ const action = verb === "Generated" ? "It is already attached to the conversation." : "The edited image is attached to the conversation.";
1180
+ return [{
1181
+ type: "text",
1182
+ text: `${verb} one image with ${value.provider}/${value.model} (${value.output}). Attachment ID: ${String(value.attachment.attachmentId)}. ${action}${saved} Respond to the user without reading or searching for the image.`
1183
+ }, {
1184
+ type: "image",
1185
+ attachment: value.attachment
1186
+ }];
1187
+ },
1188
+ presentationMeta: (args, value) => ({
1189
+ kind: "dsh-image-gen",
1190
+ attachment: attachmentMeta(value.attachment),
1191
+ provider: value.provider,
1192
+ model: value.model,
1193
+ output: value.output,
1194
+ ...verb === "Edited" ? { operation: "edit" } : {},
1195
+ ...typeof value.savedTo === "string" ? { savedTo: value.savedTo } : {},
1196
+ prompt: args.prompt
1197
+ })
1198
+ };
1199
+ }
1200
+ function attachmentMeta(ref) {
1201
+ return {
1202
+ attachmentId: String(ref.attachmentId),
1203
+ mediaType: ref.mediaType,
1204
+ bytes: ref.bytes,
1205
+ width: ref.width,
1206
+ height: ref.height,
1207
+ ...ref.name === void 0 ? {} : { name: ref.name },
1208
+ ...ref.originalDimensions === void 0 ? {} : { originalDimensions: {
1209
+ width: ref.originalDimensions.width,
1210
+ height: ref.originalDimensions.height
1211
+ } }
1212
+ };
754
1213
  }
755
- /**
756
- * Persist the generated image as a durable attachment, then — when workspace
757
- * saving is enabled — also write it as a file under the calling agent's
758
- * session workspace. A workspace write failure never discards the generated
759
- * attachment: it is reported through `saveError` instead.
760
- */
761
1214
  async function saveGenerated(ctx, generated, provider, model, output, config, exec) {
762
1215
  if (!ctx.attachments.imageLimits.mediaTypes.includes(generated.mediaType)) throw new Error(`This DSH deployment does not accept ${generated.mediaType} generated images`);
763
1216
  const attachment = await ctx.attachments.saveImage({