@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.
- package/dist/cli-cmds.mjs +49 -21
- package/dist/installer.mjs +1 -1
- package/dist/tool-server.cjs +706 -81
- package/package.json +1 -1
- package/skills/argent-create-flow/references/flow-yaml.md +30 -3
- package/skills/argent-create-flow/references/live-authoring.md +14 -12
- package/skills/argent-create-flow/references/reliability-and-recovery.md +2 -2
- package/skills/argent-device-interact/SKILL.md +3 -1
- package/skills/argent-qa-flows/SKILL.md +1 -1
package/dist/tool-server.cjs
CHANGED
|
@@ -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"], {
|
|
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.
|
|
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
|
|
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().
|
|
119428
|
-
|
|
119429
|
-
|
|
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
|
|
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 ??
|
|
119458
|
-
const
|
|
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 =
|
|
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().
|
|
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
|
|
119585
|
-
const
|
|
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
|
|
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
|
-
|
|
119597
|
-
|
|
119598
|
-
|
|
119599
|
-
|
|
119600
|
-
|
|
119601
|
-
|
|
119602
|
-
|
|
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 }
|
|
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 }
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
|
143297
|
-
|
|
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
|
|
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
|
-
|
|
143370
|
-
|
|
143371
|
-
|
|
143372
|
-
|
|
143373
|
-
|
|
143374
|
-
|
|
143375
|
-
|
|
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
|
-
|
|
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
|
-
|
|
143498
|
-
|
|
143499
|
-
|
|
143500
|
-
|
|
143501
|
-
|
|
143502
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
148404
|
-
|
|
148405
|
-
|
|
148406
|
-
the
|
|
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
|
|
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 ${
|
|
149547
|
+
message: `Argent update initiated ${versionInfo}for the ${targetLabel2}.` + crossTargetNote + restartNote + `${otherHint}${bothDegradedNote}`
|
|
148923
149548
|
};
|
|
148924
149549
|
}
|
|
148925
149550
|
};
|