@mobileai/react-native 0.9.0 → 0.9.2

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 (173) hide show
  1. package/README.md +121 -0
  2. package/lib/module/components/AIAgent.js +248 -53
  3. package/lib/module/components/AIAgent.js.map +1 -1
  4. package/lib/module/components/AIZone.js +140 -0
  5. package/lib/module/components/AIZone.js.map +1 -0
  6. package/lib/module/components/AgentErrorBoundary.js +9 -0
  7. package/lib/module/components/AgentErrorBoundary.js.map +1 -1
  8. package/lib/module/components/HighlightOverlay.js +138 -0
  9. package/lib/module/components/HighlightOverlay.js.map +1 -0
  10. package/lib/module/components/ProactiveHint.js +138 -0
  11. package/lib/module/components/ProactiveHint.js.map +1 -0
  12. package/lib/module/components/cards/InfoCard.js +65 -0
  13. package/lib/module/components/cards/InfoCard.js.map +1 -0
  14. package/lib/module/components/cards/ReviewSummary.js +74 -0
  15. package/lib/module/components/cards/ReviewSummary.js.map +1 -0
  16. package/lib/module/core/AgentRuntime.js +16 -3
  17. package/lib/module/core/AgentRuntime.js.map +1 -1
  18. package/lib/module/core/FiberTreeWalker.js +62 -85
  19. package/lib/module/core/FiberTreeWalker.js.map +1 -1
  20. package/lib/module/core/IdleDetector.js +51 -0
  21. package/lib/module/core/IdleDetector.js.map +1 -0
  22. package/lib/module/core/ZoneRegistry.js +47 -0
  23. package/lib/module/core/ZoneRegistry.js.map +1 -0
  24. package/lib/module/core/systemPrompt.js +2 -0
  25. package/lib/module/core/systemPrompt.js.map +1 -1
  26. package/lib/module/index.js +21 -0
  27. package/lib/module/index.js.map +1 -1
  28. package/lib/module/services/AudioOutputService.js +10 -0
  29. package/lib/module/services/AudioOutputService.js.map +1 -1
  30. package/lib/module/services/flags/FlagService.js +117 -0
  31. package/lib/module/services/flags/FlagService.js.map +1 -0
  32. package/lib/module/services/telemetry/MobileAI.js +66 -0
  33. package/lib/module/services/telemetry/MobileAI.js.map +1 -0
  34. package/lib/module/services/telemetry/PiiScrubber.js +17 -0
  35. package/lib/module/services/telemetry/PiiScrubber.js.map +1 -0
  36. package/lib/module/services/telemetry/TelemetryService.js +260 -0
  37. package/lib/module/services/telemetry/TelemetryService.js.map +1 -0
  38. package/lib/module/services/telemetry/TouchAutoCapture.js +159 -0
  39. package/lib/module/services/telemetry/TouchAutoCapture.js.map +1 -0
  40. package/lib/module/services/telemetry/device.js +19 -0
  41. package/lib/module/services/telemetry/device.js.map +1 -0
  42. package/lib/module/services/telemetry/index.js +9 -0
  43. package/lib/module/services/telemetry/index.js.map +1 -0
  44. package/lib/module/services/telemetry/types.js +2 -0
  45. package/lib/module/services/telemetry/types.js.map +1 -0
  46. package/lib/module/support/CSATSurvey.js +273 -0
  47. package/lib/module/support/CSATSurvey.js.map +1 -0
  48. package/lib/module/support/EscalationSocket.js +92 -0
  49. package/lib/module/support/EscalationSocket.js.map +1 -0
  50. package/lib/module/support/SupportGreeting.js +142 -0
  51. package/lib/module/support/SupportGreeting.js.map +1 -0
  52. package/lib/module/support/escalateTool.js +120 -0
  53. package/lib/module/support/escalateTool.js.map +1 -0
  54. package/lib/module/support/index.js +18 -0
  55. package/lib/module/support/index.js.map +1 -0
  56. package/lib/module/support/supportPrompt.js +47 -0
  57. package/lib/module/support/supportPrompt.js.map +1 -0
  58. package/lib/module/support/types.js +2 -0
  59. package/lib/module/support/types.js.map +1 -0
  60. package/lib/module/tools/guideTool.js +61 -0
  61. package/lib/module/tools/guideTool.js.map +1 -0
  62. package/lib/module/tools/index.js +3 -0
  63. package/lib/module/tools/index.js.map +1 -1
  64. package/lib/module/tools/restoreTool.js +31 -0
  65. package/lib/module/tools/restoreTool.js.map +1 -0
  66. package/lib/module/tools/simplifyTool.js +31 -0
  67. package/lib/module/tools/simplifyTool.js.map +1 -0
  68. package/lib/module/types/jsx.d.js +4 -0
  69. package/lib/module/types/jsx.d.js.map +1 -0
  70. package/lib/typescript/src/components/AIAgent.d.ts +21 -2
  71. package/lib/typescript/src/components/AIAgent.d.ts.map +1 -1
  72. package/lib/typescript/src/components/AIZone.d.ts +16 -0
  73. package/lib/typescript/src/components/AIZone.d.ts.map +1 -0
  74. package/lib/typescript/src/components/AgentErrorBoundary.d.ts +1 -0
  75. package/lib/typescript/src/components/AgentErrorBoundary.d.ts.map +1 -1
  76. package/lib/typescript/src/components/HighlightOverlay.d.ts +10 -0
  77. package/lib/typescript/src/components/HighlightOverlay.d.ts.map +1 -0
  78. package/lib/typescript/src/components/ProactiveHint.d.ts +10 -0
  79. package/lib/typescript/src/components/ProactiveHint.d.ts.map +1 -0
  80. package/lib/typescript/src/components/cards/InfoCard.d.ts +19 -0
  81. package/lib/typescript/src/components/cards/InfoCard.d.ts.map +1 -0
  82. package/lib/typescript/src/components/cards/ReviewSummary.d.ts +19 -0
  83. package/lib/typescript/src/components/cards/ReviewSummary.d.ts.map +1 -0
  84. package/lib/typescript/src/core/AgentRuntime.d.ts +1 -0
  85. package/lib/typescript/src/core/AgentRuntime.d.ts.map +1 -1
  86. package/lib/typescript/src/core/FiberTreeWalker.d.ts.map +1 -1
  87. package/lib/typescript/src/core/IdleDetector.d.ts +27 -0
  88. package/lib/typescript/src/core/IdleDetector.d.ts.map +1 -0
  89. package/lib/typescript/src/core/ZoneRegistry.d.ts +13 -0
  90. package/lib/typescript/src/core/ZoneRegistry.d.ts.map +1 -0
  91. package/lib/typescript/src/core/systemPrompt.d.ts.map +1 -1
  92. package/lib/typescript/src/core/types.d.ts +54 -0
  93. package/lib/typescript/src/core/types.d.ts.map +1 -1
  94. package/lib/typescript/src/index.d.ts +5 -0
  95. package/lib/typescript/src/index.d.ts.map +1 -1
  96. package/lib/typescript/src/services/AudioOutputService.d.ts.map +1 -1
  97. package/lib/typescript/src/services/flags/FlagService.d.ts +25 -0
  98. package/lib/typescript/src/services/flags/FlagService.d.ts.map +1 -0
  99. package/lib/typescript/src/services/telemetry/MobileAI.d.ts +38 -0
  100. package/lib/typescript/src/services/telemetry/MobileAI.d.ts.map +1 -0
  101. package/lib/typescript/src/services/telemetry/PiiScrubber.d.ts +6 -0
  102. package/lib/typescript/src/services/telemetry/PiiScrubber.d.ts.map +1 -0
  103. package/lib/typescript/src/services/telemetry/TelemetryService.d.ts +49 -0
  104. package/lib/typescript/src/services/telemetry/TelemetryService.d.ts.map +1 -0
  105. package/lib/typescript/src/services/telemetry/TouchAutoCapture.d.ts +28 -0
  106. package/lib/typescript/src/services/telemetry/TouchAutoCapture.d.ts.map +1 -0
  107. package/lib/typescript/src/services/telemetry/device.d.ts +7 -0
  108. package/lib/typescript/src/services/telemetry/device.d.ts.map +1 -0
  109. package/lib/typescript/src/services/telemetry/index.d.ts +7 -0
  110. package/lib/typescript/src/services/telemetry/index.d.ts.map +1 -0
  111. package/lib/typescript/src/services/telemetry/types.d.ts +50 -0
  112. package/lib/typescript/src/services/telemetry/types.d.ts.map +1 -0
  113. package/lib/typescript/src/support/CSATSurvey.d.ts +20 -0
  114. package/lib/typescript/src/support/CSATSurvey.d.ts.map +1 -0
  115. package/lib/typescript/src/support/EscalationSocket.d.ts +38 -0
  116. package/lib/typescript/src/support/EscalationSocket.d.ts.map +1 -0
  117. package/lib/typescript/src/support/SupportGreeting.d.ts +19 -0
  118. package/lib/typescript/src/support/SupportGreeting.d.ts.map +1 -0
  119. package/lib/typescript/src/support/escalateTool.d.ts +25 -0
  120. package/lib/typescript/src/support/escalateTool.d.ts.map +1 -0
  121. package/lib/typescript/src/support/index.d.ts +11 -0
  122. package/lib/typescript/src/support/index.d.ts.map +1 -0
  123. package/lib/typescript/src/support/supportPrompt.d.ts +12 -0
  124. package/lib/typescript/src/support/supportPrompt.d.ts.map +1 -0
  125. package/lib/typescript/src/support/types.d.ts +114 -0
  126. package/lib/typescript/src/support/types.d.ts.map +1 -0
  127. package/lib/typescript/src/tools/guideTool.d.ts +4 -0
  128. package/lib/typescript/src/tools/guideTool.d.ts.map +1 -0
  129. package/lib/typescript/src/tools/index.d.ts +3 -0
  130. package/lib/typescript/src/tools/index.d.ts.map +1 -1
  131. package/lib/typescript/src/tools/restoreTool.d.ts +3 -0
  132. package/lib/typescript/src/tools/restoreTool.d.ts.map +1 -0
  133. package/lib/typescript/src/tools/simplifyTool.d.ts +3 -0
  134. package/lib/typescript/src/tools/simplifyTool.d.ts.map +1 -0
  135. package/lib/typescript/src/tools/types.d.ts +2 -0
  136. package/lib/typescript/src/tools/types.d.ts.map +1 -1
  137. package/package.json +5 -1
  138. package/src/components/AIAgent.tsx +253 -15
  139. package/src/components/AIZone.tsx +147 -0
  140. package/src/components/AgentErrorBoundary.tsx +10 -0
  141. package/src/components/HighlightOverlay.tsx +136 -0
  142. package/src/components/ProactiveHint.tsx +145 -0
  143. package/src/components/cards/InfoCard.tsx +58 -0
  144. package/src/components/cards/ReviewSummary.tsx +76 -0
  145. package/src/core/AgentRuntime.ts +18 -0
  146. package/src/core/FiberTreeWalker.ts +71 -93
  147. package/src/core/IdleDetector.ts +72 -0
  148. package/src/core/ZoneRegistry.ts +44 -0
  149. package/src/core/systemPrompt.ts +2 -0
  150. package/src/core/types.ts +60 -0
  151. package/src/index.ts +31 -0
  152. package/src/services/AudioOutputService.ts +13 -0
  153. package/src/services/flags/FlagService.ts +137 -0
  154. package/src/services/telemetry/MobileAI.ts +66 -0
  155. package/src/services/telemetry/PiiScrubber.ts +17 -0
  156. package/src/services/telemetry/TelemetryService.ts +291 -0
  157. package/src/services/telemetry/TouchAutoCapture.ts +165 -0
  158. package/src/services/telemetry/device.ts +16 -0
  159. package/src/services/telemetry/index.ts +13 -0
  160. package/src/services/telemetry/types.ts +75 -0
  161. package/src/support/CSATSurvey.tsx +304 -0
  162. package/src/support/EscalationSocket.ts +113 -0
  163. package/src/support/SupportGreeting.tsx +161 -0
  164. package/src/support/escalateTool.ts +134 -0
  165. package/src/support/index.ts +27 -0
  166. package/src/support/supportPrompt.ts +55 -0
  167. package/src/support/types.ts +141 -0
  168. package/src/tools/guideTool.ts +67 -0
  169. package/src/tools/index.ts +3 -0
  170. package/src/tools/restoreTool.ts +33 -0
  171. package/src/tools/simplifyTool.ts +33 -0
  172. package/src/tools/types.ts +2 -0
  173. package/src/types/jsx.d.ts +20 -0
@@ -48,19 +48,7 @@ const VIDEO_TYPES = new Set([
48
48
  'Video', 'ExpoVideo', 'RCTVideo', 'VideoPlayer', 'VideoView',
49
49
  ]);
50
50
 
51
- // Overlay component names — these render ABOVE the active screen's subtree
52
- // (as siblings of the navigation container), so the screen-scoped walker misses them.
53
- // We detect these by name and walk them in a second pass.
54
- const OVERLAY_COMPONENT_NAMES = new Set([
55
- // Toast libraries
56
- 'Toast', 'ToastContainer', 'ToastRender', 'FlashMessage', 'DropdownAlert',
57
- 'SnackBar', 'Snackbar', 'NotificationContainer',
58
- // Dialog/Alert libraries
59
- 'Dialog', 'DialogContainer', 'AlertDialog',
60
- // Modal/BottomSheet
61
- 'BottomSheet', 'ActionSheet', 'RBSheet', 'BottomSheetModal',
62
- 'Popup', 'PopupContainer', 'Tooltip',
63
- ]);
51
+
64
52
 
65
53
  // Known RN internal component names to skip when walking up for context
66
54
  const RN_INTERNAL_NAMES = new Set([
@@ -442,70 +430,10 @@ export interface WalkResult {
442
430
  interactives: InteractiveElement[];
443
431
  }
444
432
 
445
- // ─── Overlay Detection ─────────────────────────────────────────
446
-
447
- /**
448
- * Scan the root fiber tree for visible overlay nodes (toasts, dialogs, modals)
449
- * that render OUTSIDE the active screen's subtree.
450
- *
451
- * Toast/dialog libraries (react-native-alert-notification, react-native-flash-message, etc.)
452
- * typically render their overlay as a SIBLING of the navigation container:
453
- * <Root>
454
- * {children} ← navigation tree (contains screens)
455
- * <Toast ... /> ← overlay (outside screen subtree)
456
- * </Root>
457
- *
458
- * This function finds those overlay nodes so the main walker can include them.
459
- */
460
- function findOverlayNodes(rootFiber: any, screenSubtree: any): any[] {
461
- const overlays: any[] = [];
462
-
463
- function scan(node: any): void {
464
- if (!node) return;
465
-
466
- // Skip the screen subtree itself (already walked by main pass)
467
- if (node === screenSubtree) {
468
- // Don't scan children, but DO scan siblings
469
- if (node.sibling) scan(node.sibling);
470
- return;
471
- }
472
-
473
- const name = getComponentName(node);
474
- const props = node.memoizedProps || {};
475
- const style = props.style || {};
476
- // Flatten array styles (StyleSheet.flatten equivalent)
477
- const flatStyle = Array.isArray(style)
478
- ? Object.assign({}, ...style.filter(Boolean))
479
- : style;
480
-
481
- // Detection: known overlay component name
482
- const isOverlayByName = name && OVERLAY_COMPONENT_NAMES.has(name);
483
-
484
- // Detection: position absolute with content (likely a floating overlay)
485
- const isOverlayByStyle = flatStyle.position === 'absolute' &&
486
- (flatStyle.zIndex > 100 || flatStyle.elevation > 10);
487
-
488
- if (isOverlayByName || isOverlayByStyle) {
489
- // Only include if the overlay has visible content (not empty/hidden)
490
- const hasChildren = !!node.child;
491
- const isHidden = flatStyle.opacity === 0 || flatStyle.display === 'none';
492
- if (hasChildren && !isHidden) {
493
- logger.debug('FiberTreeWalker', `Found overlay: ${name || 'unnamed'} (byName=${isOverlayByName}, byStyle=${isOverlayByStyle})`);
494
- overlays.push(node);
495
- // Don't scan children of found overlays (the main walker will handle them)
496
- if (node.sibling) scan(node.sibling);
497
- return;
498
- }
499
- }
500
-
501
- // Continue scanning children and siblings
502
- if (node.child) scan(node.child);
503
- if (node.sibling) scan(node.sibling);
504
- }
505
-
506
- scan(rootFiber);
507
- return overlays;
508
- }
433
+ // ─── Note on overlays ──────────────────────────────────────────
434
+ // Toasts, modals, and dialogs are now captured naturally because
435
+ // walkFiberTree starts from the root Fiber and prunes only
436
+ // display:none nodes (inactive screens). No separate overlay scan needed.
509
437
 
510
438
  // ─── Main Tree Walker ──────────────────────────────────────────
511
439
 
@@ -520,31 +448,50 @@ export function walkFiberTree(rootRef: any, config?: WalkConfig): WalkResult {
520
448
  return { elementsText: '', interactives: [] };
521
449
  }
522
450
 
523
- // Scope to active screen's subtree if screenName is provided
524
- let startNode = fiber;
451
+ // Always walk from the root Fiber inactive screens are pruned inside
452
+ // processNode by checking for display:none, which React Navigation applies
453
+ // to all inactive screens. This ensures navigation chrome (tab bar, header)
454
+ // is always included without hardcoding any component names.
455
+ const startNode = fiber;
525
456
  if (config?.screenName) {
526
- const screenFiber = findScreenFiberNode(fiber, config.screenName);
527
- if (screenFiber) {
528
- startNode = screenFiber;
529
- logger.debug('FiberTreeWalker', `Walk scoped to screen "${config.screenName}" (component: ${getComponentName(screenFiber)})`);
530
- } else {
531
- logger.debug('FiberTreeWalker', `Screen "${config.screenName}" not found in Fiber tree — searching entire tree`);
532
- }
457
+ logger.debug('FiberTreeWalker', `Walk active for screen "${config.screenName}" (inactive screens pruned via display:none)`);
533
458
  }
534
459
 
535
- // Collect overlay nodes (toasts, dialogs, modals) that render outside the screen subtree.
536
- // These are siblings of the navigation container at the root fiber level.
537
- const overlayNodes = config?.screenName ? findOverlayNodes(fiber, startNode) : [];
460
+ // Overlay detection is superseded by root-level walk all visible nodes
461
+ // (toasts, modals, overlays) are included naturally since they aren't hidden.
462
+ const overlayNodes: any[] = [];
538
463
 
539
464
  const interactives: InteractiveElement[] = [];
540
465
  let currentIndex = 0;
541
466
  const hasWhitelist = config?.interactiveWhitelist && (config.interactiveWhitelist.length ?? 0) > 0;
542
467
 
543
- function processNode(node: any, depth: number = 0, isInsideInteractive: boolean = false, ancestorOnPress: any = null): string {
468
+ function processNode(
469
+ node: any,
470
+ depth: number = 0,
471
+ isInsideInteractive: boolean = false,
472
+ ancestorOnPress: any = null,
473
+ currentZoneId: string | undefined = undefined
474
+ ): string {
544
475
  if (!node) return '';
545
476
 
546
477
  const props = node.memoizedProps || {};
547
478
 
479
+ // ── Prune inactive screens ──────────────────────────────────
480
+ // Two mechanisms cover all React Navigation setups:
481
+ //
482
+ // 1. react-native-screens (Expo default, recommended):
483
+ // React Navigation sets activityState=0 on inactive tab screens.
484
+ // Stack screens retain activityState=2 even in the background (NativeStack
485
+ // never decreases activityState — confirmed from react-native-screens source).
486
+ // Those are kept as useful navigation history context for the agent.
487
+ //
488
+ // 2. ResourceSavingScene (React Navigation fallback without react-native-screens):
489
+ // Wraps inactive screen content with pointerEvents="none". Prune any such wrapper.
490
+ //
491
+ // Both are prop-based — no component names, no CSS display property.
492
+ if (props.activityState === 0) return '';
493
+ if (props.pointerEvents === 'none') return '';
494
+
548
495
  // ── Security Constraints ──
549
496
  if (props.aiIgnore === true) return '';
550
497
  if (matchesRefList(node, config?.interactiveBlacklist)) {
@@ -591,6 +538,25 @@ export function walkFiberTree(rootRef: any, config?: WalkConfig): WalkResult {
591
538
  const producesOutput = shouldInclude || isTextNode || isImageNode || isVideoNode;
592
539
  const childDepth = producesOutput ? depth + 1 : depth;
593
540
 
541
+ const nextZoneId = componentName === 'AIZone' && props.id ? props.id : currentZoneId;
542
+
543
+ const indent = ' '.repeat(depth);
544
+
545
+ // ── Zone Header Emission ──────────────────────────────────
546
+ // When entering an AIZone boundary, emit a section header so the agent
547
+ // knows this zone exists, what its id is, and what it's allowed to do.
548
+ // This is what lets the agent call simplify_zone / guide_user proactively.
549
+ let zoneHeader = '';
550
+ if (componentName === 'AIZone' && props.id) {
551
+ const permissions: string[] = [];
552
+ if (props.allowSimplify) permissions.push('simplify');
553
+ if (props.allowHighlight) permissions.push('highlight');
554
+ if (props.allowInjectHint) permissions.push('hint');
555
+ if (props.allowInjectCard) permissions.push('card');
556
+ const permStr = permissions.length ? ` | permissions: ${permissions.join(', ')}` : '';
557
+ zoneHeader = `${indent}[zone: ${props.id}${permStr}]\n`;
558
+ }
559
+
594
560
  // Process children
595
561
  let childrenText = '';
596
562
  let currentChild = node.child;
@@ -600,11 +566,15 @@ export function walkFiberTree(rootRef: any, config?: WalkConfig): WalkResult {
600
566
  childDepth,
601
567
  isInsideInteractive || !!shouldInclude,
602
568
  nextAncestorOnPress,
569
+ nextZoneId,
603
570
  );
604
571
  currentChild = currentChild.sibling;
605
572
  }
606
573
 
607
- const indent = ' '.repeat(depth);
574
+ // Prepend zone header before children if this is a zone root
575
+ if (zoneHeader) {
576
+ childrenText = zoneHeader + childrenText;
577
+ }
608
578
 
609
579
  if (shouldInclude) {
610
580
  const resolvedType = elementType || 'pressable';
@@ -633,13 +603,20 @@ export function walkFiberTree(rootRef: any, config?: WalkConfig): WalkResult {
633
603
  index: currentIndex,
634
604
  type: resolvedType,
635
605
  label: label || `[${resolvedType}]`,
606
+ aiPriority: props.aiPriority,
607
+ zoneId: currentZoneId,
636
608
  fiberNode: node,
637
609
  props: { ...props },
638
610
  });
639
611
 
640
612
  // Build output tag with state attributes
641
613
  const stateAttrs = extractStateAttributes(props);
642
- const attrStr = stateAttrs ? ` ${stateAttrs}` : '';
614
+ let attrStr = stateAttrs ? ` ${stateAttrs}` : '';
615
+ if (props.aiPriority) {
616
+ attrStr += ` aiPriority="${props.aiPriority}"`;
617
+ if (currentZoneId) attrStr += ` zoneId="${currentZoneId}"`;
618
+ }
619
+
643
620
  const textContent = label || '';
644
621
  const elementOutput = `${indent}[${currentIndex}]<${resolvedType}${attrStr}>${textContent} />${childrenText.trim() ? '\n' + childrenText : ''}\n`;
645
622
  currentIndex++;
@@ -651,7 +628,8 @@ export function walkFiberTree(rootRef: any, config?: WalkConfig): WalkResult {
651
628
  if (isTextNode) {
652
629
  const textContent = extractRawText(props.children);
653
630
  if (textContent && textContent.trim() !== '') {
654
- return `${indent}${textContent.trim()}\n`;
631
+ const priorityTag = props.aiPriority ? ` (aiPriority="${props.aiPriority}"${currentZoneId ? ` zoneId="${currentZoneId}"` : ''})` : '';
632
+ return `${indent}${textContent.trim()}${priorityTag}\n`;
655
633
  }
656
634
  }
657
635
 
@@ -0,0 +1,72 @@
1
+ export interface IdleDetectorConfig {
2
+ /** Time in ms before the agent pulses subtly (e.g. 120_000 for 2m) */
3
+ pulseAfterMs: number;
4
+ /** Time in ms before the agent shows a badge (e.g. 240_000 for 4m) */
5
+ badgeAfterMs: number;
6
+ /** Callback fired when the user is idle enough for a subtle pulse */
7
+ onPulse: () => void;
8
+ /** Callback fired when the user is idle enough for a proactive badge. Receives the context suggestion. */
9
+ onBadge: (suggestion: string) => void;
10
+ /** Callback fired when the user interacts, cancelling idle states */
11
+ onReset: () => void;
12
+ /** Dynamic context suggestion generator based on current screen */
13
+ generateSuggestion?: () => string;
14
+ }
15
+
16
+ export class IdleDetector {
17
+ private pulseTimer: ReturnType<typeof setTimeout> | null = null;
18
+ private badgeTimer: ReturnType<typeof setTimeout> | null = null;
19
+ private dismissed = false;
20
+ private config: IdleDetectorConfig | null = null;
21
+
22
+ start(config: IdleDetectorConfig): void {
23
+ this.config = config;
24
+ this.dismissed = false;
25
+ this.resetTimers();
26
+ }
27
+
28
+ reset(): void {
29
+ if (!this.config || this.dismissed) return;
30
+ this.config.onReset();
31
+ this.resetTimers();
32
+ }
33
+
34
+ dismiss(): void {
35
+ this.dismissed = true;
36
+ this.clearTimers();
37
+ if (this.config) {
38
+ this.config.onReset();
39
+ }
40
+ }
41
+
42
+ destroy(): void {
43
+ this.clearTimers();
44
+ this.config = null;
45
+ }
46
+
47
+ private resetTimers(): void {
48
+ this.clearTimers();
49
+
50
+ if (!this.config || this.dismissed) return;
51
+
52
+ this.pulseTimer = setTimeout(() => {
53
+ this.config?.onPulse();
54
+ }, this.config.pulseAfterMs);
55
+
56
+ this.badgeTimer = setTimeout(() => {
57
+ const suggestion = this.config?.generateSuggestion?.() ?? "Need help with this screen?";
58
+ this.config?.onBadge(suggestion);
59
+ }, this.config.badgeAfterMs);
60
+ }
61
+
62
+ private clearTimers(): void {
63
+ if (this.pulseTimer) {
64
+ clearTimeout(this.pulseTimer);
65
+ this.pulseTimer = null;
66
+ }
67
+ if (this.badgeTimer) {
68
+ clearTimeout(this.badgeTimer);
69
+ this.badgeTimer = null;
70
+ }
71
+ }
72
+ }
@@ -0,0 +1,44 @@
1
+ import React, { createContext } from 'react';
2
+ import type { AIZoneConfig, RegisteredZone } from './types';
3
+
4
+ export class ZoneRegistry {
5
+ private zones = new Map<string, RegisteredZone>();
6
+
7
+ register(config: AIZoneConfig, ref: React.RefObject<any>): void {
8
+ if (this.zones.has(config.id)) {
9
+ console.warn(`[MobileAI] Zone ID "${config.id}" is already registered on this screen. Overwriting.`);
10
+ }
11
+ this.zones.set(config.id, { ...config, ref });
12
+ }
13
+
14
+ unregister(id: string): void {
15
+ this.zones.delete(id);
16
+ }
17
+
18
+ get(id: string): RegisteredZone | undefined {
19
+ return this.zones.get(id);
20
+ }
21
+
22
+ getAll(): RegisteredZone[] {
23
+ return Array.from(this.zones.values());
24
+ }
25
+
26
+ isActionAllowed(zoneId: string, action: 'highlight' | 'hint' | 'simplify' | 'card'): boolean {
27
+ const zone = this.get(zoneId);
28
+ if (!zone) return false;
29
+
30
+ switch (action) {
31
+ case 'highlight': return !!zone.allowHighlight;
32
+ case 'hint': return !!zone.allowInjectHint;
33
+ case 'simplify': return !!zone.allowSimplify;
34
+ case 'card': return !!zone.allowInjectCard;
35
+ default: return false;
36
+ }
37
+ }
38
+ }
39
+
40
+ // Global registry instance shared across the Agent session
41
+ export const globalZoneRegistry = new ZoneRegistry();
42
+
43
+ // Export context so AIZone components can register themselves
44
+ export const ZoneRegistryContext = createContext<ZoneRegistry>(globalZoneRegistry);
@@ -102,6 +102,7 @@ If a UI element is hidden (aiIgnore) but a matching custom action exists, use th
102
102
  - Trying too hard can be harmful. If stuck, call done() with partial results rather than repeating failed actions.
103
103
  - If you do not know how to proceed with the current screen, use ask_user to request specific instructions from the user.
104
104
  - NAVIGATION: Always use tap actions to move between screens — tap tab bar buttons, back buttons, and navigation links. This ensures all required route params (like item IDs) are passed automatically by the app. The navigate() tool is ONLY for top-level screens that require no params (e.g. Login, Settings, Cart). NEVER call navigate() on screens that require a selection or ID (e.g. DishDetail, SelectCategory, ProfileDetail) — this will crash the app. For those screens, always tap the relevant item in the parent screen.
105
+ - UI SIMPLIFICATION: If you see elements labeled \`aiPriority="low"\` inside a specific \`zoneId=...\`, and the screen looks cluttered or overwhelming to the user's immediate goal, use the \`simplify_zone(zoneId)\` tool to hide those elements. Use \`restore_zone(zoneId)\` to bring them back if needed later!
105
106
  </rules>
106
107
 
107
108
  <task_completion_rules>
@@ -271,6 +272,7 @@ If a UI element is hidden but a matching custom action exists, use the action.
271
272
  - Do NOT ask for confirmation of actions the user explicitly requested. If they said "place my order", just do it.
272
273
  - If the user's intent is ambiguous — it could mean multiple things or lead to different screens — ask the user to clarify before navigating to the wrong place.
273
274
  - NAVIGATION: Always use tap actions to move between screens — tap tab bar buttons, back buttons, and navigation links. This ensures all required route params are passed automatically by the app. The navigate() tool is ONLY for top-level screens that require no params (e.g. Login, Settings, Cart). NEVER call navigate() on screens that require a selection or ID (e.g. DishDetail, SelectCategory, ProfileDetail) — this will crash the app. For those screens, always tap the relevant item in the parent screen.
275
+ - UI SIMPLIFICATION: If you see elements labeled \`aiPriority="low"\` inside a specific \`zoneId=...\`, and the screen looks cluttered relative to the user's immediate goal, use the \`simplify_zone(zoneId)\` tool to hide those low-priority elements. Use \`restore_zone(zoneId)\` to bring them back if needed. The user does NOT need to explicitly ask for this — use your judgment based on their request.
274
276
  </rules>
275
277
 
276
278
  <capability>
package/src/core/types.ts CHANGED
@@ -22,6 +22,10 @@ export interface InteractiveElement {
22
22
  type: ElementType;
23
23
  /** Human-readable label (extracted from Text children or accessibilityLabel) */
24
24
  label: string;
25
+ /** Declarative AI priority explicitly set by the developer */
26
+ aiPriority?: 'high' | 'low';
27
+ /** The nearest enclosing AIZone ID (if any) */
28
+ zoneId?: string;
25
29
  /** Reference to the Fiber node for execution */
26
30
  fiberNode: any;
27
31
  /**
@@ -216,6 +220,14 @@ export interface AgentConfig {
216
220
  */
217
221
  onAskUser?: (question: string) => Promise<string>;
218
222
 
223
+ /**
224
+ * Called immediately before and after each agent tool execution.
225
+ * Used by AIAgent to toggle isAgentActing on TelemetryService so that
226
+ * AI-driven taps are not double-counted as user interactions.
227
+ * @param active - true = agent is acting, false = agent finished acting
228
+ */
229
+ onToolExecute?: (active: boolean) => void;
230
+
219
231
  // ─── Expo Router Support ─────────────────────────────────────────────────
220
232
 
221
233
  /**
@@ -417,3 +429,51 @@ export interface AIProvider {
417
429
  screenshot?: string,
418
430
  ): Promise<ProviderResult>;
419
431
  }
432
+
433
+ // ─── AI-Native UI (Pillar B) ───────────────────────────────────────────────────
434
+
435
+ /**
436
+ * Configuration for an AIZone declarative boundary.
437
+ */
438
+ export interface AIZoneConfig {
439
+ /** Unique identifier for this zone on the current screen */
440
+ id: string;
441
+ /** Whether the AI is allowed to use guide_user() to highlight elements here */
442
+ allowHighlight?: boolean;
443
+ /** Whether the AI is allowed to inject tooltip hints here */
444
+ allowInjectHint?: boolean;
445
+ /** Whether the AI is allowed to hide children marked with aiPriority="low" */
446
+ allowSimplify?: boolean;
447
+ /** Whether the AI is allowed to inject custom cards here */
448
+ allowInjectCard?: boolean;
449
+ /**
450
+ * Whitelist of React component templates the AI can instantiate.
451
+ * Required if allowInjectCard is true.
452
+ * IMPORTANT: The AI receives the displayName of these components
453
+ * and can only produce props for them. It cannot generate JSX.
454
+ */
455
+ templates?: React.ComponentType<any>[];
456
+ }
457
+
458
+ /**
459
+ * Internal representation of a registered zone.
460
+ */
461
+ export interface RegisteredZone extends AIZoneConfig {
462
+ /** React ref to the zone's container View */
463
+ ref: React.RefObject<any>;
464
+ }
465
+
466
+ export interface ProactiveHelpConfig {
467
+ /** Enable proactive help (default: true) */
468
+ enabled?: boolean;
469
+ /** Time in minutes before a subtle pulse (default: 2) */
470
+ pulseAfterMinutes?: number;
471
+ /** Time in minutes before showing a help badge (default: 4) */
472
+ badgeAfterMinutes?: number;
473
+ /** Default text for the badge (default: "Need help with this screen?") */
474
+ badgeText?: string;
475
+ /** If true, dismissing the badge disables proactive help for the rest of the session (default: true) */
476
+ dismissForSession?: boolean;
477
+ /** Dynamic context suggestion generator based on current screen */
478
+ generateSuggestion?: (screenName: string) => string;
479
+ }
package/src/index.ts CHANGED
@@ -7,6 +7,11 @@
7
7
 
8
8
  // ─── Components ──────────────────────────────────────────────
9
9
  export { AIAgent } from './components/AIAgent';
10
+ export { AIZone } from './components/AIZone';
11
+ // Built-in card templates for AIZone injection
12
+ // Note: displayName is set explicitly on each — required for minification-safe template lookup.
13
+ export { InfoCard } from './components/cards/InfoCard';
14
+ export { ReviewSummary } from './components/cards/ReviewSummary';
10
15
 
11
16
  // ─── Providers ───────────────────────────────────────────────
12
17
  export { GeminiProvider } from './providers/GeminiProvider';
@@ -22,6 +27,10 @@ export { AudioInputService } from './services/AudioInputService';
22
27
  export { AudioOutputService } from './services/AudioOutputService';
23
28
  export { KnowledgeBaseService } from './services/KnowledgeBaseService';
24
29
 
30
+ // ─── Analytics ───────────────────────────────────────────────
31
+ // Requires api.mobileai.dev — hidden until backend is live
32
+ // export { MobileAI } from './services/telemetry';
33
+
25
34
  // ─── Utilities ───────────────────────────────────────────────
26
35
  export { logger } from './utils/logger';
27
36
 
@@ -50,3 +59,25 @@ export type {
50
59
  VoiceServiceCallbacks,
51
60
  VoiceStatus,
52
61
  } from './services/VoiceService';
62
+
63
+ // Requires api.mobileai.dev — hidden until backend is live
64
+ // export type {
65
+ // TelemetryConfig,
66
+ // TelemetryEvent,
67
+ // } from './services/telemetry';
68
+
69
+ // ─── Support Mode ────────────────────────────────────────────
70
+ // SupportGreeting, CSATSurvey, buildSupportPrompt work standalone (no backend)
71
+ // createEscalateTool works with provider='custom' (no backend)
72
+ // EscalationSocket and provider='mobileai' require api.mobileai.dev — hidden
73
+ export { SupportGreeting, CSATSurvey, buildSupportPrompt, createEscalateTool } from './support';
74
+
75
+ export type {
76
+ SupportModeConfig,
77
+ QuickReply,
78
+ EscalationConfig,
79
+ EscalationContext,
80
+ CSATConfig,
81
+ CSATRating,
82
+ BusinessHoursConfig,
83
+ } from './support';
@@ -10,6 +10,7 @@
10
10
 
11
11
  import { logger } from '../utils/logger';
12
12
  import { base64ToFloat32 } from '../utils/audioUtils';
13
+ import { NativeModules } from 'react-native';
13
14
 
14
15
  // ─── Types ─────────────────────────────────────────────────────
15
16
 
@@ -44,6 +45,18 @@ export class AudioOutputService {
44
45
  try {
45
46
  let audioApi: any;
46
47
  try {
48
+ // Guard: NativeModules.AudioApiModule is only present in dev/prod builds.
49
+ // In Expo Go it is undefined, and require() throws a native bridge error
50
+ // that cannot be caught by a standard try/catch.
51
+ if (!NativeModules.AudioApiModule) {
52
+ const msg =
53
+ '[mobileai] react-native-audio-api native module not found. '
54
+ + 'Voice audio output requires a development build (not Expo Go). '
55
+ + 'Run: npx expo run:ios';
56
+ logger.warn('AudioOutput', msg);
57
+ this.config.onError?.(msg);
58
+ return false;
59
+ }
47
60
  const audioApiModule = ['react-native', 'audio-api'].join('-');
48
61
  audioApi = require(audioApiModule);
49
62
  } catch {
@@ -0,0 +1,137 @@
1
+ import { logger } from '../../utils/logger';
2
+ import { getDeviceId } from '../telemetry/device';
3
+
4
+ const LOG_TAG = 'FlagService';
5
+
6
+ /**
7
+ * MurmurHash3 (32-bit) implementation
8
+ */
9
+ function murmurhash3_32_gc(key: string, seed: number = 0): number {
10
+ let remainder, bytes, h1, h1b, c1, c2, k1, i;
11
+
12
+ remainder = key.length & 3; // key.length % 4
13
+ bytes = key.length - remainder;
14
+ h1 = seed;
15
+ c1 = 0xcc9e2d51;
16
+ c2 = 0x1b873593;
17
+ i = 0;
18
+
19
+ while (i < bytes) {
20
+ k1 =
21
+ ((key.charCodeAt(i) & 0xff)) |
22
+ ((key.charCodeAt(++i) & 0xff) << 8) |
23
+ ((key.charCodeAt(++i) & 0xff) << 16) |
24
+ ((key.charCodeAt(++i) & 0xff) << 24);
25
+ ++i;
26
+
27
+ k1 = ((((k1 & 0xffff) * c1) + ((((k1 >>> 16) * c1) & 0xffff) << 16))) & 0xffffffff;
28
+ k1 = (k1 << 15) | (k1 >>> 17);
29
+ k1 = ((((k1 & 0xffff) * c2) + ((((k1 >>> 16) * c2) & 0xffff) << 16))) & 0xffffffff;
30
+
31
+ h1 ^= k1;
32
+ h1 = (h1 << 13) | (h1 >>> 19);
33
+ h1b = ((((h1 & 0xffff) * 5) + ((((h1 >>> 16) * 5) & 0xffff) << 16))) & 0xffffffff;
34
+ h1 = (((h1b & 0xffff) + 0x6b64) + ((((h1b >>> 16) + 0xe654) & 0xffff) << 16));
35
+ }
36
+
37
+ if (remainder >= 1) {
38
+ k1 = 0;
39
+ if (remainder >= 3) k1 ^= (key.charCodeAt(i + 2) & 0xff) << 16;
40
+ if (remainder >= 2) k1 ^= (key.charCodeAt(i + 1) & 0xff) << 8;
41
+ k1 ^= (key.charCodeAt(i) & 0xff);
42
+ k1 = (((k1 & 0xffff) * c1) + ((((k1 >>> 16) * c1) & 0xffff) << 16)) & 0xffffffff;
43
+ k1 = (k1 << 15) | (k1 >>> 17);
44
+ k1 = (((k1 & 0xffff) * c2) + ((((k1 >>> 16) * c2) & 0xffff) << 16)) & 0xffffffff;
45
+ h1 ^= k1;
46
+ }
47
+
48
+ h1 ^= key.length;
49
+
50
+ h1 ^= h1 >>> 16;
51
+ h1 = (((h1 & 0xffff) * 0x85ebca6b) + ((((h1 >>> 16) * 0x85ebca6b) & 0xffff) << 16)) & 0xffffffff;
52
+ h1 ^= h1 >>> 13;
53
+ h1 = ((((h1 & 0xffff) * 0xc2b2ae35) + ((((h1 >>> 16) * 0xc2b2ae35) & 0xffff) << 16))) & 0xffffffff;
54
+ h1 ^= h1 >>> 16;
55
+
56
+ return h1 >>> 0;
57
+ }
58
+
59
+ export interface FeatureFlagPayload {
60
+ key: string;
61
+ variants: string[];
62
+ rollout: number[];
63
+ }
64
+
65
+ export class FlagService {
66
+ private assignments: Record<string, string> = {};
67
+ private fetched: boolean = false;
68
+
69
+ constructor(private hostUrl: string) {}
70
+
71
+ /**
72
+ * Fetch feature flags from the dashboard backend
73
+ */
74
+ async fetch(analyticsKey: string, userId?: string): Promise<void> {
75
+ try {
76
+ // Avoid fetching if already loaded, unless explicitly forced?
77
+ // For now, allow refetching just in case.
78
+ const res = await fetch(`${this.hostUrl}/api/v1/flags/sync?key=${analyticsKey}`);
79
+ if (!res.ok) {
80
+ throw new Error(`Failed to fetch flags: ${res.status}`);
81
+ }
82
+
83
+ const data = await res.json();
84
+ const flags: FeatureFlagPayload[] = data.flags || [];
85
+
86
+ this.assignAll(flags, userId);
87
+ this.fetched = true;
88
+ logger.info(LOG_TAG, `Fetched ${flags.length} flags`);
89
+ } catch (err: any) {
90
+ logger.warn(LOG_TAG, `Could not sync feature flags: ${err.message}`);
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Deterministically assign a variant using murmurhash.
96
+ */
97
+ private assignVariant(userIdentifier: string, flagKey: string, variants: string[], rollout: number[]): string {
98
+ const hash = murmurhash3_32_gc(`${userIdentifier}_${flagKey}`) % 100;
99
+ let cumulative = 0;
100
+
101
+ for (let i = 0; i < rollout.length; i++) {
102
+ cumulative += rollout[i]!;
103
+ if (hash < cumulative) {
104
+ return variants[i]!;
105
+ }
106
+ }
107
+
108
+ // Fallback if rollout doesn't equal exactly 100 or edge case
109
+ return variants[0]!
110
+ }
111
+
112
+ private assignAll(flags: FeatureFlagPayload[], userId?: string) {
113
+ const identifier = userId || getDeviceId();
114
+
115
+ const newAssignments: Record<string, string> = {};
116
+ for (const flag of flags) {
117
+ if (flag.variants.length > 0 && flag.rollout.length === flag.variants.length) {
118
+ newAssignments[flag.key] = this.assignVariant(identifier, flag.key, flag.variants, flag.rollout);
119
+ }
120
+ }
121
+
122
+ this.assignments = newAssignments;
123
+ }
124
+
125
+ /** Get a specific flag value */
126
+ getFlag(key: string, defaultValue?: string): string {
127
+ if (!this.fetched) {
128
+ logger.debug(LOG_TAG, `getFlag("${key}") called before flags were fetched. Returning default.`);
129
+ }
130
+ return this.assignments[key] ?? defaultValue ?? '';
131
+ }
132
+
133
+ /** Get all active assignments for telemetry */
134
+ getAllFlags(): Record<string, string> {
135
+ return { ...this.assignments };
136
+ }
137
+ }