@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
@@ -11,8 +11,10 @@
11
11
  */
12
12
 
13
13
  import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
14
- import { View, StyleSheet } from 'react-native';
14
+ import { View, StyleSheet, InteractionManager, Platform } from 'react-native';
15
15
  import { AgentRuntime } from "../core/AgentRuntime.js";
16
+ import { captureWireframe } from "../core/FiberTreeWalker.js";
17
+ import { humanizeScreenName } from "../utils/humanizeScreenName.js";
16
18
  import { createProvider } from "../providers/ProviderFactory.js";
17
19
  import { AgentContext } from "../hooks/useAction.js";
18
20
  import { AgentChatBar } from "./AgentChatBar.js";
@@ -26,7 +28,7 @@ import { VoiceService } from "../services/VoiceService.js";
26
28
  import { AudioInputService } from "../services/AudioInputService.js";
27
29
  import { AudioOutputService } from "../services/AudioOutputService.js";
28
30
  import { TelemetryService, bindTelemetryService } from "../services/telemetry/index.js";
29
- import { extractTouchLabel, checkRageClick } from "../services/telemetry/TouchAutoCapture.js";
31
+ import { extractTouchTargetMetadata, checkRageClick } from "../services/telemetry/TouchAutoCapture.js";
30
32
  import { initDeviceId, getDeviceId } from "../services/telemetry/device.js";
31
33
  import { AgentErrorBoundary } from "./AgentErrorBoundary.js";
32
34
  import { HighlightOverlay } from "./HighlightOverlay.js";
@@ -41,13 +43,12 @@ import { SupportChatModal } from "../support/SupportChatModal.js";
41
43
  import { ENDPOINTS } from "../config/endpoints.js";
42
44
  import * as ConversationService from "../services/ConversationService.js";
43
45
  import { createMobileAIKnowledgeRetriever } from "../services/MobileAIKnowledgeRetriever.js";
44
-
46
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
45
47
  // ─── Context ───────────────────────────────────────────────────
46
48
 
47
49
  // ─── AsyncStorage Helper (same pattern as TicketStore) ─────────
48
50
 
49
51
  /** Try to load AsyncStorage for tooltip persistence. Optional peer dep. */
50
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
51
52
  function getTooltipStorage() {
52
53
  try {
53
54
  const origError = console.error;
@@ -68,6 +69,29 @@ function getTooltipStorage() {
68
69
  return null;
69
70
  }
70
71
  }
72
+ function sanitizeWireframeScreenshot(value) {
73
+ if (!value || typeof value !== 'string') return null;
74
+ const trimmed = value.trim();
75
+ if (!trimmed) return null;
76
+ const base64 = trimmed.startsWith('data:') ? trimmed.split(',')[1] ?? '' : trimmed;
77
+ return base64.length > 0 ? base64 : null;
78
+ }
79
+ async function captureHeatmapScreenshot(rootView) {
80
+ try {
81
+ if (!rootView) return null;
82
+ const viewShot = require('react-native-view-shot');
83
+ const captureRef = viewShot.captureRef || viewShot.default?.captureRef;
84
+ if (!captureRef || typeof captureRef !== 'function') return null;
85
+ const raw = await captureRef(rootView, {
86
+ format: 'jpg',
87
+ quality: 0.28,
88
+ result: 'base64'
89
+ });
90
+ return sanitizeWireframeScreenshot(raw);
91
+ } catch {
92
+ return null;
93
+ }
94
+ }
71
95
 
72
96
  // ─── Props ─────────────────────────────────────────────────────
73
97
 
@@ -81,6 +105,8 @@ export function AIAgent({
81
105
  voiceProxyHeaders,
82
106
  provider: providerName = 'gemini',
83
107
  model,
108
+ supportStyle = 'warm-concise',
109
+ verifier,
84
110
  navRef,
85
111
  maxSteps = 25,
86
112
  showChatBar = true,
@@ -205,6 +231,9 @@ export function AIAgent({
205
231
  // ── Onboarding Journey State ────────────────────────────────
206
232
  const [isOnboardingActive, setIsOnboardingActive] = useState(false);
207
233
  const [currentOnboardingIndex, setCurrentOnboardingIndex] = useState(0);
234
+ const [androidWindowMetrics, setAndroidWindowMetrics] = useState(null);
235
+ const androidWindowMetricsRef = useRef(null);
236
+ const floatingOverlayRef = useRef(null);
208
237
  useEffect(() => {
209
238
  if (!onboarding?.enabled) return;
210
239
  if (onboarding.firstLaunchOnly !== false) {
@@ -234,7 +263,9 @@ export function AIAgent({
234
263
  try {
235
264
  const AS = getTooltipStorage();
236
265
  await AS?.setItem('@mobileai_onboarding_completed', 'true');
237
- } catch {/* graceful */}
266
+ } catch {
267
+ /* graceful */
268
+ }
238
269
  })();
239
270
  } else {
240
271
  setCurrentOnboardingIndex(prev => prev + 1);
@@ -266,9 +297,54 @@ export function AIAgent({
266
297
  try {
267
298
  const AS = getTooltipStorage();
268
299
  await AS?.setItem('@mobileai_tooltip_seen', 'true');
269
- } catch {/* graceful */}
300
+ } catch {
301
+ /* graceful */
302
+ }
270
303
  })();
271
304
  }, []);
305
+ useEffect(() => {
306
+ if (!showChatBar) {
307
+ androidWindowMetricsRef.current = null;
308
+ setAndroidWindowMetrics(null);
309
+ }
310
+ }, [showChatBar]);
311
+ const handleAndroidWindowMetricsChange = useCallback(metrics => {
312
+ if (Platform.OS !== 'android') {
313
+ return;
314
+ }
315
+ if (!showChatBar) {
316
+ androidWindowMetricsRef.current = null;
317
+ setAndroidWindowMetrics(null);
318
+ return;
319
+ }
320
+ const previousMetrics = androidWindowMetricsRef.current;
321
+ if (previousMetrics && previousMetrics.x === metrics.x && previousMetrics.y === metrics.y && previousMetrics.width === metrics.width && previousMetrics.height === metrics.height) {
322
+ return;
323
+ }
324
+ androidWindowMetricsRef.current = metrics;
325
+ if (floatingOverlayRef.current && previousMetrics) {
326
+ floatingOverlayRef.current.setAndroidWindowMetrics(metrics);
327
+ return;
328
+ }
329
+ setAndroidWindowMetrics(metrics);
330
+ }, [showChatBar]);
331
+ const handleAndroidWindowDragEnd = useCallback(metrics => {
332
+ if (Platform.OS !== 'android') {
333
+ return;
334
+ }
335
+ if (!showChatBar) {
336
+ androidWindowMetricsRef.current = null;
337
+ setAndroidWindowMetrics(null);
338
+ return;
339
+ }
340
+ androidWindowMetricsRef.current = metrics;
341
+ setAndroidWindowMetrics(prev => {
342
+ if (prev && prev.x === metrics.x && prev.y === metrics.y && prev.width === metrics.width && prev.height === metrics.height) {
343
+ return prev;
344
+ }
345
+ return metrics;
346
+ });
347
+ }, [showChatBar]);
272
348
 
273
349
  // CRITICAL: clearSupport uses REFS and functional setters — never closure values.
274
350
  // This function is captured by long-lived callbacks (escalation sockets, restored
@@ -385,17 +461,50 @@ export function AIAgent({
385
461
  });
386
462
  });
387
463
  }, []);
388
- const getResolvedScreenName = useCallback(() => {
464
+ const getDeepestRouteName = useCallback(state => {
465
+ if (!state || !Array.isArray(state.routes) || state.index == null) {
466
+ return null;
467
+ }
468
+ const route = state.routes[state.index];
469
+ if (!route) return null;
470
+ const nested = route.state;
471
+ if (nested) {
472
+ const nestedName = getDeepestRouteName(nested);
473
+ if (nestedName) return nestedName;
474
+ }
475
+ return typeof route.name === 'string' ? route.name : null;
476
+ }, []);
477
+ const resolveScreenName = useCallback(() => {
478
+ if (pathname) {
479
+ const humanizedPath = humanizeScreenName(pathname === '/' ? 'index' : pathname);
480
+ if (humanizedPath) return humanizedPath;
481
+ }
482
+ const navState = navRef?.getRootState?.() ?? navRef?.getState?.();
483
+ const deepestFromState = getDeepestRouteName(navState);
484
+ if (deepestFromState) {
485
+ const humanized = humanizeScreenName(deepestFromState);
486
+ if (humanized) return humanized;
487
+ }
389
488
  const routeName = navRef?.getCurrentRoute?.()?.name;
390
489
  if (typeof routeName === 'string' && routeName.trim().length > 0) {
391
- return routeName;
490
+ const humanized = humanizeScreenName(routeName);
491
+ if (humanized) return humanized;
392
492
  }
393
493
  const telemetryScreen = telemetryRef.current?.screen;
394
- if (typeof telemetryScreen === 'string' && telemetryScreen !== 'Unknown') {
494
+ if (typeof telemetryScreen === 'string' && telemetryScreen !== 'Unknown' && telemetryScreen.trim().length > 0) {
395
495
  return telemetryScreen;
396
496
  }
397
497
  return 'unknown';
398
- }, [navRef]);
498
+ }, [pathname, navRef, getDeepestRouteName]);
499
+ const syncTelemetryScreen = useCallback(() => {
500
+ const screen = resolveScreenName();
501
+ if (screen === 'unknown') {
502
+ return screen;
503
+ }
504
+ telemetryRef.current?.setScreen(screen);
505
+ return screen;
506
+ }, [resolveScreenName]);
507
+ const getResolvedScreenName = useCallback(() => resolveScreenName(), [resolveScreenName]);
399
508
  const resolvedKnowledgeBase = useMemo(() => {
400
509
  if (knowledgeBase) return knowledgeBase;
401
510
  if (!analyticsKey) return undefined;
@@ -505,6 +614,7 @@ export function AIAgent({
505
614
  if (!humanFrtFiredRef.current[ticketId]) {
506
615
  humanFrtFiredRef.current[ticketId] = true;
507
616
  telemetryRef.current?.track('human_first_response', {
617
+ canonical_type: 'human_first_response_sent',
508
618
  ticketId
509
619
  });
510
620
  }
@@ -703,7 +813,7 @@ export function AIAgent({
703
813
  },
704
814
  onTypingChange: setIsLiveAgentTyping,
705
815
  onTicketClosed: () => clearSupport(ticket.id),
706
- onError: err => logger.error('AIAgent', '★ Restored socket error:', err)
816
+ onError: err => logger.warn('AIAgent', '★ Restored socket error:', err)
707
817
  });
708
818
  if (ticket.wsUrl) {
709
819
  socket.connect(ticket.wsUrl);
@@ -715,7 +825,7 @@ export function AIAgent({
715
825
  logger.info('AIAgent', '★ Single ticket restored and socket cached:', ticket.id);
716
826
  }
717
827
  } catch (err) {
718
- logger.error('AIAgent', '★ Failed to restore tickets:', err);
828
+ logger.warn('AIAgent', '★ Failed to restore tickets:', err);
719
829
  }
720
830
  })();
721
831
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -949,7 +1059,7 @@ export function AIAgent({
949
1059
  }
950
1060
  clearSupport(ticketId);
951
1061
  },
952
- onError: err => logger.error('AIAgent', '★ Socket error on select:', err)
1062
+ onError: err => logger.warn('AIAgent', '★ Socket error on select:', err)
953
1063
  });
954
1064
  if (freshWsUrl) {
955
1065
  socket.connect(freshWsUrl);
@@ -1023,16 +1133,50 @@ export function AIAgent({
1023
1133
  const [pendingApprovalQuestion, setPendingApprovalQuestion] = useState(null);
1024
1134
  const overlayVisible = isThinking || !!pendingApprovalQuestion;
1025
1135
  const overlayStatusText = pendingApprovalQuestion ? 'Waiting for your approval...' : statusText;
1136
+ const effectiveProxyHeaders = useMemo(() => {
1137
+ if (!analyticsKey) return proxyHeaders;
1138
+ const isAuthMissing = !proxyHeaders || !Object.keys(proxyHeaders).some(k => k.toLowerCase() === 'authorization');
1139
+ if (isAuthMissing) {
1140
+ return {
1141
+ ...proxyHeaders,
1142
+ Authorization: `Bearer ${analyticsKey}`
1143
+ };
1144
+ }
1145
+ return proxyHeaders;
1146
+ }, [proxyHeaders, analyticsKey]);
1147
+ const effectiveVoiceProxyHeaders = useMemo(() => {
1148
+ if (!analyticsKey) return voiceProxyHeaders;
1149
+ const isAuthMissing = !voiceProxyHeaders || !Object.keys(voiceProxyHeaders).some(k => k.toLowerCase() === 'authorization');
1150
+ if (isAuthMissing) {
1151
+ return {
1152
+ ...voiceProxyHeaders,
1153
+ Authorization: `Bearer ${analyticsKey}`
1154
+ };
1155
+ }
1156
+ return voiceProxyHeaders;
1157
+ }, [voiceProxyHeaders, analyticsKey]);
1158
+ const resolvedProxyUrl = useMemo(() => {
1159
+ if (proxyUrl) return proxyUrl;
1160
+ if (analyticsKey) return ENDPOINTS.hostedTextProxy;
1161
+ return undefined;
1162
+ }, [proxyUrl, analyticsKey]);
1163
+ const resolvedVoiceProxyUrl = useMemo(() => {
1164
+ if (voiceProxyUrl) return voiceProxyUrl;
1165
+ if (analyticsKey) return ENDPOINTS.hostedVoiceProxy;
1166
+ return resolvedProxyUrl;
1167
+ }, [voiceProxyUrl, analyticsKey, resolvedProxyUrl]);
1026
1168
 
1027
1169
  // ─── Create Runtime ──────────────────────────────────────────
1028
1170
 
1029
1171
  const config = useMemo(() => ({
1030
1172
  apiKey,
1031
- proxyUrl,
1173
+ proxyUrl: resolvedProxyUrl,
1032
1174
  proxyHeaders,
1033
- voiceProxyUrl,
1175
+ voiceProxyUrl: resolvedVoiceProxyUrl,
1034
1176
  voiceProxyHeaders,
1035
1177
  model,
1178
+ supportStyle,
1179
+ verifier,
1036
1180
  language: 'en',
1037
1181
  maxSteps,
1038
1182
  interactiveBlacklist,
@@ -1139,33 +1283,11 @@ export function AIAgent({
1139
1283
  ...(event.data ?? {})
1140
1284
  });
1141
1285
  }
1142
- }), [mode, apiKey, proxyUrl, proxyHeaders, voiceProxyUrl, voiceProxyHeaders, model, maxSteps, interactiveBlacklist, interactiveWhitelist, onBeforeStep, onAfterStep, onBeforeTask, onAfterTask, transformScreenContent, customTools, instructions, stepDelay, mcpServerUrl, router, pathname, onTokenUsage, resolvedKnowledgeBase, knowledgeMaxTokens, enableUIControl, screenMap, useScreenMap, maxTokenBudget, maxCostUSD, interactionMode]);
1286
+ }), [mode, apiKey, resolvedProxyUrl, proxyHeaders, resolvedVoiceProxyUrl, voiceProxyHeaders, model, maxSteps, interactiveBlacklist, interactiveWhitelist, onBeforeStep, onAfterStep, onBeforeTask, onAfterTask, transformScreenContent, customTools, instructions, stepDelay, mcpServerUrl, router, pathname, onTokenUsage, resolvedKnowledgeBase, knowledgeMaxTokens, enableUIControl, screenMap, useScreenMap, maxTokenBudget, maxCostUSD, interactionMode, verifier, supportStyle]);
1143
1287
  useEffect(() => {
1144
1288
  logger.info('AIAgent', `⚙️ Runtime config recomputed: mode=${mode} interactionMode=${interactionMode || 'copilot(default)'} onAskUser=${mode !== 'voice'} mergedTools=${Object.keys(mergedCustomTools).join(', ') || '(none)'}`);
1145
1289
  }, [mode, interactionMode, mergedCustomTools]);
1146
- const effectiveProxyHeaders = useMemo(() => {
1147
- if (!analyticsKey) return proxyHeaders;
1148
- const isAuthMissing = !proxyHeaders || !Object.keys(proxyHeaders).some(k => k.toLowerCase() === 'authorization');
1149
- if (isAuthMissing) {
1150
- return {
1151
- ...proxyHeaders,
1152
- Authorization: `Bearer ${analyticsKey}`
1153
- };
1154
- }
1155
- return proxyHeaders;
1156
- }, [proxyHeaders, analyticsKey]);
1157
- const effectiveVoiceProxyHeaders = useMemo(() => {
1158
- if (!analyticsKey) return voiceProxyHeaders;
1159
- const isAuthMissing = !voiceProxyHeaders || !Object.keys(voiceProxyHeaders).some(k => k.toLowerCase() === 'authorization');
1160
- if (isAuthMissing) {
1161
- return {
1162
- ...voiceProxyHeaders,
1163
- Authorization: `Bearer ${analyticsKey}`
1164
- };
1165
- }
1166
- return voiceProxyHeaders;
1167
- }, [voiceProxyHeaders, analyticsKey]);
1168
- const provider = useMemo(() => createProvider(providerName, apiKey, model, proxyUrl, effectiveProxyHeaders), [providerName, apiKey, model, proxyUrl, effectiveProxyHeaders]);
1290
+ const provider = useMemo(() => createProvider(providerName, apiKey, model, resolvedProxyUrl, effectiveProxyHeaders), [providerName, apiKey, model, resolvedProxyUrl, effectiveProxyHeaders]);
1169
1291
  const runtime = useMemo(() => new AgentRuntime(provider, config, rootViewRef.current, navRef),
1170
1292
  // eslint-disable-next-line react-hooks/exhaustive-deps
1171
1293
  [provider, config]);
@@ -1193,12 +1315,12 @@ export function AIAgent({
1193
1315
  debug,
1194
1316
  onEvent: event => {
1195
1317
  // Proactive behavior triggers
1196
- if (event.type === 'rage_tap' || event.type === 'error_screen' || event.type === 'repeated_navigation') {
1318
+ if (event.type === 'rage_tap' || event.type === 'rage_click' || event.type === 'rage_click_detected' || event.type === 'error_screen' || event.type === 'repeated_navigation') {
1197
1319
  idleDetectorRef.current?.triggerBehavior(event.type, event.screen);
1198
1320
  }
1199
1321
 
1200
1322
  // Customer Success features
1201
- if (customerSuccess?.enabled && event.type === 'user_interaction' && event.data) {
1323
+ if (customerSuccess?.enabled && (event.type === 'user_interaction' || event.type === 'user_action') && event.data) {
1202
1324
  const action = String(event.data.label || event.data.action || '');
1203
1325
 
1204
1326
  // Check milestones
@@ -1227,15 +1349,14 @@ export function AIAgent({
1227
1349
  bindTelemetryService(telemetry);
1228
1350
  telemetry.start();
1229
1351
  // NavigationContainer is a child of AIAgent, so navRef may not be
1230
- // ready yet when this effect runs. Poll briefly until it's available.
1352
+ // ready yet when this effect runs. Poll briefly until it is.
1231
1353
  const resolveInitialScreen = () => {
1232
1354
  let attempts = 0;
1233
1355
  const maxAttempts = 15; // 15 × 200ms = 3s max wait
1234
1356
  const timer = setInterval(() => {
1235
1357
  attempts++;
1236
- const route = navRef?.getCurrentRoute?.();
1237
- if (route?.name) {
1238
- telemetry.setScreen(route.name);
1358
+ const cleanName = syncTelemetryScreen();
1359
+ if (cleanName !== 'unknown') {
1239
1360
  clearInterval(timer);
1240
1361
  } else if (attempts >= maxAttempts) {
1241
1362
  clearInterval(timer);
@@ -1244,22 +1365,45 @@ export function AIAgent({
1244
1365
  };
1245
1366
  resolveInitialScreen();
1246
1367
  }); // initDeviceId
1247
- }, [analyticsKey, analyticsProxyUrl, analyticsProxyHeaders, bindTelemetryService, debug, navRef]);
1368
+ }, [analyticsKey, analyticsProxyUrl, analyticsProxyHeaders, bindTelemetryService, debug, navRef, syncTelemetryScreen]);
1248
1369
 
1249
1370
  // ─── Security warnings ──────────────────────────────────────
1250
1371
 
1251
1372
  useEffect(() => {
1252
1373
  // @ts-ignore
1253
- if (typeof __DEV__ !== 'undefined' && !__DEV__ && apiKey && !proxyUrl) {
1374
+ if (typeof __DEV__ !== 'undefined' && !__DEV__ && apiKey && !resolvedProxyUrl) {
1254
1375
  logger.warn('[MobileAI] ⚠️ SECURITY WARNING: You are using `apiKey` directly in a production build. ' + 'This exposes your LLM provider key in the app binary. ' + 'Use `apiProxyUrl` to route requests through your backend instead. ' + 'See docs for details.');
1255
1376
  }
1256
- }, [apiKey, proxyUrl]);
1377
+ }, [apiKey, resolvedProxyUrl]);
1257
1378
 
1258
1379
  // Track screen changes via navRef
1259
1380
  useEffect(() => {
1260
1381
  if (!navRef?.addListener || !telemetryRef.current) return;
1261
1382
  const checkScreenMilestone = screenName => {
1262
1383
  telemetryRef.current?.setScreen(screenName);
1384
+
1385
+ // Auto-capture wireframe snapshot for privacy-safe heatmaps.
1386
+ // Deferred: wait for all animations/interactions to finish, then
1387
+ // wait one more frame for layout to settle. Zero perf impact.
1388
+ if (rootViewRef.current) {
1389
+ const handle = InteractionManager.runAfterInteractions(() => {
1390
+ requestAnimationFrame(() => {
1391
+ Promise.all([captureWireframe(rootViewRef, {
1392
+ screenName
1393
+ }), captureHeatmapScreenshot(rootViewRef.current)]).then(([wireframe, screenshot]) => {
1394
+ if (wireframe && telemetryRef.current) {
1395
+ telemetryRef.current.trackWireframe({
1396
+ ...wireframe,
1397
+ screenshot: screenshot || undefined
1398
+ });
1399
+ }
1400
+ }).catch(err => {
1401
+ if (debug) logger.debug('AIAgent', 'Wireframe capture failed:', err);
1402
+ });
1403
+ });
1404
+ });
1405
+ void handle;
1406
+ }
1263
1407
  if (customerSuccess?.enabled) {
1264
1408
  customerSuccess.successMilestones?.forEach(m => {
1265
1409
  if (m.screen && m.screen === screenName) {
@@ -1293,13 +1437,13 @@ export function AIAgent({
1293
1437
  }
1294
1438
  };
1295
1439
  const unsubscribe = navRef.addListener('state', () => {
1296
- const currentRoute = navRef.getCurrentRoute?.();
1297
- if (currentRoute?.name) {
1298
- checkScreenMilestone(currentRoute.name);
1440
+ const cleanName = resolveScreenName();
1441
+ if (cleanName) {
1442
+ checkScreenMilestone(cleanName);
1299
1443
  }
1300
1444
  });
1301
1445
  return () => unsubscribe?.();
1302
- }, [navRef, customerSuccess, isOnboardingActive, onboarding, currentOnboardingIndex, advanceOnboarding]);
1446
+ }, [navRef, resolveScreenName, customerSuccess, isOnboardingActive, onboarding, currentOnboardingIndex, advanceOnboarding]);
1303
1447
 
1304
1448
  // ─── MCP Bridge ──────────────────────────────────────────────
1305
1449
 
@@ -1336,7 +1480,7 @@ export function AIAgent({
1336
1480
  setProactiveStage('badge');
1337
1481
  },
1338
1482
  onReset: () => setProactiveStage('hidden'),
1339
- generateSuggestion: () => proactiveHelp?.generateSuggestion?.(telemetryRef.current?.screen || 'Home') || proactiveHelp?.badgeText || "Need help with this screen?",
1483
+ generateSuggestion: () => proactiveHelp?.generateSuggestion?.(telemetryRef.current?.screen || 'Home') || proactiveHelp?.badgeText || 'Need help with this screen?',
1340
1484
  behaviorTriggers: proactiveHelp?.behaviorTriggers
1341
1485
  });
1342
1486
  return () => {
@@ -1365,11 +1509,11 @@ export function AIAgent({
1365
1509
  logger.info('AIAgent', `Registering ${runtimeTools.length} tools with VoiceService: ${runtimeTools.map(t => t.name).join(', ')}`);
1366
1510
  // Use voice-adapted system prompt — same core rules as text mode
1367
1511
  // but without agent-loop directives that trigger autonomous actions
1368
- const voicePrompt = buildVoiceSystemPrompt('en', instructions?.system, !!knowledgeBase);
1512
+ const voicePrompt = buildVoiceSystemPrompt('en', instructions?.system, !!knowledgeBase, supportStyle);
1369
1513
  logger.info('AIAgent', `📝 Voice system prompt (${voicePrompt.length} chars):\n${voicePrompt}`);
1370
1514
  voiceServiceRef.current = new VoiceService({
1371
1515
  apiKey,
1372
- proxyUrl: voiceProxyUrl || proxyUrl,
1516
+ proxyUrl: resolvedVoiceProxyUrl,
1373
1517
  proxyHeaders: effectiveVoiceProxyHeaders || effectiveProxyHeaders,
1374
1518
  systemPrompt: voicePrompt,
1375
1519
  tools: runtimeTools,
@@ -1609,7 +1753,7 @@ export function AIAgent({
1609
1753
  setIsVoiceConnected(false);
1610
1754
  };
1611
1755
  // eslint-disable-next-line react-hooks/exhaustive-deps
1612
- }, [mode, apiKey, proxyUrl, proxyHeaders, voiceProxyUrl, voiceProxyHeaders, runtime, instructions]);
1756
+ }, [mode, apiKey, resolvedVoiceProxyUrl, effectiveVoiceProxyHeaders, effectiveProxyHeaders, runtime, instructions, supportStyle, knowledgeBase]);
1613
1757
 
1614
1758
  // ─── Stop Voice Session (full cleanup) ─────────────────────
1615
1759
 
@@ -1754,12 +1898,15 @@ export function AIAgent({
1754
1898
  setIsThinking(true);
1755
1899
  setStatusText('Thinking...');
1756
1900
  setLastResult(null);
1901
+ const requestStartedAt = Date.now();
1757
1902
  logger.info('AIAgent', `📨 New user request received in ${mode} mode | interactionMode=${interactionMode || 'copilot(default)'} | text="${message.trim()}"`);
1758
1903
 
1759
1904
  // Telemetry: track agent request
1760
1905
  telemetryRef.current?.track('agent_request', {
1906
+ canonical_type: 'ai_question_asked',
1761
1907
  query: message.trim(),
1762
1908
  transcript: message.trim(),
1909
+ request_topic: message.trim().slice(0, 120),
1763
1910
  mode
1764
1911
  });
1765
1912
  telemetryRef.current?.track('agent_trace', {
@@ -1778,8 +1925,11 @@ export function AIAgent({
1778
1925
  if (HIGH_RISK_ESCALATION_REGEX.test(message)) {
1779
1926
  logger.warn('AIAgent', 'High-risk support signal detected — auto-escalating to human');
1780
1927
  telemetryRef.current?.track('business_escalation', {
1928
+ canonical_type: 'support_escalated',
1781
1929
  message,
1782
- trigger: 'high_risk'
1930
+ trigger: 'high_risk',
1931
+ escalation_reason: 'high_risk',
1932
+ topic: message.trim().slice(0, 120)
1783
1933
  });
1784
1934
  const escalationResult = await escalateTool.execute({
1785
1935
  reason: `Customer needs human support: ${message.trim()}`
@@ -1880,7 +2030,9 @@ export function AIAgent({
1880
2030
  if (telemetryRef.current) {
1881
2031
  if (!agentFrtFiredRef.current) {
1882
2032
  agentFrtFiredRef.current = true;
1883
- telemetryRef.current.track('agent_first_response');
2033
+ telemetryRef.current.track('agent_first_response', {
2034
+ canonical_type: 'ai_first_response_sent'
2035
+ });
1884
2036
  }
1885
2037
  for (const step of normalizedResult.steps ?? []) {
1886
2038
  telemetryRef.current.track('agent_step', {
@@ -1894,7 +2046,12 @@ export function AIAgent({
1894
2046
  });
1895
2047
  }
1896
2048
  telemetryRef.current.track('agent_complete', {
2049
+ canonical_type: normalizedResult.success ? 'ai_answer_completed' : 'ai_answer_failed',
1897
2050
  success: normalizedResult.success,
2051
+ resolved: normalizedResult.success,
2052
+ resolution_type: normalizedResult.success ? 'ai_resolved' : 'needs_follow_up',
2053
+ latency_ms: Date.now() - requestStartedAt,
2054
+ request_topic: message.trim().slice(0, 120),
1898
2055
  steps: normalizedResult.steps?.length ?? 0,
1899
2056
  tokens: normalizedResult.tokenUsage?.totalTokens ?? 0,
1900
2057
  cost: normalizedResult.tokenUsage?.estimatedCostUSD ?? 0,
@@ -1989,7 +2146,12 @@ export function AIAgent({
1989
2146
 
1990
2147
  // Telemetry: track agent failure
1991
2148
  telemetryRef.current?.track('agent_complete', {
2149
+ canonical_type: 'ai_answer_failed',
1992
2150
  success: false,
2151
+ resolved: false,
2152
+ resolution_type: 'execution_error',
2153
+ latency_ms: Date.now() - requestStartedAt,
2154
+ request_topic: message.trim().slice(0, 120),
1993
2155
  error: error.message,
1994
2156
  response: `Error: ${error.message}`,
1995
2157
  conversation: {
@@ -2026,8 +2188,7 @@ export function AIAgent({
2026
2188
 
2027
2189
  const handleCancel = useCallback(() => {
2028
2190
  runtime.cancel();
2029
- setIsThinking(false);
2030
- setStatusText('');
2191
+ setStatusText('Stopping...');
2031
2192
  }, [runtime]);
2032
2193
 
2033
2194
  // ─── Conversation History Handlers ─────────────────────────────
@@ -2082,24 +2243,46 @@ export function AIAgent({
2082
2243
  // Skip if the AI agent is currently executing a tool — those are
2083
2244
  // already tracked as `agent_step` events with full context.
2084
2245
  if (telemetryRef.current && !telemetryRef.current.isAgentActing) {
2085
- const label = extractTouchLabel(event);
2086
- if (label && label !== 'Unknown Element' && label !== '[pressable]') {
2087
- telemetryRef.current.track('user_interaction', {
2246
+ syncTelemetryScreen();
2247
+ const target = extractTouchTargetMetadata(event);
2248
+ const pageX = Math.round(event.nativeEvent.pageX);
2249
+ const pageY = Math.round(event.nativeEvent.pageY);
2250
+ if (target.label) {
2251
+ telemetryRef.current.track('user_action', {
2252
+ canonical_type: 'button_tapped',
2088
2253
  type: 'tap',
2089
- label,
2254
+ action: target.label,
2255
+ label: target.label,
2256
+ element_label: target.label,
2257
+ element_kind: target.elementKind,
2258
+ label_confidence: target.labelConfidence,
2259
+ zone_id: target.zoneId,
2260
+ ancestor_path: target.ancestorPath,
2261
+ sibling_labels: target.siblingLabels,
2262
+ component_name: target.componentName,
2090
2263
  actor: 'user',
2091
- x: Math.round(event.nativeEvent.pageX),
2092
- y: Math.round(event.nativeEvent.pageY)
2264
+ x: pageX,
2265
+ y: pageY
2093
2266
  });
2094
2267
 
2095
2268
  // Track if user is rage-tapping this specific element
2096
- checkRageClick(label, telemetryRef.current);
2269
+ checkRageClick({
2270
+ ...target,
2271
+ x: pageX,
2272
+ y: pageY
2273
+ }, telemetryRef.current);
2097
2274
  } else {
2098
2275
  // Tapped an unlabelled/empty area
2099
2276
  telemetryRef.current.track('dead_click', {
2100
- x: Math.round(event.nativeEvent.pageX),
2101
- y: Math.round(event.nativeEvent.pageY),
2102
- screen: telemetryRef.current.screen
2277
+ canonical_type: 'dead_click_detected',
2278
+ x: pageX,
2279
+ y: pageY,
2280
+ screen: telemetryRef.current.screen,
2281
+ screen_area: 'unknown',
2282
+ zone_id: target.zoneId,
2283
+ ancestor_path: target.ancestorPath,
2284
+ sibling_labels: target.siblingLabels,
2285
+ component_name: target.componentName
2103
2286
  });
2104
2287
  }
2105
2288
  }
@@ -2115,128 +2298,255 @@ export function AIAgent({
2115
2298
  },
2116
2299
  children: children
2117
2300
  })
2118
- }), /*#__PURE__*/_jsx(FloatingOverlayWrapper, {
2301
+ }), /*#__PURE__*/_jsx(HighlightOverlay, {}), showChatBar && /*#__PURE__*/_jsx(FloatingOverlayWrapper, {
2302
+ ref: Platform.OS === 'android' ? floatingOverlayRef : undefined,
2303
+ androidWindowMetrics: Platform.OS === 'android' ? androidWindowMetricsRef.current ?? androidWindowMetrics : null,
2304
+ onAndroidWindowDragEnd: Platform.OS === 'android' ? handleAndroidWindowDragEnd : undefined,
2119
2305
  fallbackStyle: styles.floatingLayer,
2120
- children: /*#__PURE__*/_jsxs(View, {
2121
- style: StyleSheet.absoluteFill,
2122
- pointerEvents: "box-none",
2123
- children: [/*#__PURE__*/_jsx(HighlightOverlay, {}), showChatBar && /*#__PURE__*/_jsx(ProactiveHint, {
2124
- stage: proactiveStage,
2125
- badgeText: proactiveBadgeText,
2126
- onDismiss: () => idleDetectorRef.current?.dismiss(),
2127
- children: /*#__PURE__*/_jsx(AgentChatBar, {
2128
- onSend: handleSend,
2129
- isThinking: isThinking,
2130
- statusText: overlayStatusText,
2131
- lastResult: lastResult,
2132
- lastUserMessage: lastUserMessage,
2133
- chatMessages: messages,
2134
- pendingApprovalQuestion: pendingApprovalQuestion,
2135
- onPendingApprovalAction: action => {
2136
- const resolver = askUserResolverRef.current;
2137
- logger.info('AIAgent', `🔘 Approval button tapped: action=${action} | resolver=${resolver ? 'EXISTS' : 'NULL'} | pendingApprovalQuestion="${pendingApprovalQuestion}" | pendingAppApprovalRef=${pendingAppApprovalRef.current}`);
2138
- if (!resolver) {
2139
- logger.error('AIAgent', '🚫 ABORT: resolver is null when button was tapped — this means ask_user Promise was already resolved without clearing the buttons. This is a state sync bug.');
2140
- return;
2141
- }
2142
- askUserResolverRef.current = null;
2143
- pendingAskUserKindRef.current = null;
2144
- // Button was actually tapped — clear the approval gate and any queued message
2145
- pendingAppApprovalRef.current = false;
2146
- queuedApprovalAnswerRef.current = null;
2147
- setPendingApprovalQuestion(null);
2148
- const response = action === 'approve' ? '__APPROVAL_GRANTED__' : '__APPROVAL_REJECTED__';
2149
- // Restore the thinking overlay so the user can see the agent working
2150
- // after approval. onAskUser set isThinking=false when buttons appeared,
2151
- // but the agent is still running — restore the visual indicator.
2152
- if (action === 'approve') {
2153
- setIsThinking(true);
2154
- setStatusText('Working...');
2155
- }
2156
- telemetryRef.current?.track('agent_trace', {
2157
- stage: 'approval_button_pressed',
2158
- action
2159
- });
2160
- resolver(response);
2161
- },
2162
- language: 'en',
2163
- onDismiss: () => {
2164
- setLastResult(null);
2165
- setLastUserMessage(null);
2166
- },
2167
- theme: accentColor || theme ? {
2168
- ...(accentColor ? {
2169
- primaryColor: accentColor
2170
- } : {}),
2171
- ...theme
2172
- } : undefined,
2173
- availableModes: availableModes,
2174
- mode: mode,
2175
- onModeChange: newMode => {
2176
- logger.info('AIAgent', '★ onModeChange:', mode, '→', newMode, '| tickets:', tickets.length, 'selectedTicketId:', selectedTicketId);
2177
- setMode(newMode);
2178
- },
2179
- isMicActive: isMicActive,
2180
- isSpeakerMuted: isSpeakerMuted,
2181
- isAISpeaking: isAISpeaking,
2182
- isAgentTyping: isLiveAgentTyping,
2183
- onStopSession: stopVoiceSession,
2184
- isVoiceConnected: isVoiceConnected,
2185
- onMicToggle: active => {
2186
- if (active && !isVoiceConnected) {
2187
- logger.warn('AIAgent', 'Cannot toggle mic — VoiceService not connected yet');
2188
- return;
2189
- }
2190
- logger.info('AIAgent', `Mic toggle: ${active ? 'ON' : 'OFF'}`);
2191
- setIsMicActive(active);
2192
- if (active) {
2193
- logger.info('AIAgent', 'Starting AudioInput...');
2194
- audioInputRef.current?.start().then(ok => {
2195
- logger.info('AIAgent', `AudioInput start result: ${ok}`);
2196
- });
2197
- } else {
2198
- logger.info('AIAgent', 'Stopping AudioInput...');
2199
- audioInputRef.current?.stop();
2200
- }
2201
- },
2202
- onSpeakerToggle: muted => {
2203
- logger.info('AIAgent', `Speaker toggle: ${muted ? 'MUTED' : 'UNMUTED'}`);
2204
- setIsSpeakerMuted(muted);
2205
- if (muted) {
2206
- audioOutputRef.current?.mute();
2207
- } else {
2208
- audioOutputRef.current?.unmute();
2209
- }
2210
- },
2211
- tickets: tickets,
2212
- selectedTicketId: selectedTicketId,
2213
- onTicketSelect: handleTicketSelect,
2214
- onBackToTickets: handleBackToTickets,
2215
- autoExpandTrigger: autoExpandTrigger,
2216
- unreadCounts: unreadCounts,
2217
- totalUnread: totalUnread,
2218
- showDiscoveryTooltip: tooltipVisible,
2219
- discoveryTooltipMessage: discoveryTooltipMessage,
2220
- onTooltipDismiss: handleTooltipDismiss,
2221
- conversations: conversations,
2222
- isLoadingHistory: isLoadingHistory,
2223
- onConversationSelect: handleConversationSelect,
2224
- onNewConversation: handleNewConversation
2225
- })
2226
- }), /*#__PURE__*/_jsx(AgentOverlay, {
2227
- visible: overlayVisible,
2228
- statusText: overlayStatusText,
2229
- onCancel: handleCancel
2230
- }), /*#__PURE__*/_jsx(SupportChatModal, {
2231
- visible: mode === 'human' && !!selectedTicketId,
2232
- messages: supportMessages,
2306
+ children: Platform.OS === 'android' ? /*#__PURE__*/_jsx(AgentChatBar, {
2307
+ onSend: handleSend,
2308
+ onCancel: handleCancel,
2309
+ isThinking: isThinking,
2310
+ statusText: overlayStatusText,
2311
+ lastResult: lastResult,
2312
+ lastUserMessage: lastUserMessage,
2313
+ chatMessages: messages,
2314
+ pendingApprovalQuestion: pendingApprovalQuestion,
2315
+ onPendingApprovalAction: action => {
2316
+ const resolver = askUserResolverRef.current;
2317
+ logger.info('AIAgent', `🔘 Approval button tapped: action=${action} | resolver=${resolver ? 'EXISTS' : 'NULL'} | pendingApprovalQuestion="${pendingApprovalQuestion}" | pendingAppApprovalRef=${pendingAppApprovalRef.current}`);
2318
+ if (!resolver) {
2319
+ logger.error('AIAgent', '🚫 ABORT: resolver is null when button was tapped — this means ask_user Promise was already resolved without clearing the buttons. This is a state sync bug.');
2320
+ return;
2321
+ }
2322
+ askUserResolverRef.current = null;
2323
+ pendingAskUserKindRef.current = null;
2324
+ pendingAppApprovalRef.current = false;
2325
+ queuedApprovalAnswerRef.current = null;
2326
+ setPendingApprovalQuestion(null);
2327
+ const response = action === 'approve' ? '__APPROVAL_GRANTED__' : '__APPROVAL_REJECTED__';
2328
+ if (action === 'approve') {
2329
+ setIsThinking(true);
2330
+ setStatusText('Working...');
2331
+ }
2332
+ telemetryRef.current?.track('agent_trace', {
2333
+ stage: 'approval_button_pressed',
2334
+ action
2335
+ });
2336
+ resolver(response);
2337
+ },
2338
+ language: 'en',
2339
+ onDismiss: () => {
2340
+ setLastResult(null);
2341
+ setLastUserMessage(null);
2342
+ },
2343
+ theme: accentColor || theme ? {
2344
+ ...(accentColor ? {
2345
+ primaryColor: accentColor
2346
+ } : {}),
2347
+ ...theme
2348
+ } : undefined,
2349
+ availableModes: availableModes,
2350
+ mode: mode,
2351
+ onModeChange: newMode => {
2352
+ logger.info('AIAgent', '★ onModeChange:', mode, '→', newMode, '| tickets:', tickets.length, 'selectedTicketId:', selectedTicketId);
2353
+ setMode(newMode);
2354
+ },
2355
+ isMicActive: isMicActive,
2356
+ isSpeakerMuted: isSpeakerMuted,
2357
+ isAISpeaking: isAISpeaking,
2358
+ isAgentTyping: isLiveAgentTyping,
2359
+ onStopSession: stopVoiceSession,
2360
+ isVoiceConnected: isVoiceConnected,
2361
+ onMicToggle: active => {
2362
+ if (active && !isVoiceConnected) {
2363
+ logger.warn('AIAgent', 'Cannot toggle mic — VoiceService not connected yet');
2364
+ return;
2365
+ }
2366
+ logger.info('AIAgent', `Mic toggle: ${active ? 'ON' : 'OFF'}`);
2367
+ setIsMicActive(active);
2368
+ if (active) {
2369
+ logger.info('AIAgent', 'Starting AudioInput...');
2370
+ audioInputRef.current?.start().then(ok => {
2371
+ logger.info('AIAgent', `AudioInput start result: ${ok}`);
2372
+ });
2373
+ } else {
2374
+ logger.info('AIAgent', 'Stopping AudioInput...');
2375
+ audioInputRef.current?.stop();
2376
+ }
2377
+ },
2378
+ onSpeakerToggle: muted => {
2379
+ logger.info('AIAgent', `Speaker toggle: ${muted ? 'MUTED' : 'UNMUTED'}`);
2380
+ setIsSpeakerMuted(muted);
2381
+ if (muted) {
2382
+ audioOutputRef.current?.mute();
2383
+ } else {
2384
+ audioOutputRef.current?.unmute();
2385
+ }
2386
+ },
2387
+ tickets: tickets,
2388
+ selectedTicketId: selectedTicketId,
2389
+ onTicketSelect: handleTicketSelect,
2390
+ onBackToTickets: handleBackToTickets,
2391
+ autoExpandTrigger: autoExpandTrigger,
2392
+ unreadCounts: unreadCounts,
2393
+ totalUnread: totalUnread,
2394
+ showDiscoveryTooltip: tooltipVisible,
2395
+ discoveryTooltipMessage: discoveryTooltipMessage,
2396
+ onTooltipDismiss: handleTooltipDismiss,
2397
+ conversations: conversations,
2398
+ isLoadingHistory: isLoadingHistory,
2399
+ onConversationSelect: handleConversationSelect,
2400
+ onNewConversation: handleNewConversation,
2401
+ renderMode: "android-native-window",
2402
+ windowMetrics: androidWindowMetricsRef.current ?? androidWindowMetrics,
2403
+ onWindowMetricsChange: handleAndroidWindowMetricsChange
2404
+ }) : /*#__PURE__*/_jsx(ProactiveHint, {
2405
+ stage: proactiveStage,
2406
+ badgeText: proactiveBadgeText,
2407
+ onDismiss: () => idleDetectorRef.current?.dismiss(),
2408
+ children: /*#__PURE__*/_jsx(AgentChatBar, {
2233
2409
  onSend: handleSend,
2234
- onClose: handleBackToTickets,
2235
- isAgentTyping: isLiveAgentTyping,
2410
+ onCancel: handleCancel,
2236
2411
  isThinking: isThinking,
2237
- scrollToEndTrigger: chatScrollTrigger,
2238
- ticketStatus: tickets.find(t => t.id === selectedTicketId)?.status
2239
- }), /*#__PURE__*/_jsx(AIConsentDialog, {
2412
+ statusText: overlayStatusText,
2413
+ lastResult: lastResult,
2414
+ lastUserMessage: lastUserMessage,
2415
+ chatMessages: messages,
2416
+ pendingApprovalQuestion: pendingApprovalQuestion,
2417
+ onPendingApprovalAction: action => {
2418
+ const resolver = askUserResolverRef.current;
2419
+ logger.info('AIAgent', `🔘 Approval button tapped: action=${action} | resolver=${resolver ? 'EXISTS' : 'NULL'} | pendingApprovalQuestion="${pendingApprovalQuestion}" | pendingAppApprovalRef=${pendingAppApprovalRef.current}`);
2420
+ if (!resolver) {
2421
+ logger.error('AIAgent', '🚫 ABORT: resolver is null when button was tapped — this means ask_user Promise was already resolved without clearing the buttons. This is a state sync bug.');
2422
+ return;
2423
+ }
2424
+ askUserResolverRef.current = null;
2425
+ pendingAskUserKindRef.current = null;
2426
+ pendingAppApprovalRef.current = false;
2427
+ queuedApprovalAnswerRef.current = null;
2428
+ setPendingApprovalQuestion(null);
2429
+ const response = action === 'approve' ? '__APPROVAL_GRANTED__' : '__APPROVAL_REJECTED__';
2430
+ if (action === 'approve') {
2431
+ setIsThinking(true);
2432
+ setStatusText('Working...');
2433
+ }
2434
+ telemetryRef.current?.track('agent_trace', {
2435
+ stage: 'approval_button_pressed',
2436
+ action
2437
+ });
2438
+ resolver(response);
2439
+ },
2440
+ language: 'en',
2441
+ onDismiss: () => {
2442
+ setLastResult(null);
2443
+ setLastUserMessage(null);
2444
+ },
2445
+ theme: accentColor || theme ? {
2446
+ ...(accentColor ? {
2447
+ primaryColor: accentColor
2448
+ } : {}),
2449
+ ...theme
2450
+ } : undefined,
2451
+ availableModes: availableModes,
2452
+ mode: mode,
2453
+ onModeChange: newMode => {
2454
+ logger.info('AIAgent', '★ onModeChange:', mode, '→', newMode, '| tickets:', tickets.length, 'selectedTicketId:', selectedTicketId);
2455
+ setMode(newMode);
2456
+ },
2457
+ isMicActive: isMicActive,
2458
+ isSpeakerMuted: isSpeakerMuted,
2459
+ isAISpeaking: isAISpeaking,
2460
+ isAgentTyping: isLiveAgentTyping,
2461
+ onStopSession: stopVoiceSession,
2462
+ isVoiceConnected: isVoiceConnected,
2463
+ onMicToggle: active => {
2464
+ if (active && !isVoiceConnected) {
2465
+ logger.warn('AIAgent', 'Cannot toggle mic — VoiceService not connected yet');
2466
+ return;
2467
+ }
2468
+ logger.info('AIAgent', `Mic toggle: ${active ? 'ON' : 'OFF'}`);
2469
+ setIsMicActive(active);
2470
+ if (active) {
2471
+ logger.info('AIAgent', 'Starting AudioInput...');
2472
+ audioInputRef.current?.start().then(ok => {
2473
+ logger.info('AIAgent', `AudioInput start result: ${ok}`);
2474
+ });
2475
+ } else {
2476
+ logger.info('AIAgent', 'Stopping AudioInput...');
2477
+ audioInputRef.current?.stop();
2478
+ }
2479
+ },
2480
+ onSpeakerToggle: muted => {
2481
+ logger.info('AIAgent', `Speaker toggle: ${muted ? 'MUTED' : 'UNMUTED'}`);
2482
+ setIsSpeakerMuted(muted);
2483
+ if (muted) {
2484
+ audioOutputRef.current?.mute();
2485
+ } else {
2486
+ audioOutputRef.current?.unmute();
2487
+ }
2488
+ },
2489
+ tickets: tickets,
2490
+ selectedTicketId: selectedTicketId,
2491
+ onTicketSelect: handleTicketSelect,
2492
+ onBackToTickets: handleBackToTickets,
2493
+ autoExpandTrigger: autoExpandTrigger,
2494
+ unreadCounts: unreadCounts,
2495
+ totalUnread: totalUnread,
2496
+ showDiscoveryTooltip: tooltipVisible,
2497
+ discoveryTooltipMessage: discoveryTooltipMessage,
2498
+ onTooltipDismiss: handleTooltipDismiss,
2499
+ conversations: conversations,
2500
+ isLoadingHistory: isLoadingHistory,
2501
+ onConversationSelect: handleConversationSelect,
2502
+ onNewConversation: handleNewConversation,
2503
+ renderMode: "default"
2504
+ })
2505
+ })
2506
+ }), /*#__PURE__*/_jsx(AgentOverlay, {
2507
+ visible: overlayVisible,
2508
+ statusText: overlayStatusText,
2509
+ onCancel: handleCancel
2510
+ }), Platform.OS !== 'android' && /*#__PURE__*/_jsxs(_Fragment, {
2511
+ children: [/*#__PURE__*/_jsx(SupportChatModal, {
2512
+ visible: mode === 'human' && !!selectedTicketId,
2513
+ messages: supportMessages,
2514
+ onSend: handleSend,
2515
+ onClose: handleBackToTickets,
2516
+ isAgentTyping: isLiveAgentTyping,
2517
+ isThinking: isThinking,
2518
+ scrollToEndTrigger: chatScrollTrigger,
2519
+ ticketStatus: tickets.find(t => t.id === selectedTicketId)?.status
2520
+ }), /*#__PURE__*/_jsx(AIConsentDialog, {
2521
+ visible: showConsentDialog,
2522
+ provider: providerName,
2523
+ config: consentConfig,
2524
+ language: 'en',
2525
+ onConsent: async () => {
2526
+ await grantConsent();
2527
+ setShowConsentDialog(false);
2528
+ consentConfig.onConsent?.();
2529
+ logger.info('AIAgent', '✅ AI consent granted by user');
2530
+ },
2531
+ onDecline: () => {
2532
+ setShowConsentDialog(false);
2533
+ consentConfig.onDecline?.();
2534
+ logger.info('AIAgent', '❌ AI consent declined by user');
2535
+ }
2536
+ })]
2537
+ }), Platform.OS === 'android' && /*#__PURE__*/_jsxs(_Fragment, {
2538
+ children: [/*#__PURE__*/_jsx(SupportChatModal, {
2539
+ visible: mode === 'human' && !!selectedTicketId,
2540
+ messages: supportMessages,
2541
+ onSend: handleSend,
2542
+ onClose: handleBackToTickets,
2543
+ isAgentTyping: isLiveAgentTyping,
2544
+ isThinking: isThinking,
2545
+ scrollToEndTrigger: chatScrollTrigger,
2546
+ ticketStatus: tickets.find(t => t.id === selectedTicketId)?.status
2547
+ }), /*#__PURE__*/_jsx(FloatingOverlayWrapper, {
2548
+ fallbackStyle: styles.floatingLayer,
2549
+ children: /*#__PURE__*/_jsx(AIConsentDialog, {
2240
2550
  visible: showConsentDialog,
2241
2551
  provider: providerName,
2242
2552
  config: consentConfig,
@@ -2252,8 +2562,8 @@ export function AIAgent({
2252
2562
  consentConfig.onDecline?.();
2253
2563
  logger.info('AIAgent', '❌ AI consent declined by user');
2254
2564
  }
2255
- })]
2256
- })
2565
+ })
2566
+ })]
2257
2567
  })]
2258
2568
  })
2259
2569
  });