@yoqa/runner 0.3.22 → 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 +352 -111
  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.22",
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)
@@ -97580,14 +97764,18 @@ var agentDecisionSchema = exports_external.object({
97580
97764
  exports_external.literal("tap"),
97581
97765
  exports_external.literal("type"),
97582
97766
  exports_external.literal("wait"),
97767
+ exports_external.literal("alert"),
97583
97768
  exports_external.literal("verify"),
97584
97769
  exports_external.literal("done"),
97585
97770
  exports_external.literal("fail")
97586
97771
  ]),
97587
97772
  x: exports_external.number().optional(),
97588
97773
  y: exports_external.number().optional(),
97774
+ label: exports_external.string().min(1).optional(),
97775
+ id: exports_external.string().min(1).optional(),
97589
97776
  text: exports_external.string().optional(),
97590
97777
  ms: exports_external.number().optional(),
97778
+ alertAction: exports_external.union([exports_external.literal("accept"), exports_external.literal("dismiss")]).optional(),
97591
97779
  reason: exports_external.string().min(1),
97592
97780
  thoughts: exports_external.string().min(1)
97593
97781
  });
@@ -97600,17 +97788,21 @@ Every action MUST include:
97600
97788
  - "thoughts": 2\u20134 sentences describing what you see on screen and how that led to this action
97601
97789
 
97602
97790
  Valid shapes:
97791
+ {"type":"tap","label":"Allow","reason":"...","thoughts":"..."}
97792
+ {"type":"tap","id":"permission_allow_button","reason":"...","thoughts":"..."}
97603
97793
  {"type":"tap","x":0-1000,"y":0-1000,"reason":"...","thoughts":"..."}
97794
+ {"type":"alert","alertAction":"accept","reason":"...","thoughts":"..."}
97604
97795
  {"type":"type","text":"...","reason":"...","thoughts":"..."}
97605
97796
  {"type":"wait","ms":500-3000,"reason":"...","thoughts":"..."}
97606
97797
  {"type":"verify","reason":"...","thoughts":"..."}
97607
97798
  {"type":"done","reason":"...","thoughts":"..."}
97608
97799
  {"type":"fail","reason":"...","thoughts":"..."}
97609
97800
 
97610
- 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.
97611
97802
 
97612
97803
  Rules:
97613
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.
97614
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.
97615
97807
  - Splash / loading / blank screens: return wait (do not tap imaginary tabs).
97616
97808
  - Do not repeat the same tap target from Recent actions unless the screenshot still shows you are on the wrong screen.
@@ -97621,8 +97813,17 @@ function formatRecentActions(actions) {
97621
97813
  return actions.slice(-6).map((action, i) => {
97622
97814
  const n = actions.length - Math.min(actions.length, 6) + i + 1;
97623
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
+ }
97624
97822
  return `${n}. tap (${action.x ?? "?"},${action.y ?? "?"}) \u2014 ${action.reason ?? ""}`;
97625
97823
  }
97824
+ if (action.type === "alert") {
97825
+ return `${n}. alert ${action.alertAction ?? "accept"} \u2014 ${action.reason ?? ""}`;
97826
+ }
97626
97827
  if (action.type === "type") {
97627
97828
  return `${n}. type "${action.text ?? ""}" \u2014 ${action.reason ?? ""}`;
97628
97829
  }
@@ -97855,7 +98056,7 @@ class ActionNotFoundError extends Error {
97855
98056
  async function performAction(session, body) {
97856
98057
  let x = body.x;
97857
98058
  let y = body.y;
97858
- 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")) {
97859
98060
  const screen = await getScreen(session, { full: false });
97860
98061
  const elements = screen.elements ?? [];
97861
98062
  const match2 = body.id ? findElementById(elements, body.id) : findElementByLabel(elements, body.label ?? "");
@@ -98055,6 +98256,25 @@ async function executeScriptCase(deps) {
98055
98256
  latencyMs,
98056
98257
  detail: action.reason ?? `${assertion}: ${action.text}`
98057
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
+ });
98058
98278
  } else {
98059
98279
  const waitMs = Math.min(3000, Math.max(500, action.ms));
98060
98280
  await clock.sleep(waitMs);
@@ -98179,9 +98399,30 @@ async function executeAgentCase(deps) {
98179
98399
  recentActions.push(decision);
98180
98400
  recordedDecisions.push(decision);
98181
98401
  if (decision.type === "tap") {
98182
- const x = decision.x ?? 500;
98183
- const y = decision.y ?? 500;
98184
- 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
+ });
98185
98426
  await clock.sleep(settleMs);
98186
98427
  await deps.appendStep({
98187
98428
  idx: stepIdx,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yoqa/runner",
3
- "version": "0.3.22",
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",