react-native-ai-debugger 1.4.41 → 1.5.1

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)
@@ -185,8 +185,13 @@ function registerToolWithTelemetry(toolName, config, handler, emptyResultDetecto
185
185
  toolRegistry.set(toolName, { config, handler });
186
186
  server.registerTool(toolName, config, async (args) => {
187
187
  // --- Usage limit check (runs on every tool call) ---
188
+ // Promo is client-enforced: while promotionalPeriodEndsAt is in the future,
189
+ // never block — even if the backend cached canUse=false before the promo
190
+ // started, or if a backend bug flips canUse incorrectly.
188
191
  const usageInfo = getUsageInfo();
189
- if (usageInfo && !usageInfo.canUse) {
192
+ const inPromo = !!usageInfo?.promotionalPeriodEndsAt
193
+ && new Date(usageInfo.promotionalPeriodEndsAt).getTime() > Date.now();
194
+ if (!inPromo && usageInfo && !usageInfo.canUse) {
190
195
  const isCredits = usageInfo.creditsRemaining !== null;
191
196
  const dashboardUrl = `${API_BASE_URL}/dashboard/usage`;
192
197
  const message = isCredits
@@ -1494,7 +1499,7 @@ registerToolWithTelemetry("get_screen_layout", {
1494
1499
  });
1495
1500
  // Tool: Get all pressable elements on screen
1496
1501
  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).",
1502
+ 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
1503
  inputSchema: {
1499
1504
  device: z.string().optional().describe("Target device name (substring match). Omit for default device. Run get_apps to see connected devices.")
1500
1505
  }
@@ -1511,15 +1516,130 @@ registerToolWithTelemetry("get_pressable_elements", {
1511
1516
  isError: true
1512
1517
  };
1513
1518
  }
1514
- return {
1515
- content: [
1516
- {
1517
- type: "text",
1518
- text: result.result || "No pressable elements found."
1519
+ const elements = result.parsedElements;
1520
+ if (!elements || elements.length === 0) {
1521
+ return {
1522
+ content: [{ type: "text", text: result.result || "No pressable elements found." }]
1523
+ };
1524
+ }
1525
+ // Resolve target app so we can pick the right simulator / android device
1526
+ const targetApp = device ? getConnectedAppByDevice(device) : getFirstConnectedApp();
1527
+ if (!targetApp) {
1528
+ // No connected app — return raw (points) output; better than nothing
1529
+ return {
1530
+ content: [{ type: "text", text: result.result || "No pressable elements found." }]
1531
+ };
1532
+ }
1533
+ // Capture a lightweight screenshot to learn scaleFactor (downscale) and dimensions.
1534
+ // Conversion: screenshot_px = native_coord * devicePixelRatio / screenshotScale
1535
+ let lines = [];
1536
+ try {
1537
+ if (targetApp.platform === "ios") {
1538
+ const udid = targetApp.simulatorUdid;
1539
+ const shot = await iosScreenshot(undefined, udid);
1540
+ const screenshotScale = shot.scaleFactor || 1;
1541
+ const devicePixelRatio = (shot.originalWidth && shot.originalHeight
1542
+ ? inferIOSDevicePixelRatio(shot.originalWidth, shot.originalHeight)
1543
+ : null) ?? (await getDevicePixelRatio(udid)) ?? 3;
1544
+ // Fallback to 59pt (iPhone typical) when the UI driver preflight can't
1545
+ // resolve the true inset — matches ios_screenshot's default. Without this
1546
+ // shift, react-native-screens modal-presented screens report y relative
1547
+ // to content origin and taps land in the status bar instead of the button.
1548
+ const safeAreaTop = (await getIOSSafeAreaTop(udid).catch(() => 0)) || 59;
1549
+ // Keep the app's lastScreenshot metadata in sync so tap(x, y) uses the
1550
+ // same scaleFactor when converting our pixel coords back to points.
1551
+ if (shot.originalWidth && shot.originalHeight) {
1552
+ targetApp.lastScreenshot = {
1553
+ originalWidth: shot.originalWidth,
1554
+ originalHeight: shot.originalHeight,
1555
+ scaleFactor: screenshotScale
1556
+ };
1519
1557
  }
1520
- ]
1558
+ lines = formatPressablesInPixels(elements, {
1559
+ platform: "ios",
1560
+ devicePixelRatio,
1561
+ screenshotScale,
1562
+ safeAreaTop
1563
+ });
1564
+ }
1565
+ else {
1566
+ const deviceId = targetApp.deviceInfo?.deviceName;
1567
+ const shot = await androidScreenshot(undefined, deviceId);
1568
+ const screenshotScale = shot.scaleFactor || 1;
1569
+ const density = await androidGetDensity(deviceId).catch(() => ({ density: 160 }));
1570
+ const devicePixelRatio = (density.density || 160) / 160;
1571
+ if (shot.originalWidth && shot.originalHeight) {
1572
+ targetApp.lastScreenshot = {
1573
+ originalWidth: shot.originalWidth,
1574
+ originalHeight: shot.originalHeight,
1575
+ scaleFactor: screenshotScale
1576
+ };
1577
+ }
1578
+ lines = formatPressablesInPixels(elements, {
1579
+ platform: "android",
1580
+ devicePixelRatio,
1581
+ screenshotScale,
1582
+ safeAreaTop: 0
1583
+ });
1584
+ }
1585
+ }
1586
+ catch {
1587
+ // Fallback to points if screenshot/metadata unavailable
1588
+ return {
1589
+ content: [{ type: "text", text: result.result || "No pressable elements found." }]
1590
+ };
1591
+ }
1592
+ const iconCount = elements.filter((e) => !e.hasLabel).length;
1593
+ const labeledCount = elements.length - iconCount;
1594
+ const summary = `Found ${elements.length} pressable elements (${iconCount} icon-only, ${labeledCount} with text labels)`;
1595
+ const text = [summary, "", ...lines].join("\n");
1596
+ return {
1597
+ content: [{ type: "text", text }]
1521
1598
  };
1522
1599
  });
1600
+ function formatPressablesInPixels(elements, opts) {
1601
+ const { platform, devicePixelRatio, screenshotScale, safeAreaTop } = opts;
1602
+ const out = [];
1603
+ for (let i = 0; i < elements.length; i++) {
1604
+ const el = elements[i];
1605
+ // react-native-screens modal presentations report y relative to content origin;
1606
+ // shift into the window frame when the measurement falls inside the safe-area band.
1607
+ let cy = el.center.y;
1608
+ let fy = el.frame.y;
1609
+ if (platform === "ios" && safeAreaTop > 0) {
1610
+ if (cy < safeAreaTop)
1611
+ cy += safeAreaTop;
1612
+ if (fy < safeAreaTop)
1613
+ fy += safeAreaTop;
1614
+ }
1615
+ const toPx = (v) => Math.round((v * devicePixelRatio) / screenshotScale);
1616
+ const cx = toPx(el.center.x);
1617
+ const cyPx = toPx(cy);
1618
+ const fx = toPx(el.frame.x);
1619
+ const fyPx = toPx(fy);
1620
+ const fw = toPx(el.frame.width);
1621
+ const fh = toPx(el.frame.height);
1622
+ const num = i + 1;
1623
+ const label = el.hasLabel
1624
+ ? `"${el.text}"`
1625
+ : el.intent
1626
+ ? `(${el.intent} icon)`
1627
+ : "(icon/image)";
1628
+ const ids = [];
1629
+ if (el.testID)
1630
+ ids.push(`testID="${el.testID}"`);
1631
+ if (el.accessibilityLabel)
1632
+ ids.push(`a11y="${el.accessibilityLabel}"`);
1633
+ const idStr = ids.length > 0 ? ` [${ids.join(", ")}]` : "";
1634
+ const inputStr = el.isInput ? " (input)" : "";
1635
+ const wrapperStr = el.isWrapper ? " [wrapper — skip unless dismissing keyboard]" : "";
1636
+ const nearPart = el.nearbyText ? ` near "${el.nearbyText}"` : "";
1637
+ out.push(`${num}. ${el.component} ${label}${nearPart} — center:(${cx},${cyPx}) frame:(${fx},${fyPx} ${fw}x${fh})${idStr}${inputStr}${wrapperStr}`);
1638
+ if (el.path)
1639
+ out.push(` path: ${el.path}`);
1640
+ }
1641
+ return out;
1642
+ }
1523
1643
  // Tool: Inspect a specific component by name
1524
1644
  registerToolWithTelemetry("inspect_component", {
1525
1645
  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 +1865,8 @@ registerToolWithTelemetry("tap", {
1745
1865
  }
1746
1866
  return {
1747
1867
  content,
1748
- isError: !result.success,
1749
- _errorMessage: !result.success
1868
+ isError: !result.success && !result.ambiguous,
1869
+ _errorMessage: !result.success && !result.ambiguous
1750
1870
  ? `${JSON.stringify(result.query)}|${result.error || ""}`
1751
1871
  : undefined,
1752
1872
  _errorContext: errorContext,
@@ -2871,10 +2991,13 @@ registerToolWithTelemetry("android_screenshot", {
2871
2991
  // Build info text with coordinate conversion guidance
2872
2992
  const pixelWidth = result.originalWidth || 0;
2873
2993
  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 = {
2994
+ // Resolve the RN app running on THIS Android device so enrichment
2995
+ // pulls data from the right app (not whichever app is "first").
2996
+ const targetApp = getConnectedAppByAndroidDeviceId(deviceId);
2997
+ const targetDeviceName = targetApp?.deviceInfo.deviceName;
2998
+ // Store screenshot metadata on the matching app (not an arbitrary one)
2999
+ if (targetApp) {
3000
+ targetApp.lastScreenshot = {
2878
3001
  originalWidth: pixelWidth,
2879
3002
  originalHeight: pixelHeight,
2880
3003
  scaleFactor: result.scaleFactor || 1,
@@ -2902,27 +3025,28 @@ registerToolWithTelemetry("android_screenshot", {
2902
3025
  // Enrich with screen layout data (component names + tap coordinates)
2903
3026
  // Android: density ratio = densityDpi / 160 (160dpi = 1dp = 1px baseline)
2904
3027
  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)
3028
+ // Skip enrichment when no RN app is connected to this Android device —
3029
+ // otherwise we would pull fiber data from a different device's app.
3030
+ const canEnrich = !!targetApp;
2913
3031
  let pressablesText = null;
3032
+ // Screen Layout tree previously appended here was dropped — it was noisy
3033
+ // (nested Svg/G/Path duplicates). Use get_screen_layout when the tree is needed.
3034
+ // Enrich with pressable elements (filtered list of tappable components, converted to screenshot pixel coords)
2914
3035
  try {
2915
- const pressables = await getPressableElements({});
3036
+ if (!canEnrich)
3037
+ throw new Error("skip");
3038
+ const pressables = await getPressableElements({ device: targetDeviceName });
2916
3039
  if (pressables.success && pressables.parsedElements && pressables.parsedElements.length > 0) {
2917
3040
  const screenshotScale = result.scaleFactor || 1;
2918
3041
  pressablesText = pressables.parsedElements.map((el) => {
2919
3042
  const px = Math.round((el.center.x * androidPixelRatio) / screenshotScale);
2920
3043
  const py = Math.round((el.center.y * androidPixelRatio) / screenshotScale);
2921
- const label = el.accessibilityLabel || el.text || el.testID || el.component;
3044
+ const label = el.accessibilityLabel || el.text || el.testID || (el.intent ? `${el.intent} icon` : el.component);
2922
3045
  const idPart = el.testID ? ` testID="${el.testID}"` : "";
2923
3046
  const kindPart = el.isInput ? " [input]" : "";
2924
3047
  const wrapPart = el.isWrapper ? " [wrapper — skip]" : "";
2925
- return ` (${px}, ${py}) ${el.component}: "${label}"${idPart}${kindPart}${wrapPart}`;
3048
+ const nearPart = !el.text && !el.accessibilityLabel && el.nearbyText ? ` near "${el.nearbyText}"` : "";
3049
+ return ` (${px}, ${py}) ${el.component}: "${label}"${nearPart}${idPart}${kindPart}${wrapPart}`;
2926
3050
  }).join("\n");
2927
3051
  }
2928
3052
  }
@@ -2942,34 +3066,35 @@ registerToolWithTelemetry("android_screenshot", {
2942
3066
  if (pressablesText) {
2943
3067
  infoText += `\n\n🎯 Pressable elements (ready-to-tap, coordinates in screenshot pixels):`;
2944
3068
  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
3069
  infoText += `\n\n💡 Next steps:`;
2952
3070
  infoText += `\n • tap(text="Button Label") — when text is exact and unique`;
2953
3071
  infoText += `\n • tap(testID="id") or tap(component="Name") — when you know the identifier`;
2954
3072
  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`;
3073
+ infoText += `\n • get_screen_layout — full component tree when you need more than pressables`;
2956
3074
  }
2957
3075
  else {
3076
+ if (!canEnrich && connectedApps.size > 0) {
3077
+ infoText += `\n\nℹ️ Pressable enrichment skipped: no RN app is connected to device ${deviceId ?? "(default)"}.`;
3078
+ infoText += ` ${connectedApps.size} other app(s) are connected on different device(s) — their fiber data was intentionally not used to avoid mismatched output.`;
3079
+ }
2958
3080
  infoText += `\n\n💡 Next steps:`;
2959
3081
  infoText += `\n • tap(text="Button Label") — tap element by visible text`;
2960
3082
  infoText += `\n • tap(x=<px>, y=<px>) — tap at coordinates from this screenshot`;
2961
3083
  infoText += `\n • android_describe_all — get full UI tree with exact tap coordinates`;
2962
3084
  infoText += `\n • android_find_element(text="...") — find element coordinates without tapping`;
2963
3085
  }
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);
3086
+ // Check for LogBox overlay — only on the matching RN app; skip otherwise
3087
+ // to avoid surfacing warnings from a different device's app.
3088
+ if (canEnrich) {
3089
+ try {
3090
+ const logBoxState = await detectLogBox(targetDeviceName);
3091
+ if (logBoxState && logBoxState.total > 0) {
3092
+ infoText += formatLogBoxWarning(logBoxState);
3093
+ }
3094
+ }
3095
+ catch {
3096
+ // Non-fatal: LogBox detection failure should not break screenshot
2969
3097
  }
2970
- }
2971
- catch {
2972
- // Non-fatal: LogBox detection failure should not break screenshot
2973
3098
  }
2974
3099
  imageBuffer.add({
2975
3100
  id: `android-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
@@ -3459,10 +3584,15 @@ registerToolWithTelemetry("ios_screenshot", {
3459
3584
  // Build info text with coordinate guidance for iOS
3460
3585
  const pixelWidth = result.originalWidth || 0;
3461
3586
  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 = {
3587
+ // Resolve the RN app running on THIS simulator so enrichment pulls
3588
+ // fiber/layout data from the right app (not the first-connected one,
3589
+ // which may belong to a different simulator).
3590
+ const resolvedUdid = udid || (await getActiveOrBootedSimulatorUdid());
3591
+ const targetApp = resolvedUdid ? getConnectedAppBySimulatorUdid(resolvedUdid) : null;
3592
+ const targetDeviceName = targetApp?.deviceInfo.deviceName;
3593
+ // Store screenshot metadata on the matching app (not an arbitrary one)
3594
+ if (targetApp) {
3595
+ targetApp.lastScreenshot = {
3466
3596
  originalWidth: pixelWidth,
3467
3597
  originalHeight: pixelHeight,
3468
3598
  scaleFactor: result.scaleFactor || 1,
@@ -3514,28 +3644,34 @@ registerToolWithTelemetry("ios_screenshot", {
3514
3644
  pointHeight = Math.round(pixelHeight / scaleFactor);
3515
3645
  }
3516
3646
  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)
3647
+ // The Screen Layout tree was previously appended here but produced huge noisy
3648
+ // output (nested Svg/G/Path duplicates). Agents should use get_screen_layout
3649
+ // explicitly when they need the tree. The Pressable elements block below is
3650
+ // the signal most consumers actually want.
3526
3651
  let pressablesText = null;
3652
+ const canEnrich = !!targetApp;
3653
+ // Enrich with pressable elements (filtered list of tappable components, converted to screenshot pixel coords)
3527
3654
  try {
3528
- const pressables = await getPressableElements({});
3655
+ if (!canEnrich)
3656
+ throw new Error("skip");
3657
+ const pressables = await getPressableElements({ device: targetDeviceName });
3529
3658
  if (pressables.success && pressables.parsedElements && pressables.parsedElements.length > 0) {
3530
3659
  const screenshotScale = result.scaleFactor || 1;
3531
3660
  pressablesText = pressables.parsedElements.map((el) => {
3661
+ // See enrichScreenshotWithLayout: shift y when fiber reports the element
3662
+ // inside the safe-area band (a react-native-screens modal artifact).
3663
+ let centerYPoints = el.center.y;
3664
+ if (safeAreaTop > 0 && centerYPoints < safeAreaTop) {
3665
+ centerYPoints += safeAreaTop;
3666
+ }
3532
3667
  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;
3668
+ const py = Math.round((centerYPoints * scaleFactor) / screenshotScale);
3669
+ const label = el.accessibilityLabel || el.text || el.testID || (el.intent ? `${el.intent} icon` : el.component);
3535
3670
  const idPart = el.testID ? ` testID="${el.testID}"` : "";
3536
3671
  const kindPart = el.isInput ? " [input]" : "";
3537
3672
  const wrapPart = el.isWrapper ? " [wrapper — skip]" : "";
3538
- return ` (${px}, ${py}) ${el.component}: "${label}"${idPart}${kindPart}${wrapPart}`;
3673
+ const nearPart = !el.text && !el.accessibilityLabel && el.nearbyText ? ` near "${el.nearbyText}"` : "";
3674
+ return ` (${px}, ${py}) ${el.component}: "${label}"${nearPart}${idPart}${kindPart}${wrapPart}`;
3539
3675
  }).join("\n");
3540
3676
  }
3541
3677
  }
@@ -3552,34 +3688,35 @@ registerToolWithTelemetry("ios_screenshot", {
3552
3688
  if (pressablesText) {
3553
3689
  infoText += `\n\n🎯 Pressable elements (ready-to-tap, coordinates in screenshot pixels):`;
3554
3690
  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
3691
  infoText += `\n\n💡 Next steps:`;
3562
3692
  infoText += `\n • tap(text="Button Label") — when text is exact and unique`;
3563
3693
  infoText += `\n • tap(testID="id") or tap(component="Name") — when you know the identifier`;
3564
3694
  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`;
3695
+ infoText += `\n • get_screen_layout — full component tree when you need more than pressables`;
3566
3696
  }
3567
3697
  else {
3698
+ if (!canEnrich && connectedApps.size > 0) {
3699
+ infoText += `\n\nℹ️ Pressable enrichment skipped: no RN app is connected to simulator ${resolvedUdid}.`;
3700
+ infoText += ` ${connectedApps.size} other app(s) are connected on different device(s) — their fiber data was intentionally not used to avoid mismatched output.`;
3701
+ }
3568
3702
  infoText += `\n\n💡 Next steps:`;
3569
3703
  infoText += `\n • tap(text="Button Label") — tap element by visible text`;
3570
3704
  infoText += `\n • tap(x=<px>, y=<px>) — tap at coordinates from this screenshot`;
3571
3705
  infoText += `\n • ios_describe_all — get full UI tree with exact tap coordinates`;
3572
3706
  infoText += `\n • ios_find_element(label="...") — find element coordinates without tapping`;
3573
3707
  }
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);
3708
+ // Check for LogBox overlay — only on the matching RN app; skip otherwise
3709
+ // to avoid surfacing warnings from a different simulator's app.
3710
+ if (canEnrich) {
3711
+ try {
3712
+ const logBoxState = await detectLogBox(targetDeviceName);
3713
+ if (logBoxState && logBoxState.total > 0) {
3714
+ infoText += formatLogBoxWarning(logBoxState);
3715
+ }
3716
+ }
3717
+ catch {
3718
+ // Non-fatal: LogBox detection failure should not break screenshot
3579
3719
  }
3580
- }
3581
- catch {
3582
- // Non-fatal: LogBox detection failure should not break screenshot
3583
3720
  }
3584
3721
  imageBuffer.add({
3585
3722
  id: `ios-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,