@appilots/sdk 0.2.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/LICENSE +21 -0
- package/README.md +38 -0
- package/dist/chunk-GEM6TPAK.mjs +5834 -0
- package/dist/chunk-HDLZGXNF.mjs +2348 -0
- package/dist/chunk-IASJ7YHC.js +5847 -0
- package/dist/chunk-KCDNL4HV.mjs +61 -0
- package/dist/chunk-KGN3DNGX.js +67 -0
- package/dist/chunk-SIBKF7AA.js +2395 -0
- package/dist/hooks/index.d.mts +2 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.js +43 -0
- package/dist/hooks/index.mjs +2 -0
- package/dist/index-BDb1WVNo.d.mts +1300 -0
- package/dist/index-BDb1WVNo.d.ts +1300 -0
- package/dist/index.d.mts +1156 -0
- package/dist/index.d.ts +1156 -0
- package/dist/index.js +2836 -0
- package/dist/index.mjs +2658 -0
- package/dist/navigation/index.d.mts +43 -0
- package/dist/navigation/index.d.ts +43 -0
- package/dist/navigation/index.js +51 -0
- package/dist/navigation/index.mjs +2 -0
- package/dist/registerScreen-D1oGm28T.d.mts +159 -0
- package/dist/registerScreen-D1oGm28T.d.ts +159 -0
- package/metro.js +158 -0
- package/package.json +90 -0
|
@@ -0,0 +1,2395 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var React2 = require('react');
|
|
4
|
+
var reactNative = require('react-native');
|
|
5
|
+
|
|
6
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
7
|
+
|
|
8
|
+
var React2__default = /*#__PURE__*/_interopDefault(React2);
|
|
9
|
+
|
|
10
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
11
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
12
|
+
}) : x)(function(x) {
|
|
13
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
14
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// src/version.ts
|
|
18
|
+
var SDK_VERSION = "0.1.0";
|
|
19
|
+
|
|
20
|
+
// src/services/AppilotsClient.ts
|
|
21
|
+
var StreamTransportError = class extends Error {
|
|
22
|
+
constructor(message) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = "StreamTransportError";
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
var StreamAbortError = class extends Error {
|
|
28
|
+
partialContent;
|
|
29
|
+
constructor(partialContent) {
|
|
30
|
+
super("Stream aborted by caller");
|
|
31
|
+
this.name = "AbortError";
|
|
32
|
+
this.partialContent = partialContent;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
var CONTINUATION_TIMEOUT_MS = 15e4;
|
|
36
|
+
var AppilotsClient = class {
|
|
37
|
+
baseUrl;
|
|
38
|
+
projectId;
|
|
39
|
+
headers;
|
|
40
|
+
timeout;
|
|
41
|
+
debug;
|
|
42
|
+
mcpVersion;
|
|
43
|
+
user;
|
|
44
|
+
sessionId = null;
|
|
45
|
+
/** Guard so identify fires at most once per client instance. */
|
|
46
|
+
identified = false;
|
|
47
|
+
constructor(options) {
|
|
48
|
+
this.projectId = options.projectId;
|
|
49
|
+
this.baseUrl = (options.apiBaseUrl ?? "https://api.appilots.com").replace(/\/$/, "");
|
|
50
|
+
this.timeout = options.timeout ?? 6e4;
|
|
51
|
+
this.debug = options.debug ?? false;
|
|
52
|
+
this.mcpVersion = options.mcpVersion;
|
|
53
|
+
this.user = options.user;
|
|
54
|
+
this.headers = {
|
|
55
|
+
"Content-Type": "application/json",
|
|
56
|
+
// Version telemetry: lets the server correlate behavior per SDK
|
|
57
|
+
// release and pick the MCP doc matching this app build (B.4).
|
|
58
|
+
"X-Appilots-Sdk-Version": SDK_VERSION,
|
|
59
|
+
...options.appVersion ? { "X-App-Version": options.appVersion } : {},
|
|
60
|
+
...options.mcpVersion ? { "X-Appilots-Mcp-Version": options.mcpVersion } : {},
|
|
61
|
+
...options.apiKey ? { Authorization: `Bearer ${options.apiKey}` } : {},
|
|
62
|
+
...options.headers
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
log(...args) {
|
|
66
|
+
if (this.debug) console.log("[AppilotsClient]", ...args);
|
|
67
|
+
}
|
|
68
|
+
describeNonJsonResponse(url, response, body) {
|
|
69
|
+
const contentType = response.headers?.get?.("content-type") ?? "unknown content-type";
|
|
70
|
+
const snippet = body.replace(/\s+/g, " ").trim().slice(0, 120);
|
|
71
|
+
const looksHtml = /^\s*</.test(body) || contentType.includes("text/html");
|
|
72
|
+
const hint = looksHtml && this.baseUrl.includes("localhost:3000") ? " This looks like the web dashboard/dev server. For local Docker, use http://localhost:4000/api/v1 as apiBaseUrl." : "";
|
|
73
|
+
return `Appilots API returned non-JSON response for ${url} (status=${response.status}, content-type=${contentType}).${hint}` + (snippet ? ` Body starts with: ${JSON.stringify(snippet)}` : "");
|
|
74
|
+
}
|
|
75
|
+
async request(path, options = {}, timeoutMs = this.timeout) {
|
|
76
|
+
const url = `${this.baseUrl}${path}`;
|
|
77
|
+
this.log(`${options.method ?? "GET"} ${url}`);
|
|
78
|
+
const controller = new AbortController();
|
|
79
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
80
|
+
try {
|
|
81
|
+
const response = await fetch(url, {
|
|
82
|
+
...options,
|
|
83
|
+
headers: { ...this.headers, ...options.headers },
|
|
84
|
+
signal: controller.signal
|
|
85
|
+
});
|
|
86
|
+
const body = await response.text();
|
|
87
|
+
let json = null;
|
|
88
|
+
if (body.length > 0) {
|
|
89
|
+
try {
|
|
90
|
+
json = JSON.parse(body);
|
|
91
|
+
} catch {
|
|
92
|
+
throw new Error(this.describeNonJsonResponse(url, response, body));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
const errorMsg = json?.error?.message ?? json?.error ?? `HTTP ${response.status}`;
|
|
97
|
+
throw new Error(errorMsg);
|
|
98
|
+
}
|
|
99
|
+
return json.data ?? json;
|
|
100
|
+
} finally {
|
|
101
|
+
clearTimeout(timeoutId);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// ── Session ───────────────────────────────────────────────────
|
|
105
|
+
async createSession(userId, deviceInfo) {
|
|
106
|
+
const data = await this.request("/agent/session", {
|
|
107
|
+
method: "POST",
|
|
108
|
+
// Fall back to the configured user's id so sessions carry the
|
|
109
|
+
// externalUserId even when callers don't thread it through
|
|
110
|
+
// (useAppilotsChat auto-creates sessions with no args).
|
|
111
|
+
body: JSON.stringify({ userId: userId ?? this.user?.id, deviceInfo })
|
|
112
|
+
});
|
|
113
|
+
this.sessionId = data.sessionId;
|
|
114
|
+
this.log("Session created:", this.sessionId);
|
|
115
|
+
void this.identifyConfiguredUser();
|
|
116
|
+
return this.sessionId;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Upsert the configured user into the end-users registry. Fires once
|
|
120
|
+
* per client, after the first session opens; fire-and-forget — a
|
|
121
|
+
* failed identify must never break the chat.
|
|
122
|
+
*/
|
|
123
|
+
async identifyConfiguredUser() {
|
|
124
|
+
if (this.identified) return;
|
|
125
|
+
const user = this.user;
|
|
126
|
+
if (!user?.id || !user.name && !user.identifiers) return;
|
|
127
|
+
this.identified = true;
|
|
128
|
+
try {
|
|
129
|
+
await this.request("/agent/identify", {
|
|
130
|
+
method: "POST",
|
|
131
|
+
body: JSON.stringify({
|
|
132
|
+
userId: user.id,
|
|
133
|
+
...user.name ? { name: user.name } : {},
|
|
134
|
+
...user.identifiers ? { identifiers: user.identifiers } : {}
|
|
135
|
+
})
|
|
136
|
+
});
|
|
137
|
+
this.log("User identified:", user.id);
|
|
138
|
+
} catch (err) {
|
|
139
|
+
this.identified = false;
|
|
140
|
+
this.log("identify failed (suppressed):", err);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async destroySession() {
|
|
144
|
+
if (!this.sessionId) return;
|
|
145
|
+
try {
|
|
146
|
+
await this.request(`/agent/session/${this.sessionId}`, { method: "DELETE" });
|
|
147
|
+
this.log("Session destroyed:", this.sessionId);
|
|
148
|
+
} finally {
|
|
149
|
+
this.sessionId = null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
getSessionId() {
|
|
153
|
+
return this.sessionId;
|
|
154
|
+
}
|
|
155
|
+
// ── Messages ──────────────────────────────────────────────────
|
|
156
|
+
async sendMessage(content, context) {
|
|
157
|
+
if (!this.sessionId) {
|
|
158
|
+
await this.createSession();
|
|
159
|
+
}
|
|
160
|
+
const data = await this.request("/agent/message", {
|
|
161
|
+
method: "POST",
|
|
162
|
+
body: JSON.stringify({
|
|
163
|
+
content,
|
|
164
|
+
sessionId: this.sessionId,
|
|
165
|
+
context,
|
|
166
|
+
// Optional MCP version bundled with this app build. Server
|
|
167
|
+
// compares vs the active MCP doc to detect stale uploads.
|
|
168
|
+
...this.mcpVersion ? { mcpVersion: this.mcpVersion } : {}
|
|
169
|
+
})
|
|
170
|
+
});
|
|
171
|
+
if (data.sessionId) {
|
|
172
|
+
this.sessionId = data.sessionId;
|
|
173
|
+
}
|
|
174
|
+
const message = {
|
|
175
|
+
id: data.message.id,
|
|
176
|
+
role: "assistant",
|
|
177
|
+
content: data.message.content,
|
|
178
|
+
timestamp: Date.now()
|
|
179
|
+
};
|
|
180
|
+
const actions = data.message.actions?.map((a) => {
|
|
181
|
+
const rewritten = this.rewriteActionTypeIfNeeded(a.type, a.payload);
|
|
182
|
+
return {
|
|
183
|
+
id: a.id,
|
|
184
|
+
type: rewritten.type,
|
|
185
|
+
payload: this.normalizePayload(rewritten.type, rewritten.payload),
|
|
186
|
+
status: "pending"
|
|
187
|
+
};
|
|
188
|
+
});
|
|
189
|
+
const summaryMessage = data.message.summary ? {
|
|
190
|
+
id: `${data.message.id}_summary`,
|
|
191
|
+
role: "assistant",
|
|
192
|
+
content: data.message.summary,
|
|
193
|
+
timestamp: Date.now()
|
|
194
|
+
} : void 0;
|
|
195
|
+
this.log("Response received:", { messageId: message.id, actions: actions?.length ?? 0, hasSummary: !!summaryMessage });
|
|
196
|
+
return {
|
|
197
|
+
sessionId: this.sessionId,
|
|
198
|
+
message,
|
|
199
|
+
summaryMessage,
|
|
200
|
+
actions,
|
|
201
|
+
usage: data.usage
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Streaming variant of `sendMessage` — consumes the server's SSE
|
|
206
|
+
* endpoint (`/agent/message/stream`) and fires `onTextDelta` per chunk
|
|
207
|
+
* so the UI can render tokens as they arrive (OKR-008 KR1). Resolves
|
|
208
|
+
* with the same `SendMessageResponse` shape once the `done` event lands.
|
|
209
|
+
*
|
|
210
|
+
* Uses XMLHttpRequest because React Native's fetch does not expose
|
|
211
|
+
* `response.body` for incremental reads; RN's XHR delivers progressive
|
|
212
|
+
* `responseText`, which is the standard SSE transport on RN.
|
|
213
|
+
*
|
|
214
|
+
* Errors:
|
|
215
|
+
* - `StreamTransportError` — transport failed before ANY event; safe
|
|
216
|
+
* for the caller to fall back to the non-streaming endpoint.
|
|
217
|
+
* - `StreamAbortError` (name='AbortError') — options.signal fired;
|
|
218
|
+
* carries `partialContent` so the UI can keep what streamed.
|
|
219
|
+
*/
|
|
220
|
+
async sendMessageStream(content, context, options = {}) {
|
|
221
|
+
if (!this.sessionId) {
|
|
222
|
+
await this.createSession();
|
|
223
|
+
}
|
|
224
|
+
const url = `${this.baseUrl}/agent/message/stream`;
|
|
225
|
+
this.log(`POST ${url} (stream)`);
|
|
226
|
+
const XhrCtor = globalThis.XMLHttpRequest;
|
|
227
|
+
if (!XhrCtor) {
|
|
228
|
+
throw new StreamTransportError("XMLHttpRequest is not available in this environment");
|
|
229
|
+
}
|
|
230
|
+
return new Promise((resolve, reject) => {
|
|
231
|
+
const xhr = new XhrCtor();
|
|
232
|
+
let parsedIndex = 0;
|
|
233
|
+
let fullText = "";
|
|
234
|
+
let sawAnyEvent = false;
|
|
235
|
+
let settled = false;
|
|
236
|
+
let doneEvent = null;
|
|
237
|
+
const rawActions = [];
|
|
238
|
+
let idleTimer;
|
|
239
|
+
const cleanup = () => {
|
|
240
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
241
|
+
if (options.signal) options.signal.removeEventListener("abort", onAbort);
|
|
242
|
+
};
|
|
243
|
+
const fail = (err) => {
|
|
244
|
+
if (settled) return;
|
|
245
|
+
settled = true;
|
|
246
|
+
cleanup();
|
|
247
|
+
reject(err);
|
|
248
|
+
};
|
|
249
|
+
const onAbort = () => {
|
|
250
|
+
try {
|
|
251
|
+
xhr.abort();
|
|
252
|
+
} catch {
|
|
253
|
+
}
|
|
254
|
+
fail(new StreamAbortError(fullText));
|
|
255
|
+
};
|
|
256
|
+
const armIdleTimer = () => {
|
|
257
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
258
|
+
idleTimer = setTimeout(() => {
|
|
259
|
+
try {
|
|
260
|
+
xhr.abort();
|
|
261
|
+
} catch {
|
|
262
|
+
}
|
|
263
|
+
fail(
|
|
264
|
+
sawAnyEvent ? new Error(`Stream went silent for ${this.timeout}ms`) : new StreamTransportError(`Stream connection timed out after ${this.timeout}ms`)
|
|
265
|
+
);
|
|
266
|
+
}, this.timeout);
|
|
267
|
+
};
|
|
268
|
+
const handleEvent = (event) => {
|
|
269
|
+
sawAnyEvent = true;
|
|
270
|
+
switch (event.type) {
|
|
271
|
+
case "session": {
|
|
272
|
+
const sid = event.sessionId;
|
|
273
|
+
if (typeof sid === "string" && sid) {
|
|
274
|
+
this.sessionId = sid;
|
|
275
|
+
options.onSession?.(sid);
|
|
276
|
+
}
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
case "text-delta": {
|
|
280
|
+
const chunk = typeof event.content === "string" ? event.content : "";
|
|
281
|
+
if (chunk) {
|
|
282
|
+
fullText += chunk;
|
|
283
|
+
options.onTextDelta?.(chunk, fullText);
|
|
284
|
+
}
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
case "tool-call": {
|
|
288
|
+
const action = event.action;
|
|
289
|
+
if (action?.id && action.type) rawActions.push(action);
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
case "done": {
|
|
293
|
+
doneEvent = {
|
|
294
|
+
messageId: typeof event.messageId === "string" ? event.messageId : `msg_${Date.now()}`,
|
|
295
|
+
usage: event.usage
|
|
296
|
+
};
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
const consumeBuffer = (isFinal) => {
|
|
302
|
+
const text = xhr.responseText ?? "";
|
|
303
|
+
let boundary = text.lastIndexOf("\n\n");
|
|
304
|
+
if (isFinal) boundary = text.length;
|
|
305
|
+
if (boundary <= parsedIndex) return;
|
|
306
|
+
const slice = text.slice(parsedIndex, boundary);
|
|
307
|
+
parsedIndex = boundary;
|
|
308
|
+
for (const frame of slice.split("\n\n")) {
|
|
309
|
+
for (const line of frame.split("\n")) {
|
|
310
|
+
if (!line.startsWith("data: ")) continue;
|
|
311
|
+
try {
|
|
312
|
+
handleEvent(JSON.parse(line.slice(6)));
|
|
313
|
+
} catch {
|
|
314
|
+
this.log("Skipping unparseable SSE line:", line.slice(0, 120));
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
const finish = () => {
|
|
320
|
+
if (settled) return;
|
|
321
|
+
consumeBuffer(true);
|
|
322
|
+
const contentType = String(xhr.getResponseHeader?.("content-type") ?? "");
|
|
323
|
+
if (xhr.status !== 200 || !contentType.includes("text/event-stream")) {
|
|
324
|
+
let message2 = `HTTP ${xhr.status || 0}`;
|
|
325
|
+
try {
|
|
326
|
+
const json = JSON.parse(xhr.responseText || "{}");
|
|
327
|
+
message2 = json?.error?.message ?? json?.error ?? message2;
|
|
328
|
+
} catch {
|
|
329
|
+
}
|
|
330
|
+
fail(
|
|
331
|
+
sawAnyEvent ? new Error(message2) : new StreamTransportError(message2)
|
|
332
|
+
);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (!doneEvent) {
|
|
336
|
+
fail(
|
|
337
|
+
sawAnyEvent ? new Error("Stream ended before the done event") : new StreamTransportError("Stream ended without any event")
|
|
338
|
+
);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
settled = true;
|
|
342
|
+
cleanup();
|
|
343
|
+
const message = {
|
|
344
|
+
id: doneEvent.messageId,
|
|
345
|
+
role: "assistant",
|
|
346
|
+
content: fullText,
|
|
347
|
+
timestamp: Date.now()
|
|
348
|
+
};
|
|
349
|
+
const actions = rawActions.length > 0 ? rawActions.map((a) => {
|
|
350
|
+
const rewritten = this.rewriteActionTypeIfNeeded(a.type, a.payload);
|
|
351
|
+
return {
|
|
352
|
+
id: a.id,
|
|
353
|
+
type: rewritten.type,
|
|
354
|
+
payload: this.normalizePayload(rewritten.type, rewritten.payload),
|
|
355
|
+
status: "pending"
|
|
356
|
+
};
|
|
357
|
+
}) : void 0;
|
|
358
|
+
this.log("Stream complete:", { messageId: message.id, actions: actions?.length ?? 0 });
|
|
359
|
+
resolve({
|
|
360
|
+
sessionId: this.sessionId,
|
|
361
|
+
message,
|
|
362
|
+
actions,
|
|
363
|
+
usage: doneEvent.usage ? { tokensUsed: doneEvent.usage.tokensUsed, modelUsed: "", latencyMs: 0 } : void 0
|
|
364
|
+
});
|
|
365
|
+
};
|
|
366
|
+
if (options.signal) {
|
|
367
|
+
if (options.signal.aborted) {
|
|
368
|
+
fail(new StreamAbortError(""));
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
options.signal.addEventListener("abort", onAbort);
|
|
372
|
+
}
|
|
373
|
+
xhr.open("POST", url);
|
|
374
|
+
for (const [key, value] of Object.entries(this.headers)) {
|
|
375
|
+
xhr.setRequestHeader(key, value);
|
|
376
|
+
}
|
|
377
|
+
xhr.setRequestHeader("Accept", "text/event-stream");
|
|
378
|
+
xhr.onprogress = () => {
|
|
379
|
+
armIdleTimer();
|
|
380
|
+
consumeBuffer(false);
|
|
381
|
+
};
|
|
382
|
+
xhr.onload = finish;
|
|
383
|
+
xhr.onerror = () => fail(
|
|
384
|
+
sawAnyEvent ? new Error("Stream connection lost") : new StreamTransportError("Network error opening stream")
|
|
385
|
+
);
|
|
386
|
+
xhr.onabort = () => {
|
|
387
|
+
fail(new StreamAbortError(fullText));
|
|
388
|
+
};
|
|
389
|
+
armIdleTimer();
|
|
390
|
+
xhr.send(
|
|
391
|
+
JSON.stringify({
|
|
392
|
+
content,
|
|
393
|
+
sessionId: this.sessionId,
|
|
394
|
+
context,
|
|
395
|
+
...this.mcpVersion ? { mcpVersion: this.mcpVersion } : {}
|
|
396
|
+
})
|
|
397
|
+
);
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
// ── Agent loop continuation ──────────────────────────────────
|
|
401
|
+
async continueAgent(results, context, hop) {
|
|
402
|
+
if (!this.sessionId) {
|
|
403
|
+
throw new Error("continueAgent called without an active session");
|
|
404
|
+
}
|
|
405
|
+
const data = await this.request(
|
|
406
|
+
"/agent/continue",
|
|
407
|
+
{
|
|
408
|
+
method: "POST",
|
|
409
|
+
body: JSON.stringify({
|
|
410
|
+
sessionId: this.sessionId,
|
|
411
|
+
results,
|
|
412
|
+
context,
|
|
413
|
+
hop
|
|
414
|
+
})
|
|
415
|
+
},
|
|
416
|
+
// A continuation hop rides the provider's retry budget: on a TPM/429
|
|
417
|
+
// storm the server only recovers after its exponential backoff
|
|
418
|
+
// (2+4+8+16+32s ≈ 62s) and answers at ~66s — measured repeatedly on
|
|
419
|
+
// the Detox destructive-mission runs. Aborting at the default 60s
|
|
420
|
+
// kills the automation one hop before the answer arrives ("Perdi o
|
|
421
|
+
// fio (Aborted)") while the server-side turn completes and orphans
|
|
422
|
+
// its actions. Give continuations room to outlive one full backoff
|
|
423
|
+
// cycle; an explicitly larger user-configured timeout still wins.
|
|
424
|
+
Math.max(this.timeout, CONTINUATION_TIMEOUT_MS)
|
|
425
|
+
);
|
|
426
|
+
const message = {
|
|
427
|
+
id: data.message.id,
|
|
428
|
+
role: "assistant",
|
|
429
|
+
content: data.message.content,
|
|
430
|
+
timestamp: Date.now()
|
|
431
|
+
};
|
|
432
|
+
const actions = data.message.actions?.map((a) => {
|
|
433
|
+
const rewritten = this.rewriteActionTypeIfNeeded(a.type, a.payload);
|
|
434
|
+
return {
|
|
435
|
+
id: a.id,
|
|
436
|
+
type: rewritten.type,
|
|
437
|
+
payload: this.normalizePayload(rewritten.type, rewritten.payload),
|
|
438
|
+
status: "pending"
|
|
439
|
+
};
|
|
440
|
+
});
|
|
441
|
+
return {
|
|
442
|
+
sessionId: this.sessionId,
|
|
443
|
+
message,
|
|
444
|
+
actions,
|
|
445
|
+
usage: data.usage,
|
|
446
|
+
isDone: data.isDone,
|
|
447
|
+
hop: data.hop
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
// ── Actions ───────────────────────────────────────────────────
|
|
451
|
+
async executeAction(actionId) {
|
|
452
|
+
const data = await this.request(`/agent/action/${actionId}/execute`, {
|
|
453
|
+
method: "POST"
|
|
454
|
+
});
|
|
455
|
+
return {
|
|
456
|
+
id: data.actionId,
|
|
457
|
+
type: data.type,
|
|
458
|
+
payload: data.payload,
|
|
459
|
+
status: "executing"
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Rewrite an action's type when the AI used a semantically-misclassified
|
|
464
|
+
* action type. The most common case: the LLM emits
|
|
465
|
+
* { type: 'ui_interaction', payload: { action: 'select', targetId, value } }
|
|
466
|
+
* to mean "set this field's value", which is really a form_fill. We detect
|
|
467
|
+
* that here and convert it before normalization, so the executor sees the
|
|
468
|
+
* right action shape and the field's setValue() is called instead of a
|
|
469
|
+
* meaningless press on a non-pressable component.
|
|
470
|
+
*/
|
|
471
|
+
rewriteActionTypeIfNeeded(type, payload) {
|
|
472
|
+
if (type === "ui_interaction" && payload && String(payload.action ?? "").toLowerCase() === "select" && payload.value !== void 0) {
|
|
473
|
+
const targetId = payload.targetId ?? payload.componentId ?? "";
|
|
474
|
+
return {
|
|
475
|
+
type: "form_fill",
|
|
476
|
+
payload: {
|
|
477
|
+
screenName: "",
|
|
478
|
+
fields: [
|
|
479
|
+
{
|
|
480
|
+
fieldId: targetId,
|
|
481
|
+
fieldType: "select",
|
|
482
|
+
value: payload.value
|
|
483
|
+
}
|
|
484
|
+
]
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
return { type, payload };
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Normalize API tool call payloads to match SDK's expected payload types.
|
|
492
|
+
* The AI model may return slightly different field names than what the
|
|
493
|
+
* SDK executor expects, and may use vocabulary (e.g. "select", "scroll_to",
|
|
494
|
+
* "open") that the SDK doesn't natively support — we remap those to the
|
|
495
|
+
* nearest equivalent here so the executor never sees an "unknown action".
|
|
496
|
+
*/
|
|
497
|
+
normalizePayload(type, payload) {
|
|
498
|
+
if (!payload) return payload;
|
|
499
|
+
switch (type) {
|
|
500
|
+
case "form_fill":
|
|
501
|
+
return {
|
|
502
|
+
screenName: payload.screenName ?? "",
|
|
503
|
+
fields: (payload.fields ?? []).map((f) => ({
|
|
504
|
+
fieldId: f.fieldId,
|
|
505
|
+
fieldType: f.fieldType ?? "text",
|
|
506
|
+
value: f.value,
|
|
507
|
+
label: f.label
|
|
508
|
+
})),
|
|
509
|
+
submitAfterFill: payload.submitAfterFill === true ? true : void 0
|
|
510
|
+
};
|
|
511
|
+
case "navigate":
|
|
512
|
+
return {
|
|
513
|
+
screenName: payload.screenName ?? "",
|
|
514
|
+
params: payload.params,
|
|
515
|
+
navigationAction: payload.navigationAction ?? "navigate",
|
|
516
|
+
path: Array.isArray(payload.path) ? payload.path : void 0
|
|
517
|
+
};
|
|
518
|
+
case "ui_interaction": {
|
|
519
|
+
const rawAction = payload.action ?? "press";
|
|
520
|
+
const actionMap = {
|
|
521
|
+
press: "press",
|
|
522
|
+
tap: "press",
|
|
523
|
+
click: "press",
|
|
524
|
+
toggle: "press",
|
|
525
|
+
// toggle handled via toggle registry inside handler
|
|
526
|
+
open: "press",
|
|
527
|
+
close: "press",
|
|
528
|
+
refresh: "press",
|
|
529
|
+
long_press: "longPress",
|
|
530
|
+
longpress: "longPress",
|
|
531
|
+
longPress: "longPress",
|
|
532
|
+
scroll: "scroll",
|
|
533
|
+
scroll_to: "scroll",
|
|
534
|
+
scrollto: "scroll",
|
|
535
|
+
focus: "focus",
|
|
536
|
+
// Slider verb (set_value capability). MUST be in this map: the
|
|
537
|
+
// `?? 'press'` default below swallows unknown verbs, and a
|
|
538
|
+
// swallowed set_value becomes a press that can fuzzy-match a
|
|
539
|
+
// completely unrelated control. Every new ui_interaction verb
|
|
540
|
+
// has to be added here — see docs/adding-component-capabilities.md.
|
|
541
|
+
set_value: "set_value",
|
|
542
|
+
setvalue: "set_value",
|
|
543
|
+
setValue: "set_value",
|
|
544
|
+
slide: "set_value",
|
|
545
|
+
adjust: "set_value"
|
|
546
|
+
};
|
|
547
|
+
const mapped = actionMap[rawAction] ?? actionMap[rawAction.toLowerCase()] ?? "press";
|
|
548
|
+
return {
|
|
549
|
+
...payload,
|
|
550
|
+
componentId: payload.targetId ?? payload.componentId ?? "",
|
|
551
|
+
targetId: payload.targetId ?? payload.componentId ?? "",
|
|
552
|
+
action: mapped,
|
|
553
|
+
params: payload.params ?? (payload.value !== void 0 ? { value: payload.value } : void 0)
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
case "scroll_list": {
|
|
557
|
+
const rawIndex = payload.toIndex ?? payload.index ?? payload.itemIndex;
|
|
558
|
+
const toIndex = typeof rawIndex === "number" && Number.isFinite(rawIndex) ? Math.trunc(rawIndex) : void 0;
|
|
559
|
+
const rawDirection = String(payload.direction ?? "").toLowerCase();
|
|
560
|
+
const direction = rawDirection === "up" || rawDirection === "down" ? rawDirection : void 0;
|
|
561
|
+
return {
|
|
562
|
+
listId: typeof payload.listId === "string" && payload.listId.length > 0 ? payload.listId : typeof payload.targetId === "string" && payload.targetId.length > 0 ? payload.targetId : void 0,
|
|
563
|
+
toIndex,
|
|
564
|
+
direction
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
case "confirm": {
|
|
568
|
+
const destructive = payload.destructive === true || payload.severity === "danger";
|
|
569
|
+
return {
|
|
570
|
+
...payload,
|
|
571
|
+
title: payload.title ?? (destructive ? "A\xE7\xE3o destrutiva" : "Confirmation"),
|
|
572
|
+
message: payload.message ?? payload.actionDescription ?? "",
|
|
573
|
+
destructive,
|
|
574
|
+
severity: payload.severity ?? (destructive ? "danger" : void 0)
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
default:
|
|
578
|
+
return payload;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Legacy per-action status report. The agentic chat loop reports via
|
|
583
|
+
* `continueAgent` (which carries diagnose + a fresh snapshot); this
|
|
584
|
+
* endpoint remains for apps driving actions manually through
|
|
585
|
+
* `useAppilotsActions`. `diagnose` is forwarded when provided so even
|
|
586
|
+
* the legacy path gives the server a failure category.
|
|
587
|
+
*/
|
|
588
|
+
async completeAction(actionId, success, error, diagnose) {
|
|
589
|
+
await this.request(`/agent/action/${actionId}/complete`, {
|
|
590
|
+
method: "POST",
|
|
591
|
+
body: JSON.stringify({ success, error, ...diagnose ? { diagnose } : {} })
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Fetch actions the server has persisted but that haven't been
|
|
596
|
+
* completed yet. Use after an SSE drop or a hard error to recover
|
|
597
|
+
* the batch without losing the actions the model already decided.
|
|
598
|
+
*
|
|
599
|
+
* Returns an empty array if there are no pending actions or if the
|
|
600
|
+
* call fails — never throws.
|
|
601
|
+
*/
|
|
602
|
+
async recoverPendingActions(sessionId) {
|
|
603
|
+
const sid = sessionId ?? this.sessionId;
|
|
604
|
+
if (!sid) return [];
|
|
605
|
+
try {
|
|
606
|
+
const data = await this.request(`/agent/session/${sid}/pending-actions`);
|
|
607
|
+
return data.actions.map(
|
|
608
|
+
(a) => ({
|
|
609
|
+
id: a.id,
|
|
610
|
+
type: a.type,
|
|
611
|
+
payload: a.payload,
|
|
612
|
+
status: "pending"
|
|
613
|
+
})
|
|
614
|
+
);
|
|
615
|
+
} catch (err) {
|
|
616
|
+
this.log("recoverPendingActions failed (suppressed):", err);
|
|
617
|
+
return [];
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
// ── Suggested Prompts ─────────────────────────────────────────
|
|
621
|
+
/**
|
|
622
|
+
* Fetch dev-defined and empirically-popular prompts to seed the chat
|
|
623
|
+
* empty-state with one-tap chips. Cheap (cached server-side for 1h) but
|
|
624
|
+
* still a network hit, so callers should debounce around chat-open.
|
|
625
|
+
*
|
|
626
|
+
* @param screen Optional — when provided, response includes screen-scoped
|
|
627
|
+
* popular prompts in addition to globals.
|
|
628
|
+
*/
|
|
629
|
+
async getPopularPrompts(screen, limit) {
|
|
630
|
+
const queryParts = [];
|
|
631
|
+
if (screen) queryParts.push(`screen=${encodeURIComponent(screen)}`);
|
|
632
|
+
if (limit) queryParts.push(`limit=${encodeURIComponent(String(limit))}`);
|
|
633
|
+
const qs = queryParts.length > 0 ? `?${queryParts.join("&")}` : "";
|
|
634
|
+
const path = `/agent/popular-prompts${qs}`;
|
|
635
|
+
return this.request(path, { method: "GET" });
|
|
636
|
+
}
|
|
637
|
+
// ── Personalization (runtime config) ──────────────────────────
|
|
638
|
+
/**
|
|
639
|
+
* Fetch the project's dashboard-configured personalization (theme,
|
|
640
|
+
* branding, FAB color/image, locale...). Called by `AppilotsProvider`
|
|
641
|
+
* once on mount — cheap (cached server-side for 1h) and applied by
|
|
642
|
+
* `AppilotsChat` as a fallback for props not set explicitly in code.
|
|
643
|
+
*
|
|
644
|
+
* Throws on network/HTTP failure — the provider treats any failure as
|
|
645
|
+
* "no remote config" and stays on props/defaults.
|
|
646
|
+
*/
|
|
647
|
+
async getPersonalization() {
|
|
648
|
+
return this.request("/agent/personalization", { method: "GET" });
|
|
649
|
+
}
|
|
650
|
+
// ── Human escalation (docs/human-escalation.md) ───────────────
|
|
651
|
+
/**
|
|
652
|
+
* Ask for a human operator. Idempotent: if the session already has
|
|
653
|
+
* an open escalation the server returns it instead of creating a
|
|
654
|
+
* duplicate. Creates the session first when the chat had none yet
|
|
655
|
+
* (the server sends it back and we adopt it, so the whole
|
|
656
|
+
* conversation stays under one sessionId).
|
|
657
|
+
*/
|
|
658
|
+
async requestEscalation(reason, note) {
|
|
659
|
+
if (!this.sessionId) {
|
|
660
|
+
await this.createSession();
|
|
661
|
+
}
|
|
662
|
+
const data = await this.request("/agent/escalation", {
|
|
663
|
+
method: "POST",
|
|
664
|
+
body: JSON.stringify({
|
|
665
|
+
...this.sessionId ? { sessionId: this.sessionId } : {},
|
|
666
|
+
reason,
|
|
667
|
+
...note ? { note } : {}
|
|
668
|
+
})
|
|
669
|
+
});
|
|
670
|
+
if (data.sessionId) this.sessionId = data.sessionId;
|
|
671
|
+
return {
|
|
672
|
+
escalationId: data.escalationId,
|
|
673
|
+
status: data.status === "active" ? "active" : "pending"
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Tell the server the user walked away from the conversation (chat
|
|
678
|
+
* cleared) so the escalation is closed instead of leaving the
|
|
679
|
+
* operator talking to nobody. Fire-and-forget: never throws — the
|
|
680
|
+
* local cleanup must proceed even if the network is gone.
|
|
681
|
+
*/
|
|
682
|
+
async abandonEscalation(escalationId) {
|
|
683
|
+
try {
|
|
684
|
+
await this.request(`/agent/escalation/${escalationId}/abandon`, { method: "POST" });
|
|
685
|
+
} catch (err) {
|
|
686
|
+
this.log("abandonEscalation failed (suppressed):", err);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
/** User message routed to the human operator (no LLM involved). */
|
|
690
|
+
async sendEscalationMessage(escalationId, content) {
|
|
691
|
+
await this.request(`/agent/escalation/${escalationId}/message`, {
|
|
692
|
+
method: "POST",
|
|
693
|
+
body: JSON.stringify({ content })
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Poll fallback / reconnect catch-up: status + operator messages
|
|
698
|
+
* created after `since` (ISO timestamp of the last one we have).
|
|
699
|
+
*/
|
|
700
|
+
async getEscalationUpdates(escalationId, since) {
|
|
701
|
+
const qs = since ? `?since=${encodeURIComponent(since)}` : "";
|
|
702
|
+
return this.request(`/agent/escalation/${escalationId}${qs}`);
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Open the escalation SSE channel — operator replies + status
|
|
706
|
+
* changes pushed live. Same XHR transport as `sendMessageStream`
|
|
707
|
+
* (RN's fetch can't read a body incrementally).
|
|
708
|
+
*
|
|
709
|
+
* Long-lived by design: no idle watchdog — the server pings every
|
|
710
|
+
* 25s, and a genuinely dead connection surfaces as `onClose`, which
|
|
711
|
+
* the hook answers with a backoff reconnect + `since` catch-up.
|
|
712
|
+
* Returns a close function; closing never fires `onClose`.
|
|
713
|
+
*/
|
|
714
|
+
openEscalationStream(escalationId, handlers) {
|
|
715
|
+
const XhrCtor = globalThis.XMLHttpRequest;
|
|
716
|
+
if (!XhrCtor) {
|
|
717
|
+
handlers.onClose();
|
|
718
|
+
return () => {
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
const qs = handlers.since ? `?since=${encodeURIComponent(handlers.since)}` : "";
|
|
722
|
+
const url = `${this.baseUrl}/agent/escalation/${escalationId}/stream${qs}`;
|
|
723
|
+
this.log(`GET ${url} (escalation stream)`);
|
|
724
|
+
const xhr = new XhrCtor();
|
|
725
|
+
let parsedIndex = 0;
|
|
726
|
+
let closedByCaller = false;
|
|
727
|
+
let ended = false;
|
|
728
|
+
const end = () => {
|
|
729
|
+
if (ended || closedByCaller) return;
|
|
730
|
+
ended = true;
|
|
731
|
+
handlers.onClose();
|
|
732
|
+
};
|
|
733
|
+
const handleEvent = (event) => {
|
|
734
|
+
if (event.type === "human-message") {
|
|
735
|
+
const m = event.message;
|
|
736
|
+
if (m?.id && typeof m.content === "string") {
|
|
737
|
+
handlers.onHumanMessage({ id: m.id, content: m.content, createdAt: m.createdAt });
|
|
738
|
+
}
|
|
739
|
+
} else if (event.type === "status") {
|
|
740
|
+
const status = event.status;
|
|
741
|
+
if (status === "pending" || status === "active" || status === "resolved") {
|
|
742
|
+
handlers.onStatus(
|
|
743
|
+
status,
|
|
744
|
+
typeof event.operatorName === "string" ? event.operatorName : void 0
|
|
745
|
+
);
|
|
746
|
+
}
|
|
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;
|
|
758
|
+
try {
|
|
759
|
+
handleEvent(JSON.parse(line.slice(6)));
|
|
760
|
+
} catch {
|
|
761
|
+
this.log("Skipping unparseable escalation SSE line:", line.slice(0, 120));
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
xhr.open("GET", url);
|
|
767
|
+
for (const [key, value] of Object.entries(this.headers)) {
|
|
768
|
+
xhr.setRequestHeader(key, value);
|
|
769
|
+
}
|
|
770
|
+
xhr.setRequestHeader("Accept", "text/event-stream");
|
|
771
|
+
xhr.onprogress = consumeBuffer;
|
|
772
|
+
xhr.onload = () => {
|
|
773
|
+
consumeBuffer();
|
|
774
|
+
end();
|
|
775
|
+
};
|
|
776
|
+
xhr.onerror = end;
|
|
777
|
+
xhr.onabort = end;
|
|
778
|
+
xhr.send();
|
|
779
|
+
return () => {
|
|
780
|
+
closedByCaller = true;
|
|
781
|
+
try {
|
|
782
|
+
xhr.abort();
|
|
783
|
+
} catch {
|
|
784
|
+
}
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
// src/registry/ComponentRegistry.ts
|
|
790
|
+
var ComponentRegistryImpl = class {
|
|
791
|
+
components = /* @__PURE__ */ new Map();
|
|
792
|
+
listeners = /* @__PURE__ */ new Set();
|
|
793
|
+
/**
|
|
794
|
+
* Register a component. If an entry with the same ID already exists,
|
|
795
|
+
* it is replaced (this handles re-renders / hot-reload).
|
|
796
|
+
*/
|
|
797
|
+
register(id, entry) {
|
|
798
|
+
this.components.set(id, entry);
|
|
799
|
+
console.log(`[Appilots] ComponentRegistry.register: id="${id}" kind="${entry.kind}" \u2014 total count: ${this.components.size}`);
|
|
800
|
+
this.notify(id, entry);
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Unregister a component (typically on unmount).
|
|
804
|
+
*/
|
|
805
|
+
unregister(id) {
|
|
806
|
+
this.components.delete(id);
|
|
807
|
+
console.log(`[Appilots] ComponentRegistry.unregister: id="${id}" \u2014 total count: ${this.components.size}`);
|
|
808
|
+
this.notify(id, null);
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Look up a component by its ID.
|
|
812
|
+
*/
|
|
813
|
+
get(id) {
|
|
814
|
+
return this.components.get(id);
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Get a typed entry or undefined.
|
|
818
|
+
*/
|
|
819
|
+
getField(id) {
|
|
820
|
+
const entry = this.components.get(id);
|
|
821
|
+
return entry?.kind === "field" ? entry : void 0;
|
|
822
|
+
}
|
|
823
|
+
getTarget(id) {
|
|
824
|
+
const entry = this.components.get(id);
|
|
825
|
+
return entry?.kind === "target" ? entry : void 0;
|
|
826
|
+
}
|
|
827
|
+
getToggle(id) {
|
|
828
|
+
const entry = this.components.get(id);
|
|
829
|
+
return entry?.kind === "toggle" ? entry : void 0;
|
|
830
|
+
}
|
|
831
|
+
getSlider(id) {
|
|
832
|
+
const entry = this.components.get(id);
|
|
833
|
+
return entry?.kind === "slider" ? entry : void 0;
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Get all components on a given screen.
|
|
837
|
+
*/
|
|
838
|
+
getByScreen(screen) {
|
|
839
|
+
const result = /* @__PURE__ */ new Map();
|
|
840
|
+
for (const [id, entry] of this.components) {
|
|
841
|
+
if (entry.screen === screen) {
|
|
842
|
+
result.set(id, entry);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
return result;
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* Get all components of a given kind.
|
|
849
|
+
*/
|
|
850
|
+
getByKind(kind) {
|
|
851
|
+
const result = /* @__PURE__ */ new Map();
|
|
852
|
+
for (const [id, entry] of this.components) {
|
|
853
|
+
if (entry.kind === kind) {
|
|
854
|
+
result.set(id, entry);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
return result;
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* List all registered component IDs with their kind and screen.
|
|
861
|
+
* Useful for debugging and for the AI to know what's available.
|
|
862
|
+
*/
|
|
863
|
+
snapshot() {
|
|
864
|
+
return Array.from(this.components.entries()).map(([id, entry]) => ({
|
|
865
|
+
id,
|
|
866
|
+
kind: entry.kind,
|
|
867
|
+
screen: entry.screen,
|
|
868
|
+
label: entry.label
|
|
869
|
+
}));
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* Subscribe to registry changes.
|
|
873
|
+
* Returns an unsubscribe function.
|
|
874
|
+
*/
|
|
875
|
+
subscribe(listener) {
|
|
876
|
+
this.listeners.add(listener);
|
|
877
|
+
return () => {
|
|
878
|
+
this.listeners.delete(listener);
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Clear all entries (useful for testing or full reset).
|
|
883
|
+
*/
|
|
884
|
+
clear() {
|
|
885
|
+
this.components.clear();
|
|
886
|
+
}
|
|
887
|
+
/** Number of registered components */
|
|
888
|
+
get size() {
|
|
889
|
+
return this.components.size;
|
|
890
|
+
}
|
|
891
|
+
notify(id, entry) {
|
|
892
|
+
for (const listener of this.listeners) {
|
|
893
|
+
try {
|
|
894
|
+
listener(id, entry);
|
|
895
|
+
} catch {
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
};
|
|
900
|
+
var componentRegistry = new ComponentRegistryImpl();
|
|
901
|
+
function createComponentRegistry() {
|
|
902
|
+
return new ComponentRegistryImpl();
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// src/registry/ListRegistry.ts
|
|
906
|
+
var ListRegistryImpl = class {
|
|
907
|
+
lists = /* @__PURE__ */ new Map();
|
|
908
|
+
listeners = /* @__PURE__ */ new Set();
|
|
909
|
+
register(id, entry) {
|
|
910
|
+
this.lists.set(id, entry);
|
|
911
|
+
console.log(
|
|
912
|
+
`[Appilots] ListRegistry.register: id="${id}" component="${entry.component}" items=${entry.itemCount ?? "unknown"} \u2014 total count: ${this.lists.size}`
|
|
913
|
+
);
|
|
914
|
+
this.notify(id, entry);
|
|
915
|
+
}
|
|
916
|
+
unregister(id) {
|
|
917
|
+
this.lists.delete(id);
|
|
918
|
+
console.log(`[Appilots] ListRegistry.unregister: id="${id}" \u2014 total count: ${this.lists.size}`);
|
|
919
|
+
this.notify(id, null);
|
|
920
|
+
}
|
|
921
|
+
get(id) {
|
|
922
|
+
return this.lists.get(id);
|
|
923
|
+
}
|
|
924
|
+
snapshot() {
|
|
925
|
+
return Array.from(this.lists.values());
|
|
926
|
+
}
|
|
927
|
+
subscribe(listener) {
|
|
928
|
+
this.listeners.add(listener);
|
|
929
|
+
return () => {
|
|
930
|
+
this.listeners.delete(listener);
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
clear() {
|
|
934
|
+
this.lists.clear();
|
|
935
|
+
}
|
|
936
|
+
get size() {
|
|
937
|
+
return this.lists.size;
|
|
938
|
+
}
|
|
939
|
+
notify(id, entry) {
|
|
940
|
+
for (const listener of this.listeners) {
|
|
941
|
+
try {
|
|
942
|
+
listener(id, entry);
|
|
943
|
+
} catch {
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
};
|
|
948
|
+
var listRegistry = new ListRegistryImpl();
|
|
949
|
+
function createListRegistry() {
|
|
950
|
+
return new ListRegistryImpl();
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// src/registry/ElementRegistry.ts
|
|
954
|
+
var ElementRegistryImpl = class {
|
|
955
|
+
elements = /* @__PURE__ */ new Map();
|
|
956
|
+
listeners = /* @__PURE__ */ new Set();
|
|
957
|
+
replaceAll(elements) {
|
|
958
|
+
const next = /* @__PURE__ */ new Map();
|
|
959
|
+
for (const element of elements) {
|
|
960
|
+
if (!element.id) continue;
|
|
961
|
+
next.set(element.id, element);
|
|
962
|
+
}
|
|
963
|
+
this.elements = next;
|
|
964
|
+
console.log(`[Appilots] ElementRegistry.replaceAll: elements=${this.elements.size}`);
|
|
965
|
+
}
|
|
966
|
+
register(id, entry) {
|
|
967
|
+
this.elements.set(id, entry);
|
|
968
|
+
this.notify(id, entry);
|
|
969
|
+
}
|
|
970
|
+
unregister(id) {
|
|
971
|
+
this.elements.delete(id);
|
|
972
|
+
this.notify(id, null);
|
|
973
|
+
}
|
|
974
|
+
get(id) {
|
|
975
|
+
return this.elements.get(id);
|
|
976
|
+
}
|
|
977
|
+
snapshot() {
|
|
978
|
+
return Array.from(this.elements.values());
|
|
979
|
+
}
|
|
980
|
+
subscribe(listener) {
|
|
981
|
+
this.listeners.add(listener);
|
|
982
|
+
return () => {
|
|
983
|
+
this.listeners.delete(listener);
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
clear() {
|
|
987
|
+
this.elements.clear();
|
|
988
|
+
}
|
|
989
|
+
get size() {
|
|
990
|
+
return this.elements.size;
|
|
991
|
+
}
|
|
992
|
+
notify(id, entry) {
|
|
993
|
+
for (const listener of this.listeners) {
|
|
994
|
+
try {
|
|
995
|
+
listener(id, entry);
|
|
996
|
+
} catch {
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
};
|
|
1001
|
+
var elementRegistry = new ElementRegistryImpl();
|
|
1002
|
+
function createElementRegistry() {
|
|
1003
|
+
return new ElementRegistryImpl();
|
|
1004
|
+
}
|
|
1005
|
+
var ComponentRegistryContext = React2__default.default.createContext(null);
|
|
1006
|
+
function AppilotsRegistryProvider({
|
|
1007
|
+
value,
|
|
1008
|
+
children
|
|
1009
|
+
}) {
|
|
1010
|
+
return /* @__PURE__ */ React2__default.default.createElement(ComponentRegistryContext.Provider, { value }, children);
|
|
1011
|
+
}
|
|
1012
|
+
function useResolvedRegistry() {
|
|
1013
|
+
const ctx = React2__default.default.useContext(ComponentRegistryContext);
|
|
1014
|
+
return ctx ?? componentRegistry;
|
|
1015
|
+
}
|
|
1016
|
+
function getDefaultRegistry() {
|
|
1017
|
+
return componentRegistry;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// src/auto/idExtraction.ts
|
|
1021
|
+
function normalizeId(raw) {
|
|
1022
|
+
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
|
+
}
|
|
1024
|
+
function deriveSemanticInputId(props) {
|
|
1025
|
+
if (!props || typeof props !== "object") return null;
|
|
1026
|
+
if (props.secureTextEntry === true) return "password";
|
|
1027
|
+
const km = props.keyboardType ?? props.inputMode;
|
|
1028
|
+
if (typeof km === "string" && km.length > 0) {
|
|
1029
|
+
switch (km) {
|
|
1030
|
+
case "email-address":
|
|
1031
|
+
case "email":
|
|
1032
|
+
return "email";
|
|
1033
|
+
case "numeric":
|
|
1034
|
+
case "number-pad":
|
|
1035
|
+
case "decimal-pad":
|
|
1036
|
+
case "numeric-pad":
|
|
1037
|
+
return "numericInput";
|
|
1038
|
+
case "phone-pad":
|
|
1039
|
+
case "tel":
|
|
1040
|
+
return "phoneInput";
|
|
1041
|
+
case "url":
|
|
1042
|
+
return "urlInput";
|
|
1043
|
+
case "search":
|
|
1044
|
+
return "searchInput";
|
|
1045
|
+
default:
|
|
1046
|
+
return `${km.replace(/[^a-z0-9]/gi, "")}Input` || null;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
return null;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// src/auto/sectionListIndex.ts
|
|
1053
|
+
function sectionListFlatIndex(sections, section, localIndex) {
|
|
1054
|
+
if (!Array.isArray(sections)) return localIndex;
|
|
1055
|
+
const sectionIndex = sections.indexOf(section);
|
|
1056
|
+
if (sectionIndex < 0) return localIndex;
|
|
1057
|
+
let base = 0;
|
|
1058
|
+
for (let i = 0; i < sectionIndex; i++) {
|
|
1059
|
+
const data = sections[i]?.data;
|
|
1060
|
+
base += Array.isArray(data) ? data.length : 0;
|
|
1061
|
+
}
|
|
1062
|
+
return base + localIndex;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// src/auto/enableAutoTracking.tsx
|
|
1066
|
+
function extractId(props) {
|
|
1067
|
+
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
|
+
if (raw && typeof raw === "string") {
|
|
1069
|
+
const normalized = normalizeId(raw);
|
|
1070
|
+
if (normalized && normalized.length > 0) {
|
|
1071
|
+
console.log(`[Appilots] extractId: raw="${raw}" \u2192 normalized="${normalized}"`);
|
|
1072
|
+
return normalized;
|
|
1073
|
+
}
|
|
1074
|
+
console.log(`[Appilots] extractId: raw="${raw}" normalized to empty string, falling back`);
|
|
1075
|
+
}
|
|
1076
|
+
const hint = deriveSemanticInputId(props);
|
|
1077
|
+
if (hint) {
|
|
1078
|
+
console.warn(
|
|
1079
|
+
`[Appilots] extractId: no testID/nativeID/accessibilityLabel/name/placeholder/autoComplete on input \u2014 falling back to semantic hint "${hint}". Add testID to make this field reliably addressable.`
|
|
1080
|
+
);
|
|
1081
|
+
return hint;
|
|
1082
|
+
}
|
|
1083
|
+
console.log(
|
|
1084
|
+
"[Appilots] extractId: no identifiable prop found (testID/nativeID/accessibilityLabel/name/placeholder/autoComplete missing); skipping registration"
|
|
1085
|
+
);
|
|
1086
|
+
return null;
|
|
1087
|
+
}
|
|
1088
|
+
function extractLabel(props) {
|
|
1089
|
+
return props?.accessibilityLabel ?? props?.placeholder ?? props?.name ?? props?.testID ?? props?.nativeID;
|
|
1090
|
+
}
|
|
1091
|
+
var _originalCreateElement = React2__default.default.createElement;
|
|
1092
|
+
var TrackedTextInput = React2__default.default.forwardRef(function TrackedTextInput2(props, forwardedRef) {
|
|
1093
|
+
const registry = useResolvedRegistry();
|
|
1094
|
+
const id = extractId(props);
|
|
1095
|
+
const valueRef = React2__default.default.useRef(props.value ?? props.defaultValue ?? "");
|
|
1096
|
+
const onChangeRef = React2__default.default.useRef(props.onChangeText);
|
|
1097
|
+
const inputRef = React2__default.default.useRef(null);
|
|
1098
|
+
valueRef.current = props.value ?? props.defaultValue ?? "";
|
|
1099
|
+
onChangeRef.current = props.onChangeText;
|
|
1100
|
+
React2__default.default.useEffect(() => {
|
|
1101
|
+
if (!id) {
|
|
1102
|
+
console.log("[Appilots] TrackedTextInput useEffect: no id extracted, skipping registration");
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
console.log(`[Appilots] TrackedTextInput useEffect: registering field id="${id}" label="${extractLabel(props)}"`);
|
|
1106
|
+
const entry = {
|
|
1107
|
+
kind: "field",
|
|
1108
|
+
getValue: () => valueRef.current,
|
|
1109
|
+
setValue: (v) => {
|
|
1110
|
+
console.log(`[Appilots] TrackedTextInput setValue called: id="${id}" value="${v}"`);
|
|
1111
|
+
onChangeRef.current?.(v);
|
|
1112
|
+
inputRef.current?.setNativeProps?.({ text: v });
|
|
1113
|
+
},
|
|
1114
|
+
focus: () => inputRef.current?.focus?.(),
|
|
1115
|
+
fieldType: props.secureTextEntry ? "text" : props.keyboardType === "numeric" ? "number" : "text",
|
|
1116
|
+
label: extractLabel(props)
|
|
1117
|
+
};
|
|
1118
|
+
registry.register(id, entry);
|
|
1119
|
+
return () => {
|
|
1120
|
+
console.log(`[Appilots] TrackedTextInput useEffect cleanup: unregistering field id="${id}"`);
|
|
1121
|
+
registry.unregister(id);
|
|
1122
|
+
};
|
|
1123
|
+
}, [id]);
|
|
1124
|
+
const mergedRef = React2__default.default.useCallback((instance) => {
|
|
1125
|
+
inputRef.current = instance;
|
|
1126
|
+
if (typeof forwardedRef === "function") forwardedRef(instance);
|
|
1127
|
+
else if (forwardedRef && typeof forwardedRef === "object") forwardedRef.current = instance;
|
|
1128
|
+
}, [forwardedRef]);
|
|
1129
|
+
return _originalCreateElement(reactNative.TextInput, { ...props, ref: mergedRef });
|
|
1130
|
+
});
|
|
1131
|
+
var TrackedSwitch = React2__default.default.forwardRef(function TrackedSwitch2(props, forwardedRef) {
|
|
1132
|
+
const registry = useResolvedRegistry();
|
|
1133
|
+
const id = extractId(props);
|
|
1134
|
+
const valueRef = React2__default.default.useRef(props.value ?? false);
|
|
1135
|
+
const onChangeRef = React2__default.default.useRef(props.onValueChange);
|
|
1136
|
+
valueRef.current = props.value ?? false;
|
|
1137
|
+
onChangeRef.current = props.onValueChange;
|
|
1138
|
+
React2__default.default.useEffect(() => {
|
|
1139
|
+
if (!id) {
|
|
1140
|
+
console.log("[Appilots] TrackedSwitch useEffect: no id extracted, skipping registration");
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
console.log(`[Appilots] TrackedSwitch useEffect: registering toggle id="${id}" label="${extractLabel(props)}"`);
|
|
1144
|
+
const entry = {
|
|
1145
|
+
kind: "toggle",
|
|
1146
|
+
getValue: () => valueRef.current,
|
|
1147
|
+
setValue: (v) => {
|
|
1148
|
+
console.log(`[Appilots] TrackedSwitch setValue called: id="${id}" value=${v}`);
|
|
1149
|
+
onChangeRef.current?.(v);
|
|
1150
|
+
},
|
|
1151
|
+
label: extractLabel(props)
|
|
1152
|
+
};
|
|
1153
|
+
registry.register(id, entry);
|
|
1154
|
+
return () => {
|
|
1155
|
+
console.log(`[Appilots] TrackedSwitch useEffect cleanup: unregistering toggle id="${id}"`);
|
|
1156
|
+
registry.unregister(id);
|
|
1157
|
+
};
|
|
1158
|
+
}, [id]);
|
|
1159
|
+
return _originalCreateElement(reactNative.Switch, { ...props, ref: forwardedRef });
|
|
1160
|
+
});
|
|
1161
|
+
var _pressableWrapperCache = /* @__PURE__ */ new Map();
|
|
1162
|
+
function getTrackedPressable(OriginalComponent) {
|
|
1163
|
+
let cached = _pressableWrapperCache.get(OriginalComponent);
|
|
1164
|
+
if (cached) return cached;
|
|
1165
|
+
cached = React2__default.default.forwardRef(function TrackedPressable(props, forwardedRef) {
|
|
1166
|
+
const registry = useResolvedRegistry();
|
|
1167
|
+
const id = extractId(props);
|
|
1168
|
+
const onPressRef = React2__default.default.useRef(props.onPress);
|
|
1169
|
+
onPressRef.current = props.onPress;
|
|
1170
|
+
React2__default.default.useEffect(() => {
|
|
1171
|
+
if (props.__appilotsSkip === true || props.__appilotsInternal === true || props.appilotsSkip === true) {
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
if (!id || !props.onPress) {
|
|
1175
|
+
console.log(`[Appilots] TrackedPressable useEffect: skipping registration (id=${id}, hasOnPress=${!!props.onPress})`);
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
console.log(`[Appilots] TrackedPressable useEffect: registering target id="${id}" label="${extractLabel(props)}"`);
|
|
1179
|
+
const entry = {
|
|
1180
|
+
kind: "target",
|
|
1181
|
+
press: () => {
|
|
1182
|
+
console.log(`[Appilots] TrackedPressable press called: id="${id}"`);
|
|
1183
|
+
onPressRef.current?.();
|
|
1184
|
+
},
|
|
1185
|
+
label: extractLabel(props)
|
|
1186
|
+
};
|
|
1187
|
+
registry.register(id, entry);
|
|
1188
|
+
return () => {
|
|
1189
|
+
console.log(`[Appilots] TrackedPressable useEffect cleanup: unregistering target id="${id}"`);
|
|
1190
|
+
registry.unregister(id);
|
|
1191
|
+
};
|
|
1192
|
+
}, [id, !!props.onPress]);
|
|
1193
|
+
return _originalCreateElement(OriginalComponent, { ...props, ref: forwardedRef });
|
|
1194
|
+
});
|
|
1195
|
+
_pressableWrapperCache.set(OriginalComponent, cached);
|
|
1196
|
+
return cached;
|
|
1197
|
+
}
|
|
1198
|
+
var LIST_COMPONENT_NAMES = /* @__PURE__ */ new Set([
|
|
1199
|
+
"FlatList",
|
|
1200
|
+
"SectionList",
|
|
1201
|
+
"VirtualizedList",
|
|
1202
|
+
"FlashList"
|
|
1203
|
+
]);
|
|
1204
|
+
var _listWrapperCache = /* @__PURE__ */ new Map();
|
|
1205
|
+
var _generatedListIdCounter = 0;
|
|
1206
|
+
function componentDisplayName(type) {
|
|
1207
|
+
if (!type) return "";
|
|
1208
|
+
if (typeof type === "string") return type;
|
|
1209
|
+
if (typeof type === "function") return type.displayName ?? type.name ?? "";
|
|
1210
|
+
if (typeof type === "object") {
|
|
1211
|
+
if (typeof type.displayName === "string") return type.displayName;
|
|
1212
|
+
if (type.render) return componentDisplayName(type.render);
|
|
1213
|
+
if (type.type) return componentDisplayName(type.type);
|
|
1214
|
+
}
|
|
1215
|
+
return "";
|
|
1216
|
+
}
|
|
1217
|
+
function extractListId(props, componentName) {
|
|
1218
|
+
const raw = props?.appilotsListId ?? props?.testID ?? props?.nativeID ?? props?.id ?? props?.accessibilityLabel ?? props?.name;
|
|
1219
|
+
if (typeof raw === "string" && raw.trim().length > 0) {
|
|
1220
|
+
const normalized = normalizeId(raw);
|
|
1221
|
+
if (normalized) return normalized;
|
|
1222
|
+
}
|
|
1223
|
+
_generatedListIdCounter += 1;
|
|
1224
|
+
return `appilots-${componentName.toLowerCase()}-${_generatedListIdCounter}`;
|
|
1225
|
+
}
|
|
1226
|
+
function listLabel(props) {
|
|
1227
|
+
return props?.accessibilityLabel ?? props?.testID ?? props?.nativeID ?? props?.id ?? props?.name;
|
|
1228
|
+
}
|
|
1229
|
+
function resolveItemCount(props) {
|
|
1230
|
+
if (Array.isArray(props?.data)) return props.data.length;
|
|
1231
|
+
if (props?.data && typeof props.getItemCount === "function") {
|
|
1232
|
+
try {
|
|
1233
|
+
const count = props.getItemCount(props.data);
|
|
1234
|
+
return typeof count === "number" && Number.isFinite(count) ? count : void 0;
|
|
1235
|
+
} catch {
|
|
1236
|
+
return void 0;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
if (Array.isArray(props?.sections)) {
|
|
1240
|
+
return props.sections.reduce((acc, section) => {
|
|
1241
|
+
const data = Array.isArray(section?.data) ? section.data : [];
|
|
1242
|
+
return acc + data.length;
|
|
1243
|
+
}, 0);
|
|
1244
|
+
}
|
|
1245
|
+
return void 0;
|
|
1246
|
+
}
|
|
1247
|
+
var MAX_DATA_PREVIEW_ITEMS = 60;
|
|
1248
|
+
var MAX_DATA_PREVIEW_TEXT = 80;
|
|
1249
|
+
var PREVIEW_TEXT_KEYS = [
|
|
1250
|
+
"name",
|
|
1251
|
+
"nome",
|
|
1252
|
+
"title",
|
|
1253
|
+
"titulo",
|
|
1254
|
+
"label",
|
|
1255
|
+
"description",
|
|
1256
|
+
"descricao",
|
|
1257
|
+
"plate",
|
|
1258
|
+
"placa",
|
|
1259
|
+
"model",
|
|
1260
|
+
"modelo",
|
|
1261
|
+
"email",
|
|
1262
|
+
"username"
|
|
1263
|
+
];
|
|
1264
|
+
function projectItemText(item) {
|
|
1265
|
+
if (item == null) return "";
|
|
1266
|
+
if (typeof item === "string") return item.slice(0, MAX_DATA_PREVIEW_TEXT);
|
|
1267
|
+
if (typeof item === "number" || typeof item === "boolean") return String(item);
|
|
1268
|
+
if (typeof item !== "object") return "";
|
|
1269
|
+
const record = item;
|
|
1270
|
+
const parts = [];
|
|
1271
|
+
for (const key of PREVIEW_TEXT_KEYS) {
|
|
1272
|
+
const value = record[key];
|
|
1273
|
+
if (typeof value === "string" && value.trim()) parts.push(value.trim());
|
|
1274
|
+
else if (typeof value === "number") parts.push(String(value));
|
|
1275
|
+
if (parts.length >= 3) break;
|
|
1276
|
+
}
|
|
1277
|
+
if (parts.length === 0) {
|
|
1278
|
+
for (const value of Object.values(record)) {
|
|
1279
|
+
if (typeof value === "string" && value.trim()) parts.push(value.trim());
|
|
1280
|
+
else if (typeof value === "number") parts.push(String(value));
|
|
1281
|
+
if (parts.length >= 2) break;
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
return parts.join(" \xB7 ").slice(0, MAX_DATA_PREVIEW_TEXT);
|
|
1285
|
+
}
|
|
1286
|
+
function buildDataPreview(props) {
|
|
1287
|
+
let items;
|
|
1288
|
+
if (Array.isArray(props?.data)) {
|
|
1289
|
+
items = props.data;
|
|
1290
|
+
} else if (Array.isArray(props?.sections)) {
|
|
1291
|
+
items = props.sections.flatMap(
|
|
1292
|
+
(section) => Array.isArray(section?.data) ? section.data : []
|
|
1293
|
+
);
|
|
1294
|
+
}
|
|
1295
|
+
if (!items || items.length === 0) return void 0;
|
|
1296
|
+
const out = [];
|
|
1297
|
+
const cap = Math.min(items.length, MAX_DATA_PREVIEW_ITEMS);
|
|
1298
|
+
for (let i = 0; i < cap; i++) {
|
|
1299
|
+
const text = projectItemText(items[i]);
|
|
1300
|
+
if (!text) continue;
|
|
1301
|
+
out.push({
|
|
1302
|
+
index: i,
|
|
1303
|
+
key: resolveItemKey(props, items[i], i),
|
|
1304
|
+
text
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
return out.length > 0 ? out : void 0;
|
|
1308
|
+
}
|
|
1309
|
+
function readScrollMetrics(instance) {
|
|
1310
|
+
try {
|
|
1311
|
+
const metrics = instance?._scrollMetrics ?? instance?._listRef?._scrollMetrics;
|
|
1312
|
+
if (!metrics) return void 0;
|
|
1313
|
+
const { offset, visibleLength, contentLength } = metrics;
|
|
1314
|
+
if (typeof offset !== "number" || typeof visibleLength !== "number" || typeof contentLength !== "number" || !Number.isFinite(offset) || !Number.isFinite(visibleLength) || !Number.isFinite(contentLength)) {
|
|
1315
|
+
return void 0;
|
|
1316
|
+
}
|
|
1317
|
+
return { offset, visibleLength, contentLength };
|
|
1318
|
+
} catch {
|
|
1319
|
+
return void 0;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
function flatIndexToSectionLocation(sections, flatIndex) {
|
|
1323
|
+
let remaining = flatIndex;
|
|
1324
|
+
for (let sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) {
|
|
1325
|
+
const data = Array.isArray(sections[sectionIndex]?.data) ? sections[sectionIndex].data : [];
|
|
1326
|
+
if (remaining < data.length) return { sectionIndex, itemIndex: remaining };
|
|
1327
|
+
remaining -= data.length;
|
|
1328
|
+
}
|
|
1329
|
+
return null;
|
|
1330
|
+
}
|
|
1331
|
+
function resolveItemKey(props, item, index) {
|
|
1332
|
+
if (typeof props?.keyExtractor === "function") {
|
|
1333
|
+
try {
|
|
1334
|
+
const key = props.keyExtractor(item, index);
|
|
1335
|
+
if (typeof key === "string" || typeof key === "number") return String(key);
|
|
1336
|
+
} catch {
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
const raw = item?.id ?? item?.key ?? item?._id;
|
|
1340
|
+
if (typeof raw === "string" || typeof raw === "number") return String(raw);
|
|
1341
|
+
return typeof index === "number" ? String(index) : void 0;
|
|
1342
|
+
}
|
|
1343
|
+
function getTrackedList(OriginalComponent, componentName) {
|
|
1344
|
+
let cached = _listWrapperCache.get(OriginalComponent);
|
|
1345
|
+
if (cached) return cached;
|
|
1346
|
+
cached = React2__default.default.forwardRef(function TrackedList(props, forwardedRef) {
|
|
1347
|
+
const idRef = React2__default.default.useRef(null);
|
|
1348
|
+
if (!idRef.current) idRef.current = extractListId(props, componentName);
|
|
1349
|
+
const listId = idRef.current;
|
|
1350
|
+
const renderItemRef = React2__default.default.useRef(props.renderItem);
|
|
1351
|
+
const propsRef = React2__default.default.useRef(props);
|
|
1352
|
+
const instanceRef = React2__default.default.useRef(null);
|
|
1353
|
+
renderItemRef.current = props.renderItem;
|
|
1354
|
+
propsRef.current = props;
|
|
1355
|
+
const itemCount = resolveItemCount(props);
|
|
1356
|
+
const refreshing = props.refreshing === true;
|
|
1357
|
+
const label = listLabel(props);
|
|
1358
|
+
const dataPreview = React2__default.default.useMemo(
|
|
1359
|
+
() => buildDataPreview(props),
|
|
1360
|
+
[props.data, props.sections]
|
|
1361
|
+
);
|
|
1362
|
+
const mergedRef = React2__default.default.useCallback(
|
|
1363
|
+
(instance) => {
|
|
1364
|
+
instanceRef.current = instance;
|
|
1365
|
+
if (typeof forwardedRef === "function") forwardedRef(instance);
|
|
1366
|
+
else if (forwardedRef && typeof forwardedRef === "object") forwardedRef.current = instance;
|
|
1367
|
+
},
|
|
1368
|
+
[forwardedRef]
|
|
1369
|
+
);
|
|
1370
|
+
React2__default.default.useEffect(() => {
|
|
1371
|
+
listRegistry.register(listId, {
|
|
1372
|
+
kind: "list",
|
|
1373
|
+
id: listId,
|
|
1374
|
+
component: componentName,
|
|
1375
|
+
itemCount,
|
|
1376
|
+
label,
|
|
1377
|
+
refreshing,
|
|
1378
|
+
empty: typeof itemCount === "number" ? itemCount === 0 : void 0,
|
|
1379
|
+
dataPreview,
|
|
1380
|
+
scrollToIndex: (index) => {
|
|
1381
|
+
const instance = instanceRef.current;
|
|
1382
|
+
if (!instance) return false;
|
|
1383
|
+
try {
|
|
1384
|
+
if (componentName === "SectionList" && Array.isArray(propsRef.current?.sections)) {
|
|
1385
|
+
const location = flatIndexToSectionLocation(propsRef.current.sections, index);
|
|
1386
|
+
if (!location || typeof instance.scrollToLocation !== "function") return false;
|
|
1387
|
+
instance.scrollToLocation({ ...location, animated: true, viewPosition: 0.3 });
|
|
1388
|
+
return true;
|
|
1389
|
+
}
|
|
1390
|
+
if (typeof instance.scrollToIndex === "function") {
|
|
1391
|
+
instance.scrollToIndex({ index, animated: true, viewPosition: 0.3 });
|
|
1392
|
+
return true;
|
|
1393
|
+
}
|
|
1394
|
+
return false;
|
|
1395
|
+
} catch (err) {
|
|
1396
|
+
console.warn(`[Appilots] TrackedList scrollToIndex failed for "${listId}":`, err);
|
|
1397
|
+
return false;
|
|
1398
|
+
}
|
|
1399
|
+
},
|
|
1400
|
+
scrollToOffset: (offset) => {
|
|
1401
|
+
const instance = instanceRef.current;
|
|
1402
|
+
if (!instance) return false;
|
|
1403
|
+
try {
|
|
1404
|
+
if (typeof instance.scrollToOffset === "function") {
|
|
1405
|
+
instance.scrollToOffset({ offset, animated: true });
|
|
1406
|
+
return true;
|
|
1407
|
+
}
|
|
1408
|
+
const responder = typeof instance.getScrollResponder === "function" ? instance.getScrollResponder() : void 0;
|
|
1409
|
+
if (responder && typeof responder.scrollTo === "function") {
|
|
1410
|
+
responder.scrollTo({ y: offset, animated: true });
|
|
1411
|
+
return true;
|
|
1412
|
+
}
|
|
1413
|
+
return false;
|
|
1414
|
+
} catch (err) {
|
|
1415
|
+
console.warn(`[Appilots] TrackedList scrollToOffset failed for "${listId}":`, err);
|
|
1416
|
+
return false;
|
|
1417
|
+
}
|
|
1418
|
+
},
|
|
1419
|
+
getScrollMetrics: () => readScrollMetrics(instanceRef.current)
|
|
1420
|
+
});
|
|
1421
|
+
return () => {
|
|
1422
|
+
listRegistry.unregister(listId);
|
|
1423
|
+
};
|
|
1424
|
+
}, [listId, itemCount, label, refreshing, dataPreview]);
|
|
1425
|
+
const wrappedRenderItem = React2__default.default.useCallback(
|
|
1426
|
+
(info) => {
|
|
1427
|
+
const renderer = renderItemRef.current;
|
|
1428
|
+
if (typeof renderer !== "function") return null;
|
|
1429
|
+
const rendered = renderer(info);
|
|
1430
|
+
if (!React2__default.default.isValidElement(rendered)) return rendered;
|
|
1431
|
+
const localIndex = typeof info?.index === "number" ? info.index : void 0;
|
|
1432
|
+
const index = localIndex !== void 0 && componentName === "SectionList" ? sectionListFlatIndex(propsRef.current?.sections, info?.section, localIndex) : localIndex;
|
|
1433
|
+
const itemKey = localIndex !== void 0 ? resolveItemKey(propsRef.current, info?.item, localIndex) : void 0;
|
|
1434
|
+
return React2__default.default.cloneElement(rendered, {
|
|
1435
|
+
__appilotsListItem: true,
|
|
1436
|
+
__appilotsListId: listId,
|
|
1437
|
+
__appilotsItemIndex: index,
|
|
1438
|
+
__appilotsItemKey: itemKey,
|
|
1439
|
+
__appilotsItemCount: itemCount
|
|
1440
|
+
});
|
|
1441
|
+
},
|
|
1442
|
+
[listId, itemCount]
|
|
1443
|
+
);
|
|
1444
|
+
return _originalCreateElement(OriginalComponent, {
|
|
1445
|
+
...props,
|
|
1446
|
+
ref: mergedRef,
|
|
1447
|
+
__appilotsListId: listId,
|
|
1448
|
+
__appilotsListKind: componentName,
|
|
1449
|
+
__appilotsItemCount: itemCount,
|
|
1450
|
+
__appilotsListLabel: label,
|
|
1451
|
+
__appilotsRefreshing: refreshing,
|
|
1452
|
+
__appilotsEmpty: typeof itemCount === "number" ? itemCount === 0 : void 0,
|
|
1453
|
+
renderItem: typeof props.renderItem === "function" ? wrappedRenderItem : props.renderItem
|
|
1454
|
+
});
|
|
1455
|
+
});
|
|
1456
|
+
_listWrapperCache.set(OriginalComponent, cached);
|
|
1457
|
+
return cached;
|
|
1458
|
+
}
|
|
1459
|
+
var _enabled = false;
|
|
1460
|
+
function enableAppilotsAutoTracking(options) {
|
|
1461
|
+
if (_enabled) return;
|
|
1462
|
+
_enabled = true;
|
|
1463
|
+
const { trackPressable = true, debug = false } = options ?? {};
|
|
1464
|
+
if (debug) {
|
|
1465
|
+
componentRegistry.subscribe((id, entry) => {
|
|
1466
|
+
if (entry) {
|
|
1467
|
+
console.log(`[Appilots] Registered ${entry.kind}: "${id}" (${entry.label ?? "no label"})`);
|
|
1468
|
+
} else {
|
|
1469
|
+
console.log(`[Appilots] Unregistered: "${id}"`);
|
|
1470
|
+
}
|
|
1471
|
+
});
|
|
1472
|
+
listRegistry.subscribe((id, entry) => {
|
|
1473
|
+
if (entry) {
|
|
1474
|
+
console.log(
|
|
1475
|
+
`[Appilots] Registered list: "${id}" (${entry.component}, items=${entry.itemCount ?? "unknown"})`
|
|
1476
|
+
);
|
|
1477
|
+
} else {
|
|
1478
|
+
console.log(`[Appilots] Unregistered list: "${id}"`);
|
|
1479
|
+
}
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
const originalCreateElement = React2__default.default.createElement;
|
|
1483
|
+
_originalCreateElement = originalCreateElement;
|
|
1484
|
+
function isComponent(type, target, name) {
|
|
1485
|
+
if (type === target) {
|
|
1486
|
+
console.log(`[Appilots] isComponent: matched "${name}" by reference equality`);
|
|
1487
|
+
return true;
|
|
1488
|
+
}
|
|
1489
|
+
const dn = type?.displayName || type?.name || "";
|
|
1490
|
+
if (dn === name) {
|
|
1491
|
+
console.log(`[Appilots] isComponent: matched "${name}" by displayName/name (reference equality failed \u2014 possible monorepo issue)`);
|
|
1492
|
+
return true;
|
|
1493
|
+
}
|
|
1494
|
+
return false;
|
|
1495
|
+
}
|
|
1496
|
+
function interceptType(type, props) {
|
|
1497
|
+
if (!props || !type || props.__appilotsInternal) return null;
|
|
1498
|
+
const displayName = componentDisplayName(type);
|
|
1499
|
+
if (LIST_COMPONENT_NAMES.has(displayName)) {
|
|
1500
|
+
console.log(`[Appilots] intercepted list component "${displayName}"`);
|
|
1501
|
+
return { type: getTrackedList(type, displayName), props };
|
|
1502
|
+
}
|
|
1503
|
+
if (isComponent(type, reactNative.TextInput, "TextInput")) {
|
|
1504
|
+
if (props.testID || props.accessibilityLabel || props.placeholder) {
|
|
1505
|
+
console.log(`[Appilots] intercepted TextInput (testID="${props.testID}", label="${props.accessibilityLabel}", placeholder="${props.placeholder}")`);
|
|
1506
|
+
return { type: TrackedTextInput, props };
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
if (isComponent(type, reactNative.Switch, "Switch")) {
|
|
1510
|
+
if (props.testID || props.accessibilityLabel) {
|
|
1511
|
+
console.log(`[Appilots] intercepted Switch (testID="${props.testID}", label="${props.accessibilityLabel}")`);
|
|
1512
|
+
return { type: TrackedSwitch, props };
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
if (isComponent(type, reactNative.TouchableOpacity, "TouchableOpacity") || trackPressable && isComponent(type, reactNative.Pressable, "Pressable")) {
|
|
1516
|
+
if ((props.testID || props.accessibilityLabel) && props.onPress) {
|
|
1517
|
+
console.log(`[Appilots] intercepted Pressable/TouchableOpacity (testID="${props.testID}", label="${props.accessibilityLabel}")`);
|
|
1518
|
+
return { type: getTrackedPressable(type), props };
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
return null;
|
|
1522
|
+
}
|
|
1523
|
+
React2__default.default.createElement = function(type, props, ...children) {
|
|
1524
|
+
const replacement = interceptType(type, props);
|
|
1525
|
+
if (replacement) {
|
|
1526
|
+
return originalCreateElement(replacement.type, replacement.props, ...children);
|
|
1527
|
+
}
|
|
1528
|
+
return originalCreateElement(type, props, ...children);
|
|
1529
|
+
};
|
|
1530
|
+
console.log("[Appilots] Auto-tracking: React.createElement patched");
|
|
1531
|
+
_interceptType = interceptType;
|
|
1532
|
+
try {
|
|
1533
|
+
const jsxRuntime = __require("react/jsx-runtime");
|
|
1534
|
+
if (jsxRuntime) {
|
|
1535
|
+
_patchJsxRuntimeModule(jsxRuntime);
|
|
1536
|
+
}
|
|
1537
|
+
} catch (e) {
|
|
1538
|
+
console.log("[Appilots] Auto-tracking: react/jsx-runtime not available internally (will be patched by metro shim)");
|
|
1539
|
+
}
|
|
1540
|
+
try {
|
|
1541
|
+
const jsxDevRuntime = __require("react/jsx-dev-runtime");
|
|
1542
|
+
if (jsxDevRuntime) {
|
|
1543
|
+
_patchJsxDevRuntimeModule(jsxDevRuntime);
|
|
1544
|
+
}
|
|
1545
|
+
} catch (e) {
|
|
1546
|
+
console.log("[Appilots] Auto-tracking: react/jsx-dev-runtime not available internally (will be patched by metro shim)");
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
var _interceptType = null;
|
|
1550
|
+
var _jsxRuntimePatched = false;
|
|
1551
|
+
var _jsxDevRuntimePatched = false;
|
|
1552
|
+
function _patchJsxRuntimeModule(jsxRuntime) {
|
|
1553
|
+
if (_jsxRuntimePatched || !_interceptType || !jsxRuntime) return;
|
|
1554
|
+
_jsxRuntimePatched = true;
|
|
1555
|
+
const interceptType = _interceptType;
|
|
1556
|
+
const origJsx = jsxRuntime.jsx;
|
|
1557
|
+
const origJsxs = jsxRuntime.jsxs;
|
|
1558
|
+
if (origJsx) {
|
|
1559
|
+
jsxRuntime.jsx = function(type, props, key) {
|
|
1560
|
+
const replacement = interceptType(type, props);
|
|
1561
|
+
if (replacement) {
|
|
1562
|
+
return origJsx(replacement.type, replacement.props, key);
|
|
1563
|
+
}
|
|
1564
|
+
return origJsx(type, props, key);
|
|
1565
|
+
};
|
|
1566
|
+
console.log("[Appilots] Auto-tracking: react/jsx-runtime jsx() patched");
|
|
1567
|
+
}
|
|
1568
|
+
if (origJsxs) {
|
|
1569
|
+
jsxRuntime.jsxs = function(type, props, key) {
|
|
1570
|
+
const replacement = interceptType(type, props);
|
|
1571
|
+
if (replacement) {
|
|
1572
|
+
return origJsxs(replacement.type, replacement.props, key);
|
|
1573
|
+
}
|
|
1574
|
+
return origJsxs(type, props, key);
|
|
1575
|
+
};
|
|
1576
|
+
console.log("[Appilots] Auto-tracking: react/jsx-runtime jsxs() patched");
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
function _patchJsxDevRuntimeModule(jsxDevRuntime) {
|
|
1580
|
+
if (_jsxDevRuntimePatched || !_interceptType || !jsxDevRuntime) return;
|
|
1581
|
+
_jsxDevRuntimePatched = true;
|
|
1582
|
+
const interceptType = _interceptType;
|
|
1583
|
+
if (jsxDevRuntime.jsxDEV) {
|
|
1584
|
+
const origJsxDEV = jsxDevRuntime.jsxDEV;
|
|
1585
|
+
jsxDevRuntime.jsxDEV = function(type, props, key, isStaticChildren, source, self) {
|
|
1586
|
+
const replacement = interceptType(type, props);
|
|
1587
|
+
if (replacement) {
|
|
1588
|
+
return origJsxDEV(replacement.type, replacement.props, key, isStaticChildren, source, self);
|
|
1589
|
+
}
|
|
1590
|
+
return origJsxDEV(type, props, key, isStaticChildren, source, self);
|
|
1591
|
+
};
|
|
1592
|
+
console.log("[Appilots] Auto-tracking: react/jsx-dev-runtime jsxDEV() patched");
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
function _patchJsxRuntimes(jsxRuntime, jsxDevRuntime) {
|
|
1596
|
+
if (!_interceptType) {
|
|
1597
|
+
console.warn("[Appilots] _patchJsxRuntimes called but auto-tracking is not enabled");
|
|
1598
|
+
return;
|
|
1599
|
+
}
|
|
1600
|
+
console.log("[Appilots] _patchJsxRuntimes: patching from app context (metro shim)");
|
|
1601
|
+
if (jsxRuntime) _patchJsxRuntimeModule(jsxRuntime);
|
|
1602
|
+
if (jsxDevRuntime) _patchJsxDevRuntimeModule(jsxDevRuntime);
|
|
1603
|
+
}
|
|
1604
|
+
function isAutoTrackingEnabled() {
|
|
1605
|
+
return _enabled;
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
// src/core/initAppilots.ts
|
|
1609
|
+
var _globalConfig = null;
|
|
1610
|
+
var _autoConfigAttempted = false;
|
|
1611
|
+
function initAppilots(config) {
|
|
1612
|
+
console.log(`[Appilots] initAppilots called with config: projectId="${config.projectId}", apiBaseUrl="${config.apiBaseUrl ?? config.apiUrl ?? "default"}", debug=${config.debug ?? false}, autoTracking=${JSON.stringify(config.autoTracking ?? {})}`);
|
|
1613
|
+
if (config.apiUrl && !config.apiBaseUrl) {
|
|
1614
|
+
config.apiBaseUrl = config.apiUrl;
|
|
1615
|
+
}
|
|
1616
|
+
_globalConfig = config;
|
|
1617
|
+
const autoTrackingEnabled = config.autoTracking?.enabled !== false;
|
|
1618
|
+
if (autoTrackingEnabled) {
|
|
1619
|
+
console.log("[Appilots] initAppilots: auto-tracking is enabled, calling enableAppilotsAutoTracking()");
|
|
1620
|
+
enableAppilotsAutoTracking({
|
|
1621
|
+
trackPressable: config.autoTracking?.trackPressable ?? true,
|
|
1622
|
+
debug: config.debug ?? false
|
|
1623
|
+
});
|
|
1624
|
+
} else {
|
|
1625
|
+
console.log("[Appilots] initAppilots: auto-tracking is explicitly disabled");
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
function tryAutoConfig() {
|
|
1629
|
+
if (_globalConfig || _autoConfigAttempted) {
|
|
1630
|
+
console.log(`[Appilots] tryAutoConfig: skipping (alreadyConfigured=${!!_globalConfig}, alreadyAttempted=${_autoConfigAttempted})`);
|
|
1631
|
+
return;
|
|
1632
|
+
}
|
|
1633
|
+
_autoConfigAttempted = true;
|
|
1634
|
+
try {
|
|
1635
|
+
const rc = globalThis.__APPILOTS_RC__;
|
|
1636
|
+
if (rc && typeof rc === "object" && rc.projectId) {
|
|
1637
|
+
console.log(`[Appilots] tryAutoConfig: found globalThis.__APPILOTS_RC__ with projectId="${rc.projectId}"`);
|
|
1638
|
+
initAppilots(rc);
|
|
1639
|
+
} else {
|
|
1640
|
+
console.log("[Appilots] tryAutoConfig: globalThis.__APPILOTS_RC__ not found or missing projectId");
|
|
1641
|
+
}
|
|
1642
|
+
} catch {
|
|
1643
|
+
console.warn("[Appilots] tryAutoConfig: error reading globalThis.__APPILOTS_RC__");
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
function getGlobalConfig() {
|
|
1647
|
+
return _globalConfig;
|
|
1648
|
+
}
|
|
1649
|
+
|
|
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
|
+
// src/introspection/fiberRoot.ts
|
|
1710
|
+
var _fiberRoot = null;
|
|
1711
|
+
function setFiberRoot(fiber) {
|
|
1712
|
+
if (_fiberRoot !== fiber) {
|
|
1713
|
+
console.log(`[Appilots] Introspection: fiber root captured (type=${typeName(fiber)})`);
|
|
1714
|
+
_fiberRoot = fiber;
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
function getFiberRoot() {
|
|
1718
|
+
return _fiberRoot;
|
|
1719
|
+
}
|
|
1720
|
+
function typeName(fiber) {
|
|
1721
|
+
const t = fiber?.type ?? fiber?.elementType;
|
|
1722
|
+
if (!t) return "unknown";
|
|
1723
|
+
if (typeof t === "string") return t;
|
|
1724
|
+
if (typeof t === "function") return t.displayName ?? t.name ?? "anonymous";
|
|
1725
|
+
if (typeof t === "object") return t.displayName ?? t.render?.displayName ?? t.render?.name ?? "forwardRef/memo";
|
|
1726
|
+
return String(t);
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
// src/introspection/AppilotsFiberRoot.tsx
|
|
1730
|
+
var FiberSentinel = class extends React2__default.default.Component {
|
|
1731
|
+
componentDidMount() {
|
|
1732
|
+
const fiber = this._reactInternals ?? this._reactInternalFiber ?? null;
|
|
1733
|
+
if (!fiber) {
|
|
1734
|
+
console.warn(
|
|
1735
|
+
"[Appilots] FiberSentinel: this._reactInternals is undefined; snapshot capture will fall back to render-time owner. React version may be incompatible."
|
|
1736
|
+
);
|
|
1737
|
+
return;
|
|
1738
|
+
}
|
|
1739
|
+
let top = fiber;
|
|
1740
|
+
while (top.return) top = top.return;
|
|
1741
|
+
setFiberRoot(top);
|
|
1742
|
+
}
|
|
1743
|
+
render() {
|
|
1744
|
+
return this.props.children ?? null;
|
|
1745
|
+
}
|
|
1746
|
+
};
|
|
1747
|
+
function AppilotsFiberRoot({ children }) {
|
|
1748
|
+
const Internals = React2__default.default.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED_IN_THIS_VERSION_OF_REACT;
|
|
1749
|
+
const ownerFiber = Internals?.ReactCurrentOwner?.current;
|
|
1750
|
+
if (ownerFiber && !getFiberRoot()) {
|
|
1751
|
+
setFiberRoot(ownerFiber);
|
|
1752
|
+
}
|
|
1753
|
+
return /* @__PURE__ */ React2__default.default.createElement(FiberSentinel, null, children);
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
// src/debug/logger.ts
|
|
1757
|
+
var debugOverride;
|
|
1758
|
+
function setAppilotsDebugEnabled(enabled) {
|
|
1759
|
+
debugOverride = enabled;
|
|
1760
|
+
}
|
|
1761
|
+
function isDebugEnabled() {
|
|
1762
|
+
if (debugOverride !== void 0) return debugOverride;
|
|
1763
|
+
return getGlobalConfig()?.debug === true;
|
|
1764
|
+
}
|
|
1765
|
+
function appilotsDebugWarn(...args) {
|
|
1766
|
+
if (!isDebugEnabled()) return;
|
|
1767
|
+
console.log("[Appilots]", ...args);
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
// src/context/AppilotsProvider.tsx
|
|
1771
|
+
var AppilotsContext = React2.createContext(null);
|
|
1772
|
+
function AppilotsProvider({ config: configProp, children, client: externalClient }) {
|
|
1773
|
+
console.log(`[Appilots] AppilotsProvider initializing \u2014 config source: ${configProp ? "prop" : "auto (globalThis.__APPILOTS_RC__)"}`);
|
|
1774
|
+
tryAutoConfig();
|
|
1775
|
+
const globalConfig = getGlobalConfig();
|
|
1776
|
+
console.log(`[Appilots] AppilotsProvider: configProp=${!!configProp}, globalConfig=${!!globalConfig}`);
|
|
1777
|
+
const config = configProp ?? (globalConfig ? {
|
|
1778
|
+
projectId: globalConfig.projectId,
|
|
1779
|
+
apiBaseUrl: globalConfig.apiBaseUrl,
|
|
1780
|
+
apiKey: globalConfig.apiKey,
|
|
1781
|
+
permissions: globalConfig.permissions,
|
|
1782
|
+
debug: globalConfig.debug,
|
|
1783
|
+
appVersion: globalConfig.appVersion,
|
|
1784
|
+
mcpVersion: globalConfig.mcpVersion,
|
|
1785
|
+
fetchPersonalization: globalConfig.fetchPersonalization
|
|
1786
|
+
} : null);
|
|
1787
|
+
if (!config?.projectId) {
|
|
1788
|
+
throw new Error(
|
|
1789
|
+
"AppilotsProvider: No config found. Add withAppilots() to your metro.config.js and create a .appilotsrc file, or pass a config prop directly."
|
|
1790
|
+
);
|
|
1791
|
+
}
|
|
1792
|
+
React2.useEffect(() => {
|
|
1793
|
+
setAppilotsDebugEnabled(config.debug === true);
|
|
1794
|
+
}, [config.debug]);
|
|
1795
|
+
const listenersRef = React2.useRef(/* @__PURE__ */ new Set());
|
|
1796
|
+
const subscribe = React2.useCallback((handler) => {
|
|
1797
|
+
listenersRef.current.add(handler);
|
|
1798
|
+
return () => {
|
|
1799
|
+
listenersRef.current.delete(handler);
|
|
1800
|
+
};
|
|
1801
|
+
}, []);
|
|
1802
|
+
const emit = React2.useCallback((event) => {
|
|
1803
|
+
recordAppilotsEventTrace(event);
|
|
1804
|
+
listenersRef.current.forEach((handler) => handler(event));
|
|
1805
|
+
}, []);
|
|
1806
|
+
const client = React2.useMemo(
|
|
1807
|
+
() => externalClient ?? new AppilotsClient({
|
|
1808
|
+
projectId: config.projectId,
|
|
1809
|
+
apiBaseUrl: config.apiBaseUrl,
|
|
1810
|
+
apiKey: config.apiKey,
|
|
1811
|
+
debug: config.debug,
|
|
1812
|
+
appVersion: config.appVersion,
|
|
1813
|
+
mcpVersion: config.mcpVersion,
|
|
1814
|
+
user: config.user
|
|
1815
|
+
}),
|
|
1816
|
+
[config.projectId, config.apiBaseUrl, config.apiKey, config.debug, config.appVersion, config.mcpVersion, config.user, externalClient]
|
|
1817
|
+
);
|
|
1818
|
+
const fetchPersonalizationEnabled = config.fetchPersonalization !== false;
|
|
1819
|
+
const [remotePersonalization, setRemotePersonalization] = React2.useState(null);
|
|
1820
|
+
React2.useEffect(() => {
|
|
1821
|
+
if (!fetchPersonalizationEnabled) return;
|
|
1822
|
+
let cancelled = false;
|
|
1823
|
+
client.getPersonalization().then((data) => {
|
|
1824
|
+
if (!cancelled) setRemotePersonalization(data);
|
|
1825
|
+
}).catch(() => {
|
|
1826
|
+
});
|
|
1827
|
+
return () => {
|
|
1828
|
+
cancelled = true;
|
|
1829
|
+
};
|
|
1830
|
+
}, [client, fetchPersonalizationEnabled]);
|
|
1831
|
+
const value = React2.useMemo(
|
|
1832
|
+
() => ({ config, client, subscribe, emit, remotePersonalization }),
|
|
1833
|
+
[config, client, subscribe, emit, remotePersonalization]
|
|
1834
|
+
);
|
|
1835
|
+
return /* @__PURE__ */ React2__default.default.createElement(AppilotsContext.Provider, { value }, /* @__PURE__ */ React2__default.default.createElement(AppilotsFiberRoot, null, children));
|
|
1836
|
+
}
|
|
1837
|
+
function useAppilotsContext() {
|
|
1838
|
+
const context = React2.useContext(AppilotsContext);
|
|
1839
|
+
if (!context) {
|
|
1840
|
+
throw new Error("useAppilotsContext must be used within an <AppilotsProvider>");
|
|
1841
|
+
}
|
|
1842
|
+
return context;
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
// src/introspection/probeLoadingState.ts
|
|
1846
|
+
function getTypeName(type) {
|
|
1847
|
+
if (!type) return null;
|
|
1848
|
+
if (typeof type === "string") return type;
|
|
1849
|
+
if (typeof type === "function") return type.displayName ?? type.name ?? null;
|
|
1850
|
+
if (typeof type === "object") {
|
|
1851
|
+
if (type.displayName) return type.displayName;
|
|
1852
|
+
if (type.render) return type.render.displayName ?? type.render.name ?? null;
|
|
1853
|
+
if (type.type) return getTypeName(type.type);
|
|
1854
|
+
}
|
|
1855
|
+
return null;
|
|
1856
|
+
}
|
|
1857
|
+
function matchesName(fiber, name) {
|
|
1858
|
+
return getTypeName(fiber.type) === name || getTypeName(fiber.elementType) === name;
|
|
1859
|
+
}
|
|
1860
|
+
var SKELETON_NAME_RE = /skeleton|shimmer|contentloader/i;
|
|
1861
|
+
function looksLikeSkeleton(fiber) {
|
|
1862
|
+
const name = getTypeName(fiber.type) ?? getTypeName(fiber.elementType);
|
|
1863
|
+
return !!name && SKELETON_NAME_RE.test(name);
|
|
1864
|
+
}
|
|
1865
|
+
var SELF_REFERENTIAL_COMPONENTS = /* @__PURE__ */ new Set([
|
|
1866
|
+
"AppilotsChat",
|
|
1867
|
+
"AppilotsChatInner",
|
|
1868
|
+
"ActionBreadcrumb",
|
|
1869
|
+
"ConfirmDialog"
|
|
1870
|
+
]);
|
|
1871
|
+
function shouldSkipSubtree(fiber) {
|
|
1872
|
+
const props = fiber.memoizedProps;
|
|
1873
|
+
if (!props) return false;
|
|
1874
|
+
const typeName2 = getTypeName(fiber.type) ?? getTypeName(fiber.elementType);
|
|
1875
|
+
if (typeName2 && SELF_REFERENTIAL_COMPONENTS.has(typeName2)) return true;
|
|
1876
|
+
if (props.__appilotsSkip === true || props["data-appilots-skip"] === true || props.appilotsSkip === true) {
|
|
1877
|
+
return true;
|
|
1878
|
+
}
|
|
1879
|
+
if (matchesName(fiber, "Modal") && props.visible === false) return true;
|
|
1880
|
+
if (matchesName(fiber, "Screen") && props.activityState === 0) return true;
|
|
1881
|
+
const style = props.style;
|
|
1882
|
+
if (style) {
|
|
1883
|
+
const flat = Array.isArray(style) ? Object.assign({}, ...style.filter(Boolean)) : style;
|
|
1884
|
+
if (flat?.display === "none") return true;
|
|
1885
|
+
if (flat?.opacity === 0 && props.pointerEvents === "none") return true;
|
|
1886
|
+
}
|
|
1887
|
+
return false;
|
|
1888
|
+
}
|
|
1889
|
+
var TOUCHABLE_NAMES = /* @__PURE__ */ new Set([
|
|
1890
|
+
"TouchableOpacity",
|
|
1891
|
+
"TouchableHighlight",
|
|
1892
|
+
"TouchableWithoutFeedback",
|
|
1893
|
+
"TouchableNativeFeedback",
|
|
1894
|
+
"Pressable"
|
|
1895
|
+
]);
|
|
1896
|
+
function isTouchable(fiber) {
|
|
1897
|
+
return TOUCHABLE_NAMES.has(getTypeName(fiber.type) ?? "") || TOUCHABLE_NAMES.has(getTypeName(fiber.elementType) ?? "");
|
|
1898
|
+
}
|
|
1899
|
+
function pickId(props) {
|
|
1900
|
+
return props?.testID ?? props?.accessibilityLabel ?? void 0;
|
|
1901
|
+
}
|
|
1902
|
+
function idMatches(candidate, target) {
|
|
1903
|
+
if (!candidate) return false;
|
|
1904
|
+
if (candidate === target) return true;
|
|
1905
|
+
const c = candidate.toLowerCase();
|
|
1906
|
+
const t = target.toLowerCase();
|
|
1907
|
+
if (c === t) return true;
|
|
1908
|
+
return c.endsWith(`-${t}`) || c.endsWith(`_${t}`) || t.endsWith(`-${c}`) || t.endsWith(`_${c}`);
|
|
1909
|
+
}
|
|
1910
|
+
function probeLoadingState(pressedComponentId, route) {
|
|
1911
|
+
const root = getFiberRoot();
|
|
1912
|
+
const result = {
|
|
1913
|
+
loading: false,
|
|
1914
|
+
pressedDisabled: false,
|
|
1915
|
+
pressedFound: false,
|
|
1916
|
+
modalOpen: false,
|
|
1917
|
+
fingerprint: "",
|
|
1918
|
+
visitedFibers: 0
|
|
1919
|
+
};
|
|
1920
|
+
if (!root) return result;
|
|
1921
|
+
const buttonSigs = [];
|
|
1922
|
+
const inputSigs = [];
|
|
1923
|
+
const toggleSigs = [];
|
|
1924
|
+
const visited = /* @__PURE__ */ new WeakSet();
|
|
1925
|
+
const stack = [];
|
|
1926
|
+
if (root.child) stack.push(root.child);
|
|
1927
|
+
while (stack.length > 0) {
|
|
1928
|
+
const fiber = stack.pop();
|
|
1929
|
+
if (!fiber || visited.has(fiber)) continue;
|
|
1930
|
+
visited.add(fiber);
|
|
1931
|
+
result.visitedFibers++;
|
|
1932
|
+
if (fiber.sibling) stack.push(fiber.sibling);
|
|
1933
|
+
if (shouldSkipSubtree(fiber)) continue;
|
|
1934
|
+
const props = fiber.memoizedProps ?? {};
|
|
1935
|
+
if (matchesName(fiber, "ActivityIndicator") || looksLikeSkeleton(fiber)) {
|
|
1936
|
+
result.loading = true;
|
|
1937
|
+
} else if (matchesName(fiber, "Modal")) {
|
|
1938
|
+
result.modalOpen = true;
|
|
1939
|
+
} else if (isTouchable(fiber)) {
|
|
1940
|
+
if (props.__appilotsInternal !== true) {
|
|
1941
|
+
const id = pickId(props);
|
|
1942
|
+
const disabled = !!props.disabled;
|
|
1943
|
+
buttonSigs.push(`${id ?? "?"}:${disabled ? 1 : 0}`);
|
|
1944
|
+
if (pressedComponentId && idMatches(id, pressedComponentId)) {
|
|
1945
|
+
result.pressedFound = true;
|
|
1946
|
+
if (disabled) result.pressedDisabled = true;
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
} else if (matchesName(fiber, "TextInput")) {
|
|
1950
|
+
if (props.__appilotsInternal !== true) {
|
|
1951
|
+
const id = pickId(props);
|
|
1952
|
+
const editable = props.editable !== false;
|
|
1953
|
+
inputSigs.push(`${id ?? "?"}:${editable ? 1 : 0}`);
|
|
1954
|
+
}
|
|
1955
|
+
} else if (matchesName(fiber, "Switch")) {
|
|
1956
|
+
const id = pickId(props);
|
|
1957
|
+
toggleSigs.push(`${id ?? "?"}:${props.value ? 1 : 0}`);
|
|
1958
|
+
}
|
|
1959
|
+
if (fiber.child) stack.push(fiber.child);
|
|
1960
|
+
}
|
|
1961
|
+
buttonSigs.sort();
|
|
1962
|
+
inputSigs.sort();
|
|
1963
|
+
toggleSigs.sort();
|
|
1964
|
+
result.fingerprint = `r:${route ?? ""}|l:${result.loading ? 1 : 0}|m:${result.modalOpen ? 1 : 0}|b:${buttonSigs.join(",")}|i:${inputSigs.join(",")}|t:${toggleSigs.join(",")}`;
|
|
1965
|
+
return result;
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
// src/navigation/navigationState.ts
|
|
1969
|
+
var _currentScreen = null;
|
|
1970
|
+
var _navigationRef = null;
|
|
1971
|
+
function setCurrentScreen(screen) {
|
|
1972
|
+
console.log(`[Appilots] setCurrentScreen: "${_currentScreen}" \u2192 "${screen}"`);
|
|
1973
|
+
_currentScreen = screen;
|
|
1974
|
+
}
|
|
1975
|
+
function getCurrentScreen() {
|
|
1976
|
+
const activePath = getActiveRouteNames();
|
|
1977
|
+
if (activePath.length > 0) {
|
|
1978
|
+
const deepest = activePath[activePath.length - 1] ?? null;
|
|
1979
|
+
if (deepest && deepest !== _currentScreen) {
|
|
1980
|
+
if (_currentScreen) {
|
|
1981
|
+
console.log(
|
|
1982
|
+
`[Appilots] getCurrentScreen: active path differs from cached \u2014 deepest="${deepest}", cached="${_currentScreen}". Updating cache.`
|
|
1983
|
+
);
|
|
1984
|
+
}
|
|
1985
|
+
_currentScreen = deepest;
|
|
1986
|
+
}
|
|
1987
|
+
if (deepest) return deepest;
|
|
1988
|
+
}
|
|
1989
|
+
if (_navigationRef) {
|
|
1990
|
+
const liveScreen = getActiveRouteFromRef(_navigationRef);
|
|
1991
|
+
if (liveScreen) {
|
|
1992
|
+
if (liveScreen !== _currentScreen) {
|
|
1993
|
+
console.log(`[Appilots] getCurrentScreen: live reading differs from cached \u2014 live="${liveScreen}", cached="${_currentScreen}". Updating cache.`);
|
|
1994
|
+
_currentScreen = liveScreen;
|
|
1995
|
+
}
|
|
1996
|
+
return liveScreen;
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
return _currentScreen;
|
|
2000
|
+
}
|
|
2001
|
+
function setNavigationRef(ref) {
|
|
2002
|
+
_navigationRef = ref;
|
|
2003
|
+
}
|
|
2004
|
+
function getNavigationRef() {
|
|
2005
|
+
return _navigationRef;
|
|
2006
|
+
}
|
|
2007
|
+
function getActiveRouteFromState(state) {
|
|
2008
|
+
if (!state) return void 0;
|
|
2009
|
+
const route = state.routes?.[state.index ?? 0];
|
|
2010
|
+
if (!route) return void 0;
|
|
2011
|
+
if (route.state) return getActiveRouteFromState(route.state);
|
|
2012
|
+
return route.name;
|
|
2013
|
+
}
|
|
2014
|
+
function getActiveRouteNamesFromState(state, out = []) {
|
|
2015
|
+
if (!state) return out;
|
|
2016
|
+
const route = state.routes?.[state.index ?? 0];
|
|
2017
|
+
if (!route) return out;
|
|
2018
|
+
if (typeof route.name === "string" && route.name) out.push(route.name);
|
|
2019
|
+
if (route.state) return getActiveRouteNamesFromState(route.state, out);
|
|
2020
|
+
return out;
|
|
2021
|
+
}
|
|
2022
|
+
function getActiveRouteNames() {
|
|
2023
|
+
if (!_navigationRef) return _currentScreen ? [_currentScreen] : [];
|
|
2024
|
+
try {
|
|
2025
|
+
const state = _navigationRef.getRootState?.() ?? _navigationRef.getState?.();
|
|
2026
|
+
if (!state || isStaleState(state)) return _currentScreen ? [_currentScreen] : [];
|
|
2027
|
+
const names = getActiveRouteNamesFromState(state);
|
|
2028
|
+
if (names.length === 0 && _currentScreen) names.push(_currentScreen);
|
|
2029
|
+
return names;
|
|
2030
|
+
} catch {
|
|
2031
|
+
return _currentScreen ? [_currentScreen] : [];
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
function routeNamesAtState(state) {
|
|
2035
|
+
if (!state) return [];
|
|
2036
|
+
if (Array.isArray(state.routeNames)) {
|
|
2037
|
+
return state.routeNames.filter((name) => typeof name === "string" && name.length > 0);
|
|
2038
|
+
}
|
|
2039
|
+
if (Array.isArray(state.routes)) {
|
|
2040
|
+
return state.routes.map((route) => route?.name).filter((name) => typeof name === "string" && name.length > 0);
|
|
2041
|
+
}
|
|
2042
|
+
return [];
|
|
2043
|
+
}
|
|
2044
|
+
function collectRouteNamesFromState(state, out = /* @__PURE__ */ new Set()) {
|
|
2045
|
+
if (!state) return out;
|
|
2046
|
+
for (const name of routeNamesAtState(state)) out.add(name);
|
|
2047
|
+
for (const route of state.routes ?? []) {
|
|
2048
|
+
if (typeof route?.name === "string" && route.name.length > 0) out.add(route.name);
|
|
2049
|
+
if (route?.state) collectRouteNamesFromState(route.state, out);
|
|
2050
|
+
}
|
|
2051
|
+
return out;
|
|
2052
|
+
}
|
|
2053
|
+
function getDeepestNavigatorState(state) {
|
|
2054
|
+
if (!state) return void 0;
|
|
2055
|
+
const route = state.routes?.[state.index ?? 0];
|
|
2056
|
+
if (route?.state) return getDeepestNavigatorState(route.state);
|
|
2057
|
+
return state;
|
|
2058
|
+
}
|
|
2059
|
+
function getNavigationStateSnapshot() {
|
|
2060
|
+
if (!_navigationRef) {
|
|
2061
|
+
const current = _currentScreen ? [_currentScreen] : [];
|
|
2062
|
+
return current.length > 0 ? {
|
|
2063
|
+
currentRouteName: _currentScreen ?? void 0,
|
|
2064
|
+
activePath: current,
|
|
2065
|
+
rootRouteNames: current,
|
|
2066
|
+
currentRouteNames: current,
|
|
2067
|
+
routeNames: current
|
|
2068
|
+
} : null;
|
|
2069
|
+
}
|
|
2070
|
+
try {
|
|
2071
|
+
const state = _navigationRef.getRootState?.() ?? _navigationRef.getState?.();
|
|
2072
|
+
if (!state || isStaleState(state)) {
|
|
2073
|
+
const current = _currentScreen ? [_currentScreen] : [];
|
|
2074
|
+
return current.length > 0 ? {
|
|
2075
|
+
currentRouteName: _currentScreen ?? void 0,
|
|
2076
|
+
activePath: current,
|
|
2077
|
+
rootRouteNames: current,
|
|
2078
|
+
currentRouteNames: current,
|
|
2079
|
+
routeNames: current,
|
|
2080
|
+
canGoBack: typeof _navigationRef.canGoBack === "function" ? _navigationRef.canGoBack() : void 0
|
|
2081
|
+
} : null;
|
|
2082
|
+
}
|
|
2083
|
+
const activePath = getActiveRouteNamesFromState(state);
|
|
2084
|
+
const currentRouteName = activePath[activePath.length - 1] ?? _currentScreen ?? void 0;
|
|
2085
|
+
const deepestState = getDeepestNavigatorState(state);
|
|
2086
|
+
const routeNames = Array.from(collectRouteNamesFromState(state));
|
|
2087
|
+
return {
|
|
2088
|
+
currentRouteName,
|
|
2089
|
+
activePath,
|
|
2090
|
+
rootRouteNames: routeNamesAtState(state),
|
|
2091
|
+
currentRouteNames: routeNamesAtState(deepestState),
|
|
2092
|
+
routeNames,
|
|
2093
|
+
canGoBack: typeof _navigationRef.canGoBack === "function" ? _navigationRef.canGoBack() : void 0
|
|
2094
|
+
};
|
|
2095
|
+
} catch {
|
|
2096
|
+
const current = _currentScreen ? [_currentScreen] : [];
|
|
2097
|
+
return current.length > 0 ? {
|
|
2098
|
+
currentRouteName: _currentScreen ?? void 0,
|
|
2099
|
+
activePath: current,
|
|
2100
|
+
rootRouteNames: current,
|
|
2101
|
+
currentRouteNames: current,
|
|
2102
|
+
routeNames: current
|
|
2103
|
+
} : null;
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
function getActiveRouteSignatureFromState(state) {
|
|
2107
|
+
if (!state) return void 0;
|
|
2108
|
+
const route = state.routes?.[state.index ?? 0];
|
|
2109
|
+
if (!route) return void 0;
|
|
2110
|
+
if (route.state) return getActiveRouteSignatureFromState(route.state);
|
|
2111
|
+
const params = route.params;
|
|
2112
|
+
let paramsStr = "";
|
|
2113
|
+
if (params && typeof params === "object") {
|
|
2114
|
+
try {
|
|
2115
|
+
const keys = Object.keys(params).sort();
|
|
2116
|
+
const ordered = {};
|
|
2117
|
+
for (const k of keys) ordered[k] = params[k];
|
|
2118
|
+
paramsStr = JSON.stringify(ordered);
|
|
2119
|
+
} catch {
|
|
2120
|
+
paramsStr = "<unserializable>";
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
return `${route.name}|${paramsStr}`;
|
|
2124
|
+
}
|
|
2125
|
+
function getCurrentScreenSignature() {
|
|
2126
|
+
if (!_navigationRef) return null;
|
|
2127
|
+
try {
|
|
2128
|
+
const state = _navigationRef.getRootState?.() ?? _navigationRef.getState?.();
|
|
2129
|
+
if (!state) return null;
|
|
2130
|
+
if (isStaleState(state)) return null;
|
|
2131
|
+
return getActiveRouteSignatureFromState(state) ?? null;
|
|
2132
|
+
} catch {
|
|
2133
|
+
return null;
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
function isStaleState(state) {
|
|
2137
|
+
return state?.stale === true;
|
|
2138
|
+
}
|
|
2139
|
+
function getActiveRouteFromRef(navRef) {
|
|
2140
|
+
try {
|
|
2141
|
+
const state = navRef.getRootState?.() ?? navRef.getState?.();
|
|
2142
|
+
if (state) {
|
|
2143
|
+
if (isStaleState(state)) {
|
|
2144
|
+
return null;
|
|
2145
|
+
}
|
|
2146
|
+
return getActiveRouteFromState(state) ?? null;
|
|
2147
|
+
}
|
|
2148
|
+
return navRef.getCurrentRoute?.()?.name ?? null;
|
|
2149
|
+
} catch {
|
|
2150
|
+
return null;
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
2154
|
+
function normalizeRouteFamily(name) {
|
|
2155
|
+
return name.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/(tab|list|screen|details?|navigator)$/i, "");
|
|
2156
|
+
}
|
|
2157
|
+
function routesBelongToSameFeature(a, b) {
|
|
2158
|
+
const na = normalizeRouteFamily(a);
|
|
2159
|
+
const nb = normalizeRouteFamily(b);
|
|
2160
|
+
if (!na || !nb) return false;
|
|
2161
|
+
if (na === nb) return true;
|
|
2162
|
+
return na.startsWith(nb) || nb.startsWith(na);
|
|
2163
|
+
}
|
|
2164
|
+
function navigationTargetReached(target, fromScreen) {
|
|
2165
|
+
const cur = getCurrentScreen();
|
|
2166
|
+
const path = getActiveRouteNames();
|
|
2167
|
+
if (cur === target || path.includes(target)) return true;
|
|
2168
|
+
if (cur && fromScreen && cur !== fromScreen && routesBelongToSameFeature(cur, target)) {
|
|
2169
|
+
return true;
|
|
2170
|
+
}
|
|
2171
|
+
for (const route of path) {
|
|
2172
|
+
if (route !== fromScreen && routesBelongToSameFeature(route, target)) return true;
|
|
2173
|
+
}
|
|
2174
|
+
return false;
|
|
2175
|
+
}
|
|
2176
|
+
function screenDepartedBaseline(fromScreen, fromSignature) {
|
|
2177
|
+
if (fromSignature) {
|
|
2178
|
+
const curSig = getCurrentScreenSignature();
|
|
2179
|
+
return !!(curSig && curSig !== fromSignature);
|
|
2180
|
+
}
|
|
2181
|
+
const cur = getCurrentScreen();
|
|
2182
|
+
return !!(cur && fromScreen && cur !== fromScreen);
|
|
2183
|
+
}
|
|
2184
|
+
async function waitForScreenSettle(opts = {}) {
|
|
2185
|
+
const maxMs = opts.maxMs ?? 3500;
|
|
2186
|
+
const pollMs = opts.pollMs ?? 75;
|
|
2187
|
+
const stableTarget = opts.stableReads ?? 3;
|
|
2188
|
+
const mountFlushMs = opts.mountFlushMs ?? 150;
|
|
2189
|
+
const expectingChange = opts.expectingChange ?? false;
|
|
2190
|
+
const fromScreen = opts.fromScreen ?? null;
|
|
2191
|
+
const fromSignature = opts.fromSignature ?? null;
|
|
2192
|
+
const targetScreen = opts.targetScreen ?? null;
|
|
2193
|
+
const startMs = Date.now();
|
|
2194
|
+
const remaining = () => maxMs - (Date.now() - startMs);
|
|
2195
|
+
let transitioned = false;
|
|
2196
|
+
if (expectingChange) {
|
|
2197
|
+
while (remaining() > 0) {
|
|
2198
|
+
if (targetScreen) {
|
|
2199
|
+
if (navigationTargetReached(targetScreen, fromScreen)) {
|
|
2200
|
+
transitioned = true;
|
|
2201
|
+
break;
|
|
2202
|
+
}
|
|
2203
|
+
} else if (fromSignature) {
|
|
2204
|
+
const curSig = getCurrentScreenSignature();
|
|
2205
|
+
if (curSig && curSig !== fromSignature) {
|
|
2206
|
+
transitioned = true;
|
|
2207
|
+
break;
|
|
2208
|
+
}
|
|
2209
|
+
} else if (screenDepartedBaseline(fromScreen, null)) {
|
|
2210
|
+
transitioned = true;
|
|
2211
|
+
break;
|
|
2212
|
+
}
|
|
2213
|
+
await sleep(pollMs);
|
|
2214
|
+
}
|
|
2215
|
+
if (!transitioned) {
|
|
2216
|
+
const partialMove = screenDepartedBaseline(fromScreen, fromSignature);
|
|
2217
|
+
if (!partialMove) {
|
|
2218
|
+
return {
|
|
2219
|
+
screen: getCurrentScreen(),
|
|
2220
|
+
transitioned: false,
|
|
2221
|
+
timedOut: true,
|
|
2222
|
+
waitedMs: Date.now() - startMs
|
|
2223
|
+
};
|
|
2224
|
+
}
|
|
2225
|
+
transitioned = true;
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
let last = getCurrentScreen();
|
|
2229
|
+
let stableCount = 1;
|
|
2230
|
+
while (remaining() > 0) {
|
|
2231
|
+
await sleep(pollMs);
|
|
2232
|
+
const cur = getCurrentScreen();
|
|
2233
|
+
if (cur === last) {
|
|
2234
|
+
stableCount++;
|
|
2235
|
+
if (stableCount >= stableTarget) {
|
|
2236
|
+
const flush = Math.min(mountFlushMs, Math.max(0, remaining()));
|
|
2237
|
+
if (flush > 0) await sleep(flush);
|
|
2238
|
+
return {
|
|
2239
|
+
screen: cur,
|
|
2240
|
+
transitioned,
|
|
2241
|
+
timedOut: false,
|
|
2242
|
+
waitedMs: Date.now() - startMs
|
|
2243
|
+
};
|
|
2244
|
+
}
|
|
2245
|
+
} else {
|
|
2246
|
+
stableCount = 1;
|
|
2247
|
+
last = cur;
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
return {
|
|
2251
|
+
screen: getCurrentScreen(),
|
|
2252
|
+
transitioned,
|
|
2253
|
+
timedOut: true,
|
|
2254
|
+
waitedMs: Date.now() - startMs
|
|
2255
|
+
};
|
|
2256
|
+
}
|
|
2257
|
+
var sleepMs = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
2258
|
+
async function waitForLoadingSettle(opts = {}) {
|
|
2259
|
+
const maxMs = opts.maxMs ?? 6e3;
|
|
2260
|
+
const initialPollMs = opts.pollMs ?? 150;
|
|
2261
|
+
const backoffFactor = opts.backoffFactor ?? 1.5;
|
|
2262
|
+
const pollMaxMs = opts.pollMaxMs ?? 1e3;
|
|
2263
|
+
const stableTarget = opts.stableReads ?? 2;
|
|
2264
|
+
const pressedComponentId = opts.pressedComponentId ?? null;
|
|
2265
|
+
const fromScreen = opts.fromScreen ?? null;
|
|
2266
|
+
const startMs = Date.now();
|
|
2267
|
+
const remaining = () => maxMs - (Date.now() - startMs);
|
|
2268
|
+
let probe = probeLoadingState(pressedComponentId, getCurrentScreen());
|
|
2269
|
+
let probeCount = 1;
|
|
2270
|
+
let stableCount = 1;
|
|
2271
|
+
let lastFingerprint = probe.fingerprint;
|
|
2272
|
+
let interval = initialPollMs;
|
|
2273
|
+
const looksImmediatelyClean = !probe.loading && !probe.pressedDisabled && (!pressedComponentId || probe.pressedFound);
|
|
2274
|
+
if (looksImmediatelyClean) {
|
|
2275
|
+
await sleepMs(Math.min(80, Math.max(0, remaining())));
|
|
2276
|
+
const recheck = probeLoadingState(pressedComponentId, getCurrentScreen());
|
|
2277
|
+
probeCount++;
|
|
2278
|
+
if (!recheck.loading && !recheck.pressedDisabled && recheck.fingerprint === probe.fingerprint) {
|
|
2279
|
+
return {
|
|
2280
|
+
settled: true,
|
|
2281
|
+
transitioned: false,
|
|
2282
|
+
loadingPending: false,
|
|
2283
|
+
screen: getCurrentScreen(),
|
|
2284
|
+
waitedMs: Date.now() - startMs,
|
|
2285
|
+
probeCount
|
|
2286
|
+
};
|
|
2287
|
+
}
|
|
2288
|
+
probe = recheck;
|
|
2289
|
+
lastFingerprint = recheck.fingerprint;
|
|
2290
|
+
stableCount = 1;
|
|
2291
|
+
}
|
|
2292
|
+
while (remaining() > 0) {
|
|
2293
|
+
const sleepFor = Math.min(interval, Math.max(0, remaining()));
|
|
2294
|
+
if (sleepFor === 0) break;
|
|
2295
|
+
await sleepMs(sleepFor);
|
|
2296
|
+
const currentScreen = getCurrentScreen();
|
|
2297
|
+
if (fromScreen && currentScreen && currentScreen !== fromScreen) {
|
|
2298
|
+
return {
|
|
2299
|
+
settled: true,
|
|
2300
|
+
transitioned: true,
|
|
2301
|
+
loadingPending: false,
|
|
2302
|
+
screen: currentScreen,
|
|
2303
|
+
waitedMs: Date.now() - startMs,
|
|
2304
|
+
probeCount
|
|
2305
|
+
};
|
|
2306
|
+
}
|
|
2307
|
+
probe = probeLoadingState(pressedComponentId, currentScreen);
|
|
2308
|
+
probeCount++;
|
|
2309
|
+
const quiescent = !probe.loading && !probe.pressedDisabled && probe.fingerprint === lastFingerprint;
|
|
2310
|
+
if (quiescent) {
|
|
2311
|
+
stableCount++;
|
|
2312
|
+
if (stableCount >= stableTarget) {
|
|
2313
|
+
return {
|
|
2314
|
+
settled: true,
|
|
2315
|
+
transitioned: false,
|
|
2316
|
+
loadingPending: false,
|
|
2317
|
+
screen: currentScreen,
|
|
2318
|
+
waitedMs: Date.now() - startMs,
|
|
2319
|
+
probeCount
|
|
2320
|
+
};
|
|
2321
|
+
}
|
|
2322
|
+
} else {
|
|
2323
|
+
stableCount = 1;
|
|
2324
|
+
lastFingerprint = probe.fingerprint;
|
|
2325
|
+
}
|
|
2326
|
+
interval = Math.min(pollMaxMs, Math.round(interval * backoffFactor));
|
|
2327
|
+
}
|
|
2328
|
+
const stillPending = probe.loading || probe.pressedDisabled;
|
|
2329
|
+
return {
|
|
2330
|
+
settled: !stillPending,
|
|
2331
|
+
transitioned: false,
|
|
2332
|
+
loadingPending: stillPending,
|
|
2333
|
+
screen: getCurrentScreen(),
|
|
2334
|
+
waitedMs: Date.now() - startMs,
|
|
2335
|
+
probeCount
|
|
2336
|
+
};
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
// src/navigation/registerScreen.ts
|
|
2340
|
+
var screenRegistry = /* @__PURE__ */ new Map();
|
|
2341
|
+
function registerScreen(metadata) {
|
|
2342
|
+
screenRegistry.set(metadata.name, metadata);
|
|
2343
|
+
}
|
|
2344
|
+
function getScreenMetadata(name) {
|
|
2345
|
+
return screenRegistry.get(name);
|
|
2346
|
+
}
|
|
2347
|
+
function getAllScreens() {
|
|
2348
|
+
return Array.from(screenRegistry.values());
|
|
2349
|
+
}
|
|
2350
|
+
function clearScreenRegistry() {
|
|
2351
|
+
screenRegistry.clear();
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
exports.AppilotsClient = AppilotsClient;
|
|
2355
|
+
exports.AppilotsProvider = AppilotsProvider;
|
|
2356
|
+
exports.AppilotsRegistryProvider = AppilotsRegistryProvider;
|
|
2357
|
+
exports.SDK_VERSION = SDK_VERSION;
|
|
2358
|
+
exports.StreamAbortError = StreamAbortError;
|
|
2359
|
+
exports.StreamTransportError = StreamTransportError;
|
|
2360
|
+
exports._patchJsxRuntimes = _patchJsxRuntimes;
|
|
2361
|
+
exports.appilotsDebugWarn = appilotsDebugWarn;
|
|
2362
|
+
exports.clearAppilotsDebugTraces = clearAppilotsDebugTraces;
|
|
2363
|
+
exports.clearScreenRegistry = clearScreenRegistry;
|
|
2364
|
+
exports.componentRegistry = componentRegistry;
|
|
2365
|
+
exports.createComponentRegistry = createComponentRegistry;
|
|
2366
|
+
exports.createElementRegistry = createElementRegistry;
|
|
2367
|
+
exports.createListRegistry = createListRegistry;
|
|
2368
|
+
exports.elementRegistry = elementRegistry;
|
|
2369
|
+
exports.enableAppilotsAutoTracking = enableAppilotsAutoTracking;
|
|
2370
|
+
exports.getActiveRouteNames = getActiveRouteNames;
|
|
2371
|
+
exports.getAllScreens = getAllScreens;
|
|
2372
|
+
exports.getAppilotsDebugTraces = getAppilotsDebugTraces;
|
|
2373
|
+
exports.getCurrentScreen = getCurrentScreen;
|
|
2374
|
+
exports.getCurrentScreenSignature = getCurrentScreenSignature;
|
|
2375
|
+
exports.getDefaultRegistry = getDefaultRegistry;
|
|
2376
|
+
exports.getFiberRoot = getFiberRoot;
|
|
2377
|
+
exports.getGlobalConfig = getGlobalConfig;
|
|
2378
|
+
exports.getNavigationRef = getNavigationRef;
|
|
2379
|
+
exports.getNavigationStateSnapshot = getNavigationStateSnapshot;
|
|
2380
|
+
exports.getScreenMetadata = getScreenMetadata;
|
|
2381
|
+
exports.initAppilots = initAppilots;
|
|
2382
|
+
exports.isAutoTrackingEnabled = isAutoTrackingEnabled;
|
|
2383
|
+
exports.listRegistry = listRegistry;
|
|
2384
|
+
exports.probeLoadingState = probeLoadingState;
|
|
2385
|
+
exports.recordAppilotsDebugTrace = recordAppilotsDebugTrace;
|
|
2386
|
+
exports.registerScreen = registerScreen;
|
|
2387
|
+
exports.routesBelongToSameFeature = routesBelongToSameFeature;
|
|
2388
|
+
exports.screenDepartedBaseline = screenDepartedBaseline;
|
|
2389
|
+
exports.setCurrentScreen = setCurrentScreen;
|
|
2390
|
+
exports.setNavigationRef = setNavigationRef;
|
|
2391
|
+
exports.subscribeAppilotsDebugTraces = subscribeAppilotsDebugTraces;
|
|
2392
|
+
exports.useAppilotsContext = useAppilotsContext;
|
|
2393
|
+
exports.useResolvedRegistry = useResolvedRegistry;
|
|
2394
|
+
exports.waitForLoadingSettle = waitForLoadingSettle;
|
|
2395
|
+
exports.waitForScreenSettle = waitForScreenSettle;
|