@appilots/sdk 0.8.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, clampSnapshotToWireLimits, 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-C2KVVJ5K.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,36 +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);
203
+ canonicalizeRegisteredFields(snapshot);
773
204
  clampSnapshotToWireLimits(snapshot);
774
- snapshot.elements = deriveInteractionElements(snapshot);
775
- snapshot.choiceGroups = attachElementIdsToChoiceGroups(
776
- snapshot.choiceGroups,
777
- snapshot.elements
205
+ const entries = deriveInteractionElements(snapshot, { rectOf: rectForNode });
206
+ const rectById = new Map(
207
+ entries.flatMap((entry) => entry.rect ? [[entry.id, entry.rect]] : [])
778
208
  );
209
+ snapshot.elements = entries.map(toWireElement);
210
+ snapshot.choiceGroups = attachElementIdsToChoiceGroups(snapshot.choiceGroups, snapshot.elements);
779
211
  clampSnapshotToWireLimits(snapshot);
780
- elementRegistry.replaceAll(snapshot.elements);
781
- const elapsed = Date.now() - start;
782
- const totalListItems = snapshot.lists.reduce(
783
- (acc, l) => acc + l.items.length,
784
- 0
212
+ elementRegistry.replaceAll(
213
+ snapshot.elements.map((element) => {
214
+ const rect = rectById.get(element.id);
215
+ return rect ? { ...element, rect } : element;
216
+ })
785
217
  );
218
+ const elapsed = Date.now() - start;
219
+ const totalListItems = snapshot.lists.reduce((acc, l) => acc + l.items.length, 0);
786
220
  const totalListDataItems = snapshot.lists.reduce(
787
221
  (acc, l) => acc + (typeof l.itemCount === "number" ? l.itemCount : 0),
788
222
  0
789
223
  );
790
- console.log(
791
- `[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} ` + (snapshot.truncated ? "truncated=true " : "") + `(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)`
792
226
  );
793
- console.log(
794
- `[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(", ")}]`
795
229
  );
796
230
  return snapshot;
797
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
+ }
798
272
 
799
273
  // src/introspection/findInteractive.ts
800
274
  function getTypeName2(type) {
@@ -808,7 +282,7 @@ function getTypeName2(type) {
808
282
  }
809
283
  return null;
810
284
  }
811
- function matchesName2(fiber, name) {
285
+ function matchesName(fiber, name) {
812
286
  return getTypeName2(fiber.type) === name || getTypeName2(fiber.elementType) === name;
813
287
  }
814
288
  var TOUCHABLE_NAMES2 = /* @__PURE__ */ new Set([
@@ -821,33 +295,33 @@ var TOUCHABLE_NAMES2 = /* @__PURE__ */ new Set([
821
295
  function isTouchable(fiber) {
822
296
  return TOUCHABLE_NAMES2.has(getTypeName2(fiber.type) ?? "") || TOUCHABLE_NAMES2.has(getTypeName2(fiber.elementType) ?? "");
823
297
  }
824
- function hasPressHandler2(fiber) {
298
+ function hasPressHandler(fiber) {
825
299
  const props = fiber?.memoizedProps ?? {};
826
300
  return typeof props.onPress === "function" || typeof props.onPressIn === "function";
827
301
  }
828
- function isPressableTarget2(fiber) {
829
- return isTouchable(fiber) || hasPressHandler2(fiber);
302
+ function isPressableTarget(fiber) {
303
+ return isTouchable(fiber) || hasPressHandler(fiber);
830
304
  }
831
- var SELF_REFERENTIAL_COMPONENTS2 = /* @__PURE__ */ new Set([
305
+ var SELF_REFERENTIAL_COMPONENTS = /* @__PURE__ */ new Set([
832
306
  "AppilotsChat",
833
307
  "AppilotsChatInner",
834
308
  "ActionBreadcrumb",
835
309
  "ConfirmDialog"
836
310
  ]);
837
- function isAppilotsSkipMarked2(props) {
311
+ function isAppilotsSkipMarked(props) {
838
312
  if (!props) return false;
839
313
  return props.__appilotsSkip === true || props["data-appilots-skip"] === true || props.appilotsSkip === true;
840
314
  }
841
- function extractTextFromChildren2(children) {
315
+ function extractTextFromChildren(children) {
842
316
  if (children == null) return "";
843
317
  if (typeof children === "string") return children;
844
318
  if (typeof children === "number") return String(children);
845
319
  if (Array.isArray(children)) {
846
- return children.map(extractTextFromChildren2).filter(Boolean).join("");
320
+ return children.map(extractTextFromChildren).filter(Boolean).join("");
847
321
  }
848
322
  return "";
849
323
  }
850
- function findChildText2(fiber) {
324
+ function findChildText(fiber) {
851
325
  const stack = [];
852
326
  if (fiber.child) stack.push(fiber.child);
853
327
  const visited = /* @__PURE__ */ new WeakSet();
@@ -855,8 +329,8 @@ function findChildText2(fiber) {
855
329
  const node = stack.pop();
856
330
  if (!node || visited.has(node)) continue;
857
331
  visited.add(node);
858
- if (matchesName2(node, "Text")) {
859
- const text = extractTextFromChildren2(node.memoizedProps?.children);
332
+ if (matchesName(node, "Text")) {
333
+ const text = extractTextFromChildren(node.memoizedProps?.children);
860
334
  if (text && text.trim().length > 0) return text.trim();
861
335
  }
862
336
  if (node.sibling) stack.push(node.sibling);
@@ -868,10 +342,10 @@ function shouldSkipSubtree2(fiber) {
868
342
  const props = fiber.memoizedProps;
869
343
  if (!props) return false;
870
344
  const typeName = getTypeName2(fiber.type) ?? getTypeName2(fiber.elementType);
871
- if (typeName && SELF_REFERENTIAL_COMPONENTS2.has(typeName)) return true;
872
- if (isAppilotsSkipMarked2(props)) return true;
873
- if (matchesName2(fiber, "Modal") && props.visible === false) return true;
874
- 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;
875
349
  const style = props.style;
876
350
  if (style) {
877
351
  const flat = Array.isArray(style) ? Object.assign({}, ...style.filter(Boolean)) : style;
@@ -880,20 +354,19 @@ function shouldSkipSubtree2(fiber) {
880
354
  }
881
355
  return false;
882
356
  }
883
- function normalize(s) {
884
- if (!s) return "";
885
- return s.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]/g, "");
886
- }
357
+ var normalize = foldForCompare;
887
358
  function scoreMatch(fiber, queryNorm, queryRaw) {
888
359
  const props = fiber.memoizedProps ?? {};
889
360
  const candidates = [];
890
361
  if (typeof props.testID === "string") candidates.push({ val: props.testID, weight: 100 });
891
- 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 });
892
364
  if (typeof props.label === "string") candidates.push({ val: props.label, weight: 90 });
893
- 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 });
894
367
  if (typeof props.title === "string") candidates.push({ val: props.title, weight: 85 });
895
- if (isPressableTarget2(fiber)) {
896
- const childText = findChildText2(fiber);
368
+ if (isPressableTarget(fiber)) {
369
+ const childText = findChildText(fiber);
897
370
  if (childText) candidates.push({ val: childText, weight: 80 });
898
371
  }
899
372
  let best = 0;
@@ -902,7 +375,8 @@ function scoreMatch(fiber, queryNorm, queryRaw) {
902
375
  if (!vNorm) continue;
903
376
  if (val === queryRaw) best = Math.max(best, weight + 20);
904
377
  else if (vNorm === queryNorm) best = Math.max(best, weight + 10);
905
- 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);
906
380
  }
907
381
  if (best > 0 && props.disabled === true) {
908
382
  best = Math.max(1, best - 60);
@@ -933,6 +407,12 @@ function wrapTextInput(fiber) {
933
407
  if (node && typeof node.focus === "function") {
934
408
  node.focus();
935
409
  }
410
+ },
411
+ submitFromKeyboard: () => {
412
+ const { onSubmitEditing, value } = getProps();
413
+ if (typeof onSubmitEditing !== "function") return false;
414
+ onSubmitEditing({ nativeEvent: { text: value ?? "" } });
415
+ return true;
936
416
  }
937
417
  };
938
418
  }
@@ -972,14 +452,14 @@ function wrapTouchable(fiber, label) {
972
452
  function findInteractiveByIdentifier(query, options = {}) {
973
453
  const root = getFiberRoot();
974
454
  if (!root) {
975
- console.warn("[Appilots] findInteractiveByIdentifier: fiber root not captured yet");
455
+ appilotsDebugWarn("findInteractiveByIdentifier: fiber root not captured yet");
976
456
  return null;
977
457
  }
978
458
  const queryNorm = normalize(query);
979
459
  const queryRaw = query;
980
460
  let bestScore = 0;
981
461
  let bestHandle = null;
982
- let bestId2;
462
+ let bestId;
983
463
  const visited = /* @__PURE__ */ new WeakSet();
984
464
  const stack = [];
985
465
  if (root.child) stack.push(root.child);
@@ -990,40 +470,50 @@ function findInteractiveByIdentifier(query, options = {}) {
990
470
  if (fiber.sibling) stack.push(fiber.sibling);
991
471
  if (shouldSkipSubtree2(fiber)) continue;
992
472
  let kind = null;
993
- if (matchesName2(fiber, "TextInput")) kind = "field";
994
- else if (matchesName2(fiber, "Switch")) kind = "toggle";
995
- 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";
996
476
  if (kind && (!options.kind || options.kind === kind)) {
997
477
  const score = scoreMatch(fiber, queryNorm, queryRaw);
998
478
  if (score > bestScore) {
999
479
  bestScore = score;
1000
480
  const props = fiber.memoizedProps ?? {};
1001
- 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);
1002
482
  if (kind === "field") bestHandle = wrapTextInput(fiber);
1003
483
  else if (kind === "toggle") bestHandle = wrapSwitch(fiber);
1004
- else bestHandle = wrapTouchable(fiber, findChildText2(fiber));
484
+ else bestHandle = wrapTouchable(fiber, findChildText(fiber));
1005
485
  }
1006
486
  }
1007
487
  if (fiber.child) stack.push(fiber.child);
1008
488
  }
1009
489
  if (bestHandle) {
1010
- console.log(
1011
- `[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}`
1012
492
  );
1013
493
  } else {
1014
- console.log(`[Appilots] findInteractiveByIdentifier: query="${query}" \u2192 no match`);
494
+ appilotsDebugLog(`findInteractiveByIdentifier: query="${query}" \u2192 no match`);
1015
495
  }
1016
496
  return bestHandle;
1017
497
  }
1018
498
 
1019
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
+ }
1020
508
  function categorizeNavigationError(message) {
1021
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(
1022
510
  message
1023
511
  )) {
1024
512
  return "component-not-found";
1025
513
  }
1026
- 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
+ )) {
1027
517
  return "screen-timeout";
1028
518
  }
1029
519
  return "unknown";
@@ -1050,7 +540,9 @@ function screenExistsAnywhere(state, screenName) {
1050
540
  function safeNavigate(nav, screenName, params, path) {
1051
541
  if (path && path.length > 0) {
1052
542
  const { root, nestedParams } = buildNestedParams(path, screenName, params);
1053
- 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
+ );
1054
546
  nav.navigate(root, nestedParams);
1055
547
  return;
1056
548
  }
@@ -1065,11 +557,17 @@ function safeNavigate(nav, screenName, params, path) {
1065
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.`
1066
558
  );
1067
559
  }
1068
- 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
+ );
1069
563
  nav.navigate(screenName, params);
1070
564
  }
1071
565
  function navigateHandler(payload, context) {
1072
- 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
+ );
1073
571
  const { navigationRef, permissions } = context;
1074
572
  const nav = navigationRef.current;
1075
573
  if (!permissions.canNavigate) {
@@ -1079,14 +577,15 @@ function navigateHandler(payload, context) {
1079
577
  diagnose: { category: "unknown", screen: payload.screenName }
1080
578
  };
1081
579
  }
1082
- if (permissions.blockedScreens?.includes(payload.screenName)) {
580
+ const isGoBack = payload.navigationAction === "goBack";
581
+ if (!isGoBack && permissions.blockedScreens?.includes(payload.screenName)) {
1083
582
  return {
1084
583
  success: false,
1085
584
  error: `Navigation to "${payload.screenName}" is blocked`,
1086
585
  diagnose: { category: "unknown", screen: payload.screenName }
1087
586
  };
1088
587
  }
1089
- 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)) {
1090
589
  return {
1091
590
  success: false,
1092
591
  error: `Navigation to "${payload.screenName}" is not in allowed screens`,
@@ -1094,10 +593,11 @@ function navigateHandler(payload, context) {
1094
593
  };
1095
594
  }
1096
595
  if (!nav) {
596
+ reportMissingNavigationRef();
1097
597
  return {
1098
598
  success: false,
1099
- error: "Navigation ref is not available",
1100
- 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 }
1101
601
  };
1102
602
  }
1103
603
  if (!nav.isReady || typeof nav.isReady === "function" && !nav.isReady()) {
@@ -1108,7 +608,15 @@ function navigateHandler(payload, context) {
1108
608
  };
1109
609
  }
1110
610
  try {
1111
- 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;
1112
620
  switch (navigationAction) {
1113
621
  case "navigate":
1114
622
  safeNavigate(nav, screenName, params ?? {}, path);
@@ -1151,10 +659,14 @@ function navigateHandler(payload, context) {
1151
659
  diagnose: { category: "unknown", screen: payload.screenName }
1152
660
  };
1153
661
  }
1154
- 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
+ );
1155
665
  return { success: true };
1156
666
  } catch (err) {
1157
- console.warn(`[Appilots] navigateHandler: navigation failed for "${payload.screenName}": ${err?.message}`);
667
+ appilotsDebugWarn(
668
+ `navigateHandler: navigation failed for "${payload.screenName}": ${err?.message}`
669
+ );
1158
670
  const msg = String(err?.message ?? "Navigation failed");
1159
671
  const category = categorizeNavigationError(msg);
1160
672
  return {
@@ -1177,7 +689,7 @@ function parseSyntheticListItemId(id) {
1177
689
  if (listIndex < 0 || itemIndex < 1) return null;
1178
690
  return { listIndex, itemIndex };
1179
691
  }
1180
- function stringProp2(props, key) {
692
+ function stringProp(props, key) {
1181
693
  const value = props?.[key];
1182
694
  if (typeof value === "string" && value.trim()) return value.trim();
1183
695
  if (typeof value === "number") return String(value);
@@ -1185,30 +697,30 @@ function stringProp2(props, key) {
1185
697
  }
1186
698
  function listIdFromContainer(container) {
1187
699
  const props = container?.memoizedProps ?? {};
1188
- 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");
1189
701
  }
1190
702
  function listLabelFromContainer(container) {
1191
703
  const props = container?.memoizedProps ?? {};
1192
- 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");
1193
705
  }
1194
706
  function itemKeyFromFiber(fiber) {
1195
707
  const props = fiber?.memoizedProps ?? {};
1196
- 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);
1197
709
  }
1198
710
  function markedItemIndexFromFiber(fiber) {
1199
711
  const value = fiber?.memoizedProps?.__appilotsItemIndex;
1200
712
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
1201
713
  }
1202
714
  function normalize2(value) {
1203
- return String(value ?? "").toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "");
715
+ return foldForCompare(String(value ?? ""));
1204
716
  }
1205
- function hasPressHandler3(fiber) {
717
+ function hasPressHandler2(fiber) {
1206
718
  const props = fiber?.memoizedProps ?? {};
1207
719
  return typeof props.onPress === "function" || typeof props.onPressIn === "function";
1208
720
  }
1209
721
  function isPressableFiber(fiber) {
1210
722
  const typeName = getTypeName(fiber.type) ?? getTypeName(fiber.elementType) ?? "";
1211
- return TOUCHABLE_NAMES.has(typeName) || hasPressHandler3(fiber);
723
+ return TOUCHABLE_NAMES.has(typeName) || hasPressHandler2(fiber);
1212
724
  }
1213
725
  function findListContainers(options = {}) {
1214
726
  const root = getFiberRoot();
@@ -1353,13 +865,10 @@ function collectTexts(fiber) {
1353
865
  }
1354
866
  function hasUsefulTouchableIdentity(fiber) {
1355
867
  const props = fiber?.memoizedProps ?? {};
1356
- if (typeof props.testID === "string" && /[a-z0-9]/i.test(props.testID)) return true;
1357
- if (typeof props.accessibilityLabel === "string" && /[a-z0-9]/i.test(props.accessibilityLabel)) {
1358
- return true;
1359
- }
1360
- if (typeof props.label === "string" && /[a-z0-9]/i.test(props.label)) return true;
1361
- const label = findFirstText(fiber);
1362
- 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));
1363
872
  }
1364
873
  function describeTouchable(fiber) {
1365
874
  const props = fiber?.memoizedProps ?? {};
@@ -1372,12 +881,7 @@ function touchableLooksDestructive(fiber) {
1372
881
  const descriptor = describeTouchable(fiber);
1373
882
  if (descriptor && looksDestructiveActionLabel(descriptor)) return true;
1374
883
  const props = fiber?.memoizedProps ?? {};
1375
- const haystack = [
1376
- props.testID,
1377
- props.accessibilityLabel,
1378
- props.accessibilityHint,
1379
- props.label
1380
- ].filter((value) => typeof value === "string").join(" ");
884
+ const haystack = [props.testID, props.accessibilityLabel, props.accessibilityHint, props.label].filter((value) => typeof value === "string").join(" ");
1381
885
  return looksDestructiveActionLabel(haystack);
1382
886
  }
1383
887
  function describeItemCandidate(listIndex, zeroBasedIndex, item) {
@@ -1411,8 +915,8 @@ function pressResolvedListItem(list, itemFiber, listIndex, itemIndex) {
1411
915
  (candidate) => candidate !== touchable && containsFiber(touchable, candidate)
1412
916
  );
1413
917
  if (primaryWrapsNestedTargets) {
1414
- console.log(
1415
- `[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`
1416
920
  );
1417
921
  } else {
1418
922
  const candidates = safeIdentifiableTouchables.map(describeTouchable).filter((d) => !!d).slice(0, 8);
@@ -1522,14 +1026,14 @@ function pressListItemAtOrdinal(listIndex, itemIndex) {
1522
1026
  }
1523
1027
  return pressResolvedListItem(list, items[zeroBased], listIndex, itemIndex);
1524
1028
  }
1525
- function pressListItemByIdentity(locator) {
1526
- 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" } };
1527
1031
  const parsed = locator.syntheticId ? parseSyntheticListItemId(locator.syntheticId) : null;
1528
1032
  const effective = parsed ? { ...locator, listIndex: parsed.listIndex, itemIndex: parsed.itemIndex } : locator;
1529
1033
  const containers = findListContainers({ allowSingleHeuristic: true });
1530
1034
  const lists = candidateLists(containers, effective);
1531
1035
  if (lists.length === 0) {
1532
- return { ok: false, reason: "list-not-found" };
1036
+ return { ok: false, result: { ok: false, reason: "list-not-found" } };
1533
1037
  }
1534
1038
  const scoredCandidates = [];
1535
1039
  for (const { list, listIndex } of lists) {
@@ -1540,7 +1044,14 @@ function pressListItemByIdentity(locator) {
1540
1044
  }
1541
1045
  }
1542
1046
  if (scoredCandidates.length === 0) {
1543
- 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
+ };
1544
1055
  }
1545
1056
  scoredCandidates.sort((a, b) => b.score - a.score);
1546
1057
  const best = scoredCandidates[0];
@@ -1549,13 +1060,104 @@ function pressListItemByIdentity(locator) {
1549
1060
  const candidates = tiedCandidates.map((c) => describeItemCandidate(c.listIndex, c.itemIndex - 1, c.item)).slice(0, 8);
1550
1061
  return {
1551
1062
  ok: false,
1552
- reason: "ambiguous-items",
1553
- error: "Multiple visible list items matched the requested target with equal confidence." + (candidates.length > 0 ? ` Candidates: ${candidates.join(", ")}.` : ""),
1554
- 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,
1555
1122
  candidates
1556
1123
  };
1557
1124
  }
1558
- 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
+ );
1559
1161
  }
1560
1162
 
1561
1163
  // src/executor/handlers/elementDispatch.ts
@@ -1784,6 +1386,46 @@ function dispatchElement(element, registry, action) {
1784
1386
  };
1785
1387
  }
1786
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
+ }
1787
1429
  const targetId = element.targetId ?? element.label ?? element.id;
1788
1430
  return dispatchLegacyTarget(targetId, registry, "toggle");
1789
1431
  }
@@ -1813,8 +1455,8 @@ function dispatchStructuredElementId(identifier, action) {
1813
1455
  texts: parsed.itemKey ? [parsed.itemKey] : void 0
1814
1456
  });
1815
1457
  if (pressed.ok) {
1816
- console.log(
1817
- `[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}")`
1818
1460
  );
1819
1461
  return { success: true };
1820
1462
  }
@@ -1838,7 +1480,6 @@ function pressElementOrTarget(identifier, registry) {
1838
1480
 
1839
1481
  // src/executor/formFieldRewrite.ts
1840
1482
  var LIST_ORDINAL_RE = /^list-\d+-item-\d+$/i;
1841
- var PLATE_RE = /^[A-Z]{3}-?\d[A-Z0-9]\d{2}$/i;
1842
1483
  function normalize4(value) {
1843
1484
  return value.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]/g, "");
1844
1485
  }
@@ -1878,9 +1519,6 @@ function collectKnownFormFields(registry) {
1878
1519
  }
1879
1520
  return registryFields;
1880
1521
  }
1881
- function fieldHaystack(field) {
1882
- return normalize4(`${field.id} ${field.label ?? ""}`);
1883
- }
1884
1522
  function isMisplacedFieldId(fieldId, knownIds) {
1885
1523
  const trimmed = fieldId.trim();
1886
1524
  if (!trimmed) return true;
@@ -1904,50 +1542,68 @@ function valueMatchesOption(field, rawValue) {
1904
1542
  return label === value || optionValue === value || value.includes(label) || value.includes(optionValue);
1905
1543
  });
1906
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
+ }
1907
1596
  function inferFieldIdForValue(rawValue, fields, used) {
1908
1597
  const value = String(rawValue ?? "").trim();
1909
1598
  if (!value) return void 0;
1910
- const normalizedValue = normalize4(value);
1911
- for (const field of fields) {
1912
- if (used.has(field.id)) continue;
1599
+ const available = fields.filter((field) => field.id && !used.has(field.id));
1600
+ for (const field of available) {
1913
1601
  if (valueMatchesOption(field, value)) return field.id;
1914
1602
  }
1915
- if (PLATE_RE.test(value)) {
1916
- const field = fields.find((f) => /placa|plate/.test(fieldHaystack(f)) && !used.has(f.id));
1917
- if (field) return field.id;
1918
- }
1919
- if (/^(19|20)\d{2}$/.test(value)) {
1920
- const field = fields.find((f) => /ano|year/.test(fieldHaystack(f)) && !used.has(f.id));
1921
- if (field) return field.id;
1922
- }
1923
- if (/^\d+$/.test(value)) {
1924
- const mileage = fields.find((f) => /quilometragem|mileage|km/.test(fieldHaystack(f)) && !used.has(f.id));
1925
- if (mileage) return mileage.id;
1926
- const year = fields.find((f) => /ano|year/.test(fieldHaystack(f)) && !used.has(f.id));
1927
- if (year) return year.id;
1928
- }
1929
- if (/preto|branco|azul|vermelho|prata|black|white|red|blue|silver|gray|grey/.test(normalizedValue)) {
1930
- const cor = fields.find((f) => /cor|color/.test(fieldHaystack(f)) && !used.has(f.id));
1931
- if (cor) return cor.id;
1932
- }
1933
- const marca = fields.find((f) => /marca|brand|make/.test(fieldHaystack(f)) && !used.has(f.id));
1934
- if (marca && !/model/i.test(normalizedValue)) return marca.id;
1935
- const modelo = fields.find((f) => /modelo|model/.test(fieldHaystack(f)) && !used.has(f.id));
1936
- if (modelo) return modelo.id;
1937
- const tipo = fields.find((f) => /\btipo\b|type|vehicletype/.test(fieldHaystack(f)) && !used.has(f.id));
1938
- if (tipo && /carro|car|moto|truck|van|flex|gasoline|diesel|hybrid|electric|el[eé]trico/.test(normalizedValue)) {
1939
- return tipo.id;
1940
- }
1941
- const combustivel = fields.find((f) => /combustivel|fuel/.test(fieldHaystack(f)) && !used.has(f.id));
1942
- if (combustivel && /flex|gasoline|diesel|hybrid|electric|el[eé]trico/.test(normalizedValue)) {
1943
- return combustivel.id;
1944
- }
1945
- const nextText = fields.find((field) => {
1946
- if (used.has(field.id)) return false;
1947
- const type = normalize4(field.type ?? "text");
1948
- return type === "text" || type === "input" || type === "number" || type === "";
1949
- });
1950
- 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;
1951
1607
  }
1952
1608
  function rewriteMisplacedFormFillFields(fields, registry) {
1953
1609
  const knownFields = collectKnownFormFields(registry);
@@ -1972,28 +1628,36 @@ function rewriteMisplacedFormFillFields(fields, registry) {
1972
1628
  }
1973
1629
 
1974
1630
  // src/executor/formValidation.ts
1975
- 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;
1976
- 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;
1977
1633
  var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1978
- var FIELD_HINTS = [
1979
- { re: /placa|plate|matr[ií]cula/i, fieldId: "placa" },
1980
- { re: /marca|brand/i, fieldId: "marca" },
1981
- { re: /modelo|model/i, fieldId: "modelo" },
1982
- { re: /\bcor\b|color|colour/i, fieldId: "cor" },
1983
- { re: /\bano\b|year/i, fieldId: "ano" },
1984
- { re: /email|e-mail|correo/i, fieldId: "email" }
1985
- ];
1986
- function inferValidationFieldId(message) {
1987
- for (const hint of FIELD_HINTS) {
1988
- 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
+ }
1989
1647
  }
1990
- 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} `);
1991
1655
  }
1992
1656
  function readVisibleFormValidation(snap) {
1993
1657
  if (!snap) return void 0;
1994
1658
  for (const text of snap.texts ?? []) {
1995
- if (!text?.trim() || !FORM_VALIDATION_TEXT_RE.test(text)) continue;
1996
- 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 };
1997
1661
  }
1998
1662
  return void 0;
1999
1663
  }
@@ -2215,7 +1879,9 @@ function resolveComponentId(componentId, registry, action) {
2215
1879
  const snapshot = registry.snapshot();
2216
1880
  for (const comp of snapshot) {
2217
1881
  if (comp.id.toLowerCase() === lower) {
2218
- console.log(`[Appilots] resolveComponentId: case-insensitive match "${componentId}" \u2192 "${comp.id}"`);
1882
+ appilotsDebugLog(
1883
+ `resolveComponentId: case-insensitive match "${componentId}" \u2192 "${comp.id}"`
1884
+ );
2219
1885
  return comp.id;
2220
1886
  }
2221
1887
  }
@@ -2230,12 +1896,16 @@ function resolveComponentId(componentId, registry, action) {
2230
1896
  if (metaAction && typeof metaAction === "object" && metaAction.label) {
2231
1897
  const metaLabel = metaAction.label;
2232
1898
  const normalizedMetaLabel = metaLabel.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]/g, "");
2233
- 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
+ );
2234
1902
  for (const comp of snapshot) {
2235
1903
  if (!comp.label) continue;
2236
1904
  const compLabel = comp.label.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]/g, "");
2237
1905
  if (compLabel === normalizedMetaLabel || compLabel.includes(normalizedMetaLabel) || normalizedMetaLabel.includes(compLabel)) {
2238
- 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
+ );
2239
1909
  return comp.id;
2240
1910
  }
2241
1911
  }
@@ -2250,8 +1920,8 @@ function resolveComponentId(componentId, registry, action) {
2250
1920
  }));
2251
1921
  const match = resolveTarget(componentId, candidates, { action });
2252
1922
  if (match.status === "matched") {
2253
- console.log(
2254
- `[Appilots] resolveComponentId: matched "${componentId}" -> "${match.id}" (tier=${match.tier})`
1923
+ appilotsDebugLog(
1924
+ `resolveComponentId: matched "${componentId}" -> "${match.id}" (tier=${match.tier})`
2255
1925
  );
2256
1926
  return match.id;
2257
1927
  }
@@ -2340,8 +2010,8 @@ function setValueHandler(rawComponentId, requested, registry) {
2340
2010
  try {
2341
2011
  slider.setValue(applied);
2342
2012
  if (applied !== requestedValue) {
2343
- console.log(
2344
- `[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})`
2345
2015
  );
2346
2016
  }
2347
2017
  return { success: true, effect: "changed" };
@@ -2375,6 +2045,21 @@ function checkModalOcclusion(rawComponentId, action) {
2375
2045
  return null;
2376
2046
  }
2377
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
+ }
2378
2063
  const fiberRoot = getFiberRoot();
2379
2064
  if (!fiberRoot) return null;
2380
2065
  let snap;
@@ -2406,9 +2091,12 @@ function checkModalOcclusion(rawComponentId, action) {
2406
2091
  modalOptionLabels.push(button.label ?? button.id);
2407
2092
  }
2408
2093
  }
2409
- for (const toggle of snap.toggles ?? []) add(toggle.inModal ? inModal : outside, toggle.id, toggle.label);
2410
- for (const input of snap.inputs ?? []) add(input.inModal ? inModal : outside, input.id, input.label);
2411
- 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);
2412
2100
  for (const list of snap.lists ?? []) {
2413
2101
  for (const item of list.items ?? []) {
2414
2102
  const itemInModal = (item.buttons ?? []).some((b) => b.inModal) || (item.inputs ?? []).some((i) => i.inModal) || (item.toggles ?? []).some((t) => t.inModal);
@@ -2443,7 +2131,10 @@ function uiInteractionHandler(payload, context) {
2443
2131
  return {
2444
2132
  success: false,
2445
2133
  error: "UI interaction is not permitted",
2446
- diagnose: buildUiDiagnose(payload.componentId ?? payload.targetId ?? "", "unknown")
2134
+ diagnose: buildUiDiagnose(
2135
+ payload.componentId ?? payload.targetId ?? "",
2136
+ "unknown"
2137
+ )
2447
2138
  };
2448
2139
  }
2449
2140
  const { action } = payload;
@@ -2457,8 +2148,8 @@ function uiInteractionHandler(payload, context) {
2457
2148
  if (sliderId) {
2458
2149
  const smuggled = payload.value ?? payload.params?.value;
2459
2150
  if (smuggled !== void 0) {
2460
- console.log(
2461
- `[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`
2462
2153
  );
2463
2154
  return setValueHandler(rawComponentId, smuggled, registry);
2464
2155
  }
@@ -2468,18 +2159,53 @@ function uiInteractionHandler(payload, context) {
2468
2159
  diagnose: { ...buildUiDiagnose(sliderId, "unknown"), recoverable: true }
2469
2160
  };
2470
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
+ }
2471
2197
  const occluded = checkModalOcclusion(rawComponentId, actionStr);
2472
2198
  if (occluded) {
2473
- console.log(
2474
- `[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`
2475
2201
  );
2476
2202
  return occluded;
2477
2203
  }
2478
2204
  if (typeof rawComponentId === "string" && rawComponentId.trim().toLowerCase().startsWith("el:")) {
2479
2205
  const result = dispatchElementOrTarget(rawComponentId, registry, actionStr);
2480
2206
  if (result.success) {
2481
- console.log(
2482
- `[Appilots] uiInteractionHandler: interaction element dispatch succeeded for "${rawComponentId}"`
2207
+ appilotsDebugLog(
2208
+ `uiInteractionHandler: interaction element dispatch succeeded for "${rawComponentId}"`
2483
2209
  );
2484
2210
  return { success: true };
2485
2211
  }
@@ -2487,13 +2213,13 @@ function uiInteractionHandler(payload, context) {
2487
2213
  }
2488
2214
  const ordinal = parseSyntheticListItemId(rawComponentId);
2489
2215
  if (ordinal && (action === "press" || actionStr === "tap" || actionStr === "click" || actionStr === "longPress")) {
2490
- console.log(
2491
- `[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}`
2492
2218
  );
2493
2219
  const result = pressListItemAtOrdinal(ordinal.listIndex, ordinal.itemIndex);
2494
2220
  if (result.ok) {
2495
- console.log(
2496
- `[Appilots] uiInteractionHandler: synthetic ordinal press succeeded (container=${result.containerType ?? "List"})`
2221
+ appilotsDebugLog(
2222
+ `uiInteractionHandler: synthetic ordinal press succeeded (container=${result.containerType ?? "List"})`
2497
2223
  );
2498
2224
  return { success: true };
2499
2225
  }
@@ -2538,7 +2264,9 @@ function uiInteractionHandler(payload, context) {
2538
2264
  };
2539
2265
  }
2540
2266
  const componentId = resolveComponentId(rawComponentId, registry, actionStr);
2541
- console.log(`[Appilots] uiInteractionHandler: resolved componentId="${payload.componentId}" \u2192 "${componentId}"`);
2267
+ appilotsDebugLog(
2268
+ `uiInteractionHandler: resolved componentId="${payload.componentId}" \u2192 "${componentId}"`
2269
+ );
2542
2270
  const target = registry.getTarget(componentId);
2543
2271
  if (target) {
2544
2272
  try {
@@ -2589,7 +2317,9 @@ function uiInteractionHandler(payload, context) {
2589
2317
  return { success: false, error: err?.message ?? "Focus failed" };
2590
2318
  }
2591
2319
  }
2592
- 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
+ );
2593
2323
  const handle = findInteractiveByIdentifier(componentId, {
2594
2324
  kind: actionStr === "toggle" ? "toggle" : actionStr === "focus" ? "field" : "target"
2595
2325
  });
@@ -2597,7 +2327,9 @@ function uiInteractionHandler(payload, context) {
2597
2327
  try {
2598
2328
  if (actionStr === "press" && handle.kind === "target" && handle.press) {
2599
2329
  handle.press();
2600
- console.log(`[Appilots] uiInteractionHandler: fiber fallback press succeeded for "${componentId}"`);
2330
+ appilotsDebugLog(
2331
+ `uiInteractionHandler: fiber fallback press succeeded for "${componentId}"`
2332
+ );
2601
2333
  return { success: true };
2602
2334
  }
2603
2335
  if (actionStr === "press" && handle.kind === "toggle" && handle.setValue) {
@@ -2743,12 +2475,10 @@ function readVisibleValidation(fieldId, label) {
2743
2475
  try {
2744
2476
  const snap = captureSnapshot();
2745
2477
  const texts = snap.texts ?? [];
2746
- const needles = [fieldId, label].filter(
2747
- (s) => !!s && s.length > 0
2748
- );
2478
+ const needles = [fieldId, label].filter((s) => !!s && s.length > 0);
2749
2479
  for (const text of texts) {
2750
2480
  const lower = text.toLowerCase();
2751
- if (FORM_VALIDATION_TEXT_RE.test(lower)) {
2481
+ if (FORM_VALIDATION_TEXT_RE2.test(lower)) {
2752
2482
  for (const needle of needles) {
2753
2483
  if (lower.includes(needle.toLowerCase())) return text;
2754
2484
  }
@@ -2869,8 +2599,10 @@ async function tryPressSelectOption(fieldId, value, registry, options = {}) {
2869
2599
  function normalizeFieldType(type) {
2870
2600
  if (typeof type !== "string") return void 0;
2871
2601
  const normalized = normalize3(type);
2872
- if (normalized === "select" || normalized === "picker" || normalized === "dropdown") return "select";
2873
- 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";
2874
2606
  if (normalized === "date") return "date";
2875
2607
  if (normalized === "number" || normalized === "numeric") return "number";
2876
2608
  if (normalized === "custom") return "custom";
@@ -2904,10 +2636,7 @@ function fieldCueTerms(fieldId, label) {
2904
2636
  function snapshotHasFieldCue(snap, fieldId, label) {
2905
2637
  const terms = fieldCueTerms(fieldId, label);
2906
2638
  if (terms.length === 0) return false;
2907
- const visible = [
2908
- ...snap.texts ?? [],
2909
- ...(snap.buttons ?? []).map((b) => b.label ?? b.id ?? "")
2910
- ].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);
2911
2640
  return visible.some((text) => terms.some((term) => text.includes(term) || term.includes(text)));
2912
2641
  }
2913
2642
  function scoreCandidateText(value, texts) {
@@ -2981,9 +2710,7 @@ function buildChoiceCandidates(snap, fieldId, value, allowFirstVisibleWhenNoCue,
2981
2710
  fieldCueVisible && elements.length <= 12 ? 50 : 0,
2982
2711
  ...cueTexts.map((text) => {
2983
2712
  const normalized = normalize3(text);
2984
- return fieldTerms.some(
2985
- (term) => normalized.includes(term) || term.includes(normalized)
2986
- ) ? 80 : 0;
2713
+ return fieldTerms.some((term) => normalized.includes(term) || term.includes(normalized)) ? 80 : 0;
2987
2714
  })
2988
2715
  );
2989
2716
  const valueScore = genericValue ? 0 : scoreCandidateText(value, texts);
@@ -3055,7 +2782,11 @@ async function selectVisibleChoiceAsync(fieldId, value, registry, options) {
3055
2782
  return {
3056
2783
  success: false,
3057
2784
  error: `No visible option found for select field "${fieldId}"`,
3058
- 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
+ }
3059
2790
  };
3060
2791
  }
3061
2792
  const pressed = pressTargetByIdOrLabel(candidate.targetId, registry);
@@ -3087,17 +2818,19 @@ function pressContinuationButton(registry) {
3087
2818
  return pressTargetByIdOrLabel(id, registry);
3088
2819
  }
3089
2820
  function findField(fieldId, registry) {
3090
- console.log(`[Appilots] findField: searching for fieldId="${fieldId}"`);
2821
+ appilotsDebugLog(`findField: searching for fieldId="${fieldId}"`);
3091
2822
  const exact = registry.getField(fieldId);
3092
2823
  if (exact) {
3093
- console.log(`[Appilots] findField: FOUND exact match for fieldId="${fieldId}"`);
2824
+ appilotsDebugLog(`findField: FOUND exact match for fieldId="${fieldId}"`);
3094
2825
  return exact;
3095
2826
  }
3096
2827
  const lower = fieldId.toLowerCase();
3097
2828
  const snapshot = registry.snapshot();
3098
2829
  for (const comp of snapshot) {
3099
2830
  if (comp.kind === "field" && comp.id.toLowerCase() === lower) {
3100
- 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
+ );
3101
2834
  return registry.getField(comp.id);
3102
2835
  }
3103
2836
  }
@@ -3125,8 +2858,8 @@ function findField(fieldId, registry) {
3125
2858
  }
3126
2859
  }
3127
2860
  if (bestComp && bestScore >= 75) {
3128
- console.log(
3129
- `[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})`
3130
2863
  );
3131
2864
  return registry.getField(bestComp.id);
3132
2865
  }
@@ -3152,8 +2885,8 @@ function findField(fieldId, registry) {
3152
2885
  }
3153
2886
  }
3154
2887
  if (mBest && mBestScore >= 60) {
3155
- console.log(
3156
- `[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})`
3157
2890
  );
3158
2891
  return registry.getField(mBest.id);
3159
2892
  }
@@ -3161,8 +2894,8 @@ function findField(fieldId, registry) {
3161
2894
  }
3162
2895
  }
3163
2896
  if (bestComp && bestScore >= 40) {
3164
- console.log(
3165
- `[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})`
3166
2899
  );
3167
2900
  return registry.getField(bestComp.id);
3168
2901
  }
@@ -3172,17 +2905,19 @@ function findField(fieldId, registry) {
3172
2905
  return void 0;
3173
2906
  }
3174
2907
  function findToggle(fieldId, registry) {
3175
- console.log(`[Appilots] findToggle: searching for fieldId="${fieldId}"`);
2908
+ appilotsDebugLog(`findToggle: searching for fieldId="${fieldId}"`);
3176
2909
  const exact = registry.getToggle(fieldId);
3177
2910
  if (exact) {
3178
- console.log(`[Appilots] findToggle: FOUND exact match for "${fieldId}"`);
2911
+ appilotsDebugLog(`findToggle: FOUND exact match for "${fieldId}"`);
3179
2912
  return exact;
3180
2913
  }
3181
2914
  const lower = fieldId.toLowerCase();
3182
2915
  const snapshot = registry.snapshot();
3183
2916
  for (const comp of snapshot) {
3184
2917
  if (comp.kind === "toggle" && comp.id.toLowerCase() === lower) {
3185
- 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
+ );
3186
2921
  return registry.getToggle(comp.id);
3187
2922
  }
3188
2923
  }
@@ -3197,7 +2932,9 @@ function findSlider(fieldId, registry) {
3197
2932
  const lower = fieldId.toLowerCase();
3198
2933
  for (const comp of registry.snapshot()) {
3199
2934
  if (comp.kind === "slider" && comp.id.toLowerCase() === lower) {
3200
- 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
+ );
3201
2938
  return registry.getSlider(comp.id);
3202
2939
  }
3203
2940
  }
@@ -3229,7 +2966,7 @@ async function formFillHandler(payload, context) {
3229
2966
  "formFillHandler: registry is EMPTY \u2014 auto-tracking may not be active. Ensure enableAppilotsAutoTracking() runs before components mount."
3230
2967
  );
3231
2968
  } else {
3232
- console.log(
2969
+ appilotsDebugLog(
3233
2970
  `[Appilots formFillHandler] Registry has ${registrySnapshot.length} components:`,
3234
2971
  registrySnapshot.map((c) => `${c.kind}:${c.id}`).join(", ")
3235
2972
  );
@@ -3237,7 +2974,7 @@ async function formFillHandler(payload, context) {
3237
2974
  const fieldResults = [];
3238
2975
  let allSuccess = true;
3239
2976
  let selectedChoiceThisBatch = false;
3240
- console.log(`[Appilots] formFillHandler: processing ${payload.fields.length} fields`);
2977
+ appilotsDebugLog(`formFillHandler: processing ${payload.fields.length} fields`);
3241
2978
  const availableFieldIds = () => registry.snapshot().filter((comp) => comp.kind === "field").map((comp) => `${comp.id}${comp.label ? ` (${comp.label})` : ""}`).slice(0, 8);
3242
2979
  const isElementIdField = (field) => typeof field.fieldId === "string" && field.fieldId.trim().toLowerCase().startsWith("el:");
3243
2980
  for (const field of payload.fields.filter(isElementIdField)) {
@@ -3264,8 +3001,8 @@ async function formFillHandler(payload, context) {
3264
3001
  const { fieldId, fieldType, value } = field;
3265
3002
  const effectiveFieldType = getEffectiveFieldType(fieldId, fieldType, registry);
3266
3003
  let selectFailure;
3267
- console.log(
3268
- `[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)}`
3269
3006
  );
3270
3007
  const fieldEntry = findField(fieldId, registry);
3271
3008
  if (fieldEntry) {
@@ -3316,7 +3053,11 @@ async function formFillHandler(payload, context) {
3316
3053
  fieldId,
3317
3054
  success: false,
3318
3055
  error: selectResult.error ?? `Select value "${String(value)}" was not applied for "${fieldId}"`,
3319
- 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
+ }
3320
3061
  });
3321
3062
  allSuccess = false;
3322
3063
  continue;
@@ -3371,7 +3112,9 @@ async function formFillHandler(payload, context) {
3371
3112
  continue;
3372
3113
  }
3373
3114
  try {
3374
- sliderEntry.setValue(snapSliderValue(numeric, sliderEntry.min, sliderEntry.max, sliderEntry.step));
3115
+ sliderEntry.setValue(
3116
+ snapSliderValue(numeric, sliderEntry.min, sliderEntry.max, sliderEntry.step)
3117
+ );
3375
3118
  fieldResults.push({ fieldId, success: true });
3376
3119
  } catch (err) {
3377
3120
  fieldResults.push({
@@ -3420,8 +3163,8 @@ async function formFillHandler(payload, context) {
3420
3163
  }
3421
3164
  if (selectResult.success) {
3422
3165
  selectedChoiceThisBatch = true;
3423
- console.log(
3424
- `[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}"` : "")
3425
3168
  );
3426
3169
  await sleep2(120);
3427
3170
  fieldResults.push({ fieldId, success: true });
@@ -3429,7 +3172,7 @@ async function formFillHandler(payload, context) {
3429
3172
  }
3430
3173
  selectFailure = selectResult;
3431
3174
  }
3432
- 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`);
3433
3176
  const handle = findInteractiveByIdentifier(fieldId, {
3434
3177
  kind: effectiveFieldType === "toggle" ? "toggle" : "field"
3435
3178
  });
@@ -3437,7 +3180,9 @@ async function formFillHandler(payload, context) {
3437
3180
  try {
3438
3181
  handle.focus?.();
3439
3182
  handle.setValue(value);
3440
- 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
+ );
3441
3186
  fieldResults.push({ fieldId, success: true });
3442
3187
  continue;
3443
3188
  } catch (err) {
@@ -3479,7 +3224,15 @@ async function formFillHandler(payload, context) {
3479
3224
  );
3480
3225
  } else {
3481
3226
  await sleep2(120);
3482
- 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
+ );
3483
3236
  if (!submitResult.success) {
3484
3237
  appilotsDebugWarn(`formFillHandler: submitAfterFill failed \u2014 ${submitResult.error}`);
3485
3238
  return {
@@ -3517,7 +3270,7 @@ async function formFillHandler(payload, context) {
3517
3270
  }
3518
3271
  };
3519
3272
  }
3520
- console.log("[Appilots] formFillHandler: submitAfterFill \u2014 submit completed");
3273
+ appilotsDebugLog("formFillHandler: submitAfterFill \u2014 submit completed");
3521
3274
  }
3522
3275
  }
3523
3276
  const aggregatedDiagnose = aggregateFieldDiagnoses(fieldResults, screenPath);
@@ -3539,8 +3292,8 @@ function pressSubmitButton(context, registry) {
3539
3292
  if (target) {
3540
3293
  try {
3541
3294
  target.press();
3542
- console.log(
3543
- `[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}"` : "")
3544
3297
  );
3545
3298
  return { success: true, pressedTargetId: candidate };
3546
3299
  } catch (err) {
@@ -3564,9 +3317,7 @@ function pressSubmitButton(context, registry) {
3564
3317
  if (handle?.press) {
3565
3318
  try {
3566
3319
  handle.press();
3567
- console.log(
3568
- `[Appilots] formFillHandler: submitAfterFill \u2014 fiber fallback "${submitLabel}"`
3569
- );
3320
+ appilotsDebugLog(`formFillHandler: submitAfterFill \u2014 fiber fallback "${submitLabel}"`);
3570
3321
  return { success: true, pressedTargetId: submitId ?? submitLabel };
3571
3322
  } catch (err) {
3572
3323
  return {
@@ -3588,9 +3339,7 @@ function pressSubmitButton(context, registry) {
3588
3339
  if (id) {
3589
3340
  const pressed = pressTargetByIdOrLabel(id, registry);
3590
3341
  if (pressed.success) {
3591
- console.log(
3592
- `[Appilots] formFillHandler: submitAfterFill \u2014 pressed visible heuristic "${id}"`
3593
- );
3342
+ appilotsDebugLog(`formFillHandler: submitAfterFill \u2014 pressed visible heuristic "${id}"`);
3594
3343
  return { success: true, pressedTargetId: id };
3595
3344
  }
3596
3345
  }
@@ -3604,8 +3353,8 @@ function pressSubmitButton(context, registry) {
3604
3353
  if (target) {
3605
3354
  try {
3606
3355
  target.press();
3607
- console.log(
3608
- `[Appilots] formFillHandler: submitAfterFill \u2014 pressed by label heuristic "${comp.label}"`
3356
+ appilotsDebugLog(
3357
+ `formFillHandler: submitAfterFill \u2014 pressed by label heuristic "${comp.label}"`
3609
3358
  );
3610
3359
  return { success: true, pressedTargetId: comp.id };
3611
3360
  } catch (err) {
@@ -3617,9 +3366,16 @@ function pressSubmitButton(context, registry) {
3617
3366
  }
3618
3367
  }
3619
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
+ }
3620
3376
  return {
3621
3377
  success: false,
3622
- 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)',
3623
3379
  diagnose: { category: "component-not-found", screen: resolvedScreen }
3624
3380
  };
3625
3381
  }
@@ -3673,6 +3429,8 @@ function resolveListEntry(listId) {
3673
3429
  return { candidates: entries.map((e) => e.id) };
3674
3430
  }
3675
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] };
3676
3434
  return { candidates: entries.map((e) => e.id) };
3677
3435
  }
3678
3436
  function pageOffset(metrics, direction) {
@@ -3712,7 +3470,7 @@ async function scrollListHandler(payload, context) {
3712
3470
  }
3713
3471
  const { entry, candidates } = resolveListEntry(payload.listId);
3714
3472
  if (!entry) {
3715
- 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).";
3716
3474
  return {
3717
3475
  success: false,
3718
3476
  error: detail,
@@ -3720,6 +3478,13 @@ async function scrollListHandler(payload, context) {
3720
3478
  };
3721
3479
  }
3722
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
+ }
3723
3488
  if (typeof entry.itemCount === "number" && toIndex > entry.itemCount) {
3724
3489
  return {
3725
3490
  success: false,
@@ -3754,14 +3519,14 @@ async function scrollListHandler(payload, context) {
3754
3519
  }
3755
3520
  return {
3756
3521
  success: false,
3757
- error: `List "${entry.id}" does not expose a scrollable handle`,
3522
+ error: `"${entry.id}" does not expose a scrollable handle`,
3758
3523
  diagnose: buildDiagnose("unknown", entry.id)
3759
3524
  };
3760
3525
  }
3761
3526
 
3762
3527
  // src/executor/handlers/confirmHandler.ts
3763
3528
  async function confirmHandler(_actionId, _payload) {
3764
- return { success: true };
3529
+ return { success: true, effect: "unknown" };
3765
3530
  }
3766
3531
 
3767
3532
  // src/executor/postInteractionSettle.ts
@@ -3773,14 +3538,45 @@ function targetIdFromPayload(payload) {
3773
3538
  if (typeof targetId === "string" && targetId.trim()) return targetId.trim();
3774
3539
  return "";
3775
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
+ }
3776
3563
  async function waitForPostInteractionSettle(payload) {
3777
3564
  const targetId = targetIdFromPayload(payload);
3778
3565
  const fromScreen = getCurrentScreen();
3779
3566
  const baseline = probeLoadingState(targetId || null, fromScreen);
3567
+ const baselineFields = discreteFieldSignature();
3568
+ const readable = baseline.fingerprint !== "";
3780
3569
  await sleepMs(100);
3781
3570
  const afterPress = probeLoadingState(targetId || null, getCurrentScreen());
3782
3571
  const modalOrLoading = afterPress.modalOpen || afterPress.loading || afterPress.pressedDisabled;
3783
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
+ };
3784
3580
  const capMs = isListItemPressTarget(targetId) ? 5e3 : 1e4;
3785
3581
  if (modalOrLoading || fingerprintChanged) {
3786
3582
  await waitForLoadingSettle({
@@ -3788,7 +3584,7 @@ async function waitForPostInteractionSettle(payload) {
3788
3584
  fromScreen,
3789
3585
  maxMs: capMs
3790
3586
  });
3791
- return;
3587
+ return verdict();
3792
3588
  }
3793
3589
  if (isListItemPressTarget(targetId)) {
3794
3590
  await waitForLoadingSettle({
@@ -3796,6 +3592,7 @@ async function waitForPostInteractionSettle(payload) {
3796
3592
  maxMs: 3e3
3797
3593
  });
3798
3594
  }
3595
+ return verdict();
3799
3596
  }
3800
3597
 
3801
3598
  // src/executor/ActionExecutor.ts
@@ -3850,10 +3647,9 @@ function validateActionPayload(action) {
3850
3647
  const hasIndex = typeof payload.toIndex === "number";
3851
3648
  const hasDirection = payload.direction === "up" || payload.direction === "down";
3852
3649
  if (!hasIndex && !hasDirection) {
3853
- return invalid(
3854
- 'scroll_list action needs toIndex (1-based) or direction ("up"/"down")',
3855
- { targetId: typeof payload.listId === "string" ? payload.listId : void 0 }
3856
- );
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
+ });
3857
3653
  }
3858
3654
  return null;
3859
3655
  }
@@ -3861,20 +3657,44 @@ function validateActionPayload(action) {
3861
3657
  return null;
3862
3658
  }
3863
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
+ }
3864
3671
  async function executeAction(action, context) {
3865
- 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
+ );
3866
3675
  const permissions = context.permissions ?? DEFAULT_PERMISSIONS;
3867
- if (permissions.allowedActions && permissions.allowedActions.length > 0 && !permissions.allowedActions.includes(action.type)) {
3868
- return {
3869
- success: false,
3870
- error: `Action type "${action.type}" is not in the allowed actions list`
3871
- };
3872
- }
3873
3676
  context.emit({
3874
3677
  type: "agent:action:start",
3875
3678
  timestamp: Date.now(),
3876
3679
  data: { actionId: action.id, actionType: action.type }
3877
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
+ }
3878
3698
  let result;
3879
3699
  const validationFailure = validateActionPayload(action);
3880
3700
  if (validationFailure) {
@@ -3908,25 +3728,25 @@ async function executeAction(action, context) {
3908
3728
  registry: context.registry ?? getDefaultRegistry()
3909
3729
  });
3910
3730
  break;
3911
- case "ui_interaction":
3912
- result = uiInteractionHandler(action.payload, {
3731
+ case "ui_interaction": {
3732
+ const payload = action.payload;
3733
+ result = uiInteractionHandler(payload, {
3913
3734
  permissions,
3914
3735
  registry: context.registry ?? getDefaultRegistry()
3915
3736
  });
3916
3737
  if (result.success) {
3917
- await waitForPostInteractionSettle(action.payload);
3738
+ const settle = await waitForPostInteractionSettle(payload);
3739
+ result.effect = result.effect ?? effectFor(payload, settle.effect);
3918
3740
  }
3919
3741
  break;
3742
+ }
3920
3743
  case "scroll_list":
3921
3744
  result = await scrollListHandler(action.payload, {
3922
3745
  permissions
3923
3746
  });
3924
3747
  break;
3925
3748
  case "confirm":
3926
- result = await confirmHandler(
3927
- action.id,
3928
- action.payload
3929
- );
3749
+ result = await confirmHandler(action.id, action.payload);
3930
3750
  break;
3931
3751
  case "custom":
3932
3752
  result = {
@@ -3949,7 +3769,9 @@ async function executeAction(action, context) {
3949
3769
  error: err?.message ?? "Action execution failed unexpectedly"
3950
3770
  };
3951
3771
  }
3952
- 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
+ );
3953
3775
  context.emit({
3954
3776
  type: result.success ? "agent:action:complete" : "agent:action:error",
3955
3777
  timestamp: Date.now(),
@@ -4001,9 +3823,7 @@ var _patchedAlert = (title, message, buttons, options) => {
4001
3823
  return suppression.originalAlert(title, message, buttons, options);
4002
3824
  }
4003
3825
  suppression.consumed = true;
4004
- console.log(
4005
- "[Appilots] Suppressing native Alert during already-confirmed destructive agent action"
4006
- );
3826
+ appilotsDebugLog("Suppressing native Alert during already-confirmed destructive agent action");
4007
3827
  try {
4008
3828
  suppression.pending = Promise.resolve(button.onPress()).then(() => void 0).catch((err) => reportHandlerFailure(err));
4009
3829
  } catch (err) {
@@ -4128,13 +3948,10 @@ async function settleTurn(input) {
4128
3948
  targetScreen: navigateTargetScreen,
4129
3949
  maxMs: 3500
4130
3950
  });
4131
- console.log(
4132
- `[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}`
4133
3953
  );
4134
- const navigationMoved = settle.transitioned || screenDepartedBaseline(
4135
- input.preNavigateScreen ?? null,
4136
- input.preNavigateSignature ?? null
4137
- ) || !!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);
4138
3955
  if (!navigationMoved) {
4139
3956
  appilotsDebugWarn(
4140
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.`
@@ -4147,8 +3964,8 @@ async function settleTurn(input) {
4147
3964
  fromScreen: settle.screen ?? null,
4148
3965
  maxMs: 3e3
4149
3966
  });
4150
- console.log(
4151
- `[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}`
4152
3969
  );
4153
3970
  if (loadSettle.loadingPending) loadingPending = true;
4154
3971
  } else {
@@ -4162,8 +3979,8 @@ async function settleTurn(input) {
4162
3979
  fromScreen: getCurrentScreen(),
4163
3980
  maxMs: settleCapMs
4164
3981
  });
4165
- console.log(
4166
- `[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)" : ""}`
4167
3984
  );
4168
3985
  if (loadSettle.loadingPending) loadingPending = true;
4169
3986
  const listPressActions = turnActions.filter((action) => {
@@ -4184,8 +4001,8 @@ async function settleTurn(input) {
4184
4001
  maxMs: 4e3
4185
4002
  });
4186
4003
  const afterFingerprint = probeLoadingState(pressedId, getCurrentScreen()).fingerprint;
4187
- console.log(
4188
- `[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}`
4189
4006
  );
4190
4007
  if (!postPressSettle.transitioned && baselineFingerprint === afterFingerprint) {
4191
4008
  for (const action of listPressActions) {
@@ -4378,7 +4195,7 @@ function useSuggestedPrompts(options) {
4378
4195
  if (cancelled || requestIdRef.current !== requestId) return;
4379
4196
  setPrompts(mergePromptSources(localDev, data, limit));
4380
4197
  }).catch((err) => {
4381
- console.warn("[Appilots] useSuggestedPrompts: fetch failed", err);
4198
+ appilotsDebugWarn("useSuggestedPrompts: fetch failed", err);
4382
4199
  }).finally(() => {
4383
4200
  if (cancelled || requestIdRef.current !== requestId) return;
4384
4201
  setIsLoading(false);
@@ -4576,4 +4393,4 @@ function useAppilotsSlider(id, options) {
4576
4393
  }, [id, registry, getValue, setValue, min, max, step, label, screen]);
4577
4394
  }
4578
4395
 
4579
- 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 };