react-native-ai-debugger 1.6.16 → 1.8.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.
Files changed (68) hide show
  1. package/README.md +8 -1
  2. package/build/__tests__/unit/exec.test.d.ts +2 -0
  3. package/build/__tests__/unit/exec.test.d.ts.map +1 -0
  4. package/build/__tests__/unit/exec.test.js +54 -0
  5. package/build/__tests__/unit/exec.test.js.map +1 -0
  6. package/build/__tests__/unit/focusedInput.test.d.ts +2 -0
  7. package/build/__tests__/unit/focusedInput.test.d.ts.map +1 -0
  8. package/build/__tests__/unit/focusedInput.test.js +58 -0
  9. package/build/__tests__/unit/focusedInput.test.js.map +1 -0
  10. package/build/__tests__/unit/focusedInputTools.test.d.ts +2 -0
  11. package/build/__tests__/unit/focusedInputTools.test.d.ts.map +1 -0
  12. package/build/__tests__/unit/focusedInputTools.test.js +79 -0
  13. package/build/__tests__/unit/focusedInputTools.test.js.map +1 -0
  14. package/build/__tests__/unit/tap.test.js +46 -0
  15. package/build/__tests__/unit/tap.test.js.map +1 -1
  16. package/build/core/android.d.ts +3 -3
  17. package/build/core/android.d.ts.map +1 -1
  18. package/build/core/android.js +21 -14
  19. package/build/core/android.js.map +1 -1
  20. package/build/core/connection.d.ts.map +1 -1
  21. package/build/core/connection.js +3 -2
  22. package/build/core/connection.js.map +1 -1
  23. package/build/core/errors.d.ts +14 -0
  24. package/build/core/errors.d.ts.map +1 -0
  25. package/build/core/errors.js +17 -0
  26. package/build/core/errors.js.map +1 -0
  27. package/build/core/exec.d.ts +22 -0
  28. package/build/core/exec.d.ts.map +1 -0
  29. package/build/core/exec.js +54 -0
  30. package/build/core/exec.js.map +1 -0
  31. package/build/core/executor.d.ts +2 -0
  32. package/build/core/executor.d.ts.map +1 -1
  33. package/build/core/executor.js +232 -2
  34. package/build/core/executor.js.map +1 -1
  35. package/build/core/focusedInput.d.ts +27 -0
  36. package/build/core/focusedInput.d.ts.map +1 -0
  37. package/build/core/focusedInput.js +110 -0
  38. package/build/core/focusedInput.js.map +1 -0
  39. package/build/core/focusedInputTools.d.ts +21 -0
  40. package/build/core/focusedInputTools.d.ts.map +1 -0
  41. package/build/core/focusedInputTools.js +51 -0
  42. package/build/core/focusedInputTools.js.map +1 -0
  43. package/build/core/guides.d.ts.map +1 -1
  44. package/build/core/guides.js +15 -2
  45. package/build/core/guides.js.map +1 -1
  46. package/build/core/index.d.ts +1 -1
  47. package/build/core/index.d.ts.map +1 -1
  48. package/build/core/index.js +1 -1
  49. package/build/core/index.js.map +1 -1
  50. package/build/core/ios.d.ts +8 -0
  51. package/build/core/ios.d.ts.map +1 -1
  52. package/build/core/ios.js +142 -2
  53. package/build/core/ios.js.map +1 -1
  54. package/build/core/ocr.d.ts +13 -0
  55. package/build/core/ocr.d.ts.map +1 -1
  56. package/build/core/ocr.js +37 -15
  57. package/build/core/ocr.js.map +1 -1
  58. package/build/core/posthog.d.ts.map +1 -1
  59. package/build/core/posthog.js +7 -1
  60. package/build/core/posthog.js.map +1 -1
  61. package/build/index.d.ts.map +1 -1
  62. package/build/index.js +158 -31
  63. package/build/index.js.map +1 -1
  64. package/build/pro/tap.d.ts +51 -5
  65. package/build/pro/tap.d.ts.map +1 -1
  66. package/build/pro/tap.js +457 -54
  67. package/build/pro/tap.js.map +1 -1
  68. package/package.json +1 -1
package/build/index.js CHANGED
@@ -9,6 +9,8 @@ import { getGuideOverview, getGuideByTopic, getAvailableTopics, DECISION_TREE }
9
9
  import { getLicenseStatus, getDashboardUrl, getUsageInfo, getPricingInfo, formatPlanPrice } from "./core/license.js";
10
10
  import { API_BASE_URL } from "./core/config.js";
11
11
  import { getPostHogClient, identifyIfDevMode, shutdownPostHog } from "./core/posthog.js";
12
+ import { UserInputError } from "./core/errors.js";
13
+ import { clearFocusedInput, dismissKeyboard, inputTextWithReplace } from "./core/focusedInputTools.js";
12
14
  import { getInstallationId, getServerVersion, getPackageName, isDevMode, TELEMETRY_JSONL_PATH, categorizeError } from "./core/telemetry.js";
13
15
  import { isSDKInstalled, querySDKNetwork, getSDKNetworkEntry, getSDKNetworkStats, clearSDKNetwork, querySDKConsole, getSDKConsoleStats, clearSDKConsole } from "./core/sdkBridge.js";
14
16
  import { reduxDispatch, reduxGetState } from "./core/redux.js";
@@ -36,7 +38,7 @@ ANDROID_KEY_EVENTS, androidLongPress, androidSwipe, androidInputText, androidKey
36
38
  // iOS
37
39
  listIOSSimulators, iosScreenshot, iosInstallApp, iosLaunchApp, iosOpenUrl, iosTerminateApp, iosBootSimulator,
38
40
  // iOS UI driver tools
39
- isUiDriverAvailable, iosButton, iosDescribeAll, IOS_BUTTON_TYPES, getDevicePixelRatio, getIOSSafeAreaTop,
41
+ isUiDriverAvailable, iosButton, iosInputText, iosDescribeAll, detectIOSSystemOverlay, formatIOSSystemOverlayWarning, IOS_BUTTON_TYPES, getDevicePixelRatio, getIOSSafeAreaTop,
40
42
  // Telemetry
41
43
  initTelemetry, trackToolInvocation, getTargetPlatform,
42
44
  // Format utilities (TONL)
@@ -50,7 +52,7 @@ function resolveLogBuffer(device) {
50
52
  if (device) {
51
53
  const app = getConnectedAppByDevice(device);
52
54
  if (!app)
53
- throw new Error(`No connected device matches "${device}"`);
55
+ throw new UserInputError(`No connected device matches "${device}"`);
54
56
  const deviceName = app.deviceInfo.deviceName || app.deviceInfo.title || "unknown";
55
57
  return getLogBuffer(deviceName);
56
58
  }
@@ -68,7 +70,7 @@ function resolveNetworkBuffer(device) {
68
70
  if (device) {
69
71
  const app = getConnectedAppByDevice(device);
70
72
  if (!app)
71
- throw new Error(`No connected device matches "${device}"`);
73
+ throw new UserInputError(`No connected device matches "${device}"`);
72
74
  const deviceName = app.deviceInfo.deviceName || app.deviceInfo.title || "unknown";
73
75
  return getNetworkBuffer(deviceName);
74
76
  }
@@ -331,7 +333,14 @@ function registerToolWithTelemetry(toolName, config, handler, emptyResultDetecto
331
333
  catch (error) {
332
334
  success = false;
333
335
  errorMessage = error instanceof Error ? error.message : String(error);
334
- getPostHogClient()?.captureException(error, getInstallationId(), { tool: toolName, server_version: getServerVersion(), package_name: getPackageName() });
336
+ // H2 (Step 9): UserInputError marks agent-input mistakes (unknown
337
+ // device, missing predicate, ambiguous match). They flow through
338
+ // telemetry's trackToolInvocation in the finally block; we just
339
+ // skip the dedicated error-tracking pipe so the dashboard surfaces
340
+ // real product bugs rather than validation noise.
341
+ if (!(error instanceof UserInputError)) {
342
+ getPostHogClient()?.captureException(error, getInstallationId(), { tool: toolName, server_version: getServerVersion(), package_name: getPackageName() });
343
+ }
335
344
  throw error;
336
345
  }
337
346
  finally {
@@ -727,12 +736,14 @@ registerToolWithTelemetry("tap", {
727
736
  .boolean()
728
737
  .optional()
729
738
  .default(true)
730
- .describe("Capture and return a post-tap screenshot. Enabled by default. Set to false for fastest execution (no screenshots)."),
739
+ .describe("Return post-tap image bytes in the response. Default true. Set to false to drop the PNG bytes — verification still runs (set verify=false to skip that too). Combine with verify=true to get the meaningful/changeRate signal without paying the ~1MB-per-tap bandwidth cost."),
731
740
  verify: z
732
741
  .boolean()
733
742
  .optional()
734
743
  .describe("Run before/after screenshot diff to detect if the tap had a meaningful visual effect. " +
735
- "Default: true for coordinate/accessibility/ocr strategies, false for fiber. Requires screenshot=true."),
744
+ "Default: true for coordinate/accessibility/ocr strategies, false for fiber. " +
745
+ "Independent of `screenshot` — verify can run with screenshot=false (the diff is computed internally; image bytes are dropped). " +
746
+ "When skipped, the response contains `verification: { skipped: true, skippedReason }` so callers can tell apart \"ran clean\" from \"never ran\"."),
736
747
  burst: z
737
748
  .boolean()
738
749
  .optional()
@@ -1292,12 +1303,15 @@ registerToolWithTelemetry("ios_screenshot", {
1292
1303
  // explicitly when they need the tree. The Pressable elements block below is
1293
1304
  // the signal most consumers actually want.
1294
1305
  let pressablesText = null;
1295
- const canEnrich = !!targetApp;
1296
- // Enrich with pressable elements (filtered list of tappable components, converted to screenshot pixel coords)
1306
+ // Enrich with pressable elements. With targetApp present we use the
1307
+ // fiber path (most accurate). Without targetApp, getPressableElements
1308
+ // falls back to iOS accessibility tree (E1) when we pass platform.
1297
1309
  try {
1298
- if (!canEnrich)
1299
- throw new Error("skip");
1300
- const pressables = await getPressableElements({ device: targetDeviceName });
1310
+ const pressables = await getPressableElements({
1311
+ device: targetDeviceName,
1312
+ platform: "ios",
1313
+ udid: resolvedUdid ?? undefined
1314
+ });
1301
1315
  if (pressables.success && pressables.parsedElements && pressables.parsedElements.length > 0) {
1302
1316
  const screenshotScale = result.scaleFactor || 1;
1303
1317
  pressablesText = pressables.parsedElements.map((el) => {
@@ -1338,7 +1352,7 @@ registerToolWithTelemetry("ios_screenshot", {
1338
1352
  infoText += `\n • get_screen_layout — full component tree when you need more than pressables`;
1339
1353
  }
1340
1354
  else {
1341
- if (!canEnrich && connectedApps.size > 0) {
1355
+ if (!targetApp && connectedApps.size > 0) {
1342
1356
  infoText += `\n\nℹ️ Pressable enrichment skipped: no RN app is connected to simulator ${resolvedUdid}.`;
1343
1357
  infoText += ` ${connectedApps.size} other app(s) are connected on different device(s) — their fiber data was intentionally not used to avoid mismatched output.`;
1344
1358
  }
@@ -1349,7 +1363,7 @@ registerToolWithTelemetry("ios_screenshot", {
1349
1363
  }
1350
1364
  // Check for LogBox overlay — only on the matching RN app; skip otherwise
1351
1365
  // to avoid surfacing warnings from a different simulator's app.
1352
- if (canEnrich) {
1366
+ if (targetApp) {
1353
1367
  try {
1354
1368
  const logBoxState = await detectLogBox(targetDeviceName);
1355
1369
  if (logBoxState && logBoxState.total > 0) {
@@ -1360,6 +1374,20 @@ registerToolWithTelemetry("ios_screenshot", {
1360
1374
  // Non-fatal: LogBox detection failure should not break screenshot
1361
1375
  }
1362
1376
  }
1377
+ // I1 (2026-05-16): detect native iOS system overlays (auth sheets, alerts,
1378
+ // permission dialogs) that sit on top of the RN app. The pressables list
1379
+ // reflects the RN screen underneath; without this warning the agent will
1380
+ // happily tap inert RN buttons and loop. Runs whether or not there's a
1381
+ // matching RN app — the overlay belongs to the simulator, not the app.
1382
+ try {
1383
+ const overlay = await detectIOSSystemOverlay(resolvedUdid ?? undefined);
1384
+ if (overlay) {
1385
+ infoText += formatIOSSystemOverlayWarning(overlay);
1386
+ }
1387
+ }
1388
+ catch {
1389
+ // Non-fatal: overlay detection failure should not break screenshot
1390
+ }
1363
1391
  imageBuffer.add({
1364
1392
  id: `ios-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
1365
1393
  image: result.data,
@@ -1464,25 +1492,30 @@ registerToolWithTelemetry("android_screenshot", {
1464
1492
  catch {
1465
1493
  // Use defaults
1466
1494
  }
1467
- // Enrich with screen layout data (component names + tap coordinates)
1468
- // Android: density ratio = densityDpi / 160 (160dpi = 1dp = 1px baseline)
1469
- const androidPixelRatio = densityDpi / 160;
1470
- // Skip enrichment when no RN app is connected to this Android device —
1471
- // otherwise we would pull fiber data from a different device's app.
1472
- const canEnrich = !!targetApp;
1495
+ // Enrich with screen layout data (component names + tap coordinates).
1496
+ // On Bridgeless/Fabric Android (the only target architecture we support;
1497
+ // legacy arch is <5% of users and not a priority), both code paths that
1498
+ // feed this enrichment return DEVICE PIXELS:
1499
+ // - fiber path: React's measureInWindow on Fabric returns native pixels.
1500
+ // - a11y fallback: uiautomator's bounds are already device pixels.
1501
+ // The earlier formula multiplied by densityDpi/160 on the (incorrect)
1502
+ // assumption that fiber returned DP — inflating every coordinate by
1503
+ // ~2.6× on a 420dpi emulator and producing numbers like (1170, 4054)
1504
+ // for a button visually sitting near (445, 1370) in the JPEG. Drop the
1505
+ // density factor; only the scaleFactor downscale is needed.
1473
1506
  let pressablesText = null;
1474
1507
  // Screen Layout tree previously appended here was dropped — it was noisy
1475
1508
  // (nested Svg/G/Path duplicates). Use get_screen_layout when the tree is needed.
1476
- // Enrich with pressable elements (filtered list of tappable components, converted to screenshot pixel coords)
1509
+ // Enrich with pressable elements. With targetApp present we use the
1510
+ // fiber path (most accurate). Without targetApp, getPressableElements
1511
+ // falls back to the Android accessibility (uiautomator) tree (E1).
1477
1512
  try {
1478
- if (!canEnrich)
1479
- throw new Error("skip");
1480
- const pressables = await getPressableElements({ device: targetDeviceName });
1513
+ const pressables = await getPressableElements({ device: targetDeviceName, platform: "android" });
1481
1514
  if (pressables.success && pressables.parsedElements && pressables.parsedElements.length > 0) {
1482
1515
  const screenshotScale = result.scaleFactor || 1;
1483
1516
  pressablesText = pressables.parsedElements.map((el) => {
1484
- const px = Math.round((el.center.x * androidPixelRatio) / screenshotScale);
1485
- const py = Math.round((el.center.y * androidPixelRatio) / screenshotScale);
1517
+ const px = Math.round(el.center.x / screenshotScale);
1518
+ const py = Math.round(el.center.y / screenshotScale);
1486
1519
  const label = el.accessibilityLabel || el.text || el.testID || (el.intent ? `${el.intent} icon` : el.component);
1487
1520
  const idPart = el.testID ? ` testID="${el.testID}"` : "";
1488
1521
  const kindPart = el.isInput ? " [input]" : "";
@@ -1515,7 +1548,7 @@ registerToolWithTelemetry("android_screenshot", {
1515
1548
  infoText += `\n • get_screen_layout — full component tree when you need more than pressables`;
1516
1549
  }
1517
1550
  else {
1518
- if (!canEnrich && connectedApps.size > 0) {
1551
+ if (!targetApp && connectedApps.size > 0) {
1519
1552
  infoText += `\n\nℹ️ Pressable enrichment skipped: no RN app is connected to device ${deviceId ?? "(default)"}.`;
1520
1553
  infoText += ` ${connectedApps.size} other app(s) are connected on different device(s) — their fiber data was intentionally not used to avoid mismatched output.`;
1521
1554
  }
@@ -1526,7 +1559,7 @@ registerToolWithTelemetry("android_screenshot", {
1526
1559
  }
1527
1560
  // Check for LogBox overlay — only on the matching RN app; skip otherwise
1528
1561
  // to avoid surfacing warnings from a different device's app.
1529
- if (canEnrich) {
1562
+ if (targetApp) {
1530
1563
  try {
1531
1564
  const logBoxState = await detectLogBox(targetDeviceName);
1532
1565
  if (logBoxState && logBoxState.total > 0) {
@@ -1768,7 +1801,7 @@ registerToolWithTelemetry("clear_logs", {
1768
1801
  if (device) {
1769
1802
  const app = getConnectedAppByDevice(device);
1770
1803
  if (!app)
1771
- throw new Error(`No connected device matches "${device}"`);
1804
+ throw new UserInputError(`No connected device matches "${device}"`);
1772
1805
  const deviceName = app.deviceInfo.deviceName || app.deviceInfo.title || "unknown";
1773
1806
  const count = getLogBuffer(deviceName).clear();
1774
1807
  return { content: [{ type: "text", text: `Cleared ${count} log entries from ${deviceName}.` }] };
@@ -3190,7 +3223,7 @@ registerToolWithTelemetry("clear_network", {
3190
3223
  if (device) {
3191
3224
  const app = getConnectedAppByDevice(device);
3192
3225
  if (!app)
3193
- throw new Error(`No connected device matches "${device}"`);
3226
+ throw new UserInputError(`No connected device matches "${device}"`);
3194
3227
  const deviceName = app.deviceInfo.deviceName || app.deviceInfo.title || "unknown";
3195
3228
  totalCleared = getNetworkBuffer(deviceName).clear();
3196
3229
  }
@@ -3863,16 +3896,25 @@ registerToolWithTelemetry("android_input_text", {
3863
3896
  " The text will be input at the current focus point (tap an input field first)." +
3864
3897
  "\nPURPOSE: Send keystrokes to whichever input currently has focus on Android — the tool does NOT focus a field itself." +
3865
3898
  "\nWHEN TO USE: Only after an input is already focused, or when `tap(text=...)` on the input didn't take focus for some reason." +
3899
+ "\nREPLACE MODE: pass replace:true to clear the focused field first (via React onChangeText so controlled state stays consistent), then type the new value. Use for pre-filled fields where appending would corrupt the value." +
3866
3900
  "\nSEE ALSO: call get_usage_guide(topic=\"interact\") for the full UI-interaction playbook.",
3867
3901
  inputSchema: {
3868
3902
  text: z.string().describe("Text to type"),
3903
+ replace: z
3904
+ .boolean()
3905
+ .optional()
3906
+ .describe("If true, clear the focused TextInput via React onChangeText before typing. Use to set a pre-filled field to an exact value without concatenation. Requires Bridgeless/Fabric."),
3907
+ device: z
3908
+ .string()
3909
+ .optional()
3910
+ .describe("Optional RN device name (substring match) — needed by replace:true when multiple RN apps are connected, to disambiguate which device's focused input to clear. Single-device sessions can omit."),
3869
3911
  deviceId: z
3870
3912
  .string()
3871
3913
  .optional()
3872
3914
  .describe("Optional device ID. Uses first available device if not specified.")
3873
3915
  }
3874
- }, async ({ text, deviceId }) => {
3875
- const result = await androidInputText(text, deviceId);
3916
+ }, async ({ text, replace, device, deviceId }) => {
3917
+ const result = await inputTextWithReplace(text, replace === true, (t) => androidInputText(t, deviceId), () => clearFocusedInput(device));
3876
3918
  return {
3877
3919
  content: [
3878
3920
  {
@@ -4254,6 +4296,91 @@ server.registerTool("ios_button", {
4254
4296
  isError: !result.success
4255
4297
  };
4256
4298
  });
4299
+ // Tool: Clear focused text input
4300
+ registerToolWithTelemetry("clear_focused_input", {
4301
+ description: "Clear the contents of the currently focused TextInput, updating React state correctly so controlled components (Formik, react-hook-form, useState) stay consistent." +
4302
+ "\nPURPOSE: Reset whatever TextInput has focus to empty, with the React state owner notified via onChangeText. Use BEFORE typing a replacement value into a pre-filled field." +
4303
+ "\nWHEN TO USE: After tap(testID=...) focuses an input that already has text. Pair with ios_input_text/android_input_text (or use their replace:true flag for one-shot)." +
4304
+ "\nLIMITATIONS: Requires Bridgeless/Fabric (RN new architecture). Returns 'no focused TextInput' if nothing is focused — does not silently no-op." +
4305
+ "\nSEE ALSO: dismiss_keyboard, ios_input_text({replace:true}), android_input_text({replace:true}). call get_usage_guide(topic=\"interact\") for the full UI-interaction playbook.",
4306
+ inputSchema: {
4307
+ device: z
4308
+ .string()
4309
+ .optional()
4310
+ .describe("Optional device name (substring match). Uses default device if not specified.")
4311
+ }
4312
+ }, async ({ device }) => {
4313
+ const result = await clearFocusedInput(device);
4314
+ return {
4315
+ content: [
4316
+ {
4317
+ type: "text",
4318
+ text: result.success ? `Cleared focused input (via ${result.via}).` : `Error: ${result.error}`
4319
+ }
4320
+ ],
4321
+ isError: !result.success
4322
+ };
4323
+ });
4324
+ // Tool: Dismiss keyboard
4325
+ registerToolWithTelemetry("dismiss_keyboard", {
4326
+ description: "Blur the currently focused TextInput, dismissing the on-screen keyboard." +
4327
+ "\nPURPOSE: Close the keyboard when it's blocking content beneath the input, or move focus off an input before a tap that would otherwise be intercepted." +
4328
+ "\nWHEN TO USE: After typing into a field and before tapping a button that is hidden by the keyboard. Or to verify a 'tap outside dismisses' UX is wired up." +
4329
+ "\nLIMITATIONS: Requires Bridgeless/Fabric (RN new architecture). Returns 'no focused TextInput' if nothing is focused.",
4330
+ inputSchema: {
4331
+ device: z
4332
+ .string()
4333
+ .optional()
4334
+ .describe("Optional device name (substring match). Uses default device if not specified.")
4335
+ }
4336
+ }, async ({ device }) => {
4337
+ const result = await dismissKeyboard(device);
4338
+ return {
4339
+ content: [
4340
+ {
4341
+ type: "text",
4342
+ text: result.success
4343
+ ? `Dismissed keyboard (nativeTag ${result.nativeTag}).`
4344
+ : `Error: ${result.error}`
4345
+ }
4346
+ ],
4347
+ isError: !result.success
4348
+ };
4349
+ });
4350
+ // Tool: iOS input text
4351
+ registerToolWithTelemetry("ios_input_text", {
4352
+ description: "Type text on an iOS simulator." +
4353
+ platformFallbackBanner("`tap(text=...)` — it auto-focuses TextInput via the fiber tree") +
4354
+ " The text is typed into whichever field currently has focus (tap an input first). Mirrors `android_input_text` so cross-platform agents can use `<platform>_input_text` without branching on the iOS driver shell-out." +
4355
+ "\nPURPOSE: Send keystrokes to the focused field on an iOS simulator via the active UI driver (AXe — preferred — or IDB)." +
4356
+ "\nWHEN TO USE: Only after an input is already focused, or when `tap(testID=...)` on the input didn't take focus for some reason. Use the testID-first flow whenever possible — it's faster and survives UI repositioning." +
4357
+ "\nREPLACE MODE: pass replace:true to clear the focused field first (via React onChangeText so controlled state stays consistent), then type the new value. Use for pre-filled fields where appending would corrupt the value." +
4358
+ "\nLIMITATIONS: AXe types via the US-keyboard HID — non-ASCII characters (Cyrillic, CJK, Arabic) may not transmit correctly. If the active driver is AXe and the text contains non-ASCII chars, prefer pasting via the simulator pasteboard or setting IOS_DRIVER=idb." +
4359
+ "\nSEE ALSO: call get_usage_guide(topic=\"interact\") for the full UI-interaction playbook.",
4360
+ inputSchema: {
4361
+ text: z.string().describe("Text to type into the currently focused field."),
4362
+ replace: z
4363
+ .boolean()
4364
+ .optional()
4365
+ .describe("If true, clear the focused TextInput via React onChangeText before typing. Use to set a pre-filled field to an exact value without concatenation. Requires Bridgeless/Fabric."),
4366
+ device: z
4367
+ .string()
4368
+ .optional()
4369
+ .describe("Optional RN device name (substring match) — needed by replace:true when multiple RN apps are connected, to disambiguate which device's focused input to clear. Single-device sessions can omit."),
4370
+ udid: z.string().optional().describe("Optional simulator UDID (from list_ios_simulators). Uses booted simulator if not specified.")
4371
+ }
4372
+ }, async ({ text, replace, device, udid }) => {
4373
+ const result = await inputTextWithReplace(text, replace === true, (t) => iosInputText(t, udid), () => clearFocusedInput(device));
4374
+ return {
4375
+ content: [
4376
+ {
4377
+ type: "text",
4378
+ text: result.success ? result.result : `Error: ${result.error}`
4379
+ }
4380
+ ],
4381
+ isError: !result.success
4382
+ };
4383
+ });
4257
4384
  // Tool: Activate Pro license
4258
4385
  registerToolWithTelemetry("activate_license", getActivateLicenseConfig(), handleActivateLicense);
4259
4386
  // Tool: Delete account