@swmansion/argent 0.22.2-next.0 → 0.22.2-next.1

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.
@@ -93869,7 +93869,7 @@ var _CI_VENDOR_COUNT_FOR_TEST = vendors_default.length;
93869
93869
  var SESSION_ID = (0, import_node_crypto3.randomUUID)();
93870
93870
  function readCliVersion() {
93871
93871
  if (true) {
93872
- return "0.22.2-next.0";
93872
+ return "0.22.2-next.1";
93873
93873
  }
93874
93874
  return "0.0.0";
93875
93875
  }
@@ -119417,18 +119417,39 @@ Before tapping, determine the correct coordinates by using discovery tools \u201
119417
119417
  // ../tool-server/src/tools/gesture-swipe/index.ts
119418
119418
  init_zod();
119419
119419
  var sleep3 = (ms) => new Promise((r) => setTimeout(r, ms));
119420
- var SETTLE_EASE_EXPONENT = 3;
119420
+ var MOMENTUM_FREE_EASE_EXPONENT = 3;
119421
+ var DEFAULT_DURATION_MS = 300;
119422
+ var MOMENTUM_FREE_MIN_DURATION_MS = 150;
119423
+ var MAX_DURATION_MS = 1e4;
119421
119424
  var zodSchema17 = external_exports.object({
119422
119425
  udid: external_exports.string().describe("Target device id from `list-devices` (iOS UDID or Android serial)."),
119423
119426
  fromX: external_exports.number().describe("Start x: normalized 0.0\u20131.0 (not pixels; same as tap)"),
119424
119427
  fromY: external_exports.number().describe("Start y: normalized 0.0\u20131.0 (not pixels; same as tap)"),
119425
119428
  toX: external_exports.number().describe("End x: normalized 0.0\u20131.0 (not pixels; same as tap)"),
119426
119429
  toY: external_exports.number().describe("End y: normalized 0.0\u20131.0 (not pixels; same as tap)"),
119427
- durationMs: external_exports.number().optional().describe("Total gesture duration in milliseconds (default 300)"),
119428
- settle: external_exports.boolean().optional().describe(
119429
- "Momentum-free swipe: decelerate into the end point (ease-out) so the OS reads ~0 release velocity and applies little to no fling. Use for scroll-to-element loops; default false (a natural flinging swipe)."
119430
+ durationMs: external_exports.number().max(MAX_DURATION_MS, {
119431
+ message: `durationMs must be at most ${MAX_DURATION_MS} (10s): every frame is a real 16ms sleep with the finger held down, so a larger value is that many milliseconds of wall clock spent holding a touch the device cannot shake off.`
119432
+ }).optional().describe(
119433
+ `Total gesture duration in milliseconds (default 300, at most ${MAX_DURATION_MS} - the gesture holds a finger down for exactly this long)`
119434
+ ),
119435
+ momentum: external_exports.boolean().optional().describe(
119436
+ `Whether the swipe releases with momentum; default true (a natural flinging swipe). Pass false for a momentum-free swipe at the default durationMs: the finger decelerates into the end point (ease-out) so the OS reads ~0 release velocity and applies little to no fling. Use false for scroll-to-element loops. momentum: false needs durationMs >= ${MOMENTUM_FREE_MIN_DURATION_MS} and is rejected below it: a shorter ease-out gives the OS velocity fit too little wall clock to read the deceleration as a stop, and it flings harder than a plain swipe instead (on Android, backwards). At ${MOMENTUM_FREE_MIN_DURATION_MS} itself the swipe lands short of where the finger stopped, and 2 of 47 runs still flung backwards.`
119437
+ ),
119438
+ // `momentum`'s shipped spelling, with the opposite polarity. Declared so this
119439
+ // non-strict object refuses it instead of stripping it and flinging - the exact
119440
+ // inverse of the gesture the caller asked for.
119441
+ settle: external_exports.never({
119442
+ error: "gesture-swipe's `settle` was renamed to `momentum`, with the opposite sense \u2014 write `momentum: false` for the momentum-free swipe that `settle: true` used to mean (plain `settle: false` was the default, so just drop it)"
119443
+ }).optional().describe(
119444
+ "Retired: renamed to `momentum` with the opposite sense. Pass `momentum: false` for what `settle: true` meant; `settle: false` was the default, so drop the key."
119430
119445
  )
119431
- });
119446
+ }).refine(
119447
+ (p) => p.momentum !== false || (p.durationMs ?? DEFAULT_DURATION_MS) >= MOMENTUM_FREE_MIN_DURATION_MS,
119448
+ {
119449
+ message: `momentum: false needs durationMs of at least ${MOMENTUM_FREE_MIN_DURATION_MS}: below that the ease-out has too little wall clock for the OS velocity fit to read it as a stop rather than a flick, so it flings harder than a plain swipe and, on Android, backwards. Raise durationMs, or drop momentum: false for a plain flinging swipe at the duration you asked for.`,
119450
+ path: ["durationMs"]
119451
+ }
119452
+ );
119432
119453
  var capability9 = {
119433
119454
  apple: { simulator: true, device: true },
119434
119455
  appleRemote: { simulator: true },
@@ -119441,11 +119462,13 @@ var gestureSwipeTool = {
119441
119462
  completedMsg: ({ params }) => `Swiped from (${Math.round(params.fromX * 100)}%, ${Math.round(params.fromY * 100)}%) to (${Math.round(params.toX * 100)}%, ${Math.round(params.toY * 100)}%)`,
119442
119463
  failedMsg: ({ failureSignal: failureSignal2 }) => `Failed to swipe: ${failureSignal2.error_code}`
119443
119464
  },
119465
+ // The bounds are spelled out rather than interpolated: extract-tools scans this
119466
+ // description statically, so a `${}` in it drops the tool out of the scan.
119444
119467
  description: `Execute a smooth swipe / drag touch gesture between two points on the device (iOS simulator or Android emulator). All from/to positions are normalized 0.0\u20131.0 (fractions of screen width/height, not pixels), same as gesture-tap.
119445
119468
  Generates interpolated Move events for a natural feel (~60fps).
119446
119469
  Swipe up (fromY > toY) to scroll content down.
119447
119470
  Use when you need to scroll a list, dismiss a modal, drag an element, or navigate between pages. Not supported on Chromium \u2014 use gesture-scroll there instead.
119448
- Pass settle:true for a momentum-free swipe that lands exactly where the finger lifts (no fling), when you need a deterministic scroll distance. Returns { swiped: true, timestampMs }. Fails if the simulator-server / emulator backend is not reachable for the given device.`,
119471
+ Pass momentum:false for a momentum-free swipe that lands where the finger lifts (little to no fling at the 300 default), when you need a deterministic scroll distance; it needs durationMs >= 150 and is rejected below that, a shorter ease-out leaving the OS too little wall clock to read the deceleration as a stop. At 150 it lands short of the lift point instead, and 2 of 47 runs still flung backwards. A plain swipe takes any duration up to 10000ms and is delivered as close to the speed it was authored as a 16ms frame allows: below ~32ms the whole travel lands in one or two frames, which the OS flings as hard as it flings anything. Returns { swiped: true, timestampMs }. Fails if the simulator-server / emulator backend is not reachable for the given device.`,
119449
119472
  alwaysLoad: true,
119450
119473
  searchHint: "swipe scroll drag pan gesture device simulator emulator touch move",
119451
119474
  zodSchema: zodSchema17,
@@ -119453,18 +119476,47 @@ Pass settle:true for a momentum-free swipe that lands exactly where the finger l
119453
119476
  services: (params) => ({
119454
119477
  simulatorServer: simulatorServerRef(resolveDevice(params.udid))
119455
119478
  }),
119456
- async execute(services, params) {
119457
- const duration3 = params.durationMs ?? 300;
119458
- const settle = params.settle ?? false;
119479
+ async execute(services, params, ctx) {
119480
+ const duration3 = params.durationMs ?? DEFAULT_DURATION_MS;
119481
+ const momentumFree = params.momentum === false;
119459
119482
  const timestampMs = Date.now();
119460
119483
  const api = services.simulatorServer;
119461
119484
  const steps = Math.max(1, Math.round(duration3 / 16));
119485
+ let lastX = 0;
119486
+ let lastY = 0;
119462
119487
  for (let i = 0; i <= steps; i++) {
119488
+ if (ctx?.signal?.aborted) {
119489
+ if (i > 0) {
119490
+ sendCommand(api, {
119491
+ cmd: "touch",
119492
+ type: "Up",
119493
+ x: lastX,
119494
+ y: lastY,
119495
+ second_x: null,
119496
+ second_y: null
119497
+ });
119498
+ }
119499
+ const err = new Error(
119500
+ `gesture-swipe aborted - cancelled mid-gesture after ${i} of ${steps + 1} frames`
119501
+ );
119502
+ err.name = "AbortError";
119503
+ throw err;
119504
+ }
119463
119505
  const t = i / steps;
119464
- const progress = settle ? 1 - Math.pow(1 - t, SETTLE_EASE_EXPONENT) : t;
119506
+ const progress = momentumFree ? 1 - Math.pow(1 - t, MOMENTUM_FREE_EASE_EXPONENT) : t;
119465
119507
  const x = params.fromX + (params.toX - params.fromX) * progress;
119466
119508
  const y = params.fromY + (params.toY - params.fromY) * progress;
119467
119509
  const type = i === 0 ? "Down" : i === steps ? "Up" : "Move";
119510
+ if (type === "Up") {
119511
+ sendCommand(api, {
119512
+ cmd: "touch",
119513
+ type: "Move",
119514
+ x,
119515
+ y,
119516
+ second_x: null,
119517
+ second_y: null
119518
+ });
119519
+ }
119468
119520
  sendCommand(api, {
119469
119521
  cmd: "touch",
119470
119522
  type,
@@ -119473,6 +119525,8 @@ Pass settle:true for a momentum-free swipe that lands exactly where the finger l
119473
119525
  second_x: null,
119474
119526
  second_y: null
119475
119527
  });
119528
+ lastX = x;
119529
+ lastY = y;
119476
119530
  if (i < steps) await sleep3(16);
119477
119531
  }
119478
119532
  return { swiped: true, timestampMs };
@@ -119548,13 +119602,30 @@ Returns { scrolled: true, timestampMs }. Fails if the Chromium CDP session is no
119548
119602
  // ../tool-server/src/tools/gesture-drag/index.ts
119549
119603
  init_zod();
119550
119604
  var sleep5 = (ms) => new Promise((r) => setTimeout(r, ms));
119605
+ var MOMENTUM_FREE_EASE_EXPONENT2 = 3;
119606
+ var MOMENTUM_FREE_MIN_STEPS = 8;
119607
+ var MAX_DURATION_MS2 = 1e4;
119551
119608
  var zodSchema19 = external_exports.object({
119552
119609
  udid: external_exports.string().describe("Target Chromium device id from `list-devices` (chromium-cdp-<port>)."),
119553
119610
  fromX: external_exports.number().describe("Press x: normalized 0.0\u20131.0 (fraction of window width, not pixels)."),
119554
119611
  fromY: external_exports.number().describe("Press y: normalized 0.0\u20131.0 (fraction of window height, not pixels)."),
119555
119612
  toX: external_exports.number().describe("Release x: normalized 0.0\u20131.0 (not pixels; same space as tap)."),
119556
119613
  toY: external_exports.number().describe("Release y: normalized 0.0\u20131.0 (not pixels; same space as tap)."),
119557
- durationMs: external_exports.number().optional().describe("Total drag duration in milliseconds (default 300), interpolated at ~60fps.")
119614
+ durationMs: external_exports.number().max(MAX_DURATION_MS2, {
119615
+ message: `durationMs must be at most ${MAX_DURATION_MS2} (10s): the drag holds the left button down for exactly this long, one frame per ~16ms, so a larger value is that much wall clock spent mid-press.`
119616
+ }).optional().describe(
119617
+ `Total drag duration in milliseconds (default 300, at most ${MAX_DURATION_MS2} - the button stays down for exactly this long), interpolated at ~60fps.`
119618
+ ),
119619
+ momentum: external_exports.boolean().optional().describe(
119620
+ "Whether the drag releases with momentum; default true (a constant-speed drag). Pass false to decelerate into the release point (ease-out) so an app deriving fling from pointer release velocity (carousels, drag libraries) reads ~0 and applies little to no momentum \u2014 use it when the drag must stop where it was aimed rather than fling past. Deceleration needs wall clock: under ~100ms the whole drag fits inside the velocity window a page averages over (tens of ms), so some fling survives, and under ~70ms its extra frames cannot dispatch fast enough to fit durationMs. Keep durationMs at its default when the fling must be fully suppressed."
119621
+ ),
119622
+ // `momentum`'s earlier spelling, with the opposite polarity. Declared so this
119623
+ // non-strict object refuses it instead of stripping it and running the default.
119624
+ settle: external_exports.never({
119625
+ error: "gesture-drag's `settle` was renamed to `momentum`, with the opposite sense - write `momentum: false` for the momentum-free drag that `settle: true` used to mean (plain `settle: false` was the default, so just drop it)"
119626
+ }).optional().describe(
119627
+ "Retired: renamed to `momentum` with the opposite sense. Pass `momentum: false` for what `settle: true` meant; `settle: false` was the default, so drop the key."
119628
+ )
119558
119629
  });
119559
119630
  var capability11 = {
119560
119631
  chromium: { app: true }
@@ -119566,9 +119637,9 @@ var gestureDragTool = {
119566
119637
  completedMsg: ({ params }) => `Dragged from (${Math.round(params.fromX * 100)}%, ${Math.round(params.fromY * 100)}%) to (${Math.round(params.toX * 100)}%, ${Math.round(params.toY * 100)}%)`,
119567
119638
  failedMsg: ({ failureSignal: failureSignal2 }) => `Failed to drag: ${failureSignal2.error_code}`
119568
119639
  },
119569
- description: `Press the left mouse button at a start point, move to an end point, and release \u2014 a desktop mouse drag in a Chromium app. All positions are normalized 0.0\u20131.0 (fractions of the window, not pixels), same coordinate space as gesture-tap and describe. Interpolates mouse-move events at ~60fps over durationMs for a natural drag.
119640
+ description: `Press the left mouse button at a start point, move to an end point, and release \u2014 a desktop mouse drag in a Chromium app. All positions are normalized 0.0\u20131.0 (fractions of the window, not pixels), same coordinate space as gesture-tap and describe, except that a coordinate of exactly 1.0 lands one pixel inside the window edge (gesture-tap maps it to the edge itself). Interpolates mouse-move events at ~60fps over durationMs for a natural drag (a momentum-free drag samples more finely when durationMs is short, so its ease-out has a curve).
119570
119641
  Use for slider thumbs, drag-and-drop, text selection, or draggable UI elements. Dragging never scrolls content on desktop \u2014 use gesture-scroll for lists/pages. Chromium only \u2014 on iOS/Android use gesture-swipe.
119571
- Returns { dragged: true, timestampMs }. Fails if the Chromium CDP session is not reachable for the given device.`,
119642
+ Pass momentum:false for a momentum-free drag that decelerates into the release, so apps that compute a fling from the pointer stream read ~0 velocity and the drag ends where it was aimed instead of flinging past it (a durationMs under ~100ms is too short for the deceleration to suppress the fling entirely). Returns { dragged: true, timestampMs }. Fails if the Chromium CDP session is not reachable for the given device.`,
119572
119643
  alwaysLoad: true,
119573
119644
  searchHint: "drag drop slider mouse press move release chromium select",
119574
119645
  zodSchema: zodSchema19,
@@ -119576,15 +119647,49 @@ Returns { dragged: true, timestampMs }. Fails if the Chromium CDP session is not
119576
119647
  services: (params) => ({
119577
119648
  chromium: chromiumCdpRef(resolveDevice(params.udid))
119578
119649
  }),
119579
- async execute(services, params) {
119650
+ async execute(services, params, ctx) {
119580
119651
  const timestampMs = Date.now();
119581
119652
  const chromium = services.chromium;
119582
119653
  await assertChromiumWindowVisible(chromium, "drag", "chromium_drag_window_hidden");
119583
119654
  const vp = chromium.getViewport();
119584
- const startPx = { x: params.fromX * vp.width, y: params.fromY * vp.height };
119585
- const endPx = { x: params.toX * vp.width, y: params.toY * vp.height };
119655
+ const clampPx2 = (px, size) => Math.min(Math.max(px, 0), size - 1);
119656
+ const startPx = {
119657
+ x: clampPx2(params.fromX * vp.width, vp.width),
119658
+ y: clampPx2(params.fromY * vp.height, vp.height)
119659
+ };
119660
+ const endPx = {
119661
+ x: clampPx2(params.toX * vp.width, vp.width),
119662
+ y: clampPx2(params.toY * vp.height, vp.height)
119663
+ };
119586
119664
  const durationMs = params.durationMs ?? 300;
119587
- const steps = Math.max(2, Math.round(durationMs / 16));
119665
+ const momentumFree = params.momentum === false;
119666
+ const steps = Math.max(momentumFree ? MOMENTUM_FREE_MIN_STEPS : 2, Math.round(durationMs / 16));
119667
+ const frameMs = durationMs / steps;
119668
+ const t0 = Date.now();
119669
+ let lastX = startPx.x;
119670
+ let lastY = startPx.y;
119671
+ const abortError = (frame) => {
119672
+ const err = new Error(
119673
+ `gesture-drag aborted - cancelled mid-drag after ${frame} of ${steps + 1} frames`
119674
+ );
119675
+ err.name = "AbortError";
119676
+ return err;
119677
+ };
119678
+ const releaseAndAbort = async (frame) => {
119679
+ const err = abortError(frame);
119680
+ try {
119681
+ await chromium.dispatchMouseEvent({
119682
+ type: "mouseReleased",
119683
+ x: lastX,
119684
+ y: lastY,
119685
+ clickCount: 1
119686
+ });
119687
+ } catch (releaseErr) {
119688
+ err.cause = releaseErr;
119689
+ }
119690
+ throw err;
119691
+ };
119692
+ if (ctx?.signal?.aborted) throw abortError(0);
119588
119693
  await chromium.dispatchMouseEvent({
119589
119694
  type: "mousePressed",
119590
119695
  x: startPx.x,
@@ -119592,15 +119697,19 @@ Returns { dragged: true, timestampMs }. Fails if the Chromium CDP session is not
119592
119697
  clickCount: 1
119593
119698
  });
119594
119699
  for (let i = 1; i < steps; i++) {
119700
+ if (ctx?.signal?.aborted) await releaseAndAbort(i);
119701
+ await sleep5(Math.max(0, t0 + i * frameMs - Date.now()));
119595
119702
  const t = i / steps;
119596
- await chromium.dispatchMouseEvent({
119597
- type: "mouseMoved",
119598
- x: startPx.x + (endPx.x - startPx.x) * t,
119599
- y: startPx.y + (endPx.y - startPx.y) * t,
119600
- button: "left"
119601
- });
119602
- await sleep5(16);
119603
- }
119703
+ const progress = momentumFree ? 1 - Math.pow(1 - t, MOMENTUM_FREE_EASE_EXPONENT2) : t;
119704
+ const x = startPx.x + (endPx.x - startPx.x) * progress;
119705
+ const y = startPx.y + (endPx.y - startPx.y) * progress;
119706
+ await chromium.dispatchMouseEvent({ type: "mouseMoved", x, y, button: "left" });
119707
+ lastX = x;
119708
+ lastY = y;
119709
+ }
119710
+ if (ctx?.signal?.aborted) await releaseAndAbort(steps);
119711
+ await sleep5(Math.max(0, t0 + durationMs - Date.now()));
119712
+ if (ctx?.signal?.aborted) await releaseAndAbort(steps);
119604
119713
  await chromium.dispatchMouseEvent({
119605
119714
  type: "mouseReleased",
119606
119715
  x: endPx.x,
@@ -127820,9 +127929,9 @@ a prior tap), use individual tool calls instead.
127820
127929
  Allowed tools and their args (udid is auto-injected, do NOT include it in args):
127821
127930
 
127822
127931
  gesture-tap: { x: number, y: number, clickCount?: number } [ios/android/chromium]
127823
- gesture-swipe: { fromX: number, fromY: number, toX: number, toY: number, durationMs?: number } [ios/android]
127932
+ gesture-swipe: { fromX: number, fromY: number, toX: number, toY: number, durationMs?: number, momentum?: boolean } [ios/android]
127824
127933
  gesture-scroll: { x: number, y: number, deltaX?: number, deltaY?: number, durationMs?: number } [chromium only]
127825
- gesture-drag: { fromX: number, fromY: number, toX: number, toY: number, durationMs?: number } [chromium only]
127934
+ gesture-drag: { fromX: number, fromY: number, toX: number, toY: number, durationMs?: number, momentum?: boolean } [chromium only]
127826
127935
  gesture-custom: { events: [{ type: "Down"|"Move"|"Up", x: number, y: number, x2?: number, y2?: number, delayMs?: number }], interpolate?: number } [ios/android]
127827
127936
  gesture-pinch: { centerX: number, centerY: number, startDistance: number, endDistance: number, endCenterX?: number, endCenterY?: number, angle?: number, durationMs?: number } [ios/android]
127828
127937
  gesture-rotate: { centerX: number, centerY: number, radius?: number, radiusX?: number, radiusY?: number, startAngle: number, endAngle: number, durationMs?: number } [ios/android]
@@ -140883,6 +140992,12 @@ function chromiumLaunchSpec(launch) {
140883
140992
  return typeof c === "string" ? { path: c } : { path: c.path, args: c.args };
140884
140993
  }
140885
140994
  function selectorToYaml(sel) {
140995
+ const unknown2 = Object.keys(sel).filter((key2) => !WRITABLE_SELECTOR_KEYS.includes(key2));
140996
+ if (unknown2.length > 0) {
140997
+ throw new Error(
140998
+ `Cannot serialize flow selector: ${describeUnknownKeys(unknown2, WRITABLE_SELECTOR_KEYS)} - allowed keys: ${WRITABLE_SELECTOR_KEYS.join(", ")}.`
140999
+ );
141000
+ }
140886
141001
  if (sel.text !== void 0 && sel.textMatches !== void 0) {
140887
141002
  throw new Error(
140888
141003
  'Cannot serialize flow selector without losing constraints: both `text` and `textMatches` are set, but flow YAML can represent only one `text` constraint (a literal string or `{ matches: "<regex>" }`). Use either literal or regex text matching.'
@@ -141005,6 +141120,79 @@ function targetToYaml(step) {
141005
141120
  }
141006
141121
  return { x: step.x, y: step.y };
141007
141122
  }
141123
+ function swipeTargetToYaml(target, label) {
141124
+ const yaml = targetToYaml(target);
141125
+ if (typeof yaml !== "string" && "x" in yaml && !Object.keys(target).every((key2) => key2 === "x" || key2 === "y")) {
141126
+ throw new Error(`Cannot serialize flow ${label}: a coordinate target takes only { x, y }`);
141127
+ }
141128
+ return yaml;
141129
+ }
141130
+ var SWIPE_MIN_TRAVEL = 0.03;
141131
+ var SWIPE_MIN_DURATION_MS = 150;
141132
+ var SWIPE_MAX_DURATION_MS = 1e4;
141133
+ var LONG_PRESS_MAX_DURATION_MS = SWIPE_MAX_DURATION_MS;
141134
+ function swipeByToYaml(by) {
141135
+ const keys = Object.keys(by);
141136
+ if (keys.some((key2) => key2 !== "x" && key2 !== "y")) {
141137
+ throw new Error("Cannot serialize flow swipe.by: accepts only x and y");
141138
+ }
141139
+ const axes = ["x", "y"].filter((axis) => by[axis] !== void 0);
141140
+ if (axes.length === 0) {
141141
+ throw new Error("Cannot serialize flow swipe.by: needs at least one of x or y");
141142
+ }
141143
+ const result = {};
141144
+ for (const axis of axes) {
141145
+ const value = by[axis];
141146
+ if (!Number.isFinite(value) || value === 0 || value < -1 || value > 1) {
141147
+ throw new Error(
141148
+ `Cannot serialize flow swipe.by.${axis}: must be a non-zero fraction of the screen between -1 and 1`
141149
+ );
141150
+ }
141151
+ result[axis] = value;
141152
+ }
141153
+ const magnitude = Math.hypot(result.x ?? 0, result.y ?? 0);
141154
+ if (magnitude < SWIPE_MIN_TRAVEL) {
141155
+ throw new Error(
141156
+ `Cannot serialize flow swipe.by: travels only ${magnitude} \u2014 below the minimum swipe travel of ${SWIPE_MIN_TRAVEL} \u2014 a travel that small is a tap, not a swipe`
141157
+ );
141158
+ }
141159
+ return result;
141160
+ }
141161
+ function swipeByLabel(by) {
141162
+ return ["x", "y"].filter((axis) => by[axis] !== void 0).map((axis) => `${axis}=${by[axis]}`).join(", ");
141163
+ }
141164
+ function isPositiveMs(raw) {
141165
+ return typeof raw === "number" && Number.isFinite(raw) && raw > 0;
141166
+ }
141167
+ function positiveMsToYaml(value, label) {
141168
+ if (!isPositiveMs(value)) {
141169
+ throw new Error(`Cannot serialize flow ${label}: needs a positive number of milliseconds`);
141170
+ }
141171
+ return value;
141172
+ }
141173
+ function swipeDurationToYaml(value) {
141174
+ const duration3 = positiveMsToYaml(value, "swipe.duration");
141175
+ if (duration3 < SWIPE_MIN_DURATION_MS) {
141176
+ throw new Error(
141177
+ `Cannot serialize flow swipe.duration: only ${duration3}ms \u2014 below the minimum swipe duration of ${SWIPE_MIN_DURATION_MS}ms \u2014 that leaves too few 16ms frames for the content to track the travel it was given, so it overshoots instead of landing on it`
141178
+ );
141179
+ }
141180
+ if (duration3 > SWIPE_MAX_DURATION_MS) {
141181
+ throw new Error(
141182
+ `Cannot serialize flow swipe.duration: ${duration3}ms - above the maximum swipe duration of ${SWIPE_MAX_DURATION_MS}ms - the step would hold a finger on the screen for exactly that long, one dispatched frame per 16ms`
141183
+ );
141184
+ }
141185
+ return duration3;
141186
+ }
141187
+ function longPressDurationToYaml(value) {
141188
+ const duration3 = positiveMsToYaml(value, "long-press.duration");
141189
+ if (duration3 > LONG_PRESS_MAX_DURATION_MS) {
141190
+ throw new Error(
141191
+ `Cannot serialize flow long-press.duration: ${duration3}ms - above the maximum long-press duration of ${LONG_PRESS_MAX_DURATION_MS}ms - the step would hold a finger down for exactly that long`
141192
+ );
141193
+ }
141194
+ return duration3;
141195
+ }
141008
141196
  function waitToYaml(condition, selector, expectedText, textMatch, timeoutMs) {
141009
141197
  const sel = selectorToYaml(selector);
141010
141198
  let body;
@@ -141022,7 +141210,7 @@ function waitToYaml(condition, selector, expectedText, textMatch, timeoutMs) {
141022
141210
  body = textWaitToYaml(sel, expectedText, textMatch);
141023
141211
  break;
141024
141212
  }
141025
- if (timeoutMs !== void 0) body.timeout = timeoutMs;
141213
+ if (timeoutMs !== void 0) body.timeout = positiveMsToYaml(timeoutMs, "await.timeout");
141026
141214
  return body;
141027
141215
  }
141028
141216
  function idleToYaml(step) {
@@ -141058,9 +141246,29 @@ function toYamlStep(step) {
141058
141246
  case "long-press": {
141059
141247
  const target = targetToYaml(step);
141060
141248
  return {
141061
- "long-press": step.duration !== void 0 ? { on: target, duration: step.duration } : target
141249
+ "long-press": step.duration !== void 0 ? { on: target, duration: longPressDurationToYaml(step.duration) } : target
141062
141250
  };
141063
141251
  }
141252
+ case "swipe": {
141253
+ const travels = ["direction", "to", "by"].filter((key2) => step[key2] !== void 0);
141254
+ if (travels.length !== 1) {
141255
+ throw new Error("Cannot serialize flow swipe: needs exactly one of direction, to, or by");
141256
+ }
141257
+ if (step.momentum !== void 0 && typeof step.momentum !== "boolean") {
141258
+ throw new Error("Cannot serialize flow swipe.momentum: must be true or false");
141259
+ }
141260
+ if (step.direction !== void 0 && step.from === void 0 && step.to === void 0 && step.by === void 0 && step.momentum !== false && step.duration === void 0) {
141261
+ return { swipe: step.direction };
141262
+ }
141263
+ const body = {};
141264
+ if (step.from !== void 0) body.from = swipeTargetToYaml(step.from, "swipe.from");
141265
+ if (step.direction !== void 0) body.direction = step.direction;
141266
+ if (step.to !== void 0) body.to = swipeTargetToYaml(step.to, "swipe.to");
141267
+ if (step.by !== void 0) body.by = swipeByToYaml(step.by);
141268
+ if (step.momentum === false) body.momentum = false;
141269
+ if (step.duration !== void 0) body.duration = swipeDurationToYaml(step.duration);
141270
+ return { swipe: body };
141271
+ }
141064
141272
  case "type": {
141065
141273
  const body = {
141066
141274
  into: selectorToYaml(step.into),
@@ -141153,6 +141361,12 @@ function badEntry(raw, detail) {
141153
141361
  error_kind: "validation"
141154
141362
  });
141155
141363
  }
141364
+ function parsePositiveMs(raw, entry, label, example) {
141365
+ if (!isPositiveMs(raw)) {
141366
+ badEntry(entry, `${label} needs a positive number of milliseconds (e.g. \`${example}\`)`);
141367
+ }
141368
+ return raw;
141369
+ }
141156
141370
  function validatePattern(raw, pattern, where) {
141157
141371
  try {
141158
141372
  new RegExp(pattern);
@@ -141216,6 +141430,17 @@ var SELECTOR_KEYS = [
141216
141430
  "any",
141217
141431
  ...SELECTOR_RELATIONS
141218
141432
  ];
141433
+ var WRITABLE_SELECTOR_KEYS = Object.keys({
141434
+ text: true,
141435
+ textMatches: true,
141436
+ identifier: true,
141437
+ role: true,
141438
+ any: true,
141439
+ loose: true,
141440
+ within: true,
141441
+ after: true,
141442
+ next: true
141443
+ });
141219
141444
  var MAX_SELECTOR_SCOPES = 6;
141220
141445
  function parseSelector(raw, where, budget = { scopes: MAX_SELECTOR_SCOPES }) {
141221
141446
  if (budget.scopes < 0) {
@@ -141346,7 +141571,7 @@ function parseWaitFields(raw, kind) {
141346
141571
  "assert has no timeout \u2014 it is an immediate check; use `await` for a timed wait"
141347
141572
  );
141348
141573
  }
141349
- timeout = parseAwaitTimeout({ [kind]: b }, b.timeout);
141574
+ timeout = parsePositiveMs(b.timeout, { [kind]: b }, "await.timeout", "timeout: 10000");
141350
141575
  }
141351
141576
  rejectUnknownKeys(
141352
141577
  { [kind]: b },
@@ -141515,6 +141740,7 @@ var STEP_DIRECTIVE_KEYS = [
141515
141740
  "tool",
141516
141741
  "tap",
141517
141742
  "long-press",
141743
+ "swipe",
141518
141744
  "type",
141519
141745
  "await",
141520
141746
  "assert",
@@ -141613,13 +141839,19 @@ function parseLongPress(body, entry) {
141613
141839
  }
141614
141840
  const step = { kind: "long-press", ...parseTarget(obj.on, "long-press.on") };
141615
141841
  if (obj.duration !== void 0) {
141616
- if (typeof obj.duration !== "number" || !Number.isFinite(obj.duration) || obj.duration <= 0) {
141842
+ const duration3 = parsePositiveMs(
141843
+ obj.duration,
141844
+ entry,
141845
+ "long-press.duration",
141846
+ "duration: 1200"
141847
+ );
141848
+ if (duration3 > LONG_PRESS_MAX_DURATION_MS) {
141617
141849
  badEntry(
141618
141850
  entry,
141619
- "long-press.duration needs a positive number of milliseconds (e.g. `duration: 1200`)"
141851
+ `long-press.duration is ${duration3}ms - above the maximum long-press duration of ${LONG_PRESS_MAX_DURATION_MS}ms; the step holds a finger down for exactly that long, and on Chromium it dispatches a gesture-drag that refuses more`
141620
141852
  );
141621
141853
  }
141622
- step.duration = obj.duration;
141854
+ step.duration = duration3;
141623
141855
  }
141624
141856
  return step;
141625
141857
  }
@@ -141789,6 +142021,116 @@ function completeRunExtension(value) {
141789
142021
  const candidate = `${value}.yaml`;
141790
142022
  return FLOW_FILE_NAME_PATTERN.test(path32.posix.basename(candidate)) ? candidate : value;
141791
142023
  }
142024
+ var SWIPE_DIRECTIONS = ["up", "down", "left", "right"];
142025
+ var SWIPE_OPTION_KEYS = ["from", "direction", "to", "by", "momentum", "duration"];
142026
+ function parseSwipeBy(raw, entry) {
142027
+ if (raw === null || typeof raw !== "object") {
142028
+ badEntry(entry, "swipe.by needs { x } and/or { y } \u2014 signed 0\u20131 fractions of the screen");
142029
+ }
142030
+ const obj = raw;
142031
+ rejectUnknownKeys(entry, obj, ["x", "y"], "swipe.by");
142032
+ if (obj.x === void 0 && obj.y === void 0) {
142033
+ badEntry(entry, "swipe.by needs at least one of x, y");
142034
+ }
142035
+ const by = {};
142036
+ for (const axis of ["x", "y"]) {
142037
+ const v = obj[axis];
142038
+ if (v === void 0) continue;
142039
+ if (typeof v !== "number" || !Number.isFinite(v) || v === 0 || v < -1 || v > 1) {
142040
+ badEntry(
142041
+ entry,
142042
+ `swipe.by.${axis} must be a non-zero fraction of the screen between -1 and 1 (omit the axis instead of 0)`
142043
+ );
142044
+ }
142045
+ by[axis] = v;
142046
+ }
142047
+ const magnitude = Math.hypot(by.x ?? 0, by.y ?? 0);
142048
+ if (magnitude < SWIPE_MIN_TRAVEL) {
142049
+ badEntry(
142050
+ entry,
142051
+ `swipe.by travels only ${magnitude} \u2014 below the minimum swipe travel of ${SWIPE_MIN_TRAVEL}; a travel that small is a tap, not a swipe`
142052
+ );
142053
+ }
142054
+ return by;
142055
+ }
142056
+ function parseSwipe(body, entry) {
142057
+ if (typeof body === "string") {
142058
+ if (!SWIPE_DIRECTIONS.includes(body)) {
142059
+ badEntry(
142060
+ entry,
142061
+ `swipe takes a direction (${SWIPE_DIRECTIONS.join(", ")}) \u2014 to anchor on an element use swipe: { from: <target>, direction: \u2026 }`
142062
+ );
142063
+ }
142064
+ return { kind: "swipe", direction: body };
142065
+ }
142066
+ if (body === null || typeof body !== "object") {
142067
+ badEntry(entry, `swipe needs a direction (${SWIPE_DIRECTIONS.join(", ")}) or an options map`);
142068
+ }
142069
+ const obj = body;
142070
+ if (hasSelectorField(obj)) {
142071
+ badEntry(
142072
+ entry,
142073
+ 'the swipe options form takes a nested target \u2014 e.g. swipe: { from: { text: "Card" }, direction: left }'
142074
+ );
142075
+ }
142076
+ if (obj.x !== void 0 || obj.y !== void 0) {
142077
+ badEntry(
142078
+ entry,
142079
+ "the swipe options form takes a nested point \u2014 e.g. swipe: { from: { x: 0.5, y: 0.5 }, direction: left }"
142080
+ );
142081
+ }
142082
+ if (obj.settle !== void 0) {
142083
+ badEntry(
142084
+ entry,
142085
+ "swipe.settle was renamed to swipe.momentum, with the opposite sense \u2014 write `momentum: false` for the momentum-free swipe that `settle: true` used to mean (plain `settle: false` was the default, so just drop it)"
142086
+ );
142087
+ }
142088
+ rejectUnknownKeys(entry, obj, SWIPE_OPTION_KEYS, "swipe");
142089
+ const travels = ["direction", "to", "by"].filter((k) => obj[k] !== void 0);
142090
+ if (travels.length !== 1) {
142091
+ badEntry(entry, "swipe needs exactly one of `direction`, `to`, or `by`");
142092
+ }
142093
+ const step = { kind: "swipe" };
142094
+ if (obj.from !== void 0) step.from = parseTarget(obj.from, "swipe.from");
142095
+ switch (travels[0]) {
142096
+ case "direction": {
142097
+ if (typeof obj.direction !== "string" || !SWIPE_DIRECTIONS.includes(obj.direction)) {
142098
+ badEntry(entry, `swipe.direction must be one of ${SWIPE_DIRECTIONS.join(", ")}`);
142099
+ }
142100
+ step.direction = obj.direction;
142101
+ break;
142102
+ }
142103
+ case "to":
142104
+ step.to = parseTarget(obj.to, "swipe.to");
142105
+ break;
142106
+ case "by":
142107
+ step.by = parseSwipeBy(obj.by, entry);
142108
+ break;
142109
+ }
142110
+ if (obj.momentum !== void 0) {
142111
+ if (typeof obj.momentum !== "boolean") {
142112
+ badEntry(entry, "swipe.momentum must be true or false");
142113
+ }
142114
+ if (!obj.momentum) step.momentum = false;
142115
+ }
142116
+ if (obj.duration !== void 0) {
142117
+ const duration3 = parsePositiveMs(obj.duration, entry, "swipe.duration", "duration: 800");
142118
+ if (duration3 < SWIPE_MIN_DURATION_MS) {
142119
+ badEntry(
142120
+ entry,
142121
+ `swipe.duration is only ${duration3}ms \u2014 below the minimum swipe duration of ${SWIPE_MIN_DURATION_MS}ms; that leaves too few 16ms frames for the content to track the travel it was given, so it overshoots instead of landing on it`
142122
+ );
142123
+ }
142124
+ if (duration3 > SWIPE_MAX_DURATION_MS) {
142125
+ badEntry(
142126
+ entry,
142127
+ `swipe.duration is ${duration3}ms - above the maximum swipe duration of ${SWIPE_MAX_DURATION_MS}ms; the step holds a finger on the screen for exactly that long, one dispatched frame per 16ms, and nothing outside the run can cut it short`
142128
+ );
142129
+ }
142130
+ step.duration = duration3;
142131
+ }
142132
+ return step;
142133
+ }
141792
142134
  function fromYamlStep(raw, blockDepth = 0) {
141793
142135
  const entry = raw;
141794
142136
  if ("optional" in raw) {
@@ -141830,6 +142172,7 @@ function fromYamlStep(raw, blockDepth = 0) {
141830
142172
  if ("long-press" in raw) {
141831
142173
  return parseLongPress(raw["long-press"], raw);
141832
142174
  }
142175
+ if ("swipe" in raw) return parseSwipe(raw.swipe, raw);
141833
142176
  if ("type" in raw) {
141834
142177
  const body = raw.type;
141835
142178
  if (!body || typeof body !== "object") badEntry(raw, "type needs { into, text }");
@@ -142446,6 +142789,7 @@ function stepRequiresDevice(registry2, step) {
142446
142789
  case "launch":
142447
142790
  case "tap":
142448
142791
  case "long-press":
142792
+ case "swipe":
142449
142793
  case "type":
142450
142794
  case "await":
142451
142795
  case "assert":
@@ -143287,22 +143631,32 @@ async function settleTree(env, opts = {}) {
143287
143631
  if (!await sleepOrAbort(SETTLE_POLL_MS, env.signal)) return void 0;
143288
143632
  }
143289
143633
  }
143290
- async function waitForFrame(env, selector) {
143634
+ async function waitForFrames(env, selectors) {
143635
+ const pending = selectors.flatMap((selector, i) => selector ? [{ i, selector }] : []);
143636
+ if (pending.length === 0) return selectors.map(() => void 0);
143291
143637
  const deadline = Date.now() + DEFAULT_ACTION_TIMEOUT_MS;
143638
+ let unresolved = pending[0].selector;
143292
143639
  for (; ; ) {
143293
143640
  if (env.signal?.aborted) return "aborted";
143294
143641
  const tree = await settleTree(env);
143295
143642
  if (tree) {
143296
- const frame = flowSelectorToFrame(tree, selector);
143297
- if (frame) return frame;
143643
+ const frames = selectors.map((s) => s ? flowSelectorToFrame(tree, s) : void 0);
143644
+ const missing = pending.find(({ i }) => frames[i] === void 0);
143645
+ if (!missing) return frames;
143646
+ unresolved = missing.selector;
143298
143647
  } else if (env.signal?.aborted) {
143299
143648
  return "aborted";
143300
143649
  }
143301
- if (Date.now() >= deadline) return void 0;
143650
+ if (Date.now() >= deadline) return { unresolved };
143302
143651
  const sleepMs = Math.min(POLL_INTERVAL_MS, Math.max(0, deadline - Date.now()));
143303
143652
  if (!await sleepOrAbort(sleepMs, env.signal)) return "aborted";
143304
143653
  }
143305
143654
  }
143655
+ async function waitForFrame(env, selector) {
143656
+ const frames = await waitForFrames(env, [selector]);
143657
+ if (frames === "aborted") return "aborted";
143658
+ return Array.isArray(frames) ? frames[0] : void 0;
143659
+ }
143306
143660
  function framesOverlap(a, b) {
143307
143661
  return a.x < b.x + b.width && b.x < a.x + a.width && a.y < b.y + b.height && b.y < a.y + a.height;
143308
143662
  }
@@ -143366,14 +143720,19 @@ async function scrollIncrement(env, direction, region) {
143366
143720
  to = { x: clamp014(cx + dist), y: cy };
143367
143721
  break;
143368
143722
  }
143369
- await invokeOnDevice(env, "gesture-swipe", {
143370
- fromX: cx,
143371
- fromY: cy,
143372
- toX: to.x,
143373
- toY: to.y,
143374
- settle: true,
143375
- durationMs: 600
143376
- });
143723
+ try {
143724
+ await invokeOnDevice(env, "gesture-swipe", {
143725
+ fromX: cx,
143726
+ fromY: cy,
143727
+ toX: to.x,
143728
+ toY: to.y,
143729
+ momentum: false,
143730
+ durationMs: 600
143731
+ });
143732
+ } catch (err) {
143733
+ if (env.signal?.aborted) return;
143734
+ throw err;
143735
+ }
143377
143736
  }
143378
143737
  async function scrollToVisible(env, target, direction, within) {
143379
143738
  let prevFp;
@@ -143407,7 +143766,7 @@ function offscreenHint(sel) {
143407
143766
  return `no visible element matched selector ${describeSelector(sel)} \u2014 if it is off-screen, add a scroll-to step before this one`;
143408
143767
  }
143409
143768
  async function runDirective(env, step) {
143410
- if (env.device.platform === "vega" && (step.kind === "tap" || step.kind === "long-press" || step.kind === "type" || step.kind === "scroll-to" || step.kind === "pinch" || step.kind === "rotate")) {
143769
+ if (env.device.platform === "vega" && (step.kind === "tap" || step.kind === "long-press" || step.kind === "swipe" || step.kind === "type" || step.kind === "scroll-to" || step.kind === "pinch" || step.kind === "rotate")) {
143411
143770
  return {
143412
143771
  ok: false,
143413
143772
  reason: `${step.kind} is a touch directive and Vega is remote-driven \u2014 move focus with \`tool: tv-remote\` steps (and type via \`tool: keyboard\`) instead`
@@ -143424,6 +143783,8 @@ async function runDirective(env, step) {
143424
143783
  return runTap(env, step);
143425
143784
  case "long-press":
143426
143785
  return runLongPress(env, step);
143786
+ case "swipe":
143787
+ return runSwipe(env, step);
143427
143788
  case "type":
143428
143789
  return runType(env, step);
143429
143790
  case "await":
@@ -143471,10 +143832,16 @@ async function resolveTargetPoint(env, target) {
143471
143832
  }
143472
143833
  return { point: getDescribeTapPoint(frame) };
143473
143834
  }
143835
+ const point = targetPointFromFrame(target, void 0);
143836
+ if ("fail" in point) return point;
143837
+ const settle = await settleForGesture(env);
143838
+ if (settle.aborted) return { fail: ABORTED_OUTCOME };
143839
+ return { point, ...warned(settle) };
143840
+ }
143841
+ function targetPointFromFrame(target, frame) {
143842
+ if (frame) return getDescribeTapPoint(frame);
143474
143843
  if (typeof target.x === "number" && typeof target.y === "number") {
143475
- const settle = await settleForGesture(env);
143476
- if (settle.aborted) return { fail: ABORTED_OUTCOME };
143477
- return { point: { x: target.x, y: target.y }, ...warned(settle) };
143844
+ return { x: target.x, y: target.y };
143478
143845
  }
143479
143846
  return { fail: { ok: false, reason: "gesture needs a selector or x/y coordinates" } };
143480
143847
  }
@@ -143494,13 +143861,18 @@ async function runLongPress(env, step) {
143494
143861
  const point = resolved.point;
143495
143862
  const duration3 = step.duration ?? DEFAULT_LONG_PRESS_MS;
143496
143863
  if (env.device.platform === "chromium") {
143497
- await invokeOnDevice(env, "gesture-drag", {
143498
- fromX: point.x,
143499
- fromY: point.y,
143500
- toX: point.x,
143501
- toY: point.y,
143502
- durationMs: duration3
143503
- });
143864
+ try {
143865
+ await invokeOnDevice(env, "gesture-drag", {
143866
+ fromX: point.x,
143867
+ fromY: point.y,
143868
+ toX: point.x,
143869
+ toY: point.y,
143870
+ durationMs: duration3
143871
+ });
143872
+ } catch (err) {
143873
+ if (env.signal?.aborted) return ABORTED_OUTCOME;
143874
+ throw err;
143875
+ }
143504
143876
  } else {
143505
143877
  await invokeOnDevice(env, "gesture-custom", {
143506
143878
  events: [
@@ -143620,6 +143992,132 @@ async function runRotate(env, step) {
143620
143992
  }
143621
143993
  return { ok: true, ...warned(settle) };
143622
143994
  }
143995
+ var SWIPE_GEOMETRY = {
143996
+ left: { start: { x: 0.9, y: 0.5 }, axis: "x", end: 0.1 },
143997
+ right: { start: { x: 0.1, y: 0.5 }, axis: "x", end: 0.9 },
143998
+ down: { start: { x: 0.5, y: 0.2 }, axis: "y", end: 0.9 },
143999
+ up: { start: { x: 0.5, y: 0.5 }, axis: "y", end: 0.1 }
144000
+ };
144001
+ async function runSwipe(env, step) {
144002
+ const ends = [step.from, step.to];
144003
+ const selectors = ends.map((end2) => end2 && "selector" in end2 ? end2.selector : void 0);
144004
+ const frames = await waitForFrames(env, selectors);
144005
+ if (frames === "aborted") return ABORTED_OUTCOME;
144006
+ if (!Array.isArray(frames)) return { ok: false, reason: offscreenHint(frames.unresolved) };
144007
+ const [fromFrame, toFrame] = frames;
144008
+ let settle = {};
144009
+ if (selectors.every((selector) => selector === void 0)) {
144010
+ settle = await settleForGesture(env);
144011
+ if (settle.aborted) return ABORTED_OUTCOME;
144012
+ }
144013
+ let toPoint;
144014
+ if (step.to) {
144015
+ const p = targetPointFromFrame(step.to, toFrame);
144016
+ if ("fail" in p) return p.fail;
144017
+ toPoint = p;
144018
+ }
144019
+ let start2;
144020
+ if (step.from) {
144021
+ const p = targetPointFromFrame(step.from, fromFrame);
144022
+ if ("fail" in p) return p.fail;
144023
+ start2 = p;
144024
+ } else if (step.direction) {
144025
+ start2 = { ...SWIPE_GEOMETRY[step.direction].start };
144026
+ } else {
144027
+ start2 = { x: 0.5, y: 0.5 };
144028
+ }
144029
+ if (!Number.isFinite(start2.x) || start2.x < 0 || start2.x > 1 || !Number.isFinite(start2.y) || start2.y < 0 || start2.y > 1) {
144030
+ return {
144031
+ ok: false,
144032
+ reason: `swipe.from resolved outside the normalized screen: (${start2.x}, ${start2.y}); both coordinates must be between 0 and 1`
144033
+ };
144034
+ }
144035
+ let end;
144036
+ if (step.direction) {
144037
+ const g = SWIPE_GEOMETRY[step.direction];
144038
+ const startOnTravelAxis = start2[g.axis];
144039
+ const endOnTravelAxis = step.from ? clamp014(startOnTravelAxis + (g.end - g.start[g.axis])) : g.end;
144040
+ end = g.axis === "x" ? { x: endOnTravelAxis, y: start2.y } : { x: start2.x, y: endOnTravelAxis };
144041
+ const travel2 = Math.abs(endOnTravelAxis - startOnTravelAxis);
144042
+ if (travel2 < SWIPE_MIN_TRAVEL) {
144043
+ return {
144044
+ ok: false,
144045
+ reason: `cannot swipe ${step.direction} from ${g.axis}=${startOnTravelAxis}: only ${travel2} of travel to the screen edge, less than the minimum swipe travel of ${SWIPE_MIN_TRAVEL} \u2014 a tap, not a swipe`
144046
+ };
144047
+ }
144048
+ } else if (step.by) {
144049
+ const overflowAxis = ["x", "y"].find((axis) => {
144050
+ const d = step.by[axis];
144051
+ return d !== void 0 && (start2[axis] + d < 0 || start2[axis] + d > 1);
144052
+ });
144053
+ if (overflowAxis === void 0) {
144054
+ end = {
144055
+ x: step.by.x !== void 0 ? start2.x + step.by.x : start2.x,
144056
+ y: step.by.y !== void 0 ? start2.y + step.by.y : start2.y
144057
+ };
144058
+ } else if (step.from) {
144059
+ const requested = step.by[overflowAxis];
144060
+ const raw = start2[overflowAxis] + requested;
144061
+ return {
144062
+ ok: false,
144063
+ reason: `swipe.by.${overflowAxis} of ${requested} from ${overflowAxis}=${start2[overflowAxis]} lands at ${raw}, off the normalized screen; reduce the delta so from + by stays within [0, 1]`
144064
+ };
144065
+ } else {
144066
+ end = { x: start2.x, y: start2.y };
144067
+ for (const axis of ["x", "y"]) {
144068
+ const d = step.by[axis];
144069
+ if (d === void 0) {
144070
+ end[axis] = start2[axis];
144071
+ continue;
144072
+ }
144073
+ const s = start2[axis];
144074
+ const lo = Math.min(s, s + d);
144075
+ const hi = Math.max(s, s + d);
144076
+ const shift = lo < 0 ? -lo : hi > 1 ? 1 - hi : 0;
144077
+ start2[axis] = s + shift;
144078
+ end[axis] = s + d + shift;
144079
+ }
144080
+ }
144081
+ } else {
144082
+ end = toPoint;
144083
+ if (!Number.isFinite(end.x) || end.x < 0 || end.x > 1 || !Number.isFinite(end.y) || end.y < 0 || end.y > 1) {
144084
+ return {
144085
+ ok: false,
144086
+ reason: `swipe.to resolved outside the normalized screen: (${end.x}, ${end.y}); both coordinates must be between 0 and 1`
144087
+ };
144088
+ }
144089
+ if (Math.hypot(end.x - start2.x, end.y - start2.y) < SWIPE_MIN_TRAVEL) {
144090
+ return {
144091
+ ok: false,
144092
+ reason: `swipe.to (${end.x}, ${end.y}) resolved within the minimum swipe travel of the start point (${start2.x}, ${start2.y}); aim it at a point or element farther from the start`
144093
+ };
144094
+ }
144095
+ }
144096
+ const travel = {
144097
+ fromX: start2.x,
144098
+ fromY: start2.y,
144099
+ toX: end.x,
144100
+ toY: end.y,
144101
+ ...step.duration !== void 0 ? { durationMs: step.duration } : {},
144102
+ ...step.momentum === false ? { momentum: false } : {}
144103
+ };
144104
+ try {
144105
+ await invokeOnDevice(
144106
+ env,
144107
+ env.device.platform === "chromium" ? "gesture-drag" : "gesture-swipe",
144108
+ travel
144109
+ );
144110
+ } catch (err) {
144111
+ if (env.signal?.aborted) return ABORTED_OUTCOME;
144112
+ throw err;
144113
+ }
144114
+ try {
144115
+ await settleTree(env);
144116
+ } catch {
144117
+ }
144118
+ if (env.signal?.aborted) return ABORTED_OUTCOME;
144119
+ return { ok: true, ...warned(settle) };
144120
+ }
143623
144121
  async function runType(env, step) {
143624
144122
  const frame = await waitForFrame(env, step.into);
143625
144123
  if (frame === "aborted") return ABORTED_OUTCOME;
@@ -143951,6 +144449,9 @@ function textConditionLabel(sel, expectedText, textMatch) {
143951
144449
  const expected = expectedText ?? "";
143952
144450
  return textMatch === "matches" ? `text ${selector} matches /${expected}/` : textMatch === "equals" ? `text ${selector} == ${JSON.stringify(expected)}` : `text ${selector} contains ${JSON.stringify(expected)}`;
143953
144451
  }
144452
+ function targetLabel(target) {
144453
+ return "selector" in target ? selectorLabel(target.selector) : `(${target.x}, ${target.y})`;
144454
+ }
143954
144455
  var zodSchema62 = external_exports.object({
143955
144456
  name: external_exports.string().describe("Name of the flow being recorded \u2014 the one passed to flow-start-recording."),
143956
144457
  project_root: external_exports.string().describe(
@@ -144078,11 +144579,23 @@ function summarizeStep(step, n) {
144078
144579
  return `${n}. run: ${step.flow}`;
144079
144580
  case "tap":
144080
144581
  case "long-press": {
144081
- const target = step.selector ? selectorLabel(step.selector) : `(${step.x}, ${step.y})`;
144582
+ const target = targetLabel(
144583
+ step.selector ? { selector: step.selector } : { x: step.x, y: step.y }
144584
+ );
144082
144585
  const times = step.kind === "tap" && step.times !== void 0 && step.times > 1 ? ` \xD7${step.times}` : "";
144083
144586
  const held = step.kind === "long-press" && step.duration !== void 0 ? ` for ${step.duration}ms` : "";
144084
144587
  return `${n}. ${step.kind}: ${target}${times}${held}`;
144085
144588
  }
144589
+ case "swipe": {
144590
+ const travel = step.direction ?? (step.by ? `by ${swipeByLabel(step.by)}` : `to ${targetLabel(step.to)}`);
144591
+ const from2 = step.from ? ` from ${targetLabel(step.from)}` : "";
144592
+ const options = [
144593
+ ...step.momentum === false ? ["momentum-free"] : [],
144594
+ ...step.duration !== void 0 ? [`${step.duration}ms`] : []
144595
+ ];
144596
+ const tail = options.length > 0 ? ` (${options.join(", ")})` : "";
144597
+ return `${n}. swipe: ${travel}${from2}${tail}`;
144598
+ }
144086
144599
  case "type":
144087
144600
  return `${n}. type: ${selectorLabel(step.into)} \u2190 "${step.text}"`;
144088
144601
  case "await":
@@ -144124,7 +144637,7 @@ var zodSchema63 = external_exports.object({
144124
144637
  "Absolute path to the project root of the flow being recorded \u2014 the same value passed to flow-start-recording. Together with `name` it identifies which recording this step belongs to."
144125
144638
  ),
144126
144639
  command: external_exports.string().describe(
144127
- 'MCP tool name (e.g. "gesture-tap", "screenshot", "launch-app") \u2014 a TOOL, not a flow directive. A flow-file directive name ("tap", "launch", "run", "type", "await", "assert", "pinch", "echo", "wait", "long-press", "scroll-to", "snapshot", "when") is answered with guidance, and nothing runs or is recorded: most name the tool that records the directive, while "wait", "long-press", "scroll-to", "snapshot" and "when" have no recording tool at all and are answered with what to do instead. A recording tool (flow-add-step, flow-add-echo, flow-start-recording, flow-finish-recording) is refused the same way, each for its own reason \u2014 nesting one would erase this flow at replay, end the take, or write the step twice.'
144640
+ 'MCP tool name (e.g. "gesture-tap", "screenshot", "launch-app") \u2014 a TOOL, not a flow directive. A flow-file directive name ("tap", "launch", "run", "type", "await", "assert", "pinch", "swipe", "echo", "wait", "long-press", "scroll-to", "snapshot", "when") is answered with guidance, and nothing runs or is recorded: most name the tool that records the directive, while "wait", "long-press", "scroll-to", "snapshot" and "when" have no recording tool at all and are answered with what to do instead. A recording tool (flow-add-step, flow-add-echo, flow-start-recording, flow-finish-recording) is refused the same way, each for its own reason \u2014 nesting one would erase this flow at replay, end the take, or write the step twice.'
144128
144641
  ),
144129
144642
  args: external_exports.string().optional().describe(
144130
144643
  `Tool arguments as a JSON string, e.g. '{"udid": "ABC", "x": 0.5, "y": 0.3}'. Omit for tools with no arguments.`
@@ -144378,6 +144891,9 @@ function isToolNotFound(err, command) {
144378
144891
  return err instanceof ToolNotFoundError && err.toolId === command;
144379
144892
  }
144380
144893
  function directiveCommandHint(command) {
144894
+ if (command === "swipe") {
144895
+ return `"swipe" is a flow directive, not a tool. Record the movement by calling \`gesture-swipe\` (\`gesture-drag\` on chromium, where gesture-swipe is not supported) through flow-add-step. It is stored as the raw \`tool:\` step for whichever one you called; converting it to \`swipe:\` is part of the polish pass.`;
144896
+ }
144381
144897
  if (command === "echo") {
144382
144898
  return `"echo" is a flow directive, not a tool. Call \`flow-add-echo\` DIRECTLY \u2014 not through flow-add-step, which would run it as a nested tool AND record a \`tool: flow-add-echo\` step that fails on every replay.`;
144383
144899
  }
@@ -147402,15 +147918,71 @@ function displayFlowName(params) {
147402
147918
  const stem = params.flow_path === void 0 ? void 0 : path36.basename(params.flow_path, ".yaml");
147403
147919
  return params.name || stem || params.flow_path || "(unspecified)";
147404
147920
  }
147405
- function* walkSteps(steps) {
147406
- for (const step of steps) {
147407
- yield step;
147921
+ function* walkSteps(steps, within = "") {
147922
+ for (const [i, step] of steps.entries()) {
147923
+ const where = `step ${i + 1}${within}`;
147924
+ yield { step, where };
147408
147925
  const inner = blockSteps(step);
147409
- if (inner) yield* walkSteps(inner);
147926
+ if (inner) yield* walkSteps(inner, ` of the ${step.kind}: block at ${where}`);
147927
+ }
147928
+ }
147929
+ function retiredKeyGuidance(prop) {
147930
+ const schema = prop;
147931
+ if (!schema?.not || Object.keys(schema.not).length > 0) return void 0;
147932
+ return (schema.description ?? "").replace(/^Retired:\s*/, "");
147933
+ }
147934
+ function toolArgProps(registry2, tool) {
147935
+ return registry2.getTool(tool)?.inputSchema?.properties;
147936
+ }
147937
+ function retiredArgIn(props, tool, args, where) {
147938
+ for (const key2 of Object.keys(args)) {
147939
+ const guidance = retiredKeyGuidance(props[key2]);
147940
+ if (guidance !== void 0) return { where, tool, key: key2, guidance };
147941
+ }
147942
+ return void 0;
147943
+ }
147944
+ function* nestedInvocations(props, args) {
147945
+ for (const [key2, value] of Object.entries(args)) {
147946
+ if (!Object.hasOwn(props, key2)) continue;
147947
+ const entries = Array.isArray(value) ? value : [value];
147948
+ for (const [i, entry] of entries.entries()) {
147949
+ const call = entry;
147950
+ if (typeof call?.tool !== "string") continue;
147951
+ if (typeof call.args !== "object" || call.args === null || Array.isArray(call.args)) continue;
147952
+ yield {
147953
+ tool: call.tool,
147954
+ args: call.args,
147955
+ at: Array.isArray(value) ? `step ${i + 1}` : `\`${key2}\``
147956
+ };
147957
+ }
147958
+ }
147959
+ }
147960
+ function findRetiredToolArg(registry2, steps) {
147961
+ for (const { step, where } of walkSteps(steps)) {
147962
+ if (step.kind !== "tool") continue;
147963
+ const props = toolArgProps(registry2, step.name);
147964
+ if (!props) continue;
147965
+ const direct = retiredArgIn(props, step.name, step.args, where);
147966
+ if (direct) return direct;
147967
+ for (const call of nestedInvocations(props, step.args)) {
147968
+ const nestedProps = toolArgProps(registry2, call.tool);
147969
+ if (!nestedProps) continue;
147970
+ const hit = retiredArgIn(
147971
+ nestedProps,
147972
+ call.tool,
147973
+ call.args,
147974
+ `${call.at} of the ${step.name} step at ${where}`
147975
+ );
147976
+ if (hit) return hit;
147977
+ }
147410
147978
  }
147979
+ return void 0;
147980
+ }
147981
+ function retiredArgReason(use) {
147982
+ return `${use.where} as written (echo included) passes ${use.tool}'s retired \`${use.key}\` key${use.guidance ? `: ${use.guidance}` : ""}`;
147411
147983
  }
147412
147984
  function assertUploadSelfContained(flow) {
147413
- for (const step of walkSteps(flow.steps)) {
147985
+ for (const { step } of walkSteps(flow.steps)) {
147414
147986
  if (step.kind === "run") {
147415
147987
  throw new FailureError(
147416
147988
  `This flow uses run: composition ("run: ${step.flow}"), which requires a co-located client and tool server \u2014 an uploaded flow's referenced files are not available on this host.`,
@@ -147444,6 +148016,9 @@ function createRunFlowTool(registry2) {
147444
148016
  failedMsg: ({ params, failureSignal: failureSignal2 }) => `Failed to run flow ${displayFlowName(params)}: ${failureSignal2.error_code}`
147445
148017
  },
147446
148018
  description: `Run a saved flow from the .argent/flows/ directory, or an explicit boundary-managed flow_path.
148019
+ Use when a scenario is already authored as YAML and the whole of it should replay in one call with a
148020
+ per-step verdict; reach for the individual gesture tools when nothing is authored yet, and for
148021
+ run-sequence when the steps are an ad-hoc list rather than a stored flow.
147447
148022
  Steps run in order: \`launch\` starts an app from scratch (terminate + relaunch) and waits until it is
147448
148023
  ready (on iOS it also pins later element lookups to that app rather than auto-detecting the frontmost
147449
148024
  one); \`tool\` calls dispatch through the registry (a raw \`tool\` step ends that iOS pin, so lookups
@@ -147458,6 +148033,11 @@ order), \`next: <selector>\` (CSS \`+\` \u2014 the nearest such follower, which
147458
148033
  non-matching neighbour rather than failing), plus \`any: true\` (CSS \`*\` \u2014 legal only WITH a scope and
147459
148034
  never beside text/id/role). Scopes nest to disambiguate \u2014 \`within: { id: card, within: { id: list } }\`
147460
148035
  reads "inside card inside list", each container's frame inside the next);
148036
+ \`swipe\` performs one finger flick (\`swipe: left\`, or \`swipe: { from?, direction|to|by, momentum?, duration? }\` \u2014
148037
+ direction is the FINGER's travel, the opposite sense of scroll-to's content direction; \`by: { x?, y? }\` \u2014 signed
148038
+ 0\u20131 screen fractions, combined length at least 0.03 (a diagonal clears it where neither axis does); duration in ms,
148039
+ default 300, minimum 150, maximum 10000; each bound is a parse error that rejects the file before any step runs;
148040
+ \`momentum: false\` lands exactly where the finger lifts instead of flinging);
147461
148041
  \`scroll-to\` scrolls (momentum-free) until a target is visible; \`pinch\` zooms
147462
148042
  (\`pinch: { on?, scale }\` \u2014 scale > 1 in, < 1 out; screen center when \`on\` is omitted); \`rotate\` is the
147463
148043
  two-finger rotation gesture (\`rotate: { on?, by }\` \u2014 degrees, + clockwise, within \xB13000\xB0; screen center
@@ -147474,7 +148054,7 @@ baseline (a missing baseline fails the step \u2014 set updateBaselines to adopt
147474
148054
  cropped element whose size drifted fails on dimensions); \`echo\` annotates; \`run\` executes another flow
147475
148055
  inline \u2014 a YAML path resolved against the directory of the flow file that references it (co-located
147476
148056
  runs only).
147477
- A selector-less gesture \u2014 a coordinate \`tap\`/\`long-press\`, or a \`pinch\`/\`rotate\` with no \`on\` \u2014 resolves
148057
+ A selector-less gesture \u2014 a coordinate \`tap\`/\`long-press\`/\`swipe\`, or a \`pinch\`/\`rotate\` with no \`on\` \u2014 resolves
147478
148058
  no frame out of the tree, so an unreadable tree source does NOT stop it the way it stops \`idle\`: it
147479
148059
  settles best-effort, dispatches anyway, and the step PASSES carrying a \`warning\` that quotes the source's
147480
148060
  own error. That green says the gesture was SENT, not that it landed. Restore the tree source (usually
@@ -147522,6 +148102,15 @@ Pass exactly one flow source: name for a saved flow under project_root, or flow_
147522
148102
  const flowsDir = path36.dirname(canonicalPath);
147523
148103
  const flow = parseFlow(await fs51.readFile(canonicalPath, "utf8"));
147524
148104
  if (viaUpload) assertUploadSelfContained(flow);
148105
+ const retiredArg = findRetiredToolArg(registry2, flow.steps);
148106
+ if (retiredArg) {
148107
+ throw new FailureError(`Flow "${flowName}" ${retiredArgReason(retiredArg)}`, {
148108
+ error_code: FAILURE_CODES.FLOW_FILE_INVALID,
148109
+ failure_stage: "flow_run_validate",
148110
+ failure_area: "tool_server",
148111
+ error_kind: "validation"
148112
+ });
148113
+ }
147525
148114
  const rootEntry = { canonical: canonicalPath, display: flowName };
147526
148115
  if (flow.executionPrerequisite && !pinnedToChromium(params.device)) {
147527
148116
  const leading = await leadingLaunch(flow, [rootEntry]);
@@ -147792,6 +148381,9 @@ function conditionLabel(cond, renderSelector) {
147792
148381
  }
147793
148382
  return `${cond.condition} ${sel}`;
147794
148383
  }
148384
+ function gestureTargetLabel(target) {
148385
+ return "selector" in target ? selectorLabel2(target.selector) : `(${target.x}, ${target.y})`;
148386
+ }
147795
148387
  function stepTarget(step) {
147796
148388
  switch (step.kind) {
147797
148389
  case "tap":
@@ -147799,6 +148391,19 @@ function stepTarget(step) {
147799
148391
  if (step.selector) return selectorLabel2(step.selector);
147800
148392
  if (step.x !== void 0 && step.y !== void 0) return `(${step.x}, ${step.y})`;
147801
148393
  return void 0;
148394
+ case "swipe": {
148395
+ let travel;
148396
+ if (step.direction !== void 0) {
148397
+ travel = step.direction;
148398
+ } else if (step.by !== void 0) {
148399
+ travel = `by ${swipeByLabel(step.by)}`;
148400
+ } else if (step.to !== void 0) {
148401
+ travel = `to ${gestureTargetLabel(step.to)}`;
148402
+ } else {
148403
+ return void 0;
148404
+ }
148405
+ return `${travel}${step.from ? ` from ${gestureTargetLabel(step.from)}` : ""}`;
148406
+ }
147802
148407
  case "type":
147803
148408
  return `into ${selectorLabel2(step.into)}`;
147804
148409
  case "await":
@@ -148061,6 +148666,8 @@ async function execRunStep(state3, step, scope) {
148061
148666
  } catch (err) {
148062
148667
  return fail(`could not load fragment "${target}": ${errMsg3(err)}`);
148063
148668
  }
148669
+ const retiredArg = findRetiredToolArg(state3.registry, fragment.steps);
148670
+ if (retiredArg) return fail(`fragment "${target}" ${retiredArgReason(retiredArg)}`);
148064
148671
  pushReport(state3, {
148065
148672
  index,
148066
148673
  kind: "run",
@@ -148094,6 +148701,7 @@ async function execLeafStep(state3, step, index, scope) {
148094
148701
  }
148095
148702
  case "tap":
148096
148703
  case "long-press":
148704
+ case "swipe":
148097
148705
  case "type":
148098
148706
  case "await":
148099
148707
  case "assert":
@@ -148221,6 +148829,9 @@ async function execLeafStep(state3, step, index, scope) {
148221
148829
  }
148222
148830
  return { ...base, status: "pass", tool: step.name, result, outputHint, args };
148223
148831
  } catch (err) {
148832
+ if (signal?.aborted) {
148833
+ return { ...base, status: "skip", tool: step.name, reason: ABORTED_OUTCOME.reason };
148834
+ }
148224
148835
  const reframed = describeNestedParamError(registry2, err, step.name, args, step.args ?? {});
148225
148836
  return { ...base, status: "error", tool: step.name, reason: reframed ?? errMsg3(err) };
148226
148837
  }
@@ -148400,10 +149011,13 @@ var flowReadPrerequisiteTool = {
148400
149011
  failedMsg: ({ failureSignal: failureSignal2 }) => `Failed to read flow prerequisite: ${failureSignal2.error_code}`
148401
149012
  },
148402
149013
  description: `Read the execution prerequisite of a flow without running it \u2014 a saved flow from the .argent/flows/ directory, or an explicit boundary-managed flow_path.
148403
- Returns the prerequisite description so you can verify the required state is met before calling flow-execute.
148404
- Use when you need to check what app/simulator state is required before executing a flow; pass the same flow
148405
- source (name or flow_path) you will pass to flow-execute, so the prerequisite you read is the contract of
148406
- the flow that will actually run.
149014
+ Returns { flow, executionPrerequisite }: the logical name, plus the precondition its author recorded
149015
+ verbatim. Empty when none was declared, which is always so for a self-contained scenario: one opening
149016
+ on a launch may declare no prerequisite, because it builds its own start state.
149017
+ Use when deciding whether the device already sits where a fragment expects it (correct app foregrounded,
149018
+ correct account, correct screen) before committing to a run, or when relaying that requirement to a human.
149019
+ Touches no device: nothing is launched, tapped, dispatched or torn down, and no simulator or emulator
149020
+ needs booting, so calling this costs nothing but a file read.
148407
149021
  Fails if the flow file does not exist.
148408
149022
  Address the flow exactly as you will address it in flow-execute: name or flow_path, one and only one; supplying both or neither is rejected. The name goes in \`name\`, which resolves <project_root>/.argent/flows/<name>.yaml.`,
148409
149023
  zodSchema: zodSchema66,
@@ -148911,7 +149525,7 @@ var updateArgentTool = {
148911
149525
  });
148912
149526
  child.unref();
148913
149527
  }, 2e3);
148914
- const targetLabel = effectiveTarget === "both" ? "global and project-local installs" : `${effectiveTarget} install`;
149528
+ const targetLabel2 = effectiveTarget === "both" ? "global and project-local installs" : `${effectiveTarget} install`;
148915
149529
  const otherHint = requested === "auto" && resolved !== "both" ? resolved === "local" ? ` If you also have a global install, call this tool again with target "global" to update it too.` : ` If you also have a project-local install, run \`argent update --local\` in that project to update it too.` : "";
148916
149530
  const bothDegradedNote = resolved === "both" && effectiveTarget === "global" ? ` The project-local install was skipped: no project declaring ${PACKAGE_NAME2} could be located from this server \u2014 run \`argent update --local\` in the project directory for it.` : "";
148917
149531
  const versionInfo = targetsOnlyRunningInstall ? `(v${currentVersion} -> v${installableVersion}) ` : "";
@@ -148919,7 +149533,7 @@ var updateArgentTool = {
148919
149533
  const coversRunningInstall = targetsOnlyRunningInstall || effectiveTarget === "both";
148920
149534
  const restartNote = coversRunningInstall ? ` The tool server will stop and restart automatically once the update is installed. Subsequent tool calls will reconnect to the updated server.` : ` This session's tool server is not affected and keeps running; the update applies only to the targeted install.`;
148921
149535
  return {
148922
- message: `Argent update initiated ${versionInfo}for the ${targetLabel}.` + crossTargetNote + restartNote + `${otherHint}${bothDegradedNote}`
149536
+ message: `Argent update initiated ${versionInfo}for the ${targetLabel2}.` + crossTargetNote + restartNote + `${otherHint}${bothDegradedNote}`
148923
149537
  };
148924
149538
  }
148925
149539
  };