@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
@@ -1,5 +1,5 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
3
3
  import { dirname, join, resolve as external_node_path_resolve, sep } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { ReportActionDump, describeElementAtPoint, runConnectivityTest } from "@midscene/core";
@@ -9,7 +9,7 @@ import { getMidsceneRunSubDir } from "@midscene/shared/common";
9
9
  import { PLAYGROUND_SERVER_PORT } from "@midscene/shared/constants";
10
10
  import { ModelConfigManager, globalModelConfigManager, overrideAIConfig } from "@midscene/shared/env";
11
11
  import { generateElementByPoint } from "@midscene/shared/extractor";
12
- import { annotateRects, compositePointMarkerImg, imageInfoOfBase64 } from "@midscene/shared/img";
12
+ import { annotateRects, imageInfoOfBase64 } from "@midscene/shared/img";
13
13
  import { getDebug } from "@midscene/shared/logger";
14
14
  import { buildMidsceneRecorderActionSummary, buildMidsceneRecorderReplayInstruction } from "@midscene/shared/recorder";
15
15
  import { uuid as utils_uuid } from "@midscene/shared/utils";
@@ -31,8 +31,28 @@ function _define_property(obj, key, value) {
31
31
  }
32
32
  const defaultPort = PLAYGROUND_SERVER_PORT;
33
33
  const RECORDER_CAPTURE_AFTER_INTERACT_DELAY_MS = 250;
34
+ const RECORDER_TYPE_ONLY_INPUT_SETTLE_DELAY_MS = 750;
35
+ const RECORDER_NAVIGATION_OBSERVATION_INTERVAL_MS = 250;
36
+ const RECORDER_NAVIGATION_OBSERVATION_TIMEOUT_MS = 35000;
34
37
  const RECORDER_AI_DESCRIBE_AFTER_INTERACT_TIMEOUT_MS = 30000;
38
+ const RECORDER_AI_DESCRIBE_VERIFY_PROMPT = false;
35
39
  const RECORDER_AI_DESCRIBE_SCREENSHOT_DUMP_DIR = 'recorder-ai-describe-screenshots';
40
+ const RECORDER_SCREENSHOT_ASSET_DIR = 'recorder-screenshots';
41
+ const RECORDER_SCREENSHOT_ASSET_MAX_SESSION_BYTES = 134217728;
42
+ const RECORDER_SCREENSHOT_ASSET_MAX_TOTAL_BYTES = 536870912;
43
+ const recorderScreenshotAssetQuotaExceededSessions = new Set();
44
+ const recorderScreenshotAssetBytesBySession = new Map();
45
+ let recorderScreenshotAssetUsageInitialized = false;
46
+ function shouldPersistStudioPreviewRecorderScreenshot(actionType) {
47
+ return [
48
+ 'Tap',
49
+ 'DoubleClick',
50
+ 'LongPress',
51
+ 'RightClick',
52
+ 'DragAndDrop',
53
+ 'Input'
54
+ ].includes(actionType);
55
+ }
36
56
  function createElementDescriberRuntime(agent) {
37
57
  const modelConfigManager = agent.modelConfigManager;
38
58
  const missingModelRuntime = void 0;
@@ -95,8 +115,10 @@ function buildRecorderEventSummary(event) {
95
115
  pageInfo: event.pageInfo
96
116
  };
97
117
  }
98
- function shouldVerifyRecorderAiDescribeEvent(event) {
99
- return 'scroll' !== event.type;
118
+ function sanitizeRecorderPathSegment(value, fallback, maxLength = 64) {
119
+ const normalized = 'string' == typeof value && value.trim() ? value.trim() : fallback;
120
+ const sanitized = normalized.replace(/[^a-zA-Z0-9_-]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
121
+ return (sanitized || fallback).slice(0, maxLength);
100
122
  }
101
123
  function formatRecorderAiDescribeScreenshotPathParts() {
102
124
  const date = new Date();
@@ -148,6 +170,141 @@ function writeRecorderAiDescribeScreenshot(imageBase64, suffix) {
148
170
  mimeType
149
171
  };
150
172
  }
173
+ function getRecorderScreenshotAssetRoot() {
174
+ const root = external_node_path_resolve(getMidsceneRunSubDir('output'), RECORDER_SCREENSHOT_ASSET_DIR);
175
+ mkdirSync(root, {
176
+ recursive: true
177
+ });
178
+ return root;
179
+ }
180
+ function assertRecorderScreenshotAssetId(assetId) {
181
+ if (!/^[a-zA-Z0-9_-]+$/.test(assetId)) throw new Error('Invalid recorder screenshot asset id');
182
+ return assetId;
183
+ }
184
+ function recorderScreenshotAssetExtension(mimeType) {
185
+ return mimeType?.includes('jpeg') ? 'jpg' : 'png';
186
+ }
187
+ function recorderScreenshotAssetPath(assetId, mimeType) {
188
+ const root = getRecorderScreenshotAssetRoot();
189
+ const safeAssetId = assertRecorderScreenshotAssetId(assetId);
190
+ return assertPathInsideDirectory(root, join(root, `${safeAssetId}.${recorderScreenshotAssetExtension(mimeType)}`));
191
+ }
192
+ function getRecorderScreenshotAssetUsage(sessionId) {
193
+ initializeRecorderScreenshotAssetUsage();
194
+ const safeSessionId = sanitizeRecorderPathSegment(sessionId, 'session');
195
+ const sessionBytes = recorderScreenshotAssetBytesBySession.get(safeSessionId) || 0;
196
+ const totalBytes = Array.from(recorderScreenshotAssetBytesBySession.values()).reduce((total, bytes)=>total + bytes, 0);
197
+ return {
198
+ totalBytes,
199
+ sessionBytes
200
+ };
201
+ }
202
+ function initializeRecorderScreenshotAssetUsage() {
203
+ if (recorderScreenshotAssetUsageInitialized) return;
204
+ recorderScreenshotAssetUsageInitialized = true;
205
+ const root = getRecorderScreenshotAssetRoot();
206
+ try {
207
+ for (const entry of readdirSync(root, {
208
+ withFileTypes: true
209
+ })){
210
+ if (!entry.isFile()) continue;
211
+ 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);
212
+ if (!match?.[1]) continue;
213
+ const sessionId = match[1];
214
+ const bytes = statSync(join(root, entry.name)).size;
215
+ recorderScreenshotAssetBytesBySession.set(sessionId, (recorderScreenshotAssetBytesBySession.get(sessionId) || 0) + bytes);
216
+ }
217
+ } catch (error) {
218
+ debugRecorderAssets('failed to initialize recorder screenshot asset usage:', error);
219
+ }
220
+ }
221
+ function persistRecorderScreenshotAsset(sessionId, imageBase64) {
222
+ if (!imageBase64?.startsWith('data:')) return;
223
+ const { base64, mimeType } = extractBase64Payload(imageBase64);
224
+ const bytes = Buffer.from(base64, 'base64');
225
+ const safeSessionId = sanitizeRecorderPathSegment(sessionId, 'session');
226
+ const usage = getRecorderScreenshotAssetUsage(sessionId);
227
+ if (usage.sessionBytes + bytes.byteLength > RECORDER_SCREENSHOT_ASSET_MAX_SESSION_BYTES || usage.totalBytes + bytes.byteLength > RECORDER_SCREENSHOT_ASSET_MAX_TOTAL_BYTES) {
228
+ if (!recorderScreenshotAssetQuotaExceededSessions.has(safeSessionId)) {
229
+ recorderScreenshotAssetQuotaExceededSessions.add(safeSessionId);
230
+ debugRecorderAssets('recorder screenshot asset quota reached %o', {
231
+ sessionId: safeSessionId,
232
+ sessionBytes: usage.sessionBytes,
233
+ totalBytes: usage.totalBytes
234
+ });
235
+ }
236
+ return;
237
+ }
238
+ const assetId = `${safeSessionId}-${randomUUID()}`;
239
+ const normalizedMimeType = mimeType || 'image/png';
240
+ const filePath = recorderScreenshotAssetPath(assetId, normalizedMimeType);
241
+ try {
242
+ writeFileSync(filePath, bytes);
243
+ } catch (error) {
244
+ debugRecorderAssets('failed to persist recorder screenshot asset:', error);
245
+ return;
246
+ }
247
+ recorderScreenshotAssetBytesBySession.set(safeSessionId, usage.sessionBytes + bytes.byteLength);
248
+ return {
249
+ id: assetId,
250
+ mimeType: normalizedMimeType,
251
+ bytes: bytes.byteLength
252
+ };
253
+ }
254
+ function readRecorderScreenshotAsset(asset) {
255
+ const filePath = recorderScreenshotAssetPath(asset.id, asset.mimeType);
256
+ if (!existsSync(filePath)) return;
257
+ return `data:${asset.mimeType};base64,${readFileSync(filePath).toString('base64')}`;
258
+ }
259
+ function findRecorderScreenshotAssetPath(assetId) {
260
+ const safeAssetId = assertRecorderScreenshotAssetId(assetId);
261
+ const root = getRecorderScreenshotAssetRoot();
262
+ for (const extension of [
263
+ 'png',
264
+ 'jpg'
265
+ ]){
266
+ const filePath = assertPathInsideDirectory(root, join(root, `${safeAssetId}.${extension}`));
267
+ if (existsSync(filePath)) return filePath;
268
+ }
269
+ }
270
+ function removeRecorderScreenshotAssetsForSession(sessionId) {
271
+ const safeSessionId = sanitizeRecorderPathSegment(sessionId, 'session');
272
+ const prefix = `${safeSessionId}-`;
273
+ const root = getRecorderScreenshotAssetRoot();
274
+ for (const entry of readdirSync(root, {
275
+ withFileTypes: true
276
+ }))if (entry.isFile() && entry.name.startsWith(prefix)) rmSync(join(root, entry.name), {
277
+ force: true
278
+ });
279
+ recorderScreenshotAssetQuotaExceededSessions.delete(safeSessionId);
280
+ recorderScreenshotAssetBytesBySession.delete(safeSessionId);
281
+ }
282
+ function pruneRecorderScreenshotAssetsForSession(sessionId, retainedAssetIds) {
283
+ const safeSessionId = sanitizeRecorderPathSegment(sessionId, 'session');
284
+ const prefix = `${safeSessionId}-`;
285
+ const retained = new Set(retainedAssetIds.map((assetId)=>{
286
+ const safeAssetId = assertRecorderScreenshotAssetId(assetId);
287
+ if (!safeAssetId.startsWith(prefix)) throw new Error('Recorder screenshot asset does not belong to session');
288
+ return safeAssetId;
289
+ }));
290
+ const root = getRecorderScreenshotAssetRoot();
291
+ let sessionBytes = 0;
292
+ for (const entry of readdirSync(root, {
293
+ withFileTypes: true
294
+ })){
295
+ if (!entry.isFile() || !entry.name.startsWith(prefix)) continue;
296
+ const assetId = entry.name.replace(/\.(?:png|jpg)$/i, '');
297
+ const filePath = join(root, entry.name);
298
+ if (!retained.has(assetId)) {
299
+ rmSync(filePath, {
300
+ force: true
301
+ });
302
+ continue;
303
+ }
304
+ sessionBytes += statSync(filePath).size;
305
+ }
306
+ recorderScreenshotAssetBytesBySession.set(safeSessionId, sessionBytes);
307
+ }
151
308
  function calculateRecorderScreenshotAnnotation(event, imageSize, verifyResult) {
152
309
  const pageWidth = event.pageInfo?.width;
153
310
  const pageHeight = event.pageInfo?.height;
@@ -304,6 +461,9 @@ const debugMjpeg = getDebug('playground:mjpeg', {
304
461
  const debugInteract = getDebug('playground:interact', {
305
462
  console: true
306
463
  });
464
+ const debugRecorderAssets = getDebug('playground:recorder-assets', {
465
+ console: true
466
+ });
307
467
  class InteractParamsValidationError extends Error {
308
468
  constructor(message){
309
469
  super(message);
@@ -693,8 +853,10 @@ class PlaygroundServer {
693
853
  };
694
854
  }
695
855
  resetRecorderState() {
856
+ this.clearPendingTypeOnlyRecorderInputFlushTimer();
696
857
  this._recorderSessionId = null;
697
858
  this._recorderEvents = [];
859
+ this._recorderPendingTypeOnlyInput = null;
698
860
  this._recorderEventQueue = Promise.resolve();
699
861
  this._studioPreviewRecorderLastTargetPoint = void 0;
700
862
  this._studioPreviewRecorderLastScreenshot = void 0;
@@ -801,8 +963,13 @@ class PlaygroundServer {
801
963
  const initialNavigationEvent = this.buildStudioPreviewInitialNavigationEvent(initialPageState, this._studioPreviewRecorderLastScreenshot);
802
964
  if (initialNavigationEvent) this._recorderEvents.push(initialNavigationEvent);
803
965
  }
966
+ persistStudioPreviewRecorderScreenshot(screenshot) {
967
+ if (!this._recorderSessionId) return;
968
+ return persistRecorderScreenshotAsset(this._recorderSessionId, screenshot);
969
+ }
804
970
  async storeStudioPreviewRecorderEvent(payload, snapshotBefore, agent) {
805
971
  if (!this._recorderSessionId) return;
972
+ const sessionId = this._recorderSessionId;
806
973
  const before = snapshotBefore || await this.captureRecorderSnapshotBeforeInteract();
807
974
  const screenshotBefore = before?.screenshot;
808
975
  debugInteract('recorder capture scheduled after action %o', {
@@ -830,6 +997,48 @@ class PlaygroundServer {
830
997
  this._studioPreviewRecorderLastScreenshot = screenshotAfter;
831
998
  this._studioPreviewRecorderLastPageState = pageStateAfter;
832
999
  this.queueStudioPreviewRecorderEventAppend(event, navigationEvent);
1000
+ if (!navigationEvent) this.observeStudioPreviewNavigationChange(payload, before?.pageState, event.hashId, sessionId);
1001
+ }
1002
+ observeStudioPreviewNavigationChange(payload, pageStateBefore, triggerEventHashId, sessionId) {
1003
+ const actionType = 'string' == typeof payload.actionType ? payload.actionType : void 0;
1004
+ const beforeUrl = pageStateBefore?.url;
1005
+ if (!actionType || !beforeUrl || BROWSER_CHROME_NAVIGATION_ACTIONS.has(actionType)) return;
1006
+ (async ()=>{
1007
+ const deadline = Date.now() + RECORDER_NAVIGATION_OBSERVATION_TIMEOUT_MS;
1008
+ while(this._recorderSessionId === sessionId && Date.now() < deadline){
1009
+ await sleep(RECORDER_NAVIGATION_OBSERVATION_INTERVAL_MS);
1010
+ if (this._recorderSessionId !== sessionId) return;
1011
+ const currentUrl = await this.getActivePageUrl();
1012
+ if (!currentUrl || currentUrl === beforeUrl) continue;
1013
+ const screenshot = await this.takeRecorderScreenshot();
1014
+ const pageStateAfter = await this.getActiveRecorderPageState();
1015
+ const navigationEvent = this.buildStudioPreviewNavigationChangeEvent(payload, pageStateBefore, pageStateAfter, screenshot);
1016
+ if (!navigationEvent) return;
1017
+ navigationEvent.rawPayload = {
1018
+ ...navigationEvent.rawPayload,
1019
+ triggerEventHashId
1020
+ };
1021
+ if (this.appendStudioPreviewNavigationEventAfterTrigger(triggerEventHashId, navigationEvent)) {
1022
+ this._studioPreviewRecorderLastScreenshot = screenshot;
1023
+ this._studioPreviewRecorderLastPageState = pageStateAfter;
1024
+ }
1025
+ return;
1026
+ }
1027
+ })().catch((error)=>{
1028
+ debugInteract('delayed recorder navigation observation failed:', error);
1029
+ });
1030
+ }
1031
+ appendStudioPreviewNavigationEventAfterTrigger(triggerEventHashId, navigationEvent) {
1032
+ const triggerIndex = this._recorderEvents.findIndex((event)=>event.hashId === triggerEventHashId);
1033
+ if (triggerIndex < 0) return false;
1034
+ const alreadyRecorded = this._recorderEvents.some((event)=>{
1035
+ if ('NavigationChanged' !== event.actionType) return false;
1036
+ const rawPayload = event.rawPayload;
1037
+ return rawPayload?.triggerEventHashId === triggerEventHashId;
1038
+ });
1039
+ if (alreadyRecorded) return false;
1040
+ this._recorderEvents.splice(triggerIndex + 1, 0, navigationEvent);
1041
+ return true;
833
1042
  }
834
1043
  async buildStudioPreviewRecorderEvent(payload, pageState, screenshotBefore, screenshotAfter) {
835
1044
  const actionType = 'string' == typeof payload.actionType ? payload.actionType : void 0;
@@ -849,10 +1058,12 @@ class PlaygroundServer {
849
1058
  const endX = 'number' == typeof payload.endX ? payload.endX : void 0;
850
1059
  const endY = 'number' == typeof payload.endY ? payload.endY : void 0;
851
1060
  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;
852
- const screenshotWithMarker = void 0 !== x && void 0 !== y ? await this.createRecorderScreenshotWithMarker(screenshotBefore || screenshotAfter, pageInfo, {
853
- x,
854
- y
855
- }) : void 0;
1061
+ const shouldPersistScreenshot = shouldPersistStudioPreviewRecorderScreenshot(actionType);
1062
+ const retainedScreenshot = shouldPersistScreenshot ? screenshotBefore || screenshotAfter : void 0;
1063
+ const deferScreenshotPersistence = 'Input' === actionType && 'typeOnly' === payload.mode;
1064
+ const screenshotAsset = deferScreenshotPersistence ? void 0 : this.persistStudioPreviewRecorderScreenshot(retainedScreenshot);
1065
+ const shouldDiscardDataUrlScreenshot = Boolean(retainedScreenshot?.startsWith('data:'));
1066
+ const hasRetainedScreenshot = Boolean(screenshotAsset || deferScreenshotPersistence && retainedScreenshot || !shouldDiscardDataUrlScreenshot && retainedScreenshot);
856
1067
  const base = {
857
1068
  source: 'studio-preview',
858
1069
  actionType,
@@ -860,13 +1071,18 @@ class PlaygroundServer {
860
1071
  pageInfo,
861
1072
  url,
862
1073
  title,
863
- screenshotBefore,
864
- screenshotAfter,
865
- screenshotWithBox: screenshotWithMarker,
866
- semantic: {
1074
+ ...screenshotAsset ? {
1075
+ screenshotAsset
1076
+ } : deferScreenshotPersistence ? retainedScreenshot ? {
1077
+ screenshotWithBox: retainedScreenshot
1078
+ } : {} : shouldDiscardDataUrlScreenshot ? {} : {
1079
+ screenshotBefore,
1080
+ screenshotAfter
1081
+ },
1082
+ semantic: hasRetainedScreenshot ? {
867
1083
  source: 'aiDescribe',
868
1084
  status: 'pending'
869
- },
1085
+ } : buildFailedAiDescribeRecorderSemantic('Recorder screenshot was not retained because asset storage is unavailable or its quota was reached.'),
870
1086
  timestamp,
871
1087
  hashId: `studio-preview-${actionType}-${timestamp}-${Math.random().toString(36).slice(2, 8)}`
872
1088
  };
@@ -967,7 +1183,7 @@ class PlaygroundServer {
967
1183
  async enrichStudioPreviewRecorderEventWithAiDescribe(event, agent) {
968
1184
  const startedAt = new Date();
969
1185
  const startedAtMs = Date.now();
970
- const eventScreenshot = this.getRecorderAiDescribeScreenshot(event);
1186
+ const eventScreenshot = await this.getRecorderAiDescribeScreenshot(event);
971
1187
  const traceBase = createRecorderAiDescribeTraceBase(event, eventScreenshot);
972
1188
  const finishTrace = async (status, extra = {})=>{
973
1189
  let screenshotRef;
@@ -1032,11 +1248,11 @@ class PlaygroundServer {
1032
1248
  let elementDescription;
1033
1249
  let deepLocate;
1034
1250
  let verifyResult;
1251
+ const verifyPrompt = RECORDER_AI_DESCRIBE_VERIFY_PROMPT;
1035
1252
  try {
1036
1253
  if ('number' != typeof x || 'number' != typeof y) throw new Error('Skipped aiDescribe because the recorder event has no stable point target.');
1037
1254
  if (!eventScreenshot) throw new Error('Skipped aiDescribe because the recorder event has no screenshot.');
1038
1255
  if (!event.pageInfo?.width || !event.pageInfo?.height) throw new Error('Skipped aiDescribe because the recorder event has no pageInfo for coordinate mapping.');
1039
- const verifyPrompt = shouldVerifyRecorderAiDescribeEvent(event);
1040
1256
  modelCallStartedAt = Date.now();
1041
1257
  const elementDescriber = createElementDescriberRuntime(agent);
1042
1258
  const describeResult = await withTimeout(describeElementAtPoint(elementDescriber, [
@@ -1063,6 +1279,7 @@ class PlaygroundServer {
1063
1279
  const trace = await finishTrace('ready', {
1064
1280
  modelCallDurationMs,
1065
1281
  elementDescription,
1282
+ verifyPrompt,
1066
1283
  verifyPassed: false,
1067
1284
  centerDistance: verifyResult.centerDistance,
1068
1285
  verifyResult
@@ -1093,6 +1310,7 @@ class PlaygroundServer {
1093
1310
  const trace = await finishTrace('ready', {
1094
1311
  modelCallDurationMs,
1095
1312
  elementDescription,
1313
+ verifyPrompt,
1096
1314
  verifyPassed: verifyResult?.pass,
1097
1315
  centerDistance: verifyResult?.centerDistance,
1098
1316
  verifyResult
@@ -1126,6 +1344,7 @@ class PlaygroundServer {
1126
1344
  error: reportedError instanceof Error ? reportedError.message : String(reportedError),
1127
1345
  modelCallDurationMs,
1128
1346
  elementDescription,
1347
+ verifyPrompt,
1129
1348
  verifyPassed: verifyResult?.pass,
1130
1349
  centerDistance: verifyResult?.centerDistance,
1131
1350
  verifyResult
@@ -1133,7 +1352,7 @@ class PlaygroundServer {
1133
1352
  debugInteract('recorder aiDescribe trace:', trace);
1134
1353
  const aiDescribeDetails = verifyResult || trace.annotatedScreenshotRef ? {
1135
1354
  aiDescribe: {
1136
- verifyPrompt: true,
1355
+ verifyPrompt,
1137
1356
  verifyPassed: verifyResult?.pass,
1138
1357
  deepLocate,
1139
1358
  centerDistance: verifyResult?.centerDistance,
@@ -1154,16 +1373,92 @@ class PlaygroundServer {
1154
1373
  };
1155
1374
  }
1156
1375
  }
1157
- getRecorderAiDescribeScreenshot(event) {
1376
+ async getRecorderAiDescribeScreenshot(event) {
1377
+ if (event.screenshotAsset) {
1378
+ const screenshot = readRecorderScreenshotAsset(event.screenshotAsset);
1379
+ if (screenshot) return screenshot;
1380
+ }
1158
1381
  if ('click' === event.type || 'input' === event.type || 'keydown' === event.type || 'drag' === event.type) return event.screenshotBefore || event.screenshotAfter;
1159
- return event.screenshotAfter || event.screenshotBefore;
1382
+ return event.screenshotAfter || event.screenshotBefore || event.screenshotWithBox;
1160
1383
  }
1161
1384
  queueStudioPreviewRecorderEventAppend(event, navigationEvent) {
1162
1385
  const sessionId = this._recorderSessionId;
1163
1386
  if (!sessionId) return;
1387
+ if (this.isDeferredTypeOnlyRecorderInput(event) && !navigationEvent) {
1388
+ const pending = this._recorderPendingTypeOnlyInput;
1389
+ if (pending && this.canCoalesceDeferredTypeOnlyRecorderInput(pending, event)) this._recorderPendingTypeOnlyInput = this.mergeDeferredTypeOnlyRecorderInput(pending, event);
1390
+ else {
1391
+ this.flushPendingTypeOnlyRecorderInput();
1392
+ this._recorderPendingTypeOnlyInput = event;
1393
+ }
1394
+ this.schedulePendingTypeOnlyRecorderInputFlush();
1395
+ return;
1396
+ }
1397
+ this.flushPendingTypeOnlyRecorderInput();
1164
1398
  this._recorderEvents.push(event);
1165
1399
  if (navigationEvent) this._recorderEvents.push(navigationEvent);
1166
1400
  }
1401
+ isDeferredTypeOnlyRecorderInput(event) {
1402
+ return 'studio-preview' === event.source && 'input' === event.type && 'Input' === event.actionType && event.rawPayload?.mode === 'typeOnly';
1403
+ }
1404
+ canCoalesceDeferredTypeOnlyRecorderInput(current, next) {
1405
+ return current.url === next.url && current.title === next.title && current.rawPayload?.x === next.rawPayload?.x && current.rawPayload?.y === next.rawPayload?.y;
1406
+ }
1407
+ mergeDeferredTypeOnlyRecorderInput(current, next) {
1408
+ const value = `${current.value || ''}${next.value || ''}`;
1409
+ return {
1410
+ ...current,
1411
+ value,
1412
+ rawPayload: {
1413
+ ...current.rawPayload,
1414
+ ...next.rawPayload,
1415
+ value
1416
+ },
1417
+ pageInfo: next.pageInfo || current.pageInfo,
1418
+ screenshotWithBox: next.screenshotWithBox || next.screenshotAfter || next.screenshotBefore || current.screenshotWithBox,
1419
+ timestamp: next.timestamp,
1420
+ mergedHashIds: Array.from(new Set([
1421
+ current.hashId,
1422
+ ...current.mergedHashIds || [],
1423
+ next.hashId,
1424
+ ...next.mergedHashIds || []
1425
+ ]))
1426
+ };
1427
+ }
1428
+ flushPendingTypeOnlyRecorderInput() {
1429
+ this.clearPendingTypeOnlyRecorderInputFlushTimer();
1430
+ const pending = this._recorderPendingTypeOnlyInput;
1431
+ if (!pending) return;
1432
+ this._recorderPendingTypeOnlyInput = null;
1433
+ const screenshot = pending.screenshotWithBox || pending.screenshotAfter || pending.screenshotBefore;
1434
+ const screenshotAsset = this.persistStudioPreviewRecorderScreenshot(screenshot);
1435
+ if (screenshotAsset) return void this._recorderEvents.push({
1436
+ ...pending,
1437
+ screenshotAsset,
1438
+ screenshotBefore: void 0,
1439
+ screenshotAfter: void 0,
1440
+ screenshotWithBox: void 0
1441
+ });
1442
+ this._recorderEvents.push({
1443
+ ...pending,
1444
+ screenshotBefore: void 0,
1445
+ screenshotAfter: void 0,
1446
+ screenshotWithBox: void 0,
1447
+ semantic: buildFailedAiDescribeRecorderSemantic('Recorder screenshot was not retained because asset storage is unavailable or its quota was reached.')
1448
+ });
1449
+ }
1450
+ schedulePendingTypeOnlyRecorderInputFlush() {
1451
+ this.clearPendingTypeOnlyRecorderInputFlushTimer();
1452
+ this._recorderPendingTypeOnlyInputFlushTimer = setTimeout(()=>{
1453
+ this._recorderPendingTypeOnlyInputFlushTimer = void 0;
1454
+ this.flushPendingTypeOnlyRecorderInput();
1455
+ }, RECORDER_TYPE_ONLY_INPUT_SETTLE_DELAY_MS);
1456
+ }
1457
+ clearPendingTypeOnlyRecorderInputFlushTimer() {
1458
+ if (!this._recorderPendingTypeOnlyInputFlushTimer) return;
1459
+ clearTimeout(this._recorderPendingTypeOnlyInputFlushTimer);
1460
+ this._recorderPendingTypeOnlyInputFlushTimer = void 0;
1461
+ }
1167
1462
  queueStudioPreviewRecorderEvent(payload, snapshotBefore, agent) {
1168
1463
  const sessionId = this._recorderSessionId;
1169
1464
  if (!sessionId) return;
@@ -1203,8 +1498,6 @@ class PlaygroundServer {
1203
1498
  url: afterUrl,
1204
1499
  title: pageStateAfter.title,
1205
1500
  value: afterUrl,
1206
- screenshotBefore: screenshotAfter,
1207
- screenshotAfter,
1208
1501
  semantic: buildReadyRecorderSemantic('heuristic', semanticEvent, afterUrl, 'high'),
1209
1502
  timestamp,
1210
1503
  hashId: `studio-preview-navigation-${timestamp}-${Math.random().toString(36).slice(2, 8)}`
@@ -1228,26 +1521,11 @@ class PlaygroundServer {
1228
1521
  url: pageState.url,
1229
1522
  title: pageState.title,
1230
1523
  value: pageState.url,
1231
- screenshotBefore: screenshot,
1232
- screenshotAfter: screenshot,
1233
1524
  semantic: buildReadyRecorderSemantic('heuristic', semanticEvent, pageState.url, 'high'),
1234
1525
  timestamp,
1235
1526
  hashId: `studio-preview-initial-navigation-${timestamp}-${Math.random().toString(36).slice(2, 8)}`
1236
1527
  };
1237
1528
  }
1238
- async createRecorderScreenshotWithMarker(screenshot, pageInfo, point) {
1239
- if (!screenshot || !pageInfo.width || !pageInfo.height) return;
1240
- try {
1241
- return await compositePointMarkerImg({
1242
- inputImgBase64: screenshot,
1243
- size: pageInfo,
1244
- point
1245
- });
1246
- } catch (error) {
1247
- debugScreenshot('recorder screenshot marker failed:', error);
1248
- return;
1249
- }
1250
- }
1251
1529
  async destroyCurrentAgent({ preserveActiveStream = false } = {}) {
1252
1530
  if (!this._activeConnection.agent) return;
1253
1531
  try {
@@ -1605,9 +1883,12 @@ class PlaygroundServer {
1605
1883
  error: 'Another task is already running',
1606
1884
  currentTaskId: this.currentTaskId
1607
1885
  });
1886
+ let abortController = null;
1608
1887
  if (requestId) {
1609
1888
  this.currentTaskId = requestId;
1610
1889
  this.taskExecutionDumps[requestId] = null;
1890
+ abortController = new AbortController();
1891
+ this.taskAbortControllers[requestId] = abortController;
1611
1892
  agent.onDumpUpdate = (_dump, executionDump)=>{
1612
1893
  if (executionDump) this.taskExecutionDumps[requestId] = executionDump;
1613
1894
  };
@@ -1630,10 +1911,12 @@ class PlaygroundServer {
1630
1911
  params
1631
1912
  };
1632
1913
  response.result = await executeAction(agent, type, actionSpace, value, {
1914
+ requestId,
1633
1915
  deepLocate,
1634
1916
  deepThink,
1635
1917
  screenshotIncluded,
1636
1918
  domIncluded,
1919
+ abortSignal: abortController?.signal,
1637
1920
  deviceOptions,
1638
1921
  reportDisplay
1639
1922
  });
@@ -1669,6 +1952,7 @@ class PlaygroundServer {
1669
1952
  else console.log(`handle request done after ${timeCost}ms: requestId: ${requestId}`);
1670
1953
  if (requestId) {
1671
1954
  delete this.taskExecutionDumps[requestId];
1955
+ delete this.taskAbortControllers[requestId];
1672
1956
  if (this.currentTaskId === requestId) this.currentTaskId = null;
1673
1957
  }
1674
1958
  });
@@ -1700,6 +1984,8 @@ class PlaygroundServer {
1700
1984
  } catch (error) {
1701
1985
  console.warn('Failed to get execution data before cancel:', error);
1702
1986
  }
1987
+ const abortController = this.taskAbortControllers[requestId];
1988
+ abortController?.abort(new Error(`Task cancelled by user: ${requestId}`));
1703
1989
  try {
1704
1990
  await this.recreateAgent({
1705
1991
  preserveActiveStream: true
@@ -1708,6 +1994,7 @@ class PlaygroundServer {
1708
1994
  console.warn('Failed to recreate agent during cancel:', error);
1709
1995
  }
1710
1996
  delete this.taskExecutionDumps[requestId];
1997
+ delete this.taskAbortControllers[requestId];
1711
1998
  this.currentTaskId = null;
1712
1999
  res.json({
1713
2000
  status: 'cancelled',
@@ -1764,6 +2051,7 @@ class PlaygroundServer {
1764
2051
  });
1765
2052
  this._app.post('/recorder/stop', async (_req, res)=>{
1766
2053
  await this.waitForRecorderIdle();
2054
+ this.flushPendingTypeOnlyRecorderInput();
1767
2055
  this._recorderSessionId = null;
1768
2056
  this._studioPreviewRecorderLastScreenshot = void 0;
1769
2057
  this._studioPreviewRecorderLastPageState = void 0;
@@ -1772,6 +2060,7 @@ class PlaygroundServer {
1772
2060
  });
1773
2061
  });
1774
2062
  this._app.get('/recorder/events', async (req, res)=>{
2063
+ if ('false' !== req.query.flushPending) this.flushPendingTypeOnlyRecorderInput();
1775
2064
  const since = 'string' == typeof req.query.since ? Number.parseInt(req.query.since, 10) : 0;
1776
2065
  const startIndex = Number.isFinite(since) && since > 0 ? since : 0;
1777
2066
  res.json({
@@ -1779,6 +2068,48 @@ class PlaygroundServer {
1779
2068
  nextIndex: this._recorderEvents.length
1780
2069
  });
1781
2070
  });
2071
+ this._app.get('/recorder/assets/:assetId', async (req, res)=>{
2072
+ try {
2073
+ const assetId = String(req.params.assetId || '');
2074
+ const filePath = findRecorderScreenshotAssetPath(assetId);
2075
+ if (!filePath) return res.status(404).json({
2076
+ error: 'Recorder screenshot not found'
2077
+ });
2078
+ res.type(filePath.endsWith('.jpg') ? 'image/jpeg' : 'image/png');
2079
+ return res.send(readFileSync(filePath));
2080
+ } catch (error) {
2081
+ return res.status(400).json({
2082
+ error: error instanceof Error ? error.message : 'Invalid recorder screenshot request'
2083
+ });
2084
+ }
2085
+ });
2086
+ this._app.delete('/recorder/assets/session/:sessionId', async (req, res)=>{
2087
+ try {
2088
+ removeRecorderScreenshotAssetsForSession(String(req.params.sessionId || ''));
2089
+ return res.json({
2090
+ ok: true
2091
+ });
2092
+ } catch (error) {
2093
+ return res.status(400).json({
2094
+ ok: false,
2095
+ error: error instanceof Error ? error.message : 'Invalid recorder session id'
2096
+ });
2097
+ }
2098
+ });
2099
+ this._app.post('/recorder/assets/session/:sessionId/prune', async (req, res)=>{
2100
+ try {
2101
+ const assetIds = Array.isArray(req.body?.assetIds) ? req.body.assetIds.filter((assetId)=>'string' == typeof assetId) : [];
2102
+ pruneRecorderScreenshotAssetsForSession(String(req.params.sessionId || ''), assetIds);
2103
+ return res.json({
2104
+ ok: true
2105
+ });
2106
+ } catch (error) {
2107
+ return res.status(400).json({
2108
+ ok: false,
2109
+ error: error instanceof Error ? error.message : 'Invalid recorder screenshot cleanup request'
2110
+ });
2111
+ }
2112
+ });
1782
2113
  this._app.post('/recorder/describe-event', async (req, res)=>{
1783
2114
  const event = req.body?.event;
1784
2115
  if (!event || 'object' != typeof event) return res.status(400).json({
@@ -2126,6 +2457,7 @@ class PlaygroundServer {
2126
2457
  _define_property(this, "sessionSetupState", 'ready');
2127
2458
  _define_property(this, "sessionSetupBlockingReason", void 0);
2128
2459
  _define_property(this, "currentTaskId", null);
2460
+ _define_property(this, "taskAbortControllers", {});
2129
2461
  _define_property(this, "_agentReady", true);
2130
2462
  _define_property(this, "_configDirty", false);
2131
2463
  _define_property(this, "_lastAiConfigSignature", null);
@@ -2135,6 +2467,8 @@ class PlaygroundServer {
2135
2467
  _define_property(this, "_baseSidecars", void 0);
2136
2468
  _define_property(this, "_recorderSessionId", null);
2137
2469
  _define_property(this, "_recorderEvents", []);
2470
+ _define_property(this, "_recorderPendingTypeOnlyInput", null);
2471
+ _define_property(this, "_recorderPendingTypeOnlyInputFlushTimer", void 0);
2138
2472
  _define_property(this, "_recorderEventQueue", Promise.resolve());
2139
2473
  _define_property(this, "_recorderPendingCaptures", 0);
2140
2474
  _define_property(this, "_studioPreviewRecorderLastTargetPoint", void 0);