@yoqa/runner 0.3.21 → 0.3.23

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 +597 -231
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -16801,7 +16801,7 @@ function getCatalogDb() {
16801
16801
  // package.json
16802
16802
  var package_default = {
16803
16803
  name: "@yoqa/runner",
16804
- version: "0.3.21",
16804
+ version: "0.3.23",
16805
16805
  description: "Yoqa local runner \u2014 Appium + catalog HTTP API (Bun). Start with yoqa serve or the yoqa-runner bin.",
16806
16806
  license: "UNLICENSED",
16807
16807
  type: "module",
@@ -16899,6 +16899,229 @@ import { homedir as homedir6 } from "os";
16899
16899
  import { join as join7 } from "path";
16900
16900
  import { remote } from "webdriverio";
16901
16901
 
16902
+ // src/domains/devices/android-gestures.ts
16903
+ function isAndroidDriver(browser) {
16904
+ return String(browser.capabilities?.platformName ?? "").toLowerCase() === "android";
16905
+ }
16906
+ function toPx(norm, size) {
16907
+ return Math.round(Math.min(1000, Math.max(0, norm)) / 1000 * size);
16908
+ }
16909
+ function pngSizeFromBase64(base64) {
16910
+ if (!base64)
16911
+ return null;
16912
+ try {
16913
+ const buf = Buffer.from(base64.slice(0, 48), "base64");
16914
+ if (buf.length < 24)
16915
+ return null;
16916
+ if (buf[0] !== 137 || buf[1] !== 80 || buf[2] !== 78 || buf[3] !== 71)
16917
+ return null;
16918
+ const width = buf.readUInt32BE(16);
16919
+ const height = buf.readUInt32BE(20);
16920
+ if (width < 1 || height < 1 || width > 16384 || height > 16384)
16921
+ return null;
16922
+ return { width, height };
16923
+ } catch {
16924
+ return null;
16925
+ }
16926
+ }
16927
+ function preferPointerSize(window2, screenshot) {
16928
+ if (!screenshot || screenshot.width < 1 || screenshot.height < 1)
16929
+ return window2;
16930
+ if (window2.width < 1 || window2.height < 1)
16931
+ return screenshot;
16932
+ const dw = Math.abs(screenshot.width - window2.width) / window2.width;
16933
+ const dh = Math.abs(screenshot.height - window2.height) / window2.height;
16934
+ if (dw < 0.02 && dh < 0.02)
16935
+ return screenshot;
16936
+ return window2;
16937
+ }
16938
+ function w3cTapActions(x, y, holdMs) {
16939
+ return [
16940
+ {
16941
+ type: "pointer",
16942
+ id: "finger1",
16943
+ parameters: { pointerType: "touch" },
16944
+ actions: [
16945
+ { type: "pointerMove", duration: 0, x, y },
16946
+ { type: "pointerDown", button: 0 },
16947
+ { type: "pause", duration: holdMs },
16948
+ { type: "pointerUp", button: 0 }
16949
+ ]
16950
+ }
16951
+ ];
16952
+ }
16953
+ function w3cSwipeActions(x1, y1, x2, y2, durationMs) {
16954
+ return [
16955
+ {
16956
+ type: "pointer",
16957
+ id: "finger1",
16958
+ parameters: { pointerType: "touch" },
16959
+ actions: [
16960
+ { type: "pointerMove", duration: 0, x: x1, y: y1 },
16961
+ { type: "pointerDown", button: 0 },
16962
+ { type: "pointerMove", duration: durationMs, x: x2, y: y2 },
16963
+ { type: "pointerUp", button: 0 }
16964
+ ]
16965
+ }
16966
+ ];
16967
+ }
16968
+ async function w3cPerform(browser, actions) {
16969
+ await browser.performActions(actions);
16970
+ try {
16971
+ await browser.releaseActions();
16972
+ } catch {}
16973
+ }
16974
+ async function injectTap(browser, x, y, holdMs) {
16975
+ const duration = Math.max(50, holdMs);
16976
+ if (isAndroidDriver(browser)) {
16977
+ try {
16978
+ await browser.execute("mobile: clickGesture", { x, y, duration });
16979
+ return "clickGesture";
16980
+ } catch {}
16981
+ }
16982
+ await w3cPerform(browser, w3cTapActions(x, y, duration));
16983
+ return "w3c";
16984
+ }
16985
+ async function injectSwipe(browser, x1, y1, x2, y2, durationMs) {
16986
+ const duration = Math.max(50, durationMs);
16987
+ if (isAndroidDriver(browser)) {
16988
+ try {
16989
+ const distance = Math.hypot(x2 - x1, y2 - y1);
16990
+ const speed = Math.max(200, Math.round(distance / (duration / 1000)));
16991
+ await browser.execute("mobile: dragGesture", {
16992
+ startX: x1,
16993
+ startY: y1,
16994
+ endX: x2,
16995
+ endY: y2,
16996
+ speed
16997
+ });
16998
+ return "dragGesture";
16999
+ } catch {}
17000
+ }
17001
+ await w3cPerform(browser, w3cSwipeActions(x1, y1, x2, y2, duration));
17002
+ return "w3c";
17003
+ }
17004
+
17005
+ // src/domains/devices/android-alerts.ts
17006
+ var ANDROID_ACCEPT_BUTTON_LABELS = [
17007
+ "Allow",
17008
+ "ALLOW",
17009
+ "While using the app",
17010
+ "While using this app",
17011
+ "Allow all the time",
17012
+ "Allow only while using the app",
17013
+ "OK"
17014
+ ];
17015
+ var ANDROID_DISMISS_BUTTON_LABELS = [
17016
+ "Don't allow",
17017
+ "Don\u2019t allow",
17018
+ "Don't Allow",
17019
+ "Deny",
17020
+ "DENY",
17021
+ "No thanks"
17022
+ ];
17023
+ var ANDROID_ACCEPT_RESOURCE_IDS = [
17024
+ "com.android.permissioncontroller:id/permission_allow_button",
17025
+ "com.android.permissioncontroller:id/permission_allow_foreground_only_button",
17026
+ "com.android.permissioncontroller:id/permission_allow_one_time_button",
17027
+ "com.android.packageinstaller:id/permission_allow_button",
17028
+ "com.samsung.android.permissioncontroller:id/permission_allow_button"
17029
+ ];
17030
+ var ANDROID_DISMISS_RESOURCE_IDS = [
17031
+ "com.android.permissioncontroller:id/permission_deny_button",
17032
+ "com.android.permissioncontroller:id/permission_deny_and_dont_ask_again_button",
17033
+ "com.android.packageinstaller:id/permission_deny_button",
17034
+ "com.samsung.android.permissioncontroller:id/permission_deny_button"
17035
+ ];
17036
+ function androidAlertAttempts(action) {
17037
+ const labels = action === "accept" ? ANDROID_ACCEPT_BUTTON_LABELS : ANDROID_DISMISS_BUTTON_LABELS;
17038
+ const ids = action === "accept" ? ANDROID_ACCEPT_RESOURCE_IDS : ANDROID_DISMISS_RESOURCE_IDS;
17039
+ const mobile = action === "accept" ? "mobile-acceptAlert" : "mobile-dismissAlert";
17040
+ const attempts = [];
17041
+ for (const buttonLabel of labels) {
17042
+ attempts.push({ method: mobile, buttonLabel });
17043
+ }
17044
+ attempts.push({ method: mobile });
17045
+ for (const id of ids) {
17046
+ attempts.push({ method: "resource-id", id });
17047
+ }
17048
+ for (const text2 of labels) {
17049
+ attempts.push({ method: "text", text: text2 });
17050
+ }
17051
+ return attempts;
17052
+ }
17053
+ function iosAlertAttempts(action) {
17054
+ return [
17055
+ action === "accept" ? { method: "w3c-acceptAlert" } : { method: "w3c-dismissAlert" },
17056
+ { method: "mobile-alert", action }
17057
+ ];
17058
+ }
17059
+ function alertAttemptsFor(browser, action) {
17060
+ return isAndroidDriver(browser) ? androidAlertAttempts(action) : iosAlertAttempts(action);
17061
+ }
17062
+ async function tryClickSelector(browser, selector) {
17063
+ try {
17064
+ const el = browser.$(selector);
17065
+ if (await el.isExisting()) {
17066
+ await el.click();
17067
+ return true;
17068
+ }
17069
+ } catch {
17070
+ return false;
17071
+ }
17072
+ return false;
17073
+ }
17074
+ function xpathExactText(text2) {
17075
+ if (text2.includes("'") && text2.includes('"')) {
17076
+ const parts = text2.split("'").map((part) => `'${part}'`);
17077
+ return `//*[@text=concat(${parts.join(`, "'", `)})]`;
17078
+ }
17079
+ if (text2.includes("'")) {
17080
+ return `//*[@text="${text2}"]`;
17081
+ }
17082
+ return `//*[@text='${text2}']`;
17083
+ }
17084
+ async function runAttempt(browser, attempt) {
17085
+ try {
17086
+ switch (attempt.method) {
17087
+ case "mobile-acceptAlert": {
17088
+ const args = attempt.buttonLabel ? { buttonLabel: attempt.buttonLabel } : {};
17089
+ await browser.execute("mobile: acceptAlert", args);
17090
+ return true;
17091
+ }
17092
+ case "mobile-dismissAlert": {
17093
+ const args = attempt.buttonLabel ? { buttonLabel: attempt.buttonLabel } : {};
17094
+ await browser.execute("mobile: dismissAlert", args);
17095
+ return true;
17096
+ }
17097
+ case "resource-id":
17098
+ return await tryClickSelector(browser, `android=new UiSelector().resourceId("${attempt.id}")`);
17099
+ case "text":
17100
+ return await tryClickSelector(browser, xpathExactText(attempt.text));
17101
+ case "w3c-acceptAlert":
17102
+ await browser.acceptAlert();
17103
+ return true;
17104
+ case "w3c-dismissAlert":
17105
+ await browser.dismissAlert();
17106
+ return true;
17107
+ case "mobile-alert":
17108
+ await browser.execute("mobile: alert", { action: attempt.action });
17109
+ return true;
17110
+ }
17111
+ } catch {
17112
+ return false;
17113
+ }
17114
+ }
17115
+ async function resolveNativeAlert(browser, action) {
17116
+ const attempts = alertAttemptsFor(browser, action);
17117
+ for (const attempt of attempts) {
17118
+ if (await runAttempt(browser, attempt)) {
17119
+ return attempt;
17120
+ }
17121
+ }
17122
+ throw new Error(action === "accept" ? "Could not accept the system alert (no Allow / permission button found)" : "Could not dismiss the system alert (no Don't allow / Deny button found)");
17123
+ }
17124
+
16902
17125
  // src/domains/devices/application.ts
16903
17126
  import { mkdtemp as mkdtemp2, readFile, rm as rm2 } from "fs/promises";
16904
17127
  import { tmpdir as tmpdir2 } from "os";
@@ -17557,7 +17780,7 @@ class ActionGate {
17557
17780
  this.locked = false;
17558
17781
  }
17559
17782
  }
17560
- async handlePointer(browser, toPx, getWindowSize, phase, xNorm, yNorm, seq) {
17783
+ async handlePointer(browser, toPx2, getWindowSize, phase, xNorm, yNorm, seq) {
17561
17784
  if (this.locked && !this.gesture && !this.deferredTap) {
17562
17785
  throw new Error("Device is busy with another action");
17563
17786
  }
@@ -17587,7 +17810,7 @@ class ActionGate {
17587
17810
  await this.flushGesture(pending.gesture, pending.browser, pending.toPx, pending.getWindowSize);
17588
17811
  }
17589
17812
  if (this.gesture) {
17590
- await this.flushGesture(this.gesture, browser, toPx, getWindowSize);
17813
+ await this.flushGesture(this.gesture, browser, toPx2, getWindowSize);
17591
17814
  this.gesture = null;
17592
17815
  }
17593
17816
  this.locked = true;
@@ -17616,16 +17839,16 @@ class ActionGate {
17616
17839
  this.gesture.endXNorm = x;
17617
17840
  this.gesture.endYNorm = y;
17618
17841
  const size = await getWindowSize();
17619
- const startX = toPx(this.gesture.startXNorm, size.width);
17620
- const startY = toPx(this.gesture.startYNorm, size.height);
17621
- const endX = toPx(this.gesture.endXNorm, size.width);
17622
- const endY = toPx(this.gesture.endYNorm, size.height);
17842
+ const startX = toPx2(this.gesture.startXNorm, size.width);
17843
+ const startY = toPx2(this.gesture.startYNorm, size.height);
17844
+ const endX = toPx2(this.gesture.endXNorm, size.width);
17845
+ const endY = toPx2(this.gesture.endYNorm, size.height);
17623
17846
  const distance = Math.hypot(endX - startX, endY - startY);
17624
17847
  const tapSlopPx = 12;
17625
17848
  if (distance >= tapSlopPx || this.gesture.double) {
17626
17849
  const gesture2 = this.gesture;
17627
17850
  this.gesture = null;
17628
- await this.flushGesture(gesture2, browser, toPx, getWindowSize);
17851
+ await this.flushGesture(gesture2, browser, toPx2, getWindowSize);
17629
17852
  return;
17630
17853
  }
17631
17854
  const gesture = this.gesture;
@@ -17640,74 +17863,41 @@ class ActionGate {
17640
17863
  this.locked = false;
17641
17864
  });
17642
17865
  }, doubleTapMs);
17643
- this.deferredTap = { gesture, browser, toPx, getWindowSize, timer };
17866
+ this.deferredTap = { gesture, browser, toPx: toPx2, getWindowSize, timer };
17644
17867
  }
17645
- async flushGesture(gesture, browser, toPx, getWindowSize) {
17868
+ async flushGesture(gesture, browser, toPx2, getWindowSize) {
17646
17869
  const size = await getWindowSize();
17647
- const startX = toPx(gesture.startXNorm, size.width);
17648
- const startY = toPx(gesture.startYNorm, size.height);
17649
- const endX = toPx(gesture.endXNorm, size.width);
17650
- const endY = toPx(gesture.endYNorm, size.height);
17870
+ const startX = toPx2(gesture.startXNorm, size.width);
17871
+ const startY = toPx2(gesture.startYNorm, size.height);
17872
+ const endX = toPx2(gesture.endXNorm, size.width);
17873
+ const endY = toPx2(gesture.endYNorm, size.height);
17651
17874
  const distance = Math.hypot(endX - startX, endY - startY);
17652
17875
  const elapsedMs = Math.max(50, Date.now() - gesture.startedAt);
17653
17876
  const tapSlopPx = 12;
17654
17877
  try {
17655
17878
  if (distance < tapSlopPx || gesture.double) {
17656
17879
  const holdMs = Math.min(200, elapsedMs);
17657
- const tapOnce = async () => {
17658
- await browser.performActions([
17659
- {
17660
- type: "pointer",
17661
- id: "finger1",
17662
- parameters: { pointerType: "touch" },
17663
- actions: [
17664
- { type: "pointerMove", duration: 0, x: startX, y: startY },
17665
- { type: "pointerDown", button: 0 },
17666
- { type: "pause", duration: holdMs },
17667
- { type: "pointerUp", button: 0 }
17668
- ]
17669
- }
17670
- ]);
17671
- try {
17672
- await browser.releaseActions();
17673
- } catch {}
17674
- };
17675
- await tapOnce();
17880
+ await injectTap(browser, startX, startY, holdMs);
17676
17881
  if (gesture.double) {
17677
17882
  await Bun.sleep(50);
17678
- await tapOnce();
17883
+ await injectTap(browser, startX, startY, holdMs);
17679
17884
  }
17680
17885
  } else {
17681
17886
  const duration = Math.min(2000, Math.max(80, elapsedMs));
17682
- await browser.performActions([
17683
- {
17684
- type: "pointer",
17685
- id: "finger1",
17686
- parameters: { pointerType: "touch" },
17687
- actions: [
17688
- { type: "pointerMove", duration: 0, x: startX, y: startY },
17689
- { type: "pointerDown", button: 0 },
17690
- { type: "pointerMove", duration, x: endX, y: endY },
17691
- { type: "pointerUp", button: 0 }
17692
- ]
17693
- }
17694
- ]);
17695
- try {
17696
- await browser.releaseActions();
17697
- } catch {}
17887
+ await injectSwipe(browser, startX, startY, endX, endY, duration);
17698
17888
  }
17699
17889
  } finally {
17700
17890
  this.locked = false;
17701
17891
  }
17702
17892
  }
17703
- async forceEnd(browser, toPx, getWindowSize) {
17893
+ async forceEnd(browser, toPx2, getWindowSize) {
17704
17894
  if (this.deferredTap) {
17705
17895
  clearTimeout(this.deferredTap.timer);
17706
17896
  const pending = this.deferredTap;
17707
17897
  this.deferredTap = null;
17708
- if (toPx && getWindowSize) {
17898
+ if (toPx2 && getWindowSize) {
17709
17899
  try {
17710
- await this.flushGesture(pending.gesture, pending.browser, toPx, getWindowSize);
17900
+ await this.flushGesture(pending.gesture, pending.browser, toPx2, getWindowSize);
17711
17901
  } catch {
17712
17902
  this.locked = false;
17713
17903
  }
@@ -17720,7 +17910,7 @@ class ActionGate {
17720
17910
  this.locked = false;
17721
17911
  return;
17722
17912
  }
17723
- if (!toPx || !getWindowSize) {
17913
+ if (!toPx2 || !getWindowSize) {
17724
17914
  this.gesture = null;
17725
17915
  this.locked = false;
17726
17916
  return;
@@ -17728,7 +17918,7 @@ class ActionGate {
17728
17918
  const gesture = this.gesture;
17729
17919
  this.gesture = null;
17730
17920
  try {
17731
- await this.flushGesture(gesture, browser, toPx, getWindowSize);
17921
+ await this.flushGesture(gesture, browser, toPx2, getWindowSize);
17732
17922
  } catch {
17733
17923
  this.locked = false;
17734
17924
  }
@@ -17791,7 +17981,11 @@ async function createDeviceSession(options) {
17791
17981
  }
17792
17982
  };
17793
17983
  const getWindowSize = async () => guard(() => browser.getWindowSize());
17794
- const toPx = (norm, size) => Math.round(Math.min(1000, Math.max(0, norm)) / 1000 * size);
17984
+ let lastShotSize = null;
17985
+ const getPointerSize = async () => {
17986
+ const window2 = await getWindowSize();
17987
+ return preferPointerSize(window2, lastShotSize);
17988
+ };
17795
17989
  const owned = { current: null };
17796
17990
  const quit = async () => {
17797
17991
  if (openByDeviceId.get(options.deviceId) === owned.current) {
@@ -17799,7 +17993,7 @@ async function createDeviceSession(options) {
17799
17993
  }
17800
17994
  if (gate.isPointerActive() && !sessionDeadNotified) {
17801
17995
  try {
17802
- await withTimeout(gate.forceEnd(browser, toPx, getWindowSize), 3000, "live gesture flush");
17996
+ await withTimeout(gate.forceEnd(browser, toPx, getPointerSize), 3000, "live gesture flush");
17803
17997
  } catch (error) {
17804
17998
  console.warn("[yoqa-runner] gesture flush on quit:", error instanceof Error ? error.message : error);
17805
17999
  }
@@ -17818,6 +18012,7 @@ async function createDeviceSession(options) {
17818
18012
  };
17819
18013
  const captureFrame = async () => guard(async () => {
17820
18014
  const base64 = await browser.takeScreenshot();
18015
+ lastShotSize = pngSizeFromBase64(base64) ?? lastShotSize;
17821
18016
  return { base64, mime: "image/png" };
17822
18017
  });
17823
18018
  const screenshot = async () => {
@@ -17831,55 +18026,19 @@ async function createDeviceSession(options) {
17831
18026
  const tap = async (xNorm, yNorm, tapOptions) => {
17832
18027
  await gate.withLock(async () => {
17833
18028
  await guard(async () => {
17834
- const size = await getWindowSize();
18029
+ const size = await getPointerSize();
17835
18030
  const x = toPx(xNorm, size.width);
17836
18031
  const y = toPx(yNorm, size.height);
17837
18032
  const holdMs = Math.max(50, tapOptions?.durationMs ?? 50);
17838
- await browser.performActions([
17839
- {
17840
- type: "pointer",
17841
- id: "finger1",
17842
- parameters: { pointerType: "touch" },
17843
- actions: [
17844
- { type: "pointerMove", duration: 0, x, y },
17845
- { type: "pointerDown", button: 0 },
17846
- { type: "pause", duration: holdMs },
17847
- { type: "pointerUp", button: 0 }
17848
- ]
17849
- }
17850
- ]);
17851
- await browser.releaseActions();
18033
+ await injectTap(browser, x, y, holdMs);
17852
18034
  });
17853
18035
  });
17854
18036
  };
17855
18037
  const swipe = async (x1, y1, x2, y2, durationMs = 400) => {
17856
18038
  await gate.withLock(async () => {
17857
18039
  await guard(async () => {
17858
- const size = await getWindowSize();
17859
- await browser.performActions([
17860
- {
17861
- type: "pointer",
17862
- id: "finger1",
17863
- parameters: { pointerType: "touch" },
17864
- actions: [
17865
- {
17866
- type: "pointerMove",
17867
- duration: 0,
17868
- x: toPx(x1, size.width),
17869
- y: toPx(y1, size.height)
17870
- },
17871
- { type: "pointerDown", button: 0 },
17872
- {
17873
- type: "pointerMove",
17874
- duration: durationMs,
17875
- x: toPx(x2, size.width),
17876
- y: toPx(y2, size.height)
17877
- },
17878
- { type: "pointerUp", button: 0 }
17879
- ]
17880
- }
17881
- ]);
17882
- await browser.releaseActions();
18040
+ const size = await getPointerSize();
18041
+ await injectSwipe(browser, toPx(x1, size.width), toPx(y1, size.height), toPx(x2, size.width), toPx(y2, size.height), durationMs);
17883
18042
  });
17884
18043
  });
17885
18044
  };
@@ -17924,20 +18083,20 @@ async function createDeviceSession(options) {
17924
18083
  const acceptAlert = async () => {
17925
18084
  await gate.withLock(async () => {
17926
18085
  await guard(async () => {
17927
- await browser.acceptAlert();
18086
+ await resolveNativeAlert(browser, "accept");
17928
18087
  });
17929
18088
  });
17930
18089
  };
17931
18090
  const dismissAlert = async () => {
17932
18091
  await gate.withLock(async () => {
17933
18092
  await guard(async () => {
17934
- await browser.dismissAlert();
18093
+ await resolveNativeAlert(browser, "dismiss");
17935
18094
  });
17936
18095
  });
17937
18096
  };
17938
18097
  const pointerEvent = async (phase, xNorm, yNorm, seq) => {
17939
18098
  await guard(async () => {
17940
- await gate.handlePointer(browser, toPx, getWindowSize, phase, xNorm, yNorm, seq);
18099
+ await gate.handlePointer(browser, toPx, getPointerSize, phase, xNorm, yNorm, seq);
17941
18100
  });
17942
18101
  };
17943
18102
  const session = {
@@ -24118,6 +24277,11 @@ var caseScriptActionSchema = exports_external.union([
24118
24277
  text: exports_external.string().min(1),
24119
24278
  timeoutMs: exports_external.number().min(0).max(120000).optional(),
24120
24279
  reason: exports_external.string().optional()
24280
+ }),
24281
+ exports_external.object({
24282
+ type: exports_external.literal("alert"),
24283
+ alertAction: exports_external.union([exports_external.literal("accept"), exports_external.literal("dismiss")]).default("accept"),
24284
+ reason: exports_external.string().optional()
24121
24285
  })
24122
24286
  ]);
24123
24287
  var caseScriptSchema = exports_external.object({
@@ -24789,12 +24953,26 @@ function buildScriptFromDecisions(decisions, sourceRunId) {
24789
24953
  const actions = [];
24790
24954
  for (const decision of decisions) {
24791
24955
  if (decision.type === "tap") {
24792
- actions.push({
24793
- type: "tap",
24794
- x: decision.x ?? 500,
24795
- y: decision.y ?? 500,
24796
- reason: decision.reason
24797
- });
24956
+ if (decision.label) {
24957
+ actions.push({
24958
+ type: "tap",
24959
+ label: decision.label,
24960
+ reason: decision.reason
24961
+ });
24962
+ } else if (decision.id) {
24963
+ actions.push({
24964
+ type: "tap",
24965
+ id: decision.id,
24966
+ reason: decision.reason
24967
+ });
24968
+ } else {
24969
+ actions.push({
24970
+ type: "tap",
24971
+ x: decision.x ?? 500,
24972
+ y: decision.y ?? 500,
24973
+ reason: decision.reason
24974
+ });
24975
+ }
24798
24976
  } else if (decision.type === "type") {
24799
24977
  actions.push({
24800
24978
  type: "type",
@@ -24807,6 +24985,12 @@ function buildScriptFromDecisions(decisions, sourceRunId) {
24807
24985
  ms: Math.min(3000, Math.max(500, decision.ms ?? 1500)),
24808
24986
  reason: decision.reason
24809
24987
  });
24988
+ } else if (decision.type === "alert") {
24989
+ actions.push({
24990
+ type: "alert",
24991
+ alertAction: decision.alertAction === "dismiss" ? "dismiss" : "accept",
24992
+ reason: decision.reason
24993
+ });
24810
24994
  }
24811
24995
  }
24812
24996
  if (actions.length === 0)
@@ -57546,6 +57730,195 @@ var originalGenerateCallId6 = createIdGenerator3({
57546
57730
  });
57547
57731
  var defaultDownload2 = createDownload();
57548
57732
 
57733
+ // src/domains/providers/json-salvage.ts
57734
+ function charAt(raw2, index) {
57735
+ return raw2[index];
57736
+ }
57737
+ function coerceLooseJson(raw2) {
57738
+ let out = "";
57739
+ let i = 0;
57740
+ while (i < raw2.length) {
57741
+ const c = charAt(raw2, i);
57742
+ if (c === undefined)
57743
+ break;
57744
+ if (c === '"') {
57745
+ out += c;
57746
+ i += 1;
57747
+ while (i < raw2.length) {
57748
+ const d = charAt(raw2, i);
57749
+ if (d === undefined)
57750
+ break;
57751
+ out += d;
57752
+ i += 1;
57753
+ if (d === "\\") {
57754
+ const escaped = charAt(raw2, i);
57755
+ if (escaped !== undefined) {
57756
+ out += escaped;
57757
+ i += 1;
57758
+ }
57759
+ continue;
57760
+ }
57761
+ if (d === '"')
57762
+ break;
57763
+ }
57764
+ continue;
57765
+ }
57766
+ if (c === "'") {
57767
+ i += 1;
57768
+ let content = "";
57769
+ while (i < raw2.length) {
57770
+ const d = charAt(raw2, i);
57771
+ if (d === undefined)
57772
+ break;
57773
+ if (d === "\\" && i + 1 < raw2.length) {
57774
+ const next = charAt(raw2, i + 1);
57775
+ if (next === undefined)
57776
+ break;
57777
+ if (next === "n")
57778
+ content += `
57779
+ `;
57780
+ else if (next === "t")
57781
+ content += "\t";
57782
+ else if (next === "r")
57783
+ content += "\r";
57784
+ else
57785
+ content += next;
57786
+ i += 2;
57787
+ continue;
57788
+ }
57789
+ if (d === "'") {
57790
+ i += 1;
57791
+ break;
57792
+ }
57793
+ content += d;
57794
+ i += 1;
57795
+ }
57796
+ out += JSON.stringify(content);
57797
+ continue;
57798
+ }
57799
+ if (c === ",") {
57800
+ let j = i + 1;
57801
+ while (j < raw2.length) {
57802
+ const ws = charAt(raw2, j);
57803
+ if (ws === undefined || !/\s/.test(ws))
57804
+ break;
57805
+ j += 1;
57806
+ }
57807
+ const closer = charAt(raw2, j);
57808
+ if (closer === "}" || closer === "]") {
57809
+ i += 1;
57810
+ continue;
57811
+ }
57812
+ }
57813
+ out += c;
57814
+ i += 1;
57815
+ }
57816
+ return out;
57817
+ }
57818
+ function tryParseJson(text3) {
57819
+ try {
57820
+ return JSON.parse(text3);
57821
+ } catch {
57822
+ return;
57823
+ }
57824
+ }
57825
+ function closeTruncatedJson(raw2) {
57826
+ let inString = false;
57827
+ let escaped = false;
57828
+ const stack = [];
57829
+ for (let i = 0;i < raw2.length; i += 1) {
57830
+ const c = charAt(raw2, i);
57831
+ if (c === undefined)
57832
+ break;
57833
+ if (inString) {
57834
+ if (escaped) {
57835
+ escaped = false;
57836
+ continue;
57837
+ }
57838
+ if (c === "\\") {
57839
+ escaped = true;
57840
+ continue;
57841
+ }
57842
+ if (c === '"')
57843
+ inString = false;
57844
+ continue;
57845
+ }
57846
+ if (c === '"') {
57847
+ inString = true;
57848
+ continue;
57849
+ }
57850
+ if (c === "{")
57851
+ stack.push("{");
57852
+ else if (c === "[")
57853
+ stack.push("[");
57854
+ else if (c === "}" || c === "]")
57855
+ stack.pop();
57856
+ }
57857
+ let out = raw2;
57858
+ if (inString) {
57859
+ if (escaped)
57860
+ out = out.slice(0, -1);
57861
+ out += '"';
57862
+ }
57863
+ while (stack.length > 0) {
57864
+ const open = stack.pop();
57865
+ out += open === "{" ? "}" : "]";
57866
+ }
57867
+ return out;
57868
+ }
57869
+ function salvageAgentJsonText(text3) {
57870
+ const trimmed = text3.trim();
57871
+ const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/);
57872
+ const inner = fenced?.[1]?.trim() ?? trimmed;
57873
+ const start = inner.indexOf("{");
57874
+ if (start < 0)
57875
+ return trimmed;
57876
+ const fromBrace = inner.slice(start);
57877
+ const lastBrace = fromBrace.lastIndexOf("}");
57878
+ const candidates = lastBrace > 0 ? [fromBrace.slice(0, lastBrace + 1), fromBrace] : [fromBrace];
57879
+ for (const candidate of candidates) {
57880
+ const variants = [
57881
+ candidate,
57882
+ coerceLooseJson(candidate),
57883
+ closeTruncatedJson(candidate),
57884
+ closeTruncatedJson(coerceLooseJson(candidate))
57885
+ ];
57886
+ for (const variant of variants) {
57887
+ if (tryParseJson(variant) !== undefined)
57888
+ return variant;
57889
+ }
57890
+ }
57891
+ return closeTruncatedJson(coerceLooseJson(fromBrace));
57892
+ }
57893
+ function clampNormCoord(n) {
57894
+ if (!Number.isFinite(n))
57895
+ return n;
57896
+ return Math.min(1000, Math.max(0, n));
57897
+ }
57898
+ function normalizeVisionJson(value) {
57899
+ if (!value || typeof value !== "object" || Array.isArray(value))
57900
+ return value;
57901
+ const out = { ...value };
57902
+ if (typeof out.x === "number")
57903
+ out.x = clampNormCoord(out.x);
57904
+ if (typeof out.y === "number")
57905
+ out.y = clampNormCoord(out.y);
57906
+ if (typeof out.ms === "number" && Number.isFinite(out.ms)) {
57907
+ out.ms = Math.min(1e4, Math.max(0, out.ms));
57908
+ }
57909
+ const reason = typeof out.reason === "string" ? out.reason.trim() : "";
57910
+ const thoughts = typeof out.thoughts === "string" ? out.thoughts.trim() : "";
57911
+ if (reason)
57912
+ out.reason = reason;
57913
+ if (thoughts)
57914
+ out.thoughts = thoughts;
57915
+ if (reason && !thoughts)
57916
+ out.thoughts = reason;
57917
+ if (thoughts && !reason)
57918
+ out.reason = thoughts;
57919
+ return out;
57920
+ }
57921
+
57549
57922
  // src/domains/providers/vision-model.ts
57550
57923
  class AgentProviderError extends Error {
57551
57924
  constructor(message) {
@@ -57646,20 +58019,27 @@ function mapProviderError(label, error40) {
57646
58019
  }
57647
58020
  throw new AgentProviderError(`Model did not return JSON (got: ${snippet}${text3.length > 160 ? "\u2026" : ""})`);
57648
58021
  }
58022
+ if (error40 instanceof exports_external.ZodError) {
58023
+ const first = error40.issues[0];
58024
+ const detail = first ? `${first.path.length > 0 ? `${first.path.join(".")}: ` : ""}${first.message}` : error40.message;
58025
+ throw new AgentProviderError(`${label} JSON was not a valid action: ${detail}`);
58026
+ }
57649
58027
  if (error40 instanceof Error) {
57650
58028
  throw new AgentProviderError(error40.message);
57651
58029
  }
57652
58030
  throw new AgentProviderError(String(error40));
57653
58031
  }
57654
- function repairModelJsonText(text3) {
57655
- const trimmed = text3.trim();
57656
- const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/);
57657
- const candidate = fenced?.[1]?.trim() ?? trimmed;
57658
- const start = candidate.indexOf("{");
57659
- const end = candidate.lastIndexOf("}");
57660
- if (start < 0 || end <= start)
57661
- return trimmed;
57662
- return candidate.slice(start, end + 1);
58032
+ function parseVisionSchema(schema, raw2, label) {
58033
+ try {
58034
+ return schema.parse(normalizeVisionJson(raw2));
58035
+ } catch (error40) {
58036
+ if (error40 instanceof exports_external.ZodError) {
58037
+ const first = error40.issues[0];
58038
+ const detail = first ? `${first.path.length > 0 ? `${first.path.join(".")}: ` : ""}${first.message}` : error40.message;
58039
+ throw new AgentProviderError(`${label} JSON was not a valid action: ${detail}`);
58040
+ }
58041
+ throw error40;
58042
+ }
57663
58043
  }
57664
58044
  async function completeWithAiSdk(input) {
57665
58045
  const run = async (prompt) => {
@@ -57669,7 +58049,7 @@ async function completeWithAiSdk(input) {
57669
58049
  schema: input.schema,
57670
58050
  system: input.system,
57671
58051
  maxOutputTokens: VISION_MAX_TOKENS,
57672
- experimental_repairText: async ({ text: text3 }) => repairModelJsonText(text3),
58052
+ experimental_repairText: async ({ text: text3 }) => salvageAgentJsonText(text3),
57673
58053
  messages: [
57674
58054
  {
57675
58055
  role: "user",
@@ -57684,12 +58064,18 @@ async function completeWithAiSdk(input) {
57684
58064
  }
57685
58065
  ]
57686
58066
  });
57687
- return object3;
58067
+ return parseVisionSchema(input.schema, object3, input.label);
57688
58068
  } catch (error40) {
58069
+ if (error40 instanceof AgentProviderError)
58070
+ throw error40;
57689
58071
  if (NoObjectGeneratedError.isInstance(error40) && error40.text?.trim()) {
57690
58072
  try {
57691
- return input.schema.parse(JSON.parse(repairModelJsonText(error40.text)));
57692
- } catch {}
58073
+ const salvaged = salvageAgentJsonText(error40.text);
58074
+ return parseVisionSchema(input.schema, JSON.parse(salvaged), input.label);
58075
+ } catch (salvageError) {
58076
+ if (salvageError instanceof AgentProviderError)
58077
+ throw salvageError;
58078
+ }
57693
58079
  }
57694
58080
  mapProviderError(input.label, error40);
57695
58081
  }
@@ -67601,108 +67987,37 @@ var google = createGoogle();
67601
67987
 
67602
67988
  // src/domains/providers/agent-json.ts
67603
67989
  function extractAgentJsonObject(text3, providerLabel) {
67604
- const trimmed = text3.trim();
67605
- const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/);
67606
- const candidate = fenced?.[1]?.trim() ?? trimmed;
67607
- const start = candidate.indexOf("{");
67608
- const end = candidate.lastIndexOf("}");
67609
- if (start < 0 || end <= start) {
67610
- throw new AgentProviderError(`${providerLabel} did not return JSON (got: ${trimmed.replace(/\s+/g, " ").slice(0, 160)})`);
67611
- }
67612
- const slice = candidate.slice(start, end + 1);
67990
+ const snippet = text3.trim().replace(/\s+/g, " ").slice(0, 160);
67991
+ const fail = () => {
67992
+ throw new AgentProviderError(`${providerLabel} did not return JSON (got: ${snippet})`);
67993
+ };
67994
+ const salvaged = salvageAgentJsonText(text3);
67995
+ let parsed;
67613
67996
  try {
67614
- return JSON.parse(slice);
67615
- } catch (strictError) {
67997
+ parsed = JSON.parse(salvaged);
67998
+ } catch {
67616
67999
  try {
67617
- return JSON.parse(coerceLooseJson(slice));
68000
+ parsed = JSON.parse(coerceLooseJson(salvaged));
67618
68001
  } catch {
67619
- throw strictError;
68002
+ fail();
67620
68003
  }
67621
68004
  }
68005
+ if (parsed === undefined || parsed === null || typeof parsed !== "object") {
68006
+ fail();
68007
+ }
68008
+ return parsed;
67622
68009
  }
67623
- function charAt(raw2, index) {
67624
- return raw2[index];
67625
- }
67626
- function coerceLooseJson(raw2) {
67627
- let out = "";
67628
- let i = 0;
67629
- while (i < raw2.length) {
67630
- const c = charAt(raw2, i);
67631
- if (c === undefined)
67632
- break;
67633
- if (c === '"') {
67634
- out += c;
67635
- i += 1;
67636
- while (i < raw2.length) {
67637
- const d = charAt(raw2, i);
67638
- if (d === undefined)
67639
- break;
67640
- out += d;
67641
- i += 1;
67642
- if (d === "\\") {
67643
- const escaped = charAt(raw2, i);
67644
- if (escaped !== undefined) {
67645
- out += escaped;
67646
- i += 1;
67647
- }
67648
- continue;
67649
- }
67650
- if (d === '"')
67651
- break;
67652
- }
67653
- continue;
67654
- }
67655
- if (c === "'") {
67656
- i += 1;
67657
- let content = "";
67658
- while (i < raw2.length) {
67659
- const d = charAt(raw2, i);
67660
- if (d === undefined)
67661
- break;
67662
- if (d === "\\" && i + 1 < raw2.length) {
67663
- const next = charAt(raw2, i + 1);
67664
- if (next === undefined)
67665
- break;
67666
- if (next === "n")
67667
- content += `
67668
- `;
67669
- else if (next === "t")
67670
- content += "\t";
67671
- else if (next === "r")
67672
- content += "\r";
67673
- else
67674
- content += next;
67675
- i += 2;
67676
- continue;
67677
- }
67678
- if (d === "'") {
67679
- i += 1;
67680
- break;
67681
- }
67682
- content += d;
67683
- i += 1;
67684
- }
67685
- out += JSON.stringify(content);
67686
- continue;
67687
- }
67688
- if (c === ",") {
67689
- let j = i + 1;
67690
- while (j < raw2.length) {
67691
- const ws = charAt(raw2, j);
67692
- if (ws === undefined || !/\s/.test(ws))
67693
- break;
67694
- j += 1;
67695
- }
67696
- const closer = charAt(raw2, j);
67697
- if (closer === "}" || closer === "]") {
67698
- i += 1;
67699
- continue;
67700
- }
68010
+ function parseVisionObject(schema, raw2, providerLabel) {
68011
+ try {
68012
+ return schema.parse(normalizeVisionJson(raw2));
68013
+ } catch (error40) {
68014
+ if (error40 instanceof exports_external.ZodError) {
68015
+ const first = error40.issues[0];
68016
+ const detail = first ? `${first.path.length > 0 ? `${first.path.join(".")}: ` : ""}${first.message}` : error40.message;
68017
+ throw new AgentProviderError(`${providerLabel} JSON was not a valid action: ${detail}`);
67701
68018
  }
67702
- out += c;
67703
- i += 1;
68019
+ throw error40;
67704
68020
  }
67705
- return out;
67706
68021
  }
67707
68022
 
67708
68023
  // src/domains/providers/drivers/antigravity-vision.ts
@@ -67748,13 +68063,10 @@ ${result.stderr}`.trim();
67748
68063
  if (result.exitCode !== 0 && !result.stdout.trim()) {
67749
68064
  throw new AgentProviderError(combined ? `Antigravity CLI failed: ${combined.slice(0, 400)}` : `Antigravity CLI exited ${result.exitCode}`);
67750
68065
  }
67751
- return input.schema.parse(extractAgentJsonObject(result.stdout || combined, "Antigravity CLI"));
68066
+ return parseVisionObject(input.schema, extractAgentJsonObject(result.stdout || combined, "Antigravity CLI"), "Antigravity CLI");
67752
68067
  } catch (error40) {
67753
68068
  if (error40 instanceof AgentProviderError)
67754
68069
  throw error40;
67755
- if (error40 instanceof exports_external.ZodError) {
67756
- throw new AgentProviderError(`Antigravity JSON was not a valid action: ${error40.message}`);
67757
- }
67758
68070
  if (error40 instanceof SyntaxError) {
67759
68071
  throw new AgentProviderError(`Antigravity CLI returned invalid JSON: ${error40.message}`);
67760
68072
  }
@@ -76312,13 +76624,10 @@ Screenshot file (open and look at it): ${shotName}`,
76312
76624
  imageBase64: image.base64,
76313
76625
  mediaType: image.mediaType
76314
76626
  });
76315
- return input.schema.parse(extractAgentJsonObject(stdout, "Cursor Agent CLI"));
76627
+ return parseVisionObject(input.schema, extractAgentJsonObject(stdout, "Cursor Agent CLI"), "Cursor Agent CLI");
76316
76628
  } catch (error40) {
76317
76629
  if (error40 instanceof AgentProviderError)
76318
76630
  throw error40;
76319
- if (error40 instanceof exports_external.ZodError) {
76320
- throw new AgentProviderError(`Cursor JSON was not a valid action: ${error40.message}`);
76321
- }
76322
76631
  if (error40 instanceof SyntaxError) {
76323
76632
  throw new AgentProviderError(`Cursor Agent CLI returned invalid JSON: ${error40.message}`);
76324
76633
  }
@@ -97455,14 +97764,18 @@ var agentDecisionSchema = exports_external.object({
97455
97764
  exports_external.literal("tap"),
97456
97765
  exports_external.literal("type"),
97457
97766
  exports_external.literal("wait"),
97767
+ exports_external.literal("alert"),
97458
97768
  exports_external.literal("verify"),
97459
97769
  exports_external.literal("done"),
97460
97770
  exports_external.literal("fail")
97461
97771
  ]),
97462
- x: exports_external.number().min(0).max(1000).optional(),
97463
- y: exports_external.number().min(0).max(1000).optional(),
97772
+ x: exports_external.number().optional(),
97773
+ y: exports_external.number().optional(),
97774
+ label: exports_external.string().min(1).optional(),
97775
+ id: exports_external.string().min(1).optional(),
97464
97776
  text: exports_external.string().optional(),
97465
- ms: exports_external.number().min(0).max(1e4).optional(),
97777
+ ms: exports_external.number().optional(),
97778
+ alertAction: exports_external.union([exports_external.literal("accept"), exports_external.literal("dismiss")]).optional(),
97466
97779
  reason: exports_external.string().min(1),
97467
97780
  thoughts: exports_external.string().min(1)
97468
97781
  });
@@ -97475,17 +97788,21 @@ Every action MUST include:
97475
97788
  - "thoughts": 2\u20134 sentences describing what you see on screen and how that led to this action
97476
97789
 
97477
97790
  Valid shapes:
97791
+ {"type":"tap","label":"Allow","reason":"...","thoughts":"..."}
97792
+ {"type":"tap","id":"permission_allow_button","reason":"...","thoughts":"..."}
97478
97793
  {"type":"tap","x":0-1000,"y":0-1000,"reason":"...","thoughts":"..."}
97794
+ {"type":"alert","alertAction":"accept","reason":"...","thoughts":"..."}
97479
97795
  {"type":"type","text":"...","reason":"...","thoughts":"..."}
97480
97796
  {"type":"wait","ms":500-3000,"reason":"...","thoughts":"..."}
97481
97797
  {"type":"verify","reason":"...","thoughts":"..."}
97482
97798
  {"type":"done","reason":"...","thoughts":"..."}
97483
97799
  {"type":"fail","reason":"...","thoughts":"..."}
97484
97800
 
97485
- Coordinates use a 0\u20131000 normalized grid (0,0 top-left).
97801
+ Coordinates use a 0\u20131000 normalized grid (0,0 top-left). Prefer label or id over coordinates whenever the control has visible text or a resource-id.
97486
97802
 
97487
97803
  Rules:
97488
97804
  - Look at the attached screenshot before deciding. Base taps on what is visible now.
97805
+ - 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.
97489
97806
  - 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.
97490
97807
  - Splash / loading / blank screens: return wait (do not tap imaginary tabs).
97491
97808
  - Do not repeat the same tap target from Recent actions unless the screenshot still shows you are on the wrong screen.
@@ -97496,8 +97813,17 @@ function formatRecentActions(actions) {
97496
97813
  return actions.slice(-6).map((action, i) => {
97497
97814
  const n = actions.length - Math.min(actions.length, 6) + i + 1;
97498
97815
  if (action.type === "tap") {
97816
+ if (action.label) {
97817
+ return `${n}. tap label "${action.label}" \u2014 ${action.reason ?? ""}`;
97818
+ }
97819
+ if (action.id) {
97820
+ return `${n}. tap id "${action.id}" \u2014 ${action.reason ?? ""}`;
97821
+ }
97499
97822
  return `${n}. tap (${action.x ?? "?"},${action.y ?? "?"}) \u2014 ${action.reason ?? ""}`;
97500
97823
  }
97824
+ if (action.type === "alert") {
97825
+ return `${n}. alert ${action.alertAction ?? "accept"} \u2014 ${action.reason ?? ""}`;
97826
+ }
97501
97827
  if (action.type === "type") {
97502
97828
  return `${n}. type "${action.text ?? ""}" \u2014 ${action.reason ?? ""}`;
97503
97829
  }
@@ -97658,8 +97984,8 @@ function cleanPageSource(xml, window2) {
97658
97984
 
97659
97985
  // src/domains/devices/grounding.ts
97660
97986
  var groundResultSchema = exports_external.object({
97661
- x: exports_external.number().min(0).max(1000),
97662
- y: exports_external.number().min(0).max(1000)
97987
+ x: exports_external.number(),
97988
+ y: exports_external.number()
97663
97989
  });
97664
97990
  var GROUND_SYSTEM = `You locate UI elements on a mobile screen.
97665
97991
  Respond with ONLY JSON: {"x":0-1000,"y":0-1000} for the center of the best match.
@@ -97730,7 +98056,7 @@ class ActionNotFoundError extends Error {
97730
98056
  async function performAction(session, body) {
97731
98057
  let x = body.x;
97732
98058
  let y = body.y;
97733
- if ((body.id || body.label) && (body.kind === "tap" || body.kind === "input") && (x == null || y == null)) {
98059
+ if ((body.id || body.label) && (body.kind === "tap" || body.kind === "input")) {
97734
98060
  const screen = await getScreen(session, { full: false });
97735
98061
  const elements = screen.elements ?? [];
97736
98062
  const match2 = body.id ? findElementById(elements, body.id) : findElementByLabel(elements, body.label ?? "");
@@ -97930,6 +98256,25 @@ async function executeScriptCase(deps) {
97930
98256
  latencyMs,
97931
98257
  detail: action.reason ?? `${assertion}: ${action.text}`
97932
98258
  });
98259
+ } else if (action.type === "alert") {
98260
+ await perform(deps.session, {
98261
+ kind: "alert",
98262
+ alertAction: action.alertAction === "dismiss" ? "dismiss" : "accept"
98263
+ });
98264
+ await clock.sleep(settleMs);
98265
+ await deps.appendStep({
98266
+ idx: stepIdx,
98267
+ action: {
98268
+ type: "alert",
98269
+ alertAction: action.alertAction === "dismiss" ? "dismiss" : "accept",
98270
+ reason: action.reason ?? "Replayed saved script alert",
98271
+ thoughts: "Replaying the saved script without calling the AI agent."
98272
+ },
98273
+ screenshotUri: shot.path,
98274
+ ok: true,
98275
+ latencyMs,
98276
+ detail: action.reason ?? action.alertAction ?? "accept"
98277
+ });
97933
98278
  } else {
97934
98279
  const waitMs = Math.min(3000, Math.max(500, action.ms));
97935
98280
  await clock.sleep(waitMs);
@@ -98054,9 +98399,30 @@ async function executeAgentCase(deps) {
98054
98399
  recentActions.push(decision);
98055
98400
  recordedDecisions.push(decision);
98056
98401
  if (decision.type === "tap") {
98057
- const x = decision.x ?? 500;
98058
- const y = decision.y ?? 500;
98059
- await perform(deps.session, { kind: "tap", x, y });
98402
+ const tapBody = { kind: "tap" };
98403
+ if (decision.label)
98404
+ tapBody.label = decision.label;
98405
+ if (decision.id)
98406
+ tapBody.id = decision.id;
98407
+ if (!tapBody.label && !tapBody.id) {
98408
+ tapBody.x = decision.x ?? 500;
98409
+ tapBody.y = decision.y ?? 500;
98410
+ }
98411
+ await perform(deps.session, tapBody);
98412
+ await clock.sleep(settleMs);
98413
+ await deps.appendStep({
98414
+ idx: stepIdx,
98415
+ action: decision,
98416
+ screenshotUri: shot.path,
98417
+ ok: true,
98418
+ latencyMs,
98419
+ detail: decision.reason ?? null
98420
+ });
98421
+ } else if (decision.type === "alert") {
98422
+ await perform(deps.session, {
98423
+ kind: "alert",
98424
+ alertAction: decision.alertAction === "dismiss" ? "dismiss" : "accept"
98425
+ });
98060
98426
  await clock.sleep(settleMs);
98061
98427
  await deps.appendStep({
98062
98428
  idx: stepIdx,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yoqa/runner",
3
- "version": "0.3.21",
3
+ "version": "0.3.23",
4
4
  "description": "Yoqa local runner — Appium + catalog HTTP API (Bun). Start with yoqa serve or the yoqa-runner bin.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",