@swmansion/argent 0.29.1-next.14 → 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.14";
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.14";
16782
+ return "0.29.1-next.15";
16778
16783
  }
16779
16784
  return "0.0.0";
16780
16785
  }
@@ -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;
@@ -1325,6 +1325,11 @@ var init_failure_codes = __esm({
1325
1325
  // a device that is not foldable, no hinge route, a remote simulator.
1326
1326
  IOS_FOLD_FAILED: "IOS_FOLD_FAILED",
1327
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",
1328
1333
  PASTE_CLIPBOARD_SET_FAILED: "PASTE_CLIPBOARD_SET_FAILED",
1329
1334
  PASTE_CLIPBOARD_UNSUPPORTED: "PASTE_CLIPBOARD_UNSUPPORTED",
1330
1335
  SETTINGS_PERMISSION_UNSUPPORTED: "SETTINGS_PERMISSION_UNSUPPORTED",
@@ -82959,6 +82964,7 @@ __export(adb_exports, {
82959
82964
  getCachedAndroidRuntimeKind: () => getCachedAndroidRuntimeKind,
82960
82965
  hasDefaultBootSnapshot: () => hasDefaultBootSnapshot,
82961
82966
  isAndroidTv: () => isAndroidTv,
82967
+ isFoldableAvd: () => isFoldableAvd,
82962
82968
  isTerminalAdbError: () => isTerminalAdbError,
82963
82969
  listAndroidDevices: () => listAndroidDevices,
82964
82970
  listAvds: () => listAvds,
@@ -83310,6 +83316,17 @@ async function resolveAvdPath(avdName) {
83310
83316
  }
83311
83317
  return null;
83312
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
+ }
83313
83330
  async function hasDefaultBootSnapshot(avdName) {
83314
83331
  const { stat: stat7 } = await import("node:fs/promises");
83315
83332
  const avdPath = await resolveAvdPath(avdName);
@@ -90386,16 +90403,16 @@ var require_foldFlowLines = __commonJS({
90386
90403
  onFold();
90387
90404
  let res = text.slice(0, folds[0]);
90388
90405
  for (let i2 = 0; i2 < folds.length; ++i2) {
90389
- const fold = folds[i2];
90406
+ const fold2 = folds[i2];
90390
90407
  const end2 = folds[i2 + 1] || text.length;
90391
- if (fold === 0)
90408
+ if (fold2 === 0)
90392
90409
  res = `
90393
90410
  ${indent}${text.slice(0, end2)}`;
90394
90411
  else {
90395
- if (mode === FOLD_QUOTED && escapedFolds[fold])
90396
- res += `${text[fold]}\\`;
90412
+ if (mode === FOLD_QUOTED && escapedFolds[fold2])
90413
+ res += `${text[fold2]}\\`;
90397
90414
  res += `
90398
- ${indent}${text.slice(fold + 1, end2)}`;
90415
+ ${indent}${text.slice(fold2 + 1, end2)}`;
90399
90416
  }
90400
90417
  }
90401
90418
  return res;
@@ -94034,8 +94051,8 @@ var require_resolve_flow_scalar = __commonJS({
94034
94051
  if (ch === "\r" && source[i + 1] === "\n")
94035
94052
  continue;
94036
94053
  if (ch === "\n") {
94037
- const { fold, offset } = foldNewline(source, i);
94038
- res += fold;
94054
+ const { fold: fold2, offset } = foldNewline(source, i);
94055
+ res += fold2;
94039
94056
  i = offset;
94040
94057
  } else if (ch === "\\") {
94041
94058
  let next = source[++i];
@@ -94075,19 +94092,19 @@ var require_resolve_flow_scalar = __commonJS({
94075
94092
  return res;
94076
94093
  }
94077
94094
  function foldNewline(source, offset) {
94078
- let fold = "";
94095
+ let fold2 = "";
94079
94096
  let ch = source[offset + 1];
94080
94097
  while (ch === " " || ch === " " || ch === "\n" || ch === "\r") {
94081
94098
  if (ch === "\r" && source[offset + 2] !== "\n")
94082
94099
  break;
94083
94100
  if (ch === "\n")
94084
- fold += "\n";
94101
+ fold2 += "\n";
94085
94102
  offset += 1;
94086
94103
  ch = source[offset + 1];
94087
94104
  }
94088
- if (!fold)
94089
- fold = " ";
94090
- return { fold, offset };
94105
+ if (!fold2)
94106
+ fold2 = " ";
94107
+ return { fold: fold2, offset };
94091
94108
  }
94092
94109
  var escapeCodes = {
94093
94110
  "0": "\0",
@@ -100213,7 +100230,7 @@ var _CI_VENDOR_COUNT_FOR_TEST = vendors_default.length;
100213
100230
  var SESSION_ID = (0, import_node_crypto3.randomUUID)();
100214
100231
  function readCliVersion() {
100215
100232
  if (true) {
100216
- return "0.29.1-next.14";
100233
+ return "0.29.1-next.15";
100217
100234
  }
100218
100235
  return "0.0.0";
100219
100236
  }
@@ -124990,7 +125007,7 @@ Android entries also carry a 'kind' ('emulator' for a local AVD, 'device' for a
124990
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.
124991
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.
124992
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).
124993
- 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.
124994
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.
124995
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.`,
124996
125013
  alwaysLoad: true,
@@ -125054,6 +125071,11 @@ Booted/ready devices are listed first. Platforms whose CLI is unavailable are si
125054
125071
  tunnelState: d.tunnelState
125055
125072
  }));
125056
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
+ );
125057
125079
  const androidTagged = android.map((d) => ({
125058
125080
  platform: "android",
125059
125081
  serial: d.serial,
@@ -125063,7 +125085,8 @@ Booted/ready devices are listed first. Platforms whose CLI is unavailable are si
125063
125085
  model: d.model,
125064
125086
  avdName: d.avdName,
125065
125087
  sdkLevel: d.sdkLevel,
125066
- runtimeKind: d.runtimeKind
125088
+ runtimeKind: d.runtimeKind,
125089
+ ...d.avdName !== null && foldableAvds.has(d.avdName) ? { foldable: true } : {}
125067
125090
  }));
125068
125091
  const vvdShadowSerials = await resolveVvdShadowAdbSerials(androidTagged, vega);
125069
125092
  const shadowSerials = /* @__PURE__ */ new Set([...vvdShadowSerials, ...externalShadows]);
@@ -125079,7 +125102,14 @@ Booted/ready devices are listed first. Platforms whose CLI is unavailable are si
125079
125102
  ...external.map(toListedExternal)
125080
125103
  ];
125081
125104
  devices.sort((a, b) => readinessRank(a) - readinessRank(b));
125082
- 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
+ };
125083
125113
  }
125084
125114
  };
125085
125115
 
@@ -130408,11 +130438,11 @@ function scheduleGroupSigkill(pid, graceMs, opts) {
130408
130438
  }
130409
130439
  async function pollPidsUntilGone(pids, opts) {
130410
130440
  const isAlive2 = opts.isAlive ?? pidIsAlive;
130411
- const sleep15 = opts.sleep ?? ((ms) => new Promise((resolve15) => setTimeout(resolve15, ms)));
130441
+ const sleep16 = opts.sleep ?? ((ms) => new Promise((resolve15) => setTimeout(resolve15, ms)));
130412
130442
  const maxPolls = Math.max(1, Math.ceil(opts.timeoutMs / opts.pollIntervalMs));
130413
130443
  let remaining = pids.filter((pid) => isAlive2(pid));
130414
130444
  for (let poll = 0; poll < maxPolls && remaining.length > 0; poll += 1) {
130415
- await sleep15(opts.pollIntervalMs);
130445
+ await sleep16(opts.pollIntervalMs);
130416
130446
  remaining = remaining.filter((pid) => isAlive2(pid));
130417
130447
  }
130418
130448
  return remaining;
@@ -131038,7 +131068,7 @@ function transportFailureSignal(kind) {
131038
131068
  failure_stage: "ios_device_runner_transport",
131039
131069
  failure_area: "tool_server"
131040
131070
  };
131041
- const failed2 = {
131071
+ const failed3 = {
131042
131072
  ...notReady,
131043
131073
  error_code: FAILURE_CODES.IOS_DEVICE_RUNNER_TRANSPORT_FAILED
131044
131074
  };
@@ -131048,11 +131078,11 @@ function transportFailureSignal(kind) {
131048
131078
  case "runner-not-listening":
131049
131079
  return { ...notReady, error_kind: "network", network_failure: "connection_refused" };
131050
131080
  case "timeout":
131051
- return { ...failed2, error_kind: "timeout" };
131081
+ return { ...failed3, error_kind: "timeout" };
131052
131082
  case "protocol":
131053
- return { ...failed2, error_kind: "network", network_failure: "invalid_response" };
131083
+ return { ...failed3, error_kind: "network", network_failure: "invalid_response" };
131054
131084
  case "http":
131055
- return { ...failed2, error_kind: "network", network_failure: "other" };
131085
+ return { ...failed3, error_kind: "network", network_failure: "other" };
131056
131086
  }
131057
131087
  }
131058
131088
  function createRunnerClient(options) {
@@ -131808,7 +131838,7 @@ function createConsoleLogServer(consoleEvents, logWriter) {
131808
131838
  });
131809
131839
  }
131810
131840
  function makeStubSourceResolver() {
131811
- const unsupported = () => {
131841
+ const unsupported2 = () => {
131812
131842
  throw new Error(
131813
131843
  "SourceResolver is not implemented on Chromium debugger sessions \u2014 Metro symbolicate is the only backing implementation."
131814
131844
  );
@@ -131816,7 +131846,7 @@ function makeStubSourceResolver() {
131816
131846
  return {
131817
131847
  resolveDebugStack: async () => null,
131818
131848
  symbolicate: async () => null,
131819
- readSourceFragment: unsupported
131849
+ readSourceFragment: unsupported2
131820
131850
  };
131821
131851
  }
131822
131852
  var chromiumJsRuntimeDebuggerBlueprint = {
@@ -137095,8 +137125,8 @@ var vegaImpl3 = {
137095
137125
  const output2 = `${stdout}
137096
137126
  ${stderr}`;
137097
137127
  const succeeded = /\.\.\.\s*success\b/i.test(output2);
137098
- const failed2 = /\.\.\.\s*failed\b/i.test(output2);
137099
- if (!succeeded || failed2) {
137128
+ const failed3 = /\.\.\.\s*failed\b/i.test(output2);
137129
+ if (!succeeded || failed3) {
137100
137130
  throw new FailureError(`vega install-app failed: ${output2.trim()}`, {
137101
137131
  error_code: FAILURE_CODES.VEGA_CLI_COMMAND_FAILED,
137102
137132
  failure_stage: "vega_reinstall_install_app",
@@ -137780,10 +137810,10 @@ async function readAndroidSurfaceRotation(serial, peek) {
137780
137810
  } catch {
137781
137811
  }
137782
137812
  }
137783
- for (const shell of [adbServerShell, adbShell]) {
137813
+ for (const shell2 of [adbServerShell, adbShell]) {
137784
137814
  for (const { cmd, pattern } of ROTATION_PROBES) {
137785
137815
  try {
137786
- const out = await shell(serial, cmd, { timeoutMs: 5e3 });
137816
+ const out = await shell2(serial, cmd, { timeoutMs: 5e3 });
137787
137817
  const value = Number(pattern.exec(out)?.[1]);
137788
137818
  if (Number.isInteger(value) && isSurfaceRotation(value)) return value;
137789
137819
  } catch {
@@ -140662,6 +140692,20 @@ var iosRemoteImpl6 = {
140662
140692
  // ../tool-server/src/tools/shake/platforms/android.ts
140663
140693
  init_src2();
140664
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
140665
140709
  var SHAKE_AMPLITUDE = 30;
140666
140710
  var SWINGS_PER_SHAKE = 8;
140667
140711
  var SWING_INTERVAL_MS = 50;
@@ -140688,15 +140732,6 @@ function restingVectorFrom(stdout) {
140688
140732
  }
140689
140733
  var fmt = (v) => `${v.x.toFixed(4)}:${v.y.toFixed(4)}:${v.z.toFixed(4)}`;
140690
140734
  var sleep12 = (ms) => new Promise((r) => setTimeout(r, ms));
140691
- async function emuConsole(serial, args) {
140692
- const { stdout } = await runAdb(["-s", serial, "emu", ...args], { timeoutMs: 1e4 });
140693
- const lines = stdout.split("\n").map((line) => line.trim()).filter(Boolean);
140694
- const verdict = lines[lines.length - 1];
140695
- if (verdict?.startsWith("KO:")) {
140696
- throw new Error(`emulator console rejected \`${args.join(" ")}\` \u2014 ${verdict}`);
140697
- }
140698
- return stdout;
140699
- }
140700
140735
  var inFlightByDevice = /* @__PURE__ */ new Map();
140701
140736
  function withDeviceLock(serial, fn) {
140702
140737
  const previous = inFlightByDevice.get(serial) ?? Promise.resolve();
@@ -140733,12 +140768,12 @@ var androidImpl6 = {
140733
140768
  );
140734
140769
  }
140735
140770
  const setAcceleration = async (v) => {
140736
- await emuConsole(serial, ["sensor", "set", "acceleration", fmt(v)]);
140771
+ await runEmuConsole(serial, ["sensor", "set", "acceleration", fmt(v)]);
140737
140772
  };
140738
140773
  return withDeviceLock(serial, async () => {
140739
140774
  let rest;
140740
140775
  try {
140741
- rest = restingVectorFrom(await emuConsole(serial, ["sensor", "get", "acceleration"]));
140776
+ rest = restingVectorFrom(await runEmuConsole(serial, ["sensor", "get", "acceleration"]));
140742
140777
  } catch (err) {
140743
140778
  const detail = err instanceof Error ? err.message : String(err);
140744
140779
  throw new FailureError(
@@ -140837,13 +140872,270 @@ Only phone/tablet simulators and emulators are supported.`,
140837
140872
  init_zod();
140838
140873
  init_src2();
140839
140874
  init_device_info();
140875
+
140876
+ // ../tool-server/src/tools/fold/platforms/android.ts
140877
+ init_src2();
140878
+ init_adb();
140840
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"];
140841
141131
  var POSTURE_ANGLE = { "closed": 0, "half-open": 120, "open": 180 };
140842
141132
  var HINGE_REQUEST_TIMEOUT_MS = 3e4;
140843
141133
  var zodSchema27 = external_exports.object({
140844
- udid: external_exports.string().describe("Target device id from `list-devices` (a foldable iOS simulator UDID)."),
140845
- posture: external_exports.enum(POSTURES).optional().describe(
140846
- "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`."
140847
141139
  ),
140848
141140
  angle: external_exports.number().min(0).max(180).optional().describe(
140849
141141
  "The hinge angle in degrees, 0 (closed) to 180 (open). Exactly one of `posture` and `angle`."
@@ -140855,10 +141147,14 @@ var capability21 = {
140855
141147
  // Local simulators only. A remote (MoQ) simulator is driven on its main
140856
141148
  // screen and carries no hinge message; the server itself rejects a device
140857
141149
  // that is not foldable, with its own reason.
140858
- 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 }
140859
141155
  };
140860
141156
  function presetAtAngle(angle) {
140861
- return POSTURES.find((p) => POSTURE_ANGLE[p] === angle);
141157
+ return POSTURES2.find((p) => POSTURE_ANGLE[p] === angle);
140862
141158
  }
140863
141159
  function classifyHingeRefusal(err, known) {
140864
141160
  if (known?.foldable !== false) return err;
@@ -140880,7 +141176,7 @@ function hingeRequest(params) {
140880
141176
  if (params.angle !== void 0) request3.angle = params.angle;
140881
141177
  return request3;
140882
141178
  }
140883
- function targetLabel(params) {
141179
+ function targetLabel2(params) {
140884
141180
  return params.posture ?? `${params.angle}\xB0`;
140885
141181
  }
140886
141182
  function targetAngle(params) {
@@ -140906,26 +141202,8 @@ function screenResult(activeScreen, panels) {
140906
141202
  ...panel ? { width: panel.width, height: panel.height } : {}
140907
141203
  };
140908
141204
  }
140909
- var foldTool = {
140910
- id: "fold",
140911
- interaction: {
140912
- startedMsg: ({ params }) => `Folding device to ${targetLabel(params)}`,
140913
- completedMsg: ({ params, result }) => `Folded device to ${targetLabel(params)}; it renders to screen ${result.activeScreen} (${result.screen.panel})`,
140914
- failedMsg: ({ params, failureSignal: failureSignal2 }) => `Failed to fold device to ${targetLabel(params)}: ${failureSignal2.error_code}`
140915
- },
140916
- 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.
140917
- 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\`.
140918
- 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.
140919
- A fold between two angles that are not closed or open can keep the current panel. To change panels, fold to closed or open.
140920
- 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.
140921
- Fails if the device is not foldable or is a remote simulator.`,
140922
- searchHint: "fold unfold hinge foldable duo posture open closed half-open panel screen",
140923
- zodSchema: zodSchema27,
140924
- capability: capability21,
140925
- services: (params) => ({
140926
- simulatorServer: simulatorServerRef(resolveDevice(params.udid))
140927
- }),
140928
- async execute(services, params, ctx) {
141205
+ var iosImpl5 = {
141206
+ async handler(services, params, _device, ctx) {
140929
141207
  const api = services.simulatorServer;
140930
141208
  const udid = params.udid;
140931
141209
  const signal = ctx?.signal ? AbortSignal.any([ctx.signal, AbortSignal.timeout(HINGE_REQUEST_TIMEOUT_MS)]) : AbortSignal.timeout(HINGE_REQUEST_TIMEOUT_MS);
@@ -140972,7 +141250,7 @@ Fails if the device is not foldable or is a remote simulator.`,
140972
141250
  warning = `${unresolvedPanelNote(udid, reason2, "commands target", panels)} Take a screenshot to see the screen.`;
140973
141251
  } else if (expected !== void 0 && settled.screen !== expected) {
140974
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.";
140975
- 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}`;
140976
141254
  }
140977
141255
  return {
140978
141256
  activeScreen,
@@ -140983,6 +141261,36 @@ Fails if the device is not foldable or is a remote simulator.`,
140983
141261
  };
140984
141262
  }
140985
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
+ };
140986
141294
 
140987
141295
  // ../tool-server/src/tools/tv-remote/index.ts
140988
141296
  init_zod();
@@ -140995,11 +141303,11 @@ function expandButtons(button, repeat) {
140995
141303
  }
140996
141304
 
140997
141305
  // ../tool-server/src/tools/tv-remote/platforms/focus-remote.ts
140998
- async function pressFocusRemote(registry2, device, params, unsupported, options) {
141306
+ async function pressFocusRemote(registry2, device, params, unsupported2, options) {
140999
141307
  const buttons = expandButtons(params.button, params.repeat);
141000
141308
  const api = await resolveTvApi(registry2, device.id);
141001
- if (unsupported?.size) {
141002
- 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)))];
141003
141311
  if (bad.length) {
141004
141312
  throw new UnsupportedOperationError(
141005
141313
  "tv-remote",
@@ -146885,7 +147193,7 @@ Allowed tools and their args (udid is auto-injected, do NOT include it in args):
146885
147193
  paste: { text: string } (device clipboard + paste shortcut; only where a user would paste, e.g. an OTP; keyboard otherwise) [ios sim/android emu]
146886
147194
  rotate: { orientation: "Portrait"|"LandscapeLeft"|"LandscapeRight"|"PortraitUpsideDown" } [ios/android]
146887
147195
  shake: { count?: number } [ios sim/android emu]
146888
- 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]
146889
147197
  tv-remote: { button: <remote button | array of them>, repeat?: number } [apple tv/android tv/vega]
146890
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)
146891
147199
  await-ui-element: { condition: "exists"|"visible"|"hidden"|"text", selector: {text?,identifier?,role?}, expectedText?, timeoutMs?, pollIntervalMs? } [ios/android/chromium]
@@ -149356,7 +149664,7 @@ function formatDescribeTree(root, opts) {
149356
149664
  }
149357
149665
 
149358
149666
  // ../tool-server/src/tools/describe/platforms/tv.ts
149359
- var sleep13 = (ms) => new Promise((r) => setTimeout(r, ms));
149667
+ var sleep14 = (ms) => new Promise((r) => setTimeout(r, ms));
149360
149668
  var EMPTY_RETRY_ATTEMPTS = 3;
149361
149669
  var EMPTY_RETRY_DELAY_MS = 600;
149362
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.";
@@ -149397,7 +149705,7 @@ async function describeTv(registry2, device) {
149397
149705
  let res = await api.describe();
149398
149706
  if (device.platform !== "android") {
149399
149707
  for (let attempt = 1; attempt < EMPTY_RETRY_ATTEMPTS && isEmpty(res); attempt++) {
149400
- await sleep13(EMPTY_RETRY_DELAY_MS);
149708
+ await sleep14(EMPTY_RETRY_DELAY_MS);
149401
149709
  res = await api.describe();
149402
149710
  }
149403
149711
  }
@@ -149981,12 +150289,12 @@ function adaptAndroidTree(capture, screen) {
149981
150289
  unsupportedFields: LEGACY_UNSUPPORTED_FIELDS2
149982
150290
  };
149983
150291
  if (!v2) return tree;
149984
- const unsupported = [];
150292
+ const unsupported2 = [];
149985
150293
  const sdk = capture.sdkInt ?? 0;
149986
- if (sdk < 26) unsupported.push("placeholder", "hintShowing");
149987
- if (sdk < 28) unsupported.push("heading");
150294
+ if (sdk < 26) unsupported2.push("placeholder", "hintShowing");
150295
+ if (sdk < 28) unsupported2.push("heading");
149988
150296
  if (capture.captureMode === "active-window") {
149989
- unsupported.push("keyboardVisible", "foregroundApp", "alertVisible");
150297
+ unsupported2.push("keyboardVisible", "foregroundApp", "alertVisible");
149990
150298
  } else {
149991
150299
  let keyboard = false;
149992
150300
  let foreground;
@@ -150012,7 +150320,7 @@ function adaptAndroidTree(capture, screen) {
150012
150320
  }
150013
150321
  }
150014
150322
  }
150015
- tree.unsupportedFields = unsupported;
150323
+ tree.unsupportedFields = unsupported2;
150016
150324
  return tree;
150017
150325
  }
150018
150326
  async function readAndroidUiTree(registry2, device) {
@@ -150209,7 +150517,7 @@ init_src2();
150209
150517
  var MAX_CLEAR_KEYS = 100;
150210
150518
  var XCTEST_DELETE = "\b";
150211
150519
  var KEY_TIMING = { holdMs: 20, settleMs: 300, typeDelayMs: 30 };
150212
- var sleep14 = (ms) => new Promise((r) => setTimeout(r, ms));
150520
+ var sleep15 = (ms) => new Promise((r) => setTimeout(r, ms));
150213
150521
  function typeableOnSimulator(text) {
150214
150522
  for (const char of text) if (!charToKeyPress(char)) return false;
150215
150523
  return true;
@@ -150234,9 +150542,9 @@ async function pressSimulatorBackspace(registry2, device) {
150234
150542
  const ref = simulatorServerRef(device);
150235
150543
  const api = await registry2.resolveService(ref.urn, ref.options);
150236
150544
  await api.pressKey("Down", NAMED_KEYS.backspace);
150237
- await sleep14(KEY_TIMING.holdMs);
150545
+ await sleep15(KEY_TIMING.holdMs);
150238
150546
  await api.pressKey("Up", NAMED_KEYS.backspace);
150239
- await sleep14(KEY_TIMING.settleMs);
150547
+ await sleep15(KEY_TIMING.settleMs);
150240
150548
  }
150241
150549
  async function typeOnSimulator(registry2, device, text) {
150242
150550
  if (!text) return;
@@ -164206,7 +164514,7 @@ var FOCUS_REPORTING_SOURCES = /* @__PURE__ */ new Set([
164206
164514
  "cdp-dom"
164207
164515
  ]);
164208
164516
  var SETTLE_POLL_MS2 = 250;
164209
- var SETTLE_TIMEOUT_MS2 = 3e3;
164517
+ var SETTLE_TIMEOUT_MS3 = 3e3;
164210
164518
  var SETTLE_MIN_READS = 2;
164211
164519
  var MAX_SCROLL_ITERATIONS = 25;
164212
164520
  var SCROLL_INCREMENT = 0.5;
@@ -164273,7 +164581,7 @@ function readFlowTree(env) {
164273
164581
  });
164274
164582
  }
164275
164583
  async function settleTree(env, opts = {}) {
164276
- const deadline = Date.now() + SETTLE_TIMEOUT_MS2;
164584
+ const deadline = Date.now() + SETTLE_TIMEOUT_MS3;
164277
164585
  let prevFp;
164278
164586
  let prevTree;
164279
164587
  let lastError;
@@ -165522,12 +165830,12 @@ function flowExecuteOutcome(result) {
165522
165830
  function runSequenceOutcome(result) {
165523
165831
  const steps = result.steps;
165524
165832
  if (!Array.isArray(steps)) return void 0;
165525
- const failed2 = steps.find((s) => isRecord(s) && typeof s.error === "string");
165526
- if (failed2 && isRecord(failed2)) {
165527
- 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";
165528
165836
  return {
165529
165837
  status: "fail",
165530
- 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)}`
165531
165839
  };
165532
165840
  }
165533
165841
  const total = count(result.total);
@@ -166532,7 +166840,7 @@ function isTerminalResponse(response) {
166532
166840
  var DEFAULT_SCRIPT_TIMEOUT_MS = 3e4;
166533
166841
  var SCRIPT_STEP_LOG_LIMIT_BYTES = 64 * 1024;
166534
166842
  var SCRIPT_RUN_LOG_LIMIT_BYTES = 256 * 1024;
166535
- var SETTLE_TIMEOUT_MS3 = 500;
166843
+ var SETTLE_TIMEOUT_MS4 = 500;
166536
166844
  var STOP_GRACE_MS2 = 1500;
166537
166845
  var CHILD_DEADLINE_MARGIN_MS = 2e3;
166538
166846
  var GROUP_POLL_MS = 50;
@@ -166943,7 +167251,7 @@ var FlowScriptExecutor = class {
166943
167251
  const deadlinePassed = Date.now() >= deadlineAt;
166944
167252
  clearTimeout(timer);
166945
167253
  request3.signal?.removeEventListener("abort", onAbort);
166946
- await Promise.race([closed, sleep(SETTLE_TIMEOUT_MS3)]);
167254
+ await Promise.race([closed, sleep(SETTLE_TIMEOUT_MS4)]);
166947
167255
  await stop();
166948
167256
  capture.end();
166949
167257
  child.stdout?.destroy();
@@ -166983,11 +167291,11 @@ function flowScriptExecutor() {
166983
167291
  }
166984
167292
  function classifyOutcome(input2) {
166985
167293
  const { exit } = input2;
166986
- if (input2.spawnProblem) return failed("spawn", input2.spawnProblem);
166987
- 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);
166988
167296
  if (input2.terminal) {
166989
167297
  if (input2.terminal.type === "failure") {
166990
- return failed(
167298
+ return failed2(
166991
167299
  input2.terminal.failureType,
166992
167300
  clampText(input2.terminal.message, SCRIPT_MAX_FAILURE_MESSAGE_CHARS),
166993
167301
  clampText(input2.terminal.stack, SCRIPT_MAX_FAILURE_STACK_CHARS)
@@ -166996,26 +167304,26 @@ function classifyOutcome(input2) {
166996
167304
  return commitOutput(input2.terminal.outputJson);
166997
167305
  }
166998
167306
  if (input2.interrupted === "cancelled") {
166999
- 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.");
167000
167308
  }
167001
167309
  if (input2.interrupted === "timeout") return timedOut(input2.timeoutMs);
167002
167310
  if (isHeapAbort(exit, input2.heapFatalSeen)) {
167003
- 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.`);
167004
167312
  }
167005
167313
  if (!input2.startedSeen) {
167006
- return failed(
167314
+ return failed2(
167007
167315
  "protocol",
167008
167316
  `The script runner exited before it started the script (${describeExit(exit)}).`
167009
167317
  );
167010
167318
  }
167011
167319
  if (exit.signal) {
167012
167320
  if (input2.deadlinePassed) return timedOut(input2.timeoutMs);
167013
- return failed(
167321
+ return failed2(
167014
167322
  "signal",
167015
167323
  `The script process was killed by ${exit.signal} before it returned output. It did not stop itself.`
167016
167324
  );
167017
167325
  }
167018
- return failed(
167326
+ return failed2(
167019
167327
  "exit",
167020
167328
  `The script stopped its own process with exit code ${exit.code ?? 0} instead of returning; no output was captured.`
167021
167329
  );
@@ -167025,7 +167333,7 @@ function redactSecrets(verdict, secrets) {
167025
167333
  if (verdict.output) {
167026
167334
  const collision = scrubDocument(verdict.output, secrets);
167027
167335
  if (collision) {
167028
- return failed(
167336
+ return failed2(
167029
167337
  "output",
167030
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.`
167031
167339
  );
@@ -167072,7 +167380,7 @@ function scrubDocument(root, secrets) {
167072
167380
  function commitOutput(outputJson) {
167073
167381
  const bytes = Buffer.byteLength(outputJson, "utf8");
167074
167382
  if (bytes > SCRIPT_MAX_OUTPUT_BYTES) {
167075
- return failed(
167383
+ return failed2(
167076
167384
  "output",
167077
167385
  `The script returned ${describeBytes(bytes)} of encoded output; the limit is ${describeBytes(SCRIPT_MAX_OUTPUT_BYTES)}.`
167078
167386
  );
@@ -167081,14 +167389,14 @@ function commitOutput(outputJson) {
167081
167389
  try {
167082
167390
  parsed = JSON.parse(outputJson);
167083
167391
  } catch (err) {
167084
- 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)}`);
167085
167393
  }
167086
167394
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
167087
- return failed("output", "The script's output was not an object.");
167395
+ return failed2("output", "The script's output was not an object.");
167088
167396
  }
167089
167397
  const polluted = findOwnProtoKey(parsed);
167090
167398
  if (polluted !== void 0) {
167091
- return failed(
167399
+ return failed2(
167092
167400
  "output",
167093
167401
  `${polluted} has an own "__proto__" key; output must be JSON-compatible data.`
167094
167402
  );
@@ -167149,12 +167457,12 @@ function clampText(text, max) {
167149
167457
  return marked;
167150
167458
  }
167151
167459
  function timedOut(timeoutMs) {
167152
- return failed(
167460
+ return failed2(
167153
167461
  "timeout",
167154
167462
  `The script did not finish within its ${describeDuration(timeoutMs)} time limit and its process tree was stopped.`
167155
167463
  );
167156
167464
  }
167157
- function failed(kind, message, stack) {
167465
+ function failed2(kind, message, stack) {
167158
167466
  return { ok: false, failure: { kind, message, ...stack ? { stack } : {} } };
167159
167467
  }
167160
167468
  function isHeapAbort(exit, heapFatalSeen) {
@@ -171255,13 +171563,13 @@ async function frontChromiumPage(registry2, device) {
171255
171563
  }
171256
171564
  function summarize(flowName, deviceId, executionPrerequisite, steps, aborted2, timing) {
171257
171565
  let passed = 0;
171258
- let failed2 = 0;
171566
+ let failed3 = 0;
171259
171567
  let skipped = 0;
171260
171568
  let errored = 0;
171261
171569
  for (const s of steps) {
171262
171570
  if (s.kind === "echo") continue;
171263
171571
  if (s.status === "pass") passed++;
171264
- else if (s.status === "fail") failed2++;
171572
+ else if (s.status === "fail") failed3++;
171265
171573
  else if (s.status === "skip") skipped++;
171266
171574
  else errored++;
171267
171575
  }
@@ -171273,10 +171581,10 @@ function summarize(flowName, deviceId, executionPrerequisite, steps, aborted2, t
171273
171581
  // (no fail/error report), so the verdict folds the abort in directly. A
171274
171582
  // skip by itself is NOT a failure: an unmet `when:` guard skips its block
171275
171583
  // as a successful omission.
171276
- ok: failed2 === 0 && errored === 0 && !aborted2,
171584
+ ok: failed3 === 0 && errored === 0 && !aborted2,
171277
171585
  ...aborted2 ? { aborted: true } : {},
171278
171586
  passed,
171279
- failed: failed2,
171587
+ failed: failed3,
171280
171588
  skipped,
171281
171589
  errored,
171282
171590
  steps,
@@ -171579,9 +171887,10 @@ async function runFold(state3, step) {
171579
171887
  const result = await invokeSubTool(registry2, ctx, "fold", args);
171580
171888
  const size = result.screen?.width !== void 0 && result.screen?.height !== void 0 ? ` ${result.screen.width}x${result.screen.height}` : "";
171581
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})`;
171582
171891
  return {
171583
171892
  ok: true,
171584
- reason: `${target}: screen ${result.activeScreen ?? "?"} (${result.screen?.panel ?? "?"}${size})`,
171893
+ reason: `${target}: ${landed}`,
171585
171894
  ...result.warning !== void 0 ? { warning: result.warning } : {}
171586
171895
  };
171587
171896
  } catch (err) {
@@ -172485,7 +172794,7 @@ var updateArgentTool = {
172485
172794
  });
172486
172795
  child.unref();
172487
172796
  }, 2e3);
172488
- const targetLabel2 = effectiveTarget === "both" ? "global and project-local installs" : `${effectiveTarget} install`;
172797
+ const targetLabel3 = effectiveTarget === "both" ? "global and project-local installs" : `${effectiveTarget} install`;
172489
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.` : "";
172490
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.` : "";
172491
172800
  const versionInfo = targetsOnlyRunningInstall ? `(v${currentVersion} -> v${installableVersion}) ` : "";
@@ -172493,7 +172802,7 @@ var updateArgentTool = {
172493
172802
  const coversRunningInstall = targetsOnlyRunningInstall || effectiveTarget === "both";
172494
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.`;
172495
172804
  return {
172496
- 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}`
172497
172806
  };
172498
172807
  }
172499
172808
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swmansion/argent",
3
- "version": "0.29.1-next.14",
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