@swmansion/argent 0.29.1-next.13 → 0.29.1-next.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli-cmds.mjs CHANGED
@@ -9608,6 +9608,11 @@ var FAILURE_CODES = {
9608
9608
  // a device that is not foldable, no hinge route, a remote simulator.
9609
9609
  IOS_FOLD_FAILED: "IOS_FOLD_FAILED",
9610
9610
  IOS_FOLD_UNSUPPORTED: "IOS_FOLD_UNSUPPORTED",
9611
+ // The hinge of a foldable Android emulator: the console refused the move or
9612
+ // the guest never committed the state vs. nothing to fold — a phone profile,
9613
+ // a physical device.
9614
+ ANDROID_FOLD_FAILED: "ANDROID_FOLD_FAILED",
9615
+ ANDROID_FOLD_UNSUPPORTED: "ANDROID_FOLD_UNSUPPORTED",
9611
9616
  PASTE_CLIPBOARD_SET_FAILED: "PASTE_CLIPBOARD_SET_FAILED",
9612
9617
  PASTE_CLIPBOARD_UNSUPPORTED: "PASTE_CLIPBOARD_UNSUPPORTED",
9613
9618
  SETTINGS_PERMISSION_UNSUPPORTED: "SETTINGS_PERMISSION_UNSUPPORTED",
@@ -35301,7 +35306,7 @@ var _CI_VENDOR_COUNT_FOR_TEST = vendors_default.length;
35301
35306
  var SESSION_ID2 = randomUUID5();
35302
35307
  function readCliVersion() {
35303
35308
  if (true) {
35304
- return "0.29.1-next.13";
35309
+ return "0.29.1-next.15";
35305
35310
  }
35306
35311
  return "0.0.0";
35307
35312
  }
package/dist/client.js CHANGED
@@ -8811,6 +8811,11 @@ var FAILURE_CODES = {
8811
8811
  // a device that is not foldable, no hinge route, a remote simulator.
8812
8812
  IOS_FOLD_FAILED: "IOS_FOLD_FAILED",
8813
8813
  IOS_FOLD_UNSUPPORTED: "IOS_FOLD_UNSUPPORTED",
8814
+ // The hinge of a foldable Android emulator: the console refused the move or
8815
+ // the guest never committed the state vs. nothing to fold — a phone profile,
8816
+ // a physical device.
8817
+ ANDROID_FOLD_FAILED: "ANDROID_FOLD_FAILED",
8818
+ ANDROID_FOLD_UNSUPPORTED: "ANDROID_FOLD_UNSUPPORTED",
8814
8819
  PASTE_CLIPBOARD_SET_FAILED: "PASTE_CLIPBOARD_SET_FAILED",
8815
8820
  PASTE_CLIPBOARD_UNSUPPORTED: "PASTE_CLIPBOARD_UNSUPPORTED",
8816
8821
  SETTINGS_PERMISSION_UNSUPPORTED: "SETTINGS_PERMISSION_UNSUPPORTED",
@@ -15773,6 +15773,11 @@ var FAILURE_CODES = {
15773
15773
  // a device that is not foldable, no hinge route, a remote simulator.
15774
15774
  IOS_FOLD_FAILED: "IOS_FOLD_FAILED",
15775
15775
  IOS_FOLD_UNSUPPORTED: "IOS_FOLD_UNSUPPORTED",
15776
+ // The hinge of a foldable Android emulator: the console refused the move or
15777
+ // the guest never committed the state vs. nothing to fold — a phone profile,
15778
+ // a physical device.
15779
+ ANDROID_FOLD_FAILED: "ANDROID_FOLD_FAILED",
15780
+ ANDROID_FOLD_UNSUPPORTED: "ANDROID_FOLD_UNSUPPORTED",
15776
15781
  PASTE_CLIPBOARD_SET_FAILED: "PASTE_CLIPBOARD_SET_FAILED",
15777
15782
  PASTE_CLIPBOARD_UNSUPPORTED: "PASTE_CLIPBOARD_UNSUPPORTED",
15778
15783
  SETTINGS_PERMISSION_UNSUPPORTED: "SETTINGS_PERMISSION_UNSUPPORTED",
@@ -16774,7 +16779,7 @@ var _CI_VENDOR_COUNT_FOR_TEST = vendors_default.length;
16774
16779
  var SESSION_ID = randomUUID4();
16775
16780
  function readCliVersion() {
16776
16781
  if (true) {
16777
- return "0.29.1-next.13";
16782
+ return "0.29.1-next.15";
16778
16783
  }
16779
16784
  return "0.0.0";
16780
16785
  }
@@ -20093,52 +20098,6 @@ function resolveInstallModeFromFlags(opts) {
20093
20098
  return null;
20094
20099
  }
20095
20100
 
20096
- // ../argent-installer/src/install-targets.ts
20097
- function parseTargetFlags(args) {
20098
- return { global: args.includes("--global"), local: args.includes("--local") };
20099
- }
20100
- function decideInstallTargets(ctx) {
20101
- const { flags } = ctx;
20102
- if (flags.global || flags.local) {
20103
- const targets = [];
20104
- if (flags.global) targets.push("global");
20105
- if (flags.local) targets.push("local");
20106
- return { kind: "targets", targets, reason: "flags" };
20107
- }
20108
- if (ctx.globalPresent && ctx.localPresent) {
20109
- if (ctx.nonInteractive) {
20110
- return {
20111
- kind: "targets",
20112
- targets: ctx.nonInteractiveBothDefault,
20113
- reason: "noninteractive-both"
20114
- };
20115
- }
20116
- return { kind: "prompt" };
20117
- }
20118
- return { kind: "targets", targets: [ctx.defaultTarget], reason: "single" };
20119
- }
20120
- async function promptInstallTargets(verb) {
20121
- const selected = await multiselect({
20122
- message: `argent is installed both globally and in this project. Which should ${verb === "update" ? "update" : "removal"} affect?`,
20123
- options: [
20124
- {
20125
- value: "global",
20126
- label: "Global install",
20127
- hint: "the argent command on your PATH"
20128
- },
20129
- {
20130
- value: "local",
20131
- label: "This project's devDependency",
20132
- hint: `${PACKAGE_NAME} in this project's node_modules`
20133
- }
20134
- ],
20135
- initialValues: verb === "update" ? ["global", "local"] : ["local"],
20136
- required: true
20137
- });
20138
- if (isCancel(selected)) return "cancel";
20139
- return selected;
20140
- }
20141
-
20142
20101
  // ../argent-installer/src/utils.ts
20143
20102
  var PACKAGE_ROOT = resolvePackageRoot(import.meta.dirname);
20144
20103
  function resolveBundledDir(dirName) {
@@ -23289,6 +23248,52 @@ var import_picocolors11 = __toESM(require_picocolors(), 1);
23289
23248
  var import_semver5 = __toESM(require_semver2(), 1);
23290
23249
  import * as path24 from "node:path";
23291
23250
 
23251
+ // ../argent-installer/src/install-targets.ts
23252
+ function parseTargetFlags(args) {
23253
+ return { global: args.includes("--global"), local: args.includes("--local") };
23254
+ }
23255
+ function decideInstallTargets(ctx) {
23256
+ const { flags } = ctx;
23257
+ if (flags.global || flags.local) {
23258
+ const targets = [];
23259
+ if (flags.global) targets.push("global");
23260
+ if (flags.local) targets.push("local");
23261
+ return { kind: "targets", targets, reason: "flags" };
23262
+ }
23263
+ if (ctx.globalPresent && ctx.localPresent) {
23264
+ if (ctx.nonInteractive) {
23265
+ return {
23266
+ kind: "targets",
23267
+ targets: ctx.nonInteractiveBothDefault,
23268
+ reason: "noninteractive-both"
23269
+ };
23270
+ }
23271
+ return { kind: "prompt" };
23272
+ }
23273
+ return { kind: "targets", targets: [ctx.defaultTarget], reason: "single" };
23274
+ }
23275
+ async function promptInstallTargets(verb) {
23276
+ const selected = await multiselect({
23277
+ message: `argent is installed both globally and in this project. Which should ${verb === "update" ? "update" : "removal"} affect?`,
23278
+ options: [
23279
+ {
23280
+ value: "global",
23281
+ label: "Global install",
23282
+ hint: "the argent command on your PATH"
23283
+ },
23284
+ {
23285
+ value: "local",
23286
+ label: "This project's devDependency",
23287
+ hint: `${PACKAGE_NAME} in this project's node_modules`
23288
+ }
23289
+ ],
23290
+ initialValues: verb === "update" ? ["global", "local"] : ["local"],
23291
+ required: true
23292
+ });
23293
+ if (isCancel(selected)) return "cancel";
23294
+ return selected;
23295
+ }
23296
+
23292
23297
  // ../argent-installer/src/update-target.ts
23293
23298
  var import_update_core = __toESM(require_dist2(), 1);
23294
23299
  async function resolveInstallableUpdateTarget(pm, current) {
@@ -25070,6 +25070,11 @@ var FAILURE_CODES = {
25070
25070
  // a device that is not foldable, no hinge route, a remote simulator.
25071
25071
  IOS_FOLD_FAILED: "IOS_FOLD_FAILED",
25072
25072
  IOS_FOLD_UNSUPPORTED: "IOS_FOLD_UNSUPPORTED",
25073
+ // The hinge of a foldable Android emulator: the console refused the move or
25074
+ // the guest never committed the state vs. nothing to fold — a phone profile,
25075
+ // a physical device.
25076
+ ANDROID_FOLD_FAILED: "ANDROID_FOLD_FAILED",
25077
+ ANDROID_FOLD_UNSUPPORTED: "ANDROID_FOLD_UNSUPPORTED",
25073
25078
  PASTE_CLIPBOARD_SET_FAILED: "PASTE_CLIPBOARD_SET_FAILED",
25074
25079
  PASTE_CLIPBOARD_UNSUPPORTED: "PASTE_CLIPBOARD_UNSUPPORTED",
25075
25080
  SETTINGS_PERMISSION_UNSUPPORTED: "SETTINGS_PERMISSION_UNSUPPORTED",
@@ -267,9 +267,9 @@ export interface ArgentToolArgs {
267
267
  executionPrerequisite?: string;
268
268
  };
269
269
  fold: {
270
- /** Target device id from `list-devices` (a foldable iOS simulator UDID). */
270
+ /** Target device id from `list-devices` (a foldable iOS simulator UDID or Android emulator serial). */
271
271
  udid: string;
272
- /** Where to move the hinge: `closed` (0°, the cover panel), `half-open` (120°) or `open` (180°, the inner panel). Exactly one of `posture` and `angle`. */
272
+ /** Where to move the hinge: `closed` (the cover panel), `half-open` or `open` (the inner panel). Exactly one of `posture` and `angle`. */
273
273
  posture?: "closed" | "half-open" | "open";
274
274
  /** The hinge angle in degrees, 0 (closed) to 180 (open). Exactly one of `posture` and `angle`. */
275
275
  angle?: number;
@@ -957,18 +957,6 @@ var init_event_emitter = __esm({
957
957
  this.listeners.get(event2).add(listener);
958
958
  return this;
959
959
  }
960
- /**
961
- * @public
962
- * Every caller is in another workspace — tool-server (the variant-proposal
963
- * store, the chromium server's frame/fps and network listeners, both
964
- * js-runtime-debugger blueprints) and telemetry (`registry-listener`, which
965
- * detaches from `Registry.events`) — and the dead-code gate runs against an
966
- * unbuilt tree, where a cross-workspace edge cannot form. See `knip.jsonc`.
967
- *
968
- * To re-audit whether this tag is still earned, rename the member and read
969
- * the `Property 'off' does not exist` errors `tsc` reports. Do not grep
970
- * `.off(`: most hits in this repo are Node `EventEmitter`s.
971
- */
972
960
  off(event2, listener) {
973
961
  this.listeners.get(event2)?.delete(listener);
974
962
  return this;
@@ -1034,19 +1022,6 @@ var init_artifacts = __esm({
1034
1022
  };
1035
1023
  ArtifactStore = class {
1036
1024
  entries = /* @__PURE__ */ new Map();
1037
- /**
1038
- * @public
1039
- * Every caller is in tool-server — `tools/flows/flow-visual`,
1040
- * `tools/screenshot`, `tools/screenshot-diff`, `tools/screen-recording-stop`,
1041
- * both `tools/profiler/native-profiler` stop/analyze handlers and
1042
- * `tools/profiler/react/react-profiler-analyze` — and the dead-code gate runs
1043
- * against an unbuilt tree, where a cross-workspace edge cannot form. See
1044
- * `knip.jsonc`.
1045
- *
1046
- * To re-audit whether this tag is still earned, rename the member and read
1047
- * the `Property 'register' does not exist` errors `tsc --build` reports. Do
1048
- * not grep `.register(`: `registerTool` / `registerBlueprint` swamp the hits.
1049
- */
1050
1025
  async register(opts) {
1051
1026
  const { hostPath, kind } = opts;
1052
1027
  const filename = opts.filename ?? (0, import_node_path2.basename)(hostPath);
@@ -1077,21 +1052,9 @@ var init_artifacts = __esm({
1077
1052
  if (opts.saveDir) handle.saveDir = opts.saveDir;
1078
1053
  return handle;
1079
1054
  }
1080
- /**
1081
- * @public
1082
- * The only caller is the tool-server `GET /artifacts/:id` route, and the
1083
- * dead-code gate runs against an unbuilt tree, where a cross-workspace edge
1084
- * cannot form. See `knip.jsonc`.
1085
- */
1086
1055
  get(id) {
1087
1056
  return this.entries.get(id);
1088
1057
  }
1089
- /**
1090
- * @public
1091
- * The only caller is the tool-server `/artifacts` route, and the dead-code
1092
- * gate runs against an unbuilt tree, where a cross-workspace edge cannot
1093
- * form. See `knip.jsonc`.
1094
- */
1095
1058
  list() {
1096
1059
  return [...this.entries.entries()].map(([id, entry]) => ({
1097
1060
  id,
@@ -1362,6 +1325,11 @@ var init_failure_codes = __esm({
1362
1325
  // a device that is not foldable, no hinge route, a remote simulator.
1363
1326
  IOS_FOLD_FAILED: "IOS_FOLD_FAILED",
1364
1327
  IOS_FOLD_UNSUPPORTED: "IOS_FOLD_UNSUPPORTED",
1328
+ // The hinge of a foldable Android emulator: the console refused the move or
1329
+ // the guest never committed the state vs. nothing to fold — a phone profile,
1330
+ // a physical device.
1331
+ ANDROID_FOLD_FAILED: "ANDROID_FOLD_FAILED",
1332
+ ANDROID_FOLD_UNSUPPORTED: "ANDROID_FOLD_UNSUPPORTED",
1365
1333
  PASTE_CLIPBOARD_SET_FAILED: "PASTE_CLIPBOARD_SET_FAILED",
1366
1334
  PASTE_CLIPBOARD_UNSUPPORTED: "PASTE_CLIPBOARD_UNSUPPORTED",
1367
1335
  SETTINGS_PERMISSION_UNSUPPORTED: "SETTINGS_PERMISSION_UNSUPPORTED",
@@ -1690,13 +1658,6 @@ var init_errors = __esm({
1690
1658
  }
1691
1659
  };
1692
1660
  ToolNotFoundError = class extends Error {
1693
- /**
1694
- * @public
1695
- * Knip's `classMembers` pass is workspace-local, so it cannot see the caller:
1696
- * `flow-add-step` keys `isToolNotFound` on this alongside the error identity,
1697
- * so a tool that ran and reported its own "not found" is not read as the
1698
- * command being absent.
1699
- */
1700
1661
  toolId;
1701
1662
  constructor(toolId) {
1702
1663
  super(`Tool "${toolId}" not found`);
@@ -83003,6 +82964,7 @@ __export(adb_exports, {
83003
82964
  getCachedAndroidRuntimeKind: () => getCachedAndroidRuntimeKind,
83004
82965
  hasDefaultBootSnapshot: () => hasDefaultBootSnapshot,
83005
82966
  isAndroidTv: () => isAndroidTv,
82967
+ isFoldableAvd: () => isFoldableAvd,
83006
82968
  isTerminalAdbError: () => isTerminalAdbError,
83007
82969
  listAndroidDevices: () => listAndroidDevices,
83008
82970
  listAvds: () => listAvds,
@@ -83354,6 +83316,17 @@ async function resolveAvdPath(avdName) {
83354
83316
  }
83355
83317
  return null;
83356
83318
  }
83319
+ async function isFoldableAvd(avdName) {
83320
+ const { readFile: readFile19 } = await import("node:fs/promises");
83321
+ const avdPath = await resolveAvdPath(avdName);
83322
+ if (!avdPath) return false;
83323
+ try {
83324
+ const hinge = (0, import_ini.parse)(await readFile19(`${avdPath}/config.ini`, "utf-8"))["hw.sensor.hinge"];
83325
+ return hinge === true || hinge === "yes" || hinge === "true";
83326
+ } catch {
83327
+ return false;
83328
+ }
83329
+ }
83357
83330
  async function hasDefaultBootSnapshot(avdName) {
83358
83331
  const { stat: stat7 } = await import("node:fs/promises");
83359
83332
  const avdPath = await resolveAvdPath(avdName);
@@ -90430,16 +90403,16 @@ var require_foldFlowLines = __commonJS({
90430
90403
  onFold();
90431
90404
  let res = text.slice(0, folds[0]);
90432
90405
  for (let i2 = 0; i2 < folds.length; ++i2) {
90433
- const fold = folds[i2];
90406
+ const fold2 = folds[i2];
90434
90407
  const end2 = folds[i2 + 1] || text.length;
90435
- if (fold === 0)
90408
+ if (fold2 === 0)
90436
90409
  res = `
90437
90410
  ${indent}${text.slice(0, end2)}`;
90438
90411
  else {
90439
- if (mode === FOLD_QUOTED && escapedFolds[fold])
90440
- res += `${text[fold]}\\`;
90412
+ if (mode === FOLD_QUOTED && escapedFolds[fold2])
90413
+ res += `${text[fold2]}\\`;
90441
90414
  res += `
90442
- ${indent}${text.slice(fold + 1, end2)}`;
90415
+ ${indent}${text.slice(fold2 + 1, end2)}`;
90443
90416
  }
90444
90417
  }
90445
90418
  return res;
@@ -94078,8 +94051,8 @@ var require_resolve_flow_scalar = __commonJS({
94078
94051
  if (ch === "\r" && source[i + 1] === "\n")
94079
94052
  continue;
94080
94053
  if (ch === "\n") {
94081
- const { fold, offset } = foldNewline(source, i);
94082
- res += fold;
94054
+ const { fold: fold2, offset } = foldNewline(source, i);
94055
+ res += fold2;
94083
94056
  i = offset;
94084
94057
  } else if (ch === "\\") {
94085
94058
  let next = source[++i];
@@ -94119,19 +94092,19 @@ var require_resolve_flow_scalar = __commonJS({
94119
94092
  return res;
94120
94093
  }
94121
94094
  function foldNewline(source, offset) {
94122
- let fold = "";
94095
+ let fold2 = "";
94123
94096
  let ch = source[offset + 1];
94124
94097
  while (ch === " " || ch === " " || ch === "\n" || ch === "\r") {
94125
94098
  if (ch === "\r" && source[offset + 2] !== "\n")
94126
94099
  break;
94127
94100
  if (ch === "\n")
94128
- fold += "\n";
94101
+ fold2 += "\n";
94129
94102
  offset += 1;
94130
94103
  ch = source[offset + 1];
94131
94104
  }
94132
- if (!fold)
94133
- fold = " ";
94134
- return { fold, offset };
94105
+ if (!fold2)
94106
+ fold2 = " ";
94107
+ return { fold: fold2, offset };
94135
94108
  }
94136
94109
  var escapeCodes = {
94137
94110
  "0": "\0",
@@ -100257,7 +100230,7 @@ var _CI_VENDOR_COUNT_FOR_TEST = vendors_default.length;
100257
100230
  var SESSION_ID = (0, import_node_crypto3.randomUUID)();
100258
100231
  function readCliVersion() {
100259
100232
  if (true) {
100260
- return "0.29.1-next.13";
100233
+ return "0.29.1-next.15";
100261
100234
  }
100262
100235
  return "0.0.0";
100263
100236
  }
@@ -125034,7 +125007,7 @@ Android entries also carry a 'kind' ('emulator' for a local AVD, 'device' for a
125034
125007
  Physical iPhones appear as iOS entries with kind 'device' (no iPads); no boot-device. State 'connected' = cabled and usable; 'paired' = not reachable over USB, never auto-bound.
125035
125008
  TV targets are tagged with runtimeKind 'tv' (Apple TV simulators on iOS, Android TV / leanback devices on Android). These are focus-driven, not touch-driven: use \`describe\` to read focus, \`tv-remote\` for remote presses (up/down/left/right/select/back/menu/home), and \`keyboard\` to type, rather than the coordinate/gesture tools.
125036
125009
  iOS simulators from an additional CoreSimulator device set (the 'ios.additionalDeviceSets' configuration, e.g. devices created by Radon IDE) are listed alongside default-set ones, tagged with their owning 'deviceSet' path; they are driven through the same tools by udid, but run headless (no Simulator.app window attaches to them).
125037
- A foldable iOS simulator (the iPhone Duo) has 'foldable: true'. Use the \`fold\` tool to fold or unfold it.
125010
+ A foldable iOS simulator (the iPhone Duo) or Android emulator (and its AVD) has 'foldable: true'. Use the \`fold\` tool to fold or unfold it.
125038
125011
  Chromium apps are discovered by probing CDP debugging ports (default 9222; extend via the ARGENT_CHROMIUM_PORTS=<comma-separated-ports> env var). They must already be running with --remote-debugging-port=<port>; use boot-device with electronAppPath to launch one.
125039
125012
  Booted/ready devices are listed first. Platforms whose CLI is unavailable are silently omitted; an empty result usually means xcode-select, Android platform-tools, or the Vega SDK is not installed.`,
125040
125013
  alwaysLoad: true,
@@ -125098,6 +125071,11 @@ Booted/ready devices are listed first. Platforms whose CLI is unavailable are si
125098
125071
  tunnelState: d.tunnelState
125099
125072
  }));
125100
125073
  iosRemote.sort(sortIosRemote);
125074
+ const foldableAvds = new Set(
125075
+ (await Promise.all(
125076
+ [.../* @__PURE__ */ new Set([...avds.map((a) => a.name), ...android.map((d) => d.avdName)])].filter((name) => name !== null).map(async (name) => await isFoldableAvd(name) ? name : null)
125077
+ )).filter((name) => name !== null)
125078
+ );
125101
125079
  const androidTagged = android.map((d) => ({
125102
125080
  platform: "android",
125103
125081
  serial: d.serial,
@@ -125107,7 +125085,8 @@ Booted/ready devices are listed first. Platforms whose CLI is unavailable are si
125107
125085
  model: d.model,
125108
125086
  avdName: d.avdName,
125109
125087
  sdkLevel: d.sdkLevel,
125110
- runtimeKind: d.runtimeKind
125088
+ runtimeKind: d.runtimeKind,
125089
+ ...d.avdName !== null && foldableAvds.has(d.avdName) ? { foldable: true } : {}
125111
125090
  }));
125112
125091
  const vvdShadowSerials = await resolveVvdShadowAdbSerials(androidTagged, vega);
125113
125092
  const shadowSerials = /* @__PURE__ */ new Set([...vvdShadowSerials, ...externalShadows]);
@@ -125123,7 +125102,14 @@ Booted/ready devices are listed first. Platforms whose CLI is unavailable are si
125123
125102
  ...external.map(toListedExternal)
125124
125103
  ];
125125
125104
  devices.sort((a, b) => readinessRank(a) - readinessRank(b));
125126
- return { devices, avds, ...external.length > 0 ? { hint: EXTERNAL_DEVICES_HINT } : {} };
125105
+ const avdsTagged = avds.map(
125106
+ (a) => foldableAvds.has(a.name) ? { ...a, foldable: true } : a
125107
+ );
125108
+ return {
125109
+ devices,
125110
+ avds: avdsTagged,
125111
+ ...external.length > 0 ? { hint: EXTERNAL_DEVICES_HINT } : {}
125112
+ };
125127
125113
  }
125128
125114
  };
125129
125115
 
@@ -130452,11 +130438,11 @@ function scheduleGroupSigkill(pid, graceMs, opts) {
130452
130438
  }
130453
130439
  async function pollPidsUntilGone(pids, opts) {
130454
130440
  const isAlive2 = opts.isAlive ?? pidIsAlive;
130455
- const sleep15 = opts.sleep ?? ((ms) => new Promise((resolve15) => setTimeout(resolve15, ms)));
130441
+ const sleep16 = opts.sleep ?? ((ms) => new Promise((resolve15) => setTimeout(resolve15, ms)));
130456
130442
  const maxPolls = Math.max(1, Math.ceil(opts.timeoutMs / opts.pollIntervalMs));
130457
130443
  let remaining = pids.filter((pid) => isAlive2(pid));
130458
130444
  for (let poll = 0; poll < maxPolls && remaining.length > 0; poll += 1) {
130459
- await sleep15(opts.pollIntervalMs);
130445
+ await sleep16(opts.pollIntervalMs);
130460
130446
  remaining = remaining.filter((pid) => isAlive2(pid));
130461
130447
  }
130462
130448
  return remaining;
@@ -131082,7 +131068,7 @@ function transportFailureSignal(kind) {
131082
131068
  failure_stage: "ios_device_runner_transport",
131083
131069
  failure_area: "tool_server"
131084
131070
  };
131085
- const failed2 = {
131071
+ const failed3 = {
131086
131072
  ...notReady,
131087
131073
  error_code: FAILURE_CODES.IOS_DEVICE_RUNNER_TRANSPORT_FAILED
131088
131074
  };
@@ -131092,11 +131078,11 @@ function transportFailureSignal(kind) {
131092
131078
  case "runner-not-listening":
131093
131079
  return { ...notReady, error_kind: "network", network_failure: "connection_refused" };
131094
131080
  case "timeout":
131095
- return { ...failed2, error_kind: "timeout" };
131081
+ return { ...failed3, error_kind: "timeout" };
131096
131082
  case "protocol":
131097
- return { ...failed2, error_kind: "network", network_failure: "invalid_response" };
131083
+ return { ...failed3, error_kind: "network", network_failure: "invalid_response" };
131098
131084
  case "http":
131099
- return { ...failed2, error_kind: "network", network_failure: "other" };
131085
+ return { ...failed3, error_kind: "network", network_failure: "other" };
131100
131086
  }
131101
131087
  }
131102
131088
  function createRunnerClient(options) {
@@ -131852,7 +131838,7 @@ function createConsoleLogServer(consoleEvents, logWriter) {
131852
131838
  });
131853
131839
  }
131854
131840
  function makeStubSourceResolver() {
131855
- const unsupported = () => {
131841
+ const unsupported2 = () => {
131856
131842
  throw new Error(
131857
131843
  "SourceResolver is not implemented on Chromium debugger sessions \u2014 Metro symbolicate is the only backing implementation."
131858
131844
  );
@@ -131860,7 +131846,7 @@ function makeStubSourceResolver() {
131860
131846
  return {
131861
131847
  resolveDebugStack: async () => null,
131862
131848
  symbolicate: async () => null,
131863
- readSourceFragment: unsupported
131849
+ readSourceFragment: unsupported2
131864
131850
  };
131865
131851
  }
131866
131852
  var chromiumJsRuntimeDebuggerBlueprint = {
@@ -137139,8 +137125,8 @@ var vegaImpl3 = {
137139
137125
  const output2 = `${stdout}
137140
137126
  ${stderr}`;
137141
137127
  const succeeded = /\.\.\.\s*success\b/i.test(output2);
137142
- const failed2 = /\.\.\.\s*failed\b/i.test(output2);
137143
- if (!succeeded || failed2) {
137128
+ const failed3 = /\.\.\.\s*failed\b/i.test(output2);
137129
+ if (!succeeded || failed3) {
137144
137130
  throw new FailureError(`vega install-app failed: ${output2.trim()}`, {
137145
137131
  error_code: FAILURE_CODES.VEGA_CLI_COMMAND_FAILED,
137146
137132
  failure_stage: "vega_reinstall_install_app",
@@ -137824,10 +137810,10 @@ async function readAndroidSurfaceRotation(serial, peek) {
137824
137810
  } catch {
137825
137811
  }
137826
137812
  }
137827
- for (const shell of [adbServerShell, adbShell]) {
137813
+ for (const shell2 of [adbServerShell, adbShell]) {
137828
137814
  for (const { cmd, pattern } of ROTATION_PROBES) {
137829
137815
  try {
137830
- const out = await shell(serial, cmd, { timeoutMs: 5e3 });
137816
+ const out = await shell2(serial, cmd, { timeoutMs: 5e3 });
137831
137817
  const value = Number(pattern.exec(out)?.[1]);
137832
137818
  if (Number.isInteger(value) && isSurfaceRotation(value)) return value;
137833
137819
  } catch {
@@ -140706,6 +140692,20 @@ var iosRemoteImpl6 = {
140706
140692
  // ../tool-server/src/tools/shake/platforms/android.ts
140707
140693
  init_src2();
140708
140694
  init_adb();
140695
+
140696
+ // ../tool-server/src/utils/emulator-console.ts
140697
+ init_adb();
140698
+ async function runEmuConsole(serial, args) {
140699
+ const { stdout } = await runAdb(["-s", serial, "emu", ...args], { timeoutMs: 1e4 });
140700
+ const lines = stdout.split("\n").map((line) => line.trim()).filter(Boolean);
140701
+ const verdict = lines[lines.length - 1];
140702
+ if (verdict?.startsWith("KO:")) {
140703
+ throw new Error(`emulator console rejected \`${args.join(" ")}\` \u2014 ${verdict}`);
140704
+ }
140705
+ return stdout;
140706
+ }
140707
+
140708
+ // ../tool-server/src/tools/shake/platforms/android.ts
140709
140709
  var SHAKE_AMPLITUDE = 30;
140710
140710
  var SWINGS_PER_SHAKE = 8;
140711
140711
  var SWING_INTERVAL_MS = 50;
@@ -140732,15 +140732,6 @@ function restingVectorFrom(stdout) {
140732
140732
  }
140733
140733
  var fmt = (v) => `${v.x.toFixed(4)}:${v.y.toFixed(4)}:${v.z.toFixed(4)}`;
140734
140734
  var sleep12 = (ms) => new Promise((r) => setTimeout(r, ms));
140735
- async function emuConsole(serial, args) {
140736
- const { stdout } = await runAdb(["-s", serial, "emu", ...args], { timeoutMs: 1e4 });
140737
- const lines = stdout.split("\n").map((line) => line.trim()).filter(Boolean);
140738
- const verdict = lines[lines.length - 1];
140739
- if (verdict?.startsWith("KO:")) {
140740
- throw new Error(`emulator console rejected \`${args.join(" ")}\` \u2014 ${verdict}`);
140741
- }
140742
- return stdout;
140743
- }
140744
140735
  var inFlightByDevice = /* @__PURE__ */ new Map();
140745
140736
  function withDeviceLock(serial, fn) {
140746
140737
  const previous = inFlightByDevice.get(serial) ?? Promise.resolve();
@@ -140777,12 +140768,12 @@ var androidImpl6 = {
140777
140768
  );
140778
140769
  }
140779
140770
  const setAcceleration = async (v) => {
140780
- await emuConsole(serial, ["sensor", "set", "acceleration", fmt(v)]);
140771
+ await runEmuConsole(serial, ["sensor", "set", "acceleration", fmt(v)]);
140781
140772
  };
140782
140773
  return withDeviceLock(serial, async () => {
140783
140774
  let rest;
140784
140775
  try {
140785
- rest = restingVectorFrom(await emuConsole(serial, ["sensor", "get", "acceleration"]));
140776
+ rest = restingVectorFrom(await runEmuConsole(serial, ["sensor", "get", "acceleration"]));
140786
140777
  } catch (err) {
140787
140778
  const detail = err instanceof Error ? err.message : String(err);
140788
140779
  throw new FailureError(
@@ -140881,13 +140872,270 @@ Only phone/tablet simulators and emulators are supported.`,
140881
140872
  init_zod();
140882
140873
  init_src2();
140883
140874
  init_device_info();
140875
+
140876
+ // ../tool-server/src/tools/fold/platforms/android.ts
140877
+ init_src2();
140878
+ init_adb();
140884
140879
  var POSTURES = ["closed", "half-open", "open"];
140880
+ var EMULATOR_POSTURES = {
140881
+ "closed": { id: "1", state: "CLOSED" },
140882
+ "half-open": { id: "2", state: "HALF_OPENED" },
140883
+ "open": { id: "3", state: "OPENED" }
140884
+ };
140885
+ var POLL_MS = 250;
140886
+ var SETTLE_TIMEOUT_MS2 = 15e3;
140887
+ var STABLE_READS = 3;
140888
+ var MAX_DISMISS_ATTEMPTS = 3;
140889
+ var DISMISS_SPACING_MS = 1e3;
140890
+ var DISMISS_TIMEOUT_MS = 5e3;
140891
+ var WAKE_GRACE_READS = 2;
140892
+ var abortReason2 = (signal) => signal.reason instanceof Error ? signal.reason : new Error("The fold was cancelled.");
140893
+ var sleep13 = (ms, signal) => new Promise((resolve15, reject) => {
140894
+ if (signal?.aborted) return reject(abortReason2(signal));
140895
+ const timer = setTimeout(resolve15, ms);
140896
+ signal?.addEventListener(
140897
+ "abort",
140898
+ () => {
140899
+ clearTimeout(timer);
140900
+ reject(abortReason2(signal));
140901
+ },
140902
+ { once: true }
140903
+ );
140904
+ });
140905
+ function unsupported(message, stage) {
140906
+ return new FailureError(message, {
140907
+ error_code: FAILURE_CODES.ANDROID_FOLD_UNSUPPORTED,
140908
+ failure_stage: stage,
140909
+ failure_area: "tool_server",
140910
+ error_kind: "unsupported"
140911
+ });
140912
+ }
140913
+ function failed(message, stage, cause) {
140914
+ return new FailureError(
140915
+ message,
140916
+ {
140917
+ error_code: FAILURE_CODES.ANDROID_FOLD_FAILED,
140918
+ failure_stage: stage,
140919
+ failure_area: "tool_server",
140920
+ error_kind: "subprocess"
140921
+ },
140922
+ cause instanceof Error ? { cause } : void 0
140923
+ );
140924
+ }
140925
+ async function shell(serial, command) {
140926
+ const { stdout } = await runAdb(["-s", serial, "shell", command], { timeoutMs: 1e4 });
140927
+ return stdout;
140928
+ }
140929
+ function parseDeviceStates(stdout) {
140930
+ const states = /* @__PURE__ */ new Map();
140931
+ for (const match of stdout.matchAll(/identifier=(\d+), name='([A-Z_]+)'/g)) {
140932
+ states.set(match[2], match[1]);
140933
+ }
140934
+ return states;
140935
+ }
140936
+ function parseSnapshot(stdout) {
140937
+ const screenOn = !/mScreenOnFully=false/.test(stdout);
140938
+ const wakefulness = /mWakefulness=([A-Za-z]+)/.exec(stdout)?.[1];
140939
+ return {
140940
+ state: /^\s*(\d+)\s*$/m.exec(stdout)?.[1],
140941
+ screenOn,
140942
+ awake: wakefulness ? !/^(Asleep|Dozing)$/.test(wakefulness) : screenOn,
140943
+ keyguard: /isKeyguardShowing=true/.test(stdout)
140944
+ };
140945
+ }
140946
+ async function readSnapshot(serial) {
140947
+ return parseSnapshot(
140948
+ await shell(
140949
+ serial,
140950
+ "cmd device_state print-state; dumpsys window displays | grep -o 'mScreenOnFully=[a-z]*'; dumpsys power | grep -o 'mWakefulness=[A-Za-z]*'; dumpsys window | grep -o 'isKeyguardShowing=[a-z]*'"
140951
+ )
140952
+ );
140953
+ }
140954
+ function parseHingeAngle(stdout) {
140955
+ const match = /hinge-angle0\s*=\s*(-?\d+(?:\.\d+)?)/.exec(stdout);
140956
+ return match ? Number(match[1]) : void 0;
140957
+ }
140958
+ function isReady(read, expected) {
140959
+ return read.state !== void 0 && (read.screenOn || !read.awake) && (expected === void 0 || read.state === expected);
140960
+ }
140961
+ async function awaitSettled(serial, expected, wake, signal) {
140962
+ let last;
140963
+ let agreeing = 0;
140964
+ const deadline = Date.now() + SETTLE_TIMEOUT_MS2;
140965
+ while (Date.now() < deadline) {
140966
+ await sleep13(POLL_MS, signal);
140967
+ const read = await readSnapshot(serial);
140968
+ const same = read.state === last?.state && read.awake === last?.awake;
140969
+ if (!isReady(read, expected)) agreeing = 0;
140970
+ else agreeing = agreeing > 0 && same ? agreeing + 1 : 1;
140971
+ last = read;
140972
+ const mayWake = wake.wasAsleep && !read.awake && read.state !== wake.closedId;
140973
+ if (agreeing >= STABLE_READS + (mayWake ? WAKE_GRACE_READS : 0)) {
140974
+ return { last, settled: true, steady: true };
140975
+ }
140976
+ }
140977
+ const settled = last !== void 0 && isReady(last, expected);
140978
+ return { last, settled, steady: false };
140979
+ }
140980
+ async function dismissFoldLockScreen(serial, signal) {
140981
+ let clear = 0;
140982
+ let attempts = 0;
140983
+ let lastAttempt = -Infinity;
140984
+ const deadline = Date.now() + DISMISS_TIMEOUT_MS;
140985
+ for (; ; ) {
140986
+ if (!(await readSnapshot(serial)).keyguard) {
140987
+ if (++clear >= STABLE_READS) return void 0;
140988
+ } else {
140989
+ clear = 0;
140990
+ if (Date.now() >= deadline) break;
140991
+ if (attempts < MAX_DISMISS_ATTEMPTS && Date.now() - lastAttempt >= DISMISS_SPACING_MS) {
140992
+ attempts++;
140993
+ lastAttempt = Date.now();
140994
+ await shell(serial, "wm dismiss-keyguard");
140995
+ }
140996
+ }
140997
+ await sleep13(POLL_MS, signal);
140998
+ }
140999
+ return "The fold raised the lock screen and argent could not dismiss it (a PIN or password keeps it up). Unlock the device before the next action.";
141000
+ }
141001
+ function targetLabel(params) {
141002
+ return params.posture ?? `${params.angle}\xB0`;
141003
+ }
141004
+ var inFlightByDevice2 = /* @__PURE__ */ new Map();
141005
+ function withDeviceLock2(serial, fn) {
141006
+ const previous = inFlightByDevice2.get(serial) ?? Promise.resolve();
141007
+ const run2 = previous.then(fn, fn);
141008
+ const settled = run2.catch(() => {
141009
+ });
141010
+ inFlightByDevice2.set(serial, settled);
141011
+ void settled.then(() => {
141012
+ if (inFlightByDevice2.get(serial) === settled) inFlightByDevice2.delete(serial);
141013
+ });
141014
+ return run2;
141015
+ }
141016
+ var NO_DEVICE_STATE_SERVICE = /Can't find service: device_state|cmd: not found|No shell command implementation/i;
141017
+ async function readDeviceStates(serial) {
141018
+ try {
141019
+ return parseDeviceStates(await shell(serial, "cmd device_state print-states"));
141020
+ } catch (err) {
141021
+ if (!NO_DEVICE_STATE_SERVICE.test(err instanceof Error ? err.message : String(err))) throw err;
141022
+ return /* @__PURE__ */ new Map();
141023
+ }
141024
+ }
141025
+ async function fold(serial, params, device, signal) {
141026
+ signal?.throwIfAborted();
141027
+ const states = await readDeviceStates(serial);
141028
+ const byId = new Map([...states].map(([name, id]) => [id, name]));
141029
+ const supported2 = POSTURES.filter((p) => states.has(EMULATOR_POSTURES[p].state));
141030
+ if (supported2.length === 0) {
141031
+ throw unsupported(
141032
+ `${device.id} is not a foldable emulator: it lists no CLOSED, HALF_OPENED or OPENED device state. Boot a foldable AVD (a Pixel Fold or a generic foldable profile); \`list-devices\` marks them with \`foldable: true\`.`,
141033
+ "android_fold_not_foldable"
141034
+ );
141035
+ }
141036
+ const expected = params.posture ? states.get(EMULATOR_POSTURES[params.posture].state) : void 0;
141037
+ if (params.posture && expected === void 0) {
141038
+ throw unsupported(
141039
+ `${device.id} has no ${EMULATOR_POSTURES[params.posture].state} device state for the ${params.posture} posture; it supports ${supported2.join(", ")}.`,
141040
+ "android_fold_posture_unsupported"
141041
+ );
141042
+ }
141043
+ const before = await readSnapshot(serial);
141044
+ signal?.throwIfAborted();
141045
+ try {
141046
+ if (params.posture) {
141047
+ await runEmuConsole(serial, ["posture", EMULATOR_POSTURES[params.posture].id]);
141048
+ } else {
141049
+ await runEmuConsole(serial, ["sensor", "set", "hinge-angle0", String(params.angle)]);
141050
+ }
141051
+ } catch (err) {
141052
+ throw failed(
141053
+ `Failed to fold ${serial} to ${targetLabel(params)}: ${err instanceof Error ? err.message : String(err)}`,
141054
+ "android_fold_console",
141055
+ err
141056
+ );
141057
+ }
141058
+ const { last, settled, steady } = await awaitSettled(
141059
+ serial,
141060
+ expected,
141061
+ { wasAsleep: !before.awake, closedId: states.get("CLOSED") },
141062
+ signal
141063
+ );
141064
+ const stateName = last?.state !== void 0 ? byId.get(last.state) ?? last.state : "unknown";
141065
+ const darkAwake = last?.state !== void 0 && last.awake && !last.screenOn;
141066
+ const budget = `${SETTLE_TIMEOUT_MS2 / 1e3} s`;
141067
+ if (!settled && expected !== void 0) {
141068
+ throw failed(
141069
+ last?.state === expected ? `${serial} reached device state ${stateName} for the ${params.posture} posture, but the device is awake and the screen did not stay on within ${budget}. Take a screenshot before the next action.` : `${serial} did not reach the ${params.posture} posture: the guest still reports device state ${stateName}. Fold again; a screenshot shows what the device renders.`,
141070
+ "android_fold_unverified"
141071
+ );
141072
+ }
141073
+ const warnings = [];
141074
+ if (!settled) {
141075
+ warnings.push(
141076
+ darkAwake ? `The device state is ${stateName}, but the screen did not stay on within ${budget}. Take a screenshot before the next action.` : `The device state did not settle within ${budget}; it last read ${stateName}. Take a screenshot before the next action.`
141077
+ );
141078
+ } else if (!steady) {
141079
+ warnings.push(
141080
+ `The screen did not hold steady within ${budget}. Take a screenshot before the next action.`
141081
+ );
141082
+ }
141083
+ let lockScreen = last?.keyguard ?? false;
141084
+ if (stateName === "CLOSED" && !before.keyguard && last?.awake) {
141085
+ lockScreen = false;
141086
+ const warning = await dismissFoldLockScreen(serial, signal);
141087
+ if (warning) warnings.push(warning);
141088
+ }
141089
+ const screenOff = settled && last !== void 0 && !last.screenOn;
141090
+ if (screenOff && lockScreen) {
141091
+ warnings.push(
141092
+ "The screen is off and the lock screen is showing after the fold. Wake and unlock the device before the next action."
141093
+ );
141094
+ } else if (screenOff) {
141095
+ warnings.push("The screen is off after the fold. Wake the device before the next action.");
141096
+ } else if (lockScreen) {
141097
+ warnings.push(
141098
+ "The lock screen is showing after the fold. Unlock or dismiss it before the next action."
141099
+ );
141100
+ }
141101
+ let hingeAngle;
141102
+ try {
141103
+ hingeAngle = parseHingeAngle(await runEmuConsole(serial, ["sensor", "get", "hinge-angle0"]));
141104
+ } catch {
141105
+ }
141106
+ const posture = POSTURES.find((p) => EMULATOR_POSTURES[p].state === stateName);
141107
+ return {
141108
+ ...posture ? { posture } : {},
141109
+ deviceState: stateName,
141110
+ ...hingeAngle !== void 0 ? { hingeAngle } : {},
141111
+ ...warnings.length > 0 ? { warning: warnings.join(" ") } : {}
141112
+ };
141113
+ }
141114
+ var androidImpl7 = {
141115
+ requires: ["adb"],
141116
+ async handler(_services, params, device, options) {
141117
+ const serial = params.udid;
141118
+ const signal = options?.signal;
141119
+ if (!/^emulator-\d+$/.test(serial)) {
141120
+ throw unsupported(
141121
+ `Cannot fold ${serial}: only an Android emulator's hinge can be moved; fold a physical device by hand.`,
141122
+ "android_fold_not_emulator"
141123
+ );
141124
+ }
141125
+ return withDeviceLock2(serial, () => fold(serial, params, device, signal));
141126
+ }
141127
+ };
141128
+
141129
+ // ../tool-server/src/tools/fold/index.ts
141130
+ var POSTURES2 = ["closed", "half-open", "open"];
140885
141131
  var POSTURE_ANGLE = { "closed": 0, "half-open": 120, "open": 180 };
140886
141132
  var HINGE_REQUEST_TIMEOUT_MS = 3e4;
140887
141133
  var zodSchema27 = external_exports.object({
140888
- udid: external_exports.string().describe("Target device id from `list-devices` (a foldable iOS simulator UDID)."),
140889
- posture: external_exports.enum(POSTURES).optional().describe(
140890
- "Where to move the hinge: `closed` (0\xB0, the cover panel), `half-open` (120\xB0) or `open` (180\xB0, the inner panel). Exactly one of `posture` and `angle`."
141134
+ udid: external_exports.string().describe(
141135
+ "Target device id from `list-devices` (a foldable iOS simulator UDID or Android emulator serial)."
141136
+ ),
141137
+ posture: external_exports.enum(POSTURES2).optional().describe(
141138
+ "Where to move the hinge: `closed` (the cover panel), `half-open` or `open` (the inner panel). Exactly one of `posture` and `angle`."
140891
141139
  ),
140892
141140
  angle: external_exports.number().min(0).max(180).optional().describe(
140893
141141
  "The hinge angle in degrees, 0 (closed) to 180 (open). Exactly one of `posture` and `angle`."
@@ -140899,10 +141147,14 @@ var capability21 = {
140899
141147
  // Local simulators only. A remote (MoQ) simulator is driven on its main
140900
141148
  // screen and carries no hinge message; the server itself rejects a device
140901
141149
  // that is not foldable, with its own reason.
140902
- apple: { simulator: true }
141150
+ apple: { simulator: true },
141151
+ // Emulators only: the hinge is moved through the emulator console. `unknown`
141152
+ // is let through and the handler rejects a serial with no console; it also
141153
+ // rejects a profile with no hinge, which the capability cannot see.
141154
+ android: { emulator: true, unknown: true }
140903
141155
  };
140904
141156
  function presetAtAngle(angle) {
140905
- return POSTURES.find((p) => POSTURE_ANGLE[p] === angle);
141157
+ return POSTURES2.find((p) => POSTURE_ANGLE[p] === angle);
140906
141158
  }
140907
141159
  function classifyHingeRefusal(err, known) {
140908
141160
  if (known?.foldable !== false) return err;
@@ -140924,7 +141176,7 @@ function hingeRequest(params) {
140924
141176
  if (params.angle !== void 0) request3.angle = params.angle;
140925
141177
  return request3;
140926
141178
  }
140927
- function targetLabel(params) {
141179
+ function targetLabel2(params) {
140928
141180
  return params.posture ?? `${params.angle}\xB0`;
140929
141181
  }
140930
141182
  function targetAngle(params) {
@@ -140950,26 +141202,8 @@ function screenResult(activeScreen, panels) {
140950
141202
  ...panel ? { width: panel.width, height: panel.height } : {}
140951
141203
  };
140952
141204
  }
140953
- var foldTool = {
140954
- id: "fold",
140955
- interaction: {
140956
- startedMsg: ({ params }) => `Folding device to ${targetLabel(params)}`,
140957
- completedMsg: ({ params, result }) => `Folded device to ${targetLabel(params)}; it renders to screen ${result.activeScreen} (${result.screen.panel})`,
140958
- failedMsg: ({ params, failureSignal: failureSignal2 }) => `Failed to fold device to ${targetLabel(params)}: ${failureSignal2.error_code}`
140959
- },
140960
- description: `Move the hinge of a foldable iOS simulator (the iPhone Duo) to a \`posture\` (closed, half-open, open) or to an \`angle\` (0-180\xB0). The tool returns when the device accepts input again, so the next tap lands.
140961
- Use when you must see or test an app in a different posture: closed shows the cover panel, half-open and open show the inner panel. \`list-devices\` marks foldable simulators with \`foldable: true\`. To turn a device without a fold, use \`rotate\`.
140962
- All tools use the active panel (the panel that shows the UI), also after a fold made outside argent. The coordinates and the screenshot size change with the panel. Before you tap, read the element tree in this result or run \`describe\` again. Unfolded, the UI is landscape, as on a rotated iPhone.
140963
- A fold between two angles that are not closed or open can keep the current panel. To change panels, fold to closed or open.
140964
- Returns { activeScreen, screen: { id, panel, width, height }, posture?, hingeAngle, warning? }. screen names the active panel and gives its size in pixels. posture is set only at 0\xB0, 120\xB0 and 180\xB0. warning is set when argent cannot find the active panel, or when the device did not change to the expected panel.
140965
- Fails if the device is not foldable or is a remote simulator.`,
140966
- searchHint: "fold unfold hinge foldable duo posture open closed half-open panel screen",
140967
- zodSchema: zodSchema27,
140968
- capability: capability21,
140969
- services: (params) => ({
140970
- simulatorServer: simulatorServerRef(resolveDevice(params.udid))
140971
- }),
140972
- async execute(services, params, ctx) {
141205
+ var iosImpl5 = {
141206
+ async handler(services, params, _device, ctx) {
140973
141207
  const api = services.simulatorServer;
140974
141208
  const udid = params.udid;
140975
141209
  const signal = ctx?.signal ? AbortSignal.any([ctx.signal, AbortSignal.timeout(HINGE_REQUEST_TIMEOUT_MS)]) : AbortSignal.timeout(HINGE_REQUEST_TIMEOUT_MS);
@@ -141016,7 +141250,7 @@ Fails if the device is not foldable or is a remote simulator.`,
141016
141250
  warning = `${unresolvedPanelNote(udid, reason2, "commands target", panels)} Take a screenshot to see the screen.`;
141017
141251
  } else if (expected !== void 0 && settled.screen !== expected) {
141018
141252
  const advice = atStop(angle) ? "Fold again if the device did not switch; a screenshot shows what it renders." : "To switch panels, fold to closed or open first, then to the angle wanted.";
141019
- warning = `The hinge was swept to ${targetLabel(params)}, but the device kept rendering to ${screenLabel(activeScreen, panels)} rather than switching to ${screenLabel(expected, panels)}. Commands target the panel the device renders to. ${advice}`;
141253
+ warning = `The hinge was swept to ${targetLabel2(params)}, but the device kept rendering to ${screenLabel(activeScreen, panels)} rather than switching to ${screenLabel(expected, panels)}. Commands target the panel the device renders to. ${advice}`;
141020
141254
  }
141021
141255
  return {
141022
141256
  activeScreen,
@@ -141027,6 +141261,36 @@ Fails if the device is not foldable or is a remote simulator.`,
141027
141261
  };
141028
141262
  }
141029
141263
  };
141264
+ var foldTool = {
141265
+ id: "fold",
141266
+ interaction: {
141267
+ startedMsg: ({ params }) => `Folding device to ${targetLabel2(params)}`,
141268
+ completedMsg: ({ params, result }) => "activeScreen" in result ? `Folded device to ${targetLabel2(params)}; it renders to screen ${result.activeScreen} (${result.screen.panel})` : `Folded device to ${targetLabel2(params)}; it is in device state ${result.deviceState}`,
141269
+ failedMsg: ({ params, failureSignal: failureSignal2 }) => `Failed to fold device to ${targetLabel2(params)}: ${failureSignal2.error_code}`
141270
+ },
141271
+ description: `Move the hinge of a foldable iOS simulator (the iPhone Duo) or Android emulator (a Pixel Fold or generic foldable AVD) to a \`posture\` (closed, half-open, open) or to an \`angle\` (0-180\xB0). The tool returns when the device accepts input again, so the next tap lands.
141272
+ Use when you must see or test an app in a different posture: closed shows the cover panel, half-open and open show the inner panel. \`list-devices\` marks foldable devices with \`foldable: true\`. To turn a device without a fold, use \`rotate\`.
141273
+ All tools use the active panel (the panel that shows the UI), also after a fold made outside argent. The coordinates and the screenshot size change with the panel. Before you tap, read the element tree in this result or run \`describe\` again.
141274
+ iOS: unfolded, the UI is landscape, as on a rotated iPhone. A fold between two angles that are not closed or open can keep the current panel. To change panels, fold to closed or open.
141275
+ iOS returns { activeScreen, screen: { id, panel, width, height }, posture?, hingeAngle, warning? }. screen names the active panel and gives its size in pixels. posture is set only at 0\xB0, 120\xB0 and 180\xB0. warning is set when argent cannot find the active panel, or when the device did not change to the expected panel.
141276
+ Android returns { posture?, deviceState, hingeAngle?, warning? }. deviceState is the state Android committed (CLOSED, HALF_OPENED, OPENED), and posture is the matching preset. A lock screen the fold raises is dismissed; warning is set when it cannot be, or when a lock screen is showing or the screen is off after the fold. On a generic foldable AVD, closed shows only the left part of the display and describe frames do not match tap coordinates; use a Pixel Fold AVD to test the cover panel.
141277
+ Fails if the device is not foldable, is a remote simulator or a physical Android device.`,
141278
+ searchHint: "fold unfold hinge foldable duo posture open closed half-open panel screen",
141279
+ zodSchema: zodSchema27,
141280
+ capability: capability21,
141281
+ // The Android hinge is moved through `adb`, so no simulator-server is
141282
+ // spawned for it; the server follows the new panel on its own.
141283
+ services: (params) => {
141284
+ const device = resolveDevice(params.udid);
141285
+ return device.platform === "android" ? {} : { simulatorServer: simulatorServerRef(device) };
141286
+ },
141287
+ execute: dispatchByPlatform({
141288
+ toolId: "fold",
141289
+ capability: capability21,
141290
+ ios: iosImpl5,
141291
+ android: androidImpl7
141292
+ })
141293
+ };
141030
141294
 
141031
141295
  // ../tool-server/src/tools/tv-remote/index.ts
141032
141296
  init_zod();
@@ -141039,11 +141303,11 @@ function expandButtons(button, repeat) {
141039
141303
  }
141040
141304
 
141041
141305
  // ../tool-server/src/tools/tv-remote/platforms/focus-remote.ts
141042
- async function pressFocusRemote(registry2, device, params, unsupported, options) {
141306
+ async function pressFocusRemote(registry2, device, params, unsupported2, options) {
141043
141307
  const buttons = expandButtons(params.button, params.repeat);
141044
141308
  const api = await resolveTvApi(registry2, device.id);
141045
- if (unsupported?.size) {
141046
- const bad = [...new Set(buttons.filter((b) => unsupported.has(b)))];
141309
+ if (unsupported2?.size) {
141310
+ const bad = [...new Set(buttons.filter((b) => unsupported2.has(b)))];
141047
141311
  if (bad.length) {
141048
141312
  throw new UnsupportedOperationError(
141049
141313
  "tv-remote",
@@ -146929,7 +147193,7 @@ Allowed tools and their args (udid is auto-injected, do NOT include it in args):
146929
147193
  paste: { text: string } (device clipboard + paste shortcut; only where a user would paste, e.g. an OTP; keyboard otherwise) [ios sim/android emu]
146930
147194
  rotate: { orientation: "Portrait"|"LandscapeLeft"|"LandscapeRight"|"PortraitUpsideDown" } [ios/android]
146931
147195
  shake: { count?: number } [ios sim/android emu]
146932
- fold: { posture?: "closed"|"half-open"|"open", angle?: 0-180 } (one of posture/angle; the next step lands on the new panel, whose coordinates are different) [foldable ios sim]
147196
+ fold: { posture?: "closed"|"half-open"|"open", angle?: 0-180 } (one of posture/angle; the next step lands on the new panel, whose coordinates are different) [foldable ios sim/android emu]
146933
147197
  tv-remote: { button: <remote button | array of them>, repeat?: number } [apple tv/android tv/vega]
146934
147198
  buttons: up/down/left/right/select/back/home/menu/playPause (+ rewind/fastForward/next/previous/volumeUp/volumeDown/mute work on Android TV and Vega; rejected on the Apple TV simulator)
146935
147199
  await-ui-element: { condition: "exists"|"visible"|"hidden"|"text", selector: {text?,identifier?,role?}, expectedText?, timeoutMs?, pollIntervalMs? } [ios/android/chromium]
@@ -149400,7 +149664,7 @@ function formatDescribeTree(root, opts) {
149400
149664
  }
149401
149665
 
149402
149666
  // ../tool-server/src/tools/describe/platforms/tv.ts
149403
- var sleep13 = (ms) => new Promise((r) => setTimeout(r, ms));
149667
+ var sleep14 = (ms) => new Promise((r) => setTimeout(r, ms));
149404
149668
  var EMPTY_RETRY_ATTEMPTS = 3;
149405
149669
  var EMPTY_RETRY_DELAY_MS = 600;
149406
149670
  var EMPTY_HINT = "No focusable elements after retrying and recycling the read path. The app is most likely still launching (splash / loading screen) or mid-transition \u2014 this is normal right after launch-app / restart-app. Wait ~2-3s and call describe again; a React Native app only exposes focus once its JS bundle has loaded. If it stays empty, take a screenshot to confirm what's actually on screen.";
@@ -149441,7 +149705,7 @@ async function describeTv(registry2, device) {
149441
149705
  let res = await api.describe();
149442
149706
  if (device.platform !== "android") {
149443
149707
  for (let attempt = 1; attempt < EMPTY_RETRY_ATTEMPTS && isEmpty(res); attempt++) {
149444
- await sleep13(EMPTY_RETRY_DELAY_MS);
149708
+ await sleep14(EMPTY_RETRY_DELAY_MS);
149445
149709
  res = await api.describe();
149446
149710
  }
149447
149711
  }
@@ -150025,12 +150289,12 @@ function adaptAndroidTree(capture, screen) {
150025
150289
  unsupportedFields: LEGACY_UNSUPPORTED_FIELDS2
150026
150290
  };
150027
150291
  if (!v2) return tree;
150028
- const unsupported = [];
150292
+ const unsupported2 = [];
150029
150293
  const sdk = capture.sdkInt ?? 0;
150030
- if (sdk < 26) unsupported.push("placeholder", "hintShowing");
150031
- if (sdk < 28) unsupported.push("heading");
150294
+ if (sdk < 26) unsupported2.push("placeholder", "hintShowing");
150295
+ if (sdk < 28) unsupported2.push("heading");
150032
150296
  if (capture.captureMode === "active-window") {
150033
- unsupported.push("keyboardVisible", "foregroundApp", "alertVisible");
150297
+ unsupported2.push("keyboardVisible", "foregroundApp", "alertVisible");
150034
150298
  } else {
150035
150299
  let keyboard = false;
150036
150300
  let foreground;
@@ -150056,7 +150320,7 @@ function adaptAndroidTree(capture, screen) {
150056
150320
  }
150057
150321
  }
150058
150322
  }
150059
- tree.unsupportedFields = unsupported;
150323
+ tree.unsupportedFields = unsupported2;
150060
150324
  return tree;
150061
150325
  }
150062
150326
  async function readAndroidUiTree(registry2, device) {
@@ -150253,7 +150517,7 @@ init_src2();
150253
150517
  var MAX_CLEAR_KEYS = 100;
150254
150518
  var XCTEST_DELETE = "\b";
150255
150519
  var KEY_TIMING = { holdMs: 20, settleMs: 300, typeDelayMs: 30 };
150256
- var sleep14 = (ms) => new Promise((r) => setTimeout(r, ms));
150520
+ var sleep15 = (ms) => new Promise((r) => setTimeout(r, ms));
150257
150521
  function typeableOnSimulator(text) {
150258
150522
  for (const char of text) if (!charToKeyPress(char)) return false;
150259
150523
  return true;
@@ -150278,9 +150542,9 @@ async function pressSimulatorBackspace(registry2, device) {
150278
150542
  const ref = simulatorServerRef(device);
150279
150543
  const api = await registry2.resolveService(ref.urn, ref.options);
150280
150544
  await api.pressKey("Down", NAMED_KEYS.backspace);
150281
- await sleep14(KEY_TIMING.holdMs);
150545
+ await sleep15(KEY_TIMING.holdMs);
150282
150546
  await api.pressKey("Up", NAMED_KEYS.backspace);
150283
- await sleep14(KEY_TIMING.settleMs);
150547
+ await sleep15(KEY_TIMING.settleMs);
150284
150548
  }
150285
150549
  async function typeOnSimulator(registry2, device, text) {
150286
150550
  if (!text) return;
@@ -154755,6 +155019,9 @@ async function runIosProfilerPipeline(files, options = {}) {
154755
155019
  return { bottlenecks, cpuSamples, uiHangs, cpuHotspots, memoryLeaks };
154756
155020
  }
154757
155021
 
155022
+ // ../tool-server/src/utils/ios-profiler/capture-strategy/select.ts
155023
+ var import_child_process3 = require("child_process");
155024
+
154758
155025
  // ../tool-server/src/utils/ios-profiler/capture-strategy/device.ts
154759
155026
  var deviceStrategy = {
154760
155027
  name: "device",
@@ -154810,7 +155077,6 @@ var allProcessesStrategy = {
154810
155077
  };
154811
155078
 
154812
155079
  // ../tool-server/src/utils/ios-profiler/capture-strategy/select.ts
154813
- var import_child_process3 = require("child_process");
154814
155080
  var ENV_OVERRIDE = "ARGENT_IOS_CAPTURE";
154815
155081
  function parseEnvOverride() {
154816
155082
  const original = process.env[ENV_OVERRIDE]?.trim();
@@ -164248,7 +164514,7 @@ var FOCUS_REPORTING_SOURCES = /* @__PURE__ */ new Set([
164248
164514
  "cdp-dom"
164249
164515
  ]);
164250
164516
  var SETTLE_POLL_MS2 = 250;
164251
- var SETTLE_TIMEOUT_MS2 = 3e3;
164517
+ var SETTLE_TIMEOUT_MS3 = 3e3;
164252
164518
  var SETTLE_MIN_READS = 2;
164253
164519
  var MAX_SCROLL_ITERATIONS = 25;
164254
164520
  var SCROLL_INCREMENT = 0.5;
@@ -164315,7 +164581,7 @@ function readFlowTree(env) {
164315
164581
  });
164316
164582
  }
164317
164583
  async function settleTree(env, opts = {}) {
164318
- const deadline = Date.now() + SETTLE_TIMEOUT_MS2;
164584
+ const deadline = Date.now() + SETTLE_TIMEOUT_MS3;
164319
164585
  let prevFp;
164320
164586
  let prevTree;
164321
164587
  let lastError;
@@ -165564,12 +165830,12 @@ function flowExecuteOutcome(result) {
165564
165830
  function runSequenceOutcome(result) {
165565
165831
  const steps = result.steps;
165566
165832
  if (!Array.isArray(steps)) return void 0;
165567
- const failed2 = steps.find((s) => isRecord(s) && typeof s.error === "string");
165568
- if (failed2 && isRecord(failed2)) {
165569
- const tool = typeof failed2.tool === "string" ? failed2.tool : "step";
165833
+ const failed3 = steps.find((s) => isRecord(s) && typeof s.error === "string");
165834
+ if (failed3 && isRecord(failed3)) {
165835
+ const tool = typeof failed3.tool === "string" ? failed3.tool : "step";
165570
165836
  return {
165571
165837
  status: "fail",
165572
- reason: `run-sequence stopped at ${tool} after ${count(result.completed)} of ${count(result.total)} steps: ${String(failed2.error)}`
165838
+ reason: `run-sequence stopped at ${tool} after ${count(result.completed)} of ${count(result.total)} steps: ${String(failed3.error)}`
165573
165839
  };
165574
165840
  }
165575
165841
  const total = count(result.total);
@@ -166574,7 +166840,7 @@ function isTerminalResponse(response) {
166574
166840
  var DEFAULT_SCRIPT_TIMEOUT_MS = 3e4;
166575
166841
  var SCRIPT_STEP_LOG_LIMIT_BYTES = 64 * 1024;
166576
166842
  var SCRIPT_RUN_LOG_LIMIT_BYTES = 256 * 1024;
166577
- var SETTLE_TIMEOUT_MS3 = 500;
166843
+ var SETTLE_TIMEOUT_MS4 = 500;
166578
166844
  var STOP_GRACE_MS2 = 1500;
166579
166845
  var CHILD_DEADLINE_MARGIN_MS = 2e3;
166580
166846
  var GROUP_POLL_MS = 50;
@@ -166985,7 +167251,7 @@ var FlowScriptExecutor = class {
166985
167251
  const deadlinePassed = Date.now() >= deadlineAt;
166986
167252
  clearTimeout(timer);
166987
167253
  request3.signal?.removeEventListener("abort", onAbort);
166988
- await Promise.race([closed, sleep(SETTLE_TIMEOUT_MS3)]);
167254
+ await Promise.race([closed, sleep(SETTLE_TIMEOUT_MS4)]);
166989
167255
  await stop();
166990
167256
  capture.end();
166991
167257
  child.stdout?.destroy();
@@ -167025,11 +167291,11 @@ function flowScriptExecutor() {
167025
167291
  }
167026
167292
  function classifyOutcome(input2) {
167027
167293
  const { exit } = input2;
167028
- if (input2.spawnProblem) return failed("spawn", input2.spawnProblem);
167029
- if (input2.protocolProblem) return failed("protocol", input2.protocolProblem);
167294
+ if (input2.spawnProblem) return failed2("spawn", input2.spawnProblem);
167295
+ if (input2.protocolProblem) return failed2("protocol", input2.protocolProblem);
167030
167296
  if (input2.terminal) {
167031
167297
  if (input2.terminal.type === "failure") {
167032
- return failed(
167298
+ return failed2(
167033
167299
  input2.terminal.failureType,
167034
167300
  clampText(input2.terminal.message, SCRIPT_MAX_FAILURE_MESSAGE_CHARS),
167035
167301
  clampText(input2.terminal.stack, SCRIPT_MAX_FAILURE_STACK_CHARS)
@@ -167038,26 +167304,26 @@ function classifyOutcome(input2) {
167038
167304
  return commitOutput(input2.terminal.outputJson);
167039
167305
  }
167040
167306
  if (input2.interrupted === "cancelled") {
167041
- return failed("cancelled", "The run was cancelled and the script process was stopped.");
167307
+ return failed2("cancelled", "The run was cancelled and the script process was stopped.");
167042
167308
  }
167043
167309
  if (input2.interrupted === "timeout") return timedOut(input2.timeoutMs);
167044
167310
  if (isHeapAbort(exit, input2.heapFatalSeen)) {
167045
- return failed("heap", `The script exceeded its ${input2.heapLimitMb} MiB heap limit.`);
167311
+ return failed2("heap", `The script exceeded its ${input2.heapLimitMb} MiB heap limit.`);
167046
167312
  }
167047
167313
  if (!input2.startedSeen) {
167048
- return failed(
167314
+ return failed2(
167049
167315
  "protocol",
167050
167316
  `The script runner exited before it started the script (${describeExit(exit)}).`
167051
167317
  );
167052
167318
  }
167053
167319
  if (exit.signal) {
167054
167320
  if (input2.deadlinePassed) return timedOut(input2.timeoutMs);
167055
- return failed(
167321
+ return failed2(
167056
167322
  "signal",
167057
167323
  `The script process was killed by ${exit.signal} before it returned output. It did not stop itself.`
167058
167324
  );
167059
167325
  }
167060
- return failed(
167326
+ return failed2(
167061
167327
  "exit",
167062
167328
  `The script stopped its own process with exit code ${exit.code ?? 0} instead of returning; no output was captured.`
167063
167329
  );
@@ -167067,7 +167333,7 @@ function redactSecrets(verdict, secrets) {
167067
167333
  if (verdict.output) {
167068
167334
  const collision = scrubDocument(verdict.output, secrets);
167069
167335
  if (collision) {
167070
- return failed(
167336
+ return failed2(
167071
167337
  "output",
167072
167338
  `Two keys in the script's output become "${collision}" once the secret in them is redacted, so one would silently replace the other. Rename one of them.`
167073
167339
  );
@@ -167114,7 +167380,7 @@ function scrubDocument(root, secrets) {
167114
167380
  function commitOutput(outputJson) {
167115
167381
  const bytes = Buffer.byteLength(outputJson, "utf8");
167116
167382
  if (bytes > SCRIPT_MAX_OUTPUT_BYTES) {
167117
- return failed(
167383
+ return failed2(
167118
167384
  "output",
167119
167385
  `The script returned ${describeBytes(bytes)} of encoded output; the limit is ${describeBytes(SCRIPT_MAX_OUTPUT_BYTES)}.`
167120
167386
  );
@@ -167123,14 +167389,14 @@ function commitOutput(outputJson) {
167123
167389
  try {
167124
167390
  parsed = JSON.parse(outputJson);
167125
167391
  } catch (err) {
167126
- return failed("output", `The script's output did not parse: ${errorMessage(err)}`);
167392
+ return failed2("output", `The script's output did not parse: ${errorMessage(err)}`);
167127
167393
  }
167128
167394
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
167129
- return failed("output", "The script's output was not an object.");
167395
+ return failed2("output", "The script's output was not an object.");
167130
167396
  }
167131
167397
  const polluted = findOwnProtoKey(parsed);
167132
167398
  if (polluted !== void 0) {
167133
- return failed(
167399
+ return failed2(
167134
167400
  "output",
167135
167401
  `${polluted} has an own "__proto__" key; output must be JSON-compatible data.`
167136
167402
  );
@@ -167191,12 +167457,12 @@ function clampText(text, max) {
167191
167457
  return marked;
167192
167458
  }
167193
167459
  function timedOut(timeoutMs) {
167194
- return failed(
167460
+ return failed2(
167195
167461
  "timeout",
167196
167462
  `The script did not finish within its ${describeDuration(timeoutMs)} time limit and its process tree was stopped.`
167197
167463
  );
167198
167464
  }
167199
- function failed(kind, message, stack) {
167465
+ function failed2(kind, message, stack) {
167200
167466
  return { ok: false, failure: { kind, message, ...stack ? { stack } : {} } };
167201
167467
  }
167202
167468
  function isHeapAbort(exit, heapFatalSeen) {
@@ -171297,13 +171563,13 @@ async function frontChromiumPage(registry2, device) {
171297
171563
  }
171298
171564
  function summarize(flowName, deviceId, executionPrerequisite, steps, aborted2, timing) {
171299
171565
  let passed = 0;
171300
- let failed2 = 0;
171566
+ let failed3 = 0;
171301
171567
  let skipped = 0;
171302
171568
  let errored = 0;
171303
171569
  for (const s of steps) {
171304
171570
  if (s.kind === "echo") continue;
171305
171571
  if (s.status === "pass") passed++;
171306
- else if (s.status === "fail") failed2++;
171572
+ else if (s.status === "fail") failed3++;
171307
171573
  else if (s.status === "skip") skipped++;
171308
171574
  else errored++;
171309
171575
  }
@@ -171315,10 +171581,10 @@ function summarize(flowName, deviceId, executionPrerequisite, steps, aborted2, t
171315
171581
  // (no fail/error report), so the verdict folds the abort in directly. A
171316
171582
  // skip by itself is NOT a failure: an unmet `when:` guard skips its block
171317
171583
  // as a successful omission.
171318
- ok: failed2 === 0 && errored === 0 && !aborted2,
171584
+ ok: failed3 === 0 && errored === 0 && !aborted2,
171319
171585
  ...aborted2 ? { aborted: true } : {},
171320
171586
  passed,
171321
- failed: failed2,
171587
+ failed: failed3,
171322
171588
  skipped,
171323
171589
  errored,
171324
171590
  steps,
@@ -171621,9 +171887,10 @@ async function runFold(state3, step) {
171621
171887
  const result = await invokeSubTool(registry2, ctx, "fold", args);
171622
171888
  const size = result.screen?.width !== void 0 && result.screen?.height !== void 0 ? ` ${result.screen.width}x${result.screen.height}` : "";
171623
171889
  const target = step.posture ?? `${step.angle}\xB0`;
171890
+ const landed = result.deviceState !== void 0 ? `device state ${result.deviceState}` : `screen ${result.activeScreen ?? "?"} (${result.screen?.panel ?? "?"}${size})`;
171624
171891
  return {
171625
171892
  ok: true,
171626
- reason: `${target}: screen ${result.activeScreen ?? "?"} (${result.screen?.panel ?? "?"}${size})`,
171893
+ reason: `${target}: ${landed}`,
171627
171894
  ...result.warning !== void 0 ? { warning: result.warning } : {}
171628
171895
  };
171629
171896
  } catch (err) {
@@ -172527,7 +172794,7 @@ var updateArgentTool = {
172527
172794
  });
172528
172795
  child.unref();
172529
172796
  }, 2e3);
172530
- const targetLabel2 = effectiveTarget === "both" ? "global and project-local installs" : `${effectiveTarget} install`;
172797
+ const targetLabel3 = effectiveTarget === "both" ? "global and project-local installs" : `${effectiveTarget} install`;
172531
172798
  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.` : "";
172532
172799
  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.` : "";
172533
172800
  const versionInfo = targetsOnlyRunningInstall ? `(v${currentVersion} -> v${installableVersion}) ` : "";
@@ -172535,7 +172802,7 @@ var updateArgentTool = {
172535
172802
  const coversRunningInstall = targetsOnlyRunningInstall || effectiveTarget === "both";
172536
172803
  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.`;
172537
172804
  return {
172538
- message: `Argent update initiated ${versionInfo}for the ${targetLabel2}.` + crossTargetNote + restartNote + `${otherHint}${bothDegradedNote}`
172805
+ message: `Argent update initiated ${versionInfo}for the ${targetLabel3}.` + crossTargetNote + restartNote + `${otherHint}${bothDegradedNote}`
172539
172806
  };
172540
172807
  }
172541
172808
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swmansion/argent",
3
- "version": "0.29.1-next.13",
3
+ "version": "0.29.1-next.15",
4
4
  "mcpName": "io.github.software-mansion/argent",
5
5
  "description": "MCP server for iOS Simulator and Android Emulator control",
6
6
  "license": "Apache-2.0",
@@ -111,12 +111,12 @@ Scopes can combine and nest, with at most six scope keys. Use strict selectors f
111
111
 
112
112
  Directives stop the flow on failure and skip later steps. The available directives are `launch`, `tap`, `long-press`, `swipe`, `type`, `scroll-to`, `pinch`, `rotate`, `fold`, `await`, `assert`, `wait`, `snapshot`, `run`, `script`, `when`, `echo`, and `tool`.
113
113
 
114
- `fold` folds or unfolds a foldable iOS simulator. Write a posture (`fold: closed`, `fold: half-open`, `fold: open`) or an angle from 0 to 180 (`fold: 120`). The step waits until the device accepts input again. A recorded `fold` tool call becomes a `fold:` step. After a `fold` step:
114
+ `fold` folds or unfolds a foldable iOS simulator or Android emulator. Write a posture (`fold: closed`, `fold: half-open`, `fold: open`) or an angle from 0 to 180 (`fold: 120`). The step waits until the device accepts input again. A recorded `fold` tool call becomes a `fold:` step. After a `fold` step:
115
115
 
116
116
  - The coordinates change with the panel. Selectors resolve against a new tree.
117
117
  - A `snapshot` baseline is valid only for the posture that made it.
118
- - Unfolded, the UI is landscape. `swipe` and `scroll-to` directions and the reading order stay as the user sees the UI. Coordinates stay in the space of the `describe` frames.
119
- - A fold between two angles that are not `closed` or `open` can keep the current panel. The step passes, and the report names the panel. To change panels, fold to `closed` or `open`.
118
+ - iOS: unfolded, the UI is landscape. `swipe` and `scroll-to` directions and the reading order stay as the user sees the UI. Coordinates stay in the space of the `describe` frames.
119
+ - iOS: a fold between two angles that are not `closed` or `open` can keep the current panel. The step passes, and the report names the panel. To change panels, fold to `closed` or `open`.
120
120
 
121
121
  Use the launch map for cross-platform flows. A bare launch applies everywhere and becomes an app path on Chromium. The map takes `native:`, `ios:`, `android:`, `vega:`, and `chromium:`. `native:` is one id shared by iOS, Android, and Vega, and a per-platform key overrides it for that platform. `chromium:` accepts a relative or absolute app path. `ios:` also accepts `{ app, args }`; a simulator passes `args` to the app at launch. A launch that declares no id for the run's platform is an error, not a cue to switch platforms. A run on a remote simulator uses the `ios:` id, or the `native:` id when the map has no `ios:` key, so no flow needs a key for a remote run. On iOS, a successful launch also pins later tree reads to that app until the next raw `tool:` step, so read [The runner tree is not the discovery tree](#the-runner-tree-is-not-the-discovery-tree) when a read describes the wrong screen.
122
122
 
@@ -67,7 +67,7 @@ Common schemes: `messages://`, `settings://`, `maps://?q=<query>`, `tel://<numbe
67
67
  | Type text | `keyboard` | Every platform. Text or one named key per call, never both |
68
68
  | Paste text | `paste` | Only where a user would paste (OTP code, long link). Sim/emu only |
69
69
  | Rotate device | `rotate` | Orientation changes |
70
- | Fold device | `fold` | Foldable iOS simulator: closed / half-open / open, or an angle |
70
+ | Fold device | `fold` | Foldable simulator/emulator: closed / half-open / open, or angle |
71
71
  | Shake device | `shake` | Shake handlers (sim/emu only), Undo-typing prompt, RN dev menu |
72
72
  | Wait for UI | `await-ui-element` | Block until an element is visible/hidden/exists/contains text |
73
73
  | Wait for idle | `await-screen-idle` | Block until a non-empty screen tree stops changing |
@@ -201,7 +201,7 @@ Values: `Portrait`, `LandscapeLeft`, `LandscapeRight`, `PortraitUpsideDown`
201
201
 
202
202
  On an unfolded foldable simulator, the value sets the orientation of the device, not of the UI. `Portrait` gives a landscape UI. `LandscapeLeft` gives a portrait UI.
203
203
 
204
- ### fold: Fold or unfold a foldable simulator
204
+ ### fold: Fold or unfold a foldable device
205
205
 
206
206
  ```json
207
207
  { "udid": "<UDID>", "posture": "open" }
@@ -209,7 +209,7 @@ On an unfolded foldable simulator, the value sets the orientation of the device,
209
209
 
210
210
  Give `posture` (`closed`, `half-open` or `open`) or `angle` (0–180). Do not give both.
211
211
 
212
- Use `fold` only on a foldable iOS simulator. `list-devices` marks it with `foldable: true`, for example the iPhone Duo. Other devices reject the call.
212
+ Use `fold` only on a foldable iOS simulator (the iPhone Duo) or Android emulator (a Pixel Fold or generic foldable AVD). `list-devices` marks them with `foldable: true`. Other devices reject the call.
213
213
 
214
214
  Closed, the cover panel shows the UI. Half-open and open, the inner panel shows the UI. All tools use the active panel (the panel that shows the UI), also after a fold made outside argent. `fold` returns when the device accepts input again, so the next tap lands. This is also true for the next step in `run-sequence`.
215
215
 
@@ -217,10 +217,12 @@ Rules:
217
217
 
218
218
  - The coordinates change with the panel. Before you tap, read the element tree in the `fold` result, or run `describe` again. Do not use frames from before the fold.
219
219
  - The screenshot size changes with the panel. Keep one screenshot-diff baseline for each posture.
220
- - Unfolded, the UI is landscape. The screenshot shows the UI turned 90 degrees, and the `describe` frames use the same axes, as on a rotated iPhone.
221
- - A fold between two angles that are not 0 or 180 can keep the current panel. The result names the active panel. To change panels, fold to `closed` or `open`.
220
+ - iOS: unfolded, the UI is landscape. The screenshot shows the UI turned 90 degrees, and the `describe` frames use the same axes, as on a rotated iPhone.
221
+ - iOS: a fold between two angles that are not 0 or 180 can keep the current panel. The result names the active panel. To change panels, fold to `closed` or `open`.
222
222
  - Fold between gestures, not during a gesture. A gesture stays on the panel where it started.
223
- - If argent cannot find the active panel, it uses the cover panel. The tool result then has a `warning`, and a screenshot-diff summary has a `panel:` line. Take a screenshot to see what the device shows, then do the check that the warning gives.
223
+ - Android: the result gives `deviceState` (`CLOSED`, `HALF_OPENED`, `OPENED`). A lock screen that the fold to closed shows is dismissed; a `warning` says when it cannot be, or when a lock screen is up after the fold. Unlock the device before the next action.
224
+ - Android: only a Pixel Fold AVD has a cover panel. When a generic foldable AVD is closed, the emulator shows only the left part of the display. `describe` frames do not match tap coordinates. Argent can reliably tap only an element that covers the visible part, for example a full-width row. Use a Pixel Fold AVD to test the cover panel.
225
+ - iOS: if argent cannot find the active panel, it uses the cover panel. The tool result then has a `warning`, and a screenshot-diff summary has a `panel:` line. Take a screenshot to see what the device shows, then do the check that the warning gives.
224
226
 
225
227
  ### await-ui-element: Block until a UI element reaches a state
226
228