@mobileai/react-native 0.9.27 → 0.9.29

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 (65) hide show
  1. package/README.md +28 -16
  2. package/android/build.gradle +17 -0
  3. package/android/src/main/java/com/mobileai/overlay/FloatingOverlayDialogRootViewGroup.kt +243 -0
  4. package/android/src/main/java/com/mobileai/overlay/FloatingOverlayView.kt +281 -87
  5. package/android/src/newarch/com/mobileai/overlay/FloatingOverlayViewManager.kt +52 -17
  6. package/android/src/oldarch/com/mobileai/overlay/FloatingOverlayViewManager.kt +49 -2
  7. package/bin/generate-map.cjs +45 -6
  8. package/ios/MobileAIFloatingOverlayComponentView.h +8 -0
  9. package/ios/MobileAIFloatingOverlayComponentView.mm +12 -41
  10. package/ios/Podfile +63 -0
  11. package/ios/Podfile.lock +2290 -0
  12. package/ios/Podfile.properties.json +4 -0
  13. package/ios/mobileaireactnative/AppDelegate.swift +69 -0
  14. package/ios/mobileaireactnative/Images.xcassets/AppIcon.appiconset/Contents.json +13 -0
  15. package/ios/mobileaireactnative/Images.xcassets/Contents.json +6 -0
  16. package/ios/mobileaireactnative/Images.xcassets/SplashScreenLegacy.imageset/Contents.json +21 -0
  17. package/ios/mobileaireactnative/Images.xcassets/SplashScreenLegacy.imageset/SplashScreenLegacy.png +0 -0
  18. package/ios/mobileaireactnative/Info.plist +55 -0
  19. package/ios/mobileaireactnative/PrivacyInfo.xcprivacy +48 -0
  20. package/ios/mobileaireactnative/SplashScreen.storyboard +47 -0
  21. package/ios/mobileaireactnative/Supporting/Expo.plist +6 -0
  22. package/ios/mobileaireactnative/mobileaireactnative-Bridging-Header.h +3 -0
  23. package/ios/mobileaireactnative.xcodeproj/project.pbxproj +547 -0
  24. package/ios/mobileaireactnative.xcodeproj/xcshareddata/xcschemes/mobileaireactnative.xcscheme +88 -0
  25. package/ios/mobileaireactnative.xcworkspace/contents.xcworkspacedata +10 -0
  26. package/lib/module/components/AIAgent.js +501 -191
  27. package/lib/module/components/AgentChatBar.js +250 -59
  28. package/lib/module/components/FloatingOverlayWrapper.js +68 -32
  29. package/lib/module/config/endpoints.js +22 -1
  30. package/lib/module/core/AgentRuntime.js +110 -8
  31. package/lib/module/core/FiberTreeWalker.js +211 -10
  32. package/lib/module/core/OutcomeVerifier.js +149 -0
  33. package/lib/module/core/systemPrompt.js +96 -25
  34. package/lib/module/providers/GeminiProvider.js +9 -3
  35. package/lib/module/services/telemetry/TelemetryService.js +21 -2
  36. package/lib/module/services/telemetry/TouchAutoCapture.js +235 -38
  37. package/lib/module/services/telemetry/analyticsLabeling.js +187 -0
  38. package/lib/module/specs/FloatingOverlayNativeComponent.ts +7 -1
  39. package/lib/module/support/supportPrompt.js +22 -7
  40. package/lib/module/support/supportStyle.js +55 -0
  41. package/lib/module/support/types.js +2 -0
  42. package/lib/module/tools/typeTool.js +20 -0
  43. package/lib/module/utils/humanizeScreenName.js +49 -0
  44. package/lib/typescript/src/components/AIAgent.d.ts +6 -2
  45. package/lib/typescript/src/components/AgentChatBar.d.ts +15 -1
  46. package/lib/typescript/src/components/FloatingOverlayWrapper.d.ts +22 -10
  47. package/lib/typescript/src/config/endpoints.d.ts +4 -0
  48. package/lib/typescript/src/core/AgentRuntime.d.ts +12 -3
  49. package/lib/typescript/src/core/FiberTreeWalker.d.ts +12 -1
  50. package/lib/typescript/src/core/OutcomeVerifier.d.ts +46 -0
  51. package/lib/typescript/src/core/systemPrompt.d.ts +3 -10
  52. package/lib/typescript/src/core/types.d.ts +63 -0
  53. package/lib/typescript/src/index.d.ts +1 -0
  54. package/lib/typescript/src/services/telemetry/TelemetryService.d.ts +7 -1
  55. package/lib/typescript/src/services/telemetry/TouchAutoCapture.d.ts +6 -1
  56. package/lib/typescript/src/services/telemetry/analyticsLabeling.d.ts +20 -0
  57. package/lib/typescript/src/services/telemetry/types.d.ts +1 -1
  58. package/lib/typescript/src/specs/FloatingOverlayNativeComponent.d.ts +5 -0
  59. package/lib/typescript/src/support/index.d.ts +1 -0
  60. package/lib/typescript/src/support/supportStyle.d.ts +9 -0
  61. package/lib/typescript/src/support/types.d.ts +3 -0
  62. package/lib/typescript/src/utils/humanizeScreenName.d.ts +6 -0
  63. package/package.json +10 -10
  64. package/src/specs/FloatingOverlayNativeComponent.ts +7 -1
  65. package/ios/MobileAIPilotIntents.swift +0 -51
@@ -6,7 +6,7 @@
6
6
  * Does not block underlying UI natively.
7
7
  */
8
8
 
9
- import { useState, useRef, useEffect } from 'react';
9
+ import { useState, useRef, useEffect, useCallback, useMemo } from 'react';
10
10
  import { View, TextInput, Pressable, Text, StyleSheet, Animated, PanResponder, ScrollView, Keyboard, Platform, useWindowDimensions } from 'react-native';
11
11
  import { MicIcon, SpeakerIcon, SendArrowIcon, StopIcon, LoadingDots, AIBadge, HistoryIcon, NewChatIcon, CloseIcon } from "./Icons.js";
12
12
  import { logger } from "../utils/logger.js";
@@ -170,6 +170,7 @@ function TextInputRow({
170
170
  text,
171
171
  setText,
172
172
  onSend,
173
+ onCancel,
173
174
  isThinking,
174
175
  isArabic,
175
176
  theme
@@ -182,6 +183,14 @@ function TextInputRow({
182
183
  // render batch.
183
184
  inputRef.current?.clear();
184
185
  };
186
+ const handlePrimaryAction = () => {
187
+ if (isThinking) {
188
+ onCancel?.();
189
+ return;
190
+ }
191
+ if (!text.trim()) return;
192
+ handleSendWithClear();
193
+ };
185
194
  return /*#__PURE__*/_jsxs(View, {
186
195
  style: styles.inputRow,
187
196
  children: [/*#__PURE__*/_jsx(TextInput, {
@@ -208,10 +217,10 @@ function TextInputRow({
208
217
  style: [styles.sendButton, isThinking && styles.sendButtonDisabled, theme?.primaryColor ? {
209
218
  backgroundColor: theme.primaryColor
210
219
  } : undefined],
211
- onPress: handleSendWithClear,
212
- disabled: isThinking || !text.trim(),
213
- accessibilityLabel: "Send request to AI Agent",
214
- children: isThinking ? /*#__PURE__*/_jsx(LoadingDots, {
220
+ onPress: handlePrimaryAction,
221
+ disabled: !isThinking && !text.trim(),
222
+ accessibilityLabel: isThinking ? 'Stop AI Agent request' : 'Send request to AI Agent',
223
+ children: isThinking ? /*#__PURE__*/_jsx(StopIcon, {
215
224
  size: 18,
216
225
  color: theme?.textColor || '#fff'
217
226
  }) : /*#__PURE__*/_jsx(SendArrowIcon, {
@@ -288,6 +297,7 @@ function VoiceControlsRow({
288
297
 
289
298
  export function AgentChatBar({
290
299
  onSend,
300
+ onCancel,
291
301
  isThinking,
292
302
  statusText,
293
303
  lastResult,
@@ -318,7 +328,10 @@ export function AgentChatBar({
318
328
  onConversationSelect,
319
329
  onNewConversation,
320
330
  pendingApprovalQuestion,
321
- onPendingApprovalAction
331
+ onPendingApprovalAction,
332
+ renderMode = 'default',
333
+ onWindowMetricsChange,
334
+ windowMetrics
322
335
  }) {
323
336
  const [text, setText] = useState('');
324
337
  const [isExpanded, setIsExpanded] = useState(false);
@@ -336,6 +349,85 @@ export function AgentChatBar({
336
349
  const preKeyboardYRef = useRef(null);
337
350
  const previousThinkingRef = useRef(false);
338
351
  const autoCollapsedForThinkingRef = useRef(false);
352
+ const dragOriginRef = useRef({
353
+ x: 0,
354
+ y: 0
355
+ });
356
+ const panPositionRef = useRef({
357
+ x: 10,
358
+ y: height - 200
359
+ });
360
+ const panelHeightRef = useRef(0);
361
+ const isExpandedRef = useRef(false);
362
+ const isAndroidNativeWindow = renderMode === 'android-native-window';
363
+ const metricsFrameRef = useRef(null);
364
+ const pendingMetricsRef = useRef(null);
365
+ const getExpandedWindowHeight = useCallback(measuredPanelHeight => {
366
+ const minExpandedHeight = mode === 'voice' ? 150 : showHistory ? 280 : mode === 'human' ? 240 : pendingApprovalQuestion ? 220 : 164;
367
+ const maxExpandedHeight = Math.min(height * 0.65, 520);
368
+ const naturalHeight = measuredPanelHeight > 0 ? measuredPanelHeight : minExpandedHeight;
369
+ return Math.max(minExpandedHeight, Math.min(naturalHeight, maxExpandedHeight));
370
+ }, [height, mode, showHistory]);
371
+ const getWindowSize = useCallback((expanded = isExpandedRef.current, measuredPanelHeight = panelHeightRef.current) => {
372
+ return {
373
+ width: expanded ? 340 : 60,
374
+ height: expanded ? getExpandedWindowHeight(measuredPanelHeight) : 60
375
+ };
376
+ }, [getExpandedWindowHeight]);
377
+ const clampWindowPosition = useCallback((x, y, expanded = isExpandedRef.current, measuredPanelHeight = panelHeightRef.current) => {
378
+ const screenInset = 10;
379
+ const bottomInset = 24;
380
+ const {
381
+ width: windowWidth,
382
+ height: windowHeight
383
+ } = getWindowSize(expanded, measuredPanelHeight);
384
+ const maxX = Math.max(screenInset, width - windowWidth - screenInset);
385
+ const maxY = Math.max(screenInset, height - windowHeight - bottomInset);
386
+ return {
387
+ x: Math.min(Math.max(x, screenInset), maxX),
388
+ y: Math.min(Math.max(y, screenInset), maxY)
389
+ };
390
+ }, [getWindowSize, height, width]);
391
+ const publishWindowMetrics = useCallback((x = panPositionRef.current.x, y = panPositionRef.current.y, expanded = isExpandedRef.current, measuredPanelHeight = panelHeightRef.current) => {
392
+ if (!onWindowMetricsChange) return;
393
+ const {
394
+ x: clampedX,
395
+ y: clampedY
396
+ } = clampWindowPosition(x, y, expanded, measuredPanelHeight);
397
+ const {
398
+ width: resolvedWidth,
399
+ height: resolvedHeight
400
+ } = getWindowSize(expanded, measuredPanelHeight);
401
+ const nextMetrics = {
402
+ x: Math.round(clampedX),
403
+ y: Math.round(clampedY),
404
+ width: resolvedWidth,
405
+ height: Math.round(resolvedHeight)
406
+ };
407
+ onWindowMetricsChange(nextMetrics);
408
+ }, [clampWindowPosition, getWindowSize, onWindowMetricsChange]);
409
+ const publishNativeWindowPosition = useCallback((x, y, expanded = isExpandedRef.current, measuredPanelHeight = panelHeightRef.current) => {
410
+ const clampedPosition = clampWindowPosition(x, y, expanded, measuredPanelHeight);
411
+ panPositionRef.current = clampedPosition;
412
+ publishWindowMetrics(clampedPosition.x, clampedPosition.y, expanded, measuredPanelHeight);
413
+ }, [clampWindowPosition, publishWindowMetrics]);
414
+ const scheduleWindowMetricsPublish = useCallback((x = panPositionRef.current.x, y = panPositionRef.current.y, expanded = isExpandedRef.current, measuredPanelHeight = panelHeightRef.current) => {
415
+ if (!onWindowMetricsChange) return;
416
+ pendingMetricsRef.current = {
417
+ x,
418
+ y,
419
+ expanded,
420
+ measuredPanelHeight
421
+ };
422
+ if (metricsFrameRef.current != null) return;
423
+ metricsFrameRef.current = requestAnimationFrame(() => {
424
+ metricsFrameRef.current = null;
425
+ const pending = pendingMetricsRef.current;
426
+ pendingMetricsRef.current = null;
427
+ if (!pending) return;
428
+ publishWindowMetrics(pending.x, pending.y, pending.expanded, pending.measuredPanelHeight);
429
+ });
430
+ }, [onWindowMetricsChange, publishWindowMetrics]);
339
431
 
340
432
  // Track incoming AI messages while collapsed
341
433
  useEffect(() => {
@@ -350,6 +442,13 @@ export function AgentChatBar({
350
442
  useEffect(() => {
351
443
  if (autoExpandTrigger > 0) setIsExpanded(true);
352
444
  }, [autoExpandTrigger]);
445
+ useEffect(() => {
446
+ return () => {
447
+ if (metricsFrameRef.current != null) {
448
+ cancelAnimationFrame(metricsFrameRef.current);
449
+ }
450
+ };
451
+ }, []);
353
452
  useEffect(() => {
354
453
  const wasThinking = previousThinkingRef.current;
355
454
  if (pendingApprovalQuestion) {
@@ -365,17 +464,54 @@ export function AgentChatBar({
365
464
  x: 10,
366
465
  y: height - 200
367
466
  })).current;
467
+ useEffect(() => {
468
+ if (!isAndroidNativeWindow || !windowMetrics) return;
469
+ const nextPosition = {
470
+ x: windowMetrics.x,
471
+ y: windowMetrics.y
472
+ };
473
+ if (panPositionRef.current.x === nextPosition.x && panPositionRef.current.y === nextPosition.y) {
474
+ return;
475
+ }
476
+ panPositionRef.current = nextPosition;
477
+ pan.setValue(nextPosition);
478
+ }, [isAndroidNativeWindow, pan, windowMetrics]);
368
479
  const tooltipSide = fabX < width / 2 ? 'right' : 'left';
480
+ const expandedContentMinHeight = getExpandedWindowHeight(0);
369
481
  useEffect(() => {
370
- const listenerId = pan.x.addListener(({
482
+ isExpandedRef.current = isExpanded;
483
+ const clampedPosition = clampWindowPosition(panPositionRef.current.x, panPositionRef.current.y, isExpanded);
484
+ if (clampedPosition.x !== panPositionRef.current.x || clampedPosition.y !== panPositionRef.current.y) {
485
+ panPositionRef.current = clampedPosition;
486
+ pan.setValue(clampedPosition);
487
+ if (!isAndroidNativeWindow) {
488
+ setFabX(clampedPosition.x);
489
+ }
490
+ }
491
+ scheduleWindowMetricsPublish(clampedPosition.x, clampedPosition.y, isExpanded);
492
+ }, [clampWindowPosition, isAndroidNativeWindow, isExpanded, pan, scheduleWindowMetricsPublish]);
493
+ useEffect(() => {
494
+ if (isAndroidNativeWindow) return;
495
+ const xListenerId = pan.x.addListener(({
496
+ value
497
+ }) => {
498
+ panPositionRef.current.x = value;
499
+ if (!isAndroidNativeWindow) {
500
+ setFabX(value);
501
+ }
502
+ scheduleWindowMetricsPublish(value, panPositionRef.current.y);
503
+ });
504
+ const yListenerId = pan.y.addListener(({
371
505
  value
372
506
  }) => {
373
- setFabX(value);
507
+ panPositionRef.current.y = value;
508
+ scheduleWindowMetricsPublish(panPositionRef.current.x, value);
374
509
  });
375
510
  return () => {
376
- pan.x.removeListener(listenerId);
511
+ pan.x.removeListener(xListenerId);
512
+ pan.y.removeListener(yListenerId);
377
513
  };
378
- }, [pan.x]);
514
+ }, [isAndroidNativeWindow, pan.x, pan.y, scheduleWindowMetricsPublish]);
379
515
 
380
516
  // ─── Keyboard Handling ──────────────────────────────────────
381
517
  useEffect(() => {
@@ -384,6 +520,14 @@ export function AgentChatBar({
384
520
  const keyboardMargin = 12;
385
521
  const showSub = Keyboard.addListener(showEvent, e => {
386
522
  if (!isExpanded || mode !== 'text' || panelHeight <= 0) return;
523
+ if (isAndroidNativeWindow) {
524
+ const currentY = panPositionRef.current.y;
525
+ const targetY = Math.max(keyboardMargin, height - e.endCoordinates.height - panelHeight - keyboardMargin);
526
+ preKeyboardYRef.current = currentY;
527
+ if (currentY <= targetY) return;
528
+ publishNativeWindowPosition(panPositionRef.current.x, targetY);
529
+ return;
530
+ }
387
531
  pan.y.stopAnimation(currentY => {
388
532
  const targetY = Math.max(keyboardMargin, height - e.endCoordinates.height - panelHeight - keyboardMargin);
389
533
 
@@ -403,6 +547,10 @@ export function AgentChatBar({
403
547
  const restoreY = preKeyboardYRef.current;
404
548
  if (restoreY == null) return;
405
549
  preKeyboardYRef.current = null;
550
+ if (isAndroidNativeWindow) {
551
+ publishNativeWindowPosition(panPositionRef.current.x, restoreY);
552
+ return;
553
+ }
406
554
  Animated.timing(pan.y, {
407
555
  toValue: restoreY,
408
556
  duration: 200,
@@ -413,31 +561,58 @@ export function AgentChatBar({
413
561
  showSub.remove();
414
562
  hideSub.remove();
415
563
  };
416
- }, [height, isExpanded, mode, pan.y, panelHeight]);
417
- const panResponder = useRef(PanResponder.create({
564
+ }, [height, isAndroidNativeWindow, isExpanded, mode, pan.y, panelHeight, publishNativeWindowPosition]);
565
+ const panResponder = useMemo(() => PanResponder.create({
418
566
  onMoveShouldSetPanResponder: (_, gestureState) => {
419
567
  return Math.abs(gestureState.dx) > 5 || Math.abs(gestureState.dy) > 5;
420
568
  },
421
569
  onPanResponderGrant: () => {
422
- pan.setOffset({
423
- x: pan.x._value,
424
- y: pan.y._value
425
- });
570
+ if (isAndroidNativeWindow) {
571
+ dragOriginRef.current = {
572
+ ...panPositionRef.current
573
+ };
574
+ return;
575
+ }
576
+ pan.extractOffset();
426
577
  pan.setValue({
427
578
  x: 0,
428
579
  y: 0
429
580
  });
430
581
  },
431
- onPanResponderMove: Animated.event([null, {
432
- dx: pan.x,
433
- dy: pan.y
434
- }], {
435
- useNativeDriver: false
436
- }),
582
+ onPanResponderMove: (event, gestureState) => {
583
+ if (isAndroidNativeWindow) {
584
+ publishNativeWindowPosition(dragOriginRef.current.x + gestureState.dx, dragOriginRef.current.y + gestureState.dy);
585
+ return;
586
+ }
587
+ Animated.event([null, {
588
+ dx: pan.x,
589
+ dy: pan.y
590
+ }], {
591
+ useNativeDriver: false
592
+ })(event, gestureState);
593
+ },
437
594
  onPanResponderRelease: () => {
595
+ if (isAndroidNativeWindow) {
596
+ publishNativeWindowPosition(panPositionRef.current.x, panPositionRef.current.y);
597
+ return;
598
+ }
438
599
  pan.flattenOffset();
600
+ const clampedPosition = clampWindowPosition(panPositionRef.current.x, panPositionRef.current.y);
601
+ Animated.spring(pan, {
602
+ toValue: clampedPosition,
603
+ useNativeDriver: false,
604
+ tension: 120,
605
+ friction: 14
606
+ }).start(() => {
607
+ panPositionRef.current = clampedPosition;
608
+ if (!isAndroidNativeWindow) {
609
+ setFabX(clampedPosition.x);
610
+ }
611
+ scheduleWindowMetricsPublish(clampedPosition.x, clampedPosition.y);
612
+ });
439
613
  }
440
- })).current;
614
+ }), [clampWindowPosition, isAndroidNativeWindow, pan, publishNativeWindowPosition, scheduleWindowMetricsPublish]);
615
+ const jsDragHandlers = isAndroidNativeWindow ? undefined : panResponder.panHandlers;
441
616
  const handleSend = () => {
442
617
  if (text.trim() && !isThinking) {
443
618
  onSend(text.trim());
@@ -452,8 +627,8 @@ export function AgentChatBar({
452
627
 
453
628
  if (!isExpanded) {
454
629
  return /*#__PURE__*/_jsxs(Animated.View, {
455
- style: [styles.fabContainer, pan.getLayout()],
456
- ...panResponder.panHandlers,
630
+ style: [styles.fabContainer, isAndroidNativeWindow ? styles.fabContainerNativeWindow : pan.getLayout()],
631
+ ...(jsDragHandlers ?? {}),
457
632
  children: [/*#__PURE__*/_jsx(Pressable, {
458
633
  style: [styles.fab, theme?.primaryColor ? {
459
634
  backgroundColor: theme.primaryColor
@@ -471,13 +646,13 @@ export function AgentChatBar({
471
646
  }) : /*#__PURE__*/_jsx(AIBadge, {
472
647
  size: 28
473
648
  })
474
- }), showDiscoveryTooltip && /*#__PURE__*/_jsx(DiscoveryTooltip, {
649
+ }), showDiscoveryTooltip && !isAndroidNativeWindow && /*#__PURE__*/_jsx(DiscoveryTooltip, {
475
650
  language: language,
476
651
  primaryColor: theme?.primaryColor,
477
652
  message: discoveryTooltipMessage,
478
653
  side: tooltipSide,
479
654
  onDismiss: () => onTooltipDismiss?.()
480
- }), localUnread > 0 && chatMessages.length > 0 && /*#__PURE__*/_jsxs(Pressable, {
655
+ }), localUnread > 0 && chatMessages.length > 0 && !isAndroidNativeWindow && /*#__PURE__*/_jsxs(Pressable, {
481
656
  style: [styles.unreadPopup, isArabic ? styles.unreadPopupRTL : styles.unreadPopupLTR],
482
657
  onPress: () => {
483
658
  onTooltipDismiss?.();
@@ -503,7 +678,7 @@ export function AgentChatBar({
503
678
  children: displayUnread > 99 ? '99+' : displayUnread
504
679
  })
505
680
  })]
506
- }), isThinking && !pendingApprovalQuestion && /*#__PURE__*/_jsxs(Pressable, {
681
+ }), isThinking && !pendingApprovalQuestion && !isAndroidNativeWindow && /*#__PURE__*/_jsxs(Pressable, {
507
682
  style: [styles.statusPopup, isArabic ? styles.unreadPopupRTL : styles.unreadPopupLTR],
508
683
  onPress: () => {
509
684
  autoCollapsedForThinkingRef.current = false;
@@ -533,7 +708,9 @@ export function AgentChatBar({
533
708
  // ─── Expanded Widget ───────────────────────────────────────
534
709
 
535
710
  return /*#__PURE__*/_jsxs(Animated.View, {
536
- style: [styles.expandedContainer, pan.getLayout(), {
711
+ style: [styles.expandedContainer, isAndroidNativeWindow ? styles.expandedContainerNativeWindow : pan.getLayout(), isAndroidNativeWindow ? {
712
+ minHeight: expandedContentMinHeight
713
+ } : null, {
537
714
  maxHeight: height * 0.65
538
715
  }, theme?.backgroundColor ? {
539
716
  backgroundColor: theme.backgroundColor
@@ -541,11 +718,13 @@ export function AgentChatBar({
541
718
  onLayout: event => {
542
719
  const nextHeight = event.nativeEvent.layout.height;
543
720
  if (Math.abs(nextHeight - panelHeight) > 1) {
721
+ panelHeightRef.current = nextHeight;
544
722
  setPanelHeight(nextHeight);
723
+ scheduleWindowMetricsPublish(panPositionRef.current.x, panPositionRef.current.y, true, nextHeight);
545
724
  }
546
725
  },
547
726
  children: [/*#__PURE__*/_jsx(View, {
548
- ...panResponder.panHandlers,
727
+ ...(jsDragHandlers ?? {}),
549
728
  style: styles.dragHandleArea,
550
729
  accessibilityLabel: "Drag AI Agent",
551
730
  children: /*#__PURE__*/_jsx(View, {
@@ -589,34 +768,36 @@ export function AgentChatBar({
589
768
  children: "+"
590
769
  })
591
770
  })]
592
- }), /*#__PURE__*/_jsx(View, {
593
- ...panResponder.panHandlers,
594
- style: [styles.cornerHandle, styles.cornerTL],
595
- pointerEvents: "box-only",
596
- children: /*#__PURE__*/_jsx(View, {
597
- style: [styles.cornerIndicator, styles.cornerIndicatorTL]
598
- })
599
- }), /*#__PURE__*/_jsx(View, {
600
- ...panResponder.panHandlers,
601
- style: [styles.cornerHandle, styles.cornerTR],
602
- pointerEvents: "box-only",
603
- children: /*#__PURE__*/_jsx(View, {
604
- style: [styles.cornerIndicator, styles.cornerIndicatorTR]
605
- })
606
- }), /*#__PURE__*/_jsx(View, {
607
- ...panResponder.panHandlers,
608
- style: [styles.cornerHandle, styles.cornerBL],
609
- pointerEvents: "box-only",
610
- children: /*#__PURE__*/_jsx(View, {
611
- style: [styles.cornerIndicator, styles.cornerIndicatorBL]
612
- })
613
- }), /*#__PURE__*/_jsx(View, {
614
- ...panResponder.panHandlers,
615
- style: [styles.cornerHandle, styles.cornerBR],
616
- pointerEvents: "box-only",
617
- children: /*#__PURE__*/_jsx(View, {
618
- style: [styles.cornerIndicator, styles.cornerIndicatorBR]
619
- })
771
+ }), !isAndroidNativeWindow && /*#__PURE__*/_jsxs(_Fragment, {
772
+ children: [/*#__PURE__*/_jsx(View, {
773
+ ...panResponder.panHandlers,
774
+ style: [styles.cornerHandle, styles.cornerTL],
775
+ pointerEvents: "box-only",
776
+ children: /*#__PURE__*/_jsx(View, {
777
+ style: [styles.cornerIndicator, styles.cornerIndicatorTL]
778
+ })
779
+ }), /*#__PURE__*/_jsx(View, {
780
+ ...panResponder.panHandlers,
781
+ style: [styles.cornerHandle, styles.cornerTR],
782
+ pointerEvents: "box-only",
783
+ children: /*#__PURE__*/_jsx(View, {
784
+ style: [styles.cornerIndicator, styles.cornerIndicatorTR]
785
+ })
786
+ }), /*#__PURE__*/_jsx(View, {
787
+ ...panResponder.panHandlers,
788
+ style: [styles.cornerHandle, styles.cornerBL],
789
+ pointerEvents: "box-only",
790
+ children: /*#__PURE__*/_jsx(View, {
791
+ style: [styles.cornerIndicator, styles.cornerIndicatorBL]
792
+ })
793
+ }), /*#__PURE__*/_jsx(View, {
794
+ ...panResponder.panHandlers,
795
+ style: [styles.cornerHandle, styles.cornerBR],
796
+ pointerEvents: "box-only",
797
+ children: /*#__PURE__*/_jsx(View, {
798
+ style: [styles.cornerIndicator, styles.cornerIndicatorBR]
799
+ })
800
+ })]
620
801
  }), !showHistory && /*#__PURE__*/_jsx(ModeSelector, {
621
802
  modes: availableModes,
622
803
  activeMode: mode,
@@ -811,6 +992,7 @@ export function AgentChatBar({
811
992
  text: text,
812
993
  setText: setText,
813
994
  onSend: handleSend,
995
+ onCancel: onCancel,
814
996
  isThinking: isThinking,
815
997
  isArabic: isArabic,
816
998
  theme: theme
@@ -869,6 +1051,9 @@ const styles = StyleSheet.create({
869
1051
  position: 'absolute',
870
1052
  zIndex: 9999
871
1053
  },
1054
+ fabContainerNativeWindow: {
1055
+ position: 'relative'
1056
+ },
872
1057
  fab: {
873
1058
  width: 60,
874
1059
  height: 60,
@@ -970,6 +1155,9 @@ const styles = StyleSheet.create({
970
1155
  shadowOpacity: 0.4,
971
1156
  shadowRadius: 10
972
1157
  },
1158
+ expandedContainerNativeWindow: {
1159
+ position: 'relative'
1160
+ },
973
1161
  dragHandleArea: {
974
1162
  width: '100%',
975
1163
  height: 30,
@@ -1052,7 +1240,8 @@ const styles = StyleSheet.create({
1052
1240
  borderBottomRightRadius: 1
1053
1241
  },
1054
1242
  messageList: {
1055
- marginBottom: 12
1243
+ marginBottom: 12,
1244
+ flexShrink: 1
1056
1245
  },
1057
1246
  messageBubble: {
1058
1247
  padding: 12,
@@ -1094,6 +1283,8 @@ const styles = StyleSheet.create({
1094
1283
  alignItems: 'flex-end',
1095
1284
  gap: 8,
1096
1285
  justifyContent: 'center',
1286
+ minHeight: 48,
1287
+ flexShrink: 0,
1097
1288
  paddingBottom: 2 // Slight padding so buttons don't clip against bottom edge
1098
1289
  },
1099
1290
  approvalPanel: {
@@ -11,13 +11,10 @@
11
11
  * Renders ABOVE all native Modals, system alerts, and navigation chrome.
12
12
  * 2. Falls back to plain View if react-native-screens is not installed.
13
13
  *
14
- * Android (both Old and New Architecture):
15
- * 1. Native `MobileAIFloatingOverlay` ViewManager (bundled in this library).
16
- * Creates a Dialog window with TYPE_APPLICATION_PANEL (z=1000),
17
- * above normal app Dialog windows (TYPE_APPLICATION, z=2).
18
- * No SYSTEM_ALERT_WINDOW permission needed — scoped to app's own window.
19
- * 2. Falls back to plain View if the app hasn't been rebuilt after install
20
- * (graceful degradation with DEV warning).
14
+ * Android:
15
+ * 1. Uses a native panel dialog window when explicit bounds are provided.
16
+ * This keeps the floating agent compact and above native modal surfaces.
17
+ * 2. Falls back to a plain View otherwise.
21
18
  *
22
19
  * Usage:
23
20
  * <FloatingOverlayWrapper fallbackStyle={styles.floatingLayer}>
@@ -31,14 +28,14 @@
31
28
  * but passing StyleSheet.absoluteFill is often necessary to prevent dimensions collapsing conditionally.
32
29
  */
33
30
 
34
- // imports consolidated above
31
+ import React, { forwardRef, useEffect, useImperativeHandle, useRef } from 'react';
32
+ import { DeviceEventEmitter, Platform, View, StyleSheet, findNodeHandle } from 'react-native';
35
33
 
36
34
  // ─── iOS: FullWindowOverlay (react-native-screens optional peer dep) ──────────
37
-
35
+ import { jsx as _jsx } from "react/jsx-runtime";
38
36
  let FullWindowOverlay = null;
39
37
  if (Platform.OS === 'ios') {
40
38
  try {
41
- // Literal string required by Metro bundler — try/catch handles MODULE_NOT_FOUND
42
39
  const screens = require('react-native-screens');
43
40
  FullWindowOverlay = screens.FullWindowOverlay ?? null;
44
41
  } catch {
@@ -46,43 +43,66 @@ if (Platform.OS === 'ios') {
46
43
  }
47
44
  }
48
45
 
49
- // ─── Android: MobileAIFloatingOverlay (native module bundled in this library) ──
46
+ // ─── Export: whether a true elevated overlay is active ───────────────────────
50
47
 
51
48
  let NativeFloatingOverlay = null;
52
49
  if (Platform.OS === 'android') {
53
50
  try {
54
- const {
55
- requireNativeComponent
56
- } = require('react-native');
57
- // Throws if ViewManager is not registered (app hasn't been rebuilt after install)
58
- NativeFloatingOverlay = requireNativeComponent('MobileAIFloatingOverlay');
51
+ NativeFloatingOverlay = require('../specs/FloatingOverlayNativeComponent.ts').default;
59
52
  } catch {
60
- if (__DEV__) {
61
- console.warn('[MobileAI] MobileAIFloatingOverlay native module not found on Android.\n' + 'The chat bar may appear behind native Modals.\n' + 'Fix: rebuild the app with `npx react-native run-android` or `npx expo run:android`.');
62
- }
53
+ // Falls back to a plain View when the native Android overlay isn't available.
63
54
  }
64
55
  }
65
56
 
66
- // ─── Export: whether a true elevated overlay is active ───────────────────────
67
-
68
57
  /**
69
58
  * True when a native elevated overlay is available on the current platform.
70
59
  * Used by AIConsentDialog to decide whether to render as View vs Modal.
71
60
  *
72
61
  * iOS + react-native-screens installed → true
73
- * Android + native rebuild done → true
74
62
  * Everything else (fallback) → false
75
63
  */
76
- export const isNativeOverlayActive = Platform.OS === 'ios' && !!FullWindowOverlay || Platform.OS === 'android' && !!NativeFloatingOverlay;
64
+ export const isNativeOverlayActive = Platform.OS === 'ios' && !!FullWindowOverlay;
65
+ const ANDROID_WINDOW_DRAG_END_EVENT = 'mobileaiFloatingOverlayDragEnd';
77
66
 
78
67
  // ─── Component ────────────────────────────────────────────────────────────────
79
68
 
80
- import { Platform, View, StyleSheet } from 'react-native';
81
- import { jsx as _jsx } from "react/jsx-runtime";
82
- export function FloatingOverlayWrapper({
69
+ export const FloatingOverlayWrapper = /*#__PURE__*/forwardRef(function FloatingOverlayWrapper({
83
70
  children,
71
+ androidWindowMetrics,
72
+ onAndroidWindowDragEnd,
84
73
  fallbackStyle
85
- }) {
74
+ }, ref) {
75
+ const nativeOverlayRef = useRef(null);
76
+ useImperativeHandle(ref, () => ({
77
+ setAndroidWindowMetrics: metrics => {
78
+ nativeOverlayRef.current?.setNativeProps?.({
79
+ windowX: metrics.x,
80
+ windowY: metrics.y,
81
+ windowWidth: metrics.width,
82
+ windowHeight: metrics.height
83
+ });
84
+ }
85
+ }), []);
86
+ useEffect(() => {
87
+ if (Platform.OS !== 'android' || !onAndroidWindowDragEnd) return;
88
+ const subscription = DeviceEventEmitter.addListener(ANDROID_WINDOW_DRAG_END_EVENT, event => {
89
+ const nativeHandle = findNodeHandle(nativeOverlayRef.current);
90
+ if (nativeHandle != null && event.viewId != null && event.viewId !== nativeHandle) {
91
+ return;
92
+ }
93
+ if (typeof event.x !== 'number' || typeof event.y !== 'number' || typeof event.width !== 'number' || typeof event.height !== 'number') {
94
+ return;
95
+ }
96
+ onAndroidWindowDragEnd({
97
+ x: event.x,
98
+ y: event.y,
99
+ width: event.width,
100
+ height: event.height
101
+ });
102
+ });
103
+ return () => subscription.remove();
104
+ }, [onAndroidWindowDragEnd]);
105
+
86
106
  // iOS: FullWindowOverlay — separate UIWindow above everything
87
107
  if (Platform.OS === 'ios' && FullWindowOverlay) {
88
108
  // @ts-ignore - Some versions of react-native-screens don't type 'style'
@@ -91,10 +111,15 @@ export function FloatingOverlayWrapper({
91
111
  children: children
92
112
  });
93
113
  }
94
-
95
- // Android: native elevated Dialog window (TYPE_APPLICATION_PANEL)
96
- if (Platform.OS === 'android' && NativeFloatingOverlay) {
97
- return /*#__PURE__*/_jsx(NativeFloatingOverlay, {
114
+ if (Platform.OS === 'android' && NativeFloatingOverlay && androidWindowMetrics) {
115
+ const NativeFloatingOverlayComponent = NativeFloatingOverlay;
116
+ return /*#__PURE__*/_jsx(NativeFloatingOverlayComponent, {
117
+ ref: nativeOverlayRef,
118
+ style: styles.androidAnchor,
119
+ windowX: androidWindowMetrics.x,
120
+ windowY: androidWindowMetrics.y,
121
+ windowWidth: androidWindowMetrics.width,
122
+ windowHeight: androidWindowMetrics.height,
98
123
  children: children
99
124
  });
100
125
  }
@@ -102,7 +127,18 @@ export function FloatingOverlayWrapper({
102
127
  // Fallback: regular View — same behavior as before this overlay feature
103
128
  return /*#__PURE__*/_jsx(View, {
104
129
  style: fallbackStyle,
130
+ pointerEvents: "box-none",
105
131
  children: children
106
132
  });
107
- }
133
+ });
134
+ FloatingOverlayWrapper.displayName = 'FloatingOverlayWrapper';
135
+ const styles = StyleSheet.create({
136
+ androidAnchor: {
137
+ position: 'absolute',
138
+ top: 0,
139
+ left: 0,
140
+ width: 1,
141
+ height: 1
142
+ }
143
+ });
108
144
  //# sourceMappingURL=FloatingOverlayWrapper.js.map
@@ -10,8 +10,29 @@
10
10
  * to route telemetry through your own backend without touching this file.
11
11
  */
12
12
 
13
- const MOBILEAI_BASE = process.env.EXPO_PUBLIC_MOBILEAI_BASE_URL || process.env.NEXT_PUBLIC_MOBILEAI_BASE_URL || 'https://mobileai.cloud';
13
+ import { Platform } from 'react-native';
14
+ function resolveMobileAIBase() {
15
+ const configuredBase = process.env.EXPO_PUBLIC_MOBILEAI_BASE_URL || process.env.NEXT_PUBLIC_MOBILEAI_BASE_URL || 'https://mobileai.cloud';
16
+
17
+ // Android emulators cannot reach the host machine via localhost/127.0.0.1.
18
+ // Translate those hostnames to 10.0.2.2 so the Expo example can talk to the
19
+ // local dashboard/backend without affecting iOS.
20
+ if (Platform.OS === 'android') {
21
+ return configuredBase.replace(/^http:\/\/(localhost|127\.0\.0\.1)(?=[:/]|$)/, 'http://10.0.2.2');
22
+ }
23
+ return configuredBase;
24
+ }
25
+ const MOBILEAI_BASE = resolveMobileAIBase();
26
+ function toWebSocketBase(url) {
27
+ if (url.startsWith('https://')) return `wss://${url.slice('https://'.length)}`;
28
+ if (url.startsWith('http://')) return `ws://${url.slice('http://'.length)}`;
29
+ return url;
30
+ }
14
31
  export const ENDPOINTS = {
32
+ /** Hosted MobileAI text proxy — used by default when analyticsKey is set */
33
+ hostedTextProxy: `${MOBILEAI_BASE}/api/v1/hosted-proxy/text`,
34
+ /** Hosted MobileAI voice proxy — used by default when analyticsKey is set */
35
+ hostedVoiceProxy: `${toWebSocketBase(MOBILEAI_BASE)}/ws/hosted-proxy/voice`,
15
36
  /** Telemetry event ingest — receives batched SDK events */
16
37
  telemetryIngest: `${MOBILEAI_BASE}/api/v1/events`,
17
38
  /** Feature flag sync — fetches remote flags for this analyticsKey */