testdriverai 7.11.283-test → 7.11.285-test
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/docs/_data/examples-manifest.json +44 -44
- package/docs/examples/add-to-cart.mdx +1 -1
- package/docs/examples/assert.mdx +1 -1
- package/docs/examples/click-button.mdx +1 -1
- package/docs/examples/click-icon.mdx +1 -1
- package/docs/examples/element-not-found.mdx +1 -1
- package/docs/examples/findall-coffee-icons.mdx +1 -1
- package/docs/examples/installer.mdx +1 -1
- package/docs/examples/launch-vscode-linux.mdx +1 -1
- package/docs/examples/navigate-and-scroll.mdx +1 -1
- package/docs/examples/parse.mdx +1 -1
- package/docs/examples/press-keys.mdx +1 -1
- package/docs/examples/scroll.mdx +1 -1
- package/docs/examples/type.mdx +1 -1
- package/mcp-server/dist/mcp-app.html +8 -8
- package/mcp-server/dist/server.mjs +59 -22
- package/mcp-server/src/mcp-app.ts +45 -25
- package/mcp-server/src/server.ts +62 -18
- package/package.json +1 -1
|
@@ -255,6 +255,51 @@ function storeImage(data, type) {
|
|
|
255
255
|
function getStoredImage(id) {
|
|
256
256
|
return imageStoreState().images.get(id);
|
|
257
257
|
}
|
|
258
|
+
/**
|
|
259
|
+
* `_meta` key carrying the image inline in a tool result, for the MCP App.
|
|
260
|
+
*
|
|
261
|
+
* The app used to get every image with a follow-up `resources/read` against the
|
|
262
|
+
* per-connection store above. That second round trip is fragile on hosted
|
|
263
|
+
* clients: when it fails the app has nothing to show and the preview collapses.
|
|
264
|
+
* Sending the bytes with the result removes the round trip. `_meta` is passed
|
|
265
|
+
* through to the app but, unlike `content`/`structuredContent`, is not shown to
|
|
266
|
+
* the model, so the base64 stays out of the agent's context. The resource URI
|
|
267
|
+
* is still set as a fallback (and for `check`'s `referenceImageUri`).
|
|
268
|
+
*/
|
|
269
|
+
const INLINE_IMAGE_META_KEY = "testdriver/image";
|
|
270
|
+
/** Above this (base64 chars) the image is left to the resource fallback, so one
|
|
271
|
+
* huge frame can't push a tool result past a host's size limit. */
|
|
272
|
+
const MAX_INLINE_IMAGE_BASE64 = 2_000_000;
|
|
273
|
+
function inlineImageMeta(base64) {
|
|
274
|
+
if (!base64)
|
|
275
|
+
return undefined;
|
|
276
|
+
if (base64.length > MAX_INLINE_IMAGE_BASE64) {
|
|
277
|
+
logger.debug("inlineImageMeta: Image too large to inline, relying on resource URI", { length: base64.length });
|
|
278
|
+
return undefined;
|
|
279
|
+
}
|
|
280
|
+
return { [INLINE_IMAGE_META_KEY]: { mimeType: "image/png", data: base64 } };
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Store a result's images and record their resource URIs on `data`. Returns the
|
|
284
|
+
* base64 of the image the app displays (full screenshot wins over the crop,
|
|
285
|
+
* matching the app's own preference) so the caller can inline it.
|
|
286
|
+
*/
|
|
287
|
+
function attachImages(data, images) {
|
|
288
|
+
let screenshot;
|
|
289
|
+
let cropped;
|
|
290
|
+
for (const img of images ?? []) {
|
|
291
|
+
const uri = storeImage(img.base64, img.kind);
|
|
292
|
+
if (img.kind === "cropped") {
|
|
293
|
+
data.croppedImageResourceUri = uri;
|
|
294
|
+
cropped = img.base64;
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
data.screenshotResourceUri = uri;
|
|
298
|
+
screenshot = img.base64;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return screenshot ?? cropped;
|
|
302
|
+
}
|
|
258
303
|
/** Diagnostics for the active connection's image store (used in debug logs). */
|
|
259
304
|
function imageStoreDiagnostics() {
|
|
260
305
|
const images = imageStoreState().images;
|
|
@@ -297,14 +342,8 @@ function noSessionResult(err) {
|
|
|
297
342
|
*/
|
|
298
343
|
function resultToMcp(r) {
|
|
299
344
|
const data = { ...r.data };
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
if (img.kind === "cropped")
|
|
303
|
-
data.croppedImageResourceUri = uri;
|
|
304
|
-
else
|
|
305
|
-
data.screenshotResourceUri = uri;
|
|
306
|
-
}
|
|
307
|
-
return createToolResult(r.ok, r.text, data, r.code);
|
|
345
|
+
const image = attachImages(data, r.images);
|
|
346
|
+
return createToolResult(r.ok, r.text, data, r.code, image);
|
|
308
347
|
}
|
|
309
348
|
const DEFAULT_HEARTBEAT_MS = 3000;
|
|
310
349
|
function makeProgressReporter(extra) {
|
|
@@ -410,7 +449,7 @@ function cancelledResultOrNull(error, tool) {
|
|
|
410
449
|
* If generatedCode is provided, it's appended to the text response with instructions
|
|
411
450
|
* for the agent to write it to the test file.
|
|
412
451
|
*/
|
|
413
|
-
function createToolResult(success, textContent, structuredData, generatedCode) {
|
|
452
|
+
function createToolResult(success, textContent, structuredData, generatedCode, inlineImageBase64) {
|
|
414
453
|
// Build text content - append generated code if provided with directive instructions
|
|
415
454
|
let fullText = textContent;
|
|
416
455
|
if (generatedCode && success) {
|
|
@@ -444,6 +483,7 @@ function createToolResult(success, textContent, structuredData, generatedCode) {
|
|
|
444
483
|
generatedCode: generatedCode && success ? generatedCode : undefined,
|
|
445
484
|
testFile: session?.testFile || undefined,
|
|
446
485
|
},
|
|
486
|
+
_meta: inlineImageMeta(inlineImageBase64),
|
|
447
487
|
};
|
|
448
488
|
}
|
|
449
489
|
/**
|
|
@@ -631,13 +671,7 @@ Debug mode (connect to existing sandbox):
|
|
|
631
671
|
// Reproduce the server's exact success output (text + data) which differs
|
|
632
672
|
// from the core's neutral text. Images are mapped to resource URIs.
|
|
633
673
|
const data = { ...result.data, duration };
|
|
634
|
-
|
|
635
|
-
const uri = storeImage(img.base64, img.kind);
|
|
636
|
-
if (img.kind === "cropped")
|
|
637
|
-
data.croppedImageResourceUri = uri;
|
|
638
|
-
else
|
|
639
|
-
data.screenshotResourceUri = uri;
|
|
640
|
-
}
|
|
674
|
+
const image = attachImages(data, result.images);
|
|
641
675
|
if (result.data.debugMode) {
|
|
642
676
|
// Debug (existing-sandbox) success text — preserve original wording.
|
|
643
677
|
const text = `Connected to existing sandbox (debug mode)
|
|
@@ -646,7 +680,7 @@ Sandbox: ${result.data.sandboxId}
|
|
|
646
680
|
Expires in: ${Math.round(params.keepAlive / 1000)}s
|
|
647
681
|
|
|
648
682
|
You are now connected to the sandbox in its current state. Use find, click, type, etc. to interact.`;
|
|
649
|
-
return createToolResult(true, text, data, result.code);
|
|
683
|
+
return createToolResult(true, text, data, result.code, image);
|
|
650
684
|
}
|
|
651
685
|
// Normal provisioning success — append the EXACT dependency guidance block.
|
|
652
686
|
const text = `${result.text}
|
|
@@ -662,7 +696,7 @@ IMPORTANT - If creating a new test project, use these EXACT dependencies in pack
|
|
|
662
696
|
"test": "vitest"
|
|
663
697
|
}
|
|
664
698
|
}`;
|
|
665
|
-
return createToolResult(true, text, data, result.code);
|
|
699
|
+
return createToolResult(true, text, data, result.code, image);
|
|
666
700
|
}
|
|
667
701
|
catch (error) {
|
|
668
702
|
// On client cancellation, tear down the half-provisioned session so we
|
|
@@ -1502,6 +1536,7 @@ Useful for:
|
|
|
1502
1536
|
screenshotResourceUri,
|
|
1503
1537
|
duration
|
|
1504
1538
|
},
|
|
1539
|
+
_meta: inlineImageMeta(imageBase64),
|
|
1505
1540
|
};
|
|
1506
1541
|
}
|
|
1507
1542
|
catch (error) {
|
|
@@ -1533,17 +1568,19 @@ Only use 'screenshot' when you explicitly want to show something to the human us
|
|
|
1533
1568
|
logger.info("screenshot: Starting");
|
|
1534
1569
|
try {
|
|
1535
1570
|
const result = await core.screenshot();
|
|
1536
|
-
//
|
|
1537
|
-
//
|
|
1538
|
-
// text rather than the core's neutral text.
|
|
1571
|
+
// The image goes to the MCP app only (inline in `_meta`, with a resource
|
|
1572
|
+
// URI as fallback) and stays OUT of AI context. Preserve the original
|
|
1573
|
+
// user-facing text rather than the core's neutral text.
|
|
1539
1574
|
const data = { action: "screenshot" };
|
|
1575
|
+
let image;
|
|
1540
1576
|
for (const img of result.images ?? []) {
|
|
1541
1577
|
data.screenshotResourceUri = storeImage(img.base64, "screenshot");
|
|
1578
|
+
image = img.base64;
|
|
1542
1579
|
}
|
|
1543
1580
|
const duration = Date.now() - startTime;
|
|
1544
1581
|
data.duration = duration;
|
|
1545
1582
|
logger.info("screenshot: Completed", { duration, hasImage: (result.images?.length ?? 0) > 0 });
|
|
1546
|
-
return createToolResult(true, "Screenshot captured and displayed to user", data);
|
|
1583
|
+
return createToolResult(true, "Screenshot captured and displayed to user", data, undefined, image);
|
|
1547
1584
|
}
|
|
1548
1585
|
catch (error) {
|
|
1549
1586
|
if (error instanceof NoActiveSessionError) {
|
|
@@ -66,6 +66,20 @@ interface ToolResultData {
|
|
|
66
66
|
debuggerUrl?: string;
|
|
67
67
|
sessionId?: string;
|
|
68
68
|
duration?: number;
|
|
69
|
+
imageError?: string; // Why the image could not be shown (set client-side)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// `_meta` key the server uses to send the image inline with a tool result.
|
|
73
|
+
const INLINE_IMAGE_META_KEY = "testdriver/image";
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Read the image the server inlined in the tool result's `_meta`, as a data URL.
|
|
77
|
+
* Null when the result carries none (older server, or an image too large to inline).
|
|
78
|
+
*/
|
|
79
|
+
function inlineImageUrl(result: CallToolResult): string | null {
|
|
80
|
+
const image = result._meta?.[INLINE_IMAGE_META_KEY] as { mimeType?: string; data?: string } | undefined;
|
|
81
|
+
if (!image || typeof image.data !== "string" || !image.data) return null;
|
|
82
|
+
return `data:${image.mimeType || "image/png"};base64,${image.data}`;
|
|
69
83
|
}
|
|
70
84
|
|
|
71
85
|
// Store session info globally for display
|
|
@@ -345,6 +359,9 @@ function renderResult(data: ToolResultData) {
|
|
|
345
359
|
if (data.error) {
|
|
346
360
|
statusText += ` - ${data.error}`;
|
|
347
361
|
}
|
|
362
|
+
if (data.imageError) {
|
|
363
|
+
statusText += ` - ${data.imageError}`;
|
|
364
|
+
}
|
|
348
365
|
|
|
349
366
|
actionStatusEl.textContent = statusText;
|
|
350
367
|
actionStatusEl.className = statusClass;
|
|
@@ -450,28 +467,22 @@ const app = new App({ name: "TestDriver Screenshot", version: "1.0.0" });
|
|
|
450
467
|
/**
|
|
451
468
|
* Fetch screenshot blob from resource URI and convert to data URL
|
|
452
469
|
*/
|
|
453
|
-
async function fetchScreenshotFromResource(resourceUri: string): Promise<string
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
console.error("Resource did not contain blob data");
|
|
464
|
-
return null;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
// Convert base64 blob to data URL
|
|
468
|
-
const dataUrl = `data:${content.mimeType || "image/png"};base64,${content.blob}`;
|
|
469
|
-
console.info("Screenshot fetched successfully, blob length:", content.blob.length);
|
|
470
|
-
return dataUrl;
|
|
471
|
-
} catch (error) {
|
|
472
|
-
console.error("Failed to fetch screenshot resource:", error);
|
|
473
|
-
return null;
|
|
470
|
+
async function fetchScreenshotFromResource(resourceUri: string): Promise<string> {
|
|
471
|
+
console.info("Fetching screenshot from resource:", resourceUri);
|
|
472
|
+
const result = await app.request(
|
|
473
|
+
{ method: "resources/read", params: { uri: resourceUri } },
|
|
474
|
+
ReadResourceResultSchema,
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
const content = result.contents[0];
|
|
478
|
+
if (!content || !("blob" in content)) {
|
|
479
|
+
throw new Error("Resource did not contain image data");
|
|
474
480
|
}
|
|
481
|
+
|
|
482
|
+
// Convert base64 blob to data URL
|
|
483
|
+
const dataUrl = `data:${content.mimeType || "image/png"};base64,${content.blob}`;
|
|
484
|
+
console.info("Screenshot fetched successfully, blob length:", content.blob.length);
|
|
485
|
+
return dataUrl;
|
|
475
486
|
}
|
|
476
487
|
|
|
477
488
|
// 2. Register handlers BEFORE connecting
|
|
@@ -517,13 +528,22 @@ app.ontoolresult = async (result) => {
|
|
|
517
528
|
console.info("Has screenshotResourceUri:", !!data.screenshotResourceUri);
|
|
518
529
|
console.info("Has croppedImageResourceUri:", !!data.croppedImageResourceUri);
|
|
519
530
|
|
|
520
|
-
//
|
|
531
|
+
// Prefer the image the server sent inline with the result: no extra request.
|
|
532
|
+
if (!data.imageUrl) {
|
|
533
|
+
data.imageUrl = inlineImageUrl(result) ?? undefined;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Otherwise fall back to fetching it by resource URI. If that fails, say so in
|
|
537
|
+
// the status bar instead of silently rendering an empty frame.
|
|
521
538
|
const resourceUri = data.screenshotResourceUri || data.croppedImageResourceUri;
|
|
522
539
|
if (resourceUri && !data.imageUrl) {
|
|
523
540
|
showLoading("Fetching image...");
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
541
|
+
try {
|
|
542
|
+
data.imageUrl = await fetchScreenshotFromResource(resourceUri);
|
|
543
|
+
} catch (error) {
|
|
544
|
+
console.error("Failed to fetch screenshot resource:", error);
|
|
545
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
546
|
+
data.imageError = `Screenshot unavailable: ${reason}`;
|
|
527
547
|
}
|
|
528
548
|
}
|
|
529
549
|
|
package/mcp-server/src/server.ts
CHANGED
|
@@ -326,6 +326,53 @@ function getStoredImage(id: string): StoredImage | undefined {
|
|
|
326
326
|
return imageStoreState().images.get(id);
|
|
327
327
|
}
|
|
328
328
|
|
|
329
|
+
/**
|
|
330
|
+
* `_meta` key carrying the image inline in a tool result, for the MCP App.
|
|
331
|
+
*
|
|
332
|
+
* The app used to get every image with a follow-up `resources/read` against the
|
|
333
|
+
* per-connection store above. That second round trip is fragile on hosted
|
|
334
|
+
* clients: when it fails the app has nothing to show and the preview collapses.
|
|
335
|
+
* Sending the bytes with the result removes the round trip. `_meta` is passed
|
|
336
|
+
* through to the app but, unlike `content`/`structuredContent`, is not shown to
|
|
337
|
+
* the model, so the base64 stays out of the agent's context. The resource URI
|
|
338
|
+
* is still set as a fallback (and for `check`'s `referenceImageUri`).
|
|
339
|
+
*/
|
|
340
|
+
const INLINE_IMAGE_META_KEY = "testdriver/image";
|
|
341
|
+
|
|
342
|
+
/** Above this (base64 chars) the image is left to the resource fallback, so one
|
|
343
|
+
* huge frame can't push a tool result past a host's size limit. */
|
|
344
|
+
const MAX_INLINE_IMAGE_BASE64 = 2_000_000;
|
|
345
|
+
|
|
346
|
+
function inlineImageMeta(base64: string | undefined): CallToolResult["_meta"] | undefined {
|
|
347
|
+
if (!base64) return undefined;
|
|
348
|
+
if (base64.length > MAX_INLINE_IMAGE_BASE64) {
|
|
349
|
+
logger.debug("inlineImageMeta: Image too large to inline, relying on resource URI", { length: base64.length });
|
|
350
|
+
return undefined;
|
|
351
|
+
}
|
|
352
|
+
return { [INLINE_IMAGE_META_KEY]: { mimeType: "image/png", data: base64 } };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Store a result's images and record their resource URIs on `data`. Returns the
|
|
357
|
+
* base64 of the image the app displays (full screenshot wins over the crop,
|
|
358
|
+
* matching the app's own preference) so the caller can inline it.
|
|
359
|
+
*/
|
|
360
|
+
function attachImages(data: Record<string, unknown>, images: ActionResult["images"]): string | undefined {
|
|
361
|
+
let screenshot: string | undefined;
|
|
362
|
+
let cropped: string | undefined;
|
|
363
|
+
for (const img of images ?? []) {
|
|
364
|
+
const uri = storeImage(img.base64, img.kind);
|
|
365
|
+
if (img.kind === "cropped") {
|
|
366
|
+
data.croppedImageResourceUri = uri;
|
|
367
|
+
cropped = img.base64;
|
|
368
|
+
} else {
|
|
369
|
+
data.screenshotResourceUri = uri;
|
|
370
|
+
screenshot = img.base64;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return screenshot ?? cropped;
|
|
374
|
+
}
|
|
375
|
+
|
|
329
376
|
/** Diagnostics for the active connection's image store (used in debug logs). */
|
|
330
377
|
function imageStoreDiagnostics(): { imageStoreSize: number; availableKeys: string[] } {
|
|
331
378
|
const images = imageStoreState().images;
|
|
@@ -378,12 +425,8 @@ function noSessionResult(err: NoActiveSessionError): CallToolResult {
|
|
|
378
425
|
*/
|
|
379
426
|
function resultToMcp(r: ActionResult): CallToolResult {
|
|
380
427
|
const data: Record<string, unknown> = { ...r.data };
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
if (img.kind === "cropped") data.croppedImageResourceUri = uri;
|
|
384
|
-
else data.screenshotResourceUri = uri;
|
|
385
|
-
}
|
|
386
|
-
return createToolResult(r.ok, r.text, data, r.code);
|
|
428
|
+
const image = attachImages(data, r.images);
|
|
429
|
+
return createToolResult(r.ok, r.text, data, r.code, image);
|
|
387
430
|
}
|
|
388
431
|
|
|
389
432
|
// =============================================================================
|
|
@@ -534,7 +577,8 @@ function createToolResult(
|
|
|
534
577
|
success: boolean,
|
|
535
578
|
textContent: string,
|
|
536
579
|
structuredData: Record<string, unknown>,
|
|
537
|
-
generatedCode?: string
|
|
580
|
+
generatedCode?: string,
|
|
581
|
+
inlineImageBase64?: string
|
|
538
582
|
): CallToolResult {
|
|
539
583
|
// Build text content - append generated code if provided with directive instructions
|
|
540
584
|
let fullText = textContent;
|
|
@@ -572,6 +616,7 @@ function createToolResult(
|
|
|
572
616
|
generatedCode: generatedCode && success ? generatedCode : undefined,
|
|
573
617
|
testFile: session?.testFile || undefined,
|
|
574
618
|
},
|
|
619
|
+
_meta: inlineImageMeta(inlineImageBase64),
|
|
575
620
|
};
|
|
576
621
|
}
|
|
577
622
|
|
|
@@ -814,11 +859,7 @@ Debug mode (connect to existing sandbox):
|
|
|
814
859
|
// Reproduce the server's exact success output (text + data) which differs
|
|
815
860
|
// from the core's neutral text. Images are mapped to resource URIs.
|
|
816
861
|
const data: Record<string, unknown> = { ...result.data, duration };
|
|
817
|
-
|
|
818
|
-
const uri = storeImage(img.base64, img.kind);
|
|
819
|
-
if (img.kind === "cropped") data.croppedImageResourceUri = uri;
|
|
820
|
-
else data.screenshotResourceUri = uri;
|
|
821
|
-
}
|
|
862
|
+
const image = attachImages(data, result.images);
|
|
822
863
|
|
|
823
864
|
if (result.data.debugMode) {
|
|
824
865
|
// Debug (existing-sandbox) success text — preserve original wording.
|
|
@@ -828,7 +869,7 @@ Sandbox: ${result.data.sandboxId}
|
|
|
828
869
|
Expires in: ${Math.round(params.keepAlive / 1000)}s
|
|
829
870
|
|
|
830
871
|
You are now connected to the sandbox in its current state. Use find, click, type, etc. to interact.`;
|
|
831
|
-
return createToolResult(true, text, data, result.code);
|
|
872
|
+
return createToolResult(true, text, data, result.code, image);
|
|
832
873
|
}
|
|
833
874
|
|
|
834
875
|
// Normal provisioning success — append the EXACT dependency guidance block.
|
|
@@ -845,7 +886,7 @@ IMPORTANT - If creating a new test project, use these EXACT dependencies in pack
|
|
|
845
886
|
"test": "vitest"
|
|
846
887
|
}
|
|
847
888
|
}`;
|
|
848
|
-
return createToolResult(true, text, data, result.code);
|
|
889
|
+
return createToolResult(true, text, data, result.code, image);
|
|
849
890
|
} catch (error) {
|
|
850
891
|
// On client cancellation, tear down the half-provisioned session so we
|
|
851
892
|
// don't leak a connected sandbox. The underlying SDK call may still be
|
|
@@ -1829,6 +1870,7 @@ Useful for:
|
|
|
1829
1870
|
screenshotResourceUri,
|
|
1830
1871
|
duration
|
|
1831
1872
|
},
|
|
1873
|
+
_meta: inlineImageMeta(imageBase64),
|
|
1832
1874
|
};
|
|
1833
1875
|
} catch (error) {
|
|
1834
1876
|
logger.error("view_local_screenshot: Failed", { error: String(error), path: params.path });
|
|
@@ -1867,19 +1909,21 @@ Only use 'screenshot' when you explicitly want to show something to the human us
|
|
|
1867
1909
|
try {
|
|
1868
1910
|
const result = await core.screenshot();
|
|
1869
1911
|
|
|
1870
|
-
//
|
|
1871
|
-
//
|
|
1872
|
-
// text rather than the core's neutral text.
|
|
1912
|
+
// The image goes to the MCP app only (inline in `_meta`, with a resource
|
|
1913
|
+
// URI as fallback) and stays OUT of AI context. Preserve the original
|
|
1914
|
+
// user-facing text rather than the core's neutral text.
|
|
1873
1915
|
const data: Record<string, unknown> = { action: "screenshot" };
|
|
1916
|
+
let image: string | undefined;
|
|
1874
1917
|
for (const img of result.images ?? []) {
|
|
1875
1918
|
data.screenshotResourceUri = storeImage(img.base64, "screenshot");
|
|
1919
|
+
image = img.base64;
|
|
1876
1920
|
}
|
|
1877
1921
|
|
|
1878
1922
|
const duration = Date.now() - startTime;
|
|
1879
1923
|
data.duration = duration;
|
|
1880
1924
|
logger.info("screenshot: Completed", { duration, hasImage: (result.images?.length ?? 0) > 0 });
|
|
1881
1925
|
|
|
1882
|
-
return createToolResult(true, "Screenshot captured and displayed to user", data);
|
|
1926
|
+
return createToolResult(true, "Screenshot captured and displayed to user", data, undefined, image);
|
|
1883
1927
|
} catch (error) {
|
|
1884
1928
|
if (error instanceof NoActiveSessionError) {
|
|
1885
1929
|
logger.warn("screenshot: No active session");
|