@appilots/sdk 0.11.3 → 0.13.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,6 +1,261 @@
1
- import { createSnapshotDeltaTracker, 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, isSwitchLike, idLooselyMatches, normalize3, matchScore, FORM_VALIDATION_TEXT_RE, parseStableElementId, shouldSkipSubtree, isAlwaysListContainer, isHeuristicListContainer, findListItems, getTypeName, foldForCompare, isModalFiber, switchValue, getAllScreens, snapshotShowsOpenCreateForm, isReferenceable, looksDestructiveActionLabel, isGenericSelectValue, FORM_WRITE_SUCCESS_TEXT_RE, TOUCHABLE_NAMES } from './chunk-ROXXCDIG.mjs';
2
- import React, { useMemo, useRef, useSyncExternalStore, useEffect, useCallback, useState } from 'react';
3
- import { Alert } from 'react-native';
1
+ import { createSnapshotDeltaTracker, normalizeAppilotsLocale, getFiberRoot, appilotsDebugWarn, recordIntrospectionFailure, getCurrentScreen, walkFiber, getActiveRouteNames, clampSnapshotToWireLimits, deriveInteractionElements, rectForNode, toWireElement, attachElementIdsToChoiceGroups, elementRegistry, appilotsDebugLog, getLastWalkDiagnostics, SDK_VERSION, geometryStats, geometryAvailability, getNameManglingProbe, emptyDetectorCounts, getAutoTrackingState, getIntrospectionDiagnostics, getGeometryHost, isRectOnScreen, listRegistry, resolveAgentPermissions, getDefaultRegistry, useAppilotsContext, ChatSessionMachine, getCurrentScreenSignature, ActionQueueMachine, getScreenMetadata, setCurrentScreen, useResolvedRegistry, totalDetectorHits, getOpenNativeDialog, componentRegistry, permissionDeniedMessage, probeLoadingState, getLastAnsweredNativeDialog, isListItemPressTarget, waitForLoadingSettle, describeValue, waitForScreenSettle, screenDepartedBaseline, routesBelongToSameFeature, actionPressTargetId, getNavigationStateSnapshot, getNavigationRef, isOptionalStepAutomationFailure, OPTIONAL_STEP_AUTOMATION_HINT, isSourceControlId, resolveTarget, isSwitchLike, sourceControlTarget, rectOfFiber, idLooselyMatches, normalize3, matchScore, FORM_VALIDATION_TEXT_RE, parseStableElementId, shouldSkipSubtree, isAlwaysListContainer, isHeuristicListContainer, findListItems, getTypeName, foldForCompare, isModalFiber, sourceControlId, endTextInputEditing, switchValue, getAllScreens, snapshotShowsOpenCreateForm, isReferenceable, looksDestructiveActionLabel, isGenericSelectValue, SUBMIT_CONTROL_RE, isVisibleValidationText, FORM_WRITE_SUCCESS_TEXT_RE, TOUCHABLE_NAMES } from './chunk-HRWBK35Y.mjs';
2
+ import React, { createContext, useMemo, useContext, useRef, useSyncExternalStore, useEffect, useCallback, useState } from 'react';
3
+ import { Platform, NativeModules, AppState, Alert } from 'react-native';
4
+
5
+ // src/i18n/bundles/pt-BR.ts
6
+ var ptBR = {
7
+ chatTitle: "Assistente Appilots",
8
+ clearButton: "Limpar",
9
+ inputPlaceholder: "Digite uma mensagem...",
10
+ emptyTitle: "Como posso ajudar?",
11
+ emptySubtitle: "Navego, preencho formul\xE1rios e executo a\xE7\xF5es neste app.",
12
+ thinking: "Pensando...",
13
+ statusAnalyzing: "Analisando a tela...",
14
+ statusWaitingApp: "Aguardando o app responder...",
15
+ statusAdjusting: "Ajustando o plano...",
16
+ poweredBy: "Powered by Appilots",
17
+ approve: "Aprovar",
18
+ reject: "Rejeitar",
19
+ cancelledByUser: "Cancelado pelo usu\xE1rio",
20
+ confirmDestructiveTitle: "A\xE7\xE3o destrutiva",
21
+ confirmTitle: "Confirma\xE7\xE3o necess\xE1ria",
22
+ confirmMessage: 'Tem certeza que deseja "{action}"?',
23
+ confirmDestructiveApprove: "Sim, executar",
24
+ confirmApprove: "Confirmar",
25
+ confirmCancel: "Cancelar",
26
+ confirmThisAction: "esta a\xE7\xE3o",
27
+ unknownAction: "A\xE7\xE3o desconhecida",
28
+ somethingWentWrong: "Algo deu errado",
29
+ openChatA11y: "Abrir chat do assistente",
30
+ closeChatA11y: "Fechar chat do assistente",
31
+ stopGeneratingA11y: "Parar gera\xE7\xE3o",
32
+ talkToHumanA11y: "Falar com um atendente humano",
33
+ humanAgentLabel: "Atendente",
34
+ escalationRequested: "Certo! Chamei um atendente humano. Voc\xEA pode continuar usando o app normalmente enquanto isso.",
35
+ escalationConnected: "Um atendente entrou na conversa.",
36
+ escalationResolved: "O atendimento humano foi encerrado. Voltei a te ajudar por aqui.",
37
+ escalationOffer: "Se preferir, posso chamar uma pessoa para ajudar.",
38
+ escalationOfferChip: "Falar com um atendente",
39
+ escalationPendingBanner: "Aguardando um atendente...",
40
+ escalationActiveBanner: "Voc\xEA est\xE1 falando com um atendente",
41
+ pillWorking: "Trabalhando...",
42
+ pillExpandA11y: "Abrir a conversa do assistente",
43
+ pillStopA11y: "Parar o assistente",
44
+ missionPause: "Pausar",
45
+ missionResume: "Retomar tarefa",
46
+ missionCancel: "Cancelar tarefa",
47
+ missionPaused: "Pausada",
48
+ pillDone: "Pronto \u2014 toque para ver",
49
+ micStartA11y: "Segurar para falar",
50
+ micStopA11y: "Soltar para enviar",
51
+ micListening: "Ouvindo..."
52
+ };
53
+
54
+ // src/i18n/bundles/en.ts
55
+ var en = {
56
+ chatTitle: "Appilots Assistant",
57
+ clearButton: "Clear",
58
+ inputPlaceholder: "Type a message...",
59
+ emptyTitle: "How can I help?",
60
+ emptySubtitle: "I navigate, fill forms, and run actions in this app.",
61
+ thinking: "Thinking...",
62
+ statusAnalyzing: "Reading the screen...",
63
+ statusWaitingApp: "Waiting for the app...",
64
+ statusAdjusting: "Adjusting the plan...",
65
+ poweredBy: "Powered by Appilots",
66
+ approve: "Approve",
67
+ reject: "Reject",
68
+ cancelledByUser: "Cancelled by user",
69
+ confirmDestructiveTitle: "Destructive action",
70
+ confirmTitle: "Confirmation needed",
71
+ confirmMessage: 'Are you sure you want to "{action}"?',
72
+ confirmDestructiveApprove: "Yes, do it",
73
+ confirmApprove: "Confirm",
74
+ confirmCancel: "Cancel",
75
+ confirmThisAction: "this action",
76
+ unknownAction: "Unknown action",
77
+ somethingWentWrong: "Something went wrong",
78
+ openChatA11y: "Open assistant chat",
79
+ closeChatA11y: "Close assistant chat",
80
+ stopGeneratingA11y: "Stop generating",
81
+ talkToHumanA11y: "Talk to a human agent",
82
+ humanAgentLabel: "Agent",
83
+ escalationRequested: "Done! I've called a human agent. You can keep using the app normally in the meantime.",
84
+ escalationConnected: "A human agent joined the conversation.",
85
+ escalationResolved: "The human handoff has ended. I'm back to help you here.",
86
+ escalationOffer: "If you prefer, I can bring in a person to help.",
87
+ escalationOfferChip: "Talk to a human",
88
+ escalationPendingBanner: "Waiting for a human agent...",
89
+ escalationActiveBanner: "You're talking to a human agent",
90
+ pillWorking: "Working...",
91
+ pillExpandA11y: "Open the assistant conversation",
92
+ pillStopA11y: "Stop the assistant",
93
+ missionPause: "Pause",
94
+ missionResume: "Resume task",
95
+ missionCancel: "Cancel task",
96
+ missionPaused: "Paused",
97
+ pillDone: "Done \u2014 tap to read",
98
+ micStartA11y: "Hold to talk",
99
+ micStopA11y: "Release to send",
100
+ micListening: "Listening..."
101
+ };
102
+
103
+ // src/i18n/bundles/es.ts
104
+ var es = {
105
+ chatTitle: "Asistente Appilots",
106
+ clearButton: "Limpiar",
107
+ inputPlaceholder: "Escribe un mensaje...",
108
+ emptyTitle: "\xBFEn qu\xE9 puedo ayudarte?",
109
+ emptySubtitle: "Navego, relleno formularios y ejecuto acciones en esta app.",
110
+ thinking: "Pensando...",
111
+ statusAnalyzing: "Analizando la pantalla...",
112
+ statusWaitingApp: "Esperando a la app...",
113
+ statusAdjusting: "Ajustando el plan...",
114
+ poweredBy: "Hecho con Appilots",
115
+ approve: "Aprobar",
116
+ reject: "Rechazar",
117
+ cancelledByUser: "Cancelado por el usuario",
118
+ confirmDestructiveTitle: "Acci\xF3n destructiva",
119
+ confirmTitle: "Confirmaci\xF3n necesaria",
120
+ confirmMessage: '\xBFSeguro que quieres "{action}"?',
121
+ confirmDestructiveApprove: "S\xED, ejecutar",
122
+ confirmApprove: "Confirmar",
123
+ confirmCancel: "Cancelar",
124
+ confirmThisAction: "esta acci\xF3n",
125
+ unknownAction: "Acci\xF3n desconocida",
126
+ somethingWentWrong: "Algo sali\xF3 mal",
127
+ openChatA11y: "Abrir chat del asistente",
128
+ closeChatA11y: "Cerrar chat del asistente",
129
+ stopGeneratingA11y: "Detener generaci\xF3n",
130
+ talkToHumanA11y: "Hablar con un agente humano",
131
+ humanAgentLabel: "Agente",
132
+ escalationRequested: "\xA1Listo! Llam\xE9 a un agente humano. Mientras tanto puedes seguir usando la app con normalidad.",
133
+ escalationConnected: "Un agente humano se uni\xF3 a la conversaci\xF3n.",
134
+ escalationResolved: "La atenci\xF3n humana termin\xF3. Vuelvo a ayudarte por aqu\xED.",
135
+ escalationOffer: "Si prefieres, puedo llamar a una persona para ayudarte.",
136
+ escalationOfferChip: "Hablar con un agente",
137
+ escalationPendingBanner: "Esperando a un agente...",
138
+ escalationActiveBanner: "Est\xE1s hablando con un agente humano",
139
+ pillWorking: "Trabajando...",
140
+ pillExpandA11y: "Abrir la conversaci\xF3n del asistente",
141
+ pillStopA11y: "Detener el asistente",
142
+ missionPause: "Pausar",
143
+ missionResume: "Reanudar tarea",
144
+ missionCancel: "Cancelar tarea",
145
+ missionPaused: "Pausada",
146
+ pillDone: "Listo \u2014 toca para ver",
147
+ micStartA11y: "Mantener para hablar",
148
+ micStopA11y: "Soltar para enviar",
149
+ micListening: "Escuchando..."
150
+ };
151
+
152
+ // src/i18n/bundles/fr.ts
153
+ var fr = {
154
+ chatTitle: "Assistant Appilots",
155
+ clearButton: "Effacer",
156
+ inputPlaceholder: "\xC9crivez un message\u2026",
157
+ emptyTitle: "Comment puis-je vous aider ?",
158
+ emptySubtitle: "Je navigue, remplis les formulaires et effectue des actions dans cette application.",
159
+ thinking: "R\xE9flexion en cours\u2026",
160
+ statusAnalyzing: "Lecture de l\u2019\xE9cran\u2026",
161
+ statusWaitingApp: "En attente de l\u2019application\u2026",
162
+ statusAdjusting: "Ajustement du plan\u2026",
163
+ poweredBy: "Propuls\xE9 par Appilots",
164
+ approve: "Approuver",
165
+ reject: "Refuser",
166
+ cancelledByUser: "Annul\xE9 par l\u2019utilisateur",
167
+ confirmDestructiveTitle: "Action destructive",
168
+ confirmTitle: "Confirmation requise",
169
+ confirmMessage: "Voulez-vous vraiment \xAB {action} \xBB ?",
170
+ confirmDestructiveApprove: "Oui, ex\xE9cuter",
171
+ confirmApprove: "Confirmer",
172
+ confirmCancel: "Annuler",
173
+ confirmThisAction: "cette action",
174
+ unknownAction: "Action inconnue",
175
+ somethingWentWrong: "Une erreur est survenue",
176
+ openChatA11y: "Ouvrir la conversation avec l\u2019assistant",
177
+ closeChatA11y: "Fermer la conversation",
178
+ stopGeneratingA11y: "Arr\xEAter la g\xE9n\xE9ration",
179
+ talkToHumanA11y: "Parler \xE0 un conseiller",
180
+ humanAgentLabel: "Conseiller",
181
+ escalationRequested: "J\u2019ai contact\xE9 un conseiller. Vous pouvez continuer \xE0 utiliser l\u2019application en attendant.",
182
+ escalationConnected: "Un conseiller a rejoint la conversation.",
183
+ escalationResolved: "L\u2019intervention du conseiller est termin\xE9e. Je peux \xE0 nouveau vous aider.",
184
+ escalationOffer: "Si vous pr\xE9f\xE9rez, je peux faire appel \xE0 un conseiller.",
185
+ escalationOfferChip: "Parler \xE0 un conseiller",
186
+ escalationPendingBanner: "En attente d\u2019un conseiller\u2026",
187
+ escalationActiveBanner: "Vous discutez avec un conseiller",
188
+ pillWorking: "En cours\u2026",
189
+ pillExpandA11y: "Ouvrir la conversation avec l\u2019assistant",
190
+ pillStopA11y: "Arr\xEAter l\u2019assistant",
191
+ missionPause: "Mettre en pause",
192
+ missionResume: "Reprendre la t\xE2che",
193
+ missionCancel: "Annuler la t\xE2che",
194
+ missionPaused: "En pause",
195
+ pillDone: "Termin\xE9 \u2014 touchez pour lire",
196
+ micStartA11y: "Maintenez pour parler",
197
+ micStopA11y: "Rel\xE2chez pour envoyer",
198
+ micListening: "\xC9coute en cours\u2026"
199
+ };
200
+
201
+ // src/i18n/I18nProvider.tsx
202
+ var BUNDLES = {
203
+ "pt-BR": ptBR,
204
+ en,
205
+ es,
206
+ fr
207
+ };
208
+ var DEFAULT_LOCALE = "en";
209
+ function detectDeviceLocale() {
210
+ try {
211
+ let raw;
212
+ if (Platform.OS === "ios") {
213
+ const manager = NativeModules?.SettingsManager;
214
+ const settings = manager?.getConstants?.()?.settings ?? manager?.settings;
215
+ const languages = Array.isArray(settings?.AppleLanguages) ? settings.AppleLanguages : [];
216
+ for (const language of languages) {
217
+ const locale = normalizeAppilotsLocale(language);
218
+ if (locale) return locale;
219
+ }
220
+ raw = settings?.AppleLocale;
221
+ } else if (Platform.OS === "android") {
222
+ const manager = NativeModules?.I18nManager;
223
+ raw = manager?.getConstants?.()?.localeIdentifier ?? manager?.localeIdentifier;
224
+ }
225
+ if (!raw || typeof raw !== "string") return null;
226
+ return normalizeAppilotsLocale(raw) ?? null;
227
+ } catch {
228
+ return null;
229
+ }
230
+ }
231
+ function resolveLocale(prop) {
232
+ if (prop && BUNDLES[prop]) return prop;
233
+ const detected = detectDeviceLocale();
234
+ if (detected && BUNDLES[detected]) return detected;
235
+ return DEFAULT_LOCALE;
236
+ }
237
+ var I18nContext = createContext(null);
238
+ function AppilotsI18nProvider({ locale, overrides, children }) {
239
+ const value = useMemo(() => {
240
+ const resolved = resolveLocale(locale ?? null);
241
+ const merged = overrides ? { ...BUNDLES[resolved], ...overrides } : BUNDLES[resolved];
242
+ return {
243
+ locale: resolved,
244
+ strings: merged,
245
+ t: (key) => merged[key]
246
+ };
247
+ }, [locale, overrides]);
248
+ return /* @__PURE__ */ React.createElement(I18nContext.Provider, { value }, children);
249
+ }
250
+ function useAppilotsI18n() {
251
+ const ctx = useContext(I18nContext);
252
+ if (ctx) return ctx;
253
+ return {
254
+ locale: DEFAULT_LOCALE,
255
+ strings: BUNDLES[DEFAULT_LOCALE],
256
+ t: (key) => BUNDLES[DEFAULT_LOCALE][key]
257
+ };
258
+ }
4
259
 
5
260
  // src/introspection/captureSnapshot.ts
6
261
  function normalizeSliderKey(value) {
@@ -74,20 +329,35 @@ function mergeRegisteredLists(snapshot) {
74
329
  }
75
330
  for (const entry of registered) {
76
331
  if (entry.kind === "scroll") continue;
332
+ const existing = byId.get(entry.id);
333
+ if (!existing && !(entry.screen && entry.screen === snapshot.route)) continue;
77
334
  const metrics = entry.getScrollMetrics?.();
78
335
  const scrollFacts = metrics ? {
79
336
  scrollOffsetY: Math.max(0, Math.round(metrics.offset)),
80
337
  canScrollUp: metrics.offset > 8,
81
338
  canScrollDown: metrics.offset + metrics.visibleLength < metrics.contentLength - 8
82
339
  } : void 0;
83
- const existing = byId.get(entry.id);
84
340
  if (existing) {
85
341
  existing.itemCount = existing.itemCount ?? entry.itemCount;
342
+ existing.totalItemCount = existing.totalItemCount ?? entry.totalItemCount;
86
343
  existing.refreshing = existing.refreshing ?? entry.refreshing;
87
344
  existing.empty = existing.empty ?? entry.empty;
88
345
  existing.label = existing.label ?? entry.label;
89
346
  existing.source = existing.source ?? "auto-tracked";
90
347
  existing.visibleItemCount = existing.items.length;
348
+ const viewport = entry.measureViewport?.();
349
+ if (viewport && viewport.width > 0 && viewport.height > 0) {
350
+ let measured = 0;
351
+ let inViewport = 0;
352
+ for (const item of existing.items) {
353
+ const rect = rectForNode(item);
354
+ if (!rect) continue;
355
+ measured++;
356
+ item.onScreen = rect.width > 0 && rect.height > 0 && rect.x < viewport.x + viewport.width && rect.x + rect.width > viewport.x && rect.y < viewport.y + viewport.height && rect.y + rect.height > viewport.y;
357
+ if (item.onScreen) inViewport++;
358
+ }
359
+ if (measured === existing.items.length) existing.viewportItemCount = inViewport;
360
+ }
91
361
  if (entry.dataPreview && entry.dataPreview.length > 0) {
92
362
  existing.dataPreview = entry.dataPreview;
93
363
  }
@@ -104,6 +374,7 @@ function mergeRegisteredLists(snapshot) {
104
374
  containerType: entry.component,
105
375
  source: "registry",
106
376
  itemCount: entry.itemCount,
377
+ totalItemCount: entry.totalItemCount,
107
378
  visibleItemCount: 0,
108
379
  refreshing: entry.refreshing,
109
380
  empty: entry.empty,
@@ -169,6 +440,43 @@ function recordBlindWalk() {
169
440
  if (totalDetectorHits(walk.detectors) > 0) return;
170
441
  recordIntrospectionFailure("walk-recognized-nothing", null);
171
442
  }
443
+ function mergeRegisteredFieldValues(snapshot) {
444
+ const present = new Set(snapshot.inputs.map((i) => normalizeFieldKey(i.id)));
445
+ const onScreenIds = [
446
+ ...snapshot.buttons.map((b) => (b.id ?? "").toLowerCase()),
447
+ ...snapshot.toggles.map((t) => (t.id ?? "").toLowerCase())
448
+ ].filter(Boolean);
449
+ const onScreenLabels = new Set(
450
+ [...snapshot.buttons.map((b) => b.label), ...snapshot.texts].filter((l) => !!l).map((l) => l.toLowerCase())
451
+ );
452
+ for (const { id, kind, screen, label } of componentRegistry.snapshot()) {
453
+ if (kind !== "field") continue;
454
+ const key = normalizeFieldKey(id);
455
+ if (!key || present.has(key)) continue;
456
+ const entry = componentRegistry.getField(id);
457
+ if (!entry) continue;
458
+ const fieldType = entry.fieldType ?? "";
459
+ if (fieldType === "text" || fieldType === "") continue;
460
+ const declaresRoute = typeof screen === "string" && screen === snapshot.route;
461
+ if (screen && !declaresRoute && !getActiveRouteNames().includes(screen)) continue;
462
+ const idEvidence = onScreenIds.some((candidate) => candidate.split(/[^a-z0-9]+/).includes(key));
463
+ const labelEvidence = !!entry.label && onScreenLabels.has(entry.label.toLowerCase());
464
+ if (!declaresRoute && !idEvidence && !labelEvidence) continue;
465
+ let value;
466
+ try {
467
+ value = String(entry.getValue() ?? "");
468
+ } catch {
469
+ continue;
470
+ }
471
+ snapshot.inputs.push({
472
+ id,
473
+ label: entry.label ?? label,
474
+ value,
475
+ type: fieldType
476
+ });
477
+ present.add(key);
478
+ }
479
+ }
172
480
  function captureSnapshot() {
173
481
  const root = getFiberRoot();
174
482
  if (!root) {
@@ -201,6 +509,7 @@ function captureSnapshot() {
201
509
  mergeNativeDialog(snapshot);
202
510
  reconcileSliders(snapshot);
203
511
  canonicalizeRegisteredFields(snapshot);
512
+ mergeRegisteredFieldValues(snapshot);
204
513
  clampSnapshotToWireLimits(snapshot);
205
514
  const entries = deriveInteractionElements(snapshot, { rectOf: rectForNode });
206
515
  const rectById = new Map(
@@ -303,6 +612,11 @@ function flipSwitchFiber(fiber, next) {
303
612
  return { ok: true, via: handler.via };
304
613
  }
305
614
 
615
+ // src/introspection/positionalIdentity.ts
616
+ function isPositionalControlId(value) {
617
+ return /^(?:list-\d+-item-\d+-)?(?:btn|input|slider|toggle)-\d+$/i.test(value.trim());
618
+ }
619
+
306
620
  // src/introspection/findInteractive.ts
307
621
  function getTypeName2(type) {
308
622
  if (!type) return null;
@@ -390,6 +704,11 @@ function shouldSkipSubtree2(fiber) {
390
704
  var normalize = foldForCompare;
391
705
  function scoreMatchDetail(fiber, queryNorm, queryRaw) {
392
706
  const props = fiber.memoizedProps ?? {};
707
+ if (isSourceControlId(queryRaw)) {
708
+ const matches = sourceControlId(fiber) === queryRaw;
709
+ const disabled = props.disabled === true || props.accessibilityState?.disabled === true;
710
+ return { score: matches ? disabled ? 60 : 120 : 0, named: matches, disabled };
711
+ }
393
712
  const candidates = [];
394
713
  if (typeof props.testID === "string") candidates.push({ val: props.testID, weight: 100 });
395
714
  if (typeof props.accessibilityLabel === "string")
@@ -408,11 +727,11 @@ function scoreMatchDetail(fiber, queryNorm, queryRaw) {
408
727
  if (!vNorm) continue;
409
728
  if (val === queryRaw) best = Math.max(best, weight + 20);
410
729
  else if (vNorm === queryNorm) best = Math.max(best, weight + 10);
411
- else if (queryNorm && (vNorm.includes(queryNorm) || queryNorm.includes(vNorm)))
730
+ else if (!isPositionalControlId(queryRaw) && queryNorm && (vNorm.includes(queryNorm) || queryNorm.includes(vNorm)))
412
731
  best = Math.max(best, weight - 20);
413
732
  }
414
733
  const named = best >= IDENTITY_MATCH_SCORE;
415
- const disabledProp = props.disabled === true;
734
+ const disabledProp = props.disabled === true || props.editable === false || props.readOnly === true;
416
735
  const disabledA11y = typeof props.accessibilityState === "object" && props.accessibilityState !== null && props.accessibilityState.disabled === true;
417
736
  if (best > 0 && disabledProp) {
418
737
  best = Math.max(1, best - 60);
@@ -422,12 +741,18 @@ function scoreMatchDetail(fiber, queryNorm, queryRaw) {
422
741
  }
423
742
  return { score: best, named, disabled: disabledProp || disabledA11y };
424
743
  }
744
+ var inputIdentities = /* @__PURE__ */ new WeakMap();
425
745
  function wrapTextInput(fiber) {
746
+ const identity = inputIdentities.get(fiber) ?? (fiber.alternate && inputIdentities.get(fiber.alternate)) ?? {};
747
+ inputIdentities.set(fiber, identity);
748
+ if (fiber.alternate) inputIdentities.set(fiber.alternate, identity);
426
749
  const getProps = () => fiber.memoizedProps ?? {};
427
750
  return {
428
751
  kind: "field",
752
+ controlIdentity: identity,
429
753
  label: getProps().accessibilityLabel ?? getProps().label ?? getProps().placeholder ?? getProps().testID,
430
754
  getValue: () => getProps().value,
755
+ finishEditing: () => endTextInputEditing(getProps()),
431
756
  setValue: (value) => {
432
757
  const { onChangeText, onChange } = getProps();
433
758
  if (typeof onChangeText === "function") {
@@ -508,10 +833,10 @@ function describeCandidate(fiber, kind) {
508
833
  const props = fiber?.memoizedProps ?? {};
509
834
  return props.testID ?? props.accessibilityLabel ?? props.placeholder ?? (kind === "target" ? findChildText(fiber) : void 0);
510
835
  }
511
- function isDescendantOf(fiber, ancestor) {
836
+ function isDescendantOf(fiber, ancestor, parents) {
512
837
  if (!fiber || !ancestor) return false;
513
- let node = fiber.return;
514
- for (let depth = 0; node && depth < 200; depth++, node = node.return) {
838
+ let node = parents.get(fiber);
839
+ for (let depth = 0; node && depth < 200; depth++, node = parents.get(node)) {
515
840
  if (node === ancestor) return true;
516
841
  }
517
842
  return false;
@@ -532,13 +857,15 @@ function findInteractiveByIdentifier(query, options = {}) {
532
857
  let bestDisabled = false;
533
858
  let tied = [];
534
859
  const visited = /* @__PURE__ */ new WeakSet();
860
+ const parents = /* @__PURE__ */ new WeakMap();
535
861
  const stack = [];
536
- if (root.child) stack.push(root.child);
862
+ if (root.child) stack.push({ fiber: root.child, parent: root });
537
863
  while (stack.length > 0) {
538
- const fiber = stack.pop();
864
+ const { fiber, parent } = stack.pop();
539
865
  if (!fiber || visited.has(fiber)) continue;
540
866
  visited.add(fiber);
541
- if (fiber.sibling) stack.push(fiber.sibling);
867
+ parents.set(fiber, parent);
868
+ if (fiber.sibling) stack.push({ fiber: fiber.sibling, parent });
542
869
  if (shouldSkipSubtree2(fiber)) continue;
543
870
  const props = fiber.memoizedProps ?? {};
544
871
  let kind = null;
@@ -552,7 +879,7 @@ function findInteractiveByIdentifier(query, options = {}) {
552
879
  if (score > bestScore) tied = [{ fiber, kind }];
553
880
  else if (score === bestScore) tied.push({ fiber, kind });
554
881
  }
555
- const beatsBest = score > bestScore || score > 0 && score === bestScore && isDescendantOf(fiber, bestFiber);
882
+ const beatsBest = score > bestScore || score > 0 && score === bestScore && isDescendantOf(fiber, bestFiber, parents);
556
883
  if (beatsBest) {
557
884
  bestScore = score;
558
885
  bestFiber = fiber;
@@ -561,11 +888,15 @@ function findInteractiveByIdentifier(query, options = {}) {
561
888
  bestId = describeCandidate(fiber, kind);
562
889
  if (kind === "field") bestHandle = wrapTextInput(fiber);
563
890
  else if (kind === "toggle") bestHandle = wrapSwitch(fiber);
564
- else bestHandle = wrapTouchable(fiber, findChildText(fiber));
891
+ else if (isSourceControlId(queryRaw)) {
892
+ const bound = sourceControlTarget(fiber);
893
+ if (!bound || bound.disabled) bestHandle = null;
894
+ else bestHandle = wrapTouchable(bound.fiber, findChildText(fiber));
895
+ } else bestHandle = wrapTouchable(fiber, findChildText(fiber));
565
896
  }
566
897
  }
567
898
  if (kind === "toggle") continue;
568
- if (fiber.child) stack.push(fiber.child);
899
+ if (fiber.child) stack.push({ fiber: fiber.child, parent: fiber });
569
900
  }
570
901
  if (bestHandle && bestScore < (options.minScore ?? 0)) {
571
902
  appilotsDebugLog(
@@ -576,12 +907,14 @@ function findInteractiveByIdentifier(query, options = {}) {
576
907
  if (bestHandle) {
577
908
  const independent = tied.filter(
578
909
  (candidate) => !tied.some(
579
- (other) => other.fiber !== candidate.fiber && isDescendantOf(other.fiber, candidate.fiber)
910
+ (other) => other.fiber !== candidate.fiber && isDescendantOf(other.fiber, candidate.fiber, parents)
580
911
  )
581
912
  );
582
913
  bestHandle.matchCount = independent.length;
583
914
  bestHandle.named = bestNamed;
584
915
  bestHandle.disabled = bestDisabled;
916
+ const measured = bestFiber;
917
+ bestHandle.measure = () => rectOfFiber(measured);
585
918
  const candidates = independent.map(({ fiber, kind }) => describeCandidate(fiber, kind)).filter((id) => typeof id === "string" && id.length > 0).slice(0, 8);
586
919
  if (candidates.length > 0) bestHandle.matchCandidates = candidates;
587
920
  appilotsDebugLog(
@@ -593,6 +926,119 @@ function findInteractiveByIdentifier(query, options = {}) {
593
926
  return bestHandle;
594
927
  }
595
928
 
929
+ // src/introspection/spotlightTarget.ts
930
+ var listeners = /* @__PURE__ */ new Set();
931
+ var current = null;
932
+ var seq = 0;
933
+ var expiry = null;
934
+ var SPOTLIGHT_TTL_MS = 1200;
935
+ var mode = "off";
936
+ function setSpotlightMode(next) {
937
+ mode = next;
938
+ }
939
+ function getSpotlightMode() {
940
+ return mode;
941
+ }
942
+ function emit() {
943
+ for (const listener of listeners) listener(current);
944
+ }
945
+ function setSpotlightTarget(rect, label) {
946
+ if (expiry) {
947
+ clearTimeout(expiry);
948
+ expiry = null;
949
+ }
950
+ if (!rect) {
951
+ appilotsDebugLog(
952
+ `setSpotlightTarget: cleared${label ? ` (was about to mark "${label}")` : ""}`
953
+ );
954
+ if (current === null) return;
955
+ current = null;
956
+ emit();
957
+ return;
958
+ }
959
+ current = { rect, label, seq: ++seq };
960
+ appilotsDebugLog(
961
+ `setSpotlightTarget: seq=${current.seq} label="${label ?? ""}" rect=${Math.round(rect.x)},${Math.round(rect.y)} ${Math.round(rect.width)}x${Math.round(rect.height)}`
962
+ );
963
+ emit();
964
+ expiry = setTimeout(() => {
965
+ expiry = null;
966
+ current = null;
967
+ emit();
968
+ }, SPOTLIGHT_TTL_MS);
969
+ }
970
+ function getSpotlightTarget() {
971
+ return current;
972
+ }
973
+ function subscribeSpotlight(listener) {
974
+ listeners.add(listener);
975
+ return () => {
976
+ listeners.delete(listener);
977
+ };
978
+ }
979
+
980
+ // src/introspection/scrollIntoView.ts
981
+ var REVEAL_MARGIN = 24;
982
+ function offsetToReveal({ rect, viewport, metrics }) {
983
+ const maxOffset = Math.max(0, metrics.contentLength - metrics.visibleLength);
984
+ const viewTop = viewport.top + REVEAL_MARGIN;
985
+ const viewBottom = viewport.top + viewport.height - REVEAL_MARGIN;
986
+ const tallerThanViewport = rect.height > viewport.height - REVEAL_MARGIN * 2;
987
+ let delta;
988
+ if (tallerThanViewport) {
989
+ if (rect.y >= viewTop && rect.y <= viewBottom) return null;
990
+ delta = rect.y - viewTop;
991
+ } else if (rect.y < viewTop) {
992
+ delta = rect.y - viewTop;
993
+ } else if (rect.y + rect.height > viewBottom) {
994
+ delta = rect.y + rect.height - viewBottom;
995
+ } else {
996
+ return null;
997
+ }
998
+ const target = clamp(metrics.offset + delta, 0, maxOffset);
999
+ if (Math.abs(target - metrics.offset) < 1) return null;
1000
+ return target;
1001
+ }
1002
+ function clamp(value, min, max) {
1003
+ return Math.min(max, Math.max(min, value));
1004
+ }
1005
+
1006
+ // src/introspection/revealTarget.ts
1007
+ function revealTarget(rect) {
1008
+ const host = getGeometryHost();
1009
+ if (!host) return { rect, scrolled: false };
1010
+ if (isRectOnScreen(rect, host.window)) return { rect, scrolled: false };
1011
+ const scrollable = resolveScrollable();
1012
+ if (!scrollable) return { rect, scrolled: false };
1013
+ const metrics = scrollable.getScrollMetrics?.();
1014
+ if (!metrics) return { rect, scrolled: false };
1015
+ const box = scrollable.measureViewport?.();
1016
+ const viewport = box ? { top: box.y, height: box.height } : { top: 0, height: host.window.height };
1017
+ const target = offsetToReveal({ rect, viewport, metrics });
1018
+ if (target === null) return { rect, scrolled: false };
1019
+ const moved = scrollable.scrollToOffset?.(target) ?? false;
1020
+ if (!moved) {
1021
+ appilotsDebugLog(`revealTarget: "${scrollable.id}" refused scrollToOffset(${target})`);
1022
+ return { rect, scrolled: false };
1023
+ }
1024
+ const delta = target - metrics.offset;
1025
+ appilotsDebugLog(
1026
+ `revealTarget: scrolled "${scrollable.id}" by ${Math.round(delta)}px (${Math.round(metrics.offset)} \u2192 ${Math.round(target)})`
1027
+ );
1028
+ return { rect: { ...rect, y: rect.y - delta }, scrolled: true };
1029
+ }
1030
+ function resolveScrollable() {
1031
+ const screen = getCurrentScreen();
1032
+ const candidates = listRegistry.snapshot().filter((entry) => entry.scrollToOffset && entry.getScrollMetrics).filter((entry) => !screen || !entry.screen || entry.screen === screen);
1033
+ if (candidates.length === 1) return candidates[0];
1034
+ if (candidates.length > 1) {
1035
+ appilotsDebugLog(
1036
+ `revealTarget: ${candidates.length} scrollables on "${screen ?? "?"}" \u2014 cannot tell which holds the target, so not scrolling.`
1037
+ );
1038
+ }
1039
+ return null;
1040
+ }
1041
+
596
1042
  // src/executor/handlers/navigateHandler.ts
597
1043
  var _reportedMissingNavRef = false;
598
1044
  function reportMissingNavigationRef() {
@@ -635,7 +1081,18 @@ function screenExistsAnywhere(state, screenName) {
635
1081
  return false;
636
1082
  }
637
1083
  function safeNavigate(nav, screenName, params, path) {
1084
+ const state = nav.getState?.();
1085
+ const rootState = nav.getRootState?.() ?? state;
638
1086
  if (path && path.length > 0) {
1087
+ if (Array.isArray(rootState?.routeNames) && !screenExistsAnywhere(rootState, path[0]) && !state?.routeNames?.includes(path[0])) {
1088
+ if (state?.routeNames?.includes(screenName)) {
1089
+ nav.navigate(screenName, params);
1090
+ return;
1091
+ }
1092
+ throw new Error(
1093
+ `Route "${path[0]}" is not registered in any navigator. Cannot resolve the supplied path to "${screenName}".`
1094
+ );
1095
+ }
639
1096
  const { root, nestedParams } = buildNestedParams(path, screenName, params);
640
1097
  appilotsDebugLog(
641
1098
  `safeNavigate: using path-based navigation \u2014 root="${root}", nested=${JSON.stringify(nestedParams)}`
@@ -643,12 +1100,10 @@ function safeNavigate(nav, screenName, params, path) {
643
1100
  nav.navigate(root, nestedParams);
644
1101
  return;
645
1102
  }
646
- const state = nav.getState?.();
647
1103
  if (state?.routeNames?.includes(screenName)) {
648
1104
  nav.navigate(screenName, params);
649
1105
  return;
650
1106
  }
651
- const rootState = nav.getRootState?.() ?? state;
652
1107
  if (!screenExistsAnywhere(rootState, screenName)) {
653
1108
  throw new Error(
654
1109
  `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.`
@@ -670,8 +1125,8 @@ function navigateHandler(payload, context) {
670
1125
  if (!permissions.canNavigate) {
671
1126
  return {
672
1127
  success: false,
673
- error: "Navigation is not permitted",
674
- diagnose: { category: "unknown", screen: payload.screenName }
1128
+ error: permissionDeniedMessage("Navigation", "canNavigate"),
1129
+ diagnose: { category: "unknown", screen: payload.screenName, recoverable: false }
675
1130
  };
676
1131
  }
677
1132
  const isGoBack = payload.navigationAction === "goBack";
@@ -1028,8 +1483,10 @@ function pressResolvedListItem(list, itemFiber, listIndex, itemIndex) {
1028
1483
  (candidate) => candidate !== touchable && containsFiber(touchable, candidate)
1029
1484
  );
1030
1485
  if (primaryWrapsNestedTargets) {
1486
+ const outer = describeTouchable(touchable) ?? "?";
1487
+ const nested = safeIdentifiableTouchables.filter((candidate) => candidate !== touchable && containsFiber(touchable, candidate)).map((candidate) => describeTouchable(candidate) ?? "?").slice(0, 6);
1031
1488
  appilotsDebugLog(
1032
- `listItemDispatch: item "list-${listIndex}-item-${itemIndex}" has nested pressables; pressing outer row target`
1489
+ `listItemDispatch: item "list-${listIndex}-item-${itemIndex}" has nested pressables; pressing outer row target [outer=${outer}] [nested=${nested.join(", ")}] [outerHasOnPress=${typeof touchable?.memoizedProps?.onPress === "function"}]`
1033
1490
  );
1034
1491
  } else {
1035
1492
  const candidates = safeIdentifiableTouchables.map(describeTouchable).filter((d) => !!d).slice(0, 8);
@@ -1096,10 +1553,12 @@ function scoreItemCandidate(item, zeroBasedIndex, locator) {
1096
1553
  if (locator.itemKey) {
1097
1554
  if (itemKey === locator.itemKey) score += 1e3;
1098
1555
  else if (itemKey && normalize2(itemKey) === normalize2(locator.itemKey)) score += 950;
1556
+ else return 0;
1099
1557
  }
1100
1558
  if (locator.reactKey) {
1101
1559
  if (reactKey === locator.reactKey) score += 900;
1102
1560
  else if (reactKey && normalize2(reactKey) === normalize2(locator.reactKey)) score += 850;
1561
+ else if (!locator.itemKey) return 0;
1103
1562
  }
1104
1563
  if (locator.itemIndex && zeroBasedIndex === locator.itemIndex - 1) score += 750;
1105
1564
  if (locator.itemIndex && markedIndex !== void 0 && (markedIndex === locator.itemIndex || markedIndex === locator.itemIndex - 1)) {
@@ -1169,7 +1628,7 @@ function resolveListItemByIdentity(locator) {
1169
1628
  scoredCandidates.sort((a, b) => b.score - a.score);
1170
1629
  const best = scoredCandidates[0];
1171
1630
  const tiedCandidates = scoredCandidates.filter((c) => c.score === best.score);
1172
- if (tiedCandidates.length > 1 && best.score < 750) {
1631
+ if (tiedCandidates.length > 1 && (best.score < 750 || Boolean(effective.itemKey || effective.reactKey))) {
1173
1632
  const candidates = tiedCandidates.map((c) => describeItemCandidate(c.listIndex, c.itemIndex - 1, c.item)).slice(0, 8);
1174
1633
  return {
1175
1634
  ok: false,
@@ -1237,11 +1696,11 @@ function toggleListItemByIdentity(locator, options) {
1237
1696
  }
1238
1697
  const target = switches[0];
1239
1698
  const props = target?.memoizedProps ?? {};
1240
- const current = switchValue(props);
1241
- if (options?.expected !== void 0 && current === options.expected) {
1242
- return { ok: true, containerType: list.containerType, value: current, noop: true };
1699
+ const current2 = switchValue(props);
1700
+ if (options?.expected !== void 0 && current2 === options.expected) {
1701
+ return { ok: true, containerType: list.containerType, value: current2, noop: true };
1243
1702
  }
1244
- const flipped = flipSwitchFiber(target, !current);
1703
+ const flipped = flipSwitchFiber(target, !current2);
1245
1704
  if (!flipped.ok) {
1246
1705
  if (flipped.reason === "no-handler") {
1247
1706
  return {
@@ -1258,7 +1717,7 @@ function toggleListItemByIdentity(locator, options) {
1258
1717
  containerType: list.containerType
1259
1718
  };
1260
1719
  }
1261
- return { ok: true, containerType: list.containerType, value: !current };
1720
+ return { ok: true, containerType: list.containerType, value: !current2 };
1262
1721
  }
1263
1722
  function pressListItemByIdentity(locator) {
1264
1723
  const resolved = resolveListItemByIdentity(locator);
@@ -1453,6 +1912,12 @@ function dispatchFiberTarget(targetId, action) {
1453
1912
  diagnose: screenDiagnose(targetId)
1454
1913
  };
1455
1914
  }
1915
+ const measured = handle.measure?.() ?? null;
1916
+ if (measured && getSpotlightMode() === "guided") {
1917
+ setSpotlightTarget(revealTarget(measured).rect, handle.label);
1918
+ } else {
1919
+ setSpotlightTarget(measured, handle.label);
1920
+ }
1456
1921
  try {
1457
1922
  if (isPressLike(action) && handle.kind === "target" && handle.press) {
1458
1923
  handle.press();
@@ -1503,6 +1968,13 @@ function dispatchLegacyTarget(targetId, registry, action) {
1503
1968
  };
1504
1969
  }
1505
1970
  function dispatchElement(element, registry, action) {
1971
+ if (element.dispatchable === false) {
1972
+ return {
1973
+ success: false,
1974
+ error: `Element "${element.id}" is observation-only and has no exact live control binding.`,
1975
+ diagnose: { ...screenDiagnose(element.id, "ambiguous-target"), recoverable: false }
1976
+ };
1977
+ }
1506
1978
  if (element.disabled === true) {
1507
1979
  return {
1508
1980
  success: false,
@@ -1521,6 +1993,13 @@ function dispatchElement(element, registry, action) {
1521
1993
  texts: element.texts
1522
1994
  });
1523
1995
  if (pressed.ok) return { success: true };
1996
+ if (!pressed.refusal && (element.listContext.itemKey || element.listContext.reactKey)) {
1997
+ return {
1998
+ success: false,
1999
+ error: pressed.error ?? `The observed list entity "${element.label ?? element.id}" could not be resolved. Reobserve; do not substitute its old position.`,
2000
+ diagnose: { ...screenDiagnose(element.id, "ambiguous-target"), recoverable: true }
2001
+ };
2002
+ }
1524
2003
  if (pressed.refusal) {
1525
2004
  return {
1526
2005
  success: false,
@@ -1838,12 +2317,18 @@ function normalizeText(value) {
1838
2317
  function containsWord(haystack, needle) {
1839
2318
  return ` ${haystack} `.includes(` ${needle} `);
1840
2319
  }
1841
- function readVisibleFormValidation(snap) {
2320
+ function readVisibleFormValidation(snap, beforeTexts) {
1842
2321
  if (!snap) return void 0;
2322
+ const invalid = snap.inputs?.find((input) => input.invalid === true);
2323
+ const hasSubmitControl = snap.buttons?.some(
2324
+ (button) => SUBMIT_CONTROL_RE.test(button.label ?? button.id ?? "")
2325
+ );
1843
2326
  for (const text of snap.texts ?? []) {
1844
- if (!text?.trim() || !FORM_VALIDATION_TEXT_RE2.test(text)) continue;
2327
+ if (!text?.trim() || !isVisibleValidationText(text, { invalid: !!invalid, hasSubmitControl, beforeTexts }))
2328
+ continue;
1845
2329
  return { fieldId: inferValidationFieldId(text, snap.inputs), message: text };
1846
2330
  }
2331
+ if (invalid) return { fieldId: invalid.id, message: "The app marks this field as invalid." };
1847
2332
  return void 0;
1848
2333
  }
1849
2334
  function readVisibleFormSuccess(snap) {
@@ -1856,15 +2341,18 @@ function submitAsyncStillPending(submitTargetId) {
1856
2341
  if (submitTargetId && probe.pressedFound && probe.pressedDisabled) return true;
1857
2342
  return false;
1858
2343
  }
1859
- function evaluatePostSubmitSnapshot(snap, activePath) {
1860
- const validation = readVisibleFormValidation(snap);
2344
+ function evaluatePostSubmitSnapshot(snap, activePath, beforeTexts) {
2345
+ const validation = readVisibleFormValidation(snap, beforeTexts);
1861
2346
  if (validation) {
1862
2347
  return { status: "failure", fieldId: validation.fieldId, message: validation.message };
1863
2348
  }
1864
2349
  const successText = readVisibleFormSuccess(snap);
1865
2350
  if (successText) return { status: "success", message: successText };
1866
2351
  if (!snapshotShowsOpenCreateForm(snap, activePath)) {
1867
- return { status: "success" };
2352
+ return {
2353
+ status: "unverified",
2354
+ message: "The form closed without a visible success receipt. Inspect the resulting record before claiming the goal is complete."
2355
+ };
1868
2356
  }
1869
2357
  return void 0;
1870
2358
  }
@@ -1880,7 +2368,7 @@ async function waitForPostSubmitOutcome(options) {
1880
2368
  }
1881
2369
  const snap2 = captureSnapshot();
1882
2370
  const activePath2 = getActiveRouteNames();
1883
- const outcome2 = evaluatePostSubmitSnapshot(snap2, activePath2);
2371
+ const outcome2 = evaluatePostSubmitSnapshot(snap2, activePath2, options?.beforeTexts);
1884
2372
  if (outcome2) return outcome2;
1885
2373
  await sleep(pollMs);
1886
2374
  }
@@ -1892,7 +2380,7 @@ async function waitForPostSubmitOutcome(options) {
1892
2380
  }
1893
2381
  const snap = captureSnapshot();
1894
2382
  const activePath = getActiveRouteNames();
1895
- const outcome = evaluatePostSubmitSnapshot(snap, activePath);
2383
+ const outcome = evaluatePostSubmitSnapshot(snap, activePath, options?.beforeTexts);
1896
2384
  if (outcome) return outcome;
1897
2385
  return {
1898
2386
  status: "failure",
@@ -1900,91 +2388,6 @@ async function waitForPostSubmitOutcome(options) {
1900
2388
  };
1901
2389
  }
1902
2390
 
1903
- // ../shared/dist/chunk-ERITSWKE.mjs
1904
- var DEFAULT_FUZZY_FLOOR = 4;
1905
- function normalizeId(value) {
1906
- return String(value ?? "").normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]/g, "");
1907
- }
1908
- var KIND_PREFIX_WORDS = "button|switch|toggle|input|field|link|btn|slider";
1909
- var KEBAB_SNAKE_PREFIX_RE = new RegExp(`^(?:${KIND_PREFIX_WORDS})[-_]`, "i");
1910
- var CAMEL_PREFIX_RE = new RegExp(`^(?:${KIND_PREFIX_WORDS})(?=[A-Z])`, "");
1911
- function stripKindPrefix(id) {
1912
- const raw = String(id ?? "");
1913
- if (KEBAB_SNAKE_PREFIX_RE.test(raw)) return raw.replace(KEBAB_SNAKE_PREFIX_RE, "");
1914
- if (CAMEL_PREFIX_RE.test(raw)) return raw.replace(CAMEL_PREFIX_RE, "");
1915
- return raw;
1916
- }
1917
- function actionAllowsKind(action, kind) {
1918
- if (!action || !kind) return true;
1919
- switch (action) {
1920
- case "press":
1921
- case "longPress":
1922
- return kind !== "field" && kind !== "input" && kind !== "slider";
1923
- case "set_value":
1924
- return kind === "slider" || kind === "field" || kind === "input";
1925
- case "focus":
1926
- return kind === "field" || kind === "input";
1927
- case "toggle":
1928
- return kind === "toggle";
1929
- default:
1930
- return true;
1931
- }
1932
- }
1933
- function distinctIds(candidates) {
1934
- return [...new Set(candidates.map((c) => c.id))];
1935
- }
1936
- function resolveTarget(requested, candidates, opts = {}) {
1937
- const floor = opts.fuzzyFloor ?? DEFAULT_FUZZY_FLOOR;
1938
- const nReq = normalizeId(requested);
1939
- if (!nReq || candidates.length === 0) {
1940
- return { status: "unverified", reason: "not-found" };
1941
- }
1942
- for (const c of candidates) {
1943
- if (normalizeId(c.id) === nReq) {
1944
- return { status: "matched", id: c.id, tier: "exact" };
1945
- }
1946
- }
1947
- const labelExact = distinctIds(
1948
- candidates.filter((c) => c.label && normalizeId(c.label) === nReq)
1949
- );
1950
- if (labelExact.length === 1) return { status: "matched", id: labelExact[0], tier: "exact" };
1951
- if (labelExact.length > 1) return { status: "unverified", reason: "ambiguous" };
1952
- const nReqStripped = normalizeId(stripKindPrefix(requested));
1953
- for (const c of candidates) {
1954
- const nId = normalizeId(c.id);
1955
- const nIdStripped = normalizeId(stripKindPrefix(c.id));
1956
- if (nReqStripped && (nReqStripped === nId || nReqStripped === nIdStripped) || nIdStripped && nIdStripped === nReq) {
1957
- return { status: "matched", id: c.id, tier: "prefix" };
1958
- }
1959
- }
1960
- if (opts.destructive) {
1961
- return { status: "unverified", reason: "destructive-inexact" };
1962
- }
1963
- if (nReq.length < floor) {
1964
- return { status: "unverified", reason: "not-found" };
1965
- }
1966
- const fuzzy = candidates.filter((c) => {
1967
- for (const s of [c.id, c.label]) {
1968
- if (!s) continue;
1969
- const ns = normalizeId(s);
1970
- if (ns.length >= floor && (ns.includes(nReq) || nReq.includes(ns))) return true;
1971
- }
1972
- return false;
1973
- });
1974
- if (fuzzy.length === 0) {
1975
- return { status: "unverified", reason: "not-found" };
1976
- }
1977
- const typed = fuzzy.filter((c) => actionAllowsKind(opts.action, c.kind));
1978
- if (typed.length === 0) {
1979
- return { status: "unverified", reason: "type-mismatch" };
1980
- }
1981
- const ids = distinctIds(typed);
1982
- if (ids.length > 1) {
1983
- return { status: "unverified", reason: "ambiguous" };
1984
- }
1985
- return { status: "matched", id: ids[0], tier: "fuzzy" };
1986
- }
1987
-
1988
2391
  // src/executor/handlers/uiInteractionHandler.ts
1989
2392
  function findSnapshotButton(targetId) {
1990
2393
  try {
@@ -2057,6 +2460,7 @@ function stripComponentPrefix(componentId) {
2057
2460
  return null;
2058
2461
  }
2059
2462
  function resolveComponentId(componentId, registry, action) {
2463
+ if (isSourceControlId(componentId)) return componentId;
2060
2464
  if (registry.getTarget(componentId) || registry.getToggle(componentId) || registry.getField(componentId) || registry.getSlider(componentId)) {
2061
2465
  return componentId;
2062
2466
  }
@@ -2070,6 +2474,7 @@ function resolveComponentId(componentId, registry, action) {
2070
2474
  return comp.id;
2071
2475
  }
2072
2476
  }
2477
+ if (isPositionalControlId(componentId)) return componentId;
2073
2478
  const currentScreen = getCurrentScreen();
2074
2479
  if (currentScreen) {
2075
2480
  const screenMeta = getScreenMetadata(currentScreen);
@@ -2315,11 +2720,14 @@ function uiInteractionHandler(payload, context) {
2315
2720
  if (!permissions.canInteractUI) {
2316
2721
  return {
2317
2722
  success: false,
2318
- error: "UI interaction is not permitted",
2319
- diagnose: buildUiDiagnose(
2320
- payload.componentId ?? payload.targetId ?? "",
2321
- "unknown"
2322
- )
2723
+ error: permissionDeniedMessage("UI interaction", "canInteractUI"),
2724
+ diagnose: {
2725
+ ...buildUiDiagnose(
2726
+ payload.componentId ?? payload.targetId ?? "",
2727
+ "unknown"
2728
+ ),
2729
+ recoverable: false
2730
+ }
2323
2731
  };
2324
2732
  }
2325
2733
  const { action } = payload;
@@ -2549,6 +2957,28 @@ function uiInteractionHandler(payload, context) {
2549
2957
  };
2550
2958
  }
2551
2959
 
2960
+ // src/executor/completeTextEdit.ts
2961
+ var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2962
+ async function completeTextEdit(fieldId, expected, registered, origin, resolveRegistered) {
2963
+ await sleep2(80);
2964
+ const current2 = findInteractiveByIdentifier(fieldId, { kind: "field" });
2965
+ if (origin?.controlIdentity && current2?.controlIdentity !== origin.controlIdentity) return false;
2966
+ if (registered && resolveRegistered && resolveRegistered() !== registered) return false;
2967
+ if (registered?.finishEditing) registered.finishEditing();
2968
+ else if (current2 && (current2.matchCount ?? 1) === 1 && !current2.disabled)
2969
+ current2.finishEditing?.();
2970
+ const started = Date.now();
2971
+ do {
2972
+ await sleep2(80);
2973
+ const live = findInteractiveByIdentifier(fieldId, { kind: "field" });
2974
+ if (origin?.controlIdentity && live?.controlIdentity !== origin.controlIdentity) return false;
2975
+ if (registered && resolveRegistered && resolveRegistered() !== registered) return false;
2976
+ const value = registered ? registered.getValue() : live?.getValue?.();
2977
+ if (value !== void 0 && value !== null && String(value) === String(expected)) return true;
2978
+ } while (Date.now() - started < 480);
2979
+ return false;
2980
+ }
2981
+
2552
2982
  // src/executor/formSubmitResolve.ts
2553
2983
  var SUBMIT_LABEL_HINT_RE = /\b(submit|save|send|continue|next|sign\s*in|log\s*in|register|salvar|enviar|entrar|cadastrar|continuar|proximo|próximo|avancar|avançar|confirmar|guardar|registrarse|iniciar\s+sesi[óo]n|enregistrer|envoyer|valider|soumettre|confirmer|se\s+connecter|s['e]\s*inscrire)\b/i;
2554
2984
  var AGENT_CHROME_TARGET_RE = /\b(fechar|close|dismiss|cancelar|cancel|limpar|clear|chat\s+do\s+assistente|assistant\s+chat)\b/i;
@@ -2695,8 +3125,8 @@ function selectValueApplied(fieldId, requestedValue, currentValue, registry) {
2695
3125
  function getMetadataField(fieldId, registry) {
2696
3126
  const screens = /* @__PURE__ */ new Set();
2697
3127
  for (const name of getActiveRouteNames()) screens.add(name);
2698
- const current = getCurrentScreen();
2699
- if (current) screens.add(current);
3128
+ const current2 = getCurrentScreen();
3129
+ if (current2) screens.add(current2);
2700
3130
  if (registry) {
2701
3131
  for (const comp of registry.snapshot()) {
2702
3132
  if (comp.kind === "field" && comp.screen) screens.add(comp.screen);
@@ -2737,7 +3167,7 @@ async function tryOpenSelectModal(fieldId, registry) {
2737
3167
  if (!target) continue;
2738
3168
  try {
2739
3169
  target.press();
2740
- await sleep2(180);
3170
+ await sleep3(180);
2741
3171
  return true;
2742
3172
  } catch {
2743
3173
  }
@@ -3122,7 +3552,7 @@ function findSlider(fieldId, registry) {
3122
3552
  }
3123
3553
  return void 0;
3124
3554
  }
3125
- var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
3555
+ var sleep3 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
3126
3556
  async function formFillHandler(payload, context) {
3127
3557
  const { permissions } = context;
3128
3558
  const registry = context.registry ?? componentRegistry;
@@ -3131,8 +3561,8 @@ async function formFillHandler(payload, context) {
3131
3561
  if (!permissions.canFillForms) {
3132
3562
  return {
3133
3563
  success: false,
3134
- error: "Form filling is not permitted",
3135
- diagnose: { category: "unknown", screen: screenPath }
3564
+ error: permissionDeniedMessage("Form filling", "canFillForms"),
3565
+ diagnose: { category: "unknown", screen: screenPath, recoverable: false }
3136
3566
  };
3137
3567
  }
3138
3568
  if (!payload.fields || payload.fields.length === 0) {
@@ -3189,14 +3619,51 @@ async function formFillHandler(payload, context) {
3189
3619
  const fieldEntry = findField(fieldId, registry);
3190
3620
  if (fieldEntry) {
3191
3621
  try {
3622
+ const nativeField = findInteractiveByIdentifier(fieldId, { kind: "field" });
3623
+ if (nativeField?.disabled || (nativeField?.matchCount ?? 1) > 1) {
3624
+ fieldResults.push({
3625
+ fieldId,
3626
+ success: false,
3627
+ error: nativeField?.disabled ? "The field is disabled" : "The field identifier matches multiple controls",
3628
+ diagnose: {
3629
+ category: nativeField?.disabled ? "disabled" : "ambiguous-target",
3630
+ fieldId,
3631
+ screen: screenPath
3632
+ }
3633
+ });
3634
+ allSuccess = false;
3635
+ continue;
3636
+ }
3192
3637
  appilotsDebugLog(
3193
3638
  `formFillHandler: found field "${fieldId}" via registry, calling setValue(<${String(value).length} chars>)`
3194
3639
  );
3195
3640
  const beforeValue = effectiveFieldType === "date" ? fieldEntry.getValue?.() ?? "" : void 0;
3196
3641
  fieldEntry.focus?.();
3197
3642
  fieldEntry.setValue(String(value));
3643
+ if (effectiveFieldType === "text" || effectiveFieldType === "number") {
3644
+ if (!await completeTextEdit(
3645
+ fieldId,
3646
+ value,
3647
+ fieldEntry,
3648
+ nativeField,
3649
+ () => findField(fieldId, registry)
3650
+ )) {
3651
+ fieldResults.push({
3652
+ fieldId,
3653
+ success: false,
3654
+ error: "The field did not retain the requested value after editing ended. Inspect its current value before retrying.",
3655
+ diagnose: { category: "validation", fieldId, screen: screenPath }
3656
+ });
3657
+ allSuccess = false;
3658
+ continue;
3659
+ }
3660
+ }
3198
3661
  if (effectiveFieldType === "date") {
3199
- const afterValue = fieldEntry.getValue?.() ?? "";
3662
+ let afterValue = fieldEntry.getValue?.() ?? "";
3663
+ if (afterValue === beforeValue) {
3664
+ await sleep3(80);
3665
+ afterValue = fieldEntry.getValue?.() ?? "";
3666
+ }
3200
3667
  if (afterValue === beforeValue) {
3201
3668
  fieldResults.push({
3202
3669
  fieldId,
@@ -3216,7 +3683,7 @@ async function formFillHandler(payload, context) {
3216
3683
  if (effectiveFieldType === "select") {
3217
3684
  let after = fieldEntry.getValue?.() ?? "";
3218
3685
  if (!selectValueApplied(fieldId, value, after, registry)) {
3219
- await sleep2(80);
3686
+ await sleep3(80);
3220
3687
  after = fieldEntry.getValue?.() ?? "";
3221
3688
  }
3222
3689
  if (!selectValueApplied(fieldId, value, after, registry)) {
@@ -3337,7 +3804,7 @@ async function formFillHandler(payload, context) {
3337
3804
  if (!selectResult.success && selectedChoiceThisBatch) {
3338
3805
  const nextResult = pressContinuationButton(registry);
3339
3806
  if (nextResult.success) {
3340
- await sleep2(180);
3807
+ await sleep3(180);
3341
3808
  selectResult = await selectVisibleChoice(fieldId, value, registry, {
3342
3809
  allowFirstVisibleWhenNoCue: true
3343
3810
  });
@@ -3348,7 +3815,7 @@ async function formFillHandler(payload, context) {
3348
3815
  appilotsDebugLog(
3349
3816
  `formFillHandler: selected visible option for "${fieldId}"` + (selectResult.selectedLabel ? ` \u2192 "${selectResult.selectedLabel}"` : "")
3350
3817
  );
3351
- await sleep2(120);
3818
+ await sleep3(120);
3352
3819
  fieldResults.push({ fieldId, success: true });
3353
3820
  continue;
3354
3821
  }
@@ -3360,8 +3827,34 @@ async function formFillHandler(payload, context) {
3360
3827
  });
3361
3828
  if (handle?.setValue) {
3362
3829
  try {
3830
+ if (handle.disabled || (handle.matchCount ?? 1) > 1) {
3831
+ fieldResults.push({
3832
+ fieldId,
3833
+ success: false,
3834
+ error: handle.disabled ? "The field is disabled" : "The field identifier matches multiple controls",
3835
+ diagnose: {
3836
+ category: handle.disabled ? "disabled" : "ambiguous-target",
3837
+ fieldId,
3838
+ screen: screenPath,
3839
+ candidates: handle.matchCandidates
3840
+ }
3841
+ });
3842
+ allSuccess = false;
3843
+ continue;
3844
+ }
3845
+ setSpotlightTarget(handle.measure?.() ?? null, handle.label);
3363
3846
  handle.focus?.();
3364
3847
  handle.setValue(value);
3848
+ if (handle.kind === "field" && !await completeTextEdit(fieldId, value, void 0, handle)) {
3849
+ fieldResults.push({
3850
+ fieldId,
3851
+ success: false,
3852
+ error: "The field did not retain the requested value after editing ended. Inspect its current value before retrying.",
3853
+ diagnose: { category: "validation", fieldId, screen: screenPath }
3854
+ });
3855
+ allSuccess = false;
3856
+ continue;
3857
+ }
3365
3858
  appilotsDebugLog(
3366
3859
  `formFillHandler: fiber fallback succeeded for "${fieldId}" \u2192 label="${handle.label}"`
3367
3860
  );
@@ -3413,10 +3906,11 @@ async function formFillHandler(payload, context) {
3413
3906
  "formFillHandler: submitAfterFill=true requested but some fields failed; NOT submitting" + (failedIds.length > 0 ? ` (failed: ${failedIds.join(", ")})` : "")
3414
3907
  );
3415
3908
  } else {
3416
- await sleep2(120);
3909
+ await sleep3(120);
3417
3910
  if (!permissions.canSubmitForms) {
3418
3911
  return { success: false, error: "Form submission is not permitted", fieldResults };
3419
3912
  }
3913
+ const beforeSubmitTexts = captureSnapshot().texts;
3420
3914
  const submitResult = pressSubmitButton(
3421
3915
  {
3422
3916
  screen,
@@ -3436,7 +3930,8 @@ async function formFillHandler(payload, context) {
3436
3930
  };
3437
3931
  }
3438
3932
  const postSubmit = await waitForPostSubmitOutcome({
3439
- submitTargetId: submitResult.pressedTargetId
3933
+ submitTargetId: submitResult.pressedTargetId,
3934
+ beforeTexts: beforeSubmitTexts
3440
3935
  });
3441
3936
  if (postSubmit.status === "pending") {
3442
3937
  appilotsDebugWarn(`formFillHandler: submit still pending \u2014 ${postSubmit.message}`);
@@ -3463,6 +3958,9 @@ async function formFillHandler(payload, context) {
3463
3958
  }
3464
3959
  };
3465
3960
  }
3961
+ if (postSubmit.status === "unverified") {
3962
+ return { success: true, fieldResults, effect: "unknown" };
3963
+ }
3466
3964
  appilotsDebugLog("formFillHandler: submitAfterFill \u2014 submit completed");
3467
3965
  }
3468
3966
  }
@@ -3600,7 +4098,21 @@ function aggregateFieldDiagnoses(results, screen) {
3600
4098
  var FALLBACK_PAGE_PX = 600;
3601
4099
  var PAGE_FRACTION = 0.85;
3602
4100
  var POST_SCROLL_MOUNT_MS = 400;
3603
- var sleep3 = (ms) => new Promise((r) => setTimeout(r, ms));
4101
+ var PAGINATION_MAX_WAIT_MS = 4e3;
4102
+ var PAGINATION_POLL_MS = 150;
4103
+ var sleep4 = (ms) => new Promise((r) => setTimeout(r, ms));
4104
+ async function settleAfterScroll() {
4105
+ await sleep4(POST_SCROLL_MOUNT_MS);
4106
+ if (!probeLoadingState(void 0, getCurrentScreen()).loading) return;
4107
+ const deadline = Date.now() + PAGINATION_MAX_WAIT_MS;
4108
+ while (Date.now() < deadline) {
4109
+ await sleep4(PAGINATION_POLL_MS);
4110
+ if (!probeLoadingState(void 0, getCurrentScreen()).loading) {
4111
+ await sleep4(POST_SCROLL_MOUNT_MS);
4112
+ return;
4113
+ }
4114
+ }
4115
+ }
3604
4116
  function normalizeId2(value) {
3605
4117
  return String(value ?? "").toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "");
3606
4118
  }
@@ -3636,13 +4148,26 @@ function pageOffset(metrics, direction) {
3636
4148
  }
3637
4149
  return next;
3638
4150
  }
4151
+ async function finishScroll(entry, before) {
4152
+ await settleAfterScroll();
4153
+ const current2 = listRegistry.get(entry.id) ?? entry;
4154
+ const after = current2.getScrollMetrics?.();
4155
+ const grew = typeof entry.itemCount === "number" && typeof current2.itemCount === "number" && current2.itemCount > entry.itemCount;
4156
+ const moved = grew || !!(before && after && (Math.abs(after.offset - before.offset) > 2 || after.contentLength > before.contentLength + 2));
4157
+ const outcome = moved ? "moved" : before && after ? "no-progress" : "unverified";
4158
+ current2.exploration?.recordScroll(outcome);
4159
+ return {
4160
+ success: true,
4161
+ effect: moved ? "changed" : outcome === "no-progress" ? "none" : "unknown"
4162
+ };
4163
+ }
3639
4164
  async function scrollListHandler(payload, context) {
3640
4165
  const { permissions } = context;
3641
4166
  if (!permissions.canInteractUI) {
3642
4167
  return {
3643
4168
  success: false,
3644
- error: "UI interaction is not permitted",
3645
- diagnose: buildDiagnose("unknown", payload.listId)
4169
+ error: permissionDeniedMessage("UI interaction", "canInteractUI"),
4170
+ diagnose: { ...buildDiagnose("unknown", payload.listId), recoverable: false }
3646
4171
  };
3647
4172
  }
3648
4173
  const toIndex = typeof payload.toIndex === "number" ? payload.toIndex : void 0;
@@ -3670,6 +4195,15 @@ async function scrollListHandler(payload, context) {
3670
4195
  diagnose: buildDiagnose("component-not-found", payload.listId, candidates)
3671
4196
  };
3672
4197
  }
4198
+ const exploration = entry.exploration?.snapshot();
4199
+ if (exploration && (exploration.remainingScrollSteps === 0 || exploration.consecutiveNoProgress >= 2)) {
4200
+ return {
4201
+ success: false,
4202
+ effect: "none",
4203
+ error: "List exploration stopped: scroll budget exhausted or repeated no progress. Use the current loaded count/coverage and report any remaining uncertainty; do not repeat this scroll.",
4204
+ diagnose: { ...buildDiagnose("validation", entry.id), recoverable: false }
4205
+ };
4206
+ }
3673
4207
  if (toIndex !== void 0) {
3674
4208
  if (entry.kind === "scroll") {
3675
4209
  return {
@@ -3685,17 +4219,16 @@ async function scrollListHandler(payload, context) {
3685
4219
  diagnose: buildDiagnose("validation", entry.id)
3686
4220
  };
3687
4221
  }
4222
+ const before = entry.getScrollMetrics?.();
3688
4223
  const zeroBased = toIndex - 1;
3689
4224
  if (entry.scrollToIndex?.(zeroBased)) {
3690
- await sleep3(POST_SCROLL_MOUNT_MS);
3691
- return { success: true };
4225
+ return finishScroll(entry, before);
3692
4226
  }
3693
4227
  const metrics2 = entry.getScrollMetrics?.();
3694
4228
  if (metrics2 && metrics2.contentLength > 0 && typeof entry.itemCount === "number" && entry.itemCount > 0) {
3695
4229
  const estimated = zeroBased / entry.itemCount * metrics2.contentLength;
3696
4230
  if (entry.scrollToOffset?.(Math.max(0, estimated))) {
3697
- await sleep3(POST_SCROLL_MOUNT_MS);
3698
- return { success: true };
4231
+ return finishScroll(entry, before);
3699
4232
  }
3700
4233
  }
3701
4234
  return {
@@ -3705,10 +4238,21 @@ async function scrollListHandler(payload, context) {
3705
4238
  };
3706
4239
  }
3707
4240
  const metrics = entry.getScrollMetrics?.();
4241
+ if (!metrics && exploration?.lastScroll === "unverified") {
4242
+ return {
4243
+ success: false,
4244
+ effect: "unknown",
4245
+ error: "Scroll offset is still unavailable after one probe. Cannot safely page again; do not infer the end or repeat the same absolute offset.",
4246
+ diagnose: { ...buildDiagnose("validation", entry.id), recoverable: false }
4247
+ };
4248
+ }
3708
4249
  const target = pageOffset(metrics, direction);
4250
+ if (metrics && Math.abs(target - metrics.offset) <= 2) {
4251
+ entry.exploration?.recordScroll("boundary");
4252
+ return { success: true, effect: "none" };
4253
+ }
3709
4254
  if (entry.scrollToOffset?.(target)) {
3710
- await sleep3(POST_SCROLL_MOUNT_MS);
3711
- return { success: true };
4255
+ return finishScroll(entry, metrics);
3712
4256
  }
3713
4257
  return {
3714
4258
  success: false,
@@ -3792,7 +4336,7 @@ function hashFieldValue(value) {
3792
4336
  }
3793
4337
  return (hash >>> 0).toString(36);
3794
4338
  }
3795
- var DISCRETE_FIELD_TYPES = /* @__PURE__ */ new Set(["select", "date"]);
4339
+ var DISCRETE_FIELD_TYPES = /* @__PURE__ */ new Set(["select", "date", "number"]);
3796
4340
  function discreteFieldSignature() {
3797
4341
  const sigs = [];
3798
4342
  for (const { id, kind } of componentRegistry.snapshot()) {
@@ -3806,12 +4350,23 @@ function discreteFieldSignature() {
3806
4350
  }
3807
4351
  return sigs.sort().join(",");
3808
4352
  }
3809
- async function waitForPostInteractionSettle(payload) {
4353
+ function capturePreInteractionBaseline(payload) {
3810
4354
  const targetId = targetIdFromPayload(payload);
3811
- const fromScreen = getCurrentScreen();
3812
- const baseline = probeLoadingState(targetId || null, fromScreen);
3813
- const baselineFields = discreteFieldSignature();
3814
- const readable = baseline.fingerprint !== "";
4355
+ const screen = getCurrentScreen();
4356
+ const probe = probeLoadingState(targetId || null, screen);
4357
+ return {
4358
+ fingerprint: probe.fingerprint,
4359
+ fields: discreteFieldSignature(),
4360
+ screen,
4361
+ readable: probe.fingerprint !== ""
4362
+ };
4363
+ }
4364
+ async function waitForPostInteractionSettle(payload, preBaseline) {
4365
+ const targetId = targetIdFromPayload(payload);
4366
+ const baseline = preBaseline ?? capturePreInteractionBaseline(payload);
4367
+ const fromScreen = baseline.screen;
4368
+ const baselineFields = baseline.fields;
4369
+ const readable = baseline.readable;
3815
4370
  await sleepMs(100);
3816
4371
  const afterPress = probeLoadingState(targetId || null, getCurrentScreen());
3817
4372
  const modalOrLoading = afterPress.modalOpen || afterPress.loading || afterPress.pressedDisabled;
@@ -3843,12 +4398,6 @@ async function waitForPostInteractionSettle(payload) {
3843
4398
  }
3844
4399
 
3845
4400
  // src/executor/ActionExecutor.ts
3846
- var DEFAULT_PERMISSIONS = {
3847
- canNavigate: true,
3848
- canFillForms: true,
3849
- canInteractUI: true,
3850
- canSubmitForms: true
3851
- };
3852
4401
  function validateActionPayload(action) {
3853
4402
  const invalid = (message, ids) => ({
3854
4403
  success: false,
@@ -3919,7 +4468,7 @@ async function executeAction(action, context) {
3919
4468
  appilotsDebugLog(
3920
4469
  `executeAction called: type="${action.type}", id="${action.id}", payload=${JSON.stringify(action.payload)}`
3921
4470
  );
3922
- const permissions = context.permissions ?? DEFAULT_PERMISSIONS;
4471
+ const permissions = resolveAgentPermissions(context.permissions);
3923
4472
  context.emit({
3924
4473
  type: "agent:action:start",
3925
4474
  timestamp: Date.now(),
@@ -3977,12 +4526,13 @@ async function executeAction(action, context) {
3977
4526
  break;
3978
4527
  case "ui_interaction": {
3979
4528
  const payload = action.payload;
4529
+ const baseline = capturePreInteractionBaseline(payload);
3980
4530
  result = uiInteractionHandler(payload, {
3981
4531
  permissions,
3982
4532
  registry: context.registry ?? getDefaultRegistry()
3983
4533
  });
3984
4534
  if (result.success) {
3985
- const settle = await waitForPostInteractionSettle(payload);
4535
+ const settle = await waitForPostInteractionSettle(payload, baseline);
3986
4536
  result.effect = result.effect ?? effectFor(payload, settle.effect);
3987
4537
  }
3988
4538
  break;
@@ -4070,18 +4620,28 @@ var _patchedAlert = (title, message, buttons, options) => {
4070
4620
  return suppression.originalAlert(title, message, buttons, options);
4071
4621
  }
4072
4622
  suppression.consumed = true;
4623
+ const report = (handlerCompleted) => suppression.onHandled?.({
4624
+ title: title.slice(0, 300),
4625
+ message: message?.slice(0, 1e3),
4626
+ buttonLabel: button.text?.slice(0, 160),
4627
+ handlerCompleted
4628
+ });
4073
4629
  appilotsDebugLog("Suppressing native Alert during already-confirmed destructive agent action");
4074
4630
  try {
4075
- suppression.pending = Promise.resolve(button.onPress()).then(() => void 0).catch((err) => reportHandlerFailure(err));
4631
+ suppression.pending = Promise.resolve(button.onPress()).then(() => report(true)).catch((err) => {
4632
+ reportHandlerFailure(err);
4633
+ report(false);
4634
+ });
4076
4635
  } catch (err) {
4077
4636
  reportHandlerFailure(err);
4078
4637
  suppression.pending = null;
4638
+ report(false);
4079
4639
  }
4080
4640
  };
4081
4641
  function reportHandlerFailure(err) {
4082
4642
  console.error("[Appilots] The app\u2019s Alert handler threw while being auto-confirmed", err);
4083
4643
  }
4084
- async function runWithConfirmedDestructiveContext(enabled, host, fn) {
4644
+ async function runWithConfirmedDestructiveContext(enabled, host, fn, onHandled) {
4085
4645
  if (!enabled) return fn();
4086
4646
  if (_active) {
4087
4647
  _active.depth += 1;
@@ -4102,6 +4662,7 @@ async function runWithConfirmedDestructiveContext(enabled, host, fn) {
4102
4662
  depth: 1,
4103
4663
  consumed: false,
4104
4664
  pending: null,
4665
+ onHandled,
4105
4666
  timer: null
4106
4667
  };
4107
4668
  _active = suppression;
@@ -4135,9 +4696,14 @@ function resolveScreenMetadata() {
4135
4696
  return screen ? getScreenMetadata(screen) : void 0;
4136
4697
  }
4137
4698
  function buildAgentContext(extras = {}) {
4138
- if (extras.newMission === true) deltaTracker.reset();
4699
+ if (extras.newMission === true) {
4700
+ deltaTracker.reset();
4701
+ for (const entry of listRegistry.snapshot()) entry.exploration?.reset();
4702
+ }
4139
4703
  const context = {};
4140
4704
  context.platform = "react-native";
4705
+ context.missionProtocol = 1;
4706
+ context.deviceLocale = detectDeviceLocale() ?? "en";
4141
4707
  const screen = getCurrentScreen();
4142
4708
  if (screen) context.currentScreen = screen;
4143
4709
  const navigationState = getNavigationStateSnapshot();
@@ -4146,6 +4712,12 @@ function buildAgentContext(extras = {}) {
4146
4712
  if (screenMeta) context.screenMetadata = screenMeta;
4147
4713
  try {
4148
4714
  const snapshot = captureSnapshot();
4715
+ for (const list of snapshot.lists) {
4716
+ const memory = list.id ? listRegistry.get(list.id)?.exploration : void 0;
4717
+ if (!memory) continue;
4718
+ memory.observe(list.items);
4719
+ list.exploration = memory.snapshot();
4720
+ }
4149
4721
  const delta = deltaTracker.observe(snapshot);
4150
4722
  if (delta) snapshot.delta = delta;
4151
4723
  context.snapshot = snapshot;
@@ -4157,9 +4729,20 @@ function buildAgentContext(extras = {}) {
4157
4729
  if (filtered.length > 0) context.registeredComponents = filtered;
4158
4730
  const registeredLists = listRegistry.snapshot();
4159
4731
  if (registeredLists.length > 0) {
4160
- context.registeredLists = registeredLists.map(
4161
- ({ scrollToIndex, scrollToOffset, getScrollMetrics, dataPreview, ...rest }) => rest
4732
+ const snapshot = context.snapshot;
4733
+ const activeIds = new Set(
4734
+ [...snapshot?.lists ?? [], ...snapshot?.scrollables ?? []].map((l) => l.id)
4162
4735
  );
4736
+ context.registeredLists = registeredLists.filter((entry) => activeIds.has(entry.id)).map(({ id, kind, component, itemCount, totalItemCount, label, refreshing, empty }) => ({
4737
+ id,
4738
+ kind,
4739
+ component,
4740
+ itemCount,
4741
+ totalItemCount,
4742
+ label,
4743
+ refreshing,
4744
+ empty
4745
+ }));
4163
4746
  }
4164
4747
  if (extras.loadingPending === true) context.loadingPending = true;
4165
4748
  return context;
@@ -4169,6 +4752,7 @@ async function settleTurn(input) {
4169
4752
  let loadingPending = false;
4170
4753
  const { turnActions, pressedComponentId: pressedId, navigateTargetScreen } = input;
4171
4754
  const successById = new Map(input.results.map((r) => [r.actionId, r.success]));
4755
+ const effectById = new Map(input.results.map((r) => [r.actionId, r.effect]));
4172
4756
  const typeById = new Map(input.results.map((r) => [r.actionId, r.type]));
4173
4757
  const pressedAction = turnActions.find(
4174
4758
  (a) => a.type === "ui_interaction" && a.payload?.componentId === pressedId
@@ -4244,6 +4828,7 @@ async function settleTurn(input) {
4244
4828
  );
4245
4829
  if (!postPressSettle.transitioned && baselineFingerprint === afterFingerprint) {
4246
4830
  for (const action of listPressActions) {
4831
+ if (effectById.get(action.id) === "changed") continue;
4247
4832
  if (successById.get(action.id)) {
4248
4833
  toolWithoutEffectIds.add(action.id);
4249
4834
  }
@@ -4261,18 +4846,40 @@ var reactNativeChatAdapter = {
4261
4846
  };
4262
4847
  function createReactNativeActionRunner(getOptions) {
4263
4848
  return {
4849
+ buildMissionContext: () => reactNativeChatAdapter.buildContext(),
4264
4850
  async execute(action, { confirmedDestructive }) {
4265
- const { permissions, emit, navigationRef, suppressNativeConfirm } = getOptions();
4851
+ const { permissions, emit: emit2, navigationRef, suppressNativeConfirm } = getOptions();
4266
4852
  const navRef = navigationRef?.current ? navigationRef : { current: getNavigationRef() };
4267
- return runWithConfirmedDestructiveContext(
4853
+ let receipt;
4854
+ let terminal;
4855
+ const result = await runWithConfirmedDestructiveContext(
4268
4856
  confirmedDestructive && suppressNativeConfirm !== false,
4269
4857
  Alert,
4270
4858
  () => executeAction(action, {
4271
4859
  navigationRef: navRef,
4272
4860
  permissions,
4273
- emit
4274
- })
4861
+ emit: (event) => {
4862
+ if (confirmedDestructive && (event.type === "agent:action:complete" || event.type === "agent:action:error"))
4863
+ terminal = event;
4864
+ else emit2(event);
4865
+ }
4866
+ }),
4867
+ (handled) => {
4868
+ receipt = handled;
4869
+ }
4275
4870
  );
4871
+ const failed = receipt?.handlerCompleted === false;
4872
+ if (terminal)
4873
+ emit2({
4874
+ ...terminal,
4875
+ type: failed ? "agent:action:error" : terminal.type,
4876
+ data: {
4877
+ ...terminal.data,
4878
+ ...receipt ? { nativeConfirmation: receipt } : {},
4879
+ ...failed ? { success: false, error: "Native confirmation callback failed" } : {}
4880
+ }
4881
+ });
4882
+ return failed ? { ...result, success: false, error: "Native confirmation callback failed" } : result;
4276
4883
  },
4277
4884
  getCurrentScreen,
4278
4885
  getCurrentScreenSignature,
@@ -4285,16 +4892,16 @@ function createReactNativeActionRunner(getOptions) {
4285
4892
 
4286
4893
  // src/hooks/useAppilotsChat.ts
4287
4894
  function useAppilotsChat(options = {}) {
4288
- const { client, emit, subscribe } = useAppilotsContext();
4895
+ const { client, emit: emit2, subscribe } = useAppilotsContext();
4289
4896
  const machine = useMemo(
4290
4897
  () => new ChatSessionMachine(
4291
- { client, adapter: reactNativeChatAdapter, emit, warn: appilotsDebugWarn },
4898
+ { client, adapter: reactNativeChatAdapter, emit: emit2, warn: appilotsDebugWarn },
4292
4899
  options
4293
4900
  ),
4294
4901
  // A new machine per client/provider instance — options are pushed in
4295
4902
  // below so changing copy never resets the transcript.
4296
4903
  // eslint-disable-next-line react-hooks/exhaustive-deps
4297
- [client, emit]
4904
+ [client, emit2]
4298
4905
  );
4299
4906
  const optionsRef = useRef(options);
4300
4907
  optionsRef.current = options;
@@ -4302,6 +4909,14 @@ function useAppilotsChat(options = {}) {
4302
4909
  const state = useSyncExternalStore(machine.subscribeState, machine.getState, machine.getState);
4303
4910
  useEffect(() => subscribe(machine.handleEvent), [subscribe, machine]);
4304
4911
  useEffect(() => () => machine.dispose(), [machine]);
4912
+ useEffect(() => {
4913
+ void machine.restoreMission();
4914
+ const subscription = AppState?.addEventListener?.("change", (next) => {
4915
+ if (next !== "active" && machine.getState().mission?.status === "running")
4916
+ void machine.pauseMission();
4917
+ });
4918
+ return () => subscription?.remove();
4919
+ }, [machine]);
4305
4920
  const requestHuman = useCallback(
4306
4921
  (reason) => machine.requestHuman(reason),
4307
4922
  [machine]
@@ -4311,7 +4926,13 @@ function useAppilotsChat(options = {}) {
4311
4926
  const clearMessages = useCallback(() => machine.clearMessages(), [machine]);
4312
4927
  const clearError = useCallback(() => machine.clearError(), [machine]);
4313
4928
  return {
4929
+ mission: state.mission,
4930
+ pauseMission: () => machine.pauseMission(),
4931
+ resumeMission: () => machine.resumeMission(),
4932
+ cancelMission: () => machine.cancelMission(),
4933
+ hasStartedActing: state.hasStartedActing,
4314
4934
  messages: state.messages,
4935
+ replyLocale: state.replyLocale,
4315
4936
  isLoading: state.isLoading,
4316
4937
  loadingStatusKey: state.loadingStatusKey,
4317
4938
  error: state.error,
@@ -4324,24 +4945,18 @@ function useAppilotsChat(options = {}) {
4324
4945
  clearError
4325
4946
  };
4326
4947
  }
4327
- var DEFAULT_PERMISSIONS2 = {
4328
- canNavigate: true,
4329
- canFillForms: true,
4330
- canInteractUI: true,
4331
- canSubmitForms: true
4332
- };
4333
4948
  function useAppilotsActions(options = {}) {
4334
- const { client, subscribe, config, emit } = useAppilotsContext();
4949
+ const { client, subscribe, config, emit: emit2 } = useAppilotsContext();
4335
4950
  const { navigationRef, autoExecute = false } = options;
4336
4951
  const runnerOptionsRef = useRef({
4337
- permissions: config.permissions ?? DEFAULT_PERMISSIONS2,
4338
- emit,
4952
+ permissions: resolveAgentPermissions(config.permissions),
4953
+ emit: emit2,
4339
4954
  navigationRef,
4340
4955
  suppressNativeConfirm: config.suppressNativeConfirm
4341
4956
  });
4342
4957
  runnerOptionsRef.current = {
4343
- permissions: config.permissions ?? DEFAULT_PERMISSIONS2,
4344
- emit,
4958
+ permissions: resolveAgentPermissions(config.permissions),
4959
+ emit: emit2,
4345
4960
  navigationRef,
4346
4961
  suppressNativeConfirm: config.suppressNativeConfirm
4347
4962
  };
@@ -4350,13 +4965,13 @@ function useAppilotsActions(options = {}) {
4350
4965
  {
4351
4966
  client,
4352
4967
  adapter: createReactNativeActionRunner(() => runnerOptionsRef.current),
4353
- emit,
4968
+ emit: emit2,
4354
4969
  warn: appilotsDebugWarn
4355
4970
  },
4356
4971
  { autoExecute }
4357
4972
  ),
4358
4973
  // eslint-disable-next-line react-hooks/exhaustive-deps
4359
- [client, emit]
4974
+ [client, emit2]
4360
4975
  );
4361
4976
  machine.setOptions({ autoExecute });
4362
4977
  const state = useSyncExternalStore(machine.subscribeState, machine.getState, machine.getState);
@@ -4446,9 +5061,9 @@ function useSuggestedPrompts(options) {
4446
5061
  if (!enabled) return;
4447
5062
  let lastScreen = getCurrentScreen();
4448
5063
  const interval = setInterval(() => {
4449
- const current = getCurrentScreen();
4450
- if (current !== lastScreen) {
4451
- lastScreen = current;
5064
+ const current2 = getCurrentScreen();
5065
+ if (current2 !== lastScreen) {
5066
+ lastScreen = current2;
4452
5067
  setRefreshTick((t) => t + 1);
4453
5068
  }
4454
5069
  }, 800);
@@ -4461,7 +5076,7 @@ function useSuggestedPrompts(options) {
4461
5076
  };
4462
5077
  }
4463
5078
  function useAppilotsNavigation() {
4464
- const { emit } = useAppilotsContext();
5079
+ const { emit: emit2 } = useAppilotsContext();
4465
5080
  const [currentScreen, setCurrentScreenState] = useState(null);
4466
5081
  const [navigationHistory, setNavigationHistory] = useState([]);
4467
5082
  const navigationRef = useRef(null);
@@ -4470,13 +5085,13 @@ function useAppilotsNavigation() {
4470
5085
  setCurrentScreenState(screenName);
4471
5086
  setCurrentScreen(screenName);
4472
5087
  setNavigationHistory((prev) => [...prev, screenName]);
4473
- emit({
5088
+ emit2({
4474
5089
  type: "navigation:change",
4475
5090
  timestamp: Date.now(),
4476
5091
  data: { screenName }
4477
5092
  });
4478
5093
  },
4479
- [emit]
5094
+ [emit2]
4480
5095
  );
4481
5096
  useEffect(() => {
4482
5097
  const nav = navigationRef.current;
@@ -4526,6 +5141,11 @@ function useAppilots() {
4526
5141
  rejectAction: actions.rejectAction
4527
5142
  };
4528
5143
  }
5144
+ function useAppilotsElementProps(id) {
5145
+ return useMemo(() => ({ testID: id }), [id]);
5146
+ }
5147
+
5148
+ // src/hooks/useAppilotsField.ts
4529
5149
  function useAppilotsField(id, options) {
4530
5150
  const { value, onChangeText, ref, fieldType, label, screen } = options;
4531
5151
  const registry = useResolvedRegistry();
@@ -4553,6 +5173,7 @@ function useAppilotsField(id, options) {
4553
5173
  registry.unregister(id);
4554
5174
  };
4555
5175
  }, [id, registry, getValue, setValue, focus, ref, fieldType, label, screen]);
5176
+ return useAppilotsElementProps(id);
4556
5177
  }
4557
5178
  function useAppilotsTarget(id, options) {
4558
5179
  const { onPress, onLongPress, onScrollTo, label, screen } = options;
@@ -4580,6 +5201,7 @@ function useAppilotsTarget(id, options) {
4580
5201
  registry.unregister(id);
4581
5202
  };
4582
5203
  }, [id, registry, press, longPress, scrollTo, onLongPress, onScrollTo, label, screen]);
5204
+ return useAppilotsElementProps(id);
4583
5205
  }
4584
5206
  function useAppilotsToggle(id, options) {
4585
5207
  const { value, onValueChange, label, screen } = options;
@@ -4603,6 +5225,7 @@ function useAppilotsToggle(id, options) {
4603
5225
  registry.unregister(id);
4604
5226
  };
4605
5227
  }, [id, registry, getValue, setValue, label, screen]);
5228
+ return useAppilotsElementProps(id);
4606
5229
  }
4607
5230
  function useAppilotsSlider(id, options) {
4608
5231
  const { value, onValueChange, min, max, step, label, screen } = options;
@@ -4629,6 +5252,7 @@ function useAppilotsSlider(id, options) {
4629
5252
  registry.unregister(id);
4630
5253
  };
4631
5254
  }, [id, registry, getValue, setValue, min, max, step, label, screen]);
5255
+ return useAppilotsElementProps(id);
4632
5256
  }
4633
5257
 
4634
- export { appilotsSelfCheck, captureSnapshot, executeAction, shouldShowSuggestedPrompts, useAppilots, useAppilotsActions, useAppilotsChat, useAppilotsField, useAppilotsNavigation, useAppilotsSlider, useAppilotsTarget, useAppilotsToggle, useSuggestedPrompts };
5258
+ export { AppilotsI18nProvider, REVEAL_MARGIN, SPOTLIGHT_TTL_MS, appilotsSelfCheck, captureSnapshot, detectDeviceLocale, executeAction, getSpotlightMode, getSpotlightTarget, offsetToReveal, resolveLocale, revealTarget, setSpotlightMode, setSpotlightTarget, shouldShowSuggestedPrompts, subscribeSpotlight, useAppilots, useAppilotsActions, useAppilotsChat, useAppilotsField, useAppilotsI18n, useAppilotsNavigation, useAppilotsSlider, useAppilotsTarget, useAppilotsToggle, useSuggestedPrompts };