@testdriverai/agent 7.9.103-canary → 7.9.104-canary
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/agent/interface.js +7 -1
- package/agent/lib/commands.js +8 -6
- package/agent/lib/system.js +60 -6
- package/docs/docs.json +16 -1
- package/docs/v7/ai/agent.mdx +72 -0
- package/docs/v7/ai/mcp.mdx +228 -0
- package/docs/v7/ai/skills.mdx +73 -0
- package/docs/v7/find.mdx +2 -0
- package/interfaces/cli/commands/init.js +81 -2
- package/lib/init-project.js +57 -28
- package/lib/install-clients.js +470 -0
- package/mcp-server/dist/server.mjs +245 -66
- package/mcp-server/src/server.ts +250 -32
- package/package.json +1 -1
- package/sdk.js +14 -12
|
@@ -266,6 +266,102 @@ function requireActiveSession() {
|
|
|
266
266
|
sessionManager.refreshSession(session.sessionId);
|
|
267
267
|
return { valid: true };
|
|
268
268
|
}
|
|
269
|
+
const DEFAULT_HEARTBEAT_MS = 3000;
|
|
270
|
+
function makeProgressReporter(extra) {
|
|
271
|
+
const progressToken = extra?._meta?.progressToken;
|
|
272
|
+
// No token → client did not opt into progress. Return a no-op reporter.
|
|
273
|
+
if (progressToken === undefined || progressToken === null) {
|
|
274
|
+
return {
|
|
275
|
+
report: () => { },
|
|
276
|
+
heartbeat: () => () => { },
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
let progress = 0;
|
|
280
|
+
const send = (message) => {
|
|
281
|
+
progress += 1;
|
|
282
|
+
// Fire-and-forget: a failed notification must never break the tool call.
|
|
283
|
+
void extra
|
|
284
|
+
.sendNotification({
|
|
285
|
+
method: "notifications/progress",
|
|
286
|
+
params: { progressToken, progress, message },
|
|
287
|
+
})
|
|
288
|
+
.catch((err) => {
|
|
289
|
+
logger.debug("progress: sendNotification failed", { error: String(err) });
|
|
290
|
+
});
|
|
291
|
+
};
|
|
292
|
+
return {
|
|
293
|
+
report: (message) => send(message),
|
|
294
|
+
heartbeat: (message, intervalMs = DEFAULT_HEARTBEAT_MS) => {
|
|
295
|
+
send(message);
|
|
296
|
+
const timer = setInterval(() => send(message), intervalMs);
|
|
297
|
+
// Don't let the heartbeat keep the event loop alive on its own.
|
|
298
|
+
timer.unref?.();
|
|
299
|
+
return () => clearInterval(timer);
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
// =============================================================================
|
|
304
|
+
// Cancellation (MCP `notifications/cancelled` → `extra.signal`)
|
|
305
|
+
// =============================================================================
|
|
306
|
+
/** Thrown when a tool call is aborted by the client. */
|
|
307
|
+
class ToolAbortError extends Error {
|
|
308
|
+
constructor(tool) {
|
|
309
|
+
super(`${tool} was cancelled by the client`);
|
|
310
|
+
this.name = "ToolAbortError";
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
/** Reject as soon as `signal` aborts. Used to race against long SDK calls. */
|
|
314
|
+
function rejectOnAbort(signal, tool) {
|
|
315
|
+
if (!signal) {
|
|
316
|
+
// Never-resolving promise with a no-op cleanup — Promise.race ignores it.
|
|
317
|
+
return { promise: new Promise(() => { }), cleanup: () => { } };
|
|
318
|
+
}
|
|
319
|
+
let onAbort = () => { };
|
|
320
|
+
const promise = new Promise((_, reject) => {
|
|
321
|
+
onAbort = () => reject(new ToolAbortError(tool));
|
|
322
|
+
if (signal.aborted) {
|
|
323
|
+
onAbort();
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
return { promise, cleanup: () => signal.removeEventListener("abort", onAbort) };
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Run a long-running SDK call, but settle as soon as the client aborts.
|
|
333
|
+
*
|
|
334
|
+
* The wrapped SDK methods are not themselves signal-aware, so on abort the
|
|
335
|
+
* underlying work keeps running to completion in the background — but the tool
|
|
336
|
+
* call returns promptly with a `ToolAbortError` instead of blocking the client.
|
|
337
|
+
* Callers that hold cleanable resources (e.g. `session_start`) should catch
|
|
338
|
+
* `ToolAbortError` and tear them down.
|
|
339
|
+
*/
|
|
340
|
+
async function raceAbort(signal, tool, work) {
|
|
341
|
+
if (signal?.aborted) {
|
|
342
|
+
throw new ToolAbortError(tool);
|
|
343
|
+
}
|
|
344
|
+
const { promise, cleanup } = rejectOnAbort(signal, tool);
|
|
345
|
+
try {
|
|
346
|
+
return await Promise.race([work, promise]);
|
|
347
|
+
}
|
|
348
|
+
finally {
|
|
349
|
+
cleanup();
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* If `error` is a client cancellation, return a "cancelled" tool result so the
|
|
354
|
+
* caller can `return` it; otherwise return null so normal error handling (log +
|
|
355
|
+
* Sentry + rethrow) proceeds. Keeps abort out of error reporting — a user
|
|
356
|
+
* cancelling is not a failure.
|
|
357
|
+
*/
|
|
358
|
+
function cancelledResultOrNull(error, tool) {
|
|
359
|
+
if (error instanceof ToolAbortError) {
|
|
360
|
+
logger.info(`${tool}: Cancelled by client`);
|
|
361
|
+
return createToolResult(false, `${tool} was cancelled.`, { action: tool, cancelled: true });
|
|
362
|
+
}
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
269
365
|
/**
|
|
270
366
|
* Create tool result with structured content for MCP App
|
|
271
367
|
* Images: imageUrl (data URL) goes to structuredContent for UI to display
|
|
@@ -411,8 +507,9 @@ Debug mode (connect to existing sandbox):
|
|
|
411
507
|
- Use this to interactively debug failed tests without re-running from scratch`,
|
|
412
508
|
inputSchema: SessionStartInputSchema,
|
|
413
509
|
_meta: { ui: { resourceUri: RESOURCE_URI, expanded: true } },
|
|
414
|
-
}, async (params) => {
|
|
510
|
+
}, async (params, extra) => {
|
|
415
511
|
const startTime = Date.now();
|
|
512
|
+
const progress = makeProgressReporter(extra);
|
|
416
513
|
// Resolve OS with priority: explicit param > TD_OS env var > "linux" default
|
|
417
514
|
// This mirrors the behavior of the Vitest hooks (hooks.mjs) which also reads TD_OS
|
|
418
515
|
const { os: resolvedOs, warning: osWarning } = resolveOs(params.os);
|
|
@@ -495,10 +592,16 @@ Debug mode (connect to existing sandbox):
|
|
|
495
592
|
// Handle sandboxId mode - connect to existing sandbox (debug-on-failure mode)
|
|
496
593
|
if (params.sandboxId) {
|
|
497
594
|
logger.info("session_start: Connecting to existing sandbox (debug mode)", { sandboxId: params.sandboxId });
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
595
|
+
const stopHeartbeat = progress.heartbeat(`Connecting to existing sandbox ${params.sandboxId}...`);
|
|
596
|
+
try {
|
|
597
|
+
await raceAbort(extra.signal, "session_start", sdk.connect({
|
|
598
|
+
sandboxId: params.sandboxId,
|
|
599
|
+
keepAlive: params.keepAlive,
|
|
600
|
+
}));
|
|
601
|
+
}
|
|
602
|
+
finally {
|
|
603
|
+
stopHeartbeat();
|
|
604
|
+
}
|
|
502
605
|
// Get sandbox ID
|
|
503
606
|
const instance = sdk.getInstance();
|
|
504
607
|
logger.info("session_start: Connected to existing sandbox", { instanceId: instance?.instanceId });
|
|
@@ -507,7 +610,8 @@ Debug mode (connect to existing sandbox):
|
|
|
507
610
|
setSessionContext(newSession.sessionId, instance?.instanceId);
|
|
508
611
|
// Capture screenshot of current state
|
|
509
612
|
logger.debug("session_start: Capturing screenshot of existing sandbox");
|
|
510
|
-
|
|
613
|
+
progress.report("Capturing screenshot...");
|
|
614
|
+
const screenshotBase64 = await raceAbort(extra.signal, "session_start", sdk.agent.system.captureScreenBase64(1, false, true));
|
|
511
615
|
let screenshotResourceUri;
|
|
512
616
|
if (screenshotBase64) {
|
|
513
617
|
screenshotResourceUri = storeImage(screenshotBase64, "screenshot");
|
|
@@ -536,11 +640,19 @@ You are now connected to the sandbox in its current state. Use find, click, type
|
|
|
536
640
|
else {
|
|
537
641
|
logger.info("session_start: Connecting to cloud sandbox...");
|
|
538
642
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
643
|
+
{
|
|
644
|
+
const stopHeartbeat = progress.heartbeat(instanceIp ? `Connecting to self-hosted instance ${instanceIp}...` : "Connecting to cloud sandbox...");
|
|
645
|
+
try {
|
|
646
|
+
await raceAbort(extra.signal, "session_start", sdk.connect({
|
|
647
|
+
reconnect: params.reconnect,
|
|
648
|
+
keepAlive: params.keepAlive,
|
|
649
|
+
ip: instanceIp,
|
|
650
|
+
}));
|
|
651
|
+
}
|
|
652
|
+
finally {
|
|
653
|
+
stopHeartbeat();
|
|
654
|
+
}
|
|
655
|
+
}
|
|
544
656
|
// Get sandbox ID
|
|
545
657
|
const instance = sdk.getInstance();
|
|
546
658
|
logger.info("session_start: Connected to sandbox", { instanceId: instance?.instanceId });
|
|
@@ -550,52 +662,61 @@ You are now connected to the sandbox in its current state. Use find, click, type
|
|
|
550
662
|
// Get provision-specific options
|
|
551
663
|
const provisionOptions = getProvisionOptions(params);
|
|
552
664
|
let provisionCmd = "";
|
|
553
|
-
//
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
665
|
+
// Provisioning can take tens of seconds (downloading installers, booting
|
|
666
|
+
// apps). Heartbeat so the client's idle timeout keeps resetting.
|
|
667
|
+
const stopProvisionHeartbeat = progress.heartbeat(`Provisioning ${params.type}...`);
|
|
668
|
+
try {
|
|
669
|
+
// Provision based on type
|
|
670
|
+
switch (params.type) {
|
|
671
|
+
case "chrome": {
|
|
672
|
+
const chromeOpts = provisionOptions;
|
|
673
|
+
logger.info("session_start: Provisioning Chrome", { url: chromeOpts.url });
|
|
674
|
+
await raceAbort(extra.signal, "session_start", sdk.provision.chrome(chromeOpts));
|
|
675
|
+
provisionCmd = "provision.chrome";
|
|
676
|
+
logger.debug("session_start: Chrome provisioned");
|
|
677
|
+
break;
|
|
678
|
+
}
|
|
679
|
+
case "chromeExtension": {
|
|
680
|
+
const extOpts = provisionOptions;
|
|
681
|
+
logger.info("session_start: Provisioning Chrome Extension", { extensionPath: extOpts.extensionPath, extensionId: extOpts.extensionId });
|
|
682
|
+
await raceAbort(extra.signal, "session_start", sdk.provision.chromeExtension(extOpts));
|
|
683
|
+
provisionCmd = "provision.chromeExtension";
|
|
684
|
+
logger.debug("session_start: Chrome Extension provisioned");
|
|
685
|
+
break;
|
|
686
|
+
}
|
|
687
|
+
case "vscode": {
|
|
688
|
+
const vscodeOpts = provisionOptions;
|
|
689
|
+
logger.info("session_start: Provisioning VS Code", { workspace: vscodeOpts.workspace });
|
|
690
|
+
await raceAbort(extra.signal, "session_start", sdk.provision.vscode(vscodeOpts));
|
|
691
|
+
provisionCmd = "provision.vscode";
|
|
692
|
+
logger.debug("session_start: VS Code provisioned");
|
|
693
|
+
break;
|
|
694
|
+
}
|
|
695
|
+
case "installer": {
|
|
696
|
+
const installerOpts = provisionOptions;
|
|
697
|
+
logger.info("session_start: Provisioning installer", { url: installerOpts.url });
|
|
698
|
+
await raceAbort(extra.signal, "session_start", sdk.provision.installer(installerOpts));
|
|
699
|
+
provisionCmd = "provision.installer";
|
|
700
|
+
logger.debug("session_start: Installer provisioned");
|
|
701
|
+
break;
|
|
702
|
+
}
|
|
703
|
+
case "electron": {
|
|
704
|
+
const electronOpts = provisionOptions;
|
|
705
|
+
logger.info("session_start: Provisioning Electron", { appPath: electronOpts.appPath });
|
|
706
|
+
await raceAbort(extra.signal, "session_start", sdk.provision.electron(electronOpts));
|
|
707
|
+
provisionCmd = "provision.electron";
|
|
708
|
+
logger.debug("session_start: Electron app provisioned");
|
|
709
|
+
break;
|
|
710
|
+
}
|
|
594
711
|
}
|
|
595
712
|
}
|
|
713
|
+
finally {
|
|
714
|
+
stopProvisionHeartbeat();
|
|
715
|
+
}
|
|
596
716
|
// Capture initial screenshot after provisioning
|
|
597
717
|
logger.debug("session_start: Capturing initial screenshot");
|
|
598
|
-
|
|
718
|
+
progress.report("Capturing screenshot...");
|
|
719
|
+
const screenshotBase64 = await raceAbort(extra.signal, "session_start", sdk.agent.system.captureScreenBase64(1, false, true));
|
|
599
720
|
let screenshotResourceUri;
|
|
600
721
|
if (screenshotBase64) {
|
|
601
722
|
screenshotResourceUri = storeImage(screenshotBase64, "screenshot");
|
|
@@ -632,6 +753,19 @@ IMPORTANT - If creating a new test project, use these EXACT dependencies in pack
|
|
|
632
753
|
}, generatedCode);
|
|
633
754
|
}
|
|
634
755
|
catch (error) {
|
|
756
|
+
// On client cancellation, tear down the half-provisioned session so we
|
|
757
|
+
// don't leak a connected sandbox. The underlying SDK call may still be
|
|
758
|
+
// running in the background; best-effort cleanup is all we can do.
|
|
759
|
+
if (error instanceof ToolAbortError) {
|
|
760
|
+
logger.info("session_start: Cancelled by client, tearing down session");
|
|
761
|
+
try {
|
|
762
|
+
await sdk?.disconnect?.();
|
|
763
|
+
}
|
|
764
|
+
catch (cleanupErr) {
|
|
765
|
+
logger.warn("session_start: Cleanup after cancel failed", { error: String(cleanupErr) });
|
|
766
|
+
}
|
|
767
|
+
return createToolResult(false, "Session start was cancelled.", { action: "session_start", cancelled: true });
|
|
768
|
+
}
|
|
635
769
|
logger.error("session_start: Failed", { error: String(error) });
|
|
636
770
|
captureException(error, { tags: { tool: "session_start" }, extra: { params } });
|
|
637
771
|
throw error;
|
|
@@ -693,8 +827,9 @@ registerAppTool(server, "find", {
|
|
|
693
827
|
timeout: z.number().optional().describe("Timeout in ms for polling"),
|
|
694
828
|
}),
|
|
695
829
|
_meta: { ui: { resourceUri: RESOURCE_URI, expanded: true } },
|
|
696
|
-
}, async (params) => {
|
|
830
|
+
}, async (params, extra) => {
|
|
697
831
|
const startTime = Date.now();
|
|
832
|
+
const progress = makeProgressReporter(extra);
|
|
698
833
|
logger.info("find: Starting", { description: params.description, timeout: params.timeout });
|
|
699
834
|
const sessionCheck = requireActiveSession();
|
|
700
835
|
if (!sessionCheck.valid) {
|
|
@@ -703,7 +838,14 @@ registerAppTool(server, "find", {
|
|
|
703
838
|
}
|
|
704
839
|
try {
|
|
705
840
|
logger.debug("find: Calling SDK find");
|
|
706
|
-
const
|
|
841
|
+
const stopHeartbeat = progress.heartbeat(`Looking for "${params.description}"...`);
|
|
842
|
+
let element;
|
|
843
|
+
try {
|
|
844
|
+
element = await raceAbort(extra.signal, "find", sdk.find(params.description, params.timeout ? { timeout: params.timeout } : undefined));
|
|
845
|
+
}
|
|
846
|
+
finally {
|
|
847
|
+
stopHeartbeat();
|
|
848
|
+
}
|
|
707
849
|
const found = element.found();
|
|
708
850
|
const coords = element.getCoordinates();
|
|
709
851
|
// Store element ref for later use (stores actual Element instance)
|
|
@@ -784,6 +926,9 @@ registerAppTool(server, "find", {
|
|
|
784
926
|
}, generatedCode);
|
|
785
927
|
}
|
|
786
928
|
catch (error) {
|
|
929
|
+
const cancelled = cancelledResultOrNull(error, "find");
|
|
930
|
+
if (cancelled)
|
|
931
|
+
return cancelled;
|
|
787
932
|
logger.error("find: Failed", { error: String(error), description: params.description });
|
|
788
933
|
captureException(error, { tags: { tool: "find" }, extra: { description: params.description } });
|
|
789
934
|
throw error;
|
|
@@ -798,8 +943,9 @@ registerAppTool(server, "findall", {
|
|
|
798
943
|
timeout: z.number().optional().describe("Timeout in ms for polling"),
|
|
799
944
|
}),
|
|
800
945
|
_meta: { ui: { resourceUri: RESOURCE_URI, expanded: true } },
|
|
801
|
-
}, async (params) => {
|
|
946
|
+
}, async (params, extra) => {
|
|
802
947
|
const startTime = Date.now();
|
|
948
|
+
const progress = makeProgressReporter(extra);
|
|
803
949
|
logger.info("findall: Starting", { description: params.description, timeout: params.timeout });
|
|
804
950
|
const sessionCheck = requireActiveSession();
|
|
805
951
|
if (!sessionCheck.valid) {
|
|
@@ -808,7 +954,14 @@ registerAppTool(server, "findall", {
|
|
|
808
954
|
}
|
|
809
955
|
try {
|
|
810
956
|
logger.debug("findall: Calling SDK findAll");
|
|
811
|
-
const
|
|
957
|
+
const stopHeartbeat = progress.heartbeat(`Looking for all "${params.description}"...`);
|
|
958
|
+
let elements;
|
|
959
|
+
try {
|
|
960
|
+
elements = await raceAbort(extra.signal, "findall", sdk.findAll(params.description, params.timeout ? { timeout: params.timeout } : undefined));
|
|
961
|
+
}
|
|
962
|
+
finally {
|
|
963
|
+
stopHeartbeat();
|
|
964
|
+
}
|
|
812
965
|
const count = elements.length;
|
|
813
966
|
// Store element refs for later use
|
|
814
967
|
const refs = [];
|
|
@@ -893,6 +1046,9 @@ registerAppTool(server, "findall", {
|
|
|
893
1046
|
}, generatedCode);
|
|
894
1047
|
}
|
|
895
1048
|
catch (error) {
|
|
1049
|
+
const cancelled = cancelledResultOrNull(error, "findall");
|
|
1050
|
+
if (cancelled)
|
|
1051
|
+
return cancelled;
|
|
896
1052
|
logger.error("findall: Failed", { error: String(error), description: params.description });
|
|
897
1053
|
captureException(error, { tags: { tool: "findall" }, extra: { description: params.description } });
|
|
898
1054
|
throw error;
|
|
@@ -1089,8 +1245,9 @@ registerAppTool(server, "find_and_click", {
|
|
|
1089
1245
|
action: z.enum(["click", "double-click", "right-click"]).default("click"),
|
|
1090
1246
|
}),
|
|
1091
1247
|
_meta: { ui: { resourceUri: RESOURCE_URI, expanded: true } },
|
|
1092
|
-
}, async (params) => {
|
|
1248
|
+
}, async (params, extra) => {
|
|
1093
1249
|
const startTime = Date.now();
|
|
1250
|
+
const progress = makeProgressReporter(extra);
|
|
1094
1251
|
logger.info("find_and_click: Starting", { description: params.description, action: params.action });
|
|
1095
1252
|
const sessionCheck = requireActiveSession();
|
|
1096
1253
|
if (!sessionCheck.valid) {
|
|
@@ -1099,7 +1256,14 @@ registerAppTool(server, "find_and_click", {
|
|
|
1099
1256
|
}
|
|
1100
1257
|
try {
|
|
1101
1258
|
logger.debug("find_and_click: Finding element");
|
|
1102
|
-
const
|
|
1259
|
+
const stopHeartbeat = progress.heartbeat(`Looking for "${params.description}"...`);
|
|
1260
|
+
let element;
|
|
1261
|
+
try {
|
|
1262
|
+
element = await raceAbort(extra.signal, "find_and_click", sdk.find(params.description));
|
|
1263
|
+
}
|
|
1264
|
+
finally {
|
|
1265
|
+
stopHeartbeat();
|
|
1266
|
+
}
|
|
1103
1267
|
const found = element.found();
|
|
1104
1268
|
if (!found) {
|
|
1105
1269
|
logger.warn("find_and_click: Element not found", { description: params.description });
|
|
@@ -1207,6 +1371,9 @@ registerAppTool(server, "find_and_click", {
|
|
|
1207
1371
|
}, generatedCode);
|
|
1208
1372
|
}
|
|
1209
1373
|
catch (error) {
|
|
1374
|
+
const cancelled = cancelledResultOrNull(error, "find_and_click");
|
|
1375
|
+
if (cancelled)
|
|
1376
|
+
return cancelled;
|
|
1210
1377
|
logger.error("find_and_click: Failed", { error: String(error), description: params.description });
|
|
1211
1378
|
captureException(error, { tags: { tool: "find_and_click" }, extra: { description: params.description, action: params.action } });
|
|
1212
1379
|
throw error;
|
|
@@ -1364,8 +1531,9 @@ You can optionally provide a reference image URI to compare against a previous s
|
|
|
1364
1531
|
referenceImageUri: z.string().optional().describe("Optional screenshot resource URI (e.g., 'screenshot://testdriver/screenshot/screenshot-1') to compare against instead of the automatically captured 'before' screenshot. Use a screenshotResourceUri from a previous action."),
|
|
1365
1532
|
}),
|
|
1366
1533
|
_meta: { ui: { resourceUri: RESOURCE_URI, expanded: true } },
|
|
1367
|
-
}, async (params) => {
|
|
1534
|
+
}, async (params, extra) => {
|
|
1368
1535
|
const startTime = Date.now();
|
|
1536
|
+
const progress = makeProgressReporter(extra);
|
|
1369
1537
|
logger.info("check: Starting", { task: params.task, hasReferenceImageUri: !!params.referenceImageUri });
|
|
1370
1538
|
const sessionCheck = requireActiveSession();
|
|
1371
1539
|
if (!sessionCheck.valid) {
|
|
@@ -1375,6 +1543,7 @@ You can optionally provide a reference image URI to compare against a previous s
|
|
|
1375
1543
|
try {
|
|
1376
1544
|
// Capture current screenshot
|
|
1377
1545
|
logger.debug("check: Capturing current screenshot");
|
|
1546
|
+
progress.report("Capturing screenshot...");
|
|
1378
1547
|
const currentScreenshot = await sdk.agent.system.captureScreenBase64(1, false, true);
|
|
1379
1548
|
// Use provided reference image URI, last screenshot as "before" state, or current if no previous screenshot
|
|
1380
1549
|
let beforeScreenshot;
|
|
@@ -1425,12 +1594,19 @@ You can optionally provide a reference image URI to compare against a previous s
|
|
|
1425
1594
|
beforeScreenshotPreview: beforeScreenshot?.substring(0, 50),
|
|
1426
1595
|
currentScreenshotPreview: currentScreenshot?.substring(0, 50)
|
|
1427
1596
|
});
|
|
1428
|
-
const
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1597
|
+
const stopHeartbeat = progress.heartbeat(`Checking: "${params.task}"...`);
|
|
1598
|
+
let response;
|
|
1599
|
+
try {
|
|
1600
|
+
response = await raceAbort(extra.signal, "check", sdk.agent.sdk.req("check", {
|
|
1601
|
+
tasks: [params.task],
|
|
1602
|
+
images: [beforeScreenshot, currentScreenshot],
|
|
1603
|
+
mousePosition,
|
|
1604
|
+
activeWindow,
|
|
1605
|
+
}));
|
|
1606
|
+
}
|
|
1607
|
+
finally {
|
|
1608
|
+
stopHeartbeat();
|
|
1609
|
+
}
|
|
1434
1610
|
const aiResponse = response.data;
|
|
1435
1611
|
// Store screenshot for resource serving
|
|
1436
1612
|
let screenshotResourceUri;
|
|
@@ -1460,6 +1636,9 @@ You can optionally provide a reference image URI to compare against a previous s
|
|
|
1460
1636
|
});
|
|
1461
1637
|
}
|
|
1462
1638
|
catch (error) {
|
|
1639
|
+
const cancelled = cancelledResultOrNull(error, "check");
|
|
1640
|
+
if (cancelled)
|
|
1641
|
+
return cancelled;
|
|
1463
1642
|
logger.error("check: Failed", { error: String(error), task: params.task });
|
|
1464
1643
|
captureException(error, { tags: { tool: "check" }, extra: { task: params.task } });
|
|
1465
1644
|
throw error;
|