react-native-ai-devtools 1.4.18 → 1.4.19

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.
@@ -532,15 +532,125 @@ function formatTreeStructureOnly(node, indent = 0) {
532
532
  return result;
533
533
  }
534
534
  function formatScreenLayoutToTonl(elements) {
535
- const lines = ["#elements{component,path,depth,layout,id}"];
535
+ // Check if elements have tree structure (parentIndex)
536
+ const hasTree = elements.some(el => el.parentIndex !== undefined);
537
+ if (hasTree) {
538
+ return formatScreenLayoutTree(elements);
539
+ }
540
+ // Flat format fallback
541
+ const lines = ["#elements{component,path,depth,frame,layout,id}"];
536
542
  for (const el of elements) {
543
+ const frame = el.frame
544
+ ? `${Math.round(el.frame.x)},${Math.round(el.frame.y)} ${Math.round(el.frame.width)}x${Math.round(el.frame.height)}`
545
+ : "";
537
546
  const layout = el.layout
538
547
  ? Object.entries(el.layout)
539
548
  .map(([k, v]) => `${k}:${v}`)
540
549
  .join(";")
541
550
  : "";
542
551
  const id = el.identifiers?.testID || el.identifiers?.accessibilityLabel || "";
543
- lines.push(`${el.component}|${el.path}|${el.depth}|${layout}|${id}`);
552
+ lines.push(`${el.component}|${el.path}|${el.depth}|${frame}|${layout}|${id}`);
553
+ }
554
+ return lines.join("\n");
555
+ }
556
+ function formatScreenLayoutTree(elements, extended = false) {
557
+ // Build index: originalIndex -> element index in the filtered array
558
+ const indexMap = new Map();
559
+ for (let i = 0; i < elements.length; i++) {
560
+ if (elements[i].originalIndex !== undefined) {
561
+ indexMap.set(elements[i].originalIndex, i);
562
+ }
563
+ }
564
+ // Build children lists
565
+ const children = new Map();
566
+ const roots = [];
567
+ for (let i = 0; i < elements.length; i++) {
568
+ const el = elements[i];
569
+ const parentOrigIdx = el.parentIndex;
570
+ if (parentOrigIdx === undefined || parentOrigIdx === -1 || !indexMap.has(parentOrigIdx)) {
571
+ roots.push(i);
572
+ }
573
+ else {
574
+ const parentIdx = indexMap.get(parentOrigIdx);
575
+ if (!children.has(parentIdx))
576
+ children.set(parentIdx, []);
577
+ children.get(parentIdx).push(i);
578
+ }
579
+ }
580
+ // Collapse wrapper chains: if a child has the same frame as its parent,
581
+ // it's just a wrapper — promote its children and text to the parent
582
+ function sameFrame(a, b) {
583
+ if (!a.frame || !b.frame)
584
+ return false;
585
+ return Math.abs(a.frame.x - b.frame.x) < 1 &&
586
+ Math.abs(a.frame.y - b.frame.y) < 1 &&
587
+ Math.abs(a.frame.width - b.frame.width) < 1 &&
588
+ Math.abs(a.frame.height - b.frame.height) < 1;
589
+ }
590
+ function collapseNode(parentIdx) {
591
+ const kids = children.get(parentIdx);
592
+ if (!kids)
593
+ return;
594
+ const newKids = [];
595
+ for (const kidIdx of kids) {
596
+ if (sameFrame(elements[parentIdx], elements[kidIdx])) {
597
+ // Collapse: inherit text if parent has none
598
+ if (!elements[parentIdx].text && elements[kidIdx].text) {
599
+ elements[parentIdx].text = elements[kidIdx].text;
600
+ }
601
+ // Promote grandchildren to parent
602
+ const grandKids = children.get(kidIdx);
603
+ if (grandKids) {
604
+ newKids.push(...grandKids);
605
+ }
606
+ }
607
+ else {
608
+ newKids.push(kidIdx);
609
+ }
610
+ }
611
+ if (newKids.length > 0) {
612
+ children.set(parentIdx, newKids);
613
+ }
614
+ else {
615
+ children.delete(parentIdx);
616
+ }
617
+ // Recurse into the (possibly new) children
618
+ const updatedKids = children.get(parentIdx);
619
+ if (updatedKids) {
620
+ for (const kid of updatedKids) {
621
+ collapseNode(kid);
622
+ }
623
+ }
624
+ }
625
+ for (const root of roots) {
626
+ collapseNode(root);
627
+ }
628
+ const lines = [];
629
+ function printNode(idx, indent) {
630
+ const el = elements[idx];
631
+ const prefix = " ".repeat(indent);
632
+ const frame = el.frame
633
+ ? ` (${Math.round(el.frame.x)},${Math.round(el.frame.y)} ${Math.round(el.frame.width)}x${Math.round(el.frame.height)})`
634
+ : "";
635
+ const id = el.identifiers?.testID || el.identifiers?.accessibilityLabel || "";
636
+ const idStr = id ? ` [${id}]` : "";
637
+ // Only show text on leaf nodes (no meaningful children in tree)
638
+ // to avoid repeating children's text on every parent
639
+ const isLeaf = !children.has(idx);
640
+ const textStr = el.text && isLeaf ? ` "${el.text}"` : "";
641
+ const layoutStr = extended && el.layout
642
+ ? ` {${Object.entries(el.layout).map(([k, v]) => `${k}:${v}`).join("; ")}}`
643
+ : "";
644
+ lines.push(`${prefix}${el.component}${frame}${idStr}${textStr}${layoutStr}`);
645
+ const kids = children.get(idx);
646
+ if (kids) {
647
+ for (const kid of kids) {
648
+ printNode(kid, indent + 1);
649
+ }
650
+ }
651
+ }
652
+ for (const root of roots) {
653
+ printNode(root, 0);
544
654
  }
545
655
  return lines.join("\n");
546
656
  }
@@ -572,7 +682,7 @@ export async function getComponentTree(options = {}) {
572
682
  // Use lower default depth for structureOnly to keep output compact (~2-5KB)
573
683
  // Full mode uses higher depth since TONL format handles it better
574
684
  // focusedOnly mode uses moderate depth since we're already filtering to active screen
575
- const maxDepth = options.maxDepth ?? (structureOnly ? (focusedOnly ? 25 : 40) : 100);
685
+ const maxDepth = options.maxDepth ?? 5000;
576
686
  const expression = `
577
687
  (function() {
578
688
  const hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
@@ -717,7 +827,7 @@ export async function getComponentTree(options = {}) {
717
827
 
718
828
  // Find focused screen if requested
719
829
  function findFocusedScreen(fiber, depth = 0) {
720
- if (!fiber || depth > 200) return null;
830
+ if (!fiber || depth > 5000) return null;
721
831
 
722
832
  const name = getComponentName(fiber);
723
833
 
@@ -793,33 +903,57 @@ export async function getComponentTree(options = {}) {
793
903
  return result;
794
904
  }
795
905
  /**
796
- * Get layout styles for all components on the current screen.
797
- * Useful for verifying layout without screenshots.
906
+ * Get layout data for visible components on the current screen.
907
+ * Uses measureInWindow to get actual screen positions and filters
908
+ * to only components within the viewport.
909
+ *
910
+ * Two-step approach (same as inspectAtPoint):
911
+ * Step 1: Walk fiber tree, dispatch measureInWindow on host components
912
+ * Step 2: After 300ms, read measurements, filter by viewport, build results
798
913
  */
799
914
  export async function getScreenLayout(options = {}) {
800
- const { maxDepth = 65, componentsOnly = false, shortPath = true, summary = false, format = "tonl", device } = options;
801
- const expression = `
915
+ const { extended = false, summary = false, device } = options;
916
+ const maxDepth = 5000;
917
+ const componentsOnly = true;
918
+ const shortPath = true;
919
+ // --- Step 1: walk fiber tree + dispatch measureInWindow calls ---
920
+ const dispatchExpression = `
802
921
  (function() {
803
- const hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
922
+ var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
804
923
  if (!hook) return { error: 'React DevTools hook not found.' };
805
924
 
806
- let roots = [];
925
+ var roots = [];
807
926
  if (hook.getFiberRoots) {
808
- roots = [...(hook.getFiberRoots(1) || [])];
927
+ roots = Array.from(hook.getFiberRoots(1) || []);
809
928
  }
810
929
  if (roots.length === 0 && hook.renderers) {
811
- for (const [id] of hook.renderers) {
812
- const r = hook.getFiberRoots ? [...(hook.getFiberRoots(id) || [])] : [];
930
+ for (var entry of hook.renderers) {
931
+ var r = Array.from(hook.getFiberRoots ? (hook.getFiberRoots(entry[0]) || []) : []);
813
932
  if (r.length > 0) { roots = r; break; }
814
933
  }
815
934
  }
816
935
  if (roots.length === 0) return { error: 'No fiber roots found.' };
817
936
 
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
937
+ function getMeasurable(fiber) {
938
+ var sn = fiber.stateNode;
939
+ if (!sn) return null;
940
+ if (typeof sn.measureInWindow === 'function') return sn;
941
+ if (sn.canonical && sn.canonical.publicInstance &&
942
+ typeof sn.canonical.publicInstance.measureInWindow === 'function') {
943
+ return sn.canonical.publicInstance;
944
+ }
945
+ return null;
946
+ }
947
+
948
+ // Collect host fibers with their metadata
949
+ var hostFibers = [];
950
+ var fiberMeta = [];
951
+ var componentsOnlyMode = ${componentsOnly};
952
+
953
+ // RN internals and primitives — skip these when looking for meaningful component names
954
+ // Only filter internal components that can't be written as JSX.
955
+ // Keep all user-facing components (View, Text, ScrollView, Modal, etc.)
956
+ 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
957
 
824
958
  function getComponentName(fiber) {
825
959
  if (!fiber || !fiber.type) return null;
@@ -827,8 +961,196 @@ export async function getScreenLayout(options = {}) {
827
961
  return fiber.type.displayName || fiber.type.name || null;
828
962
  }
829
963
 
830
- function isHostComponent(fiber) {
831
- return typeof fiber?.type === 'string';
964
+ // Find the first measurable host descendant of a fiber
965
+ function findFirstHost(fiber, depth) {
966
+ if (!fiber || depth > 20) return null;
967
+ if (typeof fiber.type === 'string' && getMeasurable(fiber)) return fiber;
968
+ var child = fiber.child;
969
+ while (child) {
970
+ var found = findFirstHost(child, depth + 1);
971
+ if (found) return found;
972
+ child = child.sibling;
973
+ }
974
+ return null;
975
+ }
976
+
977
+ // Extract text content from a fiber subtree.
978
+ // When a fiber has a string child, return it without recursing
979
+ // (avoids duplication from Text > RCTText having the same string).
980
+ function collectText(fiber, d) {
981
+ if (!fiber || d > 30) return '';
982
+ var props = fiber.memoizedProps;
983
+ if (props) {
984
+ var ch = props.children;
985
+ // Leaf text — return without recursing into children fibers
986
+ if (typeof ch === 'string') return ch;
987
+ if (typeof ch === 'number') return String(ch);
988
+ if (Array.isArray(ch)) {
989
+ var inline = [];
990
+ for (var ci = 0; ci < ch.length; ci++) {
991
+ if (typeof ch[ci] === 'string') inline.push(ch[ci]);
992
+ else if (typeof ch[ci] === 'number') inline.push(String(ch[ci]));
993
+ }
994
+ if (inline.length > 0) return inline.join('');
995
+ }
996
+ }
997
+ // No direct text — collect from child fibers (siblings = adjacent elements)
998
+ var parts = [];
999
+ var child = fiber.child;
1000
+ while (child) {
1001
+ var t = collectText(child, d + 1);
1002
+ if (t) parts.push(t);
1003
+ child = child.sibling;
1004
+ }
1005
+ return parts.join(' ').trim();
1006
+ }
1007
+
1008
+ if (componentsOnlyMode) {
1009
+ // componentsOnly: walk tree looking for meaningful custom components,
1010
+ // measure their first host child, track parent for tree output
1011
+ function walkComponents(fiber, depth, path, parentIdx) {
1012
+ if (!fiber || depth > ${maxDepth}) return;
1013
+ var name = getComponentName(fiber);
1014
+ var isHost = typeof fiber.type === 'string';
1015
+ var isMeaningful = name && !isHost && !RN_PRIMITIVES.test(name);
1016
+
1017
+ var myIdx = parentIdx;
1018
+ if (isMeaningful) {
1019
+ var host = findFirstHost(fiber, 0);
1020
+ if (host) {
1021
+ myIdx = hostFibers.length;
1022
+ // Extract text from this component's subtree
1023
+ var text = collectText(fiber, 0);
1024
+ hostFibers.push(host);
1025
+ fiberMeta.push({
1026
+ hostName: typeof host.type === 'string' ? host.type : '',
1027
+ customName: name,
1028
+ depth: depth,
1029
+ path: path.concat([name]),
1030
+ parentIndex: parentIdx,
1031
+ text: text ? text.slice(0, 80) : null
1032
+ });
1033
+ }
1034
+ }
1035
+
1036
+ var child = fiber.child;
1037
+ while (child) {
1038
+ var childName = getComponentName(child);
1039
+ walkComponents(child, depth + 1, childName ? path.concat([childName]) : path, myIdx);
1040
+ child = child.sibling;
1041
+ }
1042
+ }
1043
+ walkComponents(roots[0].current, 0, [], -1);
1044
+ } else {
1045
+ // Default mode: collect all host fibers with ancestor info
1046
+ function walkFibers(fiber, depth, path) {
1047
+ if (!fiber || depth > ${maxDepth}) return;
1048
+ var name = getComponentName(fiber);
1049
+ var isHost = typeof fiber.type === 'string';
1050
+
1051
+ if (name && isHost && getMeasurable(fiber)) {
1052
+ // Find nearest meaningful custom component ancestor for display
1053
+ var customName = null;
1054
+ var fallbackName = null;
1055
+ var cur = fiber.return;
1056
+ while (cur) {
1057
+ if (cur.type && typeof cur.type !== 'string') {
1058
+ var cName = cur.type.displayName || cur.type.name || null;
1059
+ if (cName) {
1060
+ if (!fallbackName) fallbackName = cName;
1061
+ if (!RN_PRIMITIVES.test(cName)) {
1062
+ customName = cName;
1063
+ break;
1064
+ }
1065
+ }
1066
+ }
1067
+ cur = cur.return;
1068
+ }
1069
+ if (!customName) customName = fallbackName;
1070
+
1071
+ hostFibers.push(fiber);
1072
+ fiberMeta.push({
1073
+ hostName: name,
1074
+ customName: customName,
1075
+ depth: depth,
1076
+ path: path.slice()
1077
+ });
1078
+ }
1079
+
1080
+ var child = fiber.child;
1081
+ while (child) {
1082
+ var childName = getComponentName(child);
1083
+ walkFibers(child, depth + 1, childName ? path.concat([childName]) : path);
1084
+ child = child.sibling;
1085
+ }
1086
+ }
1087
+ walkFibers(roots[0].current, 0, []);
1088
+ }
1089
+
1090
+ if (hostFibers.length === 0) return { error: 'No measurable host components found.' };
1091
+
1092
+ // Store fibers and metadata globally for step 2
1093
+ globalThis.__layoutFibers = hostFibers;
1094
+ globalThis.__layoutMeta = fiberMeta;
1095
+ globalThis.__layoutMeasurements = new Array(hostFibers.length).fill(null);
1096
+
1097
+ // Dispatch measureInWindow on all host fibers
1098
+ for (var i = 0; i < hostFibers.length; i++) {
1099
+ try {
1100
+ (function(idx) {
1101
+ getMeasurable(hostFibers[idx]).measureInWindow(function(fx, fy, fw, fh) {
1102
+ globalThis.__layoutMeasurements[idx] = { x: fx, y: fy, width: fw, height: fh };
1103
+ });
1104
+ })(i);
1105
+ } catch(e) {}
1106
+ }
1107
+
1108
+ return { count: hostFibers.length };
1109
+ })()
1110
+ `;
1111
+ const dispatchResult = await executeInApp(dispatchExpression, false, { timeoutMs: 30000 }, device);
1112
+ if (!dispatchResult.success)
1113
+ return dispatchResult;
1114
+ try {
1115
+ const parsed = JSON.parse(dispatchResult.result || "{}");
1116
+ if (parsed.error)
1117
+ return { success: false, error: parsed.error };
1118
+ }
1119
+ catch {
1120
+ /* ignore */
1121
+ }
1122
+ // Wait for measureInWindow callbacks
1123
+ await delay(300);
1124
+ // --- Step 2: read measurements, filter visible, build results ---
1125
+ const resolveExpression = `
1126
+ (function() {
1127
+ var fibers = globalThis.__layoutFibers;
1128
+ var meta = globalThis.__layoutMeta;
1129
+ var measurements = globalThis.__layoutMeasurements;
1130
+ globalThis.__layoutFibers = null;
1131
+ globalThis.__layoutMeta = null;
1132
+ globalThis.__layoutMeasurements = null;
1133
+
1134
+ if (!fibers || !measurements || !meta) {
1135
+ return { error: 'No measurement data. Run get_screen_layout again.' };
1136
+ }
1137
+
1138
+ var componentsOnly = ${componentsOnly};
1139
+ var shortPath = ${shortPath};
1140
+ var summaryMode = ${summary};
1141
+ var pathSegments = 3;
1142
+
1143
+
1144
+ // Get viewport dimensions from the first root view measurement
1145
+ // or use a generous default
1146
+ var viewportW = 9999, viewportH = 9999;
1147
+ for (var v = 0; v < measurements.length; v++) {
1148
+ if (measurements[v] && measurements[v].x === 0 && measurements[v].y === 0 &&
1149
+ measurements[v].width > 0 && measurements[v].height > 0) {
1150
+ viewportW = measurements[v].width;
1151
+ viewportH = measurements[v].height;
1152
+ break;
1153
+ }
832
1154
  }
833
1155
 
834
1156
  function formatPath(pathArray) {
@@ -838,22 +1160,14 @@ export async function getScreenLayout(options = {}) {
838
1160
  return '... > ' + pathArray.slice(-pathSegments).join(' > ');
839
1161
  }
840
1162
 
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
1163
  function extractLayoutStyles(style) {
850
1164
  if (!style) return null;
851
- const merged = Array.isArray(style)
852
- ? Object.assign({}, ...style.filter(Boolean).map(s => typeof s === 'object' ? s : {}))
1165
+ var merged = Array.isArray(style)
1166
+ ? Object.assign.apply(null, [{}].concat(style.filter(Boolean).map(function(s) { return typeof s === 'object' ? s : {}; })))
853
1167
  : (typeof style === 'object' ? style : {});
854
1168
 
855
- const layout = {};
856
- const layoutKeys = [
1169
+ var layout = {};
1170
+ var layoutKeys = [
857
1171
  'padding', 'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight',
858
1172
  'paddingHorizontal', 'paddingVertical',
859
1173
  'margin', 'marginTop', 'marginBottom', 'marginLeft', 'marginRight',
@@ -867,76 +1181,103 @@ export async function getScreenLayout(options = {}) {
867
1181
  'backgroundColor', 'borderColor', 'borderRadius'
868
1182
  ];
869
1183
 
870
- for (const key of layoutKeys) {
871
- if (merged[key] !== undefined) layout[key] = merged[key];
1184
+ for (var k = 0; k < layoutKeys.length; k++) {
1185
+ if (merged[layoutKeys[k]] !== undefined) layout[layoutKeys[k]] = merged[layoutKeys[k]];
872
1186
  }
873
1187
  return Object.keys(layout).length > 0 ? layout : null;
874
1188
  }
875
1189
 
876
- const elements = [];
1190
+ var elements = [];
877
1191
 
878
- function walkFiber(fiber, depth, path) {
879
- if (!fiber || depth > maxDepth) return;
1192
+ for (var i = 0; i < measurements.length; i++) {
1193
+ var m = measurements[i];
1194
+ if (!m) continue;
880
1195
 
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
- }
1196
+ // Filter: only visible within viewport (with some tolerance for partial visibility)
1197
+ if (m.width <= 0 || m.height <= 0) continue;
1198
+ if (m.x + m.width < 0 || m.y + m.height < 0) continue;
1199
+ if (m.x > viewportW || m.y > viewportH) continue;
896
1200
 
897
- const element = {
898
- component: name,
899
- path: formatPath(path),
900
- depth
901
- };
902
-
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
- }
1201
+ var fiber = fibers[i];
1202
+ var info = meta[i];
1203
+
1204
+ var displayName = componentsOnly ? info.customName : info.hostName;
1205
+ if (!displayName) continue;
1206
+ if (componentsOnly && !info.customName) continue;
915
1207
 
916
- elements.push(element);
1208
+ var style = fiber.memoizedProps ? fiber.memoizedProps.style : null;
1209
+ var layout = extractLayoutStyles(style);
1210
+
1211
+ // Get text content — use pre-collected text from step 1, or fall back to host fiber
1212
+ var textContent = info.text || null;
1213
+ if (!textContent && (info.hostName === 'RCTText' || info.hostName === 'Text')) {
1214
+ var children = fiber.memoizedProps ? fiber.memoizedProps.children : null;
1215
+ if (typeof children === 'string') textContent = children;
1216
+ else if (typeof children === 'number') textContent = String(children);
917
1217
  }
918
1218
 
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;
1219
+ var element = {
1220
+ component: displayName,
1221
+ path: formatPath(info.path),
1222
+ depth: info.depth,
1223
+ frame: { x: m.x, y: m.y, width: m.width, height: m.height },
1224
+ originalIndex: i
1225
+ };
1226
+
1227
+ if (info.parentIndex !== undefined) element.parentIndex = info.parentIndex;
1228
+ if (layout) element.layout = layout;
1229
+ if (textContent) element.text = textContent.slice(0, 100);
1230
+
1231
+ // Identifiers
1232
+ if (fiber.memoizedProps) {
1233
+ var identifiers = {};
1234
+ if (fiber.memoizedProps.testID) identifiers.testID = fiber.memoizedProps.testID;
1235
+ if (fiber.memoizedProps.accessibilityLabel) identifiers.accessibilityLabel = fiber.memoizedProps.accessibilityLabel;
1236
+ if (fiber.memoizedProps.nativeID) identifiers.nativeID = fiber.memoizedProps.nativeID;
1237
+ if (fiber.key) identifiers.key = fiber.key;
1238
+ if (Object.keys(identifiers).length > 0) element.identifiers = identifiers;
925
1239
  }
1240
+
1241
+ elements.push(element);
926
1242
  }
927
1243
 
928
- walkFiber(roots[0].current, 0, []);
1244
+ // In componentsOnly mode, remove full-screen wrapper components
1245
+ // and re-parent their children to collapse the wrapper chain
1246
+ if (componentsOnly && elements.length > 0) {
1247
+ var filtered = [];
1248
+ // Map: originalIndex -> resolved parent for skipped wrappers
1249
+ var reparent = {};
1250
+
1251
+ for (var fi = 0; fi < elements.length; fi++) {
1252
+ var el = elements[fi];
1253
+ var fr = el.frame;
1254
+ var isFullScreen = fr && fr.x === 0 && fr.y === 0 &&
1255
+ Math.abs(fr.width - viewportW) < 2 && Math.abs(fr.height - viewportH) < 2;
1256
+
1257
+ if (isFullScreen) {
1258
+ // Skip this wrapper, map its originalIndex to its parent
1259
+ reparent[el.originalIndex] = el.parentIndex;
1260
+ } else {
1261
+ // Resolve parent through any skipped wrappers
1262
+ var resolvedParent = el.parentIndex;
1263
+ while (resolvedParent !== undefined && resolvedParent !== -1 && reparent[resolvedParent] !== undefined) {
1264
+ resolvedParent = reparent[resolvedParent];
1265
+ }
1266
+ el.parentIndex = resolvedParent;
1267
+ filtered.push(el);
1268
+ }
1269
+ }
1270
+ elements = filtered;
1271
+ }
929
1272
 
930
- // Summary mode: return counts by component name
931
1273
  if (summaryMode) {
932
- const counts = {};
933
- for (const el of elements) {
934
- counts[el.component] = (counts[el.component] || 0) + 1;
1274
+ var counts = {};
1275
+ for (var j = 0; j < elements.length; j++) {
1276
+ counts[elements[j].component] = (counts[elements[j].component] || 0) + 1;
935
1277
  }
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 }));
1278
+ var sorted = Object.keys(counts).map(function(name) {
1279
+ return { component: name, count: counts[name] };
1280
+ }).sort(function(a, b) { return b.count - a.count; });
940
1281
  return {
941
1282
  totalElements: elements.length,
942
1283
  uniqueComponents: sorted.length,
@@ -945,15 +1286,15 @@ export async function getScreenLayout(options = {}) {
945
1286
  }
946
1287
 
947
1288
  return {
1289
+ viewport: { width: viewportW, height: viewportH },
948
1290
  totalElements: elements.length,
949
1291
  elements: elements
950
1292
  };
951
1293
  })()
952
1294
  `;
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) {
1295
+ const result = await executeInApp(resolveExpression, false, { timeoutMs: 30000 }, device);
1296
+ // Format output as tree
1297
+ if (result.success && result.result) {
957
1298
  try {
958
1299
  const parsed = JSON.parse(result.result);
959
1300
  if (parsed.components) {
@@ -962,9 +1303,8 @@ export async function getScreenLayout(options = {}) {
962
1303
  return { success: true, result: tonl };
963
1304
  }
964
1305
  else if (parsed.elements) {
965
- // Full element list
966
- const tonl = formatScreenLayoutToTonl(parsed.elements);
967
- return { success: true, result: tonl };
1306
+ const tree = formatScreenLayoutTree(parsed.elements, extended);
1307
+ return { success: true, result: tree };
968
1308
  }
969
1309
  }
970
1310
  catch {
@@ -1280,8 +1620,6 @@ export async function findComponents(pattern, options = {}) {
1280
1620
  child = child.sibling;
1281
1621
  }
1282
1622
  } catch(e) {
1283
- // Skip nodes that throw (e.g. Reanimated animated components)
1284
- // Still try to traverse children if possible
1285
1623
  try {
1286
1624
  var child = fiber.child;
1287
1625
  while (child && results.length < maxResults) {
@@ -1294,7 +1632,6 @@ export async function findComponents(pattern, options = {}) {
1294
1632
 
1295
1633
  search(roots[0].current, [], 0);
1296
1634
 
1297
- // Summary mode: just return counts by component name
1298
1635
  if (summaryMode) {
1299
1636
  const counts = {};
1300
1637
  for (const r of results) {
@@ -1319,18 +1656,15 @@ export async function findComponents(pattern, options = {}) {
1319
1656
  })()
1320
1657
  `;
1321
1658
  const result = await executeInApp(expression, false, {}, device);
1322
- // Apply TONL formatting if requested
1323
1659
  if (format === "tonl" && result.success && result.result) {
1324
1660
  try {
1325
1661
  const parsed = JSON.parse(result.result);
1326
1662
  if (parsed.components) {
1327
1663
  if (parsed.totalMatches !== undefined) {
1328
- // Summary mode
1329
1664
  const tonl = formatSummaryToTonl(parsed.components, parsed.totalMatches);
1330
1665
  return { success: true, result: `pattern: ${parsed.pattern}\n${tonl}` };
1331
1666
  }
1332
1667
  else {
1333
- // Full list mode
1334
1668
  const tonl = formatFoundComponentsToTonl(parsed.components);
1335
1669
  return { success: true, result: `pattern: ${parsed.pattern}\nfound: ${parsed.found}\n${tonl}` };
1336
1670
  }
@@ -1387,7 +1721,7 @@ export async function pressElement(options) {
1387
1721
  }
1388
1722
 
1389
1723
  function extractText(fiber, depth) {
1390
- if (!fiber || depth > 15) return '';
1724
+ if (!fiber || depth > 5000) return '';
1391
1725
  var parts = [];
1392
1726
  var props = fiber.memoizedProps;
1393
1727
  if (props) {
@@ -1656,7 +1990,7 @@ export async function pressElement(options) {
1656
1990
  var hostFiber = null;
1657
1991
  var fallbackHost = null;
1658
1992
  function findHostChild(fiber, depth) {
1659
- if (!fiber || depth > 15 || hostFiber) return;
1993
+ if (!fiber || depth > 5000 || hostFiber) return;
1660
1994
  if (fiber.tag === 5 && fiber.stateNode) {
1661
1995
  var hostType = typeof fiber.type === 'string' ? fiber.type : '';
1662
1996
  if (target.isInput && (hostType.indexOf('TextInput') !== -1 || hostType.indexOf('textinput') !== -1)) {
@@ -1806,7 +2140,7 @@ export async function isInspectorActive(device) {
1806
2140
  if (roots.length === 0) return false;
1807
2141
 
1808
2142
  function findComponent(fiber, targetName, depth = 0) {
1809
- if (!fiber || depth > 100) return null;
2143
+ if (!fiber || depth > 5000) return null;
1810
2144
  const name = fiber.type?.displayName || fiber.type?.name;
1811
2145
  if (name === targetName) return fiber;
1812
2146
  let child = fiber.child;
@@ -1848,28 +2182,29 @@ export async function getInspectorSelection(device) {
1848
2182
  }
1849
2183
  if (roots.length === 0) return { error: 'No fiber roots found.' };
1850
2184
 
1851
- // Find InspectorPanel component
1852
- function findComponent(fiber, targetName, depth = 0) {
1853
- if (!fiber || depth > 100) return null;
2185
+ // Find all InspectorPanel instances (apps with modals may have multiple)
2186
+ function findAllPanels(fiber, targetName, depth, results) {
2187
+ if (!fiber || depth > 5000) return;
1854
2188
  const name = fiber.type?.displayName || fiber.type?.name;
1855
- if (name === targetName) return fiber;
2189
+ if (name === targetName) results.push(fiber);
1856
2190
  let child = fiber.child;
1857
2191
  while (child) {
1858
- const found = findComponent(child, targetName, depth + 1);
1859
- if (found) return found;
2192
+ findAllPanels(child, targetName, depth + 1, results);
1860
2193
  child = child.sibling;
1861
2194
  }
1862
- return null;
1863
2195
  }
1864
2196
 
1865
- const panelFiber = findComponent(roots[0].current, 'InspectorPanel');
1866
- if (!panelFiber) {
2197
+ const panels = [];
2198
+ findAllPanels(roots[0].current, 'InspectorPanel', 0, panels);
2199
+ if (panels.length === 0) {
1867
2200
  return {
1868
2201
  error: 'Element Inspector is not active.',
1869
2202
  hint: 'Use toggle_element_inspector to enable the inspector, then tap an element to select it.'
1870
2203
  };
1871
2204
  }
1872
2205
 
2206
+ // Prefer the panel that has an active selection
2207
+ const panelFiber = panels.find(p => p.memoizedProps.hierarchy?.length > 0) || panels[0];
1873
2208
  const props = panelFiber.memoizedProps;
1874
2209
  if (!props.hierarchy || props.hierarchy.length === 0) {
1875
2210
  return {
@@ -2028,7 +2363,7 @@ export async function inspectAtPoint(x, y, options = {}) {
2028
2363
 
2029
2364
  // RN primitives and internal components to skip when surfacing the "element" name.
2030
2365
  // 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.*)$/;
2366
+ 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
2367
 
2033
2368
  function getNearestNamed(fiber, skipPrimitives) {
2034
2369
  var cur = fiber;