@appilots/sdk 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
- import { getFiberRoot, getCurrentScreen, getActiveRouteNames, elementRegistry, useAppilotsContext, recordAppilotsDebugTrace, getCurrentScreenSignature, fingerprintAction, turnHasConfirmActions, waitForScreenSettle, screenDepartedBaseline, routesBelongToSameFeature, appilotsDebugWarn, waitForLoadingSettle, actionPressTargetId, isListItemPressTarget, probeLoadingState, buildContinuationFailureMessage, StreamTransportError, StreamAbortError, getDefaultRegistry, getNavigationRef, getScreenMetadata, setCurrentScreen, useResolvedRegistry, listRegistry, componentRegistry, getNavigationStateSnapshot, metadataMatchesComponentId, isOptionalStepAutomationFailure, OPTIONAL_STEP_AUTOMATION_HINT, normalize2, matchScore, getAllScreens, looksDestructiveActionLabel, isGenericSelectValue } from './chunk-QMSMXTED.mjs';
2
- import { useRef, useState, useCallback, useEffect } from 'react';
1
+ import { getFiberRoot, getCurrentScreen, getActiveRouteNames, deriveInteractionElements, attachElementIdsToChoiceGroups, elementRegistry, appilotsDebugWarn, getDefaultRegistry, useAppilotsContext, ChatSessionMachine, getCurrentScreenSignature, ActionQueueMachine, getScreenMetadata, setCurrentScreen, useResolvedRegistry, listRegistry, componentRegistry, probeLoadingState, isListItemPressTarget, waitForLoadingSettle, waitForScreenSettle, screenDepartedBaseline, routesBelongToSameFeature, actionPressTargetId, getNavigationStateSnapshot, getNavigationRef, isOptionalStepAutomationFailure, OPTIONAL_STEP_AUTOMATION_HINT, normalize3, matchScore, idLooselyMatches, parseStableElementId, getAllScreens, snapshotShowsOpenCreateForm, looksDestructiveActionLabel, isGenericSelectValue } from './chunk-JBOLV5AM.mjs';
2
+ import { useMemo, useRef, useSyncExternalStore, useEffect, useCallback, useState } from 'react';
3
3
  import { Alert } from 'react-native';
4
4
 
5
5
  // src/introspection/walkFiber.ts
@@ -654,225 +654,6 @@ function normalizeLabel(label) {
654
654
  return label.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9\s]/g, "").trim().replace(/\s+(.)/g, (_, c) => c.toUpperCase()).replace(/\s+/g, "");
655
655
  }
656
656
 
657
- // src/introspection/interactionGraph.ts
658
- function normalizePart(value) {
659
- return String(value ?? "").toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
660
- }
661
- function hashString(value) {
662
- let hash = 2166136261;
663
- for (let i = 0; i < value.length; i++) {
664
- hash ^= value.charCodeAt(i);
665
- hash = Math.imul(hash, 16777619);
666
- }
667
- return (hash >>> 0).toString(36);
668
- }
669
- function stableElementId(parts) {
670
- const raw = parts.filter((p) => p !== void 0 && String(p).length > 0).join("|");
671
- const readable = parts.map(normalizePart).filter(Boolean).join(":").slice(0, 120);
672
- return `el:${readable}:${hashString(raw)}`;
673
- }
674
- function parseStableElementId(id) {
675
- if (typeof id !== "string") return null;
676
- const trimmed = id.trim();
677
- if (!/^el:/i.test(trimmed)) return null;
678
- const parts = trimmed.split(":");
679
- if (parts.length < 3) return null;
680
- const hash = parts[parts.length - 1];
681
- const segments = parts.slice(1, -1);
682
- if (segments.length === 0) return null;
683
- const role = segments[0];
684
- const result = { role, segments, hash };
685
- if (role === "option" && segments.length >= 4) {
686
- result.listId = segments[segments.length - 2];
687
- result.itemKey = segments[segments.length - 1];
688
- }
689
- return result;
690
- }
691
- function uniqueTexts(texts) {
692
- const seen = /* @__PURE__ */ new Set();
693
- const out = [];
694
- for (const text of texts) {
695
- const t = typeof text === "string" ? text.trim() : "";
696
- if (!t || seen.has(t)) continue;
697
- seen.add(t);
698
- out.push(t);
699
- }
700
- return out;
701
- }
702
- function labelForListItem(item) {
703
- return uniqueTexts([
704
- ...item.texts ?? [],
705
- ...(item.buttons ?? []).map((button) => button.label ?? button.id)
706
- ]).slice(0, 2).join(" \xB7 ") || void 0;
707
- }
708
- function listItemElement(route, list, item) {
709
- const texts = uniqueTexts([
710
- ...item.texts ?? [],
711
- ...(item.buttons ?? []).map((button) => button.label ?? button.id)
712
- ]);
713
- const label = labelForListItem(item);
714
- const key = item.itemKey ?? item.reactKey ?? label ?? item.syntheticId;
715
- const id = stableElementId([
716
- "option",
717
- route ?? "unknown",
718
- list.id ?? list.label ?? `list-${list.index}`,
719
- key
720
- ]);
721
- const inModal = (item.buttons ?? []).some((button) => button.inModal) || (item.inputs ?? []).some((input) => input.inModal) || (item.toggles ?? []).some((toggle) => toggle.inModal);
722
- return {
723
- id,
724
- role: "option",
725
- label,
726
- texts,
727
- actions: ["press"],
728
- disabled: false,
729
- source: "list",
730
- targetId: item.syntheticId,
731
- ...inModal ? { inModal: true } : {},
732
- listContext: {
733
- listIndex: list.index,
734
- listId: list.id,
735
- listLabel: list.label,
736
- itemIndex: item.index,
737
- itemKey: item.itemKey,
738
- reactKey: item.reactKey,
739
- syntheticId: item.syntheticId
740
- }
741
- };
742
- }
743
- function buttonElement(route, button, index) {
744
- const label = button.label ?? button.id;
745
- const id = stableElementId([
746
- "button",
747
- route ?? "unknown",
748
- button.id ?? label ?? `button-${index}`
749
- ]);
750
- return {
751
- id,
752
- role: "button",
753
- label,
754
- texts: uniqueTexts([label]),
755
- actions: ["press"],
756
- disabled: button.disabled,
757
- selected: button.selected,
758
- source: "button",
759
- targetId: button.id ?? button.label,
760
- ...button.inModal ? { inModal: true } : {}
761
- };
762
- }
763
- function inputElement(route, input, index) {
764
- const label = input.label ?? input.placeholder ?? input.id;
765
- const id = stableElementId([
766
- "input",
767
- route ?? "unknown",
768
- input.id ?? label ?? `input-${index}`
769
- ]);
770
- return {
771
- id,
772
- role: "input",
773
- label,
774
- texts: uniqueTexts([label, input.placeholder]),
775
- actions: ["focus", "setValue"],
776
- disabled: input.editable === false,
777
- source: "input",
778
- targetId: input.id ?? input.label ?? input.placeholder,
779
- ...input.inModal ? { inModal: true } : {}
780
- };
781
- }
782
- function toggleElement(route, toggle, index) {
783
- const label = toggle.label ?? toggle.id;
784
- const id = stableElementId([
785
- "toggle",
786
- route ?? "unknown",
787
- toggle.id ?? label ?? `toggle-${index}`
788
- ]);
789
- return {
790
- id,
791
- role: "toggle",
792
- label,
793
- texts: uniqueTexts([label]),
794
- actions: ["toggle", "press"],
795
- selected: toggle.value,
796
- source: "toggle",
797
- targetId: toggle.id ?? toggle.label,
798
- ...toggle.inModal ? { inModal: true } : {}
799
- };
800
- }
801
- function sliderElement(route, slider, index) {
802
- const label = slider.label ?? slider.id;
803
- const id = stableElementId([
804
- "slider",
805
- route ?? "unknown",
806
- slider.id ?? label ?? `slider-${index}`
807
- ]);
808
- return {
809
- id,
810
- role: "slider",
811
- label,
812
- texts: uniqueTexts([label]),
813
- actions: ["setValue"],
814
- disabled: slider.disabled,
815
- source: "slider",
816
- targetId: slider.id ?? slider.label,
817
- ...slider.inModal ? { inModal: true } : {}
818
- };
819
- }
820
- function deriveInteractionElements(snapshot) {
821
- const elements = [];
822
- const seen = /* @__PURE__ */ new Set();
823
- const push = (element) => {
824
- if (!element.id || seen.has(element.id)) return;
825
- seen.add(element.id);
826
- elements.push(element);
827
- };
828
- for (const list of snapshot.lists ?? []) {
829
- for (const item of list.items ?? []) {
830
- push(listItemElement(snapshot.route, list, item));
831
- }
832
- }
833
- for (const [index, button] of (snapshot.buttons ?? []).entries()) {
834
- push(buttonElement(snapshot.route, button, index));
835
- }
836
- for (const [index, input] of (snapshot.inputs ?? []).entries()) {
837
- push(inputElement(snapshot.route, input, index));
838
- }
839
- for (const [index, toggle] of (snapshot.toggles ?? []).entries()) {
840
- push(toggleElement(snapshot.route, toggle, index));
841
- }
842
- for (const [index, slider] of (snapshot.sliders ?? []).entries()) {
843
- push(sliderElement(snapshot.route, slider, index));
844
- }
845
- return elements;
846
- }
847
- function findElementForChoice(elements, option) {
848
- if (option.targetId) {
849
- const byTarget = elements.find((element) => element.targetId === option.targetId);
850
- if (byTarget) return byTarget;
851
- }
852
- const optionLabel = normalizePart(option.label ?? option.texts?.join(" "));
853
- if (!optionLabel) return void 0;
854
- return elements.find((element) => {
855
- if (element.role !== "option" && element.role !== "button") return false;
856
- const label = normalizePart(element.label ?? element.texts.join(" "));
857
- return label === optionLabel || label.includes(optionLabel) || optionLabel.includes(label);
858
- });
859
- }
860
- function attachElementIdsToChoiceGroups(choiceGroups, elements) {
861
- return choiceGroups.map((group) => ({
862
- ...group,
863
- options: group.options.map((option) => {
864
- const element = findElementForChoice(elements, option);
865
- if (!element) return option;
866
- const handle = element.listContext?.syntheticId ?? element.id;
867
- return {
868
- ...option,
869
- syntheticId: handle,
870
- targetId: handle
871
- };
872
- })
873
- }));
874
- }
875
-
876
657
  // src/introspection/captureSnapshot.ts
877
658
  function normalizeSliderKey(value) {
878
659
  return (value ?? "").toLowerCase().replace(/^slider[-_]/, "");
@@ -1206,844 +987,30 @@ function findInteractiveByIdentifier(query, options = {}) {
1206
987
  if (fiber.sibling) stack.push(fiber.sibling);
1207
988
  if (shouldSkipSubtree2(fiber)) continue;
1208
989
  let kind = null;
1209
- 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
- }
990
+ if (matchesName2(fiber, "TextInput")) kind = "field";
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
- setEscalation(null);
2028
- escalationSinceRef.current = void 0;
2029
- seenHumanMessageIdsRef.current = /* @__PURE__ */ new Set();
2030
- escalationReconnectAttemptRef.current = 0;
2031
- client.destroySession();
2032
- }, [client]);
2033
- const clearError = useCallback(() => setError(null), []);
2034
- return {
2035
- messages,
2036
- isLoading,
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 normalize4(value) {
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(normalize4).filter((text) => text.length >= 2);
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(normalize4).filter(Boolean);
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 && normalize4(itemKey) === normalize4(locator.itemKey)) score += 950;
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 && normalize4(reactKey) === normalize4(locator.reactKey)) score += 850;
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 = normalize4(locator.listId);
2527
- const matches = containers.map((list, listIndex) => ({ list, listIndex })).filter(({ list }) => normalize4(list.listId) === normalizedId);
1493
+ const normalizedId = normalize2(locator.listId);
1494
+ const matches = containers.map((list, listIndex) => ({ list, listIndex })).filter(({ list }) => normalize2(list.listId) === normalizedId);
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 = normalize4(locator.listLabel);
2536
- const matches = containers.map((list, listIndex) => ({ list, listIndex })).filter(({ list }) => normalize4(list.listLabel).includes(normalizedLabel));
1502
+ const normalizedLabel = normalize2(locator.listLabel);
1503
+ const matches = containers.map((list, listIndex) => ({ list, listIndex })).filter(({ list }) => normalize2(list.listLabel).includes(normalizedLabel));
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 normalize5(value) {
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 normalize5(`${field.id} ${field.label ?? ""}`);
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 = normalize5(trimmed);
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 = normalize5(String(rawValue ?? ""));
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 = normalize5(option.label ?? "");
2933
- const optionValue = normalize5(option.value ?? "");
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 = normalize5(value);
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 = normalize5(field.type ?? "text");
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) => normalize5(field.id)));
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 FORM_VALIDATION_TEXT_RE2 = /(inv[aá]lid|obrigat[oó]ri|required|invalid|j[aá]\s+existe|already\s+exists|duplicad|formato|format|too\s+short|muito\s+curto|n[aã]o\s+(?:foi|pode)|must\s+be|deve\s+ser|selecione|escolha|select\s+(?:the|a|o|an))/i;
1972
+ var FORM_VALIDATION_TEXT_RE = /(inv[aá]lid|obrigat[oó]ri|required|invalid|j[aá]\s+existe|already\s+exists|duplicad|formato|format|too\s+short|muito\s+curto|n[aã]o\s+(?:foi|pode)|must\s+be|deve\s+ser|selecione|escolha|select\s+(?:the|a|o|an))/i;
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() || !FORM_VALIDATION_TEXT_RE2.test(text)) continue;
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 normalize6(value) {
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 = normalize6(`${id ?? ""} ${label ?? ""}`);
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 = normalize6(id ?? "");
3682
- normalize6(label ?? "");
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 (FORM_VALIDATION_TEXT_RE2.test(lower)) {
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 = normalize2(requested);
3802
- const currentNorm = normalize2(currentValue);
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 (normalize2(value) === requestedNorm && normalize2(label) === currentNorm) return true;
3808
- if (normalize2(label) === requestedNorm && normalize2(label) === currentNorm) return true;
3809
- if (normalize2(value) === requestedNorm && normalize2(value) === currentNorm) return true;
2774
+ if (normalize3(value) === requestedNorm && normalize3(label) === currentNorm) return true;
2775
+ if (normalize3(label) === requestedNorm && normalize3(label) === currentNorm) return true;
2776
+ if (normalize3(value) === requestedNorm && normalize3(value) === currentNorm) return true;
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 = normalize2(fieldId);
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" ? normalize2(field.id) : "";
3830
- const label = typeof field.label === "string" ? normalize2(field.label) : "";
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 = normalize2(requested);
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 (normalize2(optionValue) === requestedNorm || normalize2(optionLabel) === requestedNorm) {
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 = normalize2(type);
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 = normalize2(value);
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) => normalize2(text)).filter(Boolean);
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 = normalize2(String(value ?? ""));
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 = normalize2(text);
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 = normalize2(text);
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 = normalize2(text);
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(normalize2(label));
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 = normalize2(fieldId);
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 = [normalize2(comp.id)];
4142
- if (comp.label) candidates.push(normalize2(comp.label));
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 = normalize2(metaLabel);
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, normalize2(comp.label));
3145
+ const score = matchScore(metaNeedle, normalize3(comp.label));
4179
3146
  if (score > mBestScore) {
4180
3147
  mBestScore = score;
4181
3148
  mBest = comp;
@@ -4993,79 +3960,156 @@ async function executeAction(action, context) {
4993
3960
  });
4994
3961
  return result;
4995
3962
  }
4996
- var DEFAULT_PERMISSIONS2 = {
4997
- canNavigate: true,
4998
- canFillForms: true,
4999
- canInteractUI: true,
5000
- canSubmitForms: true
5001
- };
5002
- function readPayload(action) {
5003
- return action.payload ?? {};
5004
- }
5005
- function isStructuredRisky(value) {
5006
- return value === true || value === "destructive" || value === "danger" || value === "high";
3963
+ var POST_ACTION_LOADING_MAX_MS = 6e3;
3964
+ function resolveScreenMetadata() {
3965
+ const activePath = getActiveRouteNames();
3966
+ for (let i = activePath.length - 1; i >= 0; i--) {
3967
+ const name = activePath[i];
3968
+ const meta = name ? getScreenMetadata(name) : void 0;
3969
+ if (meta && (Array.isArray(meta.fields) && meta.fields.length > 0 || Array.isArray(meta.actions) && meta.actions.length > 0)) {
3970
+ return meta;
3971
+ }
3972
+ }
3973
+ const screen = getCurrentScreen();
3974
+ return screen ? getScreenMetadata(screen) : void 0;
5007
3975
  }
5008
- function actionMetadataRequiresConfirmation(meta) {
5009
- if (meta.requiresConfirmation === true || meta.destructive === true || meta.effect === "destructive" || meta.riskLevel === "high" || meta.risk === "destructive") {
5010
- return { required: true, label: meta.label, source: "screen-metadata" };
3976
+ function buildAgentContext(extras = {}) {
3977
+ const context = {};
3978
+ context.platform = "react-native";
3979
+ const screen = getCurrentScreen();
3980
+ if (screen) context.currentScreen = screen;
3981
+ const navigationState = getNavigationStateSnapshot();
3982
+ if (navigationState) context.navigationState = navigationState;
3983
+ const screenMeta = resolveScreenMetadata();
3984
+ if (screenMeta) context.screenMetadata = screenMeta;
3985
+ try {
3986
+ context.snapshot = captureSnapshot();
3987
+ } catch (err) {
3988
+ appilotsDebugWarn("buildAgentContext: captureSnapshot failed", err);
3989
+ }
3990
+ const registry = componentRegistry.snapshot();
3991
+ const filtered = screen ? registry.filter((c) => !c.screen || c.screen === screen) : registry;
3992
+ if (filtered.length > 0) context.registeredComponents = filtered;
3993
+ const registeredLists = listRegistry.snapshot();
3994
+ if (registeredLists.length > 0) {
3995
+ context.registeredLists = registeredLists.map(
3996
+ ({ scrollToIndex, scrollToOffset, getScrollMetrics, dataPreview, ...rest }) => rest
3997
+ );
5011
3998
  }
5012
- return { required: false };
3999
+ if (extras.loadingPending === true) context.loadingPending = true;
4000
+ return context;
5013
4001
  }
5014
- function payloadRequiresConfirmation(action) {
5015
- const payload = readPayload(action);
5016
- if (action.destructive === true || payload.destructive === true || payload.requiresConfirmation === true || isStructuredRisky(payload.effect) || isStructuredRisky(payload.risk) || isStructuredRisky(payload.riskLevel) || isStructuredRisky(payload.actionRisk)) {
5017
- return {
5018
- required: true,
5019
- label: typeof payload.actionDescription === "string" && payload.actionDescription || typeof payload.label === "string" && payload.label || void 0,
5020
- source: "action-policy"
5021
- };
4002
+ function resolveActionHints(screen, pressedComponentId, actionPayload) {
4003
+ if (actionPayload && typeof actionPayload === "object") {
4004
+ const inline = actionPayload._appilotsInferred;
4005
+ if (inline && typeof inline === "object") {
4006
+ return inline;
4007
+ }
5022
4008
  }
5023
- const policy = payload._appilotsPolicy;
5024
- if (policy && typeof policy === "object") {
5025
- const p = policy;
5026
- if (p.requiresConfirmation === true || isStructuredRisky(p.effect) || isStructuredRisky(p.risk) || isStructuredRisky(p.riskLevel)) {
5027
- return {
5028
- required: true,
5029
- label: typeof p.summary === "string" ? p.summary : void 0,
5030
- source: "inline-policy"
5031
- };
4009
+ if (!screen || !pressedComponentId) return void 0;
4010
+ const meta = getScreenMetadata(screen);
4011
+ const actions = meta?.actions;
4012
+ if (!actions || actions.length === 0) return void 0;
4013
+ for (const a of actions) {
4014
+ if (typeof a === "string") continue;
4015
+ if (idLooselyMatches(a.id, pressedComponentId)) {
4016
+ const inferred = a.appilotsInferred;
4017
+ if (inferred && typeof inferred === "object") return inferred;
4018
+ return void 0;
5032
4019
  }
5033
4020
  }
5034
- return { required: false };
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";
4021
+ return void 0;
5060
4022
  }
5061
- function actionHasCompletedConfirmGate(action, actions) {
5062
- return actions.some((candidate) => {
5063
- if (!isConfirmGateFor(candidate, action)) return false;
5064
- if (candidate.status !== "completed") return false;
5065
- const payload = readPayload(candidate);
5066
- return payload.destructive === true || payload.severity === "danger";
5067
- });
4023
+ async function settleTurn(input) {
4024
+ const toolWithoutEffectIds = /* @__PURE__ */ new Set();
4025
+ let loadingPending = false;
4026
+ const { turnActions, pressedComponentId: pressedId, navigateTargetScreen } = input;
4027
+ const successById = new Map(input.results.map((r) => [r.actionId, r.success]));
4028
+ const typeById = new Map(input.results.map((r) => [r.actionId, r.type]));
4029
+ if (input.hadNavigate) {
4030
+ const settle = await waitForScreenSettle({
4031
+ expectingChange: true,
4032
+ fromScreen: input.preNavigateScreen ?? null,
4033
+ fromSignature: input.preNavigateSignature ?? null,
4034
+ targetScreen: navigateTargetScreen,
4035
+ maxMs: 3500
4036
+ });
4037
+ console.log(
4038
+ `[Appilots] settleTurn: post-navigate settle done \u2014 screen="${settle.screen}" transitioned=${settle.transitioned} timedOut=${settle.timedOut} waitedMs=${settle.waitedMs}`
4039
+ );
4040
+ const navigationMoved = settle.transitioned || screenDepartedBaseline(
4041
+ input.preNavigateScreen ?? null,
4042
+ input.preNavigateSignature ?? null
4043
+ ) || !!navigateTargetScreen && !!settle.screen && routesBelongToSameFeature(settle.screen, navigateTargetScreen);
4044
+ if (!navigationMoved) {
4045
+ appilotsDebugWarn(
4046
+ `tool-without-effect: navigate emitted but screen did not change (stayed at "${input.preNavigateScreen ?? "unknown"}", waitedMs=${settle.waitedMs}). Likely causes: target screenName missing from MCP doc, nested-nav path absent, or React Navigation rejected the route.`
4047
+ );
4048
+ for (const [actionId, type] of typeById) {
4049
+ if (type === "navigate") toolWithoutEffectIds.add(actionId);
4050
+ }
4051
+ }
4052
+ const loadSettle = await waitForLoadingSettle({
4053
+ fromScreen: settle.screen ?? null,
4054
+ maxMs: 3e3
4055
+ });
4056
+ console.log(
4057
+ `[Appilots] settleTurn: post-navigate loading settle \u2014 settled=${loadSettle.settled} loadingPending=${loadSettle.loadingPending} probes=${loadSettle.probeCount} waitedMs=${loadSettle.waitedMs}`
4058
+ );
4059
+ if (loadSettle.loadingPending) loadingPending = true;
4060
+ } else {
4061
+ const pressedAction = turnActions.find(
4062
+ (a) => a.type === "ui_interaction" && a.payload?.componentId === pressedId
4063
+ );
4064
+ const hints = resolveActionHints(getCurrentScreen(), pressedId, pressedAction?.payload);
4065
+ const settleCapMs = hints?.isAsyncTrigger === true ? 1e4 : POST_ACTION_LOADING_MAX_MS;
4066
+ const loadSettle = await waitForLoadingSettle({
4067
+ pressedComponentId: pressedId,
4068
+ fromScreen: getCurrentScreen(),
4069
+ maxMs: settleCapMs
4070
+ });
4071
+ console.log(
4072
+ `[Appilots] settleTurn: post-action loading settle \u2014 pressedId="${pressedId ?? ""}" settled=${loadSettle.settled} transitioned=${loadSettle.transitioned} loadingPending=${loadSettle.loadingPending} probes=${loadSettle.probeCount} waitedMs=${loadSettle.waitedMs} cap=${settleCapMs}${hints?.isAsyncTrigger ? " (mcp-inferred)" : ""}`
4073
+ );
4074
+ if (loadSettle.loadingPending) loadingPending = true;
4075
+ const listPressActions = turnActions.filter((action) => {
4076
+ const target = actionPressTargetId(action);
4077
+ return target ? isListItemPressTarget(target) : false;
4078
+ });
4079
+ if (listPressActions.length > 0) {
4080
+ const baselineFingerprint = probeLoadingState(pressedId, getCurrentScreen()).fingerprint;
4081
+ const postPressSettle = await waitForScreenSettle({
4082
+ expectingChange: true,
4083
+ fromScreen: input.preNavigateScreen ?? null,
4084
+ fromSignature: input.preNavigateSignature ?? null,
4085
+ maxMs: 2500
4086
+ });
4087
+ await waitForLoadingSettle({
4088
+ pressedComponentId: pressedId,
4089
+ fromScreen: getCurrentScreen(),
4090
+ maxMs: 4e3
4091
+ });
4092
+ const afterFingerprint = probeLoadingState(pressedId, getCurrentScreen()).fingerprint;
4093
+ console.log(
4094
+ `[Appilots] settleTurn: post-list-press settle \u2014 screen="${postPressSettle.screen}" transitioned=${postPressSettle.transitioned} timedOut=${postPressSettle.timedOut} waitedMs=${postPressSettle.waitedMs}`
4095
+ );
4096
+ if (!postPressSettle.transitioned && baselineFingerprint === afterFingerprint) {
4097
+ for (const action of listPressActions) {
4098
+ if (successById.get(action.id)) {
4099
+ toolWithoutEffectIds.add(action.id);
4100
+ }
4101
+ }
4102
+ }
4103
+ }
4104
+ }
4105
+ return { loadingPending, toolWithoutEffectIds };
5068
4106
  }
4107
+ var reactNativeChatAdapter = {
4108
+ buildContext: buildAgentContext,
4109
+ getCurrentScreen,
4110
+ getCurrentScreenSignature,
4111
+ settleTurn
4112
+ };
5069
4113
  async function runWithConfirmedDestructiveContext(enabled, fn) {
5070
4114
  if (!enabled) return fn();
5071
4115
  const originalAlert = Alert.alert;
@@ -5095,321 +4139,120 @@ async function runWithConfirmedDestructiveContext(enabled, fn) {
5095
4139
  Alert.alert = originalAlert;
5096
4140
  }
5097
4141
  }
5098
- function useAppilotsActions(options = {}) {
5099
- const { client, subscribe, config, emit } = useAppilotsContext();
5100
- const [actions, setActions] = useState([]);
5101
- const { navigationRef, autoExecute = false } = options;
5102
- const actionsRef = useRef([]);
5103
- actionsRef.current = actions;
5104
- const navRefStable = useRef(navigationRef);
5105
- navRefStable.current = navigationRef;
5106
- useEffect(() => {
5107
- const unsubscribe = subscribe((event) => {
5108
- if (event.type === "agent:action:start") {
5109
- const action = event.data?.action;
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)
4142
+ function createReactNativeActionRunner(getOptions) {
4143
+ return {
4144
+ async execute(action, { confirmedDestructive }) {
4145
+ const { permissions, emit, navigationRef } = getOptions();
4146
+ const navRef = navigationRef?.current ? navigationRef : { current: getNavigationRef() };
4147
+ return runWithConfirmedDestructiveContext(
4148
+ confirmedDestructive,
4149
+ () => executeAction(action, {
4150
+ navigationRef: navRef,
4151
+ permissions,
4152
+ emit
4153
+ })
5316
4154
  );
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
4155
  },
5381
- [client, config.permissions, emit]
5382
- );
5383
- const rejectAction = useCallback(
5384
- async (actionId) => {
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
- }
4156
+ getCurrentScreen,
4157
+ getCurrentScreenSignature,
4158
+ getScreenActionsMetadata(screen) {
4159
+ return getScreenMetadata(screen)?.actions;
5405
4160
  },
4161
+ waitForScreenSettle
4162
+ };
4163
+ }
4164
+
4165
+ // src/hooks/useAppilotsChat.ts
4166
+ function useAppilotsChat(options = {}) {
4167
+ const { client, emit, subscribe } = useAppilotsContext();
4168
+ const machine = useMemo(
4169
+ () => new ChatSessionMachine(
4170
+ { client, adapter: reactNativeChatAdapter, emit, warn: appilotsDebugWarn },
4171
+ options
4172
+ ),
4173
+ // A new machine per client/provider instance — options are pushed in
4174
+ // below so changing copy never resets the transcript.
4175
+ // eslint-disable-next-line react-hooks/exhaustive-deps
4176
+ [client, emit]
4177
+ );
4178
+ const optionsRef = useRef(options);
4179
+ optionsRef.current = options;
4180
+ machine.setOptions(options);
4181
+ const state = useSyncExternalStore(machine.subscribeState, machine.getState, machine.getState);
4182
+ useEffect(() => subscribe(machine.handleEvent), [subscribe, machine]);
4183
+ useEffect(() => () => machine.dispose(), [machine]);
4184
+ const requestHuman = useCallback(
4185
+ (reason) => machine.requestHuman(reason),
4186
+ [machine]
4187
+ );
4188
+ const sendMessage = useCallback((content) => machine.sendMessage(content), [machine]);
4189
+ const cancelMessage = useCallback(() => machine.cancelMessage(), [machine]);
4190
+ const clearMessages = useCallback(() => machine.clearMessages(), [machine]);
4191
+ const clearError = useCallback(() => machine.clearError(), [machine]);
4192
+ return {
4193
+ messages: state.messages,
4194
+ isLoading: state.isLoading,
4195
+ loadingStatusKey: state.loadingStatusKey,
4196
+ error: state.error,
4197
+ pendingActions: state.pendingActions,
4198
+ escalation: state.escalation,
4199
+ requestHuman,
4200
+ sendMessage,
4201
+ cancelMessage,
4202
+ clearMessages,
4203
+ clearError
4204
+ };
4205
+ }
4206
+ var DEFAULT_PERMISSIONS2 = {
4207
+ canNavigate: true,
4208
+ canFillForms: true,
4209
+ canInteractUI: true,
4210
+ canSubmitForms: true
4211
+ };
4212
+ function useAppilotsActions(options = {}) {
4213
+ const { client, subscribe, config, emit } = useAppilotsContext();
4214
+ const { navigationRef, autoExecute = false } = options;
4215
+ const runnerOptionsRef = useRef({
4216
+ permissions: config.permissions ?? DEFAULT_PERMISSIONS2,
4217
+ emit,
4218
+ navigationRef
4219
+ });
4220
+ runnerOptionsRef.current = {
4221
+ permissions: config.permissions ?? DEFAULT_PERMISSIONS2,
4222
+ emit,
4223
+ navigationRef
4224
+ };
4225
+ const machine = useMemo(
4226
+ () => new ActionQueueMachine(
4227
+ {
4228
+ client,
4229
+ adapter: createReactNativeActionRunner(() => runnerOptionsRef.current),
4230
+ emit,
4231
+ warn: appilotsDebugWarn
4232
+ },
4233
+ { autoExecute }
4234
+ ),
4235
+ // eslint-disable-next-line react-hooks/exhaustive-deps
5406
4236
  [client, emit]
5407
4237
  );
4238
+ machine.setOptions({ autoExecute });
4239
+ const state = useSyncExternalStore(machine.subscribeState, machine.getState, machine.getState);
4240
+ useEffect(() => subscribe(machine.handleEvent), [subscribe, machine]);
4241
+ useEffect(() => {
4242
+ if (autoExecute) machine.scheduleDrain();
4243
+ }, [autoExecute, machine, state.actions]);
4244
+ const approveAction = useCallback(
4245
+ (actionId) => machine.approveAction(actionId),
4246
+ [machine]
4247
+ );
4248
+ const rejectAction = useCallback((actionId) => machine.rejectAction(actionId), [machine]);
5408
4249
  return {
5409
- pendingActions: actions.filter((a) => a.status === "pending"),
5410
- executingActions: actions.filter((a) => a.status === "executing"),
5411
- completedActions: actions.filter((a) => a.status === "completed" || a.status === "failed"),
5412
- approveAction: approveActionImpl,
4250
+ pendingActions: state.actions.filter((a) => a.status === "pending"),
4251
+ executingActions: state.actions.filter((a) => a.status === "executing"),
4252
+ completedActions: state.actions.filter(
4253
+ (a) => a.status === "completed" || a.status === "failed"
4254
+ ),
4255
+ approveAction,
5413
4256
  rejectAction
5414
4257
  };
5415
4258
  }