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

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
@@ -33,8 +33,11 @@ __export(index_exports, {
33
33
  SagepilotChat: () => SagepilotChat,
34
34
  SagepilotChatError: () => SagepilotChatError,
35
35
  SagepilotChatProvider: () => SagepilotChatProvider,
36
+ SagepilotFilePickerError: () => SagepilotFilePickerError,
36
37
  createAsyncStorageCacheStorage: () => createAsyncStorageCacheStorage,
37
38
  createKeychainTokenStorage: () => createKeychainTokenStorage,
39
+ createSagepilotFilePicker: () => createSagepilotFilePicker,
40
+ createSagepilotFileStore: () => createSagepilotFileStore,
38
41
  useSagepilotChat: () => useSagepilotChat
39
42
  });
40
43
  module.exports = __toCommonJS(index_exports);
@@ -56,7 +59,7 @@ var SagepilotChatError = class extends Error {
56
59
 
57
60
  // src/core/config/constants.ts
58
61
  var SDK_NAME = "@sagepilot-ai/react-native-sdk";
59
- var SDK_VERSION = "0.2.4";
62
+ var SDK_VERSION = "0.2.5";
60
63
  var DEFAULT_HOST = "https://app.sagepilot.ai";
61
64
  var DEFAULT_WIDGET_HOST = "https://app.sagepilot.ai";
62
65
  var CUSTOMER_API_PREFIX = "/customer-api/v1";
@@ -573,6 +576,9 @@ var SagepilotReactNativeChat = class {
573
576
  const { workspaceId, channelId } = parseKey(config.key);
574
577
  this.runtimeOptions = {
575
578
  behavior: config.behavior,
579
+ cacheStorage: config.cacheStorage,
580
+ filePicker: config.filePicker,
581
+ fileStore: config.fileStore,
576
582
  presentation: config.presentation,
577
583
  theme: config.theme,
578
584
  hostedClaimsStorageKeyPrefix: config.hostedClaimsStorageKeyPrefix || DEFAULT_HOSTED_CLAIMS_STORAGE_KEY_PREFIX
@@ -1187,13 +1193,323 @@ function createAsyncStorageCacheStorage(asyncStorage) {
1187
1193
  removeItem: (key) => asyncStorage.removeItem(key)
1188
1194
  };
1189
1195
  }
1196
+ function createJsonCache(storage, namespace) {
1197
+ return {
1198
+ async get(key) {
1199
+ const value = await storage.getItem(`${namespace}:${key}`);
1200
+ if (!value) return null;
1201
+ try {
1202
+ return JSON.parse(value);
1203
+ } catch {
1204
+ return null;
1205
+ }
1206
+ },
1207
+ async set(key, value) {
1208
+ await storage.setItem(`${namespace}:${key}`, JSON.stringify(value));
1209
+ },
1210
+ async remove(key) {
1211
+ await storage.removeItem(`${namespace}:${key}`);
1212
+ }
1213
+ };
1214
+ }
1215
+
1216
+ // src/core/storage/fileStore.ts
1217
+ var DEFAULT_DIRECTORY_NAME = "sagepilot-attachments";
1218
+ function sanitizeKey(key) {
1219
+ return key.replace(/[^a-zA-Z0-9._-]/g, "_").replace(/\.{2,}/g, "_");
1220
+ }
1221
+ function createSagepilotFileStore(blobUtil, options = {}) {
1222
+ const directory = `${blobUtil.fs.dirs.DocumentDir}/${options.directoryName ?? DEFAULT_DIRECTORY_NAME}`;
1223
+ const pathFor = (key) => `${directory}/${sanitizeKey(key)}`;
1224
+ let dirReady = null;
1225
+ function ensureDirectory() {
1226
+ if (!dirReady) {
1227
+ dirReady = (async () => {
1228
+ if (!await blobUtil.fs.exists(directory)) {
1229
+ await blobUtil.fs.mkdir(directory);
1230
+ }
1231
+ })().catch((error) => {
1232
+ dirReady = null;
1233
+ throw error;
1234
+ });
1235
+ }
1236
+ return dirReady;
1237
+ }
1238
+ return {
1239
+ async write(key, base64) {
1240
+ await ensureDirectory();
1241
+ await blobUtil.fs.writeFile(pathFor(key), base64, "base64");
1242
+ },
1243
+ async read(key) {
1244
+ const content = await blobUtil.fs.readFile(pathFor(key), "base64");
1245
+ if (typeof content !== "string" || content.length === 0) {
1246
+ throw new Error("Persisted attachment is empty or unreadable.");
1247
+ }
1248
+ return content;
1249
+ },
1250
+ async remove(key) {
1251
+ try {
1252
+ if (await blobUtil.fs.exists(pathFor(key))) {
1253
+ await blobUtil.fs.unlink(pathFor(key));
1254
+ }
1255
+ } catch {
1256
+ }
1257
+ },
1258
+ async prune(keepKeys) {
1259
+ try {
1260
+ if (!await blobUtil.fs.exists(directory)) return;
1261
+ const keep = new Set(keepKeys.map(sanitizeKey));
1262
+ const entries = await blobUtil.fs.ls(directory);
1263
+ await Promise.all(
1264
+ entries.filter((entry) => !keep.has(entry)).map((entry) => blobUtil.fs.unlink(`${directory}/${entry}`).catch(() => void 0))
1265
+ );
1266
+ } catch {
1267
+ }
1268
+ }
1269
+ };
1270
+ }
1271
+
1272
+ // src/core/native/filePicker.ts
1273
+ var import_react_native = require("react-native");
1274
+ var DEFAULT_IMAGE_MAX_DIMENSION = 1920;
1275
+ var DEFAULT_IMAGE_QUALITY = 0.8;
1276
+ var DEFAULT_IMAGE_MIME_TYPE = "image/jpeg";
1277
+ var DEFAULT_DOCUMENT_MIME_TYPE = "application/octet-stream";
1278
+ var DEFAULT_IMAGE_SELECTION_LIMIT = 5;
1279
+ var DEFAULT_DOCUMENT_MAX_FILE_SIZE_BYTES = 20 * 1024 * 1024;
1280
+ var DEFAULT_IMAGE_MAX_FILE_SIZE_BYTES = 15 * 1024 * 1024;
1281
+ var SagepilotFilePickerError = class extends Error {
1282
+ constructor(code, message) {
1283
+ super(message);
1284
+ this.name = "SagepilotFilePickerError";
1285
+ this.code = code;
1286
+ }
1287
+ };
1288
+ function bytesToMb(bytes) {
1289
+ return Math.round(bytes / (1024 * 1024) * 10) / 10;
1290
+ }
1291
+ function estimateBase64ByteSize(dataBase64) {
1292
+ return Math.floor(dataBase64.length * 3 / 4);
1293
+ }
1294
+ function stripFileScheme(uri) {
1295
+ return uri.startsWith("file://") ? uri.replace("file://", "") : uri;
1296
+ }
1297
+ async function ensureAndroidCameraPermission() {
1298
+ if (import_react_native.Platform.OS !== "android") return;
1299
+ let result;
1300
+ try {
1301
+ result = await import_react_native.PermissionsAndroid.request(import_react_native.PermissionsAndroid.PERMISSIONS.CAMERA);
1302
+ } catch {
1303
+ return;
1304
+ }
1305
+ if (result === import_react_native.PermissionsAndroid.RESULTS.NEVER_ASK_AGAIN) {
1306
+ throw new SagepilotFilePickerError(
1307
+ "permission_denied",
1308
+ "Camera access is turned off. Enable it for this app in Settings to take a photo."
1309
+ );
1310
+ }
1311
+ if (result === import_react_native.PermissionsAndroid.RESULTS.DENIED) {
1312
+ throw new SagepilotFilePickerError(
1313
+ "permission_denied",
1314
+ "Camera permission is required to take a photo."
1315
+ );
1316
+ }
1317
+ }
1318
+ function mapImagePickerErrorCode(errorCode) {
1319
+ switch (errorCode) {
1320
+ case "permission":
1321
+ return "permission_denied";
1322
+ case "camera_unavailable":
1323
+ return "camera_unavailable";
1324
+ default:
1325
+ return "unknown";
1326
+ }
1327
+ }
1328
+ function imageAssetsToPickedFiles(result, maxFileSizeBytes) {
1329
+ if (result.didCancel) return [];
1330
+ if (result.errorCode) {
1331
+ throw new SagepilotFilePickerError(
1332
+ mapImagePickerErrorCode(result.errorCode),
1333
+ result.errorMessage || `Could not capture the photo (${result.errorCode}).`
1334
+ );
1335
+ }
1336
+ const assets = result.assets ?? [];
1337
+ const usable = assets.filter((asset) => typeof asset.base64 === "string" && asset.base64.length > 0);
1338
+ if (assets.length > usable.length) {
1339
+ throw new SagepilotFilePickerError(
1340
+ "encode_failed",
1341
+ assets.length === 1 ? "The photo could not be processed. Please try again." : "Some photos could not be processed. Please try selecting them again."
1342
+ );
1343
+ }
1344
+ return usable.map((asset, index) => {
1345
+ const dataBase64 = asset.base64;
1346
+ const size = asset.fileSize ?? estimateBase64ByteSize(dataBase64);
1347
+ if (maxFileSizeBytes > 0 && size > maxFileSizeBytes) {
1348
+ throw new SagepilotFilePickerError(
1349
+ "file_too_large",
1350
+ `Image is too large (max ${bytesToMb(maxFileSizeBytes)}MB).`
1351
+ );
1352
+ }
1353
+ return {
1354
+ file_name: asset.fileName || `photo-${Date.now()}-${index + 1}.jpg`,
1355
+ mime_type: asset.type || DEFAULT_IMAGE_MIME_TYPE,
1356
+ size,
1357
+ data_base64: dataBase64
1358
+ };
1359
+ });
1360
+ }
1361
+ async function readUriAsBase64(uri, fileReader) {
1362
+ if (fileReader) {
1363
+ const content = await fileReader.fs.readFile(stripFileScheme(uri), "base64");
1364
+ if (typeof content !== "string") {
1365
+ throw new Error("File reader returned unexpected content.");
1366
+ }
1367
+ return content;
1368
+ }
1369
+ const response = await fetch(uri);
1370
+ const blob = await response.blob();
1371
+ return new Promise((resolve, reject) => {
1372
+ const reader = new FileReader();
1373
+ reader.onload = () => {
1374
+ const dataUrl = typeof reader.result === "string" ? reader.result : "";
1375
+ const [, payload = ""] = dataUrl.split(",");
1376
+ if (!payload) {
1377
+ reject(new Error("Could not read the selected file."));
1378
+ return;
1379
+ }
1380
+ resolve(payload);
1381
+ };
1382
+ reader.onerror = () => reject(new Error("Could not read the selected file."));
1383
+ reader.readAsDataURL(blob);
1384
+ });
1385
+ }
1386
+ function createSagepilotFilePicker(options) {
1387
+ const { imagePicker, documentsPicker, fileReader } = options;
1388
+ const imageMaxDimension = options.imageMaxDimension ?? DEFAULT_IMAGE_MAX_DIMENSION;
1389
+ const imageQuality = options.imageQuality ?? DEFAULT_IMAGE_QUALITY;
1390
+ const imageSelectionLimit = options.imageSelectionLimit ?? DEFAULT_IMAGE_SELECTION_LIMIT;
1391
+ const documentMaxFileSizeBytes = options.documentMaxFileSizeBytes ?? DEFAULT_DOCUMENT_MAX_FILE_SIZE_BYTES;
1392
+ const imageMaxFileSizeBytes = options.imageMaxFileSizeBytes ?? DEFAULT_IMAGE_MAX_FILE_SIZE_BYTES;
1393
+ const sources = [];
1394
+ if (imagePicker) sources.push("camera", "library");
1395
+ if (documentsPicker) sources.push("documents");
1396
+ if (sources.length === 0) return void 0;
1397
+ const imageOptions = {
1398
+ mediaType: "photo",
1399
+ includeBase64: true,
1400
+ maxWidth: imageMaxDimension,
1401
+ maxHeight: imageMaxDimension,
1402
+ quality: imageQuality,
1403
+ saveToPhotos: false
1404
+ };
1405
+ async function pickFromCamera() {
1406
+ if (!imagePicker) return [];
1407
+ await ensureAndroidCameraPermission();
1408
+ return imageAssetsToPickedFiles(await imagePicker.launchCamera(imageOptions), imageMaxFileSizeBytes);
1409
+ }
1410
+ async function pickFromLibrary(multiple) {
1411
+ if (!imagePicker) return [];
1412
+ return imageAssetsToPickedFiles(await imagePicker.launchImageLibrary({
1413
+ ...imageOptions,
1414
+ // Bounded multi-select: 0 (unlimited) lets a huge batch OOM the bridge.
1415
+ selectionLimit: multiple ? Math.max(0, imageSelectionLimit) : 1
1416
+ }), imageMaxFileSizeBytes);
1417
+ }
1418
+ async function pickDocuments(multiple) {
1419
+ if (!documentsPicker) return [];
1420
+ let picked;
1421
+ try {
1422
+ picked = await documentsPicker.pick({ allowMultiSelection: multiple });
1423
+ } catch (error) {
1424
+ if (error && typeof error === "object" && error.code === "OPERATION_CANCELED") {
1425
+ return [];
1426
+ }
1427
+ throw error;
1428
+ }
1429
+ const files = [];
1430
+ for (const file of picked) {
1431
+ if (documentMaxFileSizeBytes > 0 && typeof file.size === "number" && file.size > documentMaxFileSizeBytes) {
1432
+ throw new SagepilotFilePickerError(
1433
+ "file_too_large",
1434
+ `"${file.name || "File"}" is too large (max ${bytesToMb(documentMaxFileSizeBytes)}MB).`
1435
+ );
1436
+ }
1437
+ let readableUri = file.uri;
1438
+ if (documentsPicker.keepLocalCopy) {
1439
+ const fileName = file.name || `file-${Date.now()}`;
1440
+ const [copy] = await documentsPicker.keepLocalCopy({
1441
+ files: [{ uri: file.uri, fileName }],
1442
+ destination: "cachesDirectory"
1443
+ });
1444
+ if (copy?.status === "success" && copy.localUri) {
1445
+ readableUri = copy.localUri;
1446
+ }
1447
+ }
1448
+ let dataBase64;
1449
+ try {
1450
+ dataBase64 = await readUriAsBase64(readableUri, fileReader);
1451
+ } catch (error) {
1452
+ throw new SagepilotFilePickerError(
1453
+ "read_failed",
1454
+ error instanceof Error && error.message ? error.message : "Could not read the selected file."
1455
+ );
1456
+ }
1457
+ files.push({
1458
+ file_name: file.name || `file-${Date.now()}`,
1459
+ mime_type: file.type || DEFAULT_DOCUMENT_MIME_TYPE,
1460
+ size: file.size ?? estimateBase64ByteSize(dataBase64),
1461
+ data_base64: dataBase64
1462
+ });
1463
+ }
1464
+ return files;
1465
+ }
1466
+ return {
1467
+ sources,
1468
+ async pickFiles(request) {
1469
+ if (request.source === "camera") return pickFromCamera();
1470
+ if (request.source === "library") return pickFromLibrary(request.multiple);
1471
+ return pickDocuments(request.multiple);
1472
+ }
1473
+ };
1474
+ }
1190
1475
 
1191
1476
  // src/ui/SagepilotChatProvider.ts
1192
1477
  var import_react = require("react");
1193
- var import_react_native = require("react-native");
1478
+ var import_react_native2 = require("react-native");
1194
1479
  var import_react_native_webview = require("react-native-webview");
1195
1480
 
1196
1481
  // src/core/webview/mobileBridge.ts
1482
+ var FILE_PICKER_PROTOCOL_VERSION = 2;
1483
+ function buildBridgeCapabilitiesScript(capabilities) {
1484
+ const payload = { ...capabilities, filePickerProtocol: FILE_PICKER_PROTOCOL_VERSION };
1485
+ return [
1486
+ "(function(){",
1487
+ "try {",
1488
+ "if (window.SagepilotMobileBridge) {",
1489
+ `window.SagepilotMobileBridge.capabilities = Object.assign({}, window.SagepilotMobileBridge.capabilities, ${JSON.stringify(payload)});`,
1490
+ "}",
1491
+ "} catch (_) {}",
1492
+ "true;",
1493
+ "})();"
1494
+ ].join("\n");
1495
+ }
1496
+ function buildLivenessPingScript(nonce, staleMs) {
1497
+ return [
1498
+ "(function(){",
1499
+ "try {",
1500
+ "var hb = window.__sagepilotWidgetHeartbeat;",
1501
+ "var supports = typeof hb === 'number';",
1502
+ `var alive = !supports ? true : (Date.now() - hb < ${Math.max(0, Math.floor(staleMs))});`,
1503
+ "var send = function(){",
1504
+ " var msg = JSON.stringify({ type: 'sagepilot:pong', nonce: " + JSON.stringify(nonce) + ", alive: alive, supportsHeartbeat: supports });",
1505
+ " if (window.ReactNativeWebView && window.ReactNativeWebView.postMessage) { window.ReactNativeWebView.postMessage(msg); }",
1506
+ "};",
1507
+ "send();",
1508
+ "} catch (_) {}",
1509
+ "true;",
1510
+ "})();"
1511
+ ].join("\n");
1512
+ }
1197
1513
  function isHostedBridgeMessage(value) {
1198
1514
  if (!value || typeof value !== "object") return false;
1199
1515
  const message = value;
@@ -1319,9 +1635,70 @@ function readPresentationState() {
1319
1635
  function getInjectedWebViewScript() {
1320
1636
  return [
1321
1637
  internalSagepilotChat.getHostedAuthScript(),
1322
- mobileWebViewBridgeScript
1638
+ mobileWebViewBridgeScript,
1639
+ buildBridgeCapabilitiesScript({
1640
+ nativeFilePicker: Boolean(internalSagepilotChat.getConfig()?.filePicker)
1641
+ })
1323
1642
  ].filter(Boolean).join("\n");
1324
1643
  }
1644
+ var FILE_PICKER_SOURCE_LABELS = {
1645
+ camera: "Take photo",
1646
+ library: "Choose from gallery",
1647
+ documents: "Browse files"
1648
+ };
1649
+ var DELIVERY_RETRY_MS = 1500;
1650
+ var DELIVERY_LEGACY_FALLBACK_ATTEMPTS = 2;
1651
+ var DELIVERY_MAX_ATTEMPTS = 8;
1652
+ var PERSIST_MAX_TOTAL_BYTES = 2 * 1024 * 1024;
1653
+ var PENDING_FILES_CACHE_NAMESPACE = "sagepilot_rn_picker";
1654
+ var PENDING_FILES_CACHE_KEY = "pending_batch";
1655
+ var LIVENESS_HEARTBEAT_STALE_MS = 4e3;
1656
+ var LIVENESS_PONG_TIMEOUT_MS = 1500;
1657
+ var LIVENESS_MAX_PING_ATTEMPTS = 2;
1658
+ var LIVENESS_MAX_REMOUNTS = 2;
1659
+ var batchSequence = 0;
1660
+ function nextBatchId() {
1661
+ batchSequence += 1;
1662
+ const entropy = Math.random().toString(36).slice(2, 8);
1663
+ return `b_${Date.now().toString(36)}_${batchSequence}_${entropy}`;
1664
+ }
1665
+ function totalBase64Bytes(files) {
1666
+ return files.reduce((sum, file) => sum + (file.data_base64?.length ?? 0), 0);
1667
+ }
1668
+ function storageKeyFor(batchId, index) {
1669
+ return `${batchId}_${index}.bin`;
1670
+ }
1671
+ function buildDispatchScript(messageLiteral) {
1672
+ return [
1673
+ "(function(){",
1674
+ "try {",
1675
+ `var message = ${messageLiteral};`,
1676
+ "window.dispatchEvent(new MessageEvent('message', { data: message, origin: window.location.origin, source: window.parent || window }));",
1677
+ "} catch (_) {}",
1678
+ "true;",
1679
+ "})();"
1680
+ ].join("\n");
1681
+ }
1682
+ function buildFilesPickedScript(files, batchId) {
1683
+ return buildDispatchScript(`{ type: "sagepilot:files_picked", batch_id: ${JSON.stringify(batchId)}, files: ${JSON.stringify(files)} }`);
1684
+ }
1685
+ function buildFilePickerErrorScript(message, code) {
1686
+ return buildDispatchScript(
1687
+ `{ type: "sagepilot:file_picker_error", message: ${JSON.stringify(message)}${code ? `, code: ${JSON.stringify(code)}` : ""} }`
1688
+ );
1689
+ }
1690
+ function buildFilePickerCancelledScript() {
1691
+ return buildDispatchScript(`{ type: "sagepilot:file_picker_cancelled" }`);
1692
+ }
1693
+ function readFilePickerError(error) {
1694
+ if (error && typeof error === "object") {
1695
+ const candidate = error;
1696
+ const message = typeof candidate.message === "string" && candidate.message ? candidate.message : "Could not attach the selected file.";
1697
+ const code = typeof candidate.code === "string" ? candidate.code : void 0;
1698
+ return { message, code };
1699
+ }
1700
+ return { message: "Could not attach the selected file." };
1701
+ }
1325
1702
  function getHostedIdentityDispatchScript() {
1326
1703
  const message = internalSagepilotChat.getHostedIdentityMessage();
1327
1704
  if (!message) return "";
@@ -1344,10 +1721,13 @@ function readUrlOrigin(url) {
1344
1721
  return null;
1345
1722
  }
1346
1723
  }
1724
+ function isInternalWebViewScheme(url) {
1725
+ return url.startsWith("about:") || url.startsWith("data:") || url.startsWith("blob:") || url.startsWith("javascript:") || url.startsWith("file:");
1726
+ }
1347
1727
  var hostedChatWebViewProps = {
1348
1728
  allowFileAccess: true,
1349
1729
  allowFileAccessFromFileURLs: true,
1350
- ...import_react_native.Platform.OS === "ios" ? {
1730
+ ...import_react_native2.Platform.OS === "ios" ? {
1351
1731
  automaticallyAdjustContentInsets: false,
1352
1732
  contentInsetAdjustmentBehavior: "never",
1353
1733
  hideKeyboardAccessoryView: true
@@ -1362,7 +1742,7 @@ var hostedChatWebViewProps = {
1362
1742
  sharedCookiesEnabled: true,
1363
1743
  thirdPartyCookiesEnabled: true
1364
1744
  };
1365
- var AndroidInsetsView = import_react_native.Platform.OS === "android" ? SagepilotInsetsViewNativeComponent_default : import_react_native.View;
1745
+ var AndroidInsetsView = import_react_native2.Platform.OS === "android" ? SagepilotInsetsViewNativeComponent_default : import_react_native2.View;
1366
1746
  var emptyAndroidInsets = { top: 0, bottom: 0 };
1367
1747
  function SagepilotChatProvider({
1368
1748
  children,
@@ -1371,13 +1751,292 @@ function SagepilotChatProvider({
1371
1751
  }) {
1372
1752
  const [state, setState] = (0, import_react.useState)(readPresentationState);
1373
1753
  const [androidModalInsets, setAndroidModalInsets] = (0, import_react.useState)(emptyAndroidInsets);
1754
+ const [nativeWebViewKey, setNativeWebViewKey] = (0, import_react.useState)(0);
1755
+ const [preloadWebViewKey, setPreloadWebViewKey] = (0, import_react.useState)(0);
1374
1756
  const webFrameRef = (0, import_react.useRef)(null);
1375
1757
  const nativeWebViewRef = (0, import_react.useRef)(null);
1758
+ const pendingBatchesRef = (0, import_react.useRef)([]);
1759
+ const widgetReadyRef = (0, import_react.useRef)(false);
1760
+ const deliveryTimerRef = (0, import_react.useRef)(null);
1761
+ const deliveryAttemptsRef = (0, import_react.useRef)(0);
1762
+ const pendingPingRef = (0, import_react.useRef)(null);
1763
+ const pingTimeoutRef = (0, import_react.useRef)(null);
1764
+ const livenessRemountCountRef = (0, import_react.useRef)(0);
1765
+ const appStateRef = (0, import_react.useRef)(import_react_native2.AppState.currentState);
1766
+ const didReconcileRef = (0, import_react.useRef)(false);
1767
+ const [androidRepaintTick, setAndroidRepaintTick] = (0, import_react.useState)(0);
1768
+ const [sourceChooser, setSourceChooser] = (0, import_react.useState)(null);
1769
+ const getPendingCache = (0, import_react.useCallback)(() => {
1770
+ const cacheStorage = internalSagepilotChat.getConfig()?.cacheStorage;
1771
+ if (!cacheStorage) return null;
1772
+ return createJsonCache(cacheStorage, PENDING_FILES_CACHE_NAMESPACE);
1773
+ }, []);
1774
+ const writeManifest = (0, import_react.useCallback)(() => {
1775
+ const cache = getPendingCache();
1776
+ if (!cache) return;
1777
+ const queue = pendingBatchesRef.current;
1778
+ if (queue.length === 0) {
1779
+ void cache.remove(PENDING_FILES_CACHE_KEY).catch(() => void 0);
1780
+ return;
1781
+ }
1782
+ const hasFileStore = Boolean(internalSagepilotChat.getConfig()?.fileStore);
1783
+ if (hasFileStore) {
1784
+ const manifest = queue.filter((batch) => batch.storageKeys && batch.storageKeys.length === batch.files.length).map((batch) => ({
1785
+ batchId: batch.batchId,
1786
+ files: batch.files.map((file, index) => ({
1787
+ file_name: file.file_name,
1788
+ mime_type: file.mime_type,
1789
+ size: file.size,
1790
+ storageKey: batch.storageKeys[index]
1791
+ }))
1792
+ }));
1793
+ if (manifest.length === 0) {
1794
+ void cache.remove(PENDING_FILES_CACHE_KEY).catch(() => void 0);
1795
+ } else {
1796
+ void cache.set(PENDING_FILES_CACHE_KEY, manifest).catch(() => void 0);
1797
+ }
1798
+ return;
1799
+ }
1800
+ const totalBytes = queue.reduce((sum, batch) => sum + totalBase64Bytes(batch.files), 0);
1801
+ if (totalBytes <= PERSIST_MAX_TOTAL_BYTES) {
1802
+ const manifest = queue.map((batch) => ({
1803
+ batchId: batch.batchId,
1804
+ files: batch.files.map((file) => ({
1805
+ file_name: file.file_name,
1806
+ mime_type: file.mime_type,
1807
+ size: file.size,
1808
+ data_base64: file.data_base64
1809
+ }))
1810
+ }));
1811
+ void cache.set(PENDING_FILES_CACHE_KEY, manifest).catch(() => void 0);
1812
+ } else {
1813
+ void cache.remove(PENDING_FILES_CACHE_KEY).catch(() => void 0);
1814
+ }
1815
+ }, [getPendingCache]);
1816
+ const writeBatchBytes = (0, import_react.useCallback)(async (batch) => {
1817
+ const fileStore = internalSagepilotChat.getConfig()?.fileStore;
1818
+ if (!fileStore) return;
1819
+ const entries = batch.files.map((file, index) => ({
1820
+ key: storageKeyFor(batch.batchId, index),
1821
+ base64: file.data_base64
1822
+ }));
1823
+ try {
1824
+ await Promise.all(entries.map((entry) => fileStore.write(entry.key, entry.base64)));
1825
+ } catch {
1826
+ return;
1827
+ }
1828
+ if (!pendingBatchesRef.current.some((queued) => queued.batchId === batch.batchId)) {
1829
+ void Promise.all(entries.map((entry) => fileStore.remove(entry.key).catch(() => void 0)));
1830
+ }
1831
+ }, []);
1832
+ const discardBatchFiles = (0, import_react.useCallback)((batch) => {
1833
+ if (!batch?.storageKeys) return;
1834
+ const fileStore = internalSagepilotChat.getConfig()?.fileStore;
1835
+ if (!fileStore) return;
1836
+ void Promise.all(batch.storageKeys.map((key) => fileStore.remove(key).catch(() => void 0)));
1837
+ }, []);
1838
+ const pumpDelivery = (0, import_react.useCallback)(() => {
1839
+ if (deliveryTimerRef.current) {
1840
+ clearTimeout(deliveryTimerRef.current);
1841
+ deliveryTimerRef.current = null;
1842
+ }
1843
+ const batch = pendingBatchesRef.current[0];
1844
+ if (!batch) return;
1845
+ deliveryAttemptsRef.current += 1;
1846
+ const attempts = deliveryAttemptsRef.current;
1847
+ const ref = nativeWebViewRef.current;
1848
+ const shouldDeliver = ref && (widgetReadyRef.current || attempts >= DELIVERY_LEGACY_FALLBACK_ATTEMPTS);
1849
+ if (shouldDeliver && ref) {
1850
+ ref.injectJavaScript(buildFilesPickedScript(batch.files, batch.batchId));
1851
+ }
1852
+ if (attempts < DELIVERY_MAX_ATTEMPTS) {
1853
+ deliveryTimerRef.current = setTimeout(() => pumpDelivery(), DELIVERY_RETRY_MS);
1854
+ }
1855
+ }, []);
1856
+ const startDelivery = (0, import_react.useCallback)(() => {
1857
+ deliveryAttemptsRef.current = 0;
1858
+ pumpDelivery();
1859
+ }, [pumpDelivery]);
1860
+ const ensureDelivery = (0, import_react.useCallback)(() => {
1861
+ if (pendingBatchesRef.current.length === 0) return;
1862
+ if (deliveryTimerRef.current) return;
1863
+ startDelivery();
1864
+ }, [startDelivery]);
1865
+ const acknowledgeHeadBatch = (0, import_react.useCallback)(() => {
1866
+ const head = pendingBatchesRef.current[0];
1867
+ pendingBatchesRef.current = pendingBatchesRef.current.slice(1);
1868
+ deliveryAttemptsRef.current = 0;
1869
+ if (deliveryTimerRef.current) {
1870
+ clearTimeout(deliveryTimerRef.current);
1871
+ deliveryTimerRef.current = null;
1872
+ }
1873
+ discardBatchFiles(head);
1874
+ writeManifest();
1875
+ if (pendingBatchesRef.current.length > 0) startDelivery();
1876
+ }, [discardBatchFiles, writeManifest, startDelivery]);
1877
+ const queuePickedFiles = (0, import_react.useCallback)((files) => {
1878
+ const batchId = nextBatchId();
1879
+ const hasFileStore = Boolean(internalSagepilotChat.getConfig()?.fileStore);
1880
+ const storageKeys = hasFileStore ? files.map((_, index) => storageKeyFor(batchId, index)) : void 0;
1881
+ const batch = { batchId, files, storageKeys };
1882
+ pendingBatchesRef.current = [...pendingBatchesRef.current, batch];
1883
+ ensureDelivery();
1884
+ writeManifest();
1885
+ void writeBatchBytes(batch);
1886
+ }, [ensureDelivery, writeManifest, writeBatchBytes]);
1887
+ const recoverNativeWebView = (0, import_react.useCallback)(() => {
1888
+ widgetReadyRef.current = false;
1889
+ setNativeWebViewKey((key) => key + 1);
1890
+ }, []);
1891
+ const recoverPreloadWebView = (0, import_react.useCallback)(() => {
1892
+ setPreloadWebViewKey((key) => key + 1);
1893
+ }, []);
1894
+ const runNativeFilePicker = (0, import_react.useCallback)((source, multiple) => {
1895
+ const filePicker = internalSagepilotChat.getConfig()?.filePicker;
1896
+ if (!filePicker) return;
1897
+ filePicker.pickFiles({ source, multiple }).then((files) => {
1898
+ if (files.length === 0) {
1899
+ nativeWebViewRef.current?.injectJavaScript(buildFilePickerCancelledScript());
1900
+ return;
1901
+ }
1902
+ queuePickedFiles(files);
1903
+ }).catch((error) => {
1904
+ const { message, code } = readFilePickerError(error);
1905
+ nativeWebViewRef.current?.injectJavaScript(buildFilePickerErrorScript(message, code));
1906
+ });
1907
+ }, [queuePickedFiles]);
1908
+ const openNativeFilePicker = (0, import_react.useCallback)((multiple) => {
1909
+ const filePicker = internalSagepilotChat.getConfig()?.filePicker;
1910
+ if (!filePicker || filePicker.sources.length === 0) return;
1911
+ const [onlySource] = filePicker.sources;
1912
+ if (filePicker.sources.length === 1 && onlySource) {
1913
+ runNativeFilePicker(onlySource, multiple);
1914
+ return;
1915
+ }
1916
+ setSourceChooser({ multiple });
1917
+ }, [runNativeFilePicker]);
1918
+ const handleSourceChoice = (0, import_react.useCallback)((source) => {
1919
+ const chooser = sourceChooser;
1920
+ setSourceChooser(null);
1921
+ if (chooser) runNativeFilePicker(source, chooser.multiple);
1922
+ }, [sourceChooser, runNativeFilePicker]);
1376
1923
  (0, import_react.useEffect)(() => {
1377
1924
  return internalSagepilotChat.onStateChange(() => {
1378
1925
  setState(readPresentationState());
1379
1926
  });
1380
1927
  }, []);
1928
+ (0, import_react.useEffect)(() => {
1929
+ let cancelled = false;
1930
+ const cache = getPendingCache();
1931
+ if (!cache) return;
1932
+ const reconcile = async () => {
1933
+ if (didReconcileRef.current) return;
1934
+ const manifest = await cache.get(PENDING_FILES_CACHE_KEY).catch(() => null);
1935
+ if (cancelled || didReconcileRef.current || !Array.isArray(manifest) || manifest.length === 0) return;
1936
+ const fileStore = internalSagepilotChat.getConfig()?.fileStore;
1937
+ const restored = [];
1938
+ for (const batch of manifest) {
1939
+ if (!batch || typeof batch.batchId !== "string" || !Array.isArray(batch.files) || batch.files.length === 0) continue;
1940
+ const files = [];
1941
+ const storageKeys = [];
1942
+ let intact = true;
1943
+ for (const file of batch.files) {
1944
+ let dataBase64 = null;
1945
+ if (typeof file.storageKey === "string" && fileStore) {
1946
+ try {
1947
+ dataBase64 = await fileStore.read(file.storageKey);
1948
+ storageKeys.push(file.storageKey);
1949
+ } catch {
1950
+ intact = false;
1951
+ }
1952
+ } else if (typeof file.data_base64 === "string" && file.data_base64) {
1953
+ dataBase64 = file.data_base64;
1954
+ }
1955
+ if (!dataBase64) {
1956
+ intact = false;
1957
+ break;
1958
+ }
1959
+ files.push({ file_name: file.file_name, mime_type: file.mime_type, size: file.size, data_base64: dataBase64 });
1960
+ }
1961
+ if (intact && files.length === batch.files.length) {
1962
+ restored.push({
1963
+ batchId: batch.batchId,
1964
+ files,
1965
+ storageKeys: storageKeys.length === files.length ? storageKeys : void 0
1966
+ });
1967
+ }
1968
+ }
1969
+ if (cancelled || didReconcileRef.current) return;
1970
+ didReconcileRef.current = true;
1971
+ pendingBatchesRef.current = [...restored, ...pendingBatchesRef.current];
1972
+ writeManifest();
1973
+ if (fileStore) {
1974
+ const keep = pendingBatchesRef.current.flatMap(
1975
+ (batch) => batch.files.map((_, index) => storageKeyFor(batch.batchId, index))
1976
+ );
1977
+ void fileStore.prune(keep).catch(() => void 0);
1978
+ }
1979
+ startDelivery();
1980
+ };
1981
+ void reconcile();
1982
+ return () => {
1983
+ cancelled = true;
1984
+ };
1985
+ }, [getPendingCache, startDelivery, writeManifest]);
1986
+ (0, import_react.useEffect)(() => {
1987
+ return () => {
1988
+ if (deliveryTimerRef.current) {
1989
+ clearTimeout(deliveryTimerRef.current);
1990
+ deliveryTimerRef.current = null;
1991
+ }
1992
+ if (pingTimeoutRef.current) {
1993
+ clearTimeout(pingTimeoutRef.current);
1994
+ pingTimeoutRef.current = null;
1995
+ }
1996
+ };
1997
+ }, []);
1998
+ const clearPing = (0, import_react.useCallback)(() => {
1999
+ pendingPingRef.current = null;
2000
+ if (pingTimeoutRef.current) {
2001
+ clearTimeout(pingTimeoutRef.current);
2002
+ pingTimeoutRef.current = null;
2003
+ }
2004
+ }, []);
2005
+ const runLivenessProbe = (0, import_react.useCallback)(() => {
2006
+ if (import_react_native2.Platform.OS !== "android") return;
2007
+ const ref = nativeWebViewRef.current;
2008
+ if (!ref || !internalSagepilotChat.isPresented()) return;
2009
+ setAndroidRepaintTick((tick) => tick + 1);
2010
+ const attempts = (pendingPingRef.current?.attempts ?? 0) + 1;
2011
+ const nonce = `${Date.now().toString(36)}_${attempts}`;
2012
+ pendingPingRef.current = { nonce, attempts };
2013
+ ref.injectJavaScript(buildLivenessPingScript(nonce, LIVENESS_HEARTBEAT_STALE_MS));
2014
+ if (pingTimeoutRef.current) clearTimeout(pingTimeoutRef.current);
2015
+ pingTimeoutRef.current = setTimeout(() => {
2016
+ if (attempts >= LIVENESS_MAX_PING_ATTEMPTS) {
2017
+ clearPing();
2018
+ recoverNativeWebView();
2019
+ return;
2020
+ }
2021
+ runLivenessProbe();
2022
+ }, LIVENESS_PONG_TIMEOUT_MS);
2023
+ }, [clearPing, recoverNativeWebView]);
2024
+ (0, import_react.useEffect)(() => {
2025
+ const subscription = import_react_native2.AppState.addEventListener("change", (nextState) => {
2026
+ const prev = appStateRef.current;
2027
+ appStateRef.current = nextState;
2028
+ if (nextState === "active" && (prev === "background" || prev === "inactive")) {
2029
+ clearPing();
2030
+ livenessRemountCountRef.current = 0;
2031
+ runLivenessProbe();
2032
+ ensureDelivery();
2033
+ }
2034
+ });
2035
+ return () => {
2036
+ subscription.remove();
2037
+ clearPing();
2038
+ };
2039
+ }, [clearPing, runLivenessProbe, ensureDelivery]);
1381
2040
  const handleAndroidInsetsChange = (0, import_react.useCallback)((event) => {
1382
2041
  const nextBottomInset = event.nativeEvent?.bottom;
1383
2042
  const nextTopInset = event.nativeEvent?.top;
@@ -1391,11 +2050,11 @@ function SagepilotChatProvider({
1391
2050
  const presentationStyle = state.presentation?.style ?? "sheet";
1392
2051
  const isFullScreenModal = presentationStyle === "fullScreen";
1393
2052
  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" ? {
2053
+ const ModalContainer = import_react_native2.Platform.OS === "ios" && isFullScreenModal ? import_react_native2.SafeAreaView : import_react_native2.View;
2054
+ const NativeModalContainer = import_react_native2.Platform.OS === "android" ? AndroidInsetsView : ModalContainer;
2055
+ const ChatContentContainer = import_react_native2.Platform.OS === "ios" ? import_react_native2.KeyboardAvoidingView : import_react_native2.View;
2056
+ const nativeModalContainerProps = import_react_native2.Platform.OS === "android" ? { style: styles.container, onInsetsChange: handleAndroidInsetsChange } : { style: styles.container };
2057
+ const chatContentContainerProps = import_react_native2.Platform.OS === "ios" ? {
1399
2058
  behavior: "padding",
1400
2059
  enabled: true,
1401
2060
  keyboardVerticalOffset: 0,
@@ -1403,14 +2062,48 @@ function SagepilotChatProvider({
1403
2062
  } : {
1404
2063
  style: [
1405
2064
  styles.modalContent,
1406
- import_react_native.Platform.OS === "android" ? {
2065
+ import_react_native2.Platform.OS === "android" ? {
1407
2066
  paddingTop: androidModalInsets.top,
1408
2067
  paddingBottom: androidModalInsets.bottom
1409
2068
  } : null
1410
2069
  ].filter(Boolean)
1411
2070
  };
1412
2071
  const handleWebViewMessage = (event) => {
1413
- internalSagepilotChat.handleHostedBridgeMessage(parseHostedBridgeMessage(event.nativeEvent?.data));
2072
+ const message = parseHostedBridgeMessage(event.nativeEvent?.data);
2073
+ if (message?.type === "sagepilot:open_file_picker") {
2074
+ openNativeFilePicker(message.multiple ?? true);
2075
+ return;
2076
+ }
2077
+ if (message?.type === "sagepilot:widget_listener_ready") {
2078
+ widgetReadyRef.current = true;
2079
+ startDelivery();
2080
+ return;
2081
+ }
2082
+ if (message?.type === "sagepilot:files_received") {
2083
+ const ackBatchId = message.batch_id;
2084
+ const head = pendingBatchesRef.current[0];
2085
+ if (head && (!ackBatchId || ackBatchId === head.batchId)) {
2086
+ acknowledgeHeadBatch();
2087
+ }
2088
+ return;
2089
+ }
2090
+ if (message?.type === "sagepilot:pong") {
2091
+ const pending = pendingPingRef.current;
2092
+ if (pending && (!message.nonce || message.nonce === pending.nonce)) {
2093
+ if (message.alive === false) {
2094
+ clearPing();
2095
+ if (livenessRemountCountRef.current < LIVENESS_MAX_REMOUNTS) {
2096
+ livenessRemountCountRef.current += 1;
2097
+ recoverNativeWebView();
2098
+ }
2099
+ } else {
2100
+ livenessRemountCountRef.current = 0;
2101
+ clearPing();
2102
+ }
2103
+ }
2104
+ return;
2105
+ }
2106
+ internalSagepilotChat.handleHostedBridgeMessage(message);
1414
2107
  };
1415
2108
  const postIdentityToWebFrame = () => {
1416
2109
  const message = internalSagepilotChat.getHostedIdentityMessage();
@@ -1423,8 +2116,25 @@ function SagepilotChatProvider({
1423
2116
  if (!script || !nativeWebViewRef.current) return;
1424
2117
  nativeWebViewRef.current.injectJavaScript(script);
1425
2118
  };
2119
+ const handleNativeWebViewLoadEnd = () => {
2120
+ postIdentityToNativeWebView();
2121
+ widgetReadyRef.current = false;
2122
+ startDelivery();
2123
+ };
2124
+ const handleShouldStartLoadWithRequest = (0, import_react.useCallback)((request) => {
2125
+ const url = request?.url ?? "";
2126
+ if (!url || request?.isTopFrame === false || isInternalWebViewScheme(url)) {
2127
+ return true;
2128
+ }
2129
+ const widgetOrigin = readUrlOrigin(state.conversationUrl ?? state.preloadUrl);
2130
+ if (widgetOrigin && readUrlOrigin(url) === widgetOrigin) {
2131
+ return true;
2132
+ }
2133
+ import_react_native2.Linking.openURL(url).catch(() => void 0);
2134
+ return false;
2135
+ }, [state.conversationUrl, state.preloadUrl]);
1426
2136
  (0, import_react.useEffect)(() => {
1427
- if (import_react_native.Platform.OS !== "web" || typeof window === "undefined") return;
2137
+ if (import_react_native2.Platform.OS !== "web" || typeof window === "undefined") return;
1428
2138
  const trustedWidgetOrigin = readUrlOrigin(state.conversationUrl);
1429
2139
  if (!trustedWidgetOrigin) return;
1430
2140
  const handleWindowMessage = (event) => {
@@ -1435,16 +2145,16 @@ function SagepilotChatProvider({
1435
2145
  return () => window.removeEventListener("message", handleWindowMessage);
1436
2146
  }, [state.conversationUrl]);
1437
2147
  (0, import_react.useEffect)(() => {
1438
- if (import_react_native.Platform.OS !== "web" || !state.isPresented) return;
2148
+ if (import_react_native2.Platform.OS !== "web" || !state.isPresented) return;
1439
2149
  postIdentityToWebFrame();
1440
2150
  }, [state.isPresented, state.conversationUrl, state.configured]);
1441
2151
  (0, import_react.useEffect)(() => {
1442
- if (import_react_native.Platform.OS === "web" || !state.isPresented) return;
2152
+ if (import_react_native2.Platform.OS === "web" || !state.isPresented) return;
1443
2153
  postIdentityToNativeWebView();
1444
2154
  }, [state.isPresented, state.conversationUrl, state.configured]);
1445
- if (import_react_native.Platform.OS === "web") {
2155
+ if (import_react_native2.Platform.OS === "web") {
1446
2156
  return (0, import_react.createElement)(
1447
- import_react_native.View,
2157
+ import_react_native2.View,
1448
2158
  { style: styles.root },
1449
2159
  children,
1450
2160
  state.configured && !state.isPresented && state.shouldPreload && state.preloadUrl ? (0, import_react.createElement)("iframe", {
@@ -1453,20 +2163,20 @@ function SagepilotChatProvider({
1453
2163
  title: "Sagepilot chat preload"
1454
2164
  }) : null,
1455
2165
  state.configured && state.isPresented && state.conversationUrl ? (0, import_react.createElement)(
1456
- import_react_native.View,
2166
+ import_react_native2.View,
1457
2167
  { style: styles.webOverlay },
1458
2168
  (0, import_react.createElement)(
1459
- import_react_native.View,
2169
+ import_react_native2.View,
1460
2170
  { style: styles.webPanel },
1461
2171
  showCloseButton ? (0, import_react.createElement)(
1462
- import_react_native.Pressable,
2172
+ import_react_native2.Pressable,
1463
2173
  {
1464
2174
  accessibilityRole: "button",
1465
2175
  accessibilityLabel: closeLabel,
1466
2176
  onPress: () => internalSagepilotChat.dismiss(),
1467
2177
  style: styles.webCloseButton
1468
2178
  },
1469
- (0, import_react.createElement)(import_react_native.Text, { style: styles.closeText }, closeLabel)
2179
+ (0, import_react.createElement)(import_react_native2.Text, { style: styles.closeText }, closeLabel)
1470
2180
  ) : null,
1471
2181
  (0, import_react.createElement)("iframe", {
1472
2182
  ref: webFrameRef,
@@ -1480,24 +2190,32 @@ function SagepilotChatProvider({
1480
2190
  );
1481
2191
  }
1482
2192
  return (0, import_react.createElement)(
1483
- import_react_native.View,
2193
+ import_react_native2.View,
1484
2194
  { style: styles.root },
1485
2195
  children,
1486
2196
  state.configured && !state.isPresented && state.shouldPreload && state.preloadUrl ? (0, import_react.createElement)(import_react_native_webview.WebView, {
1487
2197
  ...hostedChatWebViewProps,
2198
+ // Separate key from the hosted WebView: a hidden-preload renderer crash
2199
+ // must not bump the hosted key (which would reset widgetReadyRef and
2200
+ // disrupt an in-flight delivery). onRenderProcessGone must still be
2201
+ // handled here or an unhandled renderer kill crashes the whole app.
2202
+ key: `sagepilot-preload-webview-${preloadWebViewKey}`,
1488
2203
  source: { uri: state.preloadUrl },
1489
2204
  style: styles.preloadWebview,
1490
2205
  injectedJavaScriptBeforeContentLoaded: getInjectedWebViewScript(),
1491
- onMessage: handleWebViewMessage
2206
+ onMessage: handleWebViewMessage,
2207
+ onShouldStartLoadWithRequest: handleShouldStartLoadWithRequest,
2208
+ onRenderProcessGone: recoverPreloadWebView,
2209
+ onContentProcessDidTerminate: recoverPreloadWebView
1492
2210
  }) : null,
1493
2211
  (0, import_react.createElement)(
1494
- import_react_native.Modal,
2212
+ import_react_native2.Modal,
1495
2213
  {
1496
2214
  visible: state.configured && state.isPresented,
1497
2215
  animationType,
1498
2216
  presentationStyle: isFullScreenModal ? "fullScreen" : "pageSheet",
1499
- statusBarTranslucent: import_react_native.Platform.OS === "android",
1500
- navigationBarTranslucent: import_react_native.Platform.OS === "android",
2217
+ statusBarTranslucent: import_react_native2.Platform.OS === "android",
2218
+ navigationBarTranslucent: import_react_native2.Platform.OS === "android",
1501
2219
  onRequestClose: () => internalSagepilotChat.dismiss()
1502
2220
  },
1503
2221
  (0, import_react.createElement)(
@@ -1507,41 +2225,93 @@ function SagepilotChatProvider({
1507
2225
  ChatContentContainer,
1508
2226
  chatContentContainerProps,
1509
2227
  showCloseButton ? (0, import_react.createElement)(
1510
- import_react_native.View,
2228
+ import_react_native2.View,
1511
2229
  { style: styles.header },
1512
2230
  (0, import_react.createElement)(
1513
- import_react_native.Pressable,
2231
+ import_react_native2.Pressable,
1514
2232
  {
1515
2233
  accessibilityRole: "button",
1516
2234
  accessibilityLabel: closeLabel,
1517
2235
  onPress: () => internalSagepilotChat.dismiss(),
1518
2236
  style: styles.closeButton
1519
2237
  },
1520
- (0, import_react.createElement)(import_react_native.Text, { style: styles.closeText }, closeLabel)
2238
+ (0, import_react.createElement)(import_react_native2.Text, { style: styles.closeText }, closeLabel)
1521
2239
  )
1522
2240
  ) : null,
1523
2241
  state.conversationUrl ? (0, import_react.createElement)(import_react_native_webview.WebView, {
1524
2242
  ...hostedChatWebViewProps,
2243
+ key: `sagepilot-hosted-webview-${nativeWebViewKey}`,
1525
2244
  ref: nativeWebViewRef,
1526
2245
  source: { uri: state.conversationUrl },
1527
- style: styles.webview,
2246
+ // The imperceptible opacity toggle forces the Android WebView
2247
+ // surface to re-composite on resume, clearing the blank-but-alive
2248
+ // surface bug without a reload (see runLivenessProbe).
2249
+ style: import_react_native2.Platform.OS === "android" ? [styles.webview, { opacity: 1 - androidRepaintTick % 2 * 1e-3 }] : styles.webview,
1528
2250
  startInLoadingState: true,
1529
2251
  injectedJavaScriptBeforeContentLoaded: getInjectedWebViewScript(),
1530
2252
  onMessage: handleWebViewMessage,
1531
- onLoadEnd: postIdentityToNativeWebView,
2253
+ onLoadEnd: handleNativeWebViewLoadEnd,
2254
+ onShouldStartLoadWithRequest: handleShouldStartLoadWithRequest,
2255
+ // Android: render process killed (commonly while the camera/file
2256
+ // chooser activity is foregrounded). Remount to recover.
2257
+ onRenderProcessGone: recoverNativeWebView,
2258
+ // iOS equivalent: WKWebView content process terminated.
2259
+ onContentProcessDidTerminate: recoverNativeWebView,
1532
2260
  renderLoading: () => (0, import_react.createElement)(
1533
- import_react_native.View,
2261
+ import_react_native2.View,
1534
2262
  { 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)
2263
+ (0, import_react.createElement)(import_react_native2.ActivityIndicator, null),
2264
+ (0, import_react.createElement)(import_react_native2.Text, { style: styles.loadingText }, loadingLabel)
1537
2265
  )
1538
2266
  }) : null
1539
2267
  )
1540
2268
  )
2269
+ ),
2270
+ // Native attachment-source chooser. Replaces the Android Alert (capped at
2271
+ // 3 buttons) so camera/library/documents all show on every platform.
2272
+ (0, import_react.createElement)(
2273
+ import_react_native2.Modal,
2274
+ {
2275
+ visible: sourceChooser !== null,
2276
+ transparent: true,
2277
+ animationType: "fade",
2278
+ statusBarTranslucent: true,
2279
+ onRequestClose: () => setSourceChooser(null)
2280
+ },
2281
+ (0, import_react.createElement)(
2282
+ import_react_native2.Pressable,
2283
+ { style: styles.sheetBackdrop, onPress: () => setSourceChooser(null) },
2284
+ (0, import_react.createElement)(
2285
+ import_react_native2.View,
2286
+ { style: styles.sheetCard },
2287
+ (0, import_react.createElement)(import_react_native2.Text, { style: styles.sheetTitle }, "Add attachment"),
2288
+ ...(internalSagepilotChat.getConfig()?.filePicker?.sources ?? []).map(
2289
+ (source) => (0, import_react.createElement)(
2290
+ import_react_native2.Pressable,
2291
+ {
2292
+ key: source,
2293
+ accessibilityRole: "button",
2294
+ style: styles.sheetButton,
2295
+ onPress: () => handleSourceChoice(source)
2296
+ },
2297
+ (0, import_react.createElement)(import_react_native2.Text, { style: styles.sheetButtonText }, FILE_PICKER_SOURCE_LABELS[source])
2298
+ )
2299
+ ),
2300
+ (0, import_react.createElement)(
2301
+ import_react_native2.Pressable,
2302
+ {
2303
+ accessibilityRole: "button",
2304
+ style: [styles.sheetButton, styles.sheetCancelButton],
2305
+ onPress: () => setSourceChooser(null)
2306
+ },
2307
+ (0, import_react.createElement)(import_react_native2.Text, { style: styles.sheetCancelText }, "Cancel")
2308
+ )
2309
+ )
2310
+ )
1541
2311
  )
1542
2312
  );
1543
2313
  }
1544
- var styles = import_react_native.StyleSheet.create({
2314
+ var styles = import_react_native2.StyleSheet.create({
1545
2315
  root: {
1546
2316
  flex: 1
1547
2317
  },
@@ -1627,7 +2397,7 @@ var styles = import_react_native.StyleSheet.create({
1627
2397
  borderStyle: "none"
1628
2398
  },
1629
2399
  loading: {
1630
- ...import_react_native.StyleSheet.absoluteFillObject,
2400
+ ...import_react_native2.StyleSheet.absoluteFillObject,
1631
2401
  alignItems: "center",
1632
2402
  justifyContent: "center",
1633
2403
  backgroundColor: "#ffffff"
@@ -1636,6 +2406,46 @@ var styles = import_react_native.StyleSheet.create({
1636
2406
  marginTop: 12,
1637
2407
  color: "#4b5563",
1638
2408
  fontSize: 14
2409
+ },
2410
+ sheetBackdrop: {
2411
+ flex: 1,
2412
+ justifyContent: "flex-end",
2413
+ backgroundColor: "rgba(17, 24, 39, 0.36)"
2414
+ },
2415
+ sheetCard: {
2416
+ backgroundColor: "#ffffff",
2417
+ borderTopLeftRadius: 16,
2418
+ borderTopRightRadius: 16,
2419
+ paddingTop: 8,
2420
+ paddingBottom: 24,
2421
+ paddingHorizontal: 8
2422
+ },
2423
+ sheetTitle: {
2424
+ textAlign: "center",
2425
+ color: "#6b7280",
2426
+ fontSize: 13,
2427
+ fontWeight: "600",
2428
+ paddingVertical: 10
2429
+ },
2430
+ sheetButton: {
2431
+ minHeight: 52,
2432
+ alignItems: "center",
2433
+ justifyContent: "center",
2434
+ borderRadius: 12
2435
+ },
2436
+ sheetButtonText: {
2437
+ color: "#111827",
2438
+ fontSize: 16,
2439
+ fontWeight: "500"
2440
+ },
2441
+ sheetCancelButton: {
2442
+ marginTop: 6,
2443
+ backgroundColor: "#f3f4f6"
2444
+ },
2445
+ sheetCancelText: {
2446
+ color: "#111827",
2447
+ fontSize: 16,
2448
+ fontWeight: "600"
1639
2449
  }
1640
2450
  });
1641
2451
 
@@ -1693,7 +2503,10 @@ function useSagepilotChat() {
1693
2503
  SagepilotChat,
1694
2504
  SagepilotChatError,
1695
2505
  SagepilotChatProvider,
2506
+ SagepilotFilePickerError,
1696
2507
  createAsyncStorageCacheStorage,
1697
2508
  createKeychainTokenStorage,
2509
+ createSagepilotFilePicker,
2510
+ createSagepilotFileStore,
1698
2511
  useSagepilotChat
1699
2512
  });