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