@yoqa/runner 0.3.24 → 0.3.25

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 (2) hide show
  1. package/dist/index.js +1114 -245
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -16513,7 +16513,8 @@ var runTests = sqliteTable("run_tests", {
16513
16513
  executionMode: text("execution_mode"),
16514
16514
  error: text("error"),
16515
16515
  startedAt: integer("started_at"),
16516
- finishedAt: integer("finished_at")
16516
+ finishedAt: integer("finished_at"),
16517
+ currentCommand: text("current_command")
16517
16518
  });
16518
16519
  var runSteps = sqliteTable("run_steps", {
16519
16520
  id: text("id").primaryKey(),
@@ -16524,6 +16525,7 @@ var runSteps = sqliteTable("run_steps", {
16524
16525
  ok: integer("ok").notNull().default(0),
16525
16526
  latencyMs: integer("latency_ms").notNull().default(0),
16526
16527
  detail: text("detail"),
16528
+ command: text("command"),
16527
16529
  createdAt: integer("created_at").notNull()
16528
16530
  });
16529
16531
 
@@ -16638,7 +16640,8 @@ function ensureSchema(sqlite) {
16638
16640
  execution_mode TEXT,
16639
16641
  error TEXT,
16640
16642
  started_at INTEGER,
16641
- finished_at INTEGER
16643
+ finished_at INTEGER,
16644
+ current_command TEXT
16642
16645
  );
16643
16646
 
16644
16647
  CREATE TABLE IF NOT EXISTS run_steps (
@@ -16650,6 +16653,7 @@ function ensureSchema(sqlite) {
16650
16653
  ok INTEGER NOT NULL DEFAULT 0,
16651
16654
  latency_ms INTEGER NOT NULL DEFAULT 0,
16652
16655
  detail TEXT,
16656
+ command TEXT,
16653
16657
  created_at INTEGER NOT NULL
16654
16658
  );
16655
16659
 
@@ -16668,6 +16672,7 @@ function ensureSchema(sqlite) {
16668
16672
  migrateAppsPrefix(sqlite);
16669
16673
  migrateCaseScripts(sqlite);
16670
16674
  migrateRunExecutionMode(sqlite);
16675
+ migrateRunCommands(sqlite);
16671
16676
  }
16672
16677
  function tableColumns(sqlite, table) {
16673
16678
  const rows = sqlite.query(`PRAGMA table_info(${table})`).all();
@@ -16696,6 +16701,16 @@ function migrateRunExecutionMode(sqlite) {
16696
16701
  addColumnIfMissing(sqlite, "run_tests", "execution_mode", "execution_mode TEXT", testCols);
16697
16702
  }
16698
16703
  }
16704
+ function migrateRunCommands(sqlite) {
16705
+ const testCols = tableColumns(sqlite, "run_tests");
16706
+ if (testCols.size > 0) {
16707
+ addColumnIfMissing(sqlite, "run_tests", "current_command", "current_command TEXT", testCols);
16708
+ }
16709
+ const stepCols = tableColumns(sqlite, "run_steps");
16710
+ if (stepCols.size > 0) {
16711
+ addColumnIfMissing(sqlite, "run_steps", "command", "command TEXT", stepCols);
16712
+ }
16713
+ }
16699
16714
  function migrateProvidersTable(sqlite) {
16700
16715
  const existing = tableColumns(sqlite, "providers");
16701
16716
  if (existing.size === 0)
@@ -16801,7 +16816,7 @@ function getCatalogDb() {
16801
16816
  // package.json
16802
16817
  var package_default = {
16803
16818
  name: "@yoqa/runner",
16804
- version: "0.3.24",
16819
+ version: "0.3.25",
16805
16820
  description: "Yoqa local runner \u2014 Appium + catalog HTTP API (Bun). Start with yoqa serve or the yoqa-runner bin.",
16806
16821
  license: "UNLICENSED",
16807
16822
  type: "module",
@@ -16884,12 +16899,14 @@ function trackMjpegProxy() {
16884
16899
  return controller;
16885
16900
  }
16886
16901
  function abortAllMjpegProxies() {
16902
+ const had = proxies.size > 0;
16887
16903
  for (const controller of [...proxies]) {
16888
16904
  try {
16889
16905
  controller.abort();
16890
16906
  } catch {}
16891
16907
  }
16892
16908
  proxies.clear();
16909
+ return had;
16893
16910
  }
16894
16911
 
16895
16912
  // src/domains/devices/session.ts
@@ -17597,6 +17614,7 @@ function isDeadSessionError(error) {
17597
17614
  const message = error instanceof Error ? error.message : String(error);
17598
17615
  return DEAD_SESSION_RE.test(message);
17599
17616
  }
17617
+ var DEAD_SESSION_RETRY_MS = 200;
17600
17618
  function mjpegSettingsForDevice(options) {
17601
17619
  const physicalIos = options.platform === "ios" && looksLikePhysicalIosUdid(options.deviceId);
17602
17620
  if (physicalIos) {
@@ -17981,10 +17999,17 @@ async function createDeviceSession(options) {
17981
17999
  try {
17982
18000
  return await fn();
17983
18001
  } catch (error) {
17984
- if (isDeadSessionError(error)) {
17985
- notifySessionDead();
18002
+ if (!isDeadSessionError(error))
18003
+ throw error;
18004
+ await Bun.sleep(DEAD_SESSION_RETRY_MS);
18005
+ try {
18006
+ return await fn();
18007
+ } catch (retryError) {
18008
+ if (isDeadSessionError(retryError)) {
18009
+ notifySessionDead();
18010
+ }
18011
+ throw retryError;
17986
18012
  }
17987
- throw error;
17988
18013
  }
17989
18014
  };
17990
18015
  const getWindowSize = async () => guard(() => browser.getWindowSize());
@@ -18048,10 +18073,10 @@ async function createDeviceSession(options) {
18048
18073
  });
18049
18074
  });
18050
18075
  };
18051
- const swipe = async (x1, y1, x2, y2, durationMs = 400) => {
18076
+ const swipe = async (x1, y1, x2, y2, durationMs = 400, swipeOptions) => {
18052
18077
  await gate.withLock(async () => {
18053
18078
  await guard(async () => {
18054
- const size = await getPointerSize();
18079
+ const size = swipeOptions?.coordSpace === "screenshot" ? await getScreenshotCoordSize() : await getPointerSize();
18055
18080
  await injectSwipe(browser, toPx(x1, size.width), toPx(y1, size.height), toPx(x2, size.width), toPx(y2, size.height), durationMs);
18056
18081
  });
18057
18082
  });
@@ -24263,6 +24288,8 @@ var caseScriptTapSchema = exports_external.object({
24263
24288
  y: exports_external.number().min(0).max(1000).optional(),
24264
24289
  label: exports_external.string().min(1).optional(),
24265
24290
  id: exports_external.string().min(1).optional(),
24291
+ double: exports_external.boolean().optional(),
24292
+ durationMs: exports_external.number().min(50).max(5000).optional(),
24266
24293
  reason: exports_external.string().optional()
24267
24294
  }).superRefine((value, ctx) => {
24268
24295
  const hasCoords = value.x != null && value.y != null;
@@ -24273,8 +24300,45 @@ var caseScriptTapSchema = exports_external.object({
24273
24300
  });
24274
24301
  }
24275
24302
  });
24303
+ var caseScriptSwipeSchema = exports_external.object({
24304
+ type: exports_external.literal("swipe"),
24305
+ x: exports_external.number().min(0).max(1000),
24306
+ y: exports_external.number().min(0).max(1000),
24307
+ x2: exports_external.number().min(0).max(1000),
24308
+ y2: exports_external.number().min(0).max(1000),
24309
+ durationMs: exports_external.number().min(50).max(5000).optional(),
24310
+ reason: exports_external.string().optional()
24311
+ });
24312
+ var caseScriptDragSchema = exports_external.object({
24313
+ type: exports_external.literal("drag"),
24314
+ x: exports_external.number().min(0).max(1000),
24315
+ y: exports_external.number().min(0).max(1000),
24316
+ x2: exports_external.number().min(0).max(1000),
24317
+ y2: exports_external.number().min(0).max(1000),
24318
+ durationMs: exports_external.number().min(50).max(5000).optional(),
24319
+ reason: exports_external.string().optional()
24320
+ });
24321
+ var caseScriptAppIdSchema = exports_external.union([
24322
+ exports_external.object({
24323
+ type: exports_external.literal("activate-app"),
24324
+ appId: exports_external.string().min(1),
24325
+ reason: exports_external.string().optional()
24326
+ }),
24327
+ exports_external.object({
24328
+ type: exports_external.literal("terminate-app"),
24329
+ appId: exports_external.string().min(1),
24330
+ reason: exports_external.string().optional()
24331
+ }),
24332
+ exports_external.object({
24333
+ type: exports_external.literal("restart-app"),
24334
+ appId: exports_external.string().min(1),
24335
+ reason: exports_external.string().optional()
24336
+ })
24337
+ ]);
24276
24338
  var caseScriptActionSchema = exports_external.union([
24277
24339
  caseScriptTapSchema,
24340
+ caseScriptSwipeSchema,
24341
+ caseScriptDragSchema,
24278
24342
  exports_external.object({
24279
24343
  type: exports_external.literal("type"),
24280
24344
  text: exports_external.string(),
@@ -24296,6 +24360,17 @@ var caseScriptActionSchema = exports_external.union([
24296
24360
  type: exports_external.literal("alert"),
24297
24361
  alertAction: exports_external.union([exports_external.literal("accept"), exports_external.literal("dismiss")]).default("accept"),
24298
24362
  reason: exports_external.string().optional()
24363
+ }),
24364
+ caseScriptAppIdSchema,
24365
+ exports_external.object({
24366
+ type: exports_external.literal("background-app"),
24367
+ seconds: exports_external.number().min(0).max(120).optional(),
24368
+ reason: exports_external.string().optional()
24369
+ }),
24370
+ exports_external.object({
24371
+ type: exports_external.literal("open-url"),
24372
+ url: exports_external.string().min(1),
24373
+ reason: exports_external.string().optional()
24299
24374
  })
24300
24375
  ]);
24301
24376
  var caseScriptSchema = exports_external.object({
@@ -24552,6 +24627,7 @@ var runStepSchema = exports_external.object({
24552
24627
  ok: exports_external.boolean(),
24553
24628
  latencyMs: exports_external.number().nonnegative(),
24554
24629
  detail: exports_external.string().nullable(),
24630
+ command: exports_external.string().nullable().optional(),
24555
24631
  createdAt: exports_external.number().int().nonnegative()
24556
24632
  });
24557
24633
  var runTestSchema = exports_external.object({
@@ -24563,6 +24639,7 @@ var runTestSchema = exports_external.object({
24563
24639
  error: exports_external.string().nullable(),
24564
24640
  startedAt: exports_external.number().int().nonnegative().nullable(),
24565
24641
  finishedAt: exports_external.number().int().nonnegative().nullable(),
24642
+ currentCommand: exports_external.string().nullable().optional(),
24566
24643
  steps: exports_external.array(runStepSchema).optional()
24567
24644
  });
24568
24645
  var runSchema = exports_external.object({
@@ -24736,6 +24813,59 @@ function decodeXmlEntities(value) {
24736
24813
  }
24737
24814
 
24738
24815
  // ../../packages/runner-client/src/shell-script.ts
24816
+ function shellSingleQuote(value) {
24817
+ return `'${value.replace(/'/g, `'\\''`)}'`;
24818
+ }
24819
+ function formatActionShellLine(action) {
24820
+ const parts = ["yoqa", "action", action.kind];
24821
+ if (action.id) {
24822
+ parts.push("--id", shellSingleQuote(action.id));
24823
+ }
24824
+ if (action.label) {
24825
+ parts.push("--label", shellSingleQuote(action.label));
24826
+ }
24827
+ if (action.description) {
24828
+ parts.push("-d", shellSingleQuote(action.description));
24829
+ }
24830
+ if (action.x != null)
24831
+ parts.push("--x", String(Math.round(action.x)));
24832
+ if (action.y != null)
24833
+ parts.push("--y", String(Math.round(action.y)));
24834
+ if (action.x2 != null)
24835
+ parts.push("--x2", String(Math.round(action.x2)));
24836
+ if (action.y2 != null)
24837
+ parts.push("--y2", String(Math.round(action.y2)));
24838
+ if (action.durationMs != null)
24839
+ parts.push("--duration", String(Math.round(action.durationMs)));
24840
+ if (action.double)
24841
+ parts.push("--double");
24842
+ if (action.text != null)
24843
+ parts.push("--text", shellSingleQuote(action.text));
24844
+ if (action.appId != null)
24845
+ parts.push("--app-id", shellSingleQuote(action.appId));
24846
+ if (action.url != null)
24847
+ parts.push("--url", shellSingleQuote(action.url));
24848
+ if (action.seconds != null)
24849
+ parts.push("--seconds", String(action.seconds));
24850
+ if (action.kind === "alert") {
24851
+ if (action.alertAction === "dismiss")
24852
+ parts.push("--dismiss");
24853
+ }
24854
+ return parts.join(" ");
24855
+ }
24856
+ function formatSleepShellLine(seconds) {
24857
+ const safe = Math.max(0, seconds);
24858
+ const rounded = Number.isInteger(safe) ? String(safe) : String(Number(safe.toFixed(3)));
24859
+ return `sleep ${rounded}`;
24860
+ }
24861
+ function formatAssertShellLine(input) {
24862
+ const parts = ["yoqa", "assert", input.assertion, "--text", shellSingleQuote(input.text.trim())];
24863
+ const timeout = input.timeoutSeconds ?? 5;
24864
+ if (timeout !== 5) {
24865
+ parts.push("--timeout", String(timeout));
24866
+ }
24867
+ return parts.join(" ");
24868
+ }
24739
24869
  var DEFAULT_SHELL_SCRIPT_HEADER = [
24740
24870
  "#!/usr/bin/env bash",
24741
24871
  "# Yoqa inspector script",
@@ -25163,9 +25293,16 @@ function normalizeVisionJson(value) {
25163
25293
  out.x = clampNormCoord(out.x);
25164
25294
  if (typeof out.y === "number")
25165
25295
  out.y = clampNormCoord(out.y);
25296
+ if (typeof out.x2 === "number")
25297
+ out.x2 = clampNormCoord(out.x2);
25298
+ if (typeof out.y2 === "number")
25299
+ out.y2 = clampNormCoord(out.y2);
25166
25300
  if (typeof out.ms === "number" && Number.isFinite(out.ms)) {
25167
25301
  out.ms = Math.min(1e4, Math.max(0, out.ms));
25168
25302
  }
25303
+ if (typeof out.durationMs === "number" && Number.isFinite(out.durationMs)) {
25304
+ out.durationMs = Math.min(5000, Math.max(50, Math.round(out.durationMs)));
25305
+ }
25169
25306
  const reason = typeof out.reason === "string" ? out.reason.trim() : "";
25170
25307
  const thoughts = typeof out.thoughts === "string" ? out.thoughts.trim() : "";
25171
25308
  if (reason)
@@ -96053,26 +96190,257 @@ async function completeVision(auth, input) {
96053
96190
  }
96054
96191
 
96055
96192
  // src/domains/runs/agent.ts
96193
+ var swipeDirectionSchema = exports_external.union([
96194
+ exports_external.literal("up"),
96195
+ exports_external.literal("down"),
96196
+ exports_external.literal("left"),
96197
+ exports_external.literal("right")
96198
+ ]);
96056
96199
  var agentDecisionSchema = exports_external.object({
96057
96200
  type: exports_external.union([
96058
96201
  exports_external.literal("tap"),
96202
+ exports_external.literal("swipe"),
96203
+ exports_external.literal("drag"),
96059
96204
  exports_external.literal("type"),
96205
+ exports_external.literal("input"),
96060
96206
  exports_external.literal("wait"),
96061
96207
  exports_external.literal("alert"),
96208
+ exports_external.literal("activate-app"),
96209
+ exports_external.literal("terminate-app"),
96210
+ exports_external.literal("restart-app"),
96211
+ exports_external.literal("background-app"),
96212
+ exports_external.literal("open-url"),
96213
+ exports_external.literal("assert"),
96062
96214
  exports_external.literal("verify"),
96063
96215
  exports_external.literal("done"),
96064
96216
  exports_external.literal("fail")
96065
96217
  ]),
96066
96218
  x: exports_external.number().optional(),
96067
96219
  y: exports_external.number().optional(),
96220
+ x2: exports_external.number().optional(),
96221
+ y2: exports_external.number().optional(),
96222
+ direction: swipeDirectionSchema.optional(),
96223
+ durationMs: exports_external.number().optional(),
96224
+ double: exports_external.boolean().optional(),
96068
96225
  label: exports_external.string().min(1).optional(),
96069
96226
  id: exports_external.string().min(1).optional(),
96227
+ description: exports_external.string().min(1).optional(),
96070
96228
  text: exports_external.string().optional(),
96071
96229
  ms: exports_external.number().optional(),
96072
96230
  alertAction: exports_external.union([exports_external.literal("accept"), exports_external.literal("dismiss")]).optional(),
96231
+ appId: exports_external.string().min(1).optional(),
96232
+ url: exports_external.string().min(1).optional(),
96233
+ seconds: exports_external.number().optional(),
96234
+ assertion: exports_external.union([exports_external.literal("visible"), exports_external.literal("not-visible")]).optional(),
96235
+ timeoutMs: exports_external.number().optional(),
96073
96236
  reason: exports_external.string().min(1),
96074
96237
  thoughts: exports_external.string().min(1)
96238
+ }).superRefine((value, ctx) => {
96239
+ if (value.type === "swipe") {
96240
+ const hasCoords = value.x != null && value.y != null && value.x2 != null && value.y2 != null;
96241
+ if (!hasCoords && value.direction == null) {
96242
+ ctx.addIssue({
96243
+ code: exports_external.ZodIssueCode.custom,
96244
+ message: "swipe requires direction or x,y,x2,y2"
96245
+ });
96246
+ }
96247
+ }
96248
+ if (value.type === "drag") {
96249
+ if (value.x == null || value.y == null || value.x2 == null || value.y2 == null) {
96250
+ ctx.addIssue({
96251
+ code: exports_external.ZodIssueCode.custom,
96252
+ message: "drag requires x,y,x2,y2"
96253
+ });
96254
+ }
96255
+ }
96256
+ if ((value.type === "type" || value.type === "input") && !value.text?.trim()) {
96257
+ ctx.addIssue({ code: exports_external.ZodIssueCode.custom, message: "type/input requires text" });
96258
+ }
96259
+ if (value.type === "open-url" && !value.url?.trim()) {
96260
+ ctx.addIssue({ code: exports_external.ZodIssueCode.custom, message: "open-url requires url" });
96261
+ }
96262
+ if (value.type === "assert" && !value.text?.trim()) {
96263
+ ctx.addIssue({ code: exports_external.ZodIssueCode.custom, message: "assert requires text" });
96264
+ }
96075
96265
  });
96266
+ var FINGER_SWIPE_NORMS = {
96267
+ up: { x: 500, y: 800, x2: 500, y2: 200 },
96268
+ down: { x: 500, y: 200, x2: 500, y2: 800 },
96269
+ left: { x: 800, y: 500, x2: 200, y2: 500 },
96270
+ right: { x: 200, y: 500, x2: 800, y2: 500 }
96271
+ };
96272
+ function resolveSwipeNorm(decision) {
96273
+ if (decision.x != null && decision.y != null && decision.x2 != null && decision.y2 != null) {
96274
+ return { x: decision.x, y: decision.y, x2: decision.x2, y2: decision.y2 };
96275
+ }
96276
+ if (decision.direction)
96277
+ return FINGER_SWIPE_NORMS[decision.direction];
96278
+ return null;
96279
+ }
96280
+ var MAX_SCREEN_SNAPSHOT_ROWS = 80;
96281
+ function padNorm(n) {
96282
+ return String(Math.round(n)).padStart(4, " ");
96283
+ }
96284
+ function formatScreenSnapshot(elements) {
96285
+ if (elements == null)
96286
+ return "(screen tree unavailable)";
96287
+ if (elements.length === 0)
96288
+ return "(empty tree)";
96289
+ const ordered = [...elements].sort((a, b) => a.y - b.y || a.x - b.x);
96290
+ const shown = ordered.slice(0, MAX_SCREEN_SNAPSHOT_ROWS);
96291
+ const lines = shown.map((element) => {
96292
+ const id = element.id?.trim() ? ` id=${element.id.trim()}` : "";
96293
+ const label = element.label?.trim() || "(no label)";
96294
+ return `${padNorm(element.x)},${padNorm(element.y)} ${Math.round(element.width)}x${Math.round(element.height)}${id} ${label}`;
96295
+ });
96296
+ if (ordered.length > shown.length) {
96297
+ lines.push(`\u2026 ${ordered.length - shown.length} more elements omitted`);
96298
+ }
96299
+ return lines.join(`
96300
+ `);
96301
+ }
96302
+ function decisionToActionRequest(decision, options = {}) {
96303
+ const appId = decision.appId?.trim() || options.defaultAppId?.trim() || undefined;
96304
+ switch (decision.type) {
96305
+ case "tap": {
96306
+ const body = { kind: "tap" };
96307
+ if (decision.double)
96308
+ body.double = true;
96309
+ if (decision.durationMs != null)
96310
+ body.durationMs = decision.durationMs;
96311
+ if (decision.description)
96312
+ body.description = decision.description;
96313
+ if (prefersScreenshotTap(decision)) {
96314
+ body.x = decision.x;
96315
+ body.y = decision.y;
96316
+ } else if (decision.id) {
96317
+ body.id = decision.id;
96318
+ if (decision.label)
96319
+ body.label = decision.label;
96320
+ } else if (decision.label) {
96321
+ body.label = decision.label;
96322
+ } else if (decision.description) {} else {
96323
+ body.x = decision.x ?? 500;
96324
+ body.y = decision.y ?? 500;
96325
+ }
96326
+ return body;
96327
+ }
96328
+ case "swipe": {
96329
+ const swipe = resolveSwipeNorm(decision);
96330
+ if (!swipe)
96331
+ return null;
96332
+ return {
96333
+ kind: "swipe",
96334
+ x: swipe.x,
96335
+ y: swipe.y,
96336
+ x2: swipe.x2,
96337
+ y2: swipe.y2,
96338
+ ...decision.durationMs != null ? { durationMs: decision.durationMs } : {}
96339
+ };
96340
+ }
96341
+ case "drag":
96342
+ if (decision.x == null || decision.y == null || decision.x2 == null || decision.y2 == null) {
96343
+ return null;
96344
+ }
96345
+ return {
96346
+ kind: "drag",
96347
+ x: decision.x,
96348
+ y: decision.y,
96349
+ x2: decision.x2,
96350
+ y2: decision.y2,
96351
+ ...decision.durationMs != null ? { durationMs: decision.durationMs } : {}
96352
+ };
96353
+ case "type":
96354
+ case "input": {
96355
+ const body = { kind: "input", text: decision.text ?? "" };
96356
+ if (decision.id)
96357
+ body.id = decision.id;
96358
+ if (decision.label)
96359
+ body.label = decision.label;
96360
+ if (decision.x != null)
96361
+ body.x = decision.x;
96362
+ if (decision.y != null)
96363
+ body.y = decision.y;
96364
+ if (decision.description)
96365
+ body.description = decision.description;
96366
+ return body;
96367
+ }
96368
+ case "alert":
96369
+ return {
96370
+ kind: "alert",
96371
+ alertAction: decision.alertAction === "dismiss" ? "dismiss" : "accept"
96372
+ };
96373
+ case "activate-app":
96374
+ return appId ? { kind: "activate-app", appId } : null;
96375
+ case "terminate-app":
96376
+ return appId ? { kind: "terminate-app", appId } : null;
96377
+ case "restart-app":
96378
+ return appId ? { kind: "restart-app", appId } : null;
96379
+ case "background-app":
96380
+ return {
96381
+ kind: "background-app",
96382
+ ...decision.seconds != null ? { seconds: decision.seconds } : {}
96383
+ };
96384
+ case "open-url":
96385
+ return decision.url ? { kind: "open-url", url: decision.url } : null;
96386
+ default:
96387
+ return null;
96388
+ }
96389
+ }
96390
+ var SCROLL_INTENT_RE = /\b(scroll|swipe|flick)\b/i;
96391
+ var SCROLL_UP_RE = /\b(scroll\s+up|back\s+up|to the top|content above)\b/i;
96392
+ var SCROLL_UNTIL_END_RE = /\b(scroll[\s\w]{0,48}(bottom|end)|cannot scroll|can not scroll|can't scroll|until you can'?t scroll|no longer scroll|not be able to scroll)\b/i;
96393
+ function looksLikeScrollIntent(text3) {
96394
+ return SCROLL_INTENT_RE.test(text3);
96395
+ }
96396
+ function isScrollUntilEndGoal(instructions, expectedResult) {
96397
+ return SCROLL_UNTIL_END_RE.test(`${instructions} ${expectedResult}`);
96398
+ }
96399
+ function inferFingerDirection(text3) {
96400
+ if (SCROLL_UP_RE.test(text3))
96401
+ return "down";
96402
+ if (/\bscroll\s+left\b/i.test(text3) || /\bswipe\s+left\b/i.test(text3))
96403
+ return "left";
96404
+ if (/\bscroll\s+right\b/i.test(text3) || /\bswipe\s+right\b/i.test(text3))
96405
+ return "right";
96406
+ return "up";
96407
+ }
96408
+ function asDirectionalSwipe(decision) {
96409
+ const text3 = `${decision.reason} ${decision.thoughts}`;
96410
+ return {
96411
+ type: "swipe",
96412
+ direction: inferFingerDirection(text3),
96413
+ reason: decision.reason,
96414
+ thoughts: decision.thoughts
96415
+ };
96416
+ }
96417
+ function coerceScrollIntentToSwipe(decision) {
96418
+ if (decision.type === "tap" && isSystemPermissionLabel(decision.label))
96419
+ return decision;
96420
+ if (decision.type !== "tap" && decision.type !== "wait")
96421
+ return decision;
96422
+ if (!looksLikeScrollIntent(`${decision.reason} ${decision.thoughts}`))
96423
+ return decision;
96424
+ return asDirectionalSwipe(decision);
96425
+ }
96426
+ function continueScrollingInsteadOfComplete(input) {
96427
+ if (input.decision.type !== "verify" && input.decision.type !== "done") {
96428
+ return input.decision;
96429
+ }
96430
+ const hadSwipe = input.recentActions.some((action) => action.type === "swipe");
96431
+ const untilEnd = isScrollUntilEndGoal(input.instructions, input.expectedResult);
96432
+ const claimedScroll = looksLikeScrollIntent(`${input.decision.reason} ${input.decision.thoughts}`);
96433
+ if (!hadSwipe && (untilEnd || claimedScroll)) {
96434
+ return asDirectionalSwipe(input.decision);
96435
+ }
96436
+ if (untilEnd && hadSwipe && input.lastSwipeMovedScreen) {
96437
+ return asDirectionalSwipe(input.decision);
96438
+ }
96439
+ return input.decision;
96440
+ }
96441
+ function screenshotFingerprint(base643) {
96442
+ return String(Bun.hash(base643));
96443
+ }
96076
96444
  var SYSTEM_PERMISSION_LABELS = new Set([
96077
96445
  "allow",
96078
96446
  "don't allow",
@@ -96092,7 +96460,11 @@ function isSystemPermissionLabel(label) {
96092
96460
  return SYSTEM_PERMISSION_LABELS.has(trimmed);
96093
96461
  }
96094
96462
  function prefersScreenshotTap(decision) {
96095
- return decision.x != null && decision.y != null && !isSystemPermissionLabel(decision.label);
96463
+ if (isSystemPermissionLabel(decision.label))
96464
+ return false;
96465
+ if (decision.id?.trim())
96466
+ return false;
96467
+ return decision.x != null && decision.y != null;
96096
96468
  }
96097
96469
  var SYSTEM_PROMPT = `You are a mobile QA agent controlling an app via screenshots.
96098
96470
  A screenshot of the current device screen is ALWAYS attached to the user message as an image. You can see it. Never claim that no screenshot was provided, missing, blank, or unavailable.
@@ -96106,20 +96478,51 @@ Valid shapes:
96106
96478
  {"type":"tap","label":"Allow","reason":"...","thoughts":"..."}
96107
96479
  {"type":"tap","id":"permission_allow_button","reason":"...","thoughts":"..."}
96108
96480
  {"type":"tap","x":0-1000,"y":0-1000,"reason":"...","thoughts":"..."}
96481
+ {"type":"tap","description":"the blue Login button","reason":"...","thoughts":"..."}
96482
+ {"type":"tap","x":0-1000,"y":0-1000,"double":true,"reason":"...","thoughts":"..."}
96483
+ {"type":"tap","x":0-1000,"y":0-1000,"durationMs":2000,"reason":"...","thoughts":"..."}
96484
+ {"type":"swipe","direction":"up|down|left|right","reason":"...","thoughts":"..."}
96485
+ {"type":"swipe","x":0-1000,"y":0-1000,"x2":0-1000,"y2":0-1000,"reason":"...","thoughts":"..."}
96486
+ {"type":"drag","x":0-1000,"y":0-1000,"x2":0-1000,"y2":0-1000,"reason":"...","thoughts":"..."}
96487
+ {"type":"type","text":"...","x":0-1000,"y":0-1000,"reason":"...","thoughts":"..."}
96488
+ {"type":"input","text":"...","id":"email_field","reason":"...","thoughts":"..."}
96109
96489
  {"type":"alert","alertAction":"accept","reason":"...","thoughts":"..."}
96110
- {"type":"type","text":"...","reason":"...","thoughts":"..."}
96490
+ {"type":"activate-app","appId":"com.example.app","reason":"...","thoughts":"..."}
96491
+ {"type":"terminate-app","appId":"com.example.app","reason":"...","thoughts":"..."}
96492
+ {"type":"restart-app","appId":"com.example.app","reason":"...","thoughts":"..."}
96493
+ {"type":"background-app","seconds":3,"reason":"...","thoughts":"..."}
96494
+ {"type":"open-url","url":"https://example.com","reason":"...","thoughts":"..."}
96495
+ {"type":"assert","assertion":"visible|not-visible","text":"...","reason":"...","thoughts":"..."}
96111
96496
  {"type":"wait","ms":500-3000,"reason":"...","thoughts":"..."}
96112
96497
  {"type":"verify","reason":"...","thoughts":"..."}
96113
96498
  {"type":"done","reason":"...","thoughts":"..."}
96114
96499
  {"type":"fail","reason":"...","thoughts":"..."}
96115
96500
 
96116
- Coordinates use a 0\u20131000 normalized grid (0,0 top-left of the attached screenshot). For in-app controls, tap using screenshot x,y from what you see \u2014 do not prefer label or id over those coordinates.
96501
+ Coordinates use a 0\u20131000 normalized grid (0,0 top-left of the attached screenshot). The user message also includes a screen snapshot (cleaned accessibility tree) with the same grid \u2014 x,y is each element's TOP-LEFT, not centre. Centre = x + width/2, y + height/2.
96502
+
96503
+ Use BOTH the screenshot and the screen snapshot:
96504
+ - Prefer snapshot "id=\u2026" when present (same as yoqa action tap --id).
96505
+ - System permission / notification dialogs: {"type":"tap","label":"Allow"} or {"type":"alert","alertAction":"accept"}. Never guess coordinates for these.
96506
+ - For other in-app controls, tap screenshot x,y of the visible control if there is no id.
96507
+ - If the snapshot is empty or the control is only drawn (no label/id), use the screenshot.
96508
+ - If a previous action failed (see Last action error), pick a different target from the snapshot + screenshot \u2014 do not repeat the same id/label/point.
96509
+
96510
+ Swipe direction is the FINGER movement (same as Inspector):
96511
+ - "down" (finger down, y 200\u2192800): scroll UP \u2014 reveal content above
96512
+ - "up" (finger up, y 800\u2192200): scroll DOWN \u2014 reveal content below / to the bottom
96513
+ - "left" / "right": horizontal paging
96514
+ When the case says "scroll up", emit direction "down". When it says "scroll down" or "to the bottom", emit direction "up". Never tap to scroll. Never emit wait as a stand-in for scrolling.
96117
96515
 
96118
96516
  Rules:
96119
- - Look at the attached screenshot before deciding. Base taps on what is visible now.
96120
- - For ordinary app UI, emit {"type":"tap","x":\u2026,"y":\u2026} using the screenshot grid. Include label or id only for system permission / notification dialogs.
96121
- - System permission / notification dialogs (Allow, Don't allow, While using the app): use {"type":"tap","label":"Allow"} or {"type":"alert","alertAction":"accept"}. Never guess coordinates for these buttons \u2014 coordinate taps often miss.
96122
- - If the expected result (or the goal of the instructions) is already visible, return verify or done \u2014 do not keep tapping the same control.
96517
+ - Look at the attached screenshot AND the screen snapshot before deciding.
96518
+ - Double-tap: {"type":"tap","double":true,...}. Long-press: {"type":"tap","durationMs":2000,...}. Drag a slider/chip with type drag, not swipe.
96519
+ - activate-app / terminate-app / restart-app / open-url / background-app match yoqa action. Omit appId to use the catalog app id provided in the user message.
96520
+ - Assert text on the tree with {"type":"assert","assertion":"visible","text":"\u2026"}.
96521
+ - To scroll a list or page, emit swipe \u2014 never a tap in the middle of the screen as a stand-in for scrolling.
96522
+ - "Scroll until you cannot scroll anymore" / "to the bottom": keep emitting swipe (direction "up") until a later screenshot shows the same content. Then verify. Unchanged screens after taps do not count \u2014 taps do not scroll.
96523
+ - You are given ONE current instruction. Complete only that instruction, then return verify or done. Later instructions are hidden on purpose \u2014 do not invent, skip ahead, or combine them.
96524
+ - Do not open unrelated tabs or screens (Wallet, Profile, \u2026) unless the current instruction says to. If the current target is not visible, scroll THIS screen to find it. If it cannot be found, fail with what you see.
96525
+ - When THIS instruction's expected result is visible (or the action is done and the UI has updated), return verify or done. That finishes this instruction only, not the whole case.
96123
96526
  - Splash / loading / blank screens: return wait (do not tap imaginary tabs).
96124
96527
  - Do not repeat the same tap target from Recent actions unless the screenshot still shows you are on the wrong screen.
96125
96528
  - On fail, say what you see (e.g. "splash logo only, no bottom tabs") \u2014 never "no screenshot".`;
@@ -96137,12 +96540,30 @@ function formatRecentActions(actions) {
96137
96540
  }
96138
96541
  return `${n}. tap (${action.x ?? "?"},${action.y ?? "?"}) \u2014 ${action.reason ?? ""}`;
96139
96542
  }
96543
+ if (action.type === "swipe") {
96544
+ if (action.direction) {
96545
+ return `${n}. swipe ${action.direction} \u2014 ${action.reason ?? ""}`;
96546
+ }
96547
+ return `${n}. swipe (${action.x ?? "?"},${action.y ?? "?"})\u2192(${action.x2 ?? "?"},${action.y2 ?? "?"}) \u2014 ${action.reason ?? ""}`;
96548
+ }
96140
96549
  if (action.type === "alert") {
96141
96550
  return `${n}. alert ${action.alertAction ?? "accept"} \u2014 ${action.reason ?? ""}`;
96142
96551
  }
96143
- if (action.type === "type") {
96552
+ if (action.type === "type" || action.type === "input") {
96144
96553
  return `${n}. type "${action.text ?? ""}" \u2014 ${action.reason ?? ""}`;
96145
96554
  }
96555
+ if (action.type === "drag") {
96556
+ return `${n}. drag (${action.x ?? "?"},${action.y ?? "?"})\u2192(${action.x2 ?? "?"},${action.y2 ?? "?"}) \u2014 ${action.reason ?? ""}`;
96557
+ }
96558
+ if (action.type === "assert") {
96559
+ return `${n}. assert ${action.assertion ?? "visible"} "${action.text ?? ""}" \u2014 ${action.reason ?? ""}`;
96560
+ }
96561
+ if (action.type === "activate-app" || action.type === "terminate-app" || action.type === "restart-app") {
96562
+ return `${n}. ${action.type} ${action.appId ?? ""} \u2014 ${action.reason ?? ""}`;
96563
+ }
96564
+ if (action.type === "open-url") {
96565
+ return `${n}. open-url ${action.url ?? ""} \u2014 ${action.reason ?? ""}`;
96566
+ }
96146
96567
  if (action.type === "wait") {
96147
96568
  return `${n}. wait ${action.ms ?? "?"}ms \u2014 ${action.reason ?? ""}`;
96148
96569
  }
@@ -96150,29 +96571,101 @@ function formatRecentActions(actions) {
96150
96571
  }).join(`
96151
96572
  `);
96152
96573
  }
96153
- function isAbsurdNoScreenshotFail(decision) {
96154
- if (decision.type !== "fail")
96155
- return false;
96156
- const text3 = `${decision.reason} ${decision.thoughts}`.toLowerCase();
96157
- return text3.includes("no screenshot") || text3.includes("screenshot provided") || text3.includes("screenshot was not") || text3.includes("without a screenshot") || text3.includes("missing screenshot") || text3.includes("screenshot unavailable") || text3.includes("cannot see the screen") || text3.includes("no image");
96574
+ var LIST_ITEM_START_RE = /^(?:\d+[.)]|\d+\s*[-\u2013]|[-*\u2022])\s+/;
96575
+ function splitInstructionSteps(instructions) {
96576
+ const trimmed = instructions.replace(/\r\n/g, `
96577
+ `).trim();
96578
+ if (!trimmed)
96579
+ return [""];
96580
+ const lines = trimmed.split(`
96581
+ `);
96582
+ const items = [];
96583
+ let current = [];
96584
+ let sawList = false;
96585
+ const pushCurrent = () => {
96586
+ const text3 = current.join(" ").replace(/\s+/g, " ").trim();
96587
+ if (text3)
96588
+ items.push(text3);
96589
+ current = [];
96590
+ };
96591
+ for (const rawLine of lines) {
96592
+ const line = rawLine.trim();
96593
+ if (!line)
96594
+ continue;
96595
+ if (LIST_ITEM_START_RE.test(line)) {
96596
+ sawList = true;
96597
+ pushCurrent();
96598
+ current = [line.replace(LIST_ITEM_START_RE, "").trim()];
96599
+ } else if (current.length > 0) {
96600
+ current.push(line);
96601
+ } else {
96602
+ current = [line];
96603
+ }
96604
+ }
96605
+ pushCurrent();
96606
+ if (!sawList)
96607
+ return [trimmed];
96608
+ return items.length > 0 ? items : [trimmed];
96609
+ }
96610
+ function flattenCaseInstructions(flows2) {
96611
+ const queue = [];
96612
+ for (const flow of flows2) {
96613
+ const steps = splitInstructionSteps(flow.instructions);
96614
+ const usable = steps.length > 0 ? steps : [""];
96615
+ for (let i = 0;i < usable.length; i++) {
96616
+ const last = i === usable.length - 1;
96617
+ queue.push({
96618
+ instructions: usable[i] ?? "",
96619
+ expectedResult: last ? flow.expectedResult : ""
96620
+ });
96621
+ }
96622
+ }
96623
+ return queue.length > 0 ? queue : [{ instructions: "", expectedResult: "" }];
96158
96624
  }
96159
- async function decideNextAction(input) {
96160
- const prompt = [
96625
+ function formatCompletedInstructions(completed) {
96626
+ if (completed.length === 0)
96627
+ return "(none \u2014 this is the first instruction)";
96628
+ return completed.map((text3, index) => `${index + 1}. ${text3}`).join(`
96629
+ `);
96630
+ }
96631
+ function formatDecidePrompt(input) {
96632
+ const ordinal = input.instructionOrdinal ?? input.stepIndex + 1;
96633
+ const total = input.instructionCount ?? 0;
96634
+ const remaining = total > 0 ? Math.max(0, total - ordinal) : 0;
96635
+ const expected = input.expectedResult.trim() ? input.expectedResult : "(none for this instruction \u2014 return verify once you have performed it and the UI has updated)";
96636
+ const progress = total > 0 ? `Instruction ${ordinal} of ${total} \u2014 do ONLY this instruction${remaining > 0 ? `; ${remaining} remain after it` : " (last instruction)"}` : `Step ${input.stepIndex + 1}`;
96637
+ const later = remaining > 0 ? `Later instructions exist (${remaining}) but are hidden. Do not invent or perform them. Do not open unrelated screens to skip ahead.` : "This is the last instruction of the case.";
96638
+ return [
96161
96639
  `App context: ${input.appContext || "(none)"}`,
96162
96640
  `Test case: ${input.caseTitle}`,
96163
- `Step ${input.stepIndex + 1}`,
96164
- `Instructions: ${input.instructions || "(none)"}`,
96165
- `Expected result: ${input.expectedResult || "(none)"}`,
96641
+ progress,
96642
+ "Completed instructions (already done \u2014 do not repeat):",
96643
+ formatCompletedInstructions(input.completedInstructions ?? []),
96644
+ `Current instruction (do ONLY this): ${input.instructions || "(none)"}`,
96645
+ `Expected result for this instruction: ${expected}`,
96646
+ later,
96647
+ `Catalog app id (activate/terminate/restart): ${input.defaultAppId || "(unknown)"}`,
96166
96648
  `Recent actions:
96167
96649
  ${formatRecentActions(input.recentActions ?? [])}`,
96168
- "Look at the attached screenshot image and decide the next action. A screenshot is attached.",
96650
+ `Last action error: ${input.lastError || "(none)"}`,
96651
+ "Screen snapshot (cleaned accessibility tree, 0\u20131000 grid, x,y is top-left):",
96652
+ input.screenSnapshot || "(unavailable)",
96653
+ "Look at the attached screenshot image AND the screen snapshot and decide the next action. A screenshot is attached.",
96169
96654
  'Reply with ONLY the JSON action object, including non-empty "reason" and "thoughts".'
96170
96655
  ].join(`
96171
96656
  `);
96657
+ }
96658
+ function isAbsurdNoScreenshotFail(decision) {
96659
+ if (decision.type !== "fail")
96660
+ return false;
96661
+ const text3 = `${decision.reason} ${decision.thoughts}`.toLowerCase();
96662
+ return text3.includes("no screenshot") || text3.includes("screenshot provided") || text3.includes("screenshot was not") || text3.includes("without a screenshot") || text3.includes("missing screenshot") || text3.includes("screenshot unavailable") || text3.includes("cannot see the screen") || text3.includes("no image");
96663
+ }
96664
+ async function decideNextAction(input) {
96172
96665
  return completeVision(input.auth, {
96173
96666
  schema: agentDecisionSchema,
96174
96667
  system: SYSTEM_PROMPT,
96175
- prompt,
96668
+ prompt: formatDecidePrompt(input),
96176
96669
  imageBase64: input.imageBase64
96177
96670
  });
96178
96671
  }
@@ -96187,18 +96680,24 @@ function buildScriptFromDecisions(decisions, sourceRunId) {
96187
96680
  type: "tap",
96188
96681
  x: decision.x,
96189
96682
  y: decision.y,
96683
+ ...decision.double ? { double: true } : {},
96684
+ ...decision.durationMs != null ? { durationMs: decision.durationMs } : {},
96190
96685
  reason: decision.reason
96191
96686
  });
96192
96687
  } else if (decision.label) {
96193
96688
  actions.push({
96194
96689
  type: "tap",
96195
96690
  label: decision.label,
96691
+ ...decision.double ? { double: true } : {},
96692
+ ...decision.durationMs != null ? { durationMs: decision.durationMs } : {},
96196
96693
  reason: decision.reason
96197
96694
  });
96198
96695
  } else if (decision.id) {
96199
96696
  actions.push({
96200
96697
  type: "tap",
96201
96698
  id: decision.id,
96699
+ ...decision.double ? { double: true } : {},
96700
+ ...decision.durationMs != null ? { durationMs: decision.durationMs } : {},
96202
96701
  reason: decision.reason
96203
96702
  });
96204
96703
  } else {
@@ -96209,12 +96708,70 @@ function buildScriptFromDecisions(decisions, sourceRunId) {
96209
96708
  reason: decision.reason
96210
96709
  });
96211
96710
  }
96212
- } else if (decision.type === "type") {
96711
+ } else if (decision.type === "swipe") {
96712
+ const swipe = resolveSwipeNorm(decision);
96713
+ if (!swipe)
96714
+ continue;
96715
+ actions.push({
96716
+ type: "swipe",
96717
+ x: swipe.x,
96718
+ y: swipe.y,
96719
+ x2: swipe.x2,
96720
+ y2: swipe.y2,
96721
+ ...decision.durationMs != null ? { durationMs: decision.durationMs } : {},
96722
+ reason: decision.reason
96723
+ });
96724
+ } else if (decision.type === "type" || decision.type === "input") {
96213
96725
  actions.push({
96214
96726
  type: "type",
96215
96727
  text: decision.text ?? "",
96216
96728
  reason: decision.reason
96217
96729
  });
96730
+ } else if (decision.type === "drag") {
96731
+ if (decision.x == null || decision.y == null || decision.x2 == null || decision.y2 == null) {
96732
+ continue;
96733
+ }
96734
+ actions.push({
96735
+ type: "drag",
96736
+ x: decision.x,
96737
+ y: decision.y,
96738
+ x2: decision.x2,
96739
+ y2: decision.y2,
96740
+ ...decision.durationMs != null ? { durationMs: decision.durationMs } : {},
96741
+ reason: decision.reason
96742
+ });
96743
+ } else if (decision.type === "activate-app" || decision.type === "terminate-app" || decision.type === "restart-app") {
96744
+ if (!decision.appId)
96745
+ continue;
96746
+ actions.push({
96747
+ type: decision.type,
96748
+ appId: decision.appId,
96749
+ reason: decision.reason
96750
+ });
96751
+ } else if (decision.type === "background-app") {
96752
+ actions.push({
96753
+ type: "background-app",
96754
+ ...decision.seconds != null ? { seconds: decision.seconds } : {},
96755
+ reason: decision.reason
96756
+ });
96757
+ } else if (decision.type === "open-url") {
96758
+ if (!decision.url)
96759
+ continue;
96760
+ actions.push({
96761
+ type: "open-url",
96762
+ url: decision.url,
96763
+ reason: decision.reason
96764
+ });
96765
+ } else if (decision.type === "assert") {
96766
+ if (!decision.text?.trim())
96767
+ continue;
96768
+ actions.push({
96769
+ type: "assert",
96770
+ assertion: decision.assertion === "not-visible" ? "not-visible" : "visible",
96771
+ text: decision.text,
96772
+ ...decision.timeoutMs != null ? { timeoutMs: decision.timeoutMs } : {},
96773
+ reason: decision.reason
96774
+ });
96218
96775
  } else if (decision.type === "wait") {
96219
96776
  actions.push({
96220
96777
  type: "wait",
@@ -98096,8 +98653,8 @@ ${treeSummary || "(none)"}`;
98096
98653
  }
98097
98654
 
98098
98655
  // src/domains/devices/interaction.ts
98099
- async function getScreen(session, options = {}) {
98100
- abortAllMjpegProxies();
98656
+ var MJPEG_PAUSE_SETTLE_MS = 150;
98657
+ async function readScreen(session, options) {
98101
98658
  const raw2 = await session.pageSource();
98102
98659
  const window2 = await session.getWindowSize();
98103
98660
  if (options.full) {
@@ -98110,6 +98667,14 @@ async function getScreen(session, options = {}) {
98110
98667
  elements: cleaned.elements
98111
98668
  };
98112
98669
  }
98670
+ async function getScreen(session, options = {}) {
98671
+ if (options.pauseMjpeg !== false) {
98672
+ const paused = abortAllMjpegProxies();
98673
+ if (paused)
98674
+ await Bun.sleep(MJPEG_PAUSE_SETTLE_MS);
98675
+ }
98676
+ return await readScreen(session, options);
98677
+ }
98113
98678
 
98114
98679
  class ActionValidationError extends Error {
98115
98680
  constructor(message) {
@@ -98164,7 +98729,9 @@ async function performAction(session, body) {
98164
98729
  throw new ActionValidationError(`${body.kind} requires x,y,x2,y2`);
98165
98730
  }
98166
98731
  if (body.kind === "swipe") {
98167
- await session.swipe(x, y, body.x2, body.y2, body.durationMs);
98732
+ await session.swipe(x, y, body.x2, body.y2, body.durationMs, {
98733
+ coordSpace: "screenshot"
98734
+ });
98168
98735
  } else {
98169
98736
  await session.drag(x, y, body.x2, body.y2, body.durationMs);
98170
98737
  }
@@ -98232,14 +98799,77 @@ var defaultClock = {
98232
98799
  sleep: (ms) => new Promise((resolve7) => setTimeout(resolve7, ms)),
98233
98800
  now: () => Date.now()
98234
98801
  };
98802
+ var noopSetCurrentCommand = async () => {};
98803
+ async function withCurrentCommand(setCurrentCommand, command, work) {
98804
+ await setCurrentCommand(command);
98805
+ try {
98806
+ return await work();
98807
+ } finally {
98808
+ await setCurrentCommand(null);
98809
+ }
98810
+ }
98811
+ function isRetriableActionError(error40) {
98812
+ return error40 instanceof ActionNotFoundError || error40 instanceof ActionValidationError;
98813
+ }
98814
+ async function readCleanedTree(readScreen2, session) {
98815
+ try {
98816
+ const screen = await readScreen2(session);
98817
+ const elements = screen.elements ?? [];
98818
+ return { snapshot: formatScreenSnapshot(elements), elements };
98819
+ } catch (error40) {
98820
+ if (isDeadSessionError(error40))
98821
+ throw error40;
98822
+ const message = error40 instanceof Error ? error40.message : String(error40);
98823
+ return { snapshot: `(screen tree unavailable: ${message})`, elements: [] };
98824
+ }
98825
+ }
98826
+ async function runTextAssert(input) {
98827
+ const deadline = input.clock.now() + input.timeoutMs;
98828
+ for (;; ) {
98829
+ if (input.isAborted()) {
98830
+ throw new Error("Aborted");
98831
+ }
98832
+ const screen = await input.readScreen(input.session);
98833
+ const found = screenHasText(screen.elements, input.text);
98834
+ if (input.assertion === "visible" && found)
98835
+ return;
98836
+ if (input.assertion === "not-visible" && !found)
98837
+ return;
98838
+ if (input.clock.now() >= deadline) {
98839
+ throw new Error(input.assertion === "visible" ? `Expected visible text not found within ${Math.round(input.timeoutMs / 1000)}s: ${input.text}` : `Unexpected text still visible after ${Math.round(input.timeoutMs / 1000)}s: ${input.text}`);
98840
+ }
98841
+ await input.clock.sleep(400);
98842
+ }
98843
+ }
98844
+ function commandForDecision(decision, defaultAppId) {
98845
+ if (decision.type === "verify" || decision.type === "done" || decision.type === "fail") {
98846
+ return null;
98847
+ }
98848
+ if (decision.type === "wait") {
98849
+ const waitMs = Math.min(3000, Math.max(500, decision.ms ?? 1500));
98850
+ return formatSleepShellLine(waitMs / 1000);
98851
+ }
98852
+ if (decision.type === "assert") {
98853
+ const assertion = decision.assertion === "not-visible" ? "not-visible" : "visible";
98854
+ const timeoutMs = Math.min(60000, Math.max(1000, decision.timeoutMs ?? 5000));
98855
+ return formatAssertShellLine({
98856
+ assertion,
98857
+ text: decision.text ?? "",
98858
+ timeoutSeconds: timeoutMs / 1000
98859
+ });
98860
+ }
98861
+ const body = decisionToActionRequest(decision, { defaultAppId });
98862
+ return body ? formatActionShellLine(body) : null;
98863
+ }
98235
98864
  async function executeScriptCase(deps) {
98236
98865
  const perform = deps.performAction ?? performAction;
98237
- const readScreen = deps.readScreen ?? (async (session) => {
98866
+ const readScreen2 = deps.readScreen ?? (async (session) => {
98238
98867
  const screen = await getScreen(session, { full: false });
98239
98868
  return { elements: screen.elements };
98240
98869
  });
98241
98870
  const clock = deps.clock ?? defaultClock;
98242
98871
  const settleMs = deps.settleMs ?? POST_ACTION_SETTLE_MS;
98872
+ const setCurrentCommand = deps.setCurrentCommand ?? noopSetCurrentCommand;
98243
98873
  let stepIdx = 0;
98244
98874
  let lastScreenshotUri = null;
98245
98875
  try {
@@ -98264,108 +98894,274 @@ async function executeScriptCase(deps) {
98264
98894
  tapBody.x = action.x;
98265
98895
  if (action.y != null)
98266
98896
  tapBody.y = action.y;
98267
- await perform(deps.session, tapBody);
98268
- await clock.sleep(settleMs);
98269
- await deps.appendStep({
98270
- idx: stepIdx,
98271
- action: {
98272
- type: "tap",
98273
- x: action.x,
98274
- y: action.y,
98275
- label: action.label,
98276
- id: action.id,
98277
- reason: action.reason ?? "Replayed saved script tap",
98278
- thoughts: "Replaying the saved script without calling the AI agent."
98279
- },
98280
- screenshotUri: shot.path,
98281
- ok: true,
98282
- latencyMs,
98283
- detail: action.reason ?? action.label ?? action.id ?? null
98897
+ if (action.double)
98898
+ tapBody.double = true;
98899
+ if (action.durationMs != null)
98900
+ tapBody.durationMs = action.durationMs;
98901
+ const command = formatActionShellLine(tapBody);
98902
+ await withCurrentCommand(setCurrentCommand, command, async () => {
98903
+ await perform(deps.session, tapBody);
98904
+ await clock.sleep(settleMs);
98905
+ await deps.appendStep({
98906
+ idx: stepIdx,
98907
+ action: {
98908
+ type: "tap",
98909
+ x: action.x,
98910
+ y: action.y,
98911
+ label: action.label,
98912
+ id: action.id,
98913
+ reason: action.reason ?? "Replayed saved script tap",
98914
+ thoughts: "Replaying the saved script without calling the AI agent."
98915
+ },
98916
+ screenshotUri: shot.path,
98917
+ ok: true,
98918
+ latencyMs,
98919
+ detail: action.reason ?? action.label ?? action.id ?? null,
98920
+ command
98921
+ });
98922
+ });
98923
+ } else if (action.type === "swipe") {
98924
+ const swipeBody = {
98925
+ kind: "swipe",
98926
+ x: action.x,
98927
+ y: action.y,
98928
+ x2: action.x2,
98929
+ y2: action.y2,
98930
+ ...action.durationMs != null ? { durationMs: action.durationMs } : {}
98931
+ };
98932
+ const command = formatActionShellLine(swipeBody);
98933
+ await withCurrentCommand(setCurrentCommand, command, async () => {
98934
+ await perform(deps.session, swipeBody);
98935
+ await clock.sleep(settleMs);
98936
+ await deps.appendStep({
98937
+ idx: stepIdx,
98938
+ action: {
98939
+ type: "swipe",
98940
+ x: action.x,
98941
+ y: action.y,
98942
+ x2: action.x2,
98943
+ y2: action.y2,
98944
+ durationMs: action.durationMs,
98945
+ reason: action.reason ?? "Replayed saved script swipe",
98946
+ thoughts: "Replaying the saved script without calling the AI agent."
98947
+ },
98948
+ screenshotUri: shot.path,
98949
+ ok: true,
98950
+ latencyMs,
98951
+ detail: action.reason ?? null,
98952
+ command
98953
+ });
98954
+ });
98955
+ } else if (action.type === "drag") {
98956
+ const dragBody = {
98957
+ kind: "drag",
98958
+ x: action.x,
98959
+ y: action.y,
98960
+ x2: action.x2,
98961
+ y2: action.y2,
98962
+ ...action.durationMs != null ? { durationMs: action.durationMs } : {}
98963
+ };
98964
+ const command = formatActionShellLine(dragBody);
98965
+ await withCurrentCommand(setCurrentCommand, command, async () => {
98966
+ await perform(deps.session, dragBody);
98967
+ await clock.sleep(settleMs);
98968
+ await deps.appendStep({
98969
+ idx: stepIdx,
98970
+ action: {
98971
+ type: "drag",
98972
+ x: action.x,
98973
+ y: action.y,
98974
+ x2: action.x2,
98975
+ y2: action.y2,
98976
+ durationMs: action.durationMs,
98977
+ reason: action.reason ?? "Replayed saved script drag",
98978
+ thoughts: "Replaying the saved script without calling the AI agent."
98979
+ },
98980
+ screenshotUri: shot.path,
98981
+ ok: true,
98982
+ latencyMs,
98983
+ detail: action.reason ?? null,
98984
+ command
98985
+ });
98986
+ });
98987
+ } else if (action.type === "activate-app" || action.type === "terminate-app" || action.type === "restart-app") {
98988
+ const appBody = { kind: action.type, appId: action.appId };
98989
+ const command = formatActionShellLine(appBody);
98990
+ await withCurrentCommand(setCurrentCommand, command, async () => {
98991
+ await perform(deps.session, appBody);
98992
+ await clock.sleep(settleMs);
98993
+ await deps.appendStep({
98994
+ idx: stepIdx,
98995
+ action: {
98996
+ type: action.type,
98997
+ appId: action.appId,
98998
+ reason: action.reason ?? `Replayed saved script ${action.type}`,
98999
+ thoughts: "Replaying the saved script without calling the AI agent."
99000
+ },
99001
+ screenshotUri: shot.path,
99002
+ ok: true,
99003
+ latencyMs,
99004
+ detail: action.reason ?? action.appId,
99005
+ command
99006
+ });
99007
+ });
99008
+ } else if (action.type === "background-app") {
99009
+ const backgroundBody = {
99010
+ kind: "background-app",
99011
+ ...action.seconds != null ? { seconds: action.seconds } : {}
99012
+ };
99013
+ const command = formatActionShellLine(backgroundBody);
99014
+ await withCurrentCommand(setCurrentCommand, command, async () => {
99015
+ await perform(deps.session, backgroundBody);
99016
+ await clock.sleep(settleMs);
99017
+ await deps.appendStep({
99018
+ idx: stepIdx,
99019
+ action: {
99020
+ type: "background-app",
99021
+ seconds: action.seconds,
99022
+ reason: action.reason ?? "Replayed saved script background-app",
99023
+ thoughts: "Replaying the saved script without calling the AI agent."
99024
+ },
99025
+ screenshotUri: shot.path,
99026
+ ok: true,
99027
+ latencyMs,
99028
+ detail: action.reason ?? null,
99029
+ command
99030
+ });
99031
+ });
99032
+ } else if (action.type === "open-url") {
99033
+ const urlBody = { kind: "open-url", url: action.url };
99034
+ const command = formatActionShellLine(urlBody);
99035
+ await withCurrentCommand(setCurrentCommand, command, async () => {
99036
+ await perform(deps.session, urlBody);
99037
+ await clock.sleep(settleMs);
99038
+ await deps.appendStep({
99039
+ idx: stepIdx,
99040
+ action: {
99041
+ type: "open-url",
99042
+ url: action.url,
99043
+ reason: action.reason ?? "Replayed saved script open-url",
99044
+ thoughts: "Replaying the saved script without calling the AI agent."
99045
+ },
99046
+ screenshotUri: shot.path,
99047
+ ok: true,
99048
+ latencyMs,
99049
+ detail: action.reason ?? action.url,
99050
+ command
99051
+ });
98284
99052
  });
98285
99053
  } else if (action.type === "type") {
98286
- await perform(deps.session, { kind: "input", text: action.text });
98287
- await clock.sleep(settleMs);
98288
- await deps.appendStep({
98289
- idx: stepIdx,
98290
- action: {
98291
- type: "type",
98292
- text: action.text,
98293
- reason: action.reason ?? "Replayed saved script type",
98294
- thoughts: "Replaying the saved script without calling the AI agent."
98295
- },
98296
- screenshotUri: shot.path,
98297
- ok: true,
98298
- latencyMs,
98299
- detail: action.reason ?? null
99054
+ const typeBody = { kind: "input", text: action.text };
99055
+ const command = formatActionShellLine(typeBody);
99056
+ await withCurrentCommand(setCurrentCommand, command, async () => {
99057
+ await perform(deps.session, typeBody);
99058
+ await clock.sleep(settleMs);
99059
+ await deps.appendStep({
99060
+ idx: stepIdx,
99061
+ action: {
99062
+ type: "type",
99063
+ text: action.text,
99064
+ reason: action.reason ?? "Replayed saved script type",
99065
+ thoughts: "Replaying the saved script without calling the AI agent."
99066
+ },
99067
+ screenshotUri: shot.path,
99068
+ ok: true,
99069
+ latencyMs,
99070
+ detail: action.reason ?? null,
99071
+ command
99072
+ });
98300
99073
  });
98301
99074
  } else if (action.type === "assert") {
98302
99075
  const timeoutMs = action.timeoutMs ?? 5000;
98303
99076
  const assertion = action.assertion;
98304
- const deadline = clock.now() + timeoutMs;
98305
- for (;; ) {
99077
+ const command = formatAssertShellLine({
99078
+ assertion,
99079
+ text: action.text,
99080
+ timeoutSeconds: timeoutMs / 1000
99081
+ });
99082
+ await withCurrentCommand(setCurrentCommand, command, async () => {
99083
+ const deadline = clock.now() + timeoutMs;
99084
+ for (;; ) {
99085
+ if (deps.isAborted()) {
99086
+ return;
99087
+ }
99088
+ const screen = await readScreen2(deps.session);
99089
+ const found = screenHasText(screen.elements, action.text);
99090
+ if (assertion === "visible" && found)
99091
+ break;
99092
+ if (assertion === "not-visible" && !found)
99093
+ break;
99094
+ if (clock.now() >= deadline) {
99095
+ throw new Error(assertion === "visible" ? `Expected visible text not found within ${Math.round(timeoutMs / 1000)}s: ${action.text}` : `Unexpected text still visible after ${Math.round(timeoutMs / 1000)}s: ${action.text}`);
99096
+ }
99097
+ await clock.sleep(400);
99098
+ }
98306
99099
  if (deps.isAborted()) {
98307
- return "cancelled";
99100
+ return;
98308
99101
  }
98309
- const screen = await readScreen(deps.session);
98310
- const found = screenHasText(screen.elements, action.text);
98311
- if (assertion === "visible" && found)
98312
- break;
98313
- if (assertion === "not-visible" && !found)
98314
- break;
98315
- if (clock.now() >= deadline) {
98316
- throw new Error(assertion === "visible" ? `Expected visible text not found within ${Math.round(timeoutMs / 1000)}s: ${action.text}` : `Unexpected text still visible after ${Math.round(timeoutMs / 1000)}s: ${action.text}`);
98317
- }
98318
- await clock.sleep(400);
98319
- }
98320
- await deps.appendStep({
98321
- idx: stepIdx,
98322
- action: {
98323
- type: "assert",
98324
- assertion,
98325
- text: action.text,
98326
- timeoutMs,
98327
- reason: action.reason ?? `Assert ${assertion}: ${action.text}`,
98328
- thoughts: "Replaying the saved script without calling the AI agent."
98329
- },
98330
- screenshotUri: shot.path,
98331
- ok: true,
98332
- latencyMs,
98333
- detail: action.reason ?? `${assertion}: ${action.text}`
99102
+ await deps.appendStep({
99103
+ idx: stepIdx,
99104
+ action: {
99105
+ type: "assert",
99106
+ assertion,
99107
+ text: action.text,
99108
+ timeoutMs,
99109
+ reason: action.reason ?? `Assert ${assertion}: ${action.text}`,
99110
+ thoughts: "Replaying the saved script without calling the AI agent."
99111
+ },
99112
+ screenshotUri: shot.path,
99113
+ ok: true,
99114
+ latencyMs,
99115
+ detail: action.reason ?? `${assertion}: ${action.text}`,
99116
+ command
99117
+ });
98334
99118
  });
99119
+ if (deps.isAborted()) {
99120
+ return "cancelled";
99121
+ }
98335
99122
  } else if (action.type === "alert") {
98336
- await perform(deps.session, {
99123
+ const alertBody = {
98337
99124
  kind: "alert",
98338
99125
  alertAction: action.alertAction === "dismiss" ? "dismiss" : "accept"
98339
- });
98340
- await clock.sleep(settleMs);
98341
- await deps.appendStep({
98342
- idx: stepIdx,
98343
- action: {
98344
- type: "alert",
98345
- alertAction: action.alertAction === "dismiss" ? "dismiss" : "accept",
98346
- reason: action.reason ?? "Replayed saved script alert",
98347
- thoughts: "Replaying the saved script without calling the AI agent."
98348
- },
98349
- screenshotUri: shot.path,
98350
- ok: true,
98351
- latencyMs,
98352
- detail: action.reason ?? action.alertAction ?? "accept"
99126
+ };
99127
+ const command = formatActionShellLine(alertBody);
99128
+ await withCurrentCommand(setCurrentCommand, command, async () => {
99129
+ await perform(deps.session, alertBody);
99130
+ await clock.sleep(settleMs);
99131
+ await deps.appendStep({
99132
+ idx: stepIdx,
99133
+ action: {
99134
+ type: "alert",
99135
+ alertAction: action.alertAction === "dismiss" ? "dismiss" : "accept",
99136
+ reason: action.reason ?? "Replayed saved script alert",
99137
+ thoughts: "Replaying the saved script without calling the AI agent."
99138
+ },
99139
+ screenshotUri: shot.path,
99140
+ ok: true,
99141
+ latencyMs,
99142
+ detail: action.reason ?? action.alertAction ?? "accept",
99143
+ command
99144
+ });
98353
99145
  });
98354
99146
  } else {
98355
99147
  const waitMs = Math.min(3000, Math.max(500, action.ms));
98356
- await clock.sleep(waitMs);
98357
- await deps.appendStep({
98358
- idx: stepIdx,
98359
- action: {
98360
- type: "wait",
98361
- ms: waitMs,
98362
- reason: action.reason ?? `wait ${waitMs}ms`,
98363
- thoughts: "Replaying the saved script without calling the AI agent."
98364
- },
98365
- screenshotUri: shot.path,
98366
- ok: true,
98367
- latencyMs,
98368
- detail: action.reason ?? `wait ${waitMs}ms`
99148
+ const command = formatSleepShellLine(waitMs / 1000);
99149
+ await withCurrentCommand(setCurrentCommand, command, async () => {
99150
+ await clock.sleep(waitMs);
99151
+ await deps.appendStep({
99152
+ idx: stepIdx,
99153
+ action: {
99154
+ type: "wait",
99155
+ ms: waitMs,
99156
+ reason: action.reason ?? `wait ${waitMs}ms`,
99157
+ thoughts: "Replaying the saved script without calling the AI agent."
99158
+ },
99159
+ screenshotUri: shot.path,
99160
+ ok: true,
99161
+ latencyMs,
99162
+ detail: action.reason ?? `wait ${waitMs}ms`,
99163
+ command
99164
+ });
98369
99165
  });
98370
99166
  }
98371
99167
  stepIdx += 1;
@@ -98380,7 +99176,8 @@ async function executeScriptCase(deps) {
98380
99176
  screenshotUri: lastScreenshotUri,
98381
99177
  ok: true,
98382
99178
  latencyMs: 0,
98383
- detail: "Saved script completed"
99179
+ detail: "Saved script completed",
99180
+ command: null
98384
99181
  });
98385
99182
  return "passed";
98386
99183
  } catch (error40) {
@@ -98398,7 +99195,8 @@ async function executeScriptCase(deps) {
98398
99195
  screenshotUri: lastScreenshotUri,
98399
99196
  ok: false,
98400
99197
  latencyMs: 0,
98401
- detail: message
99198
+ detail: message,
99199
+ command: null
98402
99200
  });
98403
99201
  return "errored";
98404
99202
  }
@@ -98406,165 +99204,214 @@ async function executeScriptCase(deps) {
98406
99204
  async function executeAgentCase(deps) {
98407
99205
  const decide = deps.decide ?? decideNextAction;
98408
99206
  const perform = deps.performAction ?? performAction;
99207
+ const readScreen2 = deps.readScreen ?? (async (session) => {
99208
+ const screen = await getScreen(session, { full: false });
99209
+ return { elements: screen.elements };
99210
+ });
98409
99211
  const clock = deps.clock ?? defaultClock;
98410
99212
  const settleMs = deps.settleMs ?? POST_ACTION_SETTLE_MS;
98411
99213
  const maxSteps = deps.maxStepsPerCase ?? MAX_STEPS_PER_CASE;
99214
+ const setCurrentCommand = deps.setCurrentCommand ?? noopSetCurrentCommand;
98412
99215
  let stepIdx = 0;
98413
99216
  let caseStatus = "passed";
98414
99217
  let caseError = null;
98415
99218
  let lastScreenshotUri = null;
98416
99219
  const recordedDecisions = [];
99220
+ const decideOnce = async (input) => {
99221
+ const payload = {
99222
+ auth: deps.auth,
99223
+ appContext: deps.appContext,
99224
+ caseTitle: deps.catalogCase.name,
99225
+ instructions: input.flow.instructions,
99226
+ expectedResult: input.flow.expectedResult,
99227
+ stepIndex: stepIdx,
99228
+ imageBase64: input.imageBase64,
99229
+ recentActions: input.recentActions,
99230
+ screenSnapshot: input.screenSnapshot,
99231
+ lastError: input.lastError,
99232
+ defaultAppId: deps.defaultAppId,
99233
+ completedInstructions: [...input.completedInstructions],
99234
+ instructionOrdinal: input.instructionOrdinal,
99235
+ instructionCount: input.instructionCount
99236
+ };
99237
+ let decision = await decide(payload);
99238
+ if (isAbsurdNoScreenshotFail(decision)) {
99239
+ decision = await decide(payload);
99240
+ if (isAbsurdNoScreenshotFail(decision)) {
99241
+ decision = {
99242
+ type: "fail",
99243
+ reason: "Model ignored the attached screenshot (claimed none was provided). Try a stronger vision model in Settings \u2192 Provider.",
99244
+ thoughts: "The vision model returned fail claiming no screenshot was available even though an image was attached to the request. After one retry it still denied the screenshot, so the step was marked failed."
99245
+ };
99246
+ }
99247
+ }
99248
+ decision = coerceScrollIntentToSwipe(decision);
99249
+ decision = continueScrollingInsteadOfComplete({
99250
+ decision,
99251
+ instructions: input.flow.instructions,
99252
+ expectedResult: input.flow.expectedResult,
99253
+ recentActions: input.recentActions,
99254
+ lastSwipeMovedScreen: input.lastSwipeMovedScreen
99255
+ });
99256
+ if ((decision.type === "activate-app" || decision.type === "terminate-app" || decision.type === "restart-app") && !decision.appId && deps.defaultAppId) {
99257
+ decision = { ...decision, appId: deps.defaultAppId };
99258
+ }
99259
+ return decision;
99260
+ };
99261
+ const applyDecision = async (decision) => {
99262
+ if (decision.type === "wait") {
99263
+ const waitMs = Math.min(3000, Math.max(500, decision.ms ?? 1500));
99264
+ await clock.sleep(waitMs);
99265
+ return "continue";
99266
+ }
99267
+ if (decision.type === "verify" || decision.type === "done") {
99268
+ return "done";
99269
+ }
99270
+ if (decision.type === "fail") {
99271
+ return "fail";
99272
+ }
99273
+ if (decision.type === "assert") {
99274
+ const assertion = decision.assertion === "not-visible" ? "not-visible" : "visible";
99275
+ const timeoutMs = Math.min(60000, Math.max(1000, decision.timeoutMs ?? 5000));
99276
+ await runTextAssert({
99277
+ session: deps.session,
99278
+ readScreen: readScreen2,
99279
+ clock,
99280
+ isAborted: deps.isAborted,
99281
+ assertion,
99282
+ text: decision.text ?? "",
99283
+ timeoutMs
99284
+ });
99285
+ return "continue";
99286
+ }
99287
+ const body = decisionToActionRequest(decision, { defaultAppId: deps.defaultAppId });
99288
+ if (!body) {
99289
+ throw new Error(`${decision.type} is missing required fields`);
99290
+ }
99291
+ try {
99292
+ await perform(deps.session, body);
99293
+ } catch (error40) {
99294
+ if (error40 instanceof ActionNotFoundError && (body.id || body.label) && decision.x != null && decision.y != null) {
99295
+ await perform(deps.session, {
99296
+ kind: body.kind === "input" ? "input" : "tap",
99297
+ x: decision.x,
99298
+ y: decision.y,
99299
+ text: body.text,
99300
+ double: body.double,
99301
+ durationMs: body.durationMs
99302
+ });
99303
+ } else {
99304
+ throw error40;
99305
+ }
99306
+ }
99307
+ await clock.sleep(settleMs);
99308
+ return "continue";
99309
+ };
98417
99310
  try {
98418
- const flows2 = deps.catalogCase.flows.length > 0 ? deps.catalogCase.flows : [{ id: "empty", instructions: "", expectedResult: "", flowId: null }];
98419
- for (const flow of flows2) {
99311
+ const instructionQueue = flattenCaseInstructions(deps.catalogCase.flows.length > 0 ? deps.catalogCase.flows : [{ id: "empty", instructions: "", expectedResult: "", flowId: null }]);
99312
+ const recentActions = [];
99313
+ const completedInstructions = [];
99314
+ let prevFingerprint = null;
99315
+ for (let instructionIndex = 0;instructionIndex < instructionQueue.length; instructionIndex++) {
99316
+ const instruction = instructionQueue[instructionIndex];
99317
+ if (!instruction)
99318
+ break;
98420
99319
  if (deps.isAborted()) {
98421
99320
  caseStatus = "cancelled";
98422
99321
  break;
98423
99322
  }
98424
- const recentActions = [];
98425
- let flowDone = false;
98426
- for (let attempt = 0;attempt < maxSteps && !flowDone; attempt++) {
99323
+ let instructionDone = false;
99324
+ for (let attempt = 0;attempt < maxSteps && !instructionDone; attempt++) {
98427
99325
  if (deps.isAborted()) {
98428
99326
  caseStatus = "cancelled";
98429
- flowDone = true;
99327
+ instructionDone = true;
98430
99328
  break;
98431
99329
  }
98432
99330
  const shotStarted = clock.now();
98433
99331
  const shot = await deps.session.screenshot();
98434
99332
  lastScreenshotUri = shot.path;
99333
+ const tree = await readCleanedTree(readScreen2, deps.session);
99334
+ const fingerprint = screenshotFingerprint(shot.base64);
99335
+ const lastAction = recentActions.at(-1);
99336
+ const lastSwipeMovedScreen = lastAction?.type === "swipe" && prevFingerprint != null && fingerprint !== prevFingerprint;
98435
99337
  if (deps.isAborted()) {
98436
99338
  caseStatus = "cancelled";
98437
- flowDone = true;
99339
+ instructionDone = true;
98438
99340
  break;
98439
99341
  }
98440
- let decision = await decide({
98441
- auth: deps.auth,
98442
- appContext: deps.appContext,
98443
- caseTitle: deps.catalogCase.name,
98444
- instructions: flow.instructions,
98445
- expectedResult: flow.expectedResult,
98446
- stepIndex: stepIdx,
99342
+ const decideInput = {
99343
+ flow: instruction,
98447
99344
  imageBase64: shot.base64,
98448
- recentActions
98449
- });
98450
- if (isAbsurdNoScreenshotFail(decision)) {
98451
- decision = await decide({
98452
- auth: deps.auth,
98453
- appContext: deps.appContext,
98454
- caseTitle: deps.catalogCase.name,
98455
- instructions: flow.instructions,
98456
- expectedResult: flow.expectedResult,
98457
- stepIndex: stepIdx,
98458
- imageBase64: shot.base64,
98459
- recentActions
98460
- });
98461
- if (isAbsurdNoScreenshotFail(decision)) {
98462
- decision = {
98463
- type: "fail",
98464
- reason: "Model ignored the attached screenshot (claimed none was provided). Try a stronger vision model in Settings \u2192 Provider.",
98465
- thoughts: "The vision model returned fail claiming no screenshot was available even though an image was attached to the request. After one retry it still denied the screenshot, so the step was marked failed."
98466
- };
98467
- }
98468
- }
99345
+ recentActions,
99346
+ screenSnapshot: tree.snapshot,
99347
+ lastSwipeMovedScreen,
99348
+ completedInstructions,
99349
+ instructionOrdinal: instructionIndex + 1,
99350
+ instructionCount: instructionQueue.length
99351
+ };
99352
+ let decision = await decideOnce(decideInput);
99353
+ prevFingerprint = fingerprint;
98469
99354
  const latencyMs = clock.now() - shotStarted;
98470
99355
  if (deps.isAborted()) {
98471
99356
  caseStatus = "cancelled";
98472
- flowDone = true;
99357
+ instructionDone = true;
98473
99358
  break;
98474
99359
  }
98475
- recentActions.push(decision);
98476
- recordedDecisions.push(decision);
98477
- if (decision.type === "tap") {
98478
- const tapBody = { kind: "tap" };
98479
- if (prefersScreenshotTap(decision)) {
98480
- tapBody.x = decision.x;
98481
- tapBody.y = decision.y;
98482
- } else if (decision.label) {
98483
- tapBody.label = decision.label;
98484
- if (decision.id)
98485
- tapBody.id = decision.id;
98486
- } else if (decision.id) {
98487
- tapBody.id = decision.id;
98488
- } else {
98489
- tapBody.x = decision.x ?? 500;
98490
- tapBody.y = decision.y ?? 500;
99360
+ const applyWithRetry = async () => {
99361
+ try {
99362
+ return await applyDecision(decision);
99363
+ } catch (error40) {
99364
+ if (!isRetriableActionError(error40) || deps.isAborted())
99365
+ throw error40;
99366
+ const failed = decision;
99367
+ decision = await decideOnce({
99368
+ ...decideInput,
99369
+ recentActions: [...recentActions, failed],
99370
+ lastError: error40 instanceof Error ? error40.message : String(error40)
99371
+ });
99372
+ const retryCommand = commandForDecision(decision, deps.defaultAppId);
99373
+ await setCurrentCommand(retryCommand);
99374
+ return await applyDecision(decision);
98491
99375
  }
98492
- await perform(deps.session, tapBody);
98493
- await clock.sleep(settleMs);
98494
- await deps.appendStep({
98495
- idx: stepIdx,
98496
- action: decision,
98497
- screenshotUri: shot.path,
98498
- ok: true,
98499
- latencyMs,
98500
- detail: decision.reason ?? null
98501
- });
98502
- } else if (decision.type === "alert") {
98503
- await perform(deps.session, {
98504
- kind: "alert",
98505
- alertAction: decision.alertAction === "dismiss" ? "dismiss" : "accept"
98506
- });
98507
- await clock.sleep(settleMs);
98508
- await deps.appendStep({
98509
- idx: stepIdx,
98510
- action: decision,
98511
- screenshotUri: shot.path,
98512
- ok: true,
98513
- latencyMs,
98514
- detail: decision.reason ?? null
98515
- });
98516
- } else if (decision.type === "type") {
98517
- await perform(deps.session, { kind: "input", text: decision.text ?? "" });
98518
- await clock.sleep(settleMs);
98519
- await deps.appendStep({
98520
- idx: stepIdx,
98521
- action: decision,
98522
- screenshotUri: shot.path,
98523
- ok: true,
98524
- latencyMs,
98525
- detail: decision.reason ?? null
98526
- });
98527
- } else if (decision.type === "wait") {
98528
- const waitMs = Math.min(3000, Math.max(500, decision.ms ?? 1500));
98529
- await clock.sleep(waitMs);
98530
- await deps.appendStep({
98531
- idx: stepIdx,
98532
- action: decision,
98533
- screenshotUri: shot.path,
98534
- ok: true,
98535
- latencyMs,
98536
- detail: decision.reason ?? `wait ${waitMs}ms`
98537
- });
98538
- } else if (decision.type === "verify" || decision.type === "done") {
98539
- await deps.appendStep({
98540
- idx: stepIdx,
98541
- action: decision,
98542
- screenshotUri: shot.path,
98543
- ok: true,
98544
- latencyMs,
98545
- detail: decision.reason ?? null
98546
- });
98547
- flowDone = true;
98548
- } else {
99376
+ };
99377
+ const recordStep = async (command, outcome2) => {
99378
+ recentActions.push(decision);
99379
+ recordedDecisions.push(decision);
98549
99380
  await deps.appendStep({
98550
99381
  idx: stepIdx,
98551
99382
  action: decision,
98552
99383
  screenshotUri: shot.path,
98553
- ok: false,
99384
+ ok: outcome2 !== "fail",
98554
99385
  latencyMs,
98555
- detail: decision.reason ?? "Agent failed the step"
99386
+ detail: decision.type === "wait" ? decision.reason ?? `wait ${Math.min(3000, Math.max(500, decision.ms ?? 1500))}ms` : decision.reason ?? (outcome2 === "fail" ? "Agent failed the step" : null),
99387
+ command
98556
99388
  });
99389
+ };
99390
+ const initialCommand = commandForDecision(decision, deps.defaultAppId);
99391
+ const outcome = initialCommand ? await withCurrentCommand(setCurrentCommand, initialCommand, async () => {
99392
+ const result = await applyWithRetry();
99393
+ await recordStep(commandForDecision(decision, deps.defaultAppId), result);
99394
+ return result;
99395
+ }) : await (async () => {
99396
+ const result = await applyWithRetry();
99397
+ await recordStep(null, result);
99398
+ return result;
99399
+ })();
99400
+ if (outcome === "done") {
99401
+ instructionDone = true;
99402
+ completedInstructions.push(instruction.instructions);
99403
+ } else if (outcome === "fail") {
98557
99404
  caseStatus = "errored";
98558
- caseError = decision.reason ?? "Agent marked the flow as failed";
98559
- flowDone = true;
99405
+ caseError = decision.reason ?? "Agent marked the instruction as failed";
99406
+ instructionDone = true;
98560
99407
  }
98561
99408
  stepIdx += 1;
98562
99409
  }
98563
99410
  if (caseStatus === "errored" || caseStatus === "cancelled")
98564
99411
  break;
98565
- if (!flowDone) {
99412
+ if (!instructionDone) {
98566
99413
  caseStatus = "errored";
98567
- caseError = `Exceeded max steps (${maxSteps}) for a flow`;
99414
+ caseError = `Exceeded max steps (${maxSteps}) for instruction ${instructionIndex + 1} of ${instructionQueue.length}`;
98568
99415
  break;
98569
99416
  }
98570
99417
  }
@@ -98584,7 +99431,8 @@ async function executeAgentCase(deps) {
98584
99431
  screenshotUri: lastScreenshotUri,
98585
99432
  ok: false,
98586
99433
  latencyMs: 0,
98587
- detail: caseError
99434
+ detail: caseError,
99435
+ command: null
98588
99436
  });
98589
99437
  }
98590
99438
  }
@@ -98668,6 +99516,7 @@ async function loadRun(runId) {
98668
99516
  ok: step.ok === 1,
98669
99517
  latencyMs: step.latencyMs,
98670
99518
  detail: step.detail,
99519
+ command: step.command ?? null,
98671
99520
  createdAt: step.createdAt
98672
99521
  }));
98673
99522
  const caseMode = test.executionMode === "script" || test.executionMode === "agent" ? test.executionMode : null;
@@ -98680,6 +99529,7 @@ async function loadRun(runId) {
98680
99529
  error: test.error,
98681
99530
  startedAt: test.startedAt,
98682
99531
  finishedAt: test.finishedAt,
99532
+ currentCommand: test.currentCommand ?? null,
98683
99533
  steps
98684
99534
  });
98685
99535
  }
@@ -98709,9 +99559,14 @@ async function appendStep(input) {
98709
99559
  ok: input.ok ? 1 : 0,
98710
99560
  latencyMs: Math.max(0, Math.round(input.latencyMs)),
98711
99561
  detail: input.detail,
99562
+ command: input.command,
98712
99563
  createdAt: Date.now()
98713
99564
  });
98714
99565
  }
99566
+ async function setCurrentCommand(runTestId, command) {
99567
+ const db = getCatalogDb();
99568
+ await db.update(runTests).set({ currentCommand: command }).where(eq(runTests.id, runTestId));
99569
+ }
98715
99570
  async function updateCaseLastRun(caseId, status, at) {
98716
99571
  const db = getCatalogDb();
98717
99572
  await db.update(cases).set({
@@ -98734,7 +99589,8 @@ async function persistCancelled(runId) {
98734
99589
  await db.update(runTests).set({
98735
99590
  status: "cancelled",
98736
99591
  error: "Cancelled by user",
98737
- finishedAt
99592
+ finishedAt,
99593
+ currentCommand: null
98738
99594
  }).where(eq(runTests.id, test.id));
98739
99595
  }
98740
99596
  }
@@ -98754,6 +99610,9 @@ async function executeScriptCase2(input) {
98754
99610
  ...step
98755
99611
  });
98756
99612
  },
99613
+ setCurrentCommand: async (command) => {
99614
+ await setCurrentCommand(input.runTestId, command);
99615
+ },
98757
99616
  clock: { sleep, now: () => Date.now() }
98758
99617
  });
98759
99618
  }
@@ -98774,8 +99633,12 @@ async function executeAgentCase2(input) {
98774
99633
  ...step
98775
99634
  });
98776
99635
  },
99636
+ setCurrentCommand: async (command) => {
99637
+ await setCurrentCommand(input.runTestId, command);
99638
+ },
98777
99639
  decide: decideNextAction,
98778
- clock: { sleep, now: () => Date.now() }
99640
+ clock: { sleep, now: () => Date.now() },
99641
+ defaultAppId: input.defaultAppId
98779
99642
  });
98780
99643
  }
98781
99644
  async function executeCase(input) {
@@ -98788,7 +99651,8 @@ async function executeCase(input) {
98788
99651
  status: "running",
98789
99652
  startedAt,
98790
99653
  error: null,
98791
- executionMode: input.caseMode
99654
+ executionMode: input.caseMode,
99655
+ currentCommand: null
98792
99656
  }).where(eq(runTests.id, input.runTestId));
98793
99657
  let caseStatus;
98794
99658
  let caseError = null;
@@ -98815,7 +99679,8 @@ async function executeCase(input) {
98815
99679
  caseId: input.caseId,
98816
99680
  appContext: input.appContext,
98817
99681
  session: input.session,
98818
- auth: input.auth
99682
+ auth: input.auth,
99683
+ defaultAppId: input.defaultAppId
98819
99684
  });
98820
99685
  caseStatus = result.status;
98821
99686
  decisions = result.decisions;
@@ -98828,7 +99693,8 @@ async function executeCase(input) {
98828
99693
  await db.update(runTests).set({
98829
99694
  status: caseStatus,
98830
99695
  error: caseError,
98831
- finishedAt
99696
+ finishedAt,
99697
+ currentCommand: null
98832
99698
  }).where(eq(runTests.id, input.runTestId));
98833
99699
  await updateCaseLastRun(input.caseId, caseStatus, finishedAt);
98834
99700
  if (caseStatus === "passed" && input.caseMode === "agent") {
@@ -98926,7 +99792,8 @@ async function executeRun(runId) {
98926
99792
  appContext: app.context,
98927
99793
  session,
98928
99794
  auth,
98929
- caseMode
99795
+ caseMode,
99796
+ defaultAppId: run.platform === "ios" ? app.iosBundleId || undefined : app.androidApplicationId || undefined
98930
99797
  });
98931
99798
  if (status === "cancelled" || isAborted2(runId)) {
98932
99799
  await persistCancelled(runId);
@@ -99406,7 +100273,9 @@ function createSessionRoutes() {
99406
100273
  try {
99407
100274
  const { session } = requireActiveSession();
99408
100275
  const full = c.req.query("full") === "1" || c.req.query("full") === "true";
99409
- const result = await getScreen(session, { full });
100276
+ const pauseQuery = c.req.query("pauseMjpeg");
100277
+ const pauseMjpeg = pauseQuery !== "0" && pauseQuery !== "false";
100278
+ const result = await getScreen(session, { full, pauseMjpeg });
99410
100279
  return c.json(screenResponseSchema.parse(result));
99411
100280
  } catch (error40) {
99412
100281
  const gone = sessionErrorResponse(error40);