@sagepilot-ai/react-native-sdk 0.2.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -30,15 +30,26 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ SAGEPILOT_DEFAULT_IMAGE_MAX_DIMENSION: () => SAGEPILOT_DEFAULT_IMAGE_MAX_DIMENSION,
34
+ SAGEPILOT_DEFAULT_IMAGE_MAX_FILE_SIZE_BYTES: () => SAGEPILOT_DEFAULT_IMAGE_MAX_FILE_SIZE_BYTES,
35
+ SAGEPILOT_DEFAULT_IMAGE_QUALITY: () => SAGEPILOT_DEFAULT_IMAGE_QUALITY,
33
36
  SagepilotChat: () => SagepilotChat,
34
37
  SagepilotChatError: () => SagepilotChatError,
35
38
  SagepilotChatProvider: () => SagepilotChatProvider,
39
+ SagepilotFilePickerError: () => SagepilotFilePickerError,
36
40
  createAsyncStorageCacheStorage: () => createAsyncStorageCacheStorage,
37
41
  createKeychainTokenStorage: () => createKeychainTokenStorage,
42
+ createSagepilotFilePicker: () => createSagepilotFilePicker,
43
+ createSagepilotFileStore: () => createSagepilotFileStore,
44
+ ensureSagepilotAndroidCameraPermission: () => ensureSagepilotAndroidCameraPermission,
45
+ promptOpenSagepilotCameraSettings: () => promptOpenSagepilotCameraSettings,
38
46
  useSagepilotChat: () => useSagepilotChat
39
47
  });
40
48
  module.exports = __toCommonJS(index_exports);
41
49
 
50
+ // src/public/SdkClient.ts
51
+ var import_react_native = require("react-native");
52
+
42
53
  // src/core/errors/SagepilotChatError.ts
43
54
  var SagepilotChatError = class extends Error {
44
55
  constructor(code, message, options = {}) {
@@ -56,7 +67,7 @@ var SagepilotChatError = class extends Error {
56
67
 
57
68
  // src/core/config/constants.ts
58
69
  var SDK_NAME = "@sagepilot-ai/react-native-sdk";
59
- var SDK_VERSION = "0.2.4";
70
+ var SDK_VERSION = "0.3.0";
60
71
  var DEFAULT_HOST = "https://app.sagepilot.ai";
61
72
  var DEFAULT_WIDGET_HOST = "https://app.sagepilot.ai";
62
73
  var CUSTOMER_API_PREFIX = "/customer-api/v1";
@@ -365,6 +376,34 @@ async function resolveDeviceInfo(adapter) {
365
376
  return adapter.getDeviceInfo();
366
377
  }
367
378
 
379
+ // src/core/storage/cache.ts
380
+ function createAsyncStorageCacheStorage(asyncStorage) {
381
+ return {
382
+ getItem: (key) => asyncStorage.getItem(key),
383
+ setItem: (key, value) => asyncStorage.setItem(key, value),
384
+ removeItem: (key) => asyncStorage.removeItem(key)
385
+ };
386
+ }
387
+ function createJsonCache(storage, namespace) {
388
+ return {
389
+ async get(key) {
390
+ const value = await storage.getItem(`${namespace}:${key}`);
391
+ if (!value) return null;
392
+ try {
393
+ return JSON.parse(value);
394
+ } catch {
395
+ return null;
396
+ }
397
+ },
398
+ async set(key, value) {
399
+ await storage.setItem(`${namespace}:${key}`, JSON.stringify(value));
400
+ },
401
+ async remove(key) {
402
+ await storage.removeItem(`${namespace}:${key}`);
403
+ }
404
+ };
405
+ }
406
+
368
407
  // src/resources/channels.ts
369
408
  function bootstrapChannel(http, host, channelId) {
370
409
  return http.request(
@@ -448,6 +487,9 @@ function fetchUnreadCount(http, host, channelId, sessionId, authorizationHeader)
448
487
  }
449
488
 
450
489
  // src/public/SdkClient.ts
490
+ var PRESENTATION_CACHE_NAMESPACE = "sagepilot_rn_presentation";
491
+ var PRESENTATION_CACHE_KEY = "state";
492
+ var PRESENTATION_RESTORE_MAX_AGE_MS = 15 * 60 * 1e3;
451
493
  var DEFAULT_MOBILE_LAUNCHER_CONFIG = {
452
494
  label: "Chat",
453
495
  buttonColor: "#173c2d",
@@ -517,6 +559,10 @@ function readRecordField(input, key) {
517
559
  if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
518
560
  return value;
519
561
  }
562
+ function isFreshPresentationState(updatedAt) {
563
+ if (typeof updatedAt !== "number" || !Number.isFinite(updatedAt)) return false;
564
+ return Date.now() - updatedAt <= PRESENTATION_RESTORE_MAX_AGE_MS;
565
+ }
520
566
  function toPublicSessionState(session) {
521
567
  return {
522
568
  session_id: session.session_id,
@@ -573,6 +619,9 @@ var SagepilotReactNativeChat = class {
573
619
  const { workspaceId, channelId } = parseKey(config.key);
574
620
  this.runtimeOptions = {
575
621
  behavior: config.behavior,
622
+ cacheStorage: config.cacheStorage,
623
+ filePicker: config.filePicker,
624
+ fileStore: config.fileStore,
576
625
  presentation: config.presentation,
577
626
  theme: config.theme,
578
627
  hostedClaimsStorageKeyPrefix: config.hostedClaimsStorageKeyPrefix || DEFAULT_HOSTED_CLAIMS_STORAGE_KEY_PREFIX
@@ -603,6 +652,7 @@ var SagepilotReactNativeChat = class {
603
652
  this.channel = channel;
604
653
  this.session = session;
605
654
  this.isConfigured = true;
655
+ await this.restorePresentationFromCache();
606
656
  this.emitReady();
607
657
  this.emitState();
608
658
  if (config.behavior?.enableUnreadPolling ?? true) {
@@ -742,12 +792,14 @@ var SagepilotReactNativeChat = class {
742
792
  this.hostedChatView = { screen: "home" };
743
793
  if (this.presented) {
744
794
  this.emitState();
795
+ this.persistPresentationState();
745
796
  return true;
746
797
  }
747
798
  this.presented = true;
748
799
  this.setUnreadCount(0);
749
800
  this.presentCallbacks.forEach((callback) => callback(this.getLifecycleState()));
750
801
  this.emitState();
802
+ this.persistPresentationState();
751
803
  return true;
752
804
  }
753
805
  presentMessages() {
@@ -775,6 +827,7 @@ var SagepilotReactNativeChat = class {
775
827
  this.emitUnreadChange();
776
828
  this.dismissCallbacks.forEach((callback) => callback(this.getLifecycleState()));
777
829
  this.emitState();
830
+ this.persistPresentationState();
778
831
  return true;
779
832
  }
780
833
  hide() {
@@ -909,6 +962,10 @@ var SagepilotReactNativeChat = class {
909
962
  }
910
963
  return this.session?.conversation_id ?? void 0;
911
964
  }
965
+ /** Returns the chat id that is explicitly part of the current hosted URL. */
966
+ getHostedRouteChatId() {
967
+ return this.hostedChatView.screen === "composer" ? this.hostedChatView.chatId : void 0;
968
+ }
912
969
  getHostedIdentityMessage() {
913
970
  if (!this.legacyWidgetJwt) return null;
914
971
  return {
@@ -919,6 +976,34 @@ var SagepilotReactNativeChat = class {
919
976
  }
920
977
  };
921
978
  }
979
+ /** Captures the hosted route that should receive a picked attachment batch. */
980
+ getHostedAttachmentTarget() {
981
+ return {
982
+ chatId: this.getActiveHostedChatId(),
983
+ routeChatId: this.getHostedRouteChatId(),
984
+ hostedChatView: this.serializeHostedChatView(this.hostedChatView)
985
+ };
986
+ }
987
+ /** Restores the hosted route for a queued attachment batch before delivery. */
988
+ restoreHostedAttachmentTarget(target) {
989
+ if (import_react_native.Platform.OS !== "android") return false;
990
+ if (!target) return false;
991
+ const hostedChatView = this.readPersistedHostedChatView(target.hostedChatView);
992
+ if (!hostedChatView) return false;
993
+ const currentChatId = this.getActiveHostedChatId();
994
+ const targetChatId = normalizeOptionalString(target.chatId);
995
+ const routeChatId = normalizeOptionalString(target.routeChatId) ?? (hostedChatView.screen === "composer" ? normalizeOptionalString(hostedChatView.chatId) : void 0);
996
+ const nextHostedChatView = routeChatId && hostedChatView.screen === "composer" ? this.withPinnedChatId(hostedChatView, routeChatId) : hostedChatView;
997
+ if (this.presented && (!targetChatId || currentChatId === targetChatId) && this.areHostedChatViewsEqual(this.hostedChatView, nextHostedChatView)) {
998
+ return false;
999
+ }
1000
+ this.hostedChatView = nextHostedChatView;
1001
+ this.presented = true;
1002
+ this.setUnreadCount(0);
1003
+ this.emitState();
1004
+ this.persistPresentationState();
1005
+ return true;
1006
+ }
922
1007
  handleHostedBridgeMessage(message) {
923
1008
  if (!message) return false;
924
1009
  if (message.type === "close_widget") {
@@ -986,6 +1071,7 @@ var SagepilotReactNativeChat = class {
986
1071
  }
987
1072
  destroy() {
988
1073
  this.stopUnreadPolling();
1074
+ this.persistPresentationState({ cleanShutdown: true, presented: false });
989
1075
  this.resetRuntimeState();
990
1076
  this.emitState();
991
1077
  this.identifyCallbacks.clear();
@@ -1141,8 +1227,86 @@ var SagepilotReactNativeChat = class {
1141
1227
  this.presentCallbacks.forEach((callback) => callback(this.getLifecycleState()));
1142
1228
  }
1143
1229
  this.emitState();
1230
+ this.persistPresentationState();
1144
1231
  return true;
1145
1232
  }
1233
+ /** Returns the dedicated cache used for hosted-chat presentation recovery. */
1234
+ getPresentationCache() {
1235
+ const cacheStorage = this.runtimeOptions?.cacheStorage;
1236
+ if (!cacheStorage) return null;
1237
+ return createJsonCache(cacheStorage, PRESENTATION_CACHE_NAMESPACE);
1238
+ }
1239
+ /** Removes callback-only fields so the hosted route can be safely serialized. */
1240
+ serializeHostedChatView(view) {
1241
+ if (view.screen !== "composer") return view;
1242
+ return {
1243
+ screen: "composer",
1244
+ message: view.message,
1245
+ mode: view.mode,
1246
+ chatId: view.chatId,
1247
+ metadata: view.metadata
1248
+ };
1249
+ }
1250
+ /** Validates a persisted hosted view before using it to rebuild a widget URL. */
1251
+ readPersistedHostedChatView(view) {
1252
+ if (!view || typeof view !== "object") return null;
1253
+ const record = view;
1254
+ if (record.screen === "home") return { screen: "home" };
1255
+ if (record.screen === "messages") return { screen: "messages" };
1256
+ if (record.screen !== "composer") return null;
1257
+ const mode = record.mode === "new" ? "new" : "auto";
1258
+ return {
1259
+ screen: "composer",
1260
+ message: typeof record.message === "string" ? record.message : void 0,
1261
+ mode,
1262
+ chatId: typeof record.chatId === "string" && record.chatId ? record.chatId : void 0,
1263
+ metadata: readRecordField(record, "metadata")
1264
+ };
1265
+ }
1266
+ /** Forces a persisted hosted route to target the exact chat that owns a pending attachment. */
1267
+ withPinnedChatId(view, chatId) {
1268
+ if (view.screen === "composer") {
1269
+ return {
1270
+ ...view,
1271
+ chatId
1272
+ };
1273
+ }
1274
+ return {
1275
+ screen: "composer",
1276
+ mode: "auto",
1277
+ chatId
1278
+ };
1279
+ }
1280
+ /** Compares hosted route state without relying on object identity. */
1281
+ areHostedChatViewsEqual(first, second) {
1282
+ return JSON.stringify(this.serializeHostedChatView(first)) === JSON.stringify(this.serializeHostedChatView(second));
1283
+ }
1284
+ /** Persists presentation state without blocking customer-facing SDK methods. */
1285
+ persistPresentationState(options) {
1286
+ const cache = this.getPresentationCache();
1287
+ if (!cache) return;
1288
+ const state = {
1289
+ presented: options?.presented ?? this.presented,
1290
+ hostedChatView: this.serializeHostedChatView(this.hostedChatView),
1291
+ updatedAt: Date.now(),
1292
+ cleanShutdown: options?.cleanShutdown
1293
+ };
1294
+ void cache.set(PRESENTATION_CACHE_KEY, state).catch(() => void 0);
1295
+ }
1296
+ /** Restores Android presentation state after configure without resetting the hosted route to Home. */
1297
+ async restorePresentationFromCache() {
1298
+ const cache = this.getPresentationCache();
1299
+ if (!cache || import_react_native.Platform.OS !== "android") return;
1300
+ const persisted = await cache.get(PRESENTATION_CACHE_KEY).catch(() => null);
1301
+ if (persisted?.cleanShutdown) return;
1302
+ if (!isFreshPresentationState(persisted?.updatedAt)) return;
1303
+ if (!persisted?.presented) return;
1304
+ const hostedChatView = this.readPersistedHostedChatView(persisted.hostedChatView);
1305
+ if (!hostedChatView) return;
1306
+ this.hostedChatView = hostedChatView;
1307
+ this.presented = true;
1308
+ this.setUnreadCount(0);
1309
+ }
1146
1310
  };
1147
1311
  var internalSagepilotChat = new SagepilotReactNativeChat();
1148
1312
  var SagepilotChat = {
@@ -1179,21 +1343,368 @@ var SagepilotChat = {
1179
1343
  destroy: () => internalSagepilotChat.destroy()
1180
1344
  };
1181
1345
 
1182
- // src/core/storage/cache.ts
1183
- function createAsyncStorageCacheStorage(asyncStorage) {
1346
+ // src/core/storage/fileStore.ts
1347
+ var DEFAULT_DIRECTORY_NAME = "sagepilot-attachments";
1348
+ function sanitizeKey(key) {
1349
+ return key.replace(/[^a-zA-Z0-9._-]/g, "_").replace(/\.{2,}/g, "_");
1350
+ }
1351
+ function createSagepilotFileStore(blobUtil, options = {}) {
1352
+ const directory = `${blobUtil.fs.dirs.DocumentDir}/${options.directoryName ?? DEFAULT_DIRECTORY_NAME}`;
1353
+ const pathFor = (key) => `${directory}/${sanitizeKey(key)}`;
1354
+ let dirReady = null;
1355
+ function ensureDirectory() {
1356
+ if (!dirReady) {
1357
+ dirReady = (async () => {
1358
+ if (!await blobUtil.fs.exists(directory)) {
1359
+ await blobUtil.fs.mkdir(directory);
1360
+ }
1361
+ })().catch((error) => {
1362
+ dirReady = null;
1363
+ throw error;
1364
+ });
1365
+ }
1366
+ return dirReady;
1367
+ }
1184
1368
  return {
1185
- getItem: (key) => asyncStorage.getItem(key),
1186
- setItem: (key, value) => asyncStorage.setItem(key, value),
1187
- removeItem: (key) => asyncStorage.removeItem(key)
1369
+ async write(key, base64) {
1370
+ await ensureDirectory();
1371
+ await blobUtil.fs.writeFile(pathFor(key), base64, "base64");
1372
+ },
1373
+ async read(key) {
1374
+ const content = await blobUtil.fs.readFile(pathFor(key), "base64");
1375
+ if (typeof content !== "string" || content.length === 0) {
1376
+ throw new Error("Persisted attachment is empty or unreadable.");
1377
+ }
1378
+ return content;
1379
+ },
1380
+ async remove(key) {
1381
+ try {
1382
+ if (await blobUtil.fs.exists(pathFor(key))) {
1383
+ await blobUtil.fs.unlink(pathFor(key));
1384
+ }
1385
+ } catch {
1386
+ }
1387
+ },
1388
+ async prune(keepKeys) {
1389
+ try {
1390
+ if (!await blobUtil.fs.exists(directory)) return;
1391
+ const keep = new Set(keepKeys.map(sanitizeKey));
1392
+ const entries = await blobUtil.fs.ls(directory);
1393
+ await Promise.all(
1394
+ entries.filter((entry) => !keep.has(entry)).map((entry) => blobUtil.fs.unlink(`${directory}/${entry}`).catch(() => void 0))
1395
+ );
1396
+ } catch {
1397
+ }
1398
+ }
1399
+ };
1400
+ }
1401
+
1402
+ // src/core/native/filePicker.ts
1403
+ var import_react_native2 = require("react-native");
1404
+
1405
+ // src/core/native/filePickerShared.ts
1406
+ var SagepilotFilePickerError = class extends Error {
1407
+ /** Creates a typed picker error that can be surfaced over the widget bridge. */
1408
+ constructor(code, message) {
1409
+ super(message);
1410
+ this.name = "SagepilotFilePickerError";
1411
+ this.code = code;
1412
+ }
1413
+ };
1414
+ var SAGEPILOT_DEFAULT_IMAGE_MAX_DIMENSION = 1920;
1415
+ var SAGEPILOT_DEFAULT_IMAGE_QUALITY = 0.8;
1416
+ var SAGEPILOT_DEFAULT_IMAGE_MAX_FILE_SIZE_BYTES = 15 * 1024 * 1024;
1417
+
1418
+ // src/core/native/filePicker.ts
1419
+ var DEBUG_PREFIX = "[SagepilotSDK][FilePicker]";
1420
+ var DEFAULT_IMAGE_MAX_DIMENSION = SAGEPILOT_DEFAULT_IMAGE_MAX_DIMENSION;
1421
+ var DEFAULT_IMAGE_QUALITY = SAGEPILOT_DEFAULT_IMAGE_QUALITY;
1422
+ var DEFAULT_IMAGE_MIME_TYPE = "image/jpeg";
1423
+ var DEFAULT_DOCUMENT_MIME_TYPE = "application/octet-stream";
1424
+ var DEFAULT_IMAGE_SELECTION_LIMIT = 5;
1425
+ var DEFAULT_DOCUMENT_MAX_FILE_SIZE_BYTES = 20 * 1024 * 1024;
1426
+ var DEFAULT_IMAGE_MAX_FILE_SIZE_BYTES = SAGEPILOT_DEFAULT_IMAGE_MAX_FILE_SIZE_BYTES;
1427
+ function bytesToMb(bytes) {
1428
+ return Math.round(bytes / (1024 * 1024) * 10) / 10;
1429
+ }
1430
+ function estimateBase64ByteSize(dataBase64) {
1431
+ return Math.floor(dataBase64.length * 3 / 4);
1432
+ }
1433
+ function stripFileScheme(uri) {
1434
+ return uri.startsWith("file://") ? uri.replace("file://", "") : uri;
1435
+ }
1436
+ function debugFilePicker(message, details) {
1437
+ console.log(`${DEBUG_PREFIX} ${message}`, details ?? "");
1438
+ }
1439
+ async function promptOpenSagepilotCameraSettings() {
1440
+ if (import_react_native2.Platform.OS !== "android") return;
1441
+ await new Promise((resolve) => {
1442
+ import_react_native2.Alert.alert(
1443
+ "Camera access is off",
1444
+ "Enable camera access for this app in Settings to take a photo.",
1445
+ [
1446
+ {
1447
+ text: "Not now",
1448
+ style: "cancel",
1449
+ onPress: resolve
1450
+ },
1451
+ {
1452
+ text: "Open Settings",
1453
+ onPress: () => {
1454
+ import_react_native2.Linking.openSettings().finally(resolve);
1455
+ }
1456
+ }
1457
+ ]
1458
+ );
1459
+ });
1460
+ }
1461
+ async function ensureSagepilotAndroidCameraPermission() {
1462
+ if (import_react_native2.Platform.OS !== "android") return;
1463
+ let result;
1464
+ try {
1465
+ debugFilePicker("requesting Android camera permission");
1466
+ result = await import_react_native2.PermissionsAndroid.request(import_react_native2.PermissionsAndroid.PERMISSIONS.CAMERA);
1467
+ } catch {
1468
+ debugFilePicker("camera permission request threw; continuing to native launch");
1469
+ return;
1470
+ }
1471
+ debugFilePicker("Android camera permission result", { result });
1472
+ if (result === import_react_native2.PermissionsAndroid.RESULTS.NEVER_ASK_AGAIN) {
1473
+ await promptOpenSagepilotCameraSettings();
1474
+ throw new SagepilotFilePickerError(
1475
+ "permission_denied",
1476
+ "Camera access is turned off. Enable it for this app in Settings to take a photo."
1477
+ );
1478
+ }
1479
+ if (result === import_react_native2.PermissionsAndroid.RESULTS.DENIED) {
1480
+ throw new SagepilotFilePickerError(
1481
+ "permission_denied",
1482
+ "Camera permission is required to take a photo."
1483
+ );
1484
+ }
1485
+ }
1486
+ function canUseCameraSource(imagePicker) {
1487
+ return Boolean(imagePicker);
1488
+ }
1489
+ function mapImagePickerErrorCode(errorCode) {
1490
+ switch (errorCode) {
1491
+ case "permission":
1492
+ return "permission_denied";
1493
+ case "camera_unavailable":
1494
+ return "camera_unavailable";
1495
+ default:
1496
+ return "unknown";
1497
+ }
1498
+ }
1499
+ function imageAssetsToPickedFiles(result, maxFileSizeBytes) {
1500
+ if (result.didCancel) return [];
1501
+ if (result.errorCode) {
1502
+ throw new SagepilotFilePickerError(
1503
+ mapImagePickerErrorCode(result.errorCode),
1504
+ result.errorMessage || `Could not capture the photo (${result.errorCode}).`
1505
+ );
1506
+ }
1507
+ const assets = result.assets ?? [];
1508
+ const usable = assets.filter((asset) => typeof asset.base64 === "string" && asset.base64.length > 0);
1509
+ if (assets.length > usable.length) {
1510
+ throw new SagepilotFilePickerError(
1511
+ "encode_failed",
1512
+ assets.length === 1 ? "The photo could not be processed. Please try again." : "Some photos could not be processed. Please try selecting them again."
1513
+ );
1514
+ }
1515
+ return usable.map((asset, index) => {
1516
+ const dataBase64 = asset.base64;
1517
+ const size = asset.fileSize ?? estimateBase64ByteSize(dataBase64);
1518
+ if (maxFileSizeBytes > 0 && size > maxFileSizeBytes) {
1519
+ throw new SagepilotFilePickerError(
1520
+ "file_too_large",
1521
+ `Image is too large (max ${bytesToMb(maxFileSizeBytes)}MB).`
1522
+ );
1523
+ }
1524
+ return {
1525
+ file_name: asset.fileName || `photo-${Date.now()}-${index + 1}.jpg`,
1526
+ mime_type: asset.type || DEFAULT_IMAGE_MIME_TYPE,
1527
+ size,
1528
+ data_base64: dataBase64
1529
+ };
1530
+ });
1531
+ }
1532
+ async function readUriAsBase64(uri, fileReader) {
1533
+ if (fileReader) {
1534
+ const content = await fileReader.fs.readFile(stripFileScheme(uri), "base64");
1535
+ if (typeof content !== "string") {
1536
+ throw new Error("File reader returned unexpected content.");
1537
+ }
1538
+ return content;
1539
+ }
1540
+ const response = await fetch(uri);
1541
+ const blob = await response.blob();
1542
+ return new Promise((resolve, reject) => {
1543
+ const reader = new FileReader();
1544
+ reader.onload = () => {
1545
+ const dataUrl = typeof reader.result === "string" ? reader.result : "";
1546
+ const [, payload = ""] = dataUrl.split(",");
1547
+ if (!payload) {
1548
+ reject(new Error("Could not read the selected file."));
1549
+ return;
1550
+ }
1551
+ resolve(payload);
1552
+ };
1553
+ reader.onerror = () => reject(new Error("Could not read the selected file."));
1554
+ reader.readAsDataURL(blob);
1555
+ });
1556
+ }
1557
+ function createSagepilotFilePicker(options) {
1558
+ const { imagePicker, documentsPicker, fileReader } = options;
1559
+ const imageMaxDimension = options.imageMaxDimension ?? DEFAULT_IMAGE_MAX_DIMENSION;
1560
+ const imageQuality = options.imageQuality ?? DEFAULT_IMAGE_QUALITY;
1561
+ const imageSelectionLimit = options.imageSelectionLimit ?? DEFAULT_IMAGE_SELECTION_LIMIT;
1562
+ const documentMaxFileSizeBytes = options.documentMaxFileSizeBytes ?? DEFAULT_DOCUMENT_MAX_FILE_SIZE_BYTES;
1563
+ const imageMaxFileSizeBytes = options.imageMaxFileSizeBytes ?? DEFAULT_IMAGE_MAX_FILE_SIZE_BYTES;
1564
+ const sources = [];
1565
+ if (canUseCameraSource(imagePicker)) sources.push("camera");
1566
+ if (imagePicker) sources.push("library");
1567
+ if (documentsPicker) sources.push("documents");
1568
+ debugFilePicker("adapter created", {
1569
+ platform: import_react_native2.Platform.OS,
1570
+ hasImagePicker: Boolean(imagePicker),
1571
+ hasDocumentsPicker: Boolean(documentsPicker),
1572
+ hasFileReader: Boolean(fileReader),
1573
+ sources
1574
+ });
1575
+ if (sources.length === 0) return void 0;
1576
+ const imageOptions = {
1577
+ mediaType: "photo",
1578
+ includeBase64: true,
1579
+ maxWidth: imageMaxDimension,
1580
+ maxHeight: imageMaxDimension,
1581
+ quality: imageQuality,
1582
+ saveToPhotos: false
1583
+ };
1584
+ async function pickFromCamera() {
1585
+ debugFilePicker("camera pick requested", {
1586
+ platform: import_react_native2.Platform.OS,
1587
+ imageMaxDimension,
1588
+ imageQuality,
1589
+ imageMaxFileSizeBytes
1590
+ });
1591
+ await ensureSagepilotAndroidCameraPermission();
1592
+ if (!imagePicker) {
1593
+ throw new SagepilotFilePickerError("camera_unavailable", "The camera picker is not available in this build.");
1594
+ }
1595
+ debugFilePicker("launching image-picker camera", { platform: import_react_native2.Platform.OS });
1596
+ return imageAssetsToPickedFiles(await imagePicker.launchCamera(imageOptions), imageMaxFileSizeBytes);
1597
+ }
1598
+ async function pickFromLibrary(multiple) {
1599
+ if (!imagePicker) return [];
1600
+ debugFilePicker("library pick requested", { multiple, imageSelectionLimit });
1601
+ return imageAssetsToPickedFiles(await imagePicker.launchImageLibrary({
1602
+ ...imageOptions,
1603
+ // Bounded multi-select: 0 (unlimited) lets a huge batch OOM the bridge.
1604
+ selectionLimit: multiple ? Math.max(0, imageSelectionLimit) : 1
1605
+ }), imageMaxFileSizeBytes);
1606
+ }
1607
+ async function pickDocuments(multiple) {
1608
+ if (!documentsPicker) return [];
1609
+ debugFilePicker("document pick requested", { multiple, documentMaxFileSizeBytes });
1610
+ let picked;
1611
+ try {
1612
+ picked = await documentsPicker.pick({ allowMultiSelection: multiple });
1613
+ } catch (error) {
1614
+ if (error && typeof error === "object" && error.code === "OPERATION_CANCELED") {
1615
+ return [];
1616
+ }
1617
+ throw error;
1618
+ }
1619
+ const files = [];
1620
+ for (const file of picked) {
1621
+ if (documentMaxFileSizeBytes > 0 && typeof file.size === "number" && file.size > documentMaxFileSizeBytes) {
1622
+ throw new SagepilotFilePickerError(
1623
+ "file_too_large",
1624
+ `"${file.name || "File"}" is too large (max ${bytesToMb(documentMaxFileSizeBytes)}MB).`
1625
+ );
1626
+ }
1627
+ let readableUri = file.uri;
1628
+ if (documentsPicker.keepLocalCopy) {
1629
+ const fileName = file.name || `file-${Date.now()}`;
1630
+ const [copy] = await documentsPicker.keepLocalCopy({
1631
+ files: [{ uri: file.uri, fileName }],
1632
+ destination: "cachesDirectory"
1633
+ });
1634
+ if (copy?.status === "success" && copy.localUri) {
1635
+ readableUri = copy.localUri;
1636
+ }
1637
+ }
1638
+ let dataBase64;
1639
+ try {
1640
+ dataBase64 = await readUriAsBase64(readableUri, fileReader);
1641
+ } catch (error) {
1642
+ throw new SagepilotFilePickerError(
1643
+ "read_failed",
1644
+ error instanceof Error && error.message ? error.message : "Could not read the selected file."
1645
+ );
1646
+ }
1647
+ files.push({
1648
+ file_name: file.name || `file-${Date.now()}`,
1649
+ mime_type: file.type || DEFAULT_DOCUMENT_MIME_TYPE,
1650
+ size: file.size ?? estimateBase64ByteSize(dataBase64),
1651
+ data_base64: dataBase64
1652
+ });
1653
+ }
1654
+ return files;
1655
+ }
1656
+ return {
1657
+ sources,
1658
+ async pickFiles(request) {
1659
+ debugFilePicker("pickFiles request", request);
1660
+ if (request.source === "camera") return pickFromCamera();
1661
+ if (request.source === "library") return pickFromLibrary(request.multiple);
1662
+ return pickDocuments(request.multiple);
1663
+ }
1188
1664
  };
1189
1665
  }
1190
1666
 
1191
1667
  // src/ui/SagepilotChatProvider.ts
1192
1668
  var import_react = require("react");
1193
- var import_react_native = require("react-native");
1669
+ var import_lucide_react_native = require("lucide-react-native");
1670
+ var import_react_native3 = require("react-native");
1194
1671
  var import_react_native_webview = require("react-native-webview");
1195
1672
 
1196
1673
  // src/core/webview/mobileBridge.ts
1674
+ var FILE_PICKER_PROTOCOL_VERSION = 2;
1675
+ function buildBridgeCapabilitiesPayload(capabilities) {
1676
+ return { ...capabilities, filePickerProtocol: FILE_PICKER_PROTOCOL_VERSION };
1677
+ }
1678
+ function buildBridgeCapabilitiesScript(capabilities) {
1679
+ const payload = buildBridgeCapabilitiesPayload(capabilities);
1680
+ return [
1681
+ "(function(){",
1682
+ "try {",
1683
+ "if (window.SagepilotMobileBridge) {",
1684
+ `window.SagepilotMobileBridge.capabilities = Object.assign({}, window.SagepilotMobileBridge.capabilities, ${JSON.stringify(payload)});`,
1685
+ "}",
1686
+ "} catch (_) {}",
1687
+ "true;",
1688
+ "})();"
1689
+ ].join("\n");
1690
+ }
1691
+ function buildLivenessPingScript(nonce, staleMs) {
1692
+ return [
1693
+ "(function(){",
1694
+ "try {",
1695
+ "var hb = window.__sagepilotWidgetHeartbeat;",
1696
+ "var supports = typeof hb === 'number';",
1697
+ `var alive = !supports ? true : (Date.now() - hb < ${Math.max(0, Math.floor(staleMs))});`,
1698
+ "var send = function(){",
1699
+ " var msg = JSON.stringify({ type: 'sagepilot:pong', nonce: " + JSON.stringify(nonce) + ", alive: alive, supportsHeartbeat: supports });",
1700
+ " if (window.ReactNativeWebView && window.ReactNativeWebView.postMessage) { window.ReactNativeWebView.postMessage(msg); }",
1701
+ "};",
1702
+ "send();",
1703
+ "} catch (_) {}",
1704
+ "true;",
1705
+ "})();"
1706
+ ].join("\n");
1707
+ }
1197
1708
  function isHostedBridgeMessage(value) {
1198
1709
  if (!value || typeof value !== "object") return false;
1199
1710
  const message = value;
@@ -1214,9 +1725,23 @@ function parseHostedBridgeMessage(rawData) {
1214
1725
  return null;
1215
1726
  }
1216
1727
  }
1217
- var mobileWebViewBridgeScript = `
1728
+ function buildMobileWebViewBridgeScript(capabilities) {
1729
+ const payload = buildBridgeCapabilitiesPayload(capabilities);
1730
+ return `
1218
1731
  (function () {
1219
- if (window.__sagepilotRnBridgeInstalled) return true;
1732
+ var bridgeCapabilities = ${JSON.stringify(payload)};
1733
+ var applyBridgeCapabilities = function () {
1734
+ try {
1735
+ if (window.SagepilotMobileBridge) {
1736
+ window.SagepilotMobileBridge.capabilities = Object.assign({}, window.SagepilotMobileBridge.capabilities, bridgeCapabilities);
1737
+ }
1738
+ } catch (error) {}
1739
+ };
1740
+
1741
+ if (window.__sagepilotRnBridgeInstalled) {
1742
+ applyBridgeCapabilities();
1743
+ return true;
1744
+ }
1220
1745
  window.__sagepilotRnBridgeInstalled = true;
1221
1746
 
1222
1747
  var ensureViewport = function () {
@@ -1257,9 +1782,11 @@ var mobileWebViewBridgeScript = `
1257
1782
  };
1258
1783
 
1259
1784
  window.SagepilotMobileBridge = {
1785
+ capabilities: bridgeCapabilities,
1260
1786
  postMessage: sendToReactNative,
1261
1787
  ready: function () { sendToReactNative({ type: "sagepilot:ready" }); }
1262
1788
  };
1789
+ applyBridgeCapabilities();
1263
1790
 
1264
1791
  document.addEventListener("click", function (event) {
1265
1792
  try {
@@ -1300,6 +1827,7 @@ var mobileWebViewBridgeScript = `
1300
1827
  return true;
1301
1828
  })();
1302
1829
  `;
1830
+ }
1303
1831
 
1304
1832
  // src/specs/SagepilotInsetsViewNativeComponent.ts
1305
1833
  var import_codegenNativeComponent = __toESM(require("react-native/Libraries/Utilities/codegenNativeComponent"));
@@ -1316,12 +1844,133 @@ function readPresentationState() {
1316
1844
  presentation: internalSagepilotChat.getConfig()?.presentation
1317
1845
  };
1318
1846
  }
1847
+ function getBridgeCapabilities() {
1848
+ return {
1849
+ nativeFilePicker: Boolean(internalSagepilotChat.getConfig()?.filePicker)
1850
+ };
1851
+ }
1319
1852
  function getInjectedWebViewScript() {
1853
+ const capabilities = getBridgeCapabilities();
1320
1854
  return [
1321
1855
  internalSagepilotChat.getHostedAuthScript(),
1322
- mobileWebViewBridgeScript
1856
+ buildMobileWebViewBridgeScript(capabilities),
1857
+ buildBridgeCapabilitiesScript(capabilities)
1323
1858
  ].filter(Boolean).join("\n");
1324
1859
  }
1860
+ var FILE_PICKER_SOURCE_LABELS = {
1861
+ camera: "Take photo",
1862
+ library: "Choose from gallery",
1863
+ documents: "Browse files"
1864
+ };
1865
+ var FILE_PICKER_SOURCE_DESCRIPTIONS = {
1866
+ camera: "Use the in-app camera",
1867
+ library: "Photos and videos on this device",
1868
+ documents: "PDFs, documents, and Drive files"
1869
+ };
1870
+ var FILE_PICKER_SOURCE_ICONS = {
1871
+ camera: import_lucide_react_native.Camera,
1872
+ library: import_lucide_react_native.Images,
1873
+ documents: import_lucide_react_native.FileText
1874
+ };
1875
+ var FILE_PICKER_SOURCE_ICON_COLORS = {
1876
+ camera: "#0284c7",
1877
+ library: "#16a34a",
1878
+ documents: "#7c3aed"
1879
+ };
1880
+ var DEBUG_PREFIX2 = "[SagepilotSDK][Provider]";
1881
+ var DELIVERY_RETRY_MS = 1500;
1882
+ var DELIVERY_LEGACY_FALLBACK_ATTEMPTS = 2;
1883
+ var DELIVERY_MAX_ATTEMPTS = 8;
1884
+ var PERSIST_MAX_TOTAL_BYTES = 2 * 1024 * 1024;
1885
+ var PENDING_FILES_CACHE_NAMESPACE = "sagepilot_rn_picker";
1886
+ var PENDING_FILES_CACHE_KEY = "pending_batch";
1887
+ var LIVENESS_HEARTBEAT_STALE_MS = 4e3;
1888
+ var LIVENESS_PONG_TIMEOUT_MS = 1500;
1889
+ var LIVENESS_MAX_PING_ATTEMPTS = 2;
1890
+ var LIVENESS_PICKER_GRACE_MS = 8e3;
1891
+ var LIVENESS_MAX_REMOUNTS = 2;
1892
+ var batchSequence = 0;
1893
+ function nextBatchId() {
1894
+ batchSequence += 1;
1895
+ const entropy = Math.random().toString(36).slice(2, 8);
1896
+ return `b_${Date.now().toString(36)}_${batchSequence}_${entropy}`;
1897
+ }
1898
+ function totalBase64Bytes(files) {
1899
+ return files.reduce((sum, file) => sum + (file.data_base64?.length ?? 0), 0);
1900
+ }
1901
+ function storageKeyFor(batchId, index) {
1902
+ return `${batchId}_${index}.bin`;
1903
+ }
1904
+ function renderFilePickerSourceIcon(source) {
1905
+ const Icon = FILE_PICKER_SOURCE_ICONS[source];
1906
+ return (0, import_react.createElement)(
1907
+ import_react_native3.View,
1908
+ { style: getFilePickerSourceIconStyle(source) },
1909
+ (0, import_react.createElement)(Icon, {
1910
+ color: FILE_PICKER_SOURCE_ICON_COLORS[source],
1911
+ size: 23,
1912
+ strokeWidth: 2.35
1913
+ })
1914
+ );
1915
+ }
1916
+ function getFilePickerSourceIconStyle(source) {
1917
+ if (source === "camera") return [styles.sheetOptionIcon, styles.cameraOptionIcon];
1918
+ if (source === "library") return [styles.sheetOptionIcon, styles.libraryOptionIcon];
1919
+ return [styles.sheetOptionIcon, styles.documentsOptionIcon];
1920
+ }
1921
+ function getSheetOptionStyle(pressed) {
1922
+ return [styles.sheetOption, pressed && styles.sheetOptionPressed];
1923
+ }
1924
+ function getSheetCancelStyle(pressed) {
1925
+ return [styles.sheetCancelButton, pressed && styles.sheetCancelButtonPressed];
1926
+ }
1927
+ function buildDispatchScript(messageLiteral) {
1928
+ return [
1929
+ "(function(){",
1930
+ "try {",
1931
+ `var message = ${messageLiteral};`,
1932
+ "window.dispatchEvent(new MessageEvent('message', { data: message, origin: window.location.origin, source: window.parent || window }));",
1933
+ "} catch (_) {}",
1934
+ "true;",
1935
+ "})();"
1936
+ ].join("\n");
1937
+ }
1938
+ function buildFilesPickedScript(batch) {
1939
+ return buildDispatchScript(JSON.stringify({
1940
+ type: "sagepilot:files_picked",
1941
+ batch_id: batch.batchId,
1942
+ chat_id: batch.target?.chatId,
1943
+ conversation_id: batch.target?.chatId,
1944
+ files: batch.files
1945
+ }));
1946
+ }
1947
+ function buildFilePickerErrorScript(message, code) {
1948
+ return buildDispatchScript(
1949
+ `{ type: "sagepilot:file_picker_error", message: ${JSON.stringify(message)}${code ? `, code: ${JSON.stringify(code)}` : ""} }`
1950
+ );
1951
+ }
1952
+ function buildFilePickerCancelledScript() {
1953
+ return buildDispatchScript(`{ type: "sagepilot:file_picker_cancelled" }`);
1954
+ }
1955
+ function readFilePickerError(error) {
1956
+ if (error && typeof error === "object") {
1957
+ const candidate = error;
1958
+ const message = typeof candidate.message === "string" && candidate.message ? candidate.message : "Could not attach the selected file.";
1959
+ const code = typeof candidate.code === "string" ? candidate.code : void 0;
1960
+ return { message, code };
1961
+ }
1962
+ return { message: "Could not attach the selected file." };
1963
+ }
1964
+ function debugProvider(message, details) {
1965
+ console.log(`${DEBUG_PREFIX2} ${message}`, details ?? "");
1966
+ }
1967
+ function describeAttachmentTarget(target) {
1968
+ return {
1969
+ targetChatId: target?.chatId,
1970
+ targetRouteChatId: target?.routeChatId,
1971
+ targetScreen: target?.hostedChatView.screen
1972
+ };
1973
+ }
1325
1974
  function getHostedIdentityDispatchScript() {
1326
1975
  const message = internalSagepilotChat.getHostedIdentityMessage();
1327
1976
  if (!message) return "";
@@ -1344,10 +1993,13 @@ function readUrlOrigin(url) {
1344
1993
  return null;
1345
1994
  }
1346
1995
  }
1996
+ function isInternalWebViewScheme(url) {
1997
+ return url.startsWith("about:") || url.startsWith("data:") || url.startsWith("blob:") || url.startsWith("javascript:") || url.startsWith("file:");
1998
+ }
1347
1999
  var hostedChatWebViewProps = {
1348
2000
  allowFileAccess: true,
1349
2001
  allowFileAccessFromFileURLs: true,
1350
- ...import_react_native.Platform.OS === "ios" ? {
2002
+ ...import_react_native3.Platform.OS === "ios" ? {
1351
2003
  automaticallyAdjustContentInsets: false,
1352
2004
  contentInsetAdjustmentBehavior: "never",
1353
2005
  hideKeyboardAccessoryView: true
@@ -1362,7 +2014,7 @@ var hostedChatWebViewProps = {
1362
2014
  sharedCookiesEnabled: true,
1363
2015
  thirdPartyCookiesEnabled: true
1364
2016
  };
1365
- var AndroidInsetsView = import_react_native.Platform.OS === "android" ? SagepilotInsetsViewNativeComponent_default : import_react_native.View;
2017
+ var AndroidInsetsView = import_react_native3.Platform.OS === "android" ? SagepilotInsetsViewNativeComponent_default : import_react_native3.View;
1366
2018
  var emptyAndroidInsets = { top: 0, bottom: 0 };
1367
2019
  function SagepilotChatProvider({
1368
2020
  children,
@@ -1371,13 +2023,389 @@ function SagepilotChatProvider({
1371
2023
  }) {
1372
2024
  const [state, setState] = (0, import_react.useState)(readPresentationState);
1373
2025
  const [androidModalInsets, setAndroidModalInsets] = (0, import_react.useState)(emptyAndroidInsets);
2026
+ const [nativeWebViewKey, setNativeWebViewKey] = (0, import_react.useState)(0);
2027
+ const [preloadWebViewKey, setPreloadWebViewKey] = (0, import_react.useState)(0);
1374
2028
  const webFrameRef = (0, import_react.useRef)(null);
1375
2029
  const nativeWebViewRef = (0, import_react.useRef)(null);
2030
+ const pendingBatchesRef = (0, import_react.useRef)([]);
2031
+ const widgetReadyRef = (0, import_react.useRef)(false);
2032
+ const deliveryTimerRef = (0, import_react.useRef)(null);
2033
+ const deliveryAttemptsRef = (0, import_react.useRef)(0);
2034
+ const pendingPingRef = (0, import_react.useRef)(null);
2035
+ const pingTimeoutRef = (0, import_react.useRef)(null);
2036
+ const livenessResumeTimerRef = (0, import_react.useRef)(null);
2037
+ const livenessPausedUntilRef = (0, import_react.useRef)(0);
2038
+ const pickerInFlightRef = (0, import_react.useRef)(false);
2039
+ const livenessRemountCountRef = (0, import_react.useRef)(0);
2040
+ const appStateRef = (0, import_react.useRef)(import_react_native3.AppState.currentState);
2041
+ const didReconcileRef = (0, import_react.useRef)(false);
2042
+ const [androidRepaintTick, setAndroidRepaintTick] = (0, import_react.useState)(0);
2043
+ const [sourceChooser, setSourceChooser] = (0, import_react.useState)(null);
2044
+ const getPendingCache = (0, import_react.useCallback)(() => {
2045
+ const cacheStorage = internalSagepilotChat.getConfig()?.cacheStorage;
2046
+ if (!cacheStorage) return null;
2047
+ return createJsonCache(cacheStorage, PENDING_FILES_CACHE_NAMESPACE);
2048
+ }, []);
2049
+ const writeManifest = (0, import_react.useCallback)(() => {
2050
+ const cache = getPendingCache();
2051
+ if (!cache) return;
2052
+ const queue = pendingBatchesRef.current;
2053
+ if (queue.length === 0) {
2054
+ void cache.remove(PENDING_FILES_CACHE_KEY).catch(() => void 0);
2055
+ return;
2056
+ }
2057
+ const hasFileStore = Boolean(internalSagepilotChat.getConfig()?.fileStore);
2058
+ if (hasFileStore) {
2059
+ const manifest = queue.filter((batch) => batch.storageKeys && batch.storageKeys.length === batch.files.length).map((batch) => ({
2060
+ batchId: batch.batchId,
2061
+ target: batch.target,
2062
+ files: batch.files.map((file, index) => ({
2063
+ file_name: file.file_name,
2064
+ mime_type: file.mime_type,
2065
+ size: file.size,
2066
+ storageKey: batch.storageKeys[index]
2067
+ }))
2068
+ }));
2069
+ if (manifest.length === 0) {
2070
+ void cache.remove(PENDING_FILES_CACHE_KEY).catch(() => void 0);
2071
+ } else {
2072
+ void cache.set(PENDING_FILES_CACHE_KEY, manifest).catch(() => void 0);
2073
+ }
2074
+ return;
2075
+ }
2076
+ const totalBytes = queue.reduce((sum, batch) => sum + totalBase64Bytes(batch.files), 0);
2077
+ if (totalBytes <= PERSIST_MAX_TOTAL_BYTES) {
2078
+ const manifest = queue.map((batch) => ({
2079
+ batchId: batch.batchId,
2080
+ target: batch.target,
2081
+ files: batch.files.map((file) => ({
2082
+ file_name: file.file_name,
2083
+ mime_type: file.mime_type,
2084
+ size: file.size,
2085
+ data_base64: file.data_base64
2086
+ }))
2087
+ }));
2088
+ void cache.set(PENDING_FILES_CACHE_KEY, manifest).catch(() => void 0);
2089
+ } else {
2090
+ void cache.remove(PENDING_FILES_CACHE_KEY).catch(() => void 0);
2091
+ }
2092
+ }, [getPendingCache]);
2093
+ const writeBatchBytes = (0, import_react.useCallback)(async (batch) => {
2094
+ const fileStore = internalSagepilotChat.getConfig()?.fileStore;
2095
+ if (!fileStore) return;
2096
+ const entries = batch.files.map((file, index) => ({
2097
+ key: storageKeyFor(batch.batchId, index),
2098
+ base64: file.data_base64
2099
+ }));
2100
+ try {
2101
+ await Promise.all(entries.map((entry) => fileStore.write(entry.key, entry.base64)));
2102
+ } catch {
2103
+ return;
2104
+ }
2105
+ if (!pendingBatchesRef.current.some((queued) => queued.batchId === batch.batchId)) {
2106
+ void Promise.all(entries.map((entry) => fileStore.remove(entry.key).catch(() => void 0)));
2107
+ }
2108
+ }, []);
2109
+ const discardBatchFiles = (0, import_react.useCallback)((batch) => {
2110
+ if (!batch?.storageKeys) return;
2111
+ const fileStore = internalSagepilotChat.getConfig()?.fileStore;
2112
+ if (!fileStore) return;
2113
+ void Promise.all(batch.storageKeys.map((key) => fileStore.remove(key).catch(() => void 0)));
2114
+ }, []);
2115
+ const pumpDelivery = (0, import_react.useCallback)(() => {
2116
+ if (deliveryTimerRef.current) {
2117
+ clearTimeout(deliveryTimerRef.current);
2118
+ deliveryTimerRef.current = null;
2119
+ }
2120
+ const batch = pendingBatchesRef.current[0];
2121
+ if (!batch) return;
2122
+ if (internalSagepilotChat.restoreHostedAttachmentTarget(batch.target)) {
2123
+ debugProvider("restored hosted attachment target", {
2124
+ batchId: batch.batchId,
2125
+ ...describeAttachmentTarget(batch.target)
2126
+ });
2127
+ widgetReadyRef.current = false;
2128
+ deliveryTimerRef.current = setTimeout(() => pumpDelivery(), DELIVERY_RETRY_MS);
2129
+ return;
2130
+ }
2131
+ deliveryAttemptsRef.current += 1;
2132
+ const attempts = deliveryAttemptsRef.current;
2133
+ const ref = nativeWebViewRef.current;
2134
+ const shouldDeliver = ref && (widgetReadyRef.current || attempts >= DELIVERY_LEGACY_FALLBACK_ATTEMPTS);
2135
+ if (shouldDeliver && ref) {
2136
+ debugProvider("delivering native picked files", {
2137
+ batchId: batch.batchId,
2138
+ attempt: attempts,
2139
+ widgetReady: widgetReadyRef.current,
2140
+ count: batch.files.length,
2141
+ ...describeAttachmentTarget(batch.target)
2142
+ });
2143
+ ref.injectJavaScript(buildFilesPickedScript(batch));
2144
+ }
2145
+ if (attempts < DELIVERY_MAX_ATTEMPTS) {
2146
+ deliveryTimerRef.current = setTimeout(() => pumpDelivery(), DELIVERY_RETRY_MS);
2147
+ }
2148
+ }, []);
2149
+ const startDelivery = (0, import_react.useCallback)(() => {
2150
+ deliveryAttemptsRef.current = 0;
2151
+ pumpDelivery();
2152
+ }, [pumpDelivery]);
2153
+ const ensureDelivery = (0, import_react.useCallback)(() => {
2154
+ if (pendingBatchesRef.current.length === 0) return;
2155
+ if (deliveryTimerRef.current) return;
2156
+ startDelivery();
2157
+ }, [startDelivery]);
2158
+ const acknowledgeHeadBatch = (0, import_react.useCallback)(() => {
2159
+ const head = pendingBatchesRef.current[0];
2160
+ pendingBatchesRef.current = pendingBatchesRef.current.slice(1);
2161
+ deliveryAttemptsRef.current = 0;
2162
+ if (deliveryTimerRef.current) {
2163
+ clearTimeout(deliveryTimerRef.current);
2164
+ deliveryTimerRef.current = null;
2165
+ }
2166
+ discardBatchFiles(head);
2167
+ writeManifest();
2168
+ if (pendingBatchesRef.current.length > 0) startDelivery();
2169
+ }, [discardBatchFiles, writeManifest, startDelivery]);
2170
+ const queuePickedFiles = (0, import_react.useCallback)((files) => {
2171
+ const batchId = nextBatchId();
2172
+ const hasFileStore = Boolean(internalSagepilotChat.getConfig()?.fileStore);
2173
+ const storageKeys = hasFileStore ? files.map((_, index) => storageKeyFor(batchId, index)) : void 0;
2174
+ const batch = {
2175
+ batchId,
2176
+ files,
2177
+ storageKeys,
2178
+ target: internalSagepilotChat.getHostedAttachmentTarget()
2179
+ };
2180
+ debugProvider("queued native picked files", {
2181
+ batchId,
2182
+ count: files.length,
2183
+ totalBase64Bytes: totalBase64Bytes(files),
2184
+ persistedToFileStore: hasFileStore,
2185
+ ...describeAttachmentTarget(batch.target)
2186
+ });
2187
+ pendingBatchesRef.current = [...pendingBatchesRef.current, batch];
2188
+ ensureDelivery();
2189
+ writeManifest();
2190
+ void writeBatchBytes(batch);
2191
+ }, [ensureDelivery, writeManifest, writeBatchBytes]);
2192
+ const recoverNativeWebView = (0, import_react.useCallback)((reasonOrEvent) => {
2193
+ const reason = typeof reasonOrEvent === "string" ? reasonOrEvent : "webview_render_process";
2194
+ debugProvider("recovering native WebView", { reason });
2195
+ widgetReadyRef.current = false;
2196
+ setNativeWebViewKey((key) => key + 1);
2197
+ }, []);
2198
+ const recoverPreloadWebView = (0, import_react.useCallback)(() => {
2199
+ setPreloadWebViewKey((key) => key + 1);
2200
+ }, []);
2201
+ const pauseLivenessAfterPicker = (0, import_react.useCallback)((durationMs) => {
2202
+ if (import_react_native3.Platform.OS !== "android") return;
2203
+ livenessPausedUntilRef.current = Math.max(livenessPausedUntilRef.current, Date.now() + durationMs);
2204
+ }, []);
2205
+ const runNativeFilePicker = (0, import_react.useCallback)((source, multiple) => {
2206
+ const filePicker = internalSagepilotChat.getConfig()?.filePicker;
2207
+ if (!filePicker) {
2208
+ debugProvider("native file picker requested but no adapter is configured", { source, multiple });
2209
+ return;
2210
+ }
2211
+ if (pickerInFlightRef.current) {
2212
+ debugProvider("native file picker ignored because another picker is in flight", { source, multiple });
2213
+ return;
2214
+ }
2215
+ debugProvider("native file picker starting", {
2216
+ source,
2217
+ multiple,
2218
+ configuredSources: filePicker.sources
2219
+ });
2220
+ pauseLivenessAfterPicker(6e4);
2221
+ pickerInFlightRef.current = true;
2222
+ filePicker.pickFiles({ source, multiple }).then((files) => {
2223
+ debugProvider("native file picker resolved", {
2224
+ source,
2225
+ count: files.length,
2226
+ files: files.map((file) => ({
2227
+ fileName: file.file_name,
2228
+ mimeType: file.mime_type,
2229
+ size: file.size,
2230
+ base64Length: file.data_base64.length
2231
+ }))
2232
+ });
2233
+ if (files.length === 0) {
2234
+ nativeWebViewRef.current?.injectJavaScript(buildFilePickerCancelledScript());
2235
+ return;
2236
+ }
2237
+ queuePickedFiles(files);
2238
+ }).catch((error) => {
2239
+ const { message, code } = readFilePickerError(error);
2240
+ debugProvider("native file picker failed", { source, code, message });
2241
+ nativeWebViewRef.current?.injectJavaScript(buildFilePickerErrorScript(message, code));
2242
+ }).finally(() => {
2243
+ pickerInFlightRef.current = false;
2244
+ pauseLivenessAfterPicker(LIVENESS_PICKER_GRACE_MS);
2245
+ });
2246
+ }, [pauseLivenessAfterPicker, queuePickedFiles]);
2247
+ const openNativeFilePicker = (0, import_react.useCallback)((multiple) => {
2248
+ const filePicker = internalSagepilotChat.getConfig()?.filePicker;
2249
+ if (!filePicker || filePicker.sources.length === 0) {
2250
+ debugProvider("open native file picker ignored", {
2251
+ multiple,
2252
+ hasAdapter: Boolean(filePicker),
2253
+ sources: filePicker?.sources ?? []
2254
+ });
2255
+ return;
2256
+ }
2257
+ debugProvider("open native file picker", { multiple, sources: filePicker.sources });
2258
+ const [onlySource] = filePicker.sources;
2259
+ if (filePicker.sources.length === 1 && onlySource && onlySource !== "camera") {
2260
+ runNativeFilePicker(onlySource, multiple);
2261
+ return;
2262
+ }
2263
+ setSourceChooser({ multiple });
2264
+ }, [runNativeFilePicker]);
2265
+ const handleSourceChoice = (0, import_react.useCallback)((source) => {
2266
+ const chooser = sourceChooser;
2267
+ setSourceChooser(null);
2268
+ if (chooser) runNativeFilePicker(source, chooser.multiple);
2269
+ }, [sourceChooser, runNativeFilePicker]);
1376
2270
  (0, import_react.useEffect)(() => {
1377
2271
  return internalSagepilotChat.onStateChange(() => {
1378
2272
  setState(readPresentationState());
1379
2273
  });
1380
2274
  }, []);
2275
+ (0, import_react.useEffect)(() => {
2276
+ let cancelled = false;
2277
+ const cache = getPendingCache();
2278
+ if (!cache) return;
2279
+ const reconcile = async () => {
2280
+ if (didReconcileRef.current) return;
2281
+ const manifest = await cache.get(PENDING_FILES_CACHE_KEY).catch(() => null);
2282
+ if (cancelled || didReconcileRef.current || !Array.isArray(manifest) || manifest.length === 0) return;
2283
+ const fileStore = internalSagepilotChat.getConfig()?.fileStore;
2284
+ const restored = [];
2285
+ for (const batch of manifest) {
2286
+ if (!batch || typeof batch.batchId !== "string" || !Array.isArray(batch.files) || batch.files.length === 0) continue;
2287
+ const files = [];
2288
+ const storageKeys = [];
2289
+ let intact = true;
2290
+ for (const file of batch.files) {
2291
+ let dataBase64 = null;
2292
+ if (typeof file.storageKey === "string" && fileStore) {
2293
+ try {
2294
+ dataBase64 = await fileStore.read(file.storageKey);
2295
+ storageKeys.push(file.storageKey);
2296
+ } catch {
2297
+ intact = false;
2298
+ }
2299
+ } else if (typeof file.data_base64 === "string" && file.data_base64) {
2300
+ dataBase64 = file.data_base64;
2301
+ }
2302
+ if (!dataBase64) {
2303
+ intact = false;
2304
+ break;
2305
+ }
2306
+ files.push({ file_name: file.file_name, mime_type: file.mime_type, size: file.size, data_base64: dataBase64 });
2307
+ }
2308
+ if (intact && files.length === batch.files.length) {
2309
+ restored.push({
2310
+ batchId: batch.batchId,
2311
+ files,
2312
+ storageKeys: storageKeys.length === files.length ? storageKeys : void 0,
2313
+ target: batch.target
2314
+ });
2315
+ }
2316
+ }
2317
+ if (cancelled || didReconcileRef.current) return;
2318
+ didReconcileRef.current = true;
2319
+ pendingBatchesRef.current = [...restored, ...pendingBatchesRef.current];
2320
+ writeManifest();
2321
+ if (fileStore) {
2322
+ const keep = pendingBatchesRef.current.flatMap(
2323
+ (batch) => batch.files.map((_, index) => storageKeyFor(batch.batchId, index))
2324
+ );
2325
+ void fileStore.prune(keep).catch(() => void 0);
2326
+ }
2327
+ startDelivery();
2328
+ };
2329
+ void reconcile();
2330
+ return () => {
2331
+ cancelled = true;
2332
+ };
2333
+ }, [getPendingCache, startDelivery, writeManifest]);
2334
+ const clearPing = (0, import_react.useCallback)(() => {
2335
+ pendingPingRef.current = null;
2336
+ if (pingTimeoutRef.current) {
2337
+ clearTimeout(pingTimeoutRef.current);
2338
+ pingTimeoutRef.current = null;
2339
+ }
2340
+ }, []);
2341
+ const clearDeferredLivenessProbe = (0, import_react.useCallback)(() => {
2342
+ if (livenessResumeTimerRef.current) {
2343
+ clearTimeout(livenessResumeTimerRef.current);
2344
+ livenessResumeTimerRef.current = null;
2345
+ }
2346
+ }, []);
2347
+ (0, import_react.useEffect)(() => {
2348
+ return () => {
2349
+ if (deliveryTimerRef.current) {
2350
+ clearTimeout(deliveryTimerRef.current);
2351
+ deliveryTimerRef.current = null;
2352
+ }
2353
+ if (pingTimeoutRef.current) {
2354
+ clearTimeout(pingTimeoutRef.current);
2355
+ pingTimeoutRef.current = null;
2356
+ }
2357
+ clearDeferredLivenessProbe();
2358
+ };
2359
+ }, [clearDeferredLivenessProbe]);
2360
+ const runLivenessProbe = (0, import_react.useCallback)(() => {
2361
+ if (import_react_native3.Platform.OS !== "android") return;
2362
+ const ref = nativeWebViewRef.current;
2363
+ if (!ref || !internalSagepilotChat.isPresented()) return;
2364
+ const pauseRemainingMs = livenessPausedUntilRef.current - Date.now();
2365
+ if (pickerInFlightRef.current || pauseRemainingMs > 0) {
2366
+ const retryDelayMs = pickerInFlightRef.current ? 500 : Math.max(0, pauseRemainingMs);
2367
+ debugProvider("liveness probe deferred after picker", {
2368
+ pickerInFlight: pickerInFlightRef.current,
2369
+ retryDelayMs
2370
+ });
2371
+ clearDeferredLivenessProbe();
2372
+ livenessResumeTimerRef.current = setTimeout(() => {
2373
+ livenessResumeTimerRef.current = null;
2374
+ runLivenessProbe();
2375
+ }, retryDelayMs);
2376
+ return;
2377
+ }
2378
+ setAndroidRepaintTick((tick) => tick + 1);
2379
+ const attempts = (pendingPingRef.current?.attempts ?? 0) + 1;
2380
+ const nonce = `${Date.now().toString(36)}_${attempts}`;
2381
+ pendingPingRef.current = { nonce, attempts };
2382
+ ref.injectJavaScript(buildLivenessPingScript(nonce, LIVENESS_HEARTBEAT_STALE_MS));
2383
+ if (pingTimeoutRef.current) clearTimeout(pingTimeoutRef.current);
2384
+ pingTimeoutRef.current = setTimeout(() => {
2385
+ if (attempts >= LIVENESS_MAX_PING_ATTEMPTS) {
2386
+ clearPing();
2387
+ recoverNativeWebView("liveness_timeout");
2388
+ return;
2389
+ }
2390
+ runLivenessProbe();
2391
+ }, LIVENESS_PONG_TIMEOUT_MS);
2392
+ }, [clearDeferredLivenessProbe, clearPing, recoverNativeWebView]);
2393
+ (0, import_react.useEffect)(() => {
2394
+ const subscription = import_react_native3.AppState.addEventListener("change", (nextState) => {
2395
+ const prev = appStateRef.current;
2396
+ appStateRef.current = nextState;
2397
+ if (nextState === "active" && (prev === "background" || prev === "inactive")) {
2398
+ clearPing();
2399
+ livenessRemountCountRef.current = 0;
2400
+ runLivenessProbe();
2401
+ ensureDelivery();
2402
+ }
2403
+ });
2404
+ return () => {
2405
+ subscription.remove();
2406
+ clearPing();
2407
+ };
2408
+ }, [clearPing, runLivenessProbe, ensureDelivery]);
1381
2409
  const handleAndroidInsetsChange = (0, import_react.useCallback)((event) => {
1382
2410
  const nextBottomInset = event.nativeEvent?.bottom;
1383
2411
  const nextTopInset = event.nativeEvent?.top;
@@ -1391,11 +2419,11 @@ function SagepilotChatProvider({
1391
2419
  const presentationStyle = state.presentation?.style ?? "sheet";
1392
2420
  const isFullScreenModal = presentationStyle === "fullScreen";
1393
2421
  const animationType = presentationStyle === "fullScreen" || presentationStyle === "push" ? "slide" : "fade";
1394
- const ModalContainer = import_react_native.Platform.OS === "ios" && isFullScreenModal ? import_react_native.SafeAreaView : import_react_native.View;
1395
- const NativeModalContainer = import_react_native.Platform.OS === "android" ? AndroidInsetsView : ModalContainer;
1396
- const ChatContentContainer = import_react_native.Platform.OS === "ios" ? import_react_native.KeyboardAvoidingView : import_react_native.View;
1397
- const nativeModalContainerProps = import_react_native.Platform.OS === "android" ? { style: styles.container, onInsetsChange: handleAndroidInsetsChange } : { style: styles.container };
1398
- const chatContentContainerProps = import_react_native.Platform.OS === "ios" ? {
2422
+ const ModalContainer = import_react_native3.Platform.OS === "ios" && isFullScreenModal ? import_react_native3.SafeAreaView : import_react_native3.View;
2423
+ const NativeModalContainer = import_react_native3.Platform.OS === "android" ? AndroidInsetsView : ModalContainer;
2424
+ const ChatContentContainer = import_react_native3.Platform.OS === "ios" ? import_react_native3.KeyboardAvoidingView : import_react_native3.View;
2425
+ const nativeModalContainerProps = import_react_native3.Platform.OS === "android" ? { style: styles.container, onInsetsChange: handleAndroidInsetsChange } : { style: styles.container };
2426
+ const chatContentContainerProps = import_react_native3.Platform.OS === "ios" ? {
1399
2427
  behavior: "padding",
1400
2428
  enabled: true,
1401
2429
  keyboardVerticalOffset: 0,
@@ -1403,14 +2431,57 @@ function SagepilotChatProvider({
1403
2431
  } : {
1404
2432
  style: [
1405
2433
  styles.modalContent,
1406
- import_react_native.Platform.OS === "android" ? {
2434
+ import_react_native3.Platform.OS === "android" ? {
1407
2435
  paddingTop: androidModalInsets.top,
1408
2436
  paddingBottom: androidModalInsets.bottom
1409
2437
  } : null
1410
2438
  ].filter(Boolean)
1411
2439
  };
1412
2440
  const handleWebViewMessage = (event) => {
1413
- internalSagepilotChat.handleHostedBridgeMessage(parseHostedBridgeMessage(event.nativeEvent?.data));
2441
+ const message = parseHostedBridgeMessage(event.nativeEvent?.data);
2442
+ if (message?.type === "sagepilot:open_file_picker") {
2443
+ openNativeFilePicker(message.multiple ?? true);
2444
+ return;
2445
+ }
2446
+ if (message?.type === "sagepilot:widget_listener_ready") {
2447
+ widgetReadyRef.current = true;
2448
+ startDelivery();
2449
+ return;
2450
+ }
2451
+ if (message?.type === "sagepilot:files_received") {
2452
+ const ackBatchId = message.batch_id;
2453
+ const head = pendingBatchesRef.current[0];
2454
+ if (head && (!ackBatchId || ackBatchId === head.batchId)) {
2455
+ debugProvider("native picked files acknowledged", {
2456
+ batchId: head.batchId,
2457
+ ackBatchId,
2458
+ count: message.count
2459
+ });
2460
+ acknowledgeHeadBatch();
2461
+ }
2462
+ return;
2463
+ }
2464
+ if (message?.type === "sagepilot:pong") {
2465
+ const pending = pendingPingRef.current;
2466
+ if (pending && (!message.nonce || message.nonce === pending.nonce)) {
2467
+ if (message.alive === false) {
2468
+ clearPing();
2469
+ debugProvider("liveness probe reported dead widget", {
2470
+ supportsHeartbeat: message.supportsHeartbeat,
2471
+ remountCount: livenessRemountCountRef.current
2472
+ });
2473
+ if (livenessRemountCountRef.current < LIVENESS_MAX_REMOUNTS) {
2474
+ livenessRemountCountRef.current += 1;
2475
+ recoverNativeWebView("liveness_dead_pong");
2476
+ }
2477
+ } else {
2478
+ livenessRemountCountRef.current = 0;
2479
+ clearPing();
2480
+ }
2481
+ }
2482
+ return;
2483
+ }
2484
+ internalSagepilotChat.handleHostedBridgeMessage(message);
1414
2485
  };
1415
2486
  const postIdentityToWebFrame = () => {
1416
2487
  const message = internalSagepilotChat.getHostedIdentityMessage();
@@ -1423,8 +2494,31 @@ function SagepilotChatProvider({
1423
2494
  if (!script || !nativeWebViewRef.current) return;
1424
2495
  nativeWebViewRef.current.injectJavaScript(script);
1425
2496
  };
2497
+ const injectNativeBridgeToNativeWebView = () => {
2498
+ if (!nativeWebViewRef.current) return;
2499
+ nativeWebViewRef.current.injectJavaScript(getInjectedWebViewScript());
2500
+ };
2501
+ const handleNativeWebViewLoadEnd = () => {
2502
+ debugProvider("native WebView load end");
2503
+ injectNativeBridgeToNativeWebView();
2504
+ postIdentityToNativeWebView();
2505
+ widgetReadyRef.current = false;
2506
+ startDelivery();
2507
+ };
2508
+ const handleShouldStartLoadWithRequest = (0, import_react.useCallback)((request) => {
2509
+ const url = request?.url ?? "";
2510
+ if (!url || request?.isTopFrame === false || isInternalWebViewScheme(url)) {
2511
+ return true;
2512
+ }
2513
+ const widgetOrigin = readUrlOrigin(state.conversationUrl ?? state.preloadUrl);
2514
+ if (widgetOrigin && readUrlOrigin(url) === widgetOrigin) {
2515
+ return true;
2516
+ }
2517
+ import_react_native3.Linking.openURL(url).catch(() => void 0);
2518
+ return false;
2519
+ }, [state.conversationUrl, state.preloadUrl]);
1426
2520
  (0, import_react.useEffect)(() => {
1427
- if (import_react_native.Platform.OS !== "web" || typeof window === "undefined") return;
2521
+ if (import_react_native3.Platform.OS !== "web" || typeof window === "undefined") return;
1428
2522
  const trustedWidgetOrigin = readUrlOrigin(state.conversationUrl);
1429
2523
  if (!trustedWidgetOrigin) return;
1430
2524
  const handleWindowMessage = (event) => {
@@ -1435,16 +2529,16 @@ function SagepilotChatProvider({
1435
2529
  return () => window.removeEventListener("message", handleWindowMessage);
1436
2530
  }, [state.conversationUrl]);
1437
2531
  (0, import_react.useEffect)(() => {
1438
- if (import_react_native.Platform.OS !== "web" || !state.isPresented) return;
2532
+ if (import_react_native3.Platform.OS !== "web" || !state.isPresented) return;
1439
2533
  postIdentityToWebFrame();
1440
2534
  }, [state.isPresented, state.conversationUrl, state.configured]);
1441
2535
  (0, import_react.useEffect)(() => {
1442
- if (import_react_native.Platform.OS === "web" || !state.isPresented) return;
2536
+ if (import_react_native3.Platform.OS === "web" || !state.isPresented) return;
1443
2537
  postIdentityToNativeWebView();
1444
2538
  }, [state.isPresented, state.conversationUrl, state.configured]);
1445
- if (import_react_native.Platform.OS === "web") {
2539
+ if (import_react_native3.Platform.OS === "web") {
1446
2540
  return (0, import_react.createElement)(
1447
- import_react_native.View,
2541
+ import_react_native3.View,
1448
2542
  { style: styles.root },
1449
2543
  children,
1450
2544
  state.configured && !state.isPresented && state.shouldPreload && state.preloadUrl ? (0, import_react.createElement)("iframe", {
@@ -1453,20 +2547,20 @@ function SagepilotChatProvider({
1453
2547
  title: "Sagepilot chat preload"
1454
2548
  }) : null,
1455
2549
  state.configured && state.isPresented && state.conversationUrl ? (0, import_react.createElement)(
1456
- import_react_native.View,
2550
+ import_react_native3.View,
1457
2551
  { style: styles.webOverlay },
1458
2552
  (0, import_react.createElement)(
1459
- import_react_native.View,
2553
+ import_react_native3.View,
1460
2554
  { style: styles.webPanel },
1461
2555
  showCloseButton ? (0, import_react.createElement)(
1462
- import_react_native.Pressable,
2556
+ import_react_native3.Pressable,
1463
2557
  {
1464
2558
  accessibilityRole: "button",
1465
2559
  accessibilityLabel: closeLabel,
1466
2560
  onPress: () => internalSagepilotChat.dismiss(),
1467
2561
  style: styles.webCloseButton
1468
2562
  },
1469
- (0, import_react.createElement)(import_react_native.Text, { style: styles.closeText }, closeLabel)
2563
+ (0, import_react.createElement)(import_react_native3.Text, { style: styles.closeText }, closeLabel)
1470
2564
  ) : null,
1471
2565
  (0, import_react.createElement)("iframe", {
1472
2566
  ref: webFrameRef,
@@ -1480,24 +2574,32 @@ function SagepilotChatProvider({
1480
2574
  );
1481
2575
  }
1482
2576
  return (0, import_react.createElement)(
1483
- import_react_native.View,
2577
+ import_react_native3.View,
1484
2578
  { style: styles.root },
1485
2579
  children,
1486
2580
  state.configured && !state.isPresented && state.shouldPreload && state.preloadUrl ? (0, import_react.createElement)(import_react_native_webview.WebView, {
1487
2581
  ...hostedChatWebViewProps,
2582
+ // Separate key from the hosted WebView: a hidden-preload renderer crash
2583
+ // must not bump the hosted key (which would reset widgetReadyRef and
2584
+ // disrupt an in-flight delivery). onRenderProcessGone must still be
2585
+ // handled here or an unhandled renderer kill crashes the whole app.
2586
+ key: `sagepilot-preload-webview-${preloadWebViewKey}`,
1488
2587
  source: { uri: state.preloadUrl },
1489
2588
  style: styles.preloadWebview,
1490
2589
  injectedJavaScriptBeforeContentLoaded: getInjectedWebViewScript(),
1491
- onMessage: handleWebViewMessage
2590
+ onMessage: handleWebViewMessage,
2591
+ onShouldStartLoadWithRequest: handleShouldStartLoadWithRequest,
2592
+ onRenderProcessGone: recoverPreloadWebView,
2593
+ onContentProcessDidTerminate: recoverPreloadWebView
1492
2594
  }) : null,
1493
2595
  (0, import_react.createElement)(
1494
- import_react_native.Modal,
2596
+ import_react_native3.Modal,
1495
2597
  {
1496
2598
  visible: state.configured && state.isPresented,
1497
2599
  animationType,
1498
2600
  presentationStyle: isFullScreenModal ? "fullScreen" : "pageSheet",
1499
- statusBarTranslucent: import_react_native.Platform.OS === "android",
1500
- navigationBarTranslucent: import_react_native.Platform.OS === "android",
2601
+ statusBarTranslucent: import_react_native3.Platform.OS === "android",
2602
+ navigationBarTranslucent: import_react_native3.Platform.OS === "android",
1501
2603
  onRequestClose: () => internalSagepilotChat.dismiss()
1502
2604
  },
1503
2605
  (0, import_react.createElement)(
@@ -1507,41 +2609,104 @@ function SagepilotChatProvider({
1507
2609
  ChatContentContainer,
1508
2610
  chatContentContainerProps,
1509
2611
  showCloseButton ? (0, import_react.createElement)(
1510
- import_react_native.View,
2612
+ import_react_native3.View,
1511
2613
  { style: styles.header },
1512
2614
  (0, import_react.createElement)(
1513
- import_react_native.Pressable,
2615
+ import_react_native3.Pressable,
1514
2616
  {
1515
2617
  accessibilityRole: "button",
1516
2618
  accessibilityLabel: closeLabel,
1517
2619
  onPress: () => internalSagepilotChat.dismiss(),
1518
2620
  style: styles.closeButton
1519
2621
  },
1520
- (0, import_react.createElement)(import_react_native.Text, { style: styles.closeText }, closeLabel)
2622
+ (0, import_react.createElement)(import_react_native3.Text, { style: styles.closeText }, closeLabel)
1521
2623
  )
1522
2624
  ) : null,
1523
2625
  state.conversationUrl ? (0, import_react.createElement)(import_react_native_webview.WebView, {
1524
2626
  ...hostedChatWebViewProps,
2627
+ key: `sagepilot-hosted-webview-${nativeWebViewKey}`,
1525
2628
  ref: nativeWebViewRef,
1526
2629
  source: { uri: state.conversationUrl },
1527
- style: styles.webview,
2630
+ // The imperceptible opacity toggle forces the Android WebView
2631
+ // surface to re-composite on resume, clearing the blank-but-alive
2632
+ // surface bug without a reload (see runLivenessProbe).
2633
+ style: import_react_native3.Platform.OS === "android" ? [styles.webview, { opacity: 1 - androidRepaintTick % 2 * 1e-3 }] : styles.webview,
1528
2634
  startInLoadingState: true,
1529
2635
  injectedJavaScriptBeforeContentLoaded: getInjectedWebViewScript(),
1530
2636
  onMessage: handleWebViewMessage,
1531
- onLoadEnd: postIdentityToNativeWebView,
2637
+ onLoadEnd: handleNativeWebViewLoadEnd,
2638
+ onShouldStartLoadWithRequest: handleShouldStartLoadWithRequest,
2639
+ // Android: render process killed (commonly while the camera/file
2640
+ // chooser activity is foregrounded). Remount to recover.
2641
+ onRenderProcessGone: recoverNativeWebView,
2642
+ // iOS equivalent: WKWebView content process terminated.
2643
+ onContentProcessDidTerminate: recoverNativeWebView,
1532
2644
  renderLoading: () => (0, import_react.createElement)(
1533
- import_react_native.View,
2645
+ import_react_native3.View,
1534
2646
  { style: styles.loading },
1535
- (0, import_react.createElement)(import_react_native.ActivityIndicator, null),
1536
- (0, import_react.createElement)(import_react_native.Text, { style: styles.loadingText }, loadingLabel)
2647
+ (0, import_react.createElement)(import_react_native3.ActivityIndicator, null),
2648
+ (0, import_react.createElement)(import_react_native3.Text, { style: styles.loadingText }, loadingLabel)
1537
2649
  )
1538
2650
  }) : null
1539
2651
  )
1540
2652
  )
2653
+ ),
2654
+ // Native attachment-source chooser. Replaces the Android Alert (capped at
2655
+ // 3 buttons) so camera/library/documents all show on every platform.
2656
+ (0, import_react.createElement)(
2657
+ import_react_native3.Modal,
2658
+ {
2659
+ visible: sourceChooser !== null,
2660
+ transparent: true,
2661
+ animationType: "fade",
2662
+ statusBarTranslucent: true,
2663
+ onRequestClose: () => setSourceChooser(null)
2664
+ },
2665
+ (0, import_react.createElement)(
2666
+ import_react_native3.Pressable,
2667
+ { style: styles.sheetBackdrop, onPress: () => setSourceChooser(null) },
2668
+ (0, import_react.createElement)(
2669
+ import_react_native3.View,
2670
+ { style: styles.sheetCard },
2671
+ (0, import_react.createElement)(import_react_native3.View, { style: styles.sheetHandle }),
2672
+ (0, import_react.createElement)(import_react_native3.Text, { style: styles.sheetTitle }, "Add attachment"),
2673
+ ...(internalSagepilotChat.getConfig()?.filePicker?.sources ?? []).map(
2674
+ (source) => (0, import_react.createElement)(
2675
+ import_react_native3.Pressable,
2676
+ {
2677
+ key: source,
2678
+ accessibilityRole: "button",
2679
+ android_ripple: { color: "rgba(15, 23, 42, 0.06)" },
2680
+ hitSlop: 4,
2681
+ style: ({ pressed }) => getSheetOptionStyle(pressed),
2682
+ onPress: () => handleSourceChoice(source)
2683
+ },
2684
+ renderFilePickerSourceIcon(source),
2685
+ (0, import_react.createElement)(
2686
+ import_react_native3.View,
2687
+ { style: styles.sheetOptionText },
2688
+ (0, import_react.createElement)(import_react_native3.Text, { style: styles.sheetButtonText }, FILE_PICKER_SOURCE_LABELS[source]),
2689
+ (0, import_react.createElement)(import_react_native3.Text, { style: styles.sheetButtonDescription }, FILE_PICKER_SOURCE_DESCRIPTIONS[source])
2690
+ )
2691
+ )
2692
+ ),
2693
+ (0, import_react.createElement)(
2694
+ import_react_native3.Pressable,
2695
+ {
2696
+ accessibilityRole: "button",
2697
+ android_ripple: { color: "rgba(15, 23, 42, 0.06)" },
2698
+ style: ({ pressed }) => getSheetCancelStyle(pressed),
2699
+ onPress: () => setSourceChooser(null)
2700
+ },
2701
+ (0, import_react.createElement)(import_lucide_react_native.X, { color: "#334155", size: 18, strokeWidth: 2.5, style: styles.sheetCancelIcon }),
2702
+ (0, import_react.createElement)(import_react_native3.Text, { style: styles.sheetCancelText }, "Cancel")
2703
+ )
2704
+ )
2705
+ )
1541
2706
  )
1542
2707
  );
1543
2708
  }
1544
- var styles = import_react_native.StyleSheet.create({
2709
+ var styles = import_react_native3.StyleSheet.create({
1545
2710
  root: {
1546
2711
  flex: 1
1547
2712
  },
@@ -1627,7 +2792,7 @@ var styles = import_react_native.StyleSheet.create({
1627
2792
  borderStyle: "none"
1628
2793
  },
1629
2794
  loading: {
1630
- ...import_react_native.StyleSheet.absoluteFillObject,
2795
+ ...import_react_native3.StyleSheet.absoluteFillObject,
1631
2796
  alignItems: "center",
1632
2797
  justifyContent: "center",
1633
2798
  backgroundColor: "#ffffff"
@@ -1636,6 +2801,112 @@ var styles = import_react_native.StyleSheet.create({
1636
2801
  marginTop: 12,
1637
2802
  color: "#4b5563",
1638
2803
  fontSize: 14
2804
+ },
2805
+ sheetBackdrop: {
2806
+ flex: 1,
2807
+ alignItems: "center",
2808
+ justifyContent: "flex-end",
2809
+ backgroundColor: "rgba(15, 23, 42, 0.48)"
2810
+ },
2811
+ sheetCard: {
2812
+ width: "100%",
2813
+ maxWidth: 540,
2814
+ backgroundColor: "#ffffff",
2815
+ borderTopLeftRadius: 26,
2816
+ borderTopRightRadius: 26,
2817
+ paddingTop: 10,
2818
+ paddingBottom: 18,
2819
+ paddingHorizontal: 14,
2820
+ shadowColor: "#0f172a",
2821
+ shadowOpacity: 0.22,
2822
+ shadowRadius: 28,
2823
+ shadowOffset: { width: 0, height: -10 },
2824
+ elevation: 18
2825
+ },
2826
+ sheetHandle: {
2827
+ alignSelf: "center",
2828
+ width: 42,
2829
+ height: 5,
2830
+ borderRadius: 999,
2831
+ backgroundColor: "#d1d5db",
2832
+ marginBottom: 14
2833
+ },
2834
+ sheetTitle: {
2835
+ color: "#111827",
2836
+ fontSize: 17,
2837
+ fontWeight: "700",
2838
+ paddingBottom: 12,
2839
+ paddingHorizontal: 4
2840
+ },
2841
+ sheetOption: {
2842
+ minHeight: 70,
2843
+ flexDirection: "row",
2844
+ alignItems: "center",
2845
+ borderRadius: 18,
2846
+ paddingHorizontal: 12,
2847
+ marginBottom: 6,
2848
+ backgroundColor: "#ffffff"
2849
+ },
2850
+ sheetOptionPressed: {
2851
+ backgroundColor: "#f8fafc",
2852
+ transform: [{ scale: 0.985 }]
2853
+ },
2854
+ sheetOptionIcon: {
2855
+ width: 46,
2856
+ height: 46,
2857
+ borderRadius: 16,
2858
+ marginRight: 14,
2859
+ alignItems: "center",
2860
+ justifyContent: "center",
2861
+ position: "relative",
2862
+ overflow: "hidden"
2863
+ },
2864
+ sheetOptionText: {
2865
+ flex: 1,
2866
+ justifyContent: "center"
2867
+ },
2868
+ sheetButtonText: {
2869
+ color: "#0f172a",
2870
+ fontSize: 16,
2871
+ fontWeight: "700"
2872
+ },
2873
+ sheetButtonDescription: {
2874
+ color: "#64748b",
2875
+ fontSize: 13,
2876
+ fontWeight: "500",
2877
+ marginTop: 3
2878
+ },
2879
+ cameraOptionIcon: {
2880
+ backgroundColor: "#e0f2fe"
2881
+ },
2882
+ libraryOptionIcon: {
2883
+ backgroundColor: "#ecfdf5"
2884
+ },
2885
+ documentsOptionIcon: {
2886
+ backgroundColor: "#f5f3ff"
2887
+ },
2888
+ sheetCancelButton: {
2889
+ minHeight: 56,
2890
+ flexDirection: "row",
2891
+ alignItems: "center",
2892
+ justifyContent: "center",
2893
+ borderRadius: 18,
2894
+ marginTop: 8,
2895
+ backgroundColor: "#f1f5f9"
2896
+ },
2897
+ sheetCancelButtonPressed: {
2898
+ backgroundColor: "#e2e8f0",
2899
+ transform: [{ scale: 0.985 }]
2900
+ },
2901
+ sheetCancelIcon: {
2902
+ width: 18,
2903
+ height: 18,
2904
+ marginRight: 8
2905
+ },
2906
+ sheetCancelText: {
2907
+ color: "#0f172a",
2908
+ fontSize: 16,
2909
+ fontWeight: "700"
1639
2910
  }
1640
2911
  });
1641
2912
 
@@ -1690,10 +2961,18 @@ function useSagepilotChat() {
1690
2961
  }
1691
2962
  // Annotate the CommonJS export names for ESM import in node:
1692
2963
  0 && (module.exports = {
2964
+ SAGEPILOT_DEFAULT_IMAGE_MAX_DIMENSION,
2965
+ SAGEPILOT_DEFAULT_IMAGE_MAX_FILE_SIZE_BYTES,
2966
+ SAGEPILOT_DEFAULT_IMAGE_QUALITY,
1693
2967
  SagepilotChat,
1694
2968
  SagepilotChatError,
1695
2969
  SagepilotChatProvider,
2970
+ SagepilotFilePickerError,
1696
2971
  createAsyncStorageCacheStorage,
1697
2972
  createKeychainTokenStorage,
2973
+ createSagepilotFilePicker,
2974
+ createSagepilotFileStore,
2975
+ ensureSagepilotAndroidCameraPermission,
2976
+ promptOpenSagepilotCameraSettings,
1698
2977
  useSagepilotChat
1699
2978
  });