@swmansion/argent 0.17.0 → 0.17.1-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 CHANGED
@@ -7755,7 +7755,7 @@ var _CI_VENDOR_COUNT_FOR_TEST = vendors_default.length;
7755
7755
  var SESSION_ID2 = randomUUID5();
7756
7756
  function readCliVersion() {
7757
7757
  if (true) {
7758
- return "0.17.0";
7758
+ return "0.17.1-next.1";
7759
7759
  }
7760
7760
  return "0.0.0";
7761
7761
  }
@@ -17044,7 +17044,7 @@ var _CI_VENDOR_COUNT_FOR_TEST = vendors_default.length;
17044
17044
  var SESSION_ID = randomUUID3();
17045
17045
  function readCliVersion() {
17046
17046
  if (true) {
17047
- return "0.17.0";
17047
+ return "0.17.1-next.1";
17048
17048
  }
17049
17049
  return "0.0.0";
17050
17050
  }
@@ -104493,7 +104493,7 @@ var _CI_VENDOR_COUNT_FOR_TEST = vendors_default.length;
104493
104493
  var SESSION_ID = (0, import_node_crypto3.randomUUID)();
104494
104494
  function readCliVersion() {
104495
104495
  if (true) {
104496
- return "0.17.0";
104496
+ return "0.17.1-next.1";
104497
104497
  }
104498
104498
  return "0.0.0";
104499
104499
  }
@@ -105881,7 +105881,7 @@ var import_node_path5 = __toESM(require("node:path"));
105881
105881
  var import_semver = __toESM(require_semver2());
105882
105882
 
105883
105883
  // ../tool-server/package.json
105884
- var version3 = "0.17.0";
105884
+ var version3 = "0.17.1-next.1";
105885
105885
 
105886
105886
  // ../tool-server/src/utils/update-checker.ts
105887
105887
  var import_update_core = __toESM(require_dist4());
@@ -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 radius are normalized 0.0\u20131.0 (fractions of screen width/height, not pixels)\u2014same coordinate space as gesture-tap and gesture-swipe.
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 + params.radius * Math.cos(angleRad);
126939
- const y1 = params.centerY + params.radius * Math.sin(angleRad);
126940
- const x2 = params.centerX - params.radius * Math.cos(angleRad);
126941
- const y2 = params.centerY - params.radius * Math.sin(angleRad);
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: number, startAngle: number, endAngle: number, durationMs?: number } [ios/android]
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 adaptFullHierarchyToDescribeResult(raw) {
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
- return parseDescribeResult({
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
- return { tree: adaptFullHierarchyToDescribeResult(rawResult), source: "native-devtools" };
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 { tree, source: "android-devtools" };
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); \`await\` waits
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swmansion/argent",
3
- "version": "0.17.0",
3
+ "version": "0.17.1-next.1",
4
4
  "description": "MCP server for iOS Simulator and Android Emulator control",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -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 radius 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.
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