@swmansion/argent 0.32.1-next.8 → 0.32.1-next.9

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
@@ -35821,7 +35821,7 @@ var _CI_VENDOR_COUNT_FOR_TEST = vendors_default.length;
35821
35821
  var SESSION_ID2 = randomUUID5();
35822
35822
  function readCliVersion() {
35823
35823
  if (true) {
35824
- return "0.32.1-next.8";
35824
+ return "0.32.1-next.9";
35825
35825
  }
35826
35826
  return "0.0.0";
35827
35827
  }
@@ -16946,7 +16946,7 @@ var _CI_VENDOR_COUNT_FOR_TEST = vendors_default.length;
16946
16946
  var SESSION_ID = randomUUID4();
16947
16947
  function readCliVersion() {
16948
16948
  if (true) {
16949
- return "0.32.1-next.8";
16949
+ return "0.32.1-next.9";
16950
16950
  }
16951
16951
  return "0.0.0";
16952
16952
  }
@@ -10,7 +10,7 @@ export interface ArgentToolArgs {
10
10
  minStableMs?: number;
11
11
  };
12
12
  "await-ui-element": {
13
- /** Target device id from `list-devices` (iOS UDID, Android serial, Vega serial, HarmonyOS id, or Chromium id; not Apple TV, whose tree it cannot read). */
13
+ /** Target device id from `list-devices` (iOS UDID, Android serial, Vega serial, HarmonyOS id, or Chromium id). */
14
14
  udid: string;
15
15
  /** What to wait for. `exists`: the selector matches an element anywhere in the tree. `visible`: it matches one with a non-zero on-screen frame. `hidden`: it matches nothing, or only a zero-area element (e.g. a spinner that went away). `text`: the first visible match in reading order (topmost, then leftmost; the first match overall if none is visible) contains expectedText, or equals it with textMatch `equals`. Only that match is checked, so if a loose selector hits several elements, narrow it to the one holding the text. */
16
16
  condition: "exists" | "visible" | "hidden" | "text";
@@ -95983,7 +95983,7 @@ var require_lexer = __commonJS({
95983
95983
  "../../node_modules/yaml/dist/parse/lexer.js"(exports2) {
95984
95984
  "use strict";
95985
95985
  var cst = require_cst();
95986
- function isEmpty2(ch) {
95986
+ function isEmpty(ch) {
95987
95987
  switch (ch) {
95988
95988
  case void 0:
95989
95989
  case " ":
@@ -96061,7 +96061,7 @@ var require_lexer = __commonJS({
96061
96061
  }
96062
96062
  if (ch === "-" || ch === ".") {
96063
96063
  const dt = this.buffer.substr(offset, 3);
96064
- if ((dt === "---" || dt === "...") && isEmpty2(this.buffer[offset + 3]))
96064
+ if ((dt === "---" || dt === "...") && isEmpty(this.buffer[offset + 3]))
96065
96065
  return -1;
96066
96066
  }
96067
96067
  return offset;
@@ -96160,7 +96160,7 @@ var require_lexer = __commonJS({
96160
96160
  if (!this.atEnd && !this.hasChars(4))
96161
96161
  return this.setNext("line-start");
96162
96162
  const s = this.peek(3);
96163
- if ((s === "---" || s === "...") && isEmpty2(this.charAt(3))) {
96163
+ if ((s === "---" || s === "...") && isEmpty(this.charAt(3))) {
96164
96164
  yield* this.pushCount(3);
96165
96165
  this.indentValue = 0;
96166
96166
  this.indentNext = 0;
@@ -96168,7 +96168,7 @@ var require_lexer = __commonJS({
96168
96168
  }
96169
96169
  }
96170
96170
  this.indentValue = yield* this.pushSpaces(false);
96171
- if (this.indentNext > this.indentValue && !isEmpty2(this.charAt(1)))
96171
+ if (this.indentNext > this.indentValue && !isEmpty(this.charAt(1)))
96172
96172
  this.indentNext = this.indentValue;
96173
96173
  return yield* this.parseBlockStart();
96174
96174
  }
@@ -96176,7 +96176,7 @@ var require_lexer = __commonJS({
96176
96176
  const [ch0, ch1] = this.peek(2);
96177
96177
  if (!ch1 && !this.atEnd)
96178
96178
  return this.setNext("block-start");
96179
- if ((ch0 === "-" || ch0 === "?" || ch0 === ":") && isEmpty2(ch1)) {
96179
+ if ((ch0 === "-" || ch0 === "?" || ch0 === ":") && isEmpty(ch1)) {
96180
96180
  const n = (yield* this.pushCount(1)) + (yield* this.pushSpaces(true));
96181
96181
  this.indentNext = this.indentValue + 1;
96182
96182
  this.indentValue += n;
@@ -96240,7 +96240,7 @@ var require_lexer = __commonJS({
96240
96240
  const line = this.getLine();
96241
96241
  if (line === null)
96242
96242
  return this.setNext("flow");
96243
- if (indent !== -1 && indent < this.indentNext && line[0] !== "#" || indent === 0 && (line.startsWith("---") || line.startsWith("...")) && isEmpty2(line[3])) {
96243
+ if (indent !== -1 && indent < this.indentNext && line[0] !== "#" || indent === 0 && (line.startsWith("---") || line.startsWith("...")) && isEmpty(line[3])) {
96244
96244
  const atFlowEndMarker = indent === this.indentNext - 1 && this.flowLevel === 1 && (line[0] === "]" || line[0] === "}");
96245
96245
  if (!atFlowEndMarker) {
96246
96246
  this.flowLevel = 0;
@@ -96282,7 +96282,7 @@ var require_lexer = __commonJS({
96282
96282
  return yield* this.parseQuotedScalar();
96283
96283
  case ":": {
96284
96284
  const next = this.charAt(1);
96285
- if (this.flowKey || isEmpty2(next) || next === ",") {
96285
+ if (this.flowKey || isEmpty(next) || next === ",") {
96286
96286
  this.flowKey = false;
96287
96287
  yield* this.pushCount(1);
96288
96288
  yield* this.pushSpaces(true);
@@ -96345,7 +96345,7 @@ var require_lexer = __commonJS({
96345
96345
  else if (ch !== "-")
96346
96346
  break;
96347
96347
  }
96348
- return yield* this.pushUntil((ch) => isEmpty2(ch) || ch === "#");
96348
+ return yield* this.pushUntil((ch) => isEmpty(ch) || ch === "#");
96349
96349
  }
96350
96350
  *parseBlockScalar() {
96351
96351
  let nl = this.pos - 1;
@@ -96427,10 +96427,10 @@ var require_lexer = __commonJS({
96427
96427
  while (ch = this.buffer[++i]) {
96428
96428
  if (ch === ":") {
96429
96429
  const next = this.buffer[i + 1];
96430
- if (isEmpty2(next) || inFlow && flowIndicatorChars.has(next))
96430
+ if (isEmpty(next) || inFlow && flowIndicatorChars.has(next))
96431
96431
  break;
96432
96432
  end = i;
96433
- } else if (isEmpty2(ch)) {
96433
+ } else if (isEmpty(ch)) {
96434
96434
  let next = this.buffer[i + 1];
96435
96435
  if (ch === "\r") {
96436
96436
  if (next === "\n") {
@@ -96497,7 +96497,7 @@ var require_lexer = __commonJS({
96497
96497
  case ":": {
96498
96498
  const inFlow = this.flowLevel > 0;
96499
96499
  const ch1 = this.charAt(1);
96500
- if (isEmpty2(ch1) || inFlow && flowIndicatorChars.has(ch1)) {
96500
+ if (isEmpty(ch1) || inFlow && flowIndicatorChars.has(ch1)) {
96501
96501
  if (!inFlow)
96502
96502
  this.indentNext = this.indentValue + 1;
96503
96503
  else if (this.flowKey)
@@ -96516,7 +96516,7 @@ var require_lexer = __commonJS({
96516
96516
  if (this.charAt(1) === "<") {
96517
96517
  let i = this.pos + 2;
96518
96518
  let ch = this.buffer[i];
96519
- while (!isEmpty2(ch) && ch !== ">")
96519
+ while (!isEmpty(ch) && ch !== ">")
96520
96520
  ch = this.buffer[++i];
96521
96521
  return yield* this.pushToIndex(ch === ">" ? i + 1 : i, false);
96522
96522
  } else {
@@ -101180,7 +101180,7 @@ var _CI_VENDOR_COUNT_FOR_TEST = vendors_default.length;
101180
101180
  var SESSION_ID = (0, import_node_crypto3.randomUUID)();
101181
101181
  function readCliVersion() {
101182
101182
  if (true) {
101183
- return "0.32.1-next.8";
101183
+ return "0.32.1-next.9";
101184
101184
  }
101185
101185
  return "0.0.0";
101186
101186
  }
@@ -134618,6 +134618,9 @@ var tvControlBlueprint = {
134618
134618
  focusable: r.focusable ?? []
134619
134619
  };
134620
134620
  },
134621
+ async hierarchy() {
134622
+ return await sendAx("hierarchy", 1e4);
134623
+ },
134621
134624
  async navigate(direction) {
134622
134625
  await sendJson(hidSock, `navigate ${direction}`);
134623
134626
  },
@@ -148442,6 +148445,107 @@ function describeNestedParamError(registry2, err, toolId, dispatchedArgs, author
148442
148445
  // ../tool-server/src/tools/await-ui-element/index.ts
148443
148446
  init_zod();
148444
148447
  init_device_info();
148448
+
148449
+ // ../tool-server/src/tools/describe/platforms/tv-focus.ts
148450
+ function focusRoot() {
148451
+ return { role: "AXGroup", frame: { x: 0, y: 0, width: 1, height: 1 }, children: [] };
148452
+ }
148453
+ var TV_FOCUS_WAIT_EMPTY_HINT = "The Apple TV reports nothing on screen: the app may be launching, or the screen has no text or focusable elements, which a screenshot can tell apart.";
148454
+ function isEmptyFocus(res) {
148455
+ return res.focusable.length === 0 && !res.focused;
148456
+ }
148457
+ function splitTvLabel(label) {
148458
+ const lines = (label ?? "").split("\n").map((l) => l.trim()).filter(Boolean);
148459
+ return { name: lines[0], context: lines.slice(1) };
148460
+ }
148461
+ function frameOf(element) {
148462
+ const f = element.frame;
148463
+ if (!f) return { x: 0, y: 0, width: 0, height: 0 };
148464
+ const onScreen2 = f.x < 1 && f.y < 1 && f.x + f.width > 0 && f.y + f.height > 0;
148465
+ return onScreen2 ? f : { x: f.x, y: f.y, width: 0, height: 0 };
148466
+ }
148467
+ var FOCUSED_TRAIT = "focused";
148468
+ function toNode(element) {
148469
+ const traits = [...element.traits ?? []];
148470
+ if (element.isFocused) traits.push(FOCUSED_TRAIT);
148471
+ const frame2 = frameOf(element);
148472
+ const { name, context: context2 } = splitTvLabel(element.label);
148473
+ return {
148474
+ // Traits are what a selector's `role` matches, exactly as on a phone.
148475
+ role: traits.length > 0 ? traits.join(",") : "element",
148476
+ frame: frame2,
148477
+ // The context stays selectable (a tab's "selected"), and a `text` condition
148478
+ // that contains it still holds, but `equals` compares the name `describe` shows.
148479
+ children: context2.map((label) => ({ role: "context", frame: frame2, children: [], label })),
148480
+ ...name ? { label: name } : {},
148481
+ ...context2.length > 0 && element.label ? { subtreeText: element.label } : {},
148482
+ ...element.value ? { value: element.value } : {},
148483
+ // The cursor. `format-tree` already renders this as [focused], and it is
148484
+ // what makes "wait until focus lands on X" expressible.
148485
+ ...element.isFocused ? { focused: true } : {}
148486
+ };
148487
+ }
148488
+ function contentElements(hierarchy, focusText) {
148489
+ const out = [];
148490
+ const screen = hierarchy?.screenFrame;
148491
+ const walk = (node2) => {
148492
+ for (const child of node2.children ?? []) {
148493
+ const text = child.label || child.value;
148494
+ if (text && !focusText.has(text)) {
148495
+ const f = child.frame;
148496
+ out.push({
148497
+ label: child.label,
148498
+ value: child.value,
148499
+ ...f && screen?.width && screen.height ? {
148500
+ frame: {
148501
+ x: f.x / screen.width,
148502
+ y: f.y / screen.height,
148503
+ width: f.width / screen.width,
148504
+ height: f.height / screen.height
148505
+ }
148506
+ } : {}
148507
+ });
148508
+ }
148509
+ walk(child);
148510
+ }
148511
+ };
148512
+ if (hierarchy?.tree) walk(hierarchy.tree);
148513
+ return out;
148514
+ }
148515
+ function tvFocusTree(res, hierarchy = null) {
148516
+ const elements = [...res.focusable];
148517
+ if (res.focused && !res.focusable.some((e) => e.isFocused)) {
148518
+ elements.push({ ...res.focused, isFocused: true });
148519
+ }
148520
+ const focusText = /* @__PURE__ */ new Set();
148521
+ for (const e of elements) {
148522
+ if (e.label) focusText.add(e.label);
148523
+ if (e.value) focusText.add(e.value);
148524
+ }
148525
+ const root = focusRoot();
148526
+ root.children = [...elements, ...contentElements(hierarchy, focusText)].map(toNode);
148527
+ if (root.children.length === 0) {
148528
+ return { tree: root, source: "tv-focus", hint: TV_FOCUS_WAIT_EMPTY_HINT };
148529
+ }
148530
+ return (hierarchy?.nodeCount ?? 0) > TV_HIERARCHY_NODE_CAP ? { tree: root, source: "tv-focus", partial: true, hint: TV_HIERARCHY_CAPPED_HINT } : { tree: root, source: "tv-focus" };
148531
+ }
148532
+ var TV_HIERARCHY_NODE_CAP = 800;
148533
+ var TV_HIERARCHY_CAPPED_HINT = "The Apple TV hierarchy stopped at its 800-element cap, so text past it was not read.";
148534
+ function hasTvCursor(tree) {
148535
+ return tree.children.some((n) => n.focused);
148536
+ }
148537
+ async function describeTvFocus(api) {
148538
+ const res = await api.describe();
148539
+ if (!api.hierarchy) return tvFocusTree(res);
148540
+ const hierarchy = await api.hierarchy().catch(async () => {
148541
+ await new Promise((r) => setTimeout(r, HIERARCHY_RETRY_MS));
148542
+ return api.hierarchy();
148543
+ });
148544
+ return tvFocusTree(res, hierarchy);
148545
+ }
148546
+ var HIERARCHY_RETRY_MS = 30;
148547
+
148548
+ // ../tool-server/src/tools/await-ui-element/index.ts
148445
148549
  init_adb();
148446
148550
  init_capability();
148447
148551
 
@@ -154201,6 +154305,11 @@ var capability23 = {
154201
154305
  function evaluateMatches(params, matches2) {
154202
154306
  return evaluateCondition(params.condition, params.expectedText, matches2, params.textMatch);
154203
154307
  }
154308
+ function cannotJudge(data, matches2, everMatched, condition) {
154309
+ if (isBlindRead(data, everMatched)) return true;
154310
+ if (data.partial !== true) return false;
154311
+ return condition === "exists" ? matches2.length === 0 : !matches2.some(isVisible);
154312
+ }
154204
154313
  function appendDiagnostics(base, lastData) {
154205
154314
  if (!lastData) return base;
154206
154315
  const extras = [];
@@ -154236,7 +154345,7 @@ function timeoutNote(params, lastTree, fetchError, lastData) {
154236
154345
  }
154237
154346
  var zodSchema29 = external_exports.object({
154238
154347
  udid: external_exports.string().min(1).describe(
154239
- "Target device id from `list-devices` (iOS UDID, Android serial, Vega serial, <flag:harmonyos>HarmonyOS id, </flag:harmonyos>or Chromium id; not Apple TV, whose tree it cannot read)."
154348
+ "Target device id from `list-devices` (iOS UDID, Android serial, Vega serial, <flag:harmonyos>HarmonyOS id, </flag:harmonyos>or Chromium id)."
154240
154349
  ),
154241
154350
  condition: external_exports.enum(["exists", "visible", "hidden", "text"]).describe(
154242
154351
  "What to wait for. `exists`: the selector matches an element anywhere in the tree. `visible`: it matches one with a non-zero on-screen frame. `hidden`: it matches nothing, or only a zero-area element (e.g. a spinner that went away). `text`: the first visible match in reading order (topmost, then leftmost; the first match overall if none is visible) contains expectedText, or equals it with textMatch `equals`. Only that match is checked, so if a loose selector hits several elements, narrow it to the one holding the text."
@@ -154262,12 +154371,13 @@ var zodSchema29 = external_exports.object({
154262
154371
  path: ["expectedText"]
154263
154372
  });
154264
154373
  function createAwaitUiElementTool(registry2) {
154265
- async function fetchTree(device, params, services, isTvOs, androidIsTv, budgetMs) {
154374
+ async function fetchTree(device, params, services, tvApi, androidIsTv, budgetMs) {
154266
154375
  if (device.platform === "ios" || device.platform === "ios-remote") {
154267
154376
  if (device.kind === "device") {
154268
154377
  return describeIosDevice(registry2, device);
154269
154378
  }
154270
- return describeIos(registry2, device, { bundleId: params.bundleId }, { isTvOs });
154379
+ if (tvApi) return describeTvFocus(tvApi);
154380
+ return describeIos(registry2, device, { bundleId: params.bundleId }, { isTvOs: false });
154271
154381
  }
154272
154382
  if (device.platform === "android") {
154273
154383
  return describeAndroid(registry2, device.id, void 0, androidIsTv);
@@ -154313,6 +154423,7 @@ Returns { success: boolean, elapsed: number, note?, cause? }. If the condition d
154313
154423
  else if (device.platform === "vega") await ensureDeps(vegaRequires);
154314
154424
  else if (device.platform === "harmony") await ensureDeps(harmonyRequires);
154315
154425
  const isTvOs = device.platform === "ios" && device.kind !== "device" && await isTvOsSimulator(device.id);
154426
+ const tvApi = isTvOs ? await resolveTvApi(registry2, device.id) : null;
154316
154427
  const androidIsTv = device.platform === "android" && await isAndroidTv(device.id);
154317
154428
  const start2 = Date.now();
154318
154429
  const cancelled = () => ({
@@ -154327,14 +154438,14 @@ Returns { success: boolean, elapsed: number, note?, cause? }. If the condition d
154327
154438
  let everMatched = false;
154328
154439
  let lastTrustedReadAt;
154329
154440
  const poll = await pollDescribeTree({
154330
- fetchTree: (budgetMs) => fetchTree(device, params, services, isTvOs, androidIsTv, budgetMs),
154441
+ fetchTree: (budgetMs) => fetchTree(device, params, services, tvApi, androidIsTv, budgetMs),
154331
154442
  timeoutMs,
154332
154443
  pollIntervalMs,
154333
154444
  signal,
154334
154445
  onSample: (data, nowMs) => {
154335
154446
  const matches2 = findAll(data.tree, selector);
154336
154447
  if (matches2.length > 0) everMatched = true;
154337
- const blind = isBlindRead(data, everMatched);
154448
+ const blind = cannotJudge(data, matches2, everMatched, params.condition);
154338
154449
  if (!blind && !data.stale) lastTrustedReadAt = nowMs;
154339
154450
  if (!blind && evaluateMatches(params, matches2)) {
154340
154451
  const result = { success: true, elapsed: Date.now() - start2 };
@@ -154350,7 +154461,12 @@ Returns { success: boolean, elapsed: number, note?, cause? }. If the condition d
154350
154461
  });
154351
154462
  if (poll.aborted) return cancelled();
154352
154463
  if (poll.result) return poll.result;
154353
- const finalRead = poll.lastAttempt === "unsettled" ? "unsettled" : poll.lastAttempt === "tree" && poll.lastData !== null && !poll.lastData.stale && !isBlindRead(poll.lastData, everMatched) ? "trusted" : "untrusted";
154464
+ const finalRead = poll.lastAttempt === "unsettled" ? "unsettled" : poll.lastAttempt === "tree" && poll.lastData !== null && !poll.lastData.stale && !cannotJudge(
154465
+ poll.lastData,
154466
+ findAll(poll.lastData.tree, selector),
154467
+ everMatched,
154468
+ params.condition
154469
+ ) ? "trusted" : "untrusted";
154354
154470
  return {
154355
154471
  success: false,
154356
154472
  elapsed: Date.now() - start2,
@@ -154439,7 +154555,7 @@ Allowed tools, their args (? = optional) and targets. Positions and distances ar
154439
154555
  fold: posture? "closed"|"half-open"|"open", or angle? 0\u2013180, exactly one (later steps land on the new panel, whose coordinates differ) [foldable ios sim/android emu]
154440
154556
  tv-remote: button (one button, or an array run as a path in one step), repeat? [apple tv/android tv/vega]
154441
154557
  buttons: up/down/left/right/select/back/home/menu/playPause; rewind/fastForward/next/previous/volumeUp/volumeDown/mute on Android TV and Vega only
154442
- await-ui-element: condition: "exists"|"visible"|"hidden"|"text", selector: { text?, identifier?, role? }, expectedText? (for "text"), textMatch?, timeoutMs?, pollIntervalMs? [all but apple tv]`,
154558
+ await-ui-element: condition: "exists"|"visible"|"hidden"|"text", selector: { text?, identifier?, role? }, expectedText? (for "text"), textMatch?, timeoutMs?, pollIntervalMs? [all]`,
154443
154559
  zodSchema: zodSchema30,
154444
154560
  alwaysLoad: true,
154445
154561
  searchHint: "batch sequence multiple gesture steps sequentially",
@@ -157162,21 +157278,12 @@ var EMPTY_RETRY_ATTEMPTS = 3;
157162
157278
  var EMPTY_RETRY_DELAY_MS = 600;
157163
157279
  var EMPTY_HINT2 = "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.";
157164
157280
  var ANDROID_FOCUS_EMPTY_HINT = "The Android TV focus engine reported no focusable elements \u2014 common on react-native-tvos screens that drive focus with RN's own engine (invisible to the OS accessibility tree). Falling back to the full UI tree below. `tv-remote` (direction/select) still moves focus on these screens even though the labels aren't enumerable, so you can drive blind + screenshot to confirm.";
157165
- function isEmpty(res) {
157166
- return res.focusable.length === 0 && !res.focused;
157167
- }
157168
- function primaryLabel(label) {
157169
- if (!label) return "(no label)";
157170
- const firstLine5 = label.split("\n")[0]?.trim();
157171
- return firstLine5 && firstLine5.length ? firstLine5 : "(no label)";
157172
- }
157173
157281
  function fmtElement(e) {
157174
157282
  const traits = e.traits?.length ? ` [${e.traits.join(",")}]` : "";
157175
157283
  const value = e.value ? ` = "${e.value}"` : "";
157176
- const label = primaryLabel(e.label);
157177
- const extraLines = (e.label ?? "").split("\n").slice(1).map((l) => l.trim()).filter(Boolean);
157178
- const context2 = extraLines.length ? ` (${extraLines.join(" \xB7 ")})` : "";
157179
- return `${label}${value}${traits}${context2}`;
157284
+ const { name, context: context2 } = splitTvLabel(e.label);
157285
+ const extra = context2.length ? ` (${context2.join(" \xB7 ")})` : "";
157286
+ return `${name ?? "(no label)"}${value}${traits}${extra}`;
157180
157287
  }
157181
157288
  function renderFocusView(res) {
157182
157289
  const lines = [];
@@ -157197,16 +157304,16 @@ async function describeTv(registry2, device) {
157197
157304
  const api = await resolveTvApi(registry2, device.id);
157198
157305
  let res = await api.describe();
157199
157306
  if (device.platform !== "android") {
157200
- for (let attempt = 1; attempt < EMPTY_RETRY_ATTEMPTS && isEmpty(res); attempt++) {
157307
+ for (let attempt = 1; attempt < EMPTY_RETRY_ATTEMPTS && isEmptyFocus(res); attempt++) {
157201
157308
  await sleep15(EMPTY_RETRY_DELAY_MS);
157202
157309
  res = await api.describe();
157203
157310
  }
157204
157311
  }
157205
- if (isEmpty(res) && device.platform !== "android") {
157312
+ if (isEmptyFocus(res) && device.platform !== "android") {
157206
157313
  await api.recycleAx();
157207
157314
  res = await api.describe();
157208
157315
  }
157209
- if (isEmpty(res) && device.platform === "android") {
157316
+ if (isEmptyFocus(res) && device.platform === "android") {
157210
157317
  const data = await describeAndroid(registry2, device.id, void 0, true);
157211
157318
  return {
157212
157319
  description: `${ANDROID_FOCUS_EMPTY_HINT}
@@ -157218,7 +157325,7 @@ ${formatDescribeTree(data.tree, {
157218
157325
  hint: ANDROID_FOCUS_EMPTY_HINT
157219
157326
  };
157220
157327
  }
157221
- const empty2 = isEmpty(res);
157328
+ const empty2 = isEmptyFocus(res);
157222
157329
  const description = empty2 ? `${renderFocusView(res)}
157223
157330
 
157224
157331
  Note: ${EMPTY_HINT2}` : renderFocusView(res);
@@ -158332,6 +158439,10 @@ var AWAIT_SCREEN_IDLE_TOOL_ID = "await-screen-idle";
158332
158439
  var DEFAULT_TIMEOUT_MS4 = 3e3;
158333
158440
  var DEFAULT_POLL_INTERVAL_MS2 = 200;
158334
158441
  var DEFAULT_MIN_STABLE_MS = 250;
158442
+ var TV_NO_CURSOR_STABLE_MS = 1200;
158443
+ var DARK_TAIL_POLLS = 2;
158444
+ var DARK_TAIL_MAX_MS = 2e3;
158445
+ var TV_NO_CURSOR_NOTE = "no element holds focus on this screen, so a remote press may be lost";
158335
158446
  var capability29 = {
158336
158447
  apple: { simulator: true, device: true },
158337
158448
  appleRemote: { simulator: true },
@@ -158352,6 +158463,20 @@ function treeSignature(root) {
158352
158463
  for (const child of root.children) walk(child);
158353
158464
  return parts2.join("\n");
158354
158465
  }
158466
+ function unsettledNote(lastData, lastError, darkTailMs) {
158467
+ if (lastError) return `last tree read failed: ${lastError}`;
158468
+ if (darkTailMs !== void 0) {
158469
+ return `the tree read stopped answering: nothing came back for the last ${darkTailMs}ms`;
158470
+ }
158471
+ if (!lastData) return void 0;
158472
+ if (lastData.tree.children.length > 0) return void 0;
158473
+ const parts2 = ["the screen reported no content"];
158474
+ if (lastData.should_restart) {
158475
+ parts2.push("the foreground app may need a restart for native inspection");
158476
+ }
158477
+ if (lastData.hint) parts2.push(lastData.hint);
158478
+ return parts2.join("; ");
158479
+ }
158355
158480
  var zodSchema44 = external_exports.object({
158356
158481
  udid: external_exports.string().min(1).describe(
158357
158482
  "Target device id from `list-devices` (iOS UDID, Android serial, <flag:harmonyos>HarmonyOS id, </flag:harmonyos>or Chromium id)."
@@ -158365,12 +158490,13 @@ var zodSchema44 = external_exports.object({
158365
158490
  )
158366
158491
  }).strict();
158367
158492
  function createAwaitScreenIdleTool(registry2) {
158368
- function fetchTree(device, services, isTvOs, androidIsTv, budgetMs) {
158493
+ function fetchTree(device, services, tvApi, androidIsTv, budgetMs) {
158369
158494
  if (device.platform === "ios" || device.platform === "ios-remote") {
158370
158495
  if (device.kind === "device") {
158371
158496
  return describeIosDevice(registry2, device);
158372
158497
  }
158373
- return describeIos(registry2, device, {}, { isTvOs });
158498
+ if (tvApi) return describeTvFocus(tvApi);
158499
+ return describeIos(registry2, device, {}, { isTvOs: false });
158374
158500
  }
158375
158501
  if (device.platform === "android") {
158376
158502
  return describeAndroid(registry2, device.id, void 0, androidIsTv);
@@ -158383,8 +158509,8 @@ function createAwaitScreenIdleTool(registry2) {
158383
158509
  return {
158384
158510
  id: AWAIT_SCREEN_IDLE_TOOL_ID,
158385
158511
  description: `Block until the screen has rendered content and stopped changing, or the timeout elapses. It polls the same element tree as \`describe\`.
158386
- Use when a launch or navigation must finish rendering before you take a screenshot or tap.
158387
- Returns { settled, waitedMs, polls<flag:harmonyos>, note?</flag:harmonyos> }. If the screen never goes still before timeoutMs, it returns settled=false instead of failing.<flag:harmonyos> A note on settled=true means the screen it settled on is not live: a suspended HarmonyOS panel settles instantly on its last frame, and taps land nowhere until it is woken.</flag:harmonyos>`,
158512
+ Use when a launch or navigation must finish rendering before a screenshot or tap.
158513
+ Returns { settled, waitedMs, polls, note? }. If the screen never goes still before timeoutMs, it returns settled=false instead of failing.<flag:harmonyos> A note on settled=true means the screen it settled on is not live: a suspended HarmonyOS panel settles instantly on its last frame, and taps land nowhere until it is woken.</flag:harmonyos>`,
158388
158514
  zodSchema: zodSchema44,
158389
158515
  searchHint: "wait until screen settles idle stable stops changing animation transition rendered ready before screenshot",
158390
158516
  readOnly: true,
@@ -158410,16 +158536,20 @@ Returns { settled, waitedMs, polls<flag:harmonyos>, note?</flag:harmonyos> }. If
158410
158536
  else if (device.platform === "android") await ensureDeps(androidRequires);
158411
158537
  else if (device.platform === "harmony") await ensureDeps(harmonyRequires);
158412
158538
  const isTvOs = device.platform === "ios" && device.kind !== "device" && await isTvOsSimulator(device.id);
158539
+ const tvApi = isTvOs ? await resolveTvApi(registry2, device.id) : null;
158413
158540
  const androidIsTv = device.platform === "android" && await isAndroidTv(device.id);
158414
158541
  const minStableMs = params.minStableMs ?? DEFAULT_MIN_STABLE_MS;
158542
+ const pollIntervalMs = params.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS2;
158415
158543
  let stableSignature;
158416
158544
  let stableSince = 0;
158545
+ let lastReadAt = 0;
158417
158546
  const poll = await pollDescribeTree({
158418
- fetchTree: (budgetMs) => fetchTree(device, services, isTvOs, androidIsTv, budgetMs),
158547
+ fetchTree: (budgetMs) => fetchTree(device, services, tvApi, androidIsTv, budgetMs),
158419
158548
  timeoutMs: params.timeoutMs ?? DEFAULT_TIMEOUT_MS4,
158420
- pollIntervalMs: params.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS2,
158549
+ pollIntervalMs,
158421
158550
  signal: ctx?.signal,
158422
158551
  onSample: (data, nowMs) => {
158552
+ lastReadAt = nowMs;
158423
158553
  if (data.tree.children.length === 0) {
158424
158554
  stableSignature = void 0;
158425
158555
  stableSince = 0;
@@ -158427,7 +158557,8 @@ Returns { settled, waitedMs, polls<flag:harmonyos>, note?</flag:harmonyos> }. If
158427
158557
  }
158428
158558
  const signature = treeSignature(data.tree);
158429
158559
  if (signature === stableSignature) {
158430
- if (nowMs - stableSince >= minStableMs) return { done: true, result: true };
158560
+ const requiredMs = tvApi && !hasTvCursor(data.tree) ? Math.max(minStableMs, TV_NO_CURSOR_STABLE_MS) : minStableMs;
158561
+ if (nowMs - stableSince >= requiredMs) return { done: true, result: true };
158431
158562
  } else {
158432
158563
  stableSignature = signature;
158433
158564
  stableSince = nowMs;
@@ -158436,12 +158567,17 @@ Returns { settled, waitedMs, polls<flag:harmonyos>, note?</flag:harmonyos> }. If
158436
158567
  return { done: false };
158437
158568
  }
158438
158569
  });
158439
- const note = poll.result === true && poll.lastData?.stale ? poll.lastData.hint : void 0;
158570
+ const settled = poll.result === true;
158571
+ const darkTailMs = Date.now() - lastReadAt;
158572
+ const hung = lastReadAt > 0 && darkTailMs > Math.min(DARK_TAIL_POLLS * pollIntervalMs, DARK_TAIL_MAX_MS);
158573
+ const note = settled ? poll.lastData?.stale ? poll.lastData.hint : void 0 : unsettledNote(poll.lastData, poll.lastError, hung ? darkTailMs : void 0);
158574
+ const populatedNoCursor = tvApi && poll.lastData?.tree.children.length && !hasTvCursor(poll.lastData.tree);
158575
+ const cursorNote = populatedNoCursor ? TV_NO_CURSOR_NOTE : void 0;
158440
158576
  return {
158441
- settled: poll.result === true,
158577
+ settled,
158442
158578
  waitedMs: poll.elapsedMs,
158443
158579
  polls: poll.polls,
158444
- ...note ? { note } : {}
158580
+ ...note || cursorNote ? { note: note ?? cursorNote } : {}
158445
158581
  };
158446
158582
  }
158447
158583
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swmansion/argent",
3
- "version": "0.32.1-next.8",
3
+ "version": "0.32.1-next.9",
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",
@@ -15,9 +15,9 @@ description: Control and inspect TV apps via argent, Apple TV (tvOS), Android TV
15
15
 
16
16
  ## The navigation loop
17
17
 
18
- 1. `describe`: find the cursor and your target (returns the focused element + all focusable ones, not a tap tree).
19
- 2. `tv-remote`: move focus toward the target. Prefer **one** call with a path ending in `select`, e.g. `{button:["down","right","select"]}`; count rows/columns from the frames to build the path.
20
- 3. `describe` again to confirm. On a miss, repeat.
18
+ 1. `describe`: find the cursor and your target.
19
+ 2. `tv-remote`: move focus toward the target. Prefer **one** call with a path ending in `select`, e.g. `{button:["down","right","select"]}`; count rows/columns from the list order.
20
+ 3. `describe` again to confirm, or on Apple TV `await-ui-element {condition:"exists", selector:{text:"X", role:"focused"}}`. On a miss, repeat.
21
21
 
22
22
  ## Tools
23
23
 
@@ -29,7 +29,7 @@ description: Control and inspect TV apps via argent, Apple TV (tvOS), Android TV
29
29
 
30
30
  ### Apple TV (tvOS simulator)
31
31
 
32
- - Boot like any iOS sim (`boot-device`); the AX + HID daemons auto-start on the first `describe` / `tv-remote` (first call may take a few seconds).
32
+ - Boot like any iOS sim (`boot-device`); the first `describe` / `tv-remote` starts the AX + HID daemons (a few seconds).
33
33
  - Dev build: `open-url {udid, url:"<scheme>://expo-development-client/?url=http%3A%2F%2F<HOST_IP>%3A8081"}` (`<HOST_IP>` = your Mac's LAN IP, shown on the launcher).
34
34
 
35
35
  ### Android TV (leanback emulator)