@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.
|
|
35824
|
+
return "0.32.1-next.9";
|
|
35825
35825
|
}
|
|
35826
35826
|
return "0.0.0";
|
|
35827
35827
|
}
|
package/dist/installer.mjs
CHANGED
|
@@ -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.
|
|
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
|
|
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";
|
package/dist/tool-server.cjs
CHANGED
|
@@ -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
|
|
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 === "...") &&
|
|
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 === "...") &&
|
|
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 && !
|
|
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 === ":") &&
|
|
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("...")) &&
|
|
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 ||
|
|
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) =>
|
|
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 (
|
|
96430
|
+
if (isEmpty(next) || inFlow && flowIndicatorChars.has(next))
|
|
96431
96431
|
break;
|
|
96432
96432
|
end = i;
|
|
96433
|
-
} else if (
|
|
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 (
|
|
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 (!
|
|
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.
|
|
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
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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 =
|
|
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 && !
|
|
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
|
|
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
|
|
157177
|
-
const
|
|
157178
|
-
|
|
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 &&
|
|
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 (
|
|
157312
|
+
if (isEmptyFocus(res) && device.platform !== "android") {
|
|
157206
157313
|
await api.recycleAx();
|
|
157207
157314
|
res = await api.describe();
|
|
157208
157315
|
}
|
|
157209
|
-
if (
|
|
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 =
|
|
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,
|
|
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
|
-
|
|
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
|
|
158387
|
-
Returns { settled, waitedMs, polls
|
|
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,
|
|
158547
|
+
fetchTree: (budgetMs) => fetchTree(device, services, tvApi, androidIsTv, budgetMs),
|
|
158419
158548
|
timeoutMs: params.timeoutMs ?? DEFAULT_TIMEOUT_MS4,
|
|
158420
|
-
pollIntervalMs
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
@@ -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
|
|
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
|
|
20
|
-
3. `describe` again to confirm
|
|
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
|
|
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)
|