@mobileai/react-native 0.9.18 → 0.9.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/LICENSE +28 -20
  2. package/MobileAIFloatingOverlay.podspec +25 -0
  3. package/android/build.gradle +61 -0
  4. package/android/src/main/AndroidManifest.xml +3 -0
  5. package/android/src/main/java/com/mobileai/overlay/FloatingOverlayView.kt +151 -0
  6. package/android/src/main/java/com/mobileai/overlay/MobileAIOverlayPackage.kt +23 -0
  7. package/android/src/newarch/com/mobileai/overlay/FloatingOverlayViewManager.kt +45 -0
  8. package/android/src/oldarch/com/mobileai/overlay/FloatingOverlayViewManager.kt +29 -0
  9. package/ios/MobileAIFloatingOverlayComponentView.mm +73 -0
  10. package/lib/module/components/AIAgent.js +902 -136
  11. package/lib/module/components/AIConsentDialog.js +439 -0
  12. package/lib/module/components/AgentChatBar.js +828 -134
  13. package/lib/module/components/AgentOverlay.js +2 -1
  14. package/lib/module/components/DiscoveryTooltip.js +21 -9
  15. package/lib/module/components/FloatingOverlayWrapper.js +108 -0
  16. package/lib/module/components/Icons.js +123 -0
  17. package/lib/module/config/endpoints.js +12 -2
  18. package/lib/module/core/AgentRuntime.js +373 -27
  19. package/lib/module/core/FiberAdapter.js +56 -0
  20. package/lib/module/core/FiberTreeWalker.js +186 -80
  21. package/lib/module/core/IdleDetector.js +19 -0
  22. package/lib/module/core/NativeAlertInterceptor.js +191 -0
  23. package/lib/module/core/systemPrompt.js +203 -45
  24. package/lib/module/index.js +3 -0
  25. package/lib/module/providers/GeminiProvider.js +72 -56
  26. package/lib/module/providers/ProviderFactory.js +6 -2
  27. package/lib/module/services/AudioInputService.js +3 -12
  28. package/lib/module/services/AudioOutputService.js +1 -13
  29. package/lib/module/services/ConversationService.js +166 -0
  30. package/lib/module/services/MobileAIKnowledgeRetriever.js +41 -0
  31. package/lib/module/services/VoiceService.js +29 -8
  32. package/lib/module/services/telemetry/MobileAI.js +44 -0
  33. package/lib/module/services/telemetry/TelemetryService.js +13 -1
  34. package/lib/module/services/telemetry/TouchAutoCapture.js +44 -18
  35. package/lib/module/specs/FloatingOverlayNativeComponent.ts +19 -0
  36. package/lib/module/support/CSATSurvey.js +95 -12
  37. package/lib/module/support/EscalationSocket.js +70 -1
  38. package/lib/module/support/ReportedIssueEventSource.js +148 -0
  39. package/lib/module/support/escalateTool.js +4 -2
  40. package/lib/module/support/index.js +1 -0
  41. package/lib/module/support/reportIssueTool.js +127 -0
  42. package/lib/module/support/supportPrompt.js +77 -9
  43. package/lib/module/tools/guideTool.js +2 -1
  44. package/lib/module/tools/longPressTool.js +4 -3
  45. package/lib/module/tools/pickerTool.js +6 -4
  46. package/lib/module/tools/tapTool.js +12 -3
  47. package/lib/module/tools/typeTool.js +19 -10
  48. package/lib/module/utils/logger.js +175 -6
  49. package/lib/typescript/react-native.config.d.ts +11 -0
  50. package/lib/typescript/src/components/AIAgent.d.ts +28 -2
  51. package/lib/typescript/src/components/AIConsentDialog.d.ts +153 -0
  52. package/lib/typescript/src/components/AgentChatBar.d.ts +15 -2
  53. package/lib/typescript/src/components/DiscoveryTooltip.d.ts +3 -1
  54. package/lib/typescript/src/components/FloatingOverlayWrapper.d.ts +51 -0
  55. package/lib/typescript/src/components/Icons.d.ts +8 -0
  56. package/lib/typescript/src/config/endpoints.d.ts +5 -3
  57. package/lib/typescript/src/core/AgentRuntime.d.ts +4 -0
  58. package/lib/typescript/src/core/FiberAdapter.d.ts +25 -0
  59. package/lib/typescript/src/core/FiberTreeWalker.d.ts +2 -0
  60. package/lib/typescript/src/core/IdleDetector.d.ts +11 -0
  61. package/lib/typescript/src/core/NativeAlertInterceptor.d.ts +55 -0
  62. package/lib/typescript/src/core/types.d.ts +106 -1
  63. package/lib/typescript/src/index.d.ts +9 -4
  64. package/lib/typescript/src/providers/GeminiProvider.d.ts +6 -5
  65. package/lib/typescript/src/services/ConversationService.d.ts +55 -0
  66. package/lib/typescript/src/services/MobileAIKnowledgeRetriever.d.ts +9 -0
  67. package/lib/typescript/src/services/telemetry/MobileAI.d.ts +7 -0
  68. package/lib/typescript/src/services/telemetry/TelemetryService.d.ts +1 -1
  69. package/lib/typescript/src/services/telemetry/TouchAutoCapture.d.ts +9 -6
  70. package/lib/typescript/src/services/telemetry/types.d.ts +3 -1
  71. package/lib/typescript/src/specs/FloatingOverlayNativeComponent.d.ts +17 -0
  72. package/lib/typescript/src/support/EscalationSocket.d.ts +17 -0
  73. package/lib/typescript/src/support/ReportedIssueEventSource.d.ts +24 -0
  74. package/lib/typescript/src/support/escalateTool.d.ts +5 -0
  75. package/lib/typescript/src/support/index.d.ts +2 -1
  76. package/lib/typescript/src/support/reportIssueTool.d.ts +20 -0
  77. package/lib/typescript/src/support/types.d.ts +56 -1
  78. package/lib/typescript/src/utils/logger.d.ts +15 -0
  79. package/package.json +19 -5
  80. package/react-native.config.js +12 -0
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * FiberAdapter — Defense in depth for React internals.
5
+ *
6
+ * Centralizes all direct access to React Fiber internal properties.
7
+ * If React renames an internal property (e.g., in React 19/20), we only
8
+ * need to update it here instead of auditing the entire codebase.
9
+ *
10
+ * These are intentionally simple getter functions, not a complex class abstraction,
11
+ * to ensure maximum performance during tree walk.
12
+ */
13
+
14
+ export function getChild(node) {
15
+ return node?.child ?? null;
16
+ }
17
+ export function getSibling(node) {
18
+ return node?.sibling ?? null;
19
+ }
20
+ export function getParent(node) {
21
+ return node?.return ?? null;
22
+ }
23
+ export function getProps(node) {
24
+ return node?.memoizedProps || {};
25
+ }
26
+ export function getStateNode(node) {
27
+ return node?.stateNode ?? null;
28
+ }
29
+ export function getType(node) {
30
+ return node?.type ?? null;
31
+ }
32
+ export function getDisplayName(node) {
33
+ return node?.type?.displayName ?? null;
34
+ }
35
+
36
+ /**
37
+ * Common heuristic to find the Fiber node attached to a native view.
38
+ *
39
+ * Old Architecture (Bridge): __reactFiber$<hash> or __reactInternalInstance$<hash>
40
+ * New Architecture (Fabric): __internalInstanceHandle (ReactNativeElement)
41
+ */
42
+ export function getFiberFromNativeNode(nativeNode) {
43
+ if (!nativeNode) return null;
44
+
45
+ // Old Architecture: __reactFiber$ / __reactInternalInstance$
46
+ const key = Object.keys(nativeNode).find(k => k.startsWith('__reactFiber$') || k.startsWith('__reactInternalInstance$'));
47
+ if (key) return nativeNode[key];
48
+
49
+ // New Architecture (Fabric): __internalInstanceHandle
50
+ const handle = nativeNode.__internalInstanceHandle;
51
+ if (handle && (handle.child !== undefined || handle.memoizedProps !== undefined)) {
52
+ return handle;
53
+ }
54
+ return null;
55
+ }
56
+ //# sourceMappingURL=FiberAdapter.js.map
@@ -10,6 +10,8 @@
10
10
  */
11
11
 
12
12
  import { logger } from "../utils/logger.js";
13
+ import { getChild, getSibling, getParent, getProps, getStateNode, getType, getDisplayName } from "./FiberAdapter.js";
14
+ import { getActiveAlert } from "./NativeAlertInterceptor.js";
13
15
 
14
16
  // ─── Walk Configuration ─────────
15
17
 
@@ -30,6 +32,7 @@ const VIDEO_TYPES = new Set(['Video', 'ExpoVideo', 'RCTVideo', 'VideoPlayer', 'V
30
32
 
31
33
  // Known RN internal component names to skip when walking up for context
32
34
  const RN_INTERNAL_NAMES = new Set(['View', 'RCTView', 'Pressable', 'TouchableOpacity', 'TouchableHighlight', 'ScrollView', 'RCTScrollView', 'FlatList', 'SectionList', 'SafeAreaView', 'RNCSafeAreaView', 'KeyboardAvoidingView', 'Modal', 'StatusBar', 'Text', 'RCTText', 'AnimatedComponent', 'AnimatedComponentWrapper', 'Animated']);
35
+ const LOW_SIGNAL_RUNTIME_LABELS = new Set(['button', 'buttons', 'label', 'labels', 'title', 'titles', 'name', 'text', 'value', 'values', 'content', 'card', 'cards', 'row', 'rows', 'item', 'items', 'component', 'screen']);
33
36
 
34
37
  // ─── State Extraction ──
35
38
 
@@ -85,14 +88,14 @@ export function hasAnyEventHandler(props) {
85
88
  * This provides semantic context for media elements (e.g., an Image inside "ProfileHeader").
86
89
  */
87
90
  function getNearestCustomComponentName(fiber, maxDepth = 8) {
88
- let current = fiber?.return;
91
+ let current = getParent(fiber);
89
92
  let depth = 0;
90
93
  while (current && depth < maxDepth) {
91
94
  const name = getComponentName(current);
92
95
  if (name && !RN_INTERNAL_NAMES.has(name) && !PRESSABLE_TYPES.has(name)) {
93
96
  return name;
94
97
  }
95
- current = current.return;
98
+ current = getParent(current);
96
99
  depth++;
97
100
  }
98
101
  return null;
@@ -104,18 +107,20 @@ function getNearestCustomComponentName(fiber, maxDepth = 8) {
104
107
  * Get the display name of a Fiber node's component type.
105
108
  */
106
109
  function getComponentName(fiber) {
107
- if (!fiber || !fiber.type) return null;
110
+ const type = getType(fiber);
111
+ if (!type) return null;
108
112
 
109
113
  // Host components (View, Text, etc.) — type is a string
110
- if (typeof fiber.type === 'string') return fiber.type;
114
+ if (typeof type === 'string') return type;
111
115
 
112
116
  // Function/Class components — type has displayName or name
113
- if (fiber.type.displayName) return fiber.type.displayName;
114
- if (fiber.type.name) return fiber.type.name;
117
+ const displayName = getDisplayName(fiber);
118
+ if (displayName) return displayName;
119
+ if (type.name) return type.name;
115
120
 
116
121
  // ForwardRef components
117
- if (fiber.type.render?.displayName) return fiber.type.render.displayName;
118
- if (fiber.type.render?.name) return fiber.type.render.name;
122
+ if (type.render?.displayName) return type.render.displayName;
123
+ if (type.render?.name) return type.render.name;
119
124
  return null;
120
125
  }
121
126
 
@@ -124,7 +129,7 @@ function getComponentName(fiber) {
124
129
  */
125
130
  function getElementType(fiber) {
126
131
  const name = getComponentName(fiber);
127
- const props = fiber.memoizedProps || {};
132
+ const props = getProps(fiber);
128
133
 
129
134
  // Check by component name (known React Native types)
130
135
  if (name && PRESSABLE_TYPES.has(name)) return 'pressable';
@@ -164,7 +169,7 @@ function getElementType(fiber) {
164
169
  * Check if element is disabled.
165
170
  */
166
171
  function isDisabled(fiber) {
167
- const props = fiber.memoizedProps || {};
172
+ const props = getProps(fiber);
168
173
  return props.disabled === true || props.editable === false;
169
174
  }
170
175
 
@@ -180,10 +185,10 @@ function isDisabled(fiber) {
180
185
  function extractDeepTextContent(fiber, maxDepth = 10) {
181
186
  if (!fiber || maxDepth <= 0) return '';
182
187
  const parts = [];
183
- let child = fiber.child;
188
+ let child = getChild(fiber);
184
189
  while (child) {
185
190
  const childName = getComponentName(child);
186
- const childProps = child.memoizedProps || {};
191
+ const childProps = getProps(child);
187
192
 
188
193
  // Text node — extract content
189
194
  if (childName && TEXT_TYPES.has(childName)) {
@@ -199,7 +204,7 @@ function extractDeepTextContent(fiber, maxDepth = 10) {
199
204
  const nestedText = extractDeepTextContent(child, maxDepth - 1);
200
205
  if (nestedText) parts.push(nestedText);
201
206
  }
202
- child = child.sibling;
207
+ child = getSibling(child);
203
208
  }
204
209
  return parts.join(' ').trim();
205
210
  }
@@ -217,6 +222,38 @@ function isIconGlyph(text) {
217
222
  const code = trimmed.codePointAt(0) || 0;
218
223
  return code >= 0xE000 && code <= 0xF8FF || code >= 0xF0000 && code <= 0xFFFFF || code >= 0x100000 && code <= 0x10FFFF;
219
224
  }
225
+ function normalizeRuntimeLabel(text) {
226
+ if (!text) return '';
227
+ return String(text).replace(/\s+/g, ' ').trim();
228
+ }
229
+ function scoreRuntimeLabel(text, source) {
230
+ const normalized = normalizeRuntimeLabel(text);
231
+ if (!normalized) return Number.NEGATIVE_INFINITY;
232
+ let score = 0;
233
+ const words = normalized.split(/\s+/).filter(Boolean);
234
+ const lowered = normalized.toLowerCase();
235
+ if (source === 'deep-text') score += 70;
236
+ if (source === 'accessibility') score += 80;
237
+ if (source === 'placeholder') score += 25;
238
+ if (source === 'context') score += 10;
239
+ if (source === 'icon') score -= 10;
240
+ if (source === 'test-id') score -= 25;
241
+ if (normalized.length >= 3 && normalized.length <= 32) score += 20;else if (normalized.length > 60) score -= 10;
242
+ if (words.length >= 2 && words.length <= 8) score += 20;else if (words.length === 1) score += 5;
243
+ if (/^[A-Z]/.test(normalized)) score += 8;
244
+ if (/[A-Za-z]/.test(normalized) && !/[_./]/.test(normalized)) score += 12;
245
+ if (/[_./]/.test(normalized)) score -= 20;
246
+ if (LOW_SIGNAL_RUNTIME_LABELS.has(lowered)) score -= 120;
247
+ if (source === 'accessibility' && !LOW_SIGNAL_RUNTIME_LABELS.has(lowered)) score += 15;
248
+ return score;
249
+ }
250
+ function chooseBestRuntimeLabel(candidates) {
251
+ const best = candidates.map(candidate => ({
252
+ text: normalizeRuntimeLabel(candidate.text),
253
+ score: scoreRuntimeLabel(String(candidate.text || ''), candidate.source)
254
+ })).filter(candidate => candidate.text).sort((a, b) => b.score - a.score)[0];
255
+ return best?.text || null;
256
+ }
220
257
 
221
258
  /**
222
259
  * Extract raw text from React children prop.
@@ -249,10 +286,10 @@ function extractRawText(children) {
249
286
  */
250
287
  function extractIconName(fiber, maxDepth = 5) {
251
288
  if (!fiber || maxDepth <= 0) return '';
252
- let child = fiber.child;
289
+ let child = getChild(fiber);
253
290
  while (child) {
254
291
  const componentName = getComponentName(child);
255
- const childProps = child.memoizedProps || {};
292
+ const childProps = getProps(child);
256
293
 
257
294
  // Generic icon detection: non-RN-internal component with a string `name` prop
258
295
  if (componentName && !RN_INTERNAL_NAMES.has(componentName) && !PRESSABLE_TYPES.has(componentName) && !TEXT_INPUT_TYPES.has(componentName) && typeof childProps.name === 'string' && childProps.name.length > 0) {
@@ -262,7 +299,7 @@ function extractIconName(fiber, maxDepth = 5) {
262
299
  // Recurse into ALL children (pierce through nested interactives)
263
300
  const found = extractIconName(child, maxDepth - 1);
264
301
  if (found) return found;
265
- child = child.sibling;
302
+ child = getSibling(child);
266
303
  }
267
304
  return '';
268
305
  }
@@ -316,33 +353,70 @@ function getFiberRootFromDevTools() {
316
353
  function getFiberFromRef(ref) {
317
354
  if (!ref) return null;
318
355
 
319
- // Strategy 1: __reactFiber$ keys (React DOM/RN style)
320
- // Works in both dev AND release builds — primary strategy.
356
+ // Strategy 1a: __reactFiber$ keys (Old Architecture / Bridge)
357
+ // Strategy 1b: __internalInstanceHandle (New Architecture / Fabric)
358
+ //
359
+ // In the Old Architecture, React attaches __reactFiber$<hash> to native nodes.
360
+ // In the New Architecture (Fabric, RN 0.73+), native refs are ReactNativeElement
361
+ // instances with __internalInstanceHandle — which IS the Fiber node directly.
362
+ // Both strategies are checked in a single Object.keys pass for performance.
321
363
  try {
322
364
  const keys = Object.keys(ref);
365
+ logger.info('FiberTreeWalker', `Ref keys (${keys.length}): ${keys.join(', ')}`);
366
+
367
+ // 1a: __reactFiber$ / __reactInternalInstance$ (Old Architecture)
323
368
  const fiberKey = keys.find(key => key.startsWith('__reactFiber$') || key.startsWith('__reactInternalInstance$'));
324
369
  if (fiberKey) {
325
- logger.debug('FiberTreeWalker', 'Accessed Fiber tree via __reactFiber$ key');
326
- return ref[fiberKey];
370
+ const fiber = ref[fiberKey];
371
+ logger.info('FiberTreeWalker', `✅ Strategy 1a SUCCESS (Old Arch): Fiber via "${fiberKey}" (has child: ${!!fiber?.child})`);
372
+ return fiber;
327
373
  }
328
- } catch {
329
- // Object.keys may fail on some native nodes
374
+
375
+ // 1b: __internalInstanceHandle (New Architecture / Fabric)
376
+ // ReactNativeElement stores the Fiber node reference here.
377
+ // It has .child, .sibling, .return, .memoizedProps — the full Fiber structure.
378
+ if (keys.includes('__internalInstanceHandle')) {
379
+ const handle = ref.__internalInstanceHandle;
380
+ if (handle && (handle.child !== undefined || handle.memoizedProps !== undefined)) {
381
+ logger.info('FiberTreeWalker', `✅ Strategy 1b SUCCESS (Fabric): Fiber via __internalInstanceHandle (has child: ${!!handle.child})`);
382
+ return handle;
383
+ }
384
+ logger.warn('FiberTreeWalker', '⚠️ __internalInstanceHandle found but does not look like a Fiber node');
385
+ }
386
+ logger.warn('FiberTreeWalker', '❌ Strategy 1 FAILED: No __reactFiber$ or __internalInstanceHandle key found');
387
+ } catch (e) {
388
+ logger.warn('FiberTreeWalker', `❌ Strategy 1 ERROR: ${e.message}`);
330
389
  }
331
390
 
332
391
  // Strategy 2: _reactInternals (class components)
333
- if (ref._reactInternals) return ref._reactInternals;
392
+ if (ref._reactInternals) {
393
+ logger.info('FiberTreeWalker', '✅ Strategy 2 SUCCESS: _reactInternals');
394
+ return ref._reactInternals;
395
+ }
334
396
 
335
397
  // Strategy 3: _reactInternalInstance (older React)
336
- if (ref._reactInternalInstance) return ref._reactInternalInstance;
398
+ if (ref._reactInternalInstance) {
399
+ logger.info('FiberTreeWalker', '✅ Strategy 3 SUCCESS: _reactInternalInstance');
400
+ return ref._reactInternalInstance;
401
+ }
337
402
 
338
403
  // Strategy 4: Direct fiber node properties (ref IS a fiber — used in tests)
339
- if (ref.child || ref.memoizedProps) return ref;
404
+ if (ref.child || ref.memoizedProps) {
405
+ logger.info('FiberTreeWalker', '✅ Strategy 4 SUCCESS: ref is directly a Fiber node');
406
+ return ref;
407
+ }
340
408
 
341
409
  // Strategy 5: DevTools hook (dev-only last resort)
342
- const rootFiber = getFiberRootFromDevTools();
343
- if (rootFiber) return rootFiber;
344
- console.warn('[AIAgent] Could not access React Fiber tree. ' + 'The AI agent will not be able to detect interactive elements. ' + 'Ensure the rootRef is attached to a rendered <View>.');
345
- logger.warn('FiberTreeWalker', 'All Fiber access strategies failed');
410
+ // Guarded by __DEV__ to ensure production builds never reference
411
+ // __REACT_DEVTOOLS_GLOBAL_HOOK__ avoids App Store automated scanner flags.
412
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
413
+ const rootFiber = getFiberRootFromDevTools();
414
+ if (rootFiber) {
415
+ logger.info('FiberTreeWalker', '✅ Strategy 5 SUCCESS: DevTools hook');
416
+ return rootFiber;
417
+ }
418
+ }
419
+ logger.error('FiberTreeWalker', 'ALL Fiber access strategies FAILED. ' + `Ref type: ${typeof ref}, constructor: ${ref?.constructor?.name || 'unknown'}. ` + 'The AI agent will not detect interactive elements.');
346
420
  return null;
347
421
  }
348
422
 
@@ -355,7 +429,7 @@ function getFiberFromRef(ref) {
355
429
  */
356
430
  function matchesRefList(node, refs) {
357
431
  if (!refs || refs.length === 0) return false;
358
- const stateNode = node.stateNode;
432
+ const stateNode = getStateNode(node);
359
433
  if (!stateNode) return false;
360
434
  for (const ref of refs) {
361
435
  if (ref.current && ref.current === stateNode) return true;
@@ -400,7 +474,7 @@ export function walkFiberTree(rootRef, config) {
400
474
  const hasWhitelist = config?.interactiveWhitelist && (config.interactiveWhitelist.length ?? 0) > 0;
401
475
  function processNode(node, depth = 0, isInsideInteractive = false, ancestorOnPress = null, currentZoneId = undefined) {
402
476
  if (!node) return '';
403
- const props = node.memoizedProps || {};
477
+ const props = getProps(node);
404
478
 
405
479
  // ── Prune inactive screens ──────────────────────────────────
406
480
  // Two mechanisms cover all React Navigation setups:
@@ -422,10 +496,10 @@ export function walkFiberTree(rootRef, config) {
422
496
  if (props.aiIgnore === true) return '';
423
497
  if (matchesRefList(node, config?.interactiveBlacklist)) {
424
498
  let childText = '';
425
- let currentChild = node.child;
499
+ let currentChild = getChild(node);
426
500
  while (currentChild) {
427
501
  childText += processNode(currentChild, depth, isInsideInteractive);
428
- currentChild = currentChild.sibling;
502
+ currentChild = getSibling(currentChild);
429
503
  }
430
504
  return childText;
431
505
  }
@@ -455,7 +529,8 @@ export function walkFiberTree(rootRef, config) {
455
529
  // Only visible nodes (interactives, text, images, videos) should increment
456
530
  // depth. Structural View wrappers are transparent — they pass through the
457
531
  // same depth so indentation stays flat and doesn't waste LLM tokens.
458
- const typeStr = node.type && typeof node.type === 'string' ? node.type : node.elementType && typeof node.elementType === 'string' ? node.elementType : null;
532
+ const type = getType(node);
533
+ const typeStr = type && typeof type === 'string' ? type : node.elementType && typeof node.elementType === 'string' ? node.elementType : null;
459
534
  const componentName = getComponentName(node);
460
535
  const isTextNode = typeStr === 'RCTText' || typeStr === 'Text';
461
536
  const isImageNode = !!(componentName && IMAGE_TYPES.has(componentName));
@@ -482,10 +557,10 @@ export function walkFiberTree(rootRef, config) {
482
557
 
483
558
  // Process children
484
559
  let childrenText = '';
485
- let currentChild = node.child;
560
+ let currentChild = getChild(node);
486
561
  while (currentChild) {
487
562
  childrenText += processNode(currentChild, childDepth, isInsideInteractive || !!shouldInclude, nextAncestorOnPress, nextZoneId);
488
- currentChild = currentChild.sibling;
563
+ currentChild = getSibling(currentChild);
489
564
  }
490
565
 
491
566
  // Prepend zone header before children if this is a zone root
@@ -494,33 +569,26 @@ export function walkFiberTree(rootRef, config) {
494
569
  }
495
570
  if (shouldInclude) {
496
571
  const resolvedType = elementType || 'pressable';
497
- // Primary: accessibilityLabel → deep text (pierces nested interactives)
498
- let label = props.accessibilityLabel || extractDeepTextContent(node);
499
-
500
- // Fallback: TextInput placeholder
501
- if (!label && resolvedType === 'text-input' && props.placeholder) {
502
- label = props.placeholder;
503
- }
504
- // Fallback: Icon/symbol name (any component with a `name` prop)
505
- if (!label) {
506
- label = extractIconName(node);
507
- }
508
- // Fallback: testID/nativeID
509
- if (!label && (props.testID || props.nativeID)) {
510
- label = props.testID || props.nativeID;
511
- }
512
- // Fallback: Parent component context (skip internal RN context names)
513
- if (!label) {
514
- const parentContext = getNearestCustomComponentName(node);
515
- if (parentContext) {
516
- // Skip React Native internal implementation names that leak as labels.
517
- // These are never meaningful to the AI or developer.
518
- const RN_INTERNAL_NAMES = new Set(['ScrollViewContext', 'VirtualizedListContext', 'ViewabilityHelper', 'ScrollResponder', 'AnimatedComponent', 'TouchableOpacity']);
519
- if (!RN_INTERNAL_NAMES.has(parentContext)) {
520
- label = parentContext;
521
- }
522
- }
523
- }
572
+ const parentContext = getNearestCustomComponentName(node);
573
+ const label = chooseBestRuntimeLabel([{
574
+ text: props.accessibilityLabel,
575
+ source: 'accessibility'
576
+ }, {
577
+ text: extractDeepTextContent(node),
578
+ source: 'deep-text'
579
+ }, {
580
+ text: resolvedType === 'text-input' ? props.placeholder : null,
581
+ source: 'placeholder'
582
+ }, {
583
+ text: extractIconName(node),
584
+ source: 'icon'
585
+ }, {
586
+ text: props.testID || props.nativeID,
587
+ source: 'test-id'
588
+ }, {
589
+ text: parentContext && !new Set(['ScrollViewContext', 'VirtualizedListContext', 'ViewabilityHelper', 'ScrollResponder', 'AnimatedComponent', 'TouchableOpacity']).has(parentContext) ? parentContext : null,
590
+ source: 'context'
591
+ }]);
524
592
  interactives.push({
525
593
  index: currentIndex,
526
594
  type: resolvedType,
@@ -600,8 +668,43 @@ export function walkFiberTree(rootRef, config) {
600
668
  }
601
669
  }
602
670
 
603
- // Clean up excessive blank lines
604
- elementsText = elementsText.replace(/\n{3,}/g, '\n\n');
671
+ // Clean up excessive blank lines safely to avoid regex stack depth limit on very large grids
672
+ const lines = elementsText.split('\n');
673
+ const cleanLines = [];
674
+ let emptyCount = 0;
675
+ for (const line of lines) {
676
+ if (line.trim() === '') {
677
+ emptyCount++;
678
+ if (emptyCount < 2) cleanLines.push(line);
679
+ } else {
680
+ emptyCount = 0;
681
+ cleanLines.push(line);
682
+ }
683
+ }
684
+ elementsText = cleanLines.join('\n');
685
+
686
+ // ── Inject Native OS Alerts (Virtual Elements) ──
687
+ if (config?.interceptNativeAlerts) {
688
+ const alert = getActiveAlert();
689
+ if (alert) {
690
+ elementsText += `\n\n<system_alert>\n <title>${alert.title}</title>\n <message>${alert.message}</message>\n`;
691
+ alert.buttons.forEach((btn, idx) => {
692
+ elementsText += ` [${currentIndex}]<pressable role="button">"${btn.text}" />\n`;
693
+ interactives.push({
694
+ index: currentIndex++,
695
+ type: 'pressable',
696
+ label: btn.text,
697
+ props: {},
698
+ fiberNode: null,
699
+ virtual: {
700
+ kind: 'alert_button',
701
+ alertButtonIndex: idx
702
+ }
703
+ });
704
+ });
705
+ elementsText += `</system_alert>`;
706
+ }
707
+ }
605
708
  logger.info('FiberTreeWalker', `Found ${interactives.length} interactive elements`);
606
709
  return {
607
710
  elementsText: elementsText.trim(),
@@ -643,11 +746,11 @@ function findScreenFiberNode(rootFiber, screenName) {
643
746
  }
644
747
 
645
748
  // Depth-first: search children first, then siblings
646
- let child = node.child;
749
+ let child = getChild(node);
647
750
  while (child) {
648
751
  const found = search(child);
649
752
  if (found) return found;
650
- child = child.sibling;
753
+ child = getSibling(child);
651
754
  }
652
755
  return null;
653
756
  }
@@ -700,7 +803,7 @@ export function findScrollableContainers(rootRef, screenName) {
700
803
  // For scrollable containers, we need the native scroll ref.
701
804
  // FlatList Fiber stateNode may be the component instance —
702
805
  // we need to find the underlying native ScrollView.
703
- let scrollRef = isPagerLike ? node.stateNode : resolveNativeScrollRef(node);
806
+ let scrollRef = isPagerLike ? getStateNode(node) : resolveNativeScrollRef(node);
704
807
  if (scrollRef) {
705
808
  containers.push({
706
809
  index: currentIndex++,
@@ -714,10 +817,10 @@ export function findScrollableContainers(rootRef, screenName) {
714
817
  }
715
818
 
716
819
  // Recurse into children and siblings
717
- let child = node.child;
820
+ let child = getChild(node);
718
821
  while (child) {
719
822
  walk(child);
720
- child = child.sibling;
823
+ child = getSibling(child);
721
824
  }
722
825
  }
723
826
  walk(startNode);
@@ -733,9 +836,9 @@ export function findScrollableContainers(rootRef, screenName) {
733
836
  * - FlatList/VirtualizedList: stateNode is a component instance,
734
837
  * need to find the inner ScrollView via getNativeScrollRef() or
735
838
  * by walking down the Fiber tree to find the RCTScrollView child
736
- */
839
+ */
737
840
  function resolveNativeScrollRef(fiberNode) {
738
- const stateNode = fiberNode.stateNode;
841
+ const stateNode = getStateNode(fiberNode);
739
842
 
740
843
  // Case 1: stateNode has scrollTo (native ScrollView or RCTScrollView)
741
844
  if (stateNode && typeof stateNode.scrollTo === 'function') {
@@ -769,20 +872,23 @@ function resolveNativeScrollRef(fiberNode) {
769
872
  }
770
873
 
771
874
  // Case 5: Walk down Fiber tree to find an RCTScrollView child
772
- let child = fiberNode.child;
875
+ let child = getChild(fiberNode);
773
876
  while (child) {
774
877
  const childName = getComponentName(child);
775
- if (childName === 'RCTScrollView' && child.stateNode) {
776
- return child.stateNode;
878
+ const childStateNode = getStateNode(child);
879
+ if (childName === 'RCTScrollView' && childStateNode) {
880
+ return childStateNode;
777
881
  }
778
882
  // Go one level deeper for wrapper patterns
779
- if (child.child) {
780
- const grandchildName = getComponentName(child.child);
781
- if (grandchildName === 'RCTScrollView' && child.child.stateNode) {
782
- return child.child.stateNode;
883
+ const grandchild = getChild(child);
884
+ if (grandchild) {
885
+ const grandchildName = getComponentName(grandchild);
886
+ const grandchildStateNode = getStateNode(grandchild);
887
+ if (grandchildName === 'RCTScrollView' && grandchildStateNode) {
888
+ return grandchildStateNode;
783
889
  }
784
890
  }
785
- child = child.sibling;
891
+ child = getSibling(child);
786
892
  }
787
893
  logger.debug('FiberTreeWalker', 'Could not resolve native scroll ref — returning stateNode as fallback');
788
894
  return stateNode;
@@ -26,6 +26,25 @@ export class IdleDetector {
26
26
  this.clearTimers();
27
27
  this.config = null;
28
28
  }
29
+
30
+ /**
31
+ * Instantly trigger proactive help if the behavior matches a configured trigger.
32
+ */
33
+ triggerBehavior(type, currentScreen) {
34
+ if (!this.config || this.dismissed || !this.config.behaviorTriggers) return;
35
+ const trigger = this.config.behaviorTriggers.find(t => t.type === type && (t.screen === '*' || t.screen === currentScreen));
36
+ if (trigger) {
37
+ this.clearTimers(); // Intercept normal idle flow
38
+ const message = trigger.message || `It looks like you might be having trouble. Can I help?`;
39
+ if (trigger.delayMs) {
40
+ this.badgeTimer = setTimeout(() => {
41
+ this.config?.onBadge(message);
42
+ }, trigger.delayMs);
43
+ } else {
44
+ this.config.onBadge(message);
45
+ }
46
+ }
47
+ }
29
48
  resetTimers() {
30
49
  this.clearTimers();
31
50
  if (!this.config || this.dismissed) return;