@appilots/sdk 0.7.0 → 0.10.0

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.
@@ -1,659 +1,7 @@
1
- import { getFiberRoot, recordIntrospectionFailure, getCurrentScreen, getActiveRouteNames, deriveInteractionElements, attachElementIdsToChoiceGroups, elementRegistry, appilotsDebugWarn, getDefaultRegistry, useAppilotsContext, ChatSessionMachine, getCurrentScreenSignature, ActionQueueMachine, getScreenMetadata, setCurrentScreen, useResolvedRegistry, listRegistry, componentRegistry, probeLoadingState, isListItemPressTarget, waitForLoadingSettle, appilotsDebugLog, waitForScreenSettle, screenDepartedBaseline, routesBelongToSameFeature, actionPressTargetId, getNavigationStateSnapshot, getNavigationRef, isOptionalStepAutomationFailure, OPTIONAL_STEP_AUTOMATION_HINT, normalize3, matchScore, idLooselyMatches, parseStableElementId, getAllScreens, snapshotShowsOpenCreateForm, looksDestructiveActionLabel, isGenericSelectValue } from './chunk-R4D34FEW.mjs';
2
- import { useMemo, useRef, useSyncExternalStore, useEffect, useCallback, useState } from 'react';
1
+ import { getFiberRoot, appilotsDebugWarn, recordIntrospectionFailure, getCurrentScreen, walkFiber, getActiveRouteNames, clampSnapshotToWireLimits, deriveInteractionElements, rectForNode, toWireElement, attachElementIdsToChoiceGroups, elementRegistry, appilotsDebugLog, getLastWalkDiagnostics, SDK_VERSION, geometryStats, geometryAvailability, getNameManglingProbe, emptyDetectorCounts, getAutoTrackingState, getIntrospectionDiagnostics, getDefaultRegistry, useAppilotsContext, ChatSessionMachine, getCurrentScreenSignature, ActionQueueMachine, getScreenMetadata, setCurrentScreen, useResolvedRegistry, totalDetectorHits, listRegistry, getOpenNativeDialog, componentRegistry, getLastAnsweredNativeDialog, probeLoadingState, isListItemPressTarget, waitForLoadingSettle, describeValue, waitForScreenSettle, screenDepartedBaseline, routesBelongToSameFeature, actionPressTargetId, getNavigationStateSnapshot, getNavigationRef, isOptionalStepAutomationFailure, OPTIONAL_STEP_AUTOMATION_HINT, normalize3, matchScore, FORM_VALIDATION_TEXT_RE, idLooselyMatches, parseStableElementId, shouldSkipSubtree, isAlwaysListContainer, isHeuristicListContainer, findListItems, getTypeName, foldForCompare, isModalFiber, getAllScreens, snapshotShowsOpenCreateForm, switchValue, isReferenceable, looksDestructiveActionLabel, isGenericSelectValue, FORM_WRITE_SUCCESS_TEXT_RE, isSwitchLike, TOUCHABLE_NAMES } from './chunk-AHHDOUVX.mjs';
2
+ import React, { useMemo, useRef, useSyncExternalStore, useEffect, useCallback, useState } from 'react';
3
3
  import { Alert } from 'react-native';
4
4
 
5
- // src/introspection/walkFiber.ts
6
- function getTypeName(type) {
7
- if (!type) return null;
8
- if (typeof type === "string") return type;
9
- if (typeof type === "function") {
10
- return type.displayName ?? type.name ?? null;
11
- }
12
- if (typeof type === "object") {
13
- if (type.displayName) return type.displayName;
14
- if (type.render) {
15
- return type.render.displayName ?? type.render.name ?? null;
16
- }
17
- if (type.type) return getTypeName(type.type);
18
- }
19
- return null;
20
- }
21
- function matchesName(fiber, name) {
22
- return getTypeName(fiber.type) === name || getTypeName(fiber.elementType) === name;
23
- }
24
- function extractTextFromChildren(children) {
25
- if (children == null) return "";
26
- if (typeof children === "string") return children;
27
- if (typeof children === "number") return String(children);
28
- if (Array.isArray(children)) {
29
- return children.map((c) => extractTextFromChildren(c)).filter(Boolean).join("");
30
- }
31
- return "";
32
- }
33
- function hasMeaningfulText(text) {
34
- return !!text && /[\p{L}\p{N}]/u.test(text);
35
- }
36
- function findChildText(fiber) {
37
- const stack = [];
38
- if (fiber.child) stack.push(fiber.child);
39
- const visited = /* @__PURE__ */ new WeakSet();
40
- const texts = [];
41
- while (stack.length > 0) {
42
- const node = stack.pop();
43
- if (!node || visited.has(node)) continue;
44
- visited.add(node);
45
- if (matchesName(node, "Text")) {
46
- const text = extractTextFromChildren(node.memoizedProps?.children);
47
- const trimmed = text.trim();
48
- if (hasMeaningfulText(trimmed)) {
49
- texts.push(trimmed);
50
- if (texts.length >= 3) break;
51
- }
52
- }
53
- if (node.sibling) stack.push(node.sibling);
54
- if (node.child) stack.push(node.child);
55
- }
56
- return texts.length > 0 ? texts.join(" \xB7 ") : void 0;
57
- }
58
- var SELF_REFERENTIAL_COMPONENTS = /* @__PURE__ */ new Set([
59
- "AppilotsChat",
60
- "AppilotsChatInner",
61
- "ActionBreadcrumb",
62
- "ConfirmDialog"
63
- ]);
64
- function isAppilotsSkipMarked(props) {
65
- if (!props) return false;
66
- return props.__appilotsSkip === true || props["data-appilots-skip"] === true || props.appilotsSkip === true;
67
- }
68
- function isAppilotsSensitiveMarked(props) {
69
- if (!props) return false;
70
- return props.__appilotsSensitive === true || props["data-appilots-sensitive"] === true || props.appilotsSensitive === true;
71
- }
72
- function stringProp(props, name) {
73
- const value = props?.[name];
74
- return typeof value === "string" && value.length > 0 ? value : void 0;
75
- }
76
- function numberProp(props, name) {
77
- const value = props?.[name];
78
- return typeof value === "number" && Number.isFinite(value) ? value : void 0;
79
- }
80
- function shouldSkipSubtree(fiber) {
81
- const props = fiber.memoizedProps;
82
- if (!props) return false;
83
- const typeName = getTypeName(fiber.type) ?? getTypeName(fiber.elementType);
84
- if (typeName && SELF_REFERENTIAL_COMPONENTS.has(typeName)) return true;
85
- if (isAppilotsSkipMarked(props)) return true;
86
- if (matchesName(fiber, "Modal") && props.visible === false) return true;
87
- if (matchesName(fiber, "Screen") && props.activityState === 0) return true;
88
- const style = props.style;
89
- if (style) {
90
- const flat = Array.isArray(style) ? Object.assign({}, ...style.filter(Boolean)) : style;
91
- if (flat?.display === "none") return true;
92
- if (flat?.opacity === 0 && props.pointerEvents === "none") return true;
93
- }
94
- return false;
95
- }
96
- function inferInputType(props) {
97
- if (props.secureTextEntry || isAppilotsSensitiveMarked(props)) return "password";
98
- switch (props.keyboardType) {
99
- case "email-address":
100
- return "email";
101
- case "numeric":
102
- case "number-pad":
103
- case "decimal-pad":
104
- return "number";
105
- case "phone-pad":
106
- return "phone";
107
- default:
108
- return "text";
109
- }
110
- }
111
- function inferLabel(props) {
112
- return props.accessibilityLabel ?? props.label ?? props.placeholder ?? props.testID;
113
- }
114
- function bestId(props) {
115
- return props.testID ?? props.accessibilityLabel ?? void 0;
116
- }
117
- var ALWAYS_LIST_NAMES = /* @__PURE__ */ new Set([
118
- "FlatList",
119
- "SectionList",
120
- "VirtualizedList",
121
- // Shopify's high-perf FlatList replacement — same usage shape, same
122
- // virtualization, same per-item key.
123
- "FlashList"
124
- ]);
125
- var HEURISTIC_LIST_NAMES = /* @__PURE__ */ new Set(["ScrollView"]);
126
- var LIST_CELL_NAMES = /* @__PURE__ */ new Set([
127
- "CellRenderer",
128
- "CellRendererComponent",
129
- "VirtualizedListCellContextProvider"
130
- ]);
131
- function isAlwaysListContainer(fiber) {
132
- for (const name of ALWAYS_LIST_NAMES) {
133
- if (matchesName(fiber, name)) return true;
134
- }
135
- return false;
136
- }
137
- function isHeuristicListContainer(fiber) {
138
- for (const name of HEURISTIC_LIST_NAMES) {
139
- if (matchesName(fiber, name)) return true;
140
- }
141
- return false;
142
- }
143
- function isKnownListCell(fiber) {
144
- const typeName = getTypeName(fiber.type) ?? getTypeName(fiber.elementType);
145
- return !!typeName && LIST_CELL_NAMES.has(typeName);
146
- }
147
- function isModalFiber(fiber) {
148
- return matchesName(fiber, "Modal");
149
- }
150
- function isVisibleModalFiber(fiber) {
151
- return isModalFiber(fiber) && fiber?.memoizedProps?.visible !== false;
152
- }
153
- function collectVisibleModalFibers(container, excludeRoots = []) {
154
- const excluded = /* @__PURE__ */ new WeakSet();
155
- for (const rootFiber of excludeRoots) {
156
- if (rootFiber) excluded.add(rootFiber);
157
- }
158
- const found = [];
159
- const visited = /* @__PURE__ */ new WeakSet();
160
- const stack = [];
161
- if (container?.child) stack.push(container.child);
162
- while (stack.length > 0) {
163
- const node = stack.pop();
164
- if (!node || visited.has(node)) continue;
165
- visited.add(node);
166
- if (node.sibling) stack.push(node.sibling);
167
- if (excluded.has(node) || shouldSkipSubtree(node)) continue;
168
- if (isVisibleModalFiber(node)) {
169
- found.push(node);
170
- continue;
171
- }
172
- if (node.child) stack.push(node.child);
173
- }
174
- return found;
175
- }
176
- function isMarkedListItem(fiber, listId) {
177
- const props = fiber?.memoizedProps;
178
- if (!props || props.__appilotsListItem !== true) return false;
179
- if (!listId) return true;
180
- return props.__appilotsListId === listId;
181
- }
182
- function listIdFromProps(props) {
183
- return stringProp(props, "__appilotsListId") ?? stringProp(props, "appilotsListId");
184
- }
185
- function listMetadataFromProps(props) {
186
- const id = listIdFromProps(props);
187
- const itemCount = numberProp(props, "__appilotsItemCount");
188
- const label = stringProp(props, "__appilotsListLabel") ?? stringProp(props, "accessibilityLabel") ?? stringProp(props, "testID");
189
- const refreshing = props?.refreshing === true || props?.__appilotsRefreshing === true;
190
- const empty = props?.__appilotsEmpty === true || typeof itemCount === "number" && itemCount === 0;
191
- return {
192
- id,
193
- itemCount,
194
- refreshing,
195
- empty,
196
- label,
197
- source: id ? "auto-tracked" : void 0
198
- };
199
- }
200
- function hasRenderableContent(fiber) {
201
- const stack = [fiber];
202
- const visited = /* @__PURE__ */ new WeakSet();
203
- while (stack.length > 0) {
204
- const node = stack.pop();
205
- if (!node || visited.has(node)) continue;
206
- visited.add(node);
207
- if (shouldSkipSubtree(node) || isModalFiber(node)) continue;
208
- const props = node.memoizedProps ?? {};
209
- if (matchesName(node, "Text")) {
210
- const text = extractTextFromChildren(props.children);
211
- if (text && text.trim().length > 0) return true;
212
- }
213
- if (matchesName(node, "TextInput") || matchesName(node, "Switch")) {
214
- return true;
215
- }
216
- if (TOUCHABLE_NAMES.has(getTypeName(node.type) ?? "") || TOUCHABLE_NAMES.has(getTypeName(node.elementType) ?? "")) {
217
- return true;
218
- }
219
- if (node.sibling) stack.push(node.sibling);
220
- if (node.child) stack.push(node.child);
221
- }
222
- return false;
223
- }
224
- function findListCellsByType(container, minItems) {
225
- const cells = [];
226
- const visited = /* @__PURE__ */ new WeakSet();
227
- const stack = [];
228
- if (container?.child) stack.push(container.child);
229
- while (stack.length > 0) {
230
- const node = stack.pop();
231
- if (!node || visited.has(node)) continue;
232
- visited.add(node);
233
- if (node.sibling) stack.push(node.sibling);
234
- if (shouldSkipSubtree(node) || isModalFiber(node)) continue;
235
- if (isKnownListCell(node) && hasRenderableContent(node)) {
236
- cells.push(node);
237
- continue;
238
- }
239
- if (node.child) stack.push(node.child);
240
- }
241
- return cells.length >= minItems ? cells : null;
242
- }
243
- function findMarkedListItems(container, minItems, listId) {
244
- const items = [];
245
- const visited = /* @__PURE__ */ new WeakSet();
246
- const stack = [];
247
- if (container?.child) stack.push(container.child);
248
- while (stack.length > 0) {
249
- const node = stack.pop();
250
- if (!node || visited.has(node)) continue;
251
- visited.add(node);
252
- if (node.sibling) stack.push(node.sibling);
253
- if (shouldSkipSubtree(node) || isModalFiber(node)) continue;
254
- if (isMarkedListItem(node, listId)) {
255
- items.push(node);
256
- continue;
257
- }
258
- if (node.child) stack.push(node.child);
259
- }
260
- if (items.length < minItems) return null;
261
- items.sort((a, b) => {
262
- const ai = numberProp(a.memoizedProps, "__appilotsItemIndex");
263
- const bi = numberProp(b.memoizedProps, "__appilotsItemIndex");
264
- if (ai === void 0 && bi === void 0) return 0;
265
- if (ai === void 0) return 1;
266
- if (bi === void 0) return -1;
267
- return ai - bi;
268
- });
269
- return items;
270
- }
271
- function hasAuthoredReactKey(fiber) {
272
- const key = fiber?.key;
273
- if (key === null || key === void 0 || key === "") return false;
274
- const s = String(key);
275
- if (s.startsWith(".") && !s.startsWith(".$")) return false;
276
- return true;
277
- }
278
- function findKeyedSiblingItems(container, minItems) {
279
- if (!container) return null;
280
- let current = container;
281
- const visited = /* @__PURE__ */ new WeakSet();
282
- while (current && !visited.has(current)) {
283
- visited.add(current);
284
- const siblings = [];
285
- let cur = current.child;
286
- while (cur) {
287
- if (!isModalFiber(cur)) siblings.push(cur);
288
- cur = cur.sibling;
289
- }
290
- if (siblings.length === 0) return null;
291
- const keyed = siblings.filter(hasAuthoredReactKey);
292
- if (keyed.length >= minItems) {
293
- return keyed;
294
- }
295
- if (siblings.length > 1) {
296
- return null;
297
- }
298
- current = siblings[0];
299
- }
300
- return null;
301
- }
302
- function findListItems(container, minItems = 2, preferCells = false) {
303
- if (!container) return null;
304
- const listId = listIdFromProps(container.memoizedProps);
305
- const markedItems = findMarkedListItems(container, minItems, listId);
306
- if (markedItems) return markedItems;
307
- if (preferCells) {
308
- const cells = findListCellsByType(container, minItems);
309
- if (cells) return cells;
310
- }
311
- if (minItems <= 1) {
312
- const multiItemLevel = findKeyedSiblingItems(container, 2);
313
- if (multiItemLevel) return multiItemLevel;
314
- }
315
- return findKeyedSiblingItems(container, minItems) ?? findListCellsByType(container, minItems);
316
- }
317
- var TOUCHABLE_NAMES = /* @__PURE__ */ new Set([
318
- "TouchableOpacity",
319
- "TouchableHighlight",
320
- "TouchableWithoutFeedback",
321
- "TouchableNativeFeedback",
322
- "Pressable"
323
- ]);
324
- function hasPressHandler(fiber) {
325
- const props = fiber?.memoizedProps ?? {};
326
- return typeof props.onPress === "function" || typeof props.onPressIn === "function";
327
- }
328
- function isPressableTarget(fiber) {
329
- return TOUCHABLE_NAMES.has(getTypeName(fiber.type) ?? "") || TOUCHABLE_NAMES.has(getTypeName(fiber.elementType) ?? "") || hasPressHandler(fiber);
330
- }
331
- function walkSubtree(root, detectLists, startInModal = false) {
332
- const result = {
333
- texts: [],
334
- inputs: [],
335
- buttons: [],
336
- toggles: [],
337
- sliders: [],
338
- visitedFibers: 0,
339
- skippedHidden: 0,
340
- loading: false,
341
- modalOpen: false,
342
- lists: []
343
- };
344
- if (!root) return result;
345
- const visited = /* @__PURE__ */ new WeakSet();
346
- const stack = [];
347
- if (root.child) stack.push({ fiber: root.child, inModal: startInModal });
348
- while (stack.length > 0) {
349
- const { fiber, inModal } = stack.pop();
350
- if (!fiber || visited.has(fiber)) continue;
351
- visited.add(fiber);
352
- result.visitedFibers++;
353
- if (fiber.sibling) stack.push({ fiber: fiber.sibling, inModal });
354
- if (shouldSkipSubtree(fiber)) {
355
- result.skippedHidden++;
356
- continue;
357
- }
358
- if (detectLists) {
359
- const isAlways = isAlwaysListContainer(fiber);
360
- const isHeuristic = !isAlways && isHeuristicListContainer(fiber);
361
- if (isAlways || isHeuristic) {
362
- const minItems = isAlways ? 1 : 2;
363
- const items = findListItems(fiber, minItems, isAlways);
364
- if (items && items.length >= minItems) {
365
- const containerType = getTypeName(fiber.type) ?? getTypeName(fiber.elementType) ?? "List";
366
- const listIndex = result.lists.length;
367
- const itemSnapshots = items.map((itemFiber, i) => {
368
- const itemResult = walkSubtree(itemFiber, false, inModal);
369
- result.visitedFibers += itemResult.visitedFibers;
370
- result.skippedHidden += itemResult.skippedHidden;
371
- if (itemResult.loading) result.loading = true;
372
- if (itemResult.modalOpen) result.modalOpen = true;
373
- const reactKey = typeof itemFiber.key === "string" || typeof itemFiber.key === "number" ? String(itemFiber.key) : typeof itemFiber.memoizedProps?.__appilotsItemKey === "string" || typeof itemFiber.memoizedProps?.__appilotsItemKey === "number" ? String(itemFiber.memoizedProps.__appilotsItemKey) : void 0;
374
- const itemKey = typeof itemFiber.memoizedProps?.__appilotsItemKey === "string" || typeof itemFiber.memoizedProps?.__appilotsItemKey === "number" ? String(itemFiber.memoizedProps.__appilotsItemKey) : void 0;
375
- const dataIndex = numberProp(itemFiber.memoizedProps, "__appilotsItemIndex");
376
- return {
377
- index: i + 1,
378
- dataIndex,
379
- reactKey,
380
- itemKey,
381
- syntheticId: `list-${listIndex}-item-${i + 1}`,
382
- texts: dedupeStringsInternal(itemResult.texts),
383
- buttons: dedupeButtonsInternal(itemResult.buttons),
384
- inputs: dedupeInputsInternal(itemResult.inputs),
385
- toggles: dedupeTogglesInternal(itemResult.toggles)
386
- };
387
- });
388
- const meta2 = listMetadataFromProps(fiber.memoizedProps);
389
- result.lists.push({
390
- index: listIndex,
391
- id: meta2.id,
392
- containerType,
393
- source: meta2.source ?? "fiber",
394
- itemCount: meta2.itemCount,
395
- visibleItemCount: itemSnapshots.length,
396
- refreshing: meta2.refreshing,
397
- empty: meta2.empty,
398
- label: meta2.label,
399
- items: itemSnapshots
400
- });
401
- for (const modalFiber of collectVisibleModalFibers(fiber, items)) {
402
- result.modalOpen = true;
403
- if (modalFiber.child) stack.push({ fiber: modalFiber.child, inModal: true });
404
- }
405
- continue;
406
- }
407
- const meta = listMetadataFromProps(fiber.memoizedProps);
408
- if (meta.id || meta.itemCount !== void 0 || meta.refreshing || meta.empty) {
409
- const containerType = getTypeName(fiber.type) ?? getTypeName(fiber.elementType) ?? "List";
410
- result.lists.push({
411
- index: result.lists.length,
412
- id: meta.id,
413
- containerType,
414
- source: meta.source ?? "auto-tracked",
415
- itemCount: meta.itemCount,
416
- visibleItemCount: 0,
417
- refreshing: meta.refreshing,
418
- empty: meta.empty,
419
- label: meta.label,
420
- items: []
421
- });
422
- for (const modalFiber of collectVisibleModalFibers(fiber)) {
423
- result.modalOpen = true;
424
- if (modalFiber.child) stack.push({ fiber: modalFiber.child, inModal: true });
425
- }
426
- continue;
427
- }
428
- }
429
- }
430
- const props = fiber.memoizedProps ?? {};
431
- if (matchesName(fiber, "Text")) {
432
- const text = extractTextFromChildren(props.children);
433
- if (text && text.trim().length > 0) {
434
- result.texts.push(text.trim());
435
- }
436
- } else if (matchesName(fiber, "TextInput")) {
437
- if (props.__appilotsInternal !== true) {
438
- const isSecure = !!props.secureTextEntry || isAppilotsSensitiveMarked(props);
439
- const rawValue = typeof props.value === "string" ? props.value : void 0;
440
- const safeValue = isSecure ? rawValue && rawValue.length > 0 ? "<hidden>" : void 0 : rawValue;
441
- result.inputs.push({
442
- id: bestId(props),
443
- label: inferLabel(props),
444
- value: safeValue,
445
- placeholder: props.placeholder,
446
- editable: props.editable !== false,
447
- secure: isSecure,
448
- type: inferInputType(props),
449
- ...inModal ? { inModal: true } : {}
450
- });
451
- }
452
- } else if (props.accessibilityRole === "adjustable") {
453
- if (props.__appilotsInternal !== true) {
454
- const av = props.accessibilityValue ?? {};
455
- result.sliders.push({
456
- id: bestId(props),
457
- label: props.accessibilityLabel ?? props.testID,
458
- value: typeof av.now === "number" ? av.now : void 0,
459
- min: typeof av.min === "number" ? av.min : void 0,
460
- max: typeof av.max === "number" ? av.max : void 0,
461
- ...inModal ? { inModal: true } : {}
462
- });
463
- }
464
- } else if (isPressableTarget(fiber)) {
465
- if (props.__appilotsInternal !== true) {
466
- const label = findChildText(fiber);
467
- const selected = props.accessibilityState?.selected === true;
468
- result.buttons.push({
469
- id: bestId(props) ?? (label ? normalizeLabel(label) : void 0),
470
- label,
471
- disabled: !!props.disabled,
472
- ...selected ? { selected: true } : {},
473
- ...inModal ? { inModal: true } : {}
474
- });
475
- }
476
- } else if (matchesName(fiber, "Switch")) {
477
- result.toggles.push({
478
- id: bestId(props),
479
- label: props.accessibilityLabel ?? props.testID,
480
- value: !!props.value,
481
- ...inModal ? { inModal: true } : {}
482
- });
483
- } else if (matchesName(fiber, "ActivityIndicator")) {
484
- result.loading = true;
485
- } else if (matchesName(fiber, "Modal")) {
486
- result.modalOpen = true;
487
- if (fiber.child) stack.push({ fiber: fiber.child, inModal: true });
488
- continue;
489
- }
490
- if (fiber.child) stack.push({ fiber: fiber.child, inModal });
491
- }
492
- return result;
493
- }
494
- function walkFiber(root) {
495
- const snapshot = {
496
- route: getCurrentScreen(),
497
- texts: [],
498
- inputs: [],
499
- buttons: [],
500
- toggles: [],
501
- // Sliders come from the ComponentRegistry (useAppilotsSlider) —
502
- // captureSnapshot merges them so observation only ever advertises
503
- // sliders the executor can actually set (min/max/step + setValue).
504
- sliders: [],
505
- loading: false,
506
- modalOpen: false,
507
- lists: [],
508
- choiceGroups: [],
509
- elements: [],
510
- stats: { visitedFibers: 0, skippedHidden: 0 }
511
- };
512
- if (!root) return snapshot;
513
- const sub = walkSubtree(root, true);
514
- snapshot.texts = dedupeStringsInternal(sub.texts);
515
- snapshot.inputs = dedupeInputsInternal(sub.inputs);
516
- snapshot.buttons = dedupeButtonsInternal(sub.buttons);
517
- snapshot.toggles = dedupeTogglesInternal(sub.toggles);
518
- snapshot.sliders = dedupeSliders(sub.sliders);
519
- snapshot.loading = sub.loading;
520
- snapshot.modalOpen = sub.modalOpen;
521
- snapshot.lists = sub.lists;
522
- snapshot.choiceGroups = deriveChoiceGroups(snapshot);
523
- snapshot.stats.visitedFibers = sub.visitedFibers;
524
- snapshot.stats.skippedHidden = sub.skippedHidden;
525
- return snapshot;
526
- }
527
- function dedupeButtonsInternal(buttons) {
528
- return dedupeButtons(buttons);
529
- }
530
- function dedupeInputsInternal(inputs) {
531
- return dedupeInputs(inputs);
532
- }
533
- function dedupeTogglesInternal(toggles) {
534
- return dedupeToggles(toggles);
535
- }
536
- function dedupeStringsInternal(strings) {
537
- return dedupeStrings(strings);
538
- }
539
- function isCommandLikeLabel(label) {
540
- if (!label) return true;
541
- const normalized = normalizeLabel(label);
542
- if (!normalized) return true;
543
- return /^(proximo|prox|next|continuar|continue|voltar|back|confirmar|confirm|cancelar|cancel|salvar|save|enviar|submit|ok|limpar|clear|fechar|close|adicionar|add|novo|nova|new)$/i.test(
544
- normalized
545
- );
546
- }
547
- function deriveChoiceGroups(snapshot) {
548
- const groups = [];
549
- for (const list of snapshot.lists) {
550
- const options = list.items.filter((item) => item.texts.length > 0 || item.buttons.length > 0).map((item, i) => {
551
- const texts = item.texts.length > 0 ? item.texts : item.buttons.map((b) => b.label ?? b.id ?? "").filter((text) => text.length > 0);
552
- return {
553
- index: i + 1,
554
- syntheticId: `${list.id ?? `list-${list.index}`}-choice-${i + 1}`,
555
- targetId: item.syntheticId,
556
- label: texts.slice(0, 2).join(" \xB7 "),
557
- texts,
558
- disabled: false
559
- };
560
- });
561
- if (options.length > 0) {
562
- groups.push({
563
- index: groups.length,
564
- id: list.id,
565
- label: list.label,
566
- source: "list",
567
- options
568
- });
569
- }
570
- }
571
- const buttonOptions = snapshot.buttons.filter((button) => !button.disabled).filter((button) => hasMeaningfulText(button.label ?? button.id)).filter((button) => !isCommandLikeLabel(button.label ?? button.id)).map((button, i) => {
572
- const label = button.label ?? button.id ?? "";
573
- return {
574
- index: i + 1,
575
- syntheticId: `choice-buttons-option-${i + 1}`,
576
- targetId: button.id ?? button.label,
577
- label,
578
- texts: [label],
579
- selected: !!button.selected,
580
- disabled: !!button.disabled
581
- };
582
- });
583
- if (buttonOptions.length >= 2) {
584
- groups.push({
585
- index: groups.length,
586
- id: "visible-options",
587
- label: "Visible options",
588
- source: "buttons",
589
- options: buttonOptions
590
- });
591
- }
592
- return groups;
593
- }
594
- function dedupeButtons(buttons) {
595
- const seen = /* @__PURE__ */ new Set();
596
- const out = [];
597
- for (const b of buttons) {
598
- const idTrim = b.id?.trim();
599
- const labelTrim = b.label?.trim();
600
- if (!idTrim && (!labelTrim || !/[a-z0-9]/i.test(labelTrim))) continue;
601
- const key = `${idTrim ?? ""}|${labelTrim ?? ""}|${b.disabled ? 1 : 0}|${b.inModal ? 1 : 0}|${b.selected ? 1 : 0}`;
602
- if (seen.has(key)) continue;
603
- seen.add(key);
604
- out.push(b);
605
- }
606
- return out;
607
- }
608
- function dedupeInputs(inputs) {
609
- const seen = /* @__PURE__ */ new Set();
610
- const out = [];
611
- for (const i of inputs) {
612
- const key = `${i.id ?? ""}|${i.label ?? ""}|${i.placeholder ?? ""}|${i.inModal ? 1 : 0}`;
613
- if (seen.has(key)) continue;
614
- seen.add(key);
615
- out.push(i);
616
- }
617
- return out;
618
- }
619
- function dedupeSliders(sliders) {
620
- const seen = /* @__PURE__ */ new Set();
621
- const out = [];
622
- for (const s of sliders) {
623
- const key = `${s.id ?? ""}|${s.label ?? ""}|${s.inModal ? 1 : 0}`;
624
- if (seen.has(key)) continue;
625
- seen.add(key);
626
- out.push(s);
627
- }
628
- return out;
629
- }
630
- function dedupeToggles(toggles) {
631
- const seen = /* @__PURE__ */ new Set();
632
- const out = [];
633
- for (const t of toggles) {
634
- const key = `${t.id ?? ""}|${t.label ?? ""}|${t.inModal ? 1 : 0}`;
635
- if (seen.has(key)) continue;
636
- seen.add(key);
637
- out.push(t);
638
- }
639
- return out;
640
- }
641
- function dedupeStrings(strings) {
642
- const seen = /* @__PURE__ */ new Set();
643
- const out = [];
644
- for (const s of strings) {
645
- const trimmed = s.trim();
646
- if (!trimmed) continue;
647
- if (seen.has(trimmed)) continue;
648
- seen.add(trimmed);
649
- out.push(trimmed);
650
- }
651
- return out;
652
- }
653
- function normalizeLabel(label) {
654
- return label.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9\s]/g, "").trim().replace(/\s+(.)/g, (_, c) => c.toUpperCase()).replace(/\s+/g, "");
655
- }
656
-
657
5
  // src/introspection/captureSnapshot.ts
658
6
  function normalizeSliderKey(value) {
659
7
  return (value ?? "").toLowerCase().replace(/^slider[-_]/, "");
@@ -694,6 +42,29 @@ function reconcileSliders(snapshot) {
694
42
  }
695
43
  snapshot.sliders = out;
696
44
  }
45
+ function normalizeFieldKey(value) {
46
+ return (value ?? "").toLowerCase().replace(/^(input|field)[-_]/, "");
47
+ }
48
+ function canonicalizeRegisteredFields(snapshot) {
49
+ const claimed = /* @__PURE__ */ new Set();
50
+ for (const { id, kind, label } of componentRegistry.snapshot()) {
51
+ if (kind !== "field") continue;
52
+ if (snapshot.inputs.some((input) => input.id === id)) continue;
53
+ const registryLabel = (label ?? "").toLowerCase();
54
+ const registryKey = normalizeFieldKey(id);
55
+ const candidates = snapshot.inputs.filter((input) => {
56
+ if (claimed.has(input)) return false;
57
+ const inputLabel = (input.label ?? "").toLowerCase();
58
+ if (registryLabel && inputLabel && inputLabel === registryLabel) return true;
59
+ const inputKey = normalizeFieldKey(input.id);
60
+ return !!inputKey && !!registryKey && inputKey === registryKey;
61
+ });
62
+ if (candidates.length !== 1) continue;
63
+ const match = candidates[0];
64
+ claimed.add(match);
65
+ match.id = id;
66
+ }
67
+ }
697
68
  function mergeRegisteredLists(snapshot) {
698
69
  const registered = listRegistry.snapshot();
699
70
  if (registered.length === 0) return;
@@ -702,6 +73,7 @@ function mergeRegisteredLists(snapshot) {
702
73
  if (list.id) byId.set(list.id, list);
703
74
  }
704
75
  for (const entry of registered) {
76
+ if (entry.kind === "scroll") continue;
705
77
  const metrics = entry.getScrollMetrics?.();
706
78
  const scrollFacts = metrics ? {
707
79
  scrollOffsetY: Math.max(0, Math.round(metrics.offset)),
@@ -742,11 +114,66 @@ function mergeRegisteredLists(snapshot) {
742
114
  });
743
115
  }
744
116
  }
117
+ function mergeRegisteredScrollables(snapshot) {
118
+ const registered = listRegistry.snapshot().filter((e) => e.kind === "scroll");
119
+ if (registered.length === 0) return;
120
+ const listsById = /* @__PURE__ */ new Map();
121
+ for (const list of snapshot.lists) {
122
+ if (list.id) listsById.set(list.id, list);
123
+ }
124
+ const out = [];
125
+ for (const entry of registered) {
126
+ const metrics = entry.getScrollMetrics?.();
127
+ const facts = metrics ? {
128
+ scrollOffsetY: Math.max(0, Math.round(metrics.offset)),
129
+ canScrollUp: metrics.offset > 8,
130
+ canScrollDown: metrics.offset + metrics.visibleLength < metrics.contentLength - 8
131
+ } : void 0;
132
+ const asList = listsById.get(entry.id);
133
+ if (asList) {
134
+ if (facts) {
135
+ asList.scrollOffsetY = asList.scrollOffsetY ?? facts.scrollOffsetY;
136
+ asList.canScrollUp = asList.canScrollUp ?? facts.canScrollUp;
137
+ asList.canScrollDown = asList.canScrollDown ?? facts.canScrollDown;
138
+ }
139
+ continue;
140
+ }
141
+ out.push({
142
+ id: entry.id,
143
+ containerType: entry.component,
144
+ ...entry.label ? { label: entry.label } : {},
145
+ ...facts ?? {}
146
+ });
147
+ }
148
+ if (out.length > 0) snapshot.scrollables = out;
149
+ }
150
+ function mergeNativeDialog(snapshot) {
151
+ const dialog = getOpenNativeDialog();
152
+ if (!dialog) return;
153
+ snapshot.nativeDialog = {
154
+ id: dialog.id,
155
+ ...dialog.title ? { title: dialog.title } : {},
156
+ ...dialog.message ? { message: dialog.message } : {},
157
+ buttons: dialog.buttons.map((b) => ({
158
+ label: b.label,
159
+ ...b.style ? { style: b.style } : {}
160
+ }))
161
+ };
162
+ snapshot.modalOpen = true;
163
+ }
164
+ var MIN_FIBERS_FOR_BLINDNESS = 200;
165
+ function recordBlindWalk() {
166
+ const walk = getLastWalkDiagnostics();
167
+ if (!walk) return;
168
+ if (walk.visitedFibers < MIN_FIBERS_FOR_BLINDNESS) return;
169
+ if (totalDetectorHits(walk.detectors) > 0) return;
170
+ recordIntrospectionFailure("walk-recognized-nothing", null);
171
+ }
745
172
  function captureSnapshot() {
746
173
  const root = getFiberRoot();
747
174
  if (!root) {
748
- console.warn(
749
- "[Appilots] captureSnapshot: fiber root not captured yet. Ensure <AppilotsProvider> has rendered before calling this."
175
+ appilotsDebugWarn(
176
+ "captureSnapshot: fiber root not captured yet. Ensure <AppilotsProvider> has rendered before calling this."
750
177
  );
751
178
  recordIntrospectionFailure("never-mounted", null);
752
179
  return {
@@ -765,34 +192,83 @@ function captureSnapshot() {
765
192
  }
766
193
  const start = Date.now();
767
194
  const snapshot = walkFiber(root);
195
+ recordBlindWalk();
768
196
  const activePath = getActiveRouteNames();
769
197
  const deepestRoute = activePath[activePath.length - 1];
770
198
  if (deepestRoute) snapshot.route = deepestRoute;
771
199
  mergeRegisteredLists(snapshot);
200
+ mergeRegisteredScrollables(snapshot);
201
+ mergeNativeDialog(snapshot);
772
202
  reconcileSliders(snapshot);
773
- snapshot.elements = deriveInteractionElements(snapshot);
774
- snapshot.choiceGroups = attachElementIdsToChoiceGroups(
775
- snapshot.choiceGroups,
776
- snapshot.elements
203
+ canonicalizeRegisteredFields(snapshot);
204
+ clampSnapshotToWireLimits(snapshot);
205
+ const entries = deriveInteractionElements(snapshot, { rectOf: rectForNode });
206
+ const rectById = new Map(
207
+ entries.flatMap((entry) => entry.rect ? [[entry.id, entry.rect]] : [])
777
208
  );
778
- elementRegistry.replaceAll(snapshot.elements);
779
- const elapsed = Date.now() - start;
780
- const totalListItems = snapshot.lists.reduce(
781
- (acc, l) => acc + l.items.length,
782
- 0
209
+ snapshot.elements = entries.map(toWireElement);
210
+ snapshot.choiceGroups = attachElementIdsToChoiceGroups(snapshot.choiceGroups, snapshot.elements);
211
+ clampSnapshotToWireLimits(snapshot);
212
+ elementRegistry.replaceAll(
213
+ snapshot.elements.map((element) => {
214
+ const rect = rectById.get(element.id);
215
+ return rect ? { ...element, rect } : element;
216
+ })
783
217
  );
218
+ const elapsed = Date.now() - start;
219
+ const totalListItems = snapshot.lists.reduce((acc, l) => acc + l.items.length, 0);
784
220
  const totalListDataItems = snapshot.lists.reduce(
785
221
  (acc, l) => acc + (typeof l.itemCount === "number" ? l.itemCount : 0),
786
222
  0
787
223
  );
788
- console.log(
789
- `[Appilots] captureSnapshot: route="${snapshot.route}" texts=${snapshot.texts.length} inputs=${snapshot.inputs.length} buttons=${snapshot.buttons.length} toggles=${snapshot.toggles.length} sliders=${snapshot.sliders.length} loading=${snapshot.loading} modal=${snapshot.modalOpen} lists=${snapshot.lists.length}(visibleItems=${totalListItems}, dataItems=${totalListDataItems}) choices=${snapshot.choiceGroups.length} elements=${snapshot.elements.length} (walked ${snapshot.stats?.visitedFibers ?? 0} fibers, skipped ${snapshot.stats?.skippedHidden ?? 0} hidden, ${elapsed}ms)`
224
+ appilotsDebugLog(
225
+ `captureSnapshot: route="${snapshot.route}" texts=${snapshot.texts.length} inputs=${snapshot.inputs.length} buttons=${snapshot.buttons.length} toggles=${snapshot.toggles.length} sliders=${snapshot.sliders.length} loading=${snapshot.loading} modal=${snapshot.modalOpen} lists=${snapshot.lists.length}(visibleItems=${totalListItems}, dataItems=${totalListDataItems}) choices=${snapshot.choiceGroups.length} elements=${snapshot.elements.length} ` + (snapshot.truncated ? "truncated=true " : "") + `(walked ${snapshot.stats?.visitedFibers ?? 0} fibers, skipped ${snapshot.stats?.skippedHidden ?? 0} hidden, ${elapsed}ms)`
790
226
  );
791
- console.log(
792
- `[Appilots] captureSnapshot detail: lists=[${snapshot.lists.map((l) => `${l.containerType}:${l.id ?? l.label ?? "?"}(${l.items.map((it) => it.texts[0] ?? it.buttons[0]?.id ?? "?").join("|")})`).join(", ")}] inputs=[${snapshot.inputs.map((i) => i.id ?? i.label ?? "?").slice(0, 12).join(", ")}] buttons=[${snapshot.buttons.map((b) => b.id ?? b.label ?? "?").slice(0, 15).join(", ")}]`
227
+ appilotsDebugLog(
228
+ `captureSnapshot detail: lists=[${snapshot.lists.map((l) => `${l.containerType}:${l.id ?? l.label ?? "?"}(rows=${l.items.length})`).join(", ")}] inputs=[${snapshot.inputs.map((i) => i.id ?? i.label ?? "?").slice(0, 12).join(", ")}] buttons=[${snapshot.buttons.map((b) => b.id ?? b.label ?? "?").slice(0, 15).join(", ")}]`
793
229
  );
794
230
  return snapshot;
795
231
  }
232
+ function registryCounts() {
233
+ const counts = {
234
+ fields: 0,
235
+ toggles: 0,
236
+ sliders: 0,
237
+ targets: 0,
238
+ lists: listRegistry.snapshot().length,
239
+ elements: elementRegistry.size
240
+ };
241
+ for (const { kind } of componentRegistry.snapshot()) {
242
+ if (kind === "field") counts.fields++;
243
+ else if (kind === "toggle") counts.toggles++;
244
+ else if (kind === "slider") counts.sliders++;
245
+ else if (kind === "target") counts.targets++;
246
+ }
247
+ return counts;
248
+ }
249
+ function appilotsSelfCheck() {
250
+ const root = getFiberRoot();
251
+ const start = Date.now();
252
+ if (root) walkFiber(root);
253
+ const elapsedMs = Date.now() - start;
254
+ const walk = getLastWalkDiagnostics();
255
+ return {
256
+ sdkVersion: SDK_VERSION,
257
+ reactVersion: React.version ?? null,
258
+ route: getCurrentScreen() || null,
259
+ introspection: getIntrospectionDiagnostics(),
260
+ autoTracking: getAutoTrackingState(),
261
+ walk: {
262
+ visitedFibers: root ? walk?.visitedFibers ?? 0 : 0,
263
+ skippedHidden: root ? walk?.skippedHidden ?? 0 : 0,
264
+ elapsedMs
265
+ },
266
+ detectors: root ? walk?.detectors ?? emptyDetectorCounts() : emptyDetectorCounts(),
267
+ registry: registryCounts(),
268
+ nameMangling: getNameManglingProbe(),
269
+ geometry: { availability: geometryAvailability(), ...geometryStats() }
270
+ };
271
+ }
796
272
 
797
273
  // src/introspection/findInteractive.ts
798
274
  function getTypeName2(type) {
@@ -806,7 +282,7 @@ function getTypeName2(type) {
806
282
  }
807
283
  return null;
808
284
  }
809
- function matchesName2(fiber, name) {
285
+ function matchesName(fiber, name) {
810
286
  return getTypeName2(fiber.type) === name || getTypeName2(fiber.elementType) === name;
811
287
  }
812
288
  var TOUCHABLE_NAMES2 = /* @__PURE__ */ new Set([
@@ -819,33 +295,33 @@ var TOUCHABLE_NAMES2 = /* @__PURE__ */ new Set([
819
295
  function isTouchable(fiber) {
820
296
  return TOUCHABLE_NAMES2.has(getTypeName2(fiber.type) ?? "") || TOUCHABLE_NAMES2.has(getTypeName2(fiber.elementType) ?? "");
821
297
  }
822
- function hasPressHandler2(fiber) {
298
+ function hasPressHandler(fiber) {
823
299
  const props = fiber?.memoizedProps ?? {};
824
300
  return typeof props.onPress === "function" || typeof props.onPressIn === "function";
825
301
  }
826
- function isPressableTarget2(fiber) {
827
- return isTouchable(fiber) || hasPressHandler2(fiber);
302
+ function isPressableTarget(fiber) {
303
+ return isTouchable(fiber) || hasPressHandler(fiber);
828
304
  }
829
- var SELF_REFERENTIAL_COMPONENTS2 = /* @__PURE__ */ new Set([
305
+ var SELF_REFERENTIAL_COMPONENTS = /* @__PURE__ */ new Set([
830
306
  "AppilotsChat",
831
307
  "AppilotsChatInner",
832
308
  "ActionBreadcrumb",
833
309
  "ConfirmDialog"
834
310
  ]);
835
- function isAppilotsSkipMarked2(props) {
311
+ function isAppilotsSkipMarked(props) {
836
312
  if (!props) return false;
837
313
  return props.__appilotsSkip === true || props["data-appilots-skip"] === true || props.appilotsSkip === true;
838
314
  }
839
- function extractTextFromChildren2(children) {
315
+ function extractTextFromChildren(children) {
840
316
  if (children == null) return "";
841
317
  if (typeof children === "string") return children;
842
318
  if (typeof children === "number") return String(children);
843
319
  if (Array.isArray(children)) {
844
- return children.map(extractTextFromChildren2).filter(Boolean).join("");
320
+ return children.map(extractTextFromChildren).filter(Boolean).join("");
845
321
  }
846
322
  return "";
847
323
  }
848
- function findChildText2(fiber) {
324
+ function findChildText(fiber) {
849
325
  const stack = [];
850
326
  if (fiber.child) stack.push(fiber.child);
851
327
  const visited = /* @__PURE__ */ new WeakSet();
@@ -853,8 +329,8 @@ function findChildText2(fiber) {
853
329
  const node = stack.pop();
854
330
  if (!node || visited.has(node)) continue;
855
331
  visited.add(node);
856
- if (matchesName2(node, "Text")) {
857
- const text = extractTextFromChildren2(node.memoizedProps?.children);
332
+ if (matchesName(node, "Text")) {
333
+ const text = extractTextFromChildren(node.memoizedProps?.children);
858
334
  if (text && text.trim().length > 0) return text.trim();
859
335
  }
860
336
  if (node.sibling) stack.push(node.sibling);
@@ -866,10 +342,10 @@ function shouldSkipSubtree2(fiber) {
866
342
  const props = fiber.memoizedProps;
867
343
  if (!props) return false;
868
344
  const typeName = getTypeName2(fiber.type) ?? getTypeName2(fiber.elementType);
869
- if (typeName && SELF_REFERENTIAL_COMPONENTS2.has(typeName)) return true;
870
- if (isAppilotsSkipMarked2(props)) return true;
871
- if (matchesName2(fiber, "Modal") && props.visible === false) return true;
872
- if (matchesName2(fiber, "Screen") && props.activityState === 0) return true;
345
+ if (typeName && SELF_REFERENTIAL_COMPONENTS.has(typeName)) return true;
346
+ if (isAppilotsSkipMarked(props)) return true;
347
+ if (isModalFiber(fiber) && props.visible === false) return true;
348
+ if (matchesName(fiber, "Screen") && props.activityState === 0) return true;
873
349
  const style = props.style;
874
350
  if (style) {
875
351
  const flat = Array.isArray(style) ? Object.assign({}, ...style.filter(Boolean)) : style;
@@ -878,20 +354,19 @@ function shouldSkipSubtree2(fiber) {
878
354
  }
879
355
  return false;
880
356
  }
881
- function normalize(s) {
882
- if (!s) return "";
883
- return s.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]/g, "");
884
- }
357
+ var normalize = foldForCompare;
885
358
  function scoreMatch(fiber, queryNorm, queryRaw) {
886
359
  const props = fiber.memoizedProps ?? {};
887
360
  const candidates = [];
888
361
  if (typeof props.testID === "string") candidates.push({ val: props.testID, weight: 100 });
889
- if (typeof props.accessibilityLabel === "string") candidates.push({ val: props.accessibilityLabel, weight: 95 });
362
+ if (typeof props.accessibilityLabel === "string")
363
+ candidates.push({ val: props.accessibilityLabel, weight: 95 });
890
364
  if (typeof props.label === "string") candidates.push({ val: props.label, weight: 90 });
891
- if (typeof props.placeholder === "string") candidates.push({ val: props.placeholder, weight: 70 });
365
+ if (typeof props.placeholder === "string")
366
+ candidates.push({ val: props.placeholder, weight: 70 });
892
367
  if (typeof props.title === "string") candidates.push({ val: props.title, weight: 85 });
893
- if (isPressableTarget2(fiber)) {
894
- const childText = findChildText2(fiber);
368
+ if (isPressableTarget(fiber)) {
369
+ const childText = findChildText(fiber);
895
370
  if (childText) candidates.push({ val: childText, weight: 80 });
896
371
  }
897
372
  let best = 0;
@@ -900,7 +375,8 @@ function scoreMatch(fiber, queryNorm, queryRaw) {
900
375
  if (!vNorm) continue;
901
376
  if (val === queryRaw) best = Math.max(best, weight + 20);
902
377
  else if (vNorm === queryNorm) best = Math.max(best, weight + 10);
903
- else if (vNorm.includes(queryNorm) || queryNorm.includes(vNorm)) best = Math.max(best, weight - 20);
378
+ else if (queryNorm && (vNorm.includes(queryNorm) || queryNorm.includes(vNorm)))
379
+ best = Math.max(best, weight - 20);
904
380
  }
905
381
  if (best > 0 && props.disabled === true) {
906
382
  best = Math.max(1, best - 60);
@@ -931,6 +407,12 @@ function wrapTextInput(fiber) {
931
407
  if (node && typeof node.focus === "function") {
932
408
  node.focus();
933
409
  }
410
+ },
411
+ submitFromKeyboard: () => {
412
+ const { onSubmitEditing, value } = getProps();
413
+ if (typeof onSubmitEditing !== "function") return false;
414
+ onSubmitEditing({ nativeEvent: { text: value ?? "" } });
415
+ return true;
934
416
  }
935
417
  };
936
418
  }
@@ -970,14 +452,14 @@ function wrapTouchable(fiber, label) {
970
452
  function findInteractiveByIdentifier(query, options = {}) {
971
453
  const root = getFiberRoot();
972
454
  if (!root) {
973
- console.warn("[Appilots] findInteractiveByIdentifier: fiber root not captured yet");
455
+ appilotsDebugWarn("findInteractiveByIdentifier: fiber root not captured yet");
974
456
  return null;
975
457
  }
976
458
  const queryNorm = normalize(query);
977
459
  const queryRaw = query;
978
460
  let bestScore = 0;
979
461
  let bestHandle = null;
980
- let bestId2;
462
+ let bestId;
981
463
  const visited = /* @__PURE__ */ new WeakSet();
982
464
  const stack = [];
983
465
  if (root.child) stack.push(root.child);
@@ -988,40 +470,50 @@ function findInteractiveByIdentifier(query, options = {}) {
988
470
  if (fiber.sibling) stack.push(fiber.sibling);
989
471
  if (shouldSkipSubtree2(fiber)) continue;
990
472
  let kind = null;
991
- if (matchesName2(fiber, "TextInput")) kind = "field";
992
- else if (matchesName2(fiber, "Switch")) kind = "toggle";
993
- else if (isPressableTarget2(fiber)) kind = "target";
473
+ if (matchesName(fiber, "TextInput")) kind = "field";
474
+ else if (matchesName(fiber, "Switch")) kind = "toggle";
475
+ else if (isPressableTarget(fiber)) kind = "target";
994
476
  if (kind && (!options.kind || options.kind === kind)) {
995
477
  const score = scoreMatch(fiber, queryNorm, queryRaw);
996
478
  if (score > bestScore) {
997
479
  bestScore = score;
998
480
  const props = fiber.memoizedProps ?? {};
999
- bestId2 = props.testID ?? props.accessibilityLabel ?? props.placeholder ?? (kind === "target" ? findChildText2(fiber) : void 0);
481
+ bestId = props.testID ?? props.accessibilityLabel ?? props.placeholder ?? (kind === "target" ? findChildText(fiber) : void 0);
1000
482
  if (kind === "field") bestHandle = wrapTextInput(fiber);
1001
483
  else if (kind === "toggle") bestHandle = wrapSwitch(fiber);
1002
- else bestHandle = wrapTouchable(fiber, findChildText2(fiber));
484
+ else bestHandle = wrapTouchable(fiber, findChildText(fiber));
1003
485
  }
1004
486
  }
1005
487
  if (fiber.child) stack.push(fiber.child);
1006
488
  }
1007
489
  if (bestHandle) {
1008
- console.log(
1009
- `[Appilots] findInteractiveByIdentifier: query="${query}" \u2192 kind=${bestHandle.kind} label="${bestHandle.label ?? bestId2}" score=${bestScore}`
490
+ appilotsDebugLog(
491
+ `findInteractiveByIdentifier: query="${query}" \u2192 kind=${bestHandle.kind} label="${bestHandle.label ?? bestId}" score=${bestScore}`
1010
492
  );
1011
493
  } else {
1012
- console.log(`[Appilots] findInteractiveByIdentifier: query="${query}" \u2192 no match`);
494
+ appilotsDebugLog(`findInteractiveByIdentifier: query="${query}" \u2192 no match`);
1013
495
  }
1014
496
  return bestHandle;
1015
497
  }
1016
498
 
1017
499
  // src/executor/handlers/navigateHandler.ts
500
+ var _reportedMissingNavRef = false;
501
+ function reportMissingNavigationRef() {
502
+ if (_reportedMissingNavRef) return;
503
+ _reportedMissingNavRef = true;
504
+ console.error(
505
+ "[Appilots] Navigation is not wired. The SDK never received a navigation ref, so every navigate the agent attempts will fail.\n Fix: wrap your NavigationContainer with <AppilotsNavigationContainer>:\n <AppilotsProvider config={...}>\n <AppilotsNavigationContainer>\n <NavigationContainer>{/* your navigators */}</NavigationContainer>\n </AppilotsNavigationContainer>\n <AppilotsChat />\n </AppilotsProvider>\n Docs: https://docs.appilots.com/getting-started/quick-start"
506
+ );
507
+ }
1018
508
  function categorizeNavigationError(message) {
1019
509
  if (/screen named|was not handled by any navigator|no route|not found|not registered in any navigator|couldn'?t find a route|route .* (?:doesn'?t|does not) exist/i.test(
1020
510
  message
1021
511
  )) {
1022
512
  return "component-not-found";
1023
513
  }
1024
- if (/couldn'?t find a navigation object|navigator (?:is )?not (?:rendered|ready)|not been (?:mounted|rendered)/i.test(message)) {
514
+ if (/couldn'?t find a navigation object|navigator (?:is )?not (?:rendered|ready)|not been (?:mounted|rendered)/i.test(
515
+ message
516
+ )) {
1025
517
  return "screen-timeout";
1026
518
  }
1027
519
  return "unknown";
@@ -1048,7 +540,9 @@ function screenExistsAnywhere(state, screenName) {
1048
540
  function safeNavigate(nav, screenName, params, path) {
1049
541
  if (path && path.length > 0) {
1050
542
  const { root, nestedParams } = buildNestedParams(path, screenName, params);
1051
- console.log(`[Appilots] safeNavigate: using path-based navigation \u2014 root="${root}", nested=${JSON.stringify(nestedParams)}`);
543
+ appilotsDebugLog(
544
+ `safeNavigate: using path-based navigation \u2014 root="${root}", nested=${JSON.stringify(nestedParams)}`
545
+ );
1052
546
  nav.navigate(root, nestedParams);
1053
547
  return;
1054
548
  }
@@ -1063,11 +557,17 @@ function safeNavigate(nav, screenName, params, path) {
1063
557
  `Screen "${screenName}" is not registered in any navigator. The screen may have been removed, may live inside a flow that is not currently mounted (e.g. Auth stack after login), or the MCP doc is stale.`
1064
558
  );
1065
559
  }
1066
- console.log(`[Appilots] safeNavigate: screen exists in nested navigator, attempting direct navigation to "${screenName}"`);
560
+ appilotsDebugLog(
561
+ `safeNavigate: screen exists in nested navigator, attempting direct navigation to "${screenName}"`
562
+ );
1067
563
  nav.navigate(screenName, params);
1068
564
  }
1069
565
  function navigateHandler(payload, context) {
1070
- console.log(`[Appilots] navigateHandler called: screenName="${payload.screenName}", action="${payload.navigationAction}", params=${JSON.stringify(payload.params ?? {})}`);
566
+ appilotsDebugLog(
567
+ // Params carry record ids and, on some screens, the user's own
568
+ // data — the count answers "were params passed?" and nothing else.
569
+ `navigateHandler called: screenName="${payload.screenName}" action="${payload.navigationAction}" params=${describeValue(payload.params)}`
570
+ );
1071
571
  const { navigationRef, permissions } = context;
1072
572
  const nav = navigationRef.current;
1073
573
  if (!permissions.canNavigate) {
@@ -1077,14 +577,15 @@ function navigateHandler(payload, context) {
1077
577
  diagnose: { category: "unknown", screen: payload.screenName }
1078
578
  };
1079
579
  }
1080
- if (permissions.blockedScreens?.includes(payload.screenName)) {
580
+ const isGoBack = payload.navigationAction === "goBack";
581
+ if (!isGoBack && permissions.blockedScreens?.includes(payload.screenName)) {
1081
582
  return {
1082
583
  success: false,
1083
584
  error: `Navigation to "${payload.screenName}" is blocked`,
1084
585
  diagnose: { category: "unknown", screen: payload.screenName }
1085
586
  };
1086
587
  }
1087
- if (permissions.allowedScreens && permissions.allowedScreens.length > 0 && !permissions.allowedScreens.includes(payload.screenName)) {
588
+ if (!isGoBack && permissions.allowedScreens && permissions.allowedScreens.length > 0 && !permissions.allowedScreens.includes(payload.screenName)) {
1088
589
  return {
1089
590
  success: false,
1090
591
  error: `Navigation to "${payload.screenName}" is not in allowed screens`,
@@ -1092,10 +593,11 @@ function navigateHandler(payload, context) {
1092
593
  };
1093
594
  }
1094
595
  if (!nav) {
596
+ reportMissingNavigationRef();
1095
597
  return {
1096
598
  success: false,
1097
- error: "Navigation ref is not available",
1098
- diagnose: { category: "screen-timeout", screen: payload.screenName }
599
+ error: "Navigation is not wired: <AppilotsNavigationContainer> is missing, so the SDK never received the navigation ref. Wrap your <NavigationContainer> with it. This cannot be retried \u2014 it needs a change in the host app.",
600
+ diagnose: { category: "unknown", screen: payload.screenName }
1099
601
  };
1100
602
  }
1101
603
  if (!nav.isReady || typeof nav.isReady === "function" && !nav.isReady()) {
@@ -1106,7 +608,15 @@ function navigateHandler(payload, context) {
1106
608
  };
1107
609
  }
1108
610
  try {
1109
- const { screenName, params, navigationAction, path } = payload;
611
+ const { params, navigationAction, path } = payload;
612
+ if (!isGoBack && !payload.screenName) {
613
+ return {
614
+ success: false,
615
+ error: `Navigation action "${navigationAction ?? "navigate"}" requires a screenName.`,
616
+ diagnose: { category: "unknown" }
617
+ };
618
+ }
619
+ const screenName = payload.screenName;
1110
620
  switch (navigationAction) {
1111
621
  case "navigate":
1112
622
  safeNavigate(nav, screenName, params ?? {}, path);
@@ -1149,10 +659,14 @@ function navigateHandler(payload, context) {
1149
659
  diagnose: { category: "unknown", screen: payload.screenName }
1150
660
  };
1151
661
  }
1152
- console.log(`[Appilots] navigateHandler: navigation succeeded for "${payload.screenName}" (action="${payload.navigationAction}")`);
662
+ appilotsDebugLog(
663
+ `navigateHandler: navigation succeeded for "${payload.screenName}" (action="${payload.navigationAction}")`
664
+ );
1153
665
  return { success: true };
1154
666
  } catch (err) {
1155
- console.warn(`[Appilots] navigateHandler: navigation failed for "${payload.screenName}": ${err?.message}`);
667
+ appilotsDebugWarn(
668
+ `navigateHandler: navigation failed for "${payload.screenName}": ${err?.message}`
669
+ );
1156
670
  const msg = String(err?.message ?? "Navigation failed");
1157
671
  const category = categorizeNavigationError(msg);
1158
672
  return {
@@ -1175,7 +689,7 @@ function parseSyntheticListItemId(id) {
1175
689
  if (listIndex < 0 || itemIndex < 1) return null;
1176
690
  return { listIndex, itemIndex };
1177
691
  }
1178
- function stringProp2(props, key) {
692
+ function stringProp(props, key) {
1179
693
  const value = props?.[key];
1180
694
  if (typeof value === "string" && value.trim()) return value.trim();
1181
695
  if (typeof value === "number") return String(value);
@@ -1183,30 +697,30 @@ function stringProp2(props, key) {
1183
697
  }
1184
698
  function listIdFromContainer(container) {
1185
699
  const props = container?.memoizedProps ?? {};
1186
- return stringProp2(props, "__appilotsListId") ?? stringProp2(props, "appilotsListId") ?? stringProp2(props, "testID") ?? stringProp2(props, "accessibilityLabel");
700
+ return stringProp(props, "__appilotsListId") ?? stringProp(props, "appilotsListId") ?? stringProp(props, "testID") ?? stringProp(props, "accessibilityLabel");
1187
701
  }
1188
702
  function listLabelFromContainer(container) {
1189
703
  const props = container?.memoizedProps ?? {};
1190
- return stringProp2(props, "__appilotsListLabel") ?? stringProp2(props, "accessibilityLabel") ?? stringProp2(props, "aria-label") ?? stringProp2(props, "label");
704
+ return stringProp(props, "__appilotsListLabel") ?? stringProp(props, "accessibilityLabel") ?? stringProp(props, "aria-label") ?? stringProp(props, "label");
1191
705
  }
1192
706
  function itemKeyFromFiber(fiber) {
1193
707
  const props = fiber?.memoizedProps ?? {};
1194
- return stringProp2(props, "__appilotsItemKey") ?? (fiber?.key !== void 0 && fiber?.key !== null ? String(fiber.key) : void 0);
708
+ return stringProp(props, "__appilotsItemKey") ?? (fiber?.key !== void 0 && fiber?.key !== null ? String(fiber.key) : void 0);
1195
709
  }
1196
710
  function markedItemIndexFromFiber(fiber) {
1197
711
  const value = fiber?.memoizedProps?.__appilotsItemIndex;
1198
712
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
1199
713
  }
1200
714
  function normalize2(value) {
1201
- return String(value ?? "").toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "");
715
+ return foldForCompare(String(value ?? ""));
1202
716
  }
1203
- function hasPressHandler3(fiber) {
717
+ function hasPressHandler2(fiber) {
1204
718
  const props = fiber?.memoizedProps ?? {};
1205
719
  return typeof props.onPress === "function" || typeof props.onPressIn === "function";
1206
720
  }
1207
721
  function isPressableFiber(fiber) {
1208
722
  const typeName = getTypeName(fiber.type) ?? getTypeName(fiber.elementType) ?? "";
1209
- return TOUCHABLE_NAMES.has(typeName) || hasPressHandler3(fiber);
723
+ return TOUCHABLE_NAMES.has(typeName) || hasPressHandler2(fiber);
1210
724
  }
1211
725
  function findListContainers(options = {}) {
1212
726
  const root = getFiberRoot();
@@ -1351,13 +865,10 @@ function collectTexts(fiber) {
1351
865
  }
1352
866
  function hasUsefulTouchableIdentity(fiber) {
1353
867
  const props = fiber?.memoizedProps ?? {};
1354
- if (typeof props.testID === "string" && /[a-z0-9]/i.test(props.testID)) return true;
1355
- if (typeof props.accessibilityLabel === "string" && /[a-z0-9]/i.test(props.accessibilityLabel)) {
1356
- return true;
1357
- }
1358
- if (typeof props.label === "string" && /[a-z0-9]/i.test(props.label)) return true;
1359
- const label = findFirstText(fiber);
1360
- return !!label && /[a-z0-9]/i.test(label);
868
+ if (isReferenceable(props.testID)) return true;
869
+ if (isReferenceable(props.accessibilityLabel)) return true;
870
+ if (isReferenceable(props.label)) return true;
871
+ return isReferenceable(findFirstText(fiber));
1361
872
  }
1362
873
  function describeTouchable(fiber) {
1363
874
  const props = fiber?.memoizedProps ?? {};
@@ -1370,12 +881,7 @@ function touchableLooksDestructive(fiber) {
1370
881
  const descriptor = describeTouchable(fiber);
1371
882
  if (descriptor && looksDestructiveActionLabel(descriptor)) return true;
1372
883
  const props = fiber?.memoizedProps ?? {};
1373
- const haystack = [
1374
- props.testID,
1375
- props.accessibilityLabel,
1376
- props.accessibilityHint,
1377
- props.label
1378
- ].filter((value) => typeof value === "string").join(" ");
884
+ const haystack = [props.testID, props.accessibilityLabel, props.accessibilityHint, props.label].filter((value) => typeof value === "string").join(" ");
1379
885
  return looksDestructiveActionLabel(haystack);
1380
886
  }
1381
887
  function describeItemCandidate(listIndex, zeroBasedIndex, item) {
@@ -1409,8 +915,8 @@ function pressResolvedListItem(list, itemFiber, listIndex, itemIndex) {
1409
915
  (candidate) => candidate !== touchable && containsFiber(touchable, candidate)
1410
916
  );
1411
917
  if (primaryWrapsNestedTargets) {
1412
- console.log(
1413
- `[Appilots] listItemDispatch: item "list-${listIndex}-item-${itemIndex}" has nested pressables; pressing outer row target`
918
+ appilotsDebugLog(
919
+ `listItemDispatch: item "list-${listIndex}-item-${itemIndex}" has nested pressables; pressing outer row target`
1414
920
  );
1415
921
  } else {
1416
922
  const candidates = safeIdentifiableTouchables.map(describeTouchable).filter((d) => !!d).slice(0, 8);
@@ -1520,14 +1026,14 @@ function pressListItemAtOrdinal(listIndex, itemIndex) {
1520
1026
  }
1521
1027
  return pressResolvedListItem(list, items[zeroBased], listIndex, itemIndex);
1522
1028
  }
1523
- function pressListItemByIdentity(locator) {
1524
- if (!getFiberRoot()) return { ok: false, reason: "no-fiber-root" };
1029
+ function resolveListItemByIdentity(locator) {
1030
+ if (!getFiberRoot()) return { ok: false, result: { ok: false, reason: "no-fiber-root" } };
1525
1031
  const parsed = locator.syntheticId ? parseSyntheticListItemId(locator.syntheticId) : null;
1526
1032
  const effective = parsed ? { ...locator, listIndex: parsed.listIndex, itemIndex: parsed.itemIndex } : locator;
1527
1033
  const containers = findListContainers({ allowSingleHeuristic: true });
1528
1034
  const lists = candidateLists(containers, effective);
1529
1035
  if (lists.length === 0) {
1530
- return { ok: false, reason: "list-not-found" };
1036
+ return { ok: false, result: { ok: false, reason: "list-not-found" } };
1531
1037
  }
1532
1038
  const scoredCandidates = [];
1533
1039
  for (const { list, listIndex } of lists) {
@@ -1538,7 +1044,14 @@ function pressListItemByIdentity(locator) {
1538
1044
  }
1539
1045
  }
1540
1046
  if (scoredCandidates.length === 0) {
1541
- return { ok: false, reason: "item-not-found", containerType: lists[0]?.list.containerType };
1047
+ return {
1048
+ ok: false,
1049
+ result: {
1050
+ ok: false,
1051
+ reason: "item-not-found",
1052
+ containerType: lists[0]?.list.containerType
1053
+ }
1054
+ };
1542
1055
  }
1543
1056
  scoredCandidates.sort((a, b) => b.score - a.score);
1544
1057
  const best = scoredCandidates[0];
@@ -1547,13 +1060,104 @@ function pressListItemByIdentity(locator) {
1547
1060
  const candidates = tiedCandidates.map((c) => describeItemCandidate(c.listIndex, c.itemIndex - 1, c.item)).slice(0, 8);
1548
1061
  return {
1549
1062
  ok: false,
1550
- reason: "ambiguous-items",
1551
- error: "Multiple visible list items matched the requested target with equal confidence." + (candidates.length > 0 ? ` Candidates: ${candidates.join(", ")}.` : ""),
1552
- containerType: best.list.containerType,
1063
+ result: {
1064
+ ok: false,
1065
+ reason: "ambiguous-items",
1066
+ error: "Multiple visible list items matched the requested target with equal confidence." + (candidates.length > 0 ? ` Candidates: ${candidates.join(", ")}.` : ""),
1067
+ containerType: best.list.containerType,
1068
+ candidates
1069
+ }
1070
+ };
1071
+ }
1072
+ return {
1073
+ ok: true,
1074
+ list: best.list,
1075
+ item: best.item,
1076
+ listIndex: best.listIndex,
1077
+ itemIndex: best.itemIndex
1078
+ };
1079
+ }
1080
+ function findSwitchFibers(item) {
1081
+ if (!item) return [];
1082
+ const found = [];
1083
+ const visited = /* @__PURE__ */ new WeakSet();
1084
+ const stack = [
1085
+ { fiber: item, includeSibling: false }
1086
+ ];
1087
+ while (stack.length > 0) {
1088
+ const { fiber, includeSibling } = stack.pop();
1089
+ if (!fiber || visited.has(fiber)) continue;
1090
+ visited.add(fiber);
1091
+ if (shouldSkipSubtree(fiber)) continue;
1092
+ const props = fiber.memoizedProps ?? {};
1093
+ if (isSwitchLike(fiber, props)) {
1094
+ if (props.__appilotsInternal !== true) found.push(fiber);
1095
+ if (includeSibling && fiber.sibling) {
1096
+ stack.push({ fiber: fiber.sibling, includeSibling: true });
1097
+ }
1098
+ continue;
1099
+ }
1100
+ if (includeSibling && fiber.sibling) {
1101
+ stack.push({ fiber: fiber.sibling, includeSibling: true });
1102
+ }
1103
+ if (fiber.child) stack.push({ fiber: fiber.child, includeSibling: true });
1104
+ }
1105
+ return found;
1106
+ }
1107
+ function toggleListItemByIdentity(locator, options) {
1108
+ const resolved = resolveListItemByIdentity(locator);
1109
+ if (!resolved.ok) return resolved.result;
1110
+ const { list, item } = resolved;
1111
+ const switches = findSwitchFibers(item);
1112
+ if (switches.length === 0) {
1113
+ return { ok: false, reason: "no-touchable", containerType: list.containerType };
1114
+ }
1115
+ if (switches.length > 1) {
1116
+ const candidates = switches.map((fiber, index) => describeTouchable(fiber) ?? `switch-${index + 1}`).slice(0, 8);
1117
+ return {
1118
+ ok: false,
1119
+ reason: "ambiguous-touchables",
1120
+ error: 'This row exposes more than one switch, so flipping "the" switch would be a guess.' + (candidates.length > 0 ? ` Candidates: ${candidates.join(", ")}.` : ""),
1121
+ containerType: list.containerType,
1553
1122
  candidates
1554
1123
  };
1555
1124
  }
1556
- return pressResolvedListItem(best.list, best.item, best.listIndex, best.itemIndex);
1125
+ const target = switches[0];
1126
+ const props = target?.memoizedProps ?? {};
1127
+ const current = switchValue(props);
1128
+ if (options?.expected !== void 0 && current === options.expected) {
1129
+ return { ok: true, containerType: list.containerType, value: current };
1130
+ }
1131
+ const onValueChange = props.onValueChange ?? props.onChange;
1132
+ if (typeof onValueChange !== "function") {
1133
+ return {
1134
+ ok: false,
1135
+ reason: "no-touchable",
1136
+ error: "The row has a switch but it exposes no onValueChange handler.",
1137
+ containerType: list.containerType
1138
+ };
1139
+ }
1140
+ try {
1141
+ onValueChange(!current);
1142
+ } catch (err) {
1143
+ return {
1144
+ ok: false,
1145
+ reason: "press-threw",
1146
+ error: err?.message ?? "switch handler threw",
1147
+ containerType: list.containerType
1148
+ };
1149
+ }
1150
+ return { ok: true, containerType: list.containerType, value: !current };
1151
+ }
1152
+ function pressListItemByIdentity(locator) {
1153
+ const resolved = resolveListItemByIdentity(locator);
1154
+ if (!resolved.ok) return resolved.result;
1155
+ return pressResolvedListItem(
1156
+ resolved.list,
1157
+ resolved.item,
1158
+ resolved.listIndex,
1159
+ resolved.itemIndex
1160
+ );
1557
1161
  }
1558
1162
 
1559
1163
  // src/executor/handlers/elementDispatch.ts
@@ -1782,6 +1386,46 @@ function dispatchElement(element, registry, action) {
1782
1386
  };
1783
1387
  }
1784
1388
  if (element.role === "toggle" && (action === "toggle" || isPressLike(action))) {
1389
+ if (element.listContext) {
1390
+ const toggled = toggleListItemByIdentity(
1391
+ {
1392
+ listIndex: element.listContext.listIndex,
1393
+ listId: element.listContext.listId,
1394
+ listLabel: element.listContext.listLabel,
1395
+ itemIndex: element.listContext.itemIndex,
1396
+ itemKey: element.listContext.itemKey,
1397
+ reactKey: element.listContext.reactKey,
1398
+ syntheticId: element.listContext.syntheticId,
1399
+ label: element.label,
1400
+ texts: element.texts
1401
+ },
1402
+ // The observation's `selected` is the state BEFORE this action.
1403
+ // Passing the desired end state makes the flip idempotent: a
1404
+ // retry or a recovery hop must not undo the work and call it
1405
+ // success. A toggle only knows how to invert, so without this the
1406
+ // second attempt silently returns the row to where it started.
1407
+ typeof element.selected === "boolean" ? { expected: !element.selected } : void 0
1408
+ );
1409
+ if (toggled.ok) {
1410
+ return {
1411
+ success: true,
1412
+ // Grounded: read back off the fiber after the handler ran, not
1413
+ // inferred from "the call did not throw".
1414
+ effect: "changed"
1415
+ };
1416
+ }
1417
+ return {
1418
+ success: false,
1419
+ error: toggled.error ?? `Could not flip the switch in row "${element.label ?? element.id}" (${toggled.reason})`,
1420
+ diagnose: {
1421
+ ...screenDiagnose(
1422
+ element.id,
1423
+ toggled.reason === "ambiguous-touchables" || toggled.reason === "ambiguous-items" ? "ambiguous-target" : "component-not-found"
1424
+ ),
1425
+ ...toggled.candidates && toggled.candidates.length > 0 ? { candidates: toggled.candidates } : {}
1426
+ }
1427
+ };
1428
+ }
1785
1429
  const targetId = element.targetId ?? element.label ?? element.id;
1786
1430
  return dispatchLegacyTarget(targetId, registry, "toggle");
1787
1431
  }
@@ -1811,8 +1455,8 @@ function dispatchStructuredElementId(identifier, action) {
1811
1455
  texts: parsed.itemKey ? [parsed.itemKey] : void 0
1812
1456
  });
1813
1457
  if (pressed.ok) {
1814
- console.log(
1815
- `[Appilots] elementDispatch: resolved stale el id "${identifier}" via structural identity (listId="${parsed.listId}", itemKey="${parsed.itemKey}")`
1458
+ appilotsDebugLog(
1459
+ `elementDispatch: resolved stale el id "${identifier}" via structural identity (listId="${parsed.listId}", itemKey="${parsed.itemKey}")`
1816
1460
  );
1817
1461
  return { success: true };
1818
1462
  }
@@ -1836,7 +1480,6 @@ function pressElementOrTarget(identifier, registry) {
1836
1480
 
1837
1481
  // src/executor/formFieldRewrite.ts
1838
1482
  var LIST_ORDINAL_RE = /^list-\d+-item-\d+$/i;
1839
- var PLATE_RE = /^[A-Z]{3}-?\d[A-Z0-9]\d{2}$/i;
1840
1483
  function normalize4(value) {
1841
1484
  return value.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]/g, "");
1842
1485
  }
@@ -1876,9 +1519,6 @@ function collectKnownFormFields(registry) {
1876
1519
  }
1877
1520
  return registryFields;
1878
1521
  }
1879
- function fieldHaystack(field) {
1880
- return normalize4(`${field.id} ${field.label ?? ""}`);
1881
- }
1882
1522
  function isMisplacedFieldId(fieldId, knownIds) {
1883
1523
  const trimmed = fieldId.trim();
1884
1524
  if (!trimmed) return true;
@@ -1902,50 +1542,68 @@ function valueMatchesOption(field, rawValue) {
1902
1542
  return label === value || optionValue === value || value.includes(label) || value.includes(optionValue);
1903
1543
  });
1904
1544
  }
1545
+ var NUMERIC_FIELD_TYPES = /* @__PURE__ */ new Set([
1546
+ "number",
1547
+ "numeric",
1548
+ "integer",
1549
+ "int",
1550
+ "decimal",
1551
+ "float",
1552
+ "range",
1553
+ "slider",
1554
+ "tel",
1555
+ "phone"
1556
+ ]);
1557
+ var DATE_FIELD_TYPES = /* @__PURE__ */ new Set(["date", "datetime", "datetimelocal", "time", "month"]);
1558
+ var EMAIL_FIELD_TYPES = /* @__PURE__ */ new Set(["email"]);
1559
+ var CHOICE_FIELD_TYPES = /* @__PURE__ */ new Set([
1560
+ "select",
1561
+ "picker",
1562
+ "dropdown",
1563
+ "combobox",
1564
+ "radio",
1565
+ "checkbox",
1566
+ "switch",
1567
+ "toggle",
1568
+ "segmented",
1569
+ "boolean"
1570
+ ]);
1571
+ function readValueShape(rawValue) {
1572
+ const value = String(rawValue ?? "").trim();
1573
+ if (!value) return "text";
1574
+ if (/^-?\d+(?:[.,]\d+)?$/.test(value)) return "number";
1575
+ if (/^\S+@\S+\.\S+$/.test(value)) return "email";
1576
+ if (/^\d{4}-\d{2}-\d{2}(?:[T\s]\d{2}:\d{2}(?::\d{2})?)?$/.test(value)) return "date";
1577
+ if (/^\d{1,2}[/-]\d{1,2}[/-]\d{2,4}$/.test(value)) return "date";
1578
+ return "text";
1579
+ }
1580
+ function fieldTypeMatchesShape(field, shape) {
1581
+ const type = normalize4(field.type ?? "text");
1582
+ if (CHOICE_FIELD_TYPES.has(type)) return false;
1583
+ if (shape === "number") return NUMERIC_FIELD_TYPES.has(type);
1584
+ if (shape === "date") return DATE_FIELD_TYPES.has(type);
1585
+ if (shape === "email") return EMAIL_FIELD_TYPES.has(type);
1586
+ return false;
1587
+ }
1588
+ function fieldAcceptsFreeText(field, shape) {
1589
+ const type = normalize4(field.type ?? "text");
1590
+ if (CHOICE_FIELD_TYPES.has(type)) return false;
1591
+ if (NUMERIC_FIELD_TYPES.has(type)) return shape === "number";
1592
+ if (DATE_FIELD_TYPES.has(type)) return shape === "date";
1593
+ if (EMAIL_FIELD_TYPES.has(type)) return shape === "email";
1594
+ return true;
1595
+ }
1905
1596
  function inferFieldIdForValue(rawValue, fields, used) {
1906
1597
  const value = String(rawValue ?? "").trim();
1907
1598
  if (!value) return void 0;
1908
- const normalizedValue = normalize4(value);
1909
- for (const field of fields) {
1910
- if (used.has(field.id)) continue;
1599
+ const available = fields.filter((field) => field.id && !used.has(field.id));
1600
+ for (const field of available) {
1911
1601
  if (valueMatchesOption(field, value)) return field.id;
1912
1602
  }
1913
- if (PLATE_RE.test(value)) {
1914
- const field = fields.find((f) => /placa|plate/.test(fieldHaystack(f)) && !used.has(f.id));
1915
- if (field) return field.id;
1916
- }
1917
- if (/^(19|20)\d{2}$/.test(value)) {
1918
- const field = fields.find((f) => /ano|year/.test(fieldHaystack(f)) && !used.has(f.id));
1919
- if (field) return field.id;
1920
- }
1921
- if (/^\d+$/.test(value)) {
1922
- const mileage = fields.find((f) => /quilometragem|mileage|km/.test(fieldHaystack(f)) && !used.has(f.id));
1923
- if (mileage) return mileage.id;
1924
- const year = fields.find((f) => /ano|year/.test(fieldHaystack(f)) && !used.has(f.id));
1925
- if (year) return year.id;
1926
- }
1927
- if (/preto|branco|azul|vermelho|prata|black|white|red|blue|silver|gray|grey/.test(normalizedValue)) {
1928
- const cor = fields.find((f) => /cor|color/.test(fieldHaystack(f)) && !used.has(f.id));
1929
- if (cor) return cor.id;
1930
- }
1931
- const marca = fields.find((f) => /marca|brand|make/.test(fieldHaystack(f)) && !used.has(f.id));
1932
- if (marca && !/model/i.test(normalizedValue)) return marca.id;
1933
- const modelo = fields.find((f) => /modelo|model/.test(fieldHaystack(f)) && !used.has(f.id));
1934
- if (modelo) return modelo.id;
1935
- const tipo = fields.find((f) => /\btipo\b|type|vehicletype/.test(fieldHaystack(f)) && !used.has(f.id));
1936
- if (tipo && /carro|car|moto|truck|van|flex|gasoline|diesel|hybrid|electric|el[eé]trico/.test(normalizedValue)) {
1937
- return tipo.id;
1938
- }
1939
- const combustivel = fields.find((f) => /combustivel|fuel/.test(fieldHaystack(f)) && !used.has(f.id));
1940
- if (combustivel && /flex|gasoline|diesel|hybrid|electric|el[eé]trico/.test(normalizedValue)) {
1941
- return combustivel.id;
1942
- }
1943
- const nextText = fields.find((field) => {
1944
- if (used.has(field.id)) return false;
1945
- const type = normalize4(field.type ?? "text");
1946
- return type === "text" || type === "input" || type === "number" || type === "";
1947
- });
1948
- return nextText?.id;
1603
+ const shape = readValueShape(value);
1604
+ const typed = available.find((field) => fieldTypeMatchesShape(field, shape));
1605
+ if (typed) return typed.id;
1606
+ return available.find((field) => fieldAcceptsFreeText(field, shape))?.id;
1949
1607
  }
1950
1608
  function rewriteMisplacedFormFillFields(fields, registry) {
1951
1609
  const knownFields = collectKnownFormFields(registry);
@@ -1970,28 +1628,36 @@ function rewriteMisplacedFormFillFields(fields, registry) {
1970
1628
  }
1971
1629
 
1972
1630
  // src/executor/formValidation.ts
1973
- var FORM_VALIDATION_TEXT_RE = /(inv[aá]lid|obrigat[oó]ri|required|invalid|j[aá]\s+existe|already\s+exists|duplicad|formato|format|too\s+short|muito\s+curto|n[aã]o\s+(?:foi|pode)|must\s+be|deve\s+ser|selecione|escolha|select\s+(?:the|a|o|an))/i;
1974
- var FORM_SUCCESS_TEXT_RE = /\b(cadastrad[oa]s?\s+com\s+sucesso|registrad[oa]s?\s+com\s+sucesso|salv[oa]\s+com\s+sucesso|foi\s+cadastrad[oa]|foi\s+registrad[oa]|foi\s+salv[oa]|successfully\s+(?:registered|saved|created)|(?:has\s+been|was)\s+(?:registered|saved|created))\b/i;
1631
+ var FORM_VALIDATION_TEXT_RE2 = FORM_VALIDATION_TEXT_RE;
1632
+ var FORM_SUCCESS_TEXT_RE = FORM_WRITE_SUCCESS_TEXT_RE;
1975
1633
  var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1976
- var FIELD_HINTS = [
1977
- { re: /placa|plate|matr[ií]cula/i, fieldId: "placa" },
1978
- { re: /marca|brand/i, fieldId: "marca" },
1979
- { re: /modelo|model/i, fieldId: "modelo" },
1980
- { re: /\bcor\b|color|colour/i, fieldId: "cor" },
1981
- { re: /\bano\b|year/i, fieldId: "ano" },
1982
- { re: /email|e-mail|correo/i, fieldId: "email" }
1983
- ];
1984
- function inferValidationFieldId(message) {
1985
- for (const hint of FIELD_HINTS) {
1986
- if (hint.re.test(message)) return hint.fieldId;
1634
+ function inferValidationFieldId(message, inputs) {
1635
+ const haystack = normalizeText(message);
1636
+ if (!haystack || !inputs?.length) return void 0;
1637
+ let best;
1638
+ for (const input of inputs) {
1639
+ const id = input?.id;
1640
+ if (!id) continue;
1641
+ for (const naming of [input.label, input.placeholder, id]) {
1642
+ const needle = normalizeText(naming ?? "");
1643
+ if (needle.length < 3) continue;
1644
+ if (!containsWord(haystack, needle)) continue;
1645
+ if (!best || needle.length > best.length) best = { id, length: needle.length };
1646
+ }
1987
1647
  }
1988
- return void 0;
1648
+ return best?.id;
1649
+ }
1650
+ function normalizeText(value) {
1651
+ return String(value ?? "").normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
1652
+ }
1653
+ function containsWord(haystack, needle) {
1654
+ return ` ${haystack} `.includes(` ${needle} `);
1989
1655
  }
1990
1656
  function readVisibleFormValidation(snap) {
1991
1657
  if (!snap) return void 0;
1992
1658
  for (const text of snap.texts ?? []) {
1993
- if (!text?.trim() || !FORM_VALIDATION_TEXT_RE.test(text)) continue;
1994
- return { fieldId: inferValidationFieldId(text), message: text };
1659
+ if (!text?.trim() || !FORM_VALIDATION_TEXT_RE2.test(text)) continue;
1660
+ return { fieldId: inferValidationFieldId(text, snap.inputs), message: text };
1995
1661
  }
1996
1662
  return void 0;
1997
1663
  }
@@ -2213,7 +1879,9 @@ function resolveComponentId(componentId, registry, action) {
2213
1879
  const snapshot = registry.snapshot();
2214
1880
  for (const comp of snapshot) {
2215
1881
  if (comp.id.toLowerCase() === lower) {
2216
- console.log(`[Appilots] resolveComponentId: case-insensitive match "${componentId}" \u2192 "${comp.id}"`);
1882
+ appilotsDebugLog(
1883
+ `resolveComponentId: case-insensitive match "${componentId}" \u2192 "${comp.id}"`
1884
+ );
2217
1885
  return comp.id;
2218
1886
  }
2219
1887
  }
@@ -2228,12 +1896,16 @@ function resolveComponentId(componentId, registry, action) {
2228
1896
  if (metaAction && typeof metaAction === "object" && metaAction.label) {
2229
1897
  const metaLabel = metaAction.label;
2230
1898
  const normalizedMetaLabel = metaLabel.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]/g, "");
2231
- console.log(`[Appilots] resolveComponentId: found screen metadata for "${componentId}" \u2192 label="${metaLabel}"`);
1899
+ appilotsDebugLog(
1900
+ `resolveComponentId: found screen metadata for "${componentId}" \u2192 label="${metaLabel}"`
1901
+ );
2232
1902
  for (const comp of snapshot) {
2233
1903
  if (!comp.label) continue;
2234
1904
  const compLabel = comp.label.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]/g, "");
2235
1905
  if (compLabel === normalizedMetaLabel || compLabel.includes(normalizedMetaLabel) || normalizedMetaLabel.includes(compLabel)) {
2236
- console.log(`[Appilots] resolveComponentId: FOUND via metadata: "${componentId}" \u2192 "${comp.id}" (label="${comp.label}")`);
1906
+ appilotsDebugLog(
1907
+ `resolveComponentId: FOUND via metadata: "${componentId}" \u2192 "${comp.id}" (label="${comp.label}")`
1908
+ );
2237
1909
  return comp.id;
2238
1910
  }
2239
1911
  }
@@ -2248,8 +1920,8 @@ function resolveComponentId(componentId, registry, action) {
2248
1920
  }));
2249
1921
  const match = resolveTarget(componentId, candidates, { action });
2250
1922
  if (match.status === "matched") {
2251
- console.log(
2252
- `[Appilots] resolveComponentId: matched "${componentId}" -> "${match.id}" (tier=${match.tier})`
1923
+ appilotsDebugLog(
1924
+ `resolveComponentId: matched "${componentId}" -> "${match.id}" (tier=${match.tier})`
2253
1925
  );
2254
1926
  return match.id;
2255
1927
  }
@@ -2338,8 +2010,8 @@ function setValueHandler(rawComponentId, requested, registry) {
2338
2010
  try {
2339
2011
  slider.setValue(applied);
2340
2012
  if (applied !== requestedValue) {
2341
- console.log(
2342
- `[Appilots] uiInteractionHandler: set_value snapped ${requestedValue} \u2192 ${applied} for "${componentId}" (step=${step})`
2013
+ appilotsDebugLog(
2014
+ `uiInteractionHandler: set_value snapped ${requestedValue} \u2192 ${applied} for "${componentId}" (step=${step})`
2343
2015
  );
2344
2016
  }
2345
2017
  return { success: true, effect: "changed" };
@@ -2373,6 +2045,21 @@ function checkModalOcclusion(rawComponentId, action) {
2373
2045
  return null;
2374
2046
  }
2375
2047
  if (typeof rawComponentId !== "string" || rawComponentId.trim().length === 0) return null;
2048
+ const dialog = getOpenNativeDialog();
2049
+ if (dialog) {
2050
+ const needle = normalizeIdent(rawComponentId);
2051
+ if (dialog.buttons.some((b) => normalizeIdent(b.label) === needle)) return null;
2052
+ return {
2053
+ success: false,
2054
+ error: `"${rawComponentId}" is behind a native dialog${dialog.title ? ` ("${dialog.title}")` : ""} and cannot receive touches. Answer the dialog first \u2014 press one of: ` + dialog.buttons.map((b) => b.label).join(", "),
2055
+ diagnose: {
2056
+ category: "ambiguous-target",
2057
+ targetId: rawComponentId,
2058
+ screen: getCurrentScreen() ?? void 0,
2059
+ candidates: dialog.buttons.map((b) => b.label).slice(0, 8)
2060
+ }
2061
+ };
2062
+ }
2376
2063
  const fiberRoot = getFiberRoot();
2377
2064
  if (!fiberRoot) return null;
2378
2065
  let snap;
@@ -2404,9 +2091,12 @@ function checkModalOcclusion(rawComponentId, action) {
2404
2091
  modalOptionLabels.push(button.label ?? button.id);
2405
2092
  }
2406
2093
  }
2407
- for (const toggle of snap.toggles ?? []) add(toggle.inModal ? inModal : outside, toggle.id, toggle.label);
2408
- for (const input of snap.inputs ?? []) add(input.inModal ? inModal : outside, input.id, input.label);
2409
- for (const slider of snap.sliders ?? []) add(slider.inModal ? inModal : outside, slider.id, slider.label);
2094
+ for (const toggle of snap.toggles ?? [])
2095
+ add(toggle.inModal ? inModal : outside, toggle.id, toggle.label);
2096
+ for (const input of snap.inputs ?? [])
2097
+ add(input.inModal ? inModal : outside, input.id, input.label);
2098
+ for (const slider of snap.sliders ?? [])
2099
+ add(slider.inModal ? inModal : outside, slider.id, slider.label);
2410
2100
  for (const list of snap.lists ?? []) {
2411
2101
  for (const item of list.items ?? []) {
2412
2102
  const itemInModal = (item.buttons ?? []).some((b) => b.inModal) || (item.inputs ?? []).some((i) => i.inModal) || (item.toggles ?? []).some((t) => t.inModal);
@@ -2441,7 +2131,10 @@ function uiInteractionHandler(payload, context) {
2441
2131
  return {
2442
2132
  success: false,
2443
2133
  error: "UI interaction is not permitted",
2444
- diagnose: buildUiDiagnose(payload.componentId ?? payload.targetId ?? "", "unknown")
2134
+ diagnose: buildUiDiagnose(
2135
+ payload.componentId ?? payload.targetId ?? "",
2136
+ "unknown"
2137
+ )
2445
2138
  };
2446
2139
  }
2447
2140
  const { action } = payload;
@@ -2455,8 +2148,8 @@ function uiInteractionHandler(payload, context) {
2455
2148
  if (sliderId) {
2456
2149
  const smuggled = payload.value ?? payload.params?.value;
2457
2150
  if (smuggled !== void 0) {
2458
- console.log(
2459
- `[Appilots] uiInteractionHandler: "${actionStr}" on slider "${sliderId}" carried value=${JSON.stringify(smuggled)} \u2014 routing to set_value`
2151
+ appilotsDebugLog(
2152
+ `uiInteractionHandler: "${actionStr}" on slider "${sliderId}" carried value=${JSON.stringify(smuggled)} \u2014 routing to set_value`
2460
2153
  );
2461
2154
  return setValueHandler(rawComponentId, smuggled, registry);
2462
2155
  }
@@ -2466,18 +2159,53 @@ function uiInteractionHandler(payload, context) {
2466
2159
  diagnose: { ...buildUiDiagnose(sliderId, "unknown"), recoverable: true }
2467
2160
  };
2468
2161
  }
2162
+ const openDialog = getOpenNativeDialog();
2163
+ const isPressVerb = actionStr === "press" || actionStr === "tap" || actionStr === "click";
2164
+ if (openDialog && isPressVerb) {
2165
+ const needle = normalizeIdent(rawComponentId);
2166
+ const button = openDialog.buttons.find((b) => normalizeIdent(b.label) === needle);
2167
+ if (button) {
2168
+ button.press();
2169
+ appilotsDebugLog(`uiInteractionHandler: pressed native dialog button "${button.label}"`);
2170
+ return { success: true, effect: "changed" };
2171
+ }
2172
+ }
2173
+ if (!openDialog && isPressVerb) {
2174
+ const answered = getLastAnsweredNativeDialog();
2175
+ const needle = normalizeIdent(rawComponentId);
2176
+ if (answered && needle) {
2177
+ if (answered.answeredWith && normalizeIdent(answered.answeredWith) === needle) {
2178
+ appilotsDebugLog(
2179
+ `uiInteractionHandler: native dialog button "${answered.answeredWith}" was already pressed and the dialog is closed \u2014 treating the repeat as done`
2180
+ );
2181
+ return { success: true, effect: "completed" };
2182
+ }
2183
+ if (answered.buttons.some((label) => normalizeIdent(label) === needle)) {
2184
+ return {
2185
+ success: false,
2186
+ error: `The dialog${answered.title ? ` ("${answered.title}")` : ""} was already answered with "${answered.answeredWith ?? "dismiss"}", so "${rawComponentId}" is no longer on screen. Read the current screen before acting again.`,
2187
+ diagnose: {
2188
+ category: "component-not-found",
2189
+ targetId: rawComponentId,
2190
+ screen: getCurrentScreen() ?? void 0,
2191
+ recoverable: false
2192
+ }
2193
+ };
2194
+ }
2195
+ }
2196
+ }
2469
2197
  const occluded = checkModalOcclusion(rawComponentId, actionStr);
2470
2198
  if (occluded) {
2471
- console.log(
2472
- `[Appilots] uiInteractionHandler: blocked "${actionStr}" on "${rawComponentId}" \u2014 target is behind an open modal`
2199
+ appilotsDebugLog(
2200
+ `uiInteractionHandler: blocked "${actionStr}" on "${rawComponentId}" \u2014 target is behind an open modal`
2473
2201
  );
2474
2202
  return occluded;
2475
2203
  }
2476
2204
  if (typeof rawComponentId === "string" && rawComponentId.trim().toLowerCase().startsWith("el:")) {
2477
2205
  const result = dispatchElementOrTarget(rawComponentId, registry, actionStr);
2478
2206
  if (result.success) {
2479
- console.log(
2480
- `[Appilots] uiInteractionHandler: interaction element dispatch succeeded for "${rawComponentId}"`
2207
+ appilotsDebugLog(
2208
+ `uiInteractionHandler: interaction element dispatch succeeded for "${rawComponentId}"`
2481
2209
  );
2482
2210
  return { success: true };
2483
2211
  }
@@ -2485,13 +2213,13 @@ function uiInteractionHandler(payload, context) {
2485
2213
  }
2486
2214
  const ordinal = parseSyntheticListItemId(rawComponentId);
2487
2215
  if (ordinal && (action === "press" || actionStr === "tap" || actionStr === "click" || actionStr === "longPress")) {
2488
- console.log(
2489
- `[Appilots] uiInteractionHandler: dispatching synthetic list ordinal \u2014 componentId="${rawComponentId}" list=${ordinal.listIndex} item=${ordinal.itemIndex}`
2216
+ appilotsDebugLog(
2217
+ `uiInteractionHandler: dispatching synthetic list ordinal \u2014 componentId="${rawComponentId}" list=${ordinal.listIndex} item=${ordinal.itemIndex}`
2490
2218
  );
2491
2219
  const result = pressListItemAtOrdinal(ordinal.listIndex, ordinal.itemIndex);
2492
2220
  if (result.ok) {
2493
- console.log(
2494
- `[Appilots] uiInteractionHandler: synthetic ordinal press succeeded (container=${result.containerType ?? "List"})`
2221
+ appilotsDebugLog(
2222
+ `uiInteractionHandler: synthetic ordinal press succeeded (container=${result.containerType ?? "List"})`
2495
2223
  );
2496
2224
  return { success: true };
2497
2225
  }
@@ -2536,7 +2264,9 @@ function uiInteractionHandler(payload, context) {
2536
2264
  };
2537
2265
  }
2538
2266
  const componentId = resolveComponentId(rawComponentId, registry, actionStr);
2539
- console.log(`[Appilots] uiInteractionHandler: resolved componentId="${payload.componentId}" \u2192 "${componentId}"`);
2267
+ appilotsDebugLog(
2268
+ `uiInteractionHandler: resolved componentId="${payload.componentId}" \u2192 "${componentId}"`
2269
+ );
2540
2270
  const target = registry.getTarget(componentId);
2541
2271
  if (target) {
2542
2272
  try {
@@ -2587,7 +2317,9 @@ function uiInteractionHandler(payload, context) {
2587
2317
  return { success: false, error: err?.message ?? "Focus failed" };
2588
2318
  }
2589
2319
  }
2590
- console.log(`[Appilots] uiInteractionHandler: registry miss for "${componentId}", falling back to fiber tree`);
2320
+ appilotsDebugLog(
2321
+ `uiInteractionHandler: registry miss for "${componentId}", falling back to fiber tree`
2322
+ );
2591
2323
  const handle = findInteractiveByIdentifier(componentId, {
2592
2324
  kind: actionStr === "toggle" ? "toggle" : actionStr === "focus" ? "field" : "target"
2593
2325
  });
@@ -2595,7 +2327,9 @@ function uiInteractionHandler(payload, context) {
2595
2327
  try {
2596
2328
  if (actionStr === "press" && handle.kind === "target" && handle.press) {
2597
2329
  handle.press();
2598
- console.log(`[Appilots] uiInteractionHandler: fiber fallback press succeeded for "${componentId}"`);
2330
+ appilotsDebugLog(
2331
+ `uiInteractionHandler: fiber fallback press succeeded for "${componentId}"`
2332
+ );
2599
2333
  return { success: true };
2600
2334
  }
2601
2335
  if (actionStr === "press" && handle.kind === "toggle" && handle.setValue) {
@@ -2741,12 +2475,10 @@ function readVisibleValidation(fieldId, label) {
2741
2475
  try {
2742
2476
  const snap = captureSnapshot();
2743
2477
  const texts = snap.texts ?? [];
2744
- const needles = [fieldId, label].filter(
2745
- (s) => !!s && s.length > 0
2746
- );
2478
+ const needles = [fieldId, label].filter((s) => !!s && s.length > 0);
2747
2479
  for (const text of texts) {
2748
2480
  const lower = text.toLowerCase();
2749
- if (FORM_VALIDATION_TEXT_RE.test(lower)) {
2481
+ if (FORM_VALIDATION_TEXT_RE2.test(lower)) {
2750
2482
  for (const needle of needles) {
2751
2483
  if (lower.includes(needle.toLowerCase())) return text;
2752
2484
  }
@@ -2867,8 +2599,10 @@ async function tryPressSelectOption(fieldId, value, registry, options = {}) {
2867
2599
  function normalizeFieldType(type) {
2868
2600
  if (typeof type !== "string") return void 0;
2869
2601
  const normalized = normalize3(type);
2870
- if (normalized === "select" || normalized === "picker" || normalized === "dropdown") return "select";
2871
- if (normalized === "toggle" || normalized === "switch" || normalized === "checkbox") return "toggle";
2602
+ if (normalized === "select" || normalized === "picker" || normalized === "dropdown")
2603
+ return "select";
2604
+ if (normalized === "toggle" || normalized === "switch" || normalized === "checkbox")
2605
+ return "toggle";
2872
2606
  if (normalized === "date") return "date";
2873
2607
  if (normalized === "number" || normalized === "numeric") return "number";
2874
2608
  if (normalized === "custom") return "custom";
@@ -2902,10 +2636,7 @@ function fieldCueTerms(fieldId, label) {
2902
2636
  function snapshotHasFieldCue(snap, fieldId, label) {
2903
2637
  const terms = fieldCueTerms(fieldId, label);
2904
2638
  if (terms.length === 0) return false;
2905
- const visible = [
2906
- ...snap.texts ?? [],
2907
- ...(snap.buttons ?? []).map((b) => b.label ?? b.id ?? "")
2908
- ].map((text) => normalize3(text)).filter(Boolean);
2639
+ const visible = [...snap.texts ?? [], ...(snap.buttons ?? []).map((b) => b.label ?? b.id ?? "")].map((text) => normalize3(text)).filter(Boolean);
2909
2640
  return visible.some((text) => terms.some((term) => text.includes(term) || term.includes(text)));
2910
2641
  }
2911
2642
  function scoreCandidateText(value, texts) {
@@ -2979,9 +2710,7 @@ function buildChoiceCandidates(snap, fieldId, value, allowFirstVisibleWhenNoCue,
2979
2710
  fieldCueVisible && elements.length <= 12 ? 50 : 0,
2980
2711
  ...cueTexts.map((text) => {
2981
2712
  const normalized = normalize3(text);
2982
- return fieldTerms.some(
2983
- (term) => normalized.includes(term) || term.includes(normalized)
2984
- ) ? 80 : 0;
2713
+ return fieldTerms.some((term) => normalized.includes(term) || term.includes(normalized)) ? 80 : 0;
2985
2714
  })
2986
2715
  );
2987
2716
  const valueScore = genericValue ? 0 : scoreCandidateText(value, texts);
@@ -3053,7 +2782,11 @@ async function selectVisibleChoiceAsync(fieldId, value, registry, options) {
3053
2782
  return {
3054
2783
  success: false,
3055
2784
  error: `No visible option found for select field "${fieldId}"`,
3056
- diagnose: { category: "component-not-found", fieldId, screen: getCurrentScreen() ?? void 0 }
2785
+ diagnose: {
2786
+ category: "component-not-found",
2787
+ fieldId,
2788
+ screen: getCurrentScreen() ?? void 0
2789
+ }
3057
2790
  };
3058
2791
  }
3059
2792
  const pressed = pressTargetByIdOrLabel(candidate.targetId, registry);
@@ -3085,17 +2818,19 @@ function pressContinuationButton(registry) {
3085
2818
  return pressTargetByIdOrLabel(id, registry);
3086
2819
  }
3087
2820
  function findField(fieldId, registry) {
3088
- console.log(`[Appilots] findField: searching for fieldId="${fieldId}"`);
2821
+ appilotsDebugLog(`findField: searching for fieldId="${fieldId}"`);
3089
2822
  const exact = registry.getField(fieldId);
3090
2823
  if (exact) {
3091
- console.log(`[Appilots] findField: FOUND exact match for fieldId="${fieldId}"`);
2824
+ appilotsDebugLog(`findField: FOUND exact match for fieldId="${fieldId}"`);
3092
2825
  return exact;
3093
2826
  }
3094
2827
  const lower = fieldId.toLowerCase();
3095
2828
  const snapshot = registry.snapshot();
3096
2829
  for (const comp of snapshot) {
3097
2830
  if (comp.kind === "field" && comp.id.toLowerCase() === lower) {
3098
- console.log(`[Appilots] findField: FOUND case-insensitive match: registered="${comp.id}" for searched="${fieldId}"`);
2831
+ appilotsDebugLog(
2832
+ `findField: FOUND case-insensitive match: registered="${comp.id}" for searched="${fieldId}"`
2833
+ );
3099
2834
  return registry.getField(comp.id);
3100
2835
  }
3101
2836
  }
@@ -3123,8 +2858,8 @@ function findField(fieldId, registry) {
3123
2858
  }
3124
2859
  }
3125
2860
  if (bestComp && bestScore >= 75) {
3126
- console.log(
3127
- `[Appilots] findField: scored match "${fieldId}" -> "${bestComp.id}" (label="${bestComp.label}", score=${bestScore})`
2861
+ appilotsDebugLog(
2862
+ `findField: scored match "${fieldId}" -> "${bestComp.id}" (label="${bestComp.label}", score=${bestScore})`
3128
2863
  );
3129
2864
  return registry.getField(bestComp.id);
3130
2865
  }
@@ -3150,8 +2885,8 @@ function findField(fieldId, registry) {
3150
2885
  }
3151
2886
  }
3152
2887
  if (mBest && mBestScore >= 60) {
3153
- console.log(
3154
- `[Appilots] findField: FOUND via screen metadata translation: fieldId="${fieldId}" -> metaLabel="${metaLabel}" -> registered="${mBest.id}" (score=${mBestScore})`
2888
+ appilotsDebugLog(
2889
+ `findField: FOUND via screen metadata translation: fieldId="${fieldId}" -> metaLabel="${metaLabel}" -> registered="${mBest.id}" (score=${mBestScore})`
3155
2890
  );
3156
2891
  return registry.getField(mBest.id);
3157
2892
  }
@@ -3159,8 +2894,8 @@ function findField(fieldId, registry) {
3159
2894
  }
3160
2895
  }
3161
2896
  if (bestComp && bestScore >= 40) {
3162
- console.log(
3163
- `[Appilots] findField: weak substring match "${fieldId}" -> "${bestComp.id}" (label="${bestComp.label}", score=${bestScore})`
2897
+ appilotsDebugLog(
2898
+ `findField: weak substring match "${fieldId}" -> "${bestComp.id}" (label="${bestComp.label}", score=${bestScore})`
3164
2899
  );
3165
2900
  return registry.getField(bestComp.id);
3166
2901
  }
@@ -3170,17 +2905,19 @@ function findField(fieldId, registry) {
3170
2905
  return void 0;
3171
2906
  }
3172
2907
  function findToggle(fieldId, registry) {
3173
- console.log(`[Appilots] findToggle: searching for fieldId="${fieldId}"`);
2908
+ appilotsDebugLog(`findToggle: searching for fieldId="${fieldId}"`);
3174
2909
  const exact = registry.getToggle(fieldId);
3175
2910
  if (exact) {
3176
- console.log(`[Appilots] findToggle: FOUND exact match for "${fieldId}"`);
2911
+ appilotsDebugLog(`findToggle: FOUND exact match for "${fieldId}"`);
3177
2912
  return exact;
3178
2913
  }
3179
2914
  const lower = fieldId.toLowerCase();
3180
2915
  const snapshot = registry.snapshot();
3181
2916
  for (const comp of snapshot) {
3182
2917
  if (comp.kind === "toggle" && comp.id.toLowerCase() === lower) {
3183
- console.log(`[Appilots] findToggle: FOUND case-insensitive match: registered="${comp.id}" for searched="${fieldId}"`);
2918
+ appilotsDebugLog(
2919
+ `findToggle: FOUND case-insensitive match: registered="${comp.id}" for searched="${fieldId}"`
2920
+ );
3184
2921
  return registry.getToggle(comp.id);
3185
2922
  }
3186
2923
  }
@@ -3195,7 +2932,9 @@ function findSlider(fieldId, registry) {
3195
2932
  const lower = fieldId.toLowerCase();
3196
2933
  for (const comp of registry.snapshot()) {
3197
2934
  if (comp.kind === "slider" && comp.id.toLowerCase() === lower) {
3198
- console.log(`[Appilots] findSlider: FOUND case-insensitive match: registered="${comp.id}" for searched="${fieldId}"`);
2935
+ appilotsDebugLog(
2936
+ `findSlider: FOUND case-insensitive match: registered="${comp.id}" for searched="${fieldId}"`
2937
+ );
3199
2938
  return registry.getSlider(comp.id);
3200
2939
  }
3201
2940
  }
@@ -3227,7 +2966,7 @@ async function formFillHandler(payload, context) {
3227
2966
  "formFillHandler: registry is EMPTY \u2014 auto-tracking may not be active. Ensure enableAppilotsAutoTracking() runs before components mount."
3228
2967
  );
3229
2968
  } else {
3230
- console.log(
2969
+ appilotsDebugLog(
3231
2970
  `[Appilots formFillHandler] Registry has ${registrySnapshot.length} components:`,
3232
2971
  registrySnapshot.map((c) => `${c.kind}:${c.id}`).join(", ")
3233
2972
  );
@@ -3235,7 +2974,7 @@ async function formFillHandler(payload, context) {
3235
2974
  const fieldResults = [];
3236
2975
  let allSuccess = true;
3237
2976
  let selectedChoiceThisBatch = false;
3238
- console.log(`[Appilots] formFillHandler: processing ${payload.fields.length} fields`);
2977
+ appilotsDebugLog(`formFillHandler: processing ${payload.fields.length} fields`);
3239
2978
  const availableFieldIds = () => registry.snapshot().filter((comp) => comp.kind === "field").map((comp) => `${comp.id}${comp.label ? ` (${comp.label})` : ""}`).slice(0, 8);
3240
2979
  const isElementIdField = (field) => typeof field.fieldId === "string" && field.fieldId.trim().toLowerCase().startsWith("el:");
3241
2980
  for (const field of payload.fields.filter(isElementIdField)) {
@@ -3262,8 +3001,8 @@ async function formFillHandler(payload, context) {
3262
3001
  const { fieldId, fieldType, value } = field;
3263
3002
  const effectiveFieldType = getEffectiveFieldType(fieldId, fieldType, registry);
3264
3003
  let selectFailure;
3265
- console.log(
3266
- `[Appilots] formFillHandler: looking up fieldId="${fieldId}" fieldType="${fieldType}" effectiveFieldType="${effectiveFieldType}" value="${value}"`
3004
+ appilotsDebugLog(
3005
+ `formFillHandler: looking up fieldId="${fieldId}" fieldType="${fieldType}" effectiveFieldType="${effectiveFieldType}" value=${describeValue(value)}`
3267
3006
  );
3268
3007
  const fieldEntry = findField(fieldId, registry);
3269
3008
  if (fieldEntry) {
@@ -3314,7 +3053,11 @@ async function formFillHandler(payload, context) {
3314
3053
  fieldId,
3315
3054
  success: false,
3316
3055
  error: selectResult.error ?? `Select value "${String(value)}" was not applied for "${fieldId}"`,
3317
- diagnose: selectResult.diagnose ?? { category: "component-not-found", fieldId, screen: screenPath }
3056
+ diagnose: selectResult.diagnose ?? {
3057
+ category: "component-not-found",
3058
+ fieldId,
3059
+ screen: screenPath
3060
+ }
3318
3061
  });
3319
3062
  allSuccess = false;
3320
3063
  continue;
@@ -3369,7 +3112,9 @@ async function formFillHandler(payload, context) {
3369
3112
  continue;
3370
3113
  }
3371
3114
  try {
3372
- sliderEntry.setValue(snapSliderValue(numeric, sliderEntry.min, sliderEntry.max, sliderEntry.step));
3115
+ sliderEntry.setValue(
3116
+ snapSliderValue(numeric, sliderEntry.min, sliderEntry.max, sliderEntry.step)
3117
+ );
3373
3118
  fieldResults.push({ fieldId, success: true });
3374
3119
  } catch (err) {
3375
3120
  fieldResults.push({
@@ -3418,8 +3163,8 @@ async function formFillHandler(payload, context) {
3418
3163
  }
3419
3164
  if (selectResult.success) {
3420
3165
  selectedChoiceThisBatch = true;
3421
- console.log(
3422
- `[Appilots] formFillHandler: selected visible option for "${fieldId}"` + (selectResult.selectedLabel ? ` \u2192 "${selectResult.selectedLabel}"` : "")
3166
+ appilotsDebugLog(
3167
+ `formFillHandler: selected visible option for "${fieldId}"` + (selectResult.selectedLabel ? ` \u2192 "${selectResult.selectedLabel}"` : "")
3423
3168
  );
3424
3169
  await sleep2(120);
3425
3170
  fieldResults.push({ fieldId, success: true });
@@ -3427,7 +3172,7 @@ async function formFillHandler(payload, context) {
3427
3172
  }
3428
3173
  selectFailure = selectResult;
3429
3174
  }
3430
- console.log(`[Appilots] formFillHandler: registry miss for "${fieldId}", falling back to fiber tree`);
3175
+ appilotsDebugLog(`formFillHandler: registry miss for "${fieldId}", falling back to fiber tree`);
3431
3176
  const handle = findInteractiveByIdentifier(fieldId, {
3432
3177
  kind: effectiveFieldType === "toggle" ? "toggle" : "field"
3433
3178
  });
@@ -3435,7 +3180,9 @@ async function formFillHandler(payload, context) {
3435
3180
  try {
3436
3181
  handle.focus?.();
3437
3182
  handle.setValue(value);
3438
- console.log(`[Appilots] formFillHandler: fiber fallback succeeded for "${fieldId}" \u2192 label="${handle.label}"`);
3183
+ appilotsDebugLog(
3184
+ `formFillHandler: fiber fallback succeeded for "${fieldId}" \u2192 label="${handle.label}"`
3185
+ );
3439
3186
  fieldResults.push({ fieldId, success: true });
3440
3187
  continue;
3441
3188
  } catch (err) {
@@ -3477,7 +3224,15 @@ async function formFillHandler(payload, context) {
3477
3224
  );
3478
3225
  } else {
3479
3226
  await sleep2(120);
3480
- const submitResult = pressSubmitButton({ screen, screenPath, activePath }, registry);
3227
+ const submitResult = pressSubmitButton(
3228
+ {
3229
+ screen,
3230
+ screenPath,
3231
+ activePath,
3232
+ filledFieldIds: fieldResults.filter((r) => r.success).map((r) => r.fieldId)
3233
+ },
3234
+ registry
3235
+ );
3481
3236
  if (!submitResult.success) {
3482
3237
  appilotsDebugWarn(`formFillHandler: submitAfterFill failed \u2014 ${submitResult.error}`);
3483
3238
  return {
@@ -3515,7 +3270,7 @@ async function formFillHandler(payload, context) {
3515
3270
  }
3516
3271
  };
3517
3272
  }
3518
- console.log("[Appilots] formFillHandler: submitAfterFill \u2014 submit completed");
3273
+ appilotsDebugLog("formFillHandler: submitAfterFill \u2014 submit completed");
3519
3274
  }
3520
3275
  }
3521
3276
  const aggregatedDiagnose = aggregateFieldDiagnoses(fieldResults, screenPath);
@@ -3537,8 +3292,8 @@ function pressSubmitButton(context, registry) {
3537
3292
  if (target) {
3538
3293
  try {
3539
3294
  target.press();
3540
- console.log(
3541
- `[Appilots] formFillHandler: submitAfterFill \u2014 pressed registry target "${candidate}"` + (resolvedScreen ? ` on screen "${resolvedScreen}"` : "")
3295
+ appilotsDebugLog(
3296
+ `formFillHandler: submitAfterFill \u2014 pressed registry target "${candidate}"` + (resolvedScreen ? ` on screen "${resolvedScreen}"` : "")
3542
3297
  );
3543
3298
  return { success: true, pressedTargetId: candidate };
3544
3299
  } catch (err) {
@@ -3562,9 +3317,7 @@ function pressSubmitButton(context, registry) {
3562
3317
  if (handle?.press) {
3563
3318
  try {
3564
3319
  handle.press();
3565
- console.log(
3566
- `[Appilots] formFillHandler: submitAfterFill \u2014 fiber fallback "${submitLabel}"`
3567
- );
3320
+ appilotsDebugLog(`formFillHandler: submitAfterFill \u2014 fiber fallback "${submitLabel}"`);
3568
3321
  return { success: true, pressedTargetId: submitId ?? submitLabel };
3569
3322
  } catch (err) {
3570
3323
  return {
@@ -3586,9 +3339,7 @@ function pressSubmitButton(context, registry) {
3586
3339
  if (id) {
3587
3340
  const pressed = pressTargetByIdOrLabel(id, registry);
3588
3341
  if (pressed.success) {
3589
- console.log(
3590
- `[Appilots] formFillHandler: submitAfterFill \u2014 pressed visible heuristic "${id}"`
3591
- );
3342
+ appilotsDebugLog(`formFillHandler: submitAfterFill \u2014 pressed visible heuristic "${id}"`);
3592
3343
  return { success: true, pressedTargetId: id };
3593
3344
  }
3594
3345
  }
@@ -3602,8 +3353,8 @@ function pressSubmitButton(context, registry) {
3602
3353
  if (target) {
3603
3354
  try {
3604
3355
  target.press();
3605
- console.log(
3606
- `[Appilots] formFillHandler: submitAfterFill \u2014 pressed by label heuristic "${comp.label}"`
3356
+ appilotsDebugLog(
3357
+ `formFillHandler: submitAfterFill \u2014 pressed by label heuristic "${comp.label}"`
3607
3358
  );
3608
3359
  return { success: true, pressedTargetId: comp.id };
3609
3360
  } catch (err) {
@@ -3615,9 +3366,16 @@ function pressSubmitButton(context, registry) {
3615
3366
  }
3616
3367
  }
3617
3368
  }
3369
+ for (const fieldId of [...context.filledFieldIds ?? []].reverse()) {
3370
+ const handle = findInteractiveByIdentifier(fieldId, { kind: "field" });
3371
+ if (handle?.submitFromKeyboard?.()) {
3372
+ appilotsDebugLog(`formFillHandler: submitAfterFill \u2014 return key on "${fieldId}"`);
3373
+ return { success: true, pressedTargetId: fieldId };
3374
+ }
3375
+ }
3618
3376
  return {
3619
3377
  success: false,
3620
- error: 'submitAfterFill requested but no submit button was found (declare it via registerScreen({ actions: [{ type: "submit", ... }] }) for reliable behavior)',
3378
+ error: 'submitAfterFill requested but the screen has no submit button and no field that submits on return (declare it via registerScreen({ actions: [{ type: "submit", ... }] }) for reliable behavior)',
3621
3379
  diagnose: { category: "component-not-found", screen: resolvedScreen }
3622
3380
  };
3623
3381
  }
@@ -3671,6 +3429,8 @@ function resolveListEntry(listId) {
3671
3429
  return { candidates: entries.map((e) => e.id) };
3672
3430
  }
3673
3431
  if (entries.length === 1) return { entry: entries[0] };
3432
+ const lists = entries.filter((e) => e.kind === "list");
3433
+ if (lists.length === 1) return { entry: lists[0] };
3674
3434
  return { candidates: entries.map((e) => e.id) };
3675
3435
  }
3676
3436
  function pageOffset(metrics, direction) {
@@ -3710,7 +3470,7 @@ async function scrollListHandler(payload, context) {
3710
3470
  }
3711
3471
  const { entry, candidates } = resolveListEntry(payload.listId);
3712
3472
  if (!entry) {
3713
- const detail = candidates && candidates.length > 0 ? payload.listId ? `List "${payload.listId}" is not registered. Known lists: ${candidates.join(", ")}` : `Multiple lists are on screen \u2014 specify listId. Known lists: ${candidates.join(", ")}` : "No scrollable list is registered on the current screen (enableAppilotsAutoTracking() may be off).";
3473
+ const detail = candidates && candidates.length > 0 ? payload.listId ? `List "${payload.listId}" is not registered. Known lists: ${candidates.join(", ")}` : `Multiple lists are on screen \u2014 specify listId. Known lists: ${candidates.join(", ")}` : "Nothing scrollable is registered on the current screen (enableAppilotsAutoTracking() may be off).";
3714
3474
  return {
3715
3475
  success: false,
3716
3476
  error: detail,
@@ -3718,6 +3478,13 @@ async function scrollListHandler(payload, context) {
3718
3478
  };
3719
3479
  }
3720
3480
  if (toIndex !== void 0) {
3481
+ if (entry.kind === "scroll") {
3482
+ return {
3483
+ success: false,
3484
+ error: `"${entry.id}" is a scrollable screen, not a list \u2014 it has no numbered items. Use direction "down"/"up" to page through it.`,
3485
+ diagnose: buildDiagnose("validation", entry.id)
3486
+ };
3487
+ }
3721
3488
  if (typeof entry.itemCount === "number" && toIndex > entry.itemCount) {
3722
3489
  return {
3723
3490
  success: false,
@@ -3752,14 +3519,14 @@ async function scrollListHandler(payload, context) {
3752
3519
  }
3753
3520
  return {
3754
3521
  success: false,
3755
- error: `List "${entry.id}" does not expose a scrollable handle`,
3522
+ error: `"${entry.id}" does not expose a scrollable handle`,
3756
3523
  diagnose: buildDiagnose("unknown", entry.id)
3757
3524
  };
3758
3525
  }
3759
3526
 
3760
3527
  // src/executor/handlers/confirmHandler.ts
3761
3528
  async function confirmHandler(_actionId, _payload) {
3762
- return { success: true };
3529
+ return { success: true, effect: "unknown" };
3763
3530
  }
3764
3531
 
3765
3532
  // src/executor/postInteractionSettle.ts
@@ -3771,14 +3538,45 @@ function targetIdFromPayload(payload) {
3771
3538
  if (typeof targetId === "string" && targetId.trim()) return targetId.trim();
3772
3539
  return "";
3773
3540
  }
3541
+ function hashFieldValue(value) {
3542
+ let hash = 2166136261;
3543
+ for (let i = 0; i < value.length; i++) {
3544
+ hash ^= value.charCodeAt(i);
3545
+ hash = Math.imul(hash, 16777619);
3546
+ }
3547
+ return (hash >>> 0).toString(36);
3548
+ }
3549
+ var DISCRETE_FIELD_TYPES = /* @__PURE__ */ new Set(["select", "date"]);
3550
+ function discreteFieldSignature() {
3551
+ const sigs = [];
3552
+ for (const { id, kind } of componentRegistry.snapshot()) {
3553
+ if (kind !== "field") continue;
3554
+ const entry = componentRegistry.getField(id);
3555
+ if (!entry || !DISCRETE_FIELD_TYPES.has(entry.fieldType ?? "")) continue;
3556
+ try {
3557
+ sigs.push(`${id}:${hashFieldValue(String(entry.getValue() ?? ""))}`);
3558
+ } catch {
3559
+ }
3560
+ }
3561
+ return sigs.sort().join(",");
3562
+ }
3774
3563
  async function waitForPostInteractionSettle(payload) {
3775
3564
  const targetId = targetIdFromPayload(payload);
3776
3565
  const fromScreen = getCurrentScreen();
3777
3566
  const baseline = probeLoadingState(targetId || null, fromScreen);
3567
+ const baselineFields = discreteFieldSignature();
3568
+ const readable = baseline.fingerprint !== "";
3778
3569
  await sleepMs(100);
3779
3570
  const afterPress = probeLoadingState(targetId || null, getCurrentScreen());
3780
3571
  const modalOrLoading = afterPress.modalOpen || afterPress.loading || afterPress.pressedDisabled;
3781
3572
  const fingerprintChanged = afterPress.fingerprint !== baseline.fingerprint;
3573
+ const verdict = () => {
3574
+ if (!readable) return { effect: "unknown" };
3575
+ const settled = probeLoadingState(targetId || null, getCurrentScreen());
3576
+ if (settled.fingerprint === "") return { effect: "unknown" };
3577
+ if (discreteFieldSignature() !== baselineFields) return { effect: "changed" };
3578
+ return { effect: settled.fingerprint !== baseline.fingerprint ? "changed" : "none" };
3579
+ };
3782
3580
  const capMs = isListItemPressTarget(targetId) ? 5e3 : 1e4;
3783
3581
  if (modalOrLoading || fingerprintChanged) {
3784
3582
  await waitForLoadingSettle({
@@ -3786,7 +3584,7 @@ async function waitForPostInteractionSettle(payload) {
3786
3584
  fromScreen,
3787
3585
  maxMs: capMs
3788
3586
  });
3789
- return;
3587
+ return verdict();
3790
3588
  }
3791
3589
  if (isListItemPressTarget(targetId)) {
3792
3590
  await waitForLoadingSettle({
@@ -3794,6 +3592,7 @@ async function waitForPostInteractionSettle(payload) {
3794
3592
  maxMs: 3e3
3795
3593
  });
3796
3594
  }
3595
+ return verdict();
3797
3596
  }
3798
3597
 
3799
3598
  // src/executor/ActionExecutor.ts
@@ -3848,10 +3647,9 @@ function validateActionPayload(action) {
3848
3647
  const hasIndex = typeof payload.toIndex === "number";
3849
3648
  const hasDirection = payload.direction === "up" || payload.direction === "down";
3850
3649
  if (!hasIndex && !hasDirection) {
3851
- return invalid(
3852
- 'scroll_list action needs toIndex (1-based) or direction ("up"/"down")',
3853
- { targetId: typeof payload.listId === "string" ? payload.listId : void 0 }
3854
- );
3650
+ return invalid('scroll_list action needs toIndex (1-based) or direction ("up"/"down")', {
3651
+ targetId: typeof payload.listId === "string" ? payload.listId : void 0
3652
+ });
3855
3653
  }
3856
3654
  return null;
3857
3655
  }
@@ -3859,20 +3657,44 @@ function validateActionPayload(action) {
3859
3657
  return null;
3860
3658
  }
3861
3659
  }
3660
+ function effectFor(payload, observed) {
3661
+ if (observed === "none" && isDestructivePayload(payload)) return "unknown";
3662
+ return observed;
3663
+ }
3664
+ function isDestructivePayload(payload) {
3665
+ if (!payload || typeof payload !== "object") return false;
3666
+ const p = payload;
3667
+ if (p.destructive === true) return true;
3668
+ const params = p.params;
3669
+ return !!params && typeof params === "object" && params.destructive === true;
3670
+ }
3862
3671
  async function executeAction(action, context) {
3863
- console.log(`[Appilots] executeAction called: type="${action.type}", id="${action.id}", payload=${JSON.stringify(action.payload)}`);
3672
+ appilotsDebugLog(
3673
+ `executeAction called: type="${action.type}", id="${action.id}", payload=${JSON.stringify(action.payload)}`
3674
+ );
3864
3675
  const permissions = context.permissions ?? DEFAULT_PERMISSIONS;
3865
- if (permissions.allowedActions && permissions.allowedActions.length > 0 && !permissions.allowedActions.includes(action.type)) {
3866
- return {
3867
- success: false,
3868
- error: `Action type "${action.type}" is not in the allowed actions list`
3869
- };
3870
- }
3871
3676
  context.emit({
3872
3677
  type: "agent:action:start",
3873
3678
  timestamp: Date.now(),
3874
3679
  data: { actionId: action.id, actionType: action.type }
3875
3680
  });
3681
+ if (permissions.allowedActions && permissions.allowedActions.length > 0 && !permissions.allowedActions.includes(action.type)) {
3682
+ const result2 = {
3683
+ success: false,
3684
+ error: `Action type "${action.type}" is not in the allowed actions list`
3685
+ };
3686
+ context.emit({
3687
+ type: "agent:action:error",
3688
+ timestamp: Date.now(),
3689
+ data: {
3690
+ actionId: action.id,
3691
+ actionType: action.type,
3692
+ success: false,
3693
+ error: result2.error
3694
+ }
3695
+ });
3696
+ return result2;
3697
+ }
3876
3698
  let result;
3877
3699
  const validationFailure = validateActionPayload(action);
3878
3700
  if (validationFailure) {
@@ -3906,25 +3728,25 @@ async function executeAction(action, context) {
3906
3728
  registry: context.registry ?? getDefaultRegistry()
3907
3729
  });
3908
3730
  break;
3909
- case "ui_interaction":
3910
- result = uiInteractionHandler(action.payload, {
3731
+ case "ui_interaction": {
3732
+ const payload = action.payload;
3733
+ result = uiInteractionHandler(payload, {
3911
3734
  permissions,
3912
3735
  registry: context.registry ?? getDefaultRegistry()
3913
3736
  });
3914
3737
  if (result.success) {
3915
- await waitForPostInteractionSettle(action.payload);
3738
+ const settle = await waitForPostInteractionSettle(payload);
3739
+ result.effect = result.effect ?? effectFor(payload, settle.effect);
3916
3740
  }
3917
3741
  break;
3742
+ }
3918
3743
  case "scroll_list":
3919
3744
  result = await scrollListHandler(action.payload, {
3920
3745
  permissions
3921
3746
  });
3922
3747
  break;
3923
3748
  case "confirm":
3924
- result = await confirmHandler(
3925
- action.id,
3926
- action.payload
3927
- );
3749
+ result = await confirmHandler(action.id, action.payload);
3928
3750
  break;
3929
3751
  case "custom":
3930
3752
  result = {
@@ -3947,7 +3769,9 @@ async function executeAction(action, context) {
3947
3769
  error: err?.message ?? "Action execution failed unexpectedly"
3948
3770
  };
3949
3771
  }
3950
- console.log(`[Appilots] executeAction result: type="${action.type}", id="${action.id}", success=${result.success}, error=${result.error ?? "none"}`);
3772
+ appilotsDebugLog(
3773
+ `executeAction result: type="${action.type}", id="${action.id}", success=${result.success}, error=${result.error ?? "none"}`
3774
+ );
3951
3775
  context.emit({
3952
3776
  type: result.success ? "agent:action:complete" : "agent:action:error",
3953
3777
  timestamp: Date.now(),
@@ -3963,6 +3787,93 @@ async function executeAction(action, context) {
3963
3787
  });
3964
3788
  return result;
3965
3789
  }
3790
+
3791
+ // src/platform/confirmedDestructiveAlert.ts
3792
+ var SUPPRESSION_WINDOW_MS = 1e3;
3793
+ var _active = null;
3794
+ function resolveConfirmButton(buttons) {
3795
+ if (!buttons || buttons.length === 0) return void 0;
3796
+ const destructive = buttons.find(
3797
+ (button) => button.style === "destructive" && typeof button.onPress === "function"
3798
+ );
3799
+ if (destructive) return destructive;
3800
+ return buttons.find(
3801
+ (button) => button.style !== "cancel" && typeof button.onPress === "function"
3802
+ );
3803
+ }
3804
+ function restore(suppression) {
3805
+ if (suppression.timer) {
3806
+ clearTimeout(suppression.timer);
3807
+ suppression.timer = null;
3808
+ }
3809
+ if (_active !== suppression) return;
3810
+ if (suppression.host.alert === _patchedAlert) {
3811
+ suppression.host.alert = suppression.originalAlert;
3812
+ }
3813
+ _active = null;
3814
+ }
3815
+ var _patchedAlert = (title, message, buttons, options) => {
3816
+ const suppression = _active;
3817
+ if (!suppression) return;
3818
+ if (suppression.consumed) {
3819
+ return suppression.originalAlert(title, message, buttons, options);
3820
+ }
3821
+ const button = resolveConfirmButton(buttons);
3822
+ if (!button?.onPress) {
3823
+ return suppression.originalAlert(title, message, buttons, options);
3824
+ }
3825
+ suppression.consumed = true;
3826
+ appilotsDebugLog("Suppressing native Alert during already-confirmed destructive agent action");
3827
+ try {
3828
+ suppression.pending = Promise.resolve(button.onPress()).then(() => void 0).catch((err) => reportHandlerFailure(err));
3829
+ } catch (err) {
3830
+ reportHandlerFailure(err);
3831
+ suppression.pending = null;
3832
+ }
3833
+ };
3834
+ function reportHandlerFailure(err) {
3835
+ console.error("[Appilots] The app\u2019s Alert handler threw while being auto-confirmed", err);
3836
+ }
3837
+ async function runWithConfirmedDestructiveContext(enabled, host, fn) {
3838
+ if (!enabled) return fn();
3839
+ if (_active) {
3840
+ _active.depth += 1;
3841
+ const joined = _active;
3842
+ try {
3843
+ return await fn();
3844
+ } finally {
3845
+ joined.depth -= 1;
3846
+ if (joined.depth === 0) {
3847
+ if (joined.pending) await joined.pending.catch(() => void 0);
3848
+ restore(joined);
3849
+ }
3850
+ }
3851
+ }
3852
+ const suppression = {
3853
+ host,
3854
+ originalAlert: host.alert,
3855
+ depth: 1,
3856
+ consumed: false,
3857
+ pending: null,
3858
+ timer: null
3859
+ };
3860
+ _active = suppression;
3861
+ host.alert = _patchedAlert;
3862
+ suppression.timer = setTimeout(() => {
3863
+ if (!suppression.consumed) restore(suppression);
3864
+ }, SUPPRESSION_WINDOW_MS);
3865
+ suppression.timer?.unref?.();
3866
+ try {
3867
+ const result = await fn();
3868
+ if (suppression.pending) await suppression.pending.catch(() => void 0);
3869
+ return result;
3870
+ } finally {
3871
+ suppression.depth -= 1;
3872
+ if (suppression.depth === 0) restore(suppression);
3873
+ }
3874
+ }
3875
+
3876
+ // src/platform/reactNativeAdapter.ts
3966
3877
  var POST_ACTION_LOADING_MAX_MS = 6e3;
3967
3878
  function resolveScreenMetadata() {
3968
3879
  const activePath = getActiveRouteNames();
@@ -4037,13 +3948,10 @@ async function settleTurn(input) {
4037
3948
  targetScreen: navigateTargetScreen,
4038
3949
  maxMs: 3500
4039
3950
  });
4040
- console.log(
4041
- `[Appilots] settleTurn: post-navigate settle done \u2014 screen="${settle.screen}" transitioned=${settle.transitioned} timedOut=${settle.timedOut} waitedMs=${settle.waitedMs}`
3951
+ appilotsDebugLog(
3952
+ `settleTurn: post-navigate settle done \u2014 screen="${settle.screen}" transitioned=${settle.transitioned} timedOut=${settle.timedOut} waitedMs=${settle.waitedMs}`
4042
3953
  );
4043
- const navigationMoved = settle.transitioned || screenDepartedBaseline(
4044
- input.preNavigateScreen ?? null,
4045
- input.preNavigateSignature ?? null
4046
- ) || !!navigateTargetScreen && !!settle.screen && routesBelongToSameFeature(settle.screen, navigateTargetScreen);
3954
+ const navigationMoved = settle.transitioned || screenDepartedBaseline(input.preNavigateScreen ?? null, input.preNavigateSignature ?? null) || !!navigateTargetScreen && !!settle.screen && routesBelongToSameFeature(settle.screen, navigateTargetScreen);
4047
3955
  if (!navigationMoved) {
4048
3956
  appilotsDebugWarn(
4049
3957
  `tool-without-effect: navigate emitted but screen did not change (stayed at "${input.preNavigateScreen ?? "unknown"}", waitedMs=${settle.waitedMs}). Likely causes: target screenName missing from MCP doc, nested-nav path absent, or React Navigation rejected the route.`
@@ -4056,8 +3964,8 @@ async function settleTurn(input) {
4056
3964
  fromScreen: settle.screen ?? null,
4057
3965
  maxMs: 3e3
4058
3966
  });
4059
- console.log(
4060
- `[Appilots] settleTurn: post-navigate loading settle \u2014 settled=${loadSettle.settled} loadingPending=${loadSettle.loadingPending} probes=${loadSettle.probeCount} waitedMs=${loadSettle.waitedMs}`
3967
+ appilotsDebugLog(
3968
+ `settleTurn: post-navigate loading settle \u2014 settled=${loadSettle.settled} loadingPending=${loadSettle.loadingPending} probes=${loadSettle.probeCount} waitedMs=${loadSettle.waitedMs}`
4061
3969
  );
4062
3970
  if (loadSettle.loadingPending) loadingPending = true;
4063
3971
  } else {
@@ -4071,8 +3979,8 @@ async function settleTurn(input) {
4071
3979
  fromScreen: getCurrentScreen(),
4072
3980
  maxMs: settleCapMs
4073
3981
  });
4074
- console.log(
4075
- `[Appilots] settleTurn: post-action loading settle \u2014 pressedId="${pressedId ?? ""}" settled=${loadSettle.settled} transitioned=${loadSettle.transitioned} loadingPending=${loadSettle.loadingPending} probes=${loadSettle.probeCount} waitedMs=${loadSettle.waitedMs} cap=${settleCapMs}${hints?.isAsyncTrigger ? " (mcp-inferred)" : ""}`
3982
+ appilotsDebugLog(
3983
+ `settleTurn: post-action loading settle \u2014 pressedId="${pressedId ?? ""}" settled=${loadSettle.settled} transitioned=${loadSettle.transitioned} loadingPending=${loadSettle.loadingPending} probes=${loadSettle.probeCount} waitedMs=${loadSettle.waitedMs} cap=${settleCapMs}${hints?.isAsyncTrigger ? " (mcp-inferred)" : ""}`
4076
3984
  );
4077
3985
  if (loadSettle.loadingPending) loadingPending = true;
4078
3986
  const listPressActions = turnActions.filter((action) => {
@@ -4093,8 +4001,8 @@ async function settleTurn(input) {
4093
4001
  maxMs: 4e3
4094
4002
  });
4095
4003
  const afterFingerprint = probeLoadingState(pressedId, getCurrentScreen()).fingerprint;
4096
- console.log(
4097
- `[Appilots] settleTurn: post-list-press settle \u2014 screen="${postPressSettle.screen}" transitioned=${postPressSettle.transitioned} timedOut=${postPressSettle.timedOut} waitedMs=${postPressSettle.waitedMs}`
4004
+ appilotsDebugLog(
4005
+ `settleTurn: post-list-press settle \u2014 screen="${postPressSettle.screen}" transitioned=${postPressSettle.transitioned} timedOut=${postPressSettle.timedOut} waitedMs=${postPressSettle.waitedMs}`
4098
4006
  );
4099
4007
  if (!postPressSettle.transitioned && baselineFingerprint === afterFingerprint) {
4100
4008
  for (const action of listPressActions) {
@@ -4113,42 +4021,14 @@ var reactNativeChatAdapter = {
4113
4021
  getCurrentScreenSignature,
4114
4022
  settleTurn
4115
4023
  };
4116
- async function runWithConfirmedDestructiveContext(enabled, fn) {
4117
- if (!enabled) return fn();
4118
- const originalAlert = Alert.alert;
4119
- let consumed = false;
4120
- let alertWork = null;
4121
- Alert.alert = (title, message, buttons, options) => {
4122
- if (consumed) {
4123
- return originalAlert(title, message, buttons, options);
4124
- }
4125
- const destructive = buttons?.find((button) => button.style === "destructive");
4126
- const actionable = destructive ?? buttons?.find((button) => button.style !== "cancel" && typeof button.onPress === "function");
4127
- if (actionable?.onPress) {
4128
- consumed = true;
4129
- console.log(
4130
- "[Appilots] Suppressing native Alert during already-confirmed destructive agent action"
4131
- );
4132
- alertWork = Promise.resolve(actionable.onPress()).then(() => void 0);
4133
- return;
4134
- }
4135
- return originalAlert(title, message, buttons, options);
4136
- };
4137
- try {
4138
- const result = await fn();
4139
- if (alertWork) await alertWork;
4140
- return result;
4141
- } finally {
4142
- Alert.alert = originalAlert;
4143
- }
4144
- }
4145
4024
  function createReactNativeActionRunner(getOptions) {
4146
4025
  return {
4147
4026
  async execute(action, { confirmedDestructive }) {
4148
- const { permissions, emit, navigationRef } = getOptions();
4027
+ const { permissions, emit, navigationRef, suppressNativeConfirm } = getOptions();
4149
4028
  const navRef = navigationRef?.current ? navigationRef : { current: getNavigationRef() };
4150
4029
  return runWithConfirmedDestructiveContext(
4151
- confirmedDestructive,
4030
+ confirmedDestructive && suppressNativeConfirm !== false,
4031
+ Alert,
4152
4032
  () => executeAction(action, {
4153
4033
  navigationRef: navRef,
4154
4034
  permissions,
@@ -4218,12 +4098,14 @@ function useAppilotsActions(options = {}) {
4218
4098
  const runnerOptionsRef = useRef({
4219
4099
  permissions: config.permissions ?? DEFAULT_PERMISSIONS2,
4220
4100
  emit,
4221
- navigationRef
4101
+ navigationRef,
4102
+ suppressNativeConfirm: config.suppressNativeConfirm
4222
4103
  });
4223
4104
  runnerOptionsRef.current = {
4224
4105
  permissions: config.permissions ?? DEFAULT_PERMISSIONS2,
4225
4106
  emit,
4226
- navigationRef
4107
+ navigationRef,
4108
+ suppressNativeConfirm: config.suppressNativeConfirm
4227
4109
  };
4228
4110
  const machine = useMemo(
4229
4111
  () => new ActionQueueMachine(
@@ -4313,7 +4195,7 @@ function useSuggestedPrompts(options) {
4313
4195
  if (cancelled || requestIdRef.current !== requestId) return;
4314
4196
  setPrompts(mergePromptSources(localDev, data, limit));
4315
4197
  }).catch((err) => {
4316
- console.warn("[Appilots] useSuggestedPrompts: fetch failed", err);
4198
+ appilotsDebugWarn("useSuggestedPrompts: fetch failed", err);
4317
4199
  }).finally(() => {
4318
4200
  if (cancelled || requestIdRef.current !== requestId) return;
4319
4201
  setIsLoading(false);
@@ -4511,4 +4393,4 @@ function useAppilotsSlider(id, options) {
4511
4393
  }, [id, registry, getValue, setValue, min, max, step, label, screen]);
4512
4394
  }
4513
4395
 
4514
- export { captureSnapshot, executeAction, shouldShowSuggestedPrompts, useAppilots, useAppilotsActions, useAppilotsChat, useAppilotsField, useAppilotsNavigation, useAppilotsSlider, useAppilotsTarget, useAppilotsToggle, useSuggestedPrompts };
4396
+ export { appilotsSelfCheck, captureSnapshot, executeAction, shouldShowSuggestedPrompts, useAppilots, useAppilotsActions, useAppilotsChat, useAppilotsField, useAppilotsNavigation, useAppilotsSlider, useAppilotsTarget, useAppilotsToggle, useSuggestedPrompts };