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/__tests__/integration/tap-strategies.test.js +4 -3
- package/build/__tests__/integration/tap-strategies.test.js.map +1 -1
- package/build/core/connection.d.ts +14 -0
- package/build/core/connection.d.ts.map +1 -1
- package/build/core/connection.js +49 -0
- package/build/core/connection.js.map +1 -1
- package/build/core/executor.d.ts +3 -1
- package/build/core/executor.d.ts.map +1 -1
- package/build/core/executor.js +269 -24
- package/build/core/executor.js.map +1 -1
- package/build/core/index.d.ts +2 -2
- package/build/core/index.d.ts.map +1 -1
- package/build/core/index.js +2 -2
- package/build/core/index.js.map +1 -1
- package/build/core/ios.d.ts +1 -0
- package/build/core/ios.d.ts.map +1 -1
- package/build/core/ios.js +41 -0
- package/build/core/ios.js.map +1 -1
- package/build/core/license.d.ts.map +1 -1
- package/build/core/license.js +5 -7
- package/build/core/license.js.map +1 -1
- package/build/index.js +212 -75
- package/build/index.js.map +1 -1
- package/build/pro/tap.d.ts +8 -0
- package/build/pro/tap.d.ts.map +1 -1
- package/build/pro/tap.js +188 -21
- package/build/pro/tap.js.map +1 -1
- package/package.json +1 -1
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,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
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
|
-
//
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
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
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 •
|
|
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
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
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
|
-
//
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
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
|
-
//
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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 •
|
|
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
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
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)}`,
|