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

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.
@@ -76339,15 +76339,28 @@ async function emulatorSupportsFlag(flag, options = {}) {
76339
76339
  const cached3 = emulatorFlagSupportCache.get(cacheKey3);
76340
76340
  if (cached3 !== void 0) return cached3;
76341
76341
  let output;
76342
+ let failure = null;
76342
76343
  try {
76343
76344
  const { stdout, stderr } = await execFileAsync9(emulatorPath, ["-help"], {
76344
76345
  timeout: options.timeoutMs ?? 1e4,
76346
+ // Same hazard runAdb guards against: a SIGTERM-immune child (emulator
76347
+ // wedged in Qt/graphics init) leaves execFile unsettled forever, and
76348
+ // this await has no deadline of its own.
76349
+ killSignal: "SIGKILL",
76345
76350
  maxBuffer: 8 * 1024 * 1024
76346
76351
  });
76347
76352
  output = stdout + stderr;
76348
76353
  } catch (err) {
76349
76354
  const e = err;
76350
- output = (e.stdout ?? "") + (e.stderr ?? "");
76355
+ output = e.killed ? "" : (e.stdout ?? "") + (e.stderr ?? "");
76356
+ failure = err instanceof Error ? err.message : String(err);
76357
+ }
76358
+ if (output === "") {
76359
+ process.stderr.write(
76360
+ `[argent] \`${emulatorPath} -help\` produced no complete listing ` + (failure ? `(${failure})` : "but exited successfully") + `; assuming "${flag}" unsupported for this boot and retrying on the next one
76361
+ `
76362
+ );
76363
+ return false;
76351
76364
  }
76352
76365
  const supported = output.includes(flag);
76353
76366
  emulatorFlagSupportCache.set(cacheKey3, supported);
@@ -76557,7 +76570,10 @@ async function listAvds() {
76557
76570
  const emulatorPath = await resolveAndroidBinary("emulator");
76558
76571
  if (!emulatorPath) return [];
76559
76572
  try {
76560
- const { stdout } = await execFileAsync9(emulatorPath, ["-list-avds"], { timeout: 5e3 });
76573
+ const { stdout } = await execFileAsync9(emulatorPath, ["-list-avds"], {
76574
+ timeout: 5e3,
76575
+ killSignal: "SIGKILL"
76576
+ });
76561
76577
  return stdout.split("\n").map((l) => l.trim()).filter((l) => l && AVD_NAME_PATTERN.test(l)).map((name) => ({ name }));
76562
76578
  } catch {
76563
76579
  return [];
@@ -76575,6 +76591,7 @@ async function checkSnapshotLoadable(avdName, snapshotName = "default_boot", opt
76575
76591
  ];
76576
76592
  const { stdout } = await execFileAsync9(emulatorPath, args, {
76577
76593
  timeout: options.timeoutMs ?? 1e4,
76594
+ killSignal: "SIGKILL",
76578
76595
  maxBuffer: 4 * 1024 * 1024
76579
76596
  });
76580
76597
  const tail = stdout.split("\n").slice(-6).join("\n");
@@ -93869,7 +93886,7 @@ var _CI_VENDOR_COUNT_FOR_TEST = vendors_default.length;
93869
93886
  var SESSION_ID = (0, import_node_crypto3.randomUUID)();
93870
93887
  function readCliVersion() {
93871
93888
  if (true) {
93872
- return "0.22.2-next.0";
93889
+ return "0.22.2-next.2";
93873
93890
  }
93874
93891
  return "0.0.0";
93875
93892
  }
@@ -116837,13 +116854,7 @@ var zodSchema9 = external_exports.object({
116837
116854
  function bootTarget(params) {
116838
116855
  return params.udid ?? params.avdName ?? params.vvdImage ?? params.electronAppPath ?? "device";
116839
116856
  }
116840
- var LAUNCH_HARDENING_ARGS = [
116841
- "-no-boot-anim",
116842
- "-netfast",
116843
- "-crash-report-mode",
116844
- "never",
116845
- "-no-metrics"
116846
- ];
116857
+ var LAUNCH_HARDENING_ARGS = ["-no-boot-anim", "-netfast", "-no-metrics"];
116847
116858
  function launchHardeningArgs(sound) {
116848
116859
  return sound ? [...LAUNCH_HARDENING_ARGS] : ["-noaudio", ...LAUNCH_HARDENING_ARGS];
116849
116860
  }
@@ -117357,7 +117368,7 @@ async function bootAndroidImpl(params) {
117357
117368
  } else {
117358
117369
  const RENDERER_ARGS = ["-gpu", gpuMode, ...extraEmulatorArgs];
117359
117370
  const probe3 = await checkSnapshotLoadable(params.avdName, "default_boot", {
117360
- extraArgs: [...RENDERER_ARGS, ...hardeningArgs]
117371
+ extraArgs: [...RENDERER_ARGS, ...hardeningArgs, ...crashReportArgs]
117361
117372
  });
117362
117373
  if (!probe3.loadable) {
117363
117374
  hotBootFailureReason = `-check-snapshot-loadable: ${probe3.reason ?? "unknown"}`;
@@ -119417,18 +119428,39 @@ Before tapping, determine the correct coordinates by using discovery tools \u201
119417
119428
  // ../tool-server/src/tools/gesture-swipe/index.ts
119418
119429
  init_zod();
119419
119430
  var sleep3 = (ms) => new Promise((r) => setTimeout(r, ms));
119420
- var SETTLE_EASE_EXPONENT = 3;
119431
+ var MOMENTUM_FREE_EASE_EXPONENT = 3;
119432
+ var DEFAULT_DURATION_MS = 300;
119433
+ var MOMENTUM_FREE_MIN_DURATION_MS = 150;
119434
+ var MAX_DURATION_MS = 1e4;
119421
119435
  var zodSchema17 = external_exports.object({
119422
119436
  udid: external_exports.string().describe("Target device id from `list-devices` (iOS UDID or Android serial)."),
119423
119437
  fromX: external_exports.number().describe("Start x: normalized 0.0\u20131.0 (not pixels; same as tap)"),
119424
119438
  fromY: external_exports.number().describe("Start y: normalized 0.0\u20131.0 (not pixels; same as tap)"),
119425
119439
  toX: external_exports.number().describe("End x: normalized 0.0\u20131.0 (not pixels; same as tap)"),
119426
119440
  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)."
119441
+ durationMs: external_exports.number().max(MAX_DURATION_MS, {
119442
+ 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.`
119443
+ }).optional().describe(
119444
+ `Total gesture duration in milliseconds (default 300, at most ${MAX_DURATION_MS} - the gesture holds a finger down for exactly this long)`
119445
+ ),
119446
+ momentum: external_exports.boolean().optional().describe(
119447
+ `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.`
119448
+ ),
119449
+ // `momentum`'s shipped spelling, with the opposite polarity. Declared so this
119450
+ // non-strict object refuses it instead of stripping it and flinging - the exact
119451
+ // inverse of the gesture the caller asked for.
119452
+ settle: external_exports.never({
119453
+ 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)"
119454
+ }).optional().describe(
119455
+ "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
119456
  )
119431
- });
119457
+ }).refine(
119458
+ (p) => p.momentum !== false || (p.durationMs ?? DEFAULT_DURATION_MS) >= MOMENTUM_FREE_MIN_DURATION_MS,
119459
+ {
119460
+ 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.`,
119461
+ path: ["durationMs"]
119462
+ }
119463
+ );
119432
119464
  var capability9 = {
119433
119465
  apple: { simulator: true, device: true },
119434
119466
  appleRemote: { simulator: true },
@@ -119441,11 +119473,13 @@ var gestureSwipeTool = {
119441
119473
  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
119474
  failedMsg: ({ failureSignal: failureSignal2 }) => `Failed to swipe: ${failureSignal2.error_code}`
119443
119475
  },
119476
+ // The bounds are spelled out rather than interpolated: extract-tools scans this
119477
+ // description statically, so a `${}` in it drops the tool out of the scan.
119444
119478
  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
119479
  Generates interpolated Move events for a natural feel (~60fps).
119446
119480
  Swipe up (fromY > toY) to scroll content down.
119447
119481
  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.`,
119482
+ 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
119483
  alwaysLoad: true,
119450
119484
  searchHint: "swipe scroll drag pan gesture device simulator emulator touch move",
119451
119485
  zodSchema: zodSchema17,
@@ -119453,18 +119487,47 @@ Pass settle:true for a momentum-free swipe that lands exactly where the finger l
119453
119487
  services: (params) => ({
119454
119488
  simulatorServer: simulatorServerRef(resolveDevice(params.udid))
119455
119489
  }),
119456
- async execute(services, params) {
119457
- const duration3 = params.durationMs ?? 300;
119458
- const settle = params.settle ?? false;
119490
+ async execute(services, params, ctx) {
119491
+ const duration3 = params.durationMs ?? DEFAULT_DURATION_MS;
119492
+ const momentumFree = params.momentum === false;
119459
119493
  const timestampMs = Date.now();
119460
119494
  const api = services.simulatorServer;
119461
119495
  const steps = Math.max(1, Math.round(duration3 / 16));
119496
+ let lastX = 0;
119497
+ let lastY = 0;
119462
119498
  for (let i = 0; i <= steps; i++) {
119499
+ if (ctx?.signal?.aborted) {
119500
+ if (i > 0) {
119501
+ sendCommand(api, {
119502
+ cmd: "touch",
119503
+ type: "Up",
119504
+ x: lastX,
119505
+ y: lastY,
119506
+ second_x: null,
119507
+ second_y: null
119508
+ });
119509
+ }
119510
+ const err = new Error(
119511
+ `gesture-swipe aborted - cancelled mid-gesture after ${i} of ${steps + 1} frames`
119512
+ );
119513
+ err.name = "AbortError";
119514
+ throw err;
119515
+ }
119463
119516
  const t = i / steps;
119464
- const progress = settle ? 1 - Math.pow(1 - t, SETTLE_EASE_EXPONENT) : t;
119517
+ const progress = momentumFree ? 1 - Math.pow(1 - t, MOMENTUM_FREE_EASE_EXPONENT) : t;
119465
119518
  const x = params.fromX + (params.toX - params.fromX) * progress;
119466
119519
  const y = params.fromY + (params.toY - params.fromY) * progress;
119467
119520
  const type = i === 0 ? "Down" : i === steps ? "Up" : "Move";
119521
+ if (type === "Up") {
119522
+ sendCommand(api, {
119523
+ cmd: "touch",
119524
+ type: "Move",
119525
+ x,
119526
+ y,
119527
+ second_x: null,
119528
+ second_y: null
119529
+ });
119530
+ }
119468
119531
  sendCommand(api, {
119469
119532
  cmd: "touch",
119470
119533
  type,
@@ -119473,6 +119536,8 @@ Pass settle:true for a momentum-free swipe that lands exactly where the finger l
119473
119536
  second_x: null,
119474
119537
  second_y: null
119475
119538
  });
119539
+ lastX = x;
119540
+ lastY = y;
119476
119541
  if (i < steps) await sleep3(16);
119477
119542
  }
119478
119543
  return { swiped: true, timestampMs };
@@ -119548,13 +119613,30 @@ Returns { scrolled: true, timestampMs }. Fails if the Chromium CDP session is no
119548
119613
  // ../tool-server/src/tools/gesture-drag/index.ts
119549
119614
  init_zod();
119550
119615
  var sleep5 = (ms) => new Promise((r) => setTimeout(r, ms));
119616
+ var MOMENTUM_FREE_EASE_EXPONENT2 = 3;
119617
+ var MOMENTUM_FREE_MIN_STEPS = 8;
119618
+ var MAX_DURATION_MS2 = 1e4;
119551
119619
  var zodSchema19 = external_exports.object({
119552
119620
  udid: external_exports.string().describe("Target Chromium device id from `list-devices` (chromium-cdp-<port>)."),
119553
119621
  fromX: external_exports.number().describe("Press x: normalized 0.0\u20131.0 (fraction of window width, not pixels)."),
119554
119622
  fromY: external_exports.number().describe("Press y: normalized 0.0\u20131.0 (fraction of window height, not pixels)."),
119555
119623
  toX: external_exports.number().describe("Release x: normalized 0.0\u20131.0 (not pixels; same space as tap)."),
119556
119624
  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.")
119625
+ durationMs: external_exports.number().max(MAX_DURATION_MS2, {
119626
+ 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.`
119627
+ }).optional().describe(
119628
+ `Total drag duration in milliseconds (default 300, at most ${MAX_DURATION_MS2} - the button stays down for exactly this long), interpolated at ~60fps.`
119629
+ ),
119630
+ momentum: external_exports.boolean().optional().describe(
119631
+ "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."
119632
+ ),
119633
+ // `momentum`'s earlier spelling, with the opposite polarity. Declared so this
119634
+ // non-strict object refuses it instead of stripping it and running the default.
119635
+ settle: external_exports.never({
119636
+ 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)"
119637
+ }).optional().describe(
119638
+ "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."
119639
+ )
119558
119640
  });
119559
119641
  var capability11 = {
119560
119642
  chromium: { app: true }
@@ -119566,9 +119648,9 @@ var gestureDragTool = {
119566
119648
  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
119649
  failedMsg: ({ failureSignal: failureSignal2 }) => `Failed to drag: ${failureSignal2.error_code}`
119568
119650
  },
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.
119651
+ 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
119652
  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.`,
119653
+ 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
119654
  alwaysLoad: true,
119573
119655
  searchHint: "drag drop slider mouse press move release chromium select",
119574
119656
  zodSchema: zodSchema19,
@@ -119576,15 +119658,49 @@ Returns { dragged: true, timestampMs }. Fails if the Chromium CDP session is not
119576
119658
  services: (params) => ({
119577
119659
  chromium: chromiumCdpRef(resolveDevice(params.udid))
119578
119660
  }),
119579
- async execute(services, params) {
119661
+ async execute(services, params, ctx) {
119580
119662
  const timestampMs = Date.now();
119581
119663
  const chromium = services.chromium;
119582
119664
  await assertChromiumWindowVisible(chromium, "drag", "chromium_drag_window_hidden");
119583
119665
  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 };
119666
+ const clampPx2 = (px, size) => Math.min(Math.max(px, 0), size - 1);
119667
+ const startPx = {
119668
+ x: clampPx2(params.fromX * vp.width, vp.width),
119669
+ y: clampPx2(params.fromY * vp.height, vp.height)
119670
+ };
119671
+ const endPx = {
119672
+ x: clampPx2(params.toX * vp.width, vp.width),
119673
+ y: clampPx2(params.toY * vp.height, vp.height)
119674
+ };
119586
119675
  const durationMs = params.durationMs ?? 300;
119587
- const steps = Math.max(2, Math.round(durationMs / 16));
119676
+ const momentumFree = params.momentum === false;
119677
+ const steps = Math.max(momentumFree ? MOMENTUM_FREE_MIN_STEPS : 2, Math.round(durationMs / 16));
119678
+ const frameMs = durationMs / steps;
119679
+ const t0 = Date.now();
119680
+ let lastX = startPx.x;
119681
+ let lastY = startPx.y;
119682
+ const abortError = (frame) => {
119683
+ const err = new Error(
119684
+ `gesture-drag aborted - cancelled mid-drag after ${frame} of ${steps + 1} frames`
119685
+ );
119686
+ err.name = "AbortError";
119687
+ return err;
119688
+ };
119689
+ const releaseAndAbort = async (frame) => {
119690
+ const err = abortError(frame);
119691
+ try {
119692
+ await chromium.dispatchMouseEvent({
119693
+ type: "mouseReleased",
119694
+ x: lastX,
119695
+ y: lastY,
119696
+ clickCount: 1
119697
+ });
119698
+ } catch (releaseErr) {
119699
+ err.cause = releaseErr;
119700
+ }
119701
+ throw err;
119702
+ };
119703
+ if (ctx?.signal?.aborted) throw abortError(0);
119588
119704
  await chromium.dispatchMouseEvent({
119589
119705
  type: "mousePressed",
119590
119706
  x: startPx.x,
@@ -119592,15 +119708,19 @@ Returns { dragged: true, timestampMs }. Fails if the Chromium CDP session is not
119592
119708
  clickCount: 1
119593
119709
  });
119594
119710
  for (let i = 1; i < steps; i++) {
119711
+ if (ctx?.signal?.aborted) await releaseAndAbort(i);
119712
+ await sleep5(Math.max(0, t0 + i * frameMs - Date.now()));
119595
119713
  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
- }
119714
+ const progress = momentumFree ? 1 - Math.pow(1 - t, MOMENTUM_FREE_EASE_EXPONENT2) : t;
119715
+ const x = startPx.x + (endPx.x - startPx.x) * progress;
119716
+ const y = startPx.y + (endPx.y - startPx.y) * progress;
119717
+ await chromium.dispatchMouseEvent({ type: "mouseMoved", x, y, button: "left" });
119718
+ lastX = x;
119719
+ lastY = y;
119720
+ }
119721
+ if (ctx?.signal?.aborted) await releaseAndAbort(steps);
119722
+ await sleep5(Math.max(0, t0 + durationMs - Date.now()));
119723
+ if (ctx?.signal?.aborted) await releaseAndAbort(steps);
119604
119724
  await chromium.dispatchMouseEvent({
119605
119725
  type: "mouseReleased",
119606
119726
  x: endPx.x,
@@ -127820,9 +127940,9 @@ a prior tap), use individual tool calls instead.
127820
127940
  Allowed tools and their args (udid is auto-injected, do NOT include it in args):
127821
127941
 
127822
127942
  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]
127943
+ gesture-swipe: { fromX: number, fromY: number, toX: number, toY: number, durationMs?: number, momentum?: boolean } [ios/android]
127824
127944
  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]
127945
+ gesture-drag: { fromX: number, fromY: number, toX: number, toY: number, durationMs?: number, momentum?: boolean } [chromium only]
127826
127946
  gesture-custom: { events: [{ type: "Down"|"Move"|"Up", x: number, y: number, x2?: number, y2?: number, delayMs?: number }], interpolate?: number } [ios/android]
127827
127947
  gesture-pinch: { centerX: number, centerY: number, startDistance: number, endDistance: number, endCenterX?: number, endCenterY?: number, angle?: number, durationMs?: number } [ios/android]
127828
127948
  gesture-rotate: { centerX: number, centerY: number, radius?: number, radiusX?: number, radiusY?: number, startAngle: number, endAngle: number, durationMs?: number } [ios/android]
@@ -140883,6 +141003,12 @@ function chromiumLaunchSpec(launch) {
140883
141003
  return typeof c === "string" ? { path: c } : { path: c.path, args: c.args };
140884
141004
  }
140885
141005
  function selectorToYaml(sel) {
141006
+ const unknown2 = Object.keys(sel).filter((key2) => !WRITABLE_SELECTOR_KEYS.includes(key2));
141007
+ if (unknown2.length > 0) {
141008
+ throw new Error(
141009
+ `Cannot serialize flow selector: ${describeUnknownKeys(unknown2, WRITABLE_SELECTOR_KEYS)} - allowed keys: ${WRITABLE_SELECTOR_KEYS.join(", ")}.`
141010
+ );
141011
+ }
140886
141012
  if (sel.text !== void 0 && sel.textMatches !== void 0) {
140887
141013
  throw new Error(
140888
141014
  '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 +141131,79 @@ function targetToYaml(step) {
141005
141131
  }
141006
141132
  return { x: step.x, y: step.y };
141007
141133
  }
141134
+ function swipeTargetToYaml(target, label) {
141135
+ const yaml = targetToYaml(target);
141136
+ if (typeof yaml !== "string" && "x" in yaml && !Object.keys(target).every((key2) => key2 === "x" || key2 === "y")) {
141137
+ throw new Error(`Cannot serialize flow ${label}: a coordinate target takes only { x, y }`);
141138
+ }
141139
+ return yaml;
141140
+ }
141141
+ var SWIPE_MIN_TRAVEL = 0.03;
141142
+ var SWIPE_MIN_DURATION_MS = 150;
141143
+ var SWIPE_MAX_DURATION_MS = 1e4;
141144
+ var LONG_PRESS_MAX_DURATION_MS = SWIPE_MAX_DURATION_MS;
141145
+ function swipeByToYaml(by) {
141146
+ const keys = Object.keys(by);
141147
+ if (keys.some((key2) => key2 !== "x" && key2 !== "y")) {
141148
+ throw new Error("Cannot serialize flow swipe.by: accepts only x and y");
141149
+ }
141150
+ const axes = ["x", "y"].filter((axis) => by[axis] !== void 0);
141151
+ if (axes.length === 0) {
141152
+ throw new Error("Cannot serialize flow swipe.by: needs at least one of x or y");
141153
+ }
141154
+ const result = {};
141155
+ for (const axis of axes) {
141156
+ const value = by[axis];
141157
+ if (!Number.isFinite(value) || value === 0 || value < -1 || value > 1) {
141158
+ throw new Error(
141159
+ `Cannot serialize flow swipe.by.${axis}: must be a non-zero fraction of the screen between -1 and 1`
141160
+ );
141161
+ }
141162
+ result[axis] = value;
141163
+ }
141164
+ const magnitude = Math.hypot(result.x ?? 0, result.y ?? 0);
141165
+ if (magnitude < SWIPE_MIN_TRAVEL) {
141166
+ throw new Error(
141167
+ `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`
141168
+ );
141169
+ }
141170
+ return result;
141171
+ }
141172
+ function swipeByLabel(by) {
141173
+ return ["x", "y"].filter((axis) => by[axis] !== void 0).map((axis) => `${axis}=${by[axis]}`).join(", ");
141174
+ }
141175
+ function isPositiveMs(raw) {
141176
+ return typeof raw === "number" && Number.isFinite(raw) && raw > 0;
141177
+ }
141178
+ function positiveMsToYaml(value, label) {
141179
+ if (!isPositiveMs(value)) {
141180
+ throw new Error(`Cannot serialize flow ${label}: needs a positive number of milliseconds`);
141181
+ }
141182
+ return value;
141183
+ }
141184
+ function swipeDurationToYaml(value) {
141185
+ const duration3 = positiveMsToYaml(value, "swipe.duration");
141186
+ if (duration3 < SWIPE_MIN_DURATION_MS) {
141187
+ throw new Error(
141188
+ `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`
141189
+ );
141190
+ }
141191
+ if (duration3 > SWIPE_MAX_DURATION_MS) {
141192
+ throw new Error(
141193
+ `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`
141194
+ );
141195
+ }
141196
+ return duration3;
141197
+ }
141198
+ function longPressDurationToYaml(value) {
141199
+ const duration3 = positiveMsToYaml(value, "long-press.duration");
141200
+ if (duration3 > LONG_PRESS_MAX_DURATION_MS) {
141201
+ throw new Error(
141202
+ `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`
141203
+ );
141204
+ }
141205
+ return duration3;
141206
+ }
141008
141207
  function waitToYaml(condition, selector, expectedText, textMatch, timeoutMs) {
141009
141208
  const sel = selectorToYaml(selector);
141010
141209
  let body;
@@ -141022,7 +141221,7 @@ function waitToYaml(condition, selector, expectedText, textMatch, timeoutMs) {
141022
141221
  body = textWaitToYaml(sel, expectedText, textMatch);
141023
141222
  break;
141024
141223
  }
141025
- if (timeoutMs !== void 0) body.timeout = timeoutMs;
141224
+ if (timeoutMs !== void 0) body.timeout = positiveMsToYaml(timeoutMs, "await.timeout");
141026
141225
  return body;
141027
141226
  }
141028
141227
  function idleToYaml(step) {
@@ -141058,9 +141257,29 @@ function toYamlStep(step) {
141058
141257
  case "long-press": {
141059
141258
  const target = targetToYaml(step);
141060
141259
  return {
141061
- "long-press": step.duration !== void 0 ? { on: target, duration: step.duration } : target
141260
+ "long-press": step.duration !== void 0 ? { on: target, duration: longPressDurationToYaml(step.duration) } : target
141062
141261
  };
141063
141262
  }
141263
+ case "swipe": {
141264
+ const travels = ["direction", "to", "by"].filter((key2) => step[key2] !== void 0);
141265
+ if (travels.length !== 1) {
141266
+ throw new Error("Cannot serialize flow swipe: needs exactly one of direction, to, or by");
141267
+ }
141268
+ if (step.momentum !== void 0 && typeof step.momentum !== "boolean") {
141269
+ throw new Error("Cannot serialize flow swipe.momentum: must be true or false");
141270
+ }
141271
+ if (step.direction !== void 0 && step.from === void 0 && step.to === void 0 && step.by === void 0 && step.momentum !== false && step.duration === void 0) {
141272
+ return { swipe: step.direction };
141273
+ }
141274
+ const body = {};
141275
+ if (step.from !== void 0) body.from = swipeTargetToYaml(step.from, "swipe.from");
141276
+ if (step.direction !== void 0) body.direction = step.direction;
141277
+ if (step.to !== void 0) body.to = swipeTargetToYaml(step.to, "swipe.to");
141278
+ if (step.by !== void 0) body.by = swipeByToYaml(step.by);
141279
+ if (step.momentum === false) body.momentum = false;
141280
+ if (step.duration !== void 0) body.duration = swipeDurationToYaml(step.duration);
141281
+ return { swipe: body };
141282
+ }
141064
141283
  case "type": {
141065
141284
  const body = {
141066
141285
  into: selectorToYaml(step.into),
@@ -141153,6 +141372,12 @@ function badEntry(raw, detail) {
141153
141372
  error_kind: "validation"
141154
141373
  });
141155
141374
  }
141375
+ function parsePositiveMs(raw, entry, label, example) {
141376
+ if (!isPositiveMs(raw)) {
141377
+ badEntry(entry, `${label} needs a positive number of milliseconds (e.g. \`${example}\`)`);
141378
+ }
141379
+ return raw;
141380
+ }
141156
141381
  function validatePattern(raw, pattern, where) {
141157
141382
  try {
141158
141383
  new RegExp(pattern);
@@ -141216,6 +141441,17 @@ var SELECTOR_KEYS = [
141216
141441
  "any",
141217
141442
  ...SELECTOR_RELATIONS
141218
141443
  ];
141444
+ var WRITABLE_SELECTOR_KEYS = Object.keys({
141445
+ text: true,
141446
+ textMatches: true,
141447
+ identifier: true,
141448
+ role: true,
141449
+ any: true,
141450
+ loose: true,
141451
+ within: true,
141452
+ after: true,
141453
+ next: true
141454
+ });
141219
141455
  var MAX_SELECTOR_SCOPES = 6;
141220
141456
  function parseSelector(raw, where, budget = { scopes: MAX_SELECTOR_SCOPES }) {
141221
141457
  if (budget.scopes < 0) {
@@ -141346,7 +141582,7 @@ function parseWaitFields(raw, kind) {
141346
141582
  "assert has no timeout \u2014 it is an immediate check; use `await` for a timed wait"
141347
141583
  );
141348
141584
  }
141349
- timeout = parseAwaitTimeout({ [kind]: b }, b.timeout);
141585
+ timeout = parsePositiveMs(b.timeout, { [kind]: b }, "await.timeout", "timeout: 10000");
141350
141586
  }
141351
141587
  rejectUnknownKeys(
141352
141588
  { [kind]: b },
@@ -141515,6 +141751,7 @@ var STEP_DIRECTIVE_KEYS = [
141515
141751
  "tool",
141516
141752
  "tap",
141517
141753
  "long-press",
141754
+ "swipe",
141518
141755
  "type",
141519
141756
  "await",
141520
141757
  "assert",
@@ -141613,13 +141850,19 @@ function parseLongPress(body, entry) {
141613
141850
  }
141614
141851
  const step = { kind: "long-press", ...parseTarget(obj.on, "long-press.on") };
141615
141852
  if (obj.duration !== void 0) {
141616
- if (typeof obj.duration !== "number" || !Number.isFinite(obj.duration) || obj.duration <= 0) {
141853
+ const duration3 = parsePositiveMs(
141854
+ obj.duration,
141855
+ entry,
141856
+ "long-press.duration",
141857
+ "duration: 1200"
141858
+ );
141859
+ if (duration3 > LONG_PRESS_MAX_DURATION_MS) {
141617
141860
  badEntry(
141618
141861
  entry,
141619
- "long-press.duration needs a positive number of milliseconds (e.g. `duration: 1200`)"
141862
+ `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
141863
  );
141621
141864
  }
141622
- step.duration = obj.duration;
141865
+ step.duration = duration3;
141623
141866
  }
141624
141867
  return step;
141625
141868
  }
@@ -141789,6 +142032,116 @@ function completeRunExtension(value) {
141789
142032
  const candidate = `${value}.yaml`;
141790
142033
  return FLOW_FILE_NAME_PATTERN.test(path32.posix.basename(candidate)) ? candidate : value;
141791
142034
  }
142035
+ var SWIPE_DIRECTIONS = ["up", "down", "left", "right"];
142036
+ var SWIPE_OPTION_KEYS = ["from", "direction", "to", "by", "momentum", "duration"];
142037
+ function parseSwipeBy(raw, entry) {
142038
+ if (raw === null || typeof raw !== "object") {
142039
+ badEntry(entry, "swipe.by needs { x } and/or { y } \u2014 signed 0\u20131 fractions of the screen");
142040
+ }
142041
+ const obj = raw;
142042
+ rejectUnknownKeys(entry, obj, ["x", "y"], "swipe.by");
142043
+ if (obj.x === void 0 && obj.y === void 0) {
142044
+ badEntry(entry, "swipe.by needs at least one of x, y");
142045
+ }
142046
+ const by = {};
142047
+ for (const axis of ["x", "y"]) {
142048
+ const v = obj[axis];
142049
+ if (v === void 0) continue;
142050
+ if (typeof v !== "number" || !Number.isFinite(v) || v === 0 || v < -1 || v > 1) {
142051
+ badEntry(
142052
+ entry,
142053
+ `swipe.by.${axis} must be a non-zero fraction of the screen between -1 and 1 (omit the axis instead of 0)`
142054
+ );
142055
+ }
142056
+ by[axis] = v;
142057
+ }
142058
+ const magnitude = Math.hypot(by.x ?? 0, by.y ?? 0);
142059
+ if (magnitude < SWIPE_MIN_TRAVEL) {
142060
+ badEntry(
142061
+ entry,
142062
+ `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`
142063
+ );
142064
+ }
142065
+ return by;
142066
+ }
142067
+ function parseSwipe(body, entry) {
142068
+ if (typeof body === "string") {
142069
+ if (!SWIPE_DIRECTIONS.includes(body)) {
142070
+ badEntry(
142071
+ entry,
142072
+ `swipe takes a direction (${SWIPE_DIRECTIONS.join(", ")}) \u2014 to anchor on an element use swipe: { from: <target>, direction: \u2026 }`
142073
+ );
142074
+ }
142075
+ return { kind: "swipe", direction: body };
142076
+ }
142077
+ if (body === null || typeof body !== "object") {
142078
+ badEntry(entry, `swipe needs a direction (${SWIPE_DIRECTIONS.join(", ")}) or an options map`);
142079
+ }
142080
+ const obj = body;
142081
+ if (hasSelectorField(obj)) {
142082
+ badEntry(
142083
+ entry,
142084
+ 'the swipe options form takes a nested target \u2014 e.g. swipe: { from: { text: "Card" }, direction: left }'
142085
+ );
142086
+ }
142087
+ if (obj.x !== void 0 || obj.y !== void 0) {
142088
+ badEntry(
142089
+ entry,
142090
+ "the swipe options form takes a nested point \u2014 e.g. swipe: { from: { x: 0.5, y: 0.5 }, direction: left }"
142091
+ );
142092
+ }
142093
+ if (obj.settle !== void 0) {
142094
+ badEntry(
142095
+ entry,
142096
+ "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)"
142097
+ );
142098
+ }
142099
+ rejectUnknownKeys(entry, obj, SWIPE_OPTION_KEYS, "swipe");
142100
+ const travels = ["direction", "to", "by"].filter((k) => obj[k] !== void 0);
142101
+ if (travels.length !== 1) {
142102
+ badEntry(entry, "swipe needs exactly one of `direction`, `to`, or `by`");
142103
+ }
142104
+ const step = { kind: "swipe" };
142105
+ if (obj.from !== void 0) step.from = parseTarget(obj.from, "swipe.from");
142106
+ switch (travels[0]) {
142107
+ case "direction": {
142108
+ if (typeof obj.direction !== "string" || !SWIPE_DIRECTIONS.includes(obj.direction)) {
142109
+ badEntry(entry, `swipe.direction must be one of ${SWIPE_DIRECTIONS.join(", ")}`);
142110
+ }
142111
+ step.direction = obj.direction;
142112
+ break;
142113
+ }
142114
+ case "to":
142115
+ step.to = parseTarget(obj.to, "swipe.to");
142116
+ break;
142117
+ case "by":
142118
+ step.by = parseSwipeBy(obj.by, entry);
142119
+ break;
142120
+ }
142121
+ if (obj.momentum !== void 0) {
142122
+ if (typeof obj.momentum !== "boolean") {
142123
+ badEntry(entry, "swipe.momentum must be true or false");
142124
+ }
142125
+ if (!obj.momentum) step.momentum = false;
142126
+ }
142127
+ if (obj.duration !== void 0) {
142128
+ const duration3 = parsePositiveMs(obj.duration, entry, "swipe.duration", "duration: 800");
142129
+ if (duration3 < SWIPE_MIN_DURATION_MS) {
142130
+ badEntry(
142131
+ entry,
142132
+ `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`
142133
+ );
142134
+ }
142135
+ if (duration3 > SWIPE_MAX_DURATION_MS) {
142136
+ badEntry(
142137
+ entry,
142138
+ `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`
142139
+ );
142140
+ }
142141
+ step.duration = duration3;
142142
+ }
142143
+ return step;
142144
+ }
141792
142145
  function fromYamlStep(raw, blockDepth = 0) {
141793
142146
  const entry = raw;
141794
142147
  if ("optional" in raw) {
@@ -141830,6 +142183,7 @@ function fromYamlStep(raw, blockDepth = 0) {
141830
142183
  if ("long-press" in raw) {
141831
142184
  return parseLongPress(raw["long-press"], raw);
141832
142185
  }
142186
+ if ("swipe" in raw) return parseSwipe(raw.swipe, raw);
141833
142187
  if ("type" in raw) {
141834
142188
  const body = raw.type;
141835
142189
  if (!body || typeof body !== "object") badEntry(raw, "type needs { into, text }");
@@ -142446,6 +142800,7 @@ function stepRequiresDevice(registry2, step) {
142446
142800
  case "launch":
142447
142801
  case "tap":
142448
142802
  case "long-press":
142803
+ case "swipe":
142449
142804
  case "type":
142450
142805
  case "await":
142451
142806
  case "assert":
@@ -143287,22 +143642,32 @@ async function settleTree(env, opts = {}) {
143287
143642
  if (!await sleepOrAbort(SETTLE_POLL_MS, env.signal)) return void 0;
143288
143643
  }
143289
143644
  }
143290
- async function waitForFrame(env, selector) {
143645
+ async function waitForFrames(env, selectors) {
143646
+ const pending = selectors.flatMap((selector, i) => selector ? [{ i, selector }] : []);
143647
+ if (pending.length === 0) return selectors.map(() => void 0);
143291
143648
  const deadline = Date.now() + DEFAULT_ACTION_TIMEOUT_MS;
143649
+ let unresolved = pending[0].selector;
143292
143650
  for (; ; ) {
143293
143651
  if (env.signal?.aborted) return "aborted";
143294
143652
  const tree = await settleTree(env);
143295
143653
  if (tree) {
143296
- const frame = flowSelectorToFrame(tree, selector);
143297
- if (frame) return frame;
143654
+ const frames = selectors.map((s) => s ? flowSelectorToFrame(tree, s) : void 0);
143655
+ const missing = pending.find(({ i }) => frames[i] === void 0);
143656
+ if (!missing) return frames;
143657
+ unresolved = missing.selector;
143298
143658
  } else if (env.signal?.aborted) {
143299
143659
  return "aborted";
143300
143660
  }
143301
- if (Date.now() >= deadline) return void 0;
143661
+ if (Date.now() >= deadline) return { unresolved };
143302
143662
  const sleepMs = Math.min(POLL_INTERVAL_MS, Math.max(0, deadline - Date.now()));
143303
143663
  if (!await sleepOrAbort(sleepMs, env.signal)) return "aborted";
143304
143664
  }
143305
143665
  }
143666
+ async function waitForFrame(env, selector) {
143667
+ const frames = await waitForFrames(env, [selector]);
143668
+ if (frames === "aborted") return "aborted";
143669
+ return Array.isArray(frames) ? frames[0] : void 0;
143670
+ }
143306
143671
  function framesOverlap(a, b) {
143307
143672
  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
143673
  }
@@ -143366,14 +143731,19 @@ async function scrollIncrement(env, direction, region) {
143366
143731
  to = { x: clamp014(cx + dist), y: cy };
143367
143732
  break;
143368
143733
  }
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
- });
143734
+ try {
143735
+ await invokeOnDevice(env, "gesture-swipe", {
143736
+ fromX: cx,
143737
+ fromY: cy,
143738
+ toX: to.x,
143739
+ toY: to.y,
143740
+ momentum: false,
143741
+ durationMs: 600
143742
+ });
143743
+ } catch (err) {
143744
+ if (env.signal?.aborted) return;
143745
+ throw err;
143746
+ }
143377
143747
  }
143378
143748
  async function scrollToVisible(env, target, direction, within) {
143379
143749
  let prevFp;
@@ -143407,7 +143777,7 @@ function offscreenHint(sel) {
143407
143777
  return `no visible element matched selector ${describeSelector(sel)} \u2014 if it is off-screen, add a scroll-to step before this one`;
143408
143778
  }
143409
143779
  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")) {
143780
+ 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
143781
  return {
143412
143782
  ok: false,
143413
143783
  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 +143794,8 @@ async function runDirective(env, step) {
143424
143794
  return runTap(env, step);
143425
143795
  case "long-press":
143426
143796
  return runLongPress(env, step);
143797
+ case "swipe":
143798
+ return runSwipe(env, step);
143427
143799
  case "type":
143428
143800
  return runType(env, step);
143429
143801
  case "await":
@@ -143471,10 +143843,16 @@ async function resolveTargetPoint(env, target) {
143471
143843
  }
143472
143844
  return { point: getDescribeTapPoint(frame) };
143473
143845
  }
143846
+ const point = targetPointFromFrame(target, void 0);
143847
+ if ("fail" in point) return point;
143848
+ const settle = await settleForGesture(env);
143849
+ if (settle.aborted) return { fail: ABORTED_OUTCOME };
143850
+ return { point, ...warned(settle) };
143851
+ }
143852
+ function targetPointFromFrame(target, frame) {
143853
+ if (frame) return getDescribeTapPoint(frame);
143474
143854
  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) };
143855
+ return { x: target.x, y: target.y };
143478
143856
  }
143479
143857
  return { fail: { ok: false, reason: "gesture needs a selector or x/y coordinates" } };
143480
143858
  }
@@ -143494,13 +143872,18 @@ async function runLongPress(env, step) {
143494
143872
  const point = resolved.point;
143495
143873
  const duration3 = step.duration ?? DEFAULT_LONG_PRESS_MS;
143496
143874
  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
- });
143875
+ try {
143876
+ await invokeOnDevice(env, "gesture-drag", {
143877
+ fromX: point.x,
143878
+ fromY: point.y,
143879
+ toX: point.x,
143880
+ toY: point.y,
143881
+ durationMs: duration3
143882
+ });
143883
+ } catch (err) {
143884
+ if (env.signal?.aborted) return ABORTED_OUTCOME;
143885
+ throw err;
143886
+ }
143504
143887
  } else {
143505
143888
  await invokeOnDevice(env, "gesture-custom", {
143506
143889
  events: [
@@ -143620,6 +144003,132 @@ async function runRotate(env, step) {
143620
144003
  }
143621
144004
  return { ok: true, ...warned(settle) };
143622
144005
  }
144006
+ var SWIPE_GEOMETRY = {
144007
+ left: { start: { x: 0.9, y: 0.5 }, axis: "x", end: 0.1 },
144008
+ right: { start: { x: 0.1, y: 0.5 }, axis: "x", end: 0.9 },
144009
+ down: { start: { x: 0.5, y: 0.2 }, axis: "y", end: 0.9 },
144010
+ up: { start: { x: 0.5, y: 0.5 }, axis: "y", end: 0.1 }
144011
+ };
144012
+ async function runSwipe(env, step) {
144013
+ const ends = [step.from, step.to];
144014
+ const selectors = ends.map((end2) => end2 && "selector" in end2 ? end2.selector : void 0);
144015
+ const frames = await waitForFrames(env, selectors);
144016
+ if (frames === "aborted") return ABORTED_OUTCOME;
144017
+ if (!Array.isArray(frames)) return { ok: false, reason: offscreenHint(frames.unresolved) };
144018
+ const [fromFrame, toFrame] = frames;
144019
+ let settle = {};
144020
+ if (selectors.every((selector) => selector === void 0)) {
144021
+ settle = await settleForGesture(env);
144022
+ if (settle.aborted) return ABORTED_OUTCOME;
144023
+ }
144024
+ let toPoint;
144025
+ if (step.to) {
144026
+ const p = targetPointFromFrame(step.to, toFrame);
144027
+ if ("fail" in p) return p.fail;
144028
+ toPoint = p;
144029
+ }
144030
+ let start2;
144031
+ if (step.from) {
144032
+ const p = targetPointFromFrame(step.from, fromFrame);
144033
+ if ("fail" in p) return p.fail;
144034
+ start2 = p;
144035
+ } else if (step.direction) {
144036
+ start2 = { ...SWIPE_GEOMETRY[step.direction].start };
144037
+ } else {
144038
+ start2 = { x: 0.5, y: 0.5 };
144039
+ }
144040
+ if (!Number.isFinite(start2.x) || start2.x < 0 || start2.x > 1 || !Number.isFinite(start2.y) || start2.y < 0 || start2.y > 1) {
144041
+ return {
144042
+ ok: false,
144043
+ reason: `swipe.from resolved outside the normalized screen: (${start2.x}, ${start2.y}); both coordinates must be between 0 and 1`
144044
+ };
144045
+ }
144046
+ let end;
144047
+ if (step.direction) {
144048
+ const g = SWIPE_GEOMETRY[step.direction];
144049
+ const startOnTravelAxis = start2[g.axis];
144050
+ const endOnTravelAxis = step.from ? clamp014(startOnTravelAxis + (g.end - g.start[g.axis])) : g.end;
144051
+ end = g.axis === "x" ? { x: endOnTravelAxis, y: start2.y } : { x: start2.x, y: endOnTravelAxis };
144052
+ const travel2 = Math.abs(endOnTravelAxis - startOnTravelAxis);
144053
+ if (travel2 < SWIPE_MIN_TRAVEL) {
144054
+ return {
144055
+ ok: false,
144056
+ 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`
144057
+ };
144058
+ }
144059
+ } else if (step.by) {
144060
+ const overflowAxis = ["x", "y"].find((axis) => {
144061
+ const d = step.by[axis];
144062
+ return d !== void 0 && (start2[axis] + d < 0 || start2[axis] + d > 1);
144063
+ });
144064
+ if (overflowAxis === void 0) {
144065
+ end = {
144066
+ x: step.by.x !== void 0 ? start2.x + step.by.x : start2.x,
144067
+ y: step.by.y !== void 0 ? start2.y + step.by.y : start2.y
144068
+ };
144069
+ } else if (step.from) {
144070
+ const requested = step.by[overflowAxis];
144071
+ const raw = start2[overflowAxis] + requested;
144072
+ return {
144073
+ ok: false,
144074
+ 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]`
144075
+ };
144076
+ } else {
144077
+ end = { x: start2.x, y: start2.y };
144078
+ for (const axis of ["x", "y"]) {
144079
+ const d = step.by[axis];
144080
+ if (d === void 0) {
144081
+ end[axis] = start2[axis];
144082
+ continue;
144083
+ }
144084
+ const s = start2[axis];
144085
+ const lo = Math.min(s, s + d);
144086
+ const hi = Math.max(s, s + d);
144087
+ const shift = lo < 0 ? -lo : hi > 1 ? 1 - hi : 0;
144088
+ start2[axis] = s + shift;
144089
+ end[axis] = s + d + shift;
144090
+ }
144091
+ }
144092
+ } else {
144093
+ end = toPoint;
144094
+ if (!Number.isFinite(end.x) || end.x < 0 || end.x > 1 || !Number.isFinite(end.y) || end.y < 0 || end.y > 1) {
144095
+ return {
144096
+ ok: false,
144097
+ reason: `swipe.to resolved outside the normalized screen: (${end.x}, ${end.y}); both coordinates must be between 0 and 1`
144098
+ };
144099
+ }
144100
+ if (Math.hypot(end.x - start2.x, end.y - start2.y) < SWIPE_MIN_TRAVEL) {
144101
+ return {
144102
+ ok: false,
144103
+ 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`
144104
+ };
144105
+ }
144106
+ }
144107
+ const travel = {
144108
+ fromX: start2.x,
144109
+ fromY: start2.y,
144110
+ toX: end.x,
144111
+ toY: end.y,
144112
+ ...step.duration !== void 0 ? { durationMs: step.duration } : {},
144113
+ ...step.momentum === false ? { momentum: false } : {}
144114
+ };
144115
+ try {
144116
+ await invokeOnDevice(
144117
+ env,
144118
+ env.device.platform === "chromium" ? "gesture-drag" : "gesture-swipe",
144119
+ travel
144120
+ );
144121
+ } catch (err) {
144122
+ if (env.signal?.aborted) return ABORTED_OUTCOME;
144123
+ throw err;
144124
+ }
144125
+ try {
144126
+ await settleTree(env);
144127
+ } catch {
144128
+ }
144129
+ if (env.signal?.aborted) return ABORTED_OUTCOME;
144130
+ return { ok: true, ...warned(settle) };
144131
+ }
143623
144132
  async function runType(env, step) {
143624
144133
  const frame = await waitForFrame(env, step.into);
143625
144134
  if (frame === "aborted") return ABORTED_OUTCOME;
@@ -143951,6 +144460,9 @@ function textConditionLabel(sel, expectedText, textMatch) {
143951
144460
  const expected = expectedText ?? "";
143952
144461
  return textMatch === "matches" ? `text ${selector} matches /${expected}/` : textMatch === "equals" ? `text ${selector} == ${JSON.stringify(expected)}` : `text ${selector} contains ${JSON.stringify(expected)}`;
143953
144462
  }
144463
+ function targetLabel(target) {
144464
+ return "selector" in target ? selectorLabel(target.selector) : `(${target.x}, ${target.y})`;
144465
+ }
143954
144466
  var zodSchema62 = external_exports.object({
143955
144467
  name: external_exports.string().describe("Name of the flow being recorded \u2014 the one passed to flow-start-recording."),
143956
144468
  project_root: external_exports.string().describe(
@@ -144078,11 +144590,23 @@ function summarizeStep(step, n) {
144078
144590
  return `${n}. run: ${step.flow}`;
144079
144591
  case "tap":
144080
144592
  case "long-press": {
144081
- const target = step.selector ? selectorLabel(step.selector) : `(${step.x}, ${step.y})`;
144593
+ const target = targetLabel(
144594
+ step.selector ? { selector: step.selector } : { x: step.x, y: step.y }
144595
+ );
144082
144596
  const times = step.kind === "tap" && step.times !== void 0 && step.times > 1 ? ` \xD7${step.times}` : "";
144083
144597
  const held = step.kind === "long-press" && step.duration !== void 0 ? ` for ${step.duration}ms` : "";
144084
144598
  return `${n}. ${step.kind}: ${target}${times}${held}`;
144085
144599
  }
144600
+ case "swipe": {
144601
+ const travel = step.direction ?? (step.by ? `by ${swipeByLabel(step.by)}` : `to ${targetLabel(step.to)}`);
144602
+ const from2 = step.from ? ` from ${targetLabel(step.from)}` : "";
144603
+ const options = [
144604
+ ...step.momentum === false ? ["momentum-free"] : [],
144605
+ ...step.duration !== void 0 ? [`${step.duration}ms`] : []
144606
+ ];
144607
+ const tail = options.length > 0 ? ` (${options.join(", ")})` : "";
144608
+ return `${n}. swipe: ${travel}${from2}${tail}`;
144609
+ }
144086
144610
  case "type":
144087
144611
  return `${n}. type: ${selectorLabel(step.into)} \u2190 "${step.text}"`;
144088
144612
  case "await":
@@ -144124,7 +144648,7 @@ var zodSchema63 = external_exports.object({
144124
144648
  "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
144649
  ),
144126
144650
  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.'
144651
+ '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
144652
  ),
144129
144653
  args: external_exports.string().optional().describe(
144130
144654
  `Tool arguments as a JSON string, e.g. '{"udid": "ABC", "x": 0.5, "y": 0.3}'. Omit for tools with no arguments.`
@@ -144378,6 +144902,9 @@ function isToolNotFound(err, command) {
144378
144902
  return err instanceof ToolNotFoundError && err.toolId === command;
144379
144903
  }
144380
144904
  function directiveCommandHint(command) {
144905
+ if (command === "swipe") {
144906
+ 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.`;
144907
+ }
144381
144908
  if (command === "echo") {
144382
144909
  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
144910
  }
@@ -147402,15 +147929,71 @@ function displayFlowName(params) {
147402
147929
  const stem = params.flow_path === void 0 ? void 0 : path36.basename(params.flow_path, ".yaml");
147403
147930
  return params.name || stem || params.flow_path || "(unspecified)";
147404
147931
  }
147405
- function* walkSteps(steps) {
147406
- for (const step of steps) {
147407
- yield step;
147932
+ function* walkSteps(steps, within = "") {
147933
+ for (const [i, step] of steps.entries()) {
147934
+ const where = `step ${i + 1}${within}`;
147935
+ yield { step, where };
147408
147936
  const inner = blockSteps(step);
147409
- if (inner) yield* walkSteps(inner);
147937
+ if (inner) yield* walkSteps(inner, ` of the ${step.kind}: block at ${where}`);
147410
147938
  }
147411
147939
  }
147940
+ function retiredKeyGuidance(prop) {
147941
+ const schema = prop;
147942
+ if (!schema?.not || Object.keys(schema.not).length > 0) return void 0;
147943
+ return (schema.description ?? "").replace(/^Retired:\s*/, "");
147944
+ }
147945
+ function toolArgProps(registry2, tool) {
147946
+ return registry2.getTool(tool)?.inputSchema?.properties;
147947
+ }
147948
+ function retiredArgIn(props, tool, args, where) {
147949
+ for (const key2 of Object.keys(args)) {
147950
+ const guidance = retiredKeyGuidance(props[key2]);
147951
+ if (guidance !== void 0) return { where, tool, key: key2, guidance };
147952
+ }
147953
+ return void 0;
147954
+ }
147955
+ function* nestedInvocations(props, args) {
147956
+ for (const [key2, value] of Object.entries(args)) {
147957
+ if (!Object.hasOwn(props, key2)) continue;
147958
+ const entries = Array.isArray(value) ? value : [value];
147959
+ for (const [i, entry] of entries.entries()) {
147960
+ const call = entry;
147961
+ if (typeof call?.tool !== "string") continue;
147962
+ if (typeof call.args !== "object" || call.args === null || Array.isArray(call.args)) continue;
147963
+ yield {
147964
+ tool: call.tool,
147965
+ args: call.args,
147966
+ at: Array.isArray(value) ? `step ${i + 1}` : `\`${key2}\``
147967
+ };
147968
+ }
147969
+ }
147970
+ }
147971
+ function findRetiredToolArg(registry2, steps) {
147972
+ for (const { step, where } of walkSteps(steps)) {
147973
+ if (step.kind !== "tool") continue;
147974
+ const props = toolArgProps(registry2, step.name);
147975
+ if (!props) continue;
147976
+ const direct = retiredArgIn(props, step.name, step.args, where);
147977
+ if (direct) return direct;
147978
+ for (const call of nestedInvocations(props, step.args)) {
147979
+ const nestedProps = toolArgProps(registry2, call.tool);
147980
+ if (!nestedProps) continue;
147981
+ const hit = retiredArgIn(
147982
+ nestedProps,
147983
+ call.tool,
147984
+ call.args,
147985
+ `${call.at} of the ${step.name} step at ${where}`
147986
+ );
147987
+ if (hit) return hit;
147988
+ }
147989
+ }
147990
+ return void 0;
147991
+ }
147992
+ function retiredArgReason(use) {
147993
+ return `${use.where} as written (echo included) passes ${use.tool}'s retired \`${use.key}\` key${use.guidance ? `: ${use.guidance}` : ""}`;
147994
+ }
147412
147995
  function assertUploadSelfContained(flow) {
147413
- for (const step of walkSteps(flow.steps)) {
147996
+ for (const { step } of walkSteps(flow.steps)) {
147414
147997
  if (step.kind === "run") {
147415
147998
  throw new FailureError(
147416
147999
  `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 +148027,9 @@ function createRunFlowTool(registry2) {
147444
148027
  failedMsg: ({ params, failureSignal: failureSignal2 }) => `Failed to run flow ${displayFlowName(params)}: ${failureSignal2.error_code}`
147445
148028
  },
147446
148029
  description: `Run a saved flow from the .argent/flows/ directory, or an explicit boundary-managed flow_path.
148030
+ Use when a scenario is already authored as YAML and the whole of it should replay in one call with a
148031
+ per-step verdict; reach for the individual gesture tools when nothing is authored yet, and for
148032
+ run-sequence when the steps are an ad-hoc list rather than a stored flow.
147447
148033
  Steps run in order: \`launch\` starts an app from scratch (terminate + relaunch) and waits until it is
147448
148034
  ready (on iOS it also pins later element lookups to that app rather than auto-detecting the frontmost
147449
148035
  one); \`tool\` calls dispatch through the registry (a raw \`tool\` step ends that iOS pin, so lookups
@@ -147458,6 +148044,11 @@ order), \`next: <selector>\` (CSS \`+\` \u2014 the nearest such follower, which
147458
148044
  non-matching neighbour rather than failing), plus \`any: true\` (CSS \`*\` \u2014 legal only WITH a scope and
147459
148045
  never beside text/id/role). Scopes nest to disambiguate \u2014 \`within: { id: card, within: { id: list } }\`
147460
148046
  reads "inside card inside list", each container's frame inside the next);
148047
+ \`swipe\` performs one finger flick (\`swipe: left\`, or \`swipe: { from?, direction|to|by, momentum?, duration? }\` \u2014
148048
+ direction is the FINGER's travel, the opposite sense of scroll-to's content direction; \`by: { x?, y? }\` \u2014 signed
148049
+ 0\u20131 screen fractions, combined length at least 0.03 (a diagonal clears it where neither axis does); duration in ms,
148050
+ default 300, minimum 150, maximum 10000; each bound is a parse error that rejects the file before any step runs;
148051
+ \`momentum: false\` lands exactly where the finger lifts instead of flinging);
147461
148052
  \`scroll-to\` scrolls (momentum-free) until a target is visible; \`pinch\` zooms
147462
148053
  (\`pinch: { on?, scale }\` \u2014 scale > 1 in, < 1 out; screen center when \`on\` is omitted); \`rotate\` is the
147463
148054
  two-finger rotation gesture (\`rotate: { on?, by }\` \u2014 degrees, + clockwise, within \xB13000\xB0; screen center
@@ -147474,7 +148065,7 @@ baseline (a missing baseline fails the step \u2014 set updateBaselines to adopt
147474
148065
  cropped element whose size drifted fails on dimensions); \`echo\` annotates; \`run\` executes another flow
147475
148066
  inline \u2014 a YAML path resolved against the directory of the flow file that references it (co-located
147476
148067
  runs only).
147477
- A selector-less gesture \u2014 a coordinate \`tap\`/\`long-press\`, or a \`pinch\`/\`rotate\` with no \`on\` \u2014 resolves
148068
+ A selector-less gesture \u2014 a coordinate \`tap\`/\`long-press\`/\`swipe\`, or a \`pinch\`/\`rotate\` with no \`on\` \u2014 resolves
147478
148069
  no frame out of the tree, so an unreadable tree source does NOT stop it the way it stops \`idle\`: it
147479
148070
  settles best-effort, dispatches anyway, and the step PASSES carrying a \`warning\` that quotes the source's
147480
148071
  own error. That green says the gesture was SENT, not that it landed. Restore the tree source (usually
@@ -147522,6 +148113,15 @@ Pass exactly one flow source: name for a saved flow under project_root, or flow_
147522
148113
  const flowsDir = path36.dirname(canonicalPath);
147523
148114
  const flow = parseFlow(await fs51.readFile(canonicalPath, "utf8"));
147524
148115
  if (viaUpload) assertUploadSelfContained(flow);
148116
+ const retiredArg = findRetiredToolArg(registry2, flow.steps);
148117
+ if (retiredArg) {
148118
+ throw new FailureError(`Flow "${flowName}" ${retiredArgReason(retiredArg)}`, {
148119
+ error_code: FAILURE_CODES.FLOW_FILE_INVALID,
148120
+ failure_stage: "flow_run_validate",
148121
+ failure_area: "tool_server",
148122
+ error_kind: "validation"
148123
+ });
148124
+ }
147525
148125
  const rootEntry = { canonical: canonicalPath, display: flowName };
147526
148126
  if (flow.executionPrerequisite && !pinnedToChromium(params.device)) {
147527
148127
  const leading = await leadingLaunch(flow, [rootEntry]);
@@ -147792,6 +148392,9 @@ function conditionLabel(cond, renderSelector) {
147792
148392
  }
147793
148393
  return `${cond.condition} ${sel}`;
147794
148394
  }
148395
+ function gestureTargetLabel(target) {
148396
+ return "selector" in target ? selectorLabel2(target.selector) : `(${target.x}, ${target.y})`;
148397
+ }
147795
148398
  function stepTarget(step) {
147796
148399
  switch (step.kind) {
147797
148400
  case "tap":
@@ -147799,6 +148402,19 @@ function stepTarget(step) {
147799
148402
  if (step.selector) return selectorLabel2(step.selector);
147800
148403
  if (step.x !== void 0 && step.y !== void 0) return `(${step.x}, ${step.y})`;
147801
148404
  return void 0;
148405
+ case "swipe": {
148406
+ let travel;
148407
+ if (step.direction !== void 0) {
148408
+ travel = step.direction;
148409
+ } else if (step.by !== void 0) {
148410
+ travel = `by ${swipeByLabel(step.by)}`;
148411
+ } else if (step.to !== void 0) {
148412
+ travel = `to ${gestureTargetLabel(step.to)}`;
148413
+ } else {
148414
+ return void 0;
148415
+ }
148416
+ return `${travel}${step.from ? ` from ${gestureTargetLabel(step.from)}` : ""}`;
148417
+ }
147802
148418
  case "type":
147803
148419
  return `into ${selectorLabel2(step.into)}`;
147804
148420
  case "await":
@@ -148061,6 +148677,8 @@ async function execRunStep(state3, step, scope) {
148061
148677
  } catch (err) {
148062
148678
  return fail(`could not load fragment "${target}": ${errMsg3(err)}`);
148063
148679
  }
148680
+ const retiredArg = findRetiredToolArg(state3.registry, fragment.steps);
148681
+ if (retiredArg) return fail(`fragment "${target}" ${retiredArgReason(retiredArg)}`);
148064
148682
  pushReport(state3, {
148065
148683
  index,
148066
148684
  kind: "run",
@@ -148094,6 +148712,7 @@ async function execLeafStep(state3, step, index, scope) {
148094
148712
  }
148095
148713
  case "tap":
148096
148714
  case "long-press":
148715
+ case "swipe":
148097
148716
  case "type":
148098
148717
  case "await":
148099
148718
  case "assert":
@@ -148221,6 +148840,9 @@ async function execLeafStep(state3, step, index, scope) {
148221
148840
  }
148222
148841
  return { ...base, status: "pass", tool: step.name, result, outputHint, args };
148223
148842
  } catch (err) {
148843
+ if (signal?.aborted) {
148844
+ return { ...base, status: "skip", tool: step.name, reason: ABORTED_OUTCOME.reason };
148845
+ }
148224
148846
  const reframed = describeNestedParamError(registry2, err, step.name, args, step.args ?? {});
148225
148847
  return { ...base, status: "error", tool: step.name, reason: reframed ?? errMsg3(err) };
148226
148848
  }
@@ -148400,10 +149022,13 @@ var flowReadPrerequisiteTool = {
148400
149022
  failedMsg: ({ failureSignal: failureSignal2 }) => `Failed to read flow prerequisite: ${failureSignal2.error_code}`
148401
149023
  },
148402
149024
  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.
149025
+ Returns { flow, executionPrerequisite }: the logical name, plus the precondition its author recorded
149026
+ verbatim. Empty when none was declared, which is always so for a self-contained scenario: one opening
149027
+ on a launch may declare no prerequisite, because it builds its own start state.
149028
+ Use when deciding whether the device already sits where a fragment expects it (correct app foregrounded,
149029
+ correct account, correct screen) before committing to a run, or when relaying that requirement to a human.
149030
+ Touches no device: nothing is launched, tapped, dispatched or torn down, and no simulator or emulator
149031
+ needs booting, so calling this costs nothing but a file read.
148407
149032
  Fails if the flow file does not exist.
148408
149033
  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
149034
  zodSchema: zodSchema66,
@@ -148911,7 +149536,7 @@ var updateArgentTool = {
148911
149536
  });
148912
149537
  child.unref();
148913
149538
  }, 2e3);
148914
- const targetLabel = effectiveTarget === "both" ? "global and project-local installs" : `${effectiveTarget} install`;
149539
+ const targetLabel2 = effectiveTarget === "both" ? "global and project-local installs" : `${effectiveTarget} install`;
148915
149540
  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
149541
  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
149542
  const versionInfo = targetsOnlyRunningInstall ? `(v${currentVersion} -> v${installableVersion}) ` : "";
@@ -148919,7 +149544,7 @@ var updateArgentTool = {
148919
149544
  const coversRunningInstall = targetsOnlyRunningInstall || effectiveTarget === "both";
148920
149545
  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
149546
  return {
148922
- message: `Argent update initiated ${versionInfo}for the ${targetLabel}.` + crossTargetNote + restartNote + `${otherHint}${bothDegradedNote}`
149547
+ message: `Argent update initiated ${versionInfo}for the ${targetLabel2}.` + crossTargetNote + restartNote + `${otherHint}${bothDegradedNote}`
148923
149548
  };
148924
149549
  }
148925
149550
  };