@midscene/playground 1.9.6 → 1.9.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/es/adapters/local-execution.mjs +3 -3
  2. package/dist/es/adapters/local-execution.mjs.map +1 -1
  3. package/dist/es/adapters/remote-execution.mjs +35 -0
  4. package/dist/es/adapters/remote-execution.mjs.map +1 -1
  5. package/dist/es/common.mjs +30 -3
  6. package/dist/es/common.mjs.map +1 -1
  7. package/dist/es/index.browser.mjs.map +1 -1
  8. package/dist/es/mjpeg-hub.mjs +3 -0
  9. package/dist/es/mjpeg-hub.mjs.map +1 -1
  10. package/dist/es/mjpeg-stream-handler.mjs +6 -0
  11. package/dist/es/mjpeg-stream-handler.mjs.map +1 -1
  12. package/dist/es/platform.mjs.map +1 -1
  13. package/dist/es/sdk/index.mjs +7 -0
  14. package/dist/es/sdk/index.mjs.map +1 -1
  15. package/dist/es/server.mjs +563 -42
  16. package/dist/es/server.mjs.map +1 -1
  17. package/dist/lib/adapters/local-execution.js +3 -3
  18. package/dist/lib/adapters/local-execution.js.map +1 -1
  19. package/dist/lib/adapters/remote-execution.js +35 -0
  20. package/dist/lib/adapters/remote-execution.js.map +1 -1
  21. package/dist/lib/common.js +30 -3
  22. package/dist/lib/common.js.map +1 -1
  23. package/dist/lib/index.browser.js.map +1 -1
  24. package/dist/lib/mjpeg-hub.js +3 -0
  25. package/dist/lib/mjpeg-hub.js.map +1 -1
  26. package/dist/lib/mjpeg-stream-handler.js +6 -0
  27. package/dist/lib/mjpeg-stream-handler.js.map +1 -1
  28. package/dist/lib/platform.js.map +1 -1
  29. package/dist/lib/sdk/index.js +7 -0
  30. package/dist/lib/sdk/index.js.map +1 -1
  31. package/dist/lib/server.js +560 -39
  32. package/dist/lib/server.js.map +1 -1
  33. package/dist/types/adapters/local-execution.d.ts +3 -2
  34. package/dist/types/adapters/remote-execution.d.ts +2 -1
  35. package/dist/types/index.browser.d.ts +1 -1
  36. package/dist/types/index.d.ts +1 -1
  37. package/dist/types/mjpeg-hub.d.ts +1 -0
  38. package/dist/types/mjpeg-stream-handler.d.ts +2 -0
  39. package/dist/types/platform.d.ts +104 -0
  40. package/dist/types/sdk/index.d.ts +3 -2
  41. package/dist/types/server.d.ts +9 -0
  42. package/dist/types/types.d.ts +5 -0
  43. package/package.json +3 -3
  44. package/static/index.html +1 -1
  45. package/static/static/css/{index.ab28104a.css → index.1293237f.css} +2 -2
  46. package/static/static/css/{index.ab28104a.css.map → index.1293237f.css.map} +1 -1
  47. package/static/static/js/{596.5426be9e.js → 905.8d12588e.js} +17 -17
  48. package/static/static/js/905.8d12588e.js.map +1 -0
  49. package/static/static/js/index.50e06ed5.js +948 -0
  50. package/static/static/js/index.50e06ed5.js.map +1 -0
  51. package/static/static/js/596.5426be9e.js.map +0 -1
  52. package/static/static/js/index.134d5099.js +0 -940
  53. package/static/static/js/index.134d5099.js.map +0 -1
  54. /package/static/static/js/{596.5426be9e.js.LICENSE.txt → 905.8d12588e.js.LICENSE.txt} +0 -0
  55. /package/static/static/js/{index.134d5099.js.LICENSE.txt → index.50e06ed5.js.LICENSE.txt} +0 -0
@@ -1,13 +1,16 @@
1
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
- import { dirname, join, resolve as external_node_path_resolve } from "node:path";
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { dirname, join, resolve as external_node_path_resolve, sep } from "node:path";
3
4
  import { fileURLToPath } from "node:url";
4
5
  import { ReportActionDump, runConnectivityTest } from "@midscene/core";
5
6
  import { getTmpDir, sleep } from "@midscene/core/utils";
7
+ import { getMidsceneRunSubDir } from "@midscene/shared/common";
6
8
  import { PLAYGROUND_SERVER_PORT } from "@midscene/shared/constants";
7
9
  import { globalModelConfigManager, overrideAIConfig } from "@midscene/shared/env";
8
10
  import { generateElementByPoint } from "@midscene/shared/extractor";
9
- import { compositePointMarkerImg } from "@midscene/shared/img";
11
+ import { annotateRects, compositePointMarkerImg, imageInfoOfBase64 } from "@midscene/shared/img";
10
12
  import { getDebug } from "@midscene/shared/logger";
13
+ import { buildMidsceneRecorderActionSummary, buildMidsceneRecorderReplayInstruction } from "@midscene/shared/recorder";
11
14
  import { uuid as utils_uuid } from "@midscene/shared/utils";
12
15
  import express from "express";
13
16
  import { executeAction, formatErrorMessage } from "./common.mjs";
@@ -27,6 +30,216 @@ function _define_property(obj, key, value) {
27
30
  }
28
31
  const defaultPort = PLAYGROUND_SERVER_PORT;
29
32
  const RECORDER_CAPTURE_AFTER_INTERACT_DELAY_MS = 250;
33
+ const RECORDER_AI_DESCRIBE_AFTER_INTERACT_TIMEOUT_MS = 30000;
34
+ const RECORDER_AI_DESCRIBE_SCREENSHOT_DUMP_DIR = 'recorder-ai-describe-screenshots';
35
+ function extractBase64Payload(value) {
36
+ const dataUrlMatch = value.match(/^data:([^;,]+)?(?:;[^,]*)?,([\s\S]*)$/);
37
+ if (dataUrlMatch) return {
38
+ mimeType: dataUrlMatch[1],
39
+ base64: dataUrlMatch[2] || ''
40
+ };
41
+ return {
42
+ base64: value
43
+ };
44
+ }
45
+ function estimateBase64Bytes(value) {
46
+ if (!value) return;
47
+ const { base64 } = extractBase64Payload(value);
48
+ const padding = base64.endsWith('==') ? 2 : base64.endsWith('=') ? 1 : 0;
49
+ return Math.max(0, Math.floor(3 * base64.length / 4) - padding);
50
+ }
51
+ function buildRecorderRawPayloadSummary(rawPayload) {
52
+ if (!rawPayload) return;
53
+ const allowedKeys = [
54
+ 'actionType',
55
+ 'x',
56
+ 'y',
57
+ 'endX',
58
+ 'endY',
59
+ 'duration',
60
+ 'direction',
61
+ 'scrollType',
62
+ 'distance',
63
+ 'keyName',
64
+ 'mode'
65
+ ];
66
+ const summary = {};
67
+ for (const key of allowedKeys)if (void 0 !== rawPayload[key]) summary[key] = rawPayload[key];
68
+ const value = rawPayload.value;
69
+ if ('string' == typeof value) summary.valueLength = value.length;
70
+ return Object.keys(summary).length > 0 ? summary : void 0;
71
+ }
72
+ function buildRecorderEventSummary(event) {
73
+ return {
74
+ hashId: event.hashId,
75
+ mergedHashIds: event.mergedHashIds,
76
+ type: event.type,
77
+ source: event.source,
78
+ actionType: event.actionType,
79
+ timestamp: event.timestamp,
80
+ url: event.url,
81
+ title: event.title,
82
+ valueLength: 'string' == typeof event.value ? event.value.length : void 0,
83
+ rawPayloadSummary: buildRecorderRawPayloadSummary(event.rawPayload),
84
+ elementRect: event.elementRect,
85
+ pageInfo: event.pageInfo
86
+ };
87
+ }
88
+ function shouldVerifyRecorderAiDescribeEvent(event) {
89
+ return 'scroll' !== event.type;
90
+ }
91
+ function formatRecorderAiDescribeScreenshotPathParts() {
92
+ const date = new Date();
93
+ const localDate = Number.isNaN(date.getTime()) ? new Date() : date;
94
+ const pad = (value, length = 2)=>String(value).padStart(length, '0');
95
+ const datePart = [
96
+ localDate.getFullYear(),
97
+ pad(localDate.getMonth() + 1),
98
+ pad(localDate.getDate())
99
+ ].join('-');
100
+ const timePart = [
101
+ pad(localDate.getHours()),
102
+ pad(localDate.getMinutes()),
103
+ pad(localDate.getSeconds()),
104
+ pad(localDate.getMilliseconds(), 3)
105
+ ].join('-');
106
+ const hourPart = timePart.slice(0, 2) || 'unknown-hour';
107
+ return {
108
+ datePart,
109
+ hourPart,
110
+ filePrefix: `${timePart}_${randomUUID()}`
111
+ };
112
+ }
113
+ function assertPathInsideDirectory(rootDir, targetPath) {
114
+ const resolvedRoot = external_node_path_resolve(rootDir);
115
+ const resolvedTarget = external_node_path_resolve(targetPath);
116
+ if (resolvedTarget !== resolvedRoot && !resolvedTarget.startsWith(`${resolvedRoot}${sep}`)) throw new Error(`Refusing to write recorder dump outside ${resolvedRoot}`);
117
+ return resolvedTarget;
118
+ }
119
+ function writeRecorderAiDescribeScreenshot(imageBase64, suffix) {
120
+ const { base64, mimeType } = extractBase64Payload(imageBase64);
121
+ const bytes = Buffer.from(base64, 'base64');
122
+ const sha256 = createHash('sha256').update(bytes).digest('hex');
123
+ const extension = mimeType?.includes('jpeg') ? 'jpg' : 'png';
124
+ const pathParts = formatRecorderAiDescribeScreenshotPathParts();
125
+ const dumpRoot = external_node_path_resolve(getMidsceneRunSubDir('dump'), RECORDER_AI_DESCRIBE_SCREENSHOT_DUMP_DIR);
126
+ const dumpDir = assertPathInsideDirectory(dumpRoot, join(dumpRoot, pathParts.datePart, pathParts.hourPart));
127
+ mkdirSync(dumpDir, {
128
+ recursive: true
129
+ });
130
+ const safeSuffix = 'annotated' === suffix ? 'annotated' : 'raw';
131
+ const fileName = `${pathParts.filePrefix}_${safeSuffix}.${extension}`;
132
+ const filePath = assertPathInsideDirectory(dumpRoot, join(dumpDir, fileName));
133
+ if (!existsSync(filePath)) writeFileSync(filePath, bytes);
134
+ return {
135
+ path: filePath,
136
+ sha256,
137
+ bytes: bytes.byteLength,
138
+ mimeType
139
+ };
140
+ }
141
+ function calculateRecorderScreenshotAnnotation(event, imageSize, verifyResult) {
142
+ const pageWidth = event.pageInfo?.width;
143
+ const pageHeight = event.pageInfo?.height;
144
+ const scaleX = pageWidth ? imageSize.width / pageWidth : void 0;
145
+ const scaleY = pageHeight ? imageSize.height / pageHeight : void 0;
146
+ const mapX = (value)=>scaleX ? value * scaleX : value;
147
+ const mapY = (value)=>scaleY ? value * scaleY : value;
148
+ const logicalPoint = 'number' == typeof event.elementRect?.x && 'number' == typeof event.elementRect?.y ? [
149
+ event.elementRect.x,
150
+ event.elementRect.y
151
+ ] : void 0;
152
+ const locateRect = verifyResult?.rect;
153
+ const sourceTargetRect = 'number' == typeof event.elementRect?.left && 'number' == typeof event.elementRect?.top && 'number' == typeof event.elementRect?.width && 'number' == typeof event.elementRect?.height && event.elementRect.width > 0 && event.elementRect.height > 0 ? {
154
+ left: mapX(event.elementRect.left),
155
+ top: mapY(event.elementRect.top),
156
+ width: mapX(event.elementRect.width),
157
+ height: mapY(event.elementRect.height)
158
+ } : void 0;
159
+ if (!logicalPoint && !sourceTargetRect && !locateRect) return;
160
+ const screenshotPoint = logicalPoint && scaleX && scaleY ? [
161
+ logicalPoint[0] * scaleX,
162
+ logicalPoint[1] * scaleY
163
+ ] : void 0;
164
+ const pointTargetRect = screenshotPoint && !sourceTargetRect ? {
165
+ left: Math.max(0, screenshotPoint[0] - 6),
166
+ top: Math.max(0, screenshotPoint[1] - 6),
167
+ width: 12,
168
+ height: 12
169
+ } : void 0;
170
+ const center = verifyResult?.center || (locateRect ? [
171
+ locateRect.left + locateRect.width / 2,
172
+ locateRect.top + locateRect.height / 2
173
+ ] : void 0);
174
+ const centerDelta = screenshotPoint && center ? {
175
+ x: center[0] - screenshotPoint[0],
176
+ y: center[1] - screenshotPoint[1],
177
+ distance: Math.hypot(center[0] - screenshotPoint[0], center[1] - screenshotPoint[1])
178
+ } : void 0;
179
+ const distanceOutsideRect = screenshotPoint && locateRect ? (()=>{
180
+ const right = locateRect.left + locateRect.width;
181
+ const bottom = locateRect.top + locateRect.height;
182
+ const x = screenshotPoint[0] < locateRect.left ? locateRect.left - screenshotPoint[0] : screenshotPoint[0] > right ? screenshotPoint[0] - right : 0;
183
+ const y = screenshotPoint[1] < locateRect.top ? locateRect.top - screenshotPoint[1] : screenshotPoint[1] > bottom ? screenshotPoint[1] - bottom : 0;
184
+ return {
185
+ x,
186
+ y,
187
+ distance: Math.hypot(x, y)
188
+ };
189
+ })() : void 0;
190
+ return {
191
+ inputPoint: screenshotPoint ? {
192
+ logical: logicalPoint,
193
+ screenshot: screenshotPoint
194
+ } : void 0,
195
+ sourceTargetRect: sourceTargetRect || pointTargetRect,
196
+ locateRect,
197
+ centerDelta,
198
+ distanceOutsideRect
199
+ };
200
+ }
201
+ async function persistRecorderAiDescribeScreenshot(event, eventScreenshot, verifyResult) {
202
+ if (!eventScreenshot) return;
203
+ const screenshotRef = writeRecorderAiDescribeScreenshot(eventScreenshot, 'raw');
204
+ let annotatedScreenshotRef;
205
+ let screenshotAnnotation;
206
+ let annotatedScreenshotPersistError;
207
+ try {
208
+ const imageSize = await imageInfoOfBase64(eventScreenshot);
209
+ screenshotAnnotation = calculateRecorderScreenshotAnnotation(event, imageSize, verifyResult);
210
+ const annotatedRects = [
211
+ screenshotAnnotation?.sourceTargetRect,
212
+ screenshotAnnotation?.locateRect
213
+ ].filter((rect)=>Boolean(rect));
214
+ if (annotatedRects.length > 0) {
215
+ const annotatedScreenshot = await annotateRects(eventScreenshot, annotatedRects);
216
+ annotatedScreenshotRef = writeRecorderAiDescribeScreenshot(annotatedScreenshot, 'annotated');
217
+ }
218
+ } catch (error) {
219
+ annotatedScreenshotPersistError = error instanceof Error ? error.message : String(error);
220
+ }
221
+ return {
222
+ screenshotRef,
223
+ annotatedScreenshotRef,
224
+ screenshotAnnotation,
225
+ annotatedScreenshotPersistError
226
+ };
227
+ }
228
+ function createRecorderAiDescribeTraceBase(event, eventScreenshot) {
229
+ return {
230
+ traceId: `${event.hashId || 'recorder-event'}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
231
+ eventHashId: event.hashId,
232
+ eventType: event.type,
233
+ actionType: event.actionType,
234
+ eventSummary: buildRecorderEventSummary(event),
235
+ point: 'number' == typeof event.elementRect?.x && 'number' == typeof event.elementRect?.y ? [
236
+ event.elementRect.x,
237
+ event.elementRect.y
238
+ ] : void 0,
239
+ pageInfo: event.pageInfo,
240
+ screenshotBytes: estimateBase64Bytes(eventScreenshot)
241
+ };
242
+ }
30
243
  function serializeAiConfigSignature(aiConfig) {
31
244
  return JSON.stringify(Object.entries(aiConfig).sort(([leftKey], [rightKey])=>leftKey.localeCompare(rightKey)));
32
245
  }
@@ -190,6 +403,74 @@ function buildInteractParams(actionType, body) {
190
403
  const { actionType: _omit, ...passthrough } = body;
191
404
  return passthrough;
192
405
  }
406
+ function getRecorderSemanticActionType(actionType) {
407
+ switch(actionType){
408
+ case 'Tap':
409
+ case 'DoubleClick':
410
+ case 'LongPress':
411
+ case 'RightClick':
412
+ return 'click';
413
+ case 'Swipe':
414
+ case 'DragAndDrop':
415
+ return 'drag';
416
+ case 'Input':
417
+ return 'input';
418
+ case 'KeyboardPress':
419
+ return 'keydown';
420
+ case 'Scroll':
421
+ return 'scroll';
422
+ case 'GoBack':
423
+ case 'GoForward':
424
+ case 'Reload':
425
+ case 'Stop':
426
+ case 'InitialNavigation':
427
+ case 'NavigationChanged':
428
+ return 'navigation';
429
+ default:
430
+ return 'click';
431
+ }
432
+ }
433
+ function buildRecorderSemanticAction(actionType, payload, url) {
434
+ return {
435
+ type: getRecorderSemanticActionType(actionType),
436
+ actionType,
437
+ value: 'string' == typeof payload.value ? payload.value : 'string' == typeof payload.keyName ? payload.keyName : void 0,
438
+ url
439
+ };
440
+ }
441
+ function buildReadyRecorderSemantic(source, event, elementDescription, confidence, extra) {
442
+ return {
443
+ source,
444
+ status: 'ready',
445
+ elementDescription,
446
+ replayInstruction: buildMidsceneRecorderReplayInstruction(event, elementDescription),
447
+ actionSummary: buildMidsceneRecorderActionSummary(event, elementDescription),
448
+ confidence,
449
+ ...extra
450
+ };
451
+ }
452
+ function buildFailedAiDescribeRecorderSemantic(error, extra) {
453
+ return {
454
+ source: 'aiDescribe',
455
+ status: 'failed',
456
+ error: error instanceof Error ? error.message : String(error),
457
+ ...extra
458
+ };
459
+ }
460
+ function withTimeout(promise, timeoutMs, message) {
461
+ let timeout;
462
+ const timeoutPromise = new Promise((_resolve, reject)=>{
463
+ timeout = setTimeout(()=>{
464
+ reject(new Error(message));
465
+ }, timeoutMs);
466
+ });
467
+ return Promise.race([
468
+ promise,
469
+ timeoutPromise
470
+ ]).finally(()=>{
471
+ if (timeout) clearTimeout(timeout);
472
+ });
473
+ }
193
474
  function createManualExecutorContext(actionType, param) {
194
475
  const task = {
195
476
  type: 'Action Space',
@@ -393,9 +674,14 @@ class PlaygroundServer {
393
674
  resetRecorderState() {
394
675
  this._recorderSessionId = null;
395
676
  this._recorderEvents = [];
677
+ this._recorderEventQueue = Promise.resolve();
678
+ this._studioPreviewRecorderLastTargetPoint = void 0;
396
679
  this._studioPreviewRecorderLastScreenshot = void 0;
397
680
  this._studioPreviewRecorderLastPageState = void 0;
398
681
  }
682
+ async waitForRecorderIdle() {
683
+ await this._recorderEventQueue;
684
+ }
399
685
  canRecordStudioPreviewInteractions() {
400
686
  const agent = this._activeConnection.agent;
401
687
  if (!agent) return false;
@@ -479,6 +765,13 @@ class PlaygroundServer {
479
765
  pageState: this._studioPreviewRecorderLastPageState || await this.getActiveRecorderPageState()
480
766
  };
481
767
  }
768
+ captureCachedRecorderSnapshotBeforeInteract() {
769
+ if (!this._recorderSessionId || !this._studioPreviewRecorderLastPageState || this._recorderPendingCaptures > 0) return;
770
+ return {
771
+ screenshot: this._mjpegHandler.getLastFrameBase64() || this._studioPreviewRecorderLastScreenshot,
772
+ pageState: this._studioPreviewRecorderLastPageState
773
+ };
774
+ }
482
775
  async startStudioPreviewRecorder(sessionId) {
483
776
  this._recorderSessionId = sessionId;
484
777
  this._studioPreviewRecorderLastScreenshot = await this.takeRecorderScreenshot();
@@ -487,7 +780,7 @@ class PlaygroundServer {
487
780
  const initialNavigationEvent = this.buildStudioPreviewInitialNavigationEvent(initialPageState, this._studioPreviewRecorderLastScreenshot);
488
781
  if (initialNavigationEvent) this._recorderEvents.push(initialNavigationEvent);
489
782
  }
490
- async storeStudioPreviewRecorderEvent(payload, snapshotBefore) {
783
+ async storeStudioPreviewRecorderEvent(payload, snapshotBefore, agent) {
491
784
  if (!this._recorderSessionId) return;
492
785
  const before = snapshotBefore || await this.captureRecorderSnapshotBeforeInteract();
493
786
  const screenshotBefore = before?.screenshot;
@@ -512,19 +805,26 @@ class PlaygroundServer {
512
805
  this._studioPreviewRecorderLastPageState = pageStateAfter;
513
806
  return;
514
807
  }
515
- this._recorderEvents.push(event);
516
808
  const navigationEvent = this.buildStudioPreviewNavigationChangeEvent(payload, before?.pageState, pageStateAfter, screenshotAfter);
517
- if (navigationEvent) this._recorderEvents.push(navigationEvent);
518
809
  this._studioPreviewRecorderLastScreenshot = screenshotAfter;
519
810
  this._studioPreviewRecorderLastPageState = pageStateAfter;
811
+ this.queueStudioPreviewRecorderEventAppend(event, navigationEvent);
520
812
  }
521
813
  async buildStudioPreviewRecorderEvent(payload, pageState, screenshotBefore, screenshotAfter) {
522
814
  const actionType = 'string' == typeof payload.actionType ? payload.actionType : void 0;
523
815
  if (!actionType) return null;
524
816
  const { pageInfo, url, title } = pageState;
525
817
  const timestamp = Date.now();
526
- const x = 'number' == typeof payload.x ? payload.x : void 0;
527
- const y = 'number' == typeof payload.y ? payload.y : void 0;
818
+ const payloadX = 'number' == typeof payload.x ? payload.x : void 0;
819
+ const payloadY = 'number' == typeof payload.y ? payload.y : void 0;
820
+ const canReuseLastTargetPoint = 'Input' === actionType || 'KeyboardPress' === actionType;
821
+ const inheritedTargetPoint = canReuseLastTargetPoint && (void 0 === payloadX || void 0 === payloadY) ? this._studioPreviewRecorderLastTargetPoint : void 0;
822
+ const x = payloadX ?? inheritedTargetPoint?.x;
823
+ const y = payloadY ?? inheritedTargetPoint?.y;
824
+ if (void 0 !== payloadX && void 0 !== payloadY) this._studioPreviewRecorderLastTargetPoint = {
825
+ x: payloadX,
826
+ y: payloadY
827
+ };
528
828
  const endX = 'number' == typeof payload.endX ? payload.endX : void 0;
529
829
  const endY = 'number' == typeof payload.endY ? payload.endY : void 0;
530
830
  const dragDescription = void 0 !== x && void 0 !== y && void 0 !== endX && void 0 !== endY ? `${Math.round(x)},${Math.round(y)} -> ${Math.round(endX)},${Math.round(endY)}` : void 0;
@@ -542,8 +842,10 @@ class PlaygroundServer {
542
842
  screenshotBefore,
543
843
  screenshotAfter,
544
844
  screenshotWithBox: screenshotWithMarker,
545
- descriptionLoading: true,
546
- descriptionSource: void 0,
845
+ semantic: {
846
+ source: 'aiDescribe',
847
+ status: 'pending'
848
+ },
547
849
  timestamp,
548
850
  hashId: `studio-preview-${actionType}-${timestamp}-${Math.random().toString(36).slice(2, 8)}`
549
851
  };
@@ -621,17 +923,18 @@ class PlaygroundServer {
621
923
  case 'GoForward':
622
924
  case 'Reload':
623
925
  case 'Stop':
624
- return {
625
- ...base,
626
- type: 'navigation',
627
- value: actionType,
628
- elementDescription: url || actionType,
629
- replayInstruction: 'Stop' === actionType ? 'Stop loading the current page.' : url ? `Wait for navigation to complete at \`${url}\`.` : `${actionType} in the browser.`,
630
- actionSummary: 'Stop' === actionType ? 'Stop page loading' : url ? `${actionType} to ${url}` : actionType,
631
- semanticConfidence: url ? 'high' : 'medium',
632
- descriptionLoading: false,
633
- descriptionSource: 'fallback'
634
- };
926
+ {
927
+ const elementDescription = url || actionType;
928
+ const semanticEvent = buildRecorderSemanticAction(actionType, {
929
+ value: actionType
930
+ }, url);
931
+ return {
932
+ ...base,
933
+ type: 'navigation',
934
+ value: actionType,
935
+ semantic: buildReadyRecorderSemantic('heuristic', semanticEvent, elementDescription, url ? 'high' : 'medium')
936
+ };
937
+ }
635
938
  default:
636
939
  return {
637
940
  ...base,
@@ -640,6 +943,190 @@ class PlaygroundServer {
640
943
  };
641
944
  }
642
945
  }
946
+ async enrichStudioPreviewRecorderEventWithAiDescribe(event, agent) {
947
+ const startedAt = new Date();
948
+ const startedAtMs = Date.now();
949
+ const eventScreenshot = this.getRecorderAiDescribeScreenshot(event);
950
+ const traceBase = createRecorderAiDescribeTraceBase(event, eventScreenshot);
951
+ const finishTrace = async (status, extra = {})=>{
952
+ let screenshotRef;
953
+ let annotatedScreenshotRef;
954
+ let screenshotAnnotation;
955
+ let screenshotPersistError;
956
+ let annotatedScreenshotPersistError;
957
+ if ('failed' === status) try {
958
+ const screenshotDump = await persistRecorderAiDescribeScreenshot(event, eventScreenshot, extra.verifyResult);
959
+ screenshotRef = screenshotDump?.screenshotRef;
960
+ annotatedScreenshotRef = screenshotDump?.annotatedScreenshotRef;
961
+ screenshotAnnotation = screenshotDump?.screenshotAnnotation;
962
+ annotatedScreenshotPersistError = screenshotDump?.annotatedScreenshotPersistError;
963
+ } catch (error) {
964
+ screenshotPersistError = error instanceof Error ? error.message : String(error);
965
+ }
966
+ return {
967
+ ...traceBase,
968
+ ...extra,
969
+ screenshotRef,
970
+ annotatedScreenshotRef,
971
+ screenshotAnnotation,
972
+ screenshotPersistError,
973
+ annotatedScreenshotPersistError,
974
+ status,
975
+ startedAt: startedAt.toISOString(),
976
+ durationMs: Date.now() - startedAtMs
977
+ };
978
+ };
979
+ if ('navigation' === event.type || 'setViewport' === event.type) {
980
+ const error = 'aiDescribe skipped because the event type does not target a UI element.';
981
+ const trace = await finishTrace('failed', {
982
+ error
983
+ });
984
+ debugInteract('recorder aiDescribe trace:', trace);
985
+ return {
986
+ event: {
987
+ ...event,
988
+ semantic: buildFailedAiDescribeRecorderSemantic(error)
989
+ },
990
+ trace
991
+ };
992
+ }
993
+ if ('function' != typeof agent?.describeElementAtPoint) {
994
+ const error = 'Active agent does not support describeElementAtPoint.';
995
+ const trace = await finishTrace('failed', {
996
+ error
997
+ });
998
+ debugInteract('recorder aiDescribe trace:', trace);
999
+ return {
1000
+ event: {
1001
+ ...event,
1002
+ semantic: buildFailedAiDescribeRecorderSemantic(error)
1003
+ },
1004
+ trace
1005
+ };
1006
+ }
1007
+ const x = event.elementRect?.x;
1008
+ const y = event.elementRect?.y;
1009
+ let modelCallDurationMs;
1010
+ let modelCallStartedAt;
1011
+ let elementDescription;
1012
+ let deepLocate;
1013
+ let verifyResult;
1014
+ try {
1015
+ if ('number' != typeof x || 'number' != typeof y) throw new Error('Skipped aiDescribe because the recorder event has no stable point target.');
1016
+ if (!eventScreenshot) throw new Error('Skipped aiDescribe because the recorder event has no screenshot.');
1017
+ if (!event.pageInfo?.width || !event.pageInfo?.height) throw new Error('Skipped aiDescribe because the recorder event has no pageInfo for coordinate mapping.');
1018
+ const verifyPrompt = shouldVerifyRecorderAiDescribeEvent(event);
1019
+ modelCallStartedAt = Date.now();
1020
+ const describeResult = await withTimeout(agent.describeElementAtPoint([
1021
+ x,
1022
+ y
1023
+ ], {
1024
+ verifyPrompt,
1025
+ screenshotBase64: eventScreenshot,
1026
+ coordinateSpace: 'logical',
1027
+ logicalSize: event.pageInfo,
1028
+ onProgress: (progress)=>{
1029
+ elementDescription = progress.prompt?.trim() || elementDescription;
1030
+ deepLocate = progress.deepLocate;
1031
+ verifyResult = verifyPrompt ? progress.verifyResult : void 0;
1032
+ }
1033
+ }), RECORDER_AI_DESCRIBE_AFTER_INTERACT_TIMEOUT_MS, 'Timed out while analyzing recorder event with aiDescribe.');
1034
+ modelCallDurationMs = Date.now() - modelCallStartedAt;
1035
+ elementDescription = describeResult.prompt?.trim();
1036
+ deepLocate = describeResult.deepLocate;
1037
+ verifyResult = verifyPrompt ? describeResult.verifyResult : void 0;
1038
+ if (!elementDescription) throw new Error("aiDescribe returned an empty element description.");
1039
+ if (verifyResult && false === verifyResult.pass) throw new Error('aiDescribe verification failed.');
1040
+ const semanticAction = buildRecorderSemanticAction(event.actionType || event.type, event.rawPayload || {}, event.url);
1041
+ const trace = await finishTrace('ready', {
1042
+ modelCallDurationMs,
1043
+ elementDescription,
1044
+ verifyPassed: verifyResult?.pass,
1045
+ centerDistance: verifyResult?.centerDistance,
1046
+ verifyResult
1047
+ });
1048
+ debugInteract('recorder aiDescribe trace:', trace);
1049
+ return {
1050
+ event: {
1051
+ ...event,
1052
+ semantic: buildReadyRecorderSemantic('aiDescribe', semanticAction, elementDescription, verifyResult?.pass ? 'high' : 'medium', {
1053
+ aiDescribe: {
1054
+ verifyPrompt,
1055
+ verifyPassed: verifyResult?.pass,
1056
+ deepLocate,
1057
+ centerDistance: verifyResult?.centerDistance,
1058
+ expectedCenter: [
1059
+ x,
1060
+ y
1061
+ ],
1062
+ actualCenter: verifyResult?.center
1063
+ }
1064
+ })
1065
+ },
1066
+ trace
1067
+ };
1068
+ } catch (error) {
1069
+ const reportedError = verifyResult?.pass === false ? new Error('aiDescribe verification failed.') : error;
1070
+ if (void 0 === modelCallDurationMs && modelCallStartedAt) modelCallDurationMs = Date.now() - modelCallStartedAt;
1071
+ debugInteract('canonical recorder aiDescribe failed:', reportedError);
1072
+ const trace = await finishTrace('failed', {
1073
+ error: reportedError instanceof Error ? reportedError.message : String(reportedError),
1074
+ modelCallDurationMs,
1075
+ elementDescription,
1076
+ verifyPassed: verifyResult?.pass,
1077
+ centerDistance: verifyResult?.centerDistance,
1078
+ verifyResult
1079
+ });
1080
+ debugInteract('recorder aiDescribe trace:', trace);
1081
+ const aiDescribeDetails = verifyResult || trace.annotatedScreenshotRef ? {
1082
+ aiDescribe: {
1083
+ verifyPrompt: true,
1084
+ verifyPassed: verifyResult?.pass,
1085
+ deepLocate,
1086
+ centerDistance: verifyResult?.centerDistance,
1087
+ expectedCenter: 'number' == typeof x && 'number' == typeof y ? [
1088
+ x,
1089
+ y
1090
+ ] : void 0,
1091
+ actualCenter: verifyResult?.center,
1092
+ annotatedScreenshotPath: trace.annotatedScreenshotRef?.path
1093
+ }
1094
+ } : void 0;
1095
+ return {
1096
+ event: {
1097
+ ...event,
1098
+ semantic: buildFailedAiDescribeRecorderSemantic(reportedError, aiDescribeDetails)
1099
+ },
1100
+ trace
1101
+ };
1102
+ }
1103
+ }
1104
+ getRecorderAiDescribeScreenshot(event) {
1105
+ if ('click' === event.type || 'input' === event.type || 'keydown' === event.type || 'drag' === event.type) return event.screenshotBefore || event.screenshotAfter;
1106
+ return event.screenshotAfter || event.screenshotBefore;
1107
+ }
1108
+ queueStudioPreviewRecorderEventAppend(event, navigationEvent) {
1109
+ const sessionId = this._recorderSessionId;
1110
+ if (!sessionId) return;
1111
+ this._recorderEvents.push(event);
1112
+ if (navigationEvent) this._recorderEvents.push(navigationEvent);
1113
+ }
1114
+ queueStudioPreviewRecorderEvent(payload, snapshotBefore, agent) {
1115
+ const sessionId = this._recorderSessionId;
1116
+ if (!sessionId) return;
1117
+ this._recorderPendingCaptures++;
1118
+ const queuedTask = this._recorderEventQueue.catch(()=>void 0).then(async ()=>{
1119
+ try {
1120
+ if (!this._recorderSessionId || this._recorderSessionId !== sessionId) return;
1121
+ await this.storeStudioPreviewRecorderEvent(payload, snapshotBefore, agent);
1122
+ } finally{
1123
+ this._recorderPendingCaptures = Math.max(0, this._recorderPendingCaptures - 1);
1124
+ }
1125
+ });
1126
+ this._recorderEventQueue = queuedTask.catch((error)=>{
1127
+ debugInteract('async recorder event capture failed:', error);
1128
+ });
1129
+ }
643
1130
  buildStudioPreviewNavigationChangeEvent(payload, pageStateBefore, pageStateAfter, screenshotAfter) {
644
1131
  const actionType = 'string' == typeof payload.actionType ? payload.actionType : void 0;
645
1132
  if (!actionType || BROWSER_CHROME_NAVIGATION_ACTIONS.has(actionType)) return null;
@@ -647,6 +1134,9 @@ class PlaygroundServer {
647
1134
  const afterUrl = pageStateAfter.url;
648
1135
  if (!afterUrl || beforeUrl === afterUrl) return null;
649
1136
  const timestamp = Date.now();
1137
+ const semanticEvent = buildRecorderSemanticAction('NavigationChanged', {
1138
+ value: afterUrl
1139
+ }, afterUrl);
650
1140
  return {
651
1141
  source: 'studio-preview',
652
1142
  type: 'navigation',
@@ -662,12 +1152,7 @@ class PlaygroundServer {
662
1152
  value: afterUrl,
663
1153
  screenshotBefore: screenshotAfter,
664
1154
  screenshotAfter,
665
- elementDescription: afterUrl,
666
- replayInstruction: `Wait for navigation to complete at \`${afterUrl}\`.`,
667
- actionSummary: `Wait for navigation to complete at ${afterUrl}`,
668
- semanticConfidence: 'high',
669
- descriptionLoading: false,
670
- descriptionSource: 'fallback',
1155
+ semantic: buildReadyRecorderSemantic('heuristic', semanticEvent, afterUrl, 'high'),
671
1156
  timestamp,
672
1157
  hashId: `studio-preview-navigation-${timestamp}-${Math.random().toString(36).slice(2, 8)}`
673
1158
  };
@@ -675,6 +1160,9 @@ class PlaygroundServer {
675
1160
  buildStudioPreviewInitialNavigationEvent(pageState, screenshot) {
676
1161
  if (!pageState.url) return null;
677
1162
  const timestamp = Date.now();
1163
+ const semanticEvent = buildRecorderSemanticAction('InitialNavigation', {
1164
+ value: pageState.url
1165
+ }, pageState.url);
678
1166
  return {
679
1167
  source: 'studio-preview',
680
1168
  type: 'navigation',
@@ -689,12 +1177,7 @@ class PlaygroundServer {
689
1177
  value: pageState.url,
690
1178
  screenshotBefore: screenshot,
691
1179
  screenshotAfter: screenshot,
692
- elementDescription: pageState.url,
693
- replayInstruction: `Navigate to \`${pageState.url}\`.`,
694
- actionSummary: `Navigate to ${pageState.url}`,
695
- semanticConfidence: 'high',
696
- descriptionLoading: false,
697
- descriptionSource: 'fallback',
1180
+ semantic: buildReadyRecorderSemantic('heuristic', semanticEvent, pageState.url, 'high'),
698
1181
  timestamp,
699
1182
  hashId: `studio-preview-initial-navigation-${timestamp}-${Math.random().toString(36).slice(2, 8)}`
700
1183
  };
@@ -1036,7 +1519,7 @@ class PlaygroundServer {
1036
1519
  error: error instanceof Error ? error.message : 'No active session'
1037
1520
  });
1038
1521
  }
1039
- const { type, prompt, params, requestId, deepLocate, deepThink, screenshotIncluded, domIncluded, deviceOptions } = req.body;
1522
+ const { type, prompt, params, requestId, deepLocate, deepThink, screenshotIncluded, domIncluded, deviceOptions, reportDisplay } = req.body;
1040
1523
  if (!type) return res.status(400).json({
1041
1524
  error: 'type is required'
1042
1525
  });
@@ -1085,6 +1568,7 @@ class PlaygroundServer {
1085
1568
  };
1086
1569
  const startTime = Date.now();
1087
1570
  try {
1571
+ agent.resetDump?.();
1088
1572
  await this._activeConnection.executionHooks?.beforeExecute?.();
1089
1573
  const actionSpace = agent.interface.actionSpace();
1090
1574
  const value = {
@@ -1097,7 +1581,8 @@ class PlaygroundServer {
1097
1581
  deepThink,
1098
1582
  screenshotIncluded,
1099
1583
  domIncluded,
1100
- deviceOptions
1584
+ deviceOptions,
1585
+ reportDisplay
1101
1586
  });
1102
1587
  } catch (error) {
1103
1588
  response.error = formatErrorMessage(error);
@@ -1114,7 +1599,7 @@ class PlaygroundServer {
1114
1599
  });
1115
1600
  if (dumpString) {
1116
1601
  const groupedDump = ReportActionDump.fromSerializedString(dumpString);
1117
- response.dump = groupedDump.executions?.[0] || null;
1602
+ response.dump = groupedDump;
1118
1603
  } else response.dump = null;
1119
1604
  response.reportHTML = agent.reportHTMLString({
1120
1605
  inlineScreenshots: true
@@ -1154,7 +1639,7 @@ class PlaygroundServer {
1154
1639
  });
1155
1640
  if (dumpString) {
1156
1641
  const groupedDump = ReportActionDump.fromSerializedString(dumpString);
1157
- dump = groupedDump.executions?.[0] || null;
1642
+ dump = groupedDump;
1158
1643
  }
1159
1644
  reportHTML = agent.reportHTMLString?.({
1160
1645
  inlineScreenshots: true
@@ -1225,6 +1710,7 @@ class PlaygroundServer {
1225
1710
  res.json(await this.getRecorderCapabilities());
1226
1711
  });
1227
1712
  this._app.post('/recorder/stop', async (_req, res)=>{
1713
+ await this.waitForRecorderIdle();
1228
1714
  this._recorderSessionId = null;
1229
1715
  this._studioPreviewRecorderLastScreenshot = void 0;
1230
1716
  this._studioPreviewRecorderLastPageState = void 0;
@@ -1240,6 +1726,38 @@ class PlaygroundServer {
1240
1726
  nextIndex: this._recorderEvents.length
1241
1727
  });
1242
1728
  });
1729
+ this._app.post('/recorder/describe-event', async (req, res)=>{
1730
+ const event = req.body?.event;
1731
+ if (!event || 'object' != typeof event) return res.status(400).json({
1732
+ ok: false,
1733
+ error: 'event is required'
1734
+ });
1735
+ try {
1736
+ const agent = this.getActiveAgentOrThrow();
1737
+ const { event: describedEvent, trace } = await this.enrichStudioPreviewRecorderEventWithAiDescribe(event, agent);
1738
+ res.json({
1739
+ ok: true,
1740
+ event: describedEvent,
1741
+ trace
1742
+ });
1743
+ } catch (error) {
1744
+ const startedAt = new Date();
1745
+ const traceBase = createRecorderAiDescribeTraceBase(event);
1746
+ const trace = {
1747
+ ...traceBase,
1748
+ status: 'failed',
1749
+ startedAt: startedAt.toISOString(),
1750
+ durationMs: 0,
1751
+ error: error instanceof Error ? error.message : String(error)
1752
+ };
1753
+ debugInteract('recorder aiDescribe trace:', trace);
1754
+ res.status(500).json({
1755
+ ok: false,
1756
+ error: error instanceof Error ? error.message : String(error),
1757
+ trace
1758
+ });
1759
+ }
1760
+ });
1243
1761
  this._app.get('/screenshot', async (_req, res)=>{
1244
1762
  try {
1245
1763
  let agent = this.getActiveAgentOrThrow();
@@ -1340,7 +1858,7 @@ class PlaygroundServer {
1340
1858
  recorderActive: Boolean(this._recorderSessionId),
1341
1859
  hasInputPrimitives: Boolean(agent.interface.inputPrimitives)
1342
1860
  });
1343
- const recorderSnapshotBefore = await this.captureRecorderSnapshotBeforeInteract();
1861
+ const recorderSnapshotBefore = this.captureCachedRecorderSnapshotBeforeInteract();
1344
1862
  const inputPrimitives = agent.interface.inputPrimitives;
1345
1863
  if (inputPrimitives) {
1346
1864
  await dispatchPointer(inputPrimitives, req.body ?? {}, ()=>agent.interface.size());
@@ -1348,12 +1866,12 @@ class PlaygroundServer {
1348
1866
  payload: summarizeInteractPayload(req.body ?? {}),
1349
1867
  elapsedMs: Date.now() - interactStartedAt
1350
1868
  });
1351
- await this.storeStudioPreviewRecorderEvent(req.body ?? {}, recorderSnapshotBefore);
1869
+ res.json({});
1870
+ this.queueStudioPreviewRecorderEvent(req.body ?? {}, recorderSnapshotBefore, agent);
1352
1871
  debugInteract('manual interact completed %o', {
1353
1872
  payload: summarizeInteractPayload(req.body ?? {}),
1354
1873
  elapsedMs: Date.now() - interactStartedAt
1355
1874
  });
1356
- res.json({});
1357
1875
  return;
1358
1876
  }
1359
1877
  if (!this.findInteractAction(agent, actionType) && !this.canRunBrowserChromeInteractAction(agent, actionType)) return res.status(404).json({
@@ -1365,12 +1883,12 @@ class PlaygroundServer {
1365
1883
  payload: summarizeInteractPayload(req.body ?? {}),
1366
1884
  elapsedMs: Date.now() - interactStartedAt
1367
1885
  });
1368
- await this.storeStudioPreviewRecorderEvent(req.body ?? {}, recorderSnapshotBefore);
1886
+ res.json({});
1887
+ this.queueStudioPreviewRecorderEvent(req.body ?? {}, recorderSnapshotBefore, agent);
1369
1888
  debugInteract('manual interact completed %o', {
1370
1889
  payload: summarizeInteractPayload(req.body ?? {}),
1371
1890
  elapsedMs: Date.now() - interactStartedAt
1372
1891
  });
1373
- res.json({});
1374
1892
  } catch (error) {
1375
1893
  if (error instanceof PointerInputError) return res.status(error.statusCode).json({
1376
1894
  error: error.message
@@ -1559,6 +2077,9 @@ class PlaygroundServer {
1559
2077
  _define_property(this, "_baseSidecars", void 0);
1560
2078
  _define_property(this, "_recorderSessionId", null);
1561
2079
  _define_property(this, "_recorderEvents", []);
2080
+ _define_property(this, "_recorderEventQueue", Promise.resolve());
2081
+ _define_property(this, "_recorderPendingCaptures", 0);
2082
+ _define_property(this, "_studioPreviewRecorderLastTargetPoint", void 0);
1562
2083
  _define_property(this, "_studioPreviewRecorderLastScreenshot", void 0);
1563
2084
  _define_property(this, "_studioPreviewRecorderLastPageState", void 0);
1564
2085
  _define_property(this, "_activeConnection", {