@avasapp/agent-bridge 0.5.0 → 0.6.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 (61) hide show
  1. package/README.md +19 -5
  2. package/dist/adapters/ably.cjs +79 -3
  3. package/dist/adapters/ably.d.cts +2 -1
  4. package/dist/adapters/async-storage.cjs +158 -0
  5. package/dist/adapters/async-storage.d.cts +14 -0
  6. package/dist/adapters/device.cjs +1110 -0
  7. package/dist/adapters/device.d.cts +49 -0
  8. package/dist/adapters/expo-router.cjs +112 -6
  9. package/dist/adapters/expo-router.d.cts +1 -1
  10. package/dist/adapters/jotai.cjs +266 -0
  11. package/dist/adapters/jotai.d.cts +19 -0
  12. package/dist/adapters/react-native-mmkv.cjs +80 -0
  13. package/dist/adapters/react-native-mmkv.d.cts +3 -1
  14. package/dist/adapters/react-native-webview.d.cts +1 -1
  15. package/dist/adapters/react-navigation.cjs +293 -0
  16. package/dist/adapters/react-navigation.d.cts +55 -0
  17. package/dist/adapters/redux.cjs +322 -0
  18. package/dist/adapters/redux.d.cts +28 -0
  19. package/dist/adapters/secure-store.cjs +169 -0
  20. package/dist/adapters/secure-store.d.cts +28 -0
  21. package/dist/adapters/socket-io.cjs +77 -3
  22. package/dist/adapters/socket-io.d.cts +2 -1
  23. package/dist/adapters/tanstack-query.d.cts +1 -1
  24. package/dist/adapters/zustand.cjs +193 -22
  25. package/dist/adapters/zustand.d.cts +11 -15
  26. package/dist/{chunk-3QIG7A5W.js → chunk-Y6QFU5ON.js} +233 -26
  27. package/dist/cli.js +1336 -255
  28. package/dist/client/index.cjs +233 -27
  29. package/dist/client/index.d.ts +67 -4
  30. package/dist/client/index.js +5 -3
  31. package/dist/expo/index.d.cts +1 -1
  32. package/dist/network/index.cjs +320 -73
  33. package/dist/network/index.d.cts +18 -3
  34. package/dist/noop/async-storage.cjs +26 -0
  35. package/dist/noop/device.cjs +26 -0
  36. package/dist/noop/jotai.cjs +26 -0
  37. package/dist/noop/react-navigation.cjs +26 -0
  38. package/dist/noop/realtime.cjs +6 -1
  39. package/dist/noop/redux.cjs +28 -0
  40. package/dist/noop/secure-store.cjs +26 -0
  41. package/dist/realtime/index.cjs +71 -4
  42. package/dist/realtime/index.d.cts +21 -2
  43. package/dist/runtime/index.cjs +785 -128
  44. package/dist/runtime/index.d.cts +2 -2
  45. package/dist/store-common-j0cHHvBr.d.cts +23 -0
  46. package/dist/{types-BmpykP1P.d.cts → types-qlwdcGjt.d.cts} +22 -0
  47. package/entries/async-storage.cjs +9 -0
  48. package/entries/device.cjs +9 -0
  49. package/entries/jotai.cjs +9 -0
  50. package/entries/react-navigation.cjs +9 -0
  51. package/entries/redux.cjs +9 -0
  52. package/entries/secure-store.cjs +9 -0
  53. package/package.json +32 -1
  54. package/skills/agent-bridge/SKILL.md +39 -8
  55. package/skills/agent-bridge/references/device.md +21 -0
  56. package/skills/agent-bridge/references/flows.md +13 -3
  57. package/skills/agent-bridge/references/network.md +19 -1
  58. package/skills/agent-bridge/references/realtime.md +1 -1
  59. package/skills/agent-bridge/references/restore.md +21 -3
  60. package/skills/agent-bridge/references/screen.md +8 -2
  61. package/skills/agent-bridge/references/state.md +21 -1
@@ -70,6 +70,95 @@ async function runUndos() {
70
70
  return { ran: batch.length, errors };
71
71
  }
72
72
 
73
+ // src/runtime/tools/restore.ts
74
+ var SUFFIX = ".restore";
75
+ var SELF = "bridge.restore";
76
+ var LAST = "scenario.restore";
77
+ function pendingByArea(tools, detail = false) {
78
+ const out = {};
79
+ for (const [name, d] of Object.entries(tools)) {
80
+ if (!name.endsWith(SUFFIX) || name === SELF || typeof d === "function")
81
+ continue;
82
+ const area3 = name.slice(0, -SUFFIX.length);
83
+ try {
84
+ const found = d.pending?.({ detail });
85
+ if (found) out[area3] = found;
86
+ } catch (error) {
87
+ out[area3] = `pending check failed: ${error instanceof Error ? error.message : String(error)}`;
88
+ }
89
+ }
90
+ return out;
91
+ }
92
+ function journalByArea(tools) {
93
+ const out = {};
94
+ for (const [name, d] of restorers(tools)) {
95
+ try {
96
+ const data = d.journal?.();
97
+ if (data !== void 0) out[name] = data;
98
+ } catch {
99
+ }
100
+ }
101
+ return out;
102
+ }
103
+ function journalRev(tools) {
104
+ let rev = 0;
105
+ for (const [, d] of restorers(tools)) rev += d.journalRev?.() ?? 0;
106
+ return rev;
107
+ }
108
+ function restorers(tools) {
109
+ return Object.entries(tools).filter(([name, d]) => name.endsWith(SUFFIX) && name !== SELF && typeof d !== "function").map(([name, d]) => [name.slice(0, -SUFFIX.length), d]).filter((e) => typeof e[1] !== "function");
110
+ }
111
+ var INTERNAL_TOOLS = ["bridge.adopt"];
112
+ function restoreTools(getTools) {
113
+ return {
114
+ "bridge.pending": {
115
+ maxArgs: 0,
116
+ 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.",
117
+ run: () => pendingByArea(getTools(), true)
118
+ },
119
+ "bridge.adopt": {
120
+ maxArgs: 1,
121
+ description: "Internal: a session hands back its journal after a reload.",
122
+ run: async (data) => {
123
+ const tools = getTools();
124
+ const out = {};
125
+ if (!data || typeof data !== "object" || Array.isArray(data)) throw new Error("bridge.adopt takes an object");
126
+ for (const [area3, part] of Object.entries(data)) {
127
+ const d = tools[`${area3}${SUFFIX}`];
128
+ if (typeof d === "function" || !d?.adopt) out[area3] = { adopted: [], already: [], changed: [], retry: part, error: "this area has no adopt" };
129
+ else
130
+ try {
131
+ out[area3] = await d.adopt(part);
132
+ } catch (error) {
133
+ out[area3] = { adopted: [], already: [], changed: [], retry: part, error: error instanceof Error ? error.message : String(error) };
134
+ }
135
+ }
136
+ return out;
137
+ }
138
+ },
139
+ [SELF]: {
140
+ description: "Undo what the agent changed: runs every *.restore tool in name order, scenario.restore last, and returns each result or error.",
141
+ run: async () => {
142
+ const tools = getTools();
143
+ const results = {};
144
+ const names = Object.keys(tools).filter((name) => name.endsWith(SUFFIX) && name !== SELF).sort((a, b) => Number(a === LAST) - Number(b === LAST) || a.localeCompare(b));
145
+ for (const name of names) {
146
+ const definition = tools[name];
147
+ const run = typeof definition === "function" ? definition : definition.run;
148
+ try {
149
+ results[name] = await run();
150
+ } catch (error) {
151
+ results[name] = {
152
+ error: error instanceof Error ? error.message : String(error)
153
+ };
154
+ }
155
+ }
156
+ return results;
157
+ }
158
+ }
159
+ };
160
+ }
161
+
73
162
  // src/runtime/tools/app.ts
74
163
  function selectReload(host, devSettingsReload) {
75
164
  return () => {
@@ -84,13 +173,14 @@ function selectReload(host, devSettingsReload) {
84
173
  }
85
174
  };
86
175
  }
87
- function appTools(reload) {
176
+ function appTools(reload, getTools) {
88
177
  return {
89
178
  "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.",
179
+ description: "Reload the app's JS (Expo's reloadAppAsync, else DevSettings.reload). Pending restores (store, query, mocks, pins, storage) are lost, and the reply names them: restore first. Returns before it reloads; a session waits for the new bridge and reports what was lost.",
91
180
  run: () => {
92
181
  setTimeout(reload, 50);
93
- return { reloading: true };
182
+ const lost = getTools ? Object.keys(pendingByArea(getTools())) : [];
183
+ return lost.length ? { reloading: true, lost: `pending restores are lost: ${lost.join(", ")}` } : { reloading: true };
94
184
  }
95
185
  },
96
186
  "app.restore": {
@@ -175,55 +265,6 @@ function logTools(logs) {
175
265
  };
176
266
  }
177
267
 
178
- // src/runtime/tools/restore.ts
179
- var SUFFIX = ".restore";
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
- }
197
- function restoreTools(getTools) {
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
- },
204
- [SELF]: {
205
- description: "Undo what the agent changed: runs every *.restore tool in name order, scenario.restore last, and returns each result or error.",
206
- run: async () => {
207
- const tools = getTools();
208
- const results = {};
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));
210
- for (const name of names) {
211
- const definition = tools[name];
212
- const run = typeof definition === "function" ? definition : definition.run;
213
- try {
214
- results[name] = await run();
215
- } catch (error) {
216
- results[name] = {
217
- error: error instanceof Error ? error.message : String(error)
218
- };
219
- }
220
- }
221
- return results;
222
- }
223
- }
224
- };
225
- }
226
-
227
268
  // src/runtime/options-schema.ts
228
269
  var import_json_schema = require("@cfworker/json-schema");
229
270
  var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
@@ -465,6 +506,9 @@ var MODAL_HOSTS = /* @__PURE__ */ new Set(["RCTModalHostView", "ModalHostView"])
465
506
  var OVERLAY_HOST = "RNSFullWindowOverlay";
466
507
  var SCREEN_STACK = "RNSScreenStack";
467
508
  var STACK_SCREENS = /* @__PURE__ */ new Set(["RNSScreen", "RNSModalScreen"]);
509
+ var TABS_HOSTS = /* @__PURE__ */ new Set(["RNSTabsHostIOS", "RNSTabsHostAndroid"]);
510
+ var OLD_TAB_SCREENS = /* @__PURE__ */ new Set(["RNSBottomTabsScreen", "RNSTabsScreen"]);
511
+ var TAB_SCREENS = /* @__PURE__ */ new Set(["RNSTabsScreenIOS", "RNSTabsScreenAndroid"]);
468
512
  var SEE_THROUGH = /* @__PURE__ */ new Set(["transparentModal", "containedTransparentModal", "overFullScreen"]);
469
513
  var undimmedSheet = (p) => {
470
514
  const at = p.sheetLargestUndimmedDetentIndex;
@@ -500,6 +544,23 @@ function stackScreens(stack) {
500
544
  presented: screens.slice(1).filter(coversWindow)
501
545
  };
502
546
  }
547
+ function unselectedTabs(tabs) {
548
+ const selected = propsOf(tabs)?.navStateRequest?.selectedScreenKey;
549
+ const out = [];
550
+ let matches = 0;
551
+ if (typeof selected !== "string") return out;
552
+ const todo = tabs.child ? [tabs.child] : [];
553
+ while (todo.length) {
554
+ const f = todo.pop();
555
+ if (f.sibling) todo.push(f.sibling);
556
+ if (f.tag === HOST_COMPONENT && TAB_SCREENS.has(f.type)) {
557
+ if (propsOf(f)?.screenKey === selected) matches++;
558
+ else out.push(f);
559
+ } else if (f.child) todo.push(f.child);
560
+ }
561
+ return matches === 1 ? out : [];
562
+ }
563
+ var hasDom = (recs) => recs.some((r) => !!r.host.stateNode && "getBoundingClientRect" in r.host.stateNode);
503
564
  function hostAbove(host) {
504
565
  let up = host.return;
505
566
  while (up && up.tag !== HOST_COMPONENT) up = up.return;
@@ -513,6 +574,12 @@ var inside = (modal, top) => {
513
574
  };
514
575
  var SWITCH_HOSTS = /* @__PURE__ */ new Set(["RCTSwitch", "AndroidSwitch"]);
515
576
  var isFn = (v) => typeof v === "function";
577
+ function isSwitchFiber(fiber) {
578
+ const p = propsOf(fiber);
579
+ if ((p?.accessibilityRole ?? p?.role) === "switch") return true;
580
+ const own = firstHost(fiber);
581
+ return !!own && SWITCH_HOSTS.has(String(own.type));
582
+ }
516
583
  var isInputProps = (p) => isFn(p.onChangeText) || isFn(p.onChange) && typeof p.value !== "boolean" && ("value" in p || "defaultValue" in p || "placeholder" in p);
517
584
  var labelOf = (p) => p.accessibilityLabel ?? p["aria-label"];
518
585
  function actionNamesOf(p) {
@@ -520,7 +587,37 @@ function actionNamesOf(p) {
520
587
  return p.accessibilityActions.map((a) => a?.name).filter((name) => typeof name === "string");
521
588
  }
522
589
  var activates = (rec) => !!rec.actions && !!rec.element.actions?.includes("activate");
523
- var interesting = (p) => isFn(p.onPress) || actionNamesOf(p) !== null || isFn(p.onValueChange) || isInputProps(p) || typeof p.testID === "string" || typeof labelOf(p) === "string" || webViewNameOf(p) !== void 0;
590
+ var TEXT_HOSTS = /^RCT(Virtual)?Text$/;
591
+ var changesValue = (p) => isFn(p.onValueChange) || isFn(p.onSlidingComplete) || Array.isArray(p.values) && isFn(p.onChange);
592
+ var isControl = (p) => isFn(p.onPress) || changesValue(p) || activatesProps(p);
593
+ var activatesProps = (p) => !!actionNamesOf(p)?.includes("activate");
594
+ function wrapsControls(rec) {
595
+ const skip = /* @__PURE__ */ new Set();
596
+ const markChain = (from) => {
597
+ for (let f = from; f; f = f.child) {
598
+ skip.add(f);
599
+ if (f.tag === HOST_COMPONENT) break;
600
+ }
601
+ };
602
+ markChain(rec.fiber);
603
+ let controls = 0;
604
+ const stack = rec.fiber.child ? [rec.fiber.child] : [];
605
+ while (stack.length) {
606
+ const f = stack.pop();
607
+ if (f.sibling) stack.push(f.sibling);
608
+ if (f.child) stack.push(f.child);
609
+ const p = propsOf(f);
610
+ if (!p || skip.has(f)) continue;
611
+ if (isInputProps(p)) return true;
612
+ if (!isControl(p)) continue;
613
+ const own = firstHost(f);
614
+ if (isFn(p.onPress) && !isFn(p.onValueChange) && own && TEXT_HOSTS.test(String(own.type))) continue;
615
+ markChain(f);
616
+ if (++controls >= 2) return true;
617
+ }
618
+ return false;
619
+ }
620
+ var interesting = (p) => isFn(p.onPress) || actionNamesOf(p) !== null || changesValue(p) || isInputProps(p) || typeof p.testID === "string" || typeof labelOf(p) === "string" || webViewNameOf(p) !== void 0;
524
621
  var ICON_GLYPHS = /[\uE000-\uF8FF]/g;
525
622
  function firstHost(fiber) {
526
623
  if (fiber.tag === HOST_COMPONENT) return fiber;
@@ -537,6 +634,7 @@ function absorb(rec, fiber, p) {
537
634
  const e = rec.element;
538
635
  if (!rec.input && isInputProps(p)) rec.input = fiber;
539
636
  if (!rec.press && isFn(p.onPress)) rec.press = fiber;
637
+ if (!rec.change && changesValue(p)) rec.change = fiber;
540
638
  const actions = actionNamesOf(p);
541
639
  if (!rec.actions && actions) {
542
640
  rec.actions = fiber;
@@ -589,14 +687,14 @@ var within = (host, ancestor) => {
589
687
  };
590
688
  function visibleArea(base, host, bars, window) {
591
689
  let area3 = base;
592
- const clip2 = (r) => {
690
+ const clip = (r) => {
593
691
  area3 = area3 && intersect(area3, r);
594
692
  };
595
693
  for (const bar of bars) {
596
694
  if (within(host, bar)) continue;
597
695
  const r = measureHost(bar);
598
696
  if (!usable(r) || r.width < window.width * 0.9) continue;
599
- clip2(
697
+ clip(
600
698
  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 }
601
699
  );
602
700
  }
@@ -658,6 +756,7 @@ function collectScreen(roots, window) {
658
756
  host,
659
757
  press: null,
660
758
  input: null,
759
+ change: null,
661
760
  actions: null,
662
761
  hidden,
663
762
  scroller,
@@ -671,8 +770,14 @@ function collectScreen(roots, window) {
671
770
  order.push(rec);
672
771
  return rec;
673
772
  };
773
+ const wrapper = /* @__PURE__ */ new Map();
674
774
  const buttonOf = (rec) => {
675
- for (let r = rec; r; r = r.parent) if (r.press || activates(r)) return r;
775
+ for (let r = rec; r; r = r.parent)
776
+ if (r.press || activates(r)) {
777
+ let wraps = wrapper.get(r);
778
+ if (wraps === void 0) wrapper.set(r, wraps = wrapsControls(r));
779
+ return wraps ? null : r;
780
+ }
676
781
  return null;
677
782
  };
678
783
  const textHost = (host) => {
@@ -707,6 +812,8 @@ function collectScreen(roots, window) {
707
812
  for (const s of screens.covered) covered.add(s);
708
813
  for (const s of screens.presented) presented.add(s);
709
814
  }
815
+ if (OLD_TAB_SCREENS.has(fiber.type) && p?.isFocused === false) hidden = true;
816
+ if (TABS_HOSTS.has(fiber.type)) for (const s of unselectedTabs(fiber)) covered.add(s);
710
817
  if (fiber.type === OVERLAY_HOST) overlay = true;
711
818
  if (p && !hidden && (p.accessibilityRole ?? p.role) === "tablist") bars.push(fiber);
712
819
  if (!hidden && (MODAL_HOSTS.has(fiber.type) || presented.has(fiber))) {
@@ -758,6 +865,7 @@ function collectScreen(roots, window) {
758
865
  const covers2 = (x) => !!top && !x.overlay && !inside(x.modal, top);
759
866
  for (const scrollable of scrollByHost.values())
760
867
  scrollable.hidden || (scrollable.hidden = covers2(scrollable));
868
+ let measured = false;
761
869
  for (const rec of order) {
762
870
  const e = rec.element;
763
871
  rec.hidden || (rec.hidden = covers2(rec));
@@ -766,11 +874,13 @@ function collectScreen(roots, window) {
766
874
  e.kind = e.webview !== void 0 ? "webview" : rec.input ? "input" : rec.press || activates(rec) ? "button" : rec.parts.length ? "text" : "view";
767
875
  if (e.kind === "input" && e.editable === void 0) e.editable = true;
768
876
  const rect = measureHost(rec.host);
877
+ if (rect) measured = true;
769
878
  e.rect = round(rect);
770
879
  const own = scrollByHost.get(rec.host) ?? scrollByHost.get(hostAbove(rec.host));
771
880
  if (own) rec.scroller = own;
772
881
  rec.onScreen = isOnScreen(rec, rect, window, bars);
773
882
  }
883
+ const unmeasured = order.length > 0 && !measured && !hasDom(order) && !globalThis.nativeFabricUIManager;
774
884
  const named = /* @__PURE__ */ new Set();
775
885
  const single = (rec) => {
776
886
  const name = rec.element.webview;
@@ -783,18 +893,22 @@ function collectScreen(roots, window) {
783
893
  found: order.filter(
784
894
  (rec) => (rec.element.kind !== "text" || rec.element.text !== void 0) && single(rec)
785
895
  ),
786
- scrollables: [...scrollByHost.values()]
896
+ scrollables: [...scrollByHost.values()],
897
+ unmeasured
787
898
  };
788
899
  }
900
+ var UNMEASURED_ERROR = "No Fabric UIManager found (React Native's old architecture, or a test renderer), so elements can't be measured. agent-bridge screen tools need the New Architecture.";
789
901
  var focused = (found) => found.filter((f) => !f.hidden);
790
902
  function pressFiberOf(found) {
791
- if (found.press) return { fiber: found.press, via: "onPress" };
792
- if (activates(found)) return { fiber: found.actions, via: "accessibilityAction" };
903
+ if (found.press) return { fiber: found.press, via: "onPress", ancestor: false };
904
+ if (activates(found))
905
+ return { fiber: found.actions, via: "accessibilityAction", ancestor: false };
793
906
  for (let f = found.fiber.return; f; f = f.return) {
794
907
  const p = propsOf(f);
795
908
  if (!p) continue;
796
- if (isFn(p.onPress)) return { fiber: f, via: "onPress" };
797
- if (actionNamesOf(p)?.includes("activate")) return { fiber: f, via: "accessibilityAction" };
909
+ if (isFn(p.onPress)) return { fiber: f, via: "onPress", ancestor: true };
910
+ if (actionNamesOf(p)?.includes("activate"))
911
+ return { fiber: f, via: "accessibilityAction", ancestor: true };
798
912
  }
799
913
  return null;
800
914
  }
@@ -860,15 +974,16 @@ function matchTarget(found, target) {
860
974
  }
861
975
  var isAt = (target) => typeof target === "object" && target.at !== void 0;
862
976
  var indexOf = (target) => typeof target === "object" ? target.index : void 0;
863
- var clip = (s) => s.length > 40 ? `${s.slice(0, 39)}\u2026` : s;
864
- function describe(e) {
977
+ function describe(e, max = 40, wholeIds = false) {
978
+ const clip = (s) => s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
979
+ const id = (s) => wholeIds ? s : clip(s);
865
980
  const stated = e.checked !== void 0 || e.selected !== void 0 || e.expanded !== void 0;
866
981
  const bits = [stated && e.role ? e.role : e.kind];
867
982
  if (e.webview !== void 0) bits.push(JSON.stringify(e.webview));
868
983
  if (e.testID) bits.push(`#${e.testID}`);
869
984
  if (e.text) bits.push(JSON.stringify(clip(e.text)));
870
- if (e.label && e.label !== e.text) bits.push(`label=${JSON.stringify(clip(e.label))}`);
871
- if (e.placeholder) bits.push(`placeholder=${JSON.stringify(clip(e.placeholder))}`);
985
+ if (e.label && e.label !== e.text) bits.push(`label=${JSON.stringify(id(e.label))}`);
986
+ if (e.placeholder) bits.push(`placeholder=${JSON.stringify(id(e.placeholder))}`);
872
987
  if (e.value !== void 0) bits.push(`value=${JSON.stringify(clip(e.value))}`);
873
988
  if (e.checked !== void 0) bits.push(e.checked === true ? "checked" : e.checked === false ? "unchecked" : "mixed");
874
989
  if (e.selected !== void 0) bits.push(e.selected ? "selected" : "unselected");
@@ -890,7 +1005,8 @@ function resolveTarget(found, target, prefer, options) {
890
1005
  reachable.filter((f) => f.onScreen),
891
1006
  target
892
1007
  );
893
- if (!matches.length && options?.offscreen) matches = matchTarget(reachable, target);
1008
+ const hasIndex = (options?.index ?? indexOf(target)) !== void 0;
1009
+ if (options?.offscreen && (!matches.length || hasIndex)) matches = matchTarget(reachable, target);
894
1010
  if (!matches.length) {
895
1011
  const hidden = matchTarget(found, target)[0];
896
1012
  if (hidden?.hidden)
@@ -901,7 +1017,7 @@ function resolveTarget(found, target, prefer, options) {
901
1017
  throw new Error(
902
1018
  `${showTarget(target)} matches ${describe(hidden.element)}, which is not on screen. Pass {scroll:true} to scroll it into view, or use screen.scroll`
903
1019
  );
904
- 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" : "";
1020
+ const unrendered = options?.offscreen ? "" : ". A list renders only the rows near its window: pass {scroll:true} to page it for a row it has not rendered yet";
905
1021
  throw new Error(
906
1022
  `Nothing on screen matches ${showTarget(target)}. On screen: ${onScreenSummary(found)}${unrendered}`
907
1023
  );
@@ -918,10 +1034,10 @@ function resolveTarget(found, target, prefer, options) {
918
1034
  return pick;
919
1035
  }
920
1036
  if (matches.length > 1) {
921
- const list = matches.slice(0, 10).map((f, i) => `${i}: ${describe(f.element)}`).join("; ");
1037
+ const list2 = matches.slice(0, 10).map((f, i) => `${i}: ${describe(f.element)}`).join("; ");
922
1038
  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)`;
923
1039
  throw new Error(
924
- `${showTarget(target)} matches ${matches.length} elements; pick one with an index, e.g. ${example}, or use a narrower target. ${list}`
1040
+ `${showTarget(target)} matches ${matches.length} elements; pick one with an index, e.g. ${example}, or use a narrower target. ${list2}`
925
1041
  );
926
1042
  }
927
1043
  return matches[0];
@@ -946,22 +1062,64 @@ var event = (nativeEvent, target) => ({
946
1062
  persist: noop
947
1063
  });
948
1064
  var eventCount = 0;
949
- function fillInput(found, text, options = {}) {
950
- const input = found.input;
951
- if (!input) throw new Error(`${describe(found.element)} is not a text input`);
1065
+ var has = (v) => v !== void 0 && v !== null;
1066
+ var isUncontrolled = (found) => !!found.input && !has(propsOf(found.input)?.value) && !has(propsOf(found.host)?.value);
1067
+ function setNativeText(found, text) {
1068
+ for (const fiber of [found.host, found.input]) {
1069
+ const node = fiber?.stateNode;
1070
+ for (const target of [node, node?.canonical?.publicInstance]) {
1071
+ if (target && typeof target.setNativeProps === "function") {
1072
+ target.setNativeProps({ text });
1073
+ return true;
1074
+ }
1075
+ }
1076
+ }
1077
+ const dom = found.host.stateNode;
1078
+ if (dom && typeof dom === "object" && "value" in dom) {
1079
+ dom.value = text;
1080
+ return true;
1081
+ }
1082
+ return false;
1083
+ }
1084
+ var keysOf = (text) => {
1085
+ const Segmenter = Intl.Segmenter;
1086
+ return Segmenter ? Array.from(new Segmenter(void 0, { granularity: "grapheme" }).segment(text), (x) => x.segment) : Array.from(text);
1087
+ };
1088
+ async function fillInput(found, text, options = {}) {
1089
+ if (!found.input) throw new Error(`${describe(found.element)} is not a text input`);
952
1090
  if (found.element.editable === false)
953
1091
  throw new Error(`${describe(found.element)} is not editable`);
954
1092
  const { maxLength } = found;
955
1093
  const value = maxLength === void 0 ? text : text.slice(0, Math.max(0, maxLength));
1094
+ const uncontrolled = isUncontrolled(found);
1095
+ let native = uncontrolled ? false : null;
1096
+ let current = found;
1097
+ const change = (next) => {
1098
+ const target2 = { value: next };
1099
+ if (uncontrolled && setNativeText(current, next)) native = true;
1100
+ const input2 = current.input;
1101
+ handler(input2, "onChangeText")?.(next);
1102
+ eventCount += 1;
1103
+ handler(input2, "onChange")?.(event({ text: next, eventCount }, target2));
1104
+ };
956
1105
  const target = { value };
957
- handler(input, "onFocus")?.(event({ text: value }, target));
958
- handler(input, "onChangeText")?.(value);
959
- eventCount += 1;
960
- handler(input, "onChange")?.(event({ text: value, eventCount }, target));
961
- if (options.submit)
962
- handler(input, "onSubmitEditing")?.(event({ text: value }, target));
1106
+ handler(found.input, "onFocus")?.(event({ text: value }, target));
1107
+ if (options.type && value) {
1108
+ const keys = keysOf(value);
1109
+ let typed = "";
1110
+ for (const key of keys) {
1111
+ typed += key;
1112
+ handler(current.input, "onKeyPress")?.(event({ key: key === "\n" ? "Enter" : key }));
1113
+ change(typed);
1114
+ if (options.between) await options.between();
1115
+ if (options.again) current = options.again(current);
1116
+ }
1117
+ } else change(value);
1118
+ const input = current.input;
1119
+ if (options.submit) handler(input, "onSubmitEditing")?.(event({ text: value }, target));
1120
+ handler(input, "onEndEditing")?.(event({ text: value }, target));
963
1121
  handler(input, "onBlur")?.(event({ text: value }, target));
964
- return value;
1122
+ return { filled: value, native };
965
1123
  }
966
1124
  var center = (rect) => rect ? { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 } : { x: 0, y: 0 };
967
1125
  var disabledProps = (fiber) => {
@@ -969,43 +1127,204 @@ var disabledProps = (fiber) => {
969
1127
  const state = p?.accessibilityState;
970
1128
  return p?.disabled === true || state?.disabled === true || p?.["aria-disabled"] === true;
971
1129
  };
1130
+ function refuseDisabled(found, fiber, force) {
1131
+ if (!force && (found.element.disabled || disabledProps(fiber)))
1132
+ throw new Error(
1133
+ `${describe(found.element)} is disabled; pass {"force":true} as the last argument to act on it anyway`
1134
+ );
1135
+ }
972
1136
  function pressTargetOf(found, action) {
973
1137
  if (action === void 0) return pressFiberOf(found);
974
1138
  const fiber = actionFiberOf(found, action);
975
- return fiber && { fiber, via: "accessibilityAction" };
1139
+ return fiber && { fiber, via: "accessibilityAction", ancestor: fiber !== found.actions };
1140
+ }
1141
+ var isFn2 = (v) => typeof v === "function";
1142
+ var isToggle = (found) => !found.press && !!found.change && isFn2(propsOf(found.change)?.onValueChange) && isSwitchFiber(found.change);
1143
+ var HOLD = ["onPress", "onPressIn", "onLongPress"];
1144
+ function longPressFiberOf(found) {
1145
+ let handlesPress = false;
1146
+ for (let f = found.fiber; f; f = f.child) {
1147
+ const p = propsOf(f);
1148
+ if (isFn2(p?.onLongPress)) return { fiber: f, ancestor: false };
1149
+ if (p && HOLD.some((k) => isFn2(p[k]))) handlesPress = true;
1150
+ if (f === found.host) break;
1151
+ }
1152
+ if (handlesPress) return null;
1153
+ for (let f = found.fiber.return; f; f = f.return) {
1154
+ const p = propsOf(f);
1155
+ if (isFn2(p?.onLongPress)) return { fiber: f, ancestor: true };
1156
+ if (p && HOLD.some((k) => isFn2(p[k]))) return null;
1157
+ }
1158
+ return null;
1159
+ }
1160
+ var TAP_GESTURES = /* @__PURE__ */ new Set(["TapGestureHandler", "LongPressGestureHandler"]);
1161
+ function isTapGesture(g) {
1162
+ if (!g || typeof g !== "object") return false;
1163
+ const x = g;
1164
+ if (typeof x.handlerName === "string" && TAP_GESTURES.has(x.handlerName)) return true;
1165
+ if (typeof x.type === "string" && TAP_GESTURES.has(x.type)) return true;
1166
+ return Array.isArray(x.gestures) && x.gestures.some(isTapGesture);
1167
+ }
1168
+ function tapDetectorBetween(found, until) {
1169
+ for (let f = found.fiber; f; f = f.child) {
1170
+ if (isTapGesture(propsOf(f)?.gesture)) return true;
1171
+ if (f === found.host) break;
1172
+ }
1173
+ for (let f = found.fiber.return; f && f !== until; f = f.return) {
1174
+ const p = propsOf(f);
1175
+ if (isTapGesture(p?.gesture)) return true;
1176
+ if (!until && p && HOLD.some((k) => isFn2(p[k]))) break;
1177
+ }
1178
+ return false;
976
1179
  }
1180
+ var GESTURE_NOTE = "it is inside a react-native-gesture-handler GestureDetector, whose callbacks run on the UI thread and cannot be fired from here, so use a real tap (agent-device)";
1181
+ var gestureNote = (found) => tapDetectorBetween(found, null) ? `; ${GESTURE_NOTE}` : "";
977
1182
  function pressElement(found, options = {}) {
978
1183
  const { action } = options;
1184
+ if (options.long) return longPress(found, options);
1185
+ if (action === void 0 && isToggle(found)) {
1186
+ refuseDisabled(found, found.change, options.force);
1187
+ const now2 = propsOf(found.change)?.value;
1188
+ handler(found.change, "onValueChange")?.(now2 !== true);
1189
+ return "onValueChange";
1190
+ }
979
1191
  const target = pressTargetOf(found, action);
980
1192
  if (!target) {
981
1193
  const names = found.element.actions ?? [];
982
- const has = names.length ? `; it has ${names.map((a) => JSON.stringify(a)).join(", ")}` : "";
1194
+ const has2 = names.length ? `; it has ${names.map((a) => JSON.stringify(a)).join(", ")}` : "";
983
1195
  throw new Error(
984
- action === void 0 ? `${describe(found.element)} has no onPress or "activate" accessibility action` : `${describe(found.element)} has no accessibility action ${JSON.stringify(action)}${has}`
1196
+ action === void 0 ? `${describe(found.element)} has no onPress or "activate" accessibility action${gestureNote(found)}` : `${describe(found.element)} has no accessibility action ${JSON.stringify(action)}${has2}`
985
1197
  );
986
1198
  }
987
- const { fiber, via } = target;
988
- if (!options.force && (found.element.disabled || disabledProps(fiber)))
1199
+ const { fiber, via, ancestor } = target;
1200
+ if (ancestor && action === void 0 && tapDetectorBetween(found, fiber))
989
1201
  throw new Error(
990
- `${describe(found.element)} is disabled; pass {"force":true} as the last argument to press anyway`
1202
+ `${describe(found.element)} has no onPress of its own, and ${GESTURE_NOTE}; pressing the ancestor that has one would do something else`
991
1203
  );
1204
+ const from = ancestor ? "ancestor:" : "";
1205
+ refuseDisabled(found, fiber, options.force);
992
1206
  if (via === "accessibilityAction") {
993
1207
  const actionName = action ?? "activate";
994
1208
  handler(fiber, "onAccessibilityAction")?.(event({ actionName }));
995
- return `accessibilityAction:${actionName}`;
1209
+ return `${from}accessibilityAction:${actionName}`;
996
1210
  }
1211
+ const e = () => pressEvent(found);
1212
+ handler(fiber, "onPressIn")?.(e());
1213
+ handler(fiber, "onPress")?.(e());
1214
+ handler(fiber, "onPressOut")?.(e());
1215
+ return ancestor ? "ancestor" : "onPress";
1216
+ }
1217
+ var pressEvent = (found) => {
997
1218
  const { x, y } = center(found.element.rect);
998
- const e = () => event({
1219
+ return event({
999
1220
  locationX: found.element.rect ? found.element.rect.width / 2 : 0,
1000
1221
  locationY: found.element.rect ? found.element.rect.height / 2 : 0,
1001
1222
  pageX: x,
1002
1223
  pageY: y,
1003
1224
  timestamp: Date.now()
1004
1225
  });
1005
- handler(fiber, "onPressIn")?.(e());
1006
- handler(fiber, "onPress")?.(e());
1007
- handler(fiber, "onPressOut")?.(e());
1008
- return "onPress";
1226
+ };
1227
+ function longPress(found, options) {
1228
+ if (options.action !== void 0)
1229
+ throw new Error("{long:true} and {action} cannot be combined: long presses call onLongPress, actions send an accessibility action");
1230
+ const target = longPressFiberOf(found);
1231
+ if (!target)
1232
+ throw new Error(`${describe(found.element)} has no onLongPress${gestureNote(found)}`);
1233
+ refuseDisabled(found, target.fiber, options.force);
1234
+ handler(target.fiber, "onPressIn")?.(pressEvent(found));
1235
+ handler(target.fiber, "onLongPress")?.(pressEvent(found));
1236
+ handler(target.fiber, "onPressOut")?.(pressEvent(found));
1237
+ return target.ancestor ? "ancestor:onLongPress" : "onLongPress";
1238
+ }
1239
+ function itemsOfChildren(children, out = []) {
1240
+ if (Array.isArray(children)) for (const c of children) itemsOfChildren(c, out);
1241
+ else if (children && typeof children === "object") {
1242
+ const p = children.props;
1243
+ if (p && has(p.value) && typeof p.label === "string") out.push({ value: p.value, label: p.label });
1244
+ else if (p) itemsOfChildren(p.children, out);
1245
+ }
1246
+ return out;
1247
+ }
1248
+ function pickerItems(fiber) {
1249
+ const items = itemsOfChildren(propsOf(fiber)?.children);
1250
+ if (items.length) return items;
1251
+ const stack = fiber.child ? [fiber.child] : [];
1252
+ while (stack.length) {
1253
+ const f = stack.pop();
1254
+ if (f.sibling) stack.push(f.sibling);
1255
+ if (f.child) stack.push(f.child);
1256
+ const p = propsOf(f);
1257
+ if (p && has(p.value) && typeof p.label === "string") items.push({ value: p.value, label: p.label });
1258
+ }
1259
+ return items;
1260
+ }
1261
+ function isSlider(fiber) {
1262
+ const p = propsOf(fiber);
1263
+ if (["onSlidingStart", "onSlidingComplete", "minimumValue", "maximumValue", "step"].some((k) => k in p))
1264
+ return true;
1265
+ const own = firstHost(fiber);
1266
+ return !!own && /Slider/.test(String(own.type));
1267
+ }
1268
+ function valueOf(found) {
1269
+ const p = found.change && propsOf(found.change);
1270
+ if (!p) return void 0;
1271
+ if (has(p.value)) return p.value;
1272
+ if (has(p.selectedValue)) return p.selectedValue;
1273
+ if (Array.isArray(p.values) && typeof p.selectedIndex === "number") return p.values[p.selectedIndex];
1274
+ return void 0;
1275
+ }
1276
+ var list = (xs) => xs.map((x) => JSON.stringify(x)).join(", ");
1277
+ function setControlValue(found, value, options = {}) {
1278
+ const fiber = found.change;
1279
+ const what = describe(found.element);
1280
+ if (!fiber)
1281
+ throw new Error(`${what} has no onValueChange (a Switch, Slider, Picker or SegmentedControl has one)`);
1282
+ refuseDisabled(found, fiber, options.force);
1283
+ const p = propsOf(fiber);
1284
+ const call = (name, ...args) => handler(fiber, name)?.(...args);
1285
+ if (isSwitchFiber(fiber)) {
1286
+ if (typeof value !== "boolean") throw new Error(`${what} is a switch: pass true or false; got ${JSON.stringify(value)}`);
1287
+ return void call("onValueChange", value);
1288
+ }
1289
+ if (Array.isArray(p.values)) {
1290
+ if (!p.values.includes(value))
1291
+ throw new Error(`${what} takes one of ${list(p.values)}; got ${JSON.stringify(value)}`);
1292
+ const selectedSegmentIndex = p.values.indexOf(value);
1293
+ call("onChange", event({ value, selectedSegmentIndex }));
1294
+ return void call("onValueChange", value);
1295
+ }
1296
+ const items = pickerItems(fiber);
1297
+ if (items.length || "selectedValue" in p) {
1298
+ const index = items.findIndex((i) => i.value === value);
1299
+ if (!items.length) throw new Error(`${what} is a picker but no Picker.Item children could be read`);
1300
+ if (index < 0)
1301
+ throw new Error(`${what} takes one of ${list(items.map((i) => i.value))}; got ${JSON.stringify(value)}`);
1302
+ return void call("onValueChange", value, index);
1303
+ }
1304
+ const current = valueOf(found);
1305
+ if (isSlider(fiber) || typeof current === "number") {
1306
+ if (typeof value !== "number" || !Number.isFinite(value))
1307
+ throw new Error(`${what} takes a number; got ${JSON.stringify(value)}`);
1308
+ const slider = isSlider(fiber);
1309
+ const min = typeof p.minimumValue === "number" ? p.minimumValue : slider ? 0 : void 0;
1310
+ const max = typeof p.maximumValue === "number" ? p.maximumValue : slider ? 1 : void 0;
1311
+ if (min !== void 0 && value < min || max !== void 0 && value > max)
1312
+ throw new Error(`${what} takes a number between ${min ?? "-\u221E"} and ${max ?? "\u221E"}; got ${value}`);
1313
+ const step = p.step;
1314
+ if (typeof step === "number" && step > 0) {
1315
+ const n = (value - (min ?? 0)) / step;
1316
+ if (Math.abs(n - Math.round(n)) > 1e-6) {
1317
+ const lo = (min ?? 0) + Math.floor(n) * step;
1318
+ throw new Error(`${what} moves in steps of ${step}; got ${value} (nearest: ${lo}, ${lo + step})`);
1319
+ }
1320
+ }
1321
+ call("onSlidingStart", value);
1322
+ call("onValueChange", value);
1323
+ return void call("onSlidingComplete", value);
1324
+ }
1325
+ if (typeof current === "string" && typeof value !== "string")
1326
+ throw new Error(`${what} takes a string; got ${JSON.stringify(value)}`);
1327
+ call("onValueChange", value);
1009
1328
  }
1010
1329
 
1011
1330
  // src/runtime/screen/find-text.ts
@@ -1237,6 +1556,63 @@ function mainScrollable(scrollables, window, need) {
1237
1556
  );
1238
1557
  return best.s;
1239
1558
  }
1559
+ var isVirtualized = (s) => s.fibers.some((f) => typeof propsOf(f)?.renderItem === "function");
1560
+ function pagingList(scrollables, window) {
1561
+ const all = scrollables.filter((s) => !s.hidden && scrollEnabled(s) && isVirtualized(s)).map((s) => ({ s, rect: measureHost(s.host) })).filter(({ rect }) => onWindow(rect, window)).sort((a, b) => area2(b.rect) - area2(a.rect)).map(({ s }) => s);
1562
+ const list2 = all.find((s) => !isHorizontal(s)) ?? all[0] ?? null;
1563
+ return { list: list2, skipped: all.filter((s) => s !== list2) };
1564
+ }
1565
+ async function awaitRows(read, has2, maxMs = 250) {
1566
+ const sig = () => {
1567
+ const { metrics, rows } = read();
1568
+ return { metrics, rows, key: `${rows}|${metrics?.max}|${metrics?.offset}` };
1569
+ };
1570
+ let cur = sig();
1571
+ let still = 0;
1572
+ for (let waited = 0; waited < maxMs; waited += 16) {
1573
+ if (has2()) break;
1574
+ await wait(16);
1575
+ const next = sig();
1576
+ still = next.key === cur.key ? still + 1 : 0;
1577
+ cur = next;
1578
+ if (cur.rows > 0 && still >= 4) break;
1579
+ }
1580
+ return cur.metrics;
1581
+ }
1582
+
1583
+ // src/runtime/screen/snapshot-text.ts
1584
+ var ELEMENT_KINDS = ["button", "input", "text", "view", "webview"];
1585
+ var center2 = (r) => ({ x: r.x + r.width / 2, y: r.y + r.height / 2 });
1586
+ function underScope(found, scope) {
1587
+ for (let f = found.fiber; f; f = f.return) if (f === scope.fiber) return true;
1588
+ return false;
1589
+ }
1590
+ function snapshotText(lines, hidden, empty = "(nothing on screen)") {
1591
+ const base = lines.map(({ element: e }) => {
1592
+ const bits = [describe(e, 80, true)];
1593
+ if (e.editable === false) bits.push("readonly");
1594
+ if (e.actions?.length) bits.push(`actions=${e.actions.join(",")}`);
1595
+ if (e.onScreen === false) bits.push("offscreen");
1596
+ if (e.hidden) bits.push("hidden");
1597
+ return bits.join(" ");
1598
+ });
1599
+ const seen = /* @__PURE__ */ new Map();
1600
+ for (const l of base) seen.set(l, (seen.get(l) ?? 0) + 1);
1601
+ const out = base.map((l, i) => {
1602
+ const { element: e, index } = lines[i];
1603
+ const repeats = (seen.get(l) ?? 0) > 1;
1604
+ const named = !!(e.testID || e.text || e.label || e.placeholder);
1605
+ const bits = [l];
1606
+ if (repeats && index !== void 0) bits.push(`index=${index}`);
1607
+ if (e.rect && (repeats && index === void 0 || !named)) {
1608
+ const c = center2(e.rect);
1609
+ bits.push(`@${Math.round(c.x)},${Math.round(c.y)}`);
1610
+ }
1611
+ return bits.join(" ");
1612
+ });
1613
+ if (hidden) out.push(`(${hidden} hidden: on screens not in front; {all:true} lists them)`);
1614
+ return lines.length || hidden ? out.join("\n") : empty;
1615
+ }
1240
1616
 
1241
1617
  // src/runtime/screen/settle.ts
1242
1618
  var STATE = /* @__PURE__ */ Symbol.for("@avasapp/agent-bridge/commits");
@@ -1309,6 +1685,7 @@ async function settle(options = {}) {
1309
1685
 
1310
1686
  // src/runtime/screen/wait-for.ts
1311
1687
  var POLL_MS = 50;
1688
+ var NO_PAGING = "waitFor does not page lists for rows they have not rendered; wait for the list, then use press, fill or setValue with {scroll:true}";
1312
1689
  function nearMissNote(found, target) {
1313
1690
  const text = typeof target === "string" ? target : target.text;
1314
1691
  const near = text ? nearMisses(found, text) : [];
@@ -1359,7 +1736,7 @@ function waitForTarget(collect, target, options = {}, reveal) {
1359
1736
  const what = options.gone ? "to disappear" : "to appear";
1360
1737
  reject(
1361
1738
  new Error(
1362
- `Timed out after ${timeoutMs} ms waiting for ${showTarget(target)} ${what}. On screen: ${onScreenSummary(found)}${nearMissNote(found, target)}`
1739
+ `Timed out after ${timeoutMs} ms waiting for ${showTarget(target)} ${what}. On screen: ${onScreenSummary(found)}${nearMissNote(found, target)}${options.scroll && !options.gone ? `. ${NO_PAGING}` : ""}`
1363
1740
  )
1364
1741
  );
1365
1742
  }
@@ -1379,8 +1756,25 @@ function waitForTarget(collect, target, options = {}, reveal) {
1379
1756
  }
1380
1757
 
1381
1758
  // src/runtime/screen/index.ts
1759
+ function snapshotKinds(kinds) {
1760
+ if (kinds === void 0) return [];
1761
+ const list2 = typeof kinds === "string" ? [kinds] : kinds;
1762
+ if (!Array.isArray(list2))
1763
+ throw new Error(`screen.snapshot kinds is a list like ["button","input"], not ${JSON.stringify(kinds)}`);
1764
+ for (const k of list2)
1765
+ if (!ELEMENT_KINDS.includes(k))
1766
+ throw new Error(
1767
+ `screen.snapshot kinds are ${ELEMENT_KINDS.join(", ")}; ${JSON.stringify(k)} is not one (a line that starts with switch, checkbox, tab or another role is a button: ask for "button")`
1768
+ );
1769
+ return list2;
1770
+ }
1382
1771
  function createScreen(env) {
1383
- const collectAll = () => collectScreen(env.roots(), env.window());
1772
+ const collectLoose = () => collectScreen(env.roots(), env.window());
1773
+ const collectAll = () => {
1774
+ const all = collectLoose();
1775
+ if (all.unmeasured) throw new Error(UNMEASURED_ERROR);
1776
+ return all;
1777
+ };
1384
1778
  const collect = () => collectAll().found;
1385
1779
  const collectSettled = async () => {
1386
1780
  let prev = null;
@@ -1435,39 +1829,258 @@ function createScreen(env) {
1435
1829
  );
1436
1830
  return shown;
1437
1831
  };
1832
+ const PAGE_LIMIT = 40;
1833
+ const PAGE_MS = 1e4;
1834
+ const pagingBudget = (scroll) => {
1835
+ if (scroll === null || typeof scroll !== "object") return PAGE_MS;
1836
+ const bad = Object.keys(scroll).filter((k) => k !== "timeoutMs");
1837
+ if (bad.length)
1838
+ throw new Error(`Unknown scroll option ${bad.map((k) => JSON.stringify(k)).join(", ")}. Valid: timeoutMs`);
1839
+ const ms = scroll.timeoutMs;
1840
+ if (ms === void 0) return PAGE_MS;
1841
+ if (typeof ms !== "number" || !Number.isFinite(ms) || ms <= 0)
1842
+ throw new Error(`scroll.timeoutMs is a positive number of milliseconds; got ${JSON.stringify(ms)}`);
1843
+ return ms;
1844
+ };
1845
+ const scrollableAgain = (host) => {
1846
+ const node = host.stateNode;
1847
+ return collectAll().scrollables.find(
1848
+ (s) => s.host === host || node != null && s.host.stateNode === node
1849
+ );
1850
+ };
1851
+ const rendered = (target, index) => () => matchTarget(collect().filter((f) => !f.hidden), target).length > (indexOf(target) ?? index ?? 0);
1852
+ const nameOf = (host) => {
1853
+ const el = collectAll().found.find((f) => f.host === host || f.host.stateNode === host.stateNode);
1854
+ return el ? describe(el.element) : "the list";
1855
+ };
1856
+ const pageList = async (first, skipped, has2, budgetMs) => {
1857
+ const { host } = first;
1858
+ const name = nameOf(host);
1859
+ const first0 = metricsOf(first);
1860
+ if (!first0) return { found: false, note: `${name} has no readable scroll position` };
1861
+ const start = first0;
1862
+ const size = Math.max(1, ((isHorizontal(first) ? start.view?.width : start.view?.height) ?? 400) * 0.9);
1863
+ const deadline = performance.now() + budgetMs;
1864
+ let pages = 0;
1865
+ let lowest = start.offset;
1866
+ let highest = start.offset;
1867
+ let max = start.max;
1868
+ let stop = "both ends";
1869
+ const horizontal = isHorizontal(first);
1870
+ const node = host.stateNode;
1871
+ let span = null;
1872
+ const readRows = () => {
1873
+ const now2 = scrollableAgain(host);
1874
+ const metrics = now2 ? metricsOf(now2) : null;
1875
+ let rows = 0;
1876
+ let top = Infinity;
1877
+ let bottom = -Infinity;
1878
+ for (const f of collectAll().found) {
1879
+ let inList = false;
1880
+ for (let x = f.scroller; x; x = x.parent)
1881
+ if (x.host === host || node != null && x.host.stateNode === node) inList = true;
1882
+ if (!inList) continue;
1883
+ if (f.onScreen) rows++;
1884
+ const box = metrics?.view && !metrics.inverted ? measureHost(f.host) : null;
1885
+ if (!metrics?.view || !box) continue;
1886
+ const at = (horizontal ? box.x - metrics.view.x : box.y - metrics.view.y) + metrics.offset;
1887
+ top = Math.min(top, at);
1888
+ bottom = Math.max(bottom, at + (horizontal ? box.width : box.height));
1889
+ }
1890
+ span = top <= bottom ? { top, bottom } : null;
1891
+ return { metrics, rows };
1892
+ };
1893
+ const go = async (offset) => {
1894
+ const now2 = scrollableAgain(host);
1895
+ if (!now2) return false;
1896
+ const before = metricsOf(now2);
1897
+ scrollToOffset(now2, offset);
1898
+ await awaitScroll(now2, before?.offset, offset);
1899
+ pages++;
1900
+ if (has2()) return true;
1901
+ const m = await awaitRows(readRows, has2);
1902
+ if (m) {
1903
+ lowest = Math.min(lowest, m.offset);
1904
+ highest = Math.max(highest, m.offset);
1905
+ max = m.max;
1906
+ }
1907
+ return has2();
1908
+ };
1909
+ const sweep = async (dir, until, limit) => {
1910
+ const from = pages;
1911
+ while (true) {
1912
+ if (pages - from >= limit || performance.now() >= until) {
1913
+ stop = pages >= PAGE_LIMIT ? `the ${PAGE_LIMIT}-page limit` : `the ${budgetMs} ms budget`;
1914
+ return false;
1915
+ }
1916
+ const m = readRows().metrics;
1917
+ if (!m) return false;
1918
+ const viewSize = size / 0.9;
1919
+ const reach = span;
1920
+ const want = dir === 1 ? Math.max(m.offset + size, reach ? reach.bottom : -Infinity) : Math.min(m.offset - size, reach ? reach.top - viewSize : Infinity);
1921
+ const next = dir === 1 ? Math.min(m.max ?? Infinity, want) : Math.max(0, want);
1922
+ if (Math.abs(next - m.offset) < 1) {
1923
+ const grown = await awaitRows(readRows, has2, 400);
1924
+ if (has2()) return true;
1925
+ if (dir === 1 && grown && (grown.max ?? 0) - m.offset >= 1) continue;
1926
+ return false;
1927
+ }
1928
+ if (await go(next)) return true;
1929
+ }
1930
+ };
1931
+ let found = false;
1932
+ try {
1933
+ const downFirst = start.max == null || start.max - start.offset <= start.offset;
1934
+ const order = downFirst ? [1, -1] : [-1, 1];
1935
+ const now2 = performance.now();
1936
+ for (const [i, dir] of order.entries()) {
1937
+ const otherRoom = dir === -1 ? start.max == null || start.max - start.offset >= 1 : start.offset >= 1;
1938
+ const half = i === 0 && otherRoom;
1939
+ const share = half ? now2 + budgetMs / 2 : deadline;
1940
+ const limit = half ? PAGE_LIMIT / 2 : PAGE_LIMIT - pages;
1941
+ if (dir === -1 && start.offset < 1) continue;
1942
+ if (i === 1 && pages) await go(start.offset);
1943
+ if (await sweep(dir, share, limit)) return found = true, done();
1944
+ }
1945
+ } finally {
1946
+ if (!found) {
1947
+ const now2 = scrollableAgain(host);
1948
+ if (now2) {
1949
+ await scrolled(now2, () => (scrollToOffset(now2, start.offset), start.offset));
1950
+ await settle();
1951
+ }
1952
+ }
1953
+ }
1954
+ return done();
1955
+ function done() {
1956
+ const where = `up to offset ${Math.round(lowest)} and down to offset ${Math.round(highest)} of ${max == null ? "?" : Math.round(max)}`;
1957
+ const left = skipped.length ? `. Not paged: ${skipped.map((s) => nameOf(s.host)).join(", ")}` : "";
1958
+ return {
1959
+ found,
1960
+ note: found ? "" : `{scroll:true} paged ${name} ${pages} page(s), ${where}, without rendering a match, then scrolled back to offset ${Math.round(start.offset)}. Stopped at: ${stop}${left}`
1961
+ };
1962
+ }
1963
+ };
1964
+ const pageFor = async (has2, budgetMs) => {
1965
+ const { list: list2, skipped } = pagingList(collectAll().scrollables, env.window());
1966
+ if (!list2)
1967
+ return { found: false, note: "{scroll:true} found no FlatList, SectionList or FlashList on screen to page" };
1968
+ return pageList(list2, skipped, has2, budgetMs);
1969
+ };
1438
1970
  const resolve = async (target, prefer, options) => {
1439
- const found = resolveTarget(collect(), target, prefer, {
1440
- ...options,
1441
- offscreen: options.scroll
1442
- });
1443
- return found.onScreen ? found : reveal(found);
1971
+ const pick = () => resolveTarget(collect(), target, prefer, { ...options, offscreen: !!options.scroll });
1972
+ let note = "";
1973
+ if (options.scroll && typeof target === "object" && target.at !== void 0)
1974
+ throw new Error(
1975
+ "{at:[x,y]} is a point on the screen and can't be scrolled to; drop {scroll:true} or target by text or testID"
1976
+ );
1977
+ const has2 = rendered(target, options.index);
1978
+ if (options.scroll && !has2()) {
1979
+ const paged = await pageFor(has2, pagingBudget(options.scroll));
1980
+ note = paged.note;
1981
+ if (!paged.found) {
1982
+ try {
1983
+ return await settleOn(pick());
1984
+ } catch (error) {
1985
+ throw note ? new Error(`${error.message}. ${note}`) : error;
1986
+ }
1987
+ }
1988
+ }
1989
+ return settleOn(pick());
1990
+ };
1991
+ const settleOn = async (found) => found.onScreen ? found : reveal(found);
1992
+ const build = (options) => {
1993
+ const { all, format, within: within2 } = options;
1994
+ if (format !== void 0 && format !== "json" && format !== "text")
1995
+ throw new Error(`screen.snapshot format is "json" or "text", not ${JSON.stringify(format)}`);
1996
+ const kinds = snapshotKinds(options.kinds);
1997
+ const found = all ? collectLoose().found : collect();
1998
+ let pool;
1999
+ let hidden;
2000
+ if (all) {
2001
+ pool = found;
2002
+ } else {
2003
+ const shown = focused(found);
2004
+ pool = shown.filter((f) => f.onScreen);
2005
+ hidden = found.length - shown.length || void 0;
2006
+ }
2007
+ let items = pool.map((f) => ({
2008
+ found: f,
2009
+ element: all ? { ...f.element, onScreen: f.onScreen, ...f.hidden && { hidden: true } } : f.element
2010
+ }));
2011
+ let scoped = "";
2012
+ if (within2 !== void 0) {
2013
+ const scope = scopeOf(found, within2, !!all);
2014
+ if (!scope.element.rect)
2015
+ throw new Error(`${describe(scope.element)} has no position, so it can't scope a snapshot`);
2016
+ scoped = describe(scope.element);
2017
+ items = items.filter((i) => i.found !== scope && underScope(i.found, scope));
2018
+ }
2019
+ if (kinds.length) items = items.filter((i) => kinds.includes(i.element.kind));
2020
+ const empty = kinds.length || scoped ? `(no ${kinds.length ? kinds.join("/") : ""} elements${scoped ? ` inside ${scoped}` : ""})`.replace("(no ", "(no ") : void 0;
2021
+ return {
2022
+ hidden,
2023
+ empty,
2024
+ items: items.map((i) => ({ element: i.element, index: matchIndex(pool, i.found) }))
2025
+ };
2026
+ };
2027
+ const matchIndex = (pool, f) => {
2028
+ const e = f.element;
2029
+ const name = e.testID ?? e.label ?? e.placeholder ?? e.text;
2030
+ if (!name) return void 0;
2031
+ const at = matchTarget(pool, name).indexOf(f);
2032
+ return at < 0 ? void 0 : at;
2033
+ };
2034
+ const scopeOf = (found, within2, all) => {
2035
+ const ranged = indexOf(within2) !== void 0;
2036
+ if (!ranged) {
2037
+ const matches = matchTarget(found.filter((f) => !f.hidden && (all || f.onScreen)), within2);
2038
+ if (matches.length > 1) {
2039
+ const first = matches[0].element;
2040
+ const fix = typeof within2 === "string" ? {
2041
+ [first.testID === within2 ? "testID" : first.label === within2 ? "label" : first.placeholder === within2 ? "placeholder" : "text"]: within2,
2042
+ index: 1
2043
+ } : { ...within2, index: 1 };
2044
+ throw new Error(
2045
+ `within ${showTarget(within2)} matches ${matches.length} elements; pick one with ${JSON.stringify({ within: fix })}`
2046
+ );
2047
+ }
2048
+ }
2049
+ return resolveTarget(found, within2, void 0, { offscreen: all });
1444
2050
  };
1445
2051
  return {
1446
2052
  snapshot(options = {}) {
1447
- const found = collect();
1448
- if (options.all)
1449
- return {
1450
- elements: found.map((f) => ({
1451
- ...f.element,
1452
- onScreen: f.onScreen,
1453
- ...f.hidden && { hidden: true }
1454
- }))
1455
- };
1456
- const shown = focused(found);
1457
- const elements = shown.filter((f) => f.onScreen).map((f) => f.element);
1458
- const hidden = found.length - shown.length;
2053
+ const { items, hidden } = build(options);
2054
+ const elements = items.map((i) => i.element);
1459
2055
  return hidden ? { elements, hidden } : { elements };
1460
2056
  },
1461
- findText: (text, options) => findText(collect(), text, options),
2057
+ /** `snapshot` as one line per element. */
2058
+ snapshotText(options = {}) {
2059
+ const { items, hidden, empty } = build(options);
2060
+ return snapshotText(items, hidden, empty);
2061
+ },
2062
+ findText: (text, options) => findText(collectLoose().found, text, options),
1462
2063
  async fill(target, text, options = {}) {
1463
2064
  const found = await resolve(target, (f) => !!f.input, {
1464
2065
  index: options.index,
1465
2066
  afterText: true,
1466
2067
  scroll: options.scroll
1467
2068
  });
1468
- const filled = fillInput(found, String(text), options);
2069
+ const { filled, native } = await fillInput(found, String(text), {
2070
+ ...options,
2071
+ between: async () => void await settle(),
2072
+ again: reread
2073
+ });
1469
2074
  await settle();
1470
- return { filled, element: reread(found).element };
2075
+ const element = reread(found).element;
2076
+ if (native === true) return { filled, element: { ...element, value: filled } };
2077
+ if (native === false)
2078
+ return {
2079
+ filled,
2080
+ element,
2081
+ warning: "the input has no value prop and its native text could not be set from here, so the field may not show the text"
2082
+ };
2083
+ return { filled, element };
1471
2084
  },
1472
2085
  async press(target, options = {}) {
1473
2086
  const { action } = options;
@@ -1475,13 +2088,30 @@ function createScreen(env) {
1475
2088
  throw new Error(`"action" must be an accessibility action name; got ${JSON.stringify(action)}`);
1476
2089
  const found = await resolve(
1477
2090
  target,
1478
- (f) => action === void 0 ? !!f.press || f.element.kind === "button" : !!f.element.actions?.includes(action),
2091
+ (f) => options.long ? !!longPressFiberOf(f) : action === void 0 ? !!f.press || f.element.kind === "button" || isToggle(f) : !!f.element.actions?.includes(action),
1479
2092
  options
1480
2093
  );
1481
2094
  const via = pressElement(found, options);
1482
2095
  await settle();
1483
2096
  return via === "onPress" ? found.element : { ...found.element, via };
1484
2097
  },
2098
+ /** Sets a Switch, Slider, Picker or SegmentedControl through its onValueChange, and reads the value back. */
2099
+ async setValue(target, value, options = {}) {
2100
+ const found = await resolve(target, (f) => !!f.change, options);
2101
+ const before = valueOf(found);
2102
+ setControlValue(found, value, options);
2103
+ await settle();
2104
+ const again = reread(found);
2105
+ const now2 = valueOf(again);
2106
+ const result = { value: now2, element: again.element };
2107
+ const same = typeof now2 === "number" && typeof value === "number" ? Math.abs(now2 - value) < 1e-6 : now2 === value;
2108
+ if (before !== void 0 && !same && !(found.change && isSlider(found.change)))
2109
+ return {
2110
+ ...result,
2111
+ warning: `the control still shows ${JSON.stringify(now2)}; the app did not apply ${JSON.stringify(value)}`
2112
+ };
2113
+ return result;
2114
+ },
1485
2115
  /**
1486
2116
  * Scrolls the nearest scrollable of a target (or `within`, or the main one
1487
2117
  * on screen) to the target, to either end, or by a distance in points.
@@ -1578,17 +2208,21 @@ function screenTools() {
1578
2208
  run: (text, options) => screen.findText(text, options)
1579
2209
  },
1580
2210
  "screen.snapshot": {
1581
- description: 'Buttons, inputs, text, and testID or labelled views on screen, with rects and checked/selected/expanded state (a Switch\'s value is checked). `actions` lists the accessibility actions an element handles; one with "activate" and no onPress (a swipe-to-confirm) is a button screen.press can press. 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.',
1582
- run: (options) => screen.snapshot(options)
2211
+ description: 'Buttons, inputs, text, and testID or labelled views on screen, with rects and checked/selected/expanded state (a Switch\'s value is checked). `actions` lists the accessibility actions an element handles; one with "activate" and no onPress (a swipe-to-confirm) is a button screen.press can press. 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. Options: {format:"json"|"text"} (the runtime default is JSON with rects; the CLI and the MCP server return the text form unless you pass {format:"json"}). Text is one line per element: kind (or role: switch, checkbox, tab), #testID, text, label, value, state, then `readonly`, `actions=\u2026`, `offscreen`, `hidden`. Lines that repeat get `index=N` (the {index} of the target that names them); a line with no testID, text, label or placeholder gets `@x,y`, the point for {at:[x,y]}. JSON has rects, onScreen:false and hidden:true. {kinds:["button","input"]} keeps only those kinds (kinds are button, input, text, view, webview; a switch or tab is a button); {within: target} keeps only elements under that target in the tree (a target as for screen.press; with several matches pass {testID, index} inside it).',
2212
+ run: (options) => options?.format === "text" ? screen.snapshotText(options) : screen.snapshot(options)
1583
2213
  },
1584
2214
  "screen.fill": {
1585
- description: `Type into an input, then wait for the render. ${TARGET_HELP} {submit:true} also submits.`,
2215
+ description: `Type into an input, then wait for the render. ${TARGET_HELP} {submit:true} also submits. "" clears it. {type:true} types one character at a time, waiting for a render after each (for debounced or per-character inputs; "\\n" is the Enter key). An input with no value prop gets its native text set; if that cannot be done the reply has a warning.`,
1586
2216
  run: (target, text, options) => screen.fill(target, text, options)
1587
2217
  },
1588
2218
  "screen.press": {
1589
- 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. With no onPress it sends the "activate" accessibility action, as a screen reader's double tap does (swipe-to-confirm buttons, sliders), and the reply says via:"accessibilityAction:activate". {action:"increment"} (or any name in the element's snapshot \`actions\`) sends that action instead.`,
2219
+ description: `Press a button, then wait for the render. ${TARGET_HELP} Text with no handler of its own presses the nearest ancestor that has one, and the reply says via:"ancestor". 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. With no onPress it sends the "activate" accessibility action, as a screen reader's double tap does (swipe-to-confirm buttons, sliders), and the reply says via:"accessibilityAction:activate". {action:"increment"} (or any name in the element's snapshot \`actions\`) sends that action instead. A Switch (native switch host or role switch, with onValueChange) is toggled with onValueChange(!value), not the row around it; reply via:"onValueChange". {long:true} long presses: onPressIn, onLongPress, onPressOut (never onPress); an error when there is no onLongPress, on the element or an ancestor. {scroll:true} also pages the largest vertical FlatList/SectionList/FlashList for a row it has not rendered yet (and scrolls back if it finds none; onEndReached may fire). A tap inside a react-native-gesture-handler GestureDetector cannot be pressed from here (its callbacks run on the UI thread): the error says so.`,
1590
2220
  run: (target, options) => screen.press(target, options)
1591
2221
  },
2222
+ "screen.setValue": {
2223
+ description: `Set a Switch, Slider, Picker or SegmentedControl: calls its onValueChange (and a Slider's onSlidingComplete) with the value, waits for the render and reads the value back. A Switch takes true or false, a Slider a number within minimumValue and maximumValue (0 and 1 by default) and on its step (onSlidingStart, onValueChange, onSlidingComplete), a Picker one of its Picker.Item values, a SegmentedControl one of its values (a wrong type or value is an error that lists the options). The reply has \`value\` as the app now shows it, and a warning when that is not what was set (a controlled value the app refused). Disabled controls are refused; {force:true} sets anyway. {scroll:true} scrolls or pages the target into view first (pages the largest vertical list; onEndReached may fire). ${TARGET_HELP}`,
2224
+ run: (target, value, options) => screen.setValue(target, value, options)
2225
+ },
1592
2226
  "screen.scroll": {
1593
2227
  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}`,
1594
2228
  run: (arg, options) => screen.scroll(arg, options)
@@ -1598,7 +2232,7 @@ function screenTools() {
1598
2232
  run: (target, options) => screen.refresh(target, options)
1599
2233
  },
1600
2234
  "screen.waitFor": {
1601
- 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. {scroll:true} also accepts a match rendered off screen and scrolls it into view (not with gone). Default timeout 5000 ms; a timeout lists near misses.",
2235
+ 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. {scroll:true} also accepts a match rendered off screen and scrolls it into view, (it does not page lists for rows they have not rendered: wait for the list, then press/fill/setValue with {scroll:true}; not with gone). Default timeout 5000 ms; a timeout lists near misses.",
1602
2236
  run: (target, options) => screen.waitFor(target, options)
1603
2237
  }
1604
2238
  };
@@ -1609,7 +2243,8 @@ function builtinTools(listTools, getTools, logs, getScenarios) {
1609
2243
  return {
1610
2244
  ...bridgeTools(listTools),
1611
2245
  ...appTools(
1612
- selectReload(globalThis, () => import_react_native2.DevSettings.reload())
2246
+ selectReload(globalThis, () => import_react_native2.DevSettings.reload()),
2247
+ getTools
1613
2248
  ),
1614
2249
  ...logTools(logs),
1615
2250
  ...restoreTools(getTools),
@@ -1858,32 +2493,54 @@ function toJson(value) {
1858
2493
  }
1859
2494
 
1860
2495
  // src/runtime/registry.ts
2496
+ var loadSlot = {};
2497
+ function ownsKey(slot, key) {
2498
+ if (typeof key !== "string" || key === "") return false;
2499
+ slot.key ?? (slot.key = key);
2500
+ return slot.key === key;
2501
+ }
1861
2502
  function unwrap(definition) {
1862
2503
  return typeof definition === "function" ? { run: definition } : definition;
1863
2504
  }
1864
- function createRegistry(getTools, logs, loadId2) {
1865
- const list = () => Object.entries(getTools()).map(([name, definition]) => ({
2505
+ function createRegistry(getTools, logs, loadId2, keySlot = loadSlot) {
2506
+ const list2 = () => Object.entries(getTools()).filter(([name]) => !INTERNAL_TOOLS.includes(name)).map(([name, definition]) => ({
1866
2507
  name,
1867
2508
  description: unwrap(definition).description
1868
2509
  })).sort((a, b) => a.name.localeCompare(b.name));
1869
2510
  async function dispatch(call, from) {
1870
- const end = logs?.begin(call.tool);
1871
- const result = await run(call, from).finally(end);
1872
- const errors = logs?.takeErrors();
1873
- const stamped = loadId2 ? { ...result, loadId: loadId2, ...pending() } : result;
2511
+ const quiet = call.tool === "bridge.ping" || call.tool === "bridge.pending";
2512
+ const trusted = ownsKey(keySlot, call.journalKey);
2513
+ const end = quiet ? void 0 : logs?.begin(call.tool);
2514
+ const result = INTERNAL_TOOLS.includes(call.tool) && !trusted ? await refused(call, from) : await run(call, from).finally(end);
2515
+ const errors = quiet ? void 0 : logs?.takeErrors();
2516
+ const stamped = loadId2 ? { ...result, loadId: loadId2, ...pending(), ...journal(call, trusted) } : result;
1874
2517
  return errors?.length ? { ...stamped, logs: errors } : stamped;
1875
2518
  }
1876
2519
  function pending() {
1877
2520
  const areas = Object.keys(pendingByArea(getTools()));
1878
2521
  return areas.length ? { pending: areas } : {};
1879
2522
  }
2523
+ function journal(call, trusted) {
2524
+ const rev = journalRev(getTools());
2525
+ const out = rev ? { journal: rev } : {};
2526
+ if (trusted && call.journalAck !== void 0 && call.journalAck !== `${loadId2}:${rev}`)
2527
+ out.journalData = journalByArea(getTools());
2528
+ return out;
2529
+ }
2530
+ const refused = (call, from) => ({
2531
+ id: call.id,
2532
+ from,
2533
+ ok: false,
2534
+ error: `${call.tool} is for a session's own use`,
2535
+ ms: 0
2536
+ });
1880
2537
  async function run(call, from) {
1881
2538
  const t0 = performance.now();
1882
2539
  const ms = () => Math.round((performance.now() - t0) * 100) / 100;
1883
2540
  try {
1884
2541
  const definition = getTools()[call.tool];
1885
2542
  if (!definition) {
1886
- const known = list().map((t) => t.name).join(", ");
2543
+ const known = list2().map((t) => t.name).join(", ");
1887
2544
  throw new Error(`Unknown tool "${call.tool}". Known: ${known}`);
1888
2545
  }
1889
2546
  const { run: tool, maxArgs } = unwrap(definition);
@@ -1899,7 +2556,7 @@ function createRegistry(getTools, logs, loadId2) {
1899
2556
  return { id: call.id, from, ok: false, error: message2, ms: ms() };
1900
2557
  }
1901
2558
  }
1902
- return { list, dispatch };
2559
+ return { list: list2, dispatch };
1903
2560
  }
1904
2561
 
1905
2562
  // src/runtime/index.ts