react-native-ai-debugger 1.4.41 → 1.5.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.
package/build/index.js CHANGED
@@ -13,7 +13,7 @@ import { getInstallationId, getServerVersion, getPackageName, isDevMode, TELEMET
13
13
  import { isSDKInstalled, querySDKNetwork, getSDKNetworkEntry, getSDKNetworkStats, clearSDKNetwork, querySDKConsole, getSDKConsoleStats, clearSDKConsole } from "./core/sdkBridge.js";
14
14
  import { tap, convertScreenshotToTapCoords } from "./pro/tap.js";
15
15
  import { getActivateLicenseConfig, handleActivateLicense, getDeleteAccountConfig, handleDeleteAccount, } from "./tools/accountTools.js";
16
- import { logBuffers, networkBuffers, getLogBuffer, getNetworkBuffer, getTotalLogCount, getConnectedAppByDevice, LogBuffer, NetworkBuffer, bundleErrorBuffer, imageBuffer, connectedApps, scanMetroPorts, fetchDevices, filterDebuggableDevices, connectToDevice, getConnectedApps, executeInApp, listDebugGlobals, inspectGlobal, reloadApp,
16
+ import { logBuffers, networkBuffers, getLogBuffer, getNetworkBuffer, getTotalLogCount, getConnectedAppByDevice, getConnectedAppBySimulatorUdid, getConnectedAppByAndroidDeviceId, LogBuffer, NetworkBuffer, bundleErrorBuffer, imageBuffer, connectedApps, getActiveOrBootedSimulatorUdid, scanMetroPorts, fetchDevices, filterDebuggableDevices, connectToDevice, getConnectedApps, executeInApp, listDebugGlobals, inspectGlobal, reloadApp,
17
17
  // React Component Inspection
18
18
  getComponentTree, getScreenLayout, getPressableElements, inspectComponent, findComponents, inspectAtPoint, toggleElementInspector, isInspectorActive, getInspectorSelection, getFirstConnectedApp, getLogs, searchLogs, getLogSummary, getNetworkRequests, searchNetworkRequests, getNetworkStats, formatRequestDetails,
19
19
  // Connection state
@@ -27,7 +27,7 @@ connectMetroBuildEvents, disconnectMetroBuildEvents, getBundleErrors, getBundleS
27
27
  // Error screen parsing (OCR fallback)
28
28
  parseErrorScreenText, formatParsedError,
29
29
  // OCR
30
- recognizeText, inferIOSDevicePixelRatio, enrichScreenshotWithLayout,
30
+ recognizeText, inferIOSDevicePixelRatio,
31
31
  // Android
32
32
  listAndroidDevices, androidScreenshot, androidInstallApp, androidLaunchApp, androidListPackages,
33
33
  // Android UI Input (Phase 2)
@@ -41,7 +41,7 @@ listIOSSimulators, iosScreenshot, iosInstallApp, iosLaunchApp, iosOpenUrl, iosTe
41
41
  // iOS UI driver tools
42
42
  isUiDriverAvailable, iosTap, iosSwipe, iosInputText, iosButton, iosKeyEvent, iosKeySequence, iosDescribeAll, iosDescribePoint, IOS_BUTTON_TYPES,
43
43
  // iOS Element Finding (no screenshots)
44
- iosFindElement, iosWaitForElement, getDevicePixelRatio,
44
+ iosFindElement, iosWaitForElement, getDevicePixelRatio, getIOSSafeAreaTop,
45
45
  // Telemetry
46
46
  initTelemetry, trackToolInvocation, getTargetPlatform,
47
47
  // Format utilities (TONL)
@@ -1494,7 +1494,7 @@ registerToolWithTelemetry("get_screen_layout", {
1494
1494
  });
1495
1495
  // Tool: Get all pressable elements on screen
1496
1496
  registerToolWithTelemetry("get_pressable_elements", {
1497
- description: "Find all pressable (onPress) and input (TextInput) elements currently visible on screen. Returns component names, tap-ready center coordinates (in points/dp), text labels, testID, and accessibilityLabel. Useful when you need to tap an icon or button but can't identify it from a screenshot alone. Each element includes hasLabel (true if it contains text) and isInput (true for TextInput fields).",
1497
+ description: "Find all pressable (onPress) and input (TextInput) elements currently visible on screen. Returns component names, ready-to-tap center coordinates in SCREENSHOT PIXELS (same space as ios_screenshot/android_screenshot — pass directly to tap(x, y)), text labels, testID, accessibilityLabel, and a spatial nearbyText hint for icon-only buttons. Each element includes hasLabel (true if it contains text) and isInput (true for TextInput fields).",
1498
1498
  inputSchema: {
1499
1499
  device: z.string().optional().describe("Target device name (substring match). Omit for default device. Run get_apps to see connected devices.")
1500
1500
  }
@@ -1511,15 +1511,130 @@ registerToolWithTelemetry("get_pressable_elements", {
1511
1511
  isError: true
1512
1512
  };
1513
1513
  }
1514
- return {
1515
- content: [
1516
- {
1517
- type: "text",
1518
- text: result.result || "No pressable elements found."
1514
+ const elements = result.parsedElements;
1515
+ if (!elements || elements.length === 0) {
1516
+ return {
1517
+ content: [{ type: "text", text: result.result || "No pressable elements found." }]
1518
+ };
1519
+ }
1520
+ // Resolve target app so we can pick the right simulator / android device
1521
+ const targetApp = device ? getConnectedAppByDevice(device) : getFirstConnectedApp();
1522
+ if (!targetApp) {
1523
+ // No connected app — return raw (points) output; better than nothing
1524
+ return {
1525
+ content: [{ type: "text", text: result.result || "No pressable elements found." }]
1526
+ };
1527
+ }
1528
+ // Capture a lightweight screenshot to learn scaleFactor (downscale) and dimensions.
1529
+ // Conversion: screenshot_px = native_coord * devicePixelRatio / screenshotScale
1530
+ let lines = [];
1531
+ try {
1532
+ if (targetApp.platform === "ios") {
1533
+ const udid = targetApp.simulatorUdid;
1534
+ const shot = await iosScreenshot(undefined, udid);
1535
+ const screenshotScale = shot.scaleFactor || 1;
1536
+ const devicePixelRatio = (shot.originalWidth && shot.originalHeight
1537
+ ? inferIOSDevicePixelRatio(shot.originalWidth, shot.originalHeight)
1538
+ : null) ?? (await getDevicePixelRatio(udid)) ?? 3;
1539
+ // Fallback to 59pt (iPhone typical) when the UI driver preflight can't
1540
+ // resolve the true inset — matches ios_screenshot's default. Without this
1541
+ // shift, react-native-screens modal-presented screens report y relative
1542
+ // to content origin and taps land in the status bar instead of the button.
1543
+ const safeAreaTop = (await getIOSSafeAreaTop(udid).catch(() => 0)) || 59;
1544
+ // Keep the app's lastScreenshot metadata in sync so tap(x, y) uses the
1545
+ // same scaleFactor when converting our pixel coords back to points.
1546
+ if (shot.originalWidth && shot.originalHeight) {
1547
+ targetApp.lastScreenshot = {
1548
+ originalWidth: shot.originalWidth,
1549
+ originalHeight: shot.originalHeight,
1550
+ scaleFactor: screenshotScale
1551
+ };
1519
1552
  }
1520
- ]
1553
+ lines = formatPressablesInPixels(elements, {
1554
+ platform: "ios",
1555
+ devicePixelRatio,
1556
+ screenshotScale,
1557
+ safeAreaTop
1558
+ });
1559
+ }
1560
+ else {
1561
+ const deviceId = targetApp.deviceInfo?.deviceName;
1562
+ const shot = await androidScreenshot(undefined, deviceId);
1563
+ const screenshotScale = shot.scaleFactor || 1;
1564
+ const density = await androidGetDensity(deviceId).catch(() => ({ density: 160 }));
1565
+ const devicePixelRatio = (density.density || 160) / 160;
1566
+ if (shot.originalWidth && shot.originalHeight) {
1567
+ targetApp.lastScreenshot = {
1568
+ originalWidth: shot.originalWidth,
1569
+ originalHeight: shot.originalHeight,
1570
+ scaleFactor: screenshotScale
1571
+ };
1572
+ }
1573
+ lines = formatPressablesInPixels(elements, {
1574
+ platform: "android",
1575
+ devicePixelRatio,
1576
+ screenshotScale,
1577
+ safeAreaTop: 0
1578
+ });
1579
+ }
1580
+ }
1581
+ catch {
1582
+ // Fallback to points if screenshot/metadata unavailable
1583
+ return {
1584
+ content: [{ type: "text", text: result.result || "No pressable elements found." }]
1585
+ };
1586
+ }
1587
+ const iconCount = elements.filter((e) => !e.hasLabel).length;
1588
+ const labeledCount = elements.length - iconCount;
1589
+ const summary = `Found ${elements.length} pressable elements (${iconCount} icon-only, ${labeledCount} with text labels)`;
1590
+ const text = [summary, "", ...lines].join("\n");
1591
+ return {
1592
+ content: [{ type: "text", text }]
1521
1593
  };
1522
1594
  });
1595
+ function formatPressablesInPixels(elements, opts) {
1596
+ const { platform, devicePixelRatio, screenshotScale, safeAreaTop } = opts;
1597
+ const out = [];
1598
+ for (let i = 0; i < elements.length; i++) {
1599
+ const el = elements[i];
1600
+ // react-native-screens modal presentations report y relative to content origin;
1601
+ // shift into the window frame when the measurement falls inside the safe-area band.
1602
+ let cy = el.center.y;
1603
+ let fy = el.frame.y;
1604
+ if (platform === "ios" && safeAreaTop > 0) {
1605
+ if (cy < safeAreaTop)
1606
+ cy += safeAreaTop;
1607
+ if (fy < safeAreaTop)
1608
+ fy += safeAreaTop;
1609
+ }
1610
+ const toPx = (v) => Math.round((v * devicePixelRatio) / screenshotScale);
1611
+ const cx = toPx(el.center.x);
1612
+ const cyPx = toPx(cy);
1613
+ const fx = toPx(el.frame.x);
1614
+ const fyPx = toPx(fy);
1615
+ const fw = toPx(el.frame.width);
1616
+ const fh = toPx(el.frame.height);
1617
+ const num = i + 1;
1618
+ const label = el.hasLabel
1619
+ ? `"${el.text}"`
1620
+ : el.intent
1621
+ ? `(${el.intent} icon)`
1622
+ : "(icon/image)";
1623
+ const ids = [];
1624
+ if (el.testID)
1625
+ ids.push(`testID="${el.testID}"`);
1626
+ if (el.accessibilityLabel)
1627
+ ids.push(`a11y="${el.accessibilityLabel}"`);
1628
+ const idStr = ids.length > 0 ? ` [${ids.join(", ")}]` : "";
1629
+ const inputStr = el.isInput ? " (input)" : "";
1630
+ const wrapperStr = el.isWrapper ? " [wrapper — skip unless dismissing keyboard]" : "";
1631
+ const nearPart = el.nearbyText ? ` near "${el.nearbyText}"` : "";
1632
+ out.push(`${num}. ${el.component} ${label}${nearPart} — center:(${cx},${cyPx}) frame:(${fx},${fyPx} ${fw}x${fh})${idStr}${inputStr}${wrapperStr}`);
1633
+ if (el.path)
1634
+ out.push(` path: ${el.path}`);
1635
+ }
1636
+ return out;
1637
+ }
1523
1638
  // Tool: Inspect a specific component by name
1524
1639
  registerToolWithTelemetry("inspect_component", {
1525
1640
  description: "Inspect a specific React component by name. **DRILL-DOWN TOOL**: Use after get_screen_layout or find_components to identify which component to inspect. Returns props, style, state (hooks), and optionally children tree. Use childrenDepth to control how deep nested children go.",
@@ -1745,8 +1860,8 @@ registerToolWithTelemetry("tap", {
1745
1860
  }
1746
1861
  return {
1747
1862
  content,
1748
- isError: !result.success,
1749
- _errorMessage: !result.success
1863
+ isError: !result.success && !result.ambiguous,
1864
+ _errorMessage: !result.success && !result.ambiguous
1750
1865
  ? `${JSON.stringify(result.query)}|${result.error || ""}`
1751
1866
  : undefined,
1752
1867
  _errorContext: errorContext,
@@ -2871,10 +2986,13 @@ registerToolWithTelemetry("android_screenshot", {
2871
2986
  // Build info text with coordinate conversion guidance
2872
2987
  const pixelWidth = result.originalWidth || 0;
2873
2988
  const pixelHeight = result.originalHeight || 0;
2874
- // Store screenshot metadata for coordinate conversion
2875
- const firstApp = connectedApps.values().next().value;
2876
- if (firstApp) {
2877
- firstApp.lastScreenshot = {
2989
+ // Resolve the RN app running on THIS Android device so enrichment
2990
+ // pulls data from the right app (not whichever app is "first").
2991
+ const targetApp = getConnectedAppByAndroidDeviceId(deviceId);
2992
+ const targetDeviceName = targetApp?.deviceInfo.deviceName;
2993
+ // Store screenshot metadata on the matching app (not an arbitrary one)
2994
+ if (targetApp) {
2995
+ targetApp.lastScreenshot = {
2878
2996
  originalWidth: pixelWidth,
2879
2997
  originalHeight: pixelHeight,
2880
2998
  scaleFactor: result.scaleFactor || 1,
@@ -2902,27 +3020,28 @@ registerToolWithTelemetry("android_screenshot", {
2902
3020
  // Enrich with screen layout data (component names + tap coordinates)
2903
3021
  // Android: density ratio = densityDpi / 160 (160dpi = 1dp = 1px baseline)
2904
3022
  const androidPixelRatio = densityDpi / 160;
2905
- let layoutText = null;
2906
- try {
2907
- layoutText = await enrichScreenshotWithLayout(androidPixelRatio, result.scaleFactor || 1);
2908
- }
2909
- catch {
2910
- // Non-fatal: screenshot works without layout enrichment
2911
- }
2912
- // Enrich with pressable elements (filtered list of tappable components, converted to screenshot pixel coords)
3023
+ // Skip enrichment when no RN app is connected to this Android device —
3024
+ // otherwise we would pull fiber data from a different device's app.
3025
+ const canEnrich = !!targetApp;
2913
3026
  let pressablesText = null;
3027
+ // Screen Layout tree previously appended here was dropped — it was noisy
3028
+ // (nested Svg/G/Path duplicates). Use get_screen_layout when the tree is needed.
3029
+ // Enrich with pressable elements (filtered list of tappable components, converted to screenshot pixel coords)
2914
3030
  try {
2915
- const pressables = await getPressableElements({});
3031
+ if (!canEnrich)
3032
+ throw new Error("skip");
3033
+ const pressables = await getPressableElements({ device: targetDeviceName });
2916
3034
  if (pressables.success && pressables.parsedElements && pressables.parsedElements.length > 0) {
2917
3035
  const screenshotScale = result.scaleFactor || 1;
2918
3036
  pressablesText = pressables.parsedElements.map((el) => {
2919
3037
  const px = Math.round((el.center.x * androidPixelRatio) / screenshotScale);
2920
3038
  const py = Math.round((el.center.y * androidPixelRatio) / screenshotScale);
2921
- const label = el.accessibilityLabel || el.text || el.testID || el.component;
3039
+ const label = el.accessibilityLabel || el.text || el.testID || (el.intent ? `${el.intent} icon` : el.component);
2922
3040
  const idPart = el.testID ? ` testID="${el.testID}"` : "";
2923
3041
  const kindPart = el.isInput ? " [input]" : "";
2924
3042
  const wrapPart = el.isWrapper ? " [wrapper — skip]" : "";
2925
- return ` (${px}, ${py}) ${el.component}: "${label}"${idPart}${kindPart}${wrapPart}`;
3043
+ const nearPart = !el.text && !el.accessibilityLabel && el.nearbyText ? ` near "${el.nearbyText}"` : "";
3044
+ return ` (${px}, ${py}) ${el.component}: "${label}"${nearPart}${idPart}${kindPart}${wrapPart}`;
2926
3045
  }).join("\n");
2927
3046
  }
2928
3047
  }
@@ -2942,34 +3061,35 @@ registerToolWithTelemetry("android_screenshot", {
2942
3061
  if (pressablesText) {
2943
3062
  infoText += `\n\n🎯 Pressable elements (ready-to-tap, coordinates in screenshot pixels):`;
2944
3063
  infoText += `\n${pressablesText}`;
2945
- }
2946
- if (layoutText) {
2947
- infoText += `\n\n📋 Screen Layout (all components, tap coordinates in pixels):`;
2948
- infoText += `\n${layoutText}`;
2949
- }
2950
- if (pressablesText || layoutText) {
2951
3064
  infoText += `\n\n💡 Next steps:`;
2952
3065
  infoText += `\n • tap(text="Button Label") — when text is exact and unique`;
2953
3066
  infoText += `\n • tap(testID="id") or tap(component="Name") — when you know the identifier`;
2954
3067
  infoText += `\n • tap(x=<px>, y=<px>) — use coordinates from the pressable elements list above (reliable for icons and ambiguous elements)`;
2955
- infoText += `\n • inspect_component("ComponentName") — inspect a component from the layout`;
3068
+ infoText += `\n • get_screen_layout — full component tree when you need more than pressables`;
2956
3069
  }
2957
3070
  else {
3071
+ if (!canEnrich && connectedApps.size > 0) {
3072
+ infoText += `\n\nℹ️ Pressable enrichment skipped: no RN app is connected to device ${deviceId ?? "(default)"}.`;
3073
+ infoText += ` ${connectedApps.size} other app(s) are connected on different device(s) — their fiber data was intentionally not used to avoid mismatched output.`;
3074
+ }
2958
3075
  infoText += `\n\n💡 Next steps:`;
2959
3076
  infoText += `\n • tap(text="Button Label") — tap element by visible text`;
2960
3077
  infoText += `\n • tap(x=<px>, y=<px>) — tap at coordinates from this screenshot`;
2961
3078
  infoText += `\n • android_describe_all — get full UI tree with exact tap coordinates`;
2962
3079
  infoText += `\n • android_find_element(text="...") — find element coordinates without tapping`;
2963
3080
  }
2964
- // Check for LogBox overlay (uses default CDP device — native deviceId cannot be mapped to CDP device name)
2965
- try {
2966
- const logBoxState = await detectLogBox();
2967
- if (logBoxState && logBoxState.total > 0) {
2968
- infoText += formatLogBoxWarning(logBoxState);
3081
+ // Check for LogBox overlay — only on the matching RN app; skip otherwise
3082
+ // to avoid surfacing warnings from a different device's app.
3083
+ if (canEnrich) {
3084
+ try {
3085
+ const logBoxState = await detectLogBox(targetDeviceName);
3086
+ if (logBoxState && logBoxState.total > 0) {
3087
+ infoText += formatLogBoxWarning(logBoxState);
3088
+ }
3089
+ }
3090
+ catch {
3091
+ // Non-fatal: LogBox detection failure should not break screenshot
2969
3092
  }
2970
- }
2971
- catch {
2972
- // Non-fatal: LogBox detection failure should not break screenshot
2973
3093
  }
2974
3094
  imageBuffer.add({
2975
3095
  id: `android-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
@@ -3459,10 +3579,15 @@ registerToolWithTelemetry("ios_screenshot", {
3459
3579
  // Build info text with coordinate guidance for iOS
3460
3580
  const pixelWidth = result.originalWidth || 0;
3461
3581
  const pixelHeight = result.originalHeight || 0;
3462
- // Store screenshot metadata for coordinate conversion
3463
- const firstApp = connectedApps.values().next().value;
3464
- if (firstApp) {
3465
- firstApp.lastScreenshot = {
3582
+ // Resolve the RN app running on THIS simulator so enrichment pulls
3583
+ // fiber/layout data from the right app (not the first-connected one,
3584
+ // which may belong to a different simulator).
3585
+ const resolvedUdid = udid || (await getActiveOrBootedSimulatorUdid());
3586
+ const targetApp = resolvedUdid ? getConnectedAppBySimulatorUdid(resolvedUdid) : null;
3587
+ const targetDeviceName = targetApp?.deviceInfo.deviceName;
3588
+ // Store screenshot metadata on the matching app (not an arbitrary one)
3589
+ if (targetApp) {
3590
+ targetApp.lastScreenshot = {
3466
3591
  originalWidth: pixelWidth,
3467
3592
  originalHeight: pixelHeight,
3468
3593
  scaleFactor: result.scaleFactor || 1,
@@ -3514,28 +3639,34 @@ registerToolWithTelemetry("ios_screenshot", {
3514
3639
  pointHeight = Math.round(pixelHeight / scaleFactor);
3515
3640
  }
3516
3641
  const safeAreaOffsetPixels = safeAreaTop * scaleFactor;
3517
- // Enrich with screen layout data (component names + tap coordinates)
3518
- let layoutText = null;
3519
- try {
3520
- layoutText = await enrichScreenshotWithLayout(scaleFactor, result.scaleFactor || 1);
3521
- }
3522
- catch {
3523
- // Non-fatal: screenshot works without layout enrichment
3524
- }
3525
- // Enrich with pressable elements (filtered list of tappable components, converted to screenshot pixel coords)
3642
+ // The Screen Layout tree was previously appended here but produced huge noisy
3643
+ // output (nested Svg/G/Path duplicates). Agents should use get_screen_layout
3644
+ // explicitly when they need the tree. The Pressable elements block below is
3645
+ // the signal most consumers actually want.
3526
3646
  let pressablesText = null;
3647
+ const canEnrich = !!targetApp;
3648
+ // Enrich with pressable elements (filtered list of tappable components, converted to screenshot pixel coords)
3527
3649
  try {
3528
- const pressables = await getPressableElements({});
3650
+ if (!canEnrich)
3651
+ throw new Error("skip");
3652
+ const pressables = await getPressableElements({ device: targetDeviceName });
3529
3653
  if (pressables.success && pressables.parsedElements && pressables.parsedElements.length > 0) {
3530
3654
  const screenshotScale = result.scaleFactor || 1;
3531
3655
  pressablesText = pressables.parsedElements.map((el) => {
3656
+ // See enrichScreenshotWithLayout: shift y when fiber reports the element
3657
+ // inside the safe-area band (a react-native-screens modal artifact).
3658
+ let centerYPoints = el.center.y;
3659
+ if (safeAreaTop > 0 && centerYPoints < safeAreaTop) {
3660
+ centerYPoints += safeAreaTop;
3661
+ }
3532
3662
  const px = Math.round((el.center.x * scaleFactor) / screenshotScale);
3533
- const py = Math.round((el.center.y * scaleFactor) / screenshotScale);
3534
- const label = el.accessibilityLabel || el.text || el.testID || el.component;
3663
+ const py = Math.round((centerYPoints * scaleFactor) / screenshotScale);
3664
+ const label = el.accessibilityLabel || el.text || el.testID || (el.intent ? `${el.intent} icon` : el.component);
3535
3665
  const idPart = el.testID ? ` testID="${el.testID}"` : "";
3536
3666
  const kindPart = el.isInput ? " [input]" : "";
3537
3667
  const wrapPart = el.isWrapper ? " [wrapper — skip]" : "";
3538
- return ` (${px}, ${py}) ${el.component}: "${label}"${idPart}${kindPart}${wrapPart}`;
3668
+ const nearPart = !el.text && !el.accessibilityLabel && el.nearbyText ? ` near "${el.nearbyText}"` : "";
3669
+ return ` (${px}, ${py}) ${el.component}: "${label}"${nearPart}${idPart}${kindPart}${wrapPart}`;
3539
3670
  }).join("\n");
3540
3671
  }
3541
3672
  }
@@ -3552,34 +3683,35 @@ registerToolWithTelemetry("ios_screenshot", {
3552
3683
  if (pressablesText) {
3553
3684
  infoText += `\n\n🎯 Pressable elements (ready-to-tap, coordinates in screenshot pixels):`;
3554
3685
  infoText += `\n${pressablesText}`;
3555
- }
3556
- if (layoutText) {
3557
- infoText += `\n\n📋 Screen Layout (all components, tap coordinates in pixels):`;
3558
- infoText += `\n${layoutText}`;
3559
- }
3560
- if (pressablesText || layoutText) {
3561
3686
  infoText += `\n\n💡 Next steps:`;
3562
3687
  infoText += `\n • tap(text="Button Label") — when text is exact and unique`;
3563
3688
  infoText += `\n • tap(testID="id") or tap(component="Name") — when you know the identifier`;
3564
3689
  infoText += `\n • tap(x=<px>, y=<px>) — use coordinates from the pressable elements list above (reliable for icons and ambiguous elements)`;
3565
- infoText += `\n • inspect_component("ComponentName") — inspect a component from the layout`;
3690
+ infoText += `\n • get_screen_layout — full component tree when you need more than pressables`;
3566
3691
  }
3567
3692
  else {
3693
+ if (!canEnrich && connectedApps.size > 0) {
3694
+ infoText += `\n\nℹ️ Pressable enrichment skipped: no RN app is connected to simulator ${resolvedUdid}.`;
3695
+ infoText += ` ${connectedApps.size} other app(s) are connected on different device(s) — their fiber data was intentionally not used to avoid mismatched output.`;
3696
+ }
3568
3697
  infoText += `\n\n💡 Next steps:`;
3569
3698
  infoText += `\n • tap(text="Button Label") — tap element by visible text`;
3570
3699
  infoText += `\n • tap(x=<px>, y=<px>) — tap at coordinates from this screenshot`;
3571
3700
  infoText += `\n • ios_describe_all — get full UI tree with exact tap coordinates`;
3572
3701
  infoText += `\n • ios_find_element(label="...") — find element coordinates without tapping`;
3573
3702
  }
3574
- // Check for LogBox overlay (uses default CDP device — native UDID cannot be mapped to CDP device name)
3575
- try {
3576
- const logBoxState = await detectLogBox();
3577
- if (logBoxState && logBoxState.total > 0) {
3578
- infoText += formatLogBoxWarning(logBoxState);
3703
+ // Check for LogBox overlay — only on the matching RN app; skip otherwise
3704
+ // to avoid surfacing warnings from a different simulator's app.
3705
+ if (canEnrich) {
3706
+ try {
3707
+ const logBoxState = await detectLogBox(targetDeviceName);
3708
+ if (logBoxState && logBoxState.total > 0) {
3709
+ infoText += formatLogBoxWarning(logBoxState);
3710
+ }
3711
+ }
3712
+ catch {
3713
+ // Non-fatal: LogBox detection failure should not break screenshot
3579
3714
  }
3580
- }
3581
- catch {
3582
- // Non-fatal: LogBox detection failure should not break screenshot
3583
3715
  }
3584
3716
  imageBuffer.add({
3585
3717
  id: `ios-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,