@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.
- package/dist/{chunk-GEM6TPAK.mjs → chunk-42WI3UDB.mjs} +312 -1635
- package/dist/{chunk-KGN3DNGX.js → chunk-CHY5D2PV.js} +5 -5
- package/dist/{chunk-KCDNL4HV.mjs → chunk-GEKCGVJN.mjs} +1 -1
- package/dist/{chunk-HDLZGXNF.mjs → chunk-JBOLV5AM.mjs} +2016 -96
- package/dist/{chunk-SIBKF7AA.js → chunk-V3TUIIT2.js} +2036 -97
- package/dist/{chunk-IASJ7YHC.js → chunk-Y7EA6R2G.js} +412 -1735
- package/dist/hooks/index.d.mts +1 -1
- package/dist/hooks/index.d.ts +1 -1
- package/dist/hooks/index.js +11 -11
- package/dist/hooks/index.mjs +2 -2
- package/dist/{index-BDb1WVNo.d.mts → index-Cb0D6uKH.d.mts} +367 -124
- package/dist/{index-BDb1WVNo.d.ts → index-Cb0D6uKH.d.ts} +367 -124
- package/dist/index.d.mts +109 -341
- package/dist/index.d.ts +109 -341
- package/dist/index.js +84 -462
- package/dist/index.mjs +10 -403
- package/dist/navigation/index.js +13 -13
- package/dist/navigation/index.mjs +2 -2
- package/hooks/package.json +9 -0
- package/metro.d.ts +49 -0
- package/metro.js +0 -37
- package/navigation/package.json +9 -0
- package/package.json +45 -25
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { getFiberRoot, getCurrentScreen, getActiveRouteNames, elementRegistry, useAppilotsContext,
|
|
2
|
-
import { useRef,
|
|
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
|
|
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(
|
|
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(
|
|
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 &&
|
|
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 &&
|
|
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 =
|
|
2628
|
-
const matches = containers.map((list, listIndex) => ({ list, listIndex })).filter(({ list }) =>
|
|
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 =
|
|
2637
|
-
const matches = containers.map((list, listIndex) => ({ list, listIndex })).filter(({ list }) =>
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
3034
|
-
const optionValue =
|
|
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 =
|
|
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 =
|
|
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) =>
|
|
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
|
|
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() || !
|
|
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
|
|
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 =
|
|
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 =
|
|
3837
|
-
|
|
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 (
|
|
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 =
|
|
3957
|
-
const currentNorm =
|
|
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 (
|
|
3963
|
-
if (
|
|
3964
|
-
if (
|
|
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 =
|
|
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" ?
|
|
3985
|
-
const label = typeof field.label === "string" ?
|
|
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 =
|
|
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 (
|
|
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 =
|
|
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 =
|
|
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) =>
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 = [
|
|
4297
|
-
if (comp.label) candidates.push(
|
|
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 =
|
|
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,
|
|
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
|
|
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 =
|
|
4854
|
-
const match = entries.find((e) =>
|
|
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
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
}
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
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
|
|
5175
|
-
|
|
5176
|
-
|
|
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
|
-
|
|
3999
|
+
if (extras.loadingPending === true) context.loadingPending = true;
|
|
4000
|
+
return context;
|
|
5179
4001
|
}
|
|
5180
|
-
function
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
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
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
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
|
|
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
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
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
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
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
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
5550
|
-
|
|
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(
|
|
5578
|
-
|
|
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
|
}
|