react-native-ai-devtools 1.4.18 → 1.4.20

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.
@@ -531,19 +531,184 @@ function formatTreeStructureOnly(node, indent = 0) {
531
531
  }
532
532
  return result;
533
533
  }
534
- function formatScreenLayoutToTonl(elements) {
535
- const lines = ["#elements{component,path,depth,layout,id}"];
536
- for (const el of elements) {
537
- const layout = el.layout
538
- ? Object.entries(el.layout)
539
- .map(([k, v]) => `${k}:${v}`)
540
- .join(";")
541
- : "";
542
- const id = el.identifiers?.testID || el.identifiers?.accessibilityLabel || "";
543
- lines.push(`${el.component}|${el.path}|${el.depth}|${layout}|${id}`);
534
+ /**
535
+ * Classify each root as "screen" or "overlay".
536
+ *
537
+ * A root is a **screen** if its subtree contains a navigation screen marker
538
+ * (Route(...), *Screen, *Page). Falls back to the largest root by area
539
+ * if no navigation markers are found. Everything else is an overlay.
540
+ */
541
+ function classifyRoots(roots, elements, childrenMap) {
542
+ const labels = [];
543
+ // Navigation screen markers: Route(...) wrapper or user-defined *Screen/*Page
544
+ const screenMarker = /^(Route\(|.*Screen\(|.*Page\(|.*Screen$|.*Page$)/;
545
+ function subtreeHasScreen(idx, depth) {
546
+ if (depth > 30)
547
+ return false;
548
+ if (screenMarker.test(elements[idx].component))
549
+ return true;
550
+ const kids = childrenMap.get(idx);
551
+ if (kids) {
552
+ for (const kid of kids) {
553
+ if (subtreeHasScreen(kid, depth + 1))
554
+ return true;
555
+ }
556
+ }
557
+ return false;
558
+ }
559
+ // First pass: check which roots contain a navigation screen
560
+ const hasScreen = roots.map(ri => subtreeHasScreen(ri, 0));
561
+ const anyScreenFound = hasScreen.some(Boolean);
562
+ if (anyScreenFound) {
563
+ for (let i = 0; i < roots.length; i++) {
564
+ labels.push(hasScreen[i] ? "screen" : "overlay");
565
+ }
566
+ }
567
+ else {
568
+ // Fallback: largest root by area is the screen
569
+ let maxArea = 0;
570
+ let maxIdx = 0;
571
+ for (let i = 0; i < roots.length; i++) {
572
+ const f = elements[roots[i]].frame;
573
+ if (f && f.width * f.height > maxArea) {
574
+ maxArea = f.width * f.height;
575
+ maxIdx = i;
576
+ }
577
+ }
578
+ for (let i = 0; i < roots.length; i++) {
579
+ labels.push(i === maxIdx ? "screen" : "overlay");
580
+ }
581
+ }
582
+ return { labels, hasOverlays: labels.some(l => l === "overlay") };
583
+ }
584
+ /**
585
+ * Build, collapse, classify, and render a layout tree.
586
+ *
587
+ * Shared by both get_screen_layout (points, no tap coords) and
588
+ * the screenshot layout enrichment (pixels, with tap coords).
589
+ *
590
+ * @param elements - flat list of layout nodes with parentIndex linkage
591
+ * @param renderLine - callback that produces the output line for a node,
592
+ * given (element, indent level, whether it's a leaf)
593
+ */
594
+ function formatLayoutTree(elements, renderLine) {
595
+ // Build index: originalIndex -> element index in the filtered array
596
+ const indexMap = new Map();
597
+ for (let i = 0; i < elements.length; i++) {
598
+ if (elements[i].originalIndex !== undefined) {
599
+ indexMap.set(elements[i].originalIndex, i);
600
+ }
601
+ }
602
+ // Build children lists
603
+ const children = new Map();
604
+ const roots = [];
605
+ for (let i = 0; i < elements.length; i++) {
606
+ const el = elements[i];
607
+ const parentOrigIdx = el.parentIndex;
608
+ if (parentOrigIdx === undefined || parentOrigIdx === -1 || !indexMap.has(parentOrigIdx)) {
609
+ roots.push(i);
610
+ }
611
+ else {
612
+ const parentIdx = indexMap.get(parentOrigIdx);
613
+ if (!children.has(parentIdx))
614
+ children.set(parentIdx, []);
615
+ children.get(parentIdx).push(i);
616
+ }
617
+ }
618
+ // Collapse wrapper chains: if a child has the same frame as its parent,
619
+ // it's just a wrapper — promote its children and text to the parent
620
+ function sameFrame(a, b) {
621
+ if (!a.frame || !b.frame)
622
+ return false;
623
+ return Math.abs(a.frame.x - b.frame.x) < 1 &&
624
+ Math.abs(a.frame.y - b.frame.y) < 1 &&
625
+ Math.abs(a.frame.width - b.frame.width) < 1 &&
626
+ Math.abs(a.frame.height - b.frame.height) < 1;
627
+ }
628
+ function collapseNode(parentIdx) {
629
+ const kids = children.get(parentIdx);
630
+ if (!kids)
631
+ return;
632
+ const newKids = [];
633
+ for (const kidIdx of kids) {
634
+ if (sameFrame(elements[parentIdx], elements[kidIdx])) {
635
+ if (!elements[parentIdx].text && elements[kidIdx].text) {
636
+ elements[parentIdx].text = elements[kidIdx].text;
637
+ }
638
+ const grandKids = children.get(kidIdx);
639
+ if (grandKids) {
640
+ newKids.push(...grandKids);
641
+ }
642
+ }
643
+ else {
644
+ newKids.push(kidIdx);
645
+ }
646
+ }
647
+ if (newKids.length > 0) {
648
+ children.set(parentIdx, newKids);
649
+ }
650
+ else {
651
+ children.delete(parentIdx);
652
+ }
653
+ const updatedKids = children.get(parentIdx);
654
+ if (updatedKids) {
655
+ for (const kid of updatedKids) {
656
+ collapseNode(kid);
657
+ }
658
+ }
659
+ }
660
+ for (const root of roots) {
661
+ collapseNode(root);
662
+ }
663
+ // Render tree
664
+ const lines = [];
665
+ function printNode(idx, indent) {
666
+ const isLeaf = !children.has(idx);
667
+ lines.push(renderLine(elements[idx], indent, isLeaf));
668
+ const kids = children.get(idx);
669
+ if (kids) {
670
+ for (const kid of kids) {
671
+ printNode(kid, indent + 1);
672
+ }
673
+ }
674
+ }
675
+ // Classify roots and emit layer headers
676
+ const { labels, hasOverlays } = classifyRoots(roots, elements, children);
677
+ if (hasOverlays) {
678
+ let prevLabel = "";
679
+ for (let ri = 0; ri < roots.length; ri++) {
680
+ const label = labels[ri];
681
+ if (label === "overlay" || label !== prevLabel) {
682
+ if (lines.length > 0)
683
+ lines.push("");
684
+ lines.push(`[${label}]`);
685
+ }
686
+ prevLabel = label;
687
+ printNode(roots[ri], 0);
688
+ }
689
+ }
690
+ else {
691
+ for (const root of roots) {
692
+ printNode(root, 0);
693
+ }
544
694
  }
545
695
  return lines.join("\n");
546
696
  }
697
+ function formatScreenLayoutTree(elements, extended = false) {
698
+ return formatLayoutTree(elements, (el, indent, isLeaf) => {
699
+ const prefix = " ".repeat(indent);
700
+ const frame = el.frame
701
+ ? ` (${Math.round(el.frame.x)},${Math.round(el.frame.y)} ${Math.round(el.frame.width)}x${Math.round(el.frame.height)})`
702
+ : "";
703
+ const id = el.identifiers?.testID || el.identifiers?.accessibilityLabel || "";
704
+ const idStr = id ? ` [${id}]` : "";
705
+ const textStr = el.text && isLeaf ? ` "${el.text}"` : "";
706
+ const layoutStr = extended && el.layout
707
+ ? ` {${Object.entries(el.layout).map(([k, v]) => `${k}:${v}`).join("; ")}}`
708
+ : "";
709
+ return `${prefix}${el.component}${frame}${idStr}${textStr}${layoutStr}`;
710
+ });
711
+ }
547
712
  function formatFoundComponentsToTonl(components) {
548
713
  const lines = ["#found{component,path,depth,key,layout}"];
549
714
  for (const c of components) {
@@ -572,7 +737,7 @@ export async function getComponentTree(options = {}) {
572
737
  // Use lower default depth for structureOnly to keep output compact (~2-5KB)
573
738
  // Full mode uses higher depth since TONL format handles it better
574
739
  // focusedOnly mode uses moderate depth since we're already filtering to active screen
575
- const maxDepth = options.maxDepth ?? (structureOnly ? (focusedOnly ? 25 : 40) : 100);
740
+ const maxDepth = options.maxDepth ?? 5000;
576
741
  const expression = `
577
742
  (function() {
578
743
  const hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
@@ -717,7 +882,7 @@ export async function getComponentTree(options = {}) {
717
882
 
718
883
  // Find focused screen if requested
719
884
  function findFocusedScreen(fiber, depth = 0) {
720
- if (!fiber || depth > 200) return null;
885
+ if (!fiber || depth > 5000) return null;
721
886
 
722
887
  const name = getComponentName(fiber);
723
888
 
@@ -793,33 +958,57 @@ export async function getComponentTree(options = {}) {
793
958
  return result;
794
959
  }
795
960
  /**
796
- * Get layout styles for all components on the current screen.
797
- * Useful for verifying layout without screenshots.
961
+ * Get layout data for visible components on the current screen.
962
+ * Uses measureInWindow to get actual screen positions and filters
963
+ * to only components within the viewport.
964
+ *
965
+ * Two-step approach (same as inspectAtPoint):
966
+ * Step 1: Walk fiber tree, dispatch measureInWindow on host components
967
+ * Step 2: After 300ms, read measurements, filter by viewport, build results
798
968
  */
799
969
  export async function getScreenLayout(options = {}) {
800
- const { maxDepth = 65, componentsOnly = false, shortPath = true, summary = false, format = "tonl", device } = options;
801
- const expression = `
970
+ const { extended = false, summary = false, device, raw = false } = options;
971
+ const maxDepth = 5000;
972
+ const componentsOnly = true;
973
+ const shortPath = true;
974
+ // --- Step 1: walk fiber tree + dispatch measureInWindow calls ---
975
+ const dispatchExpression = `
802
976
  (function() {
803
- const hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
977
+ var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
804
978
  if (!hook) return { error: 'React DevTools hook not found.' };
805
979
 
806
- let roots = [];
980
+ var roots = [];
807
981
  if (hook.getFiberRoots) {
808
- roots = [...(hook.getFiberRoots(1) || [])];
982
+ roots = Array.from(hook.getFiberRoots(1) || []);
809
983
  }
810
984
  if (roots.length === 0 && hook.renderers) {
811
- for (const [id] of hook.renderers) {
812
- const r = hook.getFiberRoots ? [...(hook.getFiberRoots(id) || [])] : [];
985
+ for (var entry of hook.renderers) {
986
+ var r = Array.from(hook.getFiberRoots ? (hook.getFiberRoots(entry[0]) || []) : []);
813
987
  if (r.length > 0) { roots = r; break; }
814
988
  }
815
989
  }
816
990
  if (roots.length === 0) return { error: 'No fiber roots found.' };
817
991
 
818
- const maxDepth = ${maxDepth};
819
- const componentsOnly = ${componentsOnly};
820
- const shortPath = ${shortPath};
821
- const summaryMode = ${summary};
822
- const pathSegments = 3; // Number of path segments to show in shortPath mode
992
+ function getMeasurable(fiber) {
993
+ var sn = fiber.stateNode;
994
+ if (!sn) return null;
995
+ if (typeof sn.measureInWindow === 'function') return sn;
996
+ if (sn.canonical && sn.canonical.publicInstance &&
997
+ typeof sn.canonical.publicInstance.measureInWindow === 'function') {
998
+ return sn.canonical.publicInstance;
999
+ }
1000
+ return null;
1001
+ }
1002
+
1003
+ // Collect host fibers with their metadata
1004
+ var hostFibers = [];
1005
+ var fiberMeta = [];
1006
+ var componentsOnlyMode = ${componentsOnly};
1007
+
1008
+ // RN internals and primitives — skip these when looking for meaningful component names
1009
+ // Only filter internal components that can't be written as JSX.
1010
+ // Keep all user-facing components (View, Text, ScrollView, Modal, etc.)
1011
+ var RN_PRIMITIVES = /^(Animated\\(.*|withAnimated.*|AnimatedComponent.*|ForwardRef.*|memo\\(.*|Context\\.Consumer|Context\\.Provider|RCT.*|RNS.*|RNC.*|ViewManagerAdapter_.*|VirtualizedList.*|CellRenderer.*|FrameSizeProvider.*|MaybeScreenContainer|MaybeScreen|Navigation.*|Screen$|ScreenStack|ScreenContainer|ScreenContentWrapper|SceneView|DelayedFreeze|Freeze|Suspender|DebugContainer|StaticContainer|SafeAreaProvider.*|SafeAreaFrameContext|SafeAreaInsetsContext|ExpoRoot|ExpoRootComponent|GestureHandler.*|NativeViewGestureHandler|GestureDetector|PanGestureHandler|Reanimated.*|BottomTabNavigator|TabLayout|RouteNode|Route$|KeyboardProvider|PortalProviderComponent|BottomSheetModalProviderWrapper|ThemeContext|ThemeProvider|TextAncestorContext|PressabilityDebugView|TouchableHighlightImpl|StatusBarOverlay|BottomSheetHostingContainerComponent|BottomSheetGestureHandlersProvider|BottomSheetBackdropContainerComponent|BottomSheetContainerComponent|BottomSheetDraggableViewComponent|BottomSheetHandleContainerComponent|BottomSheetBackgroundContainerComponent|DebuggingOverlay|InspectorDeferred|Inspector|InspectorOverlay|InspectorPanel|StyleInspector|BoxInspector|BoxContainer|ElementBox|BorderBox|InspectorPanelButton)$/;
823
1012
 
824
1013
  function getComponentName(fiber) {
825
1014
  if (!fiber || !fiber.type) return null;
@@ -827,8 +1016,212 @@ export async function getScreenLayout(options = {}) {
827
1016
  return fiber.type.displayName || fiber.type.name || null;
828
1017
  }
829
1018
 
830
- function isHostComponent(fiber) {
831
- return typeof fiber?.type === 'string';
1019
+ // Find the first measurable host descendant of a fiber
1020
+ function findFirstHost(fiber, depth) {
1021
+ if (!fiber || depth > 20) return null;
1022
+ if (typeof fiber.type === 'string' && getMeasurable(fiber)) return fiber;
1023
+ var child = fiber.child;
1024
+ while (child) {
1025
+ var found = findFirstHost(child, depth + 1);
1026
+ if (found) return found;
1027
+ child = child.sibling;
1028
+ }
1029
+ return null;
1030
+ }
1031
+
1032
+ // Extract text content from a fiber subtree.
1033
+ // When a fiber has a string child, return it without recursing
1034
+ // (avoids duplication from Text > RCTText having the same string).
1035
+ function collectText(fiber, d) {
1036
+ if (!fiber || d > 30) return '';
1037
+ var props = fiber.memoizedProps;
1038
+ if (props) {
1039
+ var ch = props.children;
1040
+ // Leaf text — return without recursing into children fibers
1041
+ if (typeof ch === 'string') return ch;
1042
+ if (typeof ch === 'number') return String(ch);
1043
+ if (Array.isArray(ch)) {
1044
+ var inline = [];
1045
+ for (var ci = 0; ci < ch.length; ci++) {
1046
+ if (typeof ch[ci] === 'string') inline.push(ch[ci]);
1047
+ else if (typeof ch[ci] === 'number') inline.push(String(ch[ci]));
1048
+ }
1049
+ if (inline.length > 0) return inline.join('');
1050
+ }
1051
+ }
1052
+ // No direct text — collect from child fibers (siblings = adjacent elements)
1053
+ var parts = [];
1054
+ var child = fiber.child;
1055
+ while (child) {
1056
+ var t = collectText(child, d + 1);
1057
+ if (t) parts.push(t);
1058
+ child = child.sibling;
1059
+ }
1060
+ return parts.join(' ').trim();
1061
+ }
1062
+
1063
+ if (componentsOnlyMode) {
1064
+ // componentsOnly: walk tree looking for meaningful custom components,
1065
+ // measure their first host child, track parent for tree output
1066
+ function walkComponents(fiber, depth, path, parentIdx) {
1067
+ if (!fiber || depth > ${maxDepth}) return;
1068
+ var name = getComponentName(fiber);
1069
+ var isHost = typeof fiber.type === 'string';
1070
+
1071
+ // Skip inactive screens (react-native-screens MaybeScreen with active=0)
1072
+ // active: 0 = inactive/detached, 1 = transitioning, 2 = active
1073
+ if (name === 'MaybeScreen' && fiber.memoizedProps && fiber.memoizedProps.active === 0) return;
1074
+
1075
+ // Skip unfocused screens in NativeStackNavigator (SceneView with focused=false)
1076
+ if (name === 'SceneView' && fiber.memoizedProps && fiber.memoizedProps.focused === false) return;
1077
+
1078
+ var isMeaningful = name && !isHost && !RN_PRIMITIVES.test(name);
1079
+
1080
+ var myIdx = parentIdx;
1081
+ if (isMeaningful) {
1082
+ var host = findFirstHost(fiber, 0);
1083
+ if (host) {
1084
+ myIdx = hostFibers.length;
1085
+ // Extract text from this component's subtree
1086
+ var text = collectText(fiber, 0);
1087
+ hostFibers.push(host);
1088
+ fiberMeta.push({
1089
+ hostName: typeof host.type === 'string' ? host.type : '',
1090
+ customName: name,
1091
+ depth: depth,
1092
+ path: path.concat([name]),
1093
+ parentIndex: parentIdx,
1094
+ text: text ? text.slice(0, 80) : null
1095
+ });
1096
+ }
1097
+ }
1098
+
1099
+ var child = fiber.child;
1100
+ while (child) {
1101
+ var childName = getComponentName(child);
1102
+ walkComponents(child, depth + 1, childName ? path.concat([childName]) : path, myIdx);
1103
+ child = child.sibling;
1104
+ }
1105
+ }
1106
+ walkComponents(roots[0].current, 0, [], -1);
1107
+ } else {
1108
+ // Default mode: collect all host fibers with ancestor info
1109
+ function walkFibers(fiber, depth, path) {
1110
+ if (!fiber || depth > ${maxDepth}) return;
1111
+ var name = getComponentName(fiber);
1112
+ var isHost = typeof fiber.type === 'string';
1113
+
1114
+ // Skip inactive screens (react-native-screens MaybeScreen with active=0)
1115
+ if (name === 'MaybeScreen' && fiber.memoizedProps && fiber.memoizedProps.active === 0) return;
1116
+
1117
+ // Skip unfocused screens in NativeStackNavigator (SceneView with focused=false)
1118
+ if (name === 'SceneView' && fiber.memoizedProps && fiber.memoizedProps.focused === false) return;
1119
+
1120
+ if (name && isHost && getMeasurable(fiber)) {
1121
+ // Find nearest meaningful custom component ancestor for display
1122
+ var customName = null;
1123
+ var fallbackName = null;
1124
+ var cur = fiber.return;
1125
+ while (cur) {
1126
+ if (cur.type && typeof cur.type !== 'string') {
1127
+ var cName = cur.type.displayName || cur.type.name || null;
1128
+ if (cName) {
1129
+ if (!fallbackName) fallbackName = cName;
1130
+ if (!RN_PRIMITIVES.test(cName)) {
1131
+ customName = cName;
1132
+ break;
1133
+ }
1134
+ }
1135
+ }
1136
+ cur = cur.return;
1137
+ }
1138
+ if (!customName) customName = fallbackName;
1139
+
1140
+ hostFibers.push(fiber);
1141
+ fiberMeta.push({
1142
+ hostName: name,
1143
+ customName: customName,
1144
+ depth: depth,
1145
+ path: path.slice()
1146
+ });
1147
+ }
1148
+
1149
+ var child = fiber.child;
1150
+ while (child) {
1151
+ var childName = getComponentName(child);
1152
+ walkFibers(child, depth + 1, childName ? path.concat([childName]) : path);
1153
+ child = child.sibling;
1154
+ }
1155
+ }
1156
+ walkFibers(roots[0].current, 0, []);
1157
+ }
1158
+
1159
+ if (hostFibers.length === 0) return { error: 'No measurable host components found.' };
1160
+
1161
+ // Store fibers and metadata globally for step 2
1162
+ globalThis.__layoutFibers = hostFibers;
1163
+ globalThis.__layoutMeta = fiberMeta;
1164
+ globalThis.__layoutMeasurements = new Array(hostFibers.length).fill(null);
1165
+
1166
+ // Dispatch measureInWindow on all host fibers
1167
+ for (var i = 0; i < hostFibers.length; i++) {
1168
+ try {
1169
+ (function(idx) {
1170
+ getMeasurable(hostFibers[idx]).measureInWindow(function(fx, fy, fw, fh) {
1171
+ globalThis.__layoutMeasurements[idx] = { x: fx, y: fy, width: fw, height: fh };
1172
+ });
1173
+ })(i);
1174
+ } catch(e) {}
1175
+ }
1176
+
1177
+ return { count: hostFibers.length };
1178
+ })()
1179
+ `;
1180
+ const dispatchResult = await executeInApp(dispatchExpression, false, { timeoutMs: 30000 }, device);
1181
+ if (!dispatchResult.success)
1182
+ return dispatchResult;
1183
+ try {
1184
+ const parsed = JSON.parse(dispatchResult.result || "{}");
1185
+ if (parsed.error)
1186
+ return { success: false, error: parsed.error };
1187
+ }
1188
+ catch {
1189
+ /* ignore */
1190
+ }
1191
+ // Wait for measureInWindow callbacks
1192
+ await delay(300);
1193
+ // --- Step 2: read measurements, filter visible, build results ---
1194
+ const resolveExpression = `
1195
+ (function() {
1196
+ var fibers = globalThis.__layoutFibers;
1197
+ var meta = globalThis.__layoutMeta;
1198
+ var measurements = globalThis.__layoutMeasurements;
1199
+ globalThis.__layoutFibers = null;
1200
+ globalThis.__layoutMeta = null;
1201
+ globalThis.__layoutMeasurements = null;
1202
+
1203
+ if (!fibers || !measurements || !meta) {
1204
+ return { error: 'No measurement data. Run get_screen_layout again.' };
1205
+ }
1206
+
1207
+ var componentsOnly = ${componentsOnly};
1208
+ var shortPath = ${shortPath};
1209
+ var summaryMode = ${summary};
1210
+ var pathSegments = 3;
1211
+
1212
+
1213
+ // Get viewport dimensions from the first root view measurement
1214
+ // Accept elements starting at x=0 even with negative y (safe area extensions)
1215
+ var viewportW = 9999, viewportH = 9999;
1216
+ for (var v = 0; v < measurements.length; v++) {
1217
+ if (measurements[v] && measurements[v].x === 0 && measurements[v].y <= 0 &&
1218
+ measurements[v].width > 0 && measurements[v].height > 0) {
1219
+ viewportW = measurements[v].width;
1220
+ // For wrappers extending behind safe area, the visible viewport height
1221
+ // is the total height minus the negative offset
1222
+ viewportH = measurements[v].height + measurements[v].y;
1223
+ break;
1224
+ }
832
1225
  }
833
1226
 
834
1227
  function formatPath(pathArray) {
@@ -838,22 +1231,14 @@ export async function getScreenLayout(options = {}) {
838
1231
  return '... > ' + pathArray.slice(-pathSegments).join(' > ');
839
1232
  }
840
1233
 
841
- function extractAllStyles(style) {
842
- if (!style) return null;
843
- const merged = Array.isArray(style)
844
- ? Object.assign({}, ...style.filter(Boolean).map(s => typeof s === 'object' ? s : {}))
845
- : (typeof style === 'object' ? style : {});
846
- return Object.keys(merged).length > 0 ? merged : null;
847
- }
848
-
849
1234
  function extractLayoutStyles(style) {
850
1235
  if (!style) return null;
851
- const merged = Array.isArray(style)
852
- ? Object.assign({}, ...style.filter(Boolean).map(s => typeof s === 'object' ? s : {}))
1236
+ var merged = Array.isArray(style)
1237
+ ? Object.assign.apply(null, [{}].concat(style.filter(Boolean).map(function(s) { return typeof s === 'object' ? s : {}; })))
853
1238
  : (typeof style === 'object' ? style : {});
854
1239
 
855
- const layout = {};
856
- const layoutKeys = [
1240
+ var layout = {};
1241
+ var layoutKeys = [
857
1242
  'padding', 'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight',
858
1243
  'paddingHorizontal', 'paddingVertical',
859
1244
  'margin', 'marginTop', 'marginBottom', 'marginLeft', 'marginRight',
@@ -864,79 +1249,109 @@ export async function getScreenLayout(options = {}) {
864
1249
  'position', 'top', 'bottom', 'left', 'right',
865
1250
  'gap', 'rowGap', 'columnGap',
866
1251
  'borderWidth', 'borderTopWidth', 'borderBottomWidth', 'borderLeftWidth', 'borderRightWidth',
867
- 'backgroundColor', 'borderColor', 'borderRadius'
1252
+ 'backgroundColor', 'borderColor', 'borderRadius',
1253
+ 'zIndex', 'elevation'
868
1254
  ];
869
1255
 
870
- for (const key of layoutKeys) {
871
- if (merged[key] !== undefined) layout[key] = merged[key];
1256
+ for (var k = 0; k < layoutKeys.length; k++) {
1257
+ if (merged[layoutKeys[k]] !== undefined) layout[layoutKeys[k]] = merged[layoutKeys[k]];
872
1258
  }
873
1259
  return Object.keys(layout).length > 0 ? layout : null;
874
1260
  }
875
1261
 
876
- const elements = [];
1262
+ var elements = [];
1263
+
1264
+ for (var i = 0; i < measurements.length; i++) {
1265
+ var m = measurements[i];
1266
+ if (!m) continue;
877
1267
 
878
- function walkFiber(fiber, depth, path) {
879
- if (!fiber || depth > maxDepth) return;
1268
+ // Filter: only visible within viewport (with some tolerance for partial visibility)
1269
+ if (m.width <= 0 || m.height <= 0) continue;
1270
+ if (m.x + m.width < 0 || m.y + m.height < 0) continue;
1271
+ if (m.x > viewportW || m.y > viewportH) continue;
880
1272
 
881
- const name = getComponentName(fiber);
882
- const isHost = isHostComponent(fiber);
883
-
884
- // Include host components (View, Text, etc.) or named components
885
- if (name && (!componentsOnly || !isHost)) {
886
- const style = fiber.memoizedProps?.style;
887
- const layout = extractLayoutStyles(style);
888
-
889
- // Get text content if it's a Text component
890
- let textContent = null;
891
- if (name === 'Text' || name === 'RCTText') {
892
- const children = fiber.memoizedProps?.children;
893
- if (typeof children === 'string') textContent = children;
894
- else if (typeof children === 'number') textContent = String(children);
895
- }
1273
+ var fiber = fibers[i];
1274
+ var info = meta[i];
896
1275
 
897
- const element = {
898
- component: name,
899
- path: formatPath(path),
900
- depth
901
- };
1276
+ var displayName = componentsOnly ? info.customName : info.hostName;
1277
+ if (!displayName) continue;
1278
+ if (componentsOnly && !info.customName) continue;
902
1279
 
903
- if (layout) element.layout = layout;
904
- if (textContent) element.text = textContent.slice(0, 100);
905
-
906
- // Include key props for identification
907
- if (fiber.memoizedProps) {
908
- const identifiers = {};
909
- if (fiber.memoizedProps.testID) identifiers.testID = fiber.memoizedProps.testID;
910
- if (fiber.memoizedProps.accessibilityLabel) identifiers.accessibilityLabel = fiber.memoizedProps.accessibilityLabel;
911
- if (fiber.memoizedProps.nativeID) identifiers.nativeID = fiber.memoizedProps.nativeID;
912
- if (fiber.key) identifiers.key = fiber.key;
913
- if (Object.keys(identifiers).length > 0) element.identifiers = identifiers;
914
- }
1280
+ var style = fiber.memoizedProps ? fiber.memoizedProps.style : null;
1281
+ var layout = extractLayoutStyles(style);
915
1282
 
916
- elements.push(element);
1283
+ // Get text content — use pre-collected text from step 1, or fall back to host fiber
1284
+ var textContent = info.text || null;
1285
+ if (!textContent && (info.hostName === 'RCTText' || info.hostName === 'Text')) {
1286
+ var children = fiber.memoizedProps ? fiber.memoizedProps.children : null;
1287
+ if (typeof children === 'string') textContent = children;
1288
+ else if (typeof children === 'number') textContent = String(children);
917
1289
  }
918
1290
 
919
- // Traverse children
920
- let child = fiber.child;
921
- while (child) {
922
- const childName = getComponentName(child);
923
- walkFiber(child, depth + 1, childName ? [...path, childName] : path);
924
- child = child.sibling;
1291
+ var element = {
1292
+ component: displayName,
1293
+ path: formatPath(info.path),
1294
+ depth: info.depth,
1295
+ frame: { x: m.x, y: m.y, width: m.width, height: m.height },
1296
+ originalIndex: i
1297
+ };
1298
+
1299
+ if (info.parentIndex !== undefined) element.parentIndex = info.parentIndex;
1300
+ if (layout) element.layout = layout;
1301
+ if (textContent) element.text = textContent.slice(0, 100);
1302
+
1303
+ // Identifiers
1304
+ if (fiber.memoizedProps) {
1305
+ var identifiers = {};
1306
+ if (fiber.memoizedProps.testID) identifiers.testID = fiber.memoizedProps.testID;
1307
+ if (fiber.memoizedProps.accessibilityLabel) identifiers.accessibilityLabel = fiber.memoizedProps.accessibilityLabel;
1308
+ if (fiber.memoizedProps.nativeID) identifiers.nativeID = fiber.memoizedProps.nativeID;
1309
+ if (fiber.key) identifiers.key = fiber.key;
1310
+ if (Object.keys(identifiers).length > 0) element.identifiers = identifiers;
1311
+ }
1312
+
1313
+ elements.push(element);
1314
+ }
1315
+
1316
+ // In componentsOnly mode, remove full-screen wrapper components
1317
+ // and re-parent their children to collapse the wrapper chain
1318
+ if (componentsOnly && elements.length > 0) {
1319
+ var filtered = [];
1320
+ // Map: originalIndex -> resolved parent for skipped wrappers
1321
+ var reparent = {};
1322
+
1323
+ for (var fi = 0; fi < elements.length; fi++) {
1324
+ var el = elements[fi];
1325
+ var fr = el.frame;
1326
+ // A wrapper is full-screen if it covers the entire viewport or extends beyond it
1327
+ // (e.g., y=-119 wrappers that extend behind the safe area)
1328
+ var isFullScreen = fr && fr.x <= 0 && fr.y <= 0 &&
1329
+ (fr.width >= viewportW - 2) && (fr.y + fr.height >= viewportH - 2);
1330
+
1331
+ if (isFullScreen) {
1332
+ // Skip this wrapper, map its originalIndex to its parent
1333
+ reparent[el.originalIndex] = el.parentIndex;
1334
+ } else {
1335
+ // Resolve parent through any skipped wrappers
1336
+ var resolvedParent = el.parentIndex;
1337
+ while (resolvedParent !== undefined && resolvedParent !== -1 && reparent[resolvedParent] !== undefined) {
1338
+ resolvedParent = reparent[resolvedParent];
1339
+ }
1340
+ el.parentIndex = resolvedParent;
1341
+ filtered.push(el);
1342
+ }
925
1343
  }
1344
+ elements = filtered;
926
1345
  }
927
1346
 
928
- walkFiber(roots[0].current, 0, []);
929
-
930
- // Summary mode: return counts by component name
931
1347
  if (summaryMode) {
932
- const counts = {};
933
- for (const el of elements) {
934
- counts[el.component] = (counts[el.component] || 0) + 1;
1348
+ var counts = {};
1349
+ for (var j = 0; j < elements.length; j++) {
1350
+ counts[elements[j].component] = (counts[elements[j].component] || 0) + 1;
935
1351
  }
936
- // Sort by count descending
937
- const sorted = Object.entries(counts)
938
- .sort((a, b) => b[1] - a[1])
939
- .map(([name, count]) => ({ component: name, count }));
1352
+ var sorted = Object.keys(counts).map(function(name) {
1353
+ return { component: name, count: counts[name] };
1354
+ }).sort(function(a, b) { return b.count - a.count; });
940
1355
  return {
941
1356
  totalElements: elements.length,
942
1357
  uniqueComponents: sorted.length,
@@ -945,15 +1360,15 @@ export async function getScreenLayout(options = {}) {
945
1360
  }
946
1361
 
947
1362
  return {
1363
+ viewport: { width: viewportW, height: viewportH },
948
1364
  totalElements: elements.length,
949
1365
  elements: elements
950
1366
  };
951
1367
  })()
952
1368
  `;
953
- // Use a longer timeout for layout traversal — large component trees can exceed 10s
954
- const result = await executeInApp(expression, false, { timeoutMs: 30000 }, device);
955
- // Apply TONL formatting if requested
956
- if (format === "tonl" && result.success && result.result) {
1369
+ const result = await executeInApp(resolveExpression, false, { timeoutMs: 30000 }, device);
1370
+ // Format output as tree
1371
+ if (result.success && result.result) {
957
1372
  try {
958
1373
  const parsed = JSON.parse(result.result);
959
1374
  if (parsed.components) {
@@ -962,9 +1377,16 @@ export async function getScreenLayout(options = {}) {
962
1377
  return { success: true, result: tonl };
963
1378
  }
964
1379
  else if (parsed.elements) {
965
- // Full element list
966
- const tonl = formatScreenLayoutToTonl(parsed.elements);
967
- return { success: true, result: tonl };
1380
+ if (raw) {
1381
+ return {
1382
+ success: true,
1383
+ result: result.result,
1384
+ parsedElements: parsed.elements,
1385
+ viewport: parsed.viewport
1386
+ };
1387
+ }
1388
+ const tree = formatScreenLayoutTree(parsed.elements, extended);
1389
+ return { success: true, result: tree };
968
1390
  }
969
1391
  }
970
1392
  catch {
@@ -973,6 +1395,71 @@ export async function getScreenLayout(options = {}) {
973
1395
  }
974
1396
  return result;
975
1397
  }
1398
+ /**
1399
+ * Format enriched elements as an indented tree with tap coordinates in pixels.
1400
+ * Same tree structure as get_screen_layout but with tap:(x,y) per node.
1401
+ */
1402
+ function formatEnrichedLayoutTree(elements) {
1403
+ return formatLayoutTree(elements, (el, indent, isLeaf) => {
1404
+ const prefix = " ".repeat(indent);
1405
+ const frame = `(${Math.round(el.frame.x)},${Math.round(el.frame.y)} ${Math.round(el.frame.width)}x${Math.round(el.frame.height)})`;
1406
+ const tap = ` tap:(${el.tapX},${el.tapY})`;
1407
+ const id = el.identifiers?.testID || el.identifiers?.accessibilityLabel || "";
1408
+ const idStr = id ? ` [${id}]` : "";
1409
+ const textStr = el.text && isLeaf ? ` "${el.text}"` : "";
1410
+ return `${prefix}${el.component} ${frame}${tap}${idStr}${textStr}`;
1411
+ });
1412
+ }
1413
+ /**
1414
+ * Enrich screen layout data with tap-ready pixel coordinates for bundling with screenshots.
1415
+ * Converts points/dp frame coordinates to pixels using the device pixel ratio,
1416
+ * computes center-point tapX/tapY for each element.
1417
+ *
1418
+ * @param pixelRatio - device pixel ratio (e.g., 3 for @3x iPhone)
1419
+ * @param screenshotScaleFactor - if the screenshot image was scaled down, this factor adjusts coordinates
1420
+ * @param device - optional target device name
1421
+ * @returns formatted tree string with pixel coordinates, or null if unavailable
1422
+ */
1423
+ export async function enrichScreenshotWithLayout(pixelRatio, screenshotScaleFactor, device) {
1424
+ try {
1425
+ const result = await getScreenLayout({ extended: false, summary: false, device, raw: true });
1426
+ if (!result.success || !result.parsedElements || result.parsedElements.length === 0)
1427
+ return null;
1428
+ const elements = result.parsedElements.map((el) => {
1429
+ const frame = el.frame || { x: 0, y: 0, width: 0, height: 0 };
1430
+ // Convert center point from points/dp to screenshot pixels
1431
+ // Points -> device pixels: multiply by pixelRatio
1432
+ // Device pixels -> screenshot pixels: divide by screenshotScaleFactor (if image was resized)
1433
+ const centerXPoints = frame.x + frame.width / 2;
1434
+ const centerYPoints = frame.y + frame.height / 2;
1435
+ const tapX = Math.round((centerXPoints * pixelRatio) / screenshotScaleFactor);
1436
+ const tapY = Math.round((centerYPoints * pixelRatio) / screenshotScaleFactor);
1437
+ // Convert frame to pixels too
1438
+ const pixelFrame = {
1439
+ x: Math.round((frame.x * pixelRatio) / screenshotScaleFactor),
1440
+ y: Math.round((frame.y * pixelRatio) / screenshotScaleFactor),
1441
+ width: Math.round((frame.width * pixelRatio) / screenshotScaleFactor),
1442
+ height: Math.round((frame.height * pixelRatio) / screenshotScaleFactor),
1443
+ };
1444
+ return {
1445
+ component: el.component,
1446
+ frame: pixelFrame,
1447
+ tapX,
1448
+ tapY,
1449
+ text: el.text,
1450
+ identifiers: el.identifiers,
1451
+ parentIndex: el.parentIndex,
1452
+ originalIndex: el.originalIndex,
1453
+ depth: el.depth,
1454
+ path: el.path,
1455
+ };
1456
+ });
1457
+ return formatEnrichedLayoutTree(elements);
1458
+ }
1459
+ catch {
1460
+ return null; // Non-fatal: screenshot works without layout
1461
+ }
1462
+ }
976
1463
  /**
977
1464
  * Inspect a specific component by name, returning its props, state, and layout.
978
1465
  */
@@ -1280,8 +1767,6 @@ export async function findComponents(pattern, options = {}) {
1280
1767
  child = child.sibling;
1281
1768
  }
1282
1769
  } catch(e) {
1283
- // Skip nodes that throw (e.g. Reanimated animated components)
1284
- // Still try to traverse children if possible
1285
1770
  try {
1286
1771
  var child = fiber.child;
1287
1772
  while (child && results.length < maxResults) {
@@ -1294,7 +1779,6 @@ export async function findComponents(pattern, options = {}) {
1294
1779
 
1295
1780
  search(roots[0].current, [], 0);
1296
1781
 
1297
- // Summary mode: just return counts by component name
1298
1782
  if (summaryMode) {
1299
1783
  const counts = {};
1300
1784
  for (const r of results) {
@@ -1319,18 +1803,15 @@ export async function findComponents(pattern, options = {}) {
1319
1803
  })()
1320
1804
  `;
1321
1805
  const result = await executeInApp(expression, false, {}, device);
1322
- // Apply TONL formatting if requested
1323
1806
  if (format === "tonl" && result.success && result.result) {
1324
1807
  try {
1325
1808
  const parsed = JSON.parse(result.result);
1326
1809
  if (parsed.components) {
1327
1810
  if (parsed.totalMatches !== undefined) {
1328
- // Summary mode
1329
1811
  const tonl = formatSummaryToTonl(parsed.components, parsed.totalMatches);
1330
1812
  return { success: true, result: `pattern: ${parsed.pattern}\n${tonl}` };
1331
1813
  }
1332
1814
  else {
1333
- // Full list mode
1334
1815
  const tonl = formatFoundComponentsToTonl(parsed.components);
1335
1816
  return { success: true, result: `pattern: ${parsed.pattern}\nfound: ${parsed.found}\n${tonl}` };
1336
1817
  }
@@ -1387,7 +1868,7 @@ export async function pressElement(options) {
1387
1868
  }
1388
1869
 
1389
1870
  function extractText(fiber, depth) {
1390
- if (!fiber || depth > 15) return '';
1871
+ if (!fiber || depth > 5000) return '';
1391
1872
  var parts = [];
1392
1873
  var props = fiber.memoizedProps;
1393
1874
  if (props) {
@@ -1656,7 +2137,7 @@ export async function pressElement(options) {
1656
2137
  var hostFiber = null;
1657
2138
  var fallbackHost = null;
1658
2139
  function findHostChild(fiber, depth) {
1659
- if (!fiber || depth > 15 || hostFiber) return;
2140
+ if (!fiber || depth > 5000 || hostFiber) return;
1660
2141
  if (fiber.tag === 5 && fiber.stateNode) {
1661
2142
  var hostType = typeof fiber.type === 'string' ? fiber.type : '';
1662
2143
  if (target.isInput && (hostType.indexOf('TextInput') !== -1 || hostType.indexOf('textinput') !== -1)) {
@@ -1806,7 +2287,7 @@ export async function isInspectorActive(device) {
1806
2287
  if (roots.length === 0) return false;
1807
2288
 
1808
2289
  function findComponent(fiber, targetName, depth = 0) {
1809
- if (!fiber || depth > 100) return null;
2290
+ if (!fiber || depth > 5000) return null;
1810
2291
  const name = fiber.type?.displayName || fiber.type?.name;
1811
2292
  if (name === targetName) return fiber;
1812
2293
  let child = fiber.child;
@@ -1848,28 +2329,29 @@ export async function getInspectorSelection(device) {
1848
2329
  }
1849
2330
  if (roots.length === 0) return { error: 'No fiber roots found.' };
1850
2331
 
1851
- // Find InspectorPanel component
1852
- function findComponent(fiber, targetName, depth = 0) {
1853
- if (!fiber || depth > 100) return null;
2332
+ // Find all InspectorPanel instances (apps with modals may have multiple)
2333
+ function findAllPanels(fiber, targetName, depth, results) {
2334
+ if (!fiber || depth > 5000) return;
1854
2335
  const name = fiber.type?.displayName || fiber.type?.name;
1855
- if (name === targetName) return fiber;
2336
+ if (name === targetName) results.push(fiber);
1856
2337
  let child = fiber.child;
1857
2338
  while (child) {
1858
- const found = findComponent(child, targetName, depth + 1);
1859
- if (found) return found;
2339
+ findAllPanels(child, targetName, depth + 1, results);
1860
2340
  child = child.sibling;
1861
2341
  }
1862
- return null;
1863
2342
  }
1864
2343
 
1865
- const panelFiber = findComponent(roots[0].current, 'InspectorPanel');
1866
- if (!panelFiber) {
2344
+ const panels = [];
2345
+ findAllPanels(roots[0].current, 'InspectorPanel', 0, panels);
2346
+ if (panels.length === 0) {
1867
2347
  return {
1868
2348
  error: 'Element Inspector is not active.',
1869
2349
  hint: 'Use toggle_element_inspector to enable the inspector, then tap an element to select it.'
1870
2350
  };
1871
2351
  }
1872
2352
 
2353
+ // Prefer the panel that has an active selection
2354
+ const panelFiber = panels.find(p => p.memoizedProps.hierarchy?.length > 0) || panels[0];
1873
2355
  const props = panelFiber.memoizedProps;
1874
2356
  if (!props.hierarchy || props.hierarchy.length === 0) {
1875
2357
  return {
@@ -2028,7 +2510,7 @@ export async function inspectAtPoint(x, y, options = {}) {
2028
2510
 
2029
2511
  // RN primitives and internal components to skip when surfacing the "element" name.
2030
2512
  // We want the nearest *custom* component, not a library wrapper.
2031
- var RN_PRIMITIVES = /^(View|Text|Image|ScrollView|FlatList|SectionList|TextInput|TouchableOpacity|TouchableHighlight|TouchableNativeFeedback|TouchableWithoutFeedback|Pressable|Button|Switch|ActivityIndicator|Modal|SafeAreaView|KeyboardAvoidingView|Animated\\(.*|withAnimated.*|ForwardRef.*|memo\\(.*|Context\\.Consumer|Context\\.Provider|VirtualizedList.*|CellRenderer.*|FrameSizeProvider|MaybeScreenContainer|RCT.*|RNS.*|Navigation.*|Screen$|ScreenStack|ScreenContainer|ScreenContentWrapper|SceneView|DelayedFreeze|Freeze|Suspender|DebugContainer|StaticContainer|Expo.*|LinearGradient|ViewManagerAdapter_.*|Svg.*|Defs|Path|Rect|Circle|G|Line|Polygon|Polyline|Ellipse|ClipPath|GestureHandler.*|NativeViewGestureHandler|Reanimated.*|BottomTabNavigator|TabLayout|RouteNode|Route$|MaybeScreen|SafeAreaProvider.*|GestureDetector|PanGestureHandler|DropShadow|BlurView|MaskedView.*)$/;
2513
+ var RN_PRIMITIVES = /^(View|Text|Image|ScrollView|FlatList|SectionList|TextInput|TouchableOpacity|TouchableHighlight|TouchableNativeFeedback|TouchableWithoutFeedback|Pressable|Button|Switch|ActivityIndicator|SafeAreaView|KeyboardAvoidingView|Animated\\(.*|withAnimated.*|ForwardRef.*|memo\\(.*|Context\\.Consumer|Context\\.Provider|VirtualizedList.*|CellRenderer.*|FrameSizeProvider|MaybeScreenContainer|RCT.*|RNS.*|Navigation.*|Screen$|ScreenStack|ScreenContainer|ScreenContentWrapper|SceneView|DelayedFreeze|Freeze|Suspender|DebugContainer|StaticContainer|Expo.*|LinearGradient|ViewManagerAdapter_.*|Svg.*|Defs|Path|Rect|Circle|G|Line|Polygon|Polyline|Ellipse|ClipPath|GestureHandler.*|NativeViewGestureHandler|Reanimated.*|BottomTabNavigator|TabLayout|RouteNode|Route$|MaybeScreen|SafeAreaProvider.*|GestureDetector|PanGestureHandler|DropShadow|BlurView|MaskedView.*)$/;
2032
2514
 
2033
2515
  function getNearestNamed(fiber, skipPrimitives) {
2034
2516
  var cur = fiber;