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