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

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.
Files changed (35) hide show
  1. package/dist/cli-cmds.mjs +64 -21
  2. package/dist/installer.mjs +197 -7
  3. package/dist/mcp-server.mjs +92 -23
  4. package/dist/tool-server.cjs +1017 -236
  5. package/package.json +1 -1
  6. package/rules/argent.md +7 -7
  7. package/skills/argent-create-flow/references/flow-yaml.md +30 -3
  8. package/skills/argent-create-flow/references/live-authoring.md +14 -12
  9. package/skills/argent-create-flow/references/reliability-and-recovery.md +2 -2
  10. package/skills/argent-device-interact/SKILL.md +4 -2
  11. package/skills/argent-qa-flows/SKILL.md +1 -1
  12. package/bin/linux/resources/android/LICENSE.txt +0 -186
  13. package/bin/linux/resources/android/README.md +0 -11
  14. package/bin/linux/resources/android/arm64-v8a/libscreen-sharing-agent.so +0 -0
  15. package/bin/linux/resources/android/armeabi-v7a/libscreen-sharing-agent.so +0 -0
  16. package/bin/linux/resources/android/screen-sharing-agent.jar +0 -0
  17. package/bin/linux/resources/android/x86_64/libscreen-sharing-agent.so +0 -0
  18. package/bin/linux-arm64/resources/android/LICENSE.txt +0 -186
  19. package/bin/linux-arm64/resources/android/README.md +0 -11
  20. package/bin/linux-arm64/resources/android/arm64-v8a/libscreen-sharing-agent.so +0 -0
  21. package/bin/linux-arm64/resources/android/armeabi-v7a/libscreen-sharing-agent.so +0 -0
  22. package/bin/linux-arm64/resources/android/screen-sharing-agent.jar +0 -0
  23. package/bin/linux-arm64/resources/android/x86_64/libscreen-sharing-agent.so +0 -0
  24. package/bin/win32/resources/android/LICENSE.txt +0 -186
  25. package/bin/win32/resources/android/README.md +0 -11
  26. package/bin/win32/resources/android/arm64-v8a/libscreen-sharing-agent.so +0 -0
  27. package/bin/win32/resources/android/armeabi-v7a/libscreen-sharing-agent.so +0 -0
  28. package/bin/win32/resources/android/screen-sharing-agent.jar +0 -0
  29. package/bin/win32/resources/android/x86_64/libscreen-sharing-agent.so +0 -0
  30. /package/bin/{darwin/resources → resources}/android/LICENSE.txt +0 -0
  31. /package/bin/{darwin/resources → resources}/android/README.md +0 -0
  32. /package/bin/{darwin/resources → resources}/android/arm64-v8a/libscreen-sharing-agent.so +0 -0
  33. /package/bin/{darwin/resources → resources}/android/armeabi-v7a/libscreen-sharing-agent.so +0 -0
  34. /package/bin/{darwin/resources → resources}/android/screen-sharing-agent.jar +0 -0
  35. /package/bin/{darwin/resources → resources}/android/x86_64/libscreen-sharing-agent.so +0 -0
@@ -628,6 +628,9 @@ var init_failure_codes = __esm({
628
628
  SIMULATOR_SERVER_READY_TIMEOUT: "SIMULATOR_SERVER_READY_TIMEOUT",
629
629
  SIMULATOR_SERVER_PROCESS_ERROR: "SIMULATOR_SERVER_PROCESS_ERROR",
630
630
  SIMULATOR_SERVER_TERMINATED: "SIMULATOR_SERVER_TERMINATED",
631
+ SIMULATOR_COMMAND_REJECTED: "SIMULATOR_COMMAND_REJECTED",
632
+ SIMULATOR_COMMAND_ACK_TIMEOUT: "SIMULATOR_COMMAND_ACK_TIMEOUT",
633
+ SIMULATOR_COMMAND_TRANSPORT_FAILED: "SIMULATOR_COMMAND_TRANSPORT_FAILED",
631
634
  AX_QUERY_TIMEOUT: "AX_QUERY_TIMEOUT",
632
635
  AX_DAEMON_READY_TIMEOUT: "AX_DAEMON_READY_TIMEOUT",
633
636
  AX_DAEMON_EXITED_BEFORE_READY: "AX_DAEMON_EXITED_BEFORE_READY",
@@ -76339,15 +76342,28 @@ async function emulatorSupportsFlag(flag, options = {}) {
76339
76342
  const cached3 = emulatorFlagSupportCache.get(cacheKey3);
76340
76343
  if (cached3 !== void 0) return cached3;
76341
76344
  let output;
76345
+ let failure = null;
76342
76346
  try {
76343
76347
  const { stdout, stderr } = await execFileAsync9(emulatorPath, ["-help"], {
76344
76348
  timeout: options.timeoutMs ?? 1e4,
76349
+ // Same hazard runAdb guards against: a SIGTERM-immune child (emulator
76350
+ // wedged in Qt/graphics init) leaves execFile unsettled forever, and
76351
+ // this await has no deadline of its own.
76352
+ killSignal: "SIGKILL",
76345
76353
  maxBuffer: 8 * 1024 * 1024
76346
76354
  });
76347
76355
  output = stdout + stderr;
76348
76356
  } catch (err) {
76349
76357
  const e = err;
76350
- output = (e.stdout ?? "") + (e.stderr ?? "");
76358
+ output = e.killed ? "" : (e.stdout ?? "") + (e.stderr ?? "");
76359
+ failure = err instanceof Error ? err.message : String(err);
76360
+ }
76361
+ if (output === "") {
76362
+ process.stderr.write(
76363
+ `[argent] \`${emulatorPath} -help\` produced no complete listing ` + (failure ? `(${failure})` : "but exited successfully") + `; assuming "${flag}" unsupported for this boot and retrying on the next one
76364
+ `
76365
+ );
76366
+ return false;
76351
76367
  }
76352
76368
  const supported = output.includes(flag);
76353
76369
  emulatorFlagSupportCache.set(cacheKey3, supported);
@@ -76557,7 +76573,10 @@ async function listAvds() {
76557
76573
  const emulatorPath = await resolveAndroidBinary("emulator");
76558
76574
  if (!emulatorPath) return [];
76559
76575
  try {
76560
- const { stdout } = await execFileAsync9(emulatorPath, ["-list-avds"], { timeout: 5e3 });
76576
+ const { stdout } = await execFileAsync9(emulatorPath, ["-list-avds"], {
76577
+ timeout: 5e3,
76578
+ killSignal: "SIGKILL"
76579
+ });
76561
76580
  return stdout.split("\n").map((l) => l.trim()).filter((l) => l && AVD_NAME_PATTERN.test(l)).map((name) => ({ name }));
76562
76581
  } catch {
76563
76582
  return [];
@@ -76575,6 +76594,7 @@ async function checkSnapshotLoadable(avdName, snapshotName = "default_boot", opt
76575
76594
  ];
76576
76595
  const { stdout } = await execFileAsync9(emulatorPath, args, {
76577
76596
  timeout: options.timeoutMs ?? 1e4,
76597
+ killSignal: "SIGKILL",
76578
76598
  maxBuffer: 4 * 1024 * 1024
76579
76599
  });
76580
76600
  const tail = stdout.split("\n").slice(-6).join("\n");
@@ -89978,6 +89998,10 @@ var FLAG_REGISTRY = [
89978
89998
  name: "disable-auto-screenshot",
89979
89999
  description: "Disable the automatic screenshot captured after interaction tools."
89980
90000
  },
90001
+ {
90002
+ name: "disable-auto-describe",
90003
+ description: "Disable the accessibility element tree appended after interaction tools."
90004
+ },
89981
90005
  {
89982
90006
  name: "argent-lens",
89983
90007
  description: "Argent Lens \u2014 the propose_variant / await_user_selection tools and the Electron preview window for staging UI design variants and letting a human pick among them. Off by default while the feature is in development."
@@ -90278,6 +90302,14 @@ var CONFIG_SCHEMA = [
90278
90302
  // drained/reset, not just the file rewritten.
90279
90303
  manageCommand: "argent telemetry"
90280
90304
  },
90305
+ {
90306
+ key: "allowlist.enabled",
90307
+ description: "Whether `argent update` re-applies editor auto-approve allowlist rules. Unset (the default) keeps the current behavior: update refreshes the rules for editors that already have argent configured. Set to `false` to keep update from touching editor allowlists. `false` in either scope wins, so a committed project opt-out holds for every teammate.",
90308
+ scopes: ["project", "global"],
90309
+ parse: asBoolean,
90310
+ merge: "prioritize-restrictive",
90311
+ example: "false"
90312
+ },
90281
90313
  {
90282
90314
  key: "lens.agent",
90283
90315
  description: "Coding-agent id remembered by `argent lens` to skip the picker.",
@@ -93869,7 +93901,7 @@ var _CI_VENDOR_COUNT_FOR_TEST = vendors_default.length;
93869
93901
  var SESSION_ID = (0, import_node_crypto3.randomUUID)();
93870
93902
  function readCliVersion() {
93871
93903
  if (true) {
93872
- return "0.22.2-next.0";
93904
+ return "0.22.2-next.10";
93873
93905
  }
93874
93906
  return "0.0.0";
93875
93907
  }
@@ -94237,7 +94269,10 @@ function simulatorServerBinaryPath() {
94237
94269
  }
94238
94270
  return p;
94239
94271
  }
94240
- function simulatorServerBinaryDir() {
94272
+ function simulatorServerRunDir() {
94273
+ if (fs7.existsSync(path8.join(BIN_DIR, "resources", "android"))) {
94274
+ return BIN_DIR;
94275
+ }
94241
94276
  return platformBinDir();
94242
94277
  }
94243
94278
  function requireBinIn(dir, name) {
@@ -95381,13 +95416,56 @@ var import_express = __toESM(require_express2());
95381
95416
 
95382
95417
  // ../tool-server/src/blueprints/simulator-server.ts
95383
95418
  var import_node_child_process10 = require("node:child_process");
95384
- var readline2 = __toESM(require("node:readline"));
95419
+ var readline = __toESM(require("node:readline"));
95385
95420
  init_src();
95386
95421
 
95387
95422
  // ../tool-server/src/blueprints/ax-service.ts
95388
95423
  var net = __toESM(require("node:net"));
95389
95424
  var fs13 = __toESM(require("node:fs"));
95390
- var readline = __toESM(require("node:readline"));
95425
+
95426
+ // ../tool-server/src/utils/ndjson-socket.ts
95427
+ var PREVIEW_CHARS = 80;
95428
+ function preview(raw) {
95429
+ const printable = raw.replace(/[\x00-\x1f\x7f\u2028\u2029]/g, "\xB7");
95430
+ return printable.length > PREVIEW_CHARS ? `${printable.slice(0, PREVIEW_CHARS)}\u2026` : printable;
95431
+ }
95432
+ function reportDroppedFrameToStderr(tag3) {
95433
+ return ({ bytes, preview: preview2 }) => {
95434
+ process.stderr.write(`[${tag3}] dropped unparseable frame (${bytes} bytes): ${preview2}
95435
+ `);
95436
+ };
95437
+ }
95438
+ function attachNdjsonReader(socket, handlers) {
95439
+ socket.setEncoding("utf8");
95440
+ let buf = "";
95441
+ const deliver = (raw) => {
95442
+ if (raw.length === 0 || raw === "\r") return;
95443
+ let msg;
95444
+ try {
95445
+ msg = JSON.parse(raw);
95446
+ } catch {
95447
+ handlers.onDropped({ bytes: Buffer.byteLength(raw, "utf8"), preview: preview(raw) });
95448
+ return;
95449
+ }
95450
+ handlers.onMessage(msg);
95451
+ };
95452
+ socket.on("data", (chunk) => {
95453
+ buf += typeof chunk === "string" ? chunk : chunk.toString("utf8");
95454
+ let nl;
95455
+ while ((nl = buf.indexOf("\n")) !== -1) {
95456
+ const raw = buf.slice(0, nl);
95457
+ buf = buf.slice(nl + 1);
95458
+ deliver(raw);
95459
+ }
95460
+ });
95461
+ socket.on("end", () => {
95462
+ const rest = buf;
95463
+ buf = "";
95464
+ if (rest.length > 0) deliver(rest);
95465
+ });
95466
+ }
95467
+
95468
+ // ../tool-server/src/blueprints/ax-service.ts
95391
95469
  init_src();
95392
95470
 
95393
95471
  // ../tool-server/src/utils/ios-host.ts
@@ -95686,9 +95764,8 @@ async function simctlShutdown(udid) {
95686
95764
  await run(["simctl", "shutdown", stripRemotePrefix(udid)]);
95687
95765
  }
95688
95766
  async function simctlBootstatus(udid, opts) {
95689
- const args = ["simctl", "bootstatus"];
95767
+ const args = ["simctl", "bootstatus", stripRemotePrefix(udid)];
95690
95768
  if (opts?.boot) args.push("-b");
95691
- args.push(stripRemotePrefix(udid));
95692
95769
  await run(args, { timeoutMs: 5 * 6e4 });
95693
95770
  }
95694
95771
  async function simctlLaunch(udid, bundleId, args = []) {
@@ -96223,37 +96300,33 @@ var axServiceBlueprint = {
96223
96300
  daemonSocket.destroy();
96224
96301
  }
96225
96302
  daemonSocket = socket;
96226
- const rl = readline.createInterface({ input: socket });
96227
- rl.on("line", (raw) => {
96228
- let msg;
96229
- try {
96230
- msg = JSON.parse(raw);
96231
- } catch {
96232
- return;
96233
- }
96234
- if (typeof msg.id !== "number") return;
96235
- const pending = pendingRpc.get(msg.id);
96236
- if (!pending) return;
96237
- pendingRpc.delete(msg.id);
96238
- clearTimeout(pending.timer);
96239
- if (msg.error !== void 0 && msg.error !== null) {
96240
- pending.reject(
96241
- new FailureError(
96242
- typeof msg.error === "string" ? msg.error : JSON.stringify(msg.error),
96243
- {
96244
- error_code: FAILURE_CODES.AX_QUERY_FAILED,
96245
- failure_stage: "ax_service_query_rpc",
96246
- failure_area: "tool_server",
96247
- error_kind: "unknown"
96248
- }
96249
- )
96250
- );
96251
- } else {
96252
- pending.resolve(msg.result);
96303
+ attachNdjsonReader(socket, {
96304
+ onDropped: reportDroppedFrameToStderr(`ax-service ${udid.slice(0, 8)}`),
96305
+ onMessage: (parsed) => {
96306
+ const msg = parsed;
96307
+ if (typeof msg.id !== "number") return;
96308
+ const pending = pendingRpc.get(msg.id);
96309
+ if (!pending) return;
96310
+ pendingRpc.delete(msg.id);
96311
+ clearTimeout(pending.timer);
96312
+ if (msg.error !== void 0 && msg.error !== null) {
96313
+ pending.reject(
96314
+ new FailureError(
96315
+ typeof msg.error === "string" ? msg.error : JSON.stringify(msg.error),
96316
+ {
96317
+ error_code: FAILURE_CODES.AX_QUERY_FAILED,
96318
+ failure_stage: "ax_service_query_rpc",
96319
+ failure_area: "tool_server",
96320
+ error_kind: "unknown"
96321
+ }
96322
+ )
96323
+ );
96324
+ } else {
96325
+ pending.resolve(msg.result);
96326
+ }
96253
96327
  }
96254
96328
  });
96255
96329
  socket.on("close", () => {
96256
- rl.close();
96257
96330
  if (daemonSocket === socket) {
96258
96331
  daemonSocket = null;
96259
96332
  if (!disposed) {
@@ -106263,33 +106336,127 @@ var DEFAULT_SCREENSHOT_SCALE = 0.25;
106263
106336
  var NO_IMAGE_ERROR = /no image to export/i;
106264
106337
  var FIRST_FRAME_WAIT_MS = 6e3;
106265
106338
  var FIRST_FRAME_POLL_MS = 250;
106339
+ var COMMAND_ACK_TIMEOUT_MS = 5e3;
106266
106340
  var connections = /* @__PURE__ */ new Map();
106267
106341
  var cmdId = 0;
106268
- function getOrCreateWs(api) {
106342
+ function failAllPending(conn, makeError) {
106343
+ const entries = [...conn.pending.values()];
106344
+ conn.pending.clear();
106345
+ for (const entry of entries) entry.settle(makeError(entry.cmd));
106346
+ }
106347
+ function transportError(cmd, apiUrl, detail) {
106348
+ return new FailureError(
106349
+ `simulator-server did not accept the '${cmd}' command: ${detail}. The command was NOT delivered to the device. Check that the simulator is still booted and the simulator-server for ${apiUrl} is running.`,
106350
+ {
106351
+ error_code: FAILURE_CODES.SIMULATOR_COMMAND_TRANSPORT_FAILED,
106352
+ failure_stage: "simulator_command_transport",
106353
+ failure_area: "tool_server",
106354
+ error_kind: "network",
106355
+ network_failure: "connection_reset",
106356
+ failure_command: "simulator_server"
106357
+ }
106358
+ );
106359
+ }
106360
+ function getOrCreateConnection(api) {
106269
106361
  const key2 = api.apiUrl;
106270
106362
  const existing = connections.get(key2);
106271
- if (existing && (existing.readyState === wrapper_default.OPEN || existing.readyState === wrapper_default.CONNECTING)) {
106363
+ if (existing && (existing.ws.readyState === wrapper_default.OPEN || existing.ws.readyState === wrapper_default.CONNECTING)) {
106272
106364
  return existing;
106273
106365
  }
106274
106366
  const { host } = new URL(api.apiUrl);
106275
106367
  const ws = new wrapper_default(`ws://${host}/ws`);
106276
- ws.on("error", () => connections.delete(key2));
106277
- ws.on("close", () => connections.delete(key2));
106278
- connections.set(key2, ws);
106279
- return ws;
106368
+ const conn = { ws, pending: /* @__PURE__ */ new Map() };
106369
+ ws.on("message", (data) => {
106370
+ const text = Buffer.isBuffer(data) ? data.toString() : Array.isArray(data) ? Buffer.concat(data).toString() : Buffer.from(data).toString();
106371
+ let ack;
106372
+ try {
106373
+ ack = JSON.parse(text);
106374
+ } catch {
106375
+ return;
106376
+ }
106377
+ if (ack.status !== "ok" && ack.status !== "error") return;
106378
+ if (ack.id != null && !conn.pending.has(ack.id)) return;
106379
+ const id = ack.id ?? conn.pending.keys().next().value;
106380
+ if (id == null) return;
106381
+ const entry = conn.pending.get(id);
106382
+ if (entry == null) return;
106383
+ conn.pending.delete(id);
106384
+ if (ack.status === "ok") {
106385
+ entry.settle();
106386
+ return;
106387
+ }
106388
+ entry.settle(
106389
+ new FailureError(
106390
+ `simulator-server rejected the '${entry.cmd}' command: ${ack.message ?? "unknown error"}. The command was NOT delivered to the device.`,
106391
+ {
106392
+ error_code: FAILURE_CODES.SIMULATOR_COMMAND_REJECTED,
106393
+ failure_stage: "simulator_command_rejected",
106394
+ failure_area: "tool_server",
106395
+ error_kind: "unknown",
106396
+ failure_command: "simulator_server"
106397
+ }
106398
+ )
106399
+ );
106400
+ });
106401
+ ws.on("error", (err) => {
106402
+ connections.delete(key2);
106403
+ failAllPending(conn, (cmd) => transportError(cmd, key2, err.message));
106404
+ });
106405
+ ws.on("close", () => {
106406
+ connections.delete(key2);
106407
+ failAllPending(conn, (cmd) => transportError(cmd, key2, "the connection closed"));
106408
+ });
106409
+ connections.set(key2, conn);
106410
+ return conn;
106280
106411
  }
106281
106412
  function sendCommand(api, cmd) {
106282
106413
  if (api.transport) {
106283
106414
  routeViaTransport(api.transport, cmd);
106284
- return;
106285
- }
106286
- const ws = getOrCreateWs(api);
106287
- const payload = JSON.stringify({ id: String(++cmdId), ...cmd });
106288
- if (ws.readyState === wrapper_default.OPEN) {
106289
- ws.send(payload);
106290
- } else {
106291
- ws.once("open", () => ws.send(payload));
106415
+ return Promise.resolve();
106292
106416
  }
106417
+ const conn = getOrCreateConnection(api);
106418
+ const id = String(++cmdId);
106419
+ const cmdName = typeof cmd.cmd === "string" ? cmd.cmd : "unknown";
106420
+ const payload = JSON.stringify({ id, ...cmd });
106421
+ return new Promise((resolve12, reject) => {
106422
+ let done = false;
106423
+ const settle = (err) => {
106424
+ if (done) return;
106425
+ done = true;
106426
+ clearTimeout(timer);
106427
+ conn.pending.delete(id);
106428
+ if (err) reject(err);
106429
+ else resolve12();
106430
+ };
106431
+ const timer = setTimeout(
106432
+ () => settleAndDropConnection(
106433
+ new FailureError(
106434
+ `simulator-server did not acknowledge the '${cmdName}' command within ${COMMAND_ACK_TIMEOUT_MS}ms. The command may not have reached the device \u2014 the simulator may be wedged or the simulator-server unresponsive.`,
106435
+ {
106436
+ error_code: FAILURE_CODES.SIMULATOR_COMMAND_ACK_TIMEOUT,
106437
+ failure_stage: "simulator_command_ack",
106438
+ failure_area: "tool_server",
106439
+ error_kind: "timeout",
106440
+ network_failure: "timeout",
106441
+ failure_command: "simulator_server"
106442
+ }
106443
+ )
106444
+ ),
106445
+ COMMAND_ACK_TIMEOUT_MS
106446
+ );
106447
+ timer.unref?.();
106448
+ const settleAndDropConnection = (err) => {
106449
+ settle(err);
106450
+ connections.delete(api.apiUrl);
106451
+ conn.ws.close();
106452
+ };
106453
+ conn.pending.set(id, { settle, cmd: cmdName });
106454
+ const write = () => conn.ws.send(payload, (err) => {
106455
+ if (err) settle(transportError(cmdName, api.apiUrl, err.message));
106456
+ });
106457
+ if (conn.ws.readyState === wrapper_default.OPEN) write();
106458
+ else conn.ws.once("open", write);
106459
+ });
106293
106460
  }
106294
106461
  function setPointerVisible(api, show, signal) {
106295
106462
  return pointerPost(api, { show }, signal);
@@ -106517,8 +106684,8 @@ function simulatorServerRef(device) {
106517
106684
  }
106518
106685
  var getPaths = () => {
106519
106686
  const BINARY_PATH = simulatorServerBinaryPath();
106520
- const BINARY_DIR = simulatorServerBinaryDir();
106521
- return { BINARY_PATH, BINARY_DIR };
106687
+ const RUN_DIR = simulatorServerRunDir();
106688
+ return { BINARY_PATH, RUN_DIR };
106522
106689
  };
106523
106690
  var READY_TIMEOUT_MS = 3e4;
106524
106691
  async function buildRemoteInstance(device) {
@@ -106562,12 +106729,12 @@ async function spawnSimulatorServerProcess(udid, subcommand) {
106562
106729
  throw new Error(`Refusing to start simulator-server for unsafe device id "${udid}".`);
106563
106730
  }
106564
106731
  const deviceSet = subcommand === "ios" ? await deviceSetForUdid(udid) : null;
106565
- const { BINARY_PATH, BINARY_DIR } = getPaths();
106732
+ const { BINARY_PATH, RUN_DIR } = getPaths();
106566
106733
  return new Promise((resolve12, reject) => {
106567
106734
  const args = [subcommand, "--id", udid];
106568
106735
  if (deviceSet) args.push("--device-set", deviceSet);
106569
106736
  const proc = (0, import_node_child_process10.spawn)(BINARY_PATH, args, {
106570
- cwd: BINARY_DIR,
106737
+ cwd: RUN_DIR,
106571
106738
  stdio: ["pipe", "pipe", "pipe"]
106572
106739
  });
106573
106740
  let apiUrl = null;
@@ -106575,7 +106742,7 @@ async function spawnSimulatorServerProcess(udid, subcommand) {
106575
106742
  let settled = false;
106576
106743
  const STREAM_GRACE_MS = 500;
106577
106744
  let apiReadyTimer = null;
106578
- const rl = readline2.createInterface({ input: proc.stdout });
106745
+ const rl = readline.createInterface({ input: proc.stdout });
106579
106746
  const settle = (fn, cleanup) => {
106580
106747
  if (settled) return;
106581
106748
  settled = true;
@@ -109223,7 +109390,6 @@ var variantProposalStore = new VariantProposalStore();
109223
109390
  // ../tool-server/src/blueprints/native-devtools.ts
109224
109391
  var net2 = __toESM(require("node:net"));
109225
109392
  var fs18 = __toESM(require("node:fs"));
109226
- var readline3 = __toESM(require("node:readline"));
109227
109393
  init_src();
109228
109394
  var NATIVE_DEVTOOLS_NAMESPACE = "NativeDevtools";
109229
109395
  function isInjectableBundleId(bundleId) {
@@ -109490,67 +109656,63 @@ var nativeDevtoolsBlueprint = {
109490
109656
  }
109491
109657
  const server = net2.createServer((socket) => {
109492
109658
  let bundleId = null;
109493
- const rl = readline3.createInterface({ input: socket });
109494
- rl.on("line", (raw) => {
109495
- let msg;
109496
- try {
109497
- msg = JSON.parse(raw);
109498
- } catch {
109499
- return;
109500
- }
109501
- if (bundleId === null) {
109502
- if (msg.type !== "Control") return;
109503
- bundleId = msg.payload.bundleId;
109504
- const existing = connections2.get(bundleId);
109505
- if (existing) {
109506
- existing.socket.destroy();
109507
- }
109508
- connections2.set(bundleId, { socket, networkLog: [] });
109509
- if (activatedBundleIds.has(bundleId)) {
109510
- socket.write(
109511
- JSON.stringify({
109512
- type: "Control",
109513
- payload: { command: "activateNetworkInspection" }
109514
- }) + "\n"
109515
- );
109659
+ attachNdjsonReader(socket, {
109660
+ onDropped: reportDroppedFrameToStderr(`native-devtools ${udid.slice(0, 8)}`),
109661
+ onMessage: (parsed) => {
109662
+ const msg = parsed;
109663
+ if (bundleId === null) {
109664
+ if (msg.type !== "Control") return;
109665
+ bundleId = msg.payload.bundleId;
109666
+ const existing = connections2.get(bundleId);
109667
+ if (existing) {
109668
+ existing.socket.destroy();
109669
+ }
109670
+ connections2.set(bundleId, { socket, networkLog: [] });
109671
+ if (activatedBundleIds.has(bundleId)) {
109672
+ socket.write(
109673
+ JSON.stringify({
109674
+ type: "Control",
109675
+ payload: { command: "activateNetworkInspection" }
109676
+ }) + "\n"
109677
+ );
109678
+ }
109679
+ return;
109516
109680
  }
109517
- return;
109518
- }
109519
- if (msg.type === "CDP") {
109520
- const p = msg.payload;
109521
- if (p.method && p.id === void 0) {
109522
- const conn = connections2.get(bundleId);
109523
- if (conn) {
109524
- if (conn.networkLog.length >= MAX_LOG_ENTRIES) {
109525
- conn.networkLog.shift();
109681
+ if (msg.type === "CDP") {
109682
+ const p = msg.payload;
109683
+ if (p.method && p.id === void 0) {
109684
+ const conn = connections2.get(bundleId);
109685
+ if (conn) {
109686
+ if (conn.networkLog.length >= MAX_LOG_ENTRIES) {
109687
+ conn.networkLog.shift();
109688
+ }
109689
+ conn.networkLog.push({
109690
+ method: p.method,
109691
+ params: p.params,
109692
+ timestamp: Date.now()
109693
+ });
109526
109694
  }
109527
- conn.networkLog.push({
109528
- method: p.method,
109529
- params: p.params,
109530
- timestamp: Date.now()
109531
- });
109532
109695
  }
109533
109696
  }
109534
- }
109535
- if (msg.type === "ViewInspector") {
109536
- const p = msg.payload;
109537
- const pending = pendingRpc.get(p.id);
109538
- if (!pending) return;
109539
- pendingRpc.delete(p.id);
109540
- if (p.error) {
109541
- pending.reject(
109542
- new FailureError(p.error.message, {
109543
- error_code: FAILURE_CODES.NATIVE_DEVTOOLS_RPC_ERROR,
109544
- failure_stage: "native_devtools_rpc_response",
109545
- failure_area: "tool_server",
109546
- error_kind: "subprocess"
109547
- })
109548
- );
109549
- } else pending.resolve(p.result);
109697
+ if (msg.type === "ViewInspector") {
109698
+ const p = msg.payload;
109699
+ const pending = pendingRpc.get(p.id);
109700
+ if (!pending) return;
109701
+ pendingRpc.delete(p.id);
109702
+ if (p.error) {
109703
+ pending.reject(
109704
+ new FailureError(p.error.message, {
109705
+ error_code: FAILURE_CODES.NATIVE_DEVTOOLS_RPC_ERROR,
109706
+ failure_stage: "native_devtools_rpc_response",
109707
+ failure_area: "tool_server",
109708
+ error_kind: "subprocess"
109709
+ })
109710
+ );
109711
+ } else pending.resolve(p.result);
109712
+ }
109550
109713
  }
109551
109714
  });
109552
109715
  socket.on("close", () => {
109553
- rl.close();
109554
109716
  if (bundleId !== null) {
109555
109717
  if (connections2.get(bundleId)?.socket === socket) {
109556
109718
  connections2.delete(bundleId);
@@ -109994,22 +110156,28 @@ async function describeIos(registry2, device, params, options = {}) {
109994
110156
  if (isTvOs) {
109995
110157
  return { tree: emptyTree(), source: "ax-service", hint: TVOS_HINT };
109996
110158
  }
109997
- let tree;
110159
+ let tree = emptyTree();
109998
110160
  let degraded;
109999
110161
  let resolverHint;
110162
+ let readFailureHint;
110163
+ let axApi;
110000
110164
  try {
110001
110165
  const axRef = axServiceRef(device);
110002
- const axApi = await registry2.resolveService(axRef.urn, axRef.options);
110003
- const response = await axApi.describe();
110004
- tree = adaptAXDescribeToDescribeResult(response);
110166
+ axApi = await registry2.resolveService(axRef.urn, axRef.options);
110005
110167
  degraded = axApi.degraded;
110006
110168
  } catch (err) {
110007
- tree = emptyTree();
110008
110169
  resolverHint = tcpArtifactHint(err);
110009
110170
  degraded = resolverHint === void 0;
110010
110171
  }
110172
+ if (axApi) {
110173
+ try {
110174
+ tree = adaptAXDescribeToDescribeResult(await axApi.describe());
110175
+ } catch (err) {
110176
+ readFailureHint = `The accessibility read failed (${errMsg(err)}).`;
110177
+ }
110178
+ }
110011
110179
  const degradedHint = !degraded ? void 0 : tree.children.length === 0 ? DEGRADED_BLIND_HINT : DEGRADED_STANDING_HINT;
110012
- const hint = resolverHint ?? degradedHint;
110180
+ const hint = resolverHint ?? (readFailureHint ? degradedHint ? `${degradedHint}. ${readFailureHint}` : readFailureHint : degradedHint);
110013
110181
  if (tree.children.length > 0) {
110014
110182
  return { tree, source: "ax-service", hint };
110015
110183
  }
@@ -110501,7 +110669,7 @@ function parseUiAutomatorDump(rawOutput, screenW, screenH, options = {}) {
110501
110669
 
110502
110670
  // ../tool-server/src/blueprints/android-devtools.ts
110503
110671
  var import_node_child_process13 = require("node:child_process");
110504
- var readline5 = __toESM(require("node:readline"));
110672
+ var readline2 = __toESM(require("node:readline"));
110505
110673
  init_src();
110506
110674
 
110507
110675
  // ../native-devtools-android/src/index.ts
@@ -110898,7 +111066,6 @@ async function ensureAndroidDevtoolsInstalled(serial) {
110898
111066
 
110899
111067
  // ../tool-server/src/utils/android-devtools-client.ts
110900
111068
  var net3 = __toESM(require("node:net"));
110901
- var readline4 = __toESM(require("node:readline"));
110902
111069
  init_src();
110903
111070
  var DEFAULT_RPC_TIMEOUT_MS = 5e3;
110904
111071
  var LONG_RPC_TIMEOUT_MS = 15e3;
@@ -110932,36 +111099,32 @@ function connectAndroidDevtoolsClient(localPort, onTerminated) {
110932
111099
  onTerminated(error52);
110933
111100
  };
110934
111101
  socket.once("connect", () => {
110935
- const rl = readline4.createInterface({ input: socket });
110936
- rl.on("line", (line) => {
110937
- if (!line.trim()) return;
110938
- let parsed;
110939
- try {
110940
- parsed = JSON.parse(line);
110941
- } catch {
110942
- return;
110943
- }
110944
- if (typeof parsed.id !== "number") return;
110945
- const req = pending.get(parsed.id);
110946
- if (!req) return;
110947
- pending.delete(parsed.id);
110948
- clearTimeout(req.timer);
110949
- if (parsed.error) {
110950
- const message = parsed.error.message ?? "Unknown helper error";
110951
- const type = parsed.error.type ?? "HelperError";
110952
- req.reject(
110953
- new FailureError(`${type}: ${message}`, {
110954
- error_code: FAILURE_CODES.ANDROID_DEVTOOLS_RPC_ERROR,
110955
- failure_stage: "android_devtools_rpc_response",
110956
- failure_area: "tool_server",
110957
- error_kind: "subprocess"
110958
- })
110959
- );
110960
- } else {
110961
- req.resolve(parsed.result);
111102
+ attachNdjsonReader(socket, {
111103
+ onDropped: reportDroppedFrameToStderr("android-devtools"),
111104
+ onMessage: (raw) => {
111105
+ const parsed = raw;
111106
+ if (typeof parsed.id !== "number") return;
111107
+ const req = pending.get(parsed.id);
111108
+ if (!req) return;
111109
+ pending.delete(parsed.id);
111110
+ clearTimeout(req.timer);
111111
+ if (parsed.error) {
111112
+ const message = parsed.error.message ?? "Unknown helper error";
111113
+ const type = parsed.error.type ?? "HelperError";
111114
+ req.reject(
111115
+ new FailureError(`${type}: ${message}`, {
111116
+ error_code: FAILURE_CODES.ANDROID_DEVTOOLS_RPC_ERROR,
111117
+ failure_stage: "android_devtools_rpc_response",
111118
+ failure_area: "tool_server",
111119
+ error_kind: "subprocess"
111120
+ })
111121
+ );
111122
+ } else {
111123
+ req.resolve(parsed.result);
111124
+ }
110962
111125
  }
110963
111126
  });
110964
- rl.on("close", () => cleanup());
111127
+ socket.on("close", () => cleanup());
110965
111128
  resolve12({
110966
111129
  request(method, params = {}) {
110967
111130
  const send = () => {
@@ -111076,7 +111239,7 @@ async function spawnHelper(serial) {
111076
111239
  cleanup?.();
111077
111240
  fn();
111078
111241
  };
111079
- const rl = readline5.createInterface({ input: proc.stdout });
111242
+ const rl = readline2.createInterface({ input: proc.stdout });
111080
111243
  rl.on("line", async (rawLine) => {
111081
111244
  const line = rawLine.trim();
111082
111245
  const portMatch = HELPER_PORT_MARKER.exec(line);
@@ -116837,13 +117000,7 @@ var zodSchema9 = external_exports.object({
116837
117000
  function bootTarget(params) {
116838
117001
  return params.udid ?? params.avdName ?? params.vvdImage ?? params.electronAppPath ?? "device";
116839
117002
  }
116840
- var LAUNCH_HARDENING_ARGS = [
116841
- "-no-boot-anim",
116842
- "-netfast",
116843
- "-crash-report-mode",
116844
- "never",
116845
- "-no-metrics"
116846
- ];
117003
+ var LAUNCH_HARDENING_ARGS = ["-no-boot-anim", "-netfast", "-no-metrics"];
116847
117004
  function launchHardeningArgs(sound) {
116848
117005
  return sound ? [...LAUNCH_HARDENING_ARGS] : ["-noaudio", ...LAUNCH_HARDENING_ARGS];
116849
117006
  }
@@ -117357,7 +117514,7 @@ async function bootAndroidImpl(params) {
117357
117514
  } else {
117358
117515
  const RENDERER_ARGS = ["-gpu", gpuMode, ...extraEmulatorArgs];
117359
117516
  const probe3 = await checkSnapshotLoadable(params.avdName, "default_boot", {
117360
- extraArgs: [...RENDERER_ARGS, ...hardeningArgs]
117517
+ extraArgs: [...RENDERER_ARGS, ...hardeningArgs, ...crashReportArgs]
117361
117518
  });
117362
117519
  if (!probe3.loadable) {
117363
117520
  hotBootFailureReason = `-check-snapshot-loadable: ${probe3.reason ?? "unknown"}`;
@@ -119392,7 +119549,7 @@ Before tapping, determine the correct coordinates by using discovery tools \u201
119392
119549
  const api = services.simulatorServer;
119393
119550
  for (let i = 1; i <= clickCount; i++) {
119394
119551
  if (i > 1) await sleep2(MULTI_TAP_GAP_MS);
119395
- sendCommand(api, {
119552
+ await sendCommand(api, {
119396
119553
  cmd: "touch",
119397
119554
  type: "Down",
119398
119555
  x: params.x,
@@ -119401,7 +119558,7 @@ Before tapping, determine the correct coordinates by using discovery tools \u201
119401
119558
  second_y: null
119402
119559
  });
119403
119560
  await sleep2(TAP_HOLD_MS);
119404
- sendCommand(api, {
119561
+ await sendCommand(api, {
119405
119562
  cmd: "touch",
119406
119563
  type: "Up",
119407
119564
  x: params.x,
@@ -119417,18 +119574,39 @@ Before tapping, determine the correct coordinates by using discovery tools \u201
119417
119574
  // ../tool-server/src/tools/gesture-swipe/index.ts
119418
119575
  init_zod();
119419
119576
  var sleep3 = (ms) => new Promise((r) => setTimeout(r, ms));
119420
- var SETTLE_EASE_EXPONENT = 3;
119577
+ var MOMENTUM_FREE_EASE_EXPONENT = 3;
119578
+ var DEFAULT_DURATION_MS = 300;
119579
+ var MOMENTUM_FREE_MIN_DURATION_MS = 150;
119580
+ var MAX_DURATION_MS = 1e4;
119421
119581
  var zodSchema17 = external_exports.object({
119422
119582
  udid: external_exports.string().describe("Target device id from `list-devices` (iOS UDID or Android serial)."),
119423
119583
  fromX: external_exports.number().describe("Start x: normalized 0.0\u20131.0 (not pixels; same as tap)"),
119424
119584
  fromY: external_exports.number().describe("Start y: normalized 0.0\u20131.0 (not pixels; same as tap)"),
119425
119585
  toX: external_exports.number().describe("End x: normalized 0.0\u20131.0 (not pixels; same as tap)"),
119426
119586
  toY: external_exports.number().describe("End y: normalized 0.0\u20131.0 (not pixels; same as tap)"),
119427
- durationMs: external_exports.number().optional().describe("Total gesture duration in milliseconds (default 300)"),
119428
- settle: external_exports.boolean().optional().describe(
119429
- "Momentum-free swipe: decelerate into the end point (ease-out) so the OS reads ~0 release velocity and applies little to no fling. Use for scroll-to-element loops; default false (a natural flinging swipe)."
119587
+ durationMs: external_exports.number().max(MAX_DURATION_MS, {
119588
+ 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.`
119589
+ }).optional().describe(
119590
+ `Total gesture duration in milliseconds (default 300, at most ${MAX_DURATION_MS} - the gesture holds a finger down for exactly this long)`
119591
+ ),
119592
+ momentum: external_exports.boolean().optional().describe(
119593
+ `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.`
119594
+ ),
119595
+ // `momentum`'s shipped spelling, with the opposite polarity. Declared so this
119596
+ // non-strict object refuses it instead of stripping it and flinging - the exact
119597
+ // inverse of the gesture the caller asked for.
119598
+ settle: external_exports.never({
119599
+ 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)"
119600
+ }).optional().describe(
119601
+ "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
119602
  )
119431
- });
119603
+ }).refine(
119604
+ (p) => p.momentum !== false || (p.durationMs ?? DEFAULT_DURATION_MS) >= MOMENTUM_FREE_MIN_DURATION_MS,
119605
+ {
119606
+ 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.`,
119607
+ path: ["durationMs"]
119608
+ }
119609
+ );
119432
119610
  var capability9 = {
119433
119611
  apple: { simulator: true, device: true },
119434
119612
  appleRemote: { simulator: true },
@@ -119441,11 +119619,13 @@ var gestureSwipeTool = {
119441
119619
  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
119620
  failedMsg: ({ failureSignal: failureSignal2 }) => `Failed to swipe: ${failureSignal2.error_code}`
119443
119621
  },
119622
+ // The bounds are spelled out rather than interpolated: extract-tools scans this
119623
+ // description statically, so a `${}` in it drops the tool out of the scan.
119444
119624
  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
119625
  Generates interpolated Move events for a natural feel (~60fps).
119446
119626
  Swipe up (fromY > toY) to scroll content down.
119447
119627
  Use when you need to scroll a list, dismiss a modal, drag an element, or navigate between pages. Not supported on Chromium \u2014 use gesture-scroll there instead.
119448
- Pass settle:true for a momentum-free swipe that lands exactly where the finger lifts (no fling), when you need a deterministic scroll distance. Returns { swiped: true, timestampMs }. Fails if the simulator-server / emulator backend is not reachable for the given device.`,
119628
+ 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
119629
  alwaysLoad: true,
119450
119630
  searchHint: "swipe scroll drag pan gesture device simulator emulator touch move",
119451
119631
  zodSchema: zodSchema17,
@@ -119453,19 +119633,52 @@ Pass settle:true for a momentum-free swipe that lands exactly where the finger l
119453
119633
  services: (params) => ({
119454
119634
  simulatorServer: simulatorServerRef(resolveDevice(params.udid))
119455
119635
  }),
119456
- async execute(services, params) {
119457
- const duration3 = params.durationMs ?? 300;
119458
- const settle = params.settle ?? false;
119636
+ async execute(services, params, ctx) {
119637
+ const duration3 = params.durationMs ?? DEFAULT_DURATION_MS;
119638
+ const momentumFree = params.momentum === false;
119459
119639
  const timestampMs = Date.now();
119460
119640
  const api = services.simulatorServer;
119461
119641
  const steps = Math.max(1, Math.round(duration3 / 16));
119642
+ let lastX = 0;
119643
+ let lastY = 0;
119462
119644
  for (let i = 0; i <= steps; i++) {
119645
+ if (ctx?.signal?.aborted) {
119646
+ const err = new Error(
119647
+ `gesture-swipe aborted - cancelled mid-gesture after ${i} of ${steps + 1} frames`
119648
+ );
119649
+ err.name = "AbortError";
119650
+ if (i > 0) {
119651
+ try {
119652
+ await sendCommand(api, {
119653
+ cmd: "touch",
119654
+ type: "Up",
119655
+ x: lastX,
119656
+ y: lastY,
119657
+ second_x: null,
119658
+ second_y: null
119659
+ });
119660
+ } catch (liftErr) {
119661
+ err.cause = liftErr;
119662
+ }
119663
+ }
119664
+ throw err;
119665
+ }
119463
119666
  const t = i / steps;
119464
- const progress = settle ? 1 - Math.pow(1 - t, SETTLE_EASE_EXPONENT) : t;
119667
+ const progress = momentumFree ? 1 - Math.pow(1 - t, MOMENTUM_FREE_EASE_EXPONENT) : t;
119465
119668
  const x = params.fromX + (params.toX - params.fromX) * progress;
119466
119669
  const y = params.fromY + (params.toY - params.fromY) * progress;
119467
119670
  const type = i === 0 ? "Down" : i === steps ? "Up" : "Move";
119468
- sendCommand(api, {
119671
+ if (type === "Up") {
119672
+ await sendCommand(api, {
119673
+ cmd: "touch",
119674
+ type: "Move",
119675
+ x,
119676
+ y,
119677
+ second_x: null,
119678
+ second_y: null
119679
+ });
119680
+ }
119681
+ await sendCommand(api, {
119469
119682
  cmd: "touch",
119470
119683
  type,
119471
119684
  x,
@@ -119473,6 +119686,8 @@ Pass settle:true for a momentum-free swipe that lands exactly where the finger l
119473
119686
  second_x: null,
119474
119687
  second_y: null
119475
119688
  });
119689
+ lastX = x;
119690
+ lastY = y;
119476
119691
  if (i < steps) await sleep3(16);
119477
119692
  }
119478
119693
  return { swiped: true, timestampMs };
@@ -119548,13 +119763,30 @@ Returns { scrolled: true, timestampMs }. Fails if the Chromium CDP session is no
119548
119763
  // ../tool-server/src/tools/gesture-drag/index.ts
119549
119764
  init_zod();
119550
119765
  var sleep5 = (ms) => new Promise((r) => setTimeout(r, ms));
119766
+ var MOMENTUM_FREE_EASE_EXPONENT2 = 3;
119767
+ var MOMENTUM_FREE_MIN_STEPS = 8;
119768
+ var MAX_DURATION_MS2 = 1e4;
119551
119769
  var zodSchema19 = external_exports.object({
119552
119770
  udid: external_exports.string().describe("Target Chromium device id from `list-devices` (chromium-cdp-<port>)."),
119553
119771
  fromX: external_exports.number().describe("Press x: normalized 0.0\u20131.0 (fraction of window width, not pixels)."),
119554
119772
  fromY: external_exports.number().describe("Press y: normalized 0.0\u20131.0 (fraction of window height, not pixels)."),
119555
119773
  toX: external_exports.number().describe("Release x: normalized 0.0\u20131.0 (not pixels; same space as tap)."),
119556
119774
  toY: external_exports.number().describe("Release y: normalized 0.0\u20131.0 (not pixels; same space as tap)."),
119557
- durationMs: external_exports.number().optional().describe("Total drag duration in milliseconds (default 300), interpolated at ~60fps.")
119775
+ durationMs: external_exports.number().max(MAX_DURATION_MS2, {
119776
+ 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.`
119777
+ }).optional().describe(
119778
+ `Total drag duration in milliseconds (default 300, at most ${MAX_DURATION_MS2} - the button stays down for exactly this long), interpolated at ~60fps.`
119779
+ ),
119780
+ momentum: external_exports.boolean().optional().describe(
119781
+ "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."
119782
+ ),
119783
+ // `momentum`'s earlier spelling, with the opposite polarity. Declared so this
119784
+ // non-strict object refuses it instead of stripping it and running the default.
119785
+ settle: external_exports.never({
119786
+ 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)"
119787
+ }).optional().describe(
119788
+ "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."
119789
+ )
119558
119790
  });
119559
119791
  var capability11 = {
119560
119792
  chromium: { app: true }
@@ -119566,9 +119798,9 @@ var gestureDragTool = {
119566
119798
  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
119799
  failedMsg: ({ failureSignal: failureSignal2 }) => `Failed to drag: ${failureSignal2.error_code}`
119568
119800
  },
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.
119801
+ 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
119802
  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.`,
119803
+ 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
119804
  alwaysLoad: true,
119573
119805
  searchHint: "drag drop slider mouse press move release chromium select",
119574
119806
  zodSchema: zodSchema19,
@@ -119576,15 +119808,49 @@ Returns { dragged: true, timestampMs }. Fails if the Chromium CDP session is not
119576
119808
  services: (params) => ({
119577
119809
  chromium: chromiumCdpRef(resolveDevice(params.udid))
119578
119810
  }),
119579
- async execute(services, params) {
119811
+ async execute(services, params, ctx) {
119580
119812
  const timestampMs = Date.now();
119581
119813
  const chromium = services.chromium;
119582
119814
  await assertChromiumWindowVisible(chromium, "drag", "chromium_drag_window_hidden");
119583
119815
  const vp = chromium.getViewport();
119584
- const startPx = { x: params.fromX * vp.width, y: params.fromY * vp.height };
119585
- const endPx = { x: params.toX * vp.width, y: params.toY * vp.height };
119816
+ const clampPx2 = (px, size) => Math.min(Math.max(px, 0), size - 1);
119817
+ const startPx = {
119818
+ x: clampPx2(params.fromX * vp.width, vp.width),
119819
+ y: clampPx2(params.fromY * vp.height, vp.height)
119820
+ };
119821
+ const endPx = {
119822
+ x: clampPx2(params.toX * vp.width, vp.width),
119823
+ y: clampPx2(params.toY * vp.height, vp.height)
119824
+ };
119586
119825
  const durationMs = params.durationMs ?? 300;
119587
- const steps = Math.max(2, Math.round(durationMs / 16));
119826
+ const momentumFree = params.momentum === false;
119827
+ const steps = Math.max(momentumFree ? MOMENTUM_FREE_MIN_STEPS : 2, Math.round(durationMs / 16));
119828
+ const frameMs = durationMs / steps;
119829
+ const t0 = Date.now();
119830
+ let lastX = startPx.x;
119831
+ let lastY = startPx.y;
119832
+ const abortError = (frame) => {
119833
+ const err = new Error(
119834
+ `gesture-drag aborted - cancelled mid-drag after ${frame} of ${steps + 1} frames`
119835
+ );
119836
+ err.name = "AbortError";
119837
+ return err;
119838
+ };
119839
+ const releaseAndAbort = async (frame) => {
119840
+ const err = abortError(frame);
119841
+ try {
119842
+ await chromium.dispatchMouseEvent({
119843
+ type: "mouseReleased",
119844
+ x: lastX,
119845
+ y: lastY,
119846
+ clickCount: 1
119847
+ });
119848
+ } catch (releaseErr) {
119849
+ err.cause = releaseErr;
119850
+ }
119851
+ throw err;
119852
+ };
119853
+ if (ctx?.signal?.aborted) throw abortError(0);
119588
119854
  await chromium.dispatchMouseEvent({
119589
119855
  type: "mousePressed",
119590
119856
  x: startPx.x,
@@ -119592,15 +119858,19 @@ Returns { dragged: true, timestampMs }. Fails if the Chromium CDP session is not
119592
119858
  clickCount: 1
119593
119859
  });
119594
119860
  for (let i = 1; i < steps; i++) {
119861
+ if (ctx?.signal?.aborted) await releaseAndAbort(i);
119862
+ await sleep5(Math.max(0, t0 + i * frameMs - Date.now()));
119595
119863
  const t = i / steps;
119596
- await chromium.dispatchMouseEvent({
119597
- type: "mouseMoved",
119598
- x: startPx.x + (endPx.x - startPx.x) * t,
119599
- y: startPx.y + (endPx.y - startPx.y) * t,
119600
- button: "left"
119601
- });
119602
- await sleep5(16);
119603
- }
119864
+ const progress = momentumFree ? 1 - Math.pow(1 - t, MOMENTUM_FREE_EASE_EXPONENT2) : t;
119865
+ const x = startPx.x + (endPx.x - startPx.x) * progress;
119866
+ const y = startPx.y + (endPx.y - startPx.y) * progress;
119867
+ await chromium.dispatchMouseEvent({ type: "mouseMoved", x, y, button: "left" });
119868
+ lastX = x;
119869
+ lastY = y;
119870
+ }
119871
+ if (ctx?.signal?.aborted) await releaseAndAbort(steps);
119872
+ await sleep5(Math.max(0, t0 + durationMs - Date.now()));
119873
+ if (ctx?.signal?.aborted) await releaseAndAbort(steps);
119604
119874
  await chromium.dispatchMouseEvent({
119605
119875
  type: "mouseReleased",
119606
119876
  x: endPx.x,
@@ -119651,7 +119921,7 @@ function interpolateEvents(events, steps) {
119651
119921
  return result;
119652
119922
  }
119653
119923
  function sendTouchEvent(api, type, x, y, x2, y2) {
119654
- sendCommand(api, {
119924
+ return sendCommand(api, {
119655
119925
  cmd: "touch",
119656
119926
  type,
119657
119927
  x,
@@ -119722,7 +119992,7 @@ Example pinch-to-zoom (with interpolate:10 for smoothness):
119722
119992
  const events = params.interpolate && params.interpolate > 0 ? interpolateEvents(params.events, params.interpolate) : params.events;
119723
119993
  for (const event2 of events) {
119724
119994
  await sleep6(event2.delayMs ?? 16);
119725
- sendCommand(api, {
119995
+ await sendCommand(api, {
119726
119996
  cmd: "touch",
119727
119997
  type: event2.type,
119728
119998
  x: event2.x,
@@ -119805,7 +120075,7 @@ Use when you need to zoom in or out on a map, image, or zoomable view. Returns {
119805
120075
  const y2 = cy + halfDist * sinA;
119806
120076
  const type = i === 0 ? "Down" : i === steps ? "Up" : "Move";
119807
120077
  if (i === 0) timestampMs = Date.now();
119808
- sendTouchEvent(api, type, x1, y1, x2, y2);
120078
+ await sendTouchEvent(api, type, x1, y1, x2, y2);
119809
120079
  if (i < steps) await sleep(16);
119810
120080
  }
119811
120081
  return { pinched: true, timestampMs };
@@ -119883,11 +120153,17 @@ Size the orbit with radius, or with radiusX and radiusY together (the pair overr
119883
120153
  let lastY2 = 0;
119884
120154
  for (let i = 0; i <= steps; i++) {
119885
120155
  if (ctx?.signal?.aborted) {
119886
- if (i > 0) sendTouchEvent(api, "Up", lastX1, lastY1, lastX2, lastY2);
119887
120156
  const err = new Error(
119888
120157
  `gesture-rotate aborted \u2014 cancelled mid-gesture after ${i} of ${steps + 1} frames`
119889
120158
  );
119890
120159
  err.name = "AbortError";
120160
+ if (i > 0) {
120161
+ try {
120162
+ await sendTouchEvent(api, "Up", lastX1, lastY1, lastX2, lastY2);
120163
+ } catch (liftErr) {
120164
+ err.cause = liftErr;
120165
+ }
120166
+ }
119891
120167
  throw err;
119892
120168
  }
119893
120169
  const t = i / steps;
@@ -119899,7 +120175,7 @@ Size the orbit with radius, or with radiusX and radiusY together (the pair overr
119899
120175
  const y2 = params.centerY - radiusY * Math.sin(angleRad);
119900
120176
  const type = i === 0 ? "Down" : i === steps ? "Up" : "Move";
119901
120177
  if (i === 0) timestampMs = Date.now();
119902
- sendTouchEvent(api, type, x1, y1, x2, y2);
120178
+ await sendTouchEvent(api, type, x1, y1, x2, y2);
119903
120179
  lastX1 = x1;
119904
120180
  lastY1 = y1;
119905
120181
  lastX2 = x2;
@@ -119968,13 +120244,13 @@ Fails if the device backend is not reachable \u2014 the simulator-server for iOS
119968
120244
  return { pressed: params.button };
119969
120245
  }
119970
120246
  const api = services.simulatorServer;
119971
- sendCommand(api, {
120247
+ await sendCommand(api, {
119972
120248
  cmd: "button",
119973
120249
  direction: "Down",
119974
120250
  button: params.button
119975
120251
  });
119976
120252
  await sleep7(50);
119977
- sendCommand(api, { cmd: "button", direction: "Up", button: params.button });
120253
+ await sendCommand(api, { cmd: "button", direction: "Up", button: params.button });
119978
120254
  return { pressed: params.button };
119979
120255
  }
119980
120256
  };
@@ -120785,7 +121061,7 @@ Returns { orientation }. Fails if the target device is not booted.`,
120785
121061
  }),
120786
121062
  async execute(services, params) {
120787
121063
  const api = services.simulatorServer;
120788
- sendCommand(api, { cmd: "rotate", direction: params.orientation });
121064
+ await sendCommand(api, { cmd: "rotate", direction: params.orientation });
120789
121065
  return { orientation: params.orientation };
120790
121066
  }
120791
121067
  };
@@ -127820,9 +128096,9 @@ a prior tap), use individual tool calls instead.
127820
128096
  Allowed tools and their args (udid is auto-injected, do NOT include it in args):
127821
128097
 
127822
128098
  gesture-tap: { x: number, y: number, clickCount?: number } [ios/android/chromium]
127823
- gesture-swipe: { fromX: number, fromY: number, toX: number, toY: number, durationMs?: number } [ios/android]
128099
+ gesture-swipe: { fromX: number, fromY: number, toX: number, toY: number, durationMs?: number, momentum?: boolean } [ios/android]
127824
128100
  gesture-scroll: { x: number, y: number, deltaX?: number, deltaY?: number, durationMs?: number } [chromium only]
127825
- gesture-drag: { fromX: number, fromY: number, toX: number, toY: number, durationMs?: number } [chromium only]
128101
+ gesture-drag: { fromX: number, fromY: number, toX: number, toY: number, durationMs?: number, momentum?: boolean } [chromium only]
127826
128102
  gesture-custom: { events: [{ type: "Down"|"Move"|"Up", x: number, y: number, x2?: number, y2?: number, delayMs?: number }], interpolate?: number } [ios/android]
127827
128103
  gesture-pinch: { centerX: number, centerY: number, startDistance: number, endDistance: number, endCenterX?: number, endCenterY?: number, angle?: number, durationMs?: number } [ios/android]
127828
128104
  gesture-rotate: { centerX: number, centerY: number, radius?: number, radiusX?: number, radiusY?: number, startAngle: number, endAngle: number, durationMs?: number } [ios/android]
@@ -140883,6 +141159,12 @@ function chromiumLaunchSpec(launch) {
140883
141159
  return typeof c === "string" ? { path: c } : { path: c.path, args: c.args };
140884
141160
  }
140885
141161
  function selectorToYaml(sel) {
141162
+ const unknown2 = Object.keys(sel).filter((key2) => !WRITABLE_SELECTOR_KEYS.includes(key2));
141163
+ if (unknown2.length > 0) {
141164
+ throw new Error(
141165
+ `Cannot serialize flow selector: ${describeUnknownKeys(unknown2, WRITABLE_SELECTOR_KEYS)} - allowed keys: ${WRITABLE_SELECTOR_KEYS.join(", ")}.`
141166
+ );
141167
+ }
140886
141168
  if (sel.text !== void 0 && sel.textMatches !== void 0) {
140887
141169
  throw new Error(
140888
141170
  '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 +141287,79 @@ function targetToYaml(step) {
141005
141287
  }
141006
141288
  return { x: step.x, y: step.y };
141007
141289
  }
141290
+ function swipeTargetToYaml(target, label) {
141291
+ const yaml = targetToYaml(target);
141292
+ if (typeof yaml !== "string" && "x" in yaml && !Object.keys(target).every((key2) => key2 === "x" || key2 === "y")) {
141293
+ throw new Error(`Cannot serialize flow ${label}: a coordinate target takes only { x, y }`);
141294
+ }
141295
+ return yaml;
141296
+ }
141297
+ var SWIPE_MIN_TRAVEL = 0.03;
141298
+ var SWIPE_MIN_DURATION_MS = 150;
141299
+ var SWIPE_MAX_DURATION_MS = 1e4;
141300
+ var LONG_PRESS_MAX_DURATION_MS = SWIPE_MAX_DURATION_MS;
141301
+ function swipeByToYaml(by) {
141302
+ const keys = Object.keys(by);
141303
+ if (keys.some((key2) => key2 !== "x" && key2 !== "y")) {
141304
+ throw new Error("Cannot serialize flow swipe.by: accepts only x and y");
141305
+ }
141306
+ const axes = ["x", "y"].filter((axis) => by[axis] !== void 0);
141307
+ if (axes.length === 0) {
141308
+ throw new Error("Cannot serialize flow swipe.by: needs at least one of x or y");
141309
+ }
141310
+ const result = {};
141311
+ for (const axis of axes) {
141312
+ const value = by[axis];
141313
+ if (!Number.isFinite(value) || value === 0 || value < -1 || value > 1) {
141314
+ throw new Error(
141315
+ `Cannot serialize flow swipe.by.${axis}: must be a non-zero fraction of the screen between -1 and 1`
141316
+ );
141317
+ }
141318
+ result[axis] = value;
141319
+ }
141320
+ const magnitude = Math.hypot(result.x ?? 0, result.y ?? 0);
141321
+ if (magnitude < SWIPE_MIN_TRAVEL) {
141322
+ throw new Error(
141323
+ `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`
141324
+ );
141325
+ }
141326
+ return result;
141327
+ }
141328
+ function swipeByLabel(by) {
141329
+ return ["x", "y"].filter((axis) => by[axis] !== void 0).map((axis) => `${axis}=${by[axis]}`).join(", ");
141330
+ }
141331
+ function isPositiveMs(raw) {
141332
+ return typeof raw === "number" && Number.isFinite(raw) && raw > 0;
141333
+ }
141334
+ function positiveMsToYaml(value, label) {
141335
+ if (!isPositiveMs(value)) {
141336
+ throw new Error(`Cannot serialize flow ${label}: needs a positive number of milliseconds`);
141337
+ }
141338
+ return value;
141339
+ }
141340
+ function swipeDurationToYaml(value) {
141341
+ const duration3 = positiveMsToYaml(value, "swipe.duration");
141342
+ if (duration3 < SWIPE_MIN_DURATION_MS) {
141343
+ throw new Error(
141344
+ `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`
141345
+ );
141346
+ }
141347
+ if (duration3 > SWIPE_MAX_DURATION_MS) {
141348
+ throw new Error(
141349
+ `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`
141350
+ );
141351
+ }
141352
+ return duration3;
141353
+ }
141354
+ function longPressDurationToYaml(value) {
141355
+ const duration3 = positiveMsToYaml(value, "long-press.duration");
141356
+ if (duration3 > LONG_PRESS_MAX_DURATION_MS) {
141357
+ throw new Error(
141358
+ `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`
141359
+ );
141360
+ }
141361
+ return duration3;
141362
+ }
141008
141363
  function waitToYaml(condition, selector, expectedText, textMatch, timeoutMs) {
141009
141364
  const sel = selectorToYaml(selector);
141010
141365
  let body;
@@ -141022,7 +141377,7 @@ function waitToYaml(condition, selector, expectedText, textMatch, timeoutMs) {
141022
141377
  body = textWaitToYaml(sel, expectedText, textMatch);
141023
141378
  break;
141024
141379
  }
141025
- if (timeoutMs !== void 0) body.timeout = timeoutMs;
141380
+ if (timeoutMs !== void 0) body.timeout = positiveMsToYaml(timeoutMs, "await.timeout");
141026
141381
  return body;
141027
141382
  }
141028
141383
  function idleToYaml(step) {
@@ -141058,9 +141413,29 @@ function toYamlStep(step) {
141058
141413
  case "long-press": {
141059
141414
  const target = targetToYaml(step);
141060
141415
  return {
141061
- "long-press": step.duration !== void 0 ? { on: target, duration: step.duration } : target
141416
+ "long-press": step.duration !== void 0 ? { on: target, duration: longPressDurationToYaml(step.duration) } : target
141062
141417
  };
141063
141418
  }
141419
+ case "swipe": {
141420
+ const travels = ["direction", "to", "by"].filter((key2) => step[key2] !== void 0);
141421
+ if (travels.length !== 1) {
141422
+ throw new Error("Cannot serialize flow swipe: needs exactly one of direction, to, or by");
141423
+ }
141424
+ if (step.momentum !== void 0 && typeof step.momentum !== "boolean") {
141425
+ throw new Error("Cannot serialize flow swipe.momentum: must be true or false");
141426
+ }
141427
+ if (step.direction !== void 0 && step.from === void 0 && step.to === void 0 && step.by === void 0 && step.momentum !== false && step.duration === void 0) {
141428
+ return { swipe: step.direction };
141429
+ }
141430
+ const body = {};
141431
+ if (step.from !== void 0) body.from = swipeTargetToYaml(step.from, "swipe.from");
141432
+ if (step.direction !== void 0) body.direction = step.direction;
141433
+ if (step.to !== void 0) body.to = swipeTargetToYaml(step.to, "swipe.to");
141434
+ if (step.by !== void 0) body.by = swipeByToYaml(step.by);
141435
+ if (step.momentum === false) body.momentum = false;
141436
+ if (step.duration !== void 0) body.duration = swipeDurationToYaml(step.duration);
141437
+ return { swipe: body };
141438
+ }
141064
141439
  case "type": {
141065
141440
  const body = {
141066
141441
  into: selectorToYaml(step.into),
@@ -141153,6 +141528,12 @@ function badEntry(raw, detail) {
141153
141528
  error_kind: "validation"
141154
141529
  });
141155
141530
  }
141531
+ function parsePositiveMs(raw, entry, label, example) {
141532
+ if (!isPositiveMs(raw)) {
141533
+ badEntry(entry, `${label} needs a positive number of milliseconds (e.g. \`${example}\`)`);
141534
+ }
141535
+ return raw;
141536
+ }
141156
141537
  function validatePattern(raw, pattern, where) {
141157
141538
  try {
141158
141539
  new RegExp(pattern);
@@ -141216,6 +141597,17 @@ var SELECTOR_KEYS = [
141216
141597
  "any",
141217
141598
  ...SELECTOR_RELATIONS
141218
141599
  ];
141600
+ var WRITABLE_SELECTOR_KEYS = Object.keys({
141601
+ text: true,
141602
+ textMatches: true,
141603
+ identifier: true,
141604
+ role: true,
141605
+ any: true,
141606
+ loose: true,
141607
+ within: true,
141608
+ after: true,
141609
+ next: true
141610
+ });
141219
141611
  var MAX_SELECTOR_SCOPES = 6;
141220
141612
  function parseSelector(raw, where, budget = { scopes: MAX_SELECTOR_SCOPES }) {
141221
141613
  if (budget.scopes < 0) {
@@ -141346,7 +141738,7 @@ function parseWaitFields(raw, kind) {
141346
141738
  "assert has no timeout \u2014 it is an immediate check; use `await` for a timed wait"
141347
141739
  );
141348
141740
  }
141349
- timeout = parseAwaitTimeout({ [kind]: b }, b.timeout);
141741
+ timeout = parsePositiveMs(b.timeout, { [kind]: b }, "await.timeout", "timeout: 10000");
141350
141742
  }
141351
141743
  rejectUnknownKeys(
141352
141744
  { [kind]: b },
@@ -141515,6 +141907,7 @@ var STEP_DIRECTIVE_KEYS = [
141515
141907
  "tool",
141516
141908
  "tap",
141517
141909
  "long-press",
141910
+ "swipe",
141518
141911
  "type",
141519
141912
  "await",
141520
141913
  "assert",
@@ -141613,13 +142006,19 @@ function parseLongPress(body, entry) {
141613
142006
  }
141614
142007
  const step = { kind: "long-press", ...parseTarget(obj.on, "long-press.on") };
141615
142008
  if (obj.duration !== void 0) {
141616
- if (typeof obj.duration !== "number" || !Number.isFinite(obj.duration) || obj.duration <= 0) {
142009
+ const duration3 = parsePositiveMs(
142010
+ obj.duration,
142011
+ entry,
142012
+ "long-press.duration",
142013
+ "duration: 1200"
142014
+ );
142015
+ if (duration3 > LONG_PRESS_MAX_DURATION_MS) {
141617
142016
  badEntry(
141618
142017
  entry,
141619
- "long-press.duration needs a positive number of milliseconds (e.g. `duration: 1200`)"
142018
+ `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
142019
  );
141621
142020
  }
141622
- step.duration = obj.duration;
142021
+ step.duration = duration3;
141623
142022
  }
141624
142023
  return step;
141625
142024
  }
@@ -141789,6 +142188,116 @@ function completeRunExtension(value) {
141789
142188
  const candidate = `${value}.yaml`;
141790
142189
  return FLOW_FILE_NAME_PATTERN.test(path32.posix.basename(candidate)) ? candidate : value;
141791
142190
  }
142191
+ var SWIPE_DIRECTIONS = ["up", "down", "left", "right"];
142192
+ var SWIPE_OPTION_KEYS = ["from", "direction", "to", "by", "momentum", "duration"];
142193
+ function parseSwipeBy(raw, entry) {
142194
+ if (raw === null || typeof raw !== "object") {
142195
+ badEntry(entry, "swipe.by needs { x } and/or { y } \u2014 signed 0\u20131 fractions of the screen");
142196
+ }
142197
+ const obj = raw;
142198
+ rejectUnknownKeys(entry, obj, ["x", "y"], "swipe.by");
142199
+ if (obj.x === void 0 && obj.y === void 0) {
142200
+ badEntry(entry, "swipe.by needs at least one of x, y");
142201
+ }
142202
+ const by = {};
142203
+ for (const axis of ["x", "y"]) {
142204
+ const v = obj[axis];
142205
+ if (v === void 0) continue;
142206
+ if (typeof v !== "number" || !Number.isFinite(v) || v === 0 || v < -1 || v > 1) {
142207
+ badEntry(
142208
+ entry,
142209
+ `swipe.by.${axis} must be a non-zero fraction of the screen between -1 and 1 (omit the axis instead of 0)`
142210
+ );
142211
+ }
142212
+ by[axis] = v;
142213
+ }
142214
+ const magnitude = Math.hypot(by.x ?? 0, by.y ?? 0);
142215
+ if (magnitude < SWIPE_MIN_TRAVEL) {
142216
+ badEntry(
142217
+ entry,
142218
+ `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`
142219
+ );
142220
+ }
142221
+ return by;
142222
+ }
142223
+ function parseSwipe(body, entry) {
142224
+ if (typeof body === "string") {
142225
+ if (!SWIPE_DIRECTIONS.includes(body)) {
142226
+ badEntry(
142227
+ entry,
142228
+ `swipe takes a direction (${SWIPE_DIRECTIONS.join(", ")}) \u2014 to anchor on an element use swipe: { from: <target>, direction: \u2026 }`
142229
+ );
142230
+ }
142231
+ return { kind: "swipe", direction: body };
142232
+ }
142233
+ if (body === null || typeof body !== "object") {
142234
+ badEntry(entry, `swipe needs a direction (${SWIPE_DIRECTIONS.join(", ")}) or an options map`);
142235
+ }
142236
+ const obj = body;
142237
+ if (hasSelectorField(obj)) {
142238
+ badEntry(
142239
+ entry,
142240
+ 'the swipe options form takes a nested target \u2014 e.g. swipe: { from: { text: "Card" }, direction: left }'
142241
+ );
142242
+ }
142243
+ if (obj.x !== void 0 || obj.y !== void 0) {
142244
+ badEntry(
142245
+ entry,
142246
+ "the swipe options form takes a nested point \u2014 e.g. swipe: { from: { x: 0.5, y: 0.5 }, direction: left }"
142247
+ );
142248
+ }
142249
+ if (obj.settle !== void 0) {
142250
+ badEntry(
142251
+ entry,
142252
+ "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)"
142253
+ );
142254
+ }
142255
+ rejectUnknownKeys(entry, obj, SWIPE_OPTION_KEYS, "swipe");
142256
+ const travels = ["direction", "to", "by"].filter((k) => obj[k] !== void 0);
142257
+ if (travels.length !== 1) {
142258
+ badEntry(entry, "swipe needs exactly one of `direction`, `to`, or `by`");
142259
+ }
142260
+ const step = { kind: "swipe" };
142261
+ if (obj.from !== void 0) step.from = parseTarget(obj.from, "swipe.from");
142262
+ switch (travels[0]) {
142263
+ case "direction": {
142264
+ if (typeof obj.direction !== "string" || !SWIPE_DIRECTIONS.includes(obj.direction)) {
142265
+ badEntry(entry, `swipe.direction must be one of ${SWIPE_DIRECTIONS.join(", ")}`);
142266
+ }
142267
+ step.direction = obj.direction;
142268
+ break;
142269
+ }
142270
+ case "to":
142271
+ step.to = parseTarget(obj.to, "swipe.to");
142272
+ break;
142273
+ case "by":
142274
+ step.by = parseSwipeBy(obj.by, entry);
142275
+ break;
142276
+ }
142277
+ if (obj.momentum !== void 0) {
142278
+ if (typeof obj.momentum !== "boolean") {
142279
+ badEntry(entry, "swipe.momentum must be true or false");
142280
+ }
142281
+ if (!obj.momentum) step.momentum = false;
142282
+ }
142283
+ if (obj.duration !== void 0) {
142284
+ const duration3 = parsePositiveMs(obj.duration, entry, "swipe.duration", "duration: 800");
142285
+ if (duration3 < SWIPE_MIN_DURATION_MS) {
142286
+ badEntry(
142287
+ entry,
142288
+ `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`
142289
+ );
142290
+ }
142291
+ if (duration3 > SWIPE_MAX_DURATION_MS) {
142292
+ badEntry(
142293
+ entry,
142294
+ `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`
142295
+ );
142296
+ }
142297
+ step.duration = duration3;
142298
+ }
142299
+ return step;
142300
+ }
141792
142301
  function fromYamlStep(raw, blockDepth = 0) {
141793
142302
  const entry = raw;
141794
142303
  if ("optional" in raw) {
@@ -141830,6 +142339,7 @@ function fromYamlStep(raw, blockDepth = 0) {
141830
142339
  if ("long-press" in raw) {
141831
142340
  return parseLongPress(raw["long-press"], raw);
141832
142341
  }
142342
+ if ("swipe" in raw) return parseSwipe(raw.swipe, raw);
141833
142343
  if ("type" in raw) {
141834
142344
  const body = raw.type;
141835
142345
  if (!body || typeof body !== "object") badEntry(raw, "type needs { into, text }");
@@ -142446,6 +142956,7 @@ function stepRequiresDevice(registry2, step) {
142446
142956
  case "launch":
142447
142957
  case "tap":
142448
142958
  case "long-press":
142959
+ case "swipe":
142449
142960
  case "type":
142450
142961
  case "await":
142451
142962
  case "assert":
@@ -143287,22 +143798,32 @@ async function settleTree(env, opts = {}) {
143287
143798
  if (!await sleepOrAbort(SETTLE_POLL_MS, env.signal)) return void 0;
143288
143799
  }
143289
143800
  }
143290
- async function waitForFrame(env, selector) {
143801
+ async function waitForFrames(env, selectors) {
143802
+ const pending = selectors.flatMap((selector, i) => selector ? [{ i, selector }] : []);
143803
+ if (pending.length === 0) return selectors.map(() => void 0);
143291
143804
  const deadline = Date.now() + DEFAULT_ACTION_TIMEOUT_MS;
143805
+ let unresolved = pending[0].selector;
143292
143806
  for (; ; ) {
143293
143807
  if (env.signal?.aborted) return "aborted";
143294
143808
  const tree = await settleTree(env);
143295
143809
  if (tree) {
143296
- const frame = flowSelectorToFrame(tree, selector);
143297
- if (frame) return frame;
143810
+ const frames = selectors.map((s) => s ? flowSelectorToFrame(tree, s) : void 0);
143811
+ const missing = pending.find(({ i }) => frames[i] === void 0);
143812
+ if (!missing) return frames;
143813
+ unresolved = missing.selector;
143298
143814
  } else if (env.signal?.aborted) {
143299
143815
  return "aborted";
143300
143816
  }
143301
- if (Date.now() >= deadline) return void 0;
143817
+ if (Date.now() >= deadline) return { unresolved };
143302
143818
  const sleepMs = Math.min(POLL_INTERVAL_MS, Math.max(0, deadline - Date.now()));
143303
143819
  if (!await sleepOrAbort(sleepMs, env.signal)) return "aborted";
143304
143820
  }
143305
143821
  }
143822
+ async function waitForFrame(env, selector) {
143823
+ const frames = await waitForFrames(env, [selector]);
143824
+ if (frames === "aborted") return "aborted";
143825
+ return Array.isArray(frames) ? frames[0] : void 0;
143826
+ }
143306
143827
  function framesOverlap(a, b) {
143307
143828
  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
143829
  }
@@ -143366,14 +143887,19 @@ async function scrollIncrement(env, direction, region) {
143366
143887
  to = { x: clamp014(cx + dist), y: cy };
143367
143888
  break;
143368
143889
  }
143369
- await invokeOnDevice(env, "gesture-swipe", {
143370
- fromX: cx,
143371
- fromY: cy,
143372
- toX: to.x,
143373
- toY: to.y,
143374
- settle: true,
143375
- durationMs: 600
143376
- });
143890
+ try {
143891
+ await invokeOnDevice(env, "gesture-swipe", {
143892
+ fromX: cx,
143893
+ fromY: cy,
143894
+ toX: to.x,
143895
+ toY: to.y,
143896
+ momentum: false,
143897
+ durationMs: 600
143898
+ });
143899
+ } catch (err) {
143900
+ if (env.signal?.aborted) return;
143901
+ throw err;
143902
+ }
143377
143903
  }
143378
143904
  async function scrollToVisible(env, target, direction, within) {
143379
143905
  let prevFp;
@@ -143407,7 +143933,7 @@ function offscreenHint(sel) {
143407
143933
  return `no visible element matched selector ${describeSelector(sel)} \u2014 if it is off-screen, add a scroll-to step before this one`;
143408
143934
  }
143409
143935
  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")) {
143936
+ 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
143937
  return {
143412
143938
  ok: false,
143413
143939
  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 +143950,8 @@ async function runDirective(env, step) {
143424
143950
  return runTap(env, step);
143425
143951
  case "long-press":
143426
143952
  return runLongPress(env, step);
143953
+ case "swipe":
143954
+ return runSwipe(env, step);
143427
143955
  case "type":
143428
143956
  return runType(env, step);
143429
143957
  case "await":
@@ -143471,10 +143999,16 @@ async function resolveTargetPoint(env, target) {
143471
143999
  }
143472
144000
  return { point: getDescribeTapPoint(frame) };
143473
144001
  }
144002
+ const point = targetPointFromFrame(target, void 0);
144003
+ if ("fail" in point) return point;
144004
+ const settle = await settleForGesture(env);
144005
+ if (settle.aborted) return { fail: ABORTED_OUTCOME };
144006
+ return { point, ...warned(settle) };
144007
+ }
144008
+ function targetPointFromFrame(target, frame) {
144009
+ if (frame) return getDescribeTapPoint(frame);
143474
144010
  if (typeof target.x === "number" && typeof target.y === "number") {
143475
- const settle = await settleForGesture(env);
143476
- if (settle.aborted) return { fail: ABORTED_OUTCOME };
143477
- return { point: { x: target.x, y: target.y }, ...warned(settle) };
144011
+ return { x: target.x, y: target.y };
143478
144012
  }
143479
144013
  return { fail: { ok: false, reason: "gesture needs a selector or x/y coordinates" } };
143480
144014
  }
@@ -143494,13 +144028,18 @@ async function runLongPress(env, step) {
143494
144028
  const point = resolved.point;
143495
144029
  const duration3 = step.duration ?? DEFAULT_LONG_PRESS_MS;
143496
144030
  if (env.device.platform === "chromium") {
143497
- await invokeOnDevice(env, "gesture-drag", {
143498
- fromX: point.x,
143499
- fromY: point.y,
143500
- toX: point.x,
143501
- toY: point.y,
143502
- durationMs: duration3
143503
- });
144031
+ try {
144032
+ await invokeOnDevice(env, "gesture-drag", {
144033
+ fromX: point.x,
144034
+ fromY: point.y,
144035
+ toX: point.x,
144036
+ toY: point.y,
144037
+ durationMs: duration3
144038
+ });
144039
+ } catch (err) {
144040
+ if (env.signal?.aborted) return ABORTED_OUTCOME;
144041
+ throw err;
144042
+ }
143504
144043
  } else {
143505
144044
  await invokeOnDevice(env, "gesture-custom", {
143506
144045
  events: [
@@ -143620,6 +144159,132 @@ async function runRotate(env, step) {
143620
144159
  }
143621
144160
  return { ok: true, ...warned(settle) };
143622
144161
  }
144162
+ var SWIPE_GEOMETRY = {
144163
+ left: { start: { x: 0.9, y: 0.5 }, axis: "x", end: 0.1 },
144164
+ right: { start: { x: 0.1, y: 0.5 }, axis: "x", end: 0.9 },
144165
+ down: { start: { x: 0.5, y: 0.2 }, axis: "y", end: 0.9 },
144166
+ up: { start: { x: 0.5, y: 0.5 }, axis: "y", end: 0.1 }
144167
+ };
144168
+ async function runSwipe(env, step) {
144169
+ const ends = [step.from, step.to];
144170
+ const selectors = ends.map((end2) => end2 && "selector" in end2 ? end2.selector : void 0);
144171
+ const frames = await waitForFrames(env, selectors);
144172
+ if (frames === "aborted") return ABORTED_OUTCOME;
144173
+ if (!Array.isArray(frames)) return { ok: false, reason: offscreenHint(frames.unresolved) };
144174
+ const [fromFrame, toFrame] = frames;
144175
+ let settle = {};
144176
+ if (selectors.every((selector) => selector === void 0)) {
144177
+ settle = await settleForGesture(env);
144178
+ if (settle.aborted) return ABORTED_OUTCOME;
144179
+ }
144180
+ let toPoint;
144181
+ if (step.to) {
144182
+ const p = targetPointFromFrame(step.to, toFrame);
144183
+ if ("fail" in p) return p.fail;
144184
+ toPoint = p;
144185
+ }
144186
+ let start2;
144187
+ if (step.from) {
144188
+ const p = targetPointFromFrame(step.from, fromFrame);
144189
+ if ("fail" in p) return p.fail;
144190
+ start2 = p;
144191
+ } else if (step.direction) {
144192
+ start2 = { ...SWIPE_GEOMETRY[step.direction].start };
144193
+ } else {
144194
+ start2 = { x: 0.5, y: 0.5 };
144195
+ }
144196
+ if (!Number.isFinite(start2.x) || start2.x < 0 || start2.x > 1 || !Number.isFinite(start2.y) || start2.y < 0 || start2.y > 1) {
144197
+ return {
144198
+ ok: false,
144199
+ reason: `swipe.from resolved outside the normalized screen: (${start2.x}, ${start2.y}); both coordinates must be between 0 and 1`
144200
+ };
144201
+ }
144202
+ let end;
144203
+ if (step.direction) {
144204
+ const g = SWIPE_GEOMETRY[step.direction];
144205
+ const startOnTravelAxis = start2[g.axis];
144206
+ const endOnTravelAxis = step.from ? clamp014(startOnTravelAxis + (g.end - g.start[g.axis])) : g.end;
144207
+ end = g.axis === "x" ? { x: endOnTravelAxis, y: start2.y } : { x: start2.x, y: endOnTravelAxis };
144208
+ const travel2 = Math.abs(endOnTravelAxis - startOnTravelAxis);
144209
+ if (travel2 < SWIPE_MIN_TRAVEL) {
144210
+ return {
144211
+ ok: false,
144212
+ 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`
144213
+ };
144214
+ }
144215
+ } else if (step.by) {
144216
+ const overflowAxis = ["x", "y"].find((axis) => {
144217
+ const d = step.by[axis];
144218
+ return d !== void 0 && (start2[axis] + d < 0 || start2[axis] + d > 1);
144219
+ });
144220
+ if (overflowAxis === void 0) {
144221
+ end = {
144222
+ x: step.by.x !== void 0 ? start2.x + step.by.x : start2.x,
144223
+ y: step.by.y !== void 0 ? start2.y + step.by.y : start2.y
144224
+ };
144225
+ } else if (step.from) {
144226
+ const requested = step.by[overflowAxis];
144227
+ const raw = start2[overflowAxis] + requested;
144228
+ return {
144229
+ ok: false,
144230
+ 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]`
144231
+ };
144232
+ } else {
144233
+ end = { x: start2.x, y: start2.y };
144234
+ for (const axis of ["x", "y"]) {
144235
+ const d = step.by[axis];
144236
+ if (d === void 0) {
144237
+ end[axis] = start2[axis];
144238
+ continue;
144239
+ }
144240
+ const s = start2[axis];
144241
+ const lo = Math.min(s, s + d);
144242
+ const hi = Math.max(s, s + d);
144243
+ const shift = lo < 0 ? -lo : hi > 1 ? 1 - hi : 0;
144244
+ start2[axis] = s + shift;
144245
+ end[axis] = s + d + shift;
144246
+ }
144247
+ }
144248
+ } else {
144249
+ end = toPoint;
144250
+ if (!Number.isFinite(end.x) || end.x < 0 || end.x > 1 || !Number.isFinite(end.y) || end.y < 0 || end.y > 1) {
144251
+ return {
144252
+ ok: false,
144253
+ reason: `swipe.to resolved outside the normalized screen: (${end.x}, ${end.y}); both coordinates must be between 0 and 1`
144254
+ };
144255
+ }
144256
+ if (Math.hypot(end.x - start2.x, end.y - start2.y) < SWIPE_MIN_TRAVEL) {
144257
+ return {
144258
+ ok: false,
144259
+ 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`
144260
+ };
144261
+ }
144262
+ }
144263
+ const travel = {
144264
+ fromX: start2.x,
144265
+ fromY: start2.y,
144266
+ toX: end.x,
144267
+ toY: end.y,
144268
+ ...step.duration !== void 0 ? { durationMs: step.duration } : {},
144269
+ ...step.momentum === false ? { momentum: false } : {}
144270
+ };
144271
+ try {
144272
+ await invokeOnDevice(
144273
+ env,
144274
+ env.device.platform === "chromium" ? "gesture-drag" : "gesture-swipe",
144275
+ travel
144276
+ );
144277
+ } catch (err) {
144278
+ if (env.signal?.aborted) return ABORTED_OUTCOME;
144279
+ throw err;
144280
+ }
144281
+ try {
144282
+ await settleTree(env);
144283
+ } catch {
144284
+ }
144285
+ if (env.signal?.aborted) return ABORTED_OUTCOME;
144286
+ return { ok: true, ...warned(settle) };
144287
+ }
143623
144288
  async function runType(env, step) {
143624
144289
  const frame = await waitForFrame(env, step.into);
143625
144290
  if (frame === "aborted") return ABORTED_OUTCOME;
@@ -143951,6 +144616,9 @@ function textConditionLabel(sel, expectedText, textMatch) {
143951
144616
  const expected = expectedText ?? "";
143952
144617
  return textMatch === "matches" ? `text ${selector} matches /${expected}/` : textMatch === "equals" ? `text ${selector} == ${JSON.stringify(expected)}` : `text ${selector} contains ${JSON.stringify(expected)}`;
143953
144618
  }
144619
+ function targetLabel(target) {
144620
+ return "selector" in target ? selectorLabel(target.selector) : `(${target.x}, ${target.y})`;
144621
+ }
143954
144622
  var zodSchema62 = external_exports.object({
143955
144623
  name: external_exports.string().describe("Name of the flow being recorded \u2014 the one passed to flow-start-recording."),
143956
144624
  project_root: external_exports.string().describe(
@@ -144078,11 +144746,23 @@ function summarizeStep(step, n) {
144078
144746
  return `${n}. run: ${step.flow}`;
144079
144747
  case "tap":
144080
144748
  case "long-press": {
144081
- const target = step.selector ? selectorLabel(step.selector) : `(${step.x}, ${step.y})`;
144749
+ const target = targetLabel(
144750
+ step.selector ? { selector: step.selector } : { x: step.x, y: step.y }
144751
+ );
144082
144752
  const times = step.kind === "tap" && step.times !== void 0 && step.times > 1 ? ` \xD7${step.times}` : "";
144083
144753
  const held = step.kind === "long-press" && step.duration !== void 0 ? ` for ${step.duration}ms` : "";
144084
144754
  return `${n}. ${step.kind}: ${target}${times}${held}`;
144085
144755
  }
144756
+ case "swipe": {
144757
+ const travel = step.direction ?? (step.by ? `by ${swipeByLabel(step.by)}` : `to ${targetLabel(step.to)}`);
144758
+ const from2 = step.from ? ` from ${targetLabel(step.from)}` : "";
144759
+ const options = [
144760
+ ...step.momentum === false ? ["momentum-free"] : [],
144761
+ ...step.duration !== void 0 ? [`${step.duration}ms`] : []
144762
+ ];
144763
+ const tail = options.length > 0 ? ` (${options.join(", ")})` : "";
144764
+ return `${n}. swipe: ${travel}${from2}${tail}`;
144765
+ }
144086
144766
  case "type":
144087
144767
  return `${n}. type: ${selectorLabel(step.into)} \u2190 "${step.text}"`;
144088
144768
  case "await":
@@ -144124,7 +144804,7 @@ var zodSchema63 = external_exports.object({
144124
144804
  "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
144805
  ),
144126
144806
  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.'
144807
+ '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
144808
  ),
144129
144809
  args: external_exports.string().optional().describe(
144130
144810
  `Tool arguments as a JSON string, e.g. '{"udid": "ABC", "x": 0.5, "y": 0.3}'. Omit for tools with no arguments.`
@@ -144378,6 +145058,9 @@ function isToolNotFound(err, command) {
144378
145058
  return err instanceof ToolNotFoundError && err.toolId === command;
144379
145059
  }
144380
145060
  function directiveCommandHint(command) {
145061
+ if (command === "swipe") {
145062
+ 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.`;
145063
+ }
144381
145064
  if (command === "echo") {
144382
145065
  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
145066
  }
@@ -147402,15 +148085,71 @@ function displayFlowName(params) {
147402
148085
  const stem = params.flow_path === void 0 ? void 0 : path36.basename(params.flow_path, ".yaml");
147403
148086
  return params.name || stem || params.flow_path || "(unspecified)";
147404
148087
  }
147405
- function* walkSteps(steps) {
147406
- for (const step of steps) {
147407
- yield step;
148088
+ function* walkSteps(steps, within = "") {
148089
+ for (const [i, step] of steps.entries()) {
148090
+ const where = `step ${i + 1}${within}`;
148091
+ yield { step, where };
147408
148092
  const inner = blockSteps(step);
147409
- if (inner) yield* walkSteps(inner);
148093
+ if (inner) yield* walkSteps(inner, ` of the ${step.kind}: block at ${where}`);
148094
+ }
148095
+ }
148096
+ function retiredKeyGuidance(prop) {
148097
+ const schema = prop;
148098
+ if (!schema?.not || Object.keys(schema.not).length > 0) return void 0;
148099
+ return (schema.description ?? "").replace(/^Retired:\s*/, "");
148100
+ }
148101
+ function toolArgProps(registry2, tool) {
148102
+ return registry2.getTool(tool)?.inputSchema?.properties;
148103
+ }
148104
+ function retiredArgIn(props, tool, args, where) {
148105
+ for (const key2 of Object.keys(args)) {
148106
+ const guidance = retiredKeyGuidance(props[key2]);
148107
+ if (guidance !== void 0) return { where, tool, key: key2, guidance };
147410
148108
  }
148109
+ return void 0;
148110
+ }
148111
+ function* nestedInvocations(props, args) {
148112
+ for (const [key2, value] of Object.entries(args)) {
148113
+ if (!Object.hasOwn(props, key2)) continue;
148114
+ const entries = Array.isArray(value) ? value : [value];
148115
+ for (const [i, entry] of entries.entries()) {
148116
+ const call = entry;
148117
+ if (typeof call?.tool !== "string") continue;
148118
+ if (typeof call.args !== "object" || call.args === null || Array.isArray(call.args)) continue;
148119
+ yield {
148120
+ tool: call.tool,
148121
+ args: call.args,
148122
+ at: Array.isArray(value) ? `step ${i + 1}` : `\`${key2}\``
148123
+ };
148124
+ }
148125
+ }
148126
+ }
148127
+ function findRetiredToolArg(registry2, steps) {
148128
+ for (const { step, where } of walkSteps(steps)) {
148129
+ if (step.kind !== "tool") continue;
148130
+ const props = toolArgProps(registry2, step.name);
148131
+ if (!props) continue;
148132
+ const direct = retiredArgIn(props, step.name, step.args, where);
148133
+ if (direct) return direct;
148134
+ for (const call of nestedInvocations(props, step.args)) {
148135
+ const nestedProps = toolArgProps(registry2, call.tool);
148136
+ if (!nestedProps) continue;
148137
+ const hit = retiredArgIn(
148138
+ nestedProps,
148139
+ call.tool,
148140
+ call.args,
148141
+ `${call.at} of the ${step.name} step at ${where}`
148142
+ );
148143
+ if (hit) return hit;
148144
+ }
148145
+ }
148146
+ return void 0;
148147
+ }
148148
+ function retiredArgReason(use) {
148149
+ return `${use.where} as written (echo included) passes ${use.tool}'s retired \`${use.key}\` key${use.guidance ? `: ${use.guidance}` : ""}`;
147411
148150
  }
147412
148151
  function assertUploadSelfContained(flow) {
147413
- for (const step of walkSteps(flow.steps)) {
148152
+ for (const { step } of walkSteps(flow.steps)) {
147414
148153
  if (step.kind === "run") {
147415
148154
  throw new FailureError(
147416
148155
  `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 +148183,9 @@ function createRunFlowTool(registry2) {
147444
148183
  failedMsg: ({ params, failureSignal: failureSignal2 }) => `Failed to run flow ${displayFlowName(params)}: ${failureSignal2.error_code}`
147445
148184
  },
147446
148185
  description: `Run a saved flow from the .argent/flows/ directory, or an explicit boundary-managed flow_path.
148186
+ Use when a scenario is already authored as YAML and the whole of it should replay in one call with a
148187
+ per-step verdict; reach for the individual gesture tools when nothing is authored yet, and for
148188
+ run-sequence when the steps are an ad-hoc list rather than a stored flow.
147447
148189
  Steps run in order: \`launch\` starts an app from scratch (terminate + relaunch) and waits until it is
147448
148190
  ready (on iOS it also pins later element lookups to that app rather than auto-detecting the frontmost
147449
148191
  one); \`tool\` calls dispatch through the registry (a raw \`tool\` step ends that iOS pin, so lookups
@@ -147458,6 +148200,11 @@ order), \`next: <selector>\` (CSS \`+\` \u2014 the nearest such follower, which
147458
148200
  non-matching neighbour rather than failing), plus \`any: true\` (CSS \`*\` \u2014 legal only WITH a scope and
147459
148201
  never beside text/id/role). Scopes nest to disambiguate \u2014 \`within: { id: card, within: { id: list } }\`
147460
148202
  reads "inside card inside list", each container's frame inside the next);
148203
+ \`swipe\` performs one finger flick (\`swipe: left\`, or \`swipe: { from?, direction|to|by, momentum?, duration? }\` \u2014
148204
+ direction is the FINGER's travel, the opposite sense of scroll-to's content direction; \`by: { x?, y? }\` \u2014 signed
148205
+ 0\u20131 screen fractions, combined length at least 0.03 (a diagonal clears it where neither axis does); duration in ms,
148206
+ default 300, minimum 150, maximum 10000; each bound is a parse error that rejects the file before any step runs;
148207
+ \`momentum: false\` lands exactly where the finger lifts instead of flinging);
147461
148208
  \`scroll-to\` scrolls (momentum-free) until a target is visible; \`pinch\` zooms
147462
148209
  (\`pinch: { on?, scale }\` \u2014 scale > 1 in, < 1 out; screen center when \`on\` is omitted); \`rotate\` is the
147463
148210
  two-finger rotation gesture (\`rotate: { on?, by }\` \u2014 degrees, + clockwise, within \xB13000\xB0; screen center
@@ -147474,7 +148221,7 @@ baseline (a missing baseline fails the step \u2014 set updateBaselines to adopt
147474
148221
  cropped element whose size drifted fails on dimensions); \`echo\` annotates; \`run\` executes another flow
147475
148222
  inline \u2014 a YAML path resolved against the directory of the flow file that references it (co-located
147476
148223
  runs only).
147477
- A selector-less gesture \u2014 a coordinate \`tap\`/\`long-press\`, or a \`pinch\`/\`rotate\` with no \`on\` \u2014 resolves
148224
+ A selector-less gesture \u2014 a coordinate \`tap\`/\`long-press\`/\`swipe\`, or a \`pinch\`/\`rotate\` with no \`on\` \u2014 resolves
147478
148225
  no frame out of the tree, so an unreadable tree source does NOT stop it the way it stops \`idle\`: it
147479
148226
  settles best-effort, dispatches anyway, and the step PASSES carrying a \`warning\` that quotes the source's
147480
148227
  own error. That green says the gesture was SENT, not that it landed. Restore the tree source (usually
@@ -147522,6 +148269,15 @@ Pass exactly one flow source: name for a saved flow under project_root, or flow_
147522
148269
  const flowsDir = path36.dirname(canonicalPath);
147523
148270
  const flow = parseFlow(await fs51.readFile(canonicalPath, "utf8"));
147524
148271
  if (viaUpload) assertUploadSelfContained(flow);
148272
+ const retiredArg = findRetiredToolArg(registry2, flow.steps);
148273
+ if (retiredArg) {
148274
+ throw new FailureError(`Flow "${flowName}" ${retiredArgReason(retiredArg)}`, {
148275
+ error_code: FAILURE_CODES.FLOW_FILE_INVALID,
148276
+ failure_stage: "flow_run_validate",
148277
+ failure_area: "tool_server",
148278
+ error_kind: "validation"
148279
+ });
148280
+ }
147525
148281
  const rootEntry = { canonical: canonicalPath, display: flowName };
147526
148282
  if (flow.executionPrerequisite && !pinnedToChromium(params.device)) {
147527
148283
  const leading = await leadingLaunch(flow, [rootEntry]);
@@ -147792,6 +148548,9 @@ function conditionLabel(cond, renderSelector) {
147792
148548
  }
147793
148549
  return `${cond.condition} ${sel}`;
147794
148550
  }
148551
+ function gestureTargetLabel(target) {
148552
+ return "selector" in target ? selectorLabel2(target.selector) : `(${target.x}, ${target.y})`;
148553
+ }
147795
148554
  function stepTarget(step) {
147796
148555
  switch (step.kind) {
147797
148556
  case "tap":
@@ -147799,6 +148558,19 @@ function stepTarget(step) {
147799
148558
  if (step.selector) return selectorLabel2(step.selector);
147800
148559
  if (step.x !== void 0 && step.y !== void 0) return `(${step.x}, ${step.y})`;
147801
148560
  return void 0;
148561
+ case "swipe": {
148562
+ let travel;
148563
+ if (step.direction !== void 0) {
148564
+ travel = step.direction;
148565
+ } else if (step.by !== void 0) {
148566
+ travel = `by ${swipeByLabel(step.by)}`;
148567
+ } else if (step.to !== void 0) {
148568
+ travel = `to ${gestureTargetLabel(step.to)}`;
148569
+ } else {
148570
+ return void 0;
148571
+ }
148572
+ return `${travel}${step.from ? ` from ${gestureTargetLabel(step.from)}` : ""}`;
148573
+ }
147802
148574
  case "type":
147803
148575
  return `into ${selectorLabel2(step.into)}`;
147804
148576
  case "await":
@@ -148061,6 +148833,8 @@ async function execRunStep(state3, step, scope) {
148061
148833
  } catch (err) {
148062
148834
  return fail(`could not load fragment "${target}": ${errMsg3(err)}`);
148063
148835
  }
148836
+ const retiredArg = findRetiredToolArg(state3.registry, fragment.steps);
148837
+ if (retiredArg) return fail(`fragment "${target}" ${retiredArgReason(retiredArg)}`);
148064
148838
  pushReport(state3, {
148065
148839
  index,
148066
148840
  kind: "run",
@@ -148094,6 +148868,7 @@ async function execLeafStep(state3, step, index, scope) {
148094
148868
  }
148095
148869
  case "tap":
148096
148870
  case "long-press":
148871
+ case "swipe":
148097
148872
  case "type":
148098
148873
  case "await":
148099
148874
  case "assert":
@@ -148221,6 +148996,9 @@ async function execLeafStep(state3, step, index, scope) {
148221
148996
  }
148222
148997
  return { ...base, status: "pass", tool: step.name, result, outputHint, args };
148223
148998
  } catch (err) {
148999
+ if (signal?.aborted) {
149000
+ return { ...base, status: "skip", tool: step.name, reason: ABORTED_OUTCOME.reason };
149001
+ }
148224
149002
  const reframed = describeNestedParamError(registry2, err, step.name, args, step.args ?? {});
148225
149003
  return { ...base, status: "error", tool: step.name, reason: reframed ?? errMsg3(err) };
148226
149004
  }
@@ -148400,10 +149178,13 @@ var flowReadPrerequisiteTool = {
148400
149178
  failedMsg: ({ failureSignal: failureSignal2 }) => `Failed to read flow prerequisite: ${failureSignal2.error_code}`
148401
149179
  },
148402
149180
  description: `Read the execution prerequisite of a flow without running it \u2014 a saved flow from the .argent/flows/ directory, or an explicit boundary-managed flow_path.
148403
- Returns the prerequisite description so you can verify the required state is met before calling flow-execute.
148404
- Use when you need to check what app/simulator state is required before executing a flow; pass the same flow
148405
- source (name or flow_path) you will pass to flow-execute, so the prerequisite you read is the contract of
148406
- the flow that will actually run.
149181
+ Returns { flow, executionPrerequisite }: the logical name, plus the precondition its author recorded
149182
+ verbatim. Empty when none was declared, which is always so for a self-contained scenario: one opening
149183
+ on a launch may declare no prerequisite, because it builds its own start state.
149184
+ Use when deciding whether the device already sits where a fragment expects it (correct app foregrounded,
149185
+ correct account, correct screen) before committing to a run, or when relaying that requirement to a human.
149186
+ Touches no device: nothing is launched, tapped, dispatched or torn down, and no simulator or emulator
149187
+ needs booting, so calling this costs nothing but a file read.
148407
149188
  Fails if the flow file does not exist.
148408
149189
  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
149190
  zodSchema: zodSchema66,
@@ -148911,7 +149692,7 @@ var updateArgentTool = {
148911
149692
  });
148912
149693
  child.unref();
148913
149694
  }, 2e3);
148914
- const targetLabel = effectiveTarget === "both" ? "global and project-local installs" : `${effectiveTarget} install`;
149695
+ const targetLabel2 = effectiveTarget === "both" ? "global and project-local installs" : `${effectiveTarget} install`;
148915
149696
  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
149697
  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
149698
  const versionInfo = targetsOnlyRunningInstall ? `(v${currentVersion} -> v${installableVersion}) ` : "";
@@ -148919,7 +149700,7 @@ var updateArgentTool = {
148919
149700
  const coversRunningInstall = targetsOnlyRunningInstall || effectiveTarget === "both";
148920
149701
  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
149702
  return {
148922
- message: `Argent update initiated ${versionInfo}for the ${targetLabel}.` + crossTargetNote + restartNote + `${otherHint}${bothDegradedNote}`
149703
+ message: `Argent update initiated ${versionInfo}for the ${targetLabel2}.` + crossTargetNote + restartNote + `${otherHint}${bothDegradedNote}`
148923
149704
  };
148924
149705
  }
148925
149706
  };