@avasapp/agent-bridge 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +220 -20
  2. package/dist/adapters/ably.cjs +22 -4
  3. package/dist/adapters/ably.d.cts +8 -1
  4. package/dist/adapters/expo-router.cjs +22 -1
  5. package/dist/adapters/expo-router.d.cts +5 -1
  6. package/dist/adapters/react-native-mmkv.cjs +1 -0
  7. package/dist/adapters/react-native-mmkv.d.cts +1 -1
  8. package/dist/adapters/react-native-webview.cjs +672 -0
  9. package/dist/adapters/react-native-webview.d.cts +79 -0
  10. package/dist/adapters/socket-io.cjs +3 -1
  11. package/dist/adapters/socket-io.d.cts +1 -1
  12. package/dist/adapters/tanstack-query.cjs +256 -17
  13. package/dist/adapters/tanstack-query.d.cts +1 -1
  14. package/dist/adapters/zustand.cjs +152 -12
  15. package/dist/adapters/zustand.d.cts +19 -3
  16. package/dist/{chunk-LBYZ5JHF.js → chunk-AJD3XTNT.js} +329 -46
  17. package/dist/cli.js +848 -98
  18. package/dist/client/index.cjs +311 -47
  19. package/dist/client/index.d.ts +64 -3
  20. package/dist/client/index.js +5 -3
  21. package/dist/expo/index.d.cts +1 -1
  22. package/dist/network/index.cjs +434 -66
  23. package/dist/network/index.d.cts +53 -3
  24. package/dist/noop/index.cjs +12 -0
  25. package/dist/noop/network.cjs +7 -1
  26. package/dist/noop/react-native-webview.cjs +31 -0
  27. package/dist/realtime/index.cjs +2 -1
  28. package/dist/realtime/index.d.cts +3 -1
  29. package/dist/runtime/index.cjs +1067 -111
  30. package/dist/runtime/index.d.cts +51 -5
  31. package/dist/types-BmpykP1P.d.cts +137 -0
  32. package/entries/react-native-webview.cjs +9 -0
  33. package/package.json +18 -6
  34. package/skills/agent-bridge/SKILL.md +226 -8
  35. package/dist/types-C6DUUHnB.d.cts +0 -76
@@ -22,13 +22,15 @@ var runtime_exports = {};
22
22
  __export(runtime_exports, {
23
23
  RUNTIME_MARKER: () => RUNTIME_MARKER,
24
24
  cdpTransport: () => cdpTransport,
25
+ createGate: () => createGate,
26
+ onRestore: () => onRestore,
25
27
  settle: () => settle,
26
28
  startAgentBridge: () => startAgentBridge,
27
29
  useAgentBridge: () => useAgentBridge
28
30
  });
29
31
  module.exports = __toCommonJS(runtime_exports);
30
32
  var import_react = require("react");
31
- var import_react_native2 = require("react-native");
33
+ var import_react_native3 = require("react-native");
32
34
 
33
35
  // src/shared/protocol.ts
34
36
  var PROTOCOL_VERSION = 1;
@@ -42,6 +44,73 @@ function toAsciiJson(value) {
42
44
  );
43
45
  }
44
46
 
47
+ // src/runtime/builtin-tools.ts
48
+ var import_react_native2 = require("react-native");
49
+
50
+ // src/runtime/undo.ts
51
+ var undos = [];
52
+ function onRestore(fn, label) {
53
+ if (typeof fn !== "function") throw new Error("onRestore takes a function");
54
+ undos.push({ fn, label });
55
+ }
56
+ function pendingUndos() {
57
+ return undos.map((u) => u.label ?? "undo").reverse();
58
+ }
59
+ async function runUndos() {
60
+ const batch = undos.splice(0).reverse();
61
+ const errors = [];
62
+ for (const { fn, label } of batch) {
63
+ try {
64
+ await fn();
65
+ } catch (error) {
66
+ const why = error instanceof Error ? error.message : String(error);
67
+ errors.push(label ? `${label}: ${why}` : why);
68
+ }
69
+ }
70
+ return { ran: batch.length, errors };
71
+ }
72
+
73
+ // src/runtime/tools/app.ts
74
+ function selectReload(host, devSettingsReload) {
75
+ return () => {
76
+ const expo = host.expo;
77
+ if (typeof expo?.reloadAppAsync !== "function") return devSettingsReload();
78
+ try {
79
+ Promise.resolve(expo.reloadAppAsync("agent-bridge app.reload")).catch(
80
+ () => devSettingsReload()
81
+ );
82
+ } catch {
83
+ devSettingsReload();
84
+ }
85
+ };
86
+ }
87
+ function appTools(reload) {
88
+ return {
89
+ "app.reload": {
90
+ description: "Reload the app's JS (Expo's reloadAppAsync, else DevSettings.reload). Pending restores (store, query, mocks, pins) are lost. Returns before it reloads; a session waits for the new bridge and reports what was lost.",
91
+ run: () => {
92
+ setTimeout(reload, 50);
93
+ return { reloading: true };
94
+ }
95
+ },
96
+ "app.restore": {
97
+ maxArgs: 0,
98
+ // The labels, so bridge.pending shows what the app's own tools changed.
99
+ pending: () => {
100
+ const labels = pendingUndos();
101
+ return labels.length ? labels : false;
102
+ },
103
+ description: "Undo what the app's own tools changed: runs the undos they registered with onRestore, newest first, once. Returns how many ran.",
104
+ run: async () => {
105
+ const { ran, errors } = await runUndos();
106
+ if (errors.length)
107
+ throw new Error(`${errors.join("; ")} (${ran - errors.length} of ${ran} undone)`);
108
+ return { undone: ran };
109
+ }
110
+ }
111
+ };
112
+ }
113
+
45
114
  // src/runtime/tools/bridge.ts
46
115
  function bridgeTools(listTools) {
47
116
  return {
@@ -56,6 +125,42 @@ function bridgeTools(listTools) {
56
125
  };
57
126
  }
58
127
 
128
+ // src/runtime/tools/capture.ts
129
+ var MAX_BASE64_CHARS = 2e5;
130
+ var UNAVAILABLE = "screen.capture needs react-native-view-shot, which this app does not have. Take the screenshot with agent-device, `xcrun simctl io booted screenshot <file>` (iOS simulator) or `adb exec-out screencap -p > <file>` (Android) instead.";
131
+ function loadViewShot() {
132
+ try {
133
+ const mod = require("react-native-view-shot");
134
+ return typeof mod?.captureScreen === "function" ? mod : void 0;
135
+ } catch {
136
+ return void 0;
137
+ }
138
+ }
139
+ function captureTools(load) {
140
+ return {
141
+ "screen.capture": {
142
+ description: "Screenshot the app as a PNG. Returns {path} of a file on the device. `{base64:true}` returns {base64} instead when small enough (else {path}). Only works if the app already has react-native-view-shot; otherwise errors and you use agent-device, simctl or adb.",
143
+ maxArgs: 1,
144
+ run: async (options) => {
145
+ const viewShot = load();
146
+ if (!viewShot) throw new Error(UNAVAILABLE);
147
+ if (options?.base64) {
148
+ const base64 = await viewShot.captureScreen({
149
+ format: "png",
150
+ result: "base64"
151
+ });
152
+ if (base64.length <= MAX_BASE64_CHARS) return { base64 };
153
+ }
154
+ const path2 = await viewShot.captureScreen({
155
+ format: "png",
156
+ result: "tmpfile"
157
+ });
158
+ return { path: path2 };
159
+ }
160
+ }
161
+ };
162
+ }
163
+
59
164
  // src/runtime/tools/logs.ts
60
165
  function logTools(logs) {
61
166
  return {
@@ -73,14 +178,35 @@ function logTools(logs) {
73
178
  // src/runtime/tools/restore.ts
74
179
  var SUFFIX = ".restore";
75
180
  var SELF = "bridge.restore";
181
+ var LAST = "scenario.restore";
182
+ function pendingByArea(tools, detail = false) {
183
+ const out = {};
184
+ for (const [name, d] of Object.entries(tools)) {
185
+ if (!name.endsWith(SUFFIX) || name === SELF || typeof d === "function")
186
+ continue;
187
+ const area3 = name.slice(0, -SUFFIX.length);
188
+ try {
189
+ const found = d.pending?.({ detail });
190
+ if (found) out[area3] = found;
191
+ } catch (error) {
192
+ out[area3] = `pending check failed: ${error instanceof Error ? error.message : String(error)}`;
193
+ }
194
+ }
195
+ return out;
196
+ }
76
197
  function restoreTools(getTools) {
77
198
  return {
199
+ "bridge.pending": {
200
+ maxArgs: 0,
201
+ description: "What bridge.restore would undo right now, per area (store, query, net, app, ...): the value is true, or detail such as a store's snapshot against its current value. Empty when nothing is pending. Changes nothing.",
202
+ run: () => pendingByArea(getTools(), true)
203
+ },
78
204
  [SELF]: {
79
- description: "Undo what the agent changed: runs every *.restore tool in name order and returns each result or error.",
205
+ description: "Undo what the agent changed: runs every *.restore tool in name order, scenario.restore last, and returns each result or error.",
80
206
  run: async () => {
81
207
  const tools = getTools();
82
208
  const results = {};
83
- const names = Object.keys(tools).filter((name) => name.endsWith(SUFFIX) && name !== SELF).sort();
209
+ const names = Object.keys(tools).filter((name) => name.endsWith(SUFFIX) && name !== SELF).sort((a, b) => Number(a === LAST) - Number(b === LAST) || a.localeCompare(b));
84
210
  for (const name of names) {
85
211
  const definition = tools[name];
86
212
  const run = typeof definition === "function" ? definition : definition.run;
@@ -98,6 +224,160 @@ function restoreTools(getTools) {
98
224
  };
99
225
  }
100
226
 
227
+ // src/runtime/options-schema.ts
228
+ var import_json_schema = require("@cfworker/json-schema");
229
+ var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
230
+ var CASCADE = /* @__PURE__ */ new Set([
231
+ "properties",
232
+ "patternProperties",
233
+ "additionalProperties",
234
+ "unevaluatedProperties",
235
+ "items",
236
+ "prefixItems",
237
+ "unevaluatedItems",
238
+ "allOf",
239
+ "if",
240
+ "then",
241
+ "else",
242
+ "dependentSchemas",
243
+ "$ref",
244
+ "false"
245
+ ]);
246
+ var path = (instanceLocation) => instanceLocation === "#" ? "options" : `options${instanceLocation.slice(1)}`;
247
+ function unknownKeys(schema, value, at, out) {
248
+ if (!isObject(schema)) return;
249
+ if (Array.isArray(value)) {
250
+ if (isObject(schema.items))
251
+ value.forEach((item, i) => unknownKeys(schema.items, item, `${at}/${i}`, out));
252
+ return;
253
+ }
254
+ if (!isObject(value) || !isObject(schema.properties)) return;
255
+ const props = schema.properties;
256
+ const open = schema.additionalProperties !== void 0 && schema.additionalProperties !== false || schema.patternProperties !== void 0 || // Composed schemas split their properties across branches; leave them to the validator.
257
+ ["allOf", "anyOf", "oneOf", "$ref"].some((k) => schema[k] !== void 0);
258
+ for (const [key, child] of Object.entries(value)) {
259
+ if (key in props) unknownKeys(props[key], child, `${at}/${key}`, out);
260
+ else if (!open)
261
+ out.push(`${path(at)}: unknown option "${key}". Known: ${Object.keys(props).join(", ") || "none"}`);
262
+ }
263
+ }
264
+ var validators = /* @__PURE__ */ new WeakMap();
265
+ function checkOptions(schema, options) {
266
+ let validator = validators.get(schema);
267
+ if (!validator) {
268
+ validator = new import_json_schema.Validator(schema, "2020-12", false);
269
+ validators.set(schema, validator);
270
+ }
271
+ const value = options === void 0 ? {} : options;
272
+ const problems = [];
273
+ unknownKeys(schema, value, "#", problems);
274
+ const result = validator.validate(value);
275
+ if (!result.valid) {
276
+ const leaves = result.errors.filter(
277
+ (e) => !CASCADE.has(e.keyword) && !/\/(anyOf|oneOf)\/\d/.test(e.keywordLocation)
278
+ );
279
+ const shown = leaves.length || problems.length ? leaves : result.errors;
280
+ for (const e of shown) problems.push(`${path(e.instanceLocation)}: ${e.error}`);
281
+ }
282
+ return [...new Set(problems)];
283
+ }
284
+
285
+ // src/runtime/tools/scenario.ts
286
+ var active = /* @__PURE__ */ new Map();
287
+ var message = (error) => error instanceof Error ? error.message : String(error);
288
+ async function runUndos2(entry) {
289
+ const errors = [];
290
+ for (const undo2 of entry.undos.splice(0).reverse()) {
291
+ try {
292
+ await undo2();
293
+ } catch (error) {
294
+ errors.push(message(error));
295
+ }
296
+ }
297
+ return errors;
298
+ }
299
+ async function undo(name) {
300
+ const entry = active.get(name);
301
+ if (!entry) return [];
302
+ active.delete(name);
303
+ return (await runUndos2(entry)).map((e) => `${name}: ${e}`);
304
+ }
305
+ function scenarioTools(getScenarios, getTools) {
306
+ const find = (name) => {
307
+ const scenarios = getScenarios();
308
+ const scenario = scenarios[name];
309
+ if (!scenario)
310
+ throw new Error(
311
+ `Unknown scenario "${name}". Known: ${Object.keys(scenarios).join(", ") || "none"}`
312
+ );
313
+ return scenario;
314
+ };
315
+ const call = async (tool, ...args) => {
316
+ const definition = getTools()[tool];
317
+ if (!definition) throw new Error(`Unknown tool "${tool}"`);
318
+ const run = typeof definition === "function" ? definition : definition.run;
319
+ return run(...args);
320
+ };
321
+ return {
322
+ "scenario.list": {
323
+ description: "Setups the app defines, e.g. a signed-in user: name, description, options (a JSON Schema), active.",
324
+ run: () => Object.entries(getScenarios()).map(([name, s]) => ({
325
+ name,
326
+ description: s.description,
327
+ options: s.options,
328
+ active: active.has(name)
329
+ })).sort((a, b) => a.name.localeCompare(b.name))
330
+ },
331
+ "scenario.apply": {
332
+ description: "Apply a scenario: [name, options?]. Options must fit its schema (scenario.list). Returns what it returns. Applying an active one undoes it first. bridge.restore undoes it.",
333
+ run: async (name, options) => {
334
+ const scenario = find(name);
335
+ if (scenario.options) {
336
+ const problems = checkOptions(scenario.options, options);
337
+ if (problems.length)
338
+ throw new Error(
339
+ `Scenario "${name}" got bad options, nothing was applied:
340
+ - ${problems.join("\n- ")}
341
+ scenario.list shows its options schema.`
342
+ );
343
+ }
344
+ const previous = await undo(name);
345
+ if (previous.length)
346
+ throw new Error(`Undoing the active "${name}" failed: ${previous.join("; ")}`);
347
+ const entry = { options, undos: [] };
348
+ active.set(name, entry);
349
+ const context = {
350
+ options,
351
+ call,
352
+ onUndo: (fn) => {
353
+ if (typeof fn !== "function")
354
+ throw new Error("onUndo takes a function");
355
+ entry.undos.push(fn);
356
+ }
357
+ };
358
+ try {
359
+ return await scenario.apply(context);
360
+ } catch (error) {
361
+ const errors = await undo(name);
362
+ const extra = errors.length ? ` (undo: ${errors.join("; ")})` : "";
363
+ throw new Error(`Scenario "${name}" failed: ${message(error)}${extra}`);
364
+ }
365
+ }
366
+ },
367
+ "scenario.restore": {
368
+ pending: () => active.size > 0,
369
+ description: "Undo every active scenario, newest first. bridge.restore runs this after every other restorer.",
370
+ run: async () => {
371
+ const names = [...active.keys()].reverse();
372
+ const errors = [];
373
+ for (const name of names) errors.push(...await undo(name));
374
+ if (errors.length) throw new Error(errors.join("; "));
375
+ return names;
376
+ }
377
+ }
378
+ };
379
+ }
380
+
101
381
  // src/runtime/tools/screen.ts
102
382
  var import_react_native = require("react-native");
103
383
 
@@ -141,40 +421,101 @@ function textOf(fiber) {
141
421
  }
142
422
  return null;
143
423
  }
144
- function inActiveScreen(fiber) {
145
- for (let f = fiber; f; f = f.return) {
146
- if (f.tag !== HOST_COMPONENT) continue;
147
- const props = f.memoizedProps;
148
- if (props?.activityState === 0) return false;
424
+
425
+ // src/runtime/screen/webview-mark.ts
426
+ var MARK = /^\/\*agent-bridge:webview:([^*]*)\*\//;
427
+ function webViewNameOf(props) {
428
+ const script = props.injectedJavaScriptBeforeContentLoaded;
429
+ const found = typeof script === "string" ? MARK.exec(script) : null;
430
+ if (!found) return void 0;
431
+ try {
432
+ return decodeURIComponent(found[1]);
433
+ } catch {
434
+ return found[1];
149
435
  }
436
+ }
437
+
438
+ // src/runtime/screen/elements.ts
439
+ var propsOf = (fiber) => fiber.memoizedProps && typeof fiber.memoizedProps === "object" ? fiber.memoizedProps : null;
440
+ var nodeOf = (fiber) => {
441
+ const n = fiber.stateNode;
442
+ return n && typeof n === "object" ? n : null;
443
+ };
444
+ var SCROLL_METHODS = ["scrollTo", "scrollToOffset"];
445
+ function scrolls(fiber) {
446
+ const node = nodeOf(fiber);
447
+ if (!node || !SCROLL_METHODS.some((m) => isFn(node[m]))) return false;
448
+ if (typeof node.scrollHeight === "number" && typeof node.clientHeight === "number")
449
+ return node.scrollHeight > node.clientHeight + 1 || node.scrollWidth > node.clientWidth + 1;
150
450
  return true;
151
451
  }
152
- function findTextInTree(roots, text, window, options = {}) {
153
- const stack = [...roots];
154
- const matches = [];
155
- while (stack.length) {
156
- const fiber = stack.pop();
157
- const found = textOf(fiber);
158
- if (found && (options.exact ? found.text === text : found.text.includes(text))) {
159
- const rect = found.host ? measureHost(found.host) : null;
160
- const onScreen = !!rect && inActiveScreen(fiber) && rect.width > 0 && rect.height > 0 && rect.x < window.width && rect.y < window.height && rect.x + rect.width > 0 && rect.y + rect.height > 0;
161
- matches.push({ text: found.text, rect, onScreen });
452
+ var displayNone = (style) => displayOf(style) === "none";
453
+ function displayOf(style) {
454
+ if (Array.isArray(style)) {
455
+ for (let i = style.length - 1; i >= 0; i--) {
456
+ const d = displayOf(style[i]);
457
+ if (d !== void 0) return d;
162
458
  }
163
- if (fiber.child) stack.push(fiber.child);
164
- if (fiber.sibling) stack.push(fiber.sibling);
459
+ return void 0;
460
+ }
461
+ return style && typeof style === "object" ? style.display : void 0;
462
+ }
463
+ var hiddenBy = (p) => p.activityState === 0 || displayNone(p.style) || p["aria-hidden"] === true || p.accessibilityElementsHidden === true || p.importantForAccessibility === "no-hide-descendants";
464
+ var MODAL_HOSTS = /* @__PURE__ */ new Set(["RCTModalHostView", "ModalHostView"]);
465
+ var OVERLAY_HOST = "RNSFullWindowOverlay";
466
+ var SCREEN_STACK = "RNSScreenStack";
467
+ var STACK_SCREENS = /* @__PURE__ */ new Set(["RNSScreen", "RNSModalScreen"]);
468
+ var SEE_THROUGH = /* @__PURE__ */ new Set(["transparentModal", "containedTransparentModal", "overFullScreen"]);
469
+ var undimmedSheet = (p) => {
470
+ const at = p.sheetLargestUndimmedDetentIndex;
471
+ return p.stackPresentation === "formSheet" && at !== void 0 && at !== null && at !== "none" && at !== -1;
472
+ };
473
+ function presentationOf(screen) {
474
+ const p = propsOf(screen);
475
+ if (typeof p?.stackPresentation === "string") return p.stackPresentation;
476
+ return screen.type === "RNSModalScreen" ? "modal" : null;
477
+ }
478
+ var seeThrough = (screen) => {
479
+ const presentation = presentationOf(screen);
480
+ return presentation !== null && SEE_THROUGH.has(presentation) || undimmedSheet(propsOf(screen) ?? {});
481
+ };
482
+ var coversWindow = (screen) => {
483
+ const presentation = presentationOf(screen);
484
+ return presentation !== null && presentation !== "push" && !undimmedSheet(propsOf(screen) ?? {});
485
+ };
486
+ function stackScreens(stack) {
487
+ const screens = [];
488
+ const todo = stack.child ? [stack.child] : [];
489
+ while (todo.length) {
490
+ const f = todo.pop();
491
+ if (f.sibling) todo.push(f.sibling);
492
+ if (f.tag === HOST_COMPONENT && STACK_SCREENS.has(f.type)) {
493
+ if (propsOf(f)?.activityState !== 0) screens.push(f);
494
+ } else if (f.child) todo.push(f.child);
165
495
  }
496
+ let base = screens.length - 1;
497
+ while (base > 0 && seeThrough(screens[base])) base--;
166
498
  return {
167
- found: matches.length,
168
- onScreen: matches.filter((m) => m.onScreen).length,
169
- matches
499
+ covered: screens.slice(0, Math.max(0, base)),
500
+ presented: screens.slice(1).filter(coversWindow)
170
501
  };
171
502
  }
172
-
173
- // src/runtime/screen/elements.ts
174
- var propsOf = (fiber) => fiber.memoizedProps && typeof fiber.memoizedProps === "object" ? fiber.memoizedProps : null;
503
+ function hostAbove(host) {
504
+ let up = host.return;
505
+ while (up && up.tag !== HOST_COMPONENT) up = up.return;
506
+ return up ?? host;
507
+ }
508
+ var REFRESH_HOST = /refresh/i;
509
+ var refreshTwin = (rec, host) => rec && REFRESH_HOST.test(String(rec.host.type)) && hostAbove(host) === rec.host ? rec : null;
510
+ var inside = (modal, top) => {
511
+ for (let m = modal; m; m = m.up) if (m === top) return true;
512
+ return false;
513
+ };
514
+ var SWITCH_HOSTS = /* @__PURE__ */ new Set(["RCTSwitch", "AndroidSwitch"]);
175
515
  var isFn = (v) => typeof v === "function";
176
- var isInputProps = (p) => isFn(p.onChangeText) || isFn(p.onChange) && ("value" in p || "defaultValue" in p || "placeholder" in p);
177
- var interesting = (p) => isFn(p.onPress) || isInputProps(p) || typeof p.testID === "string";
516
+ var isInputProps = (p) => isFn(p.onChangeText) || isFn(p.onChange) && typeof p.value !== "boolean" && ("value" in p || "defaultValue" in p || "placeholder" in p);
517
+ var labelOf = (p) => p.accessibilityLabel ?? p["aria-label"];
518
+ var interesting = (p) => isFn(p.onPress) || isFn(p.onValueChange) || isInputProps(p) || typeof p.testID === "string" || typeof labelOf(p) === "string" || webViewNameOf(p) !== void 0;
178
519
  var ICON_GLYPHS = /[\uE000-\uF8FF]/g;
179
520
  function firstHost(fiber) {
180
521
  if (fiber.tag === HOST_COMPONENT) return fiber;
@@ -195,22 +536,77 @@ function absorb(rec, fiber, p) {
195
536
  if (e[key] === void 0 && typeof v === "string") e[key] = v;
196
537
  };
197
538
  fill("testID", p.testID);
198
- fill("label", p.accessibilityLabel ?? p["aria-label"]);
539
+ fill("label", labelOf(p));
199
540
  fill("role", p.accessibilityRole ?? p.role);
200
541
  fill("placeholder", p.placeholder);
201
- if (e.value === void 0 && ("value" in p || "defaultValue" in p)) {
542
+ const webview = webViewNameOf(p);
543
+ if (e.webview === void 0 && webview !== void 0) e.webview = webview;
544
+ const isSwitch = SWITCH_HOSTS.has(fiber.type) || (p.accessibilityRole ?? p.role ?? e.role) === "switch";
545
+ if (isSwitch && typeof p.value === "boolean") {
546
+ if (e.checked === void 0) e.checked = p.value;
547
+ if (e.role === void 0) e.role = "switch";
548
+ } else if (e.value === void 0 && !(typeof p.value === "boolean" && isFn(p.onValueChange)) && ("value" in p || "defaultValue" in p)) {
202
549
  const v = p.value ?? p.defaultValue;
203
- if (v != null) e.value = String(v);
550
+ if (v != null && typeof v !== "object" && !isFn(v)) e.value = String(v);
204
551
  }
205
552
  if (rec.maxLength === void 0 && typeof p.maxLength === "number")
206
553
  rec.maxLength = p.maxLength;
207
554
  if (p.editable === false || p.readOnly === true) e.editable = false;
208
555
  const state = p.accessibilityState;
556
+ const checked = state?.checked ?? p["aria-checked"];
557
+ if (e.checked === void 0 && (typeof checked === "boolean" || checked === "mixed"))
558
+ e.checked = checked;
559
+ const flag = (key) => {
560
+ const v = state?.[key] ?? p[`aria-${key}`];
561
+ if (e[key] === void 0 && typeof v === "boolean") e[key] = v;
562
+ };
563
+ flag("selected");
564
+ flag("expanded");
209
565
  if (p.disabled === true || state?.disabled === true || p["aria-disabled"] === true)
210
566
  e.disabled = true;
211
567
  }
212
- function isOnScreen(rect, window) {
213
- return !!rect && rect.width > 0 && rect.height > 0 && rect.x < window.width && rect.y < window.height && rect.x + rect.width > 0 && rect.y + rect.height > 0;
568
+ var intersect = (a, b) => {
569
+ const x = Math.max(a.x, b.x);
570
+ const y = Math.max(a.y, b.y);
571
+ const right = Math.min(a.x + a.width, b.x + b.width);
572
+ const bottom = Math.min(a.y + a.height, b.y + b.height);
573
+ return right > x && bottom > y ? { x, y, width: right - x, height: bottom - y } : null;
574
+ };
575
+ var usable = (r) => !!r && r.width > 0 && r.height > 0;
576
+ var within = (host, ancestor) => {
577
+ for (let f = host; f; f = f.return) if (f === ancestor) return true;
578
+ return false;
579
+ };
580
+ function visibleArea(base, host, bars, window) {
581
+ let area3 = base;
582
+ const clip2 = (r) => {
583
+ area3 = area3 && intersect(area3, r);
584
+ };
585
+ for (const bar of bars) {
586
+ if (within(host, bar)) continue;
587
+ const r = measureHost(bar);
588
+ if (!usable(r) || r.width < window.width * 0.9) continue;
589
+ clip2(
590
+ r.y + r.height / 2 > window.height / 2 ? { x: -1e9, y: -1e9, width: 2e9, height: 1e9 + r.y } : { x: -1e9, y: r.y + r.height, width: 2e9, height: 1e9 }
591
+ );
592
+ }
593
+ return area3;
594
+ }
595
+ var viewportOf = (s, view, window) => usable(view) ? visibleArea(view, s.host, s.modal ? [] : s.bars, window) : null;
596
+ function isOnScreen(rec, rect, window, bars) {
597
+ if (!usable(rect)) return false;
598
+ const shown = intersect(rect, { x: 0, y: 0, ...window });
599
+ if (!shown) return false;
600
+ const cx = shown.x + shown.width / 2;
601
+ const cy = shown.y + shown.height / 2;
602
+ const holds = (r) => !!r && cx >= r.x && cx <= r.x + r.width && cy >= r.y && cy <= r.y + r.height;
603
+ if (!holds(visibleArea(shown, rec.host, rec.modal ? [] : bars, window))) return false;
604
+ for (let s = rec.scroller; s; s = s.parent) {
605
+ if (s.host === rec.host || s.modal !== rec.modal) continue;
606
+ const view = measureHost(s.host);
607
+ if (usable(view) && !holds(viewportOf(s, view, window))) return false;
608
+ }
609
+ return true;
214
610
  }
215
611
  var round = (r) => r && {
216
612
  x: Math.round(r.x),
@@ -234,10 +630,15 @@ function absorbChain(rec, fiber, host) {
234
630
  if (f === host) break;
235
631
  }
236
632
  }
237
- function collectElements(roots, window) {
633
+ function collectScreen(roots, window) {
238
634
  const byHost = /* @__PURE__ */ new Map();
239
635
  const order = [];
240
- const recFor = (fiber, host, parent) => {
636
+ const scrollByHost = /* @__PURE__ */ new Map();
637
+ const covered = /* @__PURE__ */ new Set();
638
+ const presented = /* @__PURE__ */ new Set();
639
+ const bars = [];
640
+ let topModal = null;
641
+ const recFor = (fiber, host, parent, hidden, scroller, modal, overlay) => {
241
642
  let rec = byHost.get(host);
242
643
  if (rec) return rec;
243
644
  rec = {
@@ -247,6 +648,10 @@ function collectElements(roots, window) {
247
648
  host,
248
649
  press: null,
249
650
  input: null,
651
+ hidden,
652
+ scroller,
653
+ modal,
654
+ overlay,
250
655
  parent,
251
656
  parts: []
252
657
  };
@@ -259,49 +664,118 @@ function collectElements(roots, window) {
259
664
  for (let r = rec; r; r = r.parent) if (r.press) return r;
260
665
  return null;
261
666
  };
262
- const stack = roots.map((fiber) => ({ fiber, rec: null }));
667
+ const textHost = (host) => {
668
+ let h = host;
669
+ for (; ; ) {
670
+ let up = h.return;
671
+ while (up && up.tag !== HOST_COMPONENT) up = up.return;
672
+ if (!up) return h;
673
+ if (h.type !== "RCTVirtualText") return h;
674
+ h = up;
675
+ }
676
+ };
677
+ const stack = roots.map((fiber) => ({
678
+ fiber,
679
+ rec: null,
680
+ hidden: false,
681
+ scroller: null,
682
+ modal: null,
683
+ overlay: false
684
+ }));
263
685
  while (stack.length) {
264
- const { fiber, rec: parentRec } = stack.pop();
265
- if (fiber.sibling) stack.push({ fiber: fiber.sibling, rec: parentRec });
686
+ const item = stack.pop();
687
+ const { fiber, rec: parentRec } = item;
688
+ if (fiber.sibling) stack.push({ ...item, fiber: fiber.sibling });
266
689
  const p = propsOf(fiber);
267
- if (fiber.tag === HOST_COMPONENT && p?.activityState === 0) continue;
690
+ let { hidden, scroller, modal, overlay } = item;
691
+ if (fiber.tag === HOST_COMPONENT) {
692
+ if (p && hiddenBy(p)) hidden = true;
693
+ if (covered.has(fiber)) hidden = true;
694
+ if (fiber.type === SCREEN_STACK) {
695
+ const screens = stackScreens(fiber);
696
+ for (const s of screens.covered) covered.add(s);
697
+ for (const s of screens.presented) presented.add(s);
698
+ }
699
+ if (fiber.type === OVERLAY_HOST) overlay = true;
700
+ if (p && !hidden && (p.accessibilityRole ?? p.role) === "tablist") bars.push(fiber);
701
+ if (!hidden && (MODAL_HOSTS.has(fiber.type) || presented.has(fiber))) {
702
+ modal = { host: fiber, up: modal };
703
+ topModal = modal;
704
+ }
705
+ }
706
+ if (scrolls(fiber)) {
707
+ const host = firstHost(fiber);
708
+ if (host) {
709
+ let found2 = scrollByHost.get(host);
710
+ if (!found2) {
711
+ found2 = { host, fibers: [], hidden, parent: scroller, modal, overlay, bars };
712
+ scrollByHost.set(host, found2);
713
+ }
714
+ found2.fibers.push(fiber);
715
+ scroller = found2;
716
+ }
717
+ }
268
718
  let rec = parentRec;
269
719
  if (p && interesting(p)) {
270
720
  const host = firstHost(fiber);
271
- const same = host && !byHost.has(host) ? sameControl(parentRec, p) : null;
272
- if (host && same) {
721
+ const twin = host && !byHost.has(host) ? refreshTwin(parentRec, host) : null;
722
+ const same = host && !twin && !byHost.has(host) ? sameControl(parentRec, p) : null;
723
+ if (host && twin) {
724
+ byHost.set(host, twin);
725
+ rec = twin;
726
+ } else if (host && same) {
273
727
  byHost.set(host, same);
274
728
  same.host = host;
275
729
  absorbChain(same, fiber, host);
276
730
  rec = same;
277
731
  } else if (host) {
278
- rec = recFor(fiber, host, parentRec);
732
+ rec = recFor(fiber, host, parentRec, hidden, scroller, modal, overlay);
279
733
  absorb(rec, fiber, p);
280
734
  }
281
735
  }
282
736
  const found = textOf(fiber);
283
737
  if (found?.host) {
284
- const owner = buttonOf(rec) ?? recFor(found.host, found.host, rec);
738
+ const host = textHost(found.host);
739
+ const owner = buttonOf(rec) ?? recFor(host, host, rec, hidden, scroller, modal, overlay);
285
740
  const last = owner.parts[owner.parts.length - 1];
286
- if (last?.host === found.host) last.text += found.text;
287
- else owner.parts.push({ host: found.host, text: found.text });
741
+ if (last?.host === host) last.text += found.text;
742
+ else owner.parts.push({ host, text: found.text });
288
743
  }
289
- if (fiber.child) stack.push({ fiber: fiber.child, rec });
744
+ if (fiber.child) stack.push({ fiber: fiber.child, rec, hidden, scroller, modal, overlay });
290
745
  }
746
+ const top = topModal;
747
+ const covers2 = (x) => !!top && !x.overlay && !inside(x.modal, top);
748
+ for (const scrollable of scrollByHost.values())
749
+ scrollable.hidden || (scrollable.hidden = covers2(scrollable));
291
750
  for (const rec of order) {
292
751
  const e = rec.element;
752
+ rec.hidden || (rec.hidden = covers2(rec));
293
753
  const text = rec.parts.map((part) => part.text.replace(ICON_GLYPHS, "").replace(/\s+/g, " ").trim()).filter(Boolean).join(" ");
294
754
  if (text) e.text = text;
295
- e.kind = rec.input ? "input" : rec.press ? "button" : rec.parts.length ? "text" : "view";
755
+ e.kind = e.webview !== void 0 ? "webview" : rec.input ? "input" : rec.press ? "button" : rec.parts.length ? "text" : "view";
296
756
  if (e.kind === "input" && e.editable === void 0) e.editable = true;
297
757
  const rect = measureHost(rec.host);
298
758
  e.rect = round(rect);
299
- rec.onScreen = isOnScreen(rect, window);
759
+ const own = scrollByHost.get(rec.host) ?? scrollByHost.get(hostAbove(rec.host));
760
+ if (own) rec.scroller = own;
761
+ rec.onScreen = isOnScreen(rec, rect, window, bars);
300
762
  }
301
- return order.filter(
302
- (rec) => rec.element.kind !== "text" || rec.element.text !== void 0
303
- );
763
+ const named = /* @__PURE__ */ new Set();
764
+ const single = (rec) => {
765
+ const name = rec.element.webview;
766
+ if (name === void 0) return true;
767
+ if (named.has(name)) return false;
768
+ named.add(name);
769
+ return true;
770
+ };
771
+ return {
772
+ found: order.filter(
773
+ (rec) => (rec.element.kind !== "text" || rec.element.text !== void 0) && single(rec)
774
+ ),
775
+ scrollables: [...scrollByHost.values()]
776
+ };
304
777
  }
778
+ var focused = (found) => found.filter((f) => !f.hidden);
305
779
  function pressFiberOf(found) {
306
780
  if (found.press) return found.press;
307
781
  for (let f = found.fiber.return; f; f = f.return) {
@@ -312,6 +786,26 @@ function pressFiberOf(found) {
312
786
  }
313
787
 
314
788
  // src/runtime/screen/targets.ts
789
+ var TARGET_KEYS = ["testID", "label", "placeholder", "text", "at", "index"];
790
+ function checkTarget(target) {
791
+ if (typeof target === "string") return;
792
+ if (target === null || typeof target !== "object" || Array.isArray(target))
793
+ throw new Error(
794
+ `A target is a string or an object with ${TARGET_KEYS.join(", ")}; got ${JSON.stringify(target)}`
795
+ );
796
+ const unknown = Object.keys(target).filter((k) => !TARGET_KEYS.includes(k));
797
+ if (unknown.length)
798
+ throw new Error(
799
+ `Unknown target key ${unknown.map((k) => JSON.stringify(k)).join(", ")}. Valid keys: ${TARGET_KEYS.join(", ")}`
800
+ );
801
+ const { at, index } = target;
802
+ if (at !== void 0 && !(Array.isArray(at) && at.length === 2 && at.every(Number.isFinite)))
803
+ throw new Error(`Target "at" must be [x, y] in window points; got ${JSON.stringify(at)}`);
804
+ if (index !== void 0 && !Number.isInteger(index))
805
+ throw new Error(`Target "index" must be an integer; got ${JSON.stringify(index)}`);
806
+ }
807
+ var covers = (e, [x, y]) => !!e.rect && x >= e.rect.x && x <= e.rect.x + e.rect.width && y >= e.rect.y && y <= e.rect.y + e.rect.height;
808
+ var area = (f) => f.element.rect ? f.element.rect.width * f.element.rect.height : 0;
315
809
  function tiers(target) {
316
810
  if (typeof target === "string") {
317
811
  return [
@@ -322,59 +816,77 @@ function tiers(target) {
322
816
  (e) => !!e.text?.includes(target)
323
817
  ];
324
818
  }
325
- const { testID, label, placeholder, text } = target;
326
- const fields = (e) => (testID === void 0 || e.testID === testID) && (label === void 0 || e.label === label) && (placeholder === void 0 || e.placeholder === placeholder);
819
+ const { testID, label, placeholder, text, at } = target;
820
+ const fields = (e) => (testID === void 0 || e.testID === testID) && (at === void 0 || covers(e, at)) && (label === void 0 || e.label === label) && (placeholder === void 0 || e.placeholder === placeholder);
327
821
  if (text === void 0) return [fields];
328
822
  return [
329
823
  (e) => fields(e) && e.text === text,
330
824
  (e) => fields(e) && !!e.text?.includes(text)
331
825
  ];
332
826
  }
827
+ var labelOnly = ({ element: e, press, input }) => e.kind === "view" && !e.testID && !e.text && !e.placeholder && !press && !input;
333
828
  function matchTarget(found, target) {
334
- for (const test of tiers(target)) {
335
- const matches = found.filter((f) => test(f.element));
336
- if (matches.length) return matches;
829
+ checkTarget(target);
830
+ for (const pool of [found.filter((f) => !labelOnly(f)), found.filter(labelOnly)]) {
831
+ for (const test of tiers(target)) {
832
+ const matches = pool.filter((f) => test(f.element));
833
+ if (matches.length)
834
+ return isAt(target) ? matches.sort((a, b) => area(a) - area(b)) : matches;
835
+ }
337
836
  }
338
837
  return [];
339
838
  }
839
+ var isAt = (target) => typeof target === "object" && target.at !== void 0;
340
840
  var indexOf = (target) => typeof target === "object" ? target.index : void 0;
341
841
  var clip = (s) => s.length > 40 ? `${s.slice(0, 39)}\u2026` : s;
342
842
  function describe(e) {
343
- const bits = [e.kind];
843
+ const stated = e.checked !== void 0 || e.selected !== void 0 || e.expanded !== void 0;
844
+ const bits = [stated && e.role ? e.role : e.kind];
845
+ if (e.webview !== void 0) bits.push(JSON.stringify(e.webview));
344
846
  if (e.testID) bits.push(`#${e.testID}`);
345
847
  if (e.text) bits.push(JSON.stringify(clip(e.text)));
346
848
  if (e.label && e.label !== e.text) bits.push(`label=${JSON.stringify(clip(e.label))}`);
347
849
  if (e.placeholder) bits.push(`placeholder=${JSON.stringify(clip(e.placeholder))}`);
348
850
  if (e.value !== void 0) bits.push(`value=${JSON.stringify(clip(e.value))}`);
851
+ if (e.checked !== void 0) bits.push(e.checked === true ? "checked" : e.checked === false ? "unchecked" : "mixed");
852
+ if (e.selected !== void 0) bits.push(e.selected ? "selected" : "unselected");
853
+ if (e.expanded !== void 0) bits.push(e.expanded ? "expanded" : "collapsed");
349
854
  if (e.disabled) bits.push("disabled");
350
855
  return bits.join(" ");
351
856
  }
352
857
  var showTarget = (target) => JSON.stringify(target);
353
858
  function onScreenSummary(found, max = 15) {
354
- const on = found.filter((f) => f.onScreen);
859
+ const on = found.filter((f) => f.onScreen && !f.hidden);
355
860
  if (!on.length) return "nothing";
356
861
  const shown = on.slice(0, max).map((f) => describe(f.element));
357
862
  if (on.length > max) shown.push(`+${on.length - max} more`);
358
863
  return shown.join("; ");
359
864
  }
360
- function resolveTarget(found, target, prefer) {
865
+ function resolveTarget(found, target, prefer, options) {
866
+ const reachable = found.filter((f) => !f.hidden);
361
867
  let matches = matchTarget(
362
- found.filter((f) => f.onScreen),
868
+ reachable.filter((f) => f.onScreen),
363
869
  target
364
870
  );
871
+ if (!matches.length && options?.offscreen) matches = matchTarget(reachable, target);
365
872
  if (!matches.length) {
366
873
  const hidden = matchTarget(found, target)[0];
874
+ if (hidden?.hidden)
875
+ throw new Error(
876
+ `${showTarget(target)} matches ${describe(hidden.element)}, which is on a screen that is not in front (an unfocused tab or stack screen, or behind a modal). screen.snapshot {all:true} lists it`
877
+ );
367
878
  if (hidden)
368
879
  throw new Error(
369
- `${showTarget(target)} matches ${describe(hidden.element)}, which is not on screen`
880
+ `${showTarget(target)} matches ${describe(hidden.element)}, which is not on screen. Pass {scroll:true} to scroll it into view, or use screen.scroll`
370
881
  );
882
+ const unrendered = options?.offscreen ? ". A list renders only the rows near its window: for a row it has not rendered yet, screen.scroll {toEnd:true} or {by: points} first" : "";
371
883
  throw new Error(
372
- `Nothing on screen matches ${showTarget(target)}. On screen: ${onScreenSummary(found)}`
884
+ `Nothing on screen matches ${showTarget(target)}. On screen: ${onScreenSummary(found)}${unrendered}`
373
885
  );
374
886
  }
375
887
  const preferred = prefer ? matches.filter(prefer) : [];
376
888
  if (preferred.length) matches = preferred;
377
- const index = indexOf(target);
889
+ const index = options?.index ?? indexOf(target) ?? (isAt(target) ? 0 : void 0);
378
890
  if (index !== void 0) {
379
891
  const pick = matches[index];
380
892
  if (!pick)
@@ -385,8 +897,9 @@ function resolveTarget(found, target, prefer) {
385
897
  }
386
898
  if (matches.length > 1) {
387
899
  const list = matches.slice(0, 10).map((f, i) => `${i}: ${describe(f.element)}`).join("; ");
900
+ const example = typeof target === "string" ? `${JSON.stringify(options?.afterText ? [target, "<text>", { index: 1 }] : [target, { index: 1 }])} (index as the last argument)` : `${JSON.stringify({ ...target, index: 1 })} (index inside the target)`;
388
901
  throw new Error(
389
- `${showTarget(target)} matches ${matches.length} elements; pass { index } or a narrower target. ${list}`
902
+ `${showTarget(target)} matches ${matches.length} elements; pick one with an index, e.g. ${example}, or use a narrower target. ${list}`
390
903
  );
391
904
  }
392
905
  return matches[0];
@@ -429,11 +942,13 @@ function fillInput(found, text, options = {}) {
429
942
  return value;
430
943
  }
431
944
  var center = (rect) => rect ? { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 } : { x: 0, y: 0 };
432
- function pressElement(found) {
945
+ function pressElement(found, options = {}) {
433
946
  const fiber = pressFiberOf(found);
434
947
  if (!fiber) throw new Error(`${describe(found.element)} has no onPress`);
435
- if (found.element.disabled || propsOf(fiber)?.disabled === true)
436
- throw new Error(`${describe(found.element)} is disabled`);
948
+ if (!options.force && (found.element.disabled || propsOf(fiber)?.disabled === true))
949
+ throw new Error(
950
+ `${describe(found.element)} is disabled; pass {"force":true} as the last argument to press anyway`
951
+ );
437
952
  const { x, y } = center(found.element.rect);
438
953
  const e = () => event({
439
954
  locationX: found.element.rect ? found.element.rect.width / 2 : 0,
@@ -447,6 +962,236 @@ function pressElement(found) {
447
962
  handler(fiber, "onPressOut")?.(e());
448
963
  }
449
964
 
965
+ // src/runtime/screen/find-text.ts
966
+ var normalize = (s) => s.toLowerCase().replace(/\s+/g, " ").trim();
967
+ function nearMisses(found, text, max = 5) {
968
+ const want = normalize(text);
969
+ if (!want) return [];
970
+ const close = (s) => {
971
+ const have = s && normalize(s);
972
+ return !!have && (have.includes(want) || have.length >= 3 && want.includes(have));
973
+ };
974
+ return found.filter((f) => !f.hidden).filter((f) => close(f.element.text) || close(f.element.label)).slice(0, max).map((f) => `${describe(f.element)}${f.onScreen ? "" : " (off screen)"}`);
975
+ }
976
+ function findText(found, text, options = {}) {
977
+ const test = (s) => s !== void 0 && (options.exact ? s === text : s.includes(text));
978
+ const matches = [];
979
+ let hidden = 0;
980
+ for (const f of found) {
981
+ const { element } = f;
982
+ const field = test(element.text) ? "text" : options.labels !== false && test(element.label) ? "label" : null;
983
+ if (!field) continue;
984
+ if (f.hidden) {
985
+ hidden += 1;
986
+ continue;
987
+ }
988
+ matches.push({
989
+ text: field === "text" ? element.text : element.label,
990
+ field,
991
+ rect: element.rect,
992
+ onScreen: f.onScreen
993
+ });
994
+ }
995
+ const result = {
996
+ found: matches.length,
997
+ onScreen: matches.filter((m) => m.onScreen).length,
998
+ matches
999
+ };
1000
+ if (hidden) result.hidden = hidden;
1001
+ if (!matches.length) result.nearMisses = nearMisses(found, text);
1002
+ return result;
1003
+ }
1004
+
1005
+ // src/runtime/screen/scroll.ts
1006
+ var COMMANDS = ["to", "toEnd", "toStart", "by"];
1007
+ function parseScroll(arg) {
1008
+ if (arg === null || typeof arg !== "object" || Array.isArray(arg) || !COMMANDS.some((k) => k in arg))
1009
+ return { kind: "to", target: arg };
1010
+ const keys = Object.keys(arg);
1011
+ if (keys.length !== 1)
1012
+ throw new Error(
1013
+ `screen.scroll takes one of {to: target}, {toEnd:true}, {toStart:true} or {by: points}; got ${JSON.stringify(arg)}`
1014
+ );
1015
+ const [key] = keys;
1016
+ const value = arg[key];
1017
+ if (key === "to") return { kind: "to", target: value };
1018
+ if (key === "by") {
1019
+ if (typeof value !== "number" || !Number.isFinite(value))
1020
+ throw new Error(`{by} is a number of points (negative scrolls back); got ${JSON.stringify(value)}`);
1021
+ return { kind: "by", amount: value };
1022
+ }
1023
+ if (value !== true) throw new Error(`{${key}} must be true; got ${JSON.stringify(value)}`);
1024
+ return { kind: key };
1025
+ }
1026
+ var nodeOf2 = (fiber) => {
1027
+ const n = fiber.stateNode;
1028
+ return n && typeof n === "object" ? n : null;
1029
+ };
1030
+ var method = (fiber, name) => {
1031
+ const node = nodeOf2(fiber);
1032
+ const fn = node?.[name];
1033
+ return typeof fn === "function" ? fn.bind(node) : null;
1034
+ };
1035
+ var isHorizontal = (s) => s.fibers.some((f) => propsOf(f)?.horizontal === true);
1036
+ function scrollNode(s) {
1037
+ if (!REFRESH_HOST.test(String(s.host.type))) return s.host;
1038
+ const todo = s.host.child ? [s.host.child] : [];
1039
+ while (todo.length) {
1040
+ const f = todo.pop();
1041
+ if (f.sibling) todo.push(f.sibling);
1042
+ if (f.tag === 5) return f;
1043
+ if (f.child) todo.push(f.child);
1044
+ }
1045
+ return s.host;
1046
+ }
1047
+ function contentHost(node) {
1048
+ const todo = node.child ? [node.child] : [];
1049
+ while (todo.length) {
1050
+ const f = todo.pop();
1051
+ if (f.sibling) todo.push(f.sibling);
1052
+ if (f.tag === 5) {
1053
+ if (!REFRESH_HOST.test(String(f.type))) return f;
1054
+ } else if (f.child) todo.push(f.child);
1055
+ }
1056
+ return null;
1057
+ }
1058
+ function isInverted(s, horizontal) {
1059
+ const axis = horizontal ? "scaleX" : "scaleY";
1060
+ return s.fibers.some((f) => {
1061
+ const p = propsOf(f);
1062
+ if (p?.inverted === true) return true;
1063
+ const style = [].concat(p?.style ?? []).flat(Infinity);
1064
+ return style.some((st) => {
1065
+ const t = st?.transform;
1066
+ return Array.isArray(t) && t.some(
1067
+ (x) => x && typeof x === "object" && (x[axis] === -1 || x.scale === -1)
1068
+ );
1069
+ });
1070
+ });
1071
+ }
1072
+ function metricsOf(s) {
1073
+ const horizontal = isHorizontal(s);
1074
+ const host = scrollNode(s);
1075
+ const node = nodeOf2(host);
1076
+ const view = measureHost(host);
1077
+ if (node && typeof node.scrollTop === "number" && typeof node.scrollHeight === "number") {
1078
+ const size = horizontal ? [node.scrollLeft, node.scrollWidth, node.clientWidth] : [node.scrollTop, node.scrollHeight, node.clientHeight];
1079
+ const [offset, total, shown2] = size;
1080
+ return {
1081
+ offset,
1082
+ max: Math.max(0, total - shown2),
1083
+ view,
1084
+ inverted: false
1085
+ };
1086
+ }
1087
+ const content = contentHost(host);
1088
+ const box = content && measureHost(content);
1089
+ if (!view || !box) return null;
1090
+ const max = Math.max(0, horizontal ? box.width - view.width : box.height - view.height);
1091
+ const shown = horizontal ? view.x - box.x : view.y - box.y;
1092
+ const inverted = isInverted(s, horizontal);
1093
+ return { offset: inverted ? max - shown : shown, max, view, inverted };
1094
+ }
1095
+ var round2 = (n) => Math.round(n * 100) / 100;
1096
+ function scrollToOffset(s, offset) {
1097
+ const horizontal = isHorizontal(s);
1098
+ const x = horizontal ? offset : 0;
1099
+ const y = horizontal ? 0 : offset;
1100
+ const target = { x, y, left: x, top: y, animated: false, behavior: "instant" };
1101
+ for (const f of [...s.fibers].reverse()) {
1102
+ const scrollTo = method(f, "scrollTo");
1103
+ if (scrollTo) return void scrollTo(target);
1104
+ }
1105
+ for (const f of s.fibers) {
1106
+ const toOffset = method(f, "scrollToOffset");
1107
+ if (toOffset) return void toOffset({ offset, animated: false });
1108
+ }
1109
+ throw new Error("This scrollable has no scrollTo or scrollToOffset");
1110
+ }
1111
+ function scrollToEnd(s) {
1112
+ for (const f of s.fibers) {
1113
+ const toEnd = method(f, "scrollToEnd");
1114
+ if (toEnd) return void toEnd({ animated: false });
1115
+ }
1116
+ const max = metricsOf(s)?.max;
1117
+ if (max == null) throw new Error("This scrollable has no scrollToEnd and its size is unknown");
1118
+ scrollToOffset(s, max);
1119
+ }
1120
+ var scrollEnabled = (s) => !s.fibers.some((f) => propsOf(f)?.scrollEnabled === false);
1121
+ function offsetFor(s, found, window) {
1122
+ const rect = found.element.rect;
1123
+ const m = metricsOf(s);
1124
+ if (!rect || !m?.view) return null;
1125
+ const view = viewportOf(s, m.view, window) ?? m.view;
1126
+ const [start, size, viewStart, viewSize] = isHorizontal(s) ? [rect.x, rect.width, view.x, view.width] : [rect.y, rect.height, view.y, view.height];
1127
+ if (start >= viewStart && start + size <= viewStart + viewSize) return null;
1128
+ const delta = size >= viewSize ? start - viewStart : start + size / 2 - (viewStart + viewSize / 2);
1129
+ const next = Math.max(0, Math.min(m.max ?? Infinity, m.offset + (m.inverted ? -delta : delta)));
1130
+ return Math.abs(next - m.offset) < 1 ? null : round2(next);
1131
+ }
1132
+ function scrollerFor(found, window) {
1133
+ for (let s = found.scroller; s; s = s.parent) {
1134
+ if (s.hidden || s.host === found.host || !scrollEnabled(s)) continue;
1135
+ const offset = offsetFor(s, found, window);
1136
+ if (offset !== null) return { scrollable: s, offset };
1137
+ }
1138
+ return null;
1139
+ }
1140
+ var wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1141
+ async function awaitScroll(s, before, want) {
1142
+ const near = (x, to) => !!x && to !== void 0 && Math.abs(x.offset - to) < 1;
1143
+ let m = metricsOf(s);
1144
+ if (!m || near(m, want)) return m;
1145
+ let idle = 0;
1146
+ for (let waited = 0; waited < 300; waited += 16) {
1147
+ await wait(16);
1148
+ const next = metricsOf(s);
1149
+ if (!next || near(next, want)) return next;
1150
+ idle = Math.abs(next.offset - m.offset) < 1 ? idle + 1 : 0;
1151
+ m = next;
1152
+ if (idle >= 2 && !near(m, before)) break;
1153
+ }
1154
+ return m;
1155
+ }
1156
+ function refreshHandler(s) {
1157
+ for (const f of s.fibers) {
1158
+ const p = propsOf(f);
1159
+ const control = p?.refreshControl?.props;
1160
+ if (typeof control?.onRefresh === "function") return control.onRefresh;
1161
+ if (typeof p?.onRefresh === "function") return p.onRefresh;
1162
+ }
1163
+ return null;
1164
+ }
1165
+ function draggable(s) {
1166
+ for (let x = s; x; x = x.parent) if (!x.hidden && scrollEnabled(x)) return x;
1167
+ return null;
1168
+ }
1169
+ function layoutOf(scrollables, window) {
1170
+ let ready = true;
1171
+ const parts = [];
1172
+ for (const s of scrollables) {
1173
+ if (s.hidden) continue;
1174
+ const view = measureHost(s.host);
1175
+ if (!view) continue;
1176
+ const m = metricsOf(s);
1177
+ if (view.width <= 0 || view.height <= 0 || !m?.view) ready = false;
1178
+ const limit = viewportOf(s, view, window);
1179
+ parts.push(JSON.stringify([view, m && [m.max, m.offset], limit]));
1180
+ }
1181
+ return { ready, sig: parts.join("|") };
1182
+ }
1183
+ var area2 = (r) => r ? r.width * r.height : 0;
1184
+ var onWindow = (r, w) => !!r && r.width > 0 && r.height > 0 && r.x < w.width && r.y < w.height && r.x + r.width > 0 && r.y + r.height > 0;
1185
+ function mainScrollable(scrollables, window, need) {
1186
+ const candidates = scrollables.filter((s) => !s.hidden && (need === "refresh" ? refreshHandler(s) : scrollEnabled(s))).map((s) => ({ s, rect: measureHost(s.host) })).filter(({ rect }) => onWindow(rect, window)).map(({ s, rect }) => ({ s, rect: viewportOf(s, rect, window) })).filter(({ rect }) => !!rect).sort((a, b) => area2(b.rect) - area2(a.rect));
1187
+ const [best] = candidates;
1188
+ if (!best)
1189
+ throw new Error(
1190
+ need === "refresh" ? "No scrollable on screen has a RefreshControl (or an onRefresh prop)" : "No scrollable on screen"
1191
+ );
1192
+ return best.s;
1193
+ }
1194
+
450
1195
  // src/runtime/screen/settle.ts
451
1196
  var STATE = /* @__PURE__ */ Symbol.for("@avasapp/agent-bridge/commits");
452
1197
  var UPDATE_LANES = (1 << 22) - 1;
@@ -518,6 +1263,11 @@ async function settle(options = {}) {
518
1263
 
519
1264
  // src/runtime/screen/wait-for.ts
520
1265
  var POLL_MS = 50;
1266
+ function nearMissNote(found, target) {
1267
+ const text = typeof target === "string" ? target : target.text;
1268
+ const near = text ? nearMisses(found, text) : [];
1269
+ return near.length ? `. Near misses: ${near.join("; ")}` : "";
1270
+ }
521
1271
  function waitForTarget(collect, target, options = {}) {
522
1272
  const timeoutMs = options.timeoutMs ?? 5e3;
523
1273
  const t0 = performance.now();
@@ -536,7 +1286,7 @@ function waitForTarget(collect, target, options = {}) {
536
1286
  try {
537
1287
  const found = collect();
538
1288
  const matches = matchTarget(
539
- found.filter((f) => f.onScreen),
1289
+ found.filter((f) => f.onScreen && !f.hidden),
540
1290
  target
541
1291
  );
542
1292
  const hit = matches[indexOf(target) ?? 0];
@@ -548,7 +1298,7 @@ function waitForTarget(collect, target, options = {}) {
548
1298
  const what = options.gone ? "to disappear" : "to appear";
549
1299
  reject(
550
1300
  new Error(
551
- `Timed out after ${timeoutMs} ms waiting for ${showTarget(target)} ${what}. On screen: ${onScreenSummary(found)}`
1301
+ `Timed out after ${timeoutMs} ms waiting for ${showTarget(target)} ${what}. On screen: ${onScreenSummary(found)}${nearMissNote(found, target)}`
552
1302
  )
553
1303
  );
554
1304
  }
@@ -569,41 +1319,184 @@ function waitForTarget(collect, target, options = {}) {
569
1319
 
570
1320
  // src/runtime/screen/index.ts
571
1321
  function createScreen(env) {
572
- const collect = () => collectElements(env.roots(), env.window());
573
- const refresh = (found) => {
1322
+ const collectAll = () => collectScreen(env.roots(), env.window());
1323
+ const collect = () => collectAll().found;
1324
+ const collectSettled = async () => {
1325
+ let prev = null;
1326
+ let same = 0;
1327
+ for (let waited = 0; ; waited += 16) {
1328
+ const all = collectAll();
1329
+ const { ready, sig } = layoutOf(all.scrollables, env.window());
1330
+ same = sig === prev ? same + 1 : 0;
1331
+ if (waited >= 320 || (ready ? same >= 3 : same >= 6)) return all;
1332
+ prev = sig;
1333
+ await wait(16);
1334
+ }
1335
+ };
1336
+ const reread = (found) => {
574
1337
  const node = found.host.stateNode;
575
1338
  const again = collect().find(
576
1339
  (f) => f.host === found.host || node != null && f.host.stateNode === node
577
1340
  );
578
- return (again ?? found).element;
1341
+ return again ?? found;
1342
+ };
1343
+ const scrollableOf = (found, what) => {
1344
+ if (!found.scroller)
1345
+ throw new Error(`${describe(found.element)} ${what}`);
1346
+ return found.scroller;
1347
+ };
1348
+ const scrolled = async (scrollable, act) => {
1349
+ const before = metricsOf(scrollable);
1350
+ const want = act();
1351
+ await settle();
1352
+ return awaitScroll(scrollable, before?.offset, want ?? before?.max ?? void 0);
1353
+ };
1354
+ const bringIntoView = async (found) => {
1355
+ let current = found;
1356
+ let metrics = null;
1357
+ for (let i = 0; i < 4; i++) {
1358
+ const plan = scrollerFor(current, env.window());
1359
+ if (!plan) break;
1360
+ metrics = await scrolled(plan.scrollable, () => {
1361
+ scrollToOffset(plan.scrollable, plan.offset);
1362
+ return plan.offset;
1363
+ });
1364
+ current = reread(current);
1365
+ }
1366
+ if (!metrics && current.scroller) metrics = metricsOf(current.scroller);
1367
+ return { found: current, metrics };
1368
+ };
1369
+ const resolve = async (target, prefer, options) => {
1370
+ let found = resolveTarget(collect(), target, prefer, {
1371
+ ...options,
1372
+ offscreen: options.scroll
1373
+ });
1374
+ if (found.onScreen) return found;
1375
+ found = (await bringIntoView(found)).found;
1376
+ if (!found.onScreen)
1377
+ throw new Error(
1378
+ `${describe(found.element)} is not on screen and no scrollable around it can bring it into view`
1379
+ );
1380
+ return found;
579
1381
  };
580
1382
  return {
581
1383
  snapshot(options = {}) {
582
1384
  const found = collect();
583
- if (!options.all)
584
- return { elements: found.filter((f) => f.onScreen).map((f) => f.element) };
585
- return {
586
- elements: found.map((f) => ({ ...f.element, onScreen: f.onScreen }))
587
- };
1385
+ if (options.all)
1386
+ return {
1387
+ elements: found.map((f) => ({
1388
+ ...f.element,
1389
+ onScreen: f.onScreen,
1390
+ ...f.hidden && { hidden: true }
1391
+ }))
1392
+ };
1393
+ const shown = focused(found);
1394
+ const elements = shown.filter((f) => f.onScreen).map((f) => f.element);
1395
+ const hidden = found.length - shown.length;
1396
+ return hidden ? { elements, hidden } : { elements };
588
1397
  },
1398
+ findText: (text, options) => findText(collect(), text, options),
589
1399
  async fill(target, text, options = {}) {
590
- const found = resolveTarget(collect(), target, (f) => !!f.input);
1400
+ const found = await resolve(target, (f) => !!f.input, {
1401
+ index: options.index,
1402
+ afterText: true,
1403
+ scroll: options.scroll
1404
+ });
591
1405
  const filled = fillInput(found, String(text), options);
592
1406
  await settle();
593
- return { filled, element: refresh(found) };
1407
+ return { filled, element: reread(found).element };
594
1408
  },
595
- async press(target) {
596
- const found = resolveTarget(collect(), target, (f) => !!f.press);
597
- pressElement(found);
1409
+ async press(target, options = {}) {
1410
+ const found = await resolve(target, (f) => !!f.press, options);
1411
+ pressElement(found, options);
598
1412
  await settle();
599
1413
  return found.element;
600
1414
  },
1415
+ /**
1416
+ * Scrolls the nearest scrollable of a target (or `within`, or the main one
1417
+ * on screen) to the target, to either end, or by a distance in points.
1418
+ */
1419
+ async scroll(arg, options = {}) {
1420
+ const command = parseScroll(arg);
1421
+ const { found: all, scrollables } = await collectSettled();
1422
+ const named = options.within ? scrollableOf(
1423
+ resolveTarget(all, options.within, void 0, { offscreen: true }),
1424
+ "is not a scrollable or inside one"
1425
+ ) : null;
1426
+ const within2 = named && draggable(named);
1427
+ if (named && !within2)
1428
+ throw new Error(
1429
+ `${describe(resolveTarget(all, options.within, void 0, { offscreen: true }).element)} can't be scrolled (scrollEnabled is false, or it is on a hidden screen) and no scrollable around it can`
1430
+ );
1431
+ const target = command.kind === "to" ? resolveTarget(all, command.target, void 0, {
1432
+ index: options.index,
1433
+ offscreen: true
1434
+ }) : null;
1435
+ let metrics;
1436
+ let again = null;
1437
+ if (target && !within2) {
1438
+ scrollableOf(target, "is not inside a scrollable");
1439
+ ({ found: again, metrics } = await bringIntoView(target));
1440
+ } else if (target && within2) {
1441
+ const offset = offsetFor(within2, target, env.window());
1442
+ metrics = offset === null ? metricsOf(within2) : await scrolled(within2, () => (scrollToOffset(within2, offset), offset));
1443
+ again = reread(target);
1444
+ } else {
1445
+ const scrollable = within2 ?? mainScrollable(scrollables, env.window(), "scroll");
1446
+ metrics = await scrolled(scrollable, () => {
1447
+ if (command.kind === "toEnd") {
1448
+ scrollToEnd(scrollable);
1449
+ return metricsOf(scrollable)?.max ?? void 0;
1450
+ }
1451
+ if (command.kind === "toStart") {
1452
+ scrollToOffset(scrollable, 0);
1453
+ return 0;
1454
+ }
1455
+ const now2 = metricsOf(scrollable);
1456
+ if (!now2)
1457
+ throw new Error("Can not read the scroll position, so can not scroll by a distance");
1458
+ const next = Math.max(
1459
+ 0,
1460
+ Math.min(now2.max ?? Infinity, now2.offset + command.amount)
1461
+ );
1462
+ scrollToOffset(scrollable, next);
1463
+ return next;
1464
+ });
1465
+ }
1466
+ const result = {};
1467
+ if (metrics) {
1468
+ result.offset = Math.round(metrics.offset * 100) / 100;
1469
+ if (metrics.max != null) result.max = Math.round(metrics.max * 100) / 100;
1470
+ }
1471
+ if (again) {
1472
+ result.element = again.element;
1473
+ result.onScreen = again.onScreen;
1474
+ }
1475
+ return result;
1476
+ },
1477
+ /** Calls the `onRefresh` of a scrollable's RefreshControl, as a pull would. */
1478
+ async refresh(target, options = {}) {
1479
+ const { found: all, scrollables } = collectAll();
1480
+ const scrollable = target ? scrollableOf(
1481
+ resolveTarget(all, target, void 0, {
1482
+ index: options.index,
1483
+ offscreen: true
1484
+ }),
1485
+ "is not a scrollable or inside one"
1486
+ ) : mainScrollable(scrollables, env.window(), "refresh");
1487
+ const onRefresh = refreshHandler(scrollable);
1488
+ if (!onRefresh) throw new Error("That scrollable has no RefreshControl (or onRefresh prop)");
1489
+ onRefresh();
1490
+ await settle();
1491
+ return { refreshed: true };
1492
+ },
601
1493
  waitFor: (target, options) => waitForTarget(collect, target, options)
602
1494
  };
603
1495
  }
604
1496
 
605
1497
  // src/runtime/tools/screen.ts
606
1498
  var devToolsHook = () => globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
1499
+ var TARGET_HELP = "target is a string (testID, label, placeholder or text) or an object that must match every key it sets: {testID, label, placeholder, text, at:[x,y], index}. Unknown keys are errors. {at:[x,y]} hits the smallest element covering that point, for icon-only buttons with no label. Several matches: add an index, either in the target or as a trailing {index:n} argument.";
607
1500
  function screenTools() {
608
1501
  const screen = createScreen({
609
1502
  roots: () => fiberRoots(devToolsHook()),
@@ -611,40 +1504,49 @@ function screenTools() {
611
1504
  });
612
1505
  return {
613
1506
  "screen.findText": {
614
- description: "Find rendered text (substring, or exact with {exact:true}) and whether it is on screen. Ignores inactive tabs and screens.",
615
- run: (text, options) => findTextInTree(
616
- fiberRoots(devToolsHook()),
617
- text,
618
- import_react_native.Dimensions.get("window"),
619
- options
620
- )
1507
+ description: "Find text on screen (substring, or exact with {exact:true}) and whether each match is on screen. Searches the same joined text screen.snapshot shows (nested Text concatenated) and accessibility labels; {labels:false} searches text only. Each match says which field matched; a miss lists near misses. Ignores screens the user can't reach (unfocused tabs and stack screens, under a modal); `hidden` counts matches there.",
1508
+ run: (text, options) => screen.findText(text, options)
621
1509
  },
622
1510
  "screen.snapshot": {
623
- description: "Buttons, inputs, text and testID views on screen, with rects. {all:true} adds off-screen ones.",
1511
+ description: "Buttons, inputs, text, and testID or labelled views on screen, with rects and checked/selected/expanded state (a Switch's value is checked). Only the focused screen: unfocused tabs and stack screens, content under an open modal, display:none and accessibility-hidden subtrees are left out, and `hidden` counts the elements skipped. {all:true} lists everything, marking off-screen (onScreen:false) and hidden (hidden:true) ones. A WebView registered with the react-native-webview adapter shows as one `webview` element with its name: use webview.* inside it.",
624
1512
  run: (options) => screen.snapshot(options)
625
1513
  },
626
1514
  "screen.fill": {
627
- description: "Type into an input by testID, label, placeholder or text, then wait for the render. {submit:true} also submits.",
1515
+ description: `Type into an input, then wait for the render. ${TARGET_HELP} {submit:true} also submits.`,
628
1516
  run: (target, text, options) => screen.fill(target, text, options)
629
1517
  },
630
1518
  "screen.press": {
631
- description: "Press a button by testID, label or text, then wait for the render.",
632
- run: (target) => screen.press(target)
1519
+ description: `Press a button, then wait for the render. ${TARGET_HELP} Disabled elements are refused; {force:true} presses anyway, to see what the app does on tap. {scroll:true} scrolls an off-screen target into view first.`,
1520
+ run: (target, options) => screen.press(target, options)
1521
+ },
1522
+ "screen.scroll": {
1523
+ description: `Scroll the nearest ScrollView/FlatList/FlashList, then wait for the render. Takes a target (or {to: target}) to bring into the middle of its scrollable, {toEnd:true}, {toStart:true}, or {by: points} (negative scrolls back); the last argument may be {within: target, index} to name the scrollable (a target inside it, or its testID). Without a target it scrolls the largest scrollable on screen. Only rendered elements can be targeted: rows a FlatList has not rendered yet need {toEnd:true} or {by} first. Returns the offset and, for a target, whether it is now on screen. ${TARGET_HELP}`,
1524
+ run: (arg, options) => screen.scroll(arg, options)
1525
+ },
1526
+ "screen.refresh": {
1527
+ description: "Pull to refresh: call the onRefresh of the RefreshControl of the scrollable holding a target (or the largest one with a RefreshControl on screen), then wait for the render. It does not wait for the data: follow with screen.waitFor.",
1528
+ run: (target, options) => screen.refresh(target, options)
633
1529
  },
634
1530
  "screen.waitFor": {
635
- description: "Wait until a target is on screen, or gone with {gone:true}. Default timeout 5000 ms.",
1531
+ description: "Wait until a target is on screen, or gone with {gone:true}. Matches testID, label, placeholder, then the same joined text screen.snapshot shows. Default timeout 5000 ms; a timeout lists near misses.",
636
1532
  run: (target, options) => screen.waitFor(target, options)
637
1533
  }
638
1534
  };
639
1535
  }
640
1536
 
641
1537
  // src/runtime/builtin-tools.ts
642
- function builtinTools(listTools, getTools, logs) {
1538
+ function builtinTools(listTools, getTools, logs, getScenarios) {
643
1539
  return {
644
1540
  ...bridgeTools(listTools),
1541
+ ...appTools(
1542
+ selectReload(globalThis, () => import_react_native2.DevSettings.reload())
1543
+ ),
645
1544
  ...logTools(logs),
646
1545
  ...restoreTools(getTools),
647
- ...screenTools()
1546
+ ...screenTools(),
1547
+ ...captureTools(loadViewShot),
1548
+ // Only when the app defines scenarios, so `tools` doesn't list empty ones.
1549
+ ...getScenarios && scenarioTools(getScenarios, getTools)
648
1550
  };
649
1551
  }
650
1552
 
@@ -673,6 +1575,41 @@ function cdpTransport() {
673
1575
  };
674
1576
  }
675
1577
 
1578
+ // src/runtime/gate.ts
1579
+ function createGate(name) {
1580
+ let holds = 0;
1581
+ const listeners = /* @__PURE__ */ new Set();
1582
+ const notify = () => {
1583
+ for (const fn of [...listeners]) fn(holds > 0);
1584
+ };
1585
+ return {
1586
+ name,
1587
+ get closed() {
1588
+ return holds > 0;
1589
+ },
1590
+ close: () => {
1591
+ let released = false;
1592
+ holds += 1;
1593
+ if (holds === 1) notify();
1594
+ return () => {
1595
+ if (released) return;
1596
+ released = true;
1597
+ holds -= 1;
1598
+ if (holds === 0) notify();
1599
+ };
1600
+ },
1601
+ subscribe: (fn) => {
1602
+ listeners.add(fn);
1603
+ return () => {
1604
+ listeners.delete(fn);
1605
+ };
1606
+ }
1607
+ };
1608
+ }
1609
+
1610
+ // src/runtime/load-id.ts
1611
+ var loadId = Math.random().toString(36).slice(2, 10);
1612
+
676
1613
  // src/runtime/logs.ts
677
1614
  var CAPACITY = 200;
678
1615
  var STACK_LINES = 10;
@@ -735,9 +1672,9 @@ function formatArgs(args) {
735
1672
  }
736
1673
  parts.push(...rest.map(describe2));
737
1674
  const error = args.find((a) => a instanceof Error);
738
- const message = parts.join(" ");
1675
+ const message2 = parts.join(" ");
739
1676
  return {
740
- message: message.length > MESSAGE_CHARS ? `${message.slice(0, MESSAGE_CHARS)}...` : message,
1677
+ message: message2.length > MESSAGE_CHARS ? `${message2.slice(0, MESSAGE_CHARS)}...` : message2,
741
1678
  ...error?.stack ? { stack: trimStack(error.stack) } : {}
742
1679
  };
743
1680
  }
@@ -756,9 +1693,9 @@ function describe2(value) {
756
1693
  var trimStack = (stack) => stack.split("\n").slice(0, STACK_LINES).join("\n");
757
1694
  function installLogHooks(capture, env) {
758
1695
  let quiet = 0;
759
- let active = true;
1696
+ let active2 = true;
760
1697
  const record = (level, args) => {
761
- if (!active || quiet) return;
1698
+ if (!active2 || quiet) return;
762
1699
  quiet++;
763
1700
  try {
764
1701
  capture.record(level, args);
@@ -801,7 +1738,7 @@ function installLogHooks(capture, env) {
801
1738
  env.addEventListener?.("unhandledrejection", onRejection);
802
1739
  }
803
1740
  return () => {
804
- active = false;
1741
+ active2 = false;
805
1742
  if (console.error === wrappers.error) console.error = originals.error;
806
1743
  if (console.warn === wrappers.warn) console.warn = originals.warn;
807
1744
  if (errorUtils && originalHandler && errorUtils.getGlobalHandler() === handler2)
@@ -854,7 +1791,7 @@ function toJson(value) {
854
1791
  function unwrap(definition) {
855
1792
  return typeof definition === "function" ? { run: definition } : definition;
856
1793
  }
857
- function createRegistry(getTools, logs) {
1794
+ function createRegistry(getTools, logs, loadId2) {
858
1795
  const list = () => Object.entries(getTools()).map(([name, definition]) => ({
859
1796
  name,
860
1797
  description: unwrap(definition).description
@@ -863,7 +1800,12 @@ function createRegistry(getTools, logs) {
863
1800
  const end = logs?.begin(call.tool);
864
1801
  const result = await run(call, from).finally(end);
865
1802
  const errors = logs?.takeErrors();
866
- return errors?.length ? { ...result, logs: errors } : result;
1803
+ const stamped = loadId2 ? { ...result, loadId: loadId2, ...pending() } : result;
1804
+ return errors?.length ? { ...stamped, logs: errors } : stamped;
1805
+ }
1806
+ function pending() {
1807
+ const areas = Object.keys(pendingByArea(getTools()));
1808
+ return areas.length ? { pending: areas } : {};
867
1809
  }
868
1810
  async function run(call, from) {
869
1811
  const t0 = performance.now();
@@ -874,11 +1816,17 @@ function createRegistry(getTools, logs) {
874
1816
  const known = list().map((t) => t.name).join(", ");
875
1817
  throw new Error(`Unknown tool "${call.tool}". Known: ${known}`);
876
1818
  }
877
- const value = await unwrap(definition).run(...call.args ?? []);
1819
+ const { run: tool, maxArgs } = unwrap(definition);
1820
+ const args = call.args ?? [];
1821
+ if (maxArgs !== void 0 && args.length > maxArgs)
1822
+ throw new Error(
1823
+ `${call.tool} takes at most ${maxArgs} argument${maxArgs === 1 ? "" : "s"}, got ${args.length}. A JSON array is spread into the arguments; wrap it once more to pass an array as one argument.`
1824
+ );
1825
+ const value = await tool(...args);
878
1826
  return { id: call.id, from, ok: true, value: toJson(value), ms: ms() };
879
1827
  } catch (error) {
880
- const message = error instanceof Error ? error.message : String(error);
881
- return { id: call.id, from, ok: false, error: message, ms: ms() };
1828
+ const message2 = error instanceof Error ? error.message : String(error);
1829
+ return { id: call.id, from, ok: false, error: message2, ms: ms() };
882
1830
  }
883
1831
  }
884
1832
  return { list, dispatch };
@@ -889,17 +1837,20 @@ var randomId = () => Math.random().toString(36).slice(2, 10);
889
1837
  function startAgentBridge(options = {}) {
890
1838
  const deviceId = randomId();
891
1839
  const userTools = () => typeof options.tools === "function" ? options.tools() : options.tools ?? {};
1840
+ const { scenarios } = options;
1841
+ const getScenarios = scenarios ? () => typeof scenarios === "function" ? scenarios() : scenarios : void 0;
892
1842
  const logs = startLogCapture();
893
1843
  const allTools = () => ({
894
- ...builtinTools(() => registry.list(), allTools, logs.capture),
1844
+ ...builtinTools(() => registry.list(), allTools, logs.capture, getScenarios),
895
1845
  ...userTools()
896
1846
  });
897
- const registry = createRegistry(allTools, logs.capture);
1847
+ const registry = createRegistry(allTools, logs.capture, loadId);
898
1848
  const info = () => ({
899
1849
  deviceId,
900
- name: options.name ?? import_react_native2.Platform.OS,
901
- platform: import_react_native2.Platform.OS,
1850
+ name: options.name ?? import_react_native3.Platform.OS,
1851
+ platform: import_react_native3.Platform.OS,
902
1852
  protocol: PROTOCOL_VERSION,
1853
+ loadId,
903
1854
  tools: registry.list()
904
1855
  });
905
1856
  const context = {
@@ -927,7 +1878,12 @@ function useAgentBridge(options = {}) {
927
1878
  tools: () => {
928
1879
  const { tools } = latest.current;
929
1880
  return typeof tools === "function" ? tools() : tools ?? {};
930
- }
1881
+ },
1882
+ // Decided at mount: scenario.* tools are there or not for the bridge's life.
1883
+ scenarios: latest.current.scenarios ? () => {
1884
+ const { scenarios } = latest.current;
1885
+ return typeof scenarios === "function" ? scenarios() : scenarios ?? {};
1886
+ } : void 0
931
1887
  });
932
1888
  }, []);
933
1889
  }