@midscene/playground 1.10.4 → 1.10.5-beta-20260714101501.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/es/adapters/remote-execution.mjs +38 -1
  2. package/dist/es/adapters/remote-execution.mjs.map +1 -1
  3. package/dist/es/mjpeg-hub.mjs +7 -6
  4. package/dist/es/mjpeg-hub.mjs.map +1 -1
  5. package/dist/es/mjpeg-stream-handler.mjs +7 -1
  6. package/dist/es/mjpeg-stream-handler.mjs.map +1 -1
  7. package/dist/es/platform.mjs.map +1 -1
  8. package/dist/es/sdk/index.mjs +10 -0
  9. package/dist/es/sdk/index.mjs.map +1 -1
  10. package/dist/es/server.mjs +369 -35
  11. package/dist/es/server.mjs.map +1 -1
  12. package/dist/lib/adapters/remote-execution.js +38 -1
  13. package/dist/lib/adapters/remote-execution.js.map +1 -1
  14. package/dist/lib/mjpeg-hub.js +7 -6
  15. package/dist/lib/mjpeg-hub.js.map +1 -1
  16. package/dist/lib/mjpeg-stream-handler.js +7 -1
  17. package/dist/lib/mjpeg-stream-handler.js.map +1 -1
  18. package/dist/lib/platform.js.map +1 -1
  19. package/dist/lib/sdk/index.js +10 -0
  20. package/dist/lib/sdk/index.js.map +1 -1
  21. package/dist/lib/server.js +367 -33
  22. package/dist/lib/server.js.map +1 -1
  23. package/dist/types/adapters/remote-execution.d.ts +3 -0
  24. package/dist/types/platform.d.ts +1 -0
  25. package/dist/types/sdk/index.d.ts +3 -0
  26. package/dist/types/server.d.ts +18 -1
  27. package/dist/types/types.d.ts +1 -0
  28. package/package.json +3 -3
  29. package/static/index.html +1 -1
  30. package/static/static/css/index.2b6b43ad.css +2 -0
  31. package/static/static/css/index.2b6b43ad.css.map +1 -0
  32. package/static/static/js/{445.884e7c8e.js → 890.03bb43a9.js} +3 -3
  33. package/static/static/js/{445.884e7c8e.js.LICENSE.txt → 890.03bb43a9.js.LICENSE.txt} +0 -2
  34. package/static/static/js/890.03bb43a9.js.map +1 -0
  35. package/static/static/js/index.2a85361c.js +960 -0
  36. package/static/static/js/index.2a85361c.js.map +1 -0
  37. package/static/static/css/index.1293237f.css +0 -2
  38. package/static/static/css/index.1293237f.css.map +0 -1
  39. package/static/static/js/445.884e7c8e.js.map +0 -1
  40. package/static/static/js/index.babddf12.js +0 -960
  41. package/static/static/js/index.babddf12.js.map +0 -1
  42. /package/static/static/js/{index.babddf12.js.LICENSE.txt → index.2a85361c.js.LICENSE.txt} +0 -0
@@ -80,8 +80,28 @@ function _define_property(obj, key, value) {
80
80
  }
81
81
  const defaultPort = constants_namespaceObject.PLAYGROUND_SERVER_PORT;
82
82
  const RECORDER_CAPTURE_AFTER_INTERACT_DELAY_MS = 250;
83
+ const RECORDER_TYPE_ONLY_INPUT_SETTLE_DELAY_MS = 750;
84
+ const RECORDER_NAVIGATION_OBSERVATION_INTERVAL_MS = 250;
85
+ const RECORDER_NAVIGATION_OBSERVATION_TIMEOUT_MS = 35000;
83
86
  const RECORDER_AI_DESCRIBE_AFTER_INTERACT_TIMEOUT_MS = 30000;
87
+ const RECORDER_AI_DESCRIBE_VERIFY_PROMPT = false;
84
88
  const RECORDER_AI_DESCRIBE_SCREENSHOT_DUMP_DIR = 'recorder-ai-describe-screenshots';
89
+ const RECORDER_SCREENSHOT_ASSET_DIR = 'recorder-screenshots';
90
+ const RECORDER_SCREENSHOT_ASSET_MAX_SESSION_BYTES = 134217728;
91
+ const RECORDER_SCREENSHOT_ASSET_MAX_TOTAL_BYTES = 536870912;
92
+ const recorderScreenshotAssetQuotaExceededSessions = new Set();
93
+ const recorderScreenshotAssetBytesBySession = new Map();
94
+ let recorderScreenshotAssetUsageInitialized = false;
95
+ function shouldPersistStudioPreviewRecorderScreenshot(actionType) {
96
+ return [
97
+ 'Tap',
98
+ 'DoubleClick',
99
+ 'LongPress',
100
+ 'RightClick',
101
+ 'DragAndDrop',
102
+ 'Input'
103
+ ].includes(actionType);
104
+ }
85
105
  function createElementDescriberRuntime(agent) {
86
106
  const modelConfigManager = agent.modelConfigManager;
87
107
  const missingModelRuntime = void 0;
@@ -144,8 +164,10 @@ function buildRecorderEventSummary(event) {
144
164
  pageInfo: event.pageInfo
145
165
  };
146
166
  }
147
- function shouldVerifyRecorderAiDescribeEvent(event) {
148
- return 'scroll' !== event.type;
167
+ function sanitizeRecorderPathSegment(value, fallback, maxLength = 64) {
168
+ const normalized = 'string' == typeof value && value.trim() ? value.trim() : fallback;
169
+ const sanitized = normalized.replace(/[^a-zA-Z0-9_-]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
170
+ return (sanitized || fallback).slice(0, maxLength);
149
171
  }
150
172
  function formatRecorderAiDescribeScreenshotPathParts() {
151
173
  const date = new Date();
@@ -197,6 +219,141 @@ function writeRecorderAiDescribeScreenshot(imageBase64, suffix) {
197
219
  mimeType
198
220
  };
199
221
  }
222
+ function getRecorderScreenshotAssetRoot() {
223
+ const root = (0, external_node_path_namespaceObject.resolve)((0, common_namespaceObject.getMidsceneRunSubDir)('output'), RECORDER_SCREENSHOT_ASSET_DIR);
224
+ (0, external_node_fs_namespaceObject.mkdirSync)(root, {
225
+ recursive: true
226
+ });
227
+ return root;
228
+ }
229
+ function assertRecorderScreenshotAssetId(assetId) {
230
+ if (!/^[a-zA-Z0-9_-]+$/.test(assetId)) throw new Error('Invalid recorder screenshot asset id');
231
+ return assetId;
232
+ }
233
+ function recorderScreenshotAssetExtension(mimeType) {
234
+ return mimeType?.includes('jpeg') ? 'jpg' : 'png';
235
+ }
236
+ function recorderScreenshotAssetPath(assetId, mimeType) {
237
+ const root = getRecorderScreenshotAssetRoot();
238
+ const safeAssetId = assertRecorderScreenshotAssetId(assetId);
239
+ return assertPathInsideDirectory(root, (0, external_node_path_namespaceObject.join)(root, `${safeAssetId}.${recorderScreenshotAssetExtension(mimeType)}`));
240
+ }
241
+ function getRecorderScreenshotAssetUsage(sessionId) {
242
+ initializeRecorderScreenshotAssetUsage();
243
+ const safeSessionId = sanitizeRecorderPathSegment(sessionId, 'session');
244
+ const sessionBytes = recorderScreenshotAssetBytesBySession.get(safeSessionId) || 0;
245
+ const totalBytes = Array.from(recorderScreenshotAssetBytesBySession.values()).reduce((total, bytes)=>total + bytes, 0);
246
+ return {
247
+ totalBytes,
248
+ sessionBytes
249
+ };
250
+ }
251
+ function initializeRecorderScreenshotAssetUsage() {
252
+ if (recorderScreenshotAssetUsageInitialized) return;
253
+ recorderScreenshotAssetUsageInitialized = true;
254
+ const root = getRecorderScreenshotAssetRoot();
255
+ try {
256
+ for (const entry of (0, external_node_fs_namespaceObject.readdirSync)(root, {
257
+ withFileTypes: true
258
+ })){
259
+ if (!entry.isFile()) continue;
260
+ const match = entry.name.match(/^(.*)-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.(?:png|jpg)$/i);
261
+ if (!match?.[1]) continue;
262
+ const sessionId = match[1];
263
+ const bytes = (0, external_node_fs_namespaceObject.statSync)((0, external_node_path_namespaceObject.join)(root, entry.name)).size;
264
+ recorderScreenshotAssetBytesBySession.set(sessionId, (recorderScreenshotAssetBytesBySession.get(sessionId) || 0) + bytes);
265
+ }
266
+ } catch (error) {
267
+ debugRecorderAssets('failed to initialize recorder screenshot asset usage:', error);
268
+ }
269
+ }
270
+ function persistRecorderScreenshotAsset(sessionId, imageBase64) {
271
+ if (!imageBase64?.startsWith('data:')) return;
272
+ const { base64, mimeType } = extractBase64Payload(imageBase64);
273
+ const bytes = Buffer.from(base64, 'base64');
274
+ const safeSessionId = sanitizeRecorderPathSegment(sessionId, 'session');
275
+ const usage = getRecorderScreenshotAssetUsage(sessionId);
276
+ if (usage.sessionBytes + bytes.byteLength > RECORDER_SCREENSHOT_ASSET_MAX_SESSION_BYTES || usage.totalBytes + bytes.byteLength > RECORDER_SCREENSHOT_ASSET_MAX_TOTAL_BYTES) {
277
+ if (!recorderScreenshotAssetQuotaExceededSessions.has(safeSessionId)) {
278
+ recorderScreenshotAssetQuotaExceededSessions.add(safeSessionId);
279
+ debugRecorderAssets('recorder screenshot asset quota reached %o', {
280
+ sessionId: safeSessionId,
281
+ sessionBytes: usage.sessionBytes,
282
+ totalBytes: usage.totalBytes
283
+ });
284
+ }
285
+ return;
286
+ }
287
+ const assetId = `${safeSessionId}-${(0, external_node_crypto_namespaceObject.randomUUID)()}`;
288
+ const normalizedMimeType = mimeType || 'image/png';
289
+ const filePath = recorderScreenshotAssetPath(assetId, normalizedMimeType);
290
+ try {
291
+ (0, external_node_fs_namespaceObject.writeFileSync)(filePath, bytes);
292
+ } catch (error) {
293
+ debugRecorderAssets('failed to persist recorder screenshot asset:', error);
294
+ return;
295
+ }
296
+ recorderScreenshotAssetBytesBySession.set(safeSessionId, usage.sessionBytes + bytes.byteLength);
297
+ return {
298
+ id: assetId,
299
+ mimeType: normalizedMimeType,
300
+ bytes: bytes.byteLength
301
+ };
302
+ }
303
+ function readRecorderScreenshotAsset(asset) {
304
+ const filePath = recorderScreenshotAssetPath(asset.id, asset.mimeType);
305
+ if (!(0, external_node_fs_namespaceObject.existsSync)(filePath)) return;
306
+ return `data:${asset.mimeType};base64,${(0, external_node_fs_namespaceObject.readFileSync)(filePath).toString('base64')}`;
307
+ }
308
+ function findRecorderScreenshotAssetPath(assetId) {
309
+ const safeAssetId = assertRecorderScreenshotAssetId(assetId);
310
+ const root = getRecorderScreenshotAssetRoot();
311
+ for (const extension of [
312
+ 'png',
313
+ 'jpg'
314
+ ]){
315
+ const filePath = assertPathInsideDirectory(root, (0, external_node_path_namespaceObject.join)(root, `${safeAssetId}.${extension}`));
316
+ if ((0, external_node_fs_namespaceObject.existsSync)(filePath)) return filePath;
317
+ }
318
+ }
319
+ function removeRecorderScreenshotAssetsForSession(sessionId) {
320
+ const safeSessionId = sanitizeRecorderPathSegment(sessionId, 'session');
321
+ const prefix = `${safeSessionId}-`;
322
+ const root = getRecorderScreenshotAssetRoot();
323
+ for (const entry of (0, external_node_fs_namespaceObject.readdirSync)(root, {
324
+ withFileTypes: true
325
+ }))if (entry.isFile() && entry.name.startsWith(prefix)) (0, external_node_fs_namespaceObject.rmSync)((0, external_node_path_namespaceObject.join)(root, entry.name), {
326
+ force: true
327
+ });
328
+ recorderScreenshotAssetQuotaExceededSessions.delete(safeSessionId);
329
+ recorderScreenshotAssetBytesBySession.delete(safeSessionId);
330
+ }
331
+ function pruneRecorderScreenshotAssetsForSession(sessionId, retainedAssetIds) {
332
+ const safeSessionId = sanitizeRecorderPathSegment(sessionId, 'session');
333
+ const prefix = `${safeSessionId}-`;
334
+ const retained = new Set(retainedAssetIds.map((assetId)=>{
335
+ const safeAssetId = assertRecorderScreenshotAssetId(assetId);
336
+ if (!safeAssetId.startsWith(prefix)) throw new Error('Recorder screenshot asset does not belong to session');
337
+ return safeAssetId;
338
+ }));
339
+ const root = getRecorderScreenshotAssetRoot();
340
+ let sessionBytes = 0;
341
+ for (const entry of (0, external_node_fs_namespaceObject.readdirSync)(root, {
342
+ withFileTypes: true
343
+ })){
344
+ if (!entry.isFile() || !entry.name.startsWith(prefix)) continue;
345
+ const assetId = entry.name.replace(/\.(?:png|jpg)$/i, '');
346
+ const filePath = (0, external_node_path_namespaceObject.join)(root, entry.name);
347
+ if (!retained.has(assetId)) {
348
+ (0, external_node_fs_namespaceObject.rmSync)(filePath, {
349
+ force: true
350
+ });
351
+ continue;
352
+ }
353
+ sessionBytes += (0, external_node_fs_namespaceObject.statSync)(filePath).size;
354
+ }
355
+ recorderScreenshotAssetBytesBySession.set(safeSessionId, sessionBytes);
356
+ }
200
357
  function calculateRecorderScreenshotAnnotation(event, imageSize, verifyResult) {
201
358
  const pageWidth = event.pageInfo?.width;
202
359
  const pageHeight = event.pageInfo?.height;
@@ -353,6 +510,9 @@ const debugMjpeg = (0, logger_namespaceObject.getDebug)('playground:mjpeg', {
353
510
  const debugInteract = (0, logger_namespaceObject.getDebug)('playground:interact', {
354
511
  console: true
355
512
  });
513
+ const debugRecorderAssets = (0, logger_namespaceObject.getDebug)('playground:recorder-assets', {
514
+ console: true
515
+ });
356
516
  class InteractParamsValidationError extends Error {
357
517
  constructor(message){
358
518
  super(message);
@@ -742,8 +902,10 @@ class PlaygroundServer {
742
902
  };
743
903
  }
744
904
  resetRecorderState() {
905
+ this.clearPendingTypeOnlyRecorderInputFlushTimer();
745
906
  this._recorderSessionId = null;
746
907
  this._recorderEvents = [];
908
+ this._recorderPendingTypeOnlyInput = null;
747
909
  this._recorderEventQueue = Promise.resolve();
748
910
  this._studioPreviewRecorderLastTargetPoint = void 0;
749
911
  this._studioPreviewRecorderLastScreenshot = void 0;
@@ -850,8 +1012,13 @@ class PlaygroundServer {
850
1012
  const initialNavigationEvent = this.buildStudioPreviewInitialNavigationEvent(initialPageState, this._studioPreviewRecorderLastScreenshot);
851
1013
  if (initialNavigationEvent) this._recorderEvents.push(initialNavigationEvent);
852
1014
  }
1015
+ persistStudioPreviewRecorderScreenshot(screenshot) {
1016
+ if (!this._recorderSessionId) return;
1017
+ return persistRecorderScreenshotAsset(this._recorderSessionId, screenshot);
1018
+ }
853
1019
  async storeStudioPreviewRecorderEvent(payload, snapshotBefore, agent) {
854
1020
  if (!this._recorderSessionId) return;
1021
+ const sessionId = this._recorderSessionId;
855
1022
  const before = snapshotBefore || await this.captureRecorderSnapshotBeforeInteract();
856
1023
  const screenshotBefore = before?.screenshot;
857
1024
  debugInteract('recorder capture scheduled after action %o', {
@@ -879,6 +1046,48 @@ class PlaygroundServer {
879
1046
  this._studioPreviewRecorderLastScreenshot = screenshotAfter;
880
1047
  this._studioPreviewRecorderLastPageState = pageStateAfter;
881
1048
  this.queueStudioPreviewRecorderEventAppend(event, navigationEvent);
1049
+ if (!navigationEvent) this.observeStudioPreviewNavigationChange(payload, before?.pageState, event.hashId, sessionId);
1050
+ }
1051
+ observeStudioPreviewNavigationChange(payload, pageStateBefore, triggerEventHashId, sessionId) {
1052
+ const actionType = 'string' == typeof payload.actionType ? payload.actionType : void 0;
1053
+ const beforeUrl = pageStateBefore?.url;
1054
+ if (!actionType || !beforeUrl || BROWSER_CHROME_NAVIGATION_ACTIONS.has(actionType)) return;
1055
+ (async ()=>{
1056
+ const deadline = Date.now() + RECORDER_NAVIGATION_OBSERVATION_TIMEOUT_MS;
1057
+ while(this._recorderSessionId === sessionId && Date.now() < deadline){
1058
+ await (0, utils_namespaceObject.sleep)(RECORDER_NAVIGATION_OBSERVATION_INTERVAL_MS);
1059
+ if (this._recorderSessionId !== sessionId) return;
1060
+ const currentUrl = await this.getActivePageUrl();
1061
+ if (!currentUrl || currentUrl === beforeUrl) continue;
1062
+ const screenshot = await this.takeRecorderScreenshot();
1063
+ const pageStateAfter = await this.getActiveRecorderPageState();
1064
+ const navigationEvent = this.buildStudioPreviewNavigationChangeEvent(payload, pageStateBefore, pageStateAfter, screenshot);
1065
+ if (!navigationEvent) return;
1066
+ navigationEvent.rawPayload = {
1067
+ ...navigationEvent.rawPayload,
1068
+ triggerEventHashId
1069
+ };
1070
+ if (this.appendStudioPreviewNavigationEventAfterTrigger(triggerEventHashId, navigationEvent)) {
1071
+ this._studioPreviewRecorderLastScreenshot = screenshot;
1072
+ this._studioPreviewRecorderLastPageState = pageStateAfter;
1073
+ }
1074
+ return;
1075
+ }
1076
+ })().catch((error)=>{
1077
+ debugInteract('delayed recorder navigation observation failed:', error);
1078
+ });
1079
+ }
1080
+ appendStudioPreviewNavigationEventAfterTrigger(triggerEventHashId, navigationEvent) {
1081
+ const triggerIndex = this._recorderEvents.findIndex((event)=>event.hashId === triggerEventHashId);
1082
+ if (triggerIndex < 0) return false;
1083
+ const alreadyRecorded = this._recorderEvents.some((event)=>{
1084
+ if ('NavigationChanged' !== event.actionType) return false;
1085
+ const rawPayload = event.rawPayload;
1086
+ return rawPayload?.triggerEventHashId === triggerEventHashId;
1087
+ });
1088
+ if (alreadyRecorded) return false;
1089
+ this._recorderEvents.splice(triggerIndex + 1, 0, navigationEvent);
1090
+ return true;
882
1091
  }
883
1092
  async buildStudioPreviewRecorderEvent(payload, pageState, screenshotBefore, screenshotAfter) {
884
1093
  const actionType = 'string' == typeof payload.actionType ? payload.actionType : void 0;
@@ -898,10 +1107,12 @@ class PlaygroundServer {
898
1107
  const endX = 'number' == typeof payload.endX ? payload.endX : void 0;
899
1108
  const endY = 'number' == typeof payload.endY ? payload.endY : void 0;
900
1109
  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;
901
- const screenshotWithMarker = void 0 !== x && void 0 !== y ? await this.createRecorderScreenshotWithMarker(screenshotBefore || screenshotAfter, pageInfo, {
902
- x,
903
- y
904
- }) : void 0;
1110
+ const shouldPersistScreenshot = shouldPersistStudioPreviewRecorderScreenshot(actionType);
1111
+ const retainedScreenshot = shouldPersistScreenshot ? screenshotBefore || screenshotAfter : void 0;
1112
+ const deferScreenshotPersistence = 'Input' === actionType && 'typeOnly' === payload.mode;
1113
+ const screenshotAsset = deferScreenshotPersistence ? void 0 : this.persistStudioPreviewRecorderScreenshot(retainedScreenshot);
1114
+ const shouldDiscardDataUrlScreenshot = Boolean(retainedScreenshot?.startsWith('data:'));
1115
+ const hasRetainedScreenshot = Boolean(screenshotAsset || deferScreenshotPersistence && retainedScreenshot || !shouldDiscardDataUrlScreenshot && retainedScreenshot);
905
1116
  const base = {
906
1117
  source: 'studio-preview',
907
1118
  actionType,
@@ -909,13 +1120,18 @@ class PlaygroundServer {
909
1120
  pageInfo,
910
1121
  url,
911
1122
  title,
912
- screenshotBefore,
913
- screenshotAfter,
914
- screenshotWithBox: screenshotWithMarker,
915
- semantic: {
1123
+ ...screenshotAsset ? {
1124
+ screenshotAsset
1125
+ } : deferScreenshotPersistence ? retainedScreenshot ? {
1126
+ screenshotWithBox: retainedScreenshot
1127
+ } : {} : shouldDiscardDataUrlScreenshot ? {} : {
1128
+ screenshotBefore,
1129
+ screenshotAfter
1130
+ },
1131
+ semantic: hasRetainedScreenshot ? {
916
1132
  source: 'aiDescribe',
917
1133
  status: 'pending'
918
- },
1134
+ } : buildFailedAiDescribeRecorderSemantic('Recorder screenshot was not retained because asset storage is unavailable or its quota was reached.'),
919
1135
  timestamp,
920
1136
  hashId: `studio-preview-${actionType}-${timestamp}-${Math.random().toString(36).slice(2, 8)}`
921
1137
  };
@@ -1016,7 +1232,7 @@ class PlaygroundServer {
1016
1232
  async enrichStudioPreviewRecorderEventWithAiDescribe(event, agent) {
1017
1233
  const startedAt = new Date();
1018
1234
  const startedAtMs = Date.now();
1019
- const eventScreenshot = this.getRecorderAiDescribeScreenshot(event);
1235
+ const eventScreenshot = await this.getRecorderAiDescribeScreenshot(event);
1020
1236
  const traceBase = createRecorderAiDescribeTraceBase(event, eventScreenshot);
1021
1237
  const finishTrace = async (status, extra = {})=>{
1022
1238
  let screenshotRef;
@@ -1081,11 +1297,11 @@ class PlaygroundServer {
1081
1297
  let elementDescription;
1082
1298
  let deepLocate;
1083
1299
  let verifyResult;
1300
+ const verifyPrompt = RECORDER_AI_DESCRIBE_VERIFY_PROMPT;
1084
1301
  try {
1085
1302
  if ('number' != typeof x || 'number' != typeof y) throw new Error('Skipped aiDescribe because the recorder event has no stable point target.');
1086
1303
  if (!eventScreenshot) throw new Error('Skipped aiDescribe because the recorder event has no screenshot.');
1087
1304
  if (!event.pageInfo?.width || !event.pageInfo?.height) throw new Error('Skipped aiDescribe because the recorder event has no pageInfo for coordinate mapping.');
1088
- const verifyPrompt = shouldVerifyRecorderAiDescribeEvent(event);
1089
1305
  modelCallStartedAt = Date.now();
1090
1306
  const elementDescriber = createElementDescriberRuntime(agent);
1091
1307
  const describeResult = await withTimeout((0, core_namespaceObject.describeElementAtPoint)(elementDescriber, [
@@ -1112,6 +1328,7 @@ class PlaygroundServer {
1112
1328
  const trace = await finishTrace('ready', {
1113
1329
  modelCallDurationMs,
1114
1330
  elementDescription,
1331
+ verifyPrompt,
1115
1332
  verifyPassed: false,
1116
1333
  centerDistance: verifyResult.centerDistance,
1117
1334
  verifyResult
@@ -1142,6 +1359,7 @@ class PlaygroundServer {
1142
1359
  const trace = await finishTrace('ready', {
1143
1360
  modelCallDurationMs,
1144
1361
  elementDescription,
1362
+ verifyPrompt,
1145
1363
  verifyPassed: verifyResult?.pass,
1146
1364
  centerDistance: verifyResult?.centerDistance,
1147
1365
  verifyResult
@@ -1175,6 +1393,7 @@ class PlaygroundServer {
1175
1393
  error: reportedError instanceof Error ? reportedError.message : String(reportedError),
1176
1394
  modelCallDurationMs,
1177
1395
  elementDescription,
1396
+ verifyPrompt,
1178
1397
  verifyPassed: verifyResult?.pass,
1179
1398
  centerDistance: verifyResult?.centerDistance,
1180
1399
  verifyResult
@@ -1182,7 +1401,7 @@ class PlaygroundServer {
1182
1401
  debugInteract('recorder aiDescribe trace:', trace);
1183
1402
  const aiDescribeDetails = verifyResult || trace.annotatedScreenshotRef ? {
1184
1403
  aiDescribe: {
1185
- verifyPrompt: true,
1404
+ verifyPrompt,
1186
1405
  verifyPassed: verifyResult?.pass,
1187
1406
  deepLocate,
1188
1407
  centerDistance: verifyResult?.centerDistance,
@@ -1203,16 +1422,92 @@ class PlaygroundServer {
1203
1422
  };
1204
1423
  }
1205
1424
  }
1206
- getRecorderAiDescribeScreenshot(event) {
1425
+ async getRecorderAiDescribeScreenshot(event) {
1426
+ if (event.screenshotAsset) {
1427
+ const screenshot = readRecorderScreenshotAsset(event.screenshotAsset);
1428
+ if (screenshot) return screenshot;
1429
+ }
1207
1430
  if ('click' === event.type || 'input' === event.type || 'keydown' === event.type || 'drag' === event.type) return event.screenshotBefore || event.screenshotAfter;
1208
- return event.screenshotAfter || event.screenshotBefore;
1431
+ return event.screenshotAfter || event.screenshotBefore || event.screenshotWithBox;
1209
1432
  }
1210
1433
  queueStudioPreviewRecorderEventAppend(event, navigationEvent) {
1211
1434
  const sessionId = this._recorderSessionId;
1212
1435
  if (!sessionId) return;
1436
+ if (this.isDeferredTypeOnlyRecorderInput(event) && !navigationEvent) {
1437
+ const pending = this._recorderPendingTypeOnlyInput;
1438
+ if (pending && this.canCoalesceDeferredTypeOnlyRecorderInput(pending, event)) this._recorderPendingTypeOnlyInput = this.mergeDeferredTypeOnlyRecorderInput(pending, event);
1439
+ else {
1440
+ this.flushPendingTypeOnlyRecorderInput();
1441
+ this._recorderPendingTypeOnlyInput = event;
1442
+ }
1443
+ this.schedulePendingTypeOnlyRecorderInputFlush();
1444
+ return;
1445
+ }
1446
+ this.flushPendingTypeOnlyRecorderInput();
1213
1447
  this._recorderEvents.push(event);
1214
1448
  if (navigationEvent) this._recorderEvents.push(navigationEvent);
1215
1449
  }
1450
+ isDeferredTypeOnlyRecorderInput(event) {
1451
+ return 'studio-preview' === event.source && 'input' === event.type && 'Input' === event.actionType && event.rawPayload?.mode === 'typeOnly';
1452
+ }
1453
+ canCoalesceDeferredTypeOnlyRecorderInput(current, next) {
1454
+ return current.url === next.url && current.title === next.title && current.rawPayload?.x === next.rawPayload?.x && current.rawPayload?.y === next.rawPayload?.y;
1455
+ }
1456
+ mergeDeferredTypeOnlyRecorderInput(current, next) {
1457
+ const value = `${current.value || ''}${next.value || ''}`;
1458
+ return {
1459
+ ...current,
1460
+ value,
1461
+ rawPayload: {
1462
+ ...current.rawPayload,
1463
+ ...next.rawPayload,
1464
+ value
1465
+ },
1466
+ pageInfo: next.pageInfo || current.pageInfo,
1467
+ screenshotWithBox: next.screenshotWithBox || next.screenshotAfter || next.screenshotBefore || current.screenshotWithBox,
1468
+ timestamp: next.timestamp,
1469
+ mergedHashIds: Array.from(new Set([
1470
+ current.hashId,
1471
+ ...current.mergedHashIds || [],
1472
+ next.hashId,
1473
+ ...next.mergedHashIds || []
1474
+ ]))
1475
+ };
1476
+ }
1477
+ flushPendingTypeOnlyRecorderInput() {
1478
+ this.clearPendingTypeOnlyRecorderInputFlushTimer();
1479
+ const pending = this._recorderPendingTypeOnlyInput;
1480
+ if (!pending) return;
1481
+ this._recorderPendingTypeOnlyInput = null;
1482
+ const screenshot = pending.screenshotWithBox || pending.screenshotAfter || pending.screenshotBefore;
1483
+ const screenshotAsset = this.persistStudioPreviewRecorderScreenshot(screenshot);
1484
+ if (screenshotAsset) return void this._recorderEvents.push({
1485
+ ...pending,
1486
+ screenshotAsset,
1487
+ screenshotBefore: void 0,
1488
+ screenshotAfter: void 0,
1489
+ screenshotWithBox: void 0
1490
+ });
1491
+ this._recorderEvents.push({
1492
+ ...pending,
1493
+ screenshotBefore: void 0,
1494
+ screenshotAfter: void 0,
1495
+ screenshotWithBox: void 0,
1496
+ semantic: buildFailedAiDescribeRecorderSemantic('Recorder screenshot was not retained because asset storage is unavailable or its quota was reached.')
1497
+ });
1498
+ }
1499
+ schedulePendingTypeOnlyRecorderInputFlush() {
1500
+ this.clearPendingTypeOnlyRecorderInputFlushTimer();
1501
+ this._recorderPendingTypeOnlyInputFlushTimer = setTimeout(()=>{
1502
+ this._recorderPendingTypeOnlyInputFlushTimer = void 0;
1503
+ this.flushPendingTypeOnlyRecorderInput();
1504
+ }, RECORDER_TYPE_ONLY_INPUT_SETTLE_DELAY_MS);
1505
+ }
1506
+ clearPendingTypeOnlyRecorderInputFlushTimer() {
1507
+ if (!this._recorderPendingTypeOnlyInputFlushTimer) return;
1508
+ clearTimeout(this._recorderPendingTypeOnlyInputFlushTimer);
1509
+ this._recorderPendingTypeOnlyInputFlushTimer = void 0;
1510
+ }
1216
1511
  queueStudioPreviewRecorderEvent(payload, snapshotBefore, agent) {
1217
1512
  const sessionId = this._recorderSessionId;
1218
1513
  if (!sessionId) return;
@@ -1252,8 +1547,6 @@ class PlaygroundServer {
1252
1547
  url: afterUrl,
1253
1548
  title: pageStateAfter.title,
1254
1549
  value: afterUrl,
1255
- screenshotBefore: screenshotAfter,
1256
- screenshotAfter,
1257
1550
  semantic: buildReadyRecorderSemantic('heuristic', semanticEvent, afterUrl, 'high'),
1258
1551
  timestamp,
1259
1552
  hashId: `studio-preview-navigation-${timestamp}-${Math.random().toString(36).slice(2, 8)}`
@@ -1277,26 +1570,11 @@ class PlaygroundServer {
1277
1570
  url: pageState.url,
1278
1571
  title: pageState.title,
1279
1572
  value: pageState.url,
1280
- screenshotBefore: screenshot,
1281
- screenshotAfter: screenshot,
1282
1573
  semantic: buildReadyRecorderSemantic('heuristic', semanticEvent, pageState.url, 'high'),
1283
1574
  timestamp,
1284
1575
  hashId: `studio-preview-initial-navigation-${timestamp}-${Math.random().toString(36).slice(2, 8)}`
1285
1576
  };
1286
1577
  }
1287
- async createRecorderScreenshotWithMarker(screenshot, pageInfo, point) {
1288
- if (!screenshot || !pageInfo.width || !pageInfo.height) return;
1289
- try {
1290
- return await (0, img_namespaceObject.compositePointMarkerImg)({
1291
- inputImgBase64: screenshot,
1292
- size: pageInfo,
1293
- point
1294
- });
1295
- } catch (error) {
1296
- debugScreenshot('recorder screenshot marker failed:', error);
1297
- return;
1298
- }
1299
- }
1300
1578
  async destroyCurrentAgent({ preserveActiveStream = false } = {}) {
1301
1579
  if (!this._activeConnection.agent) return;
1302
1580
  try {
@@ -1654,9 +1932,12 @@ class PlaygroundServer {
1654
1932
  error: 'Another task is already running',
1655
1933
  currentTaskId: this.currentTaskId
1656
1934
  });
1935
+ let abortController = null;
1657
1936
  if (requestId) {
1658
1937
  this.currentTaskId = requestId;
1659
1938
  this.taskExecutionDumps[requestId] = null;
1939
+ abortController = new AbortController();
1940
+ this.taskAbortControllers[requestId] = abortController;
1660
1941
  agent.onDumpUpdate = (_dump, executionDump)=>{
1661
1942
  if (executionDump) this.taskExecutionDumps[requestId] = executionDump;
1662
1943
  };
@@ -1679,10 +1960,12 @@ class PlaygroundServer {
1679
1960
  params
1680
1961
  };
1681
1962
  response.result = await (0, external_common_js_namespaceObject.executeAction)(agent, type, actionSpace, value, {
1963
+ requestId,
1682
1964
  deepLocate,
1683
1965
  deepThink,
1684
1966
  screenshotIncluded,
1685
1967
  domIncluded,
1968
+ abortSignal: abortController?.signal,
1686
1969
  deviceOptions,
1687
1970
  reportDisplay
1688
1971
  });
@@ -1718,6 +2001,7 @@ class PlaygroundServer {
1718
2001
  else console.log(`handle request done after ${timeCost}ms: requestId: ${requestId}`);
1719
2002
  if (requestId) {
1720
2003
  delete this.taskExecutionDumps[requestId];
2004
+ delete this.taskAbortControllers[requestId];
1721
2005
  if (this.currentTaskId === requestId) this.currentTaskId = null;
1722
2006
  }
1723
2007
  });
@@ -1749,6 +2033,8 @@ class PlaygroundServer {
1749
2033
  } catch (error) {
1750
2034
  console.warn('Failed to get execution data before cancel:', error);
1751
2035
  }
2036
+ const abortController = this.taskAbortControllers[requestId];
2037
+ abortController?.abort(new Error(`Task cancelled by user: ${requestId}`));
1752
2038
  try {
1753
2039
  await this.recreateAgent({
1754
2040
  preserveActiveStream: true
@@ -1757,6 +2043,7 @@ class PlaygroundServer {
1757
2043
  console.warn('Failed to recreate agent during cancel:', error);
1758
2044
  }
1759
2045
  delete this.taskExecutionDumps[requestId];
2046
+ delete this.taskAbortControllers[requestId];
1760
2047
  this.currentTaskId = null;
1761
2048
  res.json({
1762
2049
  status: 'cancelled',
@@ -1813,6 +2100,7 @@ class PlaygroundServer {
1813
2100
  });
1814
2101
  this._app.post('/recorder/stop', async (_req, res)=>{
1815
2102
  await this.waitForRecorderIdle();
2103
+ this.flushPendingTypeOnlyRecorderInput();
1816
2104
  this._recorderSessionId = null;
1817
2105
  this._studioPreviewRecorderLastScreenshot = void 0;
1818
2106
  this._studioPreviewRecorderLastPageState = void 0;
@@ -1821,6 +2109,7 @@ class PlaygroundServer {
1821
2109
  });
1822
2110
  });
1823
2111
  this._app.get('/recorder/events', async (req, res)=>{
2112
+ if ('false' !== req.query.flushPending) this.flushPendingTypeOnlyRecorderInput();
1824
2113
  const since = 'string' == typeof req.query.since ? Number.parseInt(req.query.since, 10) : 0;
1825
2114
  const startIndex = Number.isFinite(since) && since > 0 ? since : 0;
1826
2115
  res.json({
@@ -1828,6 +2117,48 @@ class PlaygroundServer {
1828
2117
  nextIndex: this._recorderEvents.length
1829
2118
  });
1830
2119
  });
2120
+ this._app.get('/recorder/assets/:assetId', async (req, res)=>{
2121
+ try {
2122
+ const assetId = String(req.params.assetId || '');
2123
+ const filePath = findRecorderScreenshotAssetPath(assetId);
2124
+ if (!filePath) return res.status(404).json({
2125
+ error: 'Recorder screenshot not found'
2126
+ });
2127
+ res.type(filePath.endsWith('.jpg') ? 'image/jpeg' : 'image/png');
2128
+ return res.send((0, external_node_fs_namespaceObject.readFileSync)(filePath));
2129
+ } catch (error) {
2130
+ return res.status(400).json({
2131
+ error: error instanceof Error ? error.message : 'Invalid recorder screenshot request'
2132
+ });
2133
+ }
2134
+ });
2135
+ this._app.delete('/recorder/assets/session/:sessionId', async (req, res)=>{
2136
+ try {
2137
+ removeRecorderScreenshotAssetsForSession(String(req.params.sessionId || ''));
2138
+ return res.json({
2139
+ ok: true
2140
+ });
2141
+ } catch (error) {
2142
+ return res.status(400).json({
2143
+ ok: false,
2144
+ error: error instanceof Error ? error.message : 'Invalid recorder session id'
2145
+ });
2146
+ }
2147
+ });
2148
+ this._app.post('/recorder/assets/session/:sessionId/prune', async (req, res)=>{
2149
+ try {
2150
+ const assetIds = Array.isArray(req.body?.assetIds) ? req.body.assetIds.filter((assetId)=>'string' == typeof assetId) : [];
2151
+ pruneRecorderScreenshotAssetsForSession(String(req.params.sessionId || ''), assetIds);
2152
+ return res.json({
2153
+ ok: true
2154
+ });
2155
+ } catch (error) {
2156
+ return res.status(400).json({
2157
+ ok: false,
2158
+ error: error instanceof Error ? error.message : 'Invalid recorder screenshot cleanup request'
2159
+ });
2160
+ }
2161
+ });
1831
2162
  this._app.post('/recorder/describe-event', async (req, res)=>{
1832
2163
  const event = req.body?.event;
1833
2164
  if (!event || 'object' != typeof event) return res.status(400).json({
@@ -2175,6 +2506,7 @@ class PlaygroundServer {
2175
2506
  _define_property(this, "sessionSetupState", 'ready');
2176
2507
  _define_property(this, "sessionSetupBlockingReason", void 0);
2177
2508
  _define_property(this, "currentTaskId", null);
2509
+ _define_property(this, "taskAbortControllers", {});
2178
2510
  _define_property(this, "_agentReady", true);
2179
2511
  _define_property(this, "_configDirty", false);
2180
2512
  _define_property(this, "_lastAiConfigSignature", null);
@@ -2184,6 +2516,8 @@ class PlaygroundServer {
2184
2516
  _define_property(this, "_baseSidecars", void 0);
2185
2517
  _define_property(this, "_recorderSessionId", null);
2186
2518
  _define_property(this, "_recorderEvents", []);
2519
+ _define_property(this, "_recorderPendingTypeOnlyInput", null);
2520
+ _define_property(this, "_recorderPendingTypeOnlyInputFlushTimer", void 0);
2187
2521
  _define_property(this, "_recorderEventQueue", Promise.resolve());
2188
2522
  _define_property(this, "_recorderPendingCaptures", 0);
2189
2523
  _define_property(this, "_studioPreviewRecorderLastTargetPoint", void 0);