@swmansion/argent 0.17.0 → 0.17.1-next.0
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/bin/argent-android-devtools-0.1.0.apk +0 -0
- package/bin/darwin/ax-service +0 -0
- package/bin/darwin/tvos-ax-service +0 -0
- package/bin/darwin/tvos-hid-daemon +0 -0
- package/bin/tcp/ax-service +0 -0
- package/dist/tool-server.cjs +285 -87
- package/dylibs/libArgentInjectionBootstrap.dylib +0 -0
- package/dylibs/libKeyboardPatch.dylib +0 -0
- package/dylibs/libNativeDevtoolsIos.dylib +0 -0
- package/dylibs/tcp/libArgentInjectionBootstrap.dylib +0 -0
- package/dylibs/tcp/libKeyboardPatch.dylib +0 -0
- package/dylibs/tcp/libNativeDevtoolsIos.dylib +0 -0
- package/dylibs/tvos/libArgentInjectionBootstrap.dylib +0 -0
- package/dylibs/tvos/libKeyboardPatch.dylib +0 -0
- package/dylibs/tvos/libNativeDevtoolsIos.dylib +0 -0
- package/package.json +1 -1
- package/skills/argent-create-flow/SKILL.md +3 -1
- package/skills/argent-device-interact/SKILL.md +1 -1
|
Binary file
|
package/bin/darwin/ax-service
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/bin/tcp/ax-service
CHANGED
|
Binary file
|
package/dist/tool-server.cjs
CHANGED
|
@@ -126894,6 +126894,7 @@ Use when you need to zoom in or out on a map, image, or zoomable view. Returns {
|
|
|
126894
126894
|
|
|
126895
126895
|
// ../tool-server/src/tools/gesture-rotate/index.ts
|
|
126896
126896
|
init_zod();
|
|
126897
|
+
init_src();
|
|
126897
126898
|
var zodSchema22 = external_exports.object({
|
|
126898
126899
|
udid: external_exports.string().describe("Target device id from `list-devices` (iOS UDID or Android serial)."),
|
|
126899
126900
|
centerX: external_exports.number().describe(
|
|
@@ -126902,13 +126903,33 @@ var zodSchema22 = external_exports.object({
|
|
|
126902
126903
|
centerY: external_exports.number().describe(
|
|
126903
126904
|
"Center of rotation, vertical: normalized 0.0\u20131.0 (fraction of screen height, not pixels)"
|
|
126904
126905
|
),
|
|
126905
|
-
radius: external_exports.number().describe(
|
|
126906
|
-
"Distance from center to each finger: normalized 0.0\u20131.0 (fraction of screen, not pixels). E.g. 0.15 = fingers placed 15% of screen away from center."
|
|
126906
|
+
radius: external_exports.number().optional().describe(
|
|
126907
|
+
"Distance from center to each finger: normalized 0.0\u20131.0 (fraction of screen, not pixels). E.g. 0.15 = fingers placed 15% of screen away from center. One value for both axes, so on a non-square screen the orbit is a physical ellipse \u2014 pass radiusX+radiusY instead for a true circle. Required unless radiusX and radiusY are given."
|
|
126908
|
+
),
|
|
126909
|
+
radiusX: external_exports.number().optional().describe(
|
|
126910
|
+
"Per-axis finger distance, horizontal: normalized 0.0\u20131.0 fraction of screen WIDTH. Give both radiusX and radiusY (they override radius) with radiusX\xB7screenWidth = radiusY\xB7screenHeight for a physically circular orbit \u2014 constant finger separation, no pinch coupled into the turn."
|
|
126911
|
+
),
|
|
126912
|
+
radiusY: external_exports.number().optional().describe(
|
|
126913
|
+
"Per-axis finger distance, vertical: normalized 0.0\u20131.0 fraction of screen HEIGHT. Always paired with radiusX \u2014 see radiusX."
|
|
126907
126914
|
),
|
|
126908
126915
|
startAngle: external_exports.number().describe("Starting angle in degrees (0 = right, 90 = down)"),
|
|
126909
126916
|
endAngle: external_exports.number().describe("Ending angle in degrees. endAngle > startAngle = clockwise."),
|
|
126910
126917
|
durationMs: external_exports.number().optional().describe("Total gesture duration in milliseconds (default 300)")
|
|
126911
|
-
})
|
|
126918
|
+
}).refine((p) => p.radiusX === void 0 === (p.radiusY === void 0), {
|
|
126919
|
+
message: "radiusX and radiusY must be given together (one physical radius, two normalizations)"
|
|
126920
|
+
}).refine((p) => p.radius !== void 0 || p.radiusX !== void 0, {
|
|
126921
|
+
message: "Pass radius, or both radiusX and radiusY."
|
|
126922
|
+
});
|
|
126923
|
+
var inputSchema = {
|
|
126924
|
+
...zodObjectToJsonSchema(zodSchema22),
|
|
126925
|
+
anyOf: [
|
|
126926
|
+
{ required: ["radiusX", "radiusY"] },
|
|
126927
|
+
{
|
|
126928
|
+
required: ["radius"],
|
|
126929
|
+
not: { anyOf: [{ required: ["radiusX"] }, { required: ["radiusY"] }] }
|
|
126930
|
+
}
|
|
126931
|
+
]
|
|
126932
|
+
};
|
|
126912
126933
|
var capability14 = {
|
|
126913
126934
|
apple: { simulator: true, device: true },
|
|
126914
126935
|
appleRemote: { simulator: true },
|
|
@@ -126916,32 +126937,51 @@ var capability14 = {
|
|
|
126916
126937
|
};
|
|
126917
126938
|
var gestureRotateTool = {
|
|
126918
126939
|
id: "gesture-rotate",
|
|
126919
|
-
description: `Send a two-finger circular arc gesture to rotate on-screen content by a specified angle. Two fingers are placed opposite each other at a fixed radius from the center, then swept from startAngle to endAngle degrees. All positions and
|
|
126920
|
-
endAngle > startAngle = clockwise rotation. Typical values: radius 0.15, startAngle 0, endAngle 90 for a 90\xB0 clockwise turn.
|
|
126940
|
+
description: `Send a two-finger circular arc gesture to rotate on-screen content by a specified angle. Two fingers are placed opposite each other at a fixed radius from the center, then swept from startAngle to endAngle degrees. All positions and radii are normalized 0.0\u20131.0 (fractions of screen width/height, not pixels)\u2014same coordinate space as gesture-tap and gesture-swipe.
|
|
126941
|
+
endAngle > startAngle = clockwise rotation. Typical values: radius 0.15, startAngle 0, endAngle 90 for a 90\xB0 clockwise turn. A single radius applies to both axes, so on a non-square screen it traces a physical ellipse (finger separation varies through the turn); pass radiusX+radiusY (fractions of width/height with radiusX\xB7width = radiusY\xB7height) for a physically circular orbit instead.
|
|
126921
126942
|
Auto-generates interpolated frames at ~60fps.
|
|
126922
126943
|
Unlike gesture-pinch which moves fingers linearly to zoom, this orbits fingers in an arc to change orientation.
|
|
126923
126944
|
Use when you need to rotate a map, image picker, or any rotateable UI element. Returns { rotated: true, timestampMs }. Fails if the simulator-server / emulator backend is not reachable for the given device.`,
|
|
126924
126945
|
zodSchema: zodSchema22,
|
|
126946
|
+
inputSchema,
|
|
126925
126947
|
capability: capability14,
|
|
126926
126948
|
services: (params) => ({
|
|
126927
126949
|
simulatorServer: simulatorServerRef(resolveDevice(params.udid))
|
|
126928
126950
|
}),
|
|
126929
|
-
async execute(services, params) {
|
|
126951
|
+
async execute(services, params, ctx) {
|
|
126930
126952
|
const api = services.simulatorServer;
|
|
126931
126953
|
const duration3 = params.durationMs ?? 300;
|
|
126932
126954
|
const steps = Math.max(1, Math.round(duration3 / 16));
|
|
126955
|
+
const radiusX = params.radiusX ?? params.radius;
|
|
126956
|
+
const radiusY = params.radiusY ?? params.radius;
|
|
126933
126957
|
let timestampMs = 0;
|
|
126958
|
+
let lastX1 = 0;
|
|
126959
|
+
let lastY1 = 0;
|
|
126960
|
+
let lastX2 = 0;
|
|
126961
|
+
let lastY2 = 0;
|
|
126934
126962
|
for (let i = 0; i <= steps; i++) {
|
|
126963
|
+
if (ctx?.signal?.aborted) {
|
|
126964
|
+
if (i > 0) sendTouchEvent(api, "Up", lastX1, lastY1, lastX2, lastY2);
|
|
126965
|
+
const err = new Error(
|
|
126966
|
+
`gesture-rotate aborted \u2014 cancelled mid-gesture after ${i} of ${steps + 1} frames`
|
|
126967
|
+
);
|
|
126968
|
+
err.name = "AbortError";
|
|
126969
|
+
throw err;
|
|
126970
|
+
}
|
|
126935
126971
|
const t = i / steps;
|
|
126936
126972
|
const angleDeg = params.startAngle + (params.endAngle - params.startAngle) * t;
|
|
126937
126973
|
const angleRad = angleDeg * Math.PI / 180;
|
|
126938
|
-
const x1 = params.centerX +
|
|
126939
|
-
const y1 = params.centerY +
|
|
126940
|
-
const x2 = params.centerX -
|
|
126941
|
-
const y2 = params.centerY -
|
|
126974
|
+
const x1 = params.centerX + radiusX * Math.cos(angleRad);
|
|
126975
|
+
const y1 = params.centerY + radiusY * Math.sin(angleRad);
|
|
126976
|
+
const x2 = params.centerX - radiusX * Math.cos(angleRad);
|
|
126977
|
+
const y2 = params.centerY - radiusY * Math.sin(angleRad);
|
|
126942
126978
|
const type = i === 0 ? "Down" : i === steps ? "Up" : "Move";
|
|
126943
126979
|
if (i === 0) timestampMs = Date.now();
|
|
126944
126980
|
sendTouchEvent(api, type, x1, y1, x2, y2);
|
|
126981
|
+
lastX1 = x1;
|
|
126982
|
+
lastY1 = y1;
|
|
126983
|
+
lastX2 = x2;
|
|
126984
|
+
lastY2 = y2;
|
|
126945
126985
|
if (i < steps) await sleep(16);
|
|
126946
126986
|
}
|
|
126947
126987
|
return { rotated: true, timestampMs };
|
|
@@ -134019,7 +134059,7 @@ Allowed tools and their args (udid is auto-injected, do NOT include it in args):
|
|
|
134019
134059
|
gesture-drag: { fromX: number, fromY: number, toX: number, toY: number, durationMs?: number } [chromium only]
|
|
134020
134060
|
gesture-custom: { events: [{ type: "Down"|"Move"|"Up", x: number, y: number, x2?: number, y2?: number, delayMs?: number }], interpolate?: number } [ios/android]
|
|
134021
134061
|
gesture-pinch: { centerX: number, centerY: number, startDistance: number, endDistance: number, endCenterX?: number, endCenterY?: number, angle?: number, durationMs?: number } [ios/android]
|
|
134022
|
-
gesture-rotate: { centerX: number, centerY: number, radius
|
|
134062
|
+
gesture-rotate: { centerX: number, centerY: number, radius?: number, radiusX?: number, radiusY?: number, startAngle: number, endAngle: number, durationMs?: number } [ios/android]
|
|
134023
134063
|
button: { button: "home"|"back"|"power"|"volumeUp"|"volumeDown"|"appSwitch"|"actionButton" } [ios/android]
|
|
134024
134064
|
keyboard: { text?: string, key?: string, delayMs?: number } (key pressed after text; TV: text only) [ios/android/chromium/vega/tv]
|
|
134025
134065
|
text supports {{secret:<NAME>}} placeholders, resolved server-side from ARGENT_SECRET_<NAME> env vars (prefix mandatory) \u2014 credentials never enter agent context
|
|
@@ -145949,6 +145989,123 @@ var fs39 = __toESM(require("node:fs/promises"));
|
|
|
145949
145989
|
init_src();
|
|
145950
145990
|
var import_yaml = __toESM(require_dist5());
|
|
145951
145991
|
init_src();
|
|
145992
|
+
|
|
145993
|
+
// ../tool-server/src/tools/flows/flow-pinch-geometry.ts
|
|
145994
|
+
var PINCH_RATIO_PER_GESTURE = 4;
|
|
145995
|
+
var PINCH_SETTLE_MS = 250;
|
|
145996
|
+
var TARGET_START_FRACTION = 0.82;
|
|
145997
|
+
var SCREEN_EDGE_INSET = 0.02;
|
|
145998
|
+
var MIN_VIABLE_TRAVEL = 0.03;
|
|
145999
|
+
function systemEdgeGuards(device) {
|
|
146000
|
+
return device.platform === "android" ? { left: 0.13, right: 0.13, top: 0.08, bottom: 0.08 } : { left: 0.08, right: 0.08, top: 0.08, bottom: 0.08 };
|
|
146001
|
+
}
|
|
146002
|
+
function decomposePinch(scale) {
|
|
146003
|
+
const n = Math.max(
|
|
146004
|
+
1,
|
|
146005
|
+
Math.ceil(Math.abs(Math.log(scale)) / Math.log(PINCH_RATIO_PER_GESTURE) - 1e-9)
|
|
146006
|
+
);
|
|
146007
|
+
return { n, per: scale ** (1 / n) };
|
|
146008
|
+
}
|
|
146009
|
+
function clamp2(v, lo, hi) {
|
|
146010
|
+
return v < lo ? lo : v > hi ? hi : v;
|
|
146011
|
+
}
|
|
146012
|
+
function buildAxisCandidate(input) {
|
|
146013
|
+
const { angle, center, targetSpan, per, guards } = input;
|
|
146014
|
+
const c = angle === 0 ? center.x : center.y;
|
|
146015
|
+
const gLow = angle === 0 ? guards.left : guards.top;
|
|
146016
|
+
const gHigh = angle === 0 ? guards.right : guards.bottom;
|
|
146017
|
+
const p = angle === 0 ? center.y : center.x;
|
|
146018
|
+
const pLow = angle === 0 ? guards.top : guards.left;
|
|
146019
|
+
const pHigh = angle === 0 ? guards.bottom : guards.right;
|
|
146020
|
+
const screenEndSpan = 1 - 2 * SCREEN_EDGE_INSET;
|
|
146021
|
+
const edgeSafeStartSpan = 2 * Math.max(0, Math.min(c - gLow, 1 - gHigh - c));
|
|
146022
|
+
const insetStartSpan = 2 * Math.max(0, Math.min(c - SCREEN_EDGE_INSET, 1 - SCREEN_EDGE_INSET - c));
|
|
146023
|
+
const screenStartLimit = edgeSafeStartSpan > 0 ? Math.min(screenEndSpan, edgeSafeStartSpan) : Math.min(screenEndSpan, insetStartSpan);
|
|
146024
|
+
const startLimit = targetSpan !== void 0 ? Math.min(screenStartLimit, targetSpan * TARGET_START_FRACTION) : screenStartLimit;
|
|
146025
|
+
const start2 = per > 1 ? Math.min(startLimit, screenEndSpan / per) : startLimit;
|
|
146026
|
+
const end = Math.min(screenEndSpan, start2 * per);
|
|
146027
|
+
const travel = Math.abs(end - start2);
|
|
146028
|
+
if (!(travel > 0)) return void 0;
|
|
146029
|
+
const endCenter = clamp2(c, SCREEN_EDGE_INSET + end / 2, 1 - SCREEN_EDGE_INSET - end / 2);
|
|
146030
|
+
const downLow = c - start2 / 2;
|
|
146031
|
+
const downHigh = c + start2 / 2;
|
|
146032
|
+
const axisSafe = downLow >= gLow && downHigh <= 1 - gHigh;
|
|
146033
|
+
const perpSafe = p >= pLow && p <= 1 - pHigh;
|
|
146034
|
+
const clearance = Math.min(downLow - gLow, 1 - gHigh - downHigh, p - pLow, 1 - pHigh - p);
|
|
146035
|
+
return {
|
|
146036
|
+
angle,
|
|
146037
|
+
start: start2,
|
|
146038
|
+
end,
|
|
146039
|
+
endCenter,
|
|
146040
|
+
travel,
|
|
146041
|
+
viable: travel >= MIN_VIABLE_TRAVEL,
|
|
146042
|
+
axisSafe,
|
|
146043
|
+
fullyEdgeSafe: axisSafe && perpSafe,
|
|
146044
|
+
clearance
|
|
146045
|
+
};
|
|
146046
|
+
}
|
|
146047
|
+
function selectPinchCandidate(candidates) {
|
|
146048
|
+
if (candidates.length === 0) return void 0;
|
|
146049
|
+
const rank2 = (c) => c.fullyEdgeSafe ? 0 : c.axisSafe ? 1 : 2;
|
|
146050
|
+
return [...candidates].sort((a, b) => {
|
|
146051
|
+
const byViable = Number(b.viable) - Number(a.viable);
|
|
146052
|
+
if (byViable !== 0) return byViable;
|
|
146053
|
+
const byRank = rank2(a) - rank2(b);
|
|
146054
|
+
if (byRank !== 0) return byRank;
|
|
146055
|
+
if (a.fullyEdgeSafe && b.fullyEdgeSafe) return b.travel - a.travel;
|
|
146056
|
+
return b.clearance - a.clearance || b.travel - a.travel;
|
|
146057
|
+
})[0];
|
|
146058
|
+
}
|
|
146059
|
+
|
|
146060
|
+
// ../tool-server/src/tools/flows/flow-rotate-geometry.ts
|
|
146061
|
+
var ROTATE_MS_PER_90_DEG = 300;
|
|
146062
|
+
var MAX_DERIVED_ROTATE_MS = 1e4;
|
|
146063
|
+
var MAX_ROTATE_BY_DEG = MAX_DERIVED_ROTATE_MS / ROTATE_MS_PER_90_DEG * 90;
|
|
146064
|
+
function deriveRotateDurationMs(by) {
|
|
146065
|
+
return Math.round(ROTATE_MS_PER_90_DEG * Math.max(1, Math.abs(by) / 90));
|
|
146066
|
+
}
|
|
146067
|
+
function buildRotateCandidate(input) {
|
|
146068
|
+
const { startAngle, center, targetSpan, guards, aspect } = input;
|
|
146069
|
+
const xClear = Math.min(center.x - SCREEN_EDGE_INSET, 1 - SCREEN_EDGE_INSET - center.x);
|
|
146070
|
+
const yClear = Math.min(center.y - SCREEN_EDGE_INSET, 1 - SCREEN_EDGE_INSET - center.y);
|
|
146071
|
+
const screenRx = Math.min(xClear, yClear / aspect);
|
|
146072
|
+
const targetRx = targetSpan === void 0 ? Infinity : startAngle === 0 ? targetSpan * TARGET_START_FRACTION / 2 : targetSpan * TARGET_START_FRACTION / 2 / aspect;
|
|
146073
|
+
const radiusX = Math.min(screenRx, targetRx);
|
|
146074
|
+
if (!(radiusX > 0)) return void 0;
|
|
146075
|
+
const radiusY = radiusX * aspect;
|
|
146076
|
+
const c = startAngle === 0 ? center.x : center.y;
|
|
146077
|
+
const half = startAngle === 0 ? radiusX : radiusY;
|
|
146078
|
+
const gLow = startAngle === 0 ? guards.left : guards.top;
|
|
146079
|
+
const gHigh = startAngle === 0 ? guards.right : guards.bottom;
|
|
146080
|
+
const p = startAngle === 0 ? center.y : center.x;
|
|
146081
|
+
const pLow = startAngle === 0 ? guards.top : guards.left;
|
|
146082
|
+
const pHigh = startAngle === 0 ? guards.bottom : guards.right;
|
|
146083
|
+
const downLow = c - half;
|
|
146084
|
+
const downHigh = c + half;
|
|
146085
|
+
const axisSafe = downLow >= gLow && downHigh <= 1 - gHigh;
|
|
146086
|
+
const perpSafe = p >= pLow && p <= 1 - pHigh;
|
|
146087
|
+
const clearance = Math.min(downLow - gLow, 1 - gHigh - downHigh, p - pLow, 1 - pHigh - p);
|
|
146088
|
+
return {
|
|
146089
|
+
startAngle,
|
|
146090
|
+
radiusX,
|
|
146091
|
+
radiusY,
|
|
146092
|
+
axisSafe,
|
|
146093
|
+
fullyEdgeSafe: axisSafe && perpSafe,
|
|
146094
|
+
clearance
|
|
146095
|
+
};
|
|
146096
|
+
}
|
|
146097
|
+
function selectRotateCandidate(candidates) {
|
|
146098
|
+
if (candidates.length === 0) return void 0;
|
|
146099
|
+
const rank2 = (c) => c.fullyEdgeSafe ? 0 : c.axisSafe ? 1 : 2;
|
|
146100
|
+
return [...candidates].sort((a, b) => {
|
|
146101
|
+
const byRank = rank2(a) - rank2(b);
|
|
146102
|
+
if (byRank !== 0) return byRank;
|
|
146103
|
+
if (a.fullyEdgeSafe && b.fullyEdgeSafe) return b.radiusX - a.radiusX;
|
|
146104
|
+
return b.clearance - a.clearance || b.radiusX - a.radiusX;
|
|
146105
|
+
})[0];
|
|
146106
|
+
}
|
|
146107
|
+
|
|
146108
|
+
// ../tool-server/src/tools/flows/flow-utils.ts
|
|
145952
146109
|
var FLOWS_DIR_NAME = path29.join(".argent", "flows");
|
|
145953
146110
|
var activeFlowName = null;
|
|
145954
146111
|
var activeProjectRoot = null;
|
|
@@ -146258,6 +146415,10 @@ function toYamlStep(step) {
|
|
|
146258
146415
|
return {
|
|
146259
146416
|
pinch: step.selector ? { on: selectorToYaml(step.selector), scale: step.scale } : { scale: step.scale }
|
|
146260
146417
|
};
|
|
146418
|
+
case "rotate":
|
|
146419
|
+
return {
|
|
146420
|
+
rotate: step.selector ? { on: selectorToYaml(step.selector), by: step.by } : { by: step.by }
|
|
146421
|
+
};
|
|
146261
146422
|
case "snapshot":
|
|
146262
146423
|
return step.maxMismatch === void 0 ? { snapshot: step.name } : { snapshot: { name: step.name, maxMismatch: step.maxMismatch } };
|
|
146263
146424
|
case "tool":
|
|
@@ -146531,6 +146692,7 @@ var STEP_DIRECTIVE_KEYS = [
|
|
|
146531
146692
|
"wait",
|
|
146532
146693
|
"scroll-to",
|
|
146533
146694
|
"pinch",
|
|
146695
|
+
"rotate",
|
|
146534
146696
|
"snapshot"
|
|
146535
146697
|
];
|
|
146536
146698
|
function parseTapTimes(raw, entry) {
|
|
@@ -146649,6 +146811,31 @@ function parsePinch(body, entry) {
|
|
|
146649
146811
|
if (obj.on !== void 0) step.selector = parseSelector(obj.on, "pinch.on");
|
|
146650
146812
|
return step;
|
|
146651
146813
|
}
|
|
146814
|
+
function parseRotate(body, entry) {
|
|
146815
|
+
if (body === null || typeof body !== "object") {
|
|
146816
|
+
badEntry(
|
|
146817
|
+
entry,
|
|
146818
|
+
'rotate takes an options map \u2014 e.g. rotate: { on: "Map", by: 90 } (an angle is required)'
|
|
146819
|
+
);
|
|
146820
|
+
}
|
|
146821
|
+
const obj = body;
|
|
146822
|
+
if (obj.text !== void 0 || obj.id !== void 0 || obj.identifier !== void 0 || obj.role !== void 0) {
|
|
146823
|
+
badEntry(entry, 'rotate takes a nested selector \u2014 e.g. rotate: { on: "Map", by: 90 }');
|
|
146824
|
+
}
|
|
146825
|
+
rejectUnknownKeys(entry, obj, ["on", "by"], "rotate");
|
|
146826
|
+
if (typeof obj.by !== "number" || !Number.isFinite(obj.by) || obj.by === 0) {
|
|
146827
|
+
badEntry(entry, "rotate.by must be a finite non-zero number of degrees (+CW, \u2212CCW)");
|
|
146828
|
+
}
|
|
146829
|
+
if (Math.abs(obj.by) > MAX_ROTATE_BY_DEG) {
|
|
146830
|
+
badEntry(
|
|
146831
|
+
entry,
|
|
146832
|
+
`rotate.by must be within \xB1${MAX_ROTATE_BY_DEG}\xB0 \u2014 one continuous gesture at ~300\xB0/s (10 s max)`
|
|
146833
|
+
);
|
|
146834
|
+
}
|
|
146835
|
+
const step = { kind: "rotate", by: obj.by };
|
|
146836
|
+
if (obj.on !== void 0) step.selector = parseSelector(obj.on, "rotate.on");
|
|
146837
|
+
return step;
|
|
146838
|
+
}
|
|
146652
146839
|
function parseWhenCondition(raw) {
|
|
146653
146840
|
const conditionKeys = `${WAIT_CONDITIONS.join(", ")}, platform`;
|
|
146654
146841
|
if (raw === null || typeof raw !== "object") {
|
|
@@ -146817,6 +147004,7 @@ function fromYamlStep(raw, whenDepth = 0) {
|
|
|
146817
147004
|
return step;
|
|
146818
147005
|
}
|
|
146819
147006
|
if ("pinch" in raw) return parsePinch(raw.pinch, raw);
|
|
147007
|
+
if ("rotate" in raw) return parseRotate(raw.rotate, raw);
|
|
146820
147008
|
if ("snapshot" in raw) {
|
|
146821
147009
|
const body = raw.snapshot;
|
|
146822
147010
|
if (body !== null && typeof body === "object" && !Array.isArray(body)) {
|
|
@@ -147221,7 +147409,7 @@ function projectIosNode(node, screenW, screenH) {
|
|
|
147221
147409
|
scrolls: role === "AXScrollArea"
|
|
147222
147410
|
};
|
|
147223
147411
|
}
|
|
147224
|
-
function
|
|
147412
|
+
function adaptFullHierarchy(raw) {
|
|
147225
147413
|
const windows = typeof raw === "object" && raw !== null && Array.isArray(raw.windows) ? raw.windows.map(asViewNode).filter((n) => n !== null) : [];
|
|
147226
147414
|
let screenW = 0;
|
|
147227
147415
|
let screenH = 0;
|
|
@@ -147238,11 +147426,12 @@ function adaptFullHierarchyToDescribeResult(raw) {
|
|
|
147238
147426
|
flattenHoisting(win, (n) => projectIosNode(n, screenW, screenH), children);
|
|
147239
147427
|
}
|
|
147240
147428
|
}
|
|
147241
|
-
|
|
147429
|
+
const tree = parseDescribeResult({
|
|
147242
147430
|
role: "AXGroup",
|
|
147243
147431
|
frame: { x: 0, y: 0, width: 1, height: 1 },
|
|
147244
147432
|
children
|
|
147245
147433
|
});
|
|
147434
|
+
return screenW > 0 && screenH > 0 ? { tree, screen: { width: screenW, height: screenH } } : { tree };
|
|
147246
147435
|
}
|
|
147247
147436
|
var FULL_HIERARCHY_FIELDS = [
|
|
147248
147437
|
"className",
|
|
@@ -147284,7 +147473,8 @@ async function queryFullHierarchyTree(registry2, device) {
|
|
|
147284
147473
|
if (rawResult.error) {
|
|
147285
147474
|
throw new Error(`getFullHierarchy failed for ${target.bundleId}: ${rawResult.error}`);
|
|
147286
147475
|
}
|
|
147287
|
-
|
|
147476
|
+
const { tree, screen } = adaptFullHierarchy(rawResult);
|
|
147477
|
+
return { tree, source: "native-devtools", ...screen ? { screen } : {} };
|
|
147288
147478
|
}
|
|
147289
147479
|
function errMsg(err) {
|
|
147290
147480
|
return err instanceof Error ? err.message : String(err);
|
|
@@ -147407,7 +147597,11 @@ async function queryAndroidFullHierarchy(registry2, device) {
|
|
|
147407
147597
|
devtools.getScreenSize()
|
|
147408
147598
|
]);
|
|
147409
147599
|
const tree = adaptFullAndroidHierarchyToDescribeResult(xml, size.width, size.height);
|
|
147410
|
-
return {
|
|
147600
|
+
return {
|
|
147601
|
+
tree,
|
|
147602
|
+
source: "android-devtools",
|
|
147603
|
+
...size.width > 0 && size.height > 0 ? { screen: { width: size.width, height: size.height } } : {}
|
|
147604
|
+
};
|
|
147411
147605
|
}
|
|
147412
147606
|
|
|
147413
147607
|
// ../tool-server/src/tools/flows/flow-chromium-tree.ts
|
|
@@ -147715,6 +147909,8 @@ You can still edit the .yaml file directly afterwards to remove or reorder steps
|
|
|
147715
147909
|
return `${n}. scroll-to: ${selectorLabel(step.target)} (${step.direction})`;
|
|
147716
147910
|
case "pinch":
|
|
147717
147911
|
return `${n}. pinch: scale ${step.scale}${step.selector ? ` on ${selectorLabel(step.selector)}` : ""}`;
|
|
147912
|
+
case "rotate":
|
|
147913
|
+
return `${n}. rotate: by ${step.by}\xB0${step.selector ? ` on ${selectorLabel(step.selector)}` : ""}`;
|
|
147718
147914
|
case "snapshot":
|
|
147719
147915
|
return `${n}. snapshot: ${step.name}`;
|
|
147720
147916
|
case "tool":
|
|
@@ -147741,73 +147937,6 @@ var fs45 = __toESM(require("node:fs/promises"));
|
|
|
147741
147937
|
var path33 = __toESM(require("node:path"));
|
|
147742
147938
|
init_src();
|
|
147743
147939
|
|
|
147744
|
-
// ../tool-server/src/tools/flows/flow-pinch-geometry.ts
|
|
147745
|
-
var PINCH_RATIO_PER_GESTURE = 4;
|
|
147746
|
-
var PINCH_SETTLE_MS = 250;
|
|
147747
|
-
var TARGET_START_FRACTION = 0.82;
|
|
147748
|
-
var SCREEN_EDGE_INSET = 0.02;
|
|
147749
|
-
var MIN_VIABLE_TRAVEL = 0.03;
|
|
147750
|
-
function systemEdgeGuards(device) {
|
|
147751
|
-
return device.platform === "android" ? { left: 0.13, right: 0.13, top: 0.08, bottom: 0.08 } : { left: 0.08, right: 0.08, top: 0.08, bottom: 0.08 };
|
|
147752
|
-
}
|
|
147753
|
-
function decomposePinch(scale) {
|
|
147754
|
-
const n = Math.max(
|
|
147755
|
-
1,
|
|
147756
|
-
Math.ceil(Math.abs(Math.log(scale)) / Math.log(PINCH_RATIO_PER_GESTURE) - 1e-9)
|
|
147757
|
-
);
|
|
147758
|
-
return { n, per: scale ** (1 / n) };
|
|
147759
|
-
}
|
|
147760
|
-
function clamp2(v, lo, hi) {
|
|
147761
|
-
return v < lo ? lo : v > hi ? hi : v;
|
|
147762
|
-
}
|
|
147763
|
-
function buildAxisCandidate(input) {
|
|
147764
|
-
const { angle, center, targetSpan, per, guards } = input;
|
|
147765
|
-
const c = angle === 0 ? center.x : center.y;
|
|
147766
|
-
const gLow = angle === 0 ? guards.left : guards.top;
|
|
147767
|
-
const gHigh = angle === 0 ? guards.right : guards.bottom;
|
|
147768
|
-
const p = angle === 0 ? center.y : center.x;
|
|
147769
|
-
const pLow = angle === 0 ? guards.top : guards.left;
|
|
147770
|
-
const pHigh = angle === 0 ? guards.bottom : guards.right;
|
|
147771
|
-
const screenEndSpan = 1 - 2 * SCREEN_EDGE_INSET;
|
|
147772
|
-
const edgeSafeStartSpan = 2 * Math.max(0, Math.min(c - gLow, 1 - gHigh - c));
|
|
147773
|
-
const insetStartSpan = 2 * Math.max(0, Math.min(c - SCREEN_EDGE_INSET, 1 - SCREEN_EDGE_INSET - c));
|
|
147774
|
-
const screenStartLimit = edgeSafeStartSpan > 0 ? Math.min(screenEndSpan, edgeSafeStartSpan) : Math.min(screenEndSpan, insetStartSpan);
|
|
147775
|
-
const startLimit = targetSpan !== void 0 ? Math.min(screenStartLimit, targetSpan * TARGET_START_FRACTION) : screenStartLimit;
|
|
147776
|
-
const start2 = per > 1 ? Math.min(startLimit, screenEndSpan / per) : startLimit;
|
|
147777
|
-
const end = Math.min(screenEndSpan, start2 * per);
|
|
147778
|
-
const travel = Math.abs(end - start2);
|
|
147779
|
-
if (!(travel > 0)) return void 0;
|
|
147780
|
-
const endCenter = clamp2(c, SCREEN_EDGE_INSET + end / 2, 1 - SCREEN_EDGE_INSET - end / 2);
|
|
147781
|
-
const downLow = c - start2 / 2;
|
|
147782
|
-
const downHigh = c + start2 / 2;
|
|
147783
|
-
const axisSafe = downLow >= gLow && downHigh <= 1 - gHigh;
|
|
147784
|
-
const perpSafe = p >= pLow && p <= 1 - pHigh;
|
|
147785
|
-
const clearance = Math.min(downLow - gLow, 1 - gHigh - downHigh, p - pLow, 1 - pHigh - p);
|
|
147786
|
-
return {
|
|
147787
|
-
angle,
|
|
147788
|
-
start: start2,
|
|
147789
|
-
end,
|
|
147790
|
-
endCenter,
|
|
147791
|
-
travel,
|
|
147792
|
-
viable: travel >= MIN_VIABLE_TRAVEL,
|
|
147793
|
-
axisSafe,
|
|
147794
|
-
fullyEdgeSafe: axisSafe && perpSafe,
|
|
147795
|
-
clearance
|
|
147796
|
-
};
|
|
147797
|
-
}
|
|
147798
|
-
function selectPinchCandidate(candidates) {
|
|
147799
|
-
if (candidates.length === 0) return void 0;
|
|
147800
|
-
const rank2 = (c) => c.fullyEdgeSafe ? 0 : c.axisSafe ? 1 : 2;
|
|
147801
|
-
return [...candidates].sort((a, b) => {
|
|
147802
|
-
const byViable = Number(b.viable) - Number(a.viable);
|
|
147803
|
-
if (byViable !== 0) return byViable;
|
|
147804
|
-
const byRank = rank2(a) - rank2(b);
|
|
147805
|
-
if (byRank !== 0) return byRank;
|
|
147806
|
-
if (a.fullyEdgeSafe && b.fullyEdgeSafe) return b.travel - a.travel;
|
|
147807
|
-
return b.clearance - a.clearance || b.travel - a.travel;
|
|
147808
|
-
})[0];
|
|
147809
|
-
}
|
|
147810
|
-
|
|
147811
147940
|
// ../tool-server/src/tools/flows/flow-actions.ts
|
|
147812
147941
|
var ABORTED_OUTCOME = {
|
|
147813
147942
|
ok: false,
|
|
@@ -148021,16 +148150,16 @@ function offscreenHint(sel) {
|
|
|
148021
148150
|
return `no visible element matched selector ${describeSelector(sel)} \u2014 if it is off-screen, add a scroll-to step before this one`;
|
|
148022
148151
|
}
|
|
148023
148152
|
async function runDirective(env, step) {
|
|
148024
|
-
if (env.device.platform === "vega" && (step.kind === "tap" || step.kind === "long-press" || step.kind === "type" || step.kind === "scroll-to" || step.kind === "pinch")) {
|
|
148153
|
+
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")) {
|
|
148025
148154
|
return {
|
|
148026
148155
|
ok: false,
|
|
148027
148156
|
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`
|
|
148028
148157
|
};
|
|
148029
148158
|
}
|
|
148030
|
-
if (step.kind === "pinch" && env.device.platform === "chromium") {
|
|
148159
|
+
if ((step.kind === "pinch" || step.kind === "rotate") && env.device.platform === "chromium") {
|
|
148031
148160
|
return {
|
|
148032
148161
|
ok: false,
|
|
148033
|
-
reason: "pinch is unsupported on chromium \u2014 desktop apps have no uniform pinch-zoom mapping (they zoom via ctrl+wheel or their own controls); drive the app's zoom UI with tap/keyboard instead"
|
|
148162
|
+
reason: step.kind === "pinch" ? "pinch is unsupported on chromium \u2014 desktop apps have no uniform pinch-zoom mapping (they zoom via ctrl+wheel or their own controls); drive the app's zoom UI with tap/keyboard instead" : "rotate is unsupported on chromium \u2014 desktop apps have no rotate-gesture idiom; drive the app's rotate controls with tap/keyboard instead"
|
|
148034
148163
|
};
|
|
148035
148164
|
}
|
|
148036
148165
|
switch (step.kind) {
|
|
@@ -148051,6 +148180,8 @@ async function runDirective(env, step) {
|
|
|
148051
148180
|
}
|
|
148052
148181
|
case "pinch":
|
|
148053
148182
|
return runPinch(env, step);
|
|
148183
|
+
case "rotate":
|
|
148184
|
+
return runRotate(env, step);
|
|
148054
148185
|
}
|
|
148055
148186
|
}
|
|
148056
148187
|
async function resolveTargetPoint(env, target) {
|
|
@@ -148140,6 +148271,66 @@ async function runPinch(env, step) {
|
|
|
148140
148271
|
}
|
|
148141
148272
|
return { ok: true };
|
|
148142
148273
|
}
|
|
148274
|
+
async function fetchScreenAspect(env) {
|
|
148275
|
+
try {
|
|
148276
|
+
const { screen } = await fetchFlowTree(env.registry, env.device);
|
|
148277
|
+
return screen && screen.width > 0 && screen.height > 0 ? screen.width / screen.height : void 0;
|
|
148278
|
+
} catch {
|
|
148279
|
+
return void 0;
|
|
148280
|
+
}
|
|
148281
|
+
}
|
|
148282
|
+
async function runRotate(env, step) {
|
|
148283
|
+
let center = { x: 0.5, y: 0.5 };
|
|
148284
|
+
let frame;
|
|
148285
|
+
if (step.selector) {
|
|
148286
|
+
const resolved = await waitForFrame(env, step.selector);
|
|
148287
|
+
if (resolved === "aborted") return ABORTED_OUTCOME;
|
|
148288
|
+
if (!resolved) return { ok: false, reason: offscreenHint(step.selector) };
|
|
148289
|
+
frame = resolved;
|
|
148290
|
+
center = getDescribeTapPoint(resolved);
|
|
148291
|
+
}
|
|
148292
|
+
const aspect = await fetchScreenAspect(env);
|
|
148293
|
+
const guards = systemEdgeGuards(env.device);
|
|
148294
|
+
const candidates = [
|
|
148295
|
+
buildRotateCandidate({
|
|
148296
|
+
startAngle: 0,
|
|
148297
|
+
center,
|
|
148298
|
+
targetSpan: frame?.width,
|
|
148299
|
+
guards,
|
|
148300
|
+
aspect: aspect ?? 1
|
|
148301
|
+
}),
|
|
148302
|
+
buildRotateCandidate({
|
|
148303
|
+
startAngle: 90,
|
|
148304
|
+
center,
|
|
148305
|
+
targetSpan: frame?.height,
|
|
148306
|
+
guards,
|
|
148307
|
+
aspect: aspect ?? 1
|
|
148308
|
+
})
|
|
148309
|
+
].filter((c) => c !== void 0);
|
|
148310
|
+
const selected = selectRotateCandidate(candidates);
|
|
148311
|
+
if (!selected) {
|
|
148312
|
+
return {
|
|
148313
|
+
ok: false,
|
|
148314
|
+
reason: `rotate found no on-screen orbit radius around (${center.x}, ${center.y})`
|
|
148315
|
+
};
|
|
148316
|
+
}
|
|
148317
|
+
if (env.signal?.aborted) return ABORTED_OUTCOME;
|
|
148318
|
+
try {
|
|
148319
|
+
await invokeOnDevice(env, "gesture-rotate", {
|
|
148320
|
+
centerX: center.x,
|
|
148321
|
+
centerY: center.y,
|
|
148322
|
+
...aspect === void 0 ? { radius: selected.radiusX } : { radiusX: selected.radiusX, radiusY: selected.radiusY },
|
|
148323
|
+
startAngle: selected.startAngle,
|
|
148324
|
+
// endAngle > startAngle = clockwise in the tool, matching +by.
|
|
148325
|
+
endAngle: selected.startAngle + step.by,
|
|
148326
|
+
durationMs: deriveRotateDurationMs(step.by)
|
|
148327
|
+
});
|
|
148328
|
+
} catch (err) {
|
|
148329
|
+
if (env.signal?.aborted) return ABORTED_OUTCOME;
|
|
148330
|
+
throw err;
|
|
148331
|
+
}
|
|
148332
|
+
return { ok: true };
|
|
148333
|
+
}
|
|
148143
148334
|
async function runType(env, step) {
|
|
148144
148335
|
const frame = await waitForFrame(env, step.into);
|
|
148145
148336
|
if (frame === "aborted") return ABORTED_OUTCOME;
|
|
@@ -150600,7 +150791,9 @@ ready; \`tool\` calls dispatch through the registry; \`tap\`/\`long-press\`/\`ty
|
|
|
150600
150791
|
element and act on it (\`tap: { on, times: 2 }\` double-taps; \`long-press: { on, duration }\` presses and
|
|
150601
150792
|
holds; \`tap\`/\`long-press\` alternatively take a raw normalized point \u2014 bare \`{ x, y }\` or \`on: { x, y }\`);
|
|
150602
150793
|
\`scroll-to\` scrolls (momentum-free) until a target is visible; \`pinch\` zooms
|
|
150603
|
-
(\`pinch: { on?, scale }\` \u2014 scale > 1 in, < 1 out; screen center when \`on\` is omitted); \`
|
|
150794
|
+
(\`pinch: { on?, scale }\` \u2014 scale > 1 in, < 1 out; screen center when \`on\` is omitted); \`rotate\` is the
|
|
150795
|
+
two-finger rotation gesture (\`rotate: { on?, by }\` \u2014 degrees, + clockwise, within \xB13000\xB0; screen center
|
|
150796
|
+
when \`on\` is omitted; distinct from the \`rotate\` tool, which changes device orientation); \`await\` waits
|
|
150604
150797
|
for a UI condition; \`wait\` pauses for a fixed number of milliseconds; \`assert\` checks one now; \`snapshot\`
|
|
150605
150798
|
diffs a screenshot against a stored baseline (a missing baseline fails the step \u2014 set updateBaselines
|
|
150606
150799
|
to adopt the current screen); \`echo\` annotates; \`run\` executes a referenced fragment inline.
|
|
@@ -150789,6 +150982,10 @@ function stepTarget(step) {
|
|
|
150789
150982
|
const scale = `scale ${step.scale}`;
|
|
150790
150983
|
return step.selector ? `${selectorLabel2(step.selector)} (${scale})` : scale;
|
|
150791
150984
|
}
|
|
150985
|
+
case "rotate": {
|
|
150986
|
+
const by = `by ${step.by}\xB0`;
|
|
150987
|
+
return step.selector ? `${selectorLabel2(step.selector)} (${by})` : by;
|
|
150988
|
+
}
|
|
150792
150989
|
case "snapshot":
|
|
150793
150990
|
return `"${step.name}"`;
|
|
150794
150991
|
default:
|
|
@@ -150971,7 +151168,8 @@ async function execLeafStep(state3, step, index, scope) {
|
|
|
150971
151168
|
case "await":
|
|
150972
151169
|
case "assert":
|
|
150973
151170
|
case "scroll-to":
|
|
150974
|
-
case "pinch":
|
|
151171
|
+
case "pinch":
|
|
151172
|
+
case "rotate": {
|
|
150975
151173
|
try {
|
|
150976
151174
|
const r = await runDirective(state3, step);
|
|
150977
151175
|
if (r.aborted) return { ...base, status: "skip", reason: r.reason };
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -28,6 +28,7 @@ Beyond raw `tool:` steps and `echo:`, flows support declarative directives inter
|
|
|
28
28
|
| `type` | `- type: { into: email, text: "a@b.com" }` | focus a field, type, then press Enter to submit + dismiss the keyboard |
|
|
29
29
|
| `scroll-to` | `- scroll-to: "Order #1234"` (scrolls down) or `- scroll-to: { target: …, direction: right, within: … }` | momentum-free scroll until the target is visible |
|
|
30
30
|
| `pinch` | `- pinch: { on: "Map", scale: 3 }` or `- pinch: { scale: 0.5 }` | two-finger zoom in (`scale` > 1) or out (`< 1`); big scales chain gestures; `on` optional — defaults to screen center; open-loop — assert the visible result |
|
|
31
|
+
| `rotate` | `- rotate: { on: "Map", by: 90 }` or `- rotate: { by: -45 }` | two-finger rotation by degrees (+ CW, − CCW, within ±3000°; options map only); `on` optional — screen center default; not `tool: rotate` (orientation) |
|
|
31
32
|
| `await` | `- await: { visible: Home }` | wait for a UI condition |
|
|
32
33
|
| `wait` | `- wait: 500` | pause for a fixed number of milliseconds (last resort — prefer `await`) |
|
|
33
34
|
| `assert` | `- assert: { visible: Welcome }` | check a condition, hard-fail if it never holds |
|
|
@@ -68,7 +69,7 @@ Never record a real credential into a flow — the YAML is committed to the repo
|
|
|
68
69
|
|
|
69
70
|
### TV targets (Vega)
|
|
70
71
|
|
|
71
|
-
A Vega (Fire TV) device is remote-driven — there is no touch input, so the touch directives (`tap`, `long-press`, `type`, `scroll-to`, `pinch`) fail on it with guidance. Drive focus with `tool: tv-remote` steps and type with `tool: keyboard` instead; everything else (`launch`, `await`, `assert`, `wait`, `snapshot`, `echo`, `run`, selectors) works unchanged — the tree comes from the on-device automation toolkit, which attaches at app launch (the `launch` step waits for it, so a leading `launch` also guarantees selectors resolve).
|
|
72
|
+
A Vega (Fire TV) device is remote-driven — there is no touch input, so the touch directives (`tap`, `long-press`, `type`, `scroll-to`, `pinch`, `rotate`) fail on it with guidance. Drive focus with `tool: tv-remote` steps and type with `tool: keyboard` instead; everything else (`launch`, `await`, `assert`, `wait`, `snapshot`, `echo`, `run`, selectors) works unchanged — the tree comes from the on-device automation toolkit, which attaches at app launch (the `launch` step waits for it, so a leading `launch` also guarantees selectors resolve).
|
|
72
73
|
|
|
73
74
|
```yaml
|
|
74
75
|
steps:
|
|
@@ -130,6 +131,7 @@ Record an `await-ui-element` step to **gate** the next step on a screen transiti
|
|
|
130
131
|
- `tool: await-ui-element` gating a transition → `await: { visible: "…" }` / `{ hidden: … }` / `{ text: { in: …, equals: … } }`, carrying a custom `timeoutMs` over as a `timeout` sibling key. Converting also upgrades the wait from the trimmed `describe` tree to the flow's full-hierarchy tree (see Selectors). Keep the raw `tool: await-ui-element` step only when it sets a custom `pollIntervalMs`/`bundleId` the directive can't express.
|
|
131
132
|
- A scroll-to-reach-an-element — a `tool: gesture-swipe` (or its chromium analog, `gesture-scroll`) used to bring a specific element on screen before interacting with it (a `tap`, `type`, `assert`, …) → `scroll-to: { target: "<that element>", direction: … }`, dropping the swipe. This is far more robust than a fixed-distance swipe: it scrolls momentum-free and stops exactly when the target appears, so it survives layout and content changes. (`tap`/`type` do not scroll, so a raw swipe whose fling lands differently on another device leaves the following tap unresolved — always prefer the `scroll-to` rewrite.) Keep a `gesture-swipe` as a raw `tool:` step when it isn't scrolling toward a specific element — especially a velocity-dependent gesture like swipe-to-dismiss, edge-swipe-back, or swipe-to-reveal a row action, which a momentum-free `scroll-to` would not reproduce.
|
|
132
133
|
- `tool: gesture-pinch` → `pinch: { on: "<target>", scale: … }`, deriving `scale` as `endDistance / startDistance`. Set `on:` to the element under the pinch center when the pinch was aimed at one (the map or image being zoomed); omit it for a screen-center pinch. Don't carry the recorded distances/angle over — the directive re-derives the geometry (finger placement, system-edge avoidance, chaining of large scales) at run time, so the conversion swaps device-specific coordinates for a portable selector with auto-wait. Keep the raw `tool: gesture-pinch` step when the pinch is anchored at a specific point _inside_ a large element (zooming toward a particular map location, not the map's center) or deliberately pans via `endCenterX`/`endCenterY` — `on:` takes only a selector and re-centers the pinch on the element's frame center, so converting would silently move the zoom anchor.
|
|
134
|
+
- `tool: gesture-rotate` → `rotate: { on: "<target>", by: … }`, deriving `by` as `endAngle − startAngle` (the tool's `endAngle` > `startAngle` turns clockwise, matching the directive's positive `by`). Set `on:` to the element under the rotation center when the rotation was aimed at one (the map or image being rotated); omit it for a screen-center rotation. Don't carry the recorded `centerX`/`centerY`, radii (`radius` or `radiusX`/`radiusY`), `startAngle`, or `durationMs` over — the directive re-derives the geometry (finger placement, physical-circle radius, system-edge avoidance) and runs at a fixed pace (~90° per 300 ms), so the conversion swaps device-specific coordinates for a portable selector with auto-wait. Keep the raw `tool: gesture-rotate` step when the rotation is anchored at a specific point _inside_ a large element rather than its center (the directive re-centers on the element's frame center, so converting would silently move the pivot), when the gesture's speed itself matters (the directive's pace is fixed), or when the sweep exceeds the directive's ±3000° bound.
|
|
133
135
|
|
|
134
136
|
Every other recorded tool (a velocity-dependent `gesture-swipe`, a fixed-distance `gesture-scroll` not aimed at an element, `button`, `screenshot`, …) has no directive form — leave it as a `tool:` step. The recorder already handles the rest: coordinate `gesture-tap`s are captured as portable `tap:` selector steps, a `restart-app` is captured as a `launch:` step, a `flow-execute` of a sibling fragment is captured as a `run: <name>` composition directive, and device ids are stripped. Captured selectors are emitted in the strict map form (`tap: { text: General }`), never as a loose bare string — the recorder verified the exact element the tap hit, and a bare string would re-parse as loose and route through the identifier-first fallback it was never checked against. After editing, re-run with `flow-execute` to confirm the cleaned flow still passes.
|
|
135
137
|
|
|
@@ -149,7 +149,7 @@ All values are normalized 0.0–1.0 (fractions of screen, not pixels) — same a
|
|
|
149
149
|
}
|
|
150
150
|
```
|
|
151
151
|
|
|
152
|
-
All positions and
|
|
152
|
+
All positions and radii are normalized 0.0–1.0 (fractions of screen, not pixels). `radius: 0.15` means each finger is 15% of the screen away from center. `endAngle > startAngle` = clockwise. Default duration: 300ms. Optional: `"durationMs": 500` for slower rotation, and `"radiusX"`/`"radiusY"` (fractions of screen width/height; give both — they override `radius`) with `radiusX·width = radiusY·height` for a physically circular orbit — a single `radius` traces a physical ellipse on a non-square screen, coupling a slight pinch into the turn.
|
|
153
153
|
|
|
154
154
|
### gesture-custom — Custom touch sequence
|
|
155
155
|
|