@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.
@@ -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;