@appilots/sdk 0.3.0 → 0.4.1
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-QMSMXTED.mjs → chunk-4GUPZDWT.mjs} +1439 -51
- package/dist/{chunk-MCY5YFYD.js → chunk-J75B2HWG.js} +1447 -57
- package/dist/{chunk-BKFO4DHW.mjs → chunk-L2A4EZ6V.mjs} +325 -1480
- package/dist/{chunk-DUSYQXAQ.mjs → chunk-P2BR4DFF.mjs} +1 -1
- package/dist/{chunk-XNKPS7W3.js → chunk-YSLDAQV7.js} +398 -1553
- package/dist/{chunk-N2I3CA7Q.js → chunk-ZWPABTLA.js} +5 -5
- package/dist/hooks/index.d.mts +1 -2
- package/dist/hooks/index.d.ts +1 -2
- package/dist/hooks/index.js +11 -11
- package/dist/hooks/index.mjs +2 -2
- package/dist/index-Cb0D6uKH.d.mts +1543 -0
- package/dist/index-Cb0D6uKH.d.ts +1543 -0
- package/dist/index.d.mts +5 -282
- package/dist/index.d.ts +5 -282
- package/dist/index.js +65 -65
- package/dist/index.mjs +5 -5
- 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 +31 -12
- package/dist/index-BYVRBzjJ.d.mts +0 -619
- package/dist/index-BYVRBzjJ.d.ts +0 -619
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { getFiberRoot, getCurrentScreen, getActiveRouteNames,
|
|
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, appilotsDebugLog, waitForScreenSettle, screenDepartedBaseline, routesBelongToSameFeature, actionPressTargetId, getNavigationStateSnapshot, getNavigationRef, isOptionalStepAutomationFailure, OPTIONAL_STEP_AUTOMATION_HINT, normalize3, matchScore, idLooselyMatches, parseStableElementId, getAllScreens, snapshotShowsOpenCreateForm, looksDestructiveActionLabel, isGenericSelectValue } from './chunk-4GUPZDWT.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[-_]/, "");
|
|
@@ -1207,843 +988,29 @@ function findInteractiveByIdentifier(query, options = {}) {
|
|
|
1207
988
|
if (shouldSkipSubtree2(fiber)) continue;
|
|
1208
989
|
let kind = null;
|
|
1209
990
|
if (matchesName2(fiber, "TextInput")) kind = "field";
|
|
1210
|
-
else if (matchesName2(fiber, "Switch")) kind = "toggle";
|
|
1211
|
-
else if (isPressableTarget2(fiber)) kind = "target";
|
|
1212
|
-
if (kind && (!options.kind || options.kind === kind)) {
|
|
1213
|
-
const score = scoreMatch(fiber, queryNorm, queryRaw);
|
|
1214
|
-
if (score > bestScore) {
|
|
1215
|
-
bestScore = score;
|
|
1216
|
-
const props = fiber.memoizedProps ?? {};
|
|
1217
|
-
bestId2 = props.testID ?? props.accessibilityLabel ?? props.placeholder ?? (kind === "target" ? findChildText2(fiber) : void 0);
|
|
1218
|
-
if (kind === "field") bestHandle = wrapTextInput(fiber);
|
|
1219
|
-
else if (kind === "toggle") bestHandle = wrapSwitch(fiber);
|
|
1220
|
-
else bestHandle = wrapTouchable(fiber, findChildText2(fiber));
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
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/visibleMessages.ts
|
|
1236
|
-
var INTERNAL_MARKER_RE = /^\[(?:Permission policy notice|Recovery exhausted|User rejected the proposed action)[^\]]*\]:\s*/i;
|
|
1237
|
-
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;
|
|
1238
|
-
var TECHNICAL_SELECTOR_RE = /\b(?:el:[^\s]+|list-\d+-item-\d+|[A-Za-z][\w-]*:(?!\/\/)[^\s]+|(?:testID|test-id|accessibilityId|accessibility-id)[:=_-][^\s]+)/i;
|
|
1239
|
-
function sanitizeVisibleAssistantContent(content) {
|
|
1240
|
-
return content.split("\n").map((line) => line.replace(INTERNAL_MARKER_RE, "").trimEnd()).filter((line) => !INTERNAL_MARKER_RE.test(line)).join("\n").trim();
|
|
1241
|
-
}
|
|
1242
|
-
function humanizeAssistantFallback(content) {
|
|
1243
|
-
let text = sanitizeVisibleAssistantContent(content).replace(/\blist-\d+-item-\d+\b/gi, "o item da lista").replace(/\bel:[^\s".]+/gi, "o elemento").replace(
|
|
1244
|
-
/"\s*(?:press|click|tap|navigate|form_fill|ui_interaction|scroll_list)\s+on\s+[^"]+"/gi,
|
|
1245
|
-
'"a a\xE7\xE3o solicitada"'
|
|
1246
|
-
).replace(/\bsubscription-cancel-[^\s"]+/gi, "cancelamento de assinatura").trim();
|
|
1247
|
-
if (text.length < 16) return null;
|
|
1248
|
-
const probe = {
|
|
1249
|
-
role: "assistant",
|
|
1250
|
-
content: text};
|
|
1251
|
-
if (isInternalTraceMessage(probe)) return null;
|
|
1252
|
-
return text;
|
|
1253
|
-
}
|
|
1254
|
-
function isInternalTraceMessage(message) {
|
|
1255
|
-
if (message.role !== "assistant") return false;
|
|
1256
|
-
const content = message.content.trim();
|
|
1257
|
-
if (!content) return true;
|
|
1258
|
-
return TECHNICAL_TRACE_RE.test(content) || TECHNICAL_SELECTOR_RE.test(content);
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
|
-
// src/executor/formWriteEffect.ts
|
|
1262
|
-
var CREATE_FORM_ROUTE_RE = /vehiclecreate|subscriptioncreate|vehicleedit|subscriptionedit|maintenancecreate|create|cadastr|register|novo|edit/i;
|
|
1263
|
-
var SUBMIT_BUTTON_RE = /cadastrar|registrar|salvar|submit|register|guardar|enviar|confirmar|save/i;
|
|
1264
|
-
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;
|
|
1265
|
-
function normalize3(value) {
|
|
1266
|
-
return value.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
|
|
1267
|
-
}
|
|
1268
|
-
function routeHintsCreateForm(routeNames) {
|
|
1269
|
-
return routeNames.some((name) => CREATE_FORM_ROUTE_RE.test(normalize3(name)));
|
|
1270
|
-
}
|
|
1271
|
-
function snapshotHasSubmitControl(snap) {
|
|
1272
|
-
return (snap.buttons ?? []).some(
|
|
1273
|
-
(btn) => SUBMIT_BUTTON_RE.test(normalize3(`${btn.label ?? btn.id ?? ""}`))
|
|
1274
|
-
);
|
|
1275
|
-
}
|
|
1276
|
-
function snapshotHasVisibleSubmitButton(snap) {
|
|
1277
|
-
return (snap.buttons ?? []).some(
|
|
1278
|
-
(btn) => !btn.disabled && SUBMIT_BUTTON_RE.test(normalize3(`${btn.label ?? btn.id ?? ""}`))
|
|
1279
|
-
);
|
|
1280
|
-
}
|
|
1281
|
-
function snapshotHasVisibleValidation(snap) {
|
|
1282
|
-
return (snap.texts ?? []).some((text) => typeof text === "string" && FORM_VALIDATION_TEXT_RE.test(text));
|
|
1283
|
-
}
|
|
1284
|
-
function snapshotShowsOpenCreateForm(snap, activePath = []) {
|
|
1285
|
-
if (!snap) return false;
|
|
1286
|
-
const routeNames = [
|
|
1287
|
-
...typeof snap.route === "string" && snap.route ? [snap.route] : [],
|
|
1288
|
-
...activePath
|
|
1289
|
-
];
|
|
1290
|
-
const hasFormInputs = (snap.inputs?.length ?? 0) > 0;
|
|
1291
|
-
const hasValidation = snapshotHasVisibleValidation(snap);
|
|
1292
|
-
const hasFormContext = routeHintsCreateForm(routeNames) || hasFormInputs || hasValidation;
|
|
1293
|
-
if (!hasFormContext) return false;
|
|
1294
|
-
if (snap.loading === true) return true;
|
|
1295
|
-
if (snapshotHasVisibleSubmitButton(snap)) return true;
|
|
1296
|
-
if (snapshotHasSubmitControl(snap) && hasFormInputs) return true;
|
|
1297
|
-
if (hasValidation) return true;
|
|
1298
|
-
return false;
|
|
1299
|
-
}
|
|
1300
|
-
function formFillLackedWriteEffect(action, snap, activePath = []) {
|
|
1301
|
-
if (action.type !== "form_fill") return false;
|
|
1302
|
-
if (!snapshotShowsOpenCreateForm(snap, activePath)) return false;
|
|
1303
|
-
const payload = action.payload;
|
|
1304
|
-
if (payload?.submitAfterFill === true) return true;
|
|
1305
|
-
return Array.isArray(payload?.fields) && payload.fields.length > 0;
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
// src/hooks/useAppilotsChat.ts
|
|
1309
|
-
var ESCALATION_RECONNECT_MAX_MS = 3e4;
|
|
1310
|
-
function resolveScreenMetadata() {
|
|
1311
|
-
const activePath = getActiveRouteNames();
|
|
1312
|
-
for (let i = activePath.length - 1; i >= 0; i--) {
|
|
1313
|
-
const name = activePath[i];
|
|
1314
|
-
const meta = name ? getScreenMetadata(name) : void 0;
|
|
1315
|
-
if (meta && (Array.isArray(meta.fields) && meta.fields.length > 0 || Array.isArray(meta.actions) && meta.actions.length > 0)) {
|
|
1316
|
-
return meta;
|
|
1317
|
-
}
|
|
1318
|
-
}
|
|
1319
|
-
const screen = getCurrentScreen();
|
|
1320
|
-
return screen ? getScreenMetadata(screen) : void 0;
|
|
1321
|
-
}
|
|
1322
|
-
function buildAgentContext(extras = {}) {
|
|
1323
|
-
const context = {};
|
|
1324
|
-
context.platform = "react-native";
|
|
1325
|
-
const screen = getCurrentScreen();
|
|
1326
|
-
if (screen) context.currentScreen = screen;
|
|
1327
|
-
const navigationState = getNavigationStateSnapshot();
|
|
1328
|
-
if (navigationState) context.navigationState = navigationState;
|
|
1329
|
-
const screenMeta = resolveScreenMetadata();
|
|
1330
|
-
if (screenMeta) context.screenMetadata = screenMeta;
|
|
1331
|
-
try {
|
|
1332
|
-
context.snapshot = captureSnapshot();
|
|
1333
|
-
} catch (err) {
|
|
1334
|
-
appilotsDebugWarn("buildAgentContext: captureSnapshot failed", err);
|
|
1335
|
-
}
|
|
1336
|
-
const registry = componentRegistry.snapshot();
|
|
1337
|
-
const filtered = screen ? registry.filter((c) => !c.screen || c.screen === screen) : registry;
|
|
1338
|
-
if (filtered.length > 0) context.registeredComponents = filtered;
|
|
1339
|
-
const registeredLists = listRegistry.snapshot();
|
|
1340
|
-
if (registeredLists.length > 0) {
|
|
1341
|
-
context.registeredLists = registeredLists.map(
|
|
1342
|
-
({ scrollToIndex, scrollToOffset, getScrollMetrics, dataPreview, ...rest }) => rest
|
|
1343
|
-
);
|
|
1344
|
-
}
|
|
1345
|
-
if (extras.loadingPending === true) context.loadingPending = true;
|
|
1346
|
-
return context;
|
|
1347
|
-
}
|
|
1348
|
-
function extractPressedComponentId(actions) {
|
|
1349
|
-
for (let i = actions.length - 1; i >= 0; i--) {
|
|
1350
|
-
const a = actions[i];
|
|
1351
|
-
if (!a || a.type !== "ui_interaction") continue;
|
|
1352
|
-
const p = a.payload;
|
|
1353
|
-
const interaction = p?.action ?? "press";
|
|
1354
|
-
if (interaction !== "press" && interaction !== "longPress") continue;
|
|
1355
|
-
const id = p?.componentId;
|
|
1356
|
-
if (typeof id === "string" && id.length > 0) return id;
|
|
1357
|
-
}
|
|
1358
|
-
return null;
|
|
1359
|
-
}
|
|
1360
|
-
function extractNavigateTargetScreen(actions) {
|
|
1361
|
-
for (let i = actions.length - 1; i >= 0; i--) {
|
|
1362
|
-
const a = actions[i];
|
|
1363
|
-
if (!a || a.type !== "navigate") continue;
|
|
1364
|
-
const screenName = a.payload?.screenName;
|
|
1365
|
-
if (typeof screenName === "string" && screenName.length > 0) return screenName;
|
|
1366
|
-
}
|
|
1367
|
-
return null;
|
|
1368
|
-
}
|
|
1369
|
-
function idLooselyMatches(candidate, target) {
|
|
1370
|
-
if (!candidate || !target) return false;
|
|
1371
|
-
if (candidate === target) return true;
|
|
1372
|
-
const c = candidate.toLowerCase();
|
|
1373
|
-
const t = target.toLowerCase();
|
|
1374
|
-
if (c === t) return true;
|
|
1375
|
-
return c.endsWith(`-${t}`) || c.endsWith(`_${t}`) || t.endsWith(`-${c}`) || t.endsWith(`_${c}`);
|
|
1376
|
-
}
|
|
1377
|
-
function resolveActionHints(screen, pressedComponentId, actionPayload) {
|
|
1378
|
-
if (actionPayload && typeof actionPayload === "object") {
|
|
1379
|
-
const inline = actionPayload._appilotsInferred;
|
|
1380
|
-
if (inline && typeof inline === "object") {
|
|
1381
|
-
return inline;
|
|
1382
|
-
}
|
|
1383
|
-
}
|
|
1384
|
-
if (!screen || !pressedComponentId) return void 0;
|
|
1385
|
-
const meta = getScreenMetadata(screen);
|
|
1386
|
-
const actions = meta?.actions;
|
|
1387
|
-
if (!actions || actions.length === 0) return void 0;
|
|
1388
|
-
for (const a of actions) {
|
|
1389
|
-
if (typeof a === "string") continue;
|
|
1390
|
-
if (idLooselyMatches(a.id, pressedComponentId)) {
|
|
1391
|
-
const inferred = a.appilotsInferred;
|
|
1392
|
-
if (inferred && typeof inferred === "object") return inferred;
|
|
1393
|
-
return void 0;
|
|
1394
|
-
}
|
|
1395
|
-
}
|
|
1396
|
-
return void 0;
|
|
1397
|
-
}
|
|
1398
|
-
function summarizeAction(action) {
|
|
1399
|
-
const p = action.payload;
|
|
1400
|
-
switch (action.type) {
|
|
1401
|
-
case "navigate":
|
|
1402
|
-
return `navigate to ${p?.screenName ?? "?"}`;
|
|
1403
|
-
case "form_fill": {
|
|
1404
|
-
const fields = p?.fields ?? [];
|
|
1405
|
-
if (fields.length === 0) return "form_fill (no fields)";
|
|
1406
|
-
const parts = fields.slice(0, 3).map((f) => `${f.fieldId}=${JSON.stringify(f.value)}`);
|
|
1407
|
-
const extra = fields.length > 3 ? ` +${fields.length - 3} more` : "";
|
|
1408
|
-
return `form_fill ${parts.join(", ")}${extra}`;
|
|
1409
|
-
}
|
|
1410
|
-
case "ui_interaction":
|
|
1411
|
-
return `${p?.action ?? "press"} on ${p?.componentId ?? "?"}`;
|
|
1412
|
-
case "scroll_list": {
|
|
1413
|
-
const target = typeof p?.toIndex === "number" ? `to item #${p.toIndex}` : p?.direction ?? "down";
|
|
1414
|
-
return `scroll_list ${p?.listId ? `"${p.listId}" ` : ""}${target}`;
|
|
1415
|
-
}
|
|
1416
|
-
case "confirm":
|
|
1417
|
-
return `confirm: ${p?.message ?? p?.title ?? ""}`;
|
|
1418
|
-
default:
|
|
1419
|
-
return action.type;
|
|
1420
|
-
}
|
|
1421
|
-
}
|
|
1422
|
-
var MAX_RECOVERY_ATTEMPTS_PER_ACTION = 2;
|
|
1423
|
-
var POST_ACTION_LOADING_MAX_MS = 6e3;
|
|
1424
|
-
function useAppilotsChat(options = {}) {
|
|
1425
|
-
const { client, emit, subscribe } = useAppilotsContext();
|
|
1426
|
-
const errorPrefix = options.errorPrefix ?? "Algo deu errado";
|
|
1427
|
-
const streamingEnabled = options.streaming !== false;
|
|
1428
|
-
const abortRef = useRef(null);
|
|
1429
|
-
const [messages, setMessages] = useState([]);
|
|
1430
|
-
const [pendingActions, setPendingActions] = useState([]);
|
|
1431
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
1432
|
-
const [loadingStatusKey, setLoadingStatusKey] = useState("thinking");
|
|
1433
|
-
const [error, setError] = useState(null);
|
|
1434
|
-
const messageIdCounter = useRef(0);
|
|
1435
|
-
const [escalation, setEscalation] = useState(null);
|
|
1436
|
-
const escalationRef = useRef(null);
|
|
1437
|
-
escalationRef.current = escalation;
|
|
1438
|
-
const [escalationStreamEpoch, setEscalationStreamEpoch] = useState(0);
|
|
1439
|
-
const escalationReconnectAttemptRef = useRef(0);
|
|
1440
|
-
const escalationReconnectTimerRef = useRef(null);
|
|
1441
|
-
const escalationSinceRef = useRef(void 0);
|
|
1442
|
-
const seenHumanMessageIdsRef = useRef(/* @__PURE__ */ new Set());
|
|
1443
|
-
const escalationStrings = options.escalationStrings;
|
|
1444
|
-
const currentTurnRef = useRef(null);
|
|
1445
|
-
const continuationInFlightRef = useRef(false);
|
|
1446
|
-
const recoveryCountByFingerprintRef = useRef(/* @__PURE__ */ new Map());
|
|
1447
|
-
const pendingActionsRef = useRef([]);
|
|
1448
|
-
pendingActionsRef.current = pendingActions;
|
|
1449
|
-
const messagesRef = useRef([]);
|
|
1450
|
-
messagesRef.current = messages;
|
|
1451
|
-
const addAssistantMessage = useCallback((message) => {
|
|
1452
|
-
const visibleContent = message.role === "assistant" ? sanitizeVisibleAssistantContent(message.content) : message.content;
|
|
1453
|
-
const candidate = { ...message, content: visibleContent };
|
|
1454
|
-
if (isInternalTraceMessage(candidate)) {
|
|
1455
|
-
recordAppilotsDebugTrace({
|
|
1456
|
-
type: "agent:trace",
|
|
1457
|
-
timestamp: Date.now(),
|
|
1458
|
-
messageId: candidate.id,
|
|
1459
|
-
summary: "Suppressed internal assistant trace from visible chat",
|
|
1460
|
-
data: { role: candidate.role, content: message.content }
|
|
1461
|
-
});
|
|
1462
|
-
const fallback = humanizeAssistantFallback(message.content);
|
|
1463
|
-
if (fallback) {
|
|
1464
|
-
setMessages((prev) => [...prev, { ...candidate, content: fallback }]);
|
|
1465
|
-
}
|
|
1466
|
-
return;
|
|
1467
|
-
}
|
|
1468
|
-
setMessages((prev) => [...prev, candidate]);
|
|
1469
|
-
}, []);
|
|
1470
|
-
const appendLocalSystemMessage = useCallback(
|
|
1471
|
-
(content, metadata) => {
|
|
1472
|
-
if (!content) return;
|
|
1473
|
-
setMessages((prev) => [
|
|
1474
|
-
...prev,
|
|
1475
|
-
{
|
|
1476
|
-
id: `system_${++messageIdCounter.current}`,
|
|
1477
|
-
role: "system",
|
|
1478
|
-
content,
|
|
1479
|
-
timestamp: Date.now(),
|
|
1480
|
-
...metadata ? { metadata } : {}
|
|
1481
|
-
}
|
|
1482
|
-
]);
|
|
1483
|
-
},
|
|
1484
|
-
[]
|
|
1485
|
-
);
|
|
1486
|
-
const requestHuman = useCallback(
|
|
1487
|
-
async (reason = "user_requested") => {
|
|
1488
|
-
if (escalationRef.current) return;
|
|
1489
|
-
try {
|
|
1490
|
-
const lastError = messagesRef.current.slice().reverse().find((m) => m.role === "assistant" && m.id.startsWith("error_"));
|
|
1491
|
-
const result = await client.requestEscalation(reason, lastError?.content);
|
|
1492
|
-
setEscalation({ id: result.escalationId, status: result.status });
|
|
1493
|
-
escalationReconnectAttemptRef.current = 0;
|
|
1494
|
-
appendLocalSystemMessage(escalationStrings?.requested ?? "");
|
|
1495
|
-
emit({
|
|
1496
|
-
type: "chat:escalation:start",
|
|
1497
|
-
timestamp: Date.now(),
|
|
1498
|
-
data: { escalationId: result.escalationId, reason }
|
|
1499
|
-
});
|
|
1500
|
-
} catch (err) {
|
|
1501
|
-
setError(err instanceof Error ? err.message : "Failed to request a human");
|
|
1502
|
-
}
|
|
1503
|
-
},
|
|
1504
|
-
[client, emit, appendLocalSystemMessage, escalationStrings]
|
|
1505
|
-
);
|
|
1506
|
-
useEffect(() => {
|
|
1507
|
-
if (!escalation?.id) return;
|
|
1508
|
-
const escalationId = escalation.id;
|
|
1509
|
-
const close = client.openEscalationStream(escalationId, {
|
|
1510
|
-
since: escalationSinceRef.current,
|
|
1511
|
-
onHumanMessage: (message) => {
|
|
1512
|
-
escalationReconnectAttemptRef.current = 0;
|
|
1513
|
-
if (seenHumanMessageIdsRef.current.has(message.id)) return;
|
|
1514
|
-
seenHumanMessageIdsRef.current.add(message.id);
|
|
1515
|
-
escalationSinceRef.current = message.createdAt;
|
|
1516
|
-
setMessages((prev) => [
|
|
1517
|
-
...prev,
|
|
1518
|
-
{
|
|
1519
|
-
id: message.id,
|
|
1520
|
-
role: "human_agent",
|
|
1521
|
-
content: message.content,
|
|
1522
|
-
timestamp: Date.now()
|
|
1523
|
-
}
|
|
1524
|
-
]);
|
|
1525
|
-
emit({
|
|
1526
|
-
type: "chat:escalation:message",
|
|
1527
|
-
timestamp: Date.now(),
|
|
1528
|
-
data: { escalationId, messageId: message.id }
|
|
1529
|
-
});
|
|
1530
|
-
},
|
|
1531
|
-
onStatus: (status, operatorName) => {
|
|
1532
|
-
escalationReconnectAttemptRef.current = 0;
|
|
1533
|
-
if (status === "active") {
|
|
1534
|
-
setEscalation((prev) => {
|
|
1535
|
-
if (!prev || prev.id !== escalationId) return prev;
|
|
1536
|
-
if (prev.status === "active") return prev;
|
|
1537
|
-
appendLocalSystemMessage(escalationStrings?.connected ?? "");
|
|
1538
|
-
return { ...prev, status: "active", operatorName };
|
|
1539
|
-
});
|
|
1540
|
-
} else if (status === "resolved") {
|
|
1541
|
-
appendLocalSystemMessage(escalationStrings?.resolved ?? "");
|
|
1542
|
-
setEscalation(null);
|
|
1543
|
-
emit({
|
|
1544
|
-
type: "chat:escalation:end",
|
|
1545
|
-
timestamp: Date.now(),
|
|
1546
|
-
data: { escalationId }
|
|
1547
|
-
});
|
|
1548
|
-
}
|
|
1549
|
-
},
|
|
1550
|
-
onClose: () => {
|
|
1551
|
-
if (!escalationRef.current || escalationRef.current.id !== escalationId) return;
|
|
1552
|
-
const attempt = ++escalationReconnectAttemptRef.current;
|
|
1553
|
-
const delay = Math.min(
|
|
1554
|
-
ESCALATION_RECONNECT_MAX_MS,
|
|
1555
|
-
1e3 * 2 ** Math.min(attempt, 5)
|
|
1556
|
-
);
|
|
1557
|
-
escalationReconnectTimerRef.current = setTimeout(() => {
|
|
1558
|
-
setEscalationStreamEpoch((n) => n + 1);
|
|
1559
|
-
}, delay);
|
|
1560
|
-
}
|
|
1561
|
-
});
|
|
1562
|
-
return () => {
|
|
1563
|
-
if (escalationReconnectTimerRef.current) {
|
|
1564
|
-
clearTimeout(escalationReconnectTimerRef.current);
|
|
1565
|
-
escalationReconnectTimerRef.current = null;
|
|
1566
|
-
}
|
|
1567
|
-
close();
|
|
1568
|
-
};
|
|
1569
|
-
}, [client, emit, appendLocalSystemMessage, escalationStrings, escalation?.id, escalationStreamEpoch]);
|
|
1570
|
-
const sendEscalationMessage = useCallback(
|
|
1571
|
-
async (content) => {
|
|
1572
|
-
const current = escalationRef.current;
|
|
1573
|
-
if (!current) return;
|
|
1574
|
-
const userMessage = {
|
|
1575
|
-
id: `local_${++messageIdCounter.current}`,
|
|
1576
|
-
role: "user",
|
|
1577
|
-
content,
|
|
1578
|
-
timestamp: Date.now()
|
|
1579
|
-
};
|
|
1580
|
-
setMessages((prev) => [...prev, userMessage]);
|
|
1581
|
-
try {
|
|
1582
|
-
await client.sendEscalationMessage(current.id, content);
|
|
1583
|
-
} catch (err) {
|
|
1584
|
-
setError(err instanceof Error ? err.message : "Failed to send message");
|
|
1585
|
-
}
|
|
1586
|
-
},
|
|
1587
|
-
[client]
|
|
1588
|
-
);
|
|
1589
|
-
const beginTurn = useCallback(
|
|
1590
|
-
(assistantMessageId, actions, hop) => {
|
|
1591
|
-
if (!actions || actions.length === 0) {
|
|
1592
|
-
currentTurnRef.current = null;
|
|
1593
|
-
return;
|
|
1594
|
-
}
|
|
1595
|
-
const stamped = actions.map((a) => ({ ...a, messageId: assistantMessageId }));
|
|
1596
|
-
setPendingActions((prev) => [...prev, ...stamped]);
|
|
1597
|
-
const turn = {
|
|
1598
|
-
messageId: assistantMessageId,
|
|
1599
|
-
actionIds: new Set(stamped.map((a) => a.id)),
|
|
1600
|
-
results: /* @__PURE__ */ new Map(),
|
|
1601
|
-
reportedIds: /* @__PURE__ */ new Set(),
|
|
1602
|
-
hop,
|
|
1603
|
-
hadNavigate: stamped.some((a) => a.type === "navigate"),
|
|
1604
|
-
// Snapshot the current screen NOW, before any navigate
|
|
1605
|
-
// dispatches. waitForScreenSettle will use this as the
|
|
1606
|
-
// baseline to detect that the navigate completed.
|
|
1607
|
-
preNavigateScreen: getCurrentScreen(),
|
|
1608
|
-
// Full route+params signature — lets settle detect navigate
|
|
1609
|
-
// to the same route name with different params.
|
|
1610
|
-
preNavigateSignature: getCurrentScreenSignature()
|
|
1611
|
-
};
|
|
1612
|
-
const originScreen = getCurrentScreen() ?? void 0;
|
|
1613
|
-
for (const a of stamped) {
|
|
1614
|
-
turn.results.set(a.id, {
|
|
1615
|
-
type: a.type,
|
|
1616
|
-
success: false,
|
|
1617
|
-
summary: summarizeAction(a),
|
|
1618
|
-
fingerprint: fingerprintAction(a, originScreen)
|
|
1619
|
-
});
|
|
1620
|
-
}
|
|
1621
|
-
currentTurnRef.current = turn;
|
|
1622
|
-
for (const action of stamped) {
|
|
1623
|
-
emit({
|
|
1624
|
-
type: "agent:action:start",
|
|
1625
|
-
timestamp: Date.now(),
|
|
1626
|
-
data: { action }
|
|
1627
|
-
});
|
|
1628
|
-
}
|
|
1629
|
-
if (turnHasConfirmActions(stamped)) {
|
|
1630
|
-
setIsLoading(false);
|
|
1631
|
-
setLoadingStatusKey("thinking");
|
|
1632
|
-
}
|
|
1633
|
-
},
|
|
1634
|
-
[emit]
|
|
1635
|
-
);
|
|
1636
|
-
const registerSynthesizedTurnAction = useCallback((action) => {
|
|
1637
|
-
const turn = currentTurnRef.current;
|
|
1638
|
-
if (!turn || turn.actionIds.has(action.id)) return;
|
|
1639
|
-
const stamped = { ...action, messageId: turn.messageId };
|
|
1640
|
-
turn.actionIds.add(stamped.id);
|
|
1641
|
-
turn.results.set(stamped.id, {
|
|
1642
|
-
type: stamped.type,
|
|
1643
|
-
success: false,
|
|
1644
|
-
summary: summarizeAction(stamped),
|
|
1645
|
-
fingerprint: fingerprintAction(stamped, getCurrentScreen() ?? void 0)
|
|
1646
|
-
});
|
|
1647
|
-
setPendingActions((prev) => {
|
|
1648
|
-
if (prev.some((candidate) => candidate.id === stamped.id)) return prev;
|
|
1649
|
-
return [...prev, stamped];
|
|
1650
|
-
});
|
|
1651
|
-
if (stamped.type === "confirm") {
|
|
1652
|
-
setIsLoading(false);
|
|
1653
|
-
setLoadingStatusKey("thinking");
|
|
1654
|
-
}
|
|
1655
|
-
}, []);
|
|
1656
|
-
const runContinuation = useCallback(
|
|
1657
|
-
async (turn) => {
|
|
1658
|
-
if (continuationInFlightRef.current) return;
|
|
1659
|
-
continuationInFlightRef.current = true;
|
|
1660
|
-
try {
|
|
1661
|
-
setIsLoading(true);
|
|
1662
|
-
setLoadingStatusKey("statusWaitingApp");
|
|
1663
|
-
const toolWithoutEffectIds = /* @__PURE__ */ new Set();
|
|
1664
|
-
let loadingPending = false;
|
|
1665
|
-
const turnActions = pendingActionsRef.current.filter(
|
|
1666
|
-
(a) => turn.actionIds.has(a.id)
|
|
1667
|
-
);
|
|
1668
|
-
const pressedId = extractPressedComponentId(turnActions);
|
|
1669
|
-
const navigateTargetScreen = extractNavigateTargetScreen(turnActions);
|
|
1670
|
-
if (turn.hadNavigate) {
|
|
1671
|
-
const settle = await waitForScreenSettle({
|
|
1672
|
-
expectingChange: true,
|
|
1673
|
-
fromScreen: turn.preNavigateScreen ?? null,
|
|
1674
|
-
fromSignature: turn.preNavigateSignature ?? null,
|
|
1675
|
-
targetScreen: navigateTargetScreen,
|
|
1676
|
-
maxMs: 3500
|
|
1677
|
-
});
|
|
1678
|
-
console.log(
|
|
1679
|
-
`[Appilots] runContinuation: post-navigate settle done \u2014 screen="${settle.screen}" transitioned=${settle.transitioned} timedOut=${settle.timedOut} waitedMs=${settle.waitedMs}`
|
|
1680
|
-
);
|
|
1681
|
-
const navigationMoved = settle.transitioned || screenDepartedBaseline(turn.preNavigateScreen ?? null, turn.preNavigateSignature ?? null) || !!navigateTargetScreen && !!settle.screen && routesBelongToSameFeature(settle.screen, navigateTargetScreen);
|
|
1682
|
-
if (!navigationMoved) {
|
|
1683
|
-
appilotsDebugWarn(
|
|
1684
|
-
`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.`
|
|
1685
|
-
);
|
|
1686
|
-
for (const actionId of turn.actionIds) {
|
|
1687
|
-
const r = turn.results.get(actionId);
|
|
1688
|
-
if (r && r.type === "navigate") {
|
|
1689
|
-
toolWithoutEffectIds.add(actionId);
|
|
1690
|
-
}
|
|
1691
|
-
}
|
|
1692
|
-
}
|
|
1693
|
-
const loadSettle = await waitForLoadingSettle({
|
|
1694
|
-
fromScreen: settle.screen ?? null,
|
|
1695
|
-
maxMs: 3e3
|
|
1696
|
-
});
|
|
1697
|
-
console.log(
|
|
1698
|
-
`[Appilots] runContinuation: post-navigate loading settle \u2014 settled=${loadSettle.settled} loadingPending=${loadSettle.loadingPending} probes=${loadSettle.probeCount} waitedMs=${loadSettle.waitedMs}`
|
|
1699
|
-
);
|
|
1700
|
-
if (loadSettle.loadingPending) loadingPending = true;
|
|
1701
|
-
} else {
|
|
1702
|
-
const pressedAction = turnActions.find(
|
|
1703
|
-
(a) => a.type === "ui_interaction" && a.payload?.componentId === pressedId
|
|
1704
|
-
);
|
|
1705
|
-
const hints = resolveActionHints(
|
|
1706
|
-
getCurrentScreen(),
|
|
1707
|
-
pressedId,
|
|
1708
|
-
pressedAction?.payload
|
|
1709
|
-
);
|
|
1710
|
-
const settleCapMs = hints?.isAsyncTrigger === true ? 1e4 : POST_ACTION_LOADING_MAX_MS;
|
|
1711
|
-
const loadSettle = await waitForLoadingSettle({
|
|
1712
|
-
pressedComponentId: pressedId,
|
|
1713
|
-
fromScreen: getCurrentScreen(),
|
|
1714
|
-
maxMs: settleCapMs
|
|
1715
|
-
});
|
|
1716
|
-
console.log(
|
|
1717
|
-
`[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)" : ""}`
|
|
1718
|
-
);
|
|
1719
|
-
if (loadSettle.loadingPending) loadingPending = true;
|
|
1720
|
-
const listPressActions = turnActions.filter((action) => {
|
|
1721
|
-
const target = actionPressTargetId(action);
|
|
1722
|
-
return target ? isListItemPressTarget(target) : false;
|
|
1723
|
-
});
|
|
1724
|
-
if (listPressActions.length > 0) {
|
|
1725
|
-
const baselineFingerprint = probeLoadingState(
|
|
1726
|
-
pressedId,
|
|
1727
|
-
getCurrentScreen()
|
|
1728
|
-
).fingerprint;
|
|
1729
|
-
const postPressSettle = await waitForScreenSettle({
|
|
1730
|
-
expectingChange: true,
|
|
1731
|
-
fromScreen: turn.preNavigateScreen ?? null,
|
|
1732
|
-
fromSignature: turn.preNavigateSignature ?? null,
|
|
1733
|
-
maxMs: 2500
|
|
1734
|
-
});
|
|
1735
|
-
await waitForLoadingSettle({
|
|
1736
|
-
pressedComponentId: pressedId,
|
|
1737
|
-
fromScreen: getCurrentScreen(),
|
|
1738
|
-
maxMs: 4e3
|
|
1739
|
-
});
|
|
1740
|
-
const afterFingerprint = probeLoadingState(
|
|
1741
|
-
pressedId,
|
|
1742
|
-
getCurrentScreen()
|
|
1743
|
-
).fingerprint;
|
|
1744
|
-
console.log(
|
|
1745
|
-
`[Appilots] runContinuation: post-list-press settle \u2014 screen="${postPressSettle.screen}" transitioned=${postPressSettle.transitioned} timedOut=${postPressSettle.timedOut} waitedMs=${postPressSettle.waitedMs}`
|
|
1746
|
-
);
|
|
1747
|
-
if (!postPressSettle.transitioned && baselineFingerprint === afterFingerprint) {
|
|
1748
|
-
for (const action of listPressActions) {
|
|
1749
|
-
const result = turn.results.get(action.id);
|
|
1750
|
-
if (result?.success) {
|
|
1751
|
-
toolWithoutEffectIds.add(action.id);
|
|
1752
|
-
}
|
|
1753
|
-
}
|
|
1754
|
-
}
|
|
1755
|
-
}
|
|
1756
|
-
}
|
|
1757
|
-
const context = buildAgentContext({ loadingPending });
|
|
1758
|
-
const snap = context.snapshot;
|
|
1759
|
-
const activePath = context.navigationState?.activePath ?? [];
|
|
1760
|
-
for (const action of turnActions) {
|
|
1761
|
-
const result = turn.results.get(action.id);
|
|
1762
|
-
if (result?.success && formFillLackedWriteEffect(action, snap, activePath)) {
|
|
1763
|
-
toolWithoutEffectIds.add(action.id);
|
|
1764
|
-
appilotsDebugWarn(
|
|
1765
|
-
`tool-without-effect: form_fill succeeded but create form is still open (actionId="${action.id}")`
|
|
1766
|
-
);
|
|
1767
|
-
}
|
|
1768
|
-
}
|
|
1769
|
-
const results = Array.from(turn.actionIds).map((actionId) => {
|
|
1770
|
-
const r = turn.results.get(actionId);
|
|
1771
|
-
if (!r) {
|
|
1772
|
-
return {
|
|
1773
|
-
actionId,
|
|
1774
|
-
type: "unknown",
|
|
1775
|
-
success: false,
|
|
1776
|
-
error: "no result reported",
|
|
1777
|
-
summary: actionId,
|
|
1778
|
-
diagnose: void 0,
|
|
1779
|
-
recoveryAttempt: 0,
|
|
1780
|
-
recoveryExhausted: false,
|
|
1781
|
-
toolWithoutEffect: false,
|
|
1782
|
-
effect: "unknown"
|
|
1783
|
-
};
|
|
1784
|
-
}
|
|
1785
|
-
const hasToolWithoutEffect = toolWithoutEffectIds.has(actionId);
|
|
1786
|
-
let recoveryAttempt = 0;
|
|
1787
|
-
let recoveryExhausted = false;
|
|
1788
|
-
if (!r.success || hasToolWithoutEffect) {
|
|
1789
|
-
const counts = recoveryCountByFingerprintRef.current;
|
|
1790
|
-
const previous = counts.get(r.fingerprint) ?? 0;
|
|
1791
|
-
const next = previous + 1;
|
|
1792
|
-
counts.set(r.fingerprint, next);
|
|
1793
|
-
recoveryAttempt = next;
|
|
1794
|
-
recoveryExhausted = next >= MAX_RECOVERY_ATTEMPTS_PER_ACTION;
|
|
1795
|
-
}
|
|
1796
|
-
return {
|
|
1797
|
-
actionId,
|
|
1798
|
-
type: r.type,
|
|
1799
|
-
success: r.success,
|
|
1800
|
-
error: r.error,
|
|
1801
|
-
summary: r.summary,
|
|
1802
|
-
diagnose: r.diagnose,
|
|
1803
|
-
recoveryAttempt,
|
|
1804
|
-
recoveryExhausted,
|
|
1805
|
-
toolWithoutEffect: hasToolWithoutEffect,
|
|
1806
|
-
effect: hasToolWithoutEffect ? "none" : r.effect,
|
|
1807
|
-
userVisibleStatusKey: r.userVisibleStatusKey
|
|
1808
|
-
};
|
|
1809
|
-
});
|
|
1810
|
-
const exhausted = results.filter((r) => r.recoveryExhausted);
|
|
1811
|
-
setLoadingStatusKey(
|
|
1812
|
-
results.some((r) => !r.success || r.toolWithoutEffect) ? "statusAdjusting" : "statusAnalyzing"
|
|
1813
|
-
);
|
|
1814
|
-
console.log(
|
|
1815
|
-
`[Appilots] useAppilotsChat: running continuation hop=${turn.hop} (${results.filter((r) => r.success).length}/${results.length} succeeded` + (exhausted.length > 0 ? `, ${exhausted.length} recovery-exhausted` : "") + ")"
|
|
1816
|
-
);
|
|
1817
|
-
const response = await client.continueAgent(results, context, turn.hop);
|
|
1818
|
-
addAssistantMessage(response.message);
|
|
1819
|
-
emit({
|
|
1820
|
-
type: "agent:message",
|
|
1821
|
-
timestamp: Date.now(),
|
|
1822
|
-
data: { message: response.message, usage: response.usage }
|
|
1823
|
-
});
|
|
1824
|
-
if (response.isDone || !response.actions || response.actions.length === 0) {
|
|
1825
|
-
currentTurnRef.current = null;
|
|
1826
|
-
setIsLoading(false);
|
|
1827
|
-
setLoadingStatusKey("thinking");
|
|
1828
|
-
return;
|
|
1829
|
-
}
|
|
1830
|
-
beginTurn(response.message.id, response.actions, response.hop);
|
|
1831
|
-
} catch (err) {
|
|
1832
|
-
const raw = err instanceof Error ? err.message : "Continuation failed";
|
|
1833
|
-
appilotsDebugWarn("useAppilotsChat: continuation failed \u2014", raw);
|
|
1834
|
-
const recentUserTexts = messagesRef.current.filter((m) => m.role === "user").slice(-4).map((m) => m.content);
|
|
1835
|
-
const friendly = buildContinuationFailureMessage(raw, recentUserTexts);
|
|
1836
|
-
addAssistantMessage({
|
|
1837
|
-
id: `error_${++messageIdCounter.current}`,
|
|
1838
|
-
role: "assistant",
|
|
1839
|
-
content: friendly,
|
|
1840
|
-
timestamp: Date.now()
|
|
1841
|
-
});
|
|
1842
|
-
if (!escalationRef.current && escalationStrings?.offer) {
|
|
1843
|
-
appendLocalSystemMessage(escalationStrings.offer, { escalationOffer: true });
|
|
1844
|
-
}
|
|
1845
|
-
if (turn) {
|
|
1846
|
-
setPendingActions(
|
|
1847
|
-
(prev) => prev.map(
|
|
1848
|
-
(a) => turn.actionIds.has(a.id) && (a.status === "pending" || a.status === "executing") ? { ...a, status: "failed", error: friendly } : a
|
|
1849
|
-
)
|
|
1850
|
-
);
|
|
1851
|
-
for (const actionId of turn.actionIds) {
|
|
1852
|
-
if (turn.reportedIds.has(actionId)) continue;
|
|
1853
|
-
emit({
|
|
1854
|
-
type: "agent:action:error",
|
|
1855
|
-
timestamp: Date.now(),
|
|
1856
|
-
data: { actionId, error: friendly, abandoned: true }
|
|
1857
|
-
});
|
|
1858
|
-
}
|
|
1859
|
-
}
|
|
1860
|
-
currentTurnRef.current = null;
|
|
1861
|
-
setIsLoading(false);
|
|
1862
|
-
setLoadingStatusKey("thinking");
|
|
1863
|
-
} finally {
|
|
1864
|
-
continuationInFlightRef.current = false;
|
|
1865
|
-
}
|
|
1866
|
-
},
|
|
1867
|
-
[client, emit, addAssistantMessage, beginTurn]
|
|
1868
|
-
);
|
|
1869
|
-
useEffect(() => {
|
|
1870
|
-
return subscribe((event) => {
|
|
1871
|
-
if (event.type === "agent:action:start") {
|
|
1872
|
-
const action = event.data?.action;
|
|
1873
|
-
if (!action?.id || action.type !== "confirm") return;
|
|
1874
|
-
const injectedBy = action.payload?.injectedBy;
|
|
1875
|
-
if (injectedBy !== "sdk-local") return;
|
|
1876
|
-
registerSynthesizedTurnAction(action);
|
|
1877
|
-
return;
|
|
1878
|
-
}
|
|
1879
|
-
if (event.type !== "agent:action:complete" && event.type !== "agent:action:error") return;
|
|
1880
|
-
const turn = currentTurnRef.current;
|
|
1881
|
-
if (!turn) return;
|
|
1882
|
-
const actionId = event.data?.actionId;
|
|
1883
|
-
if (!actionId || !turn.actionIds.has(actionId)) return;
|
|
1884
|
-
if (turn.reportedIds.has(actionId)) return;
|
|
1885
|
-
const success = event.type === "agent:action:complete";
|
|
1886
|
-
const errorMsg = event.data?.error || void 0;
|
|
1887
|
-
const diagnose = event.data?.diagnose;
|
|
1888
|
-
const effect = event.data?.effect;
|
|
1889
|
-
const userVisibleStatusKey = event.data?.userVisibleStatusKey;
|
|
1890
|
-
const existing = turn.results.get(actionId);
|
|
1891
|
-
if (existing) {
|
|
1892
|
-
existing.success = success;
|
|
1893
|
-
existing.error = errorMsg;
|
|
1894
|
-
existing.diagnose = diagnose;
|
|
1895
|
-
existing.effect = effect;
|
|
1896
|
-
existing.userVisibleStatusKey = userVisibleStatusKey;
|
|
1897
|
-
}
|
|
1898
|
-
turn.reportedIds.add(actionId);
|
|
1899
|
-
if (turn.reportedIds.size >= turn.actionIds.size) {
|
|
1900
|
-
void runContinuation(turn);
|
|
1901
|
-
}
|
|
1902
|
-
});
|
|
1903
|
-
}, [subscribe, runContinuation, registerSynthesizedTurnAction]);
|
|
1904
|
-
const sendMessage = useCallback(
|
|
1905
|
-
async (content) => {
|
|
1906
|
-
if (!content.trim() || isLoading) return;
|
|
1907
|
-
if (escalationRef.current) {
|
|
1908
|
-
await sendEscalationMessage(content.trim());
|
|
1909
|
-
return;
|
|
1910
|
-
}
|
|
1911
|
-
setError(null);
|
|
1912
|
-
setIsLoading(true);
|
|
1913
|
-
setLoadingStatusKey("statusAnalyzing");
|
|
1914
|
-
currentTurnRef.current = null;
|
|
1915
|
-
recoveryCountByFingerprintRef.current = /* @__PURE__ */ new Map();
|
|
1916
|
-
const userMessage = {
|
|
1917
|
-
id: `local_${++messageIdCounter.current}`,
|
|
1918
|
-
role: "user",
|
|
1919
|
-
content: content.trim(),
|
|
1920
|
-
timestamp: Date.now()
|
|
1921
|
-
};
|
|
1922
|
-
setMessages((prev) => [...prev, userMessage]);
|
|
1923
|
-
const streamingId = `streaming_${++messageIdCounter.current}`;
|
|
1924
|
-
let streamedVisible = false;
|
|
1925
|
-
const removeStreamingPlaceholder = () => {
|
|
1926
|
-
if (!streamedVisible) return;
|
|
1927
|
-
streamedVisible = false;
|
|
1928
|
-
setMessages((prev) => prev.filter((m) => m.id !== streamingId));
|
|
1929
|
-
};
|
|
1930
|
-
const abortController = new AbortController();
|
|
1931
|
-
abortRef.current = abortController;
|
|
1932
|
-
try {
|
|
1933
|
-
const context = buildAgentContext();
|
|
1934
|
-
console.log(
|
|
1935
|
-
`[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}}`
|
|
1936
|
-
);
|
|
1937
|
-
let response;
|
|
1938
|
-
if (streamingEnabled) {
|
|
1939
|
-
try {
|
|
1940
|
-
response = await client.sendMessageStream(content.trim(), context, {
|
|
1941
|
-
signal: abortController.signal,
|
|
1942
|
-
onTextDelta: (_chunk, fullText) => {
|
|
1943
|
-
const visible = sanitizeVisibleAssistantContent(fullText);
|
|
1944
|
-
if (!streamedVisible) {
|
|
1945
|
-
streamedVisible = true;
|
|
1946
|
-
setMessages((prev) => [
|
|
1947
|
-
...prev,
|
|
1948
|
-
{ id: streamingId, role: "assistant", content: visible, timestamp: Date.now() }
|
|
1949
|
-
]);
|
|
1950
|
-
} else {
|
|
1951
|
-
setMessages(
|
|
1952
|
-
(prev) => prev.map((m) => m.id === streamingId ? { ...m, content: visible } : m)
|
|
1953
|
-
);
|
|
1954
|
-
}
|
|
1955
|
-
}
|
|
1956
|
-
});
|
|
1957
|
-
} catch (err) {
|
|
1958
|
-
if (err instanceof StreamTransportError) {
|
|
1959
|
-
console.warn(`[Appilots] streaming unavailable, falling back to JSON: ${err.message}`);
|
|
1960
|
-
response = await client.sendMessage(content.trim(), context);
|
|
1961
|
-
} else {
|
|
1962
|
-
throw err;
|
|
1963
|
-
}
|
|
1964
|
-
}
|
|
1965
|
-
} else {
|
|
1966
|
-
response = await client.sendMessage(content.trim(), context);
|
|
1967
|
-
}
|
|
1968
|
-
console.log(
|
|
1969
|
-
`[Appilots] useAppilotsChat sendMessage: response received \u2014 actions=${response.actions?.length ?? 0}, hasSummary=${!!response.summaryMessage}`
|
|
1970
|
-
);
|
|
1971
|
-
removeStreamingPlaceholder();
|
|
1972
|
-
addAssistantMessage(response.message);
|
|
1973
|
-
beginTurn(response.message.id, response.actions, 1);
|
|
1974
|
-
if (response.summaryMessage) {
|
|
1975
|
-
addAssistantMessage(response.summaryMessage);
|
|
1976
|
-
}
|
|
1977
|
-
emit({
|
|
1978
|
-
type: "agent:message",
|
|
1979
|
-
timestamp: Date.now(),
|
|
1980
|
-
data: { message: response.message, usage: response.usage }
|
|
1981
|
-
});
|
|
1982
|
-
if (!response.actions || response.actions.length === 0) {
|
|
1983
|
-
setIsLoading(false);
|
|
1984
|
-
setLoadingStatusKey("thinking");
|
|
1985
|
-
}
|
|
1986
|
-
} catch (err) {
|
|
1987
|
-
if (err instanceof StreamAbortError || err instanceof Error && err.name === "AbortError") {
|
|
1988
|
-
setIsLoading(false);
|
|
1989
|
-
setLoadingStatusKey("thinking");
|
|
1990
|
-
return;
|
|
1991
|
-
}
|
|
1992
|
-
removeStreamingPlaceholder();
|
|
1993
|
-
const errorMessage = err instanceof Error ? err.message : "Failed to send message";
|
|
1994
|
-
setError(errorMessage);
|
|
1995
|
-
const errorMsg = {
|
|
1996
|
-
id: `error_${++messageIdCounter.current}`,
|
|
1997
|
-
role: "assistant",
|
|
1998
|
-
content: `${errorPrefix}: ${errorMessage}`,
|
|
1999
|
-
timestamp: Date.now()
|
|
2000
|
-
};
|
|
2001
|
-
addAssistantMessage(errorMsg);
|
|
2002
|
-
setIsLoading(false);
|
|
2003
|
-
setLoadingStatusKey("thinking");
|
|
2004
|
-
} finally {
|
|
2005
|
-
if (abortRef.current === abortController) {
|
|
2006
|
-
abortRef.current = null;
|
|
2007
|
-
}
|
|
991
|
+
else if (matchesName2(fiber, "Switch")) kind = "toggle";
|
|
992
|
+
else if (isPressableTarget2(fiber)) kind = "target";
|
|
993
|
+
if (kind && (!options.kind || options.kind === kind)) {
|
|
994
|
+
const score = scoreMatch(fiber, queryNorm, queryRaw);
|
|
995
|
+
if (score > bestScore) {
|
|
996
|
+
bestScore = score;
|
|
997
|
+
const props = fiber.memoizedProps ?? {};
|
|
998
|
+
bestId2 = props.testID ?? props.accessibilityLabel ?? props.placeholder ?? (kind === "target" ? findChildText2(fiber) : void 0);
|
|
999
|
+
if (kind === "field") bestHandle = wrapTextInput(fiber);
|
|
1000
|
+
else if (kind === "toggle") bestHandle = wrapSwitch(fiber);
|
|
1001
|
+
else bestHandle = wrapTouchable(fiber, findChildText2(fiber));
|
|
2008
1002
|
}
|
|
2009
|
-
},
|
|
2010
|
-
[client, emit, isLoading, addAssistantMessage, beginTurn, errorPrefix, streamingEnabled]
|
|
2011
|
-
);
|
|
2012
|
-
const cancelMessage = useCallback(() => {
|
|
2013
|
-
abortRef.current?.abort();
|
|
2014
|
-
}, []);
|
|
2015
|
-
const clearMessages = useCallback(() => {
|
|
2016
|
-
setMessages([]);
|
|
2017
|
-
setPendingActions([]);
|
|
2018
|
-
setError(null);
|
|
2019
|
-
setIsLoading(false);
|
|
2020
|
-
setLoadingStatusKey("thinking");
|
|
2021
|
-
currentTurnRef.current = null;
|
|
2022
|
-
continuationInFlightRef.current = false;
|
|
2023
|
-
recoveryCountByFingerprintRef.current = /* @__PURE__ */ new Map();
|
|
2024
|
-
if (escalationRef.current) {
|
|
2025
|
-
void client.abandonEscalation(escalationRef.current.id);
|
|
2026
1003
|
}
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
loadingStatusKey,
|
|
2038
|
-
error,
|
|
2039
|
-
pendingActions,
|
|
2040
|
-
escalation,
|
|
2041
|
-
requestHuman,
|
|
2042
|
-
sendMessage,
|
|
2043
|
-
cancelMessage,
|
|
2044
|
-
clearMessages,
|
|
2045
|
-
clearError
|
|
2046
|
-
};
|
|
1004
|
+
if (fiber.child) stack.push(fiber.child);
|
|
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;
|
|
2047
1014
|
}
|
|
2048
1015
|
|
|
2049
1016
|
// src/executor/handlers/navigateHandler.ts
|
|
@@ -2229,7 +1196,7 @@ function markedItemIndexFromFiber(fiber) {
|
|
|
2229
1196
|
const value = fiber?.memoizedProps?.__appilotsItemIndex;
|
|
2230
1197
|
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
2231
1198
|
}
|
|
2232
|
-
function
|
|
1199
|
+
function normalize2(value) {
|
|
2233
1200
|
return String(value ?? "").toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "");
|
|
2234
1201
|
}
|
|
2235
1202
|
function hasPressHandler3(fiber) {
|
|
@@ -2488,9 +1455,9 @@ function pressResolvedListItem(list, itemFiber, listIndex, itemIndex) {
|
|
|
2488
1455
|
}
|
|
2489
1456
|
}
|
|
2490
1457
|
function itemTextScore(item, locator) {
|
|
2491
|
-
const needles = [locator.label, ...locator.texts ?? []].map(
|
|
1458
|
+
const needles = [locator.label, ...locator.texts ?? []].map(normalize2).filter((text) => text.length >= 2);
|
|
2492
1459
|
if (needles.length === 0) return 0;
|
|
2493
|
-
const haystack = collectTexts(item).map(
|
|
1460
|
+
const haystack = collectTexts(item).map(normalize2).filter(Boolean);
|
|
2494
1461
|
if (haystack.length === 0) return 0;
|
|
2495
1462
|
let score = 0;
|
|
2496
1463
|
for (const needle of needles) {
|
|
@@ -2508,11 +1475,11 @@ function scoreItemCandidate(item, zeroBasedIndex, locator) {
|
|
|
2508
1475
|
const markedIndex = markedItemIndexFromFiber(item);
|
|
2509
1476
|
if (locator.itemKey) {
|
|
2510
1477
|
if (itemKey === locator.itemKey) score += 1e3;
|
|
2511
|
-
else if (itemKey &&
|
|
1478
|
+
else if (itemKey && normalize2(itemKey) === normalize2(locator.itemKey)) score += 950;
|
|
2512
1479
|
}
|
|
2513
1480
|
if (locator.reactKey) {
|
|
2514
1481
|
if (reactKey === locator.reactKey) score += 900;
|
|
2515
|
-
else if (reactKey &&
|
|
1482
|
+
else if (reactKey && normalize2(reactKey) === normalize2(locator.reactKey)) score += 850;
|
|
2516
1483
|
}
|
|
2517
1484
|
if (locator.itemIndex && zeroBasedIndex === locator.itemIndex - 1) score += 750;
|
|
2518
1485
|
if (locator.itemIndex && markedIndex !== void 0 && (markedIndex === locator.itemIndex || markedIndex === locator.itemIndex - 1)) {
|
|
@@ -2523,8 +1490,8 @@ function scoreItemCandidate(item, zeroBasedIndex, locator) {
|
|
|
2523
1490
|
}
|
|
2524
1491
|
function candidateLists(containers, locator) {
|
|
2525
1492
|
if (locator.listId) {
|
|
2526
|
-
const normalizedId =
|
|
2527
|
-
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);
|
|
2528
1495
|
if (matches.length > 0) return matches;
|
|
2529
1496
|
}
|
|
2530
1497
|
if (typeof locator.listIndex === "number") {
|
|
@@ -2532,8 +1499,8 @@ function candidateLists(containers, locator) {
|
|
|
2532
1499
|
return list ? [{ list, listIndex: locator.listIndex }] : [];
|
|
2533
1500
|
}
|
|
2534
1501
|
if (locator.listLabel) {
|
|
2535
|
-
const normalizedLabel =
|
|
2536
|
-
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));
|
|
2537
1504
|
if (matches.length > 0) return matches;
|
|
2538
1505
|
}
|
|
2539
1506
|
return containers.map((list, listIndex) => ({ list, listIndex }));
|
|
@@ -2869,7 +1836,7 @@ function pressElementOrTarget(identifier, registry) {
|
|
|
2869
1836
|
// src/executor/formFieldRewrite.ts
|
|
2870
1837
|
var LIST_ORDINAL_RE = /^list-\d+-item-\d+$/i;
|
|
2871
1838
|
var PLATE_RE = /^[A-Z]{3}-?\d[A-Z0-9]\d{2}$/i;
|
|
2872
|
-
function
|
|
1839
|
+
function normalize4(value) {
|
|
2873
1840
|
return value.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]/g, "");
|
|
2874
1841
|
}
|
|
2875
1842
|
function resolveScreenForFormFields() {
|
|
@@ -2909,14 +1876,14 @@ function collectKnownFormFields(registry) {
|
|
|
2909
1876
|
return registryFields;
|
|
2910
1877
|
}
|
|
2911
1878
|
function fieldHaystack(field) {
|
|
2912
|
-
return
|
|
1879
|
+
return normalize4(`${field.id} ${field.label ?? ""}`);
|
|
2913
1880
|
}
|
|
2914
1881
|
function isMisplacedFieldId(fieldId, knownIds) {
|
|
2915
1882
|
const trimmed = fieldId.trim();
|
|
2916
1883
|
if (!trimmed) return true;
|
|
2917
1884
|
if (LIST_ORDINAL_RE.test(trimmed)) return true;
|
|
2918
1885
|
if (/^\d+$/.test(trimmed)) return true;
|
|
2919
|
-
const normalized =
|
|
1886
|
+
const normalized = normalize4(trimmed);
|
|
2920
1887
|
if (knownIds.has(normalized)) return false;
|
|
2921
1888
|
for (const known of knownIds) {
|
|
2922
1889
|
if (known.length >= 4 && (known.includes(normalized) || normalized.includes(known))) {
|
|
@@ -2926,18 +1893,18 @@ function isMisplacedFieldId(fieldId, knownIds) {
|
|
|
2926
1893
|
return true;
|
|
2927
1894
|
}
|
|
2928
1895
|
function valueMatchesOption(field, rawValue) {
|
|
2929
|
-
const value =
|
|
1896
|
+
const value = normalize4(String(rawValue ?? ""));
|
|
2930
1897
|
if (!value || !Array.isArray(field.options)) return false;
|
|
2931
1898
|
return field.options.some((option) => {
|
|
2932
|
-
const label =
|
|
2933
|
-
const optionValue =
|
|
1899
|
+
const label = normalize4(option.label ?? "");
|
|
1900
|
+
const optionValue = normalize4(option.value ?? "");
|
|
2934
1901
|
return label === value || optionValue === value || value.includes(label) || value.includes(optionValue);
|
|
2935
1902
|
});
|
|
2936
1903
|
}
|
|
2937
1904
|
function inferFieldIdForValue(rawValue, fields, used) {
|
|
2938
1905
|
const value = String(rawValue ?? "").trim();
|
|
2939
1906
|
if (!value) return void 0;
|
|
2940
|
-
const normalizedValue =
|
|
1907
|
+
const normalizedValue = normalize4(value);
|
|
2941
1908
|
for (const field of fields) {
|
|
2942
1909
|
if (used.has(field.id)) continue;
|
|
2943
1910
|
if (valueMatchesOption(field, value)) return field.id;
|
|
@@ -2974,7 +1941,7 @@ function inferFieldIdForValue(rawValue, fields, used) {
|
|
|
2974
1941
|
}
|
|
2975
1942
|
const nextText = fields.find((field) => {
|
|
2976
1943
|
if (used.has(field.id)) return false;
|
|
2977
|
-
const type =
|
|
1944
|
+
const type = normalize4(field.type ?? "text");
|
|
2978
1945
|
return type === "text" || type === "input" || type === "number" || type === "";
|
|
2979
1946
|
});
|
|
2980
1947
|
return nextText?.id;
|
|
@@ -2982,7 +1949,7 @@ function inferFieldIdForValue(rawValue, fields, used) {
|
|
|
2982
1949
|
function rewriteMisplacedFormFillFields(fields, registry) {
|
|
2983
1950
|
const knownFields = collectKnownFormFields(registry);
|
|
2984
1951
|
if (knownFields.length === 0) return fields;
|
|
2985
|
-
const knownIds = new Set(knownFields.map((field) =>
|
|
1952
|
+
const knownIds = new Set(knownFields.map((field) => normalize4(field.id)));
|
|
2986
1953
|
const needsRewrite = fields.some((field) => isMisplacedFieldId(field.fieldId, knownIds));
|
|
2987
1954
|
if (!needsRewrite) return fields;
|
|
2988
1955
|
const used = /* @__PURE__ */ new Set();
|
|
@@ -3002,7 +1969,7 @@ function rewriteMisplacedFormFillFields(fields, registry) {
|
|
|
3002
1969
|
}
|
|
3003
1970
|
|
|
3004
1971
|
// src/executor/formValidation.ts
|
|
3005
|
-
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;
|
|
3006
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;
|
|
3007
1974
|
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
3008
1975
|
var FIELD_HINTS = [
|
|
@@ -3022,7 +1989,7 @@ function inferValidationFieldId(message) {
|
|
|
3022
1989
|
function readVisibleFormValidation(snap) {
|
|
3023
1990
|
if (!snap) return void 0;
|
|
3024
1991
|
for (const text of snap.texts ?? []) {
|
|
3025
|
-
if (!text?.trim() || !
|
|
1992
|
+
if (!text?.trim() || !FORM_VALIDATION_TEXT_RE.test(text)) continue;
|
|
3026
1993
|
return { fieldId: inferValidationFieldId(text), message: text };
|
|
3027
1994
|
}
|
|
3028
1995
|
return void 0;
|
|
@@ -3668,18 +2635,18 @@ function uiInteractionHandler(payload, context) {
|
|
|
3668
2635
|
// src/executor/formSubmitResolve.ts
|
|
3669
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;
|
|
3670
2637
|
var AGENT_CHROME_TARGET_RE = /\b(fechar|close|dismiss|cancelar|cancel|limpar|clear|chat\s+do\s+assistente|assistant\s+chat)\b/i;
|
|
3671
|
-
function
|
|
2638
|
+
function normalize5(value) {
|
|
3672
2639
|
return value.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
|
|
3673
2640
|
}
|
|
3674
2641
|
function isAgentChromeTarget(id, label) {
|
|
3675
|
-
const hay =
|
|
2642
|
+
const hay = normalize5(`${id ?? ""} ${label ?? ""}`);
|
|
3676
2643
|
return AGENT_CHROME_TARGET_RE.test(hay);
|
|
3677
2644
|
}
|
|
3678
2645
|
function isSubmitLikeTarget(id, label) {
|
|
3679
2646
|
if (!id && !label) return false;
|
|
3680
2647
|
if (isAgentChromeTarget(id, label)) return false;
|
|
3681
|
-
const idNorm =
|
|
3682
|
-
|
|
2648
|
+
const idNorm = normalize5(id ?? "");
|
|
2649
|
+
normalize5(label ?? "");
|
|
3683
2650
|
if (/^submit[-_]/i.test(id ?? "")) return true;
|
|
3684
2651
|
if (submitLabelHintsSubmit(label ?? "")) return true;
|
|
3685
2652
|
if (submitLabelHintsSubmit(id ?? "")) return true;
|
|
@@ -3778,7 +2745,7 @@ function readVisibleValidation(fieldId, label) {
|
|
|
3778
2745
|
);
|
|
3779
2746
|
for (const text of texts) {
|
|
3780
2747
|
const lower = text.toLowerCase();
|
|
3781
|
-
if (
|
|
2748
|
+
if (FORM_VALIDATION_TEXT_RE.test(lower)) {
|
|
3782
2749
|
for (const needle of needles) {
|
|
3783
2750
|
if (lower.includes(needle.toLowerCase())) return text;
|
|
3784
2751
|
}
|
|
@@ -3798,15 +2765,15 @@ function selectValueApplied(fieldId, requestedValue, currentValue, registry) {
|
|
|
3798
2765
|
if (!requested) return false;
|
|
3799
2766
|
const meta = getMetadataField(fieldId, registry);
|
|
3800
2767
|
const options = Array.isArray(meta?.options) ? meta.options : [];
|
|
3801
|
-
const requestedNorm =
|
|
3802
|
-
const currentNorm =
|
|
2768
|
+
const requestedNorm = normalize3(requested);
|
|
2769
|
+
const currentNorm = normalize3(currentValue);
|
|
3803
2770
|
if (currentNorm === requestedNorm) return true;
|
|
3804
2771
|
for (const option of options) {
|
|
3805
2772
|
const value = typeof option?.value === "string" ? option.value : "";
|
|
3806
2773
|
const label = typeof option?.label === "string" ? option.label : "";
|
|
3807
|
-
if (
|
|
3808
|
-
if (
|
|
3809
|
-
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;
|
|
3810
2777
|
}
|
|
3811
2778
|
return false;
|
|
3812
2779
|
}
|
|
@@ -3820,14 +2787,14 @@ function getMetadataField(fieldId, registry) {
|
|
|
3820
2787
|
if (comp.kind === "field" && comp.screen) screens.add(comp.screen);
|
|
3821
2788
|
}
|
|
3822
2789
|
}
|
|
3823
|
-
const needle =
|
|
2790
|
+
const needle = normalize3(fieldId);
|
|
3824
2791
|
for (const screenName of screens) {
|
|
3825
2792
|
const screenMeta = getScreenMetadata(screenName);
|
|
3826
2793
|
const fields = Array.isArray(screenMeta?.fields) ? screenMeta.fields : [];
|
|
3827
2794
|
const match = fields.find((field) => {
|
|
3828
2795
|
if (!field || typeof field !== "object") return false;
|
|
3829
|
-
const id = typeof field.id === "string" ?
|
|
3830
|
-
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) : "";
|
|
3831
2798
|
return id === needle || label === needle;
|
|
3832
2799
|
});
|
|
3833
2800
|
if (match) return match;
|
|
@@ -3839,11 +2806,11 @@ function resolveSelectOption(fieldId, value, registry) {
|
|
|
3839
2806
|
if (!requested) return void 0;
|
|
3840
2807
|
const meta = getMetadataField(fieldId, registry);
|
|
3841
2808
|
const options = Array.isArray(meta?.options) ? meta.options : [];
|
|
3842
|
-
const requestedNorm =
|
|
2809
|
+
const requestedNorm = normalize3(requested);
|
|
3843
2810
|
for (const option of options) {
|
|
3844
2811
|
const optionValue = typeof option?.value === "string" ? option.value : "";
|
|
3845
2812
|
const optionLabel = typeof option?.label === "string" ? option.label : "";
|
|
3846
|
-
if (
|
|
2813
|
+
if (normalize3(optionValue) === requestedNorm || normalize3(optionLabel) === requestedNorm) {
|
|
3847
2814
|
return { value: optionValue, label: optionLabel };
|
|
3848
2815
|
}
|
|
3849
2816
|
}
|
|
@@ -3898,7 +2865,7 @@ async function tryPressSelectOption(fieldId, value, registry, options = {}) {
|
|
|
3898
2865
|
}
|
|
3899
2866
|
function normalizeFieldType(type) {
|
|
3900
2867
|
if (typeof type !== "string") return void 0;
|
|
3901
|
-
const normalized =
|
|
2868
|
+
const normalized = normalize3(type);
|
|
3902
2869
|
if (normalized === "select" || normalized === "picker" || normalized === "dropdown") return "select";
|
|
3903
2870
|
if (normalized === "toggle" || normalized === "switch" || normalized === "checkbox") return "toggle";
|
|
3904
2871
|
if (normalized === "date") return "date";
|
|
@@ -3926,7 +2893,7 @@ function fieldCueTerms(fieldId, label) {
|
|
|
3926
2893
|
);
|
|
3927
2894
|
const terms = /* @__PURE__ */ new Set();
|
|
3928
2895
|
for (const value of raw) {
|
|
3929
|
-
const normalized =
|
|
2896
|
+
const normalized = normalize3(value);
|
|
3930
2897
|
if (normalized.length >= 3 && normalized !== "id") terms.add(normalized);
|
|
3931
2898
|
}
|
|
3932
2899
|
return [...terms];
|
|
@@ -3937,15 +2904,15 @@ function snapshotHasFieldCue(snap, fieldId, label) {
|
|
|
3937
2904
|
const visible = [
|
|
3938
2905
|
...snap.texts ?? [],
|
|
3939
2906
|
...(snap.buttons ?? []).map((b) => b.label ?? b.id ?? "")
|
|
3940
|
-
].map((text) =>
|
|
2907
|
+
].map((text) => normalize3(text)).filter(Boolean);
|
|
3941
2908
|
return visible.some((text) => terms.some((term) => text.includes(term) || term.includes(text)));
|
|
3942
2909
|
}
|
|
3943
2910
|
function scoreCandidateText(value, texts) {
|
|
3944
|
-
const valueNorm =
|
|
2911
|
+
const valueNorm = normalize3(String(value ?? ""));
|
|
3945
2912
|
if (!valueNorm) return 0;
|
|
3946
2913
|
let best = 0;
|
|
3947
2914
|
for (const text of texts) {
|
|
3948
|
-
const textNorm =
|
|
2915
|
+
const textNorm = normalize3(text);
|
|
3949
2916
|
if (!textNorm) continue;
|
|
3950
2917
|
best = Math.max(best, matchScore(valueNorm, textNorm));
|
|
3951
2918
|
}
|
|
@@ -3973,7 +2940,7 @@ function buildChoiceCandidates(snap, fieldId, value, allowFirstVisibleWhenNoCue,
|
|
|
3973
2940
|
const groupCueScore = Math.max(
|
|
3974
2941
|
globalCueScore,
|
|
3975
2942
|
...groupTexts.map((text) => {
|
|
3976
|
-
const normalized =
|
|
2943
|
+
const normalized = normalize3(text);
|
|
3977
2944
|
return fieldCueTerms(fieldId, fieldLabel).some(
|
|
3978
2945
|
(term) => normalized.includes(term) || term.includes(normalized)
|
|
3979
2946
|
) ? 80 : 0;
|
|
@@ -4010,7 +2977,7 @@ function buildChoiceCandidates(snap, fieldId, value, allowFirstVisibleWhenNoCue,
|
|
|
4010
2977
|
const elementCueScore = Math.max(
|
|
4011
2978
|
fieldCueVisible && elements.length <= 12 ? 50 : 0,
|
|
4012
2979
|
...cueTexts.map((text) => {
|
|
4013
|
-
const normalized =
|
|
2980
|
+
const normalized = normalize3(text);
|
|
4014
2981
|
return fieldTerms.some(
|
|
4015
2982
|
(term) => normalized.includes(term) || term.includes(normalized)
|
|
4016
2983
|
) ? 80 : 0;
|
|
@@ -4098,7 +3065,7 @@ async function selectVisibleChoiceAsync(fieldId, value, registry, options) {
|
|
|
4098
3065
|
}
|
|
4099
3066
|
function isContinuationButton(label) {
|
|
4100
3067
|
if (!label) return false;
|
|
4101
|
-
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));
|
|
4102
3069
|
}
|
|
4103
3070
|
function pressContinuationButton(registry) {
|
|
4104
3071
|
const snap = captureSnapshot();
|
|
@@ -4132,14 +3099,14 @@ function findField(fieldId, registry) {
|
|
|
4132
3099
|
}
|
|
4133
3100
|
}
|
|
4134
3101
|
const currentScreenForTieBreak = getCurrentScreen();
|
|
4135
|
-
const needle =
|
|
3102
|
+
const needle = normalize3(fieldId);
|
|
4136
3103
|
let bestComp;
|
|
4137
3104
|
let bestScore = 0;
|
|
4138
3105
|
let bestOnCurrentScreen = false;
|
|
4139
3106
|
for (const comp of snapshot) {
|
|
4140
3107
|
if (comp.kind !== "field") continue;
|
|
4141
|
-
const candidates = [
|
|
4142
|
-
if (comp.label) candidates.push(
|
|
3108
|
+
const candidates = [normalize3(comp.id)];
|
|
3109
|
+
if (comp.label) candidates.push(normalize3(comp.label));
|
|
4143
3110
|
let candidateScore = 0;
|
|
4144
3111
|
for (const cand of candidates) {
|
|
4145
3112
|
const s = matchScore(needle, cand);
|
|
@@ -4170,12 +3137,12 @@ function findField(fieldId, registry) {
|
|
|
4170
3137
|
);
|
|
4171
3138
|
if (metaField && typeof metaField === "object" && metaField.label) {
|
|
4172
3139
|
const metaLabel = metaField.label;
|
|
4173
|
-
const metaNeedle =
|
|
3140
|
+
const metaNeedle = normalize3(metaLabel);
|
|
4174
3141
|
let mBest;
|
|
4175
3142
|
let mBestScore = 0;
|
|
4176
3143
|
for (const comp of snapshot) {
|
|
4177
3144
|
if (comp.kind !== "field" || !comp.label) continue;
|
|
4178
|
-
const score = matchScore(metaNeedle,
|
|
3145
|
+
const score = matchScore(metaNeedle, normalize3(comp.label));
|
|
4179
3146
|
if (score > mBestScore) {
|
|
4180
3147
|
mBestScore = score;
|
|
4181
3148
|
mBest = comp;
|
|
@@ -4300,7 +3267,9 @@ async function formFillHandler(payload, context) {
|
|
|
4300
3267
|
const fieldEntry = findField(fieldId, registry);
|
|
4301
3268
|
if (fieldEntry) {
|
|
4302
3269
|
try {
|
|
4303
|
-
|
|
3270
|
+
appilotsDebugLog(
|
|
3271
|
+
`formFillHandler: found field "${fieldId}" via registry, calling setValue(<${String(value).length} chars>)`
|
|
3272
|
+
);
|
|
4304
3273
|
const beforeValue = effectiveFieldType === "date" ? fieldEntry.getValue?.() ?? "" : void 0;
|
|
4305
3274
|
fieldEntry.focus?.();
|
|
4306
3275
|
fieldEntry.setValue(String(value));
|
|
@@ -4330,7 +3299,7 @@ async function formFillHandler(payload, context) {
|
|
|
4330
3299
|
}
|
|
4331
3300
|
if (!selectValueApplied(fieldId, value, after, registry)) {
|
|
4332
3301
|
appilotsDebugWarn(
|
|
4333
|
-
`formFillHandler: select field "${fieldId}" setValue
|
|
3302
|
+
`formFillHandler: select field "${fieldId}" setValue did not stick (wrote <${String(value).length} chars>, current is <${String(after ?? "").length} chars>), trying visible choice fallback`
|
|
4334
3303
|
);
|
|
4335
3304
|
const selectResult = await selectVisibleChoice(fieldId, value, registry, {
|
|
4336
3305
|
allowFirstVisibleWhenNoCue: !selectedChoiceThisBatch
|
|
@@ -4993,79 +3962,156 @@ async function executeAction(action, context) {
|
|
|
4993
3962
|
});
|
|
4994
3963
|
return result;
|
|
4995
3964
|
}
|
|
4996
|
-
var
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
}
|
|
5005
|
-
|
|
5006
|
-
|
|
3965
|
+
var POST_ACTION_LOADING_MAX_MS = 6e3;
|
|
3966
|
+
function resolveScreenMetadata() {
|
|
3967
|
+
const activePath = getActiveRouteNames();
|
|
3968
|
+
for (let i = activePath.length - 1; i >= 0; i--) {
|
|
3969
|
+
const name = activePath[i];
|
|
3970
|
+
const meta = name ? getScreenMetadata(name) : void 0;
|
|
3971
|
+
if (meta && (Array.isArray(meta.fields) && meta.fields.length > 0 || Array.isArray(meta.actions) && meta.actions.length > 0)) {
|
|
3972
|
+
return meta;
|
|
3973
|
+
}
|
|
3974
|
+
}
|
|
3975
|
+
const screen = getCurrentScreen();
|
|
3976
|
+
return screen ? getScreenMetadata(screen) : void 0;
|
|
5007
3977
|
}
|
|
5008
|
-
function
|
|
5009
|
-
|
|
5010
|
-
|
|
3978
|
+
function buildAgentContext(extras = {}) {
|
|
3979
|
+
const context = {};
|
|
3980
|
+
context.platform = "react-native";
|
|
3981
|
+
const screen = getCurrentScreen();
|
|
3982
|
+
if (screen) context.currentScreen = screen;
|
|
3983
|
+
const navigationState = getNavigationStateSnapshot();
|
|
3984
|
+
if (navigationState) context.navigationState = navigationState;
|
|
3985
|
+
const screenMeta = resolveScreenMetadata();
|
|
3986
|
+
if (screenMeta) context.screenMetadata = screenMeta;
|
|
3987
|
+
try {
|
|
3988
|
+
context.snapshot = captureSnapshot();
|
|
3989
|
+
} catch (err) {
|
|
3990
|
+
appilotsDebugWarn("buildAgentContext: captureSnapshot failed", err);
|
|
3991
|
+
}
|
|
3992
|
+
const registry = componentRegistry.snapshot();
|
|
3993
|
+
const filtered = screen ? registry.filter((c) => !c.screen || c.screen === screen) : registry;
|
|
3994
|
+
if (filtered.length > 0) context.registeredComponents = filtered;
|
|
3995
|
+
const registeredLists = listRegistry.snapshot();
|
|
3996
|
+
if (registeredLists.length > 0) {
|
|
3997
|
+
context.registeredLists = registeredLists.map(
|
|
3998
|
+
({ scrollToIndex, scrollToOffset, getScrollMetrics, dataPreview, ...rest }) => rest
|
|
3999
|
+
);
|
|
5011
4000
|
}
|
|
5012
|
-
|
|
4001
|
+
if (extras.loadingPending === true) context.loadingPending = true;
|
|
4002
|
+
return context;
|
|
5013
4003
|
}
|
|
5014
|
-
function
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
source: "action-policy"
|
|
5021
|
-
};
|
|
4004
|
+
function resolveActionHints(screen, pressedComponentId, actionPayload) {
|
|
4005
|
+
if (actionPayload && typeof actionPayload === "object") {
|
|
4006
|
+
const inline = actionPayload._appilotsInferred;
|
|
4007
|
+
if (inline && typeof inline === "object") {
|
|
4008
|
+
return inline;
|
|
4009
|
+
}
|
|
5022
4010
|
}
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
4011
|
+
if (!screen || !pressedComponentId) return void 0;
|
|
4012
|
+
const meta = getScreenMetadata(screen);
|
|
4013
|
+
const actions = meta?.actions;
|
|
4014
|
+
if (!actions || actions.length === 0) return void 0;
|
|
4015
|
+
for (const a of actions) {
|
|
4016
|
+
if (typeof a === "string") continue;
|
|
4017
|
+
if (idLooselyMatches(a.id, pressedComponentId)) {
|
|
4018
|
+
const inferred = a.appilotsInferred;
|
|
4019
|
+
if (inferred && typeof inferred === "object") return inferred;
|
|
4020
|
+
return void 0;
|
|
5032
4021
|
}
|
|
5033
4022
|
}
|
|
5034
|
-
return
|
|
5035
|
-
}
|
|
5036
|
-
function isConfirmGateFor(confirm, target) {
|
|
5037
|
-
if (confirm.type !== "confirm") return false;
|
|
5038
|
-
const payload = readPayload(confirm);
|
|
5039
|
-
const injectedBy = payload.injectedBy;
|
|
5040
|
-
return payload.gatedActionId === target.id && (injectedBy === "permissions" || injectedBy === "sdk-local");
|
|
5041
|
-
}
|
|
5042
|
-
function toDestructiveConfirmPayload(confirm, policy, fallbackLabel) {
|
|
5043
|
-
const payload = readPayload(confirm);
|
|
5044
|
-
const existingDescription = typeof payload.actionDescription === "string" && payload.actionDescription ? payload.actionDescription : void 0;
|
|
5045
|
-
const labelText = policy.label ?? existingDescription ?? fallbackLabel ?? "esta a\xE7\xE3o";
|
|
5046
|
-
return {
|
|
5047
|
-
...payload,
|
|
5048
|
-
title: "A\xE7\xE3o destrutiva",
|
|
5049
|
-
message: `Tem certeza que deseja "${labelText}"?`,
|
|
5050
|
-
actionDescription: labelText,
|
|
5051
|
-
severity: "danger",
|
|
5052
|
-
destructive: true,
|
|
5053
|
-
confirmLabel: typeof payload.confirmLabel === "string" && payload.confirmLabel ? payload.confirmLabel : "Sim, executar",
|
|
5054
|
-
cancelLabel: typeof payload.cancelLabel === "string" && payload.cancelLabel ? payload.cancelLabel : "Cancelar"
|
|
5055
|
-
};
|
|
5056
|
-
}
|
|
5057
|
-
function confirmNeedsDestructiveUpgrade(confirm) {
|
|
5058
|
-
const payload = readPayload(confirm);
|
|
5059
|
-
return confirm.destructive !== true || payload.destructive !== true || payload.severity !== "danger";
|
|
4023
|
+
return void 0;
|
|
5060
4024
|
}
|
|
5061
|
-
function
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
4025
|
+
async function settleTurn(input) {
|
|
4026
|
+
const toolWithoutEffectIds = /* @__PURE__ */ new Set();
|
|
4027
|
+
let loadingPending = false;
|
|
4028
|
+
const { turnActions, pressedComponentId: pressedId, navigateTargetScreen } = input;
|
|
4029
|
+
const successById = new Map(input.results.map((r) => [r.actionId, r.success]));
|
|
4030
|
+
const typeById = new Map(input.results.map((r) => [r.actionId, r.type]));
|
|
4031
|
+
if (input.hadNavigate) {
|
|
4032
|
+
const settle = await waitForScreenSettle({
|
|
4033
|
+
expectingChange: true,
|
|
4034
|
+
fromScreen: input.preNavigateScreen ?? null,
|
|
4035
|
+
fromSignature: input.preNavigateSignature ?? null,
|
|
4036
|
+
targetScreen: navigateTargetScreen,
|
|
4037
|
+
maxMs: 3500
|
|
4038
|
+
});
|
|
4039
|
+
console.log(
|
|
4040
|
+
`[Appilots] settleTurn: post-navigate settle done \u2014 screen="${settle.screen}" transitioned=${settle.transitioned} timedOut=${settle.timedOut} waitedMs=${settle.waitedMs}`
|
|
4041
|
+
);
|
|
4042
|
+
const navigationMoved = settle.transitioned || screenDepartedBaseline(
|
|
4043
|
+
input.preNavigateScreen ?? null,
|
|
4044
|
+
input.preNavigateSignature ?? null
|
|
4045
|
+
) || !!navigateTargetScreen && !!settle.screen && routesBelongToSameFeature(settle.screen, navigateTargetScreen);
|
|
4046
|
+
if (!navigationMoved) {
|
|
4047
|
+
appilotsDebugWarn(
|
|
4048
|
+
`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.`
|
|
4049
|
+
);
|
|
4050
|
+
for (const [actionId, type] of typeById) {
|
|
4051
|
+
if (type === "navigate") toolWithoutEffectIds.add(actionId);
|
|
4052
|
+
}
|
|
4053
|
+
}
|
|
4054
|
+
const loadSettle = await waitForLoadingSettle({
|
|
4055
|
+
fromScreen: settle.screen ?? null,
|
|
4056
|
+
maxMs: 3e3
|
|
4057
|
+
});
|
|
4058
|
+
console.log(
|
|
4059
|
+
`[Appilots] settleTurn: post-navigate loading settle \u2014 settled=${loadSettle.settled} loadingPending=${loadSettle.loadingPending} probes=${loadSettle.probeCount} waitedMs=${loadSettle.waitedMs}`
|
|
4060
|
+
);
|
|
4061
|
+
if (loadSettle.loadingPending) loadingPending = true;
|
|
4062
|
+
} else {
|
|
4063
|
+
const pressedAction = turnActions.find(
|
|
4064
|
+
(a) => a.type === "ui_interaction" && a.payload?.componentId === pressedId
|
|
4065
|
+
);
|
|
4066
|
+
const hints = resolveActionHints(getCurrentScreen(), pressedId, pressedAction?.payload);
|
|
4067
|
+
const settleCapMs = hints?.isAsyncTrigger === true ? 1e4 : POST_ACTION_LOADING_MAX_MS;
|
|
4068
|
+
const loadSettle = await waitForLoadingSettle({
|
|
4069
|
+
pressedComponentId: pressedId,
|
|
4070
|
+
fromScreen: getCurrentScreen(),
|
|
4071
|
+
maxMs: settleCapMs
|
|
4072
|
+
});
|
|
4073
|
+
console.log(
|
|
4074
|
+
`[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)" : ""}`
|
|
4075
|
+
);
|
|
4076
|
+
if (loadSettle.loadingPending) loadingPending = true;
|
|
4077
|
+
const listPressActions = turnActions.filter((action) => {
|
|
4078
|
+
const target = actionPressTargetId(action);
|
|
4079
|
+
return target ? isListItemPressTarget(target) : false;
|
|
4080
|
+
});
|
|
4081
|
+
if (listPressActions.length > 0) {
|
|
4082
|
+
const baselineFingerprint = probeLoadingState(pressedId, getCurrentScreen()).fingerprint;
|
|
4083
|
+
const postPressSettle = await waitForScreenSettle({
|
|
4084
|
+
expectingChange: true,
|
|
4085
|
+
fromScreen: input.preNavigateScreen ?? null,
|
|
4086
|
+
fromSignature: input.preNavigateSignature ?? null,
|
|
4087
|
+
maxMs: 2500
|
|
4088
|
+
});
|
|
4089
|
+
await waitForLoadingSettle({
|
|
4090
|
+
pressedComponentId: pressedId,
|
|
4091
|
+
fromScreen: getCurrentScreen(),
|
|
4092
|
+
maxMs: 4e3
|
|
4093
|
+
});
|
|
4094
|
+
const afterFingerprint = probeLoadingState(pressedId, getCurrentScreen()).fingerprint;
|
|
4095
|
+
console.log(
|
|
4096
|
+
`[Appilots] settleTurn: post-list-press settle \u2014 screen="${postPressSettle.screen}" transitioned=${postPressSettle.transitioned} timedOut=${postPressSettle.timedOut} waitedMs=${postPressSettle.waitedMs}`
|
|
4097
|
+
);
|
|
4098
|
+
if (!postPressSettle.transitioned && baselineFingerprint === afterFingerprint) {
|
|
4099
|
+
for (const action of listPressActions) {
|
|
4100
|
+
if (successById.get(action.id)) {
|
|
4101
|
+
toolWithoutEffectIds.add(action.id);
|
|
4102
|
+
}
|
|
4103
|
+
}
|
|
4104
|
+
}
|
|
4105
|
+
}
|
|
4106
|
+
}
|
|
4107
|
+
return { loadingPending, toolWithoutEffectIds };
|
|
5068
4108
|
}
|
|
4109
|
+
var reactNativeChatAdapter = {
|
|
4110
|
+
buildContext: buildAgentContext,
|
|
4111
|
+
getCurrentScreen,
|
|
4112
|
+
getCurrentScreenSignature,
|
|
4113
|
+
settleTurn
|
|
4114
|
+
};
|
|
5069
4115
|
async function runWithConfirmedDestructiveContext(enabled, fn) {
|
|
5070
4116
|
if (!enabled) return fn();
|
|
5071
4117
|
const originalAlert = Alert.alert;
|
|
@@ -5095,321 +4141,120 @@ async function runWithConfirmedDestructiveContext(enabled, fn) {
|
|
|
5095
4141
|
Alert.alert = originalAlert;
|
|
5096
4142
|
}
|
|
5097
4143
|
}
|
|
5098
|
-
function
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
if (!action?.id) return;
|
|
5111
|
-
console.log(
|
|
5112
|
-
`[Appilots] useAppilotsActions: received agent:action:start event, action id="${action.id}", type="${action.type}"`
|
|
5113
|
-
);
|
|
5114
|
-
setActions((prev) => {
|
|
5115
|
-
if (prev.some((a) => a.id === action.id)) return prev;
|
|
5116
|
-
return [...prev, action];
|
|
5117
|
-
});
|
|
5118
|
-
}
|
|
5119
|
-
});
|
|
5120
|
-
return unsubscribe;
|
|
5121
|
-
}, [subscribe]);
|
|
5122
|
-
const processedRef = useRef(/* @__PURE__ */ new Set());
|
|
5123
|
-
useEffect(() => {
|
|
5124
|
-
if (!autoExecute) return;
|
|
5125
|
-
const pending = actions.filter(
|
|
5126
|
-
(a) => a.status === "pending" && !processedRef.current.has(a.id)
|
|
5127
|
-
);
|
|
5128
|
-
console.log(`[Appilots] useAppilotsActions: autoExecute check \u2014 ${pending.length} pending action(s) to process`);
|
|
5129
|
-
const findGatingConfirm = (target) => {
|
|
5130
|
-
return actions.find((a) => {
|
|
5131
|
-
if (a.type !== "confirm") return false;
|
|
5132
|
-
const payload = a.payload;
|
|
5133
|
-
const injectedBy = payload?.injectedBy;
|
|
5134
|
-
return payload?.gatedActionId === target.id && (injectedBy === "permissions" || injectedBy === "sdk-local");
|
|
5135
|
-
});
|
|
5136
|
-
};
|
|
5137
|
-
const ensureLocalConfirmIfNeeded = (target) => {
|
|
5138
|
-
if (target.type !== "ui_interaction") return false;
|
|
5139
|
-
const payload = target.payload;
|
|
5140
|
-
const componentId = typeof payload?.componentId === "string" && payload.componentId || typeof payload?.targetId === "string" && payload.targetId || "";
|
|
5141
|
-
let policy = payloadRequiresConfirmation(target);
|
|
5142
|
-
if (!policy.required && componentId) {
|
|
5143
|
-
const screen = getCurrentScreen();
|
|
5144
|
-
const meta = screen ? getScreenMetadata(screen) : void 0;
|
|
5145
|
-
const actionsMeta = meta?.actions;
|
|
5146
|
-
if (actionsMeta && Array.isArray(actionsMeta)) {
|
|
5147
|
-
const match = actionsMeta.find((a) => {
|
|
5148
|
-
if (typeof a === "string") return false;
|
|
5149
|
-
return metadataMatchesComponentId(a.id, componentId);
|
|
5150
|
-
});
|
|
5151
|
-
if (match && typeof match !== "string") {
|
|
5152
|
-
policy = actionMetadataRequiresConfirmation(match);
|
|
5153
|
-
}
|
|
5154
|
-
}
|
|
5155
|
-
}
|
|
5156
|
-
const existingGate = findGatingConfirm(target);
|
|
5157
|
-
if (existingGate) {
|
|
5158
|
-
if (policy.required && confirmNeedsDestructiveUpgrade(existingGate)) {
|
|
5159
|
-
setActions(
|
|
5160
|
-
(prev) => prev.map(
|
|
5161
|
-
(a) => a.id === existingGate.id ? {
|
|
5162
|
-
...a,
|
|
5163
|
-
destructive: true,
|
|
5164
|
-
payload: toDestructiveConfirmPayload(
|
|
5165
|
-
a,
|
|
5166
|
-
policy,
|
|
5167
|
-
componentId || target.id
|
|
5168
|
-
)
|
|
5169
|
-
} : a
|
|
5170
|
-
)
|
|
5171
|
-
);
|
|
5172
|
-
}
|
|
5173
|
-
return false;
|
|
5174
|
-
}
|
|
5175
|
-
if (!policy.required) return false;
|
|
5176
|
-
const confirmId = `confirm-local-${target.id}`;
|
|
5177
|
-
setActions((prev) => {
|
|
5178
|
-
const existingGate2 = prev.some((a) => {
|
|
5179
|
-
if (a.type !== "confirm") return false;
|
|
5180
|
-
const p = a.payload;
|
|
5181
|
-
if (!p || p.gatedActionId !== target.id) return false;
|
|
5182
|
-
return p.injectedBy === "permissions" || p.injectedBy === "sdk-local";
|
|
5183
|
-
});
|
|
5184
|
-
if (existingGate2) {
|
|
5185
|
-
const gateToUpgrade = prev.find(
|
|
5186
|
-
(a) => isConfirmGateFor(a, target) && confirmNeedsDestructiveUpgrade(a)
|
|
5187
|
-
);
|
|
5188
|
-
if (!gateToUpgrade) return prev;
|
|
5189
|
-
return prev.map(
|
|
5190
|
-
(a) => a.id === gateToUpgrade.id ? {
|
|
5191
|
-
...a,
|
|
5192
|
-
destructive: true,
|
|
5193
|
-
payload: toDestructiveConfirmPayload(
|
|
5194
|
-
a,
|
|
5195
|
-
policy,
|
|
5196
|
-
componentId || target.id
|
|
5197
|
-
)
|
|
5198
|
-
} : a
|
|
5199
|
-
);
|
|
5200
|
-
}
|
|
5201
|
-
if (prev.some((a) => a.id === confirmId)) return prev;
|
|
5202
|
-
console.log(
|
|
5203
|
-
`[Appilots] useAppilotsActions: action "${componentId || target.id}" requires confirmation (${policy.source ?? "local-policy"}) \u2014 synthesising local confirm (id="${confirmId}")`
|
|
5204
|
-
);
|
|
5205
|
-
const synth = {
|
|
5206
|
-
id: confirmId,
|
|
5207
|
-
type: "confirm",
|
|
5208
|
-
payload: toDestructiveConfirmPayload(
|
|
5209
|
-
{
|
|
5210
|
-
payload: {
|
|
5211
|
-
gatedActionId: target.id,
|
|
5212
|
-
gatedActionType: target.type,
|
|
5213
|
-
injectedBy: "sdk-local"
|
|
5214
|
-
},
|
|
5215
|
-
messageId: target.messageId
|
|
5216
|
-
},
|
|
5217
|
-
policy,
|
|
5218
|
-
componentId || "esta a\xE7\xE3o"
|
|
5219
|
-
),
|
|
5220
|
-
status: "pending",
|
|
5221
|
-
messageId: target.messageId
|
|
5222
|
-
};
|
|
5223
|
-
emit({
|
|
5224
|
-
type: "agent:action:start",
|
|
5225
|
-
timestamp: Date.now(),
|
|
5226
|
-
data: { action: synth }
|
|
5227
|
-
});
|
|
5228
|
-
const idx = prev.findIndex((a) => a.id === target.id);
|
|
5229
|
-
if (idx < 0) return [...prev, synth];
|
|
5230
|
-
return [...prev.slice(0, idx), synth, ...prev.slice(idx)];
|
|
5231
|
-
});
|
|
5232
|
-
return true;
|
|
5233
|
-
};
|
|
5234
|
-
const executeSequentially = async () => {
|
|
5235
|
-
let lastWasNavigate = false;
|
|
5236
|
-
let preNavScreen = null;
|
|
5237
|
-
let preNavSignature = null;
|
|
5238
|
-
for (const action of pending) {
|
|
5239
|
-
if (action.type === "confirm") {
|
|
5240
|
-
continue;
|
|
5241
|
-
}
|
|
5242
|
-
if (ensureLocalConfirmIfNeeded(action)) {
|
|
5243
|
-
continue;
|
|
5244
|
-
}
|
|
5245
|
-
const gate = findGatingConfirm(action);
|
|
5246
|
-
if (gate) {
|
|
5247
|
-
if (gate.status === "pending" || gate.status === "executing") {
|
|
5248
|
-
console.log(
|
|
5249
|
-
`[Appilots] useAppilotsActions: action id="${action.id}" is gated by confirm id="${gate.id}" (status=${gate.status}); waiting for user decision`
|
|
5250
|
-
);
|
|
5251
|
-
continue;
|
|
5252
|
-
}
|
|
5253
|
-
if (gate.status === "failed") {
|
|
5254
|
-
console.log(
|
|
5255
|
-
`[Appilots] useAppilotsActions: confirm id="${gate.id}" was rejected; skipping gated action id="${action.id}"`
|
|
5256
|
-
);
|
|
5257
|
-
processedRef.current.add(action.id);
|
|
5258
|
-
setActions(
|
|
5259
|
-
(prev) => prev.map(
|
|
5260
|
-
(a) => a.id === action.id ? { ...a, status: "failed", error: "Confirm canceled by user" } : a
|
|
5261
|
-
)
|
|
5262
|
-
);
|
|
5263
|
-
try {
|
|
5264
|
-
await client.completeAction(action.id, false, "Confirm canceled by user");
|
|
5265
|
-
} catch {
|
|
5266
|
-
}
|
|
5267
|
-
emit({
|
|
5268
|
-
type: "agent:action:error",
|
|
5269
|
-
timestamp: Date.now(),
|
|
5270
|
-
data: {
|
|
5271
|
-
actionId: action.id,
|
|
5272
|
-
actionType: action.type,
|
|
5273
|
-
success: false,
|
|
5274
|
-
error: "Confirm canceled by user"
|
|
5275
|
-
}
|
|
5276
|
-
});
|
|
5277
|
-
continue;
|
|
5278
|
-
}
|
|
5279
|
-
}
|
|
5280
|
-
if (lastWasNavigate) {
|
|
5281
|
-
const settle = await waitForScreenSettle({
|
|
5282
|
-
expectingChange: true,
|
|
5283
|
-
fromScreen: preNavScreen,
|
|
5284
|
-
fromSignature: preNavSignature,
|
|
5285
|
-
maxMs: 3500
|
|
5286
|
-
});
|
|
5287
|
-
console.log(
|
|
5288
|
-
`[Appilots] useAppilotsActions: post-navigate settle done \u2014 screen="${settle.screen}" transitioned=${settle.transitioned} timedOut=${settle.timedOut} waitedMs=${settle.waitedMs}`
|
|
5289
|
-
);
|
|
5290
|
-
}
|
|
5291
|
-
console.log(`[Appilots] useAppilotsActions: auto-executing action id="${action.id}" type="${action.type}"`);
|
|
5292
|
-
processedRef.current.add(action.id);
|
|
5293
|
-
if (action.type === "navigate") {
|
|
5294
|
-
preNavScreen = getCurrentScreen();
|
|
5295
|
-
preNavSignature = getCurrentScreenSignature();
|
|
5296
|
-
}
|
|
5297
|
-
const succeeded = await approveActionImpl(action.id);
|
|
5298
|
-
if (!succeeded) {
|
|
5299
|
-
appilotsDebugWarn(
|
|
5300
|
-
`useAppilotsActions: stopping current action batch after failed action id="${action.id}"`
|
|
5301
|
-
);
|
|
5302
|
-
break;
|
|
5303
|
-
}
|
|
5304
|
-
lastWasNavigate = action.type === "navigate";
|
|
5305
|
-
}
|
|
5306
|
-
};
|
|
5307
|
-
void executeSequentially();
|
|
5308
|
-
}, [actions, autoExecute]);
|
|
5309
|
-
const approveActionImpl = useCallback(
|
|
5310
|
-
async (actionId) => {
|
|
5311
|
-
const action = actionsRef.current.find((a) => a.id === actionId);
|
|
5312
|
-
if (!action) return false;
|
|
5313
|
-
console.log(`[Appilots] useAppilotsActions: action status transition: pending \u2192 executing (id="${actionId}", type="${action.type}")`);
|
|
5314
|
-
setActions(
|
|
5315
|
-
(prev) => prev.map((a) => a.id === actionId ? { ...a, status: "executing" } : a)
|
|
4144
|
+
function createReactNativeActionRunner(getOptions) {
|
|
4145
|
+
return {
|
|
4146
|
+
async execute(action, { confirmedDestructive }) {
|
|
4147
|
+
const { permissions, emit, navigationRef } = getOptions();
|
|
4148
|
+
const navRef = navigationRef?.current ? navigationRef : { current: getNavigationRef() };
|
|
4149
|
+
return runWithConfirmedDestructiveContext(
|
|
4150
|
+
confirmedDestructive,
|
|
4151
|
+
() => executeAction(action, {
|
|
4152
|
+
navigationRef: navRef,
|
|
4153
|
+
permissions,
|
|
4154
|
+
emit
|
|
4155
|
+
})
|
|
5316
4156
|
);
|
|
5317
|
-
try {
|
|
5318
|
-
const navRef = navRefStable.current?.current ? navRefStable.current : { current: getNavigationRef() };
|
|
5319
|
-
const confirmedDestructive = actionHasCompletedConfirmGate(
|
|
5320
|
-
action,
|
|
5321
|
-
actionsRef.current
|
|
5322
|
-
);
|
|
5323
|
-
const result = await runWithConfirmedDestructiveContext(
|
|
5324
|
-
confirmedDestructive,
|
|
5325
|
-
() => executeAction(
|
|
5326
|
-
{ ...action, status: "executing" },
|
|
5327
|
-
{
|
|
5328
|
-
navigationRef: navRef,
|
|
5329
|
-
permissions: config.permissions ?? DEFAULT_PERMISSIONS2,
|
|
5330
|
-
emit
|
|
5331
|
-
}
|
|
5332
|
-
)
|
|
5333
|
-
);
|
|
5334
|
-
if (result.success) {
|
|
5335
|
-
console.log(`[Appilots] useAppilotsActions: action status transition: executing \u2192 completed (id="${actionId}", type="${action.type}")`);
|
|
5336
|
-
setActions(
|
|
5337
|
-
(prev) => prev.map((a) => a.id === actionId ? { ...a, status: "completed" } : a)
|
|
5338
|
-
);
|
|
5339
|
-
try {
|
|
5340
|
-
await client.completeAction(actionId, true);
|
|
5341
|
-
} catch {
|
|
5342
|
-
}
|
|
5343
|
-
return true;
|
|
5344
|
-
} else {
|
|
5345
|
-
appilotsDebugWarn(
|
|
5346
|
-
`useAppilotsActions: action status transition: executing \u2192 failed (id="${actionId}", type="${action.type}", error="${result.error}")`
|
|
5347
|
-
);
|
|
5348
|
-
setActions(
|
|
5349
|
-
(prev) => prev.map(
|
|
5350
|
-
(a) => a.id === actionId ? { ...a, status: "failed", error: result.error } : a
|
|
5351
|
-
)
|
|
5352
|
-
);
|
|
5353
|
-
try {
|
|
5354
|
-
await client.completeAction(
|
|
5355
|
-
actionId,
|
|
5356
|
-
false,
|
|
5357
|
-
result.error,
|
|
5358
|
-
result.diagnose
|
|
5359
|
-
);
|
|
5360
|
-
} catch {
|
|
5361
|
-
}
|
|
5362
|
-
return false;
|
|
5363
|
-
}
|
|
5364
|
-
} catch (err) {
|
|
5365
|
-
const errorMsg = err instanceof Error ? err.message : "Action failed";
|
|
5366
|
-
appilotsDebugWarn(
|
|
5367
|
-
`useAppilotsActions: action status transition: executing \u2192 failed (exception) (id="${actionId}", error="${errorMsg}")`
|
|
5368
|
-
);
|
|
5369
|
-
setActions(
|
|
5370
|
-
(prev) => prev.map(
|
|
5371
|
-
(a) => a.id === actionId ? { ...a, status: "failed", error: errorMsg } : a
|
|
5372
|
-
)
|
|
5373
|
-
);
|
|
5374
|
-
try {
|
|
5375
|
-
await client.completeAction(actionId, false, errorMsg);
|
|
5376
|
-
} catch {
|
|
5377
|
-
}
|
|
5378
|
-
return false;
|
|
5379
|
-
}
|
|
5380
4157
|
},
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
setActions(
|
|
5386
|
-
(prev) => prev.map(
|
|
5387
|
-
(a) => a.id === actionId ? { ...a, status: "failed", error: "Rejected by user" } : a
|
|
5388
|
-
)
|
|
5389
|
-
);
|
|
5390
|
-
const action = actionsRef.current.find((a) => a.id === actionId);
|
|
5391
|
-
emit({
|
|
5392
|
-
type: "agent:action:error",
|
|
5393
|
-
timestamp: Date.now(),
|
|
5394
|
-
data: {
|
|
5395
|
-
actionId,
|
|
5396
|
-
actionType: action?.type,
|
|
5397
|
-
success: false,
|
|
5398
|
-
error: "Rejected by user"
|
|
5399
|
-
}
|
|
5400
|
-
});
|
|
5401
|
-
try {
|
|
5402
|
-
await client.completeAction(actionId, false, "Rejected by user");
|
|
5403
|
-
} catch {
|
|
5404
|
-
}
|
|
4158
|
+
getCurrentScreen,
|
|
4159
|
+
getCurrentScreenSignature,
|
|
4160
|
+
getScreenActionsMetadata(screen) {
|
|
4161
|
+
return getScreenMetadata(screen)?.actions;
|
|
5405
4162
|
},
|
|
4163
|
+
waitForScreenSettle
|
|
4164
|
+
};
|
|
4165
|
+
}
|
|
4166
|
+
|
|
4167
|
+
// src/hooks/useAppilotsChat.ts
|
|
4168
|
+
function useAppilotsChat(options = {}) {
|
|
4169
|
+
const { client, emit, subscribe } = useAppilotsContext();
|
|
4170
|
+
const machine = useMemo(
|
|
4171
|
+
() => new ChatSessionMachine(
|
|
4172
|
+
{ client, adapter: reactNativeChatAdapter, emit, warn: appilotsDebugWarn },
|
|
4173
|
+
options
|
|
4174
|
+
),
|
|
4175
|
+
// A new machine per client/provider instance — options are pushed in
|
|
4176
|
+
// below so changing copy never resets the transcript.
|
|
4177
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
4178
|
+
[client, emit]
|
|
4179
|
+
);
|
|
4180
|
+
const optionsRef = useRef(options);
|
|
4181
|
+
optionsRef.current = options;
|
|
4182
|
+
machine.setOptions(options);
|
|
4183
|
+
const state = useSyncExternalStore(machine.subscribeState, machine.getState, machine.getState);
|
|
4184
|
+
useEffect(() => subscribe(machine.handleEvent), [subscribe, machine]);
|
|
4185
|
+
useEffect(() => () => machine.dispose(), [machine]);
|
|
4186
|
+
const requestHuman = useCallback(
|
|
4187
|
+
(reason) => machine.requestHuman(reason),
|
|
4188
|
+
[machine]
|
|
4189
|
+
);
|
|
4190
|
+
const sendMessage = useCallback((content) => machine.sendMessage(content), [machine]);
|
|
4191
|
+
const cancelMessage = useCallback(() => machine.cancelMessage(), [machine]);
|
|
4192
|
+
const clearMessages = useCallback(() => machine.clearMessages(), [machine]);
|
|
4193
|
+
const clearError = useCallback(() => machine.clearError(), [machine]);
|
|
4194
|
+
return {
|
|
4195
|
+
messages: state.messages,
|
|
4196
|
+
isLoading: state.isLoading,
|
|
4197
|
+
loadingStatusKey: state.loadingStatusKey,
|
|
4198
|
+
error: state.error,
|
|
4199
|
+
pendingActions: state.pendingActions,
|
|
4200
|
+
escalation: state.escalation,
|
|
4201
|
+
requestHuman,
|
|
4202
|
+
sendMessage,
|
|
4203
|
+
cancelMessage,
|
|
4204
|
+
clearMessages,
|
|
4205
|
+
clearError
|
|
4206
|
+
};
|
|
4207
|
+
}
|
|
4208
|
+
var DEFAULT_PERMISSIONS2 = {
|
|
4209
|
+
canNavigate: true,
|
|
4210
|
+
canFillForms: true,
|
|
4211
|
+
canInteractUI: true,
|
|
4212
|
+
canSubmitForms: true
|
|
4213
|
+
};
|
|
4214
|
+
function useAppilotsActions(options = {}) {
|
|
4215
|
+
const { client, subscribe, config, emit } = useAppilotsContext();
|
|
4216
|
+
const { navigationRef, autoExecute = false } = options;
|
|
4217
|
+
const runnerOptionsRef = useRef({
|
|
4218
|
+
permissions: config.permissions ?? DEFAULT_PERMISSIONS2,
|
|
4219
|
+
emit,
|
|
4220
|
+
navigationRef
|
|
4221
|
+
});
|
|
4222
|
+
runnerOptionsRef.current = {
|
|
4223
|
+
permissions: config.permissions ?? DEFAULT_PERMISSIONS2,
|
|
4224
|
+
emit,
|
|
4225
|
+
navigationRef
|
|
4226
|
+
};
|
|
4227
|
+
const machine = useMemo(
|
|
4228
|
+
() => new ActionQueueMachine(
|
|
4229
|
+
{
|
|
4230
|
+
client,
|
|
4231
|
+
adapter: createReactNativeActionRunner(() => runnerOptionsRef.current),
|
|
4232
|
+
emit,
|
|
4233
|
+
warn: appilotsDebugWarn
|
|
4234
|
+
},
|
|
4235
|
+
{ autoExecute }
|
|
4236
|
+
),
|
|
4237
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
5406
4238
|
[client, emit]
|
|
5407
4239
|
);
|
|
4240
|
+
machine.setOptions({ autoExecute });
|
|
4241
|
+
const state = useSyncExternalStore(machine.subscribeState, machine.getState, machine.getState);
|
|
4242
|
+
useEffect(() => subscribe(machine.handleEvent), [subscribe, machine]);
|
|
4243
|
+
useEffect(() => {
|
|
4244
|
+
if (autoExecute) machine.scheduleDrain();
|
|
4245
|
+
}, [autoExecute, machine, state.actions]);
|
|
4246
|
+
const approveAction = useCallback(
|
|
4247
|
+
(actionId) => machine.approveAction(actionId),
|
|
4248
|
+
[machine]
|
|
4249
|
+
);
|
|
4250
|
+
const rejectAction = useCallback((actionId) => machine.rejectAction(actionId), [machine]);
|
|
5408
4251
|
return {
|
|
5409
|
-
pendingActions: actions.filter((a) => a.status === "pending"),
|
|
5410
|
-
executingActions: actions.filter((a) => a.status === "executing"),
|
|
5411
|
-
completedActions: actions.filter(
|
|
5412
|
-
|
|
4252
|
+
pendingActions: state.actions.filter((a) => a.status === "pending"),
|
|
4253
|
+
executingActions: state.actions.filter((a) => a.status === "executing"),
|
|
4254
|
+
completedActions: state.actions.filter(
|
|
4255
|
+
(a) => a.status === "completed" || a.status === "failed"
|
|
4256
|
+
),
|
|
4257
|
+
approveAction,
|
|
5413
4258
|
rejectAction
|
|
5414
4259
|
};
|
|
5415
4260
|
}
|