@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
@@ -43,16 +43,19 @@ __webpack_require__.d(__webpack_exports__, {
43
43
  InteractParamsValidationError: ()=>InteractParamsValidationError,
44
44
  buildInteractParams: ()=>buildInteractParams
45
45
  });
46
+ const external_node_crypto_namespaceObject = require("node:crypto");
46
47
  const external_node_fs_namespaceObject = require("node:fs");
47
48
  const external_node_path_namespaceObject = require("node:path");
48
49
  const external_node_url_namespaceObject = require("node:url");
49
50
  const core_namespaceObject = require("@midscene/core");
50
51
  const utils_namespaceObject = require("@midscene/core/utils");
52
+ const common_namespaceObject = require("@midscene/shared/common");
51
53
  const constants_namespaceObject = require("@midscene/shared/constants");
52
54
  const env_namespaceObject = require("@midscene/shared/env");
53
55
  const extractor_namespaceObject = require("@midscene/shared/extractor");
54
56
  const img_namespaceObject = require("@midscene/shared/img");
55
57
  const logger_namespaceObject = require("@midscene/shared/logger");
58
+ const recorder_namespaceObject = require("@midscene/shared/recorder");
56
59
  const shared_utils_namespaceObject = require("@midscene/shared/utils");
57
60
  const external_express_namespaceObject = require("express");
58
61
  var external_express_default = /*#__PURE__*/ __webpack_require__.n(external_express_namespaceObject);
@@ -73,6 +76,216 @@ function _define_property(obj, key, value) {
73
76
  }
74
77
  const defaultPort = constants_namespaceObject.PLAYGROUND_SERVER_PORT;
75
78
  const RECORDER_CAPTURE_AFTER_INTERACT_DELAY_MS = 250;
79
+ const RECORDER_AI_DESCRIBE_AFTER_INTERACT_TIMEOUT_MS = 30000;
80
+ const RECORDER_AI_DESCRIBE_SCREENSHOT_DUMP_DIR = 'recorder-ai-describe-screenshots';
81
+ function extractBase64Payload(value) {
82
+ const dataUrlMatch = value.match(/^data:([^;,]+)?(?:;[^,]*)?,([\s\S]*)$/);
83
+ if (dataUrlMatch) return {
84
+ mimeType: dataUrlMatch[1],
85
+ base64: dataUrlMatch[2] || ''
86
+ };
87
+ return {
88
+ base64: value
89
+ };
90
+ }
91
+ function estimateBase64Bytes(value) {
92
+ if (!value) return;
93
+ const { base64 } = extractBase64Payload(value);
94
+ const padding = base64.endsWith('==') ? 2 : base64.endsWith('=') ? 1 : 0;
95
+ return Math.max(0, Math.floor(3 * base64.length / 4) - padding);
96
+ }
97
+ function buildRecorderRawPayloadSummary(rawPayload) {
98
+ if (!rawPayload) return;
99
+ const allowedKeys = [
100
+ 'actionType',
101
+ 'x',
102
+ 'y',
103
+ 'endX',
104
+ 'endY',
105
+ 'duration',
106
+ 'direction',
107
+ 'scrollType',
108
+ 'distance',
109
+ 'keyName',
110
+ 'mode'
111
+ ];
112
+ const summary = {};
113
+ for (const key of allowedKeys)if (void 0 !== rawPayload[key]) summary[key] = rawPayload[key];
114
+ const value = rawPayload.value;
115
+ if ('string' == typeof value) summary.valueLength = value.length;
116
+ return Object.keys(summary).length > 0 ? summary : void 0;
117
+ }
118
+ function buildRecorderEventSummary(event) {
119
+ return {
120
+ hashId: event.hashId,
121
+ mergedHashIds: event.mergedHashIds,
122
+ type: event.type,
123
+ source: event.source,
124
+ actionType: event.actionType,
125
+ timestamp: event.timestamp,
126
+ url: event.url,
127
+ title: event.title,
128
+ valueLength: 'string' == typeof event.value ? event.value.length : void 0,
129
+ rawPayloadSummary: buildRecorderRawPayloadSummary(event.rawPayload),
130
+ elementRect: event.elementRect,
131
+ pageInfo: event.pageInfo
132
+ };
133
+ }
134
+ function shouldVerifyRecorderAiDescribeEvent(event) {
135
+ return 'scroll' !== event.type;
136
+ }
137
+ function formatRecorderAiDescribeScreenshotPathParts() {
138
+ const date = new Date();
139
+ const localDate = Number.isNaN(date.getTime()) ? new Date() : date;
140
+ const pad = (value, length = 2)=>String(value).padStart(length, '0');
141
+ const datePart = [
142
+ localDate.getFullYear(),
143
+ pad(localDate.getMonth() + 1),
144
+ pad(localDate.getDate())
145
+ ].join('-');
146
+ const timePart = [
147
+ pad(localDate.getHours()),
148
+ pad(localDate.getMinutes()),
149
+ pad(localDate.getSeconds()),
150
+ pad(localDate.getMilliseconds(), 3)
151
+ ].join('-');
152
+ const hourPart = timePart.slice(0, 2) || 'unknown-hour';
153
+ return {
154
+ datePart,
155
+ hourPart,
156
+ filePrefix: `${timePart}_${(0, external_node_crypto_namespaceObject.randomUUID)()}`
157
+ };
158
+ }
159
+ function assertPathInsideDirectory(rootDir, targetPath) {
160
+ const resolvedRoot = (0, external_node_path_namespaceObject.resolve)(rootDir);
161
+ const resolvedTarget = (0, external_node_path_namespaceObject.resolve)(targetPath);
162
+ if (resolvedTarget !== resolvedRoot && !resolvedTarget.startsWith(`${resolvedRoot}${external_node_path_namespaceObject.sep}`)) throw new Error(`Refusing to write recorder dump outside ${resolvedRoot}`);
163
+ return resolvedTarget;
164
+ }
165
+ function writeRecorderAiDescribeScreenshot(imageBase64, suffix) {
166
+ const { base64, mimeType } = extractBase64Payload(imageBase64);
167
+ const bytes = Buffer.from(base64, 'base64');
168
+ const sha256 = (0, external_node_crypto_namespaceObject.createHash)('sha256').update(bytes).digest('hex');
169
+ const extension = mimeType?.includes('jpeg') ? 'jpg' : 'png';
170
+ const pathParts = formatRecorderAiDescribeScreenshotPathParts();
171
+ const dumpRoot = (0, external_node_path_namespaceObject.resolve)((0, common_namespaceObject.getMidsceneRunSubDir)('dump'), RECORDER_AI_DESCRIBE_SCREENSHOT_DUMP_DIR);
172
+ const dumpDir = assertPathInsideDirectory(dumpRoot, (0, external_node_path_namespaceObject.join)(dumpRoot, pathParts.datePart, pathParts.hourPart));
173
+ (0, external_node_fs_namespaceObject.mkdirSync)(dumpDir, {
174
+ recursive: true
175
+ });
176
+ const safeSuffix = 'annotated' === suffix ? 'annotated' : 'raw';
177
+ const fileName = `${pathParts.filePrefix}_${safeSuffix}.${extension}`;
178
+ const filePath = assertPathInsideDirectory(dumpRoot, (0, external_node_path_namespaceObject.join)(dumpDir, fileName));
179
+ if (!(0, external_node_fs_namespaceObject.existsSync)(filePath)) (0, external_node_fs_namespaceObject.writeFileSync)(filePath, bytes);
180
+ return {
181
+ path: filePath,
182
+ sha256,
183
+ bytes: bytes.byteLength,
184
+ mimeType
185
+ };
186
+ }
187
+ function calculateRecorderScreenshotAnnotation(event, imageSize, verifyResult) {
188
+ const pageWidth = event.pageInfo?.width;
189
+ const pageHeight = event.pageInfo?.height;
190
+ const scaleX = pageWidth ? imageSize.width / pageWidth : void 0;
191
+ const scaleY = pageHeight ? imageSize.height / pageHeight : void 0;
192
+ const mapX = (value)=>scaleX ? value * scaleX : value;
193
+ const mapY = (value)=>scaleY ? value * scaleY : value;
194
+ const logicalPoint = 'number' == typeof event.elementRect?.x && 'number' == typeof event.elementRect?.y ? [
195
+ event.elementRect.x,
196
+ event.elementRect.y
197
+ ] : void 0;
198
+ const locateRect = verifyResult?.rect;
199
+ 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 ? {
200
+ left: mapX(event.elementRect.left),
201
+ top: mapY(event.elementRect.top),
202
+ width: mapX(event.elementRect.width),
203
+ height: mapY(event.elementRect.height)
204
+ } : void 0;
205
+ if (!logicalPoint && !sourceTargetRect && !locateRect) return;
206
+ const screenshotPoint = logicalPoint && scaleX && scaleY ? [
207
+ logicalPoint[0] * scaleX,
208
+ logicalPoint[1] * scaleY
209
+ ] : void 0;
210
+ const pointTargetRect = screenshotPoint && !sourceTargetRect ? {
211
+ left: Math.max(0, screenshotPoint[0] - 6),
212
+ top: Math.max(0, screenshotPoint[1] - 6),
213
+ width: 12,
214
+ height: 12
215
+ } : void 0;
216
+ const center = verifyResult?.center || (locateRect ? [
217
+ locateRect.left + locateRect.width / 2,
218
+ locateRect.top + locateRect.height / 2
219
+ ] : void 0);
220
+ const centerDelta = screenshotPoint && center ? {
221
+ x: center[0] - screenshotPoint[0],
222
+ y: center[1] - screenshotPoint[1],
223
+ distance: Math.hypot(center[0] - screenshotPoint[0], center[1] - screenshotPoint[1])
224
+ } : void 0;
225
+ const distanceOutsideRect = screenshotPoint && locateRect ? (()=>{
226
+ const right = locateRect.left + locateRect.width;
227
+ const bottom = locateRect.top + locateRect.height;
228
+ const x = screenshotPoint[0] < locateRect.left ? locateRect.left - screenshotPoint[0] : screenshotPoint[0] > right ? screenshotPoint[0] - right : 0;
229
+ const y = screenshotPoint[1] < locateRect.top ? locateRect.top - screenshotPoint[1] : screenshotPoint[1] > bottom ? screenshotPoint[1] - bottom : 0;
230
+ return {
231
+ x,
232
+ y,
233
+ distance: Math.hypot(x, y)
234
+ };
235
+ })() : void 0;
236
+ return {
237
+ inputPoint: screenshotPoint ? {
238
+ logical: logicalPoint,
239
+ screenshot: screenshotPoint
240
+ } : void 0,
241
+ sourceTargetRect: sourceTargetRect || pointTargetRect,
242
+ locateRect,
243
+ centerDelta,
244
+ distanceOutsideRect
245
+ };
246
+ }
247
+ async function persistRecorderAiDescribeScreenshot(event, eventScreenshot, verifyResult) {
248
+ if (!eventScreenshot) return;
249
+ const screenshotRef = writeRecorderAiDescribeScreenshot(eventScreenshot, 'raw');
250
+ let annotatedScreenshotRef;
251
+ let screenshotAnnotation;
252
+ let annotatedScreenshotPersistError;
253
+ try {
254
+ const imageSize = await (0, img_namespaceObject.imageInfoOfBase64)(eventScreenshot);
255
+ screenshotAnnotation = calculateRecorderScreenshotAnnotation(event, imageSize, verifyResult);
256
+ const annotatedRects = [
257
+ screenshotAnnotation?.sourceTargetRect,
258
+ screenshotAnnotation?.locateRect
259
+ ].filter((rect)=>Boolean(rect));
260
+ if (annotatedRects.length > 0) {
261
+ const annotatedScreenshot = await (0, img_namespaceObject.annotateRects)(eventScreenshot, annotatedRects);
262
+ annotatedScreenshotRef = writeRecorderAiDescribeScreenshot(annotatedScreenshot, 'annotated');
263
+ }
264
+ } catch (error) {
265
+ annotatedScreenshotPersistError = error instanceof Error ? error.message : String(error);
266
+ }
267
+ return {
268
+ screenshotRef,
269
+ annotatedScreenshotRef,
270
+ screenshotAnnotation,
271
+ annotatedScreenshotPersistError
272
+ };
273
+ }
274
+ function createRecorderAiDescribeTraceBase(event, eventScreenshot) {
275
+ return {
276
+ traceId: `${event.hashId || 'recorder-event'}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
277
+ eventHashId: event.hashId,
278
+ eventType: event.type,
279
+ actionType: event.actionType,
280
+ eventSummary: buildRecorderEventSummary(event),
281
+ point: 'number' == typeof event.elementRect?.x && 'number' == typeof event.elementRect?.y ? [
282
+ event.elementRect.x,
283
+ event.elementRect.y
284
+ ] : void 0,
285
+ pageInfo: event.pageInfo,
286
+ screenshotBytes: estimateBase64Bytes(eventScreenshot)
287
+ };
288
+ }
76
289
  function serializeAiConfigSignature(aiConfig) {
77
290
  return JSON.stringify(Object.entries(aiConfig).sort(([leftKey], [rightKey])=>leftKey.localeCompare(rightKey)));
78
291
  }
@@ -236,6 +449,74 @@ function buildInteractParams(actionType, body) {
236
449
  const { actionType: _omit, ...passthrough } = body;
237
450
  return passthrough;
238
451
  }
452
+ function getRecorderSemanticActionType(actionType) {
453
+ switch(actionType){
454
+ case 'Tap':
455
+ case 'DoubleClick':
456
+ case 'LongPress':
457
+ case 'RightClick':
458
+ return 'click';
459
+ case 'Swipe':
460
+ case 'DragAndDrop':
461
+ return 'drag';
462
+ case 'Input':
463
+ return 'input';
464
+ case 'KeyboardPress':
465
+ return 'keydown';
466
+ case 'Scroll':
467
+ return 'scroll';
468
+ case 'GoBack':
469
+ case 'GoForward':
470
+ case 'Reload':
471
+ case 'Stop':
472
+ case 'InitialNavigation':
473
+ case 'NavigationChanged':
474
+ return 'navigation';
475
+ default:
476
+ return 'click';
477
+ }
478
+ }
479
+ function buildRecorderSemanticAction(actionType, payload, url) {
480
+ return {
481
+ type: getRecorderSemanticActionType(actionType),
482
+ actionType,
483
+ value: 'string' == typeof payload.value ? payload.value : 'string' == typeof payload.keyName ? payload.keyName : void 0,
484
+ url
485
+ };
486
+ }
487
+ function buildReadyRecorderSemantic(source, event, elementDescription, confidence, extra) {
488
+ return {
489
+ source,
490
+ status: 'ready',
491
+ elementDescription,
492
+ replayInstruction: (0, recorder_namespaceObject.buildMidsceneRecorderReplayInstruction)(event, elementDescription),
493
+ actionSummary: (0, recorder_namespaceObject.buildMidsceneRecorderActionSummary)(event, elementDescription),
494
+ confidence,
495
+ ...extra
496
+ };
497
+ }
498
+ function buildFailedAiDescribeRecorderSemantic(error, extra) {
499
+ return {
500
+ source: 'aiDescribe',
501
+ status: 'failed',
502
+ error: error instanceof Error ? error.message : String(error),
503
+ ...extra
504
+ };
505
+ }
506
+ function withTimeout(promise, timeoutMs, message) {
507
+ let timeout;
508
+ const timeoutPromise = new Promise((_resolve, reject)=>{
509
+ timeout = setTimeout(()=>{
510
+ reject(new Error(message));
511
+ }, timeoutMs);
512
+ });
513
+ return Promise.race([
514
+ promise,
515
+ timeoutPromise
516
+ ]).finally(()=>{
517
+ if (timeout) clearTimeout(timeout);
518
+ });
519
+ }
239
520
  function createManualExecutorContext(actionType, param) {
240
521
  const task = {
241
522
  type: 'Action Space',
@@ -439,9 +720,14 @@ class PlaygroundServer {
439
720
  resetRecorderState() {
440
721
  this._recorderSessionId = null;
441
722
  this._recorderEvents = [];
723
+ this._recorderEventQueue = Promise.resolve();
724
+ this._studioPreviewRecorderLastTargetPoint = void 0;
442
725
  this._studioPreviewRecorderLastScreenshot = void 0;
443
726
  this._studioPreviewRecorderLastPageState = void 0;
444
727
  }
728
+ async waitForRecorderIdle() {
729
+ await this._recorderEventQueue;
730
+ }
445
731
  canRecordStudioPreviewInteractions() {
446
732
  const agent = this._activeConnection.agent;
447
733
  if (!agent) return false;
@@ -525,6 +811,13 @@ class PlaygroundServer {
525
811
  pageState: this._studioPreviewRecorderLastPageState || await this.getActiveRecorderPageState()
526
812
  };
527
813
  }
814
+ captureCachedRecorderSnapshotBeforeInteract() {
815
+ if (!this._recorderSessionId || !this._studioPreviewRecorderLastPageState || this._recorderPendingCaptures > 0) return;
816
+ return {
817
+ screenshot: this._mjpegHandler.getLastFrameBase64() || this._studioPreviewRecorderLastScreenshot,
818
+ pageState: this._studioPreviewRecorderLastPageState
819
+ };
820
+ }
528
821
  async startStudioPreviewRecorder(sessionId) {
529
822
  this._recorderSessionId = sessionId;
530
823
  this._studioPreviewRecorderLastScreenshot = await this.takeRecorderScreenshot();
@@ -533,7 +826,7 @@ class PlaygroundServer {
533
826
  const initialNavigationEvent = this.buildStudioPreviewInitialNavigationEvent(initialPageState, this._studioPreviewRecorderLastScreenshot);
534
827
  if (initialNavigationEvent) this._recorderEvents.push(initialNavigationEvent);
535
828
  }
536
- async storeStudioPreviewRecorderEvent(payload, snapshotBefore) {
829
+ async storeStudioPreviewRecorderEvent(payload, snapshotBefore, agent) {
537
830
  if (!this._recorderSessionId) return;
538
831
  const before = snapshotBefore || await this.captureRecorderSnapshotBeforeInteract();
539
832
  const screenshotBefore = before?.screenshot;
@@ -558,19 +851,26 @@ class PlaygroundServer {
558
851
  this._studioPreviewRecorderLastPageState = pageStateAfter;
559
852
  return;
560
853
  }
561
- this._recorderEvents.push(event);
562
854
  const navigationEvent = this.buildStudioPreviewNavigationChangeEvent(payload, before?.pageState, pageStateAfter, screenshotAfter);
563
- if (navigationEvent) this._recorderEvents.push(navigationEvent);
564
855
  this._studioPreviewRecorderLastScreenshot = screenshotAfter;
565
856
  this._studioPreviewRecorderLastPageState = pageStateAfter;
857
+ this.queueStudioPreviewRecorderEventAppend(event, navigationEvent);
566
858
  }
567
859
  async buildStudioPreviewRecorderEvent(payload, pageState, screenshotBefore, screenshotAfter) {
568
860
  const actionType = 'string' == typeof payload.actionType ? payload.actionType : void 0;
569
861
  if (!actionType) return null;
570
862
  const { pageInfo, url, title } = pageState;
571
863
  const timestamp = Date.now();
572
- const x = 'number' == typeof payload.x ? payload.x : void 0;
573
- const y = 'number' == typeof payload.y ? payload.y : void 0;
864
+ const payloadX = 'number' == typeof payload.x ? payload.x : void 0;
865
+ const payloadY = 'number' == typeof payload.y ? payload.y : void 0;
866
+ const canReuseLastTargetPoint = 'Input' === actionType || 'KeyboardPress' === actionType;
867
+ const inheritedTargetPoint = canReuseLastTargetPoint && (void 0 === payloadX || void 0 === payloadY) ? this._studioPreviewRecorderLastTargetPoint : void 0;
868
+ const x = payloadX ?? inheritedTargetPoint?.x;
869
+ const y = payloadY ?? inheritedTargetPoint?.y;
870
+ if (void 0 !== payloadX && void 0 !== payloadY) this._studioPreviewRecorderLastTargetPoint = {
871
+ x: payloadX,
872
+ y: payloadY
873
+ };
574
874
  const endX = 'number' == typeof payload.endX ? payload.endX : void 0;
575
875
  const endY = 'number' == typeof payload.endY ? payload.endY : void 0;
576
876
  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;
@@ -588,8 +888,10 @@ class PlaygroundServer {
588
888
  screenshotBefore,
589
889
  screenshotAfter,
590
890
  screenshotWithBox: screenshotWithMarker,
591
- descriptionLoading: true,
592
- descriptionSource: void 0,
891
+ semantic: {
892
+ source: 'aiDescribe',
893
+ status: 'pending'
894
+ },
593
895
  timestamp,
594
896
  hashId: `studio-preview-${actionType}-${timestamp}-${Math.random().toString(36).slice(2, 8)}`
595
897
  };
@@ -667,17 +969,18 @@ class PlaygroundServer {
667
969
  case 'GoForward':
668
970
  case 'Reload':
669
971
  case 'Stop':
670
- return {
671
- ...base,
672
- type: 'navigation',
673
- value: actionType,
674
- elementDescription: url || actionType,
675
- replayInstruction: 'Stop' === actionType ? 'Stop loading the current page.' : url ? `Wait for navigation to complete at \`${url}\`.` : `${actionType} in the browser.`,
676
- actionSummary: 'Stop' === actionType ? 'Stop page loading' : url ? `${actionType} to ${url}` : actionType,
677
- semanticConfidence: url ? 'high' : 'medium',
678
- descriptionLoading: false,
679
- descriptionSource: 'fallback'
680
- };
972
+ {
973
+ const elementDescription = url || actionType;
974
+ const semanticEvent = buildRecorderSemanticAction(actionType, {
975
+ value: actionType
976
+ }, url);
977
+ return {
978
+ ...base,
979
+ type: 'navigation',
980
+ value: actionType,
981
+ semantic: buildReadyRecorderSemantic('heuristic', semanticEvent, elementDescription, url ? 'high' : 'medium')
982
+ };
983
+ }
681
984
  default:
682
985
  return {
683
986
  ...base,
@@ -686,6 +989,190 @@ class PlaygroundServer {
686
989
  };
687
990
  }
688
991
  }
992
+ async enrichStudioPreviewRecorderEventWithAiDescribe(event, agent) {
993
+ const startedAt = new Date();
994
+ const startedAtMs = Date.now();
995
+ const eventScreenshot = this.getRecorderAiDescribeScreenshot(event);
996
+ const traceBase = createRecorderAiDescribeTraceBase(event, eventScreenshot);
997
+ const finishTrace = async (status, extra = {})=>{
998
+ let screenshotRef;
999
+ let annotatedScreenshotRef;
1000
+ let screenshotAnnotation;
1001
+ let screenshotPersistError;
1002
+ let annotatedScreenshotPersistError;
1003
+ if ('failed' === status) try {
1004
+ const screenshotDump = await persistRecorderAiDescribeScreenshot(event, eventScreenshot, extra.verifyResult);
1005
+ screenshotRef = screenshotDump?.screenshotRef;
1006
+ annotatedScreenshotRef = screenshotDump?.annotatedScreenshotRef;
1007
+ screenshotAnnotation = screenshotDump?.screenshotAnnotation;
1008
+ annotatedScreenshotPersistError = screenshotDump?.annotatedScreenshotPersistError;
1009
+ } catch (error) {
1010
+ screenshotPersistError = error instanceof Error ? error.message : String(error);
1011
+ }
1012
+ return {
1013
+ ...traceBase,
1014
+ ...extra,
1015
+ screenshotRef,
1016
+ annotatedScreenshotRef,
1017
+ screenshotAnnotation,
1018
+ screenshotPersistError,
1019
+ annotatedScreenshotPersistError,
1020
+ status,
1021
+ startedAt: startedAt.toISOString(),
1022
+ durationMs: Date.now() - startedAtMs
1023
+ };
1024
+ };
1025
+ if ('navigation' === event.type || 'setViewport' === event.type) {
1026
+ const error = 'aiDescribe skipped because the event type does not target a UI element.';
1027
+ const trace = await finishTrace('failed', {
1028
+ error
1029
+ });
1030
+ debugInteract('recorder aiDescribe trace:', trace);
1031
+ return {
1032
+ event: {
1033
+ ...event,
1034
+ semantic: buildFailedAiDescribeRecorderSemantic(error)
1035
+ },
1036
+ trace
1037
+ };
1038
+ }
1039
+ if ('function' != typeof agent?.describeElementAtPoint) {
1040
+ const error = 'Active agent does not support describeElementAtPoint.';
1041
+ const trace = await finishTrace('failed', {
1042
+ error
1043
+ });
1044
+ debugInteract('recorder aiDescribe trace:', trace);
1045
+ return {
1046
+ event: {
1047
+ ...event,
1048
+ semantic: buildFailedAiDescribeRecorderSemantic(error)
1049
+ },
1050
+ trace
1051
+ };
1052
+ }
1053
+ const x = event.elementRect?.x;
1054
+ const y = event.elementRect?.y;
1055
+ let modelCallDurationMs;
1056
+ let modelCallStartedAt;
1057
+ let elementDescription;
1058
+ let deepLocate;
1059
+ let verifyResult;
1060
+ try {
1061
+ if ('number' != typeof x || 'number' != typeof y) throw new Error('Skipped aiDescribe because the recorder event has no stable point target.');
1062
+ if (!eventScreenshot) throw new Error('Skipped aiDescribe because the recorder event has no screenshot.');
1063
+ if (!event.pageInfo?.width || !event.pageInfo?.height) throw new Error('Skipped aiDescribe because the recorder event has no pageInfo for coordinate mapping.');
1064
+ const verifyPrompt = shouldVerifyRecorderAiDescribeEvent(event);
1065
+ modelCallStartedAt = Date.now();
1066
+ const describeResult = await withTimeout(agent.describeElementAtPoint([
1067
+ x,
1068
+ y
1069
+ ], {
1070
+ verifyPrompt,
1071
+ screenshotBase64: eventScreenshot,
1072
+ coordinateSpace: 'logical',
1073
+ logicalSize: event.pageInfo,
1074
+ onProgress: (progress)=>{
1075
+ elementDescription = progress.prompt?.trim() || elementDescription;
1076
+ deepLocate = progress.deepLocate;
1077
+ verifyResult = verifyPrompt ? progress.verifyResult : void 0;
1078
+ }
1079
+ }), RECORDER_AI_DESCRIBE_AFTER_INTERACT_TIMEOUT_MS, 'Timed out while analyzing recorder event with aiDescribe.');
1080
+ modelCallDurationMs = Date.now() - modelCallStartedAt;
1081
+ elementDescription = describeResult.prompt?.trim();
1082
+ deepLocate = describeResult.deepLocate;
1083
+ verifyResult = verifyPrompt ? describeResult.verifyResult : void 0;
1084
+ if (!elementDescription) throw new Error("aiDescribe returned an empty element description.");
1085
+ if (verifyResult && false === verifyResult.pass) throw new Error('aiDescribe verification failed.');
1086
+ const semanticAction = buildRecorderSemanticAction(event.actionType || event.type, event.rawPayload || {}, event.url);
1087
+ const trace = await finishTrace('ready', {
1088
+ modelCallDurationMs,
1089
+ elementDescription,
1090
+ verifyPassed: verifyResult?.pass,
1091
+ centerDistance: verifyResult?.centerDistance,
1092
+ verifyResult
1093
+ });
1094
+ debugInteract('recorder aiDescribe trace:', trace);
1095
+ return {
1096
+ event: {
1097
+ ...event,
1098
+ semantic: buildReadyRecorderSemantic('aiDescribe', semanticAction, elementDescription, verifyResult?.pass ? 'high' : 'medium', {
1099
+ aiDescribe: {
1100
+ verifyPrompt,
1101
+ verifyPassed: verifyResult?.pass,
1102
+ deepLocate,
1103
+ centerDistance: verifyResult?.centerDistance,
1104
+ expectedCenter: [
1105
+ x,
1106
+ y
1107
+ ],
1108
+ actualCenter: verifyResult?.center
1109
+ }
1110
+ })
1111
+ },
1112
+ trace
1113
+ };
1114
+ } catch (error) {
1115
+ const reportedError = verifyResult?.pass === false ? new Error('aiDescribe verification failed.') : error;
1116
+ if (void 0 === modelCallDurationMs && modelCallStartedAt) modelCallDurationMs = Date.now() - modelCallStartedAt;
1117
+ debugInteract('canonical recorder aiDescribe failed:', reportedError);
1118
+ const trace = await finishTrace('failed', {
1119
+ error: reportedError instanceof Error ? reportedError.message : String(reportedError),
1120
+ modelCallDurationMs,
1121
+ elementDescription,
1122
+ verifyPassed: verifyResult?.pass,
1123
+ centerDistance: verifyResult?.centerDistance,
1124
+ verifyResult
1125
+ });
1126
+ debugInteract('recorder aiDescribe trace:', trace);
1127
+ const aiDescribeDetails = verifyResult || trace.annotatedScreenshotRef ? {
1128
+ aiDescribe: {
1129
+ verifyPrompt: true,
1130
+ verifyPassed: verifyResult?.pass,
1131
+ deepLocate,
1132
+ centerDistance: verifyResult?.centerDistance,
1133
+ expectedCenter: 'number' == typeof x && 'number' == typeof y ? [
1134
+ x,
1135
+ y
1136
+ ] : void 0,
1137
+ actualCenter: verifyResult?.center,
1138
+ annotatedScreenshotPath: trace.annotatedScreenshotRef?.path
1139
+ }
1140
+ } : void 0;
1141
+ return {
1142
+ event: {
1143
+ ...event,
1144
+ semantic: buildFailedAiDescribeRecorderSemantic(reportedError, aiDescribeDetails)
1145
+ },
1146
+ trace
1147
+ };
1148
+ }
1149
+ }
1150
+ getRecorderAiDescribeScreenshot(event) {
1151
+ if ('click' === event.type || 'input' === event.type || 'keydown' === event.type || 'drag' === event.type) return event.screenshotBefore || event.screenshotAfter;
1152
+ return event.screenshotAfter || event.screenshotBefore;
1153
+ }
1154
+ queueStudioPreviewRecorderEventAppend(event, navigationEvent) {
1155
+ const sessionId = this._recorderSessionId;
1156
+ if (!sessionId) return;
1157
+ this._recorderEvents.push(event);
1158
+ if (navigationEvent) this._recorderEvents.push(navigationEvent);
1159
+ }
1160
+ queueStudioPreviewRecorderEvent(payload, snapshotBefore, agent) {
1161
+ const sessionId = this._recorderSessionId;
1162
+ if (!sessionId) return;
1163
+ this._recorderPendingCaptures++;
1164
+ const queuedTask = this._recorderEventQueue.catch(()=>void 0).then(async ()=>{
1165
+ try {
1166
+ if (!this._recorderSessionId || this._recorderSessionId !== sessionId) return;
1167
+ await this.storeStudioPreviewRecorderEvent(payload, snapshotBefore, agent);
1168
+ } finally{
1169
+ this._recorderPendingCaptures = Math.max(0, this._recorderPendingCaptures - 1);
1170
+ }
1171
+ });
1172
+ this._recorderEventQueue = queuedTask.catch((error)=>{
1173
+ debugInteract('async recorder event capture failed:', error);
1174
+ });
1175
+ }
689
1176
  buildStudioPreviewNavigationChangeEvent(payload, pageStateBefore, pageStateAfter, screenshotAfter) {
690
1177
  const actionType = 'string' == typeof payload.actionType ? payload.actionType : void 0;
691
1178
  if (!actionType || BROWSER_CHROME_NAVIGATION_ACTIONS.has(actionType)) return null;
@@ -693,6 +1180,9 @@ class PlaygroundServer {
693
1180
  const afterUrl = pageStateAfter.url;
694
1181
  if (!afterUrl || beforeUrl === afterUrl) return null;
695
1182
  const timestamp = Date.now();
1183
+ const semanticEvent = buildRecorderSemanticAction('NavigationChanged', {
1184
+ value: afterUrl
1185
+ }, afterUrl);
696
1186
  return {
697
1187
  source: 'studio-preview',
698
1188
  type: 'navigation',
@@ -708,12 +1198,7 @@ class PlaygroundServer {
708
1198
  value: afterUrl,
709
1199
  screenshotBefore: screenshotAfter,
710
1200
  screenshotAfter,
711
- elementDescription: afterUrl,
712
- replayInstruction: `Wait for navigation to complete at \`${afterUrl}\`.`,
713
- actionSummary: `Wait for navigation to complete at ${afterUrl}`,
714
- semanticConfidence: 'high',
715
- descriptionLoading: false,
716
- descriptionSource: 'fallback',
1201
+ semantic: buildReadyRecorderSemantic('heuristic', semanticEvent, afterUrl, 'high'),
717
1202
  timestamp,
718
1203
  hashId: `studio-preview-navigation-${timestamp}-${Math.random().toString(36).slice(2, 8)}`
719
1204
  };
@@ -721,6 +1206,9 @@ class PlaygroundServer {
721
1206
  buildStudioPreviewInitialNavigationEvent(pageState, screenshot) {
722
1207
  if (!pageState.url) return null;
723
1208
  const timestamp = Date.now();
1209
+ const semanticEvent = buildRecorderSemanticAction('InitialNavigation', {
1210
+ value: pageState.url
1211
+ }, pageState.url);
724
1212
  return {
725
1213
  source: 'studio-preview',
726
1214
  type: 'navigation',
@@ -735,12 +1223,7 @@ class PlaygroundServer {
735
1223
  value: pageState.url,
736
1224
  screenshotBefore: screenshot,
737
1225
  screenshotAfter: screenshot,
738
- elementDescription: pageState.url,
739
- replayInstruction: `Navigate to \`${pageState.url}\`.`,
740
- actionSummary: `Navigate to ${pageState.url}`,
741
- semanticConfidence: 'high',
742
- descriptionLoading: false,
743
- descriptionSource: 'fallback',
1226
+ semantic: buildReadyRecorderSemantic('heuristic', semanticEvent, pageState.url, 'high'),
744
1227
  timestamp,
745
1228
  hashId: `studio-preview-initial-navigation-${timestamp}-${Math.random().toString(36).slice(2, 8)}`
746
1229
  };
@@ -1082,7 +1565,7 @@ class PlaygroundServer {
1082
1565
  error: error instanceof Error ? error.message : 'No active session'
1083
1566
  });
1084
1567
  }
1085
- const { type, prompt, params, requestId, deepLocate, deepThink, screenshotIncluded, domIncluded, deviceOptions } = req.body;
1568
+ const { type, prompt, params, requestId, deepLocate, deepThink, screenshotIncluded, domIncluded, deviceOptions, reportDisplay } = req.body;
1086
1569
  if (!type) return res.status(400).json({
1087
1570
  error: 'type is required'
1088
1571
  });
@@ -1131,6 +1614,7 @@ class PlaygroundServer {
1131
1614
  };
1132
1615
  const startTime = Date.now();
1133
1616
  try {
1617
+ agent.resetDump?.();
1134
1618
  await this._activeConnection.executionHooks?.beforeExecute?.();
1135
1619
  const actionSpace = agent.interface.actionSpace();
1136
1620
  const value = {
@@ -1143,7 +1627,8 @@ class PlaygroundServer {
1143
1627
  deepThink,
1144
1628
  screenshotIncluded,
1145
1629
  domIncluded,
1146
- deviceOptions
1630
+ deviceOptions,
1631
+ reportDisplay
1147
1632
  });
1148
1633
  } catch (error) {
1149
1634
  response.error = (0, external_common_js_namespaceObject.formatErrorMessage)(error);
@@ -1160,7 +1645,7 @@ class PlaygroundServer {
1160
1645
  });
1161
1646
  if (dumpString) {
1162
1647
  const groupedDump = core_namespaceObject.ReportActionDump.fromSerializedString(dumpString);
1163
- response.dump = groupedDump.executions?.[0] || null;
1648
+ response.dump = groupedDump;
1164
1649
  } else response.dump = null;
1165
1650
  response.reportHTML = agent.reportHTMLString({
1166
1651
  inlineScreenshots: true
@@ -1200,7 +1685,7 @@ class PlaygroundServer {
1200
1685
  });
1201
1686
  if (dumpString) {
1202
1687
  const groupedDump = core_namespaceObject.ReportActionDump.fromSerializedString(dumpString);
1203
- dump = groupedDump.executions?.[0] || null;
1688
+ dump = groupedDump;
1204
1689
  }
1205
1690
  reportHTML = agent.reportHTMLString?.({
1206
1691
  inlineScreenshots: true
@@ -1271,6 +1756,7 @@ class PlaygroundServer {
1271
1756
  res.json(await this.getRecorderCapabilities());
1272
1757
  });
1273
1758
  this._app.post('/recorder/stop', async (_req, res)=>{
1759
+ await this.waitForRecorderIdle();
1274
1760
  this._recorderSessionId = null;
1275
1761
  this._studioPreviewRecorderLastScreenshot = void 0;
1276
1762
  this._studioPreviewRecorderLastPageState = void 0;
@@ -1286,6 +1772,38 @@ class PlaygroundServer {
1286
1772
  nextIndex: this._recorderEvents.length
1287
1773
  });
1288
1774
  });
1775
+ this._app.post('/recorder/describe-event', async (req, res)=>{
1776
+ const event = req.body?.event;
1777
+ if (!event || 'object' != typeof event) return res.status(400).json({
1778
+ ok: false,
1779
+ error: 'event is required'
1780
+ });
1781
+ try {
1782
+ const agent = this.getActiveAgentOrThrow();
1783
+ const { event: describedEvent, trace } = await this.enrichStudioPreviewRecorderEventWithAiDescribe(event, agent);
1784
+ res.json({
1785
+ ok: true,
1786
+ event: describedEvent,
1787
+ trace
1788
+ });
1789
+ } catch (error) {
1790
+ const startedAt = new Date();
1791
+ const traceBase = createRecorderAiDescribeTraceBase(event);
1792
+ const trace = {
1793
+ ...traceBase,
1794
+ status: 'failed',
1795
+ startedAt: startedAt.toISOString(),
1796
+ durationMs: 0,
1797
+ error: error instanceof Error ? error.message : String(error)
1798
+ };
1799
+ debugInteract('recorder aiDescribe trace:', trace);
1800
+ res.status(500).json({
1801
+ ok: false,
1802
+ error: error instanceof Error ? error.message : String(error),
1803
+ trace
1804
+ });
1805
+ }
1806
+ });
1289
1807
  this._app.get('/screenshot', async (_req, res)=>{
1290
1808
  try {
1291
1809
  let agent = this.getActiveAgentOrThrow();
@@ -1386,7 +1904,7 @@ class PlaygroundServer {
1386
1904
  recorderActive: Boolean(this._recorderSessionId),
1387
1905
  hasInputPrimitives: Boolean(agent.interface.inputPrimitives)
1388
1906
  });
1389
- const recorderSnapshotBefore = await this.captureRecorderSnapshotBeforeInteract();
1907
+ const recorderSnapshotBefore = this.captureCachedRecorderSnapshotBeforeInteract();
1390
1908
  const inputPrimitives = agent.interface.inputPrimitives;
1391
1909
  if (inputPrimitives) {
1392
1910
  await (0, external_pointer_dispatch_js_namespaceObject.dispatchPointer)(inputPrimitives, req.body ?? {}, ()=>agent.interface.size());
@@ -1394,12 +1912,12 @@ class PlaygroundServer {
1394
1912
  payload: summarizeInteractPayload(req.body ?? {}),
1395
1913
  elapsedMs: Date.now() - interactStartedAt
1396
1914
  });
1397
- await this.storeStudioPreviewRecorderEvent(req.body ?? {}, recorderSnapshotBefore);
1915
+ res.json({});
1916
+ this.queueStudioPreviewRecorderEvent(req.body ?? {}, recorderSnapshotBefore, agent);
1398
1917
  debugInteract('manual interact completed %o', {
1399
1918
  payload: summarizeInteractPayload(req.body ?? {}),
1400
1919
  elapsedMs: Date.now() - interactStartedAt
1401
1920
  });
1402
- res.json({});
1403
1921
  return;
1404
1922
  }
1405
1923
  if (!this.findInteractAction(agent, actionType) && !this.canRunBrowserChromeInteractAction(agent, actionType)) return res.status(404).json({
@@ -1411,12 +1929,12 @@ class PlaygroundServer {
1411
1929
  payload: summarizeInteractPayload(req.body ?? {}),
1412
1930
  elapsedMs: Date.now() - interactStartedAt
1413
1931
  });
1414
- await this.storeStudioPreviewRecorderEvent(req.body ?? {}, recorderSnapshotBefore);
1932
+ res.json({});
1933
+ this.queueStudioPreviewRecorderEvent(req.body ?? {}, recorderSnapshotBefore, agent);
1415
1934
  debugInteract('manual interact completed %o', {
1416
1935
  payload: summarizeInteractPayload(req.body ?? {}),
1417
1936
  elapsedMs: Date.now() - interactStartedAt
1418
1937
  });
1419
- res.json({});
1420
1938
  } catch (error) {
1421
1939
  if (error instanceof external_pointer_dispatch_js_namespaceObject.PointerInputError) return res.status(error.statusCode).json({
1422
1940
  error: error.message
@@ -1605,6 +2123,9 @@ class PlaygroundServer {
1605
2123
  _define_property(this, "_baseSidecars", void 0);
1606
2124
  _define_property(this, "_recorderSessionId", null);
1607
2125
  _define_property(this, "_recorderEvents", []);
2126
+ _define_property(this, "_recorderEventQueue", Promise.resolve());
2127
+ _define_property(this, "_recorderPendingCaptures", 0);
2128
+ _define_property(this, "_studioPreviewRecorderLastTargetPoint", void 0);
1608
2129
  _define_property(this, "_studioPreviewRecorderLastScreenshot", void 0);
1609
2130
  _define_property(this, "_studioPreviewRecorderLastPageState", void 0);
1610
2131
  _define_property(this, "_activeConnection", {