@appilots/sdk 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-GEM6TPAK.mjs → chunk-42WI3UDB.mjs} +312 -1635
- package/dist/{chunk-KGN3DNGX.js → chunk-CHY5D2PV.js} +5 -5
- package/dist/{chunk-KCDNL4HV.mjs → chunk-GEKCGVJN.mjs} +1 -1
- package/dist/{chunk-HDLZGXNF.mjs → chunk-JBOLV5AM.mjs} +2016 -96
- package/dist/{chunk-SIBKF7AA.js → chunk-V3TUIIT2.js} +2036 -97
- package/dist/{chunk-IASJ7YHC.js → chunk-Y7EA6R2G.js} +412 -1735
- package/dist/hooks/index.d.mts +1 -1
- package/dist/hooks/index.d.ts +1 -1
- package/dist/hooks/index.js +11 -11
- package/dist/hooks/index.mjs +2 -2
- package/dist/{index-BDb1WVNo.d.mts → index-Cb0D6uKH.d.mts} +367 -124
- package/dist/{index-BDb1WVNo.d.ts → index-Cb0D6uKH.d.ts} +367 -124
- package/dist/index.d.mts +109 -341
- package/dist/index.d.ts +109 -341
- package/dist/index.js +84 -462
- package/dist/index.mjs +10 -403
- package/dist/navigation/index.js +13 -13
- package/dist/navigation/index.mjs +2 -2
- package/hooks/package.json +9 -0
- package/metro.d.ts +49 -0
- package/metro.js +0 -37
- package/navigation/package.json +9 -0
- package/package.json +45 -25
|
@@ -14,10 +14,20 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
14
14
|
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
15
15
|
});
|
|
16
16
|
|
|
17
|
-
//
|
|
18
|
-
var
|
|
17
|
+
// ../shared/dist/chunk-43YQ52Q7.mjs
|
|
18
|
+
var SENSITIVE_FIELD_NAME_RE = /(\b|_|-)(password|senha|contrase[ñn]a|mot[\s_-]*de[\s_-]*passe|passwd|pwd|secret|token|api[\s_-]*key|access[\s_-]*key|private[\s_-]*key|cvv|cvc|cvn|cvc2|cid|pin(?:[\s_-]*code)?|otp|one[\s_-]*time[\s_-]*code|2fa|mfa|security[\s_-]*code|c[óo]digo[\s_-]*seguran[çc]a|c[óo]digo[\s_-]*verifica[çc][ãa]o|verification[\s_-]*code)(\b|_|-)/i;
|
|
19
|
+
function isSensitiveFieldName(id, label, placeholder, type) {
|
|
20
|
+
if (type === "password") return true;
|
|
21
|
+
for (const candidate of [id, label, placeholder]) {
|
|
22
|
+
if (typeof candidate === "string" && SENSITIVE_FIELD_NAME_RE.test(candidate)) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
19
28
|
|
|
20
|
-
//
|
|
29
|
+
// ../client-core/dist/index.mjs
|
|
30
|
+
var SDK_VERSION = "0.1.0";
|
|
21
31
|
var StreamTransportError = class extends Error {
|
|
22
32
|
constructor(message) {
|
|
23
33
|
super(message);
|
|
@@ -744,47 +754,2016 @@ var AppilotsClient = class {
|
|
|
744
754
|
typeof event.operatorName === "string" ? event.operatorName : void 0
|
|
745
755
|
);
|
|
746
756
|
}
|
|
747
|
-
}
|
|
748
|
-
};
|
|
749
|
-
const consumeBuffer = () => {
|
|
750
|
-
const text = xhr.responseText ?? "";
|
|
751
|
-
const boundary = text.lastIndexOf("\n\n");
|
|
752
|
-
if (boundary <= parsedIndex) return;
|
|
753
|
-
const slice = text.slice(parsedIndex, boundary);
|
|
754
|
-
parsedIndex = boundary;
|
|
755
|
-
for (const frame of slice.split("\n\n")) {
|
|
756
|
-
for (const line of frame.split("\n")) {
|
|
757
|
-
if (!line.startsWith("data: ")) continue;
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
const consumeBuffer = () => {
|
|
760
|
+
const text = xhr.responseText ?? "";
|
|
761
|
+
const boundary = text.lastIndexOf("\n\n");
|
|
762
|
+
if (boundary <= parsedIndex) return;
|
|
763
|
+
const slice = text.slice(parsedIndex, boundary);
|
|
764
|
+
parsedIndex = boundary;
|
|
765
|
+
for (const frame of slice.split("\n\n")) {
|
|
766
|
+
for (const line of frame.split("\n")) {
|
|
767
|
+
if (!line.startsWith("data: ")) continue;
|
|
768
|
+
try {
|
|
769
|
+
handleEvent(JSON.parse(line.slice(6)));
|
|
770
|
+
} catch {
|
|
771
|
+
this.log("Skipping unparseable escalation SSE line:", line.slice(0, 120));
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
};
|
|
776
|
+
xhr.open("GET", url);
|
|
777
|
+
for (const [key, value] of Object.entries(this.headers)) {
|
|
778
|
+
xhr.setRequestHeader(key, value);
|
|
779
|
+
}
|
|
780
|
+
xhr.setRequestHeader("Accept", "text/event-stream");
|
|
781
|
+
xhr.onprogress = consumeBuffer;
|
|
782
|
+
xhr.onload = () => {
|
|
783
|
+
consumeBuffer();
|
|
784
|
+
end();
|
|
785
|
+
};
|
|
786
|
+
xhr.onerror = end;
|
|
787
|
+
xhr.onabort = end;
|
|
788
|
+
xhr.send();
|
|
789
|
+
return () => {
|
|
790
|
+
closedByCaller = true;
|
|
791
|
+
try {
|
|
792
|
+
xhr.abort();
|
|
793
|
+
} catch {
|
|
794
|
+
}
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
};
|
|
798
|
+
var defaultLightTheme = {
|
|
799
|
+
colors: {
|
|
800
|
+
primary: "#6366f1",
|
|
801
|
+
secondary: "#8b5cf6",
|
|
802
|
+
background: "#ffffff",
|
|
803
|
+
surface: "#f3f4f6",
|
|
804
|
+
text: "#1f2937",
|
|
805
|
+
textSecondary: "#6b7280",
|
|
806
|
+
border: "#e5e7eb",
|
|
807
|
+
// NOTE (BACKLOG 4.2): success/warning/error used to have ZERO visual
|
|
808
|
+
// consumers — nothing read them, so ActionBreadcrumb's tones and the
|
|
809
|
+
// confirm card's destructive accents were hardcoded separately with
|
|
810
|
+
// their OWN hex values (#10B981/#EF4444/#9CA3AF-ish grays). Since no
|
|
811
|
+
// other UI in the SDK reads these three tokens, we picked these
|
|
812
|
+
// defaults to exactly match those pre-existing hardcoded values —
|
|
813
|
+
// wiring the tokens up to ActionBreadcrumb (see breadcrumbPresentation.ts)
|
|
814
|
+
// is now a no-op for anyone on the bundled default theme, while a
|
|
815
|
+
// custom theme's success/warning/error now actually do something.
|
|
816
|
+
success: "#10b981",
|
|
817
|
+
warning: "#9ca3af",
|
|
818
|
+
error: "#ef4444",
|
|
819
|
+
// Same value as `text` — split out so an app can diverge them, but
|
|
820
|
+
// any app that doesn't set these renders pixel-identical to before.
|
|
821
|
+
buttonText: "#1f2937",
|
|
822
|
+
bubbleText: "#1f2937",
|
|
823
|
+
userBubbleText: "#ffffff"
|
|
824
|
+
},
|
|
825
|
+
typography: {
|
|
826
|
+
fontFamily: "System",
|
|
827
|
+
fontSizeXs: 11,
|
|
828
|
+
fontSizeSm: 13,
|
|
829
|
+
fontSizeBase: 15,
|
|
830
|
+
fontSizeLg: 18
|
|
831
|
+
},
|
|
832
|
+
radii: {
|
|
833
|
+
sm: 8,
|
|
834
|
+
md: 12,
|
|
835
|
+
lg: 16
|
|
836
|
+
},
|
|
837
|
+
shadows: {
|
|
838
|
+
appearance: "light"
|
|
839
|
+
}
|
|
840
|
+
};
|
|
841
|
+
var defaultDarkTheme = {
|
|
842
|
+
colors: {
|
|
843
|
+
primary: "#818cf8",
|
|
844
|
+
secondary: "#a78bfa",
|
|
845
|
+
background: "#0f172a",
|
|
846
|
+
surface: "#1e293b",
|
|
847
|
+
text: "#f1f5f9",
|
|
848
|
+
textSecondary: "#94a3b8",
|
|
849
|
+
border: "#334155",
|
|
850
|
+
// Kept IDENTICAL to the light theme's success/warning/error (not the
|
|
851
|
+
// usual "lighter for dark bg" adjustment other tokens get) — the
|
|
852
|
+
// breadcrumb's tones were a fixed, theme-agnostic hardcoded palette
|
|
853
|
+
// before this token existed, rendering the SAME hex in light and dark
|
|
854
|
+
// mode. Diverging them here would change dark-mode breadcrumbs even
|
|
855
|
+
// though nothing in the token tree asked for that.
|
|
856
|
+
success: "#10b981",
|
|
857
|
+
warning: "#9ca3af",
|
|
858
|
+
error: "#ef4444",
|
|
859
|
+
buttonText: "#f1f5f9",
|
|
860
|
+
bubbleText: "#f1f5f9",
|
|
861
|
+
userBubbleText: "#ffffff"
|
|
862
|
+
},
|
|
863
|
+
typography: { ...defaultLightTheme.typography },
|
|
864
|
+
radii: { ...defaultLightTheme.radii },
|
|
865
|
+
shadows: { appearance: "dark" }
|
|
866
|
+
};
|
|
867
|
+
function mergeThemeTokens(base, partial) {
|
|
868
|
+
if (!partial) return base;
|
|
869
|
+
return {
|
|
870
|
+
colors: { ...base.colors, ...partial.colors ?? {} },
|
|
871
|
+
typography: { ...base.typography, ...partial.typography ?? {} },
|
|
872
|
+
radii: { ...base.radii, ...partial.radii ?? {} },
|
|
873
|
+
shadows: { ...base.shadows, ...partial.shadows ?? {} }
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
function resolveBaseTheme(mode, systemScheme) {
|
|
877
|
+
if (mode === "dark") return defaultDarkTheme;
|
|
878
|
+
if (mode === "light") return defaultLightTheme;
|
|
879
|
+
return systemScheme === "dark" ? defaultDarkTheme : defaultLightTheme;
|
|
880
|
+
}
|
|
881
|
+
var LIST_ITEM_TARGET_RE = /^list-\d+-item-\d+$/i;
|
|
882
|
+
var DESTRUCTIVE_LABEL_RE = /\b(cancelar?\w*|cancel(?:ar|led|ation|amento)?|delete\w*|exclu\w*|delet\w*|remov\w*|remove|apag\w*|destroy\w*|unsubscribe\w*)\b/i;
|
|
883
|
+
function isListItemPressTarget(value) {
|
|
884
|
+
if (typeof value !== "string" || !value.trim()) return false;
|
|
885
|
+
return LIST_ITEM_TARGET_RE.test(value.trim());
|
|
886
|
+
}
|
|
887
|
+
function looksDestructiveActionLabel(value) {
|
|
888
|
+
if (typeof value !== "string" || !value.trim()) return false;
|
|
889
|
+
return DESTRUCTIVE_LABEL_RE.test(value.trim());
|
|
890
|
+
}
|
|
891
|
+
function actionPressTargetId(action) {
|
|
892
|
+
if (action.type !== "ui_interaction") return null;
|
|
893
|
+
const payload = action.payload;
|
|
894
|
+
const componentId = payload?.componentId;
|
|
895
|
+
if (typeof componentId === "string" && componentId.trim()) return componentId.trim();
|
|
896
|
+
const targetId = payload?.targetId;
|
|
897
|
+
if (typeof targetId === "string" && targetId.trim()) return targetId.trim();
|
|
898
|
+
return null;
|
|
899
|
+
}
|
|
900
|
+
function turnHasConfirmActions(actions) {
|
|
901
|
+
return actions.some((action) => action.type === "confirm");
|
|
902
|
+
}
|
|
903
|
+
function normalize(value) {
|
|
904
|
+
return value.normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[‘’]/g, "'").toLowerCase();
|
|
905
|
+
}
|
|
906
|
+
function detectLocaleFromUserTexts(texts) {
|
|
907
|
+
for (const raw of texts) {
|
|
908
|
+
const normalized = normalize(raw);
|
|
909
|
+
if (/[ãõáéíóúâêôç]/i.test(raw) || /\b(voce|voces|pra|assinatura|cancela|canela|segunda|certo|quais|tenho|pode|posso)\b/.test(
|
|
910
|
+
normalized
|
|
911
|
+
)) {
|
|
912
|
+
return "pt";
|
|
913
|
+
}
|
|
914
|
+
if (/[áéíóúñ¿¡]/i.test(raw) || /\b(cancela|segunda|suscripcion|puedo|cuales|hola)\b/.test(normalized)) {
|
|
915
|
+
return "es";
|
|
916
|
+
}
|
|
917
|
+
if (/[àâçéèêëîïôûùüÿœ]/i.test(raw) || /\b(annuler|deuxieme|abonnement|bonjour)\b/.test(normalized)) {
|
|
918
|
+
return "fr";
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
return "en";
|
|
922
|
+
}
|
|
923
|
+
var copy = {
|
|
924
|
+
pt: {
|
|
925
|
+
rateLimit: "O provedor de IA atingiu o limite de uso durante a automa\xE7\xE3o. Aguarde um momento e pe\xE7a para continuar, ou aumente o limite de tokens por minuto.",
|
|
926
|
+
invalidInput: "N\xE3o consegui continuar a automa\xE7\xE3o por um problema interno de sincroniza\xE7\xE3o. Pode tentar de novo ou fazer essa parte manualmente?",
|
|
927
|
+
generic: (detail) => `Perdi o fio da automa\xE7\xE3o (${detail}). Pode continuar manualmente ou me pedir de novo?`
|
|
928
|
+
},
|
|
929
|
+
es: {
|
|
930
|
+
rateLimit: "El proveedor de IA alcanz\xF3 su l\xEDmite de uso durante la automatizaci\xF3n. Espera un momento y p\xEDdeme continuar, o aumenta el l\xEDmite de tokens por minuto.",
|
|
931
|
+
invalidInput: "No pude continuar la automatizaci\xF3n por un problema interno de sincronizaci\xF3n. \xBFPuedes intentarlo de nuevo o hacer esta parte manualmente?",
|
|
932
|
+
generic: (detail) => `Perd\xED el hilo de la automatizaci\xF3n (${detail}). \xBFPuedes continuar manualmente o ped\xEDrmelo de nuevo?`
|
|
933
|
+
},
|
|
934
|
+
fr: {
|
|
935
|
+
rateLimit: "Le fournisseur d'IA a atteint sa limite d'utilisation pendant l'automatisation. Attendez un instant et demandez-moi de continuer, ou augmentez la limite de tokens par minute.",
|
|
936
|
+
invalidInput: "Je n'ai pas pu poursuivre l'automatisation \xE0 cause d'un probl\xE8me interne de synchronisation. Pouvez-vous r\xE9essayer ou faire cette \xE9tape manuellement ?",
|
|
937
|
+
generic: (detail) => `J'ai perdu le fil de l'automatisation (${detail}). Vous pouvez continuer manuellement ou me redemander ?`
|
|
938
|
+
},
|
|
939
|
+
en: {
|
|
940
|
+
rateLimit: "Your AI provider hit its rate limit during automation. Wait a moment and ask me to continue, or raise your TPM limit.",
|
|
941
|
+
invalidInput: "I could not continue the automation because of an internal sync issue. You can try again or take over manually.",
|
|
942
|
+
generic: (detail) => `I lost my footing during automation (${detail}). You can take over from here.`
|
|
943
|
+
}
|
|
944
|
+
};
|
|
945
|
+
function buildContinuationFailureMessage(rawError, recentUserTexts) {
|
|
946
|
+
const locale = detectLocaleFromUserTexts(recentUserTexts);
|
|
947
|
+
const strings = copy[locale] ?? copy.en;
|
|
948
|
+
if (/rate limit|tokens per min|tpm/i.test(rawError)) {
|
|
949
|
+
return strings.rateLimit;
|
|
950
|
+
}
|
|
951
|
+
if (/invalid input|validation_error|422/i.test(rawError)) {
|
|
952
|
+
return strings.invalidInput;
|
|
953
|
+
}
|
|
954
|
+
return strings.generic(rawError);
|
|
955
|
+
}
|
|
956
|
+
function metadataMatchesComponentId(metaId, componentId) {
|
|
957
|
+
const norm = (s) => s.toLowerCase().replace(/^el:(button|link|input|toggle|listitem)[:_-]/, "").replace(/^(btn-|btn_|button-|button_|link-|link_)/, "").trim();
|
|
958
|
+
const meta = norm(metaId);
|
|
959
|
+
const comp = norm(componentId);
|
|
960
|
+
if (!meta || !comp) return false;
|
|
961
|
+
return meta === comp;
|
|
962
|
+
}
|
|
963
|
+
function fingerprintAction(action, screen) {
|
|
964
|
+
const p = action.payload ?? {};
|
|
965
|
+
const scope = `@${screen ?? "?"}`;
|
|
966
|
+
switch (action.type) {
|
|
967
|
+
case "navigate":
|
|
968
|
+
return `navigate${scope}:${p.screenName ?? "?"}`;
|
|
969
|
+
case "form_fill": {
|
|
970
|
+
const fields = p.fields ?? [];
|
|
971
|
+
const ids = fields.map((f) => String(f?.fieldId ?? "?")).sort();
|
|
972
|
+
return `form_fill${scope}:${ids.join(",")}`;
|
|
973
|
+
}
|
|
974
|
+
case "ui_interaction":
|
|
975
|
+
return `ui_interaction${scope}:${p.action ?? "?"}:${p.componentId ?? p.targetId ?? "?"}`;
|
|
976
|
+
case "confirm":
|
|
977
|
+
return `confirm${scope}:${p.message ?? p.title ?? "?"}`;
|
|
978
|
+
default:
|
|
979
|
+
return `${action.type}${scope}:${JSON.stringify(p)}`;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
var MAX_TRACE_ENTRIES = 200;
|
|
983
|
+
var entries = [];
|
|
984
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
985
|
+
var traceCounter = 0;
|
|
986
|
+
function notify() {
|
|
987
|
+
const snapshot = [...entries];
|
|
988
|
+
listeners.forEach((listener) => listener(snapshot));
|
|
989
|
+
}
|
|
990
|
+
function append(entry) {
|
|
991
|
+
entries.push(entry);
|
|
992
|
+
if (entries.length > MAX_TRACE_ENTRIES) {
|
|
993
|
+
entries.splice(0, entries.length - MAX_TRACE_ENTRIES);
|
|
994
|
+
}
|
|
995
|
+
notify();
|
|
996
|
+
}
|
|
997
|
+
function recordAppilotsDebugTrace(entry) {
|
|
998
|
+
append({
|
|
999
|
+
...entry,
|
|
1000
|
+
id: entry.id ?? `trace_${++traceCounter}`
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
function actionFromEvent(event) {
|
|
1004
|
+
const action = event.data?.action;
|
|
1005
|
+
if (action && typeof action === "object" && typeof action.id === "string") {
|
|
1006
|
+
return action;
|
|
1007
|
+
}
|
|
1008
|
+
return void 0;
|
|
1009
|
+
}
|
|
1010
|
+
function recordAppilotsEventTrace(event) {
|
|
1011
|
+
if (!event.type.startsWith("agent:action:")) return;
|
|
1012
|
+
const action = actionFromEvent(event);
|
|
1013
|
+
const actionId = action?.id || (typeof event.data?.actionId === "string" ? event.data.actionId : void 0);
|
|
1014
|
+
const actionType = action?.type || (typeof event.data?.actionType === "string" ? event.data.actionType : void 0);
|
|
1015
|
+
recordAppilotsDebugTrace({
|
|
1016
|
+
timestamp: event.timestamp,
|
|
1017
|
+
type: event.type,
|
|
1018
|
+
messageId: action?.messageId,
|
|
1019
|
+
actionId,
|
|
1020
|
+
actionType,
|
|
1021
|
+
status: event.type === "agent:action:start" ? "started" : event.type === "agent:action:complete" ? "completed" : "error",
|
|
1022
|
+
data: event.data
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
function getAppilotsDebugTraces() {
|
|
1026
|
+
return [...entries];
|
|
1027
|
+
}
|
|
1028
|
+
function clearAppilotsDebugTraces() {
|
|
1029
|
+
entries.splice(0, entries.length);
|
|
1030
|
+
notify();
|
|
1031
|
+
}
|
|
1032
|
+
function subscribeAppilotsDebugTraces(listener) {
|
|
1033
|
+
listeners.add(listener);
|
|
1034
|
+
listener([...entries]);
|
|
1035
|
+
return () => {
|
|
1036
|
+
listeners.delete(listener);
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
var INTERNAL_MARKER_RE = /^\[(?:Permission policy notice|Recovery exhausted|User rejected the proposed action)[^\]]*\]:\s*/i;
|
|
1040
|
+
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;
|
|
1041
|
+
var TECHNICAL_SELECTOR_RE = /\b(?:el:[^\s]+|list-\d+-item-\d+|[A-Za-z][\w-]*:(?!\/\/)[^\s]+|(?:testID|test-id|accessibilityId|accessibility-id)[:=_-][^\s]+)/i;
|
|
1042
|
+
function sanitizeVisibleAssistantContent(content) {
|
|
1043
|
+
return content.split("\n").map((line) => line.replace(INTERNAL_MARKER_RE, "").trimEnd()).filter((line) => !INTERNAL_MARKER_RE.test(line)).join("\n").trim();
|
|
1044
|
+
}
|
|
1045
|
+
function humanizeAssistantFallback(content) {
|
|
1046
|
+
let text = sanitizeVisibleAssistantContent(content).replace(/\blist-\d+-item-\d+\b/gi, "o item da lista").replace(/\bel:[^\s".]+/gi, "o elemento").replace(
|
|
1047
|
+
/"\s*(?:press|click|tap|navigate|form_fill|ui_interaction|scroll_list)\s+on\s+[^"]+"/gi,
|
|
1048
|
+
'"a a\xE7\xE3o solicitada"'
|
|
1049
|
+
).replace(/\bsubscription-cancel-[^\s"]+/gi, "cancelamento de assinatura").trim();
|
|
1050
|
+
if (text.length < 16) return null;
|
|
1051
|
+
const probe = {
|
|
1052
|
+
role: "assistant",
|
|
1053
|
+
content: text
|
|
1054
|
+
};
|
|
1055
|
+
if (isInternalTraceMessage(probe)) return null;
|
|
1056
|
+
return text;
|
|
1057
|
+
}
|
|
1058
|
+
function isInternalTraceMessage(message) {
|
|
1059
|
+
if (message.role !== "assistant") return false;
|
|
1060
|
+
const content = message.content.trim();
|
|
1061
|
+
if (!content) return true;
|
|
1062
|
+
return TECHNICAL_TRACE_RE.test(content) || TECHNICAL_SELECTOR_RE.test(content);
|
|
1063
|
+
}
|
|
1064
|
+
var CREATE_FORM_ROUTE_RE = /vehiclecreate|subscriptioncreate|vehicleedit|subscriptionedit|maintenancecreate|create|cadastr|register|novo|edit/i;
|
|
1065
|
+
var SUBMIT_BUTTON_RE = /cadastrar|registrar|salvar|submit|register|guardar|enviar|confirmar|save/i;
|
|
1066
|
+
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;
|
|
1067
|
+
function normalize2(value) {
|
|
1068
|
+
return value.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
|
|
1069
|
+
}
|
|
1070
|
+
function routeHintsCreateForm(routeNames) {
|
|
1071
|
+
return routeNames.some((name) => CREATE_FORM_ROUTE_RE.test(normalize2(name)));
|
|
1072
|
+
}
|
|
1073
|
+
function snapshotHasSubmitControl(snap) {
|
|
1074
|
+
return (snap.buttons ?? []).some(
|
|
1075
|
+
(btn) => SUBMIT_BUTTON_RE.test(normalize2(`${btn.label ?? btn.id ?? ""}`))
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
function snapshotHasVisibleSubmitButton(snap) {
|
|
1079
|
+
return (snap.buttons ?? []).some(
|
|
1080
|
+
(btn) => !btn.disabled && SUBMIT_BUTTON_RE.test(normalize2(`${btn.label ?? btn.id ?? ""}`))
|
|
1081
|
+
);
|
|
1082
|
+
}
|
|
1083
|
+
function snapshotHasVisibleValidation(snap) {
|
|
1084
|
+
return (snap.texts ?? []).some((text) => typeof text === "string" && FORM_VALIDATION_TEXT_RE.test(text));
|
|
1085
|
+
}
|
|
1086
|
+
function snapshotShowsOpenCreateForm(snap, activePath = []) {
|
|
1087
|
+
if (!snap) return false;
|
|
1088
|
+
const routeNames = [
|
|
1089
|
+
...typeof snap.route === "string" && snap.route ? [snap.route] : [],
|
|
1090
|
+
...activePath
|
|
1091
|
+
];
|
|
1092
|
+
const hasFormInputs = (snap.inputs?.length ?? 0) > 0;
|
|
1093
|
+
const hasValidation = snapshotHasVisibleValidation(snap);
|
|
1094
|
+
const hasFormContext = routeHintsCreateForm(routeNames) || hasFormInputs || hasValidation;
|
|
1095
|
+
if (!hasFormContext) return false;
|
|
1096
|
+
if (snap.loading === true) return true;
|
|
1097
|
+
if (snapshotHasVisibleSubmitButton(snap)) return true;
|
|
1098
|
+
if (snapshotHasSubmitControl(snap) && hasFormInputs) return true;
|
|
1099
|
+
if (hasValidation) return true;
|
|
1100
|
+
return false;
|
|
1101
|
+
}
|
|
1102
|
+
function formFillLackedWriteEffect(action, snap, activePath = []) {
|
|
1103
|
+
if (action.type !== "form_fill") return false;
|
|
1104
|
+
if (!snapshotShowsOpenCreateForm(snap, activePath)) return false;
|
|
1105
|
+
const payload = action.payload;
|
|
1106
|
+
if (payload?.submitAfterFill === true) return true;
|
|
1107
|
+
return Array.isArray(payload?.fields) && payload.fields.length > 0;
|
|
1108
|
+
}
|
|
1109
|
+
function summarizeAction(action) {
|
|
1110
|
+
const p = action.payload;
|
|
1111
|
+
switch (action.type) {
|
|
1112
|
+
case "navigate":
|
|
1113
|
+
return `navigate to ${p?.screenName ?? "?"}`;
|
|
1114
|
+
case "form_fill": {
|
|
1115
|
+
const fields = p?.fields ?? [];
|
|
1116
|
+
if (fields.length === 0) return "form_fill (no fields)";
|
|
1117
|
+
const parts = fields.slice(0, 3).map((f) => `${f.fieldId}=${JSON.stringify(f.value)}`);
|
|
1118
|
+
const extra = fields.length > 3 ? ` +${fields.length - 3} more` : "";
|
|
1119
|
+
return `form_fill ${parts.join(", ")}${extra}`;
|
|
1120
|
+
}
|
|
1121
|
+
case "ui_interaction":
|
|
1122
|
+
return `${p?.action ?? "press"} on ${p?.componentId ?? "?"}`;
|
|
1123
|
+
case "scroll_list": {
|
|
1124
|
+
const target = typeof p?.toIndex === "number" ? `to item #${p.toIndex}` : p?.direction ?? "down";
|
|
1125
|
+
return `scroll_list ${p?.listId ? `"${p.listId}" ` : ""}${target}`;
|
|
1126
|
+
}
|
|
1127
|
+
case "confirm":
|
|
1128
|
+
return `confirm: ${p?.message ?? p?.title ?? ""}`;
|
|
1129
|
+
default:
|
|
1130
|
+
return action.type;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
function extractPressedComponentId(actions) {
|
|
1134
|
+
for (let i = actions.length - 1; i >= 0; i--) {
|
|
1135
|
+
const a = actions[i];
|
|
1136
|
+
if (!a || a.type !== "ui_interaction") continue;
|
|
1137
|
+
const p = a.payload;
|
|
1138
|
+
const interaction = p?.action ?? "press";
|
|
1139
|
+
if (interaction !== "press" && interaction !== "longPress") continue;
|
|
1140
|
+
const id = p?.componentId;
|
|
1141
|
+
if (typeof id === "string" && id.length > 0) return id;
|
|
1142
|
+
}
|
|
1143
|
+
return null;
|
|
1144
|
+
}
|
|
1145
|
+
function extractNavigateTargetScreen(actions) {
|
|
1146
|
+
for (let i = actions.length - 1; i >= 0; i--) {
|
|
1147
|
+
const a = actions[i];
|
|
1148
|
+
if (!a || a.type !== "navigate") continue;
|
|
1149
|
+
const screenName = a.payload?.screenName;
|
|
1150
|
+
if (typeof screenName === "string" && screenName.length > 0) return screenName;
|
|
1151
|
+
}
|
|
1152
|
+
return null;
|
|
1153
|
+
}
|
|
1154
|
+
function idLooselyMatches(candidate, target) {
|
|
1155
|
+
if (!candidate || !target) return false;
|
|
1156
|
+
if (candidate === target) return true;
|
|
1157
|
+
const c = candidate.toLowerCase();
|
|
1158
|
+
const t = target.toLowerCase();
|
|
1159
|
+
if (c === t) return true;
|
|
1160
|
+
return c.endsWith(`-${t}`) || c.endsWith(`_${t}`) || t.endsWith(`-${c}`) || t.endsWith(`_${c}`);
|
|
1161
|
+
}
|
|
1162
|
+
var ESCALATION_RECONNECT_MAX_MS = 3e4;
|
|
1163
|
+
var MAX_RECOVERY_ATTEMPTS_PER_ACTION = 2;
|
|
1164
|
+
var INITIAL_STATE = {
|
|
1165
|
+
messages: [],
|
|
1166
|
+
isLoading: false,
|
|
1167
|
+
loadingStatusKey: "thinking",
|
|
1168
|
+
error: null,
|
|
1169
|
+
pendingActions: [],
|
|
1170
|
+
escalation: null
|
|
1171
|
+
};
|
|
1172
|
+
var ChatSessionMachine = class {
|
|
1173
|
+
deps;
|
|
1174
|
+
options = {};
|
|
1175
|
+
state = INITIAL_STATE;
|
|
1176
|
+
listeners = /* @__PURE__ */ new Set();
|
|
1177
|
+
messageIdCounter = 0;
|
|
1178
|
+
currentTurn = null;
|
|
1179
|
+
continuationInFlight = false;
|
|
1180
|
+
/**
|
|
1181
|
+
* BACKLOG 3.2 — per-message-thread retry counter, keyed on action
|
|
1182
|
+
* fingerprint. Reset on every fresh `sendMessage`, persists across
|
|
1183
|
+
* /agent/continue hops within the same user turn so two retries of
|
|
1184
|
+
* the same action add up across hops.
|
|
1185
|
+
*/
|
|
1186
|
+
recoveryCountByFingerprint = /* @__PURE__ */ new Map();
|
|
1187
|
+
/** Abort controller for the in-flight generation (cancelMessage). */
|
|
1188
|
+
abortController = null;
|
|
1189
|
+
// ── Escalation channel bookkeeping (docs/human-escalation.md) ──
|
|
1190
|
+
escalationStreamClose = null;
|
|
1191
|
+
escalationReconnectAttempt = 0;
|
|
1192
|
+
escalationReconnectTimer = null;
|
|
1193
|
+
/** createdAt of the newest operator message — reconnect catch-up cursor. */
|
|
1194
|
+
escalationSince = void 0;
|
|
1195
|
+
/** Operator message ids already appended (SSE replay dedupe). */
|
|
1196
|
+
seenHumanMessageIds = /* @__PURE__ */ new Set();
|
|
1197
|
+
disposed = false;
|
|
1198
|
+
constructor(deps, options = {}) {
|
|
1199
|
+
this.deps = deps;
|
|
1200
|
+
this.options = options;
|
|
1201
|
+
this.subscribeState = this.subscribeState.bind(this);
|
|
1202
|
+
this.getState = this.getState.bind(this);
|
|
1203
|
+
this.handleEvent = this.handleEvent.bind(this);
|
|
1204
|
+
this.sendMessage = this.sendMessage.bind(this);
|
|
1205
|
+
this.cancelMessage = this.cancelMessage.bind(this);
|
|
1206
|
+
this.clearMessages = this.clearMessages.bind(this);
|
|
1207
|
+
this.clearError = this.clearError.bind(this);
|
|
1208
|
+
this.requestHuman = this.requestHuman.bind(this);
|
|
1209
|
+
}
|
|
1210
|
+
// ── State plumbing ───────────────────────────────────────────
|
|
1211
|
+
getState() {
|
|
1212
|
+
return this.state;
|
|
1213
|
+
}
|
|
1214
|
+
subscribeState(listener) {
|
|
1215
|
+
this.listeners.add(listener);
|
|
1216
|
+
return () => {
|
|
1217
|
+
this.listeners.delete(listener);
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
/** Update mutable options (errorPrefix, streaming, escalation copy). */
|
|
1221
|
+
setOptions(options) {
|
|
1222
|
+
this.options = options;
|
|
1223
|
+
}
|
|
1224
|
+
/** Tear down timers and the escalation stream. Safe to call twice. */
|
|
1225
|
+
dispose() {
|
|
1226
|
+
this.disposed = true;
|
|
1227
|
+
this.closeEscalationStream();
|
|
1228
|
+
}
|
|
1229
|
+
setState(partial) {
|
|
1230
|
+
this.state = { ...this.state, ...partial };
|
|
1231
|
+
for (const listener of this.listeners) listener();
|
|
1232
|
+
}
|
|
1233
|
+
warn(message, ...args) {
|
|
1234
|
+
(this.deps.warn ?? console.warn)(message, ...args);
|
|
1235
|
+
}
|
|
1236
|
+
// ── Message helpers ──────────────────────────────────────────
|
|
1237
|
+
addAssistantMessage(message) {
|
|
1238
|
+
const visibleContent = message.role === "assistant" ? sanitizeVisibleAssistantContent(message.content) : message.content;
|
|
1239
|
+
const candidate = { ...message, content: visibleContent };
|
|
1240
|
+
if (isInternalTraceMessage(candidate)) {
|
|
1241
|
+
recordAppilotsDebugTrace({
|
|
1242
|
+
type: "agent:trace",
|
|
1243
|
+
timestamp: Date.now(),
|
|
1244
|
+
messageId: candidate.id,
|
|
1245
|
+
summary: "Suppressed internal assistant trace from visible chat",
|
|
1246
|
+
data: { role: candidate.role, content: message.content }
|
|
1247
|
+
});
|
|
1248
|
+
const fallback = humanizeAssistantFallback(message.content);
|
|
1249
|
+
if (fallback) {
|
|
1250
|
+
this.setState({ messages: [...this.state.messages, { ...candidate, content: fallback }] });
|
|
1251
|
+
}
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
this.setState({ messages: [...this.state.messages, candidate] });
|
|
1255
|
+
}
|
|
1256
|
+
appendLocalSystemMessage(content, metadata) {
|
|
1257
|
+
if (!content) return;
|
|
1258
|
+
this.setState({
|
|
1259
|
+
messages: [
|
|
1260
|
+
...this.state.messages,
|
|
1261
|
+
{
|
|
1262
|
+
id: `system_${++this.messageIdCounter}`,
|
|
1263
|
+
role: "system",
|
|
1264
|
+
content,
|
|
1265
|
+
timestamp: Date.now(),
|
|
1266
|
+
...metadata ? { metadata } : {}
|
|
1267
|
+
}
|
|
1268
|
+
]
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
// ── Escalation: request, stream, and message routing ─────────
|
|
1272
|
+
async requestHuman(reason = "user_requested") {
|
|
1273
|
+
if (this.state.escalation) return;
|
|
1274
|
+
try {
|
|
1275
|
+
const lastError = this.state.messages.slice().reverse().find((m) => m.role === "assistant" && m.id.startsWith("error_"));
|
|
1276
|
+
const result = await this.deps.client.requestEscalation(reason, lastError?.content);
|
|
1277
|
+
this.setState({ escalation: { id: result.escalationId, status: result.status } });
|
|
1278
|
+
this.escalationReconnectAttempt = 0;
|
|
1279
|
+
this.appendLocalSystemMessage(this.options.escalationStrings?.requested ?? "");
|
|
1280
|
+
this.deps.emit({
|
|
1281
|
+
type: "chat:escalation:start",
|
|
1282
|
+
timestamp: Date.now(),
|
|
1283
|
+
data: { escalationId: result.escalationId, reason }
|
|
1284
|
+
});
|
|
1285
|
+
this.openEscalationStream(result.escalationId);
|
|
1286
|
+
} catch (err) {
|
|
1287
|
+
this.setState({ error: err instanceof Error ? err.message : "Failed to request a human" });
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
/**
|
|
1291
|
+
* Live channel: operator replies + status changes. Reopens with
|
|
1292
|
+
* capped exponential backoff (and a `since` catch-up cursor) when
|
|
1293
|
+
* the transport drops, for as long as the escalation stays open.
|
|
1294
|
+
*/
|
|
1295
|
+
openEscalationStream(escalationId) {
|
|
1296
|
+
this.closeEscalationStream();
|
|
1297
|
+
if (this.disposed) return;
|
|
1298
|
+
this.escalationStreamClose = this.deps.client.openEscalationStream(escalationId, {
|
|
1299
|
+
since: this.escalationSince,
|
|
1300
|
+
onHumanMessage: (message) => {
|
|
1301
|
+
this.escalationReconnectAttempt = 0;
|
|
1302
|
+
if (this.seenHumanMessageIds.has(message.id)) return;
|
|
1303
|
+
this.seenHumanMessageIds.add(message.id);
|
|
1304
|
+
this.escalationSince = message.createdAt;
|
|
1305
|
+
this.setState({
|
|
1306
|
+
messages: [
|
|
1307
|
+
...this.state.messages,
|
|
1308
|
+
{
|
|
1309
|
+
id: message.id,
|
|
1310
|
+
role: "human_agent",
|
|
1311
|
+
content: message.content,
|
|
1312
|
+
timestamp: Date.now()
|
|
1313
|
+
}
|
|
1314
|
+
]
|
|
1315
|
+
});
|
|
1316
|
+
this.deps.emit({
|
|
1317
|
+
type: "chat:escalation:message",
|
|
1318
|
+
timestamp: Date.now(),
|
|
1319
|
+
data: { escalationId, messageId: message.id }
|
|
1320
|
+
});
|
|
1321
|
+
},
|
|
1322
|
+
onStatus: (status, operatorName) => {
|
|
1323
|
+
this.escalationReconnectAttempt = 0;
|
|
1324
|
+
const current = this.state.escalation;
|
|
1325
|
+
if (status === "active") {
|
|
1326
|
+
if (!current || current.id !== escalationId) return;
|
|
1327
|
+
if (current.status === "active") return;
|
|
1328
|
+
this.appendLocalSystemMessage(this.options.escalationStrings?.connected ?? "");
|
|
1329
|
+
this.setState({ escalation: { ...current, status: "active", operatorName } });
|
|
1330
|
+
} else if (status === "resolved") {
|
|
1331
|
+
this.appendLocalSystemMessage(this.options.escalationStrings?.resolved ?? "");
|
|
1332
|
+
this.setState({ escalation: null });
|
|
1333
|
+
this.closeEscalationStream();
|
|
1334
|
+
this.deps.emit({
|
|
1335
|
+
type: "chat:escalation:end",
|
|
1336
|
+
timestamp: Date.now(),
|
|
1337
|
+
data: { escalationId }
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
},
|
|
1341
|
+
onClose: () => {
|
|
1342
|
+
const current = this.state.escalation;
|
|
1343
|
+
if (this.disposed || !current || current.id !== escalationId) return;
|
|
1344
|
+
const attempt = ++this.escalationReconnectAttempt;
|
|
1345
|
+
const delay = Math.min(ESCALATION_RECONNECT_MAX_MS, 1e3 * 2 ** Math.min(attempt, 5));
|
|
1346
|
+
this.escalationReconnectTimer = setTimeout(() => {
|
|
1347
|
+
const still = this.state.escalation;
|
|
1348
|
+
if (this.disposed || !still || still.id !== escalationId) return;
|
|
1349
|
+
this.openEscalationStream(escalationId);
|
|
1350
|
+
}, delay);
|
|
1351
|
+
}
|
|
1352
|
+
});
|
|
1353
|
+
}
|
|
1354
|
+
closeEscalationStream() {
|
|
1355
|
+
if (this.escalationReconnectTimer) {
|
|
1356
|
+
clearTimeout(this.escalationReconnectTimer);
|
|
1357
|
+
this.escalationReconnectTimer = null;
|
|
1358
|
+
}
|
|
1359
|
+
if (this.escalationStreamClose) {
|
|
1360
|
+
const close = this.escalationStreamClose;
|
|
1361
|
+
this.escalationStreamClose = null;
|
|
1362
|
+
close();
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
/** Route a user message to the human channel while escalated. */
|
|
1366
|
+
async sendEscalationMessage(content) {
|
|
1367
|
+
const current = this.state.escalation;
|
|
1368
|
+
if (!current) return;
|
|
1369
|
+
const userMessage = {
|
|
1370
|
+
id: `local_${++this.messageIdCounter}`,
|
|
1371
|
+
role: "user",
|
|
1372
|
+
content,
|
|
1373
|
+
timestamp: Date.now()
|
|
1374
|
+
};
|
|
1375
|
+
this.setState({ messages: [...this.state.messages, userMessage] });
|
|
1376
|
+
try {
|
|
1377
|
+
await this.deps.client.sendEscalationMessage(current.id, content);
|
|
1378
|
+
} catch (err) {
|
|
1379
|
+
this.setState({ error: err instanceof Error ? err.message : "Failed to send message" });
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
// ── Start a turn (or sub-turn): register actions + emit events ──
|
|
1383
|
+
beginTurn(assistantMessageId, actions, hop) {
|
|
1384
|
+
if (!actions || actions.length === 0) {
|
|
1385
|
+
this.currentTurn = null;
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
const stamped = actions.map((a) => ({ ...a, messageId: assistantMessageId }));
|
|
1389
|
+
this.setState({ pendingActions: [...this.state.pendingActions, ...stamped] });
|
|
1390
|
+
const turn = {
|
|
1391
|
+
messageId: assistantMessageId,
|
|
1392
|
+
actionIds: new Set(stamped.map((a) => a.id)),
|
|
1393
|
+
results: /* @__PURE__ */ new Map(),
|
|
1394
|
+
reportedIds: /* @__PURE__ */ new Set(),
|
|
1395
|
+
hop,
|
|
1396
|
+
hadNavigate: stamped.some((a) => a.type === "navigate"),
|
|
1397
|
+
// Snapshot the current screen NOW, before any navigate
|
|
1398
|
+
// dispatches. The platform settle will use this as the
|
|
1399
|
+
// baseline to detect that the navigate completed.
|
|
1400
|
+
preNavigateScreen: this.deps.adapter.getCurrentScreen(),
|
|
1401
|
+
// Full route+params signature — lets settle detect navigate
|
|
1402
|
+
// to the same route name with different params.
|
|
1403
|
+
preNavigateSignature: this.deps.adapter.getCurrentScreenSignature()
|
|
1404
|
+
};
|
|
1405
|
+
const originScreen = this.deps.adapter.getCurrentScreen() ?? void 0;
|
|
1406
|
+
for (const a of stamped) {
|
|
1407
|
+
turn.results.set(a.id, {
|
|
1408
|
+
type: a.type,
|
|
1409
|
+
success: false,
|
|
1410
|
+
summary: summarizeAction(a),
|
|
1411
|
+
fingerprint: fingerprintAction(a, originScreen)
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
1414
|
+
this.currentTurn = turn;
|
|
1415
|
+
for (const action of stamped) {
|
|
1416
|
+
this.deps.emit({
|
|
1417
|
+
type: "agent:action:start",
|
|
1418
|
+
timestamp: Date.now(),
|
|
1419
|
+
data: { action }
|
|
1420
|
+
});
|
|
1421
|
+
}
|
|
1422
|
+
if (turnHasConfirmActions(stamped)) {
|
|
1423
|
+
this.setState({ isLoading: false, loadingStatusKey: "thinking" });
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
registerSynthesizedTurnAction(action) {
|
|
1427
|
+
const turn = this.currentTurn;
|
|
1428
|
+
if (!turn || turn.actionIds.has(action.id)) return;
|
|
1429
|
+
const stamped = { ...action, messageId: turn.messageId };
|
|
1430
|
+
turn.actionIds.add(stamped.id);
|
|
1431
|
+
turn.results.set(stamped.id, {
|
|
1432
|
+
type: stamped.type,
|
|
1433
|
+
success: false,
|
|
1434
|
+
summary: summarizeAction(stamped),
|
|
1435
|
+
fingerprint: fingerprintAction(stamped, this.deps.adapter.getCurrentScreen() ?? void 0)
|
|
1436
|
+
});
|
|
1437
|
+
if (!this.state.pendingActions.some((candidate) => candidate.id === stamped.id)) {
|
|
1438
|
+
this.setState({ pendingActions: [...this.state.pendingActions, stamped] });
|
|
1439
|
+
}
|
|
1440
|
+
if (stamped.type === "confirm") {
|
|
1441
|
+
this.setState({ isLoading: false, loadingStatusKey: "thinking" });
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
// ── Continuation: when all current-turn actions finish, ask the
|
|
1445
|
+
// model what to do next, given the outcomes + fresh snapshot.
|
|
1446
|
+
async runContinuation(turn) {
|
|
1447
|
+
if (this.continuationInFlight) return;
|
|
1448
|
+
this.continuationInFlight = true;
|
|
1449
|
+
try {
|
|
1450
|
+
this.setState({ isLoading: true, loadingStatusKey: "statusWaitingApp" });
|
|
1451
|
+
const toolWithoutEffectIds = /* @__PURE__ */ new Set();
|
|
1452
|
+
const turnActions = this.state.pendingActions.filter(
|
|
1453
|
+
(a) => turn.actionIds.has(a.id)
|
|
1454
|
+
);
|
|
1455
|
+
const pressedId = extractPressedComponentId(turnActions);
|
|
1456
|
+
const navigateTargetScreen = extractNavigateTargetScreen(turnActions);
|
|
1457
|
+
const settle = await this.deps.adapter.settleTurn({
|
|
1458
|
+
hadNavigate: turn.hadNavigate,
|
|
1459
|
+
preNavigateScreen: turn.preNavigateScreen,
|
|
1460
|
+
preNavigateSignature: turn.preNavigateSignature,
|
|
1461
|
+
turnActions,
|
|
1462
|
+
results: Array.from(turn.results.entries()).map(([actionId, r]) => ({
|
|
1463
|
+
actionId,
|
|
1464
|
+
type: r.type,
|
|
1465
|
+
success: r.success
|
|
1466
|
+
})),
|
|
1467
|
+
pressedComponentId: pressedId,
|
|
1468
|
+
navigateTargetScreen
|
|
1469
|
+
});
|
|
1470
|
+
const loadingPending = settle.loadingPending;
|
|
1471
|
+
for (const id of settle.toolWithoutEffectIds) toolWithoutEffectIds.add(id);
|
|
1472
|
+
const context = this.deps.adapter.buildContext({ loadingPending });
|
|
1473
|
+
const snap = context.snapshot;
|
|
1474
|
+
const activePath = context.navigationState?.activePath ?? [];
|
|
1475
|
+
for (const action of turnActions) {
|
|
1476
|
+
const result = turn.results.get(action.id);
|
|
1477
|
+
if (result?.success && formFillLackedWriteEffect(action, snap, activePath)) {
|
|
1478
|
+
toolWithoutEffectIds.add(action.id);
|
|
1479
|
+
this.warn(
|
|
1480
|
+
`tool-without-effect: form_fill succeeded but create form is still open (actionId="${action.id}")`
|
|
1481
|
+
);
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
const results = Array.from(turn.actionIds).map((actionId) => {
|
|
1485
|
+
const r = turn.results.get(actionId);
|
|
1486
|
+
if (!r) {
|
|
1487
|
+
return {
|
|
1488
|
+
actionId,
|
|
1489
|
+
type: "unknown",
|
|
1490
|
+
success: false,
|
|
1491
|
+
error: "no result reported",
|
|
1492
|
+
summary: actionId,
|
|
1493
|
+
diagnose: void 0,
|
|
1494
|
+
recoveryAttempt: 0,
|
|
1495
|
+
recoveryExhausted: false,
|
|
1496
|
+
toolWithoutEffect: false,
|
|
1497
|
+
effect: "unknown"
|
|
1498
|
+
};
|
|
1499
|
+
}
|
|
1500
|
+
const hasToolWithoutEffect = toolWithoutEffectIds.has(actionId);
|
|
1501
|
+
let recoveryAttempt = 0;
|
|
1502
|
+
let recoveryExhausted = false;
|
|
1503
|
+
if (!r.success || hasToolWithoutEffect) {
|
|
1504
|
+
const previous = this.recoveryCountByFingerprint.get(r.fingerprint) ?? 0;
|
|
1505
|
+
const next = previous + 1;
|
|
1506
|
+
this.recoveryCountByFingerprint.set(r.fingerprint, next);
|
|
1507
|
+
recoveryAttempt = next;
|
|
1508
|
+
recoveryExhausted = next >= MAX_RECOVERY_ATTEMPTS_PER_ACTION;
|
|
1509
|
+
}
|
|
1510
|
+
return {
|
|
1511
|
+
actionId,
|
|
1512
|
+
type: r.type,
|
|
1513
|
+
success: r.success,
|
|
1514
|
+
error: r.error,
|
|
1515
|
+
summary: r.summary,
|
|
1516
|
+
diagnose: r.diagnose,
|
|
1517
|
+
recoveryAttempt,
|
|
1518
|
+
recoveryExhausted,
|
|
1519
|
+
toolWithoutEffect: hasToolWithoutEffect,
|
|
1520
|
+
effect: hasToolWithoutEffect ? "none" : r.effect,
|
|
1521
|
+
userVisibleStatusKey: r.userVisibleStatusKey
|
|
1522
|
+
};
|
|
1523
|
+
});
|
|
1524
|
+
const exhausted = results.filter((r) => r.recoveryExhausted);
|
|
1525
|
+
this.setState({
|
|
1526
|
+
loadingStatusKey: results.some((r) => !r.success || r.toolWithoutEffect) ? "statusAdjusting" : "statusAnalyzing"
|
|
1527
|
+
});
|
|
1528
|
+
console.log(
|
|
1529
|
+
`[Appilots] ChatSessionMachine: running continuation hop=${turn.hop} (${results.filter((r) => r.success).length}/${results.length} succeeded` + (exhausted.length > 0 ? `, ${exhausted.length} recovery-exhausted` : "") + ")"
|
|
1530
|
+
);
|
|
1531
|
+
const response = await this.deps.client.continueAgent(results, context, turn.hop);
|
|
1532
|
+
this.addAssistantMessage(response.message);
|
|
1533
|
+
this.deps.emit({
|
|
1534
|
+
type: "agent:message",
|
|
1535
|
+
timestamp: Date.now(),
|
|
1536
|
+
data: { message: response.message, usage: response.usage }
|
|
1537
|
+
});
|
|
1538
|
+
if (response.isDone || !response.actions || response.actions.length === 0) {
|
|
1539
|
+
this.currentTurn = null;
|
|
1540
|
+
this.setState({ isLoading: false, loadingStatusKey: "thinking" });
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
this.beginTurn(response.message.id, response.actions, response.hop);
|
|
1544
|
+
} catch (err) {
|
|
1545
|
+
const raw = err instanceof Error ? err.message : "Continuation failed";
|
|
1546
|
+
this.warn("ChatSessionMachine: continuation failed \u2014", raw);
|
|
1547
|
+
const recentUserTexts = this.state.messages.filter((m) => m.role === "user").slice(-4).map((m) => m.content);
|
|
1548
|
+
const friendly = buildContinuationFailureMessage(raw, recentUserTexts);
|
|
1549
|
+
this.addAssistantMessage({
|
|
1550
|
+
id: `error_${++this.messageIdCounter}`,
|
|
1551
|
+
role: "assistant",
|
|
1552
|
+
content: friendly,
|
|
1553
|
+
timestamp: Date.now()
|
|
1554
|
+
});
|
|
1555
|
+
if (!this.state.escalation && this.options.escalationStrings?.offer) {
|
|
1556
|
+
this.appendLocalSystemMessage(this.options.escalationStrings.offer, {
|
|
1557
|
+
escalationOffer: true
|
|
1558
|
+
});
|
|
1559
|
+
}
|
|
1560
|
+
if (turn) {
|
|
1561
|
+
this.setState({
|
|
1562
|
+
pendingActions: this.state.pendingActions.map(
|
|
1563
|
+
(a) => turn.actionIds.has(a.id) && (a.status === "pending" || a.status === "executing") ? { ...a, status: "failed", error: friendly } : a
|
|
1564
|
+
)
|
|
1565
|
+
});
|
|
1566
|
+
for (const actionId of turn.actionIds) {
|
|
1567
|
+
if (turn.reportedIds.has(actionId)) continue;
|
|
1568
|
+
this.deps.emit({
|
|
1569
|
+
type: "agent:action:error",
|
|
1570
|
+
timestamp: Date.now(),
|
|
1571
|
+
data: { actionId, error: friendly, abandoned: true }
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
this.currentTurn = null;
|
|
1576
|
+
this.setState({ isLoading: false, loadingStatusKey: "thinking" });
|
|
1577
|
+
} finally {
|
|
1578
|
+
this.continuationInFlight = false;
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
// ── Event intake: action lifecycle events drive the loop ─────
|
|
1582
|
+
handleEvent(event) {
|
|
1583
|
+
if (event.type === "agent:action:start") {
|
|
1584
|
+
const action = event.data?.action;
|
|
1585
|
+
if (!action?.id || action.type !== "confirm") return;
|
|
1586
|
+
const injectedBy = action.payload?.injectedBy;
|
|
1587
|
+
if (injectedBy !== "sdk-local") return;
|
|
1588
|
+
this.registerSynthesizedTurnAction(action);
|
|
1589
|
+
return;
|
|
1590
|
+
}
|
|
1591
|
+
if (event.type !== "agent:action:complete" && event.type !== "agent:action:error") return;
|
|
1592
|
+
const turn = this.currentTurn;
|
|
1593
|
+
if (!turn) return;
|
|
1594
|
+
const actionId = event.data?.actionId;
|
|
1595
|
+
if (!actionId || !turn.actionIds.has(actionId)) return;
|
|
1596
|
+
if (turn.reportedIds.has(actionId)) return;
|
|
1597
|
+
const success = event.type === "agent:action:complete";
|
|
1598
|
+
const errorMsg = event.data?.error || void 0;
|
|
1599
|
+
const diagnose = event.data?.diagnose;
|
|
1600
|
+
const effect = event.data?.effect;
|
|
1601
|
+
const userVisibleStatusKey = event.data?.userVisibleStatusKey;
|
|
1602
|
+
const existing = turn.results.get(actionId);
|
|
1603
|
+
if (existing) {
|
|
1604
|
+
existing.success = success;
|
|
1605
|
+
existing.error = errorMsg;
|
|
1606
|
+
existing.diagnose = diagnose;
|
|
1607
|
+
existing.effect = effect;
|
|
1608
|
+
existing.userVisibleStatusKey = userVisibleStatusKey;
|
|
1609
|
+
}
|
|
1610
|
+
turn.reportedIds.add(actionId);
|
|
1611
|
+
if (turn.reportedIds.size >= turn.actionIds.size) {
|
|
1612
|
+
void this.runContinuation(turn);
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
// ── Send / cancel / clear ────────────────────────────────────
|
|
1616
|
+
async sendMessage(content) {
|
|
1617
|
+
if (!content.trim() || this.state.isLoading) return;
|
|
1618
|
+
if (this.state.escalation) {
|
|
1619
|
+
await this.sendEscalationMessage(content.trim());
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1622
|
+
const errorPrefix = this.options.errorPrefix ?? "Algo deu errado";
|
|
1623
|
+
const streamingEnabled = this.options.streaming !== false;
|
|
1624
|
+
this.setState({ error: null, isLoading: true, loadingStatusKey: "statusAnalyzing" });
|
|
1625
|
+
this.currentTurn = null;
|
|
1626
|
+
this.recoveryCountByFingerprint = /* @__PURE__ */ new Map();
|
|
1627
|
+
const userMessage = {
|
|
1628
|
+
id: `local_${++this.messageIdCounter}`,
|
|
1629
|
+
role: "user",
|
|
1630
|
+
content: content.trim(),
|
|
1631
|
+
timestamp: Date.now()
|
|
1632
|
+
};
|
|
1633
|
+
this.setState({ messages: [...this.state.messages, userMessage] });
|
|
1634
|
+
const streamingId = `streaming_${++this.messageIdCounter}`;
|
|
1635
|
+
let streamedVisible = false;
|
|
1636
|
+
const removeStreamingPlaceholder = () => {
|
|
1637
|
+
if (!streamedVisible) return;
|
|
1638
|
+
streamedVisible = false;
|
|
1639
|
+
this.setState({ messages: this.state.messages.filter((m) => m.id !== streamingId) });
|
|
1640
|
+
};
|
|
1641
|
+
const abortController = new AbortController();
|
|
1642
|
+
this.abortController = abortController;
|
|
1643
|
+
try {
|
|
1644
|
+
const context = this.deps.adapter.buildContext();
|
|
1645
|
+
console.log(
|
|
1646
|
+
`[Appilots] ChatSessionMachine 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}}`
|
|
1647
|
+
);
|
|
1648
|
+
let response;
|
|
1649
|
+
if (streamingEnabled) {
|
|
1650
|
+
try {
|
|
1651
|
+
response = await this.deps.client.sendMessageStream(content.trim(), context, {
|
|
1652
|
+
signal: abortController.signal,
|
|
1653
|
+
onTextDelta: (_chunk, fullText) => {
|
|
1654
|
+
const visible = sanitizeVisibleAssistantContent(fullText);
|
|
1655
|
+
if (!streamedVisible) {
|
|
1656
|
+
streamedVisible = true;
|
|
1657
|
+
this.setState({
|
|
1658
|
+
messages: [
|
|
1659
|
+
...this.state.messages,
|
|
1660
|
+
{ id: streamingId, role: "assistant", content: visible, timestamp: Date.now() }
|
|
1661
|
+
]
|
|
1662
|
+
});
|
|
1663
|
+
} else {
|
|
1664
|
+
this.setState({
|
|
1665
|
+
messages: this.state.messages.map(
|
|
1666
|
+
(m) => m.id === streamingId ? { ...m, content: visible } : m
|
|
1667
|
+
)
|
|
1668
|
+
});
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
});
|
|
1672
|
+
} catch (err) {
|
|
1673
|
+
if (err instanceof StreamTransportError) {
|
|
1674
|
+
console.warn(
|
|
1675
|
+
`[Appilots] streaming unavailable, falling back to JSON: ${err.message}`
|
|
1676
|
+
);
|
|
1677
|
+
response = await this.deps.client.sendMessage(content.trim(), context);
|
|
1678
|
+
} else {
|
|
1679
|
+
throw err;
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
} else {
|
|
1683
|
+
response = await this.deps.client.sendMessage(content.trim(), context);
|
|
1684
|
+
}
|
|
1685
|
+
console.log(
|
|
1686
|
+
`[Appilots] ChatSessionMachine sendMessage: response received \u2014 actions=${response.actions?.length ?? 0}, hasSummary=${!!response.summaryMessage}`
|
|
1687
|
+
);
|
|
1688
|
+
removeStreamingPlaceholder();
|
|
1689
|
+
this.addAssistantMessage(response.message);
|
|
1690
|
+
this.beginTurn(response.message.id, response.actions, 1);
|
|
1691
|
+
if (response.summaryMessage) {
|
|
1692
|
+
this.addAssistantMessage(response.summaryMessage);
|
|
1693
|
+
}
|
|
1694
|
+
this.deps.emit({
|
|
1695
|
+
type: "agent:message",
|
|
1696
|
+
timestamp: Date.now(),
|
|
1697
|
+
data: { message: response.message, usage: response.usage }
|
|
1698
|
+
});
|
|
1699
|
+
if (!response.actions || response.actions.length === 0) {
|
|
1700
|
+
this.setState({ isLoading: false, loadingStatusKey: "thinking" });
|
|
1701
|
+
}
|
|
1702
|
+
} catch (err) {
|
|
1703
|
+
if (err instanceof StreamAbortError || err instanceof Error && err.name === "AbortError") {
|
|
1704
|
+
this.setState({ isLoading: false, loadingStatusKey: "thinking" });
|
|
1705
|
+
return;
|
|
1706
|
+
}
|
|
1707
|
+
removeStreamingPlaceholder();
|
|
1708
|
+
const errorMessage = err instanceof Error ? err.message : "Failed to send message";
|
|
1709
|
+
this.setState({ error: errorMessage });
|
|
1710
|
+
this.addAssistantMessage({
|
|
1711
|
+
id: `error_${++this.messageIdCounter}`,
|
|
1712
|
+
role: "assistant",
|
|
1713
|
+
content: `${errorPrefix}: ${errorMessage}`,
|
|
1714
|
+
timestamp: Date.now()
|
|
1715
|
+
});
|
|
1716
|
+
this.setState({ isLoading: false, loadingStatusKey: "thinking" });
|
|
1717
|
+
} finally {
|
|
1718
|
+
if (this.abortController === abortController) {
|
|
1719
|
+
this.abortController = null;
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
/**
|
|
1724
|
+
* Cancels the in-flight generation. Streamed partial text is kept in
|
|
1725
|
+
* the transcript; a no-op when nothing is in flight. Cancellation
|
|
1726
|
+
* only covers the generation phase — once actions start executing
|
|
1727
|
+
* they run to completion.
|
|
1728
|
+
*/
|
|
1729
|
+
cancelMessage() {
|
|
1730
|
+
this.abortController?.abort();
|
|
1731
|
+
}
|
|
1732
|
+
clearMessages() {
|
|
1733
|
+
this.setState({
|
|
1734
|
+
messages: [],
|
|
1735
|
+
pendingActions: [],
|
|
1736
|
+
error: null,
|
|
1737
|
+
isLoading: false,
|
|
1738
|
+
loadingStatusKey: "thinking"
|
|
1739
|
+
});
|
|
1740
|
+
this.currentTurn = null;
|
|
1741
|
+
this.continuationInFlight = false;
|
|
1742
|
+
this.recoveryCountByFingerprint = /* @__PURE__ */ new Map();
|
|
1743
|
+
if (this.state.escalation) {
|
|
1744
|
+
void this.deps.client.abandonEscalation(this.state.escalation.id);
|
|
1745
|
+
}
|
|
1746
|
+
this.setState({ escalation: null });
|
|
1747
|
+
this.closeEscalationStream();
|
|
1748
|
+
this.escalationSince = void 0;
|
|
1749
|
+
this.seenHumanMessageIds = /* @__PURE__ */ new Set();
|
|
1750
|
+
this.escalationReconnectAttempt = 0;
|
|
1751
|
+
void this.deps.client.destroySession();
|
|
1752
|
+
}
|
|
1753
|
+
clearError() {
|
|
1754
|
+
this.setState({ error: null });
|
|
1755
|
+
}
|
|
1756
|
+
};
|
|
1757
|
+
function readPayload(action) {
|
|
1758
|
+
return action.payload ?? {};
|
|
1759
|
+
}
|
|
1760
|
+
function isStructuredRisky(value) {
|
|
1761
|
+
return value === true || value === "destructive" || value === "danger" || value === "high";
|
|
1762
|
+
}
|
|
1763
|
+
function actionMetadataRequiresConfirmation(meta) {
|
|
1764
|
+
if (meta.requiresConfirmation === true || meta.destructive === true || meta.effect === "destructive" || meta.riskLevel === "high" || meta.risk === "destructive") {
|
|
1765
|
+
return { required: true, label: typeof meta.label === "string" ? meta.label : void 0, source: "screen-metadata" };
|
|
1766
|
+
}
|
|
1767
|
+
return { required: false };
|
|
1768
|
+
}
|
|
1769
|
+
function payloadRequiresConfirmation(action) {
|
|
1770
|
+
const payload = readPayload(action);
|
|
1771
|
+
if (action.destructive === true || payload.destructive === true || payload.requiresConfirmation === true || isStructuredRisky(payload.effect) || isStructuredRisky(payload.risk) || isStructuredRisky(payload.riskLevel) || isStructuredRisky(payload.actionRisk)) {
|
|
1772
|
+
return {
|
|
1773
|
+
required: true,
|
|
1774
|
+
label: typeof payload.actionDescription === "string" && payload.actionDescription || typeof payload.label === "string" && payload.label || void 0,
|
|
1775
|
+
source: "action-policy"
|
|
1776
|
+
};
|
|
1777
|
+
}
|
|
1778
|
+
const policy = payload._appilotsPolicy;
|
|
1779
|
+
if (policy && typeof policy === "object") {
|
|
1780
|
+
const p = policy;
|
|
1781
|
+
if (p.requiresConfirmation === true || isStructuredRisky(p.effect) || isStructuredRisky(p.risk) || isStructuredRisky(p.riskLevel)) {
|
|
1782
|
+
return {
|
|
1783
|
+
required: true,
|
|
1784
|
+
label: typeof p.summary === "string" ? p.summary : void 0,
|
|
1785
|
+
source: "inline-policy"
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
return { required: false };
|
|
1790
|
+
}
|
|
1791
|
+
function isConfirmGateFor(confirm, target) {
|
|
1792
|
+
if (confirm.type !== "confirm") return false;
|
|
1793
|
+
const payload = readPayload(confirm);
|
|
1794
|
+
const injectedBy = payload.injectedBy;
|
|
1795
|
+
return payload.gatedActionId === target.id && (injectedBy === "permissions" || injectedBy === "sdk-local");
|
|
1796
|
+
}
|
|
1797
|
+
function findGatingConfirm(target, actions) {
|
|
1798
|
+
return actions.find((a) => isConfirmGateFor(a, target));
|
|
1799
|
+
}
|
|
1800
|
+
function toDestructiveConfirmPayload(confirm, policy, fallbackLabel) {
|
|
1801
|
+
const payload = readPayload(confirm);
|
|
1802
|
+
const existingDescription = typeof payload.actionDescription === "string" && payload.actionDescription ? payload.actionDescription : void 0;
|
|
1803
|
+
const labelText = policy.label ?? existingDescription ?? fallbackLabel ?? "esta a\xE7\xE3o";
|
|
1804
|
+
return {
|
|
1805
|
+
...payload,
|
|
1806
|
+
title: "A\xE7\xE3o destrutiva",
|
|
1807
|
+
message: `Tem certeza que deseja "${labelText}"?`,
|
|
1808
|
+
actionDescription: labelText,
|
|
1809
|
+
severity: "danger",
|
|
1810
|
+
destructive: true,
|
|
1811
|
+
confirmLabel: typeof payload.confirmLabel === "string" && payload.confirmLabel ? payload.confirmLabel : "Sim, executar",
|
|
1812
|
+
cancelLabel: typeof payload.cancelLabel === "string" && payload.cancelLabel ? payload.cancelLabel : "Cancelar"
|
|
1813
|
+
};
|
|
1814
|
+
}
|
|
1815
|
+
function confirmNeedsDestructiveUpgrade(confirm) {
|
|
1816
|
+
const payload = readPayload(confirm);
|
|
1817
|
+
return confirm.destructive !== true || payload.destructive !== true || payload.severity !== "danger";
|
|
1818
|
+
}
|
|
1819
|
+
function actionHasCompletedConfirmGate(action, actions) {
|
|
1820
|
+
return actions.some((candidate) => {
|
|
1821
|
+
if (!isConfirmGateFor(candidate, action)) return false;
|
|
1822
|
+
if (candidate.status !== "completed") return false;
|
|
1823
|
+
const payload = readPayload(candidate);
|
|
1824
|
+
return payload.destructive === true || payload.severity === "danger";
|
|
1825
|
+
});
|
|
1826
|
+
}
|
|
1827
|
+
var ActionQueueMachine = class {
|
|
1828
|
+
deps;
|
|
1829
|
+
options;
|
|
1830
|
+
state = { actions: [] };
|
|
1831
|
+
listeners = /* @__PURE__ */ new Set();
|
|
1832
|
+
/** Action ids already dispatched — never auto-executed twice. */
|
|
1833
|
+
processed = /* @__PURE__ */ new Set();
|
|
1834
|
+
/** Guards against overlapping sequential runs. */
|
|
1835
|
+
draining = false;
|
|
1836
|
+
/** Set when new work arrives while a drain is in flight. */
|
|
1837
|
+
drainRequested = false;
|
|
1838
|
+
/** Set while a drain is queued on the microtask queue. */
|
|
1839
|
+
drainScheduled = false;
|
|
1840
|
+
constructor(deps, options = {}) {
|
|
1841
|
+
this.deps = deps;
|
|
1842
|
+
this.options = options;
|
|
1843
|
+
this.getState = this.getState.bind(this);
|
|
1844
|
+
this.subscribeState = this.subscribeState.bind(this);
|
|
1845
|
+
this.handleEvent = this.handleEvent.bind(this);
|
|
1846
|
+
this.approveAction = this.approveAction.bind(this);
|
|
1847
|
+
this.rejectAction = this.rejectAction.bind(this);
|
|
1848
|
+
}
|
|
1849
|
+
// ── State plumbing ───────────────────────────────────────────
|
|
1850
|
+
getState() {
|
|
1851
|
+
return this.state;
|
|
1852
|
+
}
|
|
1853
|
+
subscribeState(listener) {
|
|
1854
|
+
this.listeners.add(listener);
|
|
1855
|
+
return () => {
|
|
1856
|
+
this.listeners.delete(listener);
|
|
1857
|
+
};
|
|
1858
|
+
}
|
|
1859
|
+
setOptions(options) {
|
|
1860
|
+
this.options = options;
|
|
1861
|
+
}
|
|
1862
|
+
setActions(next) {
|
|
1863
|
+
if (next === this.state.actions) return;
|
|
1864
|
+
this.state = { actions: next };
|
|
1865
|
+
for (const listener of this.listeners) listener();
|
|
1866
|
+
}
|
|
1867
|
+
updateActions(updater) {
|
|
1868
|
+
this.setActions(updater(this.state.actions));
|
|
1869
|
+
}
|
|
1870
|
+
warn(message, ...args) {
|
|
1871
|
+
(this.deps.warn ?? console.warn)(message, ...args);
|
|
1872
|
+
}
|
|
1873
|
+
// ── Event intake: new actions from the agent ─────────────────
|
|
1874
|
+
handleEvent(event) {
|
|
1875
|
+
if (event.type !== "agent:action:start") return;
|
|
1876
|
+
const action = event.data?.action;
|
|
1877
|
+
if (!action?.id) return;
|
|
1878
|
+
console.log(
|
|
1879
|
+
`[Appilots] ActionQueueMachine: received agent:action:start event, action id="${action.id}", type="${action.type}"`
|
|
1880
|
+
);
|
|
1881
|
+
if (this.state.actions.some((a) => a.id === action.id)) return;
|
|
1882
|
+
this.setActions([...this.state.actions, action]);
|
|
1883
|
+
this.scheduleDrain();
|
|
1884
|
+
}
|
|
1885
|
+
/**
|
|
1886
|
+
* Kick the sequential executor. Safe to call at any time — it
|
|
1887
|
+
* coalesces while a drain is already running so a burst of arriving
|
|
1888
|
+
* actions produces exactly one in-order pass.
|
|
1889
|
+
*
|
|
1890
|
+
* The drain is deferred to a microtask, and that is load-bearing, not
|
|
1891
|
+
* a nicety. A turn's actions arrive as a synchronous burst of
|
|
1892
|
+
* `agent:action:start` events, and a confirm gating a destructive
|
|
1893
|
+
* action can arrive AFTER the action it protects. Draining
|
|
1894
|
+
* synchronously on the first event would execute that action while
|
|
1895
|
+
* its gate is still in flight — a delete running before the user
|
|
1896
|
+
* confirmed it. Deferring lets the whole burst land first, so the
|
|
1897
|
+
* gate is visible by the time anything executes. (The React hook this
|
|
1898
|
+
* was extracted from got the same guarantee for free, from state
|
|
1899
|
+
* batching before its effect ran.)
|
|
1900
|
+
*/
|
|
1901
|
+
scheduleDrain() {
|
|
1902
|
+
if (!this.options.autoExecute) return;
|
|
1903
|
+
if (this.draining) {
|
|
1904
|
+
this.drainRequested = true;
|
|
1905
|
+
return;
|
|
1906
|
+
}
|
|
1907
|
+
if (this.drainScheduled) return;
|
|
1908
|
+
this.drainScheduled = true;
|
|
1909
|
+
queueMicrotask(() => {
|
|
1910
|
+
this.drainScheduled = false;
|
|
1911
|
+
if (!this.options.autoExecute) return;
|
|
1912
|
+
if (this.draining) {
|
|
1913
|
+
this.drainRequested = true;
|
|
1914
|
+
return;
|
|
1915
|
+
}
|
|
1916
|
+
void this.drain();
|
|
1917
|
+
});
|
|
1918
|
+
}
|
|
1919
|
+
async drain() {
|
|
1920
|
+
this.draining = true;
|
|
1921
|
+
try {
|
|
1922
|
+
do {
|
|
1923
|
+
this.drainRequested = false;
|
|
1924
|
+
await this.executeSequentially();
|
|
1925
|
+
} while (this.drainRequested);
|
|
1926
|
+
} finally {
|
|
1927
|
+
this.draining = false;
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
// ── Confirm gating (defense-in-depth) ────────────────────────
|
|
1931
|
+
/**
|
|
1932
|
+
* Client-side defense-in-depth confirm injection.
|
|
1933
|
+
*
|
|
1934
|
+
* When the server's permission filter MISSES a destructive action
|
|
1935
|
+
* (e.g. the MCP doc is stale and doesn't have the destructive flag
|
|
1936
|
+
* for that action, but the live screen metadata DOES have
|
|
1937
|
+
* `requiresConfirmation: true`), the action would otherwise execute
|
|
1938
|
+
* immediately with no user confirmation — exactly the "delete
|
|
1939
|
+
* executed before I confirmed" bug.
|
|
1940
|
+
*
|
|
1941
|
+
* Returns `true` when the target must NOT execute this pass (either
|
|
1942
|
+
* a synthetic confirm was injected, or one is already gating it).
|
|
1943
|
+
*/
|
|
1944
|
+
ensureLocalConfirmIfNeeded(target) {
|
|
1945
|
+
if (target.type !== "ui_interaction") return false;
|
|
1946
|
+
const payload = target.payload;
|
|
1947
|
+
const componentId = typeof payload?.componentId === "string" && payload.componentId || typeof payload?.targetId === "string" && payload.targetId || "";
|
|
1948
|
+
let policy = payloadRequiresConfirmation(target);
|
|
1949
|
+
if (!policy.required && componentId) {
|
|
1950
|
+
const screen = this.deps.adapter.getCurrentScreen();
|
|
1951
|
+
const actionsMeta = screen ? this.deps.adapter.getScreenActionsMetadata(screen) : void 0;
|
|
1952
|
+
if (actionsMeta && Array.isArray(actionsMeta)) {
|
|
1953
|
+
const match = actionsMeta.find((a) => {
|
|
1954
|
+
if (typeof a === "string") return false;
|
|
1955
|
+
return metadataMatchesComponentId(a.id, componentId);
|
|
1956
|
+
});
|
|
1957
|
+
if (match && typeof match !== "string") {
|
|
1958
|
+
policy = actionMetadataRequiresConfirmation(match);
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
const existingGate = findGatingConfirm(target, this.state.actions);
|
|
1963
|
+
if (existingGate) {
|
|
1964
|
+
if (policy.required && confirmNeedsDestructiveUpgrade(existingGate)) {
|
|
1965
|
+
this.updateActions(
|
|
1966
|
+
(prev) => prev.map(
|
|
1967
|
+
(a) => a.id === existingGate.id ? {
|
|
1968
|
+
...a,
|
|
1969
|
+
destructive: true,
|
|
1970
|
+
payload: toDestructiveConfirmPayload(a, policy, componentId || target.id)
|
|
1971
|
+
} : a
|
|
1972
|
+
)
|
|
1973
|
+
);
|
|
1974
|
+
}
|
|
1975
|
+
return false;
|
|
1976
|
+
}
|
|
1977
|
+
if (!policy.required) return false;
|
|
1978
|
+
const confirmId = `confirm-local-${target.id}`;
|
|
1979
|
+
if (this.state.actions.some((a) => a.id === confirmId)) return true;
|
|
1980
|
+
console.log(
|
|
1981
|
+
`[Appilots] ActionQueueMachine: action "${componentId || target.id}" requires confirmation (${policy.source ?? "local-policy"}) \u2014 synthesising local confirm (id="${confirmId}")`
|
|
1982
|
+
);
|
|
1983
|
+
const synth = {
|
|
1984
|
+
id: confirmId,
|
|
1985
|
+
type: "confirm",
|
|
1986
|
+
// Flagged at the top level too, not only inside the payload: this
|
|
1987
|
+
// is the field a chat surface reads to render the danger variant,
|
|
1988
|
+
// and we only synthesize a confirm because the target IS
|
|
1989
|
+
// destructive. The upgrade path above sets it for the same reason.
|
|
1990
|
+
destructive: true,
|
|
1991
|
+
payload: toDestructiveConfirmPayload(
|
|
1992
|
+
{
|
|
1993
|
+
payload: {
|
|
1994
|
+
gatedActionId: target.id,
|
|
1995
|
+
gatedActionType: target.type,
|
|
1996
|
+
injectedBy: "sdk-local"
|
|
1997
|
+
},
|
|
1998
|
+
messageId: target.messageId
|
|
1999
|
+
},
|
|
2000
|
+
policy,
|
|
2001
|
+
componentId || "esta a\xE7\xE3o"
|
|
2002
|
+
),
|
|
2003
|
+
status: "pending",
|
|
2004
|
+
messageId: target.messageId
|
|
2005
|
+
};
|
|
2006
|
+
this.updateActions((prev) => {
|
|
2007
|
+
const idx = prev.findIndex((a) => a.id === target.id);
|
|
2008
|
+
if (idx < 0) return [...prev, synth];
|
|
2009
|
+
return [...prev.slice(0, idx), synth, ...prev.slice(idx)];
|
|
2010
|
+
});
|
|
2011
|
+
this.deps.emit({
|
|
2012
|
+
type: "agent:action:start",
|
|
2013
|
+
timestamp: Date.now(),
|
|
2014
|
+
data: { action: synth }
|
|
2015
|
+
});
|
|
2016
|
+
return true;
|
|
2017
|
+
}
|
|
2018
|
+
// ── Sequential auto-execution ────────────────────────────────
|
|
2019
|
+
/**
|
|
2020
|
+
* Execute pending actions in order so chained actions (e.g.
|
|
2021
|
+
* form_fill → press button) run in the correct sequence.
|
|
2022
|
+
*/
|
|
2023
|
+
async executeSequentially() {
|
|
2024
|
+
const pending = this.state.actions.filter(
|
|
2025
|
+
(a) => a.status === "pending" && !this.processed.has(a.id)
|
|
2026
|
+
);
|
|
2027
|
+
console.log(
|
|
2028
|
+
`[Appilots] ActionQueueMachine: autoExecute check \u2014 ${pending.length} pending action(s) to process`
|
|
2029
|
+
);
|
|
2030
|
+
let lastWasNavigate = false;
|
|
2031
|
+
let preNavScreen = null;
|
|
2032
|
+
let preNavSignature = null;
|
|
2033
|
+
for (const action of pending) {
|
|
2034
|
+
if (action.type === "confirm") {
|
|
2035
|
+
continue;
|
|
2036
|
+
}
|
|
2037
|
+
if (this.ensureLocalConfirmIfNeeded(action)) {
|
|
2038
|
+
continue;
|
|
2039
|
+
}
|
|
2040
|
+
const gate = findGatingConfirm(action, this.state.actions);
|
|
2041
|
+
if (gate) {
|
|
2042
|
+
if (gate.status === "pending" || gate.status === "executing") {
|
|
2043
|
+
console.log(
|
|
2044
|
+
`[Appilots] ActionQueueMachine: action id="${action.id}" is gated by confirm id="${gate.id}" (status=${gate.status}); waiting for user decision`
|
|
2045
|
+
);
|
|
2046
|
+
continue;
|
|
2047
|
+
}
|
|
2048
|
+
if (gate.status === "failed") {
|
|
2049
|
+
console.log(
|
|
2050
|
+
`[Appilots] ActionQueueMachine: confirm id="${gate.id}" was rejected; skipping gated action id="${action.id}"`
|
|
2051
|
+
);
|
|
2052
|
+
this.processed.add(action.id);
|
|
2053
|
+
this.updateActions(
|
|
2054
|
+
(prev) => prev.map(
|
|
2055
|
+
(a) => a.id === action.id ? { ...a, status: "failed", error: "Confirm canceled by user" } : a
|
|
2056
|
+
)
|
|
2057
|
+
);
|
|
758
2058
|
try {
|
|
759
|
-
|
|
2059
|
+
await this.deps.client.completeAction(action.id, false, "Confirm canceled by user");
|
|
760
2060
|
} catch {
|
|
761
|
-
this.log("Skipping unparseable escalation SSE line:", line.slice(0, 120));
|
|
762
2061
|
}
|
|
2062
|
+
this.deps.emit({
|
|
2063
|
+
type: "agent:action:error",
|
|
2064
|
+
timestamp: Date.now(),
|
|
2065
|
+
data: {
|
|
2066
|
+
actionId: action.id,
|
|
2067
|
+
actionType: action.type,
|
|
2068
|
+
success: false,
|
|
2069
|
+
error: "Confirm canceled by user"
|
|
2070
|
+
}
|
|
2071
|
+
});
|
|
2072
|
+
continue;
|
|
763
2073
|
}
|
|
764
2074
|
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
2075
|
+
if (lastWasNavigate) {
|
|
2076
|
+
const settle = await this.deps.adapter.waitForScreenSettle({
|
|
2077
|
+
expectingChange: true,
|
|
2078
|
+
fromScreen: preNavScreen,
|
|
2079
|
+
fromSignature: preNavSignature,
|
|
2080
|
+
maxMs: 3500
|
|
2081
|
+
});
|
|
2082
|
+
console.log(
|
|
2083
|
+
`[Appilots] ActionQueueMachine: post-navigate settle done \u2014 screen="${settle.screen}" transitioned=${settle.transitioned} timedOut=${settle.timedOut} waitedMs=${settle.waitedMs}`
|
|
2084
|
+
);
|
|
2085
|
+
}
|
|
2086
|
+
console.log(
|
|
2087
|
+
`[Appilots] ActionQueueMachine: auto-executing action id="${action.id}" type="${action.type}"`
|
|
2088
|
+
);
|
|
2089
|
+
this.processed.add(action.id);
|
|
2090
|
+
if (action.type === "navigate") {
|
|
2091
|
+
preNavScreen = this.deps.adapter.getCurrentScreen();
|
|
2092
|
+
preNavSignature = this.deps.adapter.getCurrentScreenSignature();
|
|
2093
|
+
}
|
|
2094
|
+
const succeeded = await this.approveAction(action.id);
|
|
2095
|
+
if (!succeeded) {
|
|
2096
|
+
this.warn(
|
|
2097
|
+
`ActionQueueMachine: stopping current action batch after failed action id="${action.id}"`
|
|
2098
|
+
);
|
|
2099
|
+
break;
|
|
2100
|
+
}
|
|
2101
|
+
lastWasNavigate = action.type === "navigate";
|
|
769
2102
|
}
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
2103
|
+
}
|
|
2104
|
+
// ── Approve & execute an action ─────────────────────────────
|
|
2105
|
+
async approveAction(actionId) {
|
|
2106
|
+
const action = this.state.actions.find((a) => a.id === actionId);
|
|
2107
|
+
if (!action) return false;
|
|
2108
|
+
console.log(
|
|
2109
|
+
`[Appilots] ActionQueueMachine: action status transition: pending \u2192 executing (id="${actionId}", type="${action.type}")`
|
|
2110
|
+
);
|
|
2111
|
+
this.updateActions(
|
|
2112
|
+
(prev) => prev.map((a) => a.id === actionId ? { ...a, status: "executing" } : a)
|
|
2113
|
+
);
|
|
2114
|
+
try {
|
|
2115
|
+
const confirmedDestructive = actionHasCompletedConfirmGate(action, this.state.actions);
|
|
2116
|
+
const result = await this.deps.adapter.execute(
|
|
2117
|
+
{ ...action, status: "executing" },
|
|
2118
|
+
{ confirmedDestructive }
|
|
2119
|
+
);
|
|
2120
|
+
if (result.success) {
|
|
2121
|
+
console.log(
|
|
2122
|
+
`[Appilots] ActionQueueMachine: action status transition: executing \u2192 completed (id="${actionId}", type="${action.type}")`
|
|
2123
|
+
);
|
|
2124
|
+
this.updateActions(
|
|
2125
|
+
(prev) => prev.map((a) => a.id === actionId ? { ...a, status: "completed" } : a)
|
|
2126
|
+
);
|
|
2127
|
+
try {
|
|
2128
|
+
await this.deps.client.completeAction(actionId, true);
|
|
2129
|
+
} catch {
|
|
2130
|
+
}
|
|
2131
|
+
this.scheduleDrain();
|
|
2132
|
+
return true;
|
|
2133
|
+
}
|
|
2134
|
+
this.warn(
|
|
2135
|
+
`ActionQueueMachine: action status transition: executing \u2192 failed (id="${actionId}", type="${action.type}", error="${result.error}")`
|
|
2136
|
+
);
|
|
2137
|
+
this.updateActions(
|
|
2138
|
+
(prev) => prev.map(
|
|
2139
|
+
(a) => a.id === actionId ? { ...a, status: "failed", error: result.error } : a
|
|
2140
|
+
)
|
|
2141
|
+
);
|
|
781
2142
|
try {
|
|
782
|
-
|
|
2143
|
+
await this.deps.client.completeAction(
|
|
2144
|
+
actionId,
|
|
2145
|
+
false,
|
|
2146
|
+
result.error,
|
|
2147
|
+
result.diagnose
|
|
2148
|
+
);
|
|
783
2149
|
} catch {
|
|
784
2150
|
}
|
|
785
|
-
|
|
2151
|
+
return false;
|
|
2152
|
+
} catch (err) {
|
|
2153
|
+
const errorMsg = err instanceof Error ? err.message : "Action failed";
|
|
2154
|
+
this.warn(
|
|
2155
|
+
`ActionQueueMachine: action status transition: executing \u2192 failed (exception) (id="${actionId}", error="${errorMsg}")`
|
|
2156
|
+
);
|
|
2157
|
+
this.updateActions(
|
|
2158
|
+
(prev) => prev.map(
|
|
2159
|
+
(a) => a.id === actionId ? { ...a, status: "failed", error: errorMsg } : a
|
|
2160
|
+
)
|
|
2161
|
+
);
|
|
2162
|
+
try {
|
|
2163
|
+
await this.deps.client.completeAction(actionId, false, errorMsg);
|
|
2164
|
+
} catch {
|
|
2165
|
+
}
|
|
2166
|
+
return false;
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
// ── Reject an action ────────────────────────────────────────
|
|
2170
|
+
async rejectAction(actionId) {
|
|
2171
|
+
this.updateActions(
|
|
2172
|
+
(prev) => prev.map(
|
|
2173
|
+
(a) => a.id === actionId ? { ...a, status: "failed", error: "Rejected by user" } : a
|
|
2174
|
+
)
|
|
2175
|
+
);
|
|
2176
|
+
const action = this.state.actions.find((a) => a.id === actionId);
|
|
2177
|
+
this.deps.emit({
|
|
2178
|
+
type: "agent:action:error",
|
|
2179
|
+
timestamp: Date.now(),
|
|
2180
|
+
data: {
|
|
2181
|
+
actionId,
|
|
2182
|
+
actionType: action?.type,
|
|
2183
|
+
success: false,
|
|
2184
|
+
error: "Rejected by user"
|
|
2185
|
+
}
|
|
2186
|
+
});
|
|
2187
|
+
try {
|
|
2188
|
+
await this.deps.client.completeAction(actionId, false, "Rejected by user");
|
|
2189
|
+
} catch {
|
|
2190
|
+
}
|
|
2191
|
+
this.scheduleDrain();
|
|
2192
|
+
}
|
|
2193
|
+
// ── Derived views ───────────────────────────────────────────
|
|
2194
|
+
get pendingActions() {
|
|
2195
|
+
return this.state.actions.filter((a) => a.status === "pending");
|
|
2196
|
+
}
|
|
2197
|
+
get executingActions() {
|
|
2198
|
+
return this.state.actions.filter((a) => a.status === "executing");
|
|
2199
|
+
}
|
|
2200
|
+
get completedActions() {
|
|
2201
|
+
return this.state.actions.filter(
|
|
2202
|
+
(a) => a.status === "completed" || a.status === "failed"
|
|
2203
|
+
);
|
|
786
2204
|
}
|
|
787
2205
|
};
|
|
2206
|
+
function normalize3(s) {
|
|
2207
|
+
return s.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]/g, "");
|
|
2208
|
+
}
|
|
2209
|
+
function matchScore(needle, hay) {
|
|
2210
|
+
if (!needle || !hay) return 0;
|
|
2211
|
+
if (needle === hay) return 100;
|
|
2212
|
+
if (hay.startsWith(needle) || needle.startsWith(hay)) return 75;
|
|
2213
|
+
if (hay.endsWith(needle) || needle.endsWith(hay)) return 60;
|
|
2214
|
+
if (hay.includes(needle) || needle.includes(hay)) return 40;
|
|
2215
|
+
return 0;
|
|
2216
|
+
}
|
|
2217
|
+
function isGenericSelectValue(value) {
|
|
2218
|
+
if (value === void 0 || value === null || value === "") return true;
|
|
2219
|
+
const normalized = normalize3(String(value));
|
|
2220
|
+
if (!normalized) return true;
|
|
2221
|
+
return /^(any|qualquer|random|aleatorio|aleatoria|auto|automatico|automatica|mock|mockado|mockada|mockados|mockadas|dummy|sample|exemplo|default|padrao|teste|test)$/.test(
|
|
2222
|
+
normalized
|
|
2223
|
+
);
|
|
2224
|
+
}
|
|
2225
|
+
var LIST_ORDINAL_RE = /^list-\d+-item-\d+$/i;
|
|
2226
|
+
function normalizeId(value) {
|
|
2227
|
+
return value.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
|
|
2228
|
+
}
|
|
2229
|
+
function isOptionalCosmeticTargetId(targetId) {
|
|
2230
|
+
if (typeof targetId !== "string" || !targetId.trim()) return false;
|
|
2231
|
+
const id = normalizeId(targetId);
|
|
2232
|
+
return /foto|photo|avatar|icone|icon|emoji|sheetaction|sheet-action|btn-foto/.test(id) || id.includes("photo-sheet");
|
|
2233
|
+
}
|
|
2234
|
+
function routeLooksLikeFormContext(route) {
|
|
2235
|
+
const haystack = normalizeId(route ?? "");
|
|
2236
|
+
return /create|edit|form|cadastr|register|novo|vehiclecreate|subscriptioncreate|vehicleedit|subscriptioncreate/.test(
|
|
2237
|
+
haystack
|
|
2238
|
+
);
|
|
2239
|
+
}
|
|
2240
|
+
function screenPathParts(screen) {
|
|
2241
|
+
if (!screen) return [];
|
|
2242
|
+
return screen.split("/").map((part) => part.trim()).filter(Boolean);
|
|
2243
|
+
}
|
|
2244
|
+
function isMisappliedListOrdinalOnForm(targetId, screen, activePath) {
|
|
2245
|
+
if (typeof targetId !== "string" || !LIST_ORDINAL_RE.test(targetId.trim())) {
|
|
2246
|
+
return false;
|
|
2247
|
+
}
|
|
2248
|
+
const routes = [...screenPathParts(screen), ...activePath ?? []];
|
|
2249
|
+
if (routes.length === 0) return routeLooksLikeFormContext(screen);
|
|
2250
|
+
return routes.some((route) => routeLooksLikeFormContext(route));
|
|
2251
|
+
}
|
|
2252
|
+
function isOptionalStepAutomationFailure(targetId, screen, activePath) {
|
|
2253
|
+
return isOptionalCosmeticTargetId(targetId) || isMisappliedListOrdinalOnForm(targetId, screen, activePath);
|
|
2254
|
+
}
|
|
2255
|
+
var OPTIONAL_STEP_AUTOMATION_HINT = "optional-step-automation-failed";
|
|
2256
|
+
function describeAction(action) {
|
|
2257
|
+
const state = mapStatus(action.status);
|
|
2258
|
+
const label = labelFor(action, state);
|
|
2259
|
+
return { label, state };
|
|
2260
|
+
}
|
|
2261
|
+
function mapStatus(status) {
|
|
2262
|
+
switch (status) {
|
|
2263
|
+
case "pending":
|
|
2264
|
+
return "pending";
|
|
2265
|
+
case "executing":
|
|
2266
|
+
return "running";
|
|
2267
|
+
case "completed":
|
|
2268
|
+
return "success";
|
|
2269
|
+
case "failed":
|
|
2270
|
+
return "failed";
|
|
2271
|
+
default:
|
|
2272
|
+
return "pending";
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
function labelFor(action, state) {
|
|
2276
|
+
if (state === "failed") {
|
|
2277
|
+
return failureLabel(action);
|
|
2278
|
+
}
|
|
2279
|
+
switch (action.type) {
|
|
2280
|
+
case "navigate":
|
|
2281
|
+
return navigateLabel(action, state);
|
|
2282
|
+
case "form_fill":
|
|
2283
|
+
return formFillLabel(action, state);
|
|
2284
|
+
case "ui_interaction":
|
|
2285
|
+
return uiInteractionLabel(action, state);
|
|
2286
|
+
case "scroll_list":
|
|
2287
|
+
return scrollListLabel(action, state);
|
|
2288
|
+
case "confirm":
|
|
2289
|
+
return confirmLabel(state);
|
|
2290
|
+
default:
|
|
2291
|
+
return genericLabel(action.type, state);
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
function navigateLabel(action, state) {
|
|
2295
|
+
const screen = readString(action.payload, "screenName") || "a outra tela";
|
|
2296
|
+
switch (state) {
|
|
2297
|
+
case "pending":
|
|
2298
|
+
return `Vou navegar para ${screen}`;
|
|
2299
|
+
case "running":
|
|
2300
|
+
return `Navegando para ${screen}...`;
|
|
2301
|
+
case "success":
|
|
2302
|
+
return `Naveguei para ${screen}`;
|
|
2303
|
+
default:
|
|
2304
|
+
return `Navega\xE7\xE3o para ${screen}`;
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
function formFillLabel(action, state) {
|
|
2308
|
+
const fields = readArray(action.payload, "fields");
|
|
2309
|
+
const summary = summariseFields(fields);
|
|
2310
|
+
switch (state) {
|
|
2311
|
+
case "pending":
|
|
2312
|
+
return summary ? `Vou preencher ${summary}` : "Vou preencher o formul\xE1rio";
|
|
2313
|
+
case "running":
|
|
2314
|
+
return summary ? `Preenchendo ${summary}...` : "Preenchendo formul\xE1rio...";
|
|
2315
|
+
case "success":
|
|
2316
|
+
return summary ? `Preenchi ${summary}` : "Preenchi o formul\xE1rio";
|
|
2317
|
+
default:
|
|
2318
|
+
return "Preenchimento do formul\xE1rio";
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
function uiInteractionLabel(action, state) {
|
|
2322
|
+
const verbBase = uiVerb(readString(action.payload, "action"));
|
|
2323
|
+
const target = humanActionTarget(action.payload);
|
|
2324
|
+
const where = target ? ` em ${target}` : "";
|
|
2325
|
+
switch (state) {
|
|
2326
|
+
case "pending":
|
|
2327
|
+
return `Vou ${verbBase.infinitive}${where}`;
|
|
2328
|
+
case "running":
|
|
2329
|
+
return `${verbBase.gerund}${where}...`;
|
|
2330
|
+
case "success":
|
|
2331
|
+
return `${verbBase.past}${where}`;
|
|
2332
|
+
default:
|
|
2333
|
+
return `Intera\xE7\xE3o${where}`;
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
function scrollListLabel(action, state) {
|
|
2337
|
+
const payload = action.payload;
|
|
2338
|
+
const toIndex = typeof payload?.toIndex === "number" ? payload.toIndex : void 0;
|
|
2339
|
+
const direction = payload?.direction === "up" ? "para cima" : "para baixo";
|
|
2340
|
+
const target = toIndex !== void 0 ? `at\xE9 o item ${toIndex}` : direction;
|
|
2341
|
+
switch (state) {
|
|
2342
|
+
case "pending":
|
|
2343
|
+
return `Vou rolar a lista ${target}`;
|
|
2344
|
+
case "running":
|
|
2345
|
+
return `Rolando a lista ${target}...`;
|
|
2346
|
+
case "success":
|
|
2347
|
+
return `Rolei a lista ${target}`;
|
|
2348
|
+
default:
|
|
2349
|
+
return "Rolagem da lista";
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
function confirmLabel(state) {
|
|
2353
|
+
switch (state) {
|
|
2354
|
+
case "pending":
|
|
2355
|
+
case "running":
|
|
2356
|
+
return "Aguardando confirma\xE7\xE3o";
|
|
2357
|
+
case "success":
|
|
2358
|
+
return "Voc\xEA confirmou";
|
|
2359
|
+
case "failed":
|
|
2360
|
+
return "Confirma\xE7\xE3o cancelada";
|
|
2361
|
+
default:
|
|
2362
|
+
return "Confirma\xE7\xE3o";
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
function genericLabel(type, state) {
|
|
2366
|
+
switch (state) {
|
|
2367
|
+
case "pending":
|
|
2368
|
+
return `Vou executar ${type}`;
|
|
2369
|
+
case "running":
|
|
2370
|
+
return `Executando ${type}...`;
|
|
2371
|
+
case "success":
|
|
2372
|
+
return `Executei ${type}`;
|
|
2373
|
+
default:
|
|
2374
|
+
return type;
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
function failureLabel(action) {
|
|
2378
|
+
const cause = humanizeError(action.error);
|
|
2379
|
+
const head = failureHead(action);
|
|
2380
|
+
return cause ? `${head} (${cause})` : head;
|
|
2381
|
+
}
|
|
2382
|
+
function failureHead(action) {
|
|
2383
|
+
switch (action.type) {
|
|
2384
|
+
case "navigate": {
|
|
2385
|
+
const screen = readString(action.payload, "screenName") || "a tela";
|
|
2386
|
+
return `N\xE3o consegui navegar para ${screen}`;
|
|
2387
|
+
}
|
|
2388
|
+
case "form_fill":
|
|
2389
|
+
return "N\xE3o consegui preencher o formul\xE1rio";
|
|
2390
|
+
case "ui_interaction": {
|
|
2391
|
+
const verb = uiVerb(readString(action.payload, "action"));
|
|
2392
|
+
const target = humanComponentId(
|
|
2393
|
+
readString(action.payload, "componentId") || readString(action.payload, "targetId")
|
|
2394
|
+
);
|
|
2395
|
+
return target ? `N\xE3o consegui ${verb.infinitive} em ${target}` : `N\xE3o consegui ${verb.infinitive}`;
|
|
2396
|
+
}
|
|
2397
|
+
case "scroll_list":
|
|
2398
|
+
return "N\xE3o consegui rolar a lista";
|
|
2399
|
+
case "confirm":
|
|
2400
|
+
return "Voc\xEA cancelou";
|
|
2401
|
+
default:
|
|
2402
|
+
return `N\xE3o consegui executar ${action.type}`;
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
var TECHNICAL_PATTERNS = [
|
|
2406
|
+
/\bat\s+\S+\s*\(/,
|
|
2407
|
+
// stack trace frame
|
|
2408
|
+
/TypeError|ReferenceError|SyntaxError|RangeError/i,
|
|
2409
|
+
/HTTP\s*[45]\d{2}/i,
|
|
2410
|
+
/^\s*\{[\s\S]*\}\s*$/,
|
|
2411
|
+
// JSON dump
|
|
2412
|
+
/undefined is not a function/i,
|
|
2413
|
+
/Cannot read propert/i
|
|
2414
|
+
];
|
|
2415
|
+
var FRIENDLY_REWRITES = [
|
|
2416
|
+
[/rejected by user/i, "cancelado pelo usu\xE1rio"],
|
|
2417
|
+
[/component '([^']+)' not found/i, "componente $1 n\xE3o encontrado"],
|
|
2418
|
+
[/field '([^']+)' not found/i, "campo $1 n\xE3o encontrado"],
|
|
2419
|
+
[/screen '([^']+)' not registered/i, "tela $1 n\xE3o registrada"],
|
|
2420
|
+
[/permission denied/i, "sem permiss\xE3o"],
|
|
2421
|
+
[/timeout|timed out/i, "tempo esgotado"],
|
|
2422
|
+
[/network/i, "falha de rede"]
|
|
2423
|
+
];
|
|
2424
|
+
var MAX_REASON_LEN = 80;
|
|
2425
|
+
function humanizeError(raw, _action) {
|
|
2426
|
+
if (!raw) return void 0;
|
|
2427
|
+
const trimmed = String(raw).trim();
|
|
2428
|
+
if (!trimmed) return void 0;
|
|
2429
|
+
for (const [pattern, replacement] of FRIENDLY_REWRITES) {
|
|
2430
|
+
if (pattern.test(trimmed)) {
|
|
2431
|
+
return finalize(trimmed.replace(pattern, replacement));
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
if (TECHNICAL_PATTERNS.some((p) => p.test(trimmed))) {
|
|
2435
|
+
return "algo deu errado";
|
|
2436
|
+
}
|
|
2437
|
+
if (trimmed.length > MAX_REASON_LEN) {
|
|
2438
|
+
return "algo deu errado";
|
|
2439
|
+
}
|
|
2440
|
+
return finalize(trimmed);
|
|
2441
|
+
}
|
|
2442
|
+
function finalize(text) {
|
|
2443
|
+
let out = text.trim();
|
|
2444
|
+
if (out.length > MAX_REASON_LEN) out = out.slice(0, MAX_REASON_LEN - 1).trimEnd() + "\u2026";
|
|
2445
|
+
if (out.length > 0) out = out[0].toLowerCase() + out.slice(1);
|
|
2446
|
+
return out;
|
|
2447
|
+
}
|
|
2448
|
+
function uiVerb(raw) {
|
|
2449
|
+
switch ((raw ?? "press").toLowerCase()) {
|
|
2450
|
+
case "press":
|
|
2451
|
+
case "tap":
|
|
2452
|
+
case "click":
|
|
2453
|
+
return { infinitive: "clicar", gerund: "Clicando", past: "Cliquei" };
|
|
2454
|
+
case "longpress":
|
|
2455
|
+
case "long_press":
|
|
2456
|
+
return { infinitive: "pressionar", gerund: "Pressionando", past: "Pressionei" };
|
|
2457
|
+
case "scroll":
|
|
2458
|
+
case "scroll_to":
|
|
2459
|
+
return { infinitive: "rolar", gerund: "Rolando", past: "Rolei" };
|
|
2460
|
+
case "focus":
|
|
2461
|
+
return { infinitive: "focar", gerund: "Focando", past: "Foquei" };
|
|
2462
|
+
default:
|
|
2463
|
+
return { infinitive: "interagir", gerund: "Interagindo", past: "Interagi" };
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
function summariseFields(fields) {
|
|
2467
|
+
if (!Array.isArray(fields) || fields.length === 0) return void 0;
|
|
2468
|
+
const parts = [];
|
|
2469
|
+
const visible = fields.slice(0, 3);
|
|
2470
|
+
for (const f of visible) {
|
|
2471
|
+
if (!f || typeof f !== "object") continue;
|
|
2472
|
+
const obj = f;
|
|
2473
|
+
const name = humanFieldName(obj.label, obj.fieldId);
|
|
2474
|
+
if (!name) continue;
|
|
2475
|
+
const fieldIdStr = typeof obj.fieldId === "string" ? obj.fieldId : void 0;
|
|
2476
|
+
const labelStr = typeof obj.label === "string" ? obj.label : void 0;
|
|
2477
|
+
const fieldTypeStr = typeof obj.fieldType === "string" ? obj.fieldType : void 0;
|
|
2478
|
+
const sensitive = isSensitiveFieldName(fieldIdStr, labelStr, void 0, fieldTypeStr);
|
|
2479
|
+
if (sensitive) {
|
|
2480
|
+
parts.push(`${name}=\u2022\u2022\u2022\u2022`);
|
|
2481
|
+
continue;
|
|
2482
|
+
}
|
|
2483
|
+
const value = formatFieldValue(obj.value);
|
|
2484
|
+
parts.push(value !== void 0 ? `${name}=${value}` : name);
|
|
2485
|
+
}
|
|
2486
|
+
if (parts.length === 0) return void 0;
|
|
2487
|
+
const extra = fields.length - parts.length;
|
|
2488
|
+
return extra > 0 ? `${parts.join(", ")} +${extra}` : parts.join(", ");
|
|
2489
|
+
}
|
|
2490
|
+
function formatFieldValue(value) {
|
|
2491
|
+
if (value === void 0 || value === null || value === "") return void 0;
|
|
2492
|
+
if (typeof value === "string") {
|
|
2493
|
+
const v = value.length > 18 ? `${value.slice(0, 17)}\u2026` : value;
|
|
2494
|
+
return `"${v}"`;
|
|
2495
|
+
}
|
|
2496
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
2497
|
+
return void 0;
|
|
2498
|
+
}
|
|
2499
|
+
function humanActionTarget(payload) {
|
|
2500
|
+
if (!payload || typeof payload !== "object") return "";
|
|
2501
|
+
const record = payload;
|
|
2502
|
+
const friendly = readFriendlyString(record, "label") || readFriendlyString(record, "actionDescription") || readFriendlyString(record, "title") || readFriendlyString(record, "name");
|
|
2503
|
+
if (friendly) return friendly;
|
|
2504
|
+
return humanComponentId(
|
|
2505
|
+
readString(payload, "componentId") || readString(payload, "targetId")
|
|
2506
|
+
);
|
|
2507
|
+
}
|
|
2508
|
+
function humanFieldName(label, fieldId) {
|
|
2509
|
+
if (typeof label === "string" && label.trim()) return label.trim();
|
|
2510
|
+
if (typeof fieldId !== "string" || isTechnicalIdentifier(fieldId)) return void 0;
|
|
2511
|
+
return humanComponentId(fieldId) || void 0;
|
|
2512
|
+
}
|
|
2513
|
+
function readFriendlyString(record, key) {
|
|
2514
|
+
const value = record[key];
|
|
2515
|
+
if (typeof value !== "string") return "";
|
|
2516
|
+
const trimmed = value.trim();
|
|
2517
|
+
if (!trimmed || isTechnicalIdentifier(trimmed)) return "";
|
|
2518
|
+
return trimmed;
|
|
2519
|
+
}
|
|
2520
|
+
function humanComponentId(id) {
|
|
2521
|
+
if (!id) return "";
|
|
2522
|
+
if (isTechnicalIdentifier(id)) return "";
|
|
2523
|
+
const cleaned = id.replace(/^(btn-|btn_|button-|component-)/i, "").replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[-_]+/g, " ").trim();
|
|
2524
|
+
if (!cleaned) return "";
|
|
2525
|
+
return cleaned[0].toUpperCase() + cleaned.slice(1);
|
|
2526
|
+
}
|
|
2527
|
+
function isTechnicalIdentifier(value) {
|
|
2528
|
+
const raw = value.trim();
|
|
2529
|
+
if (!raw) return false;
|
|
2530
|
+
if (/^el:/i.test(raw)) return true;
|
|
2531
|
+
if (/^list-\d+-item-\d+$/i.test(raw)) return true;
|
|
2532
|
+
if (/(testid|test-id|accessibilityid|accessibility-id|automationid|automation-id)/i.test(raw)) {
|
|
2533
|
+
return true;
|
|
2534
|
+
}
|
|
2535
|
+
if (/^[a-z]+:[^ ]+:[^ ]+/i.test(raw)) return true;
|
|
2536
|
+
if (/^[A-Za-z][\w-]*:[^\s]+$/i.test(raw)) return true;
|
|
2537
|
+
if (/^[a-z]+(?:[-_][a-z]+)*[-_][a-z0-9]{5,}$/i.test(raw)) return true;
|
|
2538
|
+
return false;
|
|
2539
|
+
}
|
|
2540
|
+
function readString(payload, key) {
|
|
2541
|
+
if (!payload || typeof payload !== "object") return "";
|
|
2542
|
+
const v = payload[key];
|
|
2543
|
+
return typeof v === "string" ? v : "";
|
|
2544
|
+
}
|
|
2545
|
+
function readArray(payload, key) {
|
|
2546
|
+
if (!payload || typeof payload !== "object") return [];
|
|
2547
|
+
const v = payload[key];
|
|
2548
|
+
return Array.isArray(v) ? v : [];
|
|
2549
|
+
}
|
|
2550
|
+
function normalizePart(value) {
|
|
2551
|
+
return String(value ?? "").toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
|
|
2552
|
+
}
|
|
2553
|
+
function hashString(value) {
|
|
2554
|
+
let hash = 2166136261;
|
|
2555
|
+
for (let i = 0; i < value.length; i++) {
|
|
2556
|
+
hash ^= value.charCodeAt(i);
|
|
2557
|
+
hash = Math.imul(hash, 16777619);
|
|
2558
|
+
}
|
|
2559
|
+
return (hash >>> 0).toString(36);
|
|
2560
|
+
}
|
|
2561
|
+
function stableElementId(parts) {
|
|
2562
|
+
const raw = parts.filter((p) => p !== void 0 && String(p).length > 0).join("|");
|
|
2563
|
+
const readable = parts.map(normalizePart).filter(Boolean).join(":").slice(0, 120);
|
|
2564
|
+
return `el:${readable}:${hashString(raw)}`;
|
|
2565
|
+
}
|
|
2566
|
+
function parseStableElementId(id) {
|
|
2567
|
+
if (typeof id !== "string") return null;
|
|
2568
|
+
const trimmed = id.trim();
|
|
2569
|
+
if (!/^el:/i.test(trimmed)) return null;
|
|
2570
|
+
const parts = trimmed.split(":");
|
|
2571
|
+
if (parts.length < 3) return null;
|
|
2572
|
+
const hash = parts[parts.length - 1];
|
|
2573
|
+
const segments = parts.slice(1, -1);
|
|
2574
|
+
if (segments.length === 0) return null;
|
|
2575
|
+
const role = segments[0];
|
|
2576
|
+
const result = { role, segments, hash };
|
|
2577
|
+
if (role === "option" && segments.length >= 4) {
|
|
2578
|
+
result.listId = segments[segments.length - 2];
|
|
2579
|
+
result.itemKey = segments[segments.length - 1];
|
|
2580
|
+
}
|
|
2581
|
+
return result;
|
|
2582
|
+
}
|
|
2583
|
+
function uniqueTexts(texts) {
|
|
2584
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2585
|
+
const out = [];
|
|
2586
|
+
for (const text of texts) {
|
|
2587
|
+
const t = typeof text === "string" ? text.trim() : "";
|
|
2588
|
+
if (!t || seen.has(t)) continue;
|
|
2589
|
+
seen.add(t);
|
|
2590
|
+
out.push(t);
|
|
2591
|
+
}
|
|
2592
|
+
return out;
|
|
2593
|
+
}
|
|
2594
|
+
function labelForListItem(item) {
|
|
2595
|
+
return uniqueTexts([
|
|
2596
|
+
...item.texts ?? [],
|
|
2597
|
+
...(item.buttons ?? []).map((button) => button.label ?? button.id)
|
|
2598
|
+
]).slice(0, 2).join(" \xB7 ") || void 0;
|
|
2599
|
+
}
|
|
2600
|
+
function listItemElement(route, list, item) {
|
|
2601
|
+
const texts = uniqueTexts([
|
|
2602
|
+
...item.texts ?? [],
|
|
2603
|
+
...(item.buttons ?? []).map((button) => button.label ?? button.id)
|
|
2604
|
+
]);
|
|
2605
|
+
const label = labelForListItem(item);
|
|
2606
|
+
const key = item.itemKey ?? item.reactKey ?? label ?? item.syntheticId;
|
|
2607
|
+
const id = stableElementId([
|
|
2608
|
+
"option",
|
|
2609
|
+
route ?? "unknown",
|
|
2610
|
+
list.id ?? list.label ?? `list-${list.index}`,
|
|
2611
|
+
key
|
|
2612
|
+
]);
|
|
2613
|
+
const inModal = (item.buttons ?? []).some((button) => button.inModal) || (item.inputs ?? []).some((input) => input.inModal) || (item.toggles ?? []).some((toggle) => toggle.inModal);
|
|
2614
|
+
return {
|
|
2615
|
+
id,
|
|
2616
|
+
role: "option",
|
|
2617
|
+
label,
|
|
2618
|
+
texts,
|
|
2619
|
+
actions: ["press"],
|
|
2620
|
+
disabled: false,
|
|
2621
|
+
source: "list",
|
|
2622
|
+
targetId: item.syntheticId,
|
|
2623
|
+
...inModal ? { inModal: true } : {},
|
|
2624
|
+
listContext: {
|
|
2625
|
+
listIndex: list.index,
|
|
2626
|
+
listId: list.id,
|
|
2627
|
+
listLabel: list.label,
|
|
2628
|
+
itemIndex: item.index,
|
|
2629
|
+
itemKey: item.itemKey,
|
|
2630
|
+
reactKey: item.reactKey,
|
|
2631
|
+
syntheticId: item.syntheticId
|
|
2632
|
+
}
|
|
2633
|
+
};
|
|
2634
|
+
}
|
|
2635
|
+
function buttonElement(route, button, index) {
|
|
2636
|
+
const label = button.label ?? button.id;
|
|
2637
|
+
const id = stableElementId([
|
|
2638
|
+
"button",
|
|
2639
|
+
route ?? "unknown",
|
|
2640
|
+
button.id ?? label ?? `button-${index}`
|
|
2641
|
+
]);
|
|
2642
|
+
return {
|
|
2643
|
+
id,
|
|
2644
|
+
role: "button",
|
|
2645
|
+
label,
|
|
2646
|
+
texts: uniqueTexts([label]),
|
|
2647
|
+
actions: ["press"],
|
|
2648
|
+
disabled: button.disabled,
|
|
2649
|
+
selected: button.selected,
|
|
2650
|
+
source: "button",
|
|
2651
|
+
targetId: button.id ?? button.label,
|
|
2652
|
+
...button.inModal ? { inModal: true } : {}
|
|
2653
|
+
};
|
|
2654
|
+
}
|
|
2655
|
+
function inputElement(route, input, index) {
|
|
2656
|
+
const label = input.label ?? input.placeholder ?? input.id;
|
|
2657
|
+
const id = stableElementId([
|
|
2658
|
+
"input",
|
|
2659
|
+
route ?? "unknown",
|
|
2660
|
+
input.id ?? label ?? `input-${index}`
|
|
2661
|
+
]);
|
|
2662
|
+
return {
|
|
2663
|
+
id,
|
|
2664
|
+
role: "input",
|
|
2665
|
+
label,
|
|
2666
|
+
texts: uniqueTexts([label, input.placeholder]),
|
|
2667
|
+
actions: ["focus", "setValue"],
|
|
2668
|
+
disabled: input.editable === false,
|
|
2669
|
+
source: "input",
|
|
2670
|
+
targetId: input.id ?? input.label ?? input.placeholder,
|
|
2671
|
+
...input.inModal ? { inModal: true } : {}
|
|
2672
|
+
};
|
|
2673
|
+
}
|
|
2674
|
+
function toggleElement(route, toggle, index) {
|
|
2675
|
+
const label = toggle.label ?? toggle.id;
|
|
2676
|
+
const id = stableElementId([
|
|
2677
|
+
"toggle",
|
|
2678
|
+
route ?? "unknown",
|
|
2679
|
+
toggle.id ?? label ?? `toggle-${index}`
|
|
2680
|
+
]);
|
|
2681
|
+
return {
|
|
2682
|
+
id,
|
|
2683
|
+
role: "toggle",
|
|
2684
|
+
label,
|
|
2685
|
+
texts: uniqueTexts([label]),
|
|
2686
|
+
actions: ["toggle", "press"],
|
|
2687
|
+
selected: toggle.value,
|
|
2688
|
+
source: "toggle",
|
|
2689
|
+
targetId: toggle.id ?? toggle.label,
|
|
2690
|
+
...toggle.inModal ? { inModal: true } : {}
|
|
2691
|
+
};
|
|
2692
|
+
}
|
|
2693
|
+
function sliderElement(route, slider, index) {
|
|
2694
|
+
const label = slider.label ?? slider.id;
|
|
2695
|
+
const id = stableElementId([
|
|
2696
|
+
"slider",
|
|
2697
|
+
route ?? "unknown",
|
|
2698
|
+
slider.id ?? label ?? `slider-${index}`
|
|
2699
|
+
]);
|
|
2700
|
+
return {
|
|
2701
|
+
id,
|
|
2702
|
+
role: "slider",
|
|
2703
|
+
label,
|
|
2704
|
+
texts: uniqueTexts([label]),
|
|
2705
|
+
actions: ["setValue"],
|
|
2706
|
+
disabled: slider.disabled,
|
|
2707
|
+
source: "slider",
|
|
2708
|
+
targetId: slider.id ?? slider.label,
|
|
2709
|
+
...slider.inModal ? { inModal: true } : {}
|
|
2710
|
+
};
|
|
2711
|
+
}
|
|
2712
|
+
function deriveInteractionElements(snapshot) {
|
|
2713
|
+
const elements = [];
|
|
2714
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2715
|
+
const push = (element) => {
|
|
2716
|
+
if (!element.id || seen.has(element.id)) return;
|
|
2717
|
+
seen.add(element.id);
|
|
2718
|
+
elements.push(element);
|
|
2719
|
+
};
|
|
2720
|
+
for (const list of snapshot.lists ?? []) {
|
|
2721
|
+
for (const item of list.items ?? []) {
|
|
2722
|
+
push(listItemElement(snapshot.route, list, item));
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
for (const [index, button] of (snapshot.buttons ?? []).entries()) {
|
|
2726
|
+
push(buttonElement(snapshot.route, button, index));
|
|
2727
|
+
}
|
|
2728
|
+
for (const [index, input] of (snapshot.inputs ?? []).entries()) {
|
|
2729
|
+
push(inputElement(snapshot.route, input, index));
|
|
2730
|
+
}
|
|
2731
|
+
for (const [index, toggle] of (snapshot.toggles ?? []).entries()) {
|
|
2732
|
+
push(toggleElement(snapshot.route, toggle, index));
|
|
2733
|
+
}
|
|
2734
|
+
for (const [index, slider] of (snapshot.sliders ?? []).entries()) {
|
|
2735
|
+
push(sliderElement(snapshot.route, slider, index));
|
|
2736
|
+
}
|
|
2737
|
+
return elements;
|
|
2738
|
+
}
|
|
2739
|
+
function findElementForChoice(elements, option) {
|
|
2740
|
+
if (option.targetId) {
|
|
2741
|
+
const byTarget = elements.find((element) => element.targetId === option.targetId);
|
|
2742
|
+
if (byTarget) return byTarget;
|
|
2743
|
+
}
|
|
2744
|
+
const optionLabel = normalizePart(option.label ?? option.texts?.join(" "));
|
|
2745
|
+
if (!optionLabel) return void 0;
|
|
2746
|
+
return elements.find((element) => {
|
|
2747
|
+
if (element.role !== "option" && element.role !== "button") return false;
|
|
2748
|
+
const label = normalizePart(element.label ?? element.texts.join(" "));
|
|
2749
|
+
return label === optionLabel || label.includes(optionLabel) || optionLabel.includes(label);
|
|
2750
|
+
});
|
|
2751
|
+
}
|
|
2752
|
+
function attachElementIdsToChoiceGroups(choiceGroups, elements) {
|
|
2753
|
+
return choiceGroups.map((group) => ({
|
|
2754
|
+
...group,
|
|
2755
|
+
options: group.options.map((option) => {
|
|
2756
|
+
const element = findElementForChoice(elements, option);
|
|
2757
|
+
if (!element) return option;
|
|
2758
|
+
const handle = element.listContext?.syntheticId ?? element.id;
|
|
2759
|
+
return {
|
|
2760
|
+
...option,
|
|
2761
|
+
syntheticId: handle,
|
|
2762
|
+
targetId: handle
|
|
2763
|
+
};
|
|
2764
|
+
})
|
|
2765
|
+
}));
|
|
2766
|
+
}
|
|
788
2767
|
|
|
789
2768
|
// src/registry/ComponentRegistry.ts
|
|
790
2769
|
var ComponentRegistryImpl = class {
|
|
@@ -1018,7 +2997,7 @@ function getDefaultRegistry() {
|
|
|
1018
2997
|
}
|
|
1019
2998
|
|
|
1020
2999
|
// src/auto/idExtraction.ts
|
|
1021
|
-
function
|
|
3000
|
+
function normalizeId2(raw) {
|
|
1022
3001
|
return raw.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/^(input-|btn-|toggle-|switch-)/, "").replace(/[^a-z0-9\s-_]/g, "").trim().replace(/[-_\s]+(.)/g, (_, c) => c.toUpperCase()).replace(/[-_\s]+$/, "");
|
|
1023
3002
|
}
|
|
1024
3003
|
function deriveSemanticInputId(props) {
|
|
@@ -1066,7 +3045,7 @@ function sectionListFlatIndex(sections, section, localIndex) {
|
|
|
1066
3045
|
function extractId(props) {
|
|
1067
3046
|
const raw = props?.testID ?? props?.nativeID ?? props?.id ?? props?.accessibilityLabel ?? props?.name ?? props?.placeholder ?? (typeof props?.autoComplete === "string" && props.autoComplete !== "off" ? props.autoComplete : void 0);
|
|
1068
3047
|
if (raw && typeof raw === "string") {
|
|
1069
|
-
const normalized =
|
|
3048
|
+
const normalized = normalizeId2(raw);
|
|
1070
3049
|
if (normalized && normalized.length > 0) {
|
|
1071
3050
|
console.log(`[Appilots] extractId: raw="${raw}" \u2192 normalized="${normalized}"`);
|
|
1072
3051
|
return normalized;
|
|
@@ -1217,7 +3196,7 @@ function componentDisplayName(type) {
|
|
|
1217
3196
|
function extractListId(props, componentName) {
|
|
1218
3197
|
const raw = props?.appilotsListId ?? props?.testID ?? props?.nativeID ?? props?.id ?? props?.accessibilityLabel ?? props?.name;
|
|
1219
3198
|
if (typeof raw === "string" && raw.trim().length > 0) {
|
|
1220
|
-
const normalized =
|
|
3199
|
+
const normalized = normalizeId2(raw);
|
|
1221
3200
|
if (normalized) return normalized;
|
|
1222
3201
|
}
|
|
1223
3202
|
_generatedListIdCounter += 1;
|
|
@@ -1647,65 +3626,6 @@ function getGlobalConfig() {
|
|
|
1647
3626
|
return _globalConfig;
|
|
1648
3627
|
}
|
|
1649
3628
|
|
|
1650
|
-
// src/debug/actionTraceStore.ts
|
|
1651
|
-
var MAX_TRACE_ENTRIES = 200;
|
|
1652
|
-
var entries = [];
|
|
1653
|
-
var listeners = /* @__PURE__ */ new Set();
|
|
1654
|
-
var traceCounter = 0;
|
|
1655
|
-
function notify() {
|
|
1656
|
-
const snapshot = [...entries];
|
|
1657
|
-
listeners.forEach((listener) => listener(snapshot));
|
|
1658
|
-
}
|
|
1659
|
-
function append(entry) {
|
|
1660
|
-
entries.push(entry);
|
|
1661
|
-
if (entries.length > MAX_TRACE_ENTRIES) {
|
|
1662
|
-
entries.splice(0, entries.length - MAX_TRACE_ENTRIES);
|
|
1663
|
-
}
|
|
1664
|
-
notify();
|
|
1665
|
-
}
|
|
1666
|
-
function recordAppilotsDebugTrace(entry) {
|
|
1667
|
-
append({
|
|
1668
|
-
...entry,
|
|
1669
|
-
id: entry.id ?? `trace_${++traceCounter}`
|
|
1670
|
-
});
|
|
1671
|
-
}
|
|
1672
|
-
function actionFromEvent(event) {
|
|
1673
|
-
const action = event.data?.action;
|
|
1674
|
-
if (action && typeof action === "object" && typeof action.id === "string") {
|
|
1675
|
-
return action;
|
|
1676
|
-
}
|
|
1677
|
-
return void 0;
|
|
1678
|
-
}
|
|
1679
|
-
function recordAppilotsEventTrace(event) {
|
|
1680
|
-
if (!event.type.startsWith("agent:action:")) return;
|
|
1681
|
-
const action = actionFromEvent(event);
|
|
1682
|
-
const actionId = action?.id || (typeof event.data?.actionId === "string" ? event.data.actionId : void 0);
|
|
1683
|
-
const actionType = action?.type || (typeof event.data?.actionType === "string" ? event.data.actionType : void 0);
|
|
1684
|
-
recordAppilotsDebugTrace({
|
|
1685
|
-
timestamp: event.timestamp,
|
|
1686
|
-
type: event.type,
|
|
1687
|
-
messageId: action?.messageId,
|
|
1688
|
-
actionId,
|
|
1689
|
-
actionType,
|
|
1690
|
-
status: event.type === "agent:action:start" ? "started" : event.type === "agent:action:complete" ? "completed" : "error",
|
|
1691
|
-
data: event.data
|
|
1692
|
-
});
|
|
1693
|
-
}
|
|
1694
|
-
function getAppilotsDebugTraces() {
|
|
1695
|
-
return [...entries];
|
|
1696
|
-
}
|
|
1697
|
-
function clearAppilotsDebugTraces() {
|
|
1698
|
-
entries.splice(0, entries.length);
|
|
1699
|
-
notify();
|
|
1700
|
-
}
|
|
1701
|
-
function subscribeAppilotsDebugTraces(listener) {
|
|
1702
|
-
listeners.add(listener);
|
|
1703
|
-
listener([...entries]);
|
|
1704
|
-
return () => {
|
|
1705
|
-
listeners.delete(listener);
|
|
1706
|
-
};
|
|
1707
|
-
}
|
|
1708
|
-
|
|
1709
3629
|
// src/introspection/fiberRoot.ts
|
|
1710
3630
|
var _fiberRoot = null;
|
|
1711
3631
|
function setFiberRoot(fiber) {
|
|
@@ -2351,20 +4271,27 @@ function clearScreenRegistry() {
|
|
|
2351
4271
|
screenRegistry.clear();
|
|
2352
4272
|
}
|
|
2353
4273
|
|
|
4274
|
+
exports.ActionQueueMachine = ActionQueueMachine;
|
|
2354
4275
|
exports.AppilotsClient = AppilotsClient;
|
|
2355
4276
|
exports.AppilotsProvider = AppilotsProvider;
|
|
2356
4277
|
exports.AppilotsRegistryProvider = AppilotsRegistryProvider;
|
|
4278
|
+
exports.ChatSessionMachine = ChatSessionMachine;
|
|
4279
|
+
exports.OPTIONAL_STEP_AUTOMATION_HINT = OPTIONAL_STEP_AUTOMATION_HINT;
|
|
2357
4280
|
exports.SDK_VERSION = SDK_VERSION;
|
|
2358
|
-
exports.StreamAbortError = StreamAbortError;
|
|
2359
|
-
exports.StreamTransportError = StreamTransportError;
|
|
2360
4281
|
exports._patchJsxRuntimes = _patchJsxRuntimes;
|
|
4282
|
+
exports.actionPressTargetId = actionPressTargetId;
|
|
2361
4283
|
exports.appilotsDebugWarn = appilotsDebugWarn;
|
|
4284
|
+
exports.attachElementIdsToChoiceGroups = attachElementIdsToChoiceGroups;
|
|
2362
4285
|
exports.clearAppilotsDebugTraces = clearAppilotsDebugTraces;
|
|
2363
4286
|
exports.clearScreenRegistry = clearScreenRegistry;
|
|
2364
4287
|
exports.componentRegistry = componentRegistry;
|
|
2365
4288
|
exports.createComponentRegistry = createComponentRegistry;
|
|
2366
4289
|
exports.createElementRegistry = createElementRegistry;
|
|
2367
4290
|
exports.createListRegistry = createListRegistry;
|
|
4291
|
+
exports.defaultDarkTheme = defaultDarkTheme;
|
|
4292
|
+
exports.defaultLightTheme = defaultLightTheme;
|
|
4293
|
+
exports.deriveInteractionElements = deriveInteractionElements;
|
|
4294
|
+
exports.describeAction = describeAction;
|
|
2368
4295
|
exports.elementRegistry = elementRegistry;
|
|
2369
4296
|
exports.enableAppilotsAutoTracking = enableAppilotsAutoTracking;
|
|
2370
4297
|
exports.getActiveRouteNames = getActiveRouteNames;
|
|
@@ -2378,16 +4305,28 @@ exports.getGlobalConfig = getGlobalConfig;
|
|
|
2378
4305
|
exports.getNavigationRef = getNavigationRef;
|
|
2379
4306
|
exports.getNavigationStateSnapshot = getNavigationStateSnapshot;
|
|
2380
4307
|
exports.getScreenMetadata = getScreenMetadata;
|
|
4308
|
+
exports.humanizeError = humanizeError;
|
|
4309
|
+
exports.idLooselyMatches = idLooselyMatches;
|
|
2381
4310
|
exports.initAppilots = initAppilots;
|
|
2382
4311
|
exports.isAutoTrackingEnabled = isAutoTrackingEnabled;
|
|
4312
|
+
exports.isGenericSelectValue = isGenericSelectValue;
|
|
4313
|
+
exports.isListItemPressTarget = isListItemPressTarget;
|
|
4314
|
+
exports.isOptionalStepAutomationFailure = isOptionalStepAutomationFailure;
|
|
2383
4315
|
exports.listRegistry = listRegistry;
|
|
4316
|
+
exports.looksDestructiveActionLabel = looksDestructiveActionLabel;
|
|
4317
|
+
exports.matchScore = matchScore;
|
|
4318
|
+
exports.mergeThemeTokens = mergeThemeTokens;
|
|
4319
|
+
exports.normalize3 = normalize3;
|
|
4320
|
+
exports.parseStableElementId = parseStableElementId;
|
|
2384
4321
|
exports.probeLoadingState = probeLoadingState;
|
|
2385
4322
|
exports.recordAppilotsDebugTrace = recordAppilotsDebugTrace;
|
|
2386
4323
|
exports.registerScreen = registerScreen;
|
|
4324
|
+
exports.resolveBaseTheme = resolveBaseTheme;
|
|
2387
4325
|
exports.routesBelongToSameFeature = routesBelongToSameFeature;
|
|
2388
4326
|
exports.screenDepartedBaseline = screenDepartedBaseline;
|
|
2389
4327
|
exports.setCurrentScreen = setCurrentScreen;
|
|
2390
4328
|
exports.setNavigationRef = setNavigationRef;
|
|
4329
|
+
exports.snapshotShowsOpenCreateForm = snapshotShowsOpenCreateForm;
|
|
2391
4330
|
exports.subscribeAppilotsDebugTraces = subscribeAppilotsDebugTraces;
|
|
2392
4331
|
exports.useAppilotsContext = useAppilotsContext;
|
|
2393
4332
|
exports.useResolvedRegistry = useResolvedRegistry;
|