@appilots/sdk 0.2.0 → 0.4.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,5 +1,5 @@
1
- import { getFiberRoot, getCurrentScreen, getActiveRouteNames, elementRegistry, useAppilotsContext, recordAppilotsDebugTrace, getCurrentScreenSignature, waitForScreenSettle, screenDepartedBaseline, routesBelongToSameFeature, appilotsDebugWarn, waitForLoadingSettle, probeLoadingState, StreamTransportError, StreamAbortError, getDefaultRegistry, getNavigationRef, getScreenMetadata, setCurrentScreen, useResolvedRegistry, listRegistry, componentRegistry, getNavigationStateSnapshot, getAllScreens } from './chunk-HDLZGXNF.mjs';
2
- import { useRef, useState, useCallback, useEffect } from 'react';
1
+ import { getFiberRoot, getCurrentScreen, getActiveRouteNames, deriveInteractionElements, attachElementIdsToChoiceGroups, elementRegistry, appilotsDebugWarn, getDefaultRegistry, useAppilotsContext, ChatSessionMachine, getCurrentScreenSignature, ActionQueueMachine, getScreenMetadata, setCurrentScreen, useResolvedRegistry, listRegistry, componentRegistry, probeLoadingState, isListItemPressTarget, waitForLoadingSettle, waitForScreenSettle, screenDepartedBaseline, routesBelongToSameFeature, actionPressTargetId, getNavigationStateSnapshot, getNavigationRef, isOptionalStepAutomationFailure, OPTIONAL_STEP_AUTOMATION_HINT, normalize3, matchScore, idLooselyMatches, parseStableElementId, getAllScreens, snapshotShowsOpenCreateForm, looksDestructiveActionLabel, isGenericSelectValue } from './chunk-JBOLV5AM.mjs';
2
+ import { useMemo, useRef, useSyncExternalStore, useEffect, useCallback, useState } from 'react';
3
3
  import { Alert } from 'react-native';
4
4
 
5
5
  // src/introspection/walkFiber.ts
@@ -654,225 +654,6 @@ function normalizeLabel(label) {
654
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
655
  }
656
656
 
657
- // src/introspection/interactionGraph.ts
658
- function normalizePart(value) {
659
- return String(value ?? "").toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
660
- }
661
- function hashString(value) {
662
- let hash = 2166136261;
663
- for (let i = 0; i < value.length; i++) {
664
- hash ^= value.charCodeAt(i);
665
- hash = Math.imul(hash, 16777619);
666
- }
667
- return (hash >>> 0).toString(36);
668
- }
669
- function stableElementId(parts) {
670
- const raw = parts.filter((p) => p !== void 0 && String(p).length > 0).join("|");
671
- const readable = parts.map(normalizePart).filter(Boolean).join(":").slice(0, 120);
672
- return `el:${readable}:${hashString(raw)}`;
673
- }
674
- function parseStableElementId(id) {
675
- if (typeof id !== "string") return null;
676
- const trimmed = id.trim();
677
- if (!/^el:/i.test(trimmed)) return null;
678
- const parts = trimmed.split(":");
679
- if (parts.length < 3) return null;
680
- const hash = parts[parts.length - 1];
681
- const segments = parts.slice(1, -1);
682
- if (segments.length === 0) return null;
683
- const role = segments[0];
684
- const result = { role, segments, hash };
685
- if (role === "option" && segments.length >= 4) {
686
- result.listId = segments[segments.length - 2];
687
- result.itemKey = segments[segments.length - 1];
688
- }
689
- return result;
690
- }
691
- function uniqueTexts(texts) {
692
- const seen = /* @__PURE__ */ new Set();
693
- const out = [];
694
- for (const text of texts) {
695
- const t = typeof text === "string" ? text.trim() : "";
696
- if (!t || seen.has(t)) continue;
697
- seen.add(t);
698
- out.push(t);
699
- }
700
- return out;
701
- }
702
- function labelForListItem(item) {
703
- return uniqueTexts([
704
- ...item.texts ?? [],
705
- ...(item.buttons ?? []).map((button) => button.label ?? button.id)
706
- ]).slice(0, 2).join(" \xB7 ") || void 0;
707
- }
708
- function listItemElement(route, list, item) {
709
- const texts = uniqueTexts([
710
- ...item.texts ?? [],
711
- ...(item.buttons ?? []).map((button) => button.label ?? button.id)
712
- ]);
713
- const label = labelForListItem(item);
714
- const key = item.itemKey ?? item.reactKey ?? label ?? item.syntheticId;
715
- const id = stableElementId([
716
- "option",
717
- route ?? "unknown",
718
- list.id ?? list.label ?? `list-${list.index}`,
719
- key
720
- ]);
721
- const inModal = (item.buttons ?? []).some((button) => button.inModal) || (item.inputs ?? []).some((input) => input.inModal) || (item.toggles ?? []).some((toggle) => toggle.inModal);
722
- return {
723
- id,
724
- role: "option",
725
- label,
726
- texts,
727
- actions: ["press"],
728
- disabled: false,
729
- source: "list",
730
- targetId: item.syntheticId,
731
- ...inModal ? { inModal: true } : {},
732
- listContext: {
733
- listIndex: list.index,
734
- listId: list.id,
735
- listLabel: list.label,
736
- itemIndex: item.index,
737
- itemKey: item.itemKey,
738
- reactKey: item.reactKey,
739
- syntheticId: item.syntheticId
740
- }
741
- };
742
- }
743
- function buttonElement(route, button, index) {
744
- const label = button.label ?? button.id;
745
- const id = stableElementId([
746
- "button",
747
- route ?? "unknown",
748
- button.id ?? label ?? `button-${index}`
749
- ]);
750
- return {
751
- id,
752
- role: "button",
753
- label,
754
- texts: uniqueTexts([label]),
755
- actions: ["press"],
756
- disabled: button.disabled,
757
- selected: button.selected,
758
- source: "button",
759
- targetId: button.id ?? button.label,
760
- ...button.inModal ? { inModal: true } : {}
761
- };
762
- }
763
- function inputElement(route, input, index) {
764
- const label = input.label ?? input.placeholder ?? input.id;
765
- const id = stableElementId([
766
- "input",
767
- route ?? "unknown",
768
- input.id ?? label ?? `input-${index}`
769
- ]);
770
- return {
771
- id,
772
- role: "input",
773
- label,
774
- texts: uniqueTexts([label, input.placeholder]),
775
- actions: ["focus", "setValue"],
776
- disabled: input.editable === false,
777
- source: "input",
778
- targetId: input.id ?? input.label ?? input.placeholder,
779
- ...input.inModal ? { inModal: true } : {}
780
- };
781
- }
782
- function toggleElement(route, toggle, index) {
783
- const label = toggle.label ?? toggle.id;
784
- const id = stableElementId([
785
- "toggle",
786
- route ?? "unknown",
787
- toggle.id ?? label ?? `toggle-${index}`
788
- ]);
789
- return {
790
- id,
791
- role: "toggle",
792
- label,
793
- texts: uniqueTexts([label]),
794
- actions: ["toggle", "press"],
795
- selected: toggle.value,
796
- source: "toggle",
797
- targetId: toggle.id ?? toggle.label,
798
- ...toggle.inModal ? { inModal: true } : {}
799
- };
800
- }
801
- function sliderElement(route, slider, index) {
802
- const label = slider.label ?? slider.id;
803
- const id = stableElementId([
804
- "slider",
805
- route ?? "unknown",
806
- slider.id ?? label ?? `slider-${index}`
807
- ]);
808
- return {
809
- id,
810
- role: "slider",
811
- label,
812
- texts: uniqueTexts([label]),
813
- actions: ["setValue"],
814
- disabled: slider.disabled,
815
- source: "slider",
816
- targetId: slider.id ?? slider.label,
817
- ...slider.inModal ? { inModal: true } : {}
818
- };
819
- }
820
- function deriveInteractionElements(snapshot) {
821
- const elements = [];
822
- const seen = /* @__PURE__ */ new Set();
823
- const push = (element) => {
824
- if (!element.id || seen.has(element.id)) return;
825
- seen.add(element.id);
826
- elements.push(element);
827
- };
828
- for (const list of snapshot.lists ?? []) {
829
- for (const item of list.items ?? []) {
830
- push(listItemElement(snapshot.route, list, item));
831
- }
832
- }
833
- for (const [index, button] of (snapshot.buttons ?? []).entries()) {
834
- push(buttonElement(snapshot.route, button, index));
835
- }
836
- for (const [index, input] of (snapshot.inputs ?? []).entries()) {
837
- push(inputElement(snapshot.route, input, index));
838
- }
839
- for (const [index, toggle] of (snapshot.toggles ?? []).entries()) {
840
- push(toggleElement(snapshot.route, toggle, index));
841
- }
842
- for (const [index, slider] of (snapshot.sliders ?? []).entries()) {
843
- push(sliderElement(snapshot.route, slider, index));
844
- }
845
- return elements;
846
- }
847
- function findElementForChoice(elements, option) {
848
- if (option.targetId) {
849
- const byTarget = elements.find((element) => element.targetId === option.targetId);
850
- if (byTarget) return byTarget;
851
- }
852
- const optionLabel = normalizePart(option.label ?? option.texts?.join(" "));
853
- if (!optionLabel) return void 0;
854
- return elements.find((element) => {
855
- if (element.role !== "option" && element.role !== "button") return false;
856
- const label = normalizePart(element.label ?? element.texts.join(" "));
857
- return label === optionLabel || label.includes(optionLabel) || optionLabel.includes(label);
858
- });
859
- }
860
- function attachElementIdsToChoiceGroups(choiceGroups, elements) {
861
- return choiceGroups.map((group) => ({
862
- ...group,
863
- options: group.options.map((option) => {
864
- const element = findElementForChoice(elements, option);
865
- if (!element) return option;
866
- const handle = element.listContext?.syntheticId ?? element.id;
867
- return {
868
- ...option,
869
- syntheticId: handle,
870
- targetId: handle
871
- };
872
- })
873
- }));
874
- }
875
-
876
657
  // src/introspection/captureSnapshot.ts
877
658
  function normalizeSliderKey(value) {
878
659
  return (value ?? "").toLowerCase().replace(/^slider[-_]/, "");
@@ -1221,930 +1002,15 @@ function findInteractiveByIdentifier(query, options = {}) {
1221
1002
  }
1222
1003
  }
1223
1004
  if (fiber.child) stack.push(fiber.child);
1224
- }
1225
- if (bestHandle) {
1226
- console.log(
1227
- `[Appilots] findInteractiveByIdentifier: query="${query}" \u2192 kind=${bestHandle.kind} label="${bestHandle.label ?? bestId2}" score=${bestScore}`
1228
- );
1229
- } else {
1230
- console.log(`[Appilots] findInteractiveByIdentifier: query="${query}" \u2192 no match`);
1231
- }
1232
- return bestHandle;
1233
- }
1234
-
1235
- // src/hooks/fingerprint.ts
1236
- function fingerprintAction(action, screen) {
1237
- const p = action.payload ?? {};
1238
- const scope = `@${screen ?? "?"}`;
1239
- switch (action.type) {
1240
- case "navigate":
1241
- return `navigate${scope}:${p.screenName ?? "?"}`;
1242
- case "form_fill": {
1243
- const fields = p.fields ?? [];
1244
- const ids = fields.map((f) => String(f?.fieldId ?? "?")).sort();
1245
- return `form_fill${scope}:${ids.join(",")}`;
1246
- }
1247
- case "ui_interaction":
1248
- return `ui_interaction${scope}:${p.action ?? "?"}:${p.componentId ?? p.targetId ?? "?"}`;
1249
- case "confirm":
1250
- return `confirm${scope}:${p.message ?? p.title ?? "?"}`;
1251
- default:
1252
- return `${action.type}${scope}:${JSON.stringify(p)}`;
1253
- }
1254
- }
1255
-
1256
- // src/hooks/visibleMessages.ts
1257
- var INTERNAL_MARKER_RE = /^\[(?:Permission policy notice|Recovery exhausted|User rejected the proposed action)[^\]]*\]:\s*/i;
1258
- var TECHNICAL_TRACE_RE = /(^|\n)\s*(?:Results:|[-*]\s*[✓✗]\s+|(?:press|click|tap|navigate|form_fill|ui_interaction|scroll_list)\b|(?:Cliquei|Clicked)\s+em\s+(?:el:|list-\d+-item-\d+|[A-Za-z]+:))/i;
1259
- var TECHNICAL_SELECTOR_RE = /\b(?:el:[^\s]+|list-\d+-item-\d+|[A-Za-z][\w-]*:(?!\/\/)[^\s]+|(?:testID|test-id|accessibilityId|accessibility-id)[:=_-][^\s]+)/i;
1260
- function sanitizeVisibleAssistantContent(content) {
1261
- return content.split("\n").map((line) => line.replace(INTERNAL_MARKER_RE, "").trimEnd()).filter((line) => !INTERNAL_MARKER_RE.test(line)).join("\n").trim();
1262
- }
1263
- function humanizeAssistantFallback(content) {
1264
- let text = sanitizeVisibleAssistantContent(content).replace(/\blist-\d+-item-\d+\b/gi, "o item da lista").replace(/\bel:[^\s".]+/gi, "o elemento").replace(
1265
- /"\s*(?:press|click|tap|navigate|form_fill|ui_interaction|scroll_list)\s+on\s+[^"]+"/gi,
1266
- '"a a\xE7\xE3o solicitada"'
1267
- ).replace(/\bsubscription-cancel-[^\s"]+/gi, "cancelamento de assinatura").trim();
1268
- if (text.length < 16) return null;
1269
- const probe = {
1270
- role: "assistant",
1271
- content: text};
1272
- if (isInternalTraceMessage(probe)) return null;
1273
- return text;
1274
- }
1275
- function isInternalTraceMessage(message) {
1276
- if (message.role !== "assistant") return false;
1277
- const content = message.content.trim();
1278
- if (!content) return true;
1279
- return TECHNICAL_TRACE_RE.test(content) || TECHNICAL_SELECTOR_RE.test(content);
1280
- }
1281
-
1282
- // src/hooks/continuationErrors.ts
1283
- function normalize2(value) {
1284
- return value.normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[‘’]/g, "'").toLowerCase();
1285
- }
1286
- function detectLocaleFromUserTexts(texts) {
1287
- for (const raw of texts) {
1288
- const normalized = normalize2(raw);
1289
- if (/[ãõáéíóúâêôç]/i.test(raw) || /\b(voce|voces|pra|assinatura|cancela|canela|segunda|certo|quais|tenho|pode|posso)\b/.test(
1290
- normalized
1291
- )) {
1292
- return "pt";
1293
- }
1294
- if (/[áéíóúñ¿¡]/i.test(raw) || /\b(cancela|segunda|suscripcion|puedo|cuales|hola)\b/.test(normalized)) {
1295
- return "es";
1296
- }
1297
- if (/[àâçéèêëîïôûùüÿœ]/i.test(raw) || /\b(annuler|deuxieme|abonnement|bonjour)\b/.test(normalized)) {
1298
- return "fr";
1299
- }
1300
- }
1301
- return "en";
1302
- }
1303
- var copy = {
1304
- pt: {
1305
- rateLimit: "O provedor de IA atingiu o limite de uso durante a automa\xE7\xE3o. Aguarde um momento e pe\xE7a para continuar, ou aumente o limite de tokens por minuto.",
1306
- invalidInput: "N\xE3o consegui continuar a automa\xE7\xE3o por um problema interno de sincroniza\xE7\xE3o. Pode tentar de novo ou fazer essa parte manualmente?",
1307
- generic: (detail) => `Perdi o fio da automa\xE7\xE3o (${detail}). Pode continuar manualmente ou me pedir de novo?`
1308
- },
1309
- es: {
1310
- rateLimit: "El proveedor de IA alcanz\xF3 su l\xEDmite de uso durante la automatizaci\xF3n. Espera un momento y p\xEDdeme continuar, o aumenta el l\xEDmite de tokens por minuto.",
1311
- invalidInput: "No pude continuar la automatizaci\xF3n por un problema interno de sincronizaci\xF3n. \xBFPuedes intentarlo de nuevo o hacer esta parte manualmente?",
1312
- generic: (detail) => `Perd\xED el hilo de la automatizaci\xF3n (${detail}). \xBFPuedes continuar manualmente o ped\xEDrmelo de nuevo?`
1313
- },
1314
- fr: {
1315
- rateLimit: "Le fournisseur d'IA a atteint sa limite d'utilisation pendant l'automatisation. Attendez un instant et demandez-moi de continuer, ou augmentez la limite de tokens par minute.",
1316
- invalidInput: "Je n'ai pas pu poursuivre l'automatisation \xE0 cause d'un probl\xE8me interne de synchronisation. Pouvez-vous r\xE9essayer ou faire cette \xE9tape manuellement ?",
1317
- generic: (detail) => `J'ai perdu le fil de l'automatisation (${detail}). Vous pouvez continuer manuellement ou me redemander ?`
1318
- },
1319
- en: {
1320
- rateLimit: "Your AI provider hit its rate limit during automation. Wait a moment and ask me to continue, or raise your TPM limit.",
1321
- invalidInput: "I could not continue the automation because of an internal sync issue. You can try again or take over manually.",
1322
- generic: (detail) => `I lost my footing during automation (${detail}). You can take over from here.`
1323
- }
1324
- };
1325
- function buildContinuationFailureMessage(rawError, recentUserTexts) {
1326
- const locale = detectLocaleFromUserTexts(recentUserTexts);
1327
- const strings = copy[locale] ?? copy.en;
1328
- if (/rate limit|tokens per min|tpm/i.test(rawError)) {
1329
- return strings.rateLimit;
1330
- }
1331
- if (/invalid input|validation_error|422/i.test(rawError)) {
1332
- return strings.invalidInput;
1333
- }
1334
- return strings.generic(rawError);
1335
- }
1336
-
1337
- // src/executor/formWriteEffect.ts
1338
- var CREATE_FORM_ROUTE_RE = /vehiclecreate|subscriptioncreate|vehicleedit|subscriptionedit|maintenancecreate|create|cadastr|register|novo|edit/i;
1339
- var SUBMIT_BUTTON_RE = /cadastrar|registrar|salvar|submit|register|guardar|enviar|confirmar|save/i;
1340
- 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;
1341
- function normalize3(value) {
1342
- return value.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
1343
- }
1344
- function routeHintsCreateForm(routeNames) {
1345
- return routeNames.some((name) => CREATE_FORM_ROUTE_RE.test(normalize3(name)));
1346
- }
1347
- function snapshotHasSubmitControl(snap) {
1348
- return (snap.buttons ?? []).some(
1349
- (btn) => SUBMIT_BUTTON_RE.test(normalize3(`${btn.label ?? btn.id ?? ""}`))
1350
- );
1351
- }
1352
- function snapshotHasVisibleSubmitButton(snap) {
1353
- return (snap.buttons ?? []).some(
1354
- (btn) => !btn.disabled && SUBMIT_BUTTON_RE.test(normalize3(`${btn.label ?? btn.id ?? ""}`))
1355
- );
1356
- }
1357
- function snapshotHasVisibleValidation(snap) {
1358
- return (snap.texts ?? []).some((text) => typeof text === "string" && FORM_VALIDATION_TEXT_RE.test(text));
1359
- }
1360
- function snapshotShowsOpenCreateForm(snap, activePath = []) {
1361
- if (!snap) return false;
1362
- const routeNames = [
1363
- ...typeof snap.route === "string" && snap.route ? [snap.route] : [],
1364
- ...activePath
1365
- ];
1366
- const hasFormInputs = (snap.inputs?.length ?? 0) > 0;
1367
- const hasValidation = snapshotHasVisibleValidation(snap);
1368
- const hasFormContext = routeHintsCreateForm(routeNames) || hasFormInputs || hasValidation;
1369
- if (!hasFormContext) return false;
1370
- if (snap.loading === true) return true;
1371
- if (snapshotHasVisibleSubmitButton(snap)) return true;
1372
- if (snapshotHasSubmitControl(snap) && hasFormInputs) return true;
1373
- if (hasValidation) return true;
1374
- return false;
1375
- }
1376
- function formFillLackedWriteEffect(action, snap, activePath = []) {
1377
- if (action.type !== "form_fill") return false;
1378
- if (!snapshotShowsOpenCreateForm(snap, activePath)) return false;
1379
- const payload = action.payload;
1380
- if (payload?.submitAfterFill === true) return true;
1381
- return Array.isArray(payload?.fields) && payload.fields.length > 0;
1382
- }
1383
-
1384
- // src/introspection/actionTargets.ts
1385
- var LIST_ITEM_TARGET_RE = /^list-\d+-item-\d+$/i;
1386
- var DESTRUCTIVE_LABEL_RE = /\b(cancelar?\w*|cancel(?:ar|led|ation|amento)?|delete\w*|exclu\w*|delet\w*|remov\w*|remove|apag\w*|destroy\w*|unsubscribe\w*)\b/i;
1387
- function isListItemPressTarget(value) {
1388
- if (typeof value !== "string" || !value.trim()) return false;
1389
- return LIST_ITEM_TARGET_RE.test(value.trim());
1390
- }
1391
- function looksDestructiveActionLabel(value) {
1392
- if (typeof value !== "string" || !value.trim()) return false;
1393
- return DESTRUCTIVE_LABEL_RE.test(value.trim());
1394
- }
1395
-
1396
- // src/hooks/turnContinuation.ts
1397
- function actionPressTargetId(action) {
1398
- if (action.type !== "ui_interaction") return null;
1399
- const payload = action.payload;
1400
- const componentId = payload?.componentId;
1401
- if (typeof componentId === "string" && componentId.trim()) return componentId.trim();
1402
- const targetId = payload?.targetId;
1403
- if (typeof targetId === "string" && targetId.trim()) return targetId.trim();
1404
- return null;
1405
- }
1406
- function turnHasConfirmActions(actions) {
1407
- return actions.some((action) => action.type === "confirm");
1408
- }
1409
-
1410
- // src/hooks/useAppilotsChat.ts
1411
- var ESCALATION_RECONNECT_MAX_MS = 3e4;
1412
- function resolveScreenMetadata() {
1413
- const activePath = getActiveRouteNames();
1414
- for (let i = activePath.length - 1; i >= 0; i--) {
1415
- const name = activePath[i];
1416
- const meta = name ? getScreenMetadata(name) : void 0;
1417
- if (meta && (Array.isArray(meta.fields) && meta.fields.length > 0 || Array.isArray(meta.actions) && meta.actions.length > 0)) {
1418
- return meta;
1419
- }
1420
- }
1421
- const screen = getCurrentScreen();
1422
- return screen ? getScreenMetadata(screen) : void 0;
1423
- }
1424
- function buildAgentContext(extras = {}) {
1425
- const context = {};
1426
- const screen = getCurrentScreen();
1427
- if (screen) context.currentScreen = screen;
1428
- const navigationState = getNavigationStateSnapshot();
1429
- if (navigationState) context.navigationState = navigationState;
1430
- const screenMeta = resolveScreenMetadata();
1431
- if (screenMeta) context.screenMetadata = screenMeta;
1432
- try {
1433
- context.snapshot = captureSnapshot();
1434
- } catch (err) {
1435
- appilotsDebugWarn("buildAgentContext: captureSnapshot failed", err);
1436
- }
1437
- const registry = componentRegistry.snapshot();
1438
- const filtered = screen ? registry.filter((c) => !c.screen || c.screen === screen) : registry;
1439
- if (filtered.length > 0) context.registeredComponents = filtered;
1440
- const registeredLists = listRegistry.snapshot();
1441
- if (registeredLists.length > 0) {
1442
- context.registeredLists = registeredLists.map(
1443
- ({ scrollToIndex, scrollToOffset, getScrollMetrics, dataPreview, ...rest }) => rest
1444
- );
1445
- }
1446
- if (extras.loadingPending === true) context.loadingPending = true;
1447
- return context;
1448
- }
1449
- function extractPressedComponentId(actions) {
1450
- for (let i = actions.length - 1; i >= 0; i--) {
1451
- const a = actions[i];
1452
- if (!a || a.type !== "ui_interaction") continue;
1453
- const p = a.payload;
1454
- const interaction = p?.action ?? "press";
1455
- if (interaction !== "press" && interaction !== "longPress") continue;
1456
- const id = p?.componentId;
1457
- if (typeof id === "string" && id.length > 0) return id;
1458
- }
1459
- return null;
1460
- }
1461
- function extractNavigateTargetScreen(actions) {
1462
- for (let i = actions.length - 1; i >= 0; i--) {
1463
- const a = actions[i];
1464
- if (!a || a.type !== "navigate") continue;
1465
- const screenName = a.payload?.screenName;
1466
- if (typeof screenName === "string" && screenName.length > 0) return screenName;
1467
- }
1468
- return null;
1469
- }
1470
- function idLooselyMatches(candidate, target) {
1471
- if (!candidate || !target) return false;
1472
- if (candidate === target) return true;
1473
- const c = candidate.toLowerCase();
1474
- const t = target.toLowerCase();
1475
- if (c === t) return true;
1476
- return c.endsWith(`-${t}`) || c.endsWith(`_${t}`) || t.endsWith(`-${c}`) || t.endsWith(`_${c}`);
1477
- }
1478
- function resolveActionHints(screen, pressedComponentId, actionPayload) {
1479
- if (actionPayload && typeof actionPayload === "object") {
1480
- const inline = actionPayload._appilotsInferred;
1481
- if (inline && typeof inline === "object") {
1482
- return inline;
1483
- }
1484
- }
1485
- if (!screen || !pressedComponentId) return void 0;
1486
- const meta = getScreenMetadata(screen);
1487
- const actions = meta?.actions;
1488
- if (!actions || actions.length === 0) return void 0;
1489
- for (const a of actions) {
1490
- if (typeof a === "string") continue;
1491
- if (idLooselyMatches(a.id, pressedComponentId)) {
1492
- const inferred = a.appilotsInferred;
1493
- if (inferred && typeof inferred === "object") return inferred;
1494
- return void 0;
1495
- }
1496
- }
1497
- return void 0;
1498
- }
1499
- function summarizeAction(action) {
1500
- const p = action.payload;
1501
- switch (action.type) {
1502
- case "navigate":
1503
- return `navigate to ${p?.screenName ?? "?"}`;
1504
- case "form_fill": {
1505
- const fields = p?.fields ?? [];
1506
- if (fields.length === 0) return "form_fill (no fields)";
1507
- const parts = fields.slice(0, 3).map((f) => `${f.fieldId}=${JSON.stringify(f.value)}`);
1508
- const extra = fields.length > 3 ? ` +${fields.length - 3} more` : "";
1509
- return `form_fill ${parts.join(", ")}${extra}`;
1510
- }
1511
- case "ui_interaction":
1512
- return `${p?.action ?? "press"} on ${p?.componentId ?? "?"}`;
1513
- case "scroll_list": {
1514
- const target = typeof p?.toIndex === "number" ? `to item #${p.toIndex}` : p?.direction ?? "down";
1515
- return `scroll_list ${p?.listId ? `"${p.listId}" ` : ""}${target}`;
1516
- }
1517
- case "confirm":
1518
- return `confirm: ${p?.message ?? p?.title ?? ""}`;
1519
- default:
1520
- return action.type;
1521
- }
1522
- }
1523
- var MAX_RECOVERY_ATTEMPTS_PER_ACTION = 2;
1524
- var POST_ACTION_LOADING_MAX_MS = 6e3;
1525
- function useAppilotsChat(options = {}) {
1526
- const { client, emit, subscribe } = useAppilotsContext();
1527
- const errorPrefix = options.errorPrefix ?? "Algo deu errado";
1528
- const streamingEnabled = options.streaming !== false;
1529
- const abortRef = useRef(null);
1530
- const [messages, setMessages] = useState([]);
1531
- const [pendingActions, setPendingActions] = useState([]);
1532
- const [isLoading, setIsLoading] = useState(false);
1533
- const [loadingStatusKey, setLoadingStatusKey] = useState("thinking");
1534
- const [error, setError] = useState(null);
1535
- const messageIdCounter = useRef(0);
1536
- const [escalation, setEscalation] = useState(null);
1537
- const escalationRef = useRef(null);
1538
- escalationRef.current = escalation;
1539
- const [escalationStreamEpoch, setEscalationStreamEpoch] = useState(0);
1540
- const escalationReconnectAttemptRef = useRef(0);
1541
- const escalationReconnectTimerRef = useRef(null);
1542
- const escalationSinceRef = useRef(void 0);
1543
- const seenHumanMessageIdsRef = useRef(/* @__PURE__ */ new Set());
1544
- const escalationStrings = options.escalationStrings;
1545
- const currentTurnRef = useRef(null);
1546
- const continuationInFlightRef = useRef(false);
1547
- const recoveryCountByFingerprintRef = useRef(/* @__PURE__ */ new Map());
1548
- const pendingActionsRef = useRef([]);
1549
- pendingActionsRef.current = pendingActions;
1550
- const messagesRef = useRef([]);
1551
- messagesRef.current = messages;
1552
- const addAssistantMessage = useCallback((message) => {
1553
- const visibleContent = message.role === "assistant" ? sanitizeVisibleAssistantContent(message.content) : message.content;
1554
- const candidate = { ...message, content: visibleContent };
1555
- if (isInternalTraceMessage(candidate)) {
1556
- recordAppilotsDebugTrace({
1557
- type: "agent:trace",
1558
- timestamp: Date.now(),
1559
- messageId: candidate.id,
1560
- summary: "Suppressed internal assistant trace from visible chat",
1561
- data: { role: candidate.role, content: message.content }
1562
- });
1563
- const fallback = humanizeAssistantFallback(message.content);
1564
- if (fallback) {
1565
- setMessages((prev) => [...prev, { ...candidate, content: fallback }]);
1566
- }
1567
- return;
1568
- }
1569
- setMessages((prev) => [...prev, candidate]);
1570
- }, []);
1571
- const appendLocalSystemMessage = useCallback(
1572
- (content, metadata) => {
1573
- if (!content) return;
1574
- setMessages((prev) => [
1575
- ...prev,
1576
- {
1577
- id: `system_${++messageIdCounter.current}`,
1578
- role: "system",
1579
- content,
1580
- timestamp: Date.now(),
1581
- ...metadata ? { metadata } : {}
1582
- }
1583
- ]);
1584
- },
1585
- []
1586
- );
1587
- const requestHuman = useCallback(
1588
- async (reason = "user_requested") => {
1589
- if (escalationRef.current) return;
1590
- try {
1591
- const lastError = messagesRef.current.slice().reverse().find((m) => m.role === "assistant" && m.id.startsWith("error_"));
1592
- const result = await client.requestEscalation(reason, lastError?.content);
1593
- setEscalation({ id: result.escalationId, status: result.status });
1594
- escalationReconnectAttemptRef.current = 0;
1595
- appendLocalSystemMessage(escalationStrings?.requested ?? "");
1596
- emit({
1597
- type: "chat:escalation:start",
1598
- timestamp: Date.now(),
1599
- data: { escalationId: result.escalationId, reason }
1600
- });
1601
- } catch (err) {
1602
- setError(err instanceof Error ? err.message : "Failed to request a human");
1603
- }
1604
- },
1605
- [client, emit, appendLocalSystemMessage, escalationStrings]
1606
- );
1607
- useEffect(() => {
1608
- if (!escalation?.id) return;
1609
- const escalationId = escalation.id;
1610
- const close = client.openEscalationStream(escalationId, {
1611
- since: escalationSinceRef.current,
1612
- onHumanMessage: (message) => {
1613
- escalationReconnectAttemptRef.current = 0;
1614
- if (seenHumanMessageIdsRef.current.has(message.id)) return;
1615
- seenHumanMessageIdsRef.current.add(message.id);
1616
- escalationSinceRef.current = message.createdAt;
1617
- setMessages((prev) => [
1618
- ...prev,
1619
- {
1620
- id: message.id,
1621
- role: "human_agent",
1622
- content: message.content,
1623
- timestamp: Date.now()
1624
- }
1625
- ]);
1626
- emit({
1627
- type: "chat:escalation:message",
1628
- timestamp: Date.now(),
1629
- data: { escalationId, messageId: message.id }
1630
- });
1631
- },
1632
- onStatus: (status, operatorName) => {
1633
- escalationReconnectAttemptRef.current = 0;
1634
- if (status === "active") {
1635
- setEscalation((prev) => {
1636
- if (!prev || prev.id !== escalationId) return prev;
1637
- if (prev.status === "active") return prev;
1638
- appendLocalSystemMessage(escalationStrings?.connected ?? "");
1639
- return { ...prev, status: "active", operatorName };
1640
- });
1641
- } else if (status === "resolved") {
1642
- appendLocalSystemMessage(escalationStrings?.resolved ?? "");
1643
- setEscalation(null);
1644
- emit({
1645
- type: "chat:escalation:end",
1646
- timestamp: Date.now(),
1647
- data: { escalationId }
1648
- });
1649
- }
1650
- },
1651
- onClose: () => {
1652
- if (!escalationRef.current || escalationRef.current.id !== escalationId) return;
1653
- const attempt = ++escalationReconnectAttemptRef.current;
1654
- const delay = Math.min(
1655
- ESCALATION_RECONNECT_MAX_MS,
1656
- 1e3 * 2 ** Math.min(attempt, 5)
1657
- );
1658
- escalationReconnectTimerRef.current = setTimeout(() => {
1659
- setEscalationStreamEpoch((n) => n + 1);
1660
- }, delay);
1661
- }
1662
- });
1663
- return () => {
1664
- if (escalationReconnectTimerRef.current) {
1665
- clearTimeout(escalationReconnectTimerRef.current);
1666
- escalationReconnectTimerRef.current = null;
1667
- }
1668
- close();
1669
- };
1670
- }, [client, emit, appendLocalSystemMessage, escalationStrings, escalation?.id, escalationStreamEpoch]);
1671
- const sendEscalationMessage = useCallback(
1672
- async (content) => {
1673
- const current = escalationRef.current;
1674
- if (!current) return;
1675
- const userMessage = {
1676
- id: `local_${++messageIdCounter.current}`,
1677
- role: "user",
1678
- content,
1679
- timestamp: Date.now()
1680
- };
1681
- setMessages((prev) => [...prev, userMessage]);
1682
- try {
1683
- await client.sendEscalationMessage(current.id, content);
1684
- } catch (err) {
1685
- setError(err instanceof Error ? err.message : "Failed to send message");
1686
- }
1687
- },
1688
- [client]
1689
- );
1690
- const beginTurn = useCallback(
1691
- (assistantMessageId, actions, hop) => {
1692
- if (!actions || actions.length === 0) {
1693
- currentTurnRef.current = null;
1694
- return;
1695
- }
1696
- const stamped = actions.map((a) => ({ ...a, messageId: assistantMessageId }));
1697
- setPendingActions((prev) => [...prev, ...stamped]);
1698
- const turn = {
1699
- messageId: assistantMessageId,
1700
- actionIds: new Set(stamped.map((a) => a.id)),
1701
- results: /* @__PURE__ */ new Map(),
1702
- reportedIds: /* @__PURE__ */ new Set(),
1703
- hop,
1704
- hadNavigate: stamped.some((a) => a.type === "navigate"),
1705
- // Snapshot the current screen NOW, before any navigate
1706
- // dispatches. waitForScreenSettle will use this as the
1707
- // baseline to detect that the navigate completed.
1708
- preNavigateScreen: getCurrentScreen(),
1709
- // Full route+params signature — lets settle detect navigate
1710
- // to the same route name with different params.
1711
- preNavigateSignature: getCurrentScreenSignature()
1712
- };
1713
- const originScreen = getCurrentScreen() ?? void 0;
1714
- for (const a of stamped) {
1715
- turn.results.set(a.id, {
1716
- type: a.type,
1717
- success: false,
1718
- summary: summarizeAction(a),
1719
- fingerprint: fingerprintAction(a, originScreen)
1720
- });
1721
- }
1722
- currentTurnRef.current = turn;
1723
- for (const action of stamped) {
1724
- emit({
1725
- type: "agent:action:start",
1726
- timestamp: Date.now(),
1727
- data: { action }
1728
- });
1729
- }
1730
- if (turnHasConfirmActions(stamped)) {
1731
- setIsLoading(false);
1732
- setLoadingStatusKey("thinking");
1733
- }
1734
- },
1735
- [emit]
1736
- );
1737
- const registerSynthesizedTurnAction = useCallback((action) => {
1738
- const turn = currentTurnRef.current;
1739
- if (!turn || turn.actionIds.has(action.id)) return;
1740
- const stamped = { ...action, messageId: turn.messageId };
1741
- turn.actionIds.add(stamped.id);
1742
- turn.results.set(stamped.id, {
1743
- type: stamped.type,
1744
- success: false,
1745
- summary: summarizeAction(stamped),
1746
- fingerprint: fingerprintAction(stamped, getCurrentScreen() ?? void 0)
1747
- });
1748
- setPendingActions((prev) => {
1749
- if (prev.some((candidate) => candidate.id === stamped.id)) return prev;
1750
- return [...prev, stamped];
1751
- });
1752
- if (stamped.type === "confirm") {
1753
- setIsLoading(false);
1754
- setLoadingStatusKey("thinking");
1755
- }
1756
- }, []);
1757
- const runContinuation = useCallback(
1758
- async (turn) => {
1759
- if (continuationInFlightRef.current) return;
1760
- continuationInFlightRef.current = true;
1761
- try {
1762
- setIsLoading(true);
1763
- setLoadingStatusKey("statusWaitingApp");
1764
- const toolWithoutEffectIds = /* @__PURE__ */ new Set();
1765
- let loadingPending = false;
1766
- const turnActions = pendingActionsRef.current.filter(
1767
- (a) => turn.actionIds.has(a.id)
1768
- );
1769
- const pressedId = extractPressedComponentId(turnActions);
1770
- const navigateTargetScreen = extractNavigateTargetScreen(turnActions);
1771
- if (turn.hadNavigate) {
1772
- const settle = await waitForScreenSettle({
1773
- expectingChange: true,
1774
- fromScreen: turn.preNavigateScreen ?? null,
1775
- fromSignature: turn.preNavigateSignature ?? null,
1776
- targetScreen: navigateTargetScreen,
1777
- maxMs: 3500
1778
- });
1779
- console.log(
1780
- `[Appilots] runContinuation: post-navigate settle done \u2014 screen="${settle.screen}" transitioned=${settle.transitioned} timedOut=${settle.timedOut} waitedMs=${settle.waitedMs}`
1781
- );
1782
- const navigationMoved = settle.transitioned || screenDepartedBaseline(turn.preNavigateScreen ?? null, turn.preNavigateSignature ?? null) || !!navigateTargetScreen && !!settle.screen && routesBelongToSameFeature(settle.screen, navigateTargetScreen);
1783
- if (!navigationMoved) {
1784
- appilotsDebugWarn(
1785
- `tool-without-effect: navigate emitted but screen did not change (stayed at "${turn.preNavigateScreen ?? "unknown"}", waitedMs=${settle.waitedMs}). Likely causes: target screenName missing from MCP doc, nested-nav path absent, or React Navigation rejected the route.`
1786
- );
1787
- for (const actionId of turn.actionIds) {
1788
- const r = turn.results.get(actionId);
1789
- if (r && r.type === "navigate") {
1790
- toolWithoutEffectIds.add(actionId);
1791
- }
1792
- }
1793
- }
1794
- const loadSettle = await waitForLoadingSettle({
1795
- fromScreen: settle.screen ?? null,
1796
- maxMs: 3e3
1797
- });
1798
- console.log(
1799
- `[Appilots] runContinuation: post-navigate loading settle \u2014 settled=${loadSettle.settled} loadingPending=${loadSettle.loadingPending} probes=${loadSettle.probeCount} waitedMs=${loadSettle.waitedMs}`
1800
- );
1801
- if (loadSettle.loadingPending) loadingPending = true;
1802
- } else {
1803
- const pressedAction = turnActions.find(
1804
- (a) => a.type === "ui_interaction" && a.payload?.componentId === pressedId
1805
- );
1806
- const hints = resolveActionHints(
1807
- getCurrentScreen(),
1808
- pressedId,
1809
- pressedAction?.payload
1810
- );
1811
- const settleCapMs = hints?.isAsyncTrigger === true ? 1e4 : POST_ACTION_LOADING_MAX_MS;
1812
- const loadSettle = await waitForLoadingSettle({
1813
- pressedComponentId: pressedId,
1814
- fromScreen: getCurrentScreen(),
1815
- maxMs: settleCapMs
1816
- });
1817
- console.log(
1818
- `[Appilots] runContinuation: 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)" : ""}`
1819
- );
1820
- if (loadSettle.loadingPending) loadingPending = true;
1821
- const listPressActions = turnActions.filter((action) => {
1822
- const target = actionPressTargetId(action);
1823
- return target ? isListItemPressTarget(target) : false;
1824
- });
1825
- if (listPressActions.length > 0) {
1826
- const baselineFingerprint = probeLoadingState(
1827
- pressedId,
1828
- getCurrentScreen()
1829
- ).fingerprint;
1830
- const postPressSettle = await waitForScreenSettle({
1831
- expectingChange: true,
1832
- fromScreen: turn.preNavigateScreen ?? null,
1833
- fromSignature: turn.preNavigateSignature ?? null,
1834
- maxMs: 2500
1835
- });
1836
- await waitForLoadingSettle({
1837
- pressedComponentId: pressedId,
1838
- fromScreen: getCurrentScreen(),
1839
- maxMs: 4e3
1840
- });
1841
- const afterFingerprint = probeLoadingState(
1842
- pressedId,
1843
- getCurrentScreen()
1844
- ).fingerprint;
1845
- console.log(
1846
- `[Appilots] runContinuation: post-list-press settle \u2014 screen="${postPressSettle.screen}" transitioned=${postPressSettle.transitioned} timedOut=${postPressSettle.timedOut} waitedMs=${postPressSettle.waitedMs}`
1847
- );
1848
- if (!postPressSettle.transitioned && baselineFingerprint === afterFingerprint) {
1849
- for (const action of listPressActions) {
1850
- const result = turn.results.get(action.id);
1851
- if (result?.success) {
1852
- toolWithoutEffectIds.add(action.id);
1853
- }
1854
- }
1855
- }
1856
- }
1857
- }
1858
- const context = buildAgentContext({ loadingPending });
1859
- const snap = context.snapshot;
1860
- const activePath = context.navigationState?.activePath ?? [];
1861
- for (const action of turnActions) {
1862
- const result = turn.results.get(action.id);
1863
- if (result?.success && formFillLackedWriteEffect(action, snap, activePath)) {
1864
- toolWithoutEffectIds.add(action.id);
1865
- appilotsDebugWarn(
1866
- `tool-without-effect: form_fill succeeded but create form is still open (actionId="${action.id}")`
1867
- );
1868
- }
1869
- }
1870
- const results = Array.from(turn.actionIds).map((actionId) => {
1871
- const r = turn.results.get(actionId);
1872
- if (!r) {
1873
- return {
1874
- actionId,
1875
- type: "unknown",
1876
- success: false,
1877
- error: "no result reported",
1878
- summary: actionId,
1879
- diagnose: void 0,
1880
- recoveryAttempt: 0,
1881
- recoveryExhausted: false,
1882
- toolWithoutEffect: false,
1883
- effect: "unknown"
1884
- };
1885
- }
1886
- const hasToolWithoutEffect = toolWithoutEffectIds.has(actionId);
1887
- let recoveryAttempt = 0;
1888
- let recoveryExhausted = false;
1889
- if (!r.success || hasToolWithoutEffect) {
1890
- const counts = recoveryCountByFingerprintRef.current;
1891
- const previous = counts.get(r.fingerprint) ?? 0;
1892
- const next = previous + 1;
1893
- counts.set(r.fingerprint, next);
1894
- recoveryAttempt = next;
1895
- recoveryExhausted = next >= MAX_RECOVERY_ATTEMPTS_PER_ACTION;
1896
- }
1897
- return {
1898
- actionId,
1899
- type: r.type,
1900
- success: r.success,
1901
- error: r.error,
1902
- summary: r.summary,
1903
- diagnose: r.diagnose,
1904
- recoveryAttempt,
1905
- recoveryExhausted,
1906
- toolWithoutEffect: hasToolWithoutEffect,
1907
- effect: hasToolWithoutEffect ? "none" : r.effect,
1908
- userVisibleStatusKey: r.userVisibleStatusKey
1909
- };
1910
- });
1911
- const exhausted = results.filter((r) => r.recoveryExhausted);
1912
- setLoadingStatusKey(
1913
- results.some((r) => !r.success || r.toolWithoutEffect) ? "statusAdjusting" : "statusAnalyzing"
1914
- );
1915
- console.log(
1916
- `[Appilots] useAppilotsChat: running continuation hop=${turn.hop} (${results.filter((r) => r.success).length}/${results.length} succeeded` + (exhausted.length > 0 ? `, ${exhausted.length} recovery-exhausted` : "") + ")"
1917
- );
1918
- const response = await client.continueAgent(results, context, turn.hop);
1919
- addAssistantMessage(response.message);
1920
- emit({
1921
- type: "agent:message",
1922
- timestamp: Date.now(),
1923
- data: { message: response.message, usage: response.usage }
1924
- });
1925
- if (response.isDone || !response.actions || response.actions.length === 0) {
1926
- currentTurnRef.current = null;
1927
- setIsLoading(false);
1928
- setLoadingStatusKey("thinking");
1929
- return;
1930
- }
1931
- beginTurn(response.message.id, response.actions, response.hop);
1932
- } catch (err) {
1933
- const raw = err instanceof Error ? err.message : "Continuation failed";
1934
- appilotsDebugWarn("useAppilotsChat: continuation failed \u2014", raw);
1935
- const recentUserTexts = messagesRef.current.filter((m) => m.role === "user").slice(-4).map((m) => m.content);
1936
- const friendly = buildContinuationFailureMessage(raw, recentUserTexts);
1937
- addAssistantMessage({
1938
- id: `error_${++messageIdCounter.current}`,
1939
- role: "assistant",
1940
- content: friendly,
1941
- timestamp: Date.now()
1942
- });
1943
- if (!escalationRef.current && escalationStrings?.offer) {
1944
- appendLocalSystemMessage(escalationStrings.offer, { escalationOffer: true });
1945
- }
1946
- if (turn) {
1947
- setPendingActions(
1948
- (prev) => prev.map(
1949
- (a) => turn.actionIds.has(a.id) && (a.status === "pending" || a.status === "executing") ? { ...a, status: "failed", error: friendly } : a
1950
- )
1951
- );
1952
- for (const actionId of turn.actionIds) {
1953
- if (turn.reportedIds.has(actionId)) continue;
1954
- emit({
1955
- type: "agent:action:error",
1956
- timestamp: Date.now(),
1957
- data: { actionId, error: friendly, abandoned: true }
1958
- });
1959
- }
1960
- }
1961
- currentTurnRef.current = null;
1962
- setIsLoading(false);
1963
- setLoadingStatusKey("thinking");
1964
- } finally {
1965
- continuationInFlightRef.current = false;
1966
- }
1967
- },
1968
- [client, emit, addAssistantMessage, beginTurn]
1969
- );
1970
- useEffect(() => {
1971
- return subscribe((event) => {
1972
- if (event.type === "agent:action:start") {
1973
- const action = event.data?.action;
1974
- if (!action?.id || action.type !== "confirm") return;
1975
- const injectedBy = action.payload?.injectedBy;
1976
- if (injectedBy !== "sdk-local") return;
1977
- registerSynthesizedTurnAction(action);
1978
- return;
1979
- }
1980
- if (event.type !== "agent:action:complete" && event.type !== "agent:action:error") return;
1981
- const turn = currentTurnRef.current;
1982
- if (!turn) return;
1983
- const actionId = event.data?.actionId;
1984
- if (!actionId || !turn.actionIds.has(actionId)) return;
1985
- if (turn.reportedIds.has(actionId)) return;
1986
- const success = event.type === "agent:action:complete";
1987
- const errorMsg = event.data?.error || void 0;
1988
- const diagnose = event.data?.diagnose;
1989
- const effect = event.data?.effect;
1990
- const userVisibleStatusKey = event.data?.userVisibleStatusKey;
1991
- const existing = turn.results.get(actionId);
1992
- if (existing) {
1993
- existing.success = success;
1994
- existing.error = errorMsg;
1995
- existing.diagnose = diagnose;
1996
- existing.effect = effect;
1997
- existing.userVisibleStatusKey = userVisibleStatusKey;
1998
- }
1999
- turn.reportedIds.add(actionId);
2000
- if (turn.reportedIds.size >= turn.actionIds.size) {
2001
- void runContinuation(turn);
2002
- }
2003
- });
2004
- }, [subscribe, runContinuation, registerSynthesizedTurnAction]);
2005
- const sendMessage = useCallback(
2006
- async (content) => {
2007
- if (!content.trim() || isLoading) return;
2008
- if (escalationRef.current) {
2009
- await sendEscalationMessage(content.trim());
2010
- return;
2011
- }
2012
- setError(null);
2013
- setIsLoading(true);
2014
- setLoadingStatusKey("statusAnalyzing");
2015
- currentTurnRef.current = null;
2016
- recoveryCountByFingerprintRef.current = /* @__PURE__ */ new Map();
2017
- const userMessage = {
2018
- id: `local_${++messageIdCounter.current}`,
2019
- role: "user",
2020
- content: content.trim(),
2021
- timestamp: Date.now()
2022
- };
2023
- setMessages((prev) => [...prev, userMessage]);
2024
- const streamingId = `streaming_${++messageIdCounter.current}`;
2025
- let streamedVisible = false;
2026
- const removeStreamingPlaceholder = () => {
2027
- if (!streamedVisible) return;
2028
- streamedVisible = false;
2029
- setMessages((prev) => prev.filter((m) => m.id !== streamingId));
2030
- };
2031
- const abortController = new AbortController();
2032
- abortRef.current = abortController;
2033
- try {
2034
- const context = buildAgentContext();
2035
- console.log(
2036
- `[Appilots] useAppilotsChat sendMessage: context \u2014 currentScreen="${context.currentScreen ?? "unknown"}", snapshot={texts:${context.snapshot?.texts?.length ?? 0}, inputs:${context.snapshot?.inputs?.length ?? 0}, buttons:${context.snapshot?.buttons?.length ?? 0}, elements:${context.snapshot?.elements?.length ?? 0}, lists:${context.snapshot?.lists?.length ?? 0}, listItems:${(context.snapshot?.lists ?? []).reduce((acc, l) => acc + (l.items?.length ?? 0), 0)}, choiceGroups:${context.snapshot?.choiceGroups?.length ?? 0}}`
2037
- );
2038
- let response;
2039
- if (streamingEnabled) {
2040
- try {
2041
- response = await client.sendMessageStream(content.trim(), context, {
2042
- signal: abortController.signal,
2043
- onTextDelta: (_chunk, fullText) => {
2044
- const visible = sanitizeVisibleAssistantContent(fullText);
2045
- if (!streamedVisible) {
2046
- streamedVisible = true;
2047
- setMessages((prev) => [
2048
- ...prev,
2049
- { id: streamingId, role: "assistant", content: visible, timestamp: Date.now() }
2050
- ]);
2051
- } else {
2052
- setMessages(
2053
- (prev) => prev.map((m) => m.id === streamingId ? { ...m, content: visible } : m)
2054
- );
2055
- }
2056
- }
2057
- });
2058
- } catch (err) {
2059
- if (err instanceof StreamTransportError) {
2060
- console.warn(`[Appilots] streaming unavailable, falling back to JSON: ${err.message}`);
2061
- response = await client.sendMessage(content.trim(), context);
2062
- } else {
2063
- throw err;
2064
- }
2065
- }
2066
- } else {
2067
- response = await client.sendMessage(content.trim(), context);
2068
- }
2069
- console.log(
2070
- `[Appilots] useAppilotsChat sendMessage: response received \u2014 actions=${response.actions?.length ?? 0}, hasSummary=${!!response.summaryMessage}`
2071
- );
2072
- removeStreamingPlaceholder();
2073
- addAssistantMessage(response.message);
2074
- beginTurn(response.message.id, response.actions, 1);
2075
- if (response.summaryMessage) {
2076
- addAssistantMessage(response.summaryMessage);
2077
- }
2078
- emit({
2079
- type: "agent:message",
2080
- timestamp: Date.now(),
2081
- data: { message: response.message, usage: response.usage }
2082
- });
2083
- if (!response.actions || response.actions.length === 0) {
2084
- setIsLoading(false);
2085
- setLoadingStatusKey("thinking");
2086
- }
2087
- } catch (err) {
2088
- if (err instanceof StreamAbortError || err instanceof Error && err.name === "AbortError") {
2089
- setIsLoading(false);
2090
- setLoadingStatusKey("thinking");
2091
- return;
2092
- }
2093
- removeStreamingPlaceholder();
2094
- const errorMessage = err instanceof Error ? err.message : "Failed to send message";
2095
- setError(errorMessage);
2096
- const errorMsg = {
2097
- id: `error_${++messageIdCounter.current}`,
2098
- role: "assistant",
2099
- content: `${errorPrefix}: ${errorMessage}`,
2100
- timestamp: Date.now()
2101
- };
2102
- addAssistantMessage(errorMsg);
2103
- setIsLoading(false);
2104
- setLoadingStatusKey("thinking");
2105
- } finally {
2106
- if (abortRef.current === abortController) {
2107
- abortRef.current = null;
2108
- }
2109
- }
2110
- },
2111
- [client, emit, isLoading, addAssistantMessage, beginTurn, errorPrefix, streamingEnabled]
2112
- );
2113
- const cancelMessage = useCallback(() => {
2114
- abortRef.current?.abort();
2115
- }, []);
2116
- const clearMessages = useCallback(() => {
2117
- setMessages([]);
2118
- setPendingActions([]);
2119
- setError(null);
2120
- setIsLoading(false);
2121
- setLoadingStatusKey("thinking");
2122
- currentTurnRef.current = null;
2123
- continuationInFlightRef.current = false;
2124
- recoveryCountByFingerprintRef.current = /* @__PURE__ */ new Map();
2125
- if (escalationRef.current) {
2126
- void client.abandonEscalation(escalationRef.current.id);
2127
- }
2128
- setEscalation(null);
2129
- escalationSinceRef.current = void 0;
2130
- seenHumanMessageIdsRef.current = /* @__PURE__ */ new Set();
2131
- escalationReconnectAttemptRef.current = 0;
2132
- client.destroySession();
2133
- }, [client]);
2134
- const clearError = useCallback(() => setError(null), []);
2135
- return {
2136
- messages,
2137
- isLoading,
2138
- loadingStatusKey,
2139
- error,
2140
- pendingActions,
2141
- escalation,
2142
- requestHuman,
2143
- sendMessage,
2144
- cancelMessage,
2145
- clearMessages,
2146
- clearError
2147
- };
1005
+ }
1006
+ if (bestHandle) {
1007
+ console.log(
1008
+ `[Appilots] findInteractiveByIdentifier: query="${query}" \u2192 kind=${bestHandle.kind} label="${bestHandle.label ?? bestId2}" score=${bestScore}`
1009
+ );
1010
+ } else {
1011
+ console.log(`[Appilots] findInteractiveByIdentifier: query="${query}" \u2192 no match`);
1012
+ }
1013
+ return bestHandle;
2148
1014
  }
2149
1015
 
2150
1016
  // src/executor/handlers/navigateHandler.ts
@@ -2330,7 +1196,7 @@ function markedItemIndexFromFiber(fiber) {
2330
1196
  const value = fiber?.memoizedProps?.__appilotsItemIndex;
2331
1197
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
2332
1198
  }
2333
- function normalize4(value) {
1199
+ function normalize2(value) {
2334
1200
  return String(value ?? "").toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "");
2335
1201
  }
2336
1202
  function hasPressHandler3(fiber) {
@@ -2589,9 +1455,9 @@ function pressResolvedListItem(list, itemFiber, listIndex, itemIndex) {
2589
1455
  }
2590
1456
  }
2591
1457
  function itemTextScore(item, locator) {
2592
- const needles = [locator.label, ...locator.texts ?? []].map(normalize4).filter((text) => text.length >= 2);
1458
+ const needles = [locator.label, ...locator.texts ?? []].map(normalize2).filter((text) => text.length >= 2);
2593
1459
  if (needles.length === 0) return 0;
2594
- const haystack = collectTexts(item).map(normalize4).filter(Boolean);
1460
+ const haystack = collectTexts(item).map(normalize2).filter(Boolean);
2595
1461
  if (haystack.length === 0) return 0;
2596
1462
  let score = 0;
2597
1463
  for (const needle of needles) {
@@ -2609,11 +1475,11 @@ function scoreItemCandidate(item, zeroBasedIndex, locator) {
2609
1475
  const markedIndex = markedItemIndexFromFiber(item);
2610
1476
  if (locator.itemKey) {
2611
1477
  if (itemKey === locator.itemKey) score += 1e3;
2612
- else if (itemKey && normalize4(itemKey) === normalize4(locator.itemKey)) score += 950;
1478
+ else if (itemKey && normalize2(itemKey) === normalize2(locator.itemKey)) score += 950;
2613
1479
  }
2614
1480
  if (locator.reactKey) {
2615
1481
  if (reactKey === locator.reactKey) score += 900;
2616
- else if (reactKey && normalize4(reactKey) === normalize4(locator.reactKey)) score += 850;
1482
+ else if (reactKey && normalize2(reactKey) === normalize2(locator.reactKey)) score += 850;
2617
1483
  }
2618
1484
  if (locator.itemIndex && zeroBasedIndex === locator.itemIndex - 1) score += 750;
2619
1485
  if (locator.itemIndex && markedIndex !== void 0 && (markedIndex === locator.itemIndex || markedIndex === locator.itemIndex - 1)) {
@@ -2624,8 +1490,8 @@ function scoreItemCandidate(item, zeroBasedIndex, locator) {
2624
1490
  }
2625
1491
  function candidateLists(containers, locator) {
2626
1492
  if (locator.listId) {
2627
- const normalizedId = normalize4(locator.listId);
2628
- const matches = containers.map((list, listIndex) => ({ list, listIndex })).filter(({ list }) => normalize4(list.listId) === normalizedId);
1493
+ const normalizedId = normalize2(locator.listId);
1494
+ const matches = containers.map((list, listIndex) => ({ list, listIndex })).filter(({ list }) => normalize2(list.listId) === normalizedId);
2629
1495
  if (matches.length > 0) return matches;
2630
1496
  }
2631
1497
  if (typeof locator.listIndex === "number") {
@@ -2633,8 +1499,8 @@ function candidateLists(containers, locator) {
2633
1499
  return list ? [{ list, listIndex: locator.listIndex }] : [];
2634
1500
  }
2635
1501
  if (locator.listLabel) {
2636
- const normalizedLabel = normalize4(locator.listLabel);
2637
- const matches = containers.map((list, listIndex) => ({ list, listIndex })).filter(({ list }) => normalize4(list.listLabel).includes(normalizedLabel));
1502
+ const normalizedLabel = normalize2(locator.listLabel);
1503
+ const matches = containers.map((list, listIndex) => ({ list, listIndex })).filter(({ list }) => normalize2(list.listLabel).includes(normalizedLabel));
2638
1504
  if (matches.length > 0) return matches;
2639
1505
  }
2640
1506
  return containers.map((list, listIndex) => ({ list, listIndex }));
@@ -2970,7 +1836,7 @@ function pressElementOrTarget(identifier, registry) {
2970
1836
  // src/executor/formFieldRewrite.ts
2971
1837
  var LIST_ORDINAL_RE = /^list-\d+-item-\d+$/i;
2972
1838
  var PLATE_RE = /^[A-Z]{3}-?\d[A-Z0-9]\d{2}$/i;
2973
- function normalize5(value) {
1839
+ function normalize4(value) {
2974
1840
  return value.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]/g, "");
2975
1841
  }
2976
1842
  function resolveScreenForFormFields() {
@@ -3010,14 +1876,14 @@ function collectKnownFormFields(registry) {
3010
1876
  return registryFields;
3011
1877
  }
3012
1878
  function fieldHaystack(field) {
3013
- return normalize5(`${field.id} ${field.label ?? ""}`);
1879
+ return normalize4(`${field.id} ${field.label ?? ""}`);
3014
1880
  }
3015
1881
  function isMisplacedFieldId(fieldId, knownIds) {
3016
1882
  const trimmed = fieldId.trim();
3017
1883
  if (!trimmed) return true;
3018
1884
  if (LIST_ORDINAL_RE.test(trimmed)) return true;
3019
1885
  if (/^\d+$/.test(trimmed)) return true;
3020
- const normalized = normalize5(trimmed);
1886
+ const normalized = normalize4(trimmed);
3021
1887
  if (knownIds.has(normalized)) return false;
3022
1888
  for (const known of knownIds) {
3023
1889
  if (known.length >= 4 && (known.includes(normalized) || normalized.includes(known))) {
@@ -3027,18 +1893,18 @@ function isMisplacedFieldId(fieldId, knownIds) {
3027
1893
  return true;
3028
1894
  }
3029
1895
  function valueMatchesOption(field, rawValue) {
3030
- const value = normalize5(String(rawValue ?? ""));
1896
+ const value = normalize4(String(rawValue ?? ""));
3031
1897
  if (!value || !Array.isArray(field.options)) return false;
3032
1898
  return field.options.some((option) => {
3033
- const label = normalize5(option.label ?? "");
3034
- const optionValue = normalize5(option.value ?? "");
1899
+ const label = normalize4(option.label ?? "");
1900
+ const optionValue = normalize4(option.value ?? "");
3035
1901
  return label === value || optionValue === value || value.includes(label) || value.includes(optionValue);
3036
1902
  });
3037
1903
  }
3038
1904
  function inferFieldIdForValue(rawValue, fields, used) {
3039
1905
  const value = String(rawValue ?? "").trim();
3040
1906
  if (!value) return void 0;
3041
- const normalizedValue = normalize5(value);
1907
+ const normalizedValue = normalize4(value);
3042
1908
  for (const field of fields) {
3043
1909
  if (used.has(field.id)) continue;
3044
1910
  if (valueMatchesOption(field, value)) return field.id;
@@ -3075,7 +1941,7 @@ function inferFieldIdForValue(rawValue, fields, used) {
3075
1941
  }
3076
1942
  const nextText = fields.find((field) => {
3077
1943
  if (used.has(field.id)) return false;
3078
- const type = normalize5(field.type ?? "text");
1944
+ const type = normalize4(field.type ?? "text");
3079
1945
  return type === "text" || type === "input" || type === "number" || type === "";
3080
1946
  });
3081
1947
  return nextText?.id;
@@ -3083,7 +1949,7 @@ function inferFieldIdForValue(rawValue, fields, used) {
3083
1949
  function rewriteMisplacedFormFillFields(fields, registry) {
3084
1950
  const knownFields = collectKnownFormFields(registry);
3085
1951
  if (knownFields.length === 0) return fields;
3086
- const knownIds = new Set(knownFields.map((field) => normalize5(field.id)));
1952
+ const knownIds = new Set(knownFields.map((field) => normalize4(field.id)));
3087
1953
  const needsRewrite = fields.some((field) => isMisplacedFieldId(field.fieldId, knownIds));
3088
1954
  if (!needsRewrite) return fields;
3089
1955
  const used = /* @__PURE__ */ new Set();
@@ -3103,7 +1969,7 @@ function rewriteMisplacedFormFillFields(fields, registry) {
3103
1969
  }
3104
1970
 
3105
1971
  // src/executor/formValidation.ts
3106
- var FORM_VALIDATION_TEXT_RE2 = /(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;
1972
+ 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;
3107
1973
  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;
3108
1974
  var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
3109
1975
  var FIELD_HINTS = [
@@ -3123,7 +1989,7 @@ function inferValidationFieldId(message) {
3123
1989
  function readVisibleFormValidation(snap) {
3124
1990
  if (!snap) return void 0;
3125
1991
  for (const text of snap.texts ?? []) {
3126
- if (!text?.trim() || !FORM_VALIDATION_TEXT_RE2.test(text)) continue;
1992
+ if (!text?.trim() || !FORM_VALIDATION_TEXT_RE.test(text)) continue;
3127
1993
  return { fieldId: inferValidationFieldId(text), message: text };
3128
1994
  }
3129
1995
  return void 0;
@@ -3182,27 +2048,6 @@ async function waitForPostSubmitOutcome(options) {
3182
2048
  };
3183
2049
  }
3184
2050
 
3185
- // src/executor/handlers/formFillMatching.ts
3186
- function normalize6(s) {
3187
- return s.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]/g, "");
3188
- }
3189
- function matchScore(needle, hay) {
3190
- if (!needle || !hay) return 0;
3191
- if (needle === hay) return 100;
3192
- if (hay.startsWith(needle) || needle.startsWith(hay)) return 75;
3193
- if (hay.endsWith(needle) || needle.endsWith(hay)) return 60;
3194
- if (hay.includes(needle) || needle.includes(hay)) return 40;
3195
- return 0;
3196
- }
3197
- function isGenericSelectValue(value) {
3198
- if (value === void 0 || value === null || value === "") return true;
3199
- const normalized = normalize6(String(value));
3200
- if (!normalized) return true;
3201
- return /^(any|qualquer|random|aleatorio|aleatoria|auto|automatico|automatica|mock|mockado|mockada|mockados|mockadas|dummy|sample|exemplo|default|padrao|teste|test)$/.test(
3202
- normalized
3203
- );
3204
- }
3205
-
3206
2051
  // ../shared/dist/chunk-ERITSWKE.mjs
3207
2052
  var DEFAULT_FUZZY_FLOOR = 4;
3208
2053
  function normalizeId(value) {
@@ -3288,39 +2133,6 @@ function resolveTarget(requested, candidates, opts = {}) {
3288
2133
  return { status: "matched", id: ids[0], tier: "fuzzy" };
3289
2134
  }
3290
2135
 
3291
- // src/executor/interactionGuidance.ts
3292
- var LIST_ORDINAL_RE2 = /^list-\d+-item-\d+$/i;
3293
- function normalizeId2(value) {
3294
- return value.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
3295
- }
3296
- function isOptionalCosmeticTargetId(targetId) {
3297
- if (typeof targetId !== "string" || !targetId.trim()) return false;
3298
- const id = normalizeId2(targetId);
3299
- return /foto|photo|avatar|icone|icon|emoji|sheetaction|sheet-action|btn-foto/.test(id) || id.includes("photo-sheet");
3300
- }
3301
- function routeLooksLikeFormContext(route) {
3302
- const haystack = normalizeId2(route ?? "");
3303
- return /create|edit|form|cadastr|register|novo|vehiclecreate|subscriptioncreate|vehicleedit|subscriptioncreate/.test(
3304
- haystack
3305
- );
3306
- }
3307
- function screenPathParts(screen) {
3308
- if (!screen) return [];
3309
- return screen.split("/").map((part) => part.trim()).filter(Boolean);
3310
- }
3311
- function isMisappliedListOrdinalOnForm(targetId, screen, activePath) {
3312
- if (typeof targetId !== "string" || !LIST_ORDINAL_RE2.test(targetId.trim())) {
3313
- return false;
3314
- }
3315
- const routes = [...screenPathParts(screen), ...activePath ?? []];
3316
- if (routes.length === 0) return routeLooksLikeFormContext(screen);
3317
- return routes.some((route) => routeLooksLikeFormContext(route));
3318
- }
3319
- function isOptionalStepAutomationFailure(targetId, screen, activePath) {
3320
- return isOptionalCosmeticTargetId(targetId) || isMisappliedListOrdinalOnForm(targetId, screen, activePath);
3321
- }
3322
- var OPTIONAL_STEP_AUTOMATION_HINT = "optional-step-automation-failed";
3323
-
3324
2136
  // src/executor/handlers/uiInteractionHandler.ts
3325
2137
  function findSnapshotButton(targetId) {
3326
2138
  try {
@@ -3823,18 +2635,18 @@ function uiInteractionHandler(payload, context) {
3823
2635
  // src/executor/formSubmitResolve.ts
3824
2636
  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;
3825
2637
  var AGENT_CHROME_TARGET_RE = /\b(fechar|close|dismiss|cancelar|cancel|limpar|clear|chat\s+do\s+assistente|assistant\s+chat)\b/i;
3826
- function normalize7(value) {
2638
+ function normalize5(value) {
3827
2639
  return value.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
3828
2640
  }
3829
2641
  function isAgentChromeTarget(id, label) {
3830
- const hay = normalize7(`${id ?? ""} ${label ?? ""}`);
2642
+ const hay = normalize5(`${id ?? ""} ${label ?? ""}`);
3831
2643
  return AGENT_CHROME_TARGET_RE.test(hay);
3832
2644
  }
3833
2645
  function isSubmitLikeTarget(id, label) {
3834
2646
  if (!id && !label) return false;
3835
2647
  if (isAgentChromeTarget(id, label)) return false;
3836
- const idNorm = normalize7(id ?? "");
3837
- normalize7(label ?? "");
2648
+ const idNorm = normalize5(id ?? "");
2649
+ normalize5(label ?? "");
3838
2650
  if (/^submit[-_]/i.test(id ?? "")) return true;
3839
2651
  if (submitLabelHintsSubmit(label ?? "")) return true;
3840
2652
  if (submitLabelHintsSubmit(id ?? "")) return true;
@@ -3933,7 +2745,7 @@ function readVisibleValidation(fieldId, label) {
3933
2745
  );
3934
2746
  for (const text of texts) {
3935
2747
  const lower = text.toLowerCase();
3936
- if (FORM_VALIDATION_TEXT_RE2.test(lower)) {
2748
+ if (FORM_VALIDATION_TEXT_RE.test(lower)) {
3937
2749
  for (const needle of needles) {
3938
2750
  if (lower.includes(needle.toLowerCase())) return text;
3939
2751
  }
@@ -3953,15 +2765,15 @@ function selectValueApplied(fieldId, requestedValue, currentValue, registry) {
3953
2765
  if (!requested) return false;
3954
2766
  const meta = getMetadataField(fieldId, registry);
3955
2767
  const options = Array.isArray(meta?.options) ? meta.options : [];
3956
- const requestedNorm = normalize6(requested);
3957
- const currentNorm = normalize6(currentValue);
2768
+ const requestedNorm = normalize3(requested);
2769
+ const currentNorm = normalize3(currentValue);
3958
2770
  if (currentNorm === requestedNorm) return true;
3959
2771
  for (const option of options) {
3960
2772
  const value = typeof option?.value === "string" ? option.value : "";
3961
2773
  const label = typeof option?.label === "string" ? option.label : "";
3962
- if (normalize6(value) === requestedNorm && normalize6(label) === currentNorm) return true;
3963
- if (normalize6(label) === requestedNorm && normalize6(label) === currentNorm) return true;
3964
- if (normalize6(value) === requestedNorm && normalize6(value) === currentNorm) return true;
2774
+ if (normalize3(value) === requestedNorm && normalize3(label) === currentNorm) return true;
2775
+ if (normalize3(label) === requestedNorm && normalize3(label) === currentNorm) return true;
2776
+ if (normalize3(value) === requestedNorm && normalize3(value) === currentNorm) return true;
3965
2777
  }
3966
2778
  return false;
3967
2779
  }
@@ -3975,14 +2787,14 @@ function getMetadataField(fieldId, registry) {
3975
2787
  if (comp.kind === "field" && comp.screen) screens.add(comp.screen);
3976
2788
  }
3977
2789
  }
3978
- const needle = normalize6(fieldId);
2790
+ const needle = normalize3(fieldId);
3979
2791
  for (const screenName of screens) {
3980
2792
  const screenMeta = getScreenMetadata(screenName);
3981
2793
  const fields = Array.isArray(screenMeta?.fields) ? screenMeta.fields : [];
3982
2794
  const match = fields.find((field) => {
3983
2795
  if (!field || typeof field !== "object") return false;
3984
- const id = typeof field.id === "string" ? normalize6(field.id) : "";
3985
- const label = typeof field.label === "string" ? normalize6(field.label) : "";
2796
+ const id = typeof field.id === "string" ? normalize3(field.id) : "";
2797
+ const label = typeof field.label === "string" ? normalize3(field.label) : "";
3986
2798
  return id === needle || label === needle;
3987
2799
  });
3988
2800
  if (match) return match;
@@ -3994,11 +2806,11 @@ function resolveSelectOption(fieldId, value, registry) {
3994
2806
  if (!requested) return void 0;
3995
2807
  const meta = getMetadataField(fieldId, registry);
3996
2808
  const options = Array.isArray(meta?.options) ? meta.options : [];
3997
- const requestedNorm = normalize6(requested);
2809
+ const requestedNorm = normalize3(requested);
3998
2810
  for (const option of options) {
3999
2811
  const optionValue = typeof option?.value === "string" ? option.value : "";
4000
2812
  const optionLabel = typeof option?.label === "string" ? option.label : "";
4001
- if (normalize6(optionValue) === requestedNorm || normalize6(optionLabel) === requestedNorm) {
2813
+ if (normalize3(optionValue) === requestedNorm || normalize3(optionLabel) === requestedNorm) {
4002
2814
  return { value: optionValue, label: optionLabel };
4003
2815
  }
4004
2816
  }
@@ -4053,7 +2865,7 @@ async function tryPressSelectOption(fieldId, value, registry, options = {}) {
4053
2865
  }
4054
2866
  function normalizeFieldType(type) {
4055
2867
  if (typeof type !== "string") return void 0;
4056
- const normalized = normalize6(type);
2868
+ const normalized = normalize3(type);
4057
2869
  if (normalized === "select" || normalized === "picker" || normalized === "dropdown") return "select";
4058
2870
  if (normalized === "toggle" || normalized === "switch" || normalized === "checkbox") return "toggle";
4059
2871
  if (normalized === "date") return "date";
@@ -4081,7 +2893,7 @@ function fieldCueTerms(fieldId, label) {
4081
2893
  );
4082
2894
  const terms = /* @__PURE__ */ new Set();
4083
2895
  for (const value of raw) {
4084
- const normalized = normalize6(value);
2896
+ const normalized = normalize3(value);
4085
2897
  if (normalized.length >= 3 && normalized !== "id") terms.add(normalized);
4086
2898
  }
4087
2899
  return [...terms];
@@ -4092,15 +2904,15 @@ function snapshotHasFieldCue(snap, fieldId, label) {
4092
2904
  const visible = [
4093
2905
  ...snap.texts ?? [],
4094
2906
  ...(snap.buttons ?? []).map((b) => b.label ?? b.id ?? "")
4095
- ].map((text) => normalize6(text)).filter(Boolean);
2907
+ ].map((text) => normalize3(text)).filter(Boolean);
4096
2908
  return visible.some((text) => terms.some((term) => text.includes(term) || term.includes(text)));
4097
2909
  }
4098
2910
  function scoreCandidateText(value, texts) {
4099
- const valueNorm = normalize6(String(value ?? ""));
2911
+ const valueNorm = normalize3(String(value ?? ""));
4100
2912
  if (!valueNorm) return 0;
4101
2913
  let best = 0;
4102
2914
  for (const text of texts) {
4103
- const textNorm = normalize6(text);
2915
+ const textNorm = normalize3(text);
4104
2916
  if (!textNorm) continue;
4105
2917
  best = Math.max(best, matchScore(valueNorm, textNorm));
4106
2918
  }
@@ -4128,7 +2940,7 @@ function buildChoiceCandidates(snap, fieldId, value, allowFirstVisibleWhenNoCue,
4128
2940
  const groupCueScore = Math.max(
4129
2941
  globalCueScore,
4130
2942
  ...groupTexts.map((text) => {
4131
- const normalized = normalize6(text);
2943
+ const normalized = normalize3(text);
4132
2944
  return fieldCueTerms(fieldId, fieldLabel).some(
4133
2945
  (term) => normalized.includes(term) || term.includes(normalized)
4134
2946
  ) ? 80 : 0;
@@ -4165,7 +2977,7 @@ function buildChoiceCandidates(snap, fieldId, value, allowFirstVisibleWhenNoCue,
4165
2977
  const elementCueScore = Math.max(
4166
2978
  fieldCueVisible && elements.length <= 12 ? 50 : 0,
4167
2979
  ...cueTexts.map((text) => {
4168
- const normalized = normalize6(text);
2980
+ const normalized = normalize3(text);
4169
2981
  return fieldTerms.some(
4170
2982
  (term) => normalized.includes(term) || term.includes(normalized)
4171
2983
  ) ? 80 : 0;
@@ -4253,7 +3065,7 @@ async function selectVisibleChoiceAsync(fieldId, value, registry, options) {
4253
3065
  }
4254
3066
  function isContinuationButton(label) {
4255
3067
  if (!label) return false;
4256
- return /^(next|continue|proximo|prox|continuar|avancar|avançar)$/i.test(normalize6(label));
3068
+ return /^(next|continue|proximo|prox|continuar|avancar|avançar)$/i.test(normalize3(label));
4257
3069
  }
4258
3070
  function pressContinuationButton(registry) {
4259
3071
  const snap = captureSnapshot();
@@ -4287,14 +3099,14 @@ function findField(fieldId, registry) {
4287
3099
  }
4288
3100
  }
4289
3101
  const currentScreenForTieBreak = getCurrentScreen();
4290
- const needle = normalize6(fieldId);
3102
+ const needle = normalize3(fieldId);
4291
3103
  let bestComp;
4292
3104
  let bestScore = 0;
4293
3105
  let bestOnCurrentScreen = false;
4294
3106
  for (const comp of snapshot) {
4295
3107
  if (comp.kind !== "field") continue;
4296
- const candidates = [normalize6(comp.id)];
4297
- if (comp.label) candidates.push(normalize6(comp.label));
3108
+ const candidates = [normalize3(comp.id)];
3109
+ if (comp.label) candidates.push(normalize3(comp.label));
4298
3110
  let candidateScore = 0;
4299
3111
  for (const cand of candidates) {
4300
3112
  const s = matchScore(needle, cand);
@@ -4325,12 +3137,12 @@ function findField(fieldId, registry) {
4325
3137
  );
4326
3138
  if (metaField && typeof metaField === "object" && metaField.label) {
4327
3139
  const metaLabel = metaField.label;
4328
- const metaNeedle = normalize6(metaLabel);
3140
+ const metaNeedle = normalize3(metaLabel);
4329
3141
  let mBest;
4330
3142
  let mBestScore = 0;
4331
3143
  for (const comp of snapshot) {
4332
3144
  if (comp.kind !== "field" || !comp.label) continue;
4333
- const score = matchScore(metaNeedle, normalize6(comp.label));
3145
+ const score = matchScore(metaNeedle, normalize3(comp.label));
4334
3146
  if (score > mBestScore) {
4335
3147
  mBestScore = score;
4336
3148
  mBest = comp;
@@ -4835,7 +3647,7 @@ var FALLBACK_PAGE_PX = 600;
4835
3647
  var PAGE_FRACTION = 0.85;
4836
3648
  var POST_SCROLL_MOUNT_MS = 400;
4837
3649
  var sleep3 = (ms) => new Promise((r) => setTimeout(r, ms));
4838
- function normalizeId3(value) {
3650
+ function normalizeId2(value) {
4839
3651
  return String(value ?? "").toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "");
4840
3652
  }
4841
3653
  function buildDiagnose(category, targetId, candidates) {
@@ -4850,8 +3662,8 @@ function resolveListEntry(listId) {
4850
3662
  const entries = listRegistry.snapshot();
4851
3663
  if (entries.length === 0) return {};
4852
3664
  if (listId) {
4853
- const wanted = normalizeId3(listId);
4854
- const match = entries.find((e) => normalizeId3(e.id) === wanted);
3665
+ const wanted = normalizeId2(listId);
3666
+ const match = entries.find((e) => normalizeId2(e.id) === wanted);
4855
3667
  if (match) return { entry: match };
4856
3668
  return { candidates: entries.map((e) => e.id) };
4857
3669
  }
@@ -5148,90 +3960,156 @@ async function executeAction(action, context) {
5148
3960
  });
5149
3961
  return result;
5150
3962
  }
5151
-
5152
- // src/hooks/confirmMatching.ts
5153
- function metadataMatchesComponentId(metaId, componentId) {
5154
- const norm = (s) => s.toLowerCase().replace(/^el:(button|link|input|toggle|listitem)[:_-]/, "").replace(/^(btn-|btn_|button-|button_|link-|link_)/, "").trim();
5155
- const meta = norm(metaId);
5156
- const comp = norm(componentId);
5157
- if (!meta || !comp) return false;
5158
- return meta === comp;
5159
- }
5160
-
5161
- // src/hooks/useAppilotsActions.ts
5162
- var DEFAULT_PERMISSIONS2 = {
5163
- canNavigate: true,
5164
- canFillForms: true,
5165
- canInteractUI: true,
5166
- canSubmitForms: true
5167
- };
5168
- function readPayload(action) {
5169
- return action.payload ?? {};
5170
- }
5171
- function isStructuredRisky(value) {
5172
- return value === true || value === "destructive" || value === "danger" || value === "high";
3963
+ var POST_ACTION_LOADING_MAX_MS = 6e3;
3964
+ function resolveScreenMetadata() {
3965
+ const activePath = getActiveRouteNames();
3966
+ for (let i = activePath.length - 1; i >= 0; i--) {
3967
+ const name = activePath[i];
3968
+ const meta = name ? getScreenMetadata(name) : void 0;
3969
+ if (meta && (Array.isArray(meta.fields) && meta.fields.length > 0 || Array.isArray(meta.actions) && meta.actions.length > 0)) {
3970
+ return meta;
3971
+ }
3972
+ }
3973
+ const screen = getCurrentScreen();
3974
+ return screen ? getScreenMetadata(screen) : void 0;
5173
3975
  }
5174
- function actionMetadataRequiresConfirmation(meta) {
5175
- if (meta.requiresConfirmation === true || meta.destructive === true || meta.effect === "destructive" || meta.riskLevel === "high" || meta.risk === "destructive") {
5176
- return { required: true, label: meta.label, source: "screen-metadata" };
3976
+ function buildAgentContext(extras = {}) {
3977
+ const context = {};
3978
+ context.platform = "react-native";
3979
+ const screen = getCurrentScreen();
3980
+ if (screen) context.currentScreen = screen;
3981
+ const navigationState = getNavigationStateSnapshot();
3982
+ if (navigationState) context.navigationState = navigationState;
3983
+ const screenMeta = resolveScreenMetadata();
3984
+ if (screenMeta) context.screenMetadata = screenMeta;
3985
+ try {
3986
+ context.snapshot = captureSnapshot();
3987
+ } catch (err) {
3988
+ appilotsDebugWarn("buildAgentContext: captureSnapshot failed", err);
3989
+ }
3990
+ const registry = componentRegistry.snapshot();
3991
+ const filtered = screen ? registry.filter((c) => !c.screen || c.screen === screen) : registry;
3992
+ if (filtered.length > 0) context.registeredComponents = filtered;
3993
+ const registeredLists = listRegistry.snapshot();
3994
+ if (registeredLists.length > 0) {
3995
+ context.registeredLists = registeredLists.map(
3996
+ ({ scrollToIndex, scrollToOffset, getScrollMetrics, dataPreview, ...rest }) => rest
3997
+ );
5177
3998
  }
5178
- return { required: false };
3999
+ if (extras.loadingPending === true) context.loadingPending = true;
4000
+ return context;
5179
4001
  }
5180
- function payloadRequiresConfirmation(action) {
5181
- const payload = readPayload(action);
5182
- if (action.destructive === true || payload.destructive === true || payload.requiresConfirmation === true || isStructuredRisky(payload.effect) || isStructuredRisky(payload.risk) || isStructuredRisky(payload.riskLevel) || isStructuredRisky(payload.actionRisk)) {
5183
- return {
5184
- required: true,
5185
- label: typeof payload.actionDescription === "string" && payload.actionDescription || typeof payload.label === "string" && payload.label || void 0,
5186
- source: "action-policy"
5187
- };
4002
+ function resolveActionHints(screen, pressedComponentId, actionPayload) {
4003
+ if (actionPayload && typeof actionPayload === "object") {
4004
+ const inline = actionPayload._appilotsInferred;
4005
+ if (inline && typeof inline === "object") {
4006
+ return inline;
4007
+ }
5188
4008
  }
5189
- const policy = payload._appilotsPolicy;
5190
- if (policy && typeof policy === "object") {
5191
- const p = policy;
5192
- if (p.requiresConfirmation === true || isStructuredRisky(p.effect) || isStructuredRisky(p.risk) || isStructuredRisky(p.riskLevel)) {
5193
- return {
5194
- required: true,
5195
- label: typeof p.summary === "string" ? p.summary : void 0,
5196
- source: "inline-policy"
5197
- };
4009
+ if (!screen || !pressedComponentId) return void 0;
4010
+ const meta = getScreenMetadata(screen);
4011
+ const actions = meta?.actions;
4012
+ if (!actions || actions.length === 0) return void 0;
4013
+ for (const a of actions) {
4014
+ if (typeof a === "string") continue;
4015
+ if (idLooselyMatches(a.id, pressedComponentId)) {
4016
+ const inferred = a.appilotsInferred;
4017
+ if (inferred && typeof inferred === "object") return inferred;
4018
+ return void 0;
5198
4019
  }
5199
4020
  }
5200
- return { required: false };
5201
- }
5202
- function isConfirmGateFor(confirm, target) {
5203
- if (confirm.type !== "confirm") return false;
5204
- const payload = readPayload(confirm);
5205
- const injectedBy = payload.injectedBy;
5206
- return payload.gatedActionId === target.id && (injectedBy === "permissions" || injectedBy === "sdk-local");
5207
- }
5208
- function toDestructiveConfirmPayload(confirm, policy, fallbackLabel) {
5209
- const payload = readPayload(confirm);
5210
- const existingDescription = typeof payload.actionDescription === "string" && payload.actionDescription ? payload.actionDescription : void 0;
5211
- const labelText = policy.label ?? existingDescription ?? fallbackLabel ?? "esta a\xE7\xE3o";
5212
- return {
5213
- ...payload,
5214
- title: "A\xE7\xE3o destrutiva",
5215
- message: `Tem certeza que deseja "${labelText}"?`,
5216
- actionDescription: labelText,
5217
- severity: "danger",
5218
- destructive: true,
5219
- confirmLabel: typeof payload.confirmLabel === "string" && payload.confirmLabel ? payload.confirmLabel : "Sim, executar",
5220
- cancelLabel: typeof payload.cancelLabel === "string" && payload.cancelLabel ? payload.cancelLabel : "Cancelar"
5221
- };
5222
- }
5223
- function confirmNeedsDestructiveUpgrade(confirm) {
5224
- const payload = readPayload(confirm);
5225
- return confirm.destructive !== true || payload.destructive !== true || payload.severity !== "danger";
4021
+ return void 0;
5226
4022
  }
5227
- function actionHasCompletedConfirmGate(action, actions) {
5228
- return actions.some((candidate) => {
5229
- if (!isConfirmGateFor(candidate, action)) return false;
5230
- if (candidate.status !== "completed") return false;
5231
- const payload = readPayload(candidate);
5232
- return payload.destructive === true || payload.severity === "danger";
5233
- });
4023
+ async function settleTurn(input) {
4024
+ const toolWithoutEffectIds = /* @__PURE__ */ new Set();
4025
+ let loadingPending = false;
4026
+ const { turnActions, pressedComponentId: pressedId, navigateTargetScreen } = input;
4027
+ const successById = new Map(input.results.map((r) => [r.actionId, r.success]));
4028
+ const typeById = new Map(input.results.map((r) => [r.actionId, r.type]));
4029
+ if (input.hadNavigate) {
4030
+ const settle = await waitForScreenSettle({
4031
+ expectingChange: true,
4032
+ fromScreen: input.preNavigateScreen ?? null,
4033
+ fromSignature: input.preNavigateSignature ?? null,
4034
+ targetScreen: navigateTargetScreen,
4035
+ maxMs: 3500
4036
+ });
4037
+ console.log(
4038
+ `[Appilots] settleTurn: post-navigate settle done \u2014 screen="${settle.screen}" transitioned=${settle.transitioned} timedOut=${settle.timedOut} waitedMs=${settle.waitedMs}`
4039
+ );
4040
+ const navigationMoved = settle.transitioned || screenDepartedBaseline(
4041
+ input.preNavigateScreen ?? null,
4042
+ input.preNavigateSignature ?? null
4043
+ ) || !!navigateTargetScreen && !!settle.screen && routesBelongToSameFeature(settle.screen, navigateTargetScreen);
4044
+ if (!navigationMoved) {
4045
+ appilotsDebugWarn(
4046
+ `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.`
4047
+ );
4048
+ for (const [actionId, type] of typeById) {
4049
+ if (type === "navigate") toolWithoutEffectIds.add(actionId);
4050
+ }
4051
+ }
4052
+ const loadSettle = await waitForLoadingSettle({
4053
+ fromScreen: settle.screen ?? null,
4054
+ maxMs: 3e3
4055
+ });
4056
+ console.log(
4057
+ `[Appilots] settleTurn: post-navigate loading settle \u2014 settled=${loadSettle.settled} loadingPending=${loadSettle.loadingPending} probes=${loadSettle.probeCount} waitedMs=${loadSettle.waitedMs}`
4058
+ );
4059
+ if (loadSettle.loadingPending) loadingPending = true;
4060
+ } else {
4061
+ const pressedAction = turnActions.find(
4062
+ (a) => a.type === "ui_interaction" && a.payload?.componentId === pressedId
4063
+ );
4064
+ const hints = resolveActionHints(getCurrentScreen(), pressedId, pressedAction?.payload);
4065
+ const settleCapMs = hints?.isAsyncTrigger === true ? 1e4 : POST_ACTION_LOADING_MAX_MS;
4066
+ const loadSettle = await waitForLoadingSettle({
4067
+ pressedComponentId: pressedId,
4068
+ fromScreen: getCurrentScreen(),
4069
+ maxMs: settleCapMs
4070
+ });
4071
+ console.log(
4072
+ `[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)" : ""}`
4073
+ );
4074
+ if (loadSettle.loadingPending) loadingPending = true;
4075
+ const listPressActions = turnActions.filter((action) => {
4076
+ const target = actionPressTargetId(action);
4077
+ return target ? isListItemPressTarget(target) : false;
4078
+ });
4079
+ if (listPressActions.length > 0) {
4080
+ const baselineFingerprint = probeLoadingState(pressedId, getCurrentScreen()).fingerprint;
4081
+ const postPressSettle = await waitForScreenSettle({
4082
+ expectingChange: true,
4083
+ fromScreen: input.preNavigateScreen ?? null,
4084
+ fromSignature: input.preNavigateSignature ?? null,
4085
+ maxMs: 2500
4086
+ });
4087
+ await waitForLoadingSettle({
4088
+ pressedComponentId: pressedId,
4089
+ fromScreen: getCurrentScreen(),
4090
+ maxMs: 4e3
4091
+ });
4092
+ const afterFingerprint = probeLoadingState(pressedId, getCurrentScreen()).fingerprint;
4093
+ console.log(
4094
+ `[Appilots] settleTurn: post-list-press settle \u2014 screen="${postPressSettle.screen}" transitioned=${postPressSettle.transitioned} timedOut=${postPressSettle.timedOut} waitedMs=${postPressSettle.waitedMs}`
4095
+ );
4096
+ if (!postPressSettle.transitioned && baselineFingerprint === afterFingerprint) {
4097
+ for (const action of listPressActions) {
4098
+ if (successById.get(action.id)) {
4099
+ toolWithoutEffectIds.add(action.id);
4100
+ }
4101
+ }
4102
+ }
4103
+ }
4104
+ }
4105
+ return { loadingPending, toolWithoutEffectIds };
5234
4106
  }
4107
+ var reactNativeChatAdapter = {
4108
+ buildContext: buildAgentContext,
4109
+ getCurrentScreen,
4110
+ getCurrentScreenSignature,
4111
+ settleTurn
4112
+ };
5235
4113
  async function runWithConfirmedDestructiveContext(enabled, fn) {
5236
4114
  if (!enabled) return fn();
5237
4115
  const originalAlert = Alert.alert;
@@ -5261,321 +4139,120 @@ async function runWithConfirmedDestructiveContext(enabled, fn) {
5261
4139
  Alert.alert = originalAlert;
5262
4140
  }
5263
4141
  }
5264
- function useAppilotsActions(options = {}) {
5265
- const { client, subscribe, config, emit } = useAppilotsContext();
5266
- const [actions, setActions] = useState([]);
5267
- const { navigationRef, autoExecute = false } = options;
5268
- const actionsRef = useRef([]);
5269
- actionsRef.current = actions;
5270
- const navRefStable = useRef(navigationRef);
5271
- navRefStable.current = navigationRef;
5272
- useEffect(() => {
5273
- const unsubscribe = subscribe((event) => {
5274
- if (event.type === "agent:action:start") {
5275
- const action = event.data?.action;
5276
- if (!action?.id) return;
5277
- console.log(
5278
- `[Appilots] useAppilotsActions: received agent:action:start event, action id="${action.id}", type="${action.type}"`
5279
- );
5280
- setActions((prev) => {
5281
- if (prev.some((a) => a.id === action.id)) return prev;
5282
- return [...prev, action];
5283
- });
5284
- }
5285
- });
5286
- return unsubscribe;
5287
- }, [subscribe]);
5288
- const processedRef = useRef(/* @__PURE__ */ new Set());
5289
- useEffect(() => {
5290
- if (!autoExecute) return;
5291
- const pending = actions.filter(
5292
- (a) => a.status === "pending" && !processedRef.current.has(a.id)
5293
- );
5294
- console.log(`[Appilots] useAppilotsActions: autoExecute check \u2014 ${pending.length} pending action(s) to process`);
5295
- const findGatingConfirm = (target) => {
5296
- return actions.find((a) => {
5297
- if (a.type !== "confirm") return false;
5298
- const payload = a.payload;
5299
- const injectedBy = payload?.injectedBy;
5300
- return payload?.gatedActionId === target.id && (injectedBy === "permissions" || injectedBy === "sdk-local");
5301
- });
5302
- };
5303
- const ensureLocalConfirmIfNeeded = (target) => {
5304
- if (target.type !== "ui_interaction") return false;
5305
- const payload = target.payload;
5306
- const componentId = typeof payload?.componentId === "string" && payload.componentId || typeof payload?.targetId === "string" && payload.targetId || "";
5307
- let policy = payloadRequiresConfirmation(target);
5308
- if (!policy.required && componentId) {
5309
- const screen = getCurrentScreen();
5310
- const meta = screen ? getScreenMetadata(screen) : void 0;
5311
- const actionsMeta = meta?.actions;
5312
- if (actionsMeta && Array.isArray(actionsMeta)) {
5313
- const match = actionsMeta.find((a) => {
5314
- if (typeof a === "string") return false;
5315
- return metadataMatchesComponentId(a.id, componentId);
5316
- });
5317
- if (match && typeof match !== "string") {
5318
- policy = actionMetadataRequiresConfirmation(match);
5319
- }
5320
- }
5321
- }
5322
- const existingGate = findGatingConfirm(target);
5323
- if (existingGate) {
5324
- if (policy.required && confirmNeedsDestructiveUpgrade(existingGate)) {
5325
- setActions(
5326
- (prev) => prev.map(
5327
- (a) => a.id === existingGate.id ? {
5328
- ...a,
5329
- destructive: true,
5330
- payload: toDestructiveConfirmPayload(
5331
- a,
5332
- policy,
5333
- componentId || target.id
5334
- )
5335
- } : a
5336
- )
5337
- );
5338
- }
5339
- return false;
5340
- }
5341
- if (!policy.required) return false;
5342
- const confirmId = `confirm-local-${target.id}`;
5343
- setActions((prev) => {
5344
- const existingGate2 = prev.some((a) => {
5345
- if (a.type !== "confirm") return false;
5346
- const p = a.payload;
5347
- if (!p || p.gatedActionId !== target.id) return false;
5348
- return p.injectedBy === "permissions" || p.injectedBy === "sdk-local";
5349
- });
5350
- if (existingGate2) {
5351
- const gateToUpgrade = prev.find(
5352
- (a) => isConfirmGateFor(a, target) && confirmNeedsDestructiveUpgrade(a)
5353
- );
5354
- if (!gateToUpgrade) return prev;
5355
- return prev.map(
5356
- (a) => a.id === gateToUpgrade.id ? {
5357
- ...a,
5358
- destructive: true,
5359
- payload: toDestructiveConfirmPayload(
5360
- a,
5361
- policy,
5362
- componentId || target.id
5363
- )
5364
- } : a
5365
- );
5366
- }
5367
- if (prev.some((a) => a.id === confirmId)) return prev;
5368
- console.log(
5369
- `[Appilots] useAppilotsActions: action "${componentId || target.id}" requires confirmation (${policy.source ?? "local-policy"}) \u2014 synthesising local confirm (id="${confirmId}")`
5370
- );
5371
- const synth = {
5372
- id: confirmId,
5373
- type: "confirm",
5374
- payload: toDestructiveConfirmPayload(
5375
- {
5376
- payload: {
5377
- gatedActionId: target.id,
5378
- gatedActionType: target.type,
5379
- injectedBy: "sdk-local"
5380
- },
5381
- messageId: target.messageId
5382
- },
5383
- policy,
5384
- componentId || "esta a\xE7\xE3o"
5385
- ),
5386
- status: "pending",
5387
- messageId: target.messageId
5388
- };
5389
- emit({
5390
- type: "agent:action:start",
5391
- timestamp: Date.now(),
5392
- data: { action: synth }
5393
- });
5394
- const idx = prev.findIndex((a) => a.id === target.id);
5395
- if (idx < 0) return [...prev, synth];
5396
- return [...prev.slice(0, idx), synth, ...prev.slice(idx)];
5397
- });
5398
- return true;
5399
- };
5400
- const executeSequentially = async () => {
5401
- let lastWasNavigate = false;
5402
- let preNavScreen = null;
5403
- let preNavSignature = null;
5404
- for (const action of pending) {
5405
- if (action.type === "confirm") {
5406
- continue;
5407
- }
5408
- if (ensureLocalConfirmIfNeeded(action)) {
5409
- continue;
5410
- }
5411
- const gate = findGatingConfirm(action);
5412
- if (gate) {
5413
- if (gate.status === "pending" || gate.status === "executing") {
5414
- console.log(
5415
- `[Appilots] useAppilotsActions: action id="${action.id}" is gated by confirm id="${gate.id}" (status=${gate.status}); waiting for user decision`
5416
- );
5417
- continue;
5418
- }
5419
- if (gate.status === "failed") {
5420
- console.log(
5421
- `[Appilots] useAppilotsActions: confirm id="${gate.id}" was rejected; skipping gated action id="${action.id}"`
5422
- );
5423
- processedRef.current.add(action.id);
5424
- setActions(
5425
- (prev) => prev.map(
5426
- (a) => a.id === action.id ? { ...a, status: "failed", error: "Confirm canceled by user" } : a
5427
- )
5428
- );
5429
- try {
5430
- await client.completeAction(action.id, false, "Confirm canceled by user");
5431
- } catch {
5432
- }
5433
- emit({
5434
- type: "agent:action:error",
5435
- timestamp: Date.now(),
5436
- data: {
5437
- actionId: action.id,
5438
- actionType: action.type,
5439
- success: false,
5440
- error: "Confirm canceled by user"
5441
- }
5442
- });
5443
- continue;
5444
- }
5445
- }
5446
- if (lastWasNavigate) {
5447
- const settle = await waitForScreenSettle({
5448
- expectingChange: true,
5449
- fromScreen: preNavScreen,
5450
- fromSignature: preNavSignature,
5451
- maxMs: 3500
5452
- });
5453
- console.log(
5454
- `[Appilots] useAppilotsActions: post-navigate settle done \u2014 screen="${settle.screen}" transitioned=${settle.transitioned} timedOut=${settle.timedOut} waitedMs=${settle.waitedMs}`
5455
- );
5456
- }
5457
- console.log(`[Appilots] useAppilotsActions: auto-executing action id="${action.id}" type="${action.type}"`);
5458
- processedRef.current.add(action.id);
5459
- if (action.type === "navigate") {
5460
- preNavScreen = getCurrentScreen();
5461
- preNavSignature = getCurrentScreenSignature();
5462
- }
5463
- const succeeded = await approveActionImpl(action.id);
5464
- if (!succeeded) {
5465
- appilotsDebugWarn(
5466
- `useAppilotsActions: stopping current action batch after failed action id="${action.id}"`
5467
- );
5468
- break;
5469
- }
5470
- lastWasNavigate = action.type === "navigate";
5471
- }
5472
- };
5473
- void executeSequentially();
5474
- }, [actions, autoExecute]);
5475
- const approveActionImpl = useCallback(
5476
- async (actionId) => {
5477
- const action = actionsRef.current.find((a) => a.id === actionId);
5478
- if (!action) return false;
5479
- console.log(`[Appilots] useAppilotsActions: action status transition: pending \u2192 executing (id="${actionId}", type="${action.type}")`);
5480
- setActions(
5481
- (prev) => prev.map((a) => a.id === actionId ? { ...a, status: "executing" } : a)
4142
+ function createReactNativeActionRunner(getOptions) {
4143
+ return {
4144
+ async execute(action, { confirmedDestructive }) {
4145
+ const { permissions, emit, navigationRef } = getOptions();
4146
+ const navRef = navigationRef?.current ? navigationRef : { current: getNavigationRef() };
4147
+ return runWithConfirmedDestructiveContext(
4148
+ confirmedDestructive,
4149
+ () => executeAction(action, {
4150
+ navigationRef: navRef,
4151
+ permissions,
4152
+ emit
4153
+ })
5482
4154
  );
5483
- try {
5484
- const navRef = navRefStable.current?.current ? navRefStable.current : { current: getNavigationRef() };
5485
- const confirmedDestructive = actionHasCompletedConfirmGate(
5486
- action,
5487
- actionsRef.current
5488
- );
5489
- const result = await runWithConfirmedDestructiveContext(
5490
- confirmedDestructive,
5491
- () => executeAction(
5492
- { ...action, status: "executing" },
5493
- {
5494
- navigationRef: navRef,
5495
- permissions: config.permissions ?? DEFAULT_PERMISSIONS2,
5496
- emit
5497
- }
5498
- )
5499
- );
5500
- if (result.success) {
5501
- console.log(`[Appilots] useAppilotsActions: action status transition: executing \u2192 completed (id="${actionId}", type="${action.type}")`);
5502
- setActions(
5503
- (prev) => prev.map((a) => a.id === actionId ? { ...a, status: "completed" } : a)
5504
- );
5505
- try {
5506
- await client.completeAction(actionId, true);
5507
- } catch {
5508
- }
5509
- return true;
5510
- } else {
5511
- appilotsDebugWarn(
5512
- `useAppilotsActions: action status transition: executing \u2192 failed (id="${actionId}", type="${action.type}", error="${result.error}")`
5513
- );
5514
- setActions(
5515
- (prev) => prev.map(
5516
- (a) => a.id === actionId ? { ...a, status: "failed", error: result.error } : a
5517
- )
5518
- );
5519
- try {
5520
- await client.completeAction(
5521
- actionId,
5522
- false,
5523
- result.error,
5524
- result.diagnose
5525
- );
5526
- } catch {
5527
- }
5528
- return false;
5529
- }
5530
- } catch (err) {
5531
- const errorMsg = err instanceof Error ? err.message : "Action failed";
5532
- appilotsDebugWarn(
5533
- `useAppilotsActions: action status transition: executing \u2192 failed (exception) (id="${actionId}", error="${errorMsg}")`
5534
- );
5535
- setActions(
5536
- (prev) => prev.map(
5537
- (a) => a.id === actionId ? { ...a, status: "failed", error: errorMsg } : a
5538
- )
5539
- );
5540
- try {
5541
- await client.completeAction(actionId, false, errorMsg);
5542
- } catch {
5543
- }
5544
- return false;
5545
- }
5546
4155
  },
5547
- [client, config.permissions, emit]
5548
- );
5549
- const rejectAction = useCallback(
5550
- async (actionId) => {
5551
- setActions(
5552
- (prev) => prev.map(
5553
- (a) => a.id === actionId ? { ...a, status: "failed", error: "Rejected by user" } : a
5554
- )
5555
- );
5556
- const action = actionsRef.current.find((a) => a.id === actionId);
5557
- emit({
5558
- type: "agent:action:error",
5559
- timestamp: Date.now(),
5560
- data: {
5561
- actionId,
5562
- actionType: action?.type,
5563
- success: false,
5564
- error: "Rejected by user"
5565
- }
5566
- });
5567
- try {
5568
- await client.completeAction(actionId, false, "Rejected by user");
5569
- } catch {
5570
- }
4156
+ getCurrentScreen,
4157
+ getCurrentScreenSignature,
4158
+ getScreenActionsMetadata(screen) {
4159
+ return getScreenMetadata(screen)?.actions;
5571
4160
  },
4161
+ waitForScreenSettle
4162
+ };
4163
+ }
4164
+
4165
+ // src/hooks/useAppilotsChat.ts
4166
+ function useAppilotsChat(options = {}) {
4167
+ const { client, emit, subscribe } = useAppilotsContext();
4168
+ const machine = useMemo(
4169
+ () => new ChatSessionMachine(
4170
+ { client, adapter: reactNativeChatAdapter, emit, warn: appilotsDebugWarn },
4171
+ options
4172
+ ),
4173
+ // A new machine per client/provider instance — options are pushed in
4174
+ // below so changing copy never resets the transcript.
4175
+ // eslint-disable-next-line react-hooks/exhaustive-deps
4176
+ [client, emit]
4177
+ );
4178
+ const optionsRef = useRef(options);
4179
+ optionsRef.current = options;
4180
+ machine.setOptions(options);
4181
+ const state = useSyncExternalStore(machine.subscribeState, machine.getState, machine.getState);
4182
+ useEffect(() => subscribe(machine.handleEvent), [subscribe, machine]);
4183
+ useEffect(() => () => machine.dispose(), [machine]);
4184
+ const requestHuman = useCallback(
4185
+ (reason) => machine.requestHuman(reason),
4186
+ [machine]
4187
+ );
4188
+ const sendMessage = useCallback((content) => machine.sendMessage(content), [machine]);
4189
+ const cancelMessage = useCallback(() => machine.cancelMessage(), [machine]);
4190
+ const clearMessages = useCallback(() => machine.clearMessages(), [machine]);
4191
+ const clearError = useCallback(() => machine.clearError(), [machine]);
4192
+ return {
4193
+ messages: state.messages,
4194
+ isLoading: state.isLoading,
4195
+ loadingStatusKey: state.loadingStatusKey,
4196
+ error: state.error,
4197
+ pendingActions: state.pendingActions,
4198
+ escalation: state.escalation,
4199
+ requestHuman,
4200
+ sendMessage,
4201
+ cancelMessage,
4202
+ clearMessages,
4203
+ clearError
4204
+ };
4205
+ }
4206
+ var DEFAULT_PERMISSIONS2 = {
4207
+ canNavigate: true,
4208
+ canFillForms: true,
4209
+ canInteractUI: true,
4210
+ canSubmitForms: true
4211
+ };
4212
+ function useAppilotsActions(options = {}) {
4213
+ const { client, subscribe, config, emit } = useAppilotsContext();
4214
+ const { navigationRef, autoExecute = false } = options;
4215
+ const runnerOptionsRef = useRef({
4216
+ permissions: config.permissions ?? DEFAULT_PERMISSIONS2,
4217
+ emit,
4218
+ navigationRef
4219
+ });
4220
+ runnerOptionsRef.current = {
4221
+ permissions: config.permissions ?? DEFAULT_PERMISSIONS2,
4222
+ emit,
4223
+ navigationRef
4224
+ };
4225
+ const machine = useMemo(
4226
+ () => new ActionQueueMachine(
4227
+ {
4228
+ client,
4229
+ adapter: createReactNativeActionRunner(() => runnerOptionsRef.current),
4230
+ emit,
4231
+ warn: appilotsDebugWarn
4232
+ },
4233
+ { autoExecute }
4234
+ ),
4235
+ // eslint-disable-next-line react-hooks/exhaustive-deps
5572
4236
  [client, emit]
5573
4237
  );
4238
+ machine.setOptions({ autoExecute });
4239
+ const state = useSyncExternalStore(machine.subscribeState, machine.getState, machine.getState);
4240
+ useEffect(() => subscribe(machine.handleEvent), [subscribe, machine]);
4241
+ useEffect(() => {
4242
+ if (autoExecute) machine.scheduleDrain();
4243
+ }, [autoExecute, machine, state.actions]);
4244
+ const approveAction = useCallback(
4245
+ (actionId) => machine.approveAction(actionId),
4246
+ [machine]
4247
+ );
4248
+ const rejectAction = useCallback((actionId) => machine.rejectAction(actionId), [machine]);
5574
4249
  return {
5575
- pendingActions: actions.filter((a) => a.status === "pending"),
5576
- executingActions: actions.filter((a) => a.status === "executing"),
5577
- completedActions: actions.filter((a) => a.status === "completed" || a.status === "failed"),
5578
- approveAction: approveActionImpl,
4250
+ pendingActions: state.actions.filter((a) => a.status === "pending"),
4251
+ executingActions: state.actions.filter((a) => a.status === "executing"),
4252
+ completedActions: state.actions.filter(
4253
+ (a) => a.status === "completed" || a.status === "failed"
4254
+ ),
4255
+ approveAction,
5579
4256
  rejectAction
5580
4257
  };
5581
4258
  }